diff --git a/.build/build-bench.xml b/.build/build-bench.xml index b10fc644250e..5cd171b8a271 100644 --- a/.build/build-bench.xml +++ b/.build/build-bench.xml @@ -20,6 +20,11 @@ xmlns:if="ant:if" xmlns:unless="ant:unless"> + + + + + @@ -81,6 +86,7 @@ + @@ -103,7 +109,7 @@ - + @@ -111,10 +117,9 @@ - - + - + diff --git a/.build/build-rat.xml b/.build/build-rat.xml index 7219e0cf8aa6..4a3080918d78 100644 --- a/.build/build-rat.xml +++ b/.build/build-rat.xml @@ -50,12 +50,16 @@ + + + + @@ -68,8 +72,10 @@ + + @@ -79,6 +85,8 @@ + + @@ -93,6 +101,9 @@ + + + diff --git a/.build/build-resolver.xml b/.build/build-resolver.xml index 09263d42aa6e..0392dabbb406 100644 --- a/.build/build-resolver.xml +++ b/.build/build-resolver.xml @@ -53,11 +53,16 @@ + + + + - - + + + + + + + + + - + + - + + @@ -269,24 +277,29 @@ + + + - - + + + - + + - + @@ -308,6 +321,9 @@ + + + diff --git a/.build/cassandra-build-deps-template.xml b/.build/cassandra-build-deps-template.xml index cc1a25a8c1fc..95481db29c58 100644 --- a/.build/cassandra-build-deps-template.xml +++ b/.build/cassandra-build-deps-template.xml @@ -17,8 +17,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 - cassandra-parent - org.apache.cassandra + dse-db-parent + com.datastax.dse @version@ @final.name@-parent.pom @@ -135,10 +135,6 @@ com.github.tomakehurst wiremock-jre8 - - de.jflex - jflex - com.carrotsearch.randomizedtesting randomizedtesting-runner @@ -155,5 +151,9 @@ org.bouncycastle bcutil-jdk18on + + com.bpodgursky + jbool_expressions + diff --git a/.build/cassandra-deps-template.xml b/.build/cassandra-deps-template.xml index cfbc5eddaea1..281a306d9e87 100644 --- a/.build/cassandra-deps-template.xml +++ b/.build/cassandra-deps-template.xml @@ -17,12 +17,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 - org.apache.cassandra - cassandra-parent + com.datastax.dse + dse-db-parent @version@ @final.name@-parent.pom - cassandra-all + dse-db-all @version@ Apache Cassandra The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model. @@ -35,9 +35,9 @@ - scm:https://gitbox.apache.org/repos/asf/cassandra.git - scm:https://gitbox.apache.org/repos/asf/cassandra.git - https://gitbox.apache.org/repos/asf?p=cassandra.git + scm:git:ssh://git@github.com:datastax/cassandra.git + scm:git:ssh://git@github.com:datastax/cassandra.git + scm:git:ssh://git@github.com:datastax/cassandra.git @@ -104,6 +104,14 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + org.msgpack + jackson-dataformat-msgpack + + + com.googlecode.json-simple + json-simple + com.boundary high-scale-lib @@ -136,6 +144,10 @@ com.clearspring.analytics stream + + com.esri.geometry + esri-geometry-api + ch.qos.logback logback-core @@ -201,6 +213,10 @@ net.openhft chronicle-threads + + net.openhft + chronicle-map + net.openhft @@ -320,6 +336,10 @@ org.hdrhistogram HdrHistogram + + com.dynatrace.hash4j + hash4j + com.googlecode.concurrent-trees concurrent-trees @@ -368,6 +388,10 @@ org.apache.lucene lucene-analysis-common + + org.apache.lucene + lucene-backward-codecs + io.github.jbellis jvector @@ -376,5 +400,17 @@ com.vdurmont semver4j + + io.micrometer + micrometer-core + + + org.latencyutils + LatencyUtils + + + de.huxhorn.sulky + de.huxhorn.sulky.ulid + diff --git a/.build/checkstyle.xml b/.build/checkstyle.xml index 2ec5ecab1610..aa5cac13d0be 100644 --- a/.build/checkstyle.xml +++ b/.build/checkstyle.xml @@ -169,9 +169,9 @@ - - - + + + diff --git a/.build/docker/_create_user.sh b/.build/docker/_create_user.sh index 2da9f4913eec..7922e7a3565d 100755 --- a/.build/docker/_create_user.sh +++ b/.build/docker/_create_user.sh @@ -51,6 +51,12 @@ echo "${username} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/${username} chmod 0440 /etc/sudoers.d/${username} mkdir -p ${BUILD_HOME}/docker ${DIST_DIR} ${BUILD_HOME}/.ssh +# rsync in cached maven dependencies +echo "Syncing maven dependencies and gradle wrapper" +rsync -a /home/image-cache/.m2/repository/ ${BUILD_HOME}/.m2/repository/ +cp -a /home/image-cache/.gradle ${BUILD_HOME}/ +chown -R ${username}:${username} ${BUILD_HOME}/.gradle ${BUILD_HOME}/.m2 + # we need to make SSH less strict to prevent various dtests from failing when they attempt to # git clone a given commit/tag/etc echo 'Host *\n UserKnownHostsFile /dev/null\n StrictHostKeyChecking no' > ${BUILD_HOME}/.ssh/config diff --git a/.build/docker/_docker_run.sh b/.build/docker/_docker_run.sh index 1a67840bde2e..a5da381f1385 100755 --- a/.build/docker/_docker_run.sh +++ b/.build/docker/_docker_run.sh @@ -31,6 +31,8 @@ # variables, with defaults [ "x${cassandra_dir}" != "x" ] || cassandra_dir="$(readlink -f $(dirname -- "$0")/../..)" [ "x${build_dir}" != "x" ] || build_dir="${cassandra_dir}/build" +# parameterise the maven repository host directory, as it cannot be shared across containers +# m2_dir fails under /tmp on macos [ "x${m2_dir}" != "x" ] || m2_dir="${HOME}/.m2/repository" [ -d "${build_dir}" ] || { mkdir -p "${build_dir}" ; } [ -d "${m2_dir}" ] || { mkdir -p "${m2_dir}" ; } @@ -99,7 +101,7 @@ if ! ( [[ "$(docker images -q ${image_name} 2>/dev/null)" != "" ]] ) ; then if ! ( docker pull -q ${image_name} >/dev/null 2>/dev/null ) ; then # Create build images containing the build tool-chain, Java and an Apache Cassandra git working directory, with retry echo "Building docker image..." - until docker build -t ${image_name} -f docker/${dockerfile} . ; do + until docker build -t ${image_name} -f docker/${dockerfile} --load . ; do echo "docker build failed… trying again in 10s… " sleep 10 done diff --git a/.build/docker/_prepopulate_maven_deps.sh b/.build/docker/_prepopulate_maven_deps.sh new file mode 100755 index 000000000000..34f2ccec2305 --- /dev/null +++ b/.build/docker/_prepopulate_maven_deps.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +# Script to prepopulate Maven repository with dependencies from multiple Cassandra branches +# This will download all dependencies to a custom Maven repository directory + +# pre-conditions +command -v ant >/dev/null 2>&1 || { error 1 "ant needs to be installed"; } +command -v git >/dev/null 2>&1 || { error 1 "git needs to be installed"; } + + +error() { + echo >&2 $2; + set -x + exit $1 +} + +# Function to download dependencies for a branch +download_deps_for_branch() { + local branch=$1 + local branch_name=$(echo "$branch" | sed 's|origin/||') + + # Check if branch exists + if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then + echo "WARNING: Branch $branch does not exist, skipping..." + return + fi + + git checkout "$branch" + + echo "" + echo "Downloading dependencies for $branch to $CUSTOM_M2_REPO..." + echo "" + + # ensure git modules are initialised + ant init + # download all dependencies + ant -Dmaven.repo.local="$CUSTOM_M2_REPO" -Dlocal.repository="$CUSTOM_M2_REPO" resolver-dist-lib +} + +CUSTOM_M2_REPO="${1:-$HOME/.m2/repository}" +TMP_DIR=${TMP_DIR:-/tmp} + +cd $TMP_DIR +git clone https://github.com/apache/cassandra.git +cd cassandra +git config advice.detachedHead false + +# Automatically detect branches from cassandra-5.0 onwards to trunk +echo "Detecting branches..." +BRANCHES=() + +# Get all origin branches matching cassandra-5.x+, cassandra-6.x+, etc., and trunk +# Pattern matches: cassandra-5.0, cassandra-5.0.0, cassandra-10.0, cassandra-10.0.1, trunk +while IFS= read -r branch; do + BRANCHES+=("$branch") +done < <(git branch -r | grep -E "^\s*origin/(cassandra-[5-9][0-9]*\.[0-9]+(\.[0-9]+)?|trunk)$" | sed 's/^[[:space:]]*//' | sort -V) + +# If no branches found, fail +if [ ${#BRANCHES[@]} -eq 0 ]; then + echo "ERROR: No branches auto-detected matching pattern origin/cassandra-[5+].x or origin/trunk" + echo "Please ensure you have fetched remote branches: git fetch origin" + exit 1 +fi + +echo "Branches to process:" +for branch in "${BRANCHES[@]}"; do + echo " - $branch" +done +echo "==========================================" +echo "" + +# Create custom Maven repository directory +mkdir -p "$CUSTOM_M2_REPO" + +# Process each branch +for branch in "${BRANCHES[@]}"; do + download_deps_for_branch "$branch" +done + +cd - +rm -rf $TMP_DIR/cassandra \ No newline at end of file diff --git a/.build/docker/almalinux-build.docker b/.build/docker/almalinux-build.docker index 89832b67b346..cb612dc32398 100644 --- a/.build/docker/almalinux-build.docker +++ b/.build/docker/almalinux-build.docker @@ -26,7 +26,7 @@ ENV CASSANDRA_DIR=$BUILD_HOME/cassandra ARG UID_ARG=1000 ARG GID_ARG=1000 -LABEL org.cassandra.buildenv=almalinux +LABEL org.cassandra.buildenv=almalinux_build RUN echo "Building with arguments:" \ && echo " - DIST_DIR=${DIST_DIR}" \ @@ -55,3 +55,12 @@ RUN rpm -i --nodeps ant-junit-1.9.4-2.el7.noarch.rpm # python3 is needed for the gen-doc target RUN pip3 install --upgrade pip + +# Prepopulate Maven repository with dependencies from all branches. see _create_user.sh +COPY docker/_prepopulate_maven_deps.sh /tmp/_prepopulate_maven_deps.sh +RUN alternatives --set java $(alternatives --display java | grep "family java-11-openjdk" | cut -d' ' -f1) +RUN alternatives --set javac $(alternatives --display javac | grep "family java-11-openjdk" | cut -d' ' -f1) +RUN mkdir -p /home/image-cache && chmod -R a+rwx /home/image-cache +RUN JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:/bin/javac::") \ + bash /tmp/_prepopulate_maven_deps.sh /home/image-cache/.m2/repository && rm /tmp/_prepopulate_maven_deps.sh +RUN cp -a /root/.gradle /home/image-cache/.gradle diff --git a/.build/docker/bullseye-build.docker b/.build/docker/bullseye-build.docker index b31bf03b3a75..fbb5654f6e98 100644 --- a/.build/docker/bullseye-build.docker +++ b/.build/docker/bullseye-build.docker @@ -23,7 +23,7 @@ ENV DIST_DIR=/dist ENV BUILD_HOME=/home/build ENV CASSANDRA_DIR=$BUILD_HOME/cassandra -LABEL org.cassandra.buildenv=bullseye +LABEL org.cassandra.buildenv=debian_build RUN echo "Building with arguments:" \ && echo " - DIST_DIR=${DIST_DIR}" \ @@ -53,21 +53,8 @@ RUN pip install --upgrade pip # dependencies for .build/ci/ci_parser.py RUN pip install beautifulsoup4==4.12.3 jinja2==3.1.3 -# install golang. GO_VERSION_SHA must be updated with VERSION -RUN sh -c '\ - GO_VERSION="1.24.3" ;\ - GO_VERSION_SHAS="3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8 a463cb59382bd7ae7d8f4c68846e73c4d589f223c589ac76871b66811ded7836 13e6fe3fcf65689d77d40e633de1e31c6febbdbcb846eb05fc2434ed2213e92b 64a3fa22142f627e78fac3018ce3d4aeace68b743eff0afda8aae0411df5e4fb" ;\ - GO_OS=linux ;\ - [ $(uname) = "Darwin" ] && GO_OS=darwin ;\ - GO_PLATFORM=amd64 ;\ - [ $(uname -m) = "aarch64" ] && GO_PLATFORM=arm64 ;\ - GO_TAR="go${GO_VERSION}.${GO_OS}-${GO_PLATFORM}.tar.gz" ;\ - curl -L --fail --silent --retry 2 --retry-delay 5 --max-time 30 https://go.dev/dl/$GO_TAR -o $GO_TAR ;\ - GO_SHA="$(sha256sum $GO_TAR | cut -d" " -f2)" ;\ - echo "$GO_VERSION_SHAS" | sed "s/ /\n/g" | grep -q "$GO_SHA" || { echo "SHA256 mismatch for $GO_TAR $GO_SHA"; exit 1; } ;\ - tar -C /usr/local -xzf $GO_TAR ;\ - rm $GO_TAR' - -ENV GOROOT="/usr/local/go" -ENV GOPATH="$BUILD_HOME/go" -ENV PATH="$PATH:/usr/local/go/bin" \ No newline at end of file +# Prepopulate Maven repository with dependencies from all branches. see _create_user.sh +COPY docker/_prepopulate_maven_deps.sh /tmp/_prepopulate_maven_deps.sh +RUN mkdir -p /home/image-cache && chmod -R a+rwx /home/image-cache +RUN bash /tmp/_prepopulate_maven_deps.sh /home/image-cache/.m2/repository && rm /tmp/_prepopulate_maven_deps.sh +RUN cp -a /root/.gradle /home/image-cache/.gradle \ No newline at end of file diff --git a/.build/docker/run-tests.sh b/.build/docker/run-tests.sh index ffade4adc8cc..a8c6a7354932 100755 --- a/.build/docker/run-tests.sh +++ b/.build/docker/run-tests.sh @@ -25,6 +25,8 @@ [ "x${cassandra_dir}" != "x" ] || cassandra_dir="$(readlink -f $(dirname -- "$0")/../..)" [ "x${cassandra_dtest_dir}" != "x" ] || cassandra_dtest_dir="${cassandra_dir}/../cassandra-dtest" [ "x${build_dir}" != "x" ] || build_dir="${cassandra_dir}/build" +# parameterise the maven repository host directory, as it cannot be shared across containers. +# m2_dir fails under /tmp on macos [ "x${m2_dir}" != "x" ] || m2_dir="${HOME}/.m2/repository" [ "x${docker_timeout_hours}" != "x" ] || docker_timeout_hours="1" [ -d "${build_dir}" ] || { mkdir -p "${build_dir}" ; } @@ -134,7 +136,7 @@ docker --version pushd ${cassandra_dir}/.build >/dev/null # build test image -dockerfile="ubuntu2004_test.docker" +dockerfile="ubuntu-test.docker" image_tag="$(md5sum docker/${dockerfile} | cut -d' ' -f1)" image_name="apache/cassandra-${dockerfile/.docker/}:${image_tag}" docker_mounts="-v ${cassandra_dir}:/home/cassandra/cassandra -v "${build_dir}":/home/cassandra/cassandra/build -v ${m2_dir}:/home/cassandra/.m2/repository" @@ -147,7 +149,7 @@ if ! ( [[ "$(docker images -q ${image_name} 2>/dev/null)" != "" ]] ) ; then if ! ( docker pull -q ${image_name} >/dev/null 2>/dev/null ) ; then # Create build images containing the build tool-chain, Java and an Apache Cassandra git working directory, with retry echo "Building docker image..." - until docker build -t ${image_name} -f docker/${dockerfile} . ; do + until docker build -t ${image_name} -f docker/${dockerfile} --load . ; do echo "docker build failed… trying again in 10s… " sleep 10 done @@ -294,10 +296,11 @@ docker_command="source \${CASSANDRA_DIR}/.build/docker/_set_java.sh ${java_versi # start the container, timeout after 4 hours docker_id=$(docker run --name ${container_name} ${docker_flags} ${docker_envs} ${docker_mounts} ${docker_volume_opt} ${image_name} sleep ${docker_timeout_hours}h) -echo "Running container ${container_name} ${docker_id}" +echo "Running container ${container_name} ${docker_id} using image ${image_name}" docker exec --user root ${container_name} bash -c "\${CASSANDRA_DIR}/.build/docker/_create_user.sh cassandra $(id -u) $(id -g)" | tee -a ${logfile} docker exec --user root ${container_name} update-alternatives --set python /usr/bin/python${python_version} | tee -a ${logfile} +docker exec --user root ${container_name} update-alternatives --set python3 /usr/bin/python${python_version} | tee -a ${logfile} if [ -n "${DTEST_TMPDIR_LOCAL}" ] && [[ "${target}" =~ ^dtest-upgrade ]] ; then # prepopulate a tmp ccm repository directory, if running dtest-upgrade tests diff --git a/.build/docker/ubuntu-test.docker b/.build/docker/ubuntu-test.docker new file mode 100644 index 000000000000..96039b93d96a --- /dev/null +++ b/.build/docker/ubuntu-test.docker @@ -0,0 +1,219 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM ubuntu:22.04 +LABEL org.opencontainers.image.authors="Apache Cassandra " + +# CONTEXT is expected to be cassandra/.build + +ENV BUILD_HOME=/home/cassandra +ENV CASSANDRA_DIR=$BUILD_HOME/cassandra +ENV DIST_DIR=$CASSANDRA_DIR/build +ENV LANG=en_US.UTF-8 +ENV LC_CTYPE=en_US.UTF-8 +ENV PYTHONIOENCODING=utf-8 +ENV PYTHONUNBUFFERED=true + +LABEL org.cassandra.buildenv=ubuntu_test + +RUN echo "Building with arguments:" \ + && echo " - DIST_DIR=${DIST_DIR}" \ + && echo " - BUILD_HOME=${BUILD_HOME}" \ + && echo " - CASSANDRA_DIR=${CASSANDRA_DIR}" \ + && echo " - UID_ARG=${UID_ARG}" \ + && echo " - GID_ARG=${GID_ARG}" + +# configure apt to retry downloads +RUN echo 'APT::Acquire::Retries "99";' > /etc/apt/apt.conf.d/80-retries +RUN echo 'Acquire::http::Timeout "60";' > /etc/apt/apt.conf.d/80proxy.conf +RUN echo 'Acquire::ftp::Timeout "60";' >> /etc/apt/apt.conf.d/80proxy.conf + +# install our python dependencies and some other stuff we need +# libev4 libev-dev are for the python driver + +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get install -y --no-install-recommends software-properties-common apt-utils gnupg + +RUN export DEBIAN_FRONTEND=noninteractive && \ + add-apt-repository -y ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get install -y curl git-core python3-pip \ + python3.8 python3.8-venv python3.8-dev \ + python3.10 python3.10-venv python3.10-dev \ + python3.11 python3.11-venv python3.11-dev \ + python3.12 python3.12-venv python3.12-dev \ + python3.13 python3.13-venv python3.13-dev \ + virtualenv net-tools libev4 libev-dev wget gcc libxml2 libxslt1-dev \ + vim lsof sudo libjemalloc2 dumb-init locales rsync \ + openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk ant ant-optional + +RUN update-alternatives --remove java /usr/lib/jvm/java-8-openjdk-$(dpkg --print-architecture)/jre/bin/java +RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-8-openjdk-$(dpkg --print-architecture)/bin/java 1081 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.8 1 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.10 2 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.11 3 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.12 4 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.13 5 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 2 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 3 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 4 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.13 5 +RUN python3.8 -m pip install --upgrade pip + +# generate locales for the standard en_US.UTF8 value we use for testing +RUN locale-gen en_US.UTF-8 + +# as we only need the requirements.txt file from the dtest repo, let's just get it from GitHub as a raw asset +# so we can avoid needing to clone the entire repo just to get this file +RUN curl https://raw.githubusercontent.com/apache/cassandra-dtest/trunk/requirements.txt --output /opt/requirements.txt +RUN chmod 0644 /opt/requirements.txt + +# now setup python via virtualenv with all of the python dependencies we need according to requirements.txt +RUN pip3 install virtualenv virtualenv-clone +RUN pip3 install --upgrade wheel + +# make Java 8 the default executable (we use to run all tests against Java 8) +RUN update-java-alternatives --set java-1.8.0-openjdk-$(dpkg --print-architecture) + +# enable legacy TLSv1 and TLSv1.1 (CASSANDRA-16848) +RUN find /etc -type f -name java.security -exec sed -i 's/TLSv1, TLSv1.1//' {} \; +RUN find /etc -type f -name java.security -exec sed -i 's/3DES_EDE_CBC$/3DES_EDE_CBC, TLSv1, TLSv1.1/' {} \; + +# create and change to cassandra-tmp user, use an rare uid to avoid collision later on +RUN mkdir -p /home/image-cache && chmod -R a+rwx /home/image-cache +RUN adduser --disabled-login --uid 901743 --lastuid 901743 --gecos cassandra cassandra-tmp +RUN gpasswd -a cassandra-tmp sudo +RUN echo "cassandra-tmp ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/build +RUN chmod 0440 /etc/sudoers.d/build + +# switch to the cassandra user +RUN mkdir -p ${BUILD_HOME} && chmod a+rwx ${BUILD_HOME} +USER cassandra-tmp +ENV HOME=${BUILD_HOME} +WORKDIR ${BUILD_HOME} + +ENV ANT_HOME=/usr/share/ant + +# Prepopulate Maven repository with dependencies from all branches. see _create_user.sh +COPY docker/_prepopulate_maven_deps.sh /tmp/_prepopulate_maven_deps.sh +RUN bash /tmp/_prepopulate_maven_deps.sh /home/image-cache/.m2/repository +RUN cp -a /home/cassandra-tmp/.gradle /home/image-cache/.gradle + +# run pip commands and setup virtualenv (note we do this after we switch to cassandra user so we +# setup the virtualenv for the cassandra user, not root) for Python 3.8-3.13 +# Don't build cython extensions when installing cassandra-driver. During test execution the driver +# dependency is refreshed via pip install --upgrade, so that driver changes can be pulled in without +# requiring the image to be rebuilt. Rebuilding compiled extensions is costly and is disabled by +# default in test jobs using the CASS_DRIVER_X env vars below. However, if the extensions are +# included in the base image, the compiled objects are not updated by pip at run time, which can +# cause errors if the tests rely on new driver functionality or bug fixes. + +RUN virtualenv --python=python3.8 ${BUILD_HOME}/env3.8 +RUN chmod +x ${BUILD_HOME}/env3.8/bin/activate + +RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ + && source ${BUILD_HOME}/env3.8/bin/activate \ + && pip3 install --upgrade pip \ + && pip3 install -r /opt/requirements.txt \ + && pip3 freeze --user" + +RUN virtualenv --python=python3.10 ${BUILD_HOME}/env3.10 +RUN chmod +x ${BUILD_HOME}/env3.10/bin/activate + +RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ + && source ${BUILD_HOME}/env3.10/bin/activate \ + && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 \ + && pip3 install --upgrade \"pip<25.0\" \"setuptools==60.8.2\" wheel \ + && pip3 install --no-build-isolation -r /opt/requirements.txt \ + && pip3 freeze --user" + +RUN python3.11 -m venv ${BUILD_HOME}/env3.11 +RUN chmod +x ${BUILD_HOME}/env3.11/bin/activate + +RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ + && source ${BUILD_HOME}/env3.11/bin/activate \ + && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \ + && pip3 install --upgrade \"pip<25.0\" \"setuptools==60.8.2\" wheel \ + && pip3 install --no-build-isolation -r /opt/requirements.txt \ + && pip3 freeze --user" + +RUN virtualenv --python=python3.12 ${BUILD_HOME}/env3.12 +RUN chmod +x ${BUILD_HOME}/env3.12/bin/activate + +RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ + && source ${BUILD_HOME}/env3.12/bin/activate \ + && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.12 \ + && pip3 install --upgrade \"pip<25.0\" \"setuptools>=65.5.0,<70.0.0\" wheel \ + && sed -i 's/pkgutil.ImpImporter/type(\"ImpImporter\", (object,), {})/g' ${BUILD_HOME}/env3.12/lib/python3.12/site-packages/pkg_resources/__init__.py \ + && pip3 install --no-build-isolation -r /opt/requirements.txt \ + && pip3 freeze --user" + +RUN virtualenv --python=python3.13 ${BUILD_HOME}/env3.13 +RUN chmod +x ${BUILD_HOME}/env3.13/bin/activate + +RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ + && source ${BUILD_HOME}/env3.13/bin/activate \ + && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.13 \ + && pip3 install --upgrade \"pip<25.0\" \"setuptools>=65.5.0,<70.0.0\" wheel \ + && sed -i 's/pkgutil.ImpImporter/type(\"ImpImporter\", (object,), {})/g' ${BUILD_HOME}/env3.13/lib/python3.13/site-packages/pkg_resources/__init__.py \ + && pip3 install --no-build-isolation -r /opt/requirements.txt \ + && pip3 freeze --user" + +# 4* requires java8, sudo doesn't work on cross-platform builds +USER root +RUN update-alternatives --set java /usr/lib/jvm/java-8-openjdk-$(dpkg --print-architecture)/bin/java +RUN update-alternatives --set javac /usr/lib/jvm/java-8-openjdk-$(dpkg --print-architecture)/bin/javac +USER cassandra-tmp + +# Initialize the CCM git repo as well as this also can fail to clone +RUN /bin/bash -c "source ${BUILD_HOME}/env3.8/bin/activate && \ + ccm create -n 1 -v git:cassandra-4.1 test && ccm remove test && \ + ccm create -n 1 -v git:cassandra-4.0 test && ccm remove test" + +# Initialize ccm versions. branch heads and all versions iterating through to the latest version found on downloads.apache.org/cassandra +RUN bash -c 'source ${BUILD_HOME}/env3.8/bin/activate && \ + latest_4_0=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")4\.0\.[0-9]+" | sort -V | tail -1 | cut -d"." -f3) && \ + for i in $(seq 1 $latest_4_0); do echo $i ; ccm create --quiet -n 1 -v binary:4.0.$i test && ccm remove test ; done && \ + latest_4_1=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")4\.1\.[0-9]+" | sort -V | tail -1 | cut -d"." -f3) && \ + for i in $(seq 1 $latest_4_1); do echo $i ; ccm create --quiet -n 1 -v binary:4.1.$i test && ccm remove test ; done' + +# 5+ requires java11, sudo doesn't work on cross-platform builds +USER root +RUN update-alternatives --set java /usr/lib/jvm/java-11-openjdk-$(dpkg --print-architecture)/bin/java +RUN update-alternatives --set javac /usr/lib/jvm/java-11-openjdk-$(dpkg --print-architecture)/bin/javac +USER cassandra-tmp + +# Initialize ccm versions. branch heads and all versions iterating through to the latest version found on downloads.apache.org/cassandra +RUN rm -fr ${BUILD_HOME}/.ccm/repository/_git_cache_apache +RUN /bin/bash -c 'source ${BUILD_HOME}/env3.8/bin/activate && \ + ccm create --quiet -n 1 -v git:cassandra-5.0 test && ccm remove test && \ + ccm create --quiet -n 1 -v git:cassandra-6.0 test && ccm remove test && \ + ccm create --quiet -n 1 -v git:trunk test && ccm remove test && \ + latest_5_0=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")5\.0\.[0-9]+" | sort -V | tail -1 | cut -d"." -f3) && \ + for i in $(seq 1 $latest_5_0); do echo $i ; ccm create --quiet -n 1 -v binary:5.0.$i test && ccm remove test ; done' + # TODO uncomment when 6.0.0 is released + #latest_6_0=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")6\.0\.[0-9]+" | sort -V | tail -1 | cut -d"." -f3) && \ + #for i in $(seq 1 $latest_6_0); do echo $i ; ccm create --quiet -n 1 -v binary:6.0.$i test && ccm remove test ; done' + +# the .git subdirectories to pip installed cassandra-driver breaks virtualenv-clone, so just remove them +# and other directories we don't need in image +RUN rm -rf ${BUILD_HOME}/env*/src/cassandra-driver/.git /home/cassandra-tmp/.m2 /tmp/ccm-*.tar.gz +# fix permissions, runtime user has different uid/gid +RUN chmod -R og+wx ${BUILD_HOME}/.ccm ${BUILD_HOME}/env* ${BUILD_HOME}/.cache + +# mark "/tmp" as a volume so it will get mounted as an ext4 mount and not +# the stupid aufs/CoW stuff that the actual docker container mounts will have. +# we've been seeing 3+ minute hangs when calling sync on an aufs backed mount +# so it greatly makes tests flaky as things can hang basically anywhere +VOLUME ["/tmp"] diff --git a/.build/docker/ubuntu2004_test.docker b/.build/docker/ubuntu2004_test.docker deleted file mode 100644 index 9d19baef18b6..000000000000 --- a/.build/docker/ubuntu2004_test.docker +++ /dev/null @@ -1,155 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM ubuntu:20.04 -MAINTAINER Apache Cassandra - -# CONTEXT is expected to be cassandra/.build - -ENV BUILD_HOME=/home/cassandra -ENV CASSANDRA_DIR=$BUILD_HOME/cassandra -ENV DIST_DIR=$CASSANDRA_DIR/build -ENV LANG=en_US.UTF-8 -ENV LC_CTYPE=en_US.UTF-8 -ENV PYTHONIOENCODING=utf-8 -ENV PYTHONUNBUFFERED=true - -LABEL org.cassandra.buildenv=ubuntu_2004 - -RUN echo "Building with arguments:" \ - && echo " - DIST_DIR=${DIST_DIR}" \ - && echo " - BUILD_HOME=${BUILD_HOME}" \ - && echo " - CASSANDRA_DIR=${CASSANDRA_DIR}" \ - && echo " - UID_ARG=${UID_ARG}" \ - && echo " - GID_ARG=${GID_ARG}" - -# configure apt to retry downloads -RUN echo 'APT::Acquire::Retries "99";' > /etc/apt/apt.conf.d/80-retries -RUN echo 'Acquire::http::Timeout "60";' > /etc/apt/apt.conf.d/80proxy.conf -RUN echo 'Acquire::ftp::Timeout "60";' >> /etc/apt/apt.conf.d/80proxy.conf - -# install our python dependencies and some other stuff we need -# libev4 libev-dev are for the python driver - -RUN export DEBIAN_FRONTEND=noninteractive && \ - apt-get update && \ - apt-get install -y --no-install-recommends software-properties-common apt-utils - -RUN export DEBIAN_FRONTEND=noninteractive && \ - add-apt-repository -y ppa:deadsnakes/ppa && \ - apt-get update && \ - apt-get install -y curl git-core python3-pip \ - python3.8 python3.8-venv python3.8-dev \ - python3.11 python3.11-venv python3.11-dev \ - virtualenv net-tools libev4 libev-dev wget gcc libxml2 libxslt1-dev \ - vim lsof sudo libjemalloc2 dumb-init locales rsync \ - openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk ant ant-optional - - -RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.8 2 -RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.11 3 -RUN python3.8 -m pip install --upgrade pip - -# generate locales for the standard en_US.UTF8 value we use for testing -RUN locale-gen en_US.UTF-8 - -# as we only need the requirements.txt file from the dtest repo, let's just get it from GitHub as a raw asset -# so we can avoid needing to clone the entire repo just to get this file -RUN curl https://raw.githubusercontent.com/apache/cassandra-dtest/trunk/requirements.txt --output /opt/requirements.txt -RUN chmod 0644 /opt/requirements.txt - -# now setup python via virtualenv with all of the python dependencies we need according to requirements.txt -RUN pip3 install virtualenv virtualenv-clone -RUN pip3 install --upgrade wheel - -# make Java 8 the default executable (we use to run all tests against Java 8) -RUN update-java-alternatives --set java-1.8.0-openjdk-$(dpkg --print-architecture) - -# enable legacy TLSv1 and TLSv1.1 (CASSANDRA-16848) -RUN find /etc -type f -name java.security -exec sed -i 's/TLSv1, TLSv1.1//' {} \; -RUN find /etc -type f -name java.security -exec sed -i 's/3DES_EDE_CBC$/3DES_EDE_CBC, TLSv1, TLSv1.1/' {} \; - -# create and change to cassandra-tmp user, use an rare uid to avoid collision later on -RUN adduser --disabled-login --uid 901743 --lastuid 901743 --gecos cassandra cassandra-tmp -RUN gpasswd -a cassandra-tmp sudo -RUN echo "cassandra-tmp ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/build -RUN chmod 0440 /etc/sudoers.d/build - -# switch to the cassandra user -RUN mkdir -p ${BUILD_HOME} && chmod a+rwx ${BUILD_HOME} -USER cassandra-tmp -ENV HOME ${BUILD_HOME} -WORKDIR ${BUILD_HOME} - -ENV ANT_HOME=/usr/share/ant - -# run pip commands and setup virtualenv (note we do this after we switch to cassandra user so we -# setup the virtualenv for the cassandra user and not the root user by accident) for Python 3.8/3.11 -# Don't build cython extensions when installing cassandra-driver. During test execution the driver -# dependency is refreshed via pip install --upgrade, so that driver changes can be pulled in without -# requiring the image to be rebuilt. Rebuilding compiled extensions is costly and is disabled by -# default in test jobs using the CASS_DRIVER_X env vars below. However, if the extensions are -# included in the base image, the compiled objects are not updated by pip at run time, which can -# cause errors if the tests rely on new driver functionality or bug fixes. - -RUN virtualenv --python=python3.8 ${BUILD_HOME}/env3.8 -RUN chmod +x ${BUILD_HOME}/env3.8/bin/activate - -RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ - && source ${BUILD_HOME}/env3.8/bin/activate \ - && pip3 install --upgrade pip \ - && pip3 install -r /opt/requirements.txt \ - && pip3 freeze --user" - -RUN virtualenv --python=python3.11 ${BUILD_HOME}/env3.11 -RUN chmod +x ${BUILD_HOME}/env3.11/bin/activate - -RUN /bin/bash -c "export CASS_DRIVER_NO_CYTHON=1 CASS_DRIVER_NO_EXTENSIONS=1 \ - && source ${BUILD_HOME}/env3.11/bin/activate \ - && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \ - && pip3 install -r /opt/requirements.txt \ - && pip3 freeze --user" - -# Initialize the CCM git repo as well as this also can fail to clone -RUN /bin/bash -c "source ${BUILD_HOME}/env3.8/bin/activate && \ - ccm create -n 1 -v git:cassandra-4.1 test && ccm remove test && \ - ccm create -n 1 -v git:cassandra-4.0 test && ccm remove test" - -# Initialize ccm versions. branch heads and all versions iterating through to the latest version found on downloads.apache.org/cassandra -RUN bash -c 'source ${BUILD_HOME}/env3.8/bin/activate && \ - latest_4_0=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")4\.0\.[0-9]+(?=\")" | sort -V | tail -1 | cut -d"." -f3) && \ - for i in $(seq 1 $latest_4_0); do echo $i ; ccm create --quiet -n 1 -v binary:4.0.$i test && ccm remove test ; done && \ - latest_4_1=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")4\.1\.[0-9]+(?=\")" | sort -V | tail -1 | cut -d"." -f3) && \ - for i in $(seq 1 $latest_4_1); do echo $i ; ccm create --quiet -n 1 -v binary:4.1.$i test && ccm remove test ; done' - -# 5+ requires java11 -RUN sudo update-java-alternatives --set java-1.11.0-openjdk-$(dpkg --print-architecture) - -# Initialize ccm versions. branch heads and all versions iterating through to the latest version found on downloads.apache.org/cassandra -RUN rm -fr ${BUILD_HOME}/.ccm/repository/_git_cache_apache -RUN /bin/bash -c 'source ${BUILD_HOME}/env3.8/bin/activate && \ - ccm create --quiet -n 1 -v git:cassandra-5.0 test && ccm remove test && \ - ccm create --quiet -n 1 -v git:trunk test && ccm remove test && \ - latest_5_0=$(curl -s https://downloads.apache.org/cassandra/ | grep -oP "(?<=href=\")5\.0\.[0-9]+(?=\")" | sort -V | tail -1 | cut -d"." -f3) && \ - for i in $(seq 1 $latest_5_0); do echo $i ; ccm create --quiet -n 1 -v binary:5.0.$i test && ccm remove test ; done' - -# the .git subdirectories to pip installed cassandra-driver breaks virtualenv-clone, so just remove them -# and other directories we don't need in image -RUN rm -rf ${BUILD_HOME}/env*/src/cassandra-driver/.git /home/cassandra-tmp/.m2 /tmp/ccm-*.tar.gz -# fix permissions, runtime user has different uid/gid -RUN chmod -R og+wx ${BUILD_HOME}/.ccm ${BUILD_HOME}/env* ${BUILD_HOME}/.cache - -# mark "/tmp" as a volume so it will get mounted as an ext4 mount and not -# the stupid aufs/CoW stuff that the actual docker container mounts will have. -# we've been seeing 3+ minute hangs when calling sync on an aufs backed mount -# so it greatly makes tests flaky as things can hang basically anywhere -VOLUME ["/tmp"] diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml index 782b1957ec40..972873921503 100644 --- a/.build/parent-pom-template.xml +++ b/.build/parent-pom-template.xml @@ -21,13 +21,13 @@ org.apache 22 - org.apache.cassandra - cassandra-parent + com.datastax.dse + dse-db-parent @version@ pom - Apache Cassandra - The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model. - https://cassandra.apache.org + Datastax DB + The Apache Cassandra Project develops a highly scalable second-generation distributed database. DataStax, Inc. provides additional improvements on top of Apache Cassandra + https://datastax.com 2009 @@ -36,8 +36,8 @@ - 1.12.13 - 4.0.20 + 1.14.17 + 4.0.23 0.5.1 @@ -46,7 +46,6 @@ @allocation-instrumenter.version@ @ecj.version@ @jacoco.version@ - @jflex.version@ @@ -239,9 +238,9 @@ - scm:https://gitbox.apache.org/repos/asf/cassandra.git - scm:https://gitbox.apache.org/repos/asf/cassandra.git - https://gitbox.apache.org/repos/asf?p=cassandra.git + scm:git:ssh://git@github.com:datastax/cassandra.git + scm:git:ssh://git@github.com:datastax/cassandra.git + scm:git:ssh://git@github.com:datastax/cassandra.git @@ -291,12 +290,12 @@ org.xerial.snappy snappy-java - 1.1.10.4 + 1.1.10.7 at.yawk.lz4 lz4-java - 1.10.1 + 1.10.2 com.github.luben @@ -306,7 +305,7 @@ com.google.guava guava - 32.0.1-jre + 33.4.0-jre jsr305 @@ -346,6 +345,11 @@ HdrHistogram 2.1.12 + + com.dynatrace.hash4j + hash4j + 0.30.0 + commons-cli commons-cli @@ -412,37 +416,47 @@ ch.qos.logback logback-core - 1.5.18 + 1.5.35 ch.qos.logback logback-classic - 1.5.18 + 1.5.35 com.fasterxml.jackson.core jackson-core - 2.19.2 + 2.21.4 com.fasterxml.jackson.core jackson-databind - 2.19.2 + 2.21.4 com.fasterxml.jackson.core jackson-annotations - 2.19.2 + 2.21 + + + com.googlecode.json-simple + json-simple + 1.1 com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.19.2 + 2.21.4 + + + org.msgpack + jackson-dataformat-msgpack + 0.9.11 com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.19.2 + 2.21.4 test @@ -465,12 +479,12 @@ org.yaml snakeyaml - 2.1 + 2.4 junit junit - 4.12 + 4.13 test @@ -552,7 +566,7 @@ com.puppycrawl.tools checkstyle - 10.12.1 + 10.26.1 test @@ -746,7 +760,7 @@ io.netty netty-all - 4.1.130.Final + 4.1.136.Final io.netty @@ -760,10 +774,6 @@ io.netty netty-codec-http2 - - io.netty - netty-codec-http - io.netty netty-codec-memcache @@ -817,7 +827,7 @@ io.netty netty-tcnative-boringssl-static - 2.0.70.Final + 2.0.74.Final org.bouncycastle @@ -840,18 +850,18 @@ io.netty netty-transport-native-epoll - 4.1.130.Final + 4.1.136.Final io.netty netty-transport-native-epoll - 4.1.130.Final + 4.1.136.Final linux-x86_64 io.netty netty-transport-native-epoll - 4.1.130.Final + 4.1.136.Final linux-aarch_64 @@ -859,7 +869,7 @@ net.openhft chronicle-queue - 5.23.37 + 5.24ea27 tools @@ -875,7 +885,7 @@ net.openhft chronicle-core - 2.23.36 + 2.24ea28 chronicle-analytics @@ -890,7 +900,7 @@ net.openhft chronicle-bytes - 2.23.33 + 2.24ea20 annotations @@ -901,7 +911,7 @@ net.openhft chronicle-wire - 2.23.39 + 2.24ea27 compiler @@ -917,7 +927,19 @@ net.openhft chronicle-threads - 2.23.25 + 2.24ea14 + + + + net.openhft + affinity + + + + + net.openhft + chronicle-map + 3.24ea4 @@ -943,7 +965,7 @@ com.google.code.findbugs jsr305 - 2.0.2 + 3.0.0 com.clearspring.analytics @@ -956,6 +978,11 @@ + + com.esri.geometry + esri-geometry-api + 2.2.4 + org.apache.cassandra cassandra-driver-core @@ -1039,17 +1066,6 @@ hppc 0.8.1 - - de.jflex - jflex - ${jflex.version} - - - ant - org.apache.ant - - - com.googlecode.concurrent-trees concurrent-trees @@ -1218,27 +1234,32 @@ com.github.seancfoley ipaddress - 5.3.3 + 5.6.2 org.agrona agrona - 1.17.1 + 1.20.0 org.apache.lucene lucene-core - 9.7.0 + 9.8.0 org.apache.lucene lucene-analysis-common - 9.7.0 + 9.8.0 + + + org.apache.lucene + lucene-backward-codecs + 9.8.0 io.github.jbellis jvector - 1.0.2 + 4.0.0-rc.8-hf1 com.carrotsearch.randomizedtesting @@ -1262,6 +1283,27 @@ semver4j 3.1.0 + + com.bpodgursky + jbool_expressions + 1.24 + test + + + io.micrometer + micrometer-core + 1.5.5 + + + org.latencyutils + LatencyUtils + 2.0.3 + + + de.huxhorn.sulky + de.huxhorn.sulky.ulid + 8.2.0 + diff --git a/.build/run-ci b/.build/run-ci index 04a257d85850..2cb0f6cb61ad 100755 --- a/.build/run-ci +++ b/.build/run-ci @@ -203,10 +203,10 @@ def parse_arguments() -> argparse.Namespace: """ args = argument_parser().parse_args() - assert args.repository.startswith("https://github.com/") and args.repository.endswith("cassandra.git"),\ + assert args.repository.startswith("https://github.com/") and args.repository.removesuffix(".git").endswith("cassandra"),\ f"Only github apache/cassandra (forked) repository supported, got: {args.repository}" - assert args.dtest_repository.startswith("https://github.com/") and args.dtest_repository.endswith("cassandra-dtest.git"),\ - f"Only github apache/cassandra (forked) repository supported, got: {args.dtest_repository}" + assert args.dtest_repository.startswith("https://github.com/") and args.dtest_repository.removesuffix(".git").endswith("cassandra-dtest"),\ + f"Only github apache/cassandra-dtest (forked) repository supported, got: {args.dtest_repository}" 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." @@ -305,27 +305,36 @@ def get_jenkins(k8s_client: client.CoreV1Api, args, kube_ns: str) -> Tuple[str, return ip, server -def trigger_jenkins_build(server: jenkins.Jenkins, job_name: str, **build_params) -> dict: - """Triggers a Jenkins build with specified parameters and returns the queue item.""" +def ensure_job_parameters_visible(server: jenkins.Jenkins, job_name: str): + """ + If necessary, triggers a non-parameter build to make parameterised builds visible. + """ + job_info = server.get_job_info(job_name) + if any(param.get("parameterDefinitions") for param in job_info.get("property", [])): + return - def check_for_parameter_build(server: jenkins.Jenkins, job_name: str): - """ - If necessary, triggers a non-parameter build (which makes the parameterised build visible). - """ - job_info = server.get_job_info(job_name) - if not any(param.get("parameterDefinitions") for param in job_info.get("property", [])): - print("Parameters are not visible; initiating non-parameter build.") - queue_item = server.build_job(job_name) - build_number = wait_for_build_number(server, queue_item) - time.sleep(6) - try: - server.stop_build(job_name, build_number) - except client.exceptions.ApiException as e: - print(f"Failed to stop non-parameter build {job_name} {build_number} for job : {e}") - print("Parameters should now be available.") + print(f"Parameters are not visible for job {job_name}; initiating non-parameter build.") + queue_item = server.build_job(job_name) + build_number = wait_for_build_number(server, queue_item) + time.sleep(6) + try: + server.stop_build(job_name, build_number) + except client.exceptions.ApiException as e: + print(f"Failed to stop non-parameter build {job_name} {build_number}: {e}") + print(f"Parameters should now be available for job {job_name}.") + + +def ensure_cassandra_job_parameters_visible(server: jenkins.Jenkins): + """Ensures parameterised builds are visible for all cassandra* jobs.""" + for job in server.get_jobs(): + job_name = job.get("name", "") + if job_name.startswith("cassandra"): + ensure_job_parameters_visible(server, job_name) - # Check and trigger non-parameter build if parameters are not visible - check_for_parameter_build(server, job_name) + +def trigger_jenkins_build(server: jenkins.Jenkins, job_name: str, **build_params) -> dict: + """Triggers a Jenkins build with specified parameters and returns the queue item.""" + ensure_job_parameters_visible(server, job_name) print("Triggering Jenkins build… ") return server.build_job(job_name, parameters=build_params) @@ -818,6 +827,8 @@ def main(): install_jenkins(args.kubeconfig, args.kubecontext, DEFAULT_KUBE_NS) (ip, server) = get_jenkins(k8s_client, args, DEFAULT_KUBE_NS) + if args.setup or args.only_setup: + ensure_cassandra_job_parameters_visible(server) if args.only_setup: return if args.download_results: diff --git a/.build/run-python-dtests.sh b/.build/run-python-dtests.sh index f57f69668a57..be092b2c3d93 100755 --- a/.build/run-python-dtests.sh +++ b/.build/run-python-dtests.sh @@ -105,6 +105,7 @@ ALLOWED_DTEST_VARIANTS="large|latest|upgrade|novnode|latest" [[ "${DTEST_TARGET}" =~ ^dtest(-(${ALLOWED_DTEST_VARIANTS}))*$ ]] || { echo >&2 "Unknown dtest target: ${DTEST_TARGET}. Allowed variants are ${ALLOWED_DTEST_VARIANTS}"; exit 1; } java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F. '{print $1}') +project_name=$(grep '/dev/null - until git clone --quiet --depth 1 --no-single-branch https://github.com/apache/cassandra.git cassandra-dtest-jars ; do echo "git clone failed… trying again… " ; done + until git clone --quiet --depth 1 --no-single-branch --tags https://github.com/apache/cassandra.git cassandra-dtest-jars ; do echo "git clone failed… trying again… " ; done popd >/dev/null fi @@ -202,7 +200,10 @@ _build_all_dtest_jars() { [ "${java_version}" -eq 11 ] && export CASSANDRA_USE_JDK11=true pushd ${TMP_DIR}/cassandra-dtest-jars >/dev/null - for branch in cassandra-4.0 cassandra-4.1 cassandra-5.0 ; do + # Converged Core skips its corresponding branch (e.g. cassandra-5.0) as its always behind it + # Note: cassandra-5.0.7 tag is used instead of cassandra-5.0 branch to enable + # testing upgrades from 5.0.7 to the current local build for autorepair feature + for branch in cassandra-4.0 cassandra-4.1 cassandra-5.0.7 ; do git clean -qxdff && git reset --hard HEAD || echo "failed to reset/clean ${TMP_DIR}/cassandra-dtest-jars… continuing…" git checkout --quiet $branch dtest_jar_version=$(grep 'property\s*name=\"base.version\"' build.xml |sed -ne 's/.*value=\"\([^"]*\)\".*/\1/p') @@ -279,16 +280,85 @@ _run_testlist() { [ "${_test_iterations}" -eq 1 ] || printf "––––\nfailure rate: ${failures}/${_test_iterations}\n" } +_list_microbench_tests() { + # Extract blacklist from build-bench.xml property (see CASSANDRA-18873) + local blacklist_pattern=$(grep 'name="microbench.exclude.pattern"' .build/build-bench.xml | sed -n 's/.*value="\([^"]*\)".*/\1/p') + + # Find all *Bench.java files, strip prefix, sort, and filter out blacklisted ones + find "test/microbench" -name '*Bench.java' | \ + sed "s;^test/microbench/;;g" | \ + sort | \ + grep -vE "(${blacklist_pattern})\.java$" +} + +_run_microbench() { + local _target=$1 + local _test_name_regexp=$2 + local _split_chunk=$3 + local testlist="" + + # Assert no *Test.java files exist under test/microbench + # uncomment once CachingBenchTest and GcCompactionBenchTest are rewritten to JMH benchmarks + #_list_tests "microbench" | grep -q 'Test\.java$' && error 1 "Found *Test.java files under test/microbench, these should be moved to test/unit" + + # Build test list from either regexp or split + if [ -n "${_test_name_regexp}" ]; then + echo "Running tests: ${_test_name_regexp}" + # test regexp can come in csv + for i in ${_test_name_regexp//,/ }; do + [ -n "${testlist}" ] && testlist="${testlist}"$'\n' + testlist="${testlist}$( _list_microbench_tests | _split_tests "${i}")" + done + [[ -z "${testlist}" ]] && error 1 "No tests found in test name regexp: ${_test_name_regexp}" + else + [ -n "${_split_chunk}" ] || { error 1 "Neither name regexp or split chunk defined"; } + echo "Running split: ${_split_chunk}" + testlist="$( _list_microbench_tests | _split_tests "${_split_chunk}")" + if [[ -z "${testlist}" ]]; then + echo "No microbench tests in split ${_split_chunk}, skipping" + return 0 + fi + fi + + # Convert file paths to the JMH classname pattern + local benchmark_pattern=$(echo "${testlist}" | sed 's/\.java$//g' | sed 's|^org/apache/cassandra/test/microbench/||g' | sed 's/\//./g' | tr '\n' '|' | sed 's/|$//') + echo "Running benchmarks: ${benchmark_pattern}" + + # override build.test.output.dir, adding jdk and arch to output path for report separation + local -r java_version="$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F. '{print $1}')" + local -r arch="$(uname -m)" + local -r output_dir="${DIST_DIR}/test/output/${_target}/jdk${java_version}/${arch}/${_split_chunk//\//_}" + + ant $_target ${ANT_TEST_OPTS} -Dbuild.test.output.dir=${output_dir} -Dbenchmark.name="${benchmark_pattern}" -Dmaven.test.failure.ignore=true + + # Post-process jmh-result.json to add jdk and arch parameters + local jmh_result="${output_dir}/jmh-result.json" + if [ -f "${jmh_result}" ]; then + python3 -c " +import json,sys +with open('${jmh_result}','r') as f: + data=json.load(f) +for r in (data if isinstance(data,list) else [data]): + if 'params' not in r: + r['params']={} + r['params']['jdk']='${java_version}' + r['params']['arch']='${arch}' +with open('${jmh_result}','w') as f: + json.dump(data,f) +" + fi +} + _main() { # parameters local -r target="${test_target/-repeat/}" - local -r split_chunk="${chunk:-'1/1'}" # Chunks formatted as "K/N" for the Kth chunk of N chunks + local -r split_chunk="${chunk:-1/1}" # Chunks formatted as "K/N" for the Kth chunk of N chunks # check split_chunk is compatible with target (if not a regexp) if [[ "${_split_chunk}" =~ ^\d+/\d+$ ]] && [[ "1/1" != "${split_chunk}" ]] ; then case ${target} in - "stress-test" | "fqltool-test" | "microbench" | "cqlsh-test" | "simulator-dtest") - error 1 "Target ${target} does not suport splits." + "stress-test" | "fqltool-test" | "cqlsh-test" | "simulator-dtest") + error 1 "Target ${target} does not support splits." ;; *) ;; @@ -310,6 +380,7 @@ _main() { # jdk check local -r java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F. '{print $1}') + local -r project_name=$(grep ' - - - -patch by ; reviewed by for CASSANDRA-##### - -Co-authored-by: Name1 -Co-authored-by: Name2 - -``` - -The [Cassandra Jira](https://issues.apache.org/jira/projects/CASSANDRA/issues/) +### What is the issue +... +### What does this PR fix and why was it fixed +... diff --git a/.github/scripts/run_sonar_analysis.sh b/.github/scripts/run_sonar_analysis.sh new file mode 100755 index 000000000000..2a66577ed2cd --- /dev/null +++ b/.github/scripts/run_sonar_analysis.sh @@ -0,0 +1,152 @@ +#!/bin/bash +set +e + +# Get Git/GitHub context +GIT_BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" +GIT_REPO_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" +GIT_BASE_BRANCH="${GITHUB_BASE_REF:-main}" + +# SonarQube configuration (from job env vars) +PROJECT_KEY="${SONAR_PROJECT_KEY}" +PROJECT_NAME="${SONAR_PROJECT_NAME}" +SONAR_HOST_URL="${SONAR_HOST}" + +# Retry configuration +MAX_RETRIES=3 +RETRY_DELAY=30 + +echo "==========================================" +echo "SonarQube Analysis Configuration" +echo "==========================================" +echo "Project Key: $PROJECT_KEY" +echo "Project Name: $PROJECT_NAME" +echo "Branch: $GIT_BRANCH" +echo "SonarQube Host: $SONAR_HOST_URL" +echo "Max Retries: $MAX_RETRIES" +echo "==========================================" + +# Note: Authentication uses the SONAR_TOKEN environment variable. +# This is the standard SonarQube practice - sonar-scanner automatically +# reads SONAR_TOKEN from the environment. +# +# Note: No truststore configuration needed. The whitewater.ibm.com endpoint +# uses Cloudflare with public DigiCert certificates trusted by system CA bundles. + +# Build sonar-scanner arguments +SONAR_ARGS=( + -Dsonar.projectKey="$PROJECT_KEY" + -Dsonar.projectName="$PROJECT_NAME" + -Dsonar.host.url="$SONAR_HOST_URL" + -Dsonar.token="$SONAR_TOKEN" + -Dsonar.links.homepage="$GIT_REPO_URL" + -Dsonar.qualitygate.wait=true +) + +# Add PR-specific or branch-specific arguments +if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + GIT_SHA="${GITHUB_EVENT_PULL_REQUEST_HEAD_SHA}" + GIT_PR="${GITHUB_EVENT_PULL_REQUEST_NUMBER}" + + SONAR_ARGS+=( + -Dsonar.pullrequest.key="$GIT_PR" + -Dsonar.pullrequest.branch="$GIT_BRANCH" + -Dsonar.pullrequest.base="$GIT_BASE_BRANCH" + -Dsonar.scm.revision="$GIT_SHA" + -Dsonar.links.scm="$GIT_REPO_URL/pull/$GIT_PR" + ) +else + SONAR_ARGS+=( + -Dsonar.branch.name="$GIT_BRANCH" + -Dsonar.links.scm="$GIT_REPO_URL/tree/$GIT_BRANCH" + ) + + # Only set reference branch when analyzing a branch that is different from the base branch + # Sonar rejects setting the reference branch to the same branch being analyzed + if [[ "$GIT_BRANCH" != "$GIT_BASE_BRANCH" ]]; then + SONAR_ARGS+=(-Dsonar.newCode.referenceBranch="$GIT_BASE_BRANCH") + fi +fi + +# Add debug flag if enabled +if [[ "$DEBUG_MODE" == "true" ]]; then + echo "Debug mode enabled: Sonar verbose output" + SONAR_ARGS+=(-Dsonar.verbose=true) +fi + +# Retry loop +for attempt in $(seq 1 $MAX_RETRIES); do + echo "" + echo "==========================================" + echo "Attempt $attempt of $MAX_RETRIES" + echo "==========================================" + + if [[ $attempt -gt 1 ]]; then + echo "Waiting ${RETRY_DELAY}s before retry..." + sleep $RETRY_DELAY + RETRY_DELAY=$((RETRY_DELAY * 2)) + fi + + echo "Starting SonarQube analysis..." + set -x + sonar-scanner "${SONAR_ARGS[@]}" 2>&1 | tee sonar-output.log + RESULT=$? + set +x + + # Check for Quality Gate failure first (this is NOT an error to retry) + if grep -q "QUALITY GATE STATUS: FAILED" sonar-output.log; then + echo "" + echo "==========================================" + echo "⚠️ Quality Gate FAILED" + echo "==========================================" + echo "Dashboard: $SONAR_HOST_URL/dashboard?id=$PROJECT_KEY&branch=$GIT_BRANCH" + echo "This is a code quality issue - fix the issues reported and re-run." + echo "result=quality_gate_failed" >> $GITHUB_OUTPUT + exit 1 + fi + + # Check for errors in log even if exit code is 0 (sonar-scanner bug in newer versions) + if grep -Eq "ERROR|FAILURE|BUILD FAILURE|Failed to|IllegalStateException|EXECUTION FAILURE" sonar-output.log; then + echo "ERROR detected in scanner output, treating as failure" + RESULT=1 + fi + + if [[ $RESULT -eq 0 ]]; then + echo "" + echo "==========================================" + echo "✓ SonarQube analysis completed successfully!" + echo "==========================================" + + # Extract dashboard URL from report-task.txt (like Jenkins does) + if [[ -f ".scannerwork/report-task.txt" ]]; then + DASHBOARD_URL=$(grep "^dashboardUrl=" .scannerwork/report-task.txt | cut -d'=' -f2-) + if [[ -n "$DASHBOARD_URL" ]]; then + echo "Dashboard URL: $DASHBOARD_URL" + echo "dashboard_url=$DASHBOARD_URL" >> $GITHUB_OUTPUT + fi + fi + + echo "result=success" >> $GITHUB_OUTPUT + exit 0 + else + echo "" + echo "==========================================" + echo "✗ SonarQube analysis failed with exit code: $RESULT" + echo "==========================================" + + # Check if this is a transient error (retry these) + if grep -Eqi "503|Service Unavailable|Timeout|Connection reset|temporarily unavailable|ConnectException|SocketTimeoutException" sonar-output.log; then + if [[ $attempt -lt $MAX_RETRIES ]]; then + echo "Transient error detected. Will retry..." + else + echo "Max retries reached. Failing." + echo "result=failure" >> $GITHUB_OUTPUT + exit $RESULT + fi + else + # Other errors - don't retry + echo "Non-transient error. Not retrying." + echo "result=failure" >> $GITHUB_OUTPUT + exit $RESULT + fi + fi +done diff --git a/.github/workflows/checklist_comment_on_new_pr.yml b/.github/workflows/checklist_comment_on_new_pr.yml new file mode 100644 index 000000000000..c7af619972e4 --- /dev/null +++ b/.github/workflows/checklist_comment_on_new_pr.yml @@ -0,0 +1,18 @@ +name: Comment on new Pull Request with checklist +on: + pull_request: + types: opened + +jobs: + checklist-comment: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Add comment + run: | + sed "s/{{PR_NUMBER}}/$PRNUM/" .github/workflows/pr_checklist.md | gh pr comment $PRNUM --body-file - + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PRNUM: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr_checklist.md b/.github/workflows/pr_checklist.md new file mode 100644 index 000000000000..e32f5998d9a4 --- /dev/null +++ b/.github/workflows/pr_checklist.md @@ -0,0 +1,12 @@ +### Checklist before you submit for review +- [ ] This PR adheres to [the Definition of Done](https://github.com/riptano/cndb/blob/main/DEFINITION_OF_DONE.md) +- [ ] Make sure there is a PR and ticket in the CNDB project updating the Converged Cassandra version +- [ ] Use `NoSpamLogger` for log lines that may appear frequently in the logs +- [ ] Verify test results on Butler +- [ ] Test coverage for new/modified code is > 80%, check manually at [SonarCloud page](https://sonarcloud.io/summary/new_code?id=cassandra-stargazer&pullRequest={{PR_NUMBER}}) +- [ ] Proper code formatting +- [ ] Proper title for each commit staring with the project-issue number, like CNDB-1234 +- [ ] Each commit has a meaningful description +- [ ] Each commit is not very long and contains related changes +- [ ] Renames, moves and reformatting are in distinct commits +- [ ] All new files should contain the IBM copyright header instead of the Apache License one (no DataStax copyright any longer) diff --git a/.github/workflows/sonarqube-scan.yaml b/.github/workflows/sonarqube-scan.yaml new file mode 100644 index 000000000000..bbcbea154657 --- /dev/null +++ b/.github/workflows/sonarqube-scan.yaml @@ -0,0 +1,143 @@ +name: SonarQube Code Quality Scan + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to scan (default: main)' + required: false + default: 'main' + type: string + debug: + description: 'Enable debug output (Sonar verbose)' + required: false + default: false + type: boolean + +concurrency: + group: sonarqube-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + sonar-analysis: + runs-on: ubuntu-latest + name: SonarQube Analysis + env: + SONAR_HOST: https://sonarqube-prod.whitewater.ibm.com + SONAR_DASHBOARD_HOST: https://sonarqube-prod.apps.wdc-sonarqube-prod.core.cirrus.ibm.com + SONAR_PROJECT_KEY: 544478-247111689 + SONAR_PROJECT_NAME: datastax/cassandra + steps: + - name: Set target branch + id: set-branch + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "branch=${{ inputs.branch }}" >> $GITHUB_OUTPUT + else + echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT + fi + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ steps.set-branch.outputs.branch }} + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Cache Ant dependencies + uses: actions/cache@v4 + with: + path: | + ~/.ant + lib + key: ant-${{ hashFiles('build.xml') }} + restore-keys: ant- + + - name: Build Cassandra + run: | + echo "Building Cassandra..." + ant clean jar + echo "Build completed" + + - name: Check SonarQube server availability + id: health-check + continue-on-error: true + run: | + echo "Checking SonarQube server health..." + response=$(curl -s -o /dev/null -w "%{http_code}" $SONAR_HOST/api/system/status 2>&1) + echo "Server response code: $response" + if [[ "$response" == "200" ]]; then + echo "server_available=true" >> $GITHUB_OUTPUT + else + echo "server_available=false" >> $GITHUB_OUTPUT + fi + + - name: Install SonarQube Scanner + run: | + echo "Installing SonarQube Scanner CLI..." + wget -q https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-linux-x64.zip + unzip -q sonar-scanner-cli-8.1.0.6389-linux-x64.zip + echo "$(pwd)/sonar-scanner-8.1.0.6389-linux-x64/bin" >> $GITHUB_PATH + + - name: Run SonarQube analysis + id: sonar-scan + env: + SONAR_TOKEN: ${{ secrets.IBM_SONARQUBE_API_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEBUG_MODE: ${{ inputs.debug }} + run: | + chmod +x .github/scripts/run_sonar_analysis.sh + .github/scripts/run_sonar_analysis.sh + + - name: Publish analysis summary + if: always() + run: | + echo "## SonarQube Analysis Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.sonar-scan.outputs.result }}" == "success" ]]; then + echo "[OK] **Status**: Analysis completed successfully" >> $GITHUB_STEP_SUMMARY + elif [[ "${{ steps.sonar-scan.outputs.result }}" == "quality_gate_failed" ]]; then + echo "[WARN] **Status**: Quality Gate Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**This is a code quality issue, not an infrastructure problem.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Click the dashboard link below to see which quality conditions failed" >> $GITHUB_STEP_SUMMARY + echo "2. Common issues: low test coverage, code smells, bugs, security hotspots, duplicated code" >> $GITHUB_STEP_SUMMARY + echo "3. Fix the code quality issues and push again" >> $GITHUB_STEP_SUMMARY + else + echo "[ERROR] **Status**: Analysis failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch**: \`${{ steps.set-branch.outputs.branch }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Run ID**: ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.health-check.outputs.server_available }}" == "false" ]]; then + echo "[WARN] **Warning**: SonarQube server health check failed before analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo "### Links" >> $GITHUB_STEP_SUMMARY + + # Use extracted dashboard URL if available, otherwise construct it + DASHBOARD_URL="${{ steps.sonar-scan.outputs.dashboard_url }}" + if [[ -n "$DASHBOARD_URL" ]]; then + echo "- [SonarQube Dashboard]($DASHBOARD_URL)" >> $GITHUB_STEP_SUMMARY + else + echo "- [SonarQube Dashboard]($SONAR_DASHBOARD_HOST/dashboard?id=$SONAR_PROJECT_KEY&branch=${{ steps.set-branch.outputs.branch }})" >> $GITHUB_STEP_SUMMARY + fi + + echo "- [Workflow Run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + - name: Fail job if analysis failed + if: steps.sonar-scan.outputs.result != 'success' + run: exit 1 diff --git a/.gitignore b/.gitignore index 4531bc1e6773..3210784b8286 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ logs/ data/ !test/data conf/hotspot_compiler +doc/antora.yml doc/cql3/CQL.html doc/build/ lib/ @@ -60,15 +61,22 @@ nbactions.xml #VS code .vscode/ +# Aider (aider.chat) +.aider* + # Maven, etc. out/ target/ +# AI agents +.ai/ + # General **/__pycache__ *.pyc *~ *.bak +*.log *.sw[o,p] *.tmp .DS_Store @@ -93,3 +101,6 @@ cassandra-builds/ cassandra-dtest/ conf/triggers/trigger-example.jar + +agent_log +.bob \ No newline at end of file diff --git a/.jenkins/Jenkinsfile b/.jenkins/Jenkinsfile index faf3f410cf24..8db95dec44c3 100644 --- a/.jenkins/Jenkinsfile +++ b/.jenkins/Jenkinsfile @@ -1,375 +1,85 @@ -#!/usr/bin/env groovy -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// -// Jenkins CI declaration. -// -// The declarative pipeline is presented first as a high level view. -// -// Build and Test Stages are dynamic, the full possible list defined by the `tasks()` function. -// There is a choice of pipeline profles with sets of tasks that are run, see `pipelineProfiles()`. -// -// All tasks use the dockerised CI-agnostic scripts found under `.build/docker/` -// The `type: test` always `.build/docker/run-tests.sh` -// -// -// This Jenkinsfile is expected to work on any Jenkins infrastructure. -// The controller should have 4 cpu, 12GB ram (and be configured to use `-XX:+UseG1GC -Xmx8G`) -// -// It is required to have agents providing 6+ labels, each that can provide docker and the following capabilities: -// -// - cassandra-small + cassandra-${arch}-small : 1 cpu, 1GB ram (alias for above but for any arch) -// - cassandra-medium + cassandra-${arch}-medium : 3 cpu, 5GB ram -// - cassandra-large + cassandra-${arch}-large : 7 cpu, 16GB ram -// -// Performance targets required a `cassandra-${arch}-large-dedicated` labelled nodes. -// -// When running builds parameterised to other architectures the corresponding labels are expected. -// For example 'arm64' requires the labels: cassandra-arm64-small, cassandra-arm64-medium, cassandra-arm64-large. -// -// Plugins required are: -// git, workflow-job, workflow-cps, junit, workflow-aggregator, ws-cleanup, pipeline-build-step, test-stability, copyartifact, jmh-report. -// See .jenkins/k8s/jenkins-deployment.yaml for up to date list of plugins. -// -// Any functionality that depends upon ASF Infra ( i.e. the canonical ci-cassandra.a.o ) -// will be ignored when run on other environments. -// Note there are also differences when CI is being run pre- or post-commit. -// -// CAUTION! When running CI with changes in this file, ensure the "Pipeline script from SCM" scm details match -// the brances being tested. These details don't honour the per-build repository and branch parameterisation. -// -// Validate/lint this file using the following command -// `curl -X POST -F "jenkinsfile=<.jenkins/Jenkinsfile" https://ci-cassandra.apache.org/pipeline-model-converter/validate` -// - -/** CONSTANTS for both the pipeline and scripting **/ -import groovy.transform.Field -@Field List archsSupported = ["amd64", "arm64"] -@Field List pythonsSupported = ["3.8", "3.11"] -@Field String pythonDefault = "3.8" -/** CONSTANTS end **********************************/ - -pipeline { - agent { label 'cassandra-small' } - options { - // must have: avoids agents waste in idle time on controller bottleneck - durabilityHint('PERFORMANCE_OPTIMIZED') - disableResume() - } - parameters { - string(name: 'repository', defaultValue: params.repository ?: scm.userRemoteConfigs[0].url, description: 'Cassandra Repository') - string(name: 'branch', defaultValue: params.branch ?: scm.userRemoteConfigs[0].refspec, description: 'Branch') - - choice(name: 'profile', choices: pipelineProfileNames(params.profile ?: ''), description: 'Pick a pipeline profile.') - string(name: 'profile_custom_regexp', defaultValue: params.profile_custom_regexp ?: '', description: 'Regexp for stages when using custom profile. See `testSteps` in Jenkinsfile for list of stages. Example: stress.*|jvm-dtest.*') - - choice(name: 'architecture', choices: archsSupported + "all", description: 'Pick architecture. The ARM64 is disabled by default at the moment.') - string(name: 'jdk', defaultValue: params.jdk ?: '', description: 'Restrict JDK versions. (e.g. "11", "17", etc)') - - string(name: 'dtest_repository', defaultValue: params.dtest_repository ?: 'https://github.com/apache/cassandra-dtest', description: 'Cassandra DTest Repository') - string(name: 'dtest_branch', defaultValue: params.dtest_branch ?: 'trunk', description: 'DTest Branch') - } - stages { - stage('init') { - steps { - script { - // this helps assure folk their parameters are correct and will be used (despite the earlier output about the configured job coordinates) - echo "Printing parameters used for this build" - ["Repository: ${params.repository}", "Branch: ${params.branch}", "Profile: ${params.profile}", "Custom Profile Regexp: ${params.profile_custom_regexp}", "Architecture: ${params.architecture}", "JDK: ${params.jdk}", "DTest Repository: ${params.dtest_repository}", "DTest Branch: ${params.dtest_branch}"].each { println it } - } - } - } - stage('jar') { - // the jar stage executes only the 'jar' build step, via the build(…) function - // the results of these (per jdk, per arch) are then stashed and used for every other build and test step - steps { - script { - parallel(getJarTasks()) - } - } - } - stage('Tests') { - // the Tests stage executes all other build and task steps. - // build steps are sent to the build(…) function, test steps sent to the test(…) function - // these steps are parameterised and split by the tasks() function - when { - expression { hasNonJarTasks() } - } - steps { - script { - parallel(tasks()['tests']) - } - } - } - stage('Summary') { - // generate the ci_summary.html and results_details.tar.xz artefacts - steps { - generateTestReports() - } - } - } - post { - failure { - echo "ERROR pipeline failed – not all tests were run" - } - always { - sendNotifications() - } - } +#!groovy + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +properties([ + buildDiscarder(logRotator(daysToKeepStr: '14', numToKeepStr: '512')), + disableConcurrentBuilds(), + parameters([ + string(defaultValue: 'https://github.com/apache/cassandra.git', description: 'What git repository should be used to build and test from?', name: 'repository'), + string(defaultValue: 'trunk', description: 'What git branch should be used to build and test from?', name: 'branch'), + string(defaultValue: 'https://github.com/apache/cassandra-dtest.git', description: 'What git repository should be used to build and test Python dtests from?', name: 'dtest_repository'), + string(defaultValue: 'trunk', description: 'What git branch should be used to build and test Python dtests from?', name: 'dtest_branch'), + text(defaultValue: '{"steps": ["jar", "jvm-dtest-upgrade", "jvm-dtest", "jvm-dtest-upgrade-40", "jvm-dtest-upgrade-41", "jvm-upgrade-dtest", "jvm-upgrade-dtest-vnode", "jvm-upgrade-dtest-no-vnode", "jvm-upgrade-dtest-large", "jvm-upgrade-dtest-40", "jvm-upgrade-dtest-41", "jvm-upgrade-dtest-ssl", "jvm-upgrade-dtest-no-preview", "jvm-upgrade-dtest-storage", "jvm-upgrade-dtest-latest-killer", "jvm-upgrade-dtest-tls", "jvm-upgrade-dtest-auth", "jvm-dtest-latest", "jvm-dtest-latest-vnode", "jvm-dtest-latest-cdc", "jvm-dtest-latest-auth", "jvm-dtest-latest-compression", "jvm-dtest-latest-legacy-sstable", "jvm-dtest-latest-oa", "jvm-dtest-latest-large", "jvm-dtest-latest-no-vnode", "jvm-dtest-latest-sai", "jvm-dtest-latest-ssl", "jvm-dtest-latest-tls", "jvm-dtest-latest-upgrade", "jvm-dtest-latest-vnode-upgrade", "jvm-dtest-latest-trigger", "jvm-dtest-latest-materialized-view", "jvm-dtest-latest-transient-replication", "jvm-dtest-latest-counters", "jvm-dtest-latest-repair", "jvm-dtest-latest-secondary-index", "jvm-dtest-latest-paging", "jvm-dtest-latest-topology", "jvm-dtest-latest-system-keyspace-directory", "jvm-dtest-latest-stress", "jvm-dtest-latest-fqltool", "jvm-dtest-latest-reverse-query", "jvm-dtest-latest-cql", "jvm-dtest-latest-jmx", "jvm-dtest-latest-sql", "jvm-dtest-latest-bti", "jvm-dtest-latest-accord", "microbench", "cqlsh-test"], "cells": [{"step": "jar", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-upgrade", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-upgrade-40", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-upgrade-41", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-vnode", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-no-vnode", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-large", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-40", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-41", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-ssl", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-no-preview", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-storage", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-latest-killer", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-tls", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-upgrade-dtest-auth", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-vnode", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-cdc", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-auth", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-compression", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-legacy-sstable", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-oa", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-large", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-no-vnode", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-sai", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-ssl", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-tls", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-upgrade", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-vnode-upgrade", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-trigger", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-materialized-view", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-transient-replication", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-counters", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-repair", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-secondary-index", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-paging", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-topology", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-system-keyspace-directory", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-stress", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-fqltool", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-reverse-query", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-cql", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-jmx", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-sql", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-bti", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "jvm-dtest-latest-accord", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "microbench", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "yes"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.8", "cython": "no"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.10", "cython": "yes"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.10", "cython": "no"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.11", "cython": "yes"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.11", "cython": "no"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.12", "cython": "yes"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.12", "cython": "no"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.13", "cython": "yes"}, {"step": "cqlsh-test", "arch": "amd64", "jdk": "11", "python": "3.13", "cython": "no"}], "splits": 4}', description: 'Describe all test runs', name: 'config') + ]) +]) + +def getConfig() { + return readJSON(text: params.config) } -/////////////////////////// -//// scripting support //// -/////////////////////////// - -@NonCPS -def pipelineProfiles() { - return [ - 'packaging': ['artifacts', 'lint', 'debian', 'redhat'], - 'skinny': ['lint', 'cqlsh-test', 'test', 'jvm-dtest', 'simulator-dtest', 'dtest'], - 'pre-commit': ['artifacts', 'lint', 'debian', 'redhat', 'fqltool-test', 'cqlsh-test', 'test', 'test-latest', 'stress-test', 'test-burn', 'jvm-dtest', 'simulator-dtest', 'dtest', 'dtest-latest', 'microbench-test'], - 'pre-commit w/ upgrades': ['artifacts', 'lint', 'debian', 'redhat', 'fqltool-test', 'cqlsh-test', 'test', 'test-latest', 'stress-test', 'test-burn', 'jvm-dtest', 'jvm-dtest-upgrade', 'simulator-dtest', 'dtest', 'dtest-novnode', 'dtest-latest', 'dtest-upgrade', 'microbench-test'], - 'post-commit': ['artifacts', 'lint', 'debian', 'redhat', 'fqltool-test', 'cqlsh-test', 'test-cdc', 'test', 'test-latest', 'test-compression', 'stress-test', 'test-burn', 'long-test', 'test-oa', 'test-system-keyspace-directory', 'jvm-dtest', 'jvm-dtest-upgrade', 'simulator-dtest', 'dtest', 'dtest-novnode', 'dtest-latest', 'dtest-large', 'dtest-large-novnode', 'dtest-large-latest', 'dtest-upgrade', 'dtest-upgrade-novnode', 'dtest-upgrade-large', 'dtest-upgrade-large-novnode', 'microbench-test'], - 'performance': ['microbench'], - 'custom': [] - ] -} - -@NonCPS -def pipelineProfileNames(putFirst) { - set = pipelineProfiles().keySet() as List - set = set - putFirst - set.add(0, putFirst) - return set -} - -@Field Map cachedTasks = null - -def tasks() { - if (null != cachedTasks) return cachedTasks - - // Steps config - def buildSteps = [ - 'jar': [script: 'build-jars.sh', toCopy: null], - 'artifacts': [script: 'build-artifacts.sh', toCopy: 'apache-cassandra-*.tar.gz,apache-cassandra-*.jar,apache-cassandra-*.pom'], - 'lint': [script: 'check-code.sh', toCopy: null], - 'debian': [script: 'build-debian.sh', toCopy: 'cassandra_*,cassandra-tools_*'], - 'redhat': [script: 'build-redhat.sh rpm', toCopy: '*.rpm'], - ] - buildSteps.each() { - it.value.put('type', 'build') - it.value.put('size', 'small') - it.value.put('splits', 1) - } - - def testSteps = [ - // Each splits size need to be high enough to avoid the one hour per split timeout, - // and low enough so test time is factors more than the setup+build time in each split. - // Splits can also be poorly balanced: splitting or renaming test classes is the best tactic. - // On unsaturated systems 10 minutes per split is optimal, higher with saturation - // (some buffer on the heaviest split under the 1h max is required, ref `timeout(…)` in `test(…)`) - 'cqlsh-test': [splits: 1], - 'fqltool-test': [splits: 1, size: 'small'], - 'test-cdc': [splits: 8], - 'test': [splits: 16], - 'test-latest': [splits: 16], - 'test-compression': [splits: 16], - 'stress-test': [splits: 1, size: 'small'], - 'test-burn': [splits: 2], - 'long-test': [splits: 4], - 'test-oa': [splits: 16], - 'test-system-keyspace-directory': [splits: 16], - 'jvm-dtest': [splits: 12], - 'jvm-dtest-upgrade': [splits: 6], - 'simulator-dtest': [splits: 1, size: 'large'], - 'dtest': [splits: 64, size: 'large'], - 'dtest-novnode': [splits: 64, size: 'large'], - 'dtest-latest': [splits: 64, size: 'large'], - 'dtest-large': [splits: 6, size: 'large'], - 'dtest-large-novnode': [splits: 6, size: 'large'], - 'dtest-large-latest': [splits: 6, size: 'large'], - 'dtest-upgrade': [splits: 128, size: 'large'], - 'dtest-upgrade-novnode': [splits: 128, size: 'large'], - 'dtest-upgrade-large': [splits: 32, size: 'large'], - 'dtest-upgrade-large-novnode': [splits: 32, size: 'large'], - 'microbench-test': [splits: 1, size: 'large'], - // performance tests need 'cassandra-*large-dedicated' nodes - 'microbench': [splits: 1, size: 'large', timeout_hours: 6, benchmark: true], - ] - testSteps.each() { - it.value.put('type', 'test') - if (!it.value['size']) { - it.value.put('size', 'medium') - } - if (!it.value['timeout_hours']) { - // default 1 hour - it.value.put('timeout_hours', 1) - } - if (it.key.startsWith('dtest')) { - it.value.put('python-dtest', true) - } - } - - def stepsMap = buildSteps + testSteps - - // find the default JDK and the supported JDKs defined in the build.xml - def build_xml = readFile(file: 'build.xml') - def javaVersionDefaultMatch = (build_xml =~ /property\s*name="java\.default"\s*value="([^"]*)"/) - assert javaVersionDefaultMatch, "java.default property not found in build.xml" - def javaVersionDefault = javaVersionDefaultMatch[0][1] - def javaVersionsSupportedMatch = (build_xml =~ /property\s*name="java\.supported"\s*value="([^"]*)"/) - assert javaVersionsSupportedMatch, "java.supported property not found in build.xml" - def javaVersionsSupported = javaVersionsSupportedMatch[0][1].split(',') as List - - // define matrix axes - def Map matrix_axes = [ - arch: archsSupported, - jdk: javaVersionsSupported, - python: pythonsSupported, - cython: ['yes', 'no'], - step: stepsMap.keySet(), - split: (1..testSteps.values().splits.max()).toList() - ] - - def List _axes = getMatrixAxes(matrix_axes).findAll { axis -> - (isArchEnabled(axis['arch'])) && // skip disabled archs - (isJdkEnabled(axis['jdk'])) && // skip disabled jdks - (isStageEnabled(axis['step'])) && // skip disabled steps - !(axis['python'] != pythonDefault && 'cqlsh-test' != axis['step']) && // Use only python 3.8 for all tests but cqlsh-test - !(axis['cython'] != 'no' && 'cqlsh-test' != axis['step']) && // cython only for cqlsh-test, disable for others - !(axis['jdk'] != javaVersionDefault && ('cqlsh-test' == axis['step'] || 'simulator-dtest' == axis['step'] || axis['step'].contains('dtest-upgrade'))) && // run cqlsh-test, simulator-dtest, *dtest-upgrade only with jdk11 - // Disable splits for all but proper stages - !(axis['split'] > 1 && !stepsMap.findAll { entry -> entry.value.splits >= axis['split'] }.keySet().contains(axis['step'])) && - // run only the build types on non-amd64 - !(axis['arch'] != 'amd64' && !stepsMap.findAll { entry -> 'build' == entry.value.type }.keySet().contains(axis['step'])) - } - - def Map tasks = [ - jars: [failFast: true], - tests: [failFast: true] - ] - - for (def axis in _axes) { - def cell = axis - def name = getStepName(cell, stepsMap[cell.step]) - tasks[cell.step == "jar" ? "jars" : "tests"][name] = { -> - "${stepsMap[cell.step].type}"(stepsMap[cell.step], cell) - } - } - - return cachedTasks = tasks -} - -@NonCPS -def List getMatrixAxes(Map matrix_axes) { - def List axes = [] - matrix_axes.each { axis, values -> - List axisList = [] - values.each { value -> - axisList << [(axis): value] - } - axes << axisList +def getNodeLabel(command, cell) { + if (command.label) { + return command.label } - axes.combinations()*.sum() -} - -def getStepName(cell, command) { - def arch = "amd64" == cell.arch ? "" : " ${cell.arch}" - def python = "cqlsh-test" != cell.step ? "" : " python${cell.python}" - def cython = "no" == cell.cython ? "" : " cython" - def split = command.splits > 1 ? " ${cell.split}/${command.splits}" : "" - return "${cell.step}${arch} jdk${cell.jdk}${python}${cython}${split}" -} - -def getJarTasks() { - Map jars = tasks()['jars'] - assert jars.size() > 1, "Nothing to build. Check parameters: jdk ${params.jdk}, arch ${params.architecture}" - return jars -} - -def hasNonJarTasks() { - return tasks()['tests'].size() > 1 -} - -/** - * Is this a post-commit build (or a pre-commit build) - **/ -def isPostCommit() { - // any build of a branch found on github.com/apache/cassandra is considered a post-commit (post-merge) CI run - return params.repository && params.repository.contains("apache/cassandra") // no params exist first build -} - -/** - * Are we running on ci-cassandra.apache.org ? - **/ -def isCanonical() { - return "${JENKINS_URL}".contains("ci-cassandra.apache.org") -} - -def isStageEnabled(stage) { - return "jar" == stage || pipelineProfiles()[params.profile]?.contains(stage) || ("custom" == params.profile && stage ==~ params.profile_custom_regexp) + return cell.arch == 'arm64' ? 'linux && arm64' : 'linux && amd64' } -def isArchEnabled(arch) { - return params.architecture == arch || "all" == params.architecture -} - -def isJdkEnabled(jdk) { - return !params.jdk?.trim() || params.jdk.trim() == jdk +def copyToNightlies(artifacts, target) { + sh """ + mkdir -p /var/lib/jenkins/workspace/Nightlies/${JOB_NAME}/${BUILD_NUMBER}/${target} + cp -r ${artifacts} /var/lib/jenkins/workspace/Nightlies/${JOB_NAME}/${BUILD_NUMBER}/${target} + """ } -/** - * Renders build script into pipeline steps - **/ def build(command, cell) { - def build_script = ".build/docker/${command.script}" - def maxAttempts = 2 - def attempt = 0 - def nodeExclusion = "" - retry(maxAttempts) { - attempt++ - node(getNodeLabel(command, cell) + nodeExclusion) { - nodeExclusion = "&&!${NODE_NAME}" - withEnv(cell.collect { k, v -> "${k}=${v}" }) { - ws("workspace/${JOB_NAME}/${BUILD_NUMBER}/${cell.step}/${cell.arch}/jdk-${cell.jdk}") { + if (command.script) { + sh label: "RUNNING ${command.script}...", script: command.script + return + } + node(getNodeLabel(command, cell)) { + withEnv(cell.collect { k, v -> "${k}=${v}" }) { + ws("workspace/${JOB_NAME}/${BUILD_NUMBER}/${cell.step}/${cell.arch}/jdk-${cell.jdk}/python-${cell.python}") { + try { fetchSource(cell.step, cell.arch, cell.jdk) - sh """ - 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; } - """ - fetchDockerImages("redhat" == cell.step ? ['almalinux-build'] : ['bullseye-build']) - def cell_suffix = "_jdk${cell.jdk}_${cell.arch}" - def logfile = "stage-logs/${JOB_NAME}_${BUILD_NUMBER}_${cell.step}${cell_suffix}_attempt${attempt}.log.xz" - def script_vars = "#!/bin/bash \n set -o pipefail ; " // pipe to tee needs pipefail + fetchDockerImages(['ubuntu-test']) + def cell_suffix = "_jdk${cell.jdk}_python_${cell.python}_${cell.cython}_${cell.arch}" + def logfile = "stage-logs/${JOB_NAME}_${BUILD_NUMBER}_${cell.step}${cell_suffix}.log.xz" + def script_vars = "#!/bin/bash \n set -o pipefail ; " + script_vars = "${script_vars} python_version=\'${cell.python}\'" script_vars = "${script_vars} m2_dir=\'${WORKSPACE}/build/m2\'" - timeout(time: 1, unit: 'HOURS') { - def status = sh label: "RUNNING ${cell.step}...", script: "${script_vars} ${build_script} ${cell.jdk} 2>&1 | tee >( xz -c > build/${logfile} )", returnStatus: true - dir("build") { - archiveArtifacts artifacts: "${logfile}", fingerprint: true - copyToNightlies("${logfile}", "${cell.step}/jdk${cell.jdk}/${cell.arch}/") - } - if (0 != status) { error("Stage ${cell.step}${cell_suffix} failed with exit status ${status}") } - if ("jar" == cell.step) { - stash name: "${cell.arch}_${cell.jdk}" + script_vars = fetchDTestsSource(command, script_vars) + timeout(time: command.timeout_hours, unit: 'HOURS') { + try { + buildJVMDTestJars(cell, script_vars, logfile) + sh label: "RUNNING ${cell.step}...", script: "${script_vars} .build/docker/run-tests.sh -a ${cell.step} -j ${cell.jdk} 2>&1 | tee >( xz -c > build/${logfile} )" + } finally { + dir("build") { + archiveArtifacts artifacts: "${logfile}", fingerprint: true + stash name: "${cell.arch}_${cell.jdk}", includes: '**/*.tar.gz,**/*.deb,**/*.changes,**/apache-cassandra-*.jar,**/stage-logs/*.xz,**/test/**' + if (command.toCopy) { + copyToNightlies("${command.toCopy}", "${cell.step}/jdk${cell.jdk}/${cell.arch}/") + } + } } } - dir("build") { - copyToNightlies("${command.toCopy}", "${cell.step}/jdk${cell.jdk}/${cell.arch}/") - } + } finally { cleanAgent(cell.step) } } @@ -389,63 +99,72 @@ def test(command, cell) { nodeExclusion = "&&!${NODE_NAME}" withEnv(cell.collect { k, v -> "${k}=${v}" }) { ws("workspace/${JOB_NAME}/${BUILD_NUMBER}/${cell.step}/${cell.arch}/jdk-${cell.jdk}/python-${cell.python}") { - fetchSource(cell.step, cell.arch, cell.jdk) - fetchDockerImages(['ubuntu2004_test']) - def cell_suffix = "_jdk${cell.jdk}_python_${cell.python}_${cell.cython}_${cell.arch}_${cell.split}_${splits}" - def logfile = "stage-logs/${JOB_NAME}_${BUILD_NUMBER}_${cell.step}${cell_suffix}_attempt${attempt}.log.xz" - def script_vars = "#!/bin/bash \n set -o pipefail ; " // pipe to tee needs pipefail - script_vars = "${script_vars} python_version=\'${cell.python}\'" - script_vars = "${script_vars} m2_dir=\'${WORKSPACE}/build/m2\'" - if ("cqlsh-test" == cell.step) { - script_vars = "${script_vars} cython=\'${cell.cython}\'" - } - script_vars = fetchDTestsSource(command, script_vars) - timeout(time: command.timeout_hours, unit: 'HOURS') { // best throughput with each cell at ~10 minutes - def timer = System.currentTimeMillis() - try { - buildJVMDTestJars(cell, script_vars, logfile) - script_vars = "${script_vars} docker_timeout_hours=\"${command.timeout_hours}\"" - def status = sh label: "RUNNING TESTS ${cell.step}...", script: "${script_vars} .build/docker/run-tests.sh -a ${cell.step} -c '${cell.split}/${splits}' -j ${cell.jdk} 2>&1 | tee >( xz -c > build/${logfile} )", returnStatus: true - dir("build") { - archiveArtifacts artifacts: "${logfile}", fingerprint: true - } - if (0 != status) { error("Stage ${cell.step}${cell_suffix} failed with exit status ${status}") } - } catch (exc) { - if (exc.getClass().getName() == "org.jenkinsci.plugins.workflow.steps.FlowInterruptedException") { - for (def causeOfInterruption in exc.getCauses()) { - echo "CauseOfInterruption: ${causeOfInterruption.getClass().getName()} - ${causeOfInterruption.getShortDescription()}" + try { + fetchSource(cell.step, cell.arch, cell.jdk) + fetchDockerImages(['ubuntu-test']) + def cell_suffix = "_jdk${cell.jdk}_python_${cell.python}_${cell.cython}_${cell.arch}_${cell.split}_${splits}" + def logfile = "stage-logs/${JOB_NAME}_${BUILD_NUMBER}_${cell.step}${cell_suffix}_attempt${attempt}.log.xz" + def script_vars = "#!/bin/bash \n set -o pipefail ; " // pipe to tee needs pipefail + script_vars = "${script_vars} python_version=\'${cell.python}\'" + script_vars = "${script_vars} m2_dir=\'${WORKSPACE}/build/m2\'" + if ("cqlsh-test" == cell.step) { + script_vars = "${script_vars} cython=\'${cell.cython}\'" + } + script_vars = fetchDTestsSource(command, script_vars) + timeout(time: command.timeout_hours, unit: 'HOURS') { // best throughput with each cell at ~10 minutes + def timer = System.currentTimeMillis() + try { + buildJVMDTestJars(cell, script_vars, logfile) + script_vars = "${script_vars} docker_timeout_hours=\"${command.timeout_hours}\"" + def status = sh label: "RUNNING TESTS ${cell.step}...", script: "${script_vars} .build/docker/run-tests.sh -a ${cell.step} -c '${cell.split}/${splits}' -j ${cell.jdk} 2>&1 | tee >( xz -c > build/${logfile} )", returnStatus: true + dir("build") { + archiveArtifacts artifacts: "${logfile}", fingerprint: true } + if (0 != status) { error("Stage ${cell.step}${cell_suffix} failed with exit status ${status}") } + } catch (exc) { + if ("org.jenkinsci.plugins.workflow.steps.FlowInterruptedException" == exc.getClass().getName()) { + 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 + } + descriptions.add(cause.getShortDescription()) + } + error("Retryable interruption: ${descriptions.join(', ')}") + } + throw exc + } finally { + def duration = System.currentTimeMillis() - timer + def formattedTime = String.format("%tT.%tL", duration, duration) + echo "Time ${cell.step}${cell_suffix}: ${formattedTime}" } - throw exc - } finally { - def duration = System.currentTimeMillis() - timer - def formattedTime = String.format("%tT.%tL", duration, duration) - echo "Time ${cell.step}${cell_suffix}: ${formattedTime}" } - } - 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 ';' - """ - if (!cell.step.startsWith("microbench")) { - junit testResults: "test/**/TEST-*.xml,test/**/cqlshlib*.xml,test/**/nosetests*.xml", testDataPublishers: [[$class: 'StabilityTestDataPublisher']] + 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 ';' + """ + 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 + """ + 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("/", "_")) } - // 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 - """ - 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/**", "${cell.step}/${cell.arch}/jdk${cell.jdk}/python${cell.python}/cython_${cell.cython}/" + "split_${cell.split}_${splits}".replace("/", "_")) + } finally { + cleanAgent(cell.step) } - cleanAgent(cell.step) } } } @@ -486,180 +205,110 @@ 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 - for dockerfile in ${dockerfilesVar} ; do - image_tag="\$(md5sum .build/docker/\${dockerfile}.docker | cut -d' ' -f1)" - image_name="apache/cassandra-\${dockerfile}:\${image_tag}" - if ! ( [[ "" != "\$(docker images -q \${image_name} 2>/dev/null)" ]] ) ; then - docker pull -q apache.jfrog.io/cassan-docker/\${image_name} & - fi - done - docker pull -q apache.jfrog.io/cassan-docker/alpine:3.19.1 & - wait - """ + sh ".build/docker/_docker_pull_base.sh ${dockerfilesVar}" } -def getNodeLabel(command, cell) { - def label = "cassandra-${cell.arch}-${command.size}" - if (command.containsKey('benchmark') && command.benchmark) { - // to provide reliable results the "microbench" target - // expects to be running on baremetal jenkins agents configured with only one executor - // those jenkins agents need to be manually configured to have the "cassandra-amd64-large-dedicated" label - label = "${label}-dedicated" +def cleanAgent(stage) { + if ("jar" == stage) { + deleteDir() } - echo "using node label: ${label}" - return label + sh ''' + docker ps -aq --no-trunc \ + | xargs -r docker rm -f -v + docker volume ls -q \ + | xargs -r docker volume rm -f + docker system prune --volumes -af || true + ''' } -def copyToNightlies(sourceFiles, remoteDirectory='') { - if (isCanonical() && sourceFiles?.trim()) { - def remotePath = remoteDirectory.startsWith("cassandra/") ? "${remoteDirectory}" : "cassandra/${JOB_NAME}/${BUILD_NUMBER}/${remoteDirectory}" - def attempt = 1 - retry(9) { - if (attempt > 1) { sleep(60 * attempt) } - sshPublisher( - continueOnError: true, failOnError: false, - publishers: [ - sshPublisherDesc( - configName: "Nightlies", - transfers: [ sshTransfer( sourceFiles: sourceFiles, remoteDirectory: remotePath) ] - ) - ]) +def runCommands(commandType) { + def config = getConfig() + def commands = config.steps.collectEntries { [(it): [:]] } + commands.putAll([ + 'jar': [target: this.&build, timeout_hours: 1, toCopy: 'apache-cassandra-*.jar,apache-cassandra-*.tar.gz,apache-cassandra-*.deb,apache-cassandra-*.changes'], + 'microbench': [target: this.&test, timeout_hours: 2], + 'cqlsh-test': [target: this.&test, timeout_hours: 1], + 'jvm-dtest': [target: this.&test, timeout_hours: 1, 'python-dtest': true], + 'jvm-dtest-upgrade': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-upgrade-40': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-upgrade-41': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-vnode': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-no-vnode': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-large': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-40': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-41': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-ssl': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-no-preview': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-storage': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-latest-killer': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-tls': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-upgrade-dtest-auth': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-vnode': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-cdc': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-auth': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-compression': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-legacy-sstable': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-oa': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-large': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-no-vnode': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-sai': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-ssl': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-tls': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-upgrade': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-vnode-upgrade': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-trigger': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-materialized-view': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-transient-replication': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-counters': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-repair': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-secondary-index': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-paging': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-topology': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-system-keyspace-directory': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-stress': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-fqltool': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-reverse-query': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-cql': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-jmx': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-sql': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-bti': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + 'jvm-dtest-latest-accord': [target: this.&test, timeout_hours: 2, 'python-dtest': true], + ]) + + def parallelMap = [:] + config.cells.findAll { it.step in commands.keySet() }.each { cell -> + def command = commands[cell.step] + if (commandType == 'build' && command.target != this.&build) { + return } - echo "archived to https://nightlies.apache.org/${remotePath}" - } -} - -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/" - cleanAgentDocker(job_name, agentScriptsUrl) - logAgentInfo(job_name, agentScriptsUrl) - } - cleanWs() -} - -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 - set +e - wget -q ${agentScriptsUrl}/docker_image_pruner.py - wget -q ${agentScriptsUrl}/docker_agent_cleaner.sh - bash docker_agent_cleaner.sh ${maxBuildHours} - """ -} - -def logAgentInfo(job_name, agentScriptsUrl) { - sh """#!/bin/bash - set +e -o pipefail - wget -q ${agentScriptsUrl}/agent_report.sh - bash -x agent_report.sh | tee -a \$(date +"%Y%m%d%H%M")-disk-usage-stats.txt - """ - copyToNightlies("*-disk-usage-stats.txt", "cassandra/ci-cassandra.apache.org/agents/${NODE_NAME}/disk-usage/") -} - -///////////////////////////////////////// -////// scripting support for summary //// -///////////////////////////////////////// - -def generateTestReports() { - node("cassandra-medium") { - cleanAgent("generateTestReports") - checkout changelog: false, scm: scmGit(branches: [[name: params.branch]], extensions: [cloneOption(depth: 1, noTags: true, reference: '', shallow: true)], userRemoteConfigs: [[url: params.repository]]) - def logfile = "stage-logs/${JOB_NAME}_${BUILD_NUMBER}_generateTestReports.log.xz" - sh "mkdir -p build/stage-logs" - def teeSuffix = "2>&1 | tee >( xz -c > build/${logfile} )" - def script_vars = "#!/bin/bash -x \n " - if (isCanonical()) { - // copyArtifacts takes >4hrs, hack with manual download - sh """${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} - """ - } else { - copyArtifacts filter: 'test/**/TEST-*.xml.xz,test/**/cqlshlib*.xml.xz,test/**/nosetests*.xml.xz,test/jmh-result.json', fingerprintArtifacts: true, projectName: env.JOB_NAME, selector: specific(env.BUILD_NUMBER), target: "build/", optional: true + if (commandType == 'test' && command.target != this.&test) { + return } - if (fileExists('build/test/output')) { - // 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} ( - 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 - - for target in \$(ls build/test/output/) ; do - if test -d build/test/output/\${target} ; then - mkdir -p build/test/reports/\${target} - echo "Report for \${target} (\$(find build/test/output/\${target} -name '*.xml' | wc -l) test files)" - CASSANDRA_DOCKER_ANT_OPTS="-Dbuild.test.output.dir=build/test/output/\${target} -Dbuild.test.report.dir=build/test/reports/\${target}" - export CASSANDRA_DOCKER_ANT_OPTS - .build/docker/_docker_run.sh bullseye-build.docker ci/generate-test-report.sh - fi - done - - .build/docker/_docker_run.sh bullseye-build.docker ci/generate-ci-summary.sh || echo "failed generate-ci-summary.sh" - - tar -cf build/results_details.tar -C build/test/ reports - xz -8f build/results_details.tar ) ${teeSuffix} - """ - - dir('build/') { - archiveArtifacts artifacts: "ci_summary.html,results_details.tar.xz,${logfile}", fingerprint: true - copyToNightlies('ci_summary.html,results_details.tar.xz,${logfile},test/jmh-result.json') - } - } - if (fileExists('build/test/jmh-result.json')) { - jmhReport('build/test/jmh-result.json') + parallelMap["${cell.step}_${cell.arch}_jdk${cell.jdk}_py${cell.python}_${cell.cython}${cell.split ? "_split${cell.split}" : ''}"] = { + command.target(command, cell) } } + parallel parallelMap } -def sendNotifications() { - if (isPostCommit() && isCanonical()) { - // the following is expected only to work on ci-cassandra.apache.org - def changes = '?' - try { - script { - changes = formatChangeLogChanges(currentBuild.changeSets) - echo "changes: ${changes}" +pipeline { + agent none + stages { + stage('Build') { + steps { + script { + runCommands('build') + } } - slackSend channel: '#cassandra-builds', message: ":apache: <${BUILD_URL}|${currentBuild.fullDisplayName}> completed: ${currentBuild.result}. \n${changes}" - emailext to: 'builds@cassandra.apache.org', subject: "Build complete: ${currentBuild.fullDisplayName} [${currentBuild.result}] ${GIT_COMMIT}", presendScript: 'msg.removeHeader("In-Reply-To"); msg.removeHeader("References")', body: emailContent() - } catch (Exception ex) { - echo 'failed to send notifications ' + ex.toString() } - } -} - -def formatChangeLogChanges(changeLogSets) { - def result = '' - for (int i = 0; i < changeLogSets.size(); i++) { - def entries = changeLogSets[i].items - for (int j = 0; j < entries.length; j++) { - def entry = entries[j] - result = result + "${entry.commitId} by ${entry.author} on ${new Date(entry.timestamp)}: ${entry.msg}\n" + stage('Test') { + steps { + script { + runCommands('test') + } + } } } - return result } - -def emailContent() { - return ''' - ------------------------------------------------------------------------------- - Build ${ENV,var="JOB_NAME"} #${BUILD_NUMBER} ${BUILD_STATUS} - URL: ${BUILD_URL} - ------------------------------------------------------------------------------- - Changes: - ${CHANGES} - ------------------------------------------------------------------------------- - Failed Tests: - ${FAILED_TESTS,maxTests=500,showMessage=false,showStack=false} - ------------------------------------------------------------------------------- - For complete test report and logs see https://nightlies.apache.org/cassandra/${JOB_NAME}/${BUILD_NUMBER}/ - ''' -} \ No newline at end of file diff --git a/.jenkins/k8s/README.md b/.jenkins/k8s/README.md index 671b61961a08..844130651331 100644 --- a/.jenkins/k8s/README.md +++ b/.jenkins/k8s/README.md @@ -24,7 +24,7 @@ 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 n2-highcpu-4 --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 --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 @@ -32,6 +32,12 @@ gcloud container node-pools create agents-medium --cluster ${CLUSTER_NAME} --mac # 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} + +# 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 +# and agent.podTemplates.*.resourceLimitCpu and agent.podTemplates.*.resourceLimitMemory (adding gke/eks requirements) in https://github.com/apache/cassandra/blob/cassandra-6.0/.jenkins/k8s/jenkins-deployment.yaml +# The jenkins resource requirements should fit into the corresponding dind podTemplate limits. +# Remember to allow a buffer for gke/eks pods deployed on each node. ``` diff --git a/.jenkins/k8s/jenkins-deployment.yaml b/.jenkins/k8s/jenkins-deployment.yaml index 46cc77fc3c17..98290e601158 100644 --- a/.jenkins/k8s/jenkins-deployment.yaml +++ b/.jenkins/k8s/jenkins-deployment.yaml @@ -36,6 +36,7 @@ controller: customJenkinsLabels: - controller resources: + # increase cpu/memory as agent pool sizes get bigger (pre-ci.c.a.o uses 8 and 20g) requests: cpu: 4 memory: 16G @@ -97,6 +98,23 @@ controller: } } } + - script: > + pipelineJob('cassandra-6.0') { + definition { + cpsScm { + scm { + git { + remote { + url('https://github.com/apache/cassandra') + } + branch('cassandra-6.0') + scriptPath('.jenkins/Jenkinsfile') + } + } + lightweight() + } + } + } - script: > pipelineJob('cassandra-5.0') { definition { @@ -216,6 +234,19 @@ agent: - emptyDirVolume: memory: 'false' mountPath: /certs + # limit one agent pod per node for simpler operations (like orphan cleanup) + yaml: | + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: jenkins/cassius-jenkins-agent + operator: In + values: + - "true" + topologyKey: kubernetes.io/hostname agent-dind-medium: | - name: agent-dind-medium label: agent-dind cassandra-medium cassandra-amd64-medium @@ -293,6 +324,19 @@ agent: - emptyDirVolume: memory: 'false' mountPath: /certs + # limit one agent pod per node for simpler operations (like orphan cleanup) + yaml: | + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: jenkins/cassius-jenkins-agent + operator: In + values: + - "true" + topologyKey: kubernetes.io/hostname agent-dind-large: | - name: agent-dind-large label: agent-dind cassandra-large cassandra-amd64-large cassandra-amd64-large-dedicated @@ -370,5 +414,18 @@ agent: - emptyDirVolume: memory: 'false' mountPath: /certs + # limit one agent pod per node for simpler operations (like orphan cleanup) + yaml: | + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: jenkins/cassius-jenkins-agent + operator: In + values: + - "true" + topologyKey: kubernetes.io/hostname diff --git a/CHANGES.txt b/CHANGES.txt index ab0e548b0d7e..be2d7094a430 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,49 @@ +5.0.9 + * Coordinator load-shedding returns OverloadedException without setting streamId, misrouting query responses (CASSANDRA-21508) + * SAI Component Checksum Validation Should be Segment-Aware (CASSANDRA-21516) + * Support Python 3.12 and 3.13 in cqlsh (CASSANDRA-20997) + * Fix AssertionError in hasReplicaWithOngoingRepair when parallel_repair_count > 1 (CASSANDRA-21426) + * putShortVolatile is not volatile in InMemoryTrie (CASSANDRA-21353) + * Fix RequestFailureReason serializer and nits in a few others (CASSANDRA-21437) + * Remove golang dependency in gen-doc and replace with python implementation (CASSANDRA-21432) + * 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) + * 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) +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) + * Consider first token of SSTable when calculating SSTable intersection in LeveledScanner (CASSANDRA-21369) + * Remove inFlightEcho entry on ECHO_REQ failure (CASSANDRA-21428) + * Validate snapshot names (CASSANDRA-21389) + * BTree.FastBuilder.reset() fails to clear savedBuffer and savedNextKey, causing ClassCastException and SSTable header corruption during schema disagreement (CASSANDRA-21216, CASSANDRA-21260) + * Backport CASSANDRA-17810 fix and improve RTBoundValidator error messages (CASSANDRA-18282) + + +5.0.8 + * Backport Automated Repair Inside Cassandra for CEP-37 (CASSANDRA-21138) + * Update cassandra-stress to support TLS 1.3 by default by auto-negotiation (CASSANDRA-21007) + * Ensure schema created before 2.1 without tableId in folder name can be loaded in SnapshotLoader (CASSANDRA-21173) +Merged from 4.1: + * Harden data resurrection startup check with atomic heartbeat file write with fallback (CASSANDRA-21290) +Merged from 4.0: +Backported from 6.0: + * Improved observability in AutoRepair to report both expected vs. actual repair bytes and expected vs. actual keyspaces (CASSANDRA-20581) + * Stop repair scheduler if two major versions are detected (CASSANDRA-20048) + * AutoRepair: Safeguard Full repair against disk protection (CASSANDRA-20045) + * Stop AutoRepair monitoring thread upon Cassandra shutdown (CASSANDRA-20623) + * Fix race condition in auto-repair scheduler (CASSANDRA-20265) + * Implement minimum repair task duration setting for auto-repair scheduler (CASSANDRA-20160) + * Implement preview_repaired auto-repair type (CASSANDRA-20046) + * Automated Repair Inside Cassandra for CEP-37 (CASSANDRA-19918) + + 5.0.7 + * Clear BTree.FastBuilder saved overflow state on reset to prevent cross-table column contamination after schema disagreement (CASSANDRA-21216, CASSANDRA-21260) * Refactor SAI ANN query execution to use score ordered iterators for correctness and speed (CASSANDRA-20086) * Disallow binding an identity to a superuser when the user is a regular user (CASSANDRA-21219) * Fix ConcurrentModificationException in compaction garbagecollect (CASSANDRA-21065) @@ -95,6 +140,10 @@ Merged from 4.0: * Fix Dropwizard Meter causes timeouts when infrequently used (CASSANDRA-19332) +Merged from 5.1: + * Expose current compaction throughput in nodetool (CASSANDRA-13890) + + 5.0.4 * Update netty to 4.1.119.Final and netty-tcnative to 2.0.70.Final (CASSANDRA-20314) * Serialization can lose complex deletions in a mutation with multiple collections in a row (CASSANDRA-20449) @@ -349,13 +398,11 @@ Merged from 3.0: 5.0-alpha2 - * Add support for vector search in SAI (CASSANDRA-18715) * Remove crc_check_chance from CompressionParams (CASSANDRA-18872) * Fix schema loading of UDTs inside vectors inside UDTs (CASSANDRA-18964) * Add cqlsh autocompletion for the vector data type (CASSANDRA-18946) * Fix nodetool tablehistograms output to avoid printing repeated information and ensure at most two arguments (CASSANDRA-18955) * Change the checksum algorithm SAI-related files use from CRC32 to CRC32C (CASSANDRA-18836) - * Correctly remove Index.Group from IndexRegistry (CASSANDRA-18905) * Fix vector type to support DDM's mask_default function (CASSANDRA-18889) * Remove unnecessary reporter-config3 dependency (CASSANDRA-18907) * Remove support for empty values on the vector data type (CASSANDRA-18876) @@ -668,6 +715,8 @@ Merged from 3.0: * Do not remove SSTables when cause of FSReadError is OutOfMemoryError while using best_effort disk failure policy (CASSANDRA-18336) * Do not remove truncated_at entry in system.local while dropping an index (CASSANDRA-18105) +4.0.14 + * Fix memory leak in BTree.FastBuilder (CASSANDRA-19785) 4.0.9 * Update zstd-jni library to version 1.5.5 (CASSANDRA-18429) diff --git a/CONTRIBUTING_CC.md b/CONTRIBUTING_CC.md new file mode 100644 index 000000000000..482d97360c32 --- /dev/null +++ b/CONTRIBUTING_CC.md @@ -0,0 +1,26 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# Merging + +Feel free to make your contributions: Patches, PRs, diffs,... all welcomed. +- Create a PR against main. +- Ensure all your commits are squashed into 1 or under a sensible grouping. Rebase to account for any changes. +- Follow the checklist in the PR and DoD (Definition of Done). This needs to be reviewed and approved before merging. +- Next you have 2 options: you merge to main and it will eventually be cherrypicked into main-5.0 when it catches up. +- Or you create a PR for main-5.0 and get approval likewise. +- When merging your PRs: Squash and merge or Rebase and merge can be used. +- IMPORTANT: Do not forward merge main into main-5.0. diff --git a/NEWS.txt b/NEWS.txt index 1192f4178a5d..a63e0e36d102 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -65,6 +65,45 @@ restore snapshots created with the previous major version using the 'sstableloader' tool. You can upgrade the file format of your snapshots using the provided 'sstableupgrade' tool. +5.0.8 +====== + +New features +------------ + - CEP-37 Auto Repair is a fully automated scheduler that provides repair orchestration within Apache Cassandra. This + significantly reduces operational overhead by eliminating the need for operators to deploy external tools to submit + and manage repairs. See + https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-37+Apache+Cassandra+Unified+Repair+Solution for more + details on the motivation and design. + +Upgrading +--------- + - The auto-repair feature requires enabling the JVM property `cassandra.autorepair.enable=true` (add + `-Dcassandra.autorepair.enable=true` to JVM options) before starting the node. This property creates the required + schema elements for auto-repair, including the auto_repair column in system_schema.tables and system_schema.views, + as well as the auto_repair_history and auto_repair_priority tables in system_distributed. After enabling this + property, you still need to enable auto-repair scheduling either in cassandra.yaml under the `auto_repair` section + or at runtime via JMX. + + Users who do not intend to use auto-repair can leave this property disabled (the default) to maintain schema + compatibility with pre-5.0.8 nodes during rolling upgrades. This property must be set consistently across all + nodes before startup and cannot be changed at runtime. + + WARNING: This property is non-reversible. Once enabled, it cannot be disabled. Attempting to start a node + with `cassandra.autorepair.enable=false` after it was previously enabled will cause the node to fail during + initialization due to schema incompatibility (the persisted schema contains auto-repair columns that are not + recognized when the property is disabled). To disable auto-repair scheduling after the property has been + enabled, use cassandra.yaml or JMX instead of changing the JVM property. + + IMPORTANT: The `cassandra.autorepair.enable` property must be enabled consistently across all nodes in the + cluster before any schema changes are made. When some nodes have the property enabled and others do not, the + system_distributed keyspace schema generation will differ between nodes (generation 7 with auto-repair vs + generation 6 without), causing schema disagreement. This is similar to what happens during a major version + upgrade when new system tables are added. Any schema change (e.g. CREATE KEYSPACE) attempted while nodes + are in this inconsistent state will time out and schema versions will not converge until all nodes are + brought up with the same setting. Once all nodes have the property set consistently, schema will converge + automatically. + 5.0.7 ====== @@ -136,7 +175,6 @@ New features src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.md - New `VectorType` (cql `vector`) which adds new fixed-length element arrays. See CASSANDRA-18504 - Added new vector similarity functions `similarity_cosine`, `similarity_euclidean` and `similarity_dot_product`. - - Added ANN vector similarity search via ORDER BY ANN OF syntax on SAI indexes (using jvector library). - Removed UDT type migration logic for 3.6+ clusters upgrading to 4.0. If migration has been disabled, it must be enabled before upgrading to 5.0 if the cluster used UDTs. See CASSANDRA-18504 - Entended max expiration time from 2038-01-19T03:14:06+00:00 to 2106-02-07T06:28:13+00:00 @@ -321,6 +359,11 @@ Deprecation Cluster hosts running with dual native ports were not correctly identified in the system.peers tables and server-sent EVENTs, causing clients that encrypt traffic to fail to maintain correct connection pools. For more information, see CASSANDRA-19392. - Deprecated `use_deterministic_table_id` in cassandra.yaml. Table IDs may still be supplied explicitly on CREATE. + - Chronicle Queue has changed the enums used for log rolling (cassandra.yaml -> full_query_logging_options:roll_cycle). + Older legacy options will still work for the foreseeable future but you will see warnings in logs and future dependency + upgrades may break your log rolling param. The default log rolling param will be changed with the next major release + from HOURLY to FAST_HOURLY, primarily different on how frequently indexes are built (256 in FAST_HOURLY vs. 16 in HOURLY). + For more info refer to: net.openhft.chronicle.queue.RollCycles 4.1 === diff --git a/NOTICE.txt b/NOTICE.txt index fd185210450f..5a2c26ae740d 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -8,3 +8,9 @@ Android Code Copyright 2005-2008 The Android Open Source Project This product includes software developed as part of The Android Open Source Project (http://source.android.com). + +This project includes software from the Apache Lucene project. Relevant +portions of its NOTICE are excerpted below: +======================================================================= +Apache Lucene +Copyright 2001-2018 The Apache Software Foundation diff --git a/README.asc b/README.asc index 5c6713580d30..684c8f6b4cdc 100644 --- a/README.asc +++ b/README.asc @@ -25,8 +25,8 @@ and running, and demonstrate some simple reads and writes. For a more-complete g First, we'll unpack our archive: - $ tar -zxvf apache-cassandra-$VERSION.tar.gz - $ cd apache-cassandra-$VERSION + $ tar -zxvf dse-db-$VERSION.tar.gz + $ cd dse-db-$VERSION After that we start the server. Running the startup script with the -f argument will cause Cassandra to remain in the foreground and log to standard out; it can be stopped with ctrl-C. diff --git a/bin/cassandra.in.sh b/bin/cassandra.in.sh index b838c2d4cf9c..5d83b4ed673e 100644 --- a/bin/cassandra.in.sh +++ b/bin/cassandra.in.sh @@ -30,7 +30,7 @@ CLASSPATH="$CASSANDRA_CONF" # compiled classes. NOTE: This isn't needed by the startup script, # it's just used here in constructing the classpath. if [ -d $CASSANDRA_HOME/build ] ; then - jars_cnt="`ls -1 $CASSANDRA_HOME/build/apache-cassandra*.jar | grep -v 'javadoc.jar' | grep -v 'sources.jar' | wc -l | xargs echo`" + jars_cnt="`ls -1 $CASSANDRA_HOME/build/dse-db*.jar | grep -v 'javadoc.jar' | grep -v 'sources.jar' | wc -l | xargs echo`" if [ "$jars_cnt" -gt 1 ]; then dir="`cd $CASSANDRA_HOME/build; pwd`" echo "There are JAR artifacts for multiple versions in the $dir directory. Please clean the project with 'ant realclean' and build it again." 1>&2 @@ -38,8 +38,8 @@ if [ -d $CASSANDRA_HOME/build ] ; then fi if [ "$jars_cnt" = "1" ]; then - cassandra_bin="`ls -1 $CASSANDRA_HOME/build/apache-cassandra*.jar | grep -v javadoc | grep -v sources`" - CLASSPATH="$CLASSPATH:$cassandra_bin" + dse_db_bin="`ls -1 $CASSANDRA_HOME/build/dse-db*.jar | grep -v javadoc | grep -v sources`" + CLASSPATH="$CLASSPATH:$dse_db_bin" fi fi @@ -122,11 +122,16 @@ jvmver=`echo "$java_ver_output" | grep '[openjdk|java] version' | awk -F'"' 'NR= JVM_VERSION=${jvmver%_*} short=$(echo "${jvmver}" | cut -c1-2) -JAVA_VERSION=17 +JAVA_VERSION=22 if [ "$short" = "11" ] ; then JAVA_VERSION=11 elif [ "$JVM_VERSION" \< "17" ] ; then - echo "Cassandra 5.0 requires Java 11 or Java 17." + echo "DSE DB 5.0 requires Java 11 or higher." + exit 1; +elif [ "$short" = "17" ] ; then + JAVA_VERSION=17 +elif [ "$JVM_VERSION" \< "22" ] ; then + echo "DSE DB 5.0 requires Java 11 or higher." exit 1; fi @@ -151,7 +156,9 @@ esac # Read user-defined JVM options from jvm-server.options file JVM_OPTS_FILE=$CASSANDRA_CONF/jvm${jvmoptions_variant:--clients}.options -if [ $JAVA_VERSION -ge 17 ] ; then +if [ $JAVA_VERSION -ge 22 ] ; then + JVM_DEP_OPTS_FILE=$CASSANDRA_CONF/jvm22${jvmoptions_variant:--clients}.options +elif [ $JAVA_VERSION -ge 17 ] ; then JVM_DEP_OPTS_FILE=$CASSANDRA_CONF/jvm17${jvmoptions_variant:--clients}.options elif [ $JAVA_VERSION -ge 11 ] ; then JVM_DEP_OPTS_FILE=$CASSANDRA_CONF/jvm11${jvmoptions_variant:--clients}.options diff --git a/bin/cqlsh b/bin/cqlsh index 2a9651968b4b..25fa029b4f57 100755 --- a/bin/cqlsh +++ b/bin/cqlsh @@ -63,7 +63,7 @@ is_supported_version() { major_version="${version%.*}" minor_version="${version#*.}" # python 3.8-3.11 are supported - if [ "$major_version" = 3 ] && [ "$minor_version" -ge 8 ] && [ "$minor_version" -le 11 ]; then + if [ "$major_version" = 3 ] && [ "$minor_version" -ge 8 ] && [ "$minor_version" -le 13 ]; then echo "supported" # python 3.6-3.7 are deprecated elif [ "$major_version" = 3 ] && [ "$minor_version" -ge 6 ] && [ "$minor_version" -le 7 ]; then @@ -88,7 +88,7 @@ run_if_supported_version() { exec "$interpreter" "$($interpreter -c "import os; print(os.path.dirname(os.path.realpath('$0')))")/cqlsh.py" "$@" exit else - echo "Warning: unsupported version of Python, required 3.6-3.11 but found" "$version" >&2 + echo "Warning: unsupported version of Python, required 3.6-3.13 but found" "$version" >&2 fi fi } diff --git a/bin/cqlsh.py b/bin/cqlsh.py index 738f0aeeb716..d7aa56e41c9a 100755 --- a/bin/cqlsh.py +++ b/bin/cqlsh.py @@ -21,8 +21,8 @@ import sys from glob import glob -if sys.version_info < (3, 6) or sys.version_info >= (3, 12): - sys.exit("\ncqlsh requires Python 3.6-3.11\n") +if sys.version_info < (3, 6) or sys.version_info >= (3, 14): + sys.exit("\ncqlsh requires Python 3.6-3.13\n") # see CASSANDRA-10428 if platform.python_implementation().startswith('Jython'): @@ -56,7 +56,7 @@ def find_zip(libprefix): sys.path.insert(0, os.path.join(cql_zip, 'cassandra-driver-' + ver)) # the driver needs dependencies -third_parties = ('pure_sasl-', 'wcwidth-') +third_parties = ('pure_sasl-', 'wcwidth-', 'pyasyncore-', 'geomet-', 'datastax_db_*-') for lib in third_parties: lib_zip = find_zip(lib) diff --git a/build.properties.default b/build.properties.default index 36676f5712d8..380270479620 100644 --- a/build.properties.default +++ b/build.properties.default @@ -21,3 +21,4 @@ artifact.remoteRepository.central: https://repo1.maven.org/maven2 artifact.remoteRepository.apache: https://repo.maven.apache.org/maven2 artifact.remoteRepository.apacheSnapshot: https://repository.apache.org/content/repositories/snapshots +artifact.remoteRepository.datastax: https://repo.datastax.com/dse diff --git a/build.xml b/build.xml index 15298ede273b..ba2944f41c1e 100644 --- a/build.xml +++ b/build.xml @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - @@ -33,19 +33,18 @@ - - - - + + + + - + @@ -95,11 +94,12 @@ - - - + + + @@ -108,19 +108,20 @@ + - + - + - - + + @@ -133,16 +134,24 @@ - + - + + + + + + + + + - + @@ -160,7 +169,6 @@ - + + -XX:G1RSetUpdatingPauseTimePercent=5 + -XX:MaxGCPauseMillis=100 + + + -XX:-RestrictContended + -XX:+UseThreadPriorities + -XX:+DebugNonSafepoints + -XX:+UseStringDeduplication + -XX:StringTableSize=1000003 + -XX:+PerfDisableSharedMem + -XX:+AlwaysPreTouch + -XX:+UseTLAB + -XX:+ResizeTLAB + -XX:+UseNUMA + + + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED + --add-exports java.base/jdk.internal.ref=ALL-UNNAMED + --add-exports java.base/jdk.internal.perf=ALL-UNNAMED + --add-exports java.base/sun.nio.ch=ALL-UNNAMED + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED + --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + --add-exports jdk.naming.dns/com.sun.jndi.dns=java.naming + --add-exports jdk.unsupported/sun.misc=ALL-UNNAMED + + --add-opens java.base/java.io=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.lang.module=ALL-UNNAMED + --add-opens java.base/java.lang.ref=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.base/java.math=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/java.nio=ALL-UNNAMED + --add-opens java.base/java.nio.charset=ALL-UNNAMED + --add-opens java.base/java.nio.file.spi=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/jdk.internal.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens java.base/jdk.internal.vm=ALL-UNNAMED + --add-opens java.base/sun.nio.ch=ALL-UNNAMED + --add-opens jdk.compiler/com.sun.tools.javac=ALL-UNNAMED + --add-opens jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + --add-opens jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED + + --add-opens java.base/java.nio.file.attribute=ALL-UNNAMED + + + --add-opens java.base/java.util.concurrent=ALL-UNNAMED + --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + + + + + + + --add-modules jdk.incubator.vector + + + + + + + + - + + failonerror="true" + fork="true" + outputproperty="antlr.output" + errorproperty="antlr.error"> @@ -479,15 +592,6 @@ - - - - - - @@ -508,6 +612,7 @@ + @@ -536,7 +641,7 @@ - @@ -716,7 +821,6 @@ - @@ -728,6 +832,15 @@ + + + + + dse-db-all]]> + com.datastax.db + db-all]]> + + description="Assemble DSE DB JAR files"> @@ -749,9 +862,9 @@ - + - + @@ -760,7 +873,7 @@ + description="Assemble DSE DB JAR files"> @@ -775,7 +888,7 @@ + description="Assemble DSE DB JAR files"> @@ -791,7 +904,7 @@ + description="Assemble DSE DB JAR files"> @@ -826,13 +939,13 @@ + description="Assemble DSE DB JAR files"> - + @@ -842,7 +955,7 @@ - + @@ -869,7 +982,6 @@ - @@ -910,8 +1022,8 @@ - + @@ -980,7 +1092,7 @@ - + @@ -996,6 +1108,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + @@ -1276,6 +1515,8 @@ + + @@ -1296,6 +1537,8 @@ + + @@ -1318,6 +1561,7 @@ + @@ -1330,7 +1574,7 @@ - @@ -1339,10 +1583,32 @@ + + + + + + + + + + + + + + + + + + + + + @@ -1359,10 +1625,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1388,6 +1690,7 @@ + @@ -1401,6 +1704,7 @@ + @@ -1413,6 +1717,7 @@ + @@ -1449,6 +1754,22 @@ + + + + + + + + + + + + + + + + @@ -1457,22 +1778,41 @@ + + + + + + + + + + + + + + + + + + + @@ -1480,6 +1820,7 @@ + @@ -1494,6 +1835,7 @@ timeout="${test.long.timeout}"> + @@ -1501,6 +1843,7 @@ + @@ -1519,6 +1862,7 @@ + @@ -1565,6 +1909,7 @@ + @@ -1708,6 +2053,13 @@ + + + + + + + @@ -1715,6 +2067,13 @@ + + + + + + + @@ -1777,6 +2136,7 @@ + @@ -1816,6 +2176,7 @@ + @@ -1833,6 +2194,7 @@ + @@ -1921,7 +2283,7 @@ - + @@ -1943,10 +2305,10 @@ ]]> - IDE configuration in .idea/ updated for use with JDK${ant.java.version}. + IDE configuration in .idea/ updated for use with JDK${ant.java.version}. - In IntelliJ verify that the SDK is ${ant.java.version}, and its path is valid. - This can be verified in 'Project Structure/Project Setting/Project' and 'Project Structure/Platform Setting/SDKs'. + In IntelliJ verify that the SDK is ${ant.java.version}, and its path is valid. + This can be verified in 'Project Structure/Project Setting/Project' and 'Project Structure/Platform Setting/SDKs'. @@ -1996,6 +2358,7 @@ + @@ -2003,8 +2366,8 @@ + - ${eclipse-libs-list} ]]> @@ -2046,7 +2409,7 @@ file="${build.dir}/${final.name}-parent.pom" packaging="pom"/> - + + + + + + @@ -2067,7 +2440,7 @@ file="${build.dir}/${final.name}-parent.pom" packaging="pom"/> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/cassandra-env.sh b/conf/cassandra-env.sh index 2d83763b8994..14caee2366d8 100644 --- a/conf/cassandra-env.sh +++ b/conf/cassandra-env.sh @@ -80,7 +80,7 @@ echo "$JVM_OPTS" | grep -qe "-[X]log:gc" if [ "$?" = "1" ] ; then # [X] to prevent ccm from replacing this line # only add -Xlog:gc if it's not mentioned in jvm-server.options file mkdir -p ${CASSANDRA_LOG_DIR} - JVM_OPTS="$JVM_OPTS -Xlog:gc=info,heap*=trace,age*=debug,safepoint=info,promotion*=trace:file=${CASSANDRA_LOG_DIR}/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760" + JVM_OPTS="$JVM_OPTS -Xlog:gc=info,heap*=debug,age*=debug,safepoint=info,promotion*=debug:file=${CASSANDRA_LOG_DIR}/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760" fi # Check what parameters were defined on jvm-server.options file to avoid conflicts @@ -221,9 +221,9 @@ JVM_ON_OUT_OF_MEMORY_ERROR_OPT="-XX:OnOutOfMemoryError=kill -9 %p" # for more on configuring JMX through firewalls, etc. (Short version: # get it working with no firewall first.) # -# Cassandra ships with JMX accessible *only* from localhost. +# Cassandra ships with JMX accessible *only* from localhost. # To enable remote JMX connections, uncomment lines below -# with authentication and/or ssl enabled. See https://wiki.apache.org/cassandra/JmxSecurity +# with authentication and/or ssl enabled. See https://wiki.apache.org/cassandra/JmxSecurity # if [ "x$LOCAL_JMX" = "x" ]; then LOCAL_JMX=yes diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index e09518188a60..f620f68d3cc9 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -395,6 +395,11 @@ partitioner: org.apache.cassandra.dht.Murmur3Partitioner # data_file_directories: # - /var/lib/cassandra/data +# Metadata directory that holds information about the cluster, local node and its peers. +# Currently, only a single subdirectory called 'nodes' will be used. +# If not set, the default directory is $CASSANDRA_HOME/data/metadata. +# metadata_directory: /var/lib/cassandra/metadata + # Directory were Cassandra should store the data of the local system keyspaces. # By default Cassandra will store the data of the local system keyspaces in the first of the data directories specified # by data_file_directories. @@ -687,6 +692,8 @@ commitlog_disk_access_mode: legacy # none : Flush without compressing blocks but while still doing checksums. # fast : Flush with a fast compressor. If the table is already using a # fast compressor that compressor is used. +# adaptive : Flush with a fast adaptive compressor. If the table is already using a +# fast compressor that compressor is used. # table: Always flush with the same compressor that the table uses. This # was the pre 4.0 behavior. # @@ -822,7 +829,7 @@ memtable: # # offheap_objects # off heap objects -memtable_allocation_type: heap_buffers +memtable_allocation_type: offheap_objects # Limit memory usage for Merkle tree calculations during repairs of a certain # table and common token range. Repair commands targetting multiple tables or @@ -848,7 +855,7 @@ memtable_allocation_type: heap_buffers # There isn't a limit by default for backwards compatibility, but this can # produce OOM for commands repairing multiple tables or multiple virtual nodes. # A limit of just 1 simultaneous Merkle tree request is generally recommended -# with no virtual nodes so repair_session_space, and thereof the Merkle tree +# with no virtual nodes so repair_session_space, and therefore the Merkle tree # resolution, can be high. For virtual nodes a value of 1 with the default # repair_session_space value will produce higher resolution Merkle trees # at the expense of speed. Alternatively, when working with virtual nodes it @@ -951,7 +958,7 @@ index_summary_resize_interval: 60m # buffers. Enable this to avoid sudden dirty buffer flushing from # impacting read latencies. Almost always a good idea on SSDs; not # necessarily on platters. -trickle_fsync: false +trickle_fsync: true # Min unit: KiB trickle_fsync_interval: 10240KiB @@ -1264,7 +1271,8 @@ sstable_preemptive_open_interval: 50MiB # set to true, each newly created sstable will have a UUID based generation identifier and such files are # not readable by previous Cassandra versions. At some point, this option will become true by default # and eventually get removed from the configuration. -uuid_sstable_identifiers_enabled: false +# In Converged Cassandra, we enable this option by default +uuid_sstable_identifiers_enabled: true # When enabled, permits Cassandra to zero-copy stream entire eligible # SSTables between nodes, including every component. @@ -1316,14 +1324,21 @@ uuid_sstable_identifiers_enabled: false # low is equally ill-advised since clients could get timeouts even for successful # operations just because the timeout setting is too tight. -# How long the coordinator should wait for read operations to complete. +# How long the coordinator should wait for read operations to complete. This +# timeout does not apply to aggregated queries such as SELECT COUNT(*), MIN(x), etc. # Lowest acceptable value is 10 ms. # Min unit: ms read_request_timeout: 5000ms -# How long the coordinator should wait for seq or index scans to complete. +# How long the coordinator should wait for seq or index scans to complete. This +# timeout does not apply to aggregated queries such as SELECT COUNT(*), MIN(x), etc. # Lowest acceptable value is 10 ms. # Min unit: ms range_request_timeout: 10000ms +# How long the coordinator should wait for aggregation read operations to complete, +# such as SELECT COUNT(*), MIN(x), etc. +# Lowest acceptable value is 10 ms. +# Min unit: ms +aggregation_request_timeout: 120000ms # How long the coordinator should wait for writes to complete. # Lowest acceptable value is 10 ms. # Min unit: ms @@ -1347,6 +1362,16 @@ truncate_request_timeout: 60000ms # Lowest acceptable value is 10 ms. # Min unit: ms request_timeout: 10000ms +# Upper bound for how long any request received via native transport +# should be considered live and serviceable by the system. This is +# currently considered at two points: when the message is dequeued and +# executed by the NATIVE_TRANSPORT_REQUESTS stage, and when the message +# is dequeued and executed by an async stage if NATIVE_TRANSPORT_ASYNC_READ_WRITE_ENABLED +# is set to true. If the request is not completed within this time, an +# OverloadedException is thrown. +# Min unit: ms +native_transport_timeout: 12000ms + # Defensive settings for protecting Cassandra from true network partitions. # See (CASSANDRA-14358) for details. @@ -1861,6 +1886,11 @@ unlogged_batch_across_partitions_warn_threshold: 10 # Audit logging - Logs every incoming CQL command request, authentication to a node. See the docs # on audit_logging for full details about the various configuration options and production tips. +# NOTE: Chronicle Queue has changed the enums used for log rolling roll_cycle). +# Older legacy options will still work for the foreseeable future, but you will see warnings in logs and future dependency +# upgrades may break your log rolling param. The default log rolling param will be changed with the next major release +# from HOURLY to FAST_HOURLY, primarily different on how frequently indexes are built. For more info refer to: +# net.openhft.chronicle.queue.RollCycles audit_logging_options: enabled: false logger: @@ -1943,6 +1973,10 @@ report_unconfirmed_repaired_data_mismatches: false # Defaults to false to disable dynamic data masking. # dynamic_data_masking_enabled: false +# This is the page size used internally by aggregation queries. It aims to limit the memory used by aggregation +# queries when there is a lot of data to aggregate. +# aggregation_subpage_size_in_kb: 2048 + ######################### # EXPERIMENTAL FEATURES # ######################### @@ -1951,10 +1985,6 @@ report_unconfirmed_repaired_data_mismatches: false # Materialized views are considered experimental and are not recommended for production use. materialized_views_enabled: false -# Enables SASI index creation on this node. -# SASI indexes are considered experimental and are not recommended for production use. -sasi_indexes_enabled: false - # Enables creation of transiently replicated keyspaces on this node. # Transient replication is experimental and is not recommended for production use. transient_replication_enabled: false @@ -2011,7 +2041,7 @@ drop_compact_storage_enabled: false # columns_per_table_warn_threshold: -1 # columns_per_table_fail_threshold: -1 # -# Guardrail to warn or fail when creating more secondary indexes per table than threshold. +# Guardrail to warn or fail when creating more secondary indexes per table than threshold (does not apply to CUSTOM INDEX StorageAttachedIndex). # The two thresholds default to -1 to disable. # secondary_indexes_per_table_warn_threshold: -1 # secondary_indexes_per_table_fail_threshold: -1 @@ -2019,6 +2049,22 @@ drop_compact_storage_enabled: false # Guardrail to enable or disable the creation of secondary indexes # secondary_indexes_enabled: true # +# Failure threshold for number of StorageAttachedIndex per table (only applies to CUSTOM INDEX StorageAttachedIndex) +# Default is 10 (same when apply_dbaas_defaults is enabled) +# sai_indexes_per_table_warn_threshold: -1 +# sai_indexes_per_table_fail_threshold: 10 +# +# Failure threshold for total number of StorageAttachedIndex across all keyspaces (only applies to CUSTOM INDEX StorageAttachedIndex) +# Default is 10 (same when apply_dbaas_defaults is enabled) +# sai_indexes_total_warn_threshold: -1 +# sai_indexes_total_fail_threshold: 100 +# +# Guardrail to warn or fail when creating more trusted custom indexes (cassandra.trusted_index_implementations) +# per table than threshold, counted per implementation class. +# The two thresholds default to -1 to disable. +# trusted_indexes_per_table_warn_threshold: -1 +# trusted_indexes_per_table_fail_threshold: -1 +# # Guardrail to warn or fail when creating more materialized views per table than threshold. # The two thresholds default to -1 to disable. # materialized_views_per_table_warn_threshold: -1 @@ -2147,7 +2193,7 @@ drop_compact_storage_enabled: false # Guardrail to warn or fail when creating a vector column with more dimensions than threshold. # Default -1 to disable. # vector_dimensions_warn_threshold: -1 -# vector_dimensions_fail_threshold: -1 +# vector_dimensions_fail_threshold: 8192 # # Guardrail to indicate whether or not users are allowed to use ALTER TABLE commands to make column changes to tables # alter_table_enabled: true @@ -2218,6 +2264,20 @@ drop_compact_storage_enabled: false # sai_vector_term_size_warn_threshold: 16KiB # sai_vector_term_size_fail_threshold: 32KiB +# Guardrail to warn or fail when using LIMIT/OFFSET paging skipping more rows than threshold. +# Default offset_rows_warn_threshold is 10000, may differ if emulate_dbaas_defaults is enabled +# Default offset_rows_failure_threshold is 20000, may differ if emulate_dbaas_defaults is enabled +# offset_rows_warn_threshold: 10000 +# offset_rows_failure_threshold: 20000 + +# Guardrail to warn or fail when a SELECT query has more column value filters than threshold. +# Note that restrictions on indexed columns can be expanded to multiple column filters if the indexes have an analyzer. +# In that case, there will be a filter for every token produced by the analyzer for the queried column value. This can +# prevent that productive analyzers such as n-gram explode the query to a large number of filtering operations. +# Default -1 to disable, may differ if emulate_dbaas_defaults is enabled +# query_filters_warn_threshold: -1 +# query_filters_fail_threshold: -1 + # The default secondary index implementation when CREATE INDEX does not specify one via USING. # ex. "legacy_local_table" - (default) legacy secondary index, implemented as a hidden table # ex. "sai" - "storage-attched" index, implemented via optimized SSTable/Memtable-attached indexes @@ -2256,23 +2316,21 @@ drop_compact_storage_enabled: false # This property indicates with what Cassandra major version the storage format will be compatible with. # # The chosen storage compatibility mode will determine the versions of the written sstables, commitlogs, hints, etc. -# For example, if we're going to remain compatible with Cassandra 4.x, the value of this property should be 4, which -# will make us use sstables in the latest N version of the BIG format. # # This will also determine if certain features that depend on newer formats are available. For example, extended TTL # (up to 2106) depends on the sstable, commit-log, hints, and messaging versions introduced by Cassandra 5.0, so that -# feature won't be available if this property is set to CASSANDRA_4. See the upgrade guide for more details. +# feature won't be available if this property is set to HCD_1. # # Possible values are: # -# ** CASSANDRA_4: Stays compatible with the 4.x line in features, formats and component versions. +# ** HCD_1: Stays compatible with the 4.x line in features, formats and component versions. # ** UPGRADING: The cluster monitors the version of each node during this interim stage. This has a cost but ensures # all new features, formats, versions, etc. are enabled safely. # ** NONE: Start with all the new features and formats enabled. # # A typical upgrade would be: # -# . Do a rolling upgrade, starting all nodes in CASSANDRA_X compatibility mode. +# . Do a rolling upgrade, starting all nodes in HCD_1 compatibility mode. # . Once the new binary is rendered stable, do a rolling restart with the UPGRADING mode. The cluster will keep new # features disabled until all nodes are started in the UPGRADING mode; when that happens, new features controlled by # the storage compatibility mode are enabled. @@ -2280,4 +2338,7 @@ drop_compact_storage_enabled: false # and ensures stability. If Cassandra was started at the previous version by accident, a node with disabled # compatibility mode would no longer toggle behaviors as when it was running in the UPGRADING mode. # -storage_compatibility_mode: CASSANDRA_4 +storage_compatibility_mode: HCD_1 + +# Changes defaults considered production safest for HCD users +# hcd_guardrail_defaults: false diff --git a/conf/cassandra_latest.yaml b/conf/cassandra_latest.yaml index 0c7a792c8f40..ccedb80640af 100644 --- a/conf/cassandra_latest.yaml +++ b/conf/cassandra_latest.yaml @@ -1331,6 +1331,15 @@ truncate_request_timeout: 60000ms # Lowest acceptable value is 10 ms. # Min unit: ms request_timeout: 10000ms +# Upper bound for how long any request received via native transport +# should be considered live and serviceable by the system. This is +# currently considered at two points: when the message is dequeued and +# executed by the NATIVE_TRANSPORT_REQUESTS stage, and when the message +# is dequeued and executed by an async stage if NATIVE_TRANSPORT_ASYNC_READ_WRITE_ENABLED +# is set to true. If the request is not completed within this time, an +# OverloadedException is thrown. +# Min unit: ms +native_transport_timeout: 12000ms # Defensive settings for protecting Cassandra from true network partitions. # See (CASSANDRA-14358) for details. @@ -1556,6 +1565,35 @@ dynamic_snitch_reset_interval: 600000ms # until the pinned host was 20% worse than the fastest. dynamic_snitch_badness_threshold: 1.0 +# Paxos variant for lightweight transactions (LWTs) +# Options: +# v1 +# - Legacy Paxos. Expect 4RTs for a write and 3RTs for a read. (default) +# +# v1_without_linearizable_reads_or_rejected_writes +# - Legacy Paxos. Expect 4RTs for a write and 2RTs for a read. +# With legacy semantics for read/read and rejected write linearizability, i.e. not guaranteed. +# +# v2 +# - Optimized Paxos. Expect 2RTs for a write, and either 1RT or 2RT for a read. (recommended) +# +# v2_without_linearizable_reads +# - Optimized Paxos. Expect 2RTs for a write and 1RT for a read. +# +# v2_without_linearizable_reads_or_rejected_writes +# - Optimized Paxos. Expect 2RTs for a write and 1RT for a read. +# With legacy semantics for read/read and rejected write linearizability, i.e. not guaranteed. +# +# To upgrade from v1: +# 1. Ensure all nodes are on same Cassandra version 4.1+. +# 2. Run `nodetool repair --full -pr` on each node. +# 3. Set paxos_variant: v2 on each node and rolling restart. +# Rollback is safe: revert to v1 and rolling restart. No data migration needed. +# +# With any v2 variant and `paxos_state_purging: repaired` it is safe to use ANY Commit consistency. +# +paxos_variant: v2 + # Configures Java crypto provider. By default, it will use DefaultCryptoProvider # which will install Amazon Correto Crypto Provider. # @@ -2248,3 +2286,167 @@ default_secondary_index_enabled: true # compatibility mode would no longer toggle behaviors as when it was running in the UPGRADING mode. # storage_compatibility_mode: NONE + +# Prevents preparing a repair session or beginning a repair streaming session if pending compactions is over +# the given value. Defaults to disabled. +# reject_repair_compaction_threshold: 1024 + +# Ratio of disk that must be unused to run repair. It is useful to avoid disks filling up during +# repair as anti-compaction during repair may contribute to additional space temporarily. +# For example, setting this to 0.2 means at least 20% of disk must be unused. +# Set to 0.0 to disable this check. Defaults to 0.0 (disabled) on 5.0 for backward-compatibility. +# repair_disk_headroom_reject_ratio: 0.0 + +# Configuration for Auto Repair Scheduler. +# +# This feature is disabled by default. +# +# NOTE: The auto-repair feature requires enabling the JVM property `cassandra.autorepair.enable=true`. +# +# See: https://cassandra.apache.org/doc/latest/cassandra/managing/operating/auto_repair.html for an overview of this +# feature. +# +# auto_repair: +# # Enable/Disable the auto-repair scheduler. +# # If set to false, the scheduler thread will not be started. +# # If set to true, the repair scheduler thread will be created. The thread will +# # check for secondary configuration available for each repair type (full, incremental, +# # and preview_repaired), and based on that, it will schedule repairs. +# enabled: true +# repair_type_overrides: +# full: +# # Enable/Disable full auto-repair +# enabled: true +# # Minimum duration between repairing the same node again. This is useful for tiny clusters, +# # such as clusters with 5 nodes that finish repairs quickly. This means that if the scheduler completes one +# # round on all nodes in less than this duration, it will not start a new repair round on a given node until +# # this much time has passed since the last repair completed. Consider increasing to a larger value to reduce +# # the impact of repairs, however note that one should attempt to run repairs at a smaller interval than +# # gc_grace_seconds to avoid potential data resurrection. +# min_repair_interval: 24h +# token_range_splitter: +# # Implementation of IAutoRepairTokenRangeSplitter; responsible for splitting token ranges +# # for repair assignments. +# # +# # Out of the box, Cassandra provides org.apache.cassandra.repair.autorepair.{RepairTokenRangeSplitter, +# # FixedTokenRangeSplitter}. +# # +# # - RepairTokenRangeSplitter (default) attempts to intelligently split ranges based on data size and partition +# # count. +# # - FixedTokenRangeSplitter splits into fixed ranges based on the 'number_of_subranges' option. +# # class_name: org.apache.cassandra.repair.autorepair.RepairTokenRangeSplitter +# +# # Optional parameters can be specified in the form of: +# # parameters: +# # param_key1: param_value1 +# parameters: +# # The target and maximum amount of compressed bytes that should be included in a repair assignment. +# # This scopes the amount of work involved in a repair and includes the data covering the range being +# # repaired. +# bytes_per_assignment: 50GiB +# # The maximum number of bytes to cover in an individual schedule. This serves as +# # a mechanism to throttle the work done in each repair cycle. You may reduce this +# # value if the impact of repairs is causing too much load on the cluster or increase it +# # if writes outpace the amount of data being repaired. Alternatively, adjust the +# # min_repair_interval. +# # This is set to a large value for full repair to attempt to repair all data per repair schedule. +# max_bytes_per_schedule: 100000GiB +# incremental: +# enabled: false +# # Incremental repairs operate over unrepaired data and should finish quickly. Running incremental repair +# # frequently keeps the unrepaired set smaller and thus causes repairs to operate over a smaller set of data, +# # so a more frequent schedule such as 1h is recommended. +# # NOTE: Please consult +# # https://cassandra.apache.org/doc/latest/cassandra/managing/operating/auto_repair.html#enabling-ir +# # for guidance on enabling incremental repair on ane exiting cluster. +# min_repair_interval: 24h +# token_range_splitter: +# parameters: +# # Configured to attempt repairing 50GiB of compressed data per repair. +# # This throttles the amount of incremental repair and anticompaction done per schedule after incremental +# # repairs are turned on. +# bytes_per_assignment: 50GiB +# # Restricts the maximum number of bytes to cover in an individual schedule to the configured +# # max_bytes_per_schedule value (defaults to 100GiB for incremental). +# # Consider increasing this value if more data is written than this limit within the min_repair_interval. +# max_bytes_per_schedule: 100GiB +# preview_repaired: +# # Performs preview repair over repaired SSTables, useful to detect possible inconsistencies in the repaired +# # data set. +# enabled: false +# min_repair_interval: 24h +# token_range_splitter: +# parameters: +# bytes_per_assignment: 50GiB +# max_bytes_per_schedule: 100000GiB +# # Time interval between successive checks to see if ongoing repairs are complete or if it is time to schedule +# # repairs. +# repair_check_interval: 5m +# # Minimum duration for the execution of a single repair task. This prevents the scheduler from overwhelming +# # the node by scheduling too many repair tasks in a short period of time. +# repair_task_min_duration: 5s +# # The scheduler needs to adjust its order when nodes leave the ring. Deleted hosts are tracked in metadata +# # for a specified duration to ensure they are indeed removed before adjustments are made to the schedule. +# history_clear_delete_hosts_buffer_interval: 2h +# # By default repair is disabled if there are mixed major versions detected - which would happen +# # if a major version upgrade is being performed on the cluster, but a user can enable it using this flag +# mixed_major_version_repair_enabled: false +# # NOTE: Each of the below settings can be overridden per repair type under repair_type_overrides +# global_settings: +# # If true, attempts to group tables in the same keyspace into one repair; otherwise, each table is repaired +# # individually. +# repair_by_keyspace: true +# # Number of threads to use for each repair job scheduled by the scheduler. Similar to the -j option in nodetool +# # repair. +# number_of_repair_threads: 1 +# # Number of nodes running repair in parallel. If parallel_repair_percentage is set, the larger value is used. +# parallel_repair_count: 3 +# # Percentage of nodes in the cluster running repair in parallel. If parallel_repair_count is set, the larger value +# # is used. +# parallel_repair_percentage: 3 +# # Whether to allow a node to take its turn running repair while one or more of its replicas are running repair. +# # Defaults to false, as running repairs concurrently on replicas can increase load and also cause anticompaction +# # conflicts while running incremental repair. +# allow_parallel_replica_repair: false +# # An addition to allow_parallel_replica_repair that also blocks repairs when replicas (including this node itself) +# # are repairing in any schedule. For example, if a replica is executing full repairs, a value of false will +# # prevent starting incremental repairs for this node. Defaults to true and is only evaluated when +# # allow_parallel_replica_repair is false. +# allow_parallel_replica_repair_across_schedules: true +# # Repairs materialized views if true. +# materialized_view_repair_enabled: false +# # Delay before starting repairs after a node restarts to avoid repairs starting immediately after a restart. +# initial_scheduler_delay: 5m +# # Timeout for retrying stuck repair sessions. +# repair_session_timeout: 3h +# # Force immediate repair on new nodes after they join the ring. +# force_repair_new_node: false +# # Threshold to skip repairing tables with too many SSTables. Defaults to 10,000 SSTables to avoid penalizing good +# # tables. +# sstable_upper_threshold: 50000 +# # Maximum time allowed for repairing one table on a given node. If exceeded, the repair proceeds to the +# # next table. +# table_max_repair_time: 6h +# # Avoid running repairs in specific data centers. By default, repairs run in all data centers. Specify data +# # centers to exclude in this list. Note that repair sessions will still consider all replicas from excluded +# # data centers. Useful if you have keyspaces that are not replicated in certain data centers, and you want to +# # not run repair schedule in certain data centers. +# ignore_dcs: [] +# # Repair only the primary ranges owned by a node. Equivalent to the -pr option in nodetool repair. Defaults +# # to true. General advice is to keep this true. +# repair_primary_token_range_only: true +# # Maximum number of retries for a repair session. +# repair_max_retries: 3 +# # Backoff time before retrying a repair session. +# repair_retry_backoff: 30s +# token_range_splitter: +# # Splitter implementation to generate repair assignments. Defaults to RepairTokenRangeSplitter. +# class_name: org.apache.cassandra.repair.autorepair.RepairTokenRangeSplitter +# parameters: +# # Maximum number of partitions to include in a repair assignment. Used to reduce number of partitions +# # present in merkle tree leaf nodes to avoid overstreaming. +# partitions_per_assignment: 1048576 +# # Maximum number of tables to include in a repair assignment. This reduces the number of repairs, +# # especially in keyspaces with many tables. The splitter avoids batching tables together if they +# # exceed other configuration parameters like bytes_per_assignment or partitions_per_assignment. +# max_tables_per_assignment: 64 diff --git a/conf/cqlshrc.sample b/conf/cqlshrc.sample index 79d719460e29..abc11b6084fe 100644 --- a/conf/cqlshrc.sample +++ b/conf/cqlshrc.sample @@ -32,6 +32,15 @@ ; classname = PlainTextAuthProvider ; username = user1 +[auth_provider] +;; you can specify any auth provider found in your python environment +;; module and class will be used to dynamically load the class +;; all other properties found here and in the credentials file under the class name +;; will be passed to the constructor +; module = cassandra.auth +; classname = PlainTextAuthProvider +; username = user1 + [protocol] ;; Specify a specific protcol version otherwise the client will default and downgrade as necessary ; version = None diff --git a/conf/cqlshrc.sample.cloud b/conf/cqlshrc.sample.cloud new file mode 100644 index 000000000000..62528670c48b --- /dev/null +++ b/conf/cqlshrc.sample.cloud @@ -0,0 +1,17 @@ +; Copyright DataStax, Inc. +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. +; +; Sample ~/.cqlshrc file with cloud configuration. +[connection] +secure_connect_bundle = /path/to/creds.zip diff --git a/conf/jvm11-clients.options b/conf/jvm11-clients.options index 3d59816c045f..08ce8f2a30f6 100644 --- a/conf/jvm11-clients.options +++ b/conf/jvm11-clients.options @@ -29,18 +29,28 @@ -Djdk.attach.allowAttachSelf=true --add-exports java.base/jdk.internal.misc=ALL-UNNAMED --add-exports java.base/jdk.internal.ref=ALL-UNNAMED +--add-exports java.base/jdk.internal.util=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED --add-exports java.sql/java.sql=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.unsupported/sun.misc=ALL-UNNAMED +--add-opens java.base/java.io=ALL-UNNAMED +--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.module=ALL-UNNAMED +--add-opens java.base/java.lang.reflect=ALL-UNNAMED +--add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.math=ALL-UNNAMED --add-opens java.base/jdk.internal.module=ALL-UNNAMED +--add-opens java.base/java.util=ALL-UNNAMED +--add-opens java.base/jdk.internal.util=ALL-UNNAMED --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED diff --git a/conf/jvm11-server.options b/conf/jvm11-server.options index 857e07857a05..05e180a4d995 100644 --- a/conf/jvm11-server.options +++ b/conf/jvm11-server.options @@ -30,6 +30,8 @@ # Disable biased locking as it does not benefit Cassandra. -XX:-UseBiasedLocking +-XX:ThreadPriorityPolicy=1 +-XX:+UseThreadPriorities ################# # GC SETTINGS # @@ -104,14 +106,21 @@ --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED --add-exports java.sql/java.sql=ALL-UNNAMED +--add-exports jdk.unsupported/sun.misc=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-opens java.base/java.io=ALL-UNNAMED +--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.module=ALL-UNNAMED +--add-opens java.base/java.lang.reflect=ALL-UNNAMED +--add-opens=java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.math=ALL-UNNAMED --add-opens java.base/jdk.internal.module=ALL-UNNAMED --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED @@ -120,7 +129,7 @@ # Java 11 (and newer) GC logging options: # See description of https://bugs.openjdk.java.net/browse/JDK-8046148 for details about the syntax # The following is the equivalent to -XX:+PrintGCDetails -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M -#-Xlog:gc=info,heap*=trace,age*=debug,safepoint=info,promotion*=trace:file=/var/log/cassandra/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760 +#-Xlog:gc=info,heap*=debug,age*=debug,safepoint=info,promotion*=debug:file=/var/log/cassandra/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760 # Notes for Java 8 migration: # diff --git a/conf/jvm17-clients.options b/conf/jvm17-clients.options index 671d91b21f95..36e15c838fce 100644 --- a/conf/jvm17-clients.options +++ b/conf/jvm17-clients.options @@ -28,6 +28,8 @@ -Djdk.attach.allowAttachSelf=true --add-exports java.base/jdk.internal.misc=ALL-UNNAMED +--add-exports java.base/jdk.internal.ref=ALL-UNNAMED +--add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED diff --git a/conf/jvm17-server.options b/conf/jvm17-server.options index 9a695aa351d7..ee121a0c86da 100644 --- a/conf/jvm17-server.options +++ b/conf/jvm17-server.options @@ -22,6 +22,9 @@ # See jvm-server.options. This file is specific for Java 17 and newer. # ########################################################################### +-XX:ThreadPriorityPolicy=1 +-XX:+UseThreadPriorities + ################# # GC SETTINGS # ################# @@ -46,7 +49,7 @@ # Main G1GC tunable: lowering the pause target will lower throughput and vise versa. # 200ms is the JVM default and lowest viable setting # 1000ms increases throughput. Keep it smaller than the timeouts in cassandra.yaml. --XX:MaxGCPauseMillis=300 +-XX:MaxGCPauseMillis=500 ## Optional G1 Settings # Save CPU time on large (>= 16GB) heaps by delaying region scanning @@ -67,6 +70,8 @@ -Djdk.attach.allowAttachSelf=true --add-exports java.base/jdk.internal.misc=ALL-UNNAMED +--add-exports java.base/jdk.internal.ref=ALL-UNNAMED +--add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED --add-exports java.management/com.sun.jmx.remote.security=ALL-UNNAMED --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED @@ -74,8 +79,8 @@ --add-exports java.sql/java.sql=ALL-UNNAMED --add-exports java.base/java.lang.ref=ALL-UNNAMED --add-exports jdk.unsupported/sun.misc=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED ---add-opens java.base/java.lang.module=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED @@ -87,15 +92,20 @@ --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED +--add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac=ALL-UNNAMED + +# required for org.apache.cassandra.Util.getSupportedMTimeGranularity +--add-opens java.base/java.nio.file.attribute=ALL-UNNAMED ### GC logging options -- uncomment to enable # Java 11 (and newer) GC logging options: # See description of https://bugs.openjdk.java.net/browse/JDK-8046148 for details about the syntax # The following is the equivalent to -XX:+PrintGCDetails -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M -#-Xlog:gc=info,heap*=trace,age*=debug,safepoint=info,promotion*=trace:file=/var/log/cassandra/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760 +# -Xlog:gc=info,heap*=debug,age*=debug,safepoint=info,promotion*=debug:file=/var/log/cassandra/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760 # Notes for Java 8 migration: # @@ -118,5 +128,12 @@ # Revert changes in defaults introduced in https://netty.io/news/2022/03/10/4-1-75-Final.html -Dio.netty.allocator.useCacheForAllThreads=true -Dio.netty.allocator.maxOrder=11 +### Enable vector incubator feature (simd support) + +--add-modules jdk.incubator.vector + +### Compatibility Options +--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED +-Djava.security.manager=allow # The newline in the end of file is intentional diff --git a/conf/jvm22-clients.options b/conf/jvm22-clients.options new file mode 100644 index 000000000000..81af895ed216 --- /dev/null +++ b/conf/jvm22-clients.options @@ -0,0 +1,50 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +########################################################################### +# jvm22-clients.options # +# # +# See jvm-clients.options. This file is specific for Java 22 and newer. # +########################################################################### + +################### +# JPMS SETTINGS # +################### + +-Djdk.attach.allowAttachSelf=true +--add-exports java.base/jdk.internal.misc=ALL-UNNAMED +--add-exports java.base/jdk.internal.ref=ALL-UNNAMED +--add-exports java.base/sun.nio.ch=ALL-UNNAMED +--add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED +--add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED +--add-exports java.rmi/sun.rmi.server=ALL-UNNAMED +--add-exports java.sql/java.sql=ALL-UNNAMED +--add-exports jdk.attach/sun.tools.attach=ALL-UNNAMED + +--add-opens java.base/java.io=ALL-UNNAMED +--add-opens java.base/java.lang.module=ALL-UNNAMED +--add-opens java.base/java.lang.reflect=ALL-UNNAMED +--add-opens java.base/jdk.internal.loader=ALL-UNNAMED +--add-opens java.base/jdk.internal.math=ALL-UNNAMED +--add-opens java.base/jdk.internal.module=ALL-UNNAMED +--add-opens java.base/jdk.internal.ref=ALL-UNNAMED +--add-opens java.base/jdk.internal.reflect=ALL-UNNAMED +--add-opens java.base/sun.nio.ch=ALL-UNNAMED +--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + +# The newline in the end of file is intentional diff --git a/conf/jvm22-server.options b/conf/jvm22-server.options new file mode 100644 index 000000000000..b836204660bb --- /dev/null +++ b/conf/jvm22-server.options @@ -0,0 +1,128 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +########################################################################### +# jvm22-server.options # +# # +# See jvm-server.options. This file is specific for Java 22 and newer. # +########################################################################### + +################# +# GC SETTINGS # +################# + +### G1 Settings +## Use the Hotspot garbage-first collector. +-XX:+UseG1GC +-XX:+ParallelRefProcEnabled + +# +## Have the JVM do less remembered set work during STW, instead +## preferring concurrent GC. Reduces p99.9 latency. +-XX:G1RSetUpdatingPauseTimePercent=5 +# +## Main G1GC tunable: lowering the pause target will lower throughput and vise versa. +## 200ms is the JVM default and lowest viable setting +## 1000ms increases throughput. Keep it smaller than the timeouts in cassandra.yaml. +-XX:MaxGCPauseMillis=500 + +## Optional G1 Settings +# Save CPU time on large (>= 16GB) heaps by delaying region scanning +# until the heap is 70% full. The default in Hotspot 8u40 is 40%. +#-XX:InitiatingHeapOccupancyPercent=70 + +# For systems with > 8 cores, the default ParallelGCThreads is 5/8 the number of logical cores. +# Otherwise equal to the number of cores when 8 or less. +# Machines with > 10 cores should try setting these to <= full cores. +#-XX:ParallelGCThreads=16 +# By default, ConcGCThreads is 1/4 of ParallelGCThreads. +# Setting both to the same value can reduce STW durations. +#-XX:ConcGCThreads=16 + + +### JPMS + +-Djdk.attach.allowAttachSelf=true +-Djava.security.manager=allow +--add-exports java.base/jdk.internal.misc=ALL-UNNAMED +--add-exports java.base/jdk.internal.ref=ALL-UNNAMED +--add-exports java.base/jdk.internal.perf=ALL-UNNAMED +--add-exports java.base/sun.nio.ch=ALL-UNNAMED +--add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED +--add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED +--add-exports java.rmi/sun.rmi.server=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.naming.dns/com.sun.jndi.dns=java.naming +--add-exports jdk.unsupported/sun.misc=ALL-UNNAMED + +--add-opens java.base/java.io=ALL-UNNAMED +--add-opens java.base/java.lang.module=ALL-UNNAMED +--add-opens java.base/java.lang=ALL-UNNAMED +--add-opens java.base/java.lang.reflect=ALL-UNNAMED +--add-opens java.base/java.nio.charset=ALL-UNNAMED +--add-opens java.base/java.nio.file.spi=ALL-UNNAMED +--add-opens java.base/java.nio=ALL-UNNAMED +--add-opens java.base/java.net=ALL-UNNAMED +--add-opens java.base/java.util=ALL-UNNAMED +--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED +--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED +--add-opens java.base/jdk.internal.loader=ALL-UNNAMED +--add-opens java.base/jdk.internal.math=ALL-UNNAMED +--add-opens java.base/jdk.internal.module=ALL-UNNAMED +--add-opens java.base/jdk.internal.ref=ALL-UNNAMED +--add-opens java.base/jdk.internal.reflect=ALL-UNNAMED +--add-opens java.base/jdk.internal.vm=ALL-UNNAMED +--add-opens java.base/sun.nio.ch=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac=ALL-UNNAMED +--add-opens jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED +--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED +--add-opens jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED + +# required for org.apache.cassandra.Util.getSupportedMTimeGranularity +--add-opens java.base/java.nio.file.attribute=ALL-UNNAMED + +### GC logging options -- uncomment to enable + +# Java 11 (and newer) GC logging options: +# See description of https://bugs.openjdk.java.net/browse/JDK-8046148 for details about the syntax +# The following is the equivalent to -XX:+PrintGCDetails -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M +# -Xlog:gc=info,heap*=debug,age*=debug,safepoint=info,promotion*=debug:file=/var/log/cassandra/gc.log:time,uptime,pid,tid,level:filecount=10,filesize=10485760 + +# Notes for Java 8 migration: +# +# -XX:+PrintGCDetails maps to -Xlog:gc*:... - i.e. add a '*' after "gc" +# -XX:+PrintGCDateStamps maps to decorator 'time' +# +# -XX:+PrintHeapAtGC maps to 'heap' with level 'trace' +# -XX:+PrintTenuringDistribution maps to 'age' with level 'debug' +# -XX:+PrintGCApplicationStoppedTime maps to 'safepoint' with level 'info' +# -XX:+PrintPromotionFailure maps to 'promotion' with level 'trace' +# -XX:PrintFLSStatistics=1 maps to 'freelist' with level 'trace' + +### Netty Options + +# On Java >= 9 Netty requires the io.netty.tryReflectionSetAccessible system property to be set to true to enable +# creation of direct buffers using Unsafe. Without it, this falls back to ByteBuffer.allocateDirect which has +# inferior performance and risks exceeding MaxDirectMemory +-Dio.netty.tryReflectionSetAccessible=true + +### Enable vector incubator feature (simd support) + +--add-modules jdk.incubator.vector + +# The newline in the end of file is intentional diff --git a/debian/changelog b/debian/changelog index 432841232af9..1f04f7a05b7a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +cassandra (5.0.9) unstable; urgency=medium + + * New release + + -- Stefan Miklosovic Tue, 28 Jul 2026 12:57:11 +0200 + +cassandra (5.0.8) unstable; urgency=medium + + * New release + + -- Stefan Miklosovic Fri, 10 Apr 2026 11:33:31 +0200 + cassandra (5.0.7) unstable; urgency=medium * New release diff --git a/doc/Makefile b/doc/Makefile index dac8e11fb06f..b1722037f5f6 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -11,9 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +GENERATE_ANTORA_YML = ./scripts/gen-antora-yml.py GENERATE_NODETOOL_DOCS = ./scripts/gen-nodetool-docs.py MAKE_CASSANDRA_YAML = ./scripts/convert_yaml_to_adoc.py ../conf/cassandra.yaml ./modules/cassandra/pages/managing/configuration/cass_yaml_file.adoc -PROCESS_NATIVE_PROC_SPECS = ./scripts/process-native-protocol-specs-in-docker.sh +GEN_NATIVE_PROTOCOL_DOCS = ./scripts/gen-native-protocol-docs.sh .PHONY: html html: @@ -21,8 +22,9 @@ html: .PHONY: gen-asciidoc gen-asciidoc: + python3 $(GENERATE_ANTORA_YML) @mkdir -p modules/cassandra/pages/managing/tools/nodetool @mkdir -p modules/cassandra/examples/TEXT/NODETOOL python3 $(GENERATE_NODETOOL_DOCS) python3 $(MAKE_CASSANDRA_YAML) - $(PROCESS_NATIVE_PROC_SPECS) + $(GEN_NATIVE_PROTOCOL_DOCS) diff --git a/doc/README.md b/doc/README.md index 608d236cb75b..0d534a52b97a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -36,7 +36,9 @@ The source for the official documentation for Apache Cassandra can be found in the `modules/cassandra/pages` subdirectory. The documentation uses [antora](http://www.antora.org/) and is thus written in [asciidoc](http://asciidoc.org). -To generate the asciidoc files for cassandra.yaml and the nodetool commands, run (from project root): +The `antora.yml` file is auto-generated and should not be manually edited. It is generated from the version in `build.xml` using `scripts/gen-antora-yml.py` and automatically detects whether building from a release tag or branch HEAD to set the appropriate version. + +To generate the asciidoc files (including antora.yml) for cassandra.yaml and the nodetool commands, run (from project root): ```bash ant gen-asciidoc ``` diff --git a/doc/SASI.md b/doc/SASI.md deleted file mode 100644 index fc38845ce2cd..000000000000 --- a/doc/SASI.md +++ /dev/null @@ -1,818 +0,0 @@ - - -# SASIIndex - -[`SASIIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java), -or "SASI" for short, is an implementation of Cassandra's -`Index` interface that can be used as an alternative to the -existing implementations. SASI's indexing and querying improves on -existing implementations by tailoring it specifically to Cassandra's -needs. SASI has superior performance in cases where queries would -previously require filtering. In achieving this performance, SASI aims -to be significantly less resource intensive than existing -implementations, in memory, disk, and CPU usage. In addition, SASI -supports prefix and contains queries on strings (similar to SQL's -`LIKE = "foo*"` or `LIKE = "*foo*"'`). - -The following goes on describe how to get up and running with SASI, -demonstrates usage with examples, and provides some details on its -implementation. - -## Using SASI - -The examples below walk through creating a table and indexes on its -columns, and performing queries on some inserted data. - -The examples below assume the `demo` keyspace has been created and is -in use. - -``` -cqlsh> CREATE KEYSPACE demo WITH replication = { - ... 'class': 'SimpleStrategy', - ... 'replication_factor': '1' - ... }; -cqlsh> USE demo; -``` - -All examples are performed on the `sasi` table: - -``` -cqlsh:demo> CREATE TABLE sasi (id uuid, first_name text, last_name text, - ... age int, height int, created_at bigint, primary key (id)); -``` - -#### Creating Indexes - -To create SASI indexes use CQLs `CREATE CUSTOM INDEX` statement: - -``` -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (first_name) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = { - ... 'analyzer_class': - ... 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer', - ... 'case_sensitive': 'false' - ... }; - -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (last_name) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = {'mode': 'CONTAINS'}; - -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (age) USING 'org.apache.cassandra.index.sasi.SASIIndex'; - -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (created_at) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = {'mode': 'SPARSE'}; -``` - -The indexes created have some options specified that customize their -behaviour and potentially performance. The index on `first_name` is -case-insensitive. The analyzers are discussed more in a subsequent -example. The `NonTokenizingAnalyzer` performs no analysis on the -text. Each index has a mode: `PREFIX`, `CONTAINS`, or `SPARSE`, the -first being the default. The `last_name` index is created with the -mode `CONTAINS` which matches terms on suffixes instead of prefix -only. Examples of this are available below and more detail can be -found in the section on -[OnDiskIndex](#ondiskindexbuilder).The -`created_at` column is created with its mode set to `SPARSE`, which is -meant to improve performance of querying large, dense number ranges -like timestamps for data inserted every millisecond. Details of the -`SPARSE` implementation can also be found in the section on the -[OnDiskIndex](#ondiskindexbuilder). The `age` -index is created with the default `PREFIX` mode and no -case-sensitivity or text analysis options are specified since the -field is numeric. - -After inserting the following data and performing a `nodetool flush`, -SASI performing index flushes to disk can be seen in Cassandra's logs --- although the direct call to flush is not required (see -[IndexMemtable](#indexmemtable) for more details). - -``` -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (556ebd54-cbe5-4b75-9aae-bf2a31a24500, 'Pavel', 'Yaskevich', 27, 181, 1442959315018); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (5770382a-c56f-4f3f-b755-450e24d55217, 'Jordan', 'West', 26, 173, 1442959315019); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (96053844-45c3-4f15-b1b7-b02c441d3ee1, 'Mikhail', 'Stepura', 36, 173, 1442959315020); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (f5dfcabe-de96-4148-9b80-a1c41ed276b4, 'Michael', 'Kjellman', 26, 180, 1442959315021); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (2970da43-e070-41a8-8bcb-35df7a0e608a, 'Johnny', 'Zhang', 32, 175, 1442959315022); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (6b757016-631d-4fdb-ac62-40b127ccfbc7, 'Jason', 'Brown', 40, 182, 1442959315023); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (8f909e8a-008e-49dd-8d43-1b0df348ed44, 'Vijay', 'Parthasarathy', 34, 183, 1442959315024); - -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi; - - first_name | last_name | age | height | created_at -------------+---------------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - Mikhail | Stepura | 36 | 173 | 1442959315020 - Jason | Brown | 40 | 182 | 1442959315023 - Pavel | Yaskevich | 27 | 181 | 1442959315018 - Vijay | Parthasarathy | 34 | 183 | 1442959315024 - Jordan | West | 26 | 173 | 1442959315019 - Johnny | Zhang | 32 | 175 | 1442959315022 - -(7 rows) -``` - -#### Equality & Prefix Queries - -SASI supports all queries already supported by CQL, including LIKE statement -for PREFIX, CONTAINS and SUFFIX searches. - -``` -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name = 'Pavel'; - - first_name | last_name | age | height | created_at --------------+-----------+-----+--------+--------------- - Pavel | Yaskevich | 27 | 181 | 1442959315018 - -(1 rows) -``` - -``` -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name = 'pavel'; - - first_name | last_name | age | height | created_at --------------+-----------+-----+--------+--------------- - Pavel | Yaskevich | 27 | 181 | 1442959315018 - -(1 rows) -``` - -``` -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name LIKE 'M%'; - - first_name | last_name | age | height | created_at -------------+-----------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - Mikhail | Stepura | 36 | 173 | 1442959315020 - -(2 rows) -``` - -Of course, the case of the query does not matter for the `first_name` -column because of the options provided at index creation time. - -``` -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name LIKE 'm%'; - - first_name | last_name | age | height | created_at -------------+-----------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - Mikhail | Stepura | 36 | 173 | 1442959315020 - -(2 rows) -``` - -#### Compound Queries - -SASI supports queries with multiple predicates, however, due to the -nature of the default indexing implementation, CQL requires the user -to specify `ALLOW FILTERING` to opt-in to the potential performance -pitfalls of such a query. With SASI, while the requirement to include -`ALLOW FILTERING` remains, to reduce modifications to the grammar, the -performance pitfalls do not exist because filtering is not -performed. Details on how SASI joins data from multiple predicates is -available below in the -[Implementation Details](#implementation-details) -section. - -``` -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name LIKE 'M%' and age < 30 ALLOW FILTERING; - - first_name | last_name | age | height | created_at -------------+-----------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - -(1 rows) -``` - -#### Suffix Queries - -The next example demonstrates `CONTAINS` mode on the `last_name` -column. By using this mode, predicates can search for any strings -containing the search string as a sub-string. In this case the strings -containing "a" or "an". - -``` -cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%a%'; - - id | age | created_at | first_name | height | last_name ---------------------------------------+-----+---------------+------------+--------+--------------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman - 96053844-45c3-4f15-b1b7-b02c441d3ee1 | 36 | 1442959315020 | Mikhail | 173 | Stepura - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | 1442959315018 | Pavel | 181 | Yaskevich - 8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | 1442959315024 | Vijay | 183 | Parthasarathy - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang - -(5 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%an%'; - - id | age | created_at | first_name | height | last_name ---------------------------------------+-----+---------------+------------+--------+----------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang - -(2 rows) -``` - -#### Expressions on Non-Indexed Columns - -SASI also supports filtering on non-indexed columns like `height`. The -expression can only narrow down an existing query using `AND`. - -``` -cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%a%' AND height >= 175 ALLOW FILTERING; - - id | age | created_at | first_name | height | last_name ---------------------------------------+-----+---------------+------------+--------+--------------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | 1442959315018 | Pavel | 181 | Yaskevich - 8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | 1442959315024 | Vijay | 183 | Parthasarathy - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang - -(4 rows) -``` - -#### Delimiter based Tokenization Analysis - -A simple text analysis provided is delimiter based tokenization. This provides an alternative to indexing collections, -as delimiter separated text can be indexed without the overhead of `CONTAINS` mode nor using `PREFIX` or `SUFFIX` queries. - -``` -cqlsh:demo> ALTER TABLE sasi ADD aliases text; -cqlsh:demo> CREATE CUSTOM INDEX on sasi (aliases) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = { - ... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer', - ... 'delimiter': ',', - ... 'mode': 'prefix', - ... 'analyzed': 'true'}; -cqlsh:demo> UPDATE sasi SET aliases = 'Mike,Mick,Mikey,Mickey' WHERE id = f5dfcabe-de96-4148-9b80-a1c41ed276b4; -cqlsh:demo> SELECT * FROM sasi WHERE aliases LIKE 'Mikey' ALLOW FILTERING; - - id | age | aliases | created_at | first_name | height | last_name ---------------------------------------+-----+------------------------+---------------+------------+--------+----------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | Mike,Mick,Mikey,Mickey | 1442959315021 | Michael | 180 | Kjellman -``` - -#### Text Analysis (Tokenization and Stemming) - -Lastly, to demonstrate text analysis an additional column is needed on -the table. Its definition, index, and statements to update rows are shown below. - -``` -cqlsh:demo> ALTER TABLE sasi ADD bio text; -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (bio) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = { - ... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer', - ... 'tokenization_enable_stemming': 'true', - ... 'analyzed': 'true', - ... 'tokenization_normalize_lowercase': 'true', - ... 'tokenization_locale': 'en' - ... }; -cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, who likes distributed systems, doesnt like to argue.' WHERE id = 5770382a-c56f-4f3f-b755-450e24d55217; -cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, works on the freight distribution at nights and likes arguing' WHERE id = 556ebd54-cbe5-4b75-9aae-bf2a31a24500; -cqlsh:demo> SELECT * FROM sasi; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+--------------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | null | 1442959315021 | Michael | 180 | Kjellman - 96053844-45c3-4f15-b1b7-b02c441d3ee1 | 36 | null | 1442959315020 | Mikhail | 173 | Stepura - 6b757016-631d-4fdb-ac62-40b127ccfbc7 | 40 | null | 1442959315023 | Jason | 182 | Brown - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | null | 1442959315024 | Vijay | 183 | Parthasarathy - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | null | 1442959315022 | Johnny | 175 | Zhang - -(7 rows) -``` - -Index terms and query search strings are stemmed for the `bio` column -because it was configured to use the -[`StandardAnalyzer`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java) -and `analyzed` is set to `true`. The -`tokenization_normalize_lowercase` is similar to the `case_sensitive` -property but for the -[`StandardAnalyzer`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java). These -query demonstrates the stemming applied by [`StandardAnalyzer`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java). - -``` -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'distributing'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - -(2 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'they argued'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - -(2 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'working at the company'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - -(1 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'soft eng'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - -(2 rows) -``` - -## Implementation Details - -While SASI, at the surface, is simply an implementation of the -`Index` interface, at its core there are several data -structures and algorithms used to satisfy it. These are described -here. Additionally, the changes internal to Cassandra to support SASI's -integration are described. - -The `Index` interface divides responsibility of the -implementer into two parts: Indexing and Querying. Further, Cassandra -makes it possible to divide those responsibilities into the memory and -disk components. SASI takes advantage of Cassandra's write-once, -immutable, ordered data model to build indexes along with the flushing -of the memtable to disk -- this is the origin of the name "SSTable -Attached Secondary Index". - -The SASI index data structures are built in memory as the SSTable is -being written and they are flushed to disk before the writing of the -SSTable completes. The writing of each index file only requires -sequential writes to disk. In some cases, partial flushes are -performed, and later stitched back together, to reduce memory -usage. These data structures are optimized for this use case. - -Taking advantage of Cassandra's ordered data model, at query time, -candidate indexes are narrowed down for searching, minimizing the amount -of work done. Searching is then performed using an efficient method -that streams data off disk as needed. - -### Indexing - -Per SSTable, SASI writes an index file for each indexed column. The -data for these files is built in memory using the -[`OnDiskIndexBuilder`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java). Once -flushed to disk, the data is read using the -[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java) -class. These are composed of bytes representing indexed terms, -organized for efficient writing or searching respectively. The keys -and values they hold represent tokens and positions in an SSTable and -these are stored per-indexed term in -[`TokenTreeBuilder`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java)s -for writing, and -[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s -for querying. These index files are memory mapped after being written -to disk, for quicker access. For indexing data in the memtable, SASI -uses its -[`IndexMemtable`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java) -class. - -#### OnDiskIndex(Builder) - -Each -[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java) -is an instance of a modified -[Suffix Array](https://en.wikipedia.org/wiki/Suffix_array) data -structure. The -[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java) -is comprised of page-size blocks of sorted terms and pointers to the -terms' associated data, as well as the data itself, stored also in one -or more page-sized blocks. The -[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java) -is structured as a tree of arrays, where each level describes the -terms in the level below, the final level being the terms -themselves. The `PointerLevel`s and their `PointerBlock`s contain -terms and pointers to other blocks that *end* with those terms. The -`DataLevel`, the final level, and its `DataBlock`s contain terms and -point to the data itself, contained in [`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s. - -The terms written to the -[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java) -vary depending on its "mode": either `PREFIX`, `CONTAINS`, or -`SPARSE`. In the `PREFIX` and `SPARSE` cases, terms' exact values are -written exactly once per `OnDiskIndex`. For example, when using a `PREFIX` index -with terms `Jason`, `Jordan`, `Pavel`, all three will be included in -the index. A `CONTAINS` index writes additional terms for each suffix of -each term recursively. Continuing with the example, a `CONTAINS` index -storing the previous terms would also store `ason`, `ordan`, `avel`, -`son`, `rdan`, `vel`, etc. This allows for queries on the suffix of -strings. The `SPARSE` mode differs from `PREFIX` in that for every 64 -blocks of terms a -[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java) -is built merging all the -[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s -for each term into a single one. This copy of the data is used for -efficient iteration of large ranges of e.g. timestamps. The index -"mode" is configurable per column at index creation time. - -#### TokenTree(Builder) - -The -[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java) -is an implementation of the well-known -[B+-tree](https://en.wikipedia.org/wiki/B%2B_tree) that has been -modified to optimize for its use-case. In particular, it has been -optimized to associate tokens, longs, with a set of positions in an -SSTable, also longs. Allowing the set of long values accommodates -the possibility of a hash collision in the token, but the data -structure is optimized for the unlikely possibility of such a -collision. - -To optimize for its write-once environment the -[`TokenTreeBuilder`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java) -completely loads its interior nodes as the tree is built and it uses -the well-known algorithm optimized for bulk-loading the data -structure. - -[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s provide the means to iterate over tokens, and file -positions, that match a given term, and to skip forward in that -iteration, an operation used heavily at query time. - -#### IndexMemtable - -The -[`IndexMemtable`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java) -handles indexing the in-memory data held in the memtable. The -[`IndexMemtable`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java) -in turn manages either a -[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java) -or a -[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java) -per-column. The choice of which index type is used is data -dependent. The -[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java) -is used for literal types. `AsciiType` and `UTF8Type` are literal -types by default but any column can be configured as a literal type -using the `is_literal` option at index creation time. For non-literal -types the -[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java) -is used. The -[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java) -is an implementation that can efficiently support prefix queries on -character-like data. The -[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java), -conversely, is better suited for other Cassandra data types like -numbers. - -The -[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java) -is built using either the `ConcurrentRadixTree` or -`ConcurrentSuffixTree` from the `com.goooglecode.concurrenttrees` -package. The choice between the two is made based on the indexing -mode, `PREFIX` or other modes, and `CONTAINS` mode, respectively. - -The -[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java) -is built on top of `java.util.concurrent.ConcurrentSkipListSet`. - -### Querying - -Responsible for converting the internal `IndexExpression` -representation into SASI's -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) -and -[`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java) -trees, optimizing the trees to reduce the amount of work done, and -driving the query itself, the -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -is the work horse of SASI's querying implementation. To efficiently -perform union and intersection operations, SASI provides several -iterators similar to Cassandra's `MergeIterator`, but tailored -specifically for SASI's use while including more features. The -[`RangeUnionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java), -like its name suggests, performs set unions over sets of tokens/keys -matching the query, only reading as much data as it needs from each -set to satisfy the query. The -[`RangeIntersectionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java), -similar to its counterpart, performs set intersections over its data. - -#### QueryPlan - -The -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -instantiated per search query is at the core of SASI's querying -implementation. Its work can be divided in two stages: analysis and -execution. - -During the analysis phase, -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -converts from Cassandra's internal representation of -`IndexExpression`s, which has also been modified to support encoding -queries that contain ORs and groupings of expressions using -parentheses (see the -[Cassandra Internal Changes](#cassandra-internal-changes) -section below for more details). This process produces a tree of -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s, which in turn may contain [`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s, all of which -provide an alternative, more efficient, representation of the query. - -During execution, the -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -uses the `DecoratedKey`-generating iterator created from the -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) tree. These keys are read from disk and a final check to -ensure they satisfy the query is made, once again using the -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) tree. At the point the desired amount of matching data has -been found, or there is no more matching data, the result set is -returned to the coordinator through the existing internal components. - -The number of queries (total/failed/timed-out), and their latencies, -are maintined per-table/column family. - -SASI also supports concurrently iterating terms for the same index -across SSTables. The concurrency factor is controlled by the -`cassandra.search_concurrency_factor` system property. The default is -`1`. - -##### QueryController - -Each -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -references a -[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java) -used throughout the execution phase. The -[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java) -has two responsibilities: to manage and ensure the proper cleanup of -resources (indexes), and to strictly enforce the time bound per query, -specified by the user via the range slice timeout. All indexes are -accessed via the -[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java) -so that they can be safely released by it later. The -[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java)'s -`checkpoint` function is called in specific places in the execution -path to ensure the time-bound is enforced. - -##### QueryPlan Optimizations - -While in the analysis phase, the -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -performs several potential optimizations to the query. The goal of -these optimizations is to reduce the amount of work performed during -the execution phase. - -The simplest optimization performed is compacting multiple expressions -joined by logical intersections (`AND`) into a single [`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) with -three or more [`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s. For example, the query `WHERE age < 100 AND -fname = 'p*' AND first_name != 'pa*' AND age > 21` would, -without modification, have the following tree: - - ┌───────┐ - ┌────────│ AND │──────┐ - │ └───────┘ │ - ▼ ▼ - ┌───────┐ ┌──────────┐ - ┌─────│ AND │─────┐ │age < 100 │ - │ └───────┘ │ └──────────┘ - ▼ ▼ - ┌──────────┐ ┌───────┐ - │ fname=p* │ ┌─│ AND │───┐ - └──────────┘ │ └───────┘ │ - ▼ ▼ - ┌──────────┐ ┌──────────┐ - │fname!=pa*│ │ age > 21 │ - └──────────┘ └──────────┘ - -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -will remove the redundant right branch whose root is the final `AND` -and has leaves `fname != pa*` and `age > 21`. These [`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s will -be compacted into the parent `AND`, a safe operation due to `AND` -being associative and commutative. The resulting tree looks like the -following: - - ┌───────┐ - ┌────────│ AND │──────┐ - │ └───────┘ │ - ▼ ▼ - ┌───────┐ ┌──────────┐ - ┌───────────│ AND │────────┐ │age < 100 │ - │ └───────┘ │ └──────────┘ - ▼ │ ▼ - ┌──────────┐ │ ┌──────────┐ - │ fname=p* │ ▼ │ age > 21 │ - └──────────┘ ┌──────────┐ └──────────┘ - │fname!=pa*│ - └──────────┘ - -When excluding results from the result set, using `!=`, the -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -determines the best method for handling it. For range queries, for -example, it may be optimal to divide the range into multiple parts -with a hole for the exclusion. For string queries, such as this one, -it is more optimal, however, to simply note which data to skip, or -exclude, while scanning the index. Following this optimization the -tree looks like this: - - ┌───────┐ - ┌────────│ AND │──────┐ - │ └───────┘ │ - ▼ ▼ - ┌───────┐ ┌──────────┐ - ┌───────│ AND │────────┐ │age < 100 │ - │ └───────┘ │ └──────────┘ - ▼ ▼ - ┌──────────────────┐ ┌──────────┐ - │ fname=p* │ │ age > 21 │ - │ exclusions=[pa*] │ └──────────┘ - └──────────────────┘ - -The last type of optimization applied, for this query, is to merge -range expressions across branches of the tree -- without modifying the -meaning of the query, of course. In this case, because the query -contains all `AND`s the `age` expressions can be collapsed. Along with -this optimization, the initial collapsing of unneeded `AND`s can also -be applied once more to result in this final tree using to execute the -query: - - ┌───────┐ - ┌──────│ AND │───────┐ - │ └───────┘ │ - ▼ ▼ - ┌──────────────────┐ ┌────────────────┐ - │ fname=p* │ │ 21 < age < 100 │ - │ exclusions=[pa*] │ └────────────────┘ - └──────────────────┘ - -#### Operations and Expressions - -As discussed, the -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -optimizes a tree represented by -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s -as interior nodes, and -[`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s -as leaves. The -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) -class, more specifically, can have zero, one, or two -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s -as children and an unlimited number of expressions. The iterators used -to perform the queries, discussed below in the -"Range(Union|Intersection)Iterator" section, implement the necessary -logic to merge results transparently regardless of the -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s -children. - -Besides participating in the optimizations performed by the -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java), -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) -is also responsible for taking a row that has been returned by the -query and performing a final validation that it in fact does match. This -`satisfiesBy` operation is performed recursively from the root of the -[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) -tree for a given query. These checks are performed directly on the -data in a given row. For more details on how `satisfiesBy` works, see -the documentation -[in the code](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java#L87-L123). - -#### Range(Union|Intersection)Iterator - -The abstract `RangeIterator` class provides a unified interface over -the two main operations performed by SASI at various layers in the -execution path: set intersection and union. These operations are -performed in a iterated, or "streaming", fashion to prevent unneeded -reads of elements from either set. In both the intersection and union -cases the algorithms take advantage of the data being pre-sorted using -the same sort order, e.g. term or token order. - -The -[`RangeUnionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java) -performs the "Merge-Join" portion of the -[Sort-Merge-Join](https://en.wikipedia.org/wiki/Sort-merge_join) -algorithm, with the properties of an outer-join, or union. It is -implemented with several optimizations to improve its performance over -a large number of iterators -- sets to union. Specifically, the -iterator exploits the likely case of the data having many sub-groups -of overlapping ranges and the unlikely case that all ranges will -overlap each other. For more details see the -[javadoc](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java#L9-L21). - -The -[`RangeIntersectionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java) -itself is not a subclass of `RangeIterator`. It is a container for -several classes, one of which, `AbstractIntersectionIterator`, -sub-classes `RangeIterator`. SASI supports two methods of performing -the intersection operation, and the ability to be adaptive in choosing -between them based on some properties of the data. - -`BounceIntersectionIterator`, and the `BOUNCE` strategy, works like -the -[`RangeUnionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java) -in that it performs a "Merge-Join", however, its nature is similar to -a inner-join, where like values are merged by a data-specific merge -function (e.g. merging two tokens in a list to lookup in a SSTable -later). See the -[javadoc](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L88-L101) -for more details on its implementation. - -`LookupIntersectionIterator`, and the `LOOKUP` strategy, performs a -different operation, more similar to a lookup in an associative data -structure, or "hash lookup" in database terminology. Once again, -details on the implementation can be found in the -[javadoc](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L199-L208). - -The choice between the two iterators, or the `ADAPTIVE` strategy, is -based upon the ratio of data set sizes of the minimum and maximum -range of the sets being intersected. If the number of the elements in -minimum range divided by the number of elements is the maximum range -is less than or equal to `0.01`, then the `ADAPTIVE` strategy chooses -the `LookupIntersectionIterator`, otherwise the -`BounceIntersectionIterator` is chosen. - -### The SASIIndex Class - -The above components are glued together by the -[`SASIIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java) -class which implements `Index`, and is instantiated -per-table containing SASI indexes. It manages all indexes for a table -via the -[`sasi.conf.DataTracker`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java) -and -[`sasi.conf.view.View`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/view/View.java) -components, controls writing of all indexes for an SSTable via its -[`PerSSTableIndexWriter`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java), and initiates searches with -`Searcher`. These classes glue the previously -mentioned indexing components together with Cassandra's SSTable -life-cycle ensuring indexes are not only written when Memtable's flush, -but also as SSTable's are compacted. For querying, the -`Searcher` does little but defer to -[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java) -and update e.g. latency metrics exposed by SASI. - -### Cassandra Internal Changes - -To support the above changes and integrate them into Cassandra a few -minor internal changes were made to Cassandra itself. These are -described here. - -#### SSTable Write Life-cycle Notifications - -The `SSTableFlushObserver` is an observer pattern-like interface, -whose sub-classes can register to be notified about events in the -life-cycle of writing out a SSTable. Sub-classes can be notified when a -flush begins and ends, as well as when each next row is about to be -written, and each next column. SASI's `PerSSTableIndexWriter`, -discussed above, is the only current subclass. - -### Limitations and Caveats - -The following are items that can be addressed in future updates but are not -available in this repository or are not currently implemented. - -* The cluster must be configured to use a partitioner that produces - `LongToken`s, e.g. `Murmur3Partitioner`. Other existing partitioners which - don't produce LongToken e.g. `ByteOrderedPartitioner` and `RandomPartitioner` - will not work with SASI. -* Not Equals and OR support have been removed in this release while - changes are made to Cassandra itself to support them. - -### Contributors - -* [Pavel Yaskevich](https://github.com/xedin) -* [Jordan West](https://github.com/jrwest) -* [Michael Kjellman](https://github.com/mkjellman) -* [Jason Brown](https://github.com/jasobrown) -* [Mikhail Stepura](https://github.com/mishail) diff --git a/doc/antora.yml b/doc/antora.yml deleted file mode 100644 index d01016f59340..000000000000 --- a/doc/antora.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Cassandra -version: '5.0' -display_version: '5.0' -asciidoc: - attributes: - cass_url: 'http://cassandra.apache.org/' - cass-50: 'Cassandra 5.0' - cassandra: 'Cassandra' - product: 'Apache Cassandra' - -nav: -- modules/ROOT/nav.adoc -- modules/cassandra/nav.adoc diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 959533f77186..7bc18722f951 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -501,7 +501,7 @@ h3(#createIndexStmt). CREATE INDEX __Syntax:__ bc(syntax).. - ::= CREATE ( CUSTOM )? INDEX ( IF NOT EXISTS )? ( )? + ::= CREATE ( CUSTOM )? INDEX ( IF NOT EXISTS )? ( )? ON '(' ')' ( USING ( WITH OPTIONS = )? )? @@ -721,6 +721,8 @@ bc(syntax).. '(' ( ',' )* ')' ( CALLED | RETURNS NULL ) ON NULL INPUT RETURNS + ( DETERMINISTIC )? + ( MONOTONIC ( ON )? )? LANGUAGE AS p. @@ -766,6 +768,10 @@ If the optional @IF NOT EXISTS@ keywords are used, the function will only be cre @OR REPLACE@ and @IF NOT EXIST@ cannot be used together. +The optional @DETERMINISTIC@ keyword specifies that the function is deterministic. This means that given a particular input, the function will always produce the same output. + +The optional @MONOTONIC@ keyword specifies that the function is monotonic. This means that it is either entirely nonincreasing or nondecreasing. Even if the function is not monotonic on all its arguments, it is possible to specify that it is monotonic @ON@ one of its arguments, meaning that partial applications of the function over that argument will be monotonic. Monotonicity is required to use the function in a @GROUP BY@ clause. + Functions belong to a keyspace. If no keyspace is specified in @@, the current keyspace is used (i.e. the keyspace specified using the "@USE@":#useStmt statement). It is not possible to create a user-defined function in one of the system keyspaces. See the section on "user-defined functions":#udfs for more information. @@ -806,6 +812,7 @@ bc(syntax).. STYPE ( FINALFUNC )? ( INITCOND )? + ( DETERMINISTIC )? p. __Sample:__ @@ -826,6 +833,8 @@ See the section on "user-defined aggregates":#udas for a complete example. @OR REPLACE@ and @IF NOT EXIST@ cannot be used together. +The optional @DETERMINISTIC@ keyword specifies that the aggregate function is deterministic. This means that given a particular input, the function will always produce the same output. + Aggregates belong to a keyspace. If no keyspace is specified in @@, the current keyspace is used (i.e. the keyspace specified using the "@USE@":#useStmt statement). It is not possible to create a user-defined aggregate in one of the system keyspaces. Signatures for user-defined aggregates follow the "same rules":#functionSignature as for user-defined functions. @@ -1092,8 +1101,9 @@ bc(syntax).. ( GROUP BY )? ( ORDER BY )? ( PER PARTITION LIMIT )? - ( LIMIT )? + ( LIMIT ( OFFSET )? )? ( ALLOW FILTERING )? + ( WITH )? ::= DISTINCT? @@ -1125,6 +1135,12 @@ bc(syntax).. ::= ( ',' )* ::= ( ASC | DESC )? ::= '(' (',' )* ')' + + ::= ( AND )* + ::= ann_options '=' + | included_indexes '=' + | excluded_indexes '=' + ::= '{' ( ',' )* '}' p. __Sample:__ @@ -1228,9 +1244,9 @@ Aggregate functions will produce a separate value for each group. If no @GROUP B If a column is selected without an aggregate function, in a statement with a @GROUP BY@, the first value encounter in each group will be returned. -h4(#selectLimit). @LIMIT@ and @PER PARTITION LIMIT@ +h4(#selectLimit). @LIMIT@, @OFFSET@ and @PER PARTITION LIMIT@ -The @LIMIT@ option to a @SELECT@ statement limits the number of rows returned by a query, while the @PER PARTITION LIMIT@ option limits the number of rows returned for a given partition by the query. Note that both type of limit can used in the same statement. +The @LIMIT@ option in a @SELECT@ statement limits the number of rows returned by a query. The @LIMIT@ option can include an @OFFSET@ option to skip the first rows of the query result. The @PER PARTITION LIMIT@ option limits the number of rows returned for a given partition by the query. Note that both type of limit can used in the same statement. h4(#selectAllowFiltering). @ALLOW FILTERING@ diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 3d367ad242a1..a484e704d21d 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -1,7 +1,7 @@ * xref:index.adoc[Main] ** xref:master@_:ROOT:glossary.adoc[Glossary] ** xref:master@_:ROOT:bugs.adoc[How to report bugs] -** xref:master@_:ROOT:contactus.adoc[Contact us] +** xref:master@_:ROOT:community.adoc[Contact us] ** xref:master@_:ROOT:development/index.adoc[Development] *** xref:master@_:ROOT:development/gettingstarted.adoc[Getting started] *** xref:master@_:ROOT:development/ide.adoc[Building and IDE integration] diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index beeb121f220e..f90812a58a66 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -47,7 +47,7 @@ If you would like to contribute to this documentation, you are welcome to do so == Meta information * xref:master@_:ROOT:bugs.adoc[Reporting bugs] -* xref:master@_:ROOT:contactus.adoc[Contact us] +* xref:master@_:ROOT:community.adoc[Contact us] * xref:master@_:ROOT:development/index.adoc[Contributing code] * xref:master@_:ROOT:docdev/index.adoc[Contributing to the docs] * xref:master@_:ROOT:community.adoc[Community] diff --git a/doc/modules/cassandra/assets/license_compliance.rst b/doc/modules/cassandra/assets/license_compliance.rst index e2eba2ab8256..eb3ee826c70a 100644 --- a/doc/modules/cassandra/assets/license_compliance.rst +++ b/doc/modules/cassandra/assets/license_compliance.rst @@ -30,7 +30,7 @@ The Apache Cassandra project enforces and verifies ASF License header conformanc With a few exceptions, source files consisting of works submitted directly to the ASF by the copyright owner or owner's agent must contain the appropriate ASF license header. Files without any degree of creativity don't require a license header. -Currently, RAT checks all .bat, .btm, .cql, .css, .g, .hmtl, .iml, .java, .jflex, .jks, .md, .mod, .name, .pom, .py, .sh, .spec, .textile, .yml, .yaml, .xml files for a LICENSE header. +Currently, RAT checks all .bat, .btm, .cql, .css, .g, .hmtl, .iml, .java, .jks, .md, .mod, .name, .pom, .py, .sh, .spec, .textile, .yml, .yaml, .xml files for a LICENSE header. If there is an incompliance, the build will fail with the following warning: diff --git a/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf b/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf index c0126a23ffd8..1207ec06328c 100644 --- a/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf +++ b/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf @@ -4,3 +4,4 @@ create_aggregate_statement ::= CREATE [ OR REPLACE ] AGGREGATE [ IF NOT EXISTS ] STYPE cql_type: [ FINALFUNC function_name] [ INITCOND term ] + [ DETERMINISTIC ] diff --git a/doc/modules/cassandra/examples/BNF/create_function_statement.bnf b/doc/modules/cassandra/examples/BNF/create_function_statement.bnf index 0da769a11fb0..82be39d42911 100644 --- a/doc/modules/cassandra/examples/BNF/create_function_statement.bnf +++ b/doc/modules/cassandra/examples/BNF/create_function_statement.bnf @@ -1,6 +1,8 @@ create_function_statement::= CREATE [ OR REPLACE ] FUNCTION [ IF NOT EXISTS] function_name '(' arguments_declaration ')' [ CALLED | RETURNS NULL ] ON NULL INPUT - RETURNS cql_type + RETURNS cql_type + [ DETERMINISTIC ] + [ MONOTONIC [ ON arg_name ] ] LANGUAGE identifier AS string arguments_declaration: identifier cql_type ( ',' identifier cql_type )* diff --git a/doc/modules/cassandra/examples/BNF/select_statement.bnf b/doc/modules/cassandra/examples/BNF/select_statement.bnf index f53da41da57c..d9906eea12f3 100644 --- a/doc/modules/cassandra/examples/BNF/select_statement.bnf +++ b/doc/modules/cassandra/examples/BNF/select_statement.bnf @@ -4,8 +4,9 @@ select_statement::= SELECT [ JSON | DISTINCT ] ( select_clause | '*' ) [ GROUP BY `group_by_clause` ] [ ORDER BY `ordering_clause` ] [ PER PARTITION LIMIT (`integer` | `bind_marker`) ] - [ LIMIT (`integer` | `bind_marker`) ] + [ LIMIT (`integer` | `bind_marker`) [ OFFSET (`integer` | `bind_marker`) ] ] [ ALLOW FILTERING ] + [ WITH `select_options` ] select_clause::= `selector` [ AS `identifier` ] ( ',' `selector` [ AS `identifier` ] ) selector::== `column_name` | `term` @@ -17,5 +18,10 @@ relation::= column_name operator term '(' column_name ( ',' column_name )* ')' operator tuple_literal TOKEN '(' column_name# ( ',' column_name )* ')' operator term operator::= '=' | '<' | '>' | '<=' | '>=' | '!=' | IN | CONTAINS | CONTAINS KEY -group_by_clause::= column_name ( ',' column_name )* +group_by_clause::= column_name ( ',' column_name )* ordering_clause::= column_name [ ASC | DESC ] ( ',' column_name [ ASC | DESC ] )* +select_options::= `select_option` ( AND `select_option` )* +select_option::= ann_options '=' + | included_indexes '=' `index_names` + | excluded_indexes '=' `index_names` +index_names::= '{' index_name ( ',' index_name )* '}' diff --git a/doc/modules/cassandra/examples/CQL/comments-table.cql b/doc/modules/cassandra/examples/CQL/comments-table.cql index 7ce38f5218a7..ac4dd0361ed2 100644 --- a/doc/modules/cassandra/examples/CQL/comments-table.cql +++ b/doc/modules/cassandra/examples/CQL/comments-table.cql @@ -25,7 +25,7 @@ DROP INDEX IF EXISTS cycling.fn_sparse; // tag::fn_sparse[] CREATE CUSTOM INDEX IF NOT EXISTS fn_sparse ON cycling.comments (created_at) -USING 'org.apache.cassandra.index.sasi.SASIIndex' +USING 'org.apache.cassandra.index.sai.StorageAttachedIndex' WITH OPTIONS = { 'mode': 'SPARSE' }; // end::fn_sparse[] diff --git a/doc/modules/cassandra/examples/CQL/query_with_ann_options.cql b/doc/modules/cassandra/examples/CQL/query_with_ann_options.cql new file mode 100644 index 000000000000..14ddcd47b938 --- /dev/null +++ b/doc/modules/cassandra/examples/CQL/query_with_ann_options.cql @@ -0,0 +1 @@ +SELECT * FROM embeddings ORDER BY vector ANN OF [1.2, 3.4] LIMIT 100 WITH ann_options = { 'rerank_k': 1000 } diff --git a/doc/modules/cassandra/examples/CQL/query_with_index_hints.cql b/doc/modules/cassandra/examples/CQL/query_with_index_hints.cql new file mode 100644 index 000000000000..99cdbc915943 --- /dev/null +++ b/doc/modules/cassandra/examples/CQL/query_with_index_hints.cql @@ -0,0 +1,7 @@ +CREATE INDEX birth_year_idx ON users (birth_year); +CREATE INDEX country_idx ON users (country); + +SELECT * FROM users + WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING + WITH included_indexes = {birth_year_idx} + AND excluded_indexes = {country_idx}; diff --git a/doc/modules/cassandra/nav.adoc b/doc/modules/cassandra/nav.adoc index 7c1a02cfaa94..a528f499f5ac 100644 --- a/doc/modules/cassandra/nav.adoc +++ b/doc/modules/cassandra/nav.adoc @@ -84,6 +84,7 @@ **** xref:cassandra:managing/configuration/cass_jvm_options_file.adoc[jvm-* files] **** xref:cassandra:managing/configuration/configuration.adoc[Liberating cassandra.yaml Parameters' Names from Their Units] *** xref:cassandra:managing/operating/index.adoc[] +**** xref:cassandra:managing/operating/auto_repair.adoc[Auto Repair] **** xref:cassandra:managing/operating/backups.adoc[Backups] **** xref:cassandra:managing/operating/bloom_filters.adoc[Bloom filters] **** xref:cassandra:managing/operating/bulk_loading.adoc[Bulk loading] @@ -108,7 +109,7 @@ **** xref:cassandra:managing/tools/cqlsh.adoc[cqlsh: the CQL shell] **** xref:cassandra:managing/tools/nodetool/nodetool.adoc[nodetool] **** xref:cassandra:managing/tools/sstable/index.adoc[SSTable tools] -**** xref:cassandra:managing/tools/cassandra_stress.adoc[cassandra-stress] +**** xref:cassandra:tooling/cassandra-stress.adoc[cassandra-stress] ** xref:cassandra:troubleshooting/index.adoc[Troubleshooting] *** xref:cassandra:troubleshooting/finding_nodes.adoc[Finding misbehaving nodes] @@ -118,11 +119,11 @@ ** xref:reference/index.adoc[] *** xref:reference/cql-commands/commands-toc.adoc[CQL commands] -*** xref:developing/cql/cql_singlefile.html[CQL specification] +*** xref:cassandra:developing/cql/cql_singlefile.adoc[CQL specification] *** xref:reference/java17.adoc[Java 17] *** xref:reference/native-protocol.adoc[Native Protocol specification] *** xref:reference/sai-virtual-table-indexes.adoc[SAI virtual table] *** xref:reference/static.adoc[Static columns] *** xref:reference/vector-data-type.adoc[Vector data type] -** xref:integrating/plugins/index.adoc[] \ No newline at end of file +** xref:integrating/plugins/index.adoc[] diff --git a/doc/modules/cassandra/pages/architecture/index.adoc b/doc/modules/cassandra/pages/architecture/index.adoc index 9e674d95a2bb..5b69a8c9f94b 100644 --- a/doc/modules/cassandra/pages/architecture/index.adoc +++ b/doc/modules/cassandra/pages/architecture/index.adoc @@ -6,4 +6,4 @@ This section describes the general architecture of Apache Cassandra. * xref:architecture/dynamo.adoc[Dynamo] * xref:architecture/storage-engine.adoc[Storage Engine] * xref:architecture/guarantees.adoc[Guarantees] -* xref:architecture/snitch.adoc[Snitches] +* xref:cassandra:managing/operating/snitch.adoc[Snitches] diff --git a/doc/modules/cassandra/pages/architecture/storage-engine.adoc b/doc/modules/cassandra/pages/architecture/storage-engine.adoc index 5580120a83ff..8a654ca791f7 100644 --- a/doc/modules/cassandra/pages/architecture/storage-engine.adoc +++ b/doc/modules/cassandra/pages/architecture/storage-engine.adoc @@ -30,7 +30,7 @@ Once the defined size is reached, a new commit log segment is created. Commit log segments can be archived, deleted, or recycled once all the data is flushed to https://cassandra.apache.org/_/glossary.html#sstable[SSTables]. Commit log segments are truncated when Cassandra has written data older than a certain point to the SSTables. -Running xref:managing:tools/nodetool/drain.adoc[`nodetool drain`] before stopping Cassandra will write everything in the memtables +Running xref:cassandra:managing/tools/nodetool/drain.adoc[`nodetool drain`] before stopping Cassandra will write everything in the memtables to SSTables and remove the need to sync with the commit logs on startup. * xref:cassandra:managing/configuration/cass_yaml_file.adoc#commitlog_segment_size [`commitlog_segment_size`]: The default size is 32MiB, which is almost always fine, but if you are archiving commitlog segments (see commitlog_archiving.properties), then you probably want a finer granularity of archiving; 8 or 16 MiB is reasonable. @@ -105,7 +105,7 @@ A partition index is also created on the disk that maps the tokens to a location The queue can be configured with either the xref:cassandra:managing/configuration/cass_yaml_file.adoc#memtable_heap_space[`memtable_heap_space`] or xref:cassandra:managing/configuration/cass_yaml_file.adoc#memtable_offheap_space[`memtable_offheap_space`] setting in the `cassandra.yaml` file. If the data to be flushed exceeds the `memtable_cleanup_threshold`, Cassandra blocks writes until the next flush succeeds. -You can manually flush a table using xref:managing:tools/nodetool/flush.adoc[`nodetool flush`] or `nodetool drain` (flushes memtables without listening for connections to other nodes). +You can manually flush a table using xref:cassandra:managing/tools/nodetool/flush.adoc[`nodetool flush`] or `nodetool drain` (flushes memtables without listening for connections to other nodes). To reduce the commit log replay time, the recommended best practice is to flush the memtable before you restart the nodes. If a node stops working, replaying the commit log restores writes to the memtable that were there before it stopped. diff --git a/doc/modules/cassandra/pages/developing/cql/SASI.adoc b/doc/modules/cassandra/pages/developing/cql/SASI.adoc deleted file mode 100644 index 93d87f8ff385..000000000000 --- a/doc/modules/cassandra/pages/developing/cql/SASI.adoc +++ /dev/null @@ -1,809 +0,0 @@ -= SASI Index - -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java[`SASIIndex`], -or ``SASI`` for short, is an implementation of Cassandra's `Index` -interface that can be used as an alternative to the existing -implementations. SASI's indexing and querying improves on existing -implementations by tailoring it specifically to Cassandra's needs. SASI -has superior performance in cases where queries would previously require -filtering. In achieving this performance, SASI aims to be significantly -less resource intensive than existing implementations, in memory, disk, -and CPU usage. In addition, SASI supports prefix and contains queries on -strings (similar to SQL's ``LIKE = "foo\*"`` or ``LIKE = "*foo*"`` ). - -The following goes on describe how to get up and running with SASI, -demonstrates usage with examples, and provides some details on its -implementation. - -=== Using SASI - -The examples below walk through creating a table and indexes on its -columns, and performing queries on some inserted data. - -The examples below assume the `demo` keyspace has been created and is in -use. - -.... -cqlsh> CREATE KEYSPACE demo WITH replication = { - ... 'class': 'SimpleStrategy', - ... 'replication_factor': '1' - ... }; -cqlsh> USE demo; -.... - -All examples are performed on the `sasi` table: - -.... -cqlsh:demo> CREATE TABLE sasi (id uuid, first_name text, last_name text, - ... age int, height int, created_at bigint, primary key (id)); -.... - -==== Creating Indexes - -To create SASI indexes use CQLs `CREATE CUSTOM INDEX` statement: - -.... -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (first_name) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = { - ... 'analyzer_class': - ... 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer', - ... 'case_sensitive': 'false' - ... }; - -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (last_name) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = {'mode': 'CONTAINS'}; - -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (age) USING 'org.apache.cassandra.index.sasi.SASIIndex'; - -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (created_at) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = {'mode': 'SPARSE'}; -.... - -The indexes created have some options specified that customize their -behaviour and potentially performance. The index on `first_name` is -case-insensitive. The analyzers are discussed more in a subsequent -example. The `NonTokenizingAnalyzer` performs no analysis on the text. -Each index has a mode: `PREFIX`, `CONTAINS`, or `SPARSE`, the first -being the default. The `last_name` index is created with the mode -`CONTAINS` which matches terms on suffixes instead of prefix only. -Examples of this are available below and more detail can be found in the -section on link:#ondiskindexbuilder[OnDiskIndex].The `created_at` column -is created with its mode set to `SPARSE`, which is meant to improve -performance of querying large, dense number ranges like timestamps for -data inserted every millisecond. Details of the `SPARSE` implementation -can also be found in the section on the -link:#ondiskindexbuilder[OnDiskIndex]. The `age` index is created with -the default `PREFIX` mode and no case-sensitivity or text analysis -options are specified since the field is numeric. - -After inserting the following data and performing a `nodetool flush`, -SASI performing index flushes to disk can be seen in Cassandra's logs – -although the direct call to flush is not required (see -link:#indexmemtable[IndexMemtable] for more details). - -.... -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (556ebd54-cbe5-4b75-9aae-bf2a31a24500, 'Pavel', 'Yaskevich', 27, 181, 1442959315018); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (5770382a-c56f-4f3f-b755-450e24d55217, 'Jordan', 'West', 26, 173, 1442959315019); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (96053844-45c3-4f15-b1b7-b02c441d3ee1, 'Mikhail', 'Stepura', 36, 173, 1442959315020); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (f5dfcabe-de96-4148-9b80-a1c41ed276b4, 'Michael', 'Kjellman', 26, 180, 1442959315021); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (2970da43-e070-41a8-8bcb-35df7a0e608a, 'Johnny', 'Zhang', 32, 175, 1442959315022); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (6b757016-631d-4fdb-ac62-40b127ccfbc7, 'Jason', 'Brown', 40, 182, 1442959315023); - -cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at) - ... VALUES (8f909e8a-008e-49dd-8d43-1b0df348ed44, 'Vijay', 'Parthasarathy', 34, 183, 1442959315024); - -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi; - - first_name | last_name | age | height | created_at -------------+---------------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - Mikhail | Stepura | 36 | 173 | 1442959315020 - Jason | Brown | 40 | 182 | 1442959315023 - Pavel | Yaskevich | 27 | 181 | 1442959315018 - Vijay | Parthasarathy | 34 | 183 | 1442959315024 - Jordan | West | 26 | 173 | 1442959315019 - Johnny | Zhang | 32 | 175 | 1442959315022 - -(7 rows) -.... - -==== Equality & Prefix Queries - -SASI supports all queries already supported by CQL, including LIKE -statement for PREFIX, CONTAINS and SUFFIX searches. - -.... -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name = 'Pavel'; - - first_name | last_name | age | height | created_at --------------+-----------+-----+--------+--------------- - Pavel | Yaskevich | 27 | 181 | 1442959315018 - -(1 rows) -.... - -.... -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name = 'pavel'; - - first_name | last_name | age | height | created_at --------------+-----------+-----+--------+--------------- - Pavel | Yaskevich | 27 | 181 | 1442959315018 - -(1 rows) -.... - -.... -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name LIKE 'M%'; - - first_name | last_name | age | height | created_at -------------+-----------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - Mikhail | Stepura | 36 | 173 | 1442959315020 - -(2 rows) -.... - -Of course, the case of the query does not matter for the `first_name` -column because of the options provided at index creation time. - -.... -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name LIKE 'm%'; - - first_name | last_name | age | height | created_at -------------+-----------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - Mikhail | Stepura | 36 | 173 | 1442959315020 - -(2 rows) -.... - -==== Compound Queries - -SASI supports queries with multiple predicates, however, due to the -nature of the default indexing implementation, CQL requires the user to -specify `ALLOW FILTERING` to opt-in to the potential performance -pitfalls of such a query. With SASI, while the requirement to include -`ALLOW FILTERING` remains, to reduce modifications to the grammar, the -performance pitfalls do not exist because filtering is not performed. -Details on how SASI joins data from multiple predicates is available -below in the link:#implementation-details[Implementation Details] -section. - -.... -cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi - ... WHERE first_name LIKE 'M%' and age < 30 ALLOW FILTERING; - - first_name | last_name | age | height | created_at -------------+-----------+-----+--------+--------------- - Michael | Kjellman | 26 | 180 | 1442959315021 - -(1 rows) -.... - -==== Suffix Queries - -The next example demonstrates `CONTAINS` mode on the `last_name` column. -By using this mode, predicates can search for any strings containing the -search string as a sub-string. In this case the strings containing ``a'' -or ``an''. - -.... -cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%a%'; - - id | age | created_at | first_name | height | last_name ---------------------------------------+-----+---------------+------------+--------+--------------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman - 96053844-45c3-4f15-b1b7-b02c441d3ee1 | 36 | 1442959315020 | Mikhail | 173 | Stepura - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | 1442959315018 | Pavel | 181 | Yaskevich - 8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | 1442959315024 | Vijay | 183 | Parthasarathy - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang - -(5 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%an%'; - - id | age | created_at | first_name | height | last_name ---------------------------------------+-----+---------------+------------+--------+----------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang - -(2 rows) -.... - -==== Expressions on Non-Indexed Columns - -SASI also supports filtering on non-indexed columns like `height`. The -expression can only narrow down an existing query using `AND`. - -.... -cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%a%' AND height >= 175 ALLOW FILTERING; - - id | age | created_at | first_name | height | last_name ---------------------------------------+-----+---------------+------------+--------+--------------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | 1442959315018 | Pavel | 181 | Yaskevich - 8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | 1442959315024 | Vijay | 183 | Parthasarathy - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang - -(4 rows) -.... - -==== Delimiter based Tokenization Analysis - -A simple text analysis provided is delimiter based tokenization. This -provides an alternative to indexing collections, as delimiter separated -text can be indexed without the overhead of `CONTAINS` mode nor using -`PREFIX` or `SUFFIX` queries. - -.... -cqlsh:demo> ALTER TABLE sasi ADD aliases text; -cqlsh:demo> CREATE CUSTOM INDEX on sasi (aliases) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = { - ... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer', - ... 'delimiter': ',', - ... 'mode': 'prefix', - ... 'analyzed': 'true'}; -cqlsh:demo> UPDATE sasi SET aliases = 'Mike,Mick,Mikey,Mickey' WHERE id = f5dfcabe-de96-4148-9b80-a1c41ed276b4; -cqlsh:demo> SELECT * FROM sasi WHERE aliases LIKE 'Mikey' ALLOW FILTERING; - - id | age | aliases | created_at | first_name | height | last_name ---------------------------------------+-----+------------------------+---------------+------------+--------+----------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | Mike,Mick,Mikey,Mickey | 1442959315021 | Michael | 180 | Kjellman -.... - -==== Text Analysis (Tokenization and Stemming) - -Lastly, to demonstrate text analysis an additional column is needed on -the table. Its definition, index, and statements to update rows are -shown below. - -.... -cqlsh:demo> ALTER TABLE sasi ADD bio text; -cqlsh:demo> CREATE CUSTOM INDEX ON sasi (bio) USING 'org.apache.cassandra.index.sasi.SASIIndex' - ... WITH OPTIONS = { - ... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer', - ... 'tokenization_enable_stemming': 'true', - ... 'analyzed': 'true', - ... 'tokenization_normalize_lowercase': 'true', - ... 'tokenization_locale': 'en' - ... }; -cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, who likes distributed systems, doesnt like to argue.' WHERE id = 5770382a-c56f-4f3f-b755-450e24d55217; -cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, works on the freight distribution at nights and likes arguing' WHERE id = 556ebd54-cbe5-4b75-9aae-bf2a31a24500; -cqlsh:demo> SELECT * FROM sasi; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+--------------- - f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | null | 1442959315021 | Michael | 180 | Kjellman - 96053844-45c3-4f15-b1b7-b02c441d3ee1 | 36 | null | 1442959315020 | Mikhail | 173 | Stepura - 6b757016-631d-4fdb-ac62-40b127ccfbc7 | 40 | null | 1442959315023 | Jason | 182 | Brown - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | null | 1442959315024 | Vijay | 183 | Parthasarathy - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - 2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | null | 1442959315022 | Johnny | 175 | Zhang - -(7 rows) -.... - -Index terms and query search strings are stemmed for the `bio` column -because it was configured to use the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java[`StandardAnalyzer`] -and `analyzed` is set to `true`. The `tokenization_normalize_lowercase` -is similar to the `case_sensitive` property but for the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java[`StandardAnalyzer`]. -These query demonstrates the stemming applied by -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java[`StandardAnalyzer`]. - -.... -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'distributing'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - -(2 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'they argued'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - -(2 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'working at the company'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - -(1 rows) - -cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'soft eng'; - - id | age | bio | created_at | first_name | height | last_name ---------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+----------- - 556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich - 5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West - -(2 rows) -.... - -=== Implementation Details - -While SASI, at the surface, is simply an implementation of the `Index` -interface, at its core there are several data structures and algorithms -used to satisfy it. These are described here. Additionally, the changes -internal to Cassandra to support SASI's integration are described. - -The `Index` interface divides responsibility of the implementer into two -parts: Indexing and Querying. Further, Cassandra makes it possible to -divide those responsibilities into the memory and disk components. SASI -takes advantage of Cassandra's write-once, immutable, ordered data model -to build indexes along with the flushing of the memtable to disk – this -is the origin of the name `SSTable Attached Secondary Index`. - -The SASI index data structures are built in memory as the SSTable is -being written and they are flushed to disk before the writing of the -SSTable completes. The writing of each index file only requires -sequential writes to disk. In some cases, partial flushes are performed, -and later stitched back together, to reduce memory usage. These data -structures are optimized for this use case. - -Taking advantage of Cassandra's ordered data model, at query time, -candidate indexes are narrowed down for searching, minimizing the amount -of work done. Searching is then performed using an efficient method that -streams data off disk as needed. - -==== Indexing - -Per SSTable, SASI writes an index file for each indexed column. The data -for these files is built in memory using the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java[`OnDiskIndexBuilder`]. -Once flushed to disk, the data is read using the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`] -class. These are composed of bytes representing indexed terms, organized -for efficient writing or searching respectively. The keys and values -they hold represent tokens and positions in an SSTable and these are -stored per-indexed term in -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java[`TokenTreeBuilder`]s -for writing, and -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s -for querying. These index files are memory mapped after being written to -disk, for quicker access. For indexing data in the memtable, SASI uses -its -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java[`IndexMemtable`] -class. - -===== OnDiskIndex(Builder) - -Each -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`] -is an instance of a modified -https://en.wikipedia.org/wiki/Suffix_array[Suffix Array] data structure. -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`] -is comprised of page-size blocks of sorted terms and pointers to the -terms' associated data, as well as the data itself, stored also in one -or more page-sized blocks. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`] -is structured as a tree of arrays, where each level describes the terms -in the level below, the final level being the terms themselves. The -``PointerLevel``s and their ``PointerBlock``s contain terms and pointers to -other blocks that _end_ with those terms. The `DataLevel`, the final -level, and its ``DataBlock``s contain terms and point to the data itself, -contained in -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s. - -The terms written to the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`] -vary depending on its `mode` : either `PREFIX`, `CONTAINS`, or -`SPARSE`. In the `PREFIX` and `SPARSE` cases, terms' exact values are -written exactly once per `OnDiskIndex`. For example, when using a -`PREFIX` index with terms `Jason`, `Jordan`, `Pavel`, all three will be -included in the index. A `CONTAINS` index writes additional terms for -each suffix of each term recursively. Continuing with the example, a -`CONTAINS` index storing the previous terms would also store `ason`, -`ordan`, `avel`, `son`, `rdan`, `vel`, etc. This allows for queries on -the suffix of strings. The `SPARSE` mode differs from `PREFIX` in that -for every 64 blocks of terms a -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`] -is built merging all the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s -for each term into a single one. This copy of the data is used for -efficient iteration of large ranges of e.g. timestamps. The index -`mode` is configurable per column at index creation time. - -===== TokenTree(Builder) - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`] -is an implementation of the well-known -https://en.wikipedia.org/wiki/B%2B_tree[B+ tree] that has been modified -to optimize for its use-case. In particular, it has been optimized to -associate tokens, longs, with a set of positions in an SSTable, also -longs. Allowing the set of long values accommodates the possibility of a -hash collision in the token, but the data structure is optimized for the -unlikely possibility of such a collision. - -To optimize for its write-once environment the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java[`TokenTreeBuilder`] -completely loads its interior nodes as the tree is built and it uses the -well-known algorithm optimized for bulk-loading the data structure. - -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s -provide the means to iterate over tokens, and file positions, that match -a given term, and to skip forward in that iteration, an operation used -heavily at query time. - -===== IndexMemtable - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java[`IndexMemtable`] -handles indexing the in-memory data held in the memtable. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java[`IndexMemtable`] -in turn manages either a -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`] -or a -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`] -per-column. The choice of which index type is used is data dependent. -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`] -is used for literal types. `AsciiType` and `UTF8Type` are literal types -by default but any column can be configured as a literal type using the -`is_literal` option at index creation time. For non-literal types the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`] -is used. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`] -is an implementation that can efficiently support prefix queries on -character-like data. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`], -conversely, is better suited for other Cassandra data types like -numbers. - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`] -is built using either the `ConcurrentRadixTree` or -`ConcurrentSuffixTree` from the `com.goooglecode.concurrenttrees` -package. The choice between the two is made based on the indexing mode, -`PREFIX` or other modes, and `CONTAINS` mode, respectively. - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`] -is built on top of `java.util.concurrent.ConcurrentSkipListSet`. - -==== Querying - -Responsible for converting the internal `IndexExpression` representation -into SASI's -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -and -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`] -trees, optimizing the trees to reduce the amount of work done, and -driving the query itself, the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -is the work horse of SASI's querying implementation. To efficiently -perform union and intersection operations, SASI provides several -iterators similar to Cassandra's `MergeIterator`, but tailored -specifically for SASI's use while including more features. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java[`RangeUnionIterator`], -like its name suggests, performs set unions over sets of tokens/keys -matching the query, only reading as much data as it needs from each set -to satisfy the query. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java[`RangeIntersectionIterator`], -similar to its counterpart, performs set intersections over its data. - -===== QueryPlan - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -instantiated per search query is at the core of SASI's querying -implementation. Its work can be divided in two stages: analysis and -execution. - -During the analysis phase, -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -converts from Cassandra's internal representation of ``IndexExpression``s, -which has also been modified to support encoding queries that contain -ORs and groupings of expressions using parentheses (see the -link:#cassandra-internal-changes[Cassandra Internal Changes] section -below for more details). This process produces a tree of -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s, -which in turn may contain -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s, -all of which provide an alternative, more efficient, representation of -the query. - -During execution, the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -uses the `DecoratedKey`-generating iterator created from the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -tree. These keys are read from disk and a final check to ensure they -satisfy the query is made, once again using the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -tree. At the point the desired amount of matching data has been found, -or there is no more matching data, the result set is returned to the -coordinator through the existing internal components. - -The number of queries (total/failed/timed-out), and their latencies, are -maintined per-table/column family. - -SASI also supports concurrently iterating terms for the same index -across SSTables. The concurrency factor is controlled by the -`cassandra.search_concurrency_factor` system property. The default is -`1`. - -====== QueryController - -Each -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -references a -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`] -used throughout the execution phase. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`] -has two responsibilities: to manage and ensure the proper cleanup of -resources (indexes), and to strictly enforce the time bound per query, -specified by the user via the range slice timeout. All indexes are -accessed via the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`] -so that they can be safely released by it later. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`]'s -`checkpoint` function is called in specific places in the execution path -to ensure the time-bound is enforced. - -====== QueryPlan Optimizations - -While in the analysis phase, the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -performs several potential optimizations to the query. The goal of these -optimizations is to reduce the amount of work performed during the -execution phase. - -The simplest optimization performed is compacting multiple expressions -joined by logical intersections (`AND`) into a single -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -with three or more -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s. -For example, the query -`WHERE age < 100 AND fname = 'p*' AND first_name != 'pa*' AND age > 21` -would, without modification, have the following tree: - -.... - ┌───────┐ - ┌────────│ AND │──────┐ - │ └───────┘ │ - ▼ ▼ - ┌───────┐ ┌──────────┐ - ┌─────│ AND │─────┐ │age < 100 │ - │ └───────┘ │ └──────────┘ - ▼ ▼ -┌──────────┐ ┌───────┐ -│ fname=p* │ ┌─│ AND │───┐ -└──────────┘ │ └───────┘ │ - ▼ ▼ - ┌──────────┐ ┌──────────┐ - │fname!=pa*│ │ age > 21 │ - └──────────┘ └──────────┘ -.... - -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -will remove the redundant right branch whose root is the final `AND` and -has leaves `fname != pa*` and `age > 21`. These -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s -will be compacted into the parent `AND`, a safe operation due to `AND` -being associative and commutative. The resulting tree looks like the -following: - -.... - ┌───────┐ - ┌────────│ AND │──────┐ - │ └───────┘ │ - ▼ ▼ - ┌───────┐ ┌──────────┐ - ┌───────────│ AND │────────┐ │age < 100 │ - │ └───────┘ │ └──────────┘ - ▼ │ ▼ -┌──────────┐ │ ┌──────────┐ -│ fname=p* │ ▼ │ age > 21 │ -└──────────┘ ┌──────────┐ └──────────┘ - │fname!=pa*│ - └──────────┘ -.... - -When excluding results from the result set, using `!=`, the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -determines the best method for handling it. For range queries, for -example, it may be optimal to divide the range into multiple parts with -a hole for the exclusion. For string queries, such as this one, it is -more optimal, however, to simply note which data to skip, or exclude, -while scanning the index. Following this optimization the tree looks -like this: - -.... - ┌───────┐ - ┌────────│ AND │──────┐ - │ └───────┘ │ - ▼ ▼ - ┌───────┐ ┌──────────┐ - ┌───────│ AND │────────┐ │age < 100 │ - │ └───────┘ │ └──────────┘ - ▼ ▼ - ┌──────────────────┐ ┌──────────┐ - │ fname=p* │ │ age > 21 │ - │ exclusions=[pa*] │ └──────────┘ - └──────────────────┘ -.... - -The last type of optimization applied, for this query, is to merge range -expressions across branches of the tree – without modifying the meaning -of the query, of course. In this case, because the query contains all -``AND``s the `age` expressions can be collapsed. Along with this -optimization, the initial collapsing of unneeded ``AND``s can also be -applied once more to result in this final tree using to execute the -query: - -.... - ┌───────┐ - ┌──────│ AND │───────┐ - │ └───────┘ │ - ▼ ▼ - ┌──────────────────┐ ┌────────────────┐ - │ fname=p* │ │ 21 < age < 100 │ - │ exclusions=[pa*] │ └────────────────┘ - └──────────────────┘ -.... - -===== Operations and Expressions - -As discussed, the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -optimizes a tree represented by -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s -as interior nodes, and -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s -as leaves. The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -class, more specifically, can have zero, one, or two -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s -as children and an unlimited number of expressions. The iterators used -to perform the queries, discussed below in the -`Range(Union|Intersection)Iterator` section, implement the necessary -logic to merge results transparently regardless of the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s -children. - -Besides participating in the optimizations performed by the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`], -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -is also responsible for taking a row that has been returned by the query -and performing a final validation that it in fact does match. This -`satisfiesBy` operation is performed recursively from the root of the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`] -tree for a given query. These checks are performed directly on the data -in a given row. For more details on how `satisfiesBy` works, see the -documentation -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java#L87-L123[in -the code]. - -===== Range(Union|Intersection)Iterator - -The abstract `RangeIterator` class provides a unified interface over the -two main operations performed by SASI at various layers in the execution -path: set intersection and union. These operations are performed in a -iterated, or `streaming`, fashion to prevent unneeded reads of -elements from either set. In both the intersection and union cases the -algorithms take advantage of the data being pre-sorted using the same -sort order, e.g. term or token order. - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java[`RangeUnionIterator`] -performs the `Merge-Join` portion of the -https://en.wikipedia.org/wiki/Sort-merge_join[Sort-Merge-Join] -algorithm, with the properties of an outer-join, or union. It is -implemented with several optimizations to improve its performance over a -large number of iterators – sets to union. Specifically, the iterator -exploits the likely case of the data having many sub-groups of -overlapping ranges and the unlikely case that all ranges will overlap -each other. For more details see the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java#L9-L21[javadoc]. - -The -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java[`RangeIntersectionIterator`] -itself is not a subclass of `RangeIterator`. It is a container for -several classes, one of which, `AbstractIntersectionIterator`, -sub-classes `RangeIterator`. SASI supports two methods of performing the -intersection operation, and the ability to be adaptive in choosing -between them based on some properties of the data. - -`BounceIntersectionIterator`, and the `BOUNCE` strategy, works like the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java[`RangeUnionIterator`] -in that it performs a `Merge-Join`, however, its nature is similar to -a inner-join, where like values are merged by a data-specific merge -function (e.g. merging two tokens in a list to lookup in a SSTable -later). See the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L88-L101[javadoc] -for more details on its implementation. - -`LookupIntersectionIterator`, and the `LOOKUP` strategy, performs a -different operation, more similar to a lookup in an associative data -structure, or `hash lookup` in database terminology. Once again, -details on the implementation can be found in the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L199-L208[javadoc]. - -The choice between the two iterators, or the `ADAPTIVE` strategy, is -based upon the ratio of data set sizes of the minimum and maximum range -of the sets being intersected. If the number of the elements in minimum -range divided by the number of elements is the maximum range is less -than or equal to `0.01`, then the `ADAPTIVE` strategy chooses the -`LookupIntersectionIterator`, otherwise the `BounceIntersectionIterator` -is chosen. - -==== The SASIIndex Class - -The above components are glued together by the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java[`SASIIndex`] -class which implements `Index`, and is instantiated per-table containing -SASI indexes. It manages all indexes for a table via the -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java[`sasi.conf.DataTracker`] -and -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/view/View.java[`sasi.conf.view.View`] -components, controls writing of all indexes for an SSTable via its -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java[`PerSSTableIndexWriter`], -and initiates searches with `Searcher`. These classes glue the -previously mentioned indexing components together with Cassandra's -SSTable life-cycle ensuring indexes are not only written when Memtable's -flush, but also as SSTable's are compacted. For querying, the `Searcher` -does little but defer to -https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`] -and update e.g. latency metrics exposed by SASI. - -==== Cassandra Internal Changes - -To support the above changes and integrate them into Cassandra a few -minor internal changes were made to Cassandra itself. These are -described here. - -===== SSTable Write Life-cycle Notifications - -The `SSTableFlushObserver` is an observer pattern-like interface, whose -sub-classes can register to be notified about events in the life-cycle -of writing out a SSTable. Sub-classes can be notified when a flush -begins and ends, as well as when each next row is about to be written, -and each next column. SASI's `PerSSTableIndexWriter`, discussed above, -is the only current subclass. - -==== Limitations and Caveats - -The following are items that can be addressed in future updates but are -not available in this repository or are not currently implemented. - -* The cluster must be configured to use a partitioner that produces -``LongToken``s, e.g. `Murmur3Partitioner`. Other existing partitioners -which don't produce LongToken e.g. `ByteOrderedPartitioner` and -`RandomPartitioner` will not work with SASI. -* Not Equals and OR support have been removed in this release while -changes are made to Cassandra itself to support them. - -==== Contributors - -* https://github.com/xedin[Pavel Yaskevich] -* https://github.com/jrwest[Jordan West] -* https://github.com/mkjellman[Michael Kjellman] -* https://github.com/jasobrown[Jason Brown] -* https://github.com/mishail[Mikhail Stepura] diff --git a/doc/modules/cassandra/pages/developing/cql/batch/batch-good-example.adoc b/doc/modules/cassandra/pages/developing/cql/batch/batch-good-example.adoc index 93887253569b..86c248b3b328 100644 --- a/doc/modules/cassandra/pages/developing/cql/batch/batch-good-example.adoc +++ b/doc/modules/cassandra/pages/developing/cql/batch/batch-good-example.adoc @@ -24,7 +24,7 @@ All the `INSERT` and `UPDATE` statements in this batch write to the same partiti include::cassandra:example$CQL/cyclist_expenses-table.cql[tag=batch_Vera] ---- + -This batching example includes conditional updates combined with using xref:reference:static.adoc[static columns]. +This batching example includes conditional updates combined with using xref:cassandra:developing/cql/ddl.adoc#static-column[static columns]. Recall that single partition batches are not logged. + [NOTE] diff --git a/doc/modules/cassandra/pages/developing/cql/collections/list.adoc b/doc/modules/cassandra/pages/developing/cql/collections/list.adoc index 8113b5aa267e..6be7c9938f46 100644 --- a/doc/modules/cassandra/pages/developing/cql/collections/list.adoc +++ b/doc/modules/cassandra/pages/developing/cql/collections/list.adoc @@ -9,7 +9,7 @@ Use the `list` data type to store data that has a possible many-to-many relation == Prerequisite -* xref:developing/cql/keyspace-check.adoc[Keyspace] must exist +* xref:cassandra:developing/cql/ddl.adoc#create-keyspace-statement[Keyspace] must exist In the following example, a `list` called `events` stores all the race events on an upcoming calendar. The table is called `upcoming_calendar`. Each event listed in the `list` will have a `text` data type. diff --git a/doc/modules/cassandra/pages/developing/cql/collections/map.adoc b/doc/modules/cassandra/pages/developing/cql/collections/map.adoc index f3ce072bf258..148b8b569733 100644 --- a/doc/modules/cassandra/pages/developing/cql/collections/map.adoc +++ b/doc/modules/cassandra/pages/developing/cql/collections/map.adoc @@ -11,7 +11,7 @@ Each element can have an individual time-to-live and expire when the TTL ends. == Prerequisite -* xref:developing/cql/keyspace-check.adoc[Keyspace] must exist +* xref:cassandra:developing/cql/ddl.adoc#create-keyspace-statement[Keyspace] must exist In the following example, each team listed in the `map` called `teams` will have a `year` of integer type and a `team name` of text type. The table is named `cyclist_teams`. diff --git a/doc/modules/cassandra/pages/developing/cql/collections/set.adoc b/doc/modules/cassandra/pages/developing/cql/collections/set.adoc index 4834c8f21a1f..db5d85f4e4d6 100644 --- a/doc/modules/cassandra/pages/developing/cql/collections/set.adoc +++ b/doc/modules/cassandra/pages/developing/cql/collections/set.adoc @@ -8,7 +8,7 @@ Use the `set` data type to store data that has a many-to-one relationship with a == Prerequisite -* xref:developing/cql/keyspace-check.adoc[Keyspace] must exist +* xref:cassandra:developing/cql/ddl.adoc#create-keyspace-statement[Keyspace] must exist In the following example, a `set` called `teams` stores all the teams that a cyclist has been a member of during their career. The table is `cyclist_career_teams`. diff --git a/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc b/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc index 4de16fbb713e..7cef8f5a6543 100644 --- a/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc +++ b/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc @@ -1227,6 +1227,8 @@ CREATE FUNCTION akeyspace.fname IF NOT EXISTS ( someArg int ) CALLED ON NULL INPUT RETURNS text +( DETERMINISTIC )? +( MONOTONIC ( ON )? )? LANGUAGE java AS $$ // some Java code @@ -1266,6 +1268,17 @@ exist. `OR REPLACE` and `IF NOT EXIST` cannot be used together. +The optional `DETERMINISTIC` keyword specifies that the function is +deterministic. This means that given a particular input, the function +will always produce the same output. + +The optional `MONOTONIC` keyword specifies that the function is monotonic. +This means that it is either entirely nonincreasing or nondecreasing. +Even if the function is not monotonic on all its arguments, it is possible +to specify that it is monotonic `ON` one of its arguments, meaning that +partial applications of the function over that argument will be monotonic. +Monotonicity is required to use the function in a `GROUP BY` clause. + Functions belong to a keyspace. If no keyspace is specified in ``, the current keyspace is used (i.e. the keyspace specified using the link:#useStmt[`USE`] statement). It is not possible @@ -1318,6 +1331,7 @@ SFUNC STYPE ( FINALFUNC )? ( INITCOND )? +( DETERMINISTIC )? ---- _Sample:_ @@ -1346,6 +1360,10 @@ creates an aggregate if it does not already exist. `OR REPLACE` and `IF NOT EXIST` cannot be used together. +The optional `DETERMINISTIC` keyword specifies that the aggregate +function is deterministic. This means that given a particular input, +the function will always produce the same output. + Aggregates belong to a keyspace. If no keyspace is specified in ``, the current keyspace is used (i.e. the keyspace specified using the link:#useStmt[`USE`] statement). It is not possible @@ -1756,8 +1774,9 @@ FROM ( GROUP BY )? ( ORDER BY )? ( PER PARTITION LIMIT )? -( LIMIT )? +( LIMIT ( OFFSET )? )? + ( ALLOW FILTERING )? +( WITH ann_options = )? ::= DISTINCT? @@ -1985,12 +2004,12 @@ with a `GROUP BY`, the first value encounter in each group will be returned. [[selectLimit]] -===== `LIMIT` and `PER PARTITION LIMIT` +===== `LIMIT`, `OFFSET` and `PER PARTITION LIMIT` -The `LIMIT` option to a `SELECT` statement limits the number of rows -returned by a query, while the `PER PARTITION LIMIT` option limits the -number of rows returned for a given partition by the query. Note that -both type of limit can used in the same statement. +The `LIMIT` option in a `SELECT` statement limits the number of rows returned by a query. +The `LIMIT` option can include an `OFFSET` option to skip the first rows of the query result. +The `PER PARTITION LIMIT` option limits the number of rows returned for a given partition by the query. +Note that both types of limits can used in the same statement. [[selectAllowFiltering]] ===== `ALLOW FILTERING` diff --git a/doc/modules/cassandra/pages/developing/cql/create-custom-index.adoc b/doc/modules/cassandra/pages/developing/cql/create-custom-index.adoc index 29fcfbc1bd27..5d91c31dc420 100644 --- a/doc/modules/cassandra/pages/developing/cql/create-custom-index.adoc +++ b/doc/modules/cassandra/pages/developing/cql/create-custom-index.adoc @@ -5,7 +5,7 @@ include::cassandra:partial$sai/support-databases.adoc[] Creates a Storage-Attached Indexing (SAI) index. You can create multiple secondary indexes on the same database table, with each SAI index based on any column in the table. -All column date types except the following are supported for SAI indexes: +All column data types except the following are supported for SAI indexes: * `counter` * geospatial types: `PointType`, `LineStringType`, `PolygonType` @@ -55,7 +55,7 @@ SAI allows only alphanumeric characters and underscores in names. SAI returns `InvalidRequestException` if you try to define an index on a column name that contains other characters, and does not create the index. map_name:: -Used with xref:cassandra:developing/collections/collection-create.adoc[collections], identifier of the `map_name` specified in `CREATE TABLE` ... +Used with xref:cassandra:developing/cql/collections/collection-create.adoc[collections], identifier of the `map_name` specified in `CREATE TABLE` ... `map()`. The regular column syntax applies for collection types `list` and `set`. @@ -119,7 +119,7 @@ Also refer xref:cassandra:developing/cql/indexing/sai/sai-query.adoc[Examine SAI === SAI collection map examples with keys, values, and entries The following examples demonstrate using collection maps of multiple types (`keys`, `values`, `entries`) in SAI indexes. -For related information, see xref:cassandra:developing/collections/collection-create.adoc[Creating collections] and xref:cassandra:developing/collections/map.adoc[Using map type]. +For related information, see xref:cassandra:developing/cql/collections/collection-create.adoc[Creating collections] and xref:cassandra:developing/cql/collections/map.adoc[Using map type]. Also refer to the SAI collection examples of type xref:#saiCreateCustomIndexCollectionsListAndSetExamples[list and set] in this topic. @@ -293,9 +293,9 @@ Remember that in CQL queries using SAI indexes, the `CONTAINS` clauses are suppo These examples demonstrate using collections with the `list` and `set` types in SAI indexes. For related information, see: -* xref:cassandra:developing/collections/collection-create.adoc[Creating collections] -* xref:cassandra:developing/collections/list.adoc[Using list type] -* xref:cassandra:developing/collections/set.adoc[Using set type] +* xref:cassandra:developing/cql/collections/collection-create.adoc[Creating collections] +* xref:cassandra:developing/cql/collections/list.adoc[Using list type] +* xref:cassandra:developing/cql/collections/set.adoc[Using set type] [source,language-cql] ---- diff --git a/doc/modules/cassandra/pages/developing/cql/create-index.adoc b/doc/modules/cassandra/pages/developing/cql/create-index.adoc index 88721030f052..d3114d6610e1 100644 --- a/doc/modules/cassandra/pages/developing/cql/create-index.adoc +++ b/doc/modules/cassandra/pages/developing/cql/create-index.adoc @@ -8,7 +8,7 @@ After an index has been created, it is automatically updated when data in the co Indexing via this `CREATE INDEX` command can impact performance. Before creating an index, be aware of when and xref:cassandra:developing/cql/indexing/2i/2i-when-to-use.adoc#when-no-index[when not to create an index]. -Use xref:cassandra:developing/cql/indexing/create-custom-index.adoc[CREATE CUSTOM INDEX] for Storage-Attached Indexes (SAI). +Use xref:cassandra:developing/cql/create-custom-index.adoc[CREATE CUSTOM INDEX] for Storage-Attached Indexes (SAI). *Restriction:* Indexing counter columns is not supported. For maps, index the key, value, or entries. @@ -90,7 +90,7 @@ To index map keys, use the `KEYS` keyword and map name in nested parentheses: include::cassandra:example$CQL/sai/cyclist_teams-table.cql[tag=keysidx] ---- -To query the table, you can use xref:cassandra:reference/cql-commands/select.adoc#filtering-on-collections[CONTAINS KEY] in `WHERE` clauses. +To query the table, you can use xref:cassandra:developing/cql/dml.adoc#allow-filtering[CONTAINS KEY] in `WHERE` clauses. [source,language-cql] ---- diff --git a/doc/modules/cassandra/pages/developing/cql/ddl.adoc b/doc/modules/cassandra/pages/developing/cql/ddl.adoc index d771f4e2bcb2..93d9f5c1ed06 100644 --- a/doc/modules/cassandra/pages/developing/cql/ddl.adoc +++ b/doc/modules/cassandra/pages/developing/cql/ddl.adoc @@ -166,7 +166,7 @@ will result in: include::cassandra:example$RESULTS/autoexpand_exclude_dc.result[] ---- -If xref:new/transientreplication.adoc[transient replication] has been enabled, transient replicas can be +If xref:cassandra:managing/operating/transientreplication.adoc[transient replication] has been enabled, transient replicas can be configured for both `SimpleStrategy` and `NetworkTopologyStrategy` by defining replication factors in the format `'/'` diff --git a/doc/modules/cassandra/pages/developing/cql/definitions.adoc b/doc/modules/cassandra/pages/developing/cql/definitions.adoc index 3e0251cf4c52..64536cec6565 100644 --- a/doc/modules/cassandra/pages/developing/cql/definitions.adoc +++ b/doc/modules/cassandra/pages/developing/cql/definitions.adoc @@ -119,7 +119,7 @@ include::cassandra:example$BNF/term.bnf[] A term is thus one of: -* A xref:cassandra:developing/cql/defintions.adoc#constants[constant] +* A xref:cassandra:developing/cql/definitions.adoc#constants[constant] * A literal for either a xref:cassandra:developing/cql/types.adoc#collections[collection], a xref:cassandra:developing/cql/types.adoc#vectors[vector], a xref:cassandra:developing/cql/types.adoc#udts[user-defined type] or a xref:cassandra:developing/cql/types.adoc#tuples[tuple] * A xref:cassandra:developing/cql/functions.adoc#cql-functions[function] call, either a xref:cassandra:developing/cql/functions.adoc#scalar-native-functions[native function] diff --git a/doc/modules/cassandra/pages/developing/cql/dml.adoc b/doc/modules/cassandra/pages/developing/cql/dml.adoc index 674ede814518..3696f90b24b0 100644 --- a/doc/modules/cassandra/pages/developing/cql/dml.adoc +++ b/doc/modules/cassandra/pages/developing/cql/dml.adoc @@ -214,9 +214,10 @@ or the reverse [[limit-clause]] === Limiting results -The `LIMIT` option to a `SELECT` statement limits the number of rows -returned by a query. The `PER PARTITION LIMIT` option limits the -number of rows returned for a given partition by the query. Both types of limits can used in the same statement. +The `LIMIT` option in a `SELECT` statement limits the number of rows returned by a query. +The `LIMIT` option can include an `OFFSET` option to skip the first rows of the query result. +The `PER PARTITION LIMIT` option limits the number of rows returned for a given partition by the query. +Note that both types of limits can used in the same statement. [[allow-filtering]] === Allowing filtering @@ -264,6 +265,38 @@ execute: include::cassandra:example$CQL/query_nofail_allow_filtering.cql[] ---- +[[ann-options]] +=== ANN options + +`SELECT` queries using `ANN` ordering can provide a set of options to control the behavior of the ANN search: + +[source,cql] +---- +include::example$CQL/query_with_ann_options.cql[] +---- + +[[index-hints]] +=== Index hints + +`SELECT` statements allow to provide sets of included and excluded indexes: + +[source,cql] +---- +include::example$CQL/query_with_index_hints.cql[] +---- +The included indexes are indexes that should be used by the query. +Queries will fail if it's not possible to use the included indexes. +That might happen because the query doesn't have a restriction for those indexes, +or because there is a restriction that can use the index, +but it is not compatible with other restrictions, +or because the underlying index implementation isn't able to use the index for whatever reason. + +The excluded indexes are indexes that should not be used by the query. +Excluded indexes will never fail the query unless they reference a non-existent index, +since it's always possible to exclude indexes that are not used by the query. + +The indexes mentioned in included or excluded sets must exist, otherwise the query will fail. + [[insert-statement]] == INSERT diff --git a/doc/modules/cassandra/pages/developing/cql/functions.adoc b/doc/modules/cassandra/pages/developing/cql/functions.adoc index 9599b98a2434..d82a0f0e884e 100644 --- a/doc/modules/cassandra/pages/developing/cql/functions.adoc +++ b/doc/modules/cassandra/pages/developing/cql/functions.adoc @@ -288,6 +288,43 @@ A number of functions allow to obtain the similarity score between vectors of fl include::cassandra:partial$vector-search/vector_functions.adoc[] +[[index-functions]] +===== Index functions + +====== `sai_analyze` + +The `sai_analyze` functions returns the tokens that a SAI index will generate for a certain text value. The arguments +are that text value and the JSON configuration of the SAI analyzer. This JSON configuration is the same as the one used +to create the SAI index. For example, this function call: + +[source,cql] +---- +sai_analyze('johnny apples seedlings', + '{ + "tokenizer": {"name": "whitespace"} + }') +---- +Will return `['johnny', 'apples', 'seedlings']` + +This other function call: +[source,cql] +---- +sai_analyze('johnny apples seedlings', + '{ + "tokenizer": {"name": "whitespace"}, + "filters": [{"name": "porterstem"}] + }') +---- +Will return `['johnni', 'appl', 'seedl']` + + +[[vector-functions]] +===== Vector functions + +A number of functions to operate with vectors of floats. + +include::cassandra:partial$vector-search/vector_functions.adoc[] + [[user-defined-scalar-functions]] === User-defined functions @@ -378,6 +415,16 @@ If the optional `IF NOT EXISTS` keywords are used, the function will only be cre exist. `OR REPLACE` and `IF NOT EXISTS` cannot be used together. +The optional `DETERMINISTIC` keyword specifies that the aggregate function is deterministic. +This means that given a particular input, the function will always produce the same output. + +The optional `MONOTONIC` keyword specifies that the function is monotonic. +This means that it is either entirely nonincreasing or nondecreasing. +Even if the function is not monotonic on all its arguments, it is possible +to specify that it is monotonic `ON` one of its arguments, meaning that +partial applications of the function over that argument will be monotonic. +Monotonicity is required to use the function in a `GROUP BY` clause. + Behavior for `null` input values must be defined for each function: * `RETURNS NULL ON NULL INPUT` declares that the function will always return `null` if any of the input arguments is `null`. @@ -540,6 +587,9 @@ A `CREATE AGGREGATE` without `OR REPLACE` fails if an aggregate with the same si The `CREATE AGGREGATE` command with the optional `IF NOT EXISTS` keywords creates an aggregate if it does not already exist. The `OR REPLACE` and `IF NOT EXISTS` phrases cannot be used together. +The optional `DETERMINISTIC` keyword specifies that the aggregate function is deterministic. +This means that given a particular input, the function will always produce the same output. + The `STYPE` value defines the type of the state value and must be specified. The optional `INITCOND` defines the initial state value for the aggregate; the default value is `null`. A non-null `INITCOND` must be specified for state functions that are declared with `RETURNS NULL ON NULL INPUT`. diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/2i/2i-working-with.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/2i/2i-working-with.adoc index 09fd5a638c7d..27cdafd6f202 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/2i/2i-working-with.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/2i/2i-working-with.adoc @@ -4,8 +4,8 @@ == Prerequisites -* xref:developing/keyspace-create.adoc[Keyspace created] -* xref:developing/table-create.adoc[Table created] +* xref:cassandra:developing/cql/ddl.adoc#create-keyspace-statement[Keyspace created] +* xref:cassandra:developing/cql/ddl.adoc#create-table-statement[Table created] include::_2i-create.adoc[leveloffset=+1] diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/2i/operations/2i-build.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/2i/operations/2i-build.adoc index 80201b133834..37caa0977542 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/2i/operations/2i-build.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/2i/operations/2i-build.adoc @@ -7,4 +7,4 @@ Indexes are built in the background automatically, without blocking reads or wri Client-maintained _tables as indexes_ must be created manually; for example, if the artists column had been indexed by creating a table such as `songs_by_artist`, your client application would have to populate the table with data from the songs table. -To perform a hot rebuild of an index, use the xref:cassandra:tools/nodetool/rebuild_index.adoc[nodetool rebuild_index] command. +To perform a hot rebuild of an index, use the xref:cassandra:managing/tools/nodetool/rebuild_index.adoc[nodetool rebuild_index] command. diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc index d7a5c708f8cc..45e9362837d0 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc @@ -1,7 +1,9 @@ = Create SAI index :description: Create SAI index for CQL table schema using `cqlsh`. -To create an SAI index, you must define the index name, table name, and column name for the column to be indexed. +To create an SAI index, you must define the index name, table name, and column name for the column to be indexed. + +include::cassandra:partial$index-naming.adoc[] To create a simple SAI index: @@ -74,4 +76,4 @@ include::cassandra:example$CQL/sai/index-sai-similarity-function.cql[] ''' Other resources -See xref:developing/cql/indexing/create-custom-index.adoc[CREATE CUSTOM INDEX] for more information about creating SAI indexes. \ No newline at end of file +See xref:cassandra:developing/cql/create-custom-index.adoc[CREATE CUSTOM INDEX] for more information about creating SAI indexes. \ No newline at end of file diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/sai/operations/configuring.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/sai/operations/configuring.adoc index 0acd114746cd..7293c099abcd 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/sai/operations/configuring.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/sai/operations/configuring.adoc @@ -3,8 +3,9 @@ // LLP: *NOT DONE* -Configuring your {product} environment for Storage-Attached Indexing (SAI) may require some customization of the `cassandra.yaml` file. +Configuring your {product} environment for Storage-Attached Indexing (SAI) may require some customization of the `cassandra.yaml` file. +[[saiConfigure__saiCompactionStrategies]] == Compaction strategies Read queries perform better with compaction strategies that produce fewer SSTables. @@ -20,13 +21,13 @@ While in a time window, TWCS compacts all SSTables flushed from memory into larg At the end of the time window, all of these SSTables are compacted into a single SSTable. Then the next time window starts and the process repeats. The duration of the time window is the only setting required. -See xref:reference:cql-commands/create-table.adoc#compactSubprop__TWCS[TimeWindowCompactionStrategy]. +See xref:cassandra:reference/cql-commands/compact-subproperties.adoc#TWCS[TimeWindowCompactionStrategy]. For more information about TWCS, see xref:cassandra:managing/operating/compaction/twcs.adoc[Time Window Compaction Strategy]. In general, do not use `LeveledCompactionStrategy` (LCS) unless your index queries restrict the token range, either directly or by providing a restriction on the partition key. However, if you decide to use LCS, use the following guidelines: -* The `160` MB default for the `CREATE TABLE` command's `sstable_size_in_mb` option, described in this xref:reference:cql-commands/create-table.adoc#compactSubprop__LCS[topic], may result in suboptimal performance for index queries that do not restrict on token range or partition key. +* The `160` MB default for the `CREATE TABLE` command's `sstable_size_in_mb` option, described in this xref:cassandra:reference/cql-commands/compact-subproperties.adoc#LCS[topic], may result in suboptimal performance for index queries that do not restrict on token range or partition key. * While even higher values may be appropriate, depending on your hardware, we recommend at least doubling the default value of `sstable_size_in_mb`. Example: diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-faq.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-faq.adoc index 95f6783a25f6..e91a17b08647 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-faq.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-faq.adoc @@ -314,7 +314,7 @@ the post filtering on `col2` is carried out on the replicas themselves. Another case were post-filtering comes into play is when constructing a query that involves more than two SAI indexes. Refer to this xref:#saiAndQueriesFaq[related FAQ] about `AND` queries. -== Can I create an SAI index based on a xref:reference:static.adoc[static column]? +== Can I create an SAI index based on a xref:cassandra:developing/cql/ddl.adoc#static-column[static column]? Yes. For example, consider a `transaction_by_customer` table where you have a primary key `customer_id`, plus static columns to contain each customer's `address`, `phone_number`, and `date_of_birth`. diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-read-write-paths.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-read-write-paths.adoc index e735a9d210c9..4a34688d853e 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-read-write-paths.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-read-write-paths.adoc @@ -27,7 +27,7 @@ SAI calculates an estimate of the incremental heap consumption of the new entry. This estimate counts against the heap usage of the underlying Memtable. This feature also means that as more columns are indexed on a table, the Memtable flush rate will increase, and the size of flushed SSTables will decrease. The number of total writes and the estimated heap usage of all live Memtable indexes are exposed as metrics. -See xref:cassandra:developing/cql/indexing/sai/monitoring.adoc#saiMonitorMetrics[SAI metrics]. +See xref:cassandra:developing/cql/indexing/sai/operations/monitoring.adoc#saiMonitorMetrics[SAI metrics]. === Memtable flush @@ -94,7 +94,7 @@ image::sai/saiOnDiskStructureWithOffsets.png[alt=SAI on-disk layout as described The actual segment flushing process is very similar to a Memtable flush. However, buffered terms are sorted before they can be written with their postings to their respective type-specific on-disk structures. At the end of compaction for a given index, a special empty marker file is flagged to indicate success, and the number of segments is recorded in SAI metrics. -See xref:developing:indexing/sai/monitoring.adoc#saiGlobalIndexingMetrics[Global indexing metrics]. +See xref:cassandra:developing/cql/indexing/sai/operations/monitoring.adoc#saiGlobalIndexingMetrics[Global indexing metrics]. When the entire compaction task finishes, SAI receives an SSTable List Changed Notification that contains the SSTables added and removed during the transaction. SSTable Context Manager and Index View Manager are responsible for replacing old SSTable indexes with new ones atomically. diff --git a/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-working-with.adoc b/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-working-with.adoc index 98d8e2653a3e..6117786dbe43 100644 --- a/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-working-with.adoc +++ b/doc/modules/cassandra/pages/developing/cql/indexing/sai/sai-working-with.adoc @@ -4,8 +4,8 @@ == Prerequisites -* xref:developing/keyspace-create.adoc[Keyspace created] -* xref:developing/table-create.adoc[Table created] +* xref:cassandra:developing/cql/ddl.adoc#create-keyspace-statement[Keyspace created] +* xref:cassandra:developing/cql/ddl.adoc#create-table-statement[Table created] include::_sai-create.adoc[leveloffset=+1] diff --git a/doc/modules/cassandra/pages/developing/cql/security.adoc b/doc/modules/cassandra/pages/developing/cql/security.adoc index 0af30a9d1541..b94ae3379c44 100644 --- a/doc/modules/cassandra/pages/developing/cql/security.adoc +++ b/doc/modules/cassandra/pages/developing/cql/security.adoc @@ -167,11 +167,11 @@ used and the role does not exist the statement is a no-op. DROP ROLE intentionally does not terminate any open user sessions. Currently connected sessions will remain connected and will retain the ability to perform any database actions which do not require -xref:cassandra:developing/cql/security.adoc#authorization[authorization]. +xref:cassandra:managing/operating/security.adoc#authorization[authorization]. However, if authorization is enabled, xref:cassandra:developing/cql/security.adoc#cql-permissions[permissions] of the dropped role are also revoked, -subject to the xref:cassandra:developing/cql/security.adoc#auth-caching[caching options] configured in xref:cassandra:developing/cql/configuring.adoc#cassandra.yaml[cassandra-yaml] file. -Should a dropped role be subsequently recreated and have new xref:security.adoc#grant-permission-statement[permissions] or -xref:security.adoc#grant-role-statement[roles] granted to it, any client sessions still +subject to the xref:cassandra:managing/operating/security.adoc#auth-caching[caching options] configured in xref:cassandra:managing/configuration/cass_yaml_file.adoc[cassandra-yaml] file. +Should a dropped role be subsequently recreated and have new xref:cassandra:developing/cql/security.adoc#grant-permission-statement[permissions] or +xref:cassandra:developing/cql/security.adoc#grant-role-statement[roles] granted to it, any client sessions still connected will acquire the newly granted permissions and roles. ==== @@ -332,7 +332,7 @@ Existing users can be listed using the `LIST USERS` statement: include::cassandra:example$BNF/list_users_statement.bnf[] ---- -Note that this statement is equivalent to xref:security.adoc#list-roles-statement[LIST ROLES], but only roles with the `LOGIN` privilege are included in the output. +Note that this statement is equivalent to xref:cassandra:developing/cql/security.adoc#list-roles-statement[LIST ROLES], but only roles with the `LOGIN` privilege are included in the output. == Data Control diff --git a/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_logical.adoc b/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_logical.adoc index 80ddf3b6f0d6..ba91fae929d7 100644 --- a/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_logical.adoc +++ b/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_logical.adoc @@ -34,7 +34,7 @@ informative way to visualize the relationships between queries and tables in your designs. This figure shows the Chebotko notation for a logical data model. -image::cassandra:developing/data-modeling/data_modeling_chebotko_logical.png[image] +image::data_modeling_chebotko_logical.png[image] Each table is shown with its title and a list of columns. Primary key columns are identified via symbols such as *K* for partition key columns @@ -51,7 +51,7 @@ dedicated tables for rooms or amenities, as you had in the relational design. This is because the workflow didn't identify any queries requiring this direct access. -image::cassandra:developing/data-modeling/data_modeling_hotel_logical.png[image] +image::data_modeling_hotel_logical.png[image] Let's explore the details of each of these tables. @@ -127,7 +127,7 @@ shows a logical data model for reservations. You'll notice that these tables represent a denormalized design; the same data appears in multiple tables, with differing keys. -image::cassandra:developing/data-modeling/data_modeling_reservation_logical.png[image] +image::data_modeling_reservation_logical.png[image] In order to satisfy Q6, the `reservations_by_guest` table can be used to look up the reservation by guest name. You could envision query Q7 being diff --git a/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_queries.adoc b/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_queries.adoc index b33e91e05e4f..88f47758c746 100644 --- a/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_queries.adoc +++ b/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_queries.adoc @@ -28,7 +28,7 @@ here, however, you'll want to think not only from the customer perspective in terms of how the data is written, but also in terms of how the data will be queried by downstream use cases. -You natural tendency as might be to focus first on designing the tables +Your natural tendency might be to focus first on designing the tables to store reservation and guest records, and only then start thinking about the queries that would access them. You may have felt a similar tension already when discussing the shopping queries before, thinking diff --git a/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_schema.adoc b/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_schema.adoc index 68c0cfcea708..cf788933c011 100644 --- a/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_schema.adoc +++ b/doc/modules/cassandra/pages/developing/data-modeling/data-modeling_schema.adoc @@ -114,12 +114,11 @@ CREATE TABLE reservation.guests ( first_name text, last_name text, title text, - emails set, - phone_numbers list, - addresses map, - confirm_number text ) - WITH comment = 'Q9. Find guest by ID'; + emails set, + phone_numbers list, + addresses map>, + confirm_number text +) WITH comment = 'Q9. Find guest by ID'; ---- You now have a complete Cassandra schema for storing data for a hotel diff --git a/doc/modules/cassandra/pages/getting-started/production.adoc b/doc/modules/cassandra/pages/getting-started/production.adoc index ad28a35f5638..475c1f7fb8f1 100644 --- a/doc/modules/cassandra/pages/getting-started/production.adoc +++ b/doc/modules/cassandra/pages/getting-started/production.adoc @@ -118,7 +118,7 @@ https://thelastpickle.com/blog/2018/08/08/compression_performance.html[The Last == Compaction -There are different xref:compaction/index.adoc[compaction] strategies available +There are different xref:cassandra:managing/operating/compaction/index.adoc[compaction] strategies available for different workloads. We recommend reading about the different strategies to understand which is the best for your environment. diff --git a/doc/modules/cassandra/pages/getting-started/querying.adoc b/doc/modules/cassandra/pages/getting-started/querying.adoc index 78866cef0e69..2e1fe2361bea 100644 --- a/doc/modules/cassandra/pages/getting-started/querying.adoc +++ b/doc/modules/cassandra/pages/getting-started/querying.adoc @@ -22,7 +22,7 @@ include::cassandra:example$BASH/cqlsh_localhost.sh[] ---- include::cassandra:example$RESULTS/cqlsh_localhost.result[] ---- -If the command is used without specifying a node, `localhost` is the default. See the xref:tools/cqlsh.adoc[`cqlsh` section] for full documentation. +If the command is used without specifying a node, `localhost` is the default. See the xref:cassandra:managing/tools/cqlsh.adoc[`cqlsh` section] for full documentation. == Client drivers diff --git a/doc/modules/cassandra/pages/getting-started/sai-quickstart.adoc b/doc/modules/cassandra/pages/getting-started/sai-quickstart.adoc index 12cb6092e69f..16cd3e66ff3e 100644 --- a/doc/modules/cassandra/pages/getting-started/sai-quickstart.adoc +++ b/doc/modules/cassandra/pages/getting-started/sai-quickstart.adoc @@ -40,6 +40,8 @@ Use xref:reference/cql-commands/create-custom-index.adoc[CREATE CUSTOM INDEX] co include::cassandra:example$CQL/sai/cyclist_semi_pro_sai_indices.cql[tag=createQuickStartIndices] ---- +include::cassandra:partial$index-naming.adoc[] + Let's take a look at the description of the table and its indexes: [tabs] ==== diff --git a/doc/modules/cassandra/pages/installing/installing.adoc b/doc/modules/cassandra/pages/installing/installing.adoc index e5043914e00c..7ee741ea214f 100644 --- a/doc/modules/cassandra/pages/installing/installing.adoc +++ b/doc/modules/cassandra/pages/installing/installing.adoc @@ -279,4 +279,4 @@ include::cassandra:partial$nodetool_and_cqlsh_nobin.adoc[] == Further installation info -For help with installation issues, see the xref:cassandra:troubleshooting/index.html[Troubleshooting] section. +For help with installation issues, see the xref:cassandra:troubleshooting/index.adoc[Troubleshooting] section. diff --git a/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc b/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc index 309b15b17dd0..b1bfb9639305 100644 --- a/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc +++ b/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc @@ -31,11 +31,6 @@ In a multi-instance deployment, multiple Cassandra instances will independently assume that all CPU processors are available to it. This setting allows you to specify a smaller set of processors. -== `cassandra.boot_without_jna=true` - -If JNA fails to initialize, Cassandra fails to boot. Use this command to -boot Cassandra without JNA. - == `cassandra.config=` The directory location of the `cassandra.yaml file`. The default diff --git a/doc/modules/cassandra/pages/managing/operating/audit_logging.adoc b/doc/modules/cassandra/pages/managing/operating/audit_logging.adoc index a40fcc5b2e43..023a8e07fd54 100644 --- a/doc/modules/cassandra/pages/managing/operating/audit_logging.adoc +++ b/doc/modules/cassandra/pages/managing/operating/audit_logging.adoc @@ -150,8 +150,12 @@ auditlogviewer [...] [options] waiting for more records `-r,--roll_cycle`:: How often to roll the log file was rolled. May be;; - necessary for Chronicle to correctly parse file names. (MINUTELY, - HOURLY, DAILY). Default HOURLY. + necessary for Chronicle to correctly parse file names. Some available options are: +FIVE_MINUTELY, FAST_HOURLY, FAST_DAILY, LargeRollCycles.LARGE_DAILY, LargeRollCycles.XLARGE_DAILY, +LargeRollCycles.HUGE_DAILY. Deprecated ones still availble but not recommended for new deployments: +MINUTELY, HOURLY, DAILY +For more options, refer: net.openhft.chronicle.queue.RollCycles. +Default is set to FAST_HOURLY `-h,--help`:: display this help message diff --git a/doc/modules/cassandra/pages/managing/operating/auto_repair.adoc b/doc/modules/cassandra/pages/managing/operating/auto_repair.adoc new file mode 100644 index 000000000000..7971263b5027 --- /dev/null +++ b/doc/modules/cassandra/pages/managing/operating/auto_repair.adoc @@ -0,0 +1,473 @@ += Auto Repair +:navtitle: Auto Repair +:description: Auto Repair concepts - How it works, how to configure it, and more. +:keywords: CEP-37, Repair, Incremental, Preview + +Auto Repair is a fully automated scheduler that provides repair orchestration within Apache Cassandra. This +significantly reduces operational overhead by eliminating the need for operators to deploy external tools to submit and +manage repairs. + +At a high level, a dedicated thread pool is assigned to the repair scheduler. The repair scheduler in Cassandra +maintains a new replicated table, `system_distributed.auto_repair_history`, which stores the repair history for all +nodes, including details such as the last repair time. The scheduler selects the node(s) to begin repairs and +orchestrates the process to ensure that every table and its token ranges are repaired. + +The algorithm can run repairs simultaneously on multiple nodes and splits token ranges into subranges, with necessary +retries to handle transient failures. Automatic repair starts as soon as a Cassandra cluster is launched, similar to +compaction, and if configured appropriately, does not require human intervention. + +The scheduler currently supports Full, Incremental, and Preview repair types with the following features. New repair +types, such as Paxos repair or other future repair mechanisms, can be integrated with minimal development effort! + + +== Features +- Capability to run repairs on multiple nodes simultaneously. +- A default implementation and an interface to override the dataset being repaired per session. +- Extendable token split algorithms with two implementations readily available: +. Splits token ranges by placing a cap on the size of data repaired in one session and a maximum cap at the schedule +level using xref:#repair-token-range-splitter[RepairTokenRangeSplitter] (default). +. Splits tokens evenly based on the specified number of splits using +xref:#fixed-split-token-range-splitter[FixedSplitTokenRangeSplitter]. +- A new xref:#table-configuration[CQL table property] (`auto_repair`) offering: +. The ability to disable specific repair types at the table level, allowing the scheduler to skip one or more tables. +. Configuring repair priorities for certain tables to prioritize them over others. +- Dynamic enablement or disablement of the scheduler for each repair type. +- Configurable settings tailored to each repair job. +- Rich configuration options for each repair type (e.g., Full, Incremental, or Preview repairs). +- Comprehensive observability features that allow operators to configure alarms as needed. + +== Availability + +Auto Repair was introduced in Cassandra 6.0 via CEP-37 and backported to 5.0.8. + +In 5.0.8, auto-repair requires enabling the JVM property `-Dcassandra.autorepair.enable=true` before starting the +node. This property creates the required schema elements (the `auto_repair` column in `system_schema.tables` and +`system_schema.views`, and the `auto_repair_history` and `auto_repair_priority` tables in `system_distributed`). +After enabling this property, auto-repair scheduling still needs to be enabled either in `cassandra.yaml` under +the `auto_repair` section or at runtime via JMX. + +WARNING: The `cassandra.autorepair.enable` property is non-reversible. Once enabled, it cannot be disabled. +See the xref:#upgrading[Upgrading] section in NEWS.txt for details. + +== Considerations + +Before enabling Auto Repair, please consult the xref:managing/operating/repair.adoc[Repair] guide to establish a base +understanding of repairs. + +=== Full Repair + +Full Repairs operate over all data in the token range being repaired. It is therefore important to run full repair +with a longer schedule and with smaller assignments. + +=== Incremental Repair + +When enabled from the inception of a cluster, incremental repairs operate over unrepaired data and should finish +quickly when run more frequently. + +Once incremental repair has been run, SSTables will be separated between data that have been incrementally repaired +and data that have not. Therefore, it is important to continually run incremental repair once it has been enabled so +newly written data can be compacted together with previously repaired data, allowing overwritten and expired data to +be eventually purged. + +Running incremental repair more frequently keeps the unrepaired set smaller and thus causes repairs to operate over +a smaller set of data, so a shorter `min_repair_interval` such as `1h` is recommended for new clusters. + +==== Enabling Incremental Repair on existing clusters with a large amount of data +[#enabling-ir] +One should be careful when enabling incremental repair on a cluster for the first time. While +xref:#repair-token-range-splitter[RepairTokenRangeSplitter] includes a default configuration to attempt to gracefully +migrate to incremental repair over time, failure to take proper precaution could overwhelm the cluster with +xref:managing/operating/compaction/overview.adoc#types-of-compaction[anticompactions]. + +No matter how one goes about enabling and running incremental repair, it is recommended to run a cycle of full repairs +for the entire cluster as pre-flight step to running incremental repair. This will put the cluster into a more +consistent state which will reduce the amount of streaming between replicas when incremental repair initially runs. + +If you do not have strong data consistency requirements, one may consider using +xref:managing/tools/sstable/sstablerepairedset.adoc[nodetool sstablerepairedset] to mark all SSTables as repaired +before enabling incremental repair scheduling using Auto Repair. This will reduce the burden of initially running +incremental repair because all existing data will be considered as repaired, so subsequent incremental repairs will +only run against new data. + +If you do have strong data consistency requirements, then one must treat all data as initially unrepaired and run +incremental repair against it. Consult +xref:#incremental-repair-defaults[RepairTokenRangeSplitter's Incremental repair defaults]. + +In particular one should be mindful of the xref:managing/operating/compaction/overview.adoc[compaction strategy] +you use for your tables and how it might impact incremental repair before running incremental repair for the first +time: + +- *Large SSTables*: When using xref:managing/operating/compaction/stcs.adoc[SizeTieredCompactionStrategy] or any + compaction strategy which can create large SSTables including many partitions the amount of + xref:managing/operating/compaction/overview.adoc#types-of-compaction[anticompaction] that might be required could be + excessive. Using a small `bytes_per_assignment` might contribute to repeated anticompactions over the same + unrepaired data. +- *Partitions overlapping many SSTables*: If partitions overlap between many SSTables, the amount of SSTables included + in a repair might be large. Therefore it is important to consider that many SSTables may be included in a repair + session and must all be anticompacted. xref:managing/operating/compaction/lcs.adoc[LeveledCompactionStrategy] is less + susceptible to this issue as it prevents overlapping of partitions within levels outside of L0, but if SSTables + start accumulating in L0 between incremental repairs, the cost of anticompaction will increase. + xref:managing/operating/compaction/ucs#sharding[UnifiedCompactionStrategy's sharding] can also be used to avoid + partitions overlapping SSTables. + +The xref:#repair-token-range-splitter[token_range_splitter] configuration for incremental repair includes a default +configuration that attempts to conservatively migrate 100GiB of compressed data every day per node. Depending on +requirements, data set and capability of a cluster's hardware, one may consider tuning these values to be more +aggressive or conservative. + +=== Previewing Repaired Data + +The `preview_repaired` repair type executes repairs over the repaired data set to detect possible data inconsistencies. + +Inconsistencies in the repaired data set should not happen in practice and could indicate a possible bug in incremental +repair. + +Running preview repairs is useful when considering using the +xref:cassandra:managing/operating/compaction/tombstones.adoc#deletion[only_purge_repaired_tombstones] table compaction +option to prevent data from possibly being resurrected when inconsistent replicas are missing tombstones from deletes. + +When enabled, the `BytesPreviewedDesynchronized` and `TokenRangesPreviewedDesynchronized` +xref:cassandra:managing/operating/metrics.adoc#table-metrics[table metrics] can be used to detect inconsistencies in the +repaired data set. + +== Configuring Auto Repair in cassandra.yaml + +Configuration for Auto Repair is managed in the `cassandra.yaml` file by the `auto_repair` property. + +A rich set of configuration exists for configuring Auto Repair with sensible defaults. However, the expectation +is that some tuning might be needed particulary when it comes to tuning how often repair should run +(`min_repair_interval`) and how repair assignments as created (`token_range_splitter`). + +The following is a practical example of an auto_repair configuration that one might use. + +[source, yaml] +---- +auto_repair: + enabled: true + repair_type_overrides: + full: + enabled: true + min_repair_interval: 5d + incremental: + enabled: true + min_repair_interval: 1h + token_range_splitter: + parameters: + bytes_per_assignment: 50GiB + max_bytes_per_schedule: 100GiB + preview_repaired: + enabled: true + min_repair_interval: 1d + global_settings: + repair_by_keyspace: true + parallel_repair_count: 1 +---- + + +=== Top level settings +The following settings are defined at the top level of the configuration file and apply universally across all +repair types. + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| enabled | false | Enable/Disable the auto-repair scheduler. If set to false, the scheduler thread will not be started. +If set to true, the repair scheduler thread will be created. The thread will check for secondary configuration available +for each repair type (full, incremental, and preview_repaired), and based on that, it will schedule repairs. +| repair_check_interval | 5m | Time interval between successive checks to see if ongoing repairs are complete or if it +is time to schedule repairs. +| repair_max_retries | 3 | Maximum number of retries for a repair session. +| history_clear_delete_hosts_buffer_interval | 2h | The scheduler needs to adjust its order when nodes leave the ring. +Deleted hosts are tracked in metadata for a specified duration to ensure they are indeed removed before adjustments +are made to the schedule. +| mixed_major_version_repair_enabled | false | Enable/Disable running repairs on the cluster when there are mixed +major versions detected, which usually occurs when the cluster is being upgraded. Repairs between nodes of +different major versions is not something that is tested, so this may lead to data compatibility issues. +It is strongly discouraged to set this to true without doing extensive testing beforehand. +|=== + + +=== Repair level settings +The following settings can be configured globally using `global_settings` or tailored individually for each repair +type by using `repair_type_overrides`. + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| enabled | false | Whether the given repair types should be enabled +| min_repair_interval | 24h | Minimum duration between repairing the same node again. This is useful for tiny clusters, +such as clusters with 5 nodes that finish repairs quickly. This means that if the scheduler completes one round on all +nodes in less than this duration, it will not start a new repair round on a given node until this much time has +passed since the last repair completed. Consider increasing to a larger value to reduce the impact of repairs, +however note that one should attempt to run repairs at a smaller interval than gc_grace_seconds to +avoid xref:cassandra:managing/operating/compaction/tombstones.adoc#zombies[data resurrection]. +| token_range_splitter.class_name | org.apache.cassandra.repair.autorepair.RepairTokenRangeSplitter | Implementation of +IAutoRepairTokenRangeSplitter to use; responsible for splitting token ranges for repair assignments. Out of the box, +Cassandra provides org.apache.cassandra.repair.autorepair.{RepairTokenRangeSplitter,FixedTokenRangeSplitter}. +| repair_by_keyspace | true | If true, attempts to group tables in the same keyspace into one repair; otherwise, +each table is repaired individually. +| number_of_repair_threads | 1 | Number of threads to use for each repair job scheduled by the scheduler. Similar to +the -j option in nodetool repair. +| parallel_repair_count | 3 | Number of nodes running repair in parallel. If `parallel_repair_percentage` is set, the +larger value is used. +| parallel_repair_percentage | 3 | Percentage of nodes in the cluster running repair in parallel. If +`parallel_repair_count is set`, the larger value is used. +| allow_parallel_replica_repair | false | Whether to allow a node to take its turn running repair while one or more of +its replicas are running repair. Defaults to false, as running repairs concurrently on replicas can increase load and +also cause anticompaction conflicts while running incremental repair. +| allow_parallel_replica_repair_across_schedules | true | An addition to allow_parallel_repair that also blocks repairs +when replicas (including this node itself) are repairing in any schedule. +For example, if a replica is executing full repairs, a value of false will prevent starting incremental repairs for this +node. Defaults to true and is only evaluated when allow_parallel_replica_repair is false. +| materialized_view_repair_enabled | false | Repairs materialized views if true. +| initial_scheduler_delay | 5m | Delay before starting repairs after a node restarts to avoid repairs starting +immediately after a restart. +| repair_session_timeout | 3h | Timeout for retrying stuck repair sessions. +| force_repair_new_node | false | Force immediate repair on new nodes after they join the ring. +| sstable_upper_threshold | 50000 | Threshold to skip repairing tables with too many SSTables. +| table_max_repair_time | 6h | Maximum time allowed for repairing one table on a given node. If exceeded, the repair +proceeds to the next table. +| ignore_dcs | [] | Avoid running repairs in specific data centers. By default, repairs run in all data centers. Specify +data centers to exclude in this list. Note that repair sessions will still consider all replicas from excluded data +centers. Useful if you have keyspaces that are not replicated in certain data centers, and you want to not run repair +schedule in certain data centers. +| repair_primary_token_range_only | true | Repair only the primary ranges owned by a node. Equivalent to the -pr option +in nodetool repair. General advice is to keep this true. +| repair_retry_backoff | 30s | Backoff time before retrying a repair session. +| repair_task_min_duration | 5s | Minimum duration for the execution of a single repair task. This prevents the +scheduler from overwhelming the node by scheduling too many repair tasks in a short period of time. +|=== + +=== `RepairTokenRangeSplitter` configuration +[#repair-token-range-splitter] + +`RepairTokenRangeSplitter` is the default implementation of `IAutoRepairTokenRangeSplitter` that attempts to create +token range assignments meeting the following goals: + +- *Create smaller, consistent repair times*: Long repairs, such as those lasting 15 hours, can be problematic. If a +node fails 14 hours into the repair, the entire process must be restarted. The goal is to reduce the impact of +disturbances or failures. However, making the repairs too short can lead to overhead from repair orchestration becoming +the main bottleneck. + +- *Minimize the impact on hosts*: Repairs should not heavily affect the host systems. For incremental repairs, this +might involve anti-compaction work. In full repairs, streaming large amounts of data—especially with wide partitions +can lead to issues with disk usage and higher compaction costs. + +- *Reduce overstreaming*: The Merkle tree, which represents data within each partition and range, has a maximum size. +If a repair covers too many partitions, the tree’s leaves represent larger data ranges. Even a small change in a leaf +can trigger excessive data streaming, making the process inefficient. + +- *Reduce number of repairs*: If there are many small tables, it's beneficial to batch these tables together under a +single parent repair. This prevents the repair overhead from becoming a bottleneck, especially when dealing with +hundreds of tables. Running individual repairs for each table can significantly impact performance and efficiency. + +To achieve these goals, this implementation inspects SSTable metadata to estimate the bytes and number of partitions +within a range and splits it accordingly to bound the size of the token ranges used for repair assignments. + +==== Parameter defaults + +The following `parameters` include the same defaults for all repair types. + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| partitions_per_assignment | 1048576 | Maximum number of partitions to include in a repair +assignment. Used to reduce number of partitions present in merkle tree leaf nodes to avoid overstreaming. +| max_tables_per_assignment | 64 | Maximum number of tables to include in a repair assignment. +This reduces the number of repairs, especially in keyspaces with many tables. The splitter avoids batching tables +together if they exceed other configuration parameters like `bytes_per_assignment` or `partitions_per_assignment`. +|=== + +==== Full & Preview Repaired repair defaults + +The following `parameters` defaults are established for both `full` and `preview_repaired` repair scheduling: + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| bytes_per_assignment | 50GiB | The target and maximum amount of *compressed* bytes that should be included in a +repair assignment. *Note*: For full and preview_repaired, only the portion of an SSTable that covers the ranges +being repaired are accounted for in this calculation. +| max_bytes_per_schedule | 100000GiB | The maximum number of bytes to cover in an individual +schedule. This serves as a mechanism to throttle the work done in each repair cycle. You may reduce this value if the +impact of repairs is causing too much load on the cluster or increase it if writes outpace the amount of data being +repaired. Alternatively, adjust the `min_repair_interval`. This is set to a large value for full repair to attempt to +repair all data per repair schedule. +|=== + +==== Incremental repair defaults + +The following `parameters` defaults are established for `incremental` repair scheduling: + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| bytes_per_assignment | 50GiB | The target and maximum amount of *compressed* bytes that should be +included in a repair assignment. *Note*: For incremental repair, the *entire size* of *unrepaired* SSTables +including ranges being repaired are accounted for in this calculation. This is to account for the anticompaction +work required to split the candidate data to repair from the data that won't be repaired. +| max_bytes_per_schedule | 100GiB | The maximum number of bytes to cover in an individual schedule. +Consider increasing if more data is written than this limit within the `min_repair_interval`. +|=== + +=== `FixedSplitTokenRangeSplitter` configuration +[#fixed-split-token-range-splitter] + +`FixedSplitTokenRangeSplitter` is a more simple implementation of `IAutoRepairTokenRangeSplitter` that creates repair +assignments by splitting a node's token ranges into an even number of splits. + +The following `parameters` apply for `FixedSplitTokenRangeSplitter` configuration: + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| number_of_subranges | 32 | Number of evenly split subranges to create for each node that repair runs for. +If vnodes are configured using `num_tokens`, attempts to evenly subdivide subranges by each range. For example, for +`num_tokens: 16` and `number_of_subranges: 32`, 2 (32/16) repair assignments will be created for each token range. At +least one repair assignment will be created for each token range. +|=== + +=== Other cassandra.yaml Considerations + +==== Enable `reject_repair_compaction_threshold` + +When enabling auto_repair, it is advisable to configure the top level `reject_repair_compaction_threshold` +configuration in cassandra.yaml as a backpressure mechanism to reject new repairs on instances that have many +pending compactions. + +==== Tune `repair_disk_headroom_reject_ratio` + +By default, repairs will be rejected if less than 20% of disk is available. If one wishes to be +conservative this top level configuration could be increased to a larger value to prevent filling your data directories. + +== Table configuration + +If Auto Repair is enabled in cassandra.yaml, the `auto_repair` property may be optionally configured at the table +level, e.g.: + +[source,cql] +---- +ALTER TABLE cycling.cyclist_races +WITH auto_repair = {'incremental_enabled': 'false', 'priority': '0'}; +---- + +[cols=",,",options="header",] +|=== +| Name | Default | Description +| priority | 0 | Indicates the priority at which this table should be given when issuing repairs. The higher the number +the more priority will be given to repair the table (e.g. 3 will be repaired before 2). When `repair_by_keyspace` is +set to `true` tables sharing the same priority may be grouped in the same repair assignment. +| full_enabled | true | Whether full repair is enabled for this table. If full.enabled is not true in cassandra.yaml +this will not be evaluated. +| incremental_enabled | true | Whether incremental repair is enabled for this table. If incremental.enabled is not +true in cassandra.yaml this will not be evaluated. +| preview_repaired_enabled | true | Whether preview repair is enabled for this table. If preview_repaired.enabled is +not true in cassandra.yaml this will not be evaluated. +|=== + +== Nodetool Configuration +=== nodetool getautorepairconfig + +Retrieves the runtime configuration of Auto Repair for the targeted node. + +[source,none] +---- +$> nodetool getautorepairconfig +repair scheduler configuration: + repair_check_interval: 5m + repair_max_retries: 3 + history_clear_delete_hosts_buffer_interval: 2h +configuration for repair_type: full + enabled: true + min_repair_interval: 24h + repair_by_keyspace: true + number_of_repair_threads: 1 + sstable_upper_threshold: 50000 + table_max_repair_time: 6h + ignore_dcs: [] + repair_primary_token_range_only: true + parallel_repair_count: 3 + parallel_repair_percentage: 3 + materialized_view_repair_enabled: false + initial_scheduler_delay: 5m + repair_session_timeout: 3h + force_repair_new_node: false + repair_retry_backoff: 30s + repair_task_min_duration: 5s + token_range_splitter: org.apache.cassandra.repair.autorepair.RepairTokenRangeSplitter + token_range_splitter.bytes_per_assignment: 50GiB + token_range_splitter.partitions_per_assignment: 1048576 + token_range_splitter.max_tables_per_assignment: 64 + token_range_splitter.max_bytes_per_schedule: 100000GiB +configuration for repair_type: incremental + enabled: true + min_repair_interval: 1h + repair_by_keyspace: true + number_of_repair_threads: 1 + sstable_upper_threshold: 50000 + table_max_repair_time: 6h + ignore_dcs: [] + repair_primary_token_range_only: true + parallel_repair_count: 3 + parallel_repair_percentage: 3 + materialized_view_repair_enabled: false + initial_scheduler_delay: 5m + repair_session_timeout: 3h + force_repair_new_node: false + repair_retry_backoff: 30s + repair_task_min_duration: 5s + token_range_splitter: org.apache.cassandra.repair.autorepair.RepairTokenRangeSplitter + token_range_splitter.bytes_per_assignment: 50GiB + token_range_splitter.partitions_per_assignment: 1048576 + token_range_splitter.max_tables_per_assignment: 64 + token_range_splitter.max_bytes_per_schedule: 100GiB +configuration for repair_type: preview_repaired + enabled: false +---- + +=== nodetool autorepairstatus + +Provides currently running Auto Repair status. + +[source,none] +---- +$> nodetool autorepairstatus -t incremental +Active Repairs +425cea55-09aa-46e0-8911-9f37a4424574 + + +$> nodetool autorepairstatus -t full +Active Repairs +NONE + +---- + +=== nodetool setautorepairconfig + +Dynamic configuration changes can be made by using `setautorepairconfig`. Note that this only applies on the node being +targeted and these changes are not retained when a node is bounced. + +The following disables the `incremental` repair schedule: + +[source,none] +---- +$> nodetool setautorepairconfig -t incremental enabled false +---- + +The following adjusts the `min_repair_interval` option to `5d` specifically for the `full` repair schedule: + +[source,none] +---- +$> nodetool setautorepairconfig -t full min_repair_interval 5d +---- + +The following configures the `bytes_per_assignment` parameter for `incremental` repair's `token_range_splitter` to +`10GiB`: + +[source,none] +---- +$> nodetool setautorepairconfig -t incremental token_range_splitter.bytes_per_assignment 10GiB +---- + +==== More details +https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-37+Apache+Cassandra+Unified+Repair+Solution[CEP-37] diff --git a/doc/modules/cassandra/pages/managing/operating/bulk_loading.adoc b/doc/modules/cassandra/pages/managing/operating/bulk_loading.adoc index 2630fbf76c4a..fbddb149dd94 100644 --- a/doc/modules/cassandra/pages/managing/operating/bulk_loading.adoc +++ b/doc/modules/cassandra/pages/managing/operating/bulk_loading.adoc @@ -25,7 +25,7 @@ The `sstableloader` and `nodetool import` are accessible if the Cassandra installation `bin` directory is in the `PATH` environment variable. Or these may be accessed directly from the `bin` directory. -The examples use the keyspaces and tables created in xref:cassandra:developing/cql/operating/backups.adoc[Backups]. +The examples use the keyspaces and tables created in xref:cassandra:managing/operating/backups.adoc[Backups]. == Using sstableloader diff --git a/doc/modules/cassandra/pages/managing/operating/compaction/overview.adoc b/doc/modules/cassandra/pages/managing/operating/compaction/overview.adoc index 3fdbb6d5a7d7..6a396106b8ee 100644 --- a/doc/modules/cassandra/pages/managing/operating/compaction/overview.adoc +++ b/doc/modules/cassandra/pages/managing/operating/compaction/overview.adoc @@ -2,18 +2,18 @@ == What is compaction? -Data in {cassandra} is created in xref:cassandra:architecture/storage-engine.adoc#memtables[memtables]. +Data in {cassandra} is created in xref:cassandra:architecture/storage-engine.adoc#memtables[memtables].  Once a memory threshold is reached, to free up memory again, the data is written to an xref:cassandra:architecture/storage-engine.adoc#SSTables[SSTable], an https://cassandra.apache.org/_/glossary.html#immutable[immutable] file residing on disk. -Because SSTables are immutable, when data is updated or deleted, the old data is not overwritten with inserts or updates, or removed from the SSTable. -Instead, a new SSTable is created with the updated data with a new timestamp, and the old SSTable is marked for deletion. +Because SSTables are immutable, when data is updated or deleted, the old data is not overwritten with inserts or updates, or removed from the SSTable.  +Instead, a new SSTable is created with the updated data with a new timestamp, and the old SSTable is marked for deletion.  The piece of deleted data is known as a https://cassandra.apache.org/_/glossary.html#tombstone[tombstone]. -Over time, Cassandra may write many versions of a row in different SSTables. -Each version may have a unique set of columns stored with a different timestamp. +Over time, Cassandra may write many versions of a row in different SSTables.  +Each version may have a unique set of columns stored with a different timestamp.  As SSTables accumulate, the distribution of data can require accessing more and more SSTables to retrieve a complete row. -To keep the database healthy, Cassandra periodically merges SSTables and discards old data. +To keep the database healthy, Cassandra periodically merges SSTables and discards old data.  This process is called https://cassandra.apache.org/_/glossary.html#compaction[compaction]. == Why must compaction be run? @@ -26,22 +26,22 @@ Deleting, updating, or expiring data are all valid triggers for compaction. == What does compaction accomplish? Two important factors accomplished by compaction are performance improvement and disk space reclamation. -If SSTables have duplicate data that must be read, read operations are slower. +If SSTables have duplicate data that must be read, read operations are slower.  Once tombstones and duplicates are removed, read operations are faster. SSTables use disk space, and reducing the size of SSTables through compaction frees up disk space. == How does compaction work? -Compaction works on a collection of SSTables. -From these SSTables, compaction collects all versions of each unique row and assembles one complete row, using the most up-to-date version (by timestamp) of each of the row's columns. -The merge process is performant, because rows are sorted by partition key within each SSTable, and the merge process does not use random I/O. -The new versions of each row is written to a new SSTable. +Compaction works on a collection of SSTables.  +From these SSTables, compaction collects all versions of each unique row and assembles one complete row, using the most up-to-date version (by timestamp) of each of the row's columns.  +The merge process is performant, because rows are sorted by partition key within each SSTable, and the merge process does not use random I/O.  +The new versions of each row is written to a new SSTable.  The old versions, along with any rows that are ready for deletion, are left in the old SSTables, and are deleted as soon as pending reads are completed. == Types of compaction The concept of compaction is used for different kinds of operations in -{cassandra}, the common thing about these operations is that it takes one +{cassandra}, the common thing about these operations is that they take one or more SSTables, merges, and outputs new SSTables. The types of compactions are: Minor compaction:: @@ -56,11 +56,11 @@ A major compaction is triggered when a user executes a compaction over all SSTab User defined compaction:: Similar to a major compaction, a user-defined compaction executes when a user triggers a compaction on a given set of SSTables. Scrub:: -A scrub triggers a compaction to try to fix any broken SSTables. +A scrub triggers a compaction to try to fix any broken SSTables.  This can actually remove valid data if that data is corrupted. If that happens you will need to run a full repair on the node. UpgradeSSTables:: -A compaction occurs when you upgrade SSTables to the latest version. +A compaction occurs when you upgrade SSTables to the latest version.  Run this after upgrading to a new major version. Cleanup:: Compaction executes to remove any ranges that a node no longer owns. @@ -71,8 +71,8 @@ Anticompaction:: After repair, the ranges that were actually repaired are split out of the SSTables that existed when repair started. This type of compaction rewrites SSTables to accomplish this task. Sub range compaction:: It is possible to only compact a given sub range - this action is useful if you know a token that has been misbehaving - either gathering many updates or many deletes. -The command `nodetool compact -st x -et y` will pick all SSTables containing the range between x and y and issue a compaction for those SSTables. -For Size Tiered Compaction Strategy, this will most likely include all SSTables, but with Leveled Compaction Strategy, it can issue the compaction for a subset of the SSTables. +The command `nodetool compact -st x -et y` will pick all SSTables containing the range between x and y and issue a compaction for those SSTables.  +For Size Tiered Compaction Strategy, this will most likely include all SSTables, but with Leveled Compaction Strategy, it can issue the compaction for a subset of the SSTables.  With LCS the resulting SSTable will end up in L0. == Strategies @@ -82,14 +82,14 @@ Picking the right compaction strategy for your workload will ensure the best per xref:cassandra:managing/operating/compaction/ucs.adoc[`Unified Compaction Strategy (UCS)`]:: UCS is a good choice for most workloads and is recommended for new workloads. -This compaction strategy is designed to handle a wide variety of workloads. -It is designed to be able to handle both immutable time-series data and workloads with lots of updates and deletes. -It is also designed to be able to handle both spinning disks and SSDs. -xref:cassandra:managing/operating/compaction/stcs.adoc[`Size Tiered Compaction Strategy (STCS)`]:: -STCS is the default compaction strategy, because it is useful as a fallback when other strategies don't fit the workload. +This compaction strategy is designed to handle a wide variety of workloads.  +It is designed to be able to handle both immutable time-series data and workloads with lots of updates and deletes.  +It is also designed to be able to handle both spinning disks and SSDs.   +xref:cassandra:managing/operating/compaction/stcs.adoc[`Size Tiered Compaction Strategy (STCS)`]::  +STCS is the default compaction strategy, because it is useful as a fallback when other strategies don't fit the workload.  Most useful for not strictly time-series workloads with spinning disks, or when the I/O from `LCS` is too high. xref:cassandra:managing/operating/compaction/lcs.adoc[`Leveled Compaction Strategy (LCS)`]:: -Leveled Compaction Strategy (LCS) is optimized for read heavy workloads, or workloads with lots of updates and deletes. +Leveled Compaction Strategy (LCS) is optimized for read heavy workloads, or workloads with lots of updates and deletes.  It is not a good choice for immutable time-series data. xref:cassandra:managing/operating/compaction/twcs.adoc[`Time Window Compaction Strategy (TWCS)`]:: Time Window Compaction Strategy is designed for TTL'ed, mostly immutable time-series data. @@ -107,19 +107,6 @@ of the TTL) Cassandra will have a hard time dropping the tombstones created since the partition might span many SSTables and not all are compacted at once. -== Fully expired SSTables - -If an SSTable contains only tombstones and it is guaranteed that -SSTable is not shadowing data in any other SSTable, then the compaction can drop -that SSTable. If you see SSTables with only tombstones (note that TTL-ed -data is considered tombstones once the time-to-live has expired), but it -is not being dropped by compaction, it is likely that other SSTables -contain older data. There is a tool called `sstableexpiredblockers` that -will list which SSTables are droppable and which are blocking them from -being dropped. With `TimeWindowCompactionStrategy` it -is possible to remove the guarantee (not check for shadowing data) by -enabling `unsafe_aggressive_sstable_expiration`. - == Repaired/unrepaired data With incremental repairs Cassandra must keep track of what data is @@ -161,8 +148,8 @@ When an SSTable is written a histogram with the tombstone expiry times is created and this is used to try to find SSTables with very many tombstones and run single SSTable compaction on that SSTable in hope of being able to drop tombstones in that SSTable. Before starting this it -is also checked how likely it is that any tombstones will actually will -be able to be dropped how much this SSTable overlaps with other +is also checked how likely it is that any tombstones will actually +be able to be dropped and how much this SSTable overlaps with other SSTables. To avoid most of these checks the compaction option `unchecked_tombstone_compaction` can be enabled. @@ -178,11 +165,11 @@ How much of the SSTable should be tombstones for us to consider doing a single S `tombstone_compaction_interval` (default: 86400s (1 day)):: Since it might not be possible to drop any tombstones when doing a single SSTable compaction we need to make sure that one SSTable is not constantly getting recompacted - this option states how often we should try for a given SSTable. `log_all` (default: false):: -New detailed compaction logging, see `below `. +New detailed compaction logging, see <>. `unchecked_tombstone_compaction` (default: false):: -The single SSTable compaction has quite strict checks for whether it should be started, this option disables those checks and for some use cases this might be needed. +The single SSTable compaction has quite strict checks for whether it should be started, this option disables those checks and for some use cases this might be needed.  Note that this does not change anything for the actual compaction, tombstones are only dropped if it is safe to do so - it might just rewrite an SSTable without being able to drop any tombstones. -`only_purge_repaired_tombstone` (default: false):: +`only_purge_repaired_tombstones` (default: false):: Option to enable the extra safety of making sure that tombstones are only dropped if the data has been repaired. `min_threshold` (default: 4):: Lower limit of number of SSTables before a compaction is triggered. @@ -195,7 +182,7 @@ Further, see the section on each strategy for specific additional options. == Compaction nodetool commands -The `nodetool ` utility provides a number of commands related to compaction: +The `nodetool` utility provides a number of commands related to compaction: `enableautocompaction`:: Enable compaction. @@ -212,7 +199,7 @@ Set the min/max SSTable count for when to trigger compaction, defaults to 4/32. == Switching the compaction strategy and options using JMX -It is possible to switch compaction strategies and its options on just a single node using JMX, this is a great way to experiment with settings without affecting the whole cluster. +It is possible to switch compaction strategies and its options on just a single node using JMX, this is a great way to experiment with settings without affecting the whole cluster.  The mbean is: [source,console] diff --git a/doc/modules/cassandra/pages/managing/operating/compaction/tombstones.adoc b/doc/modules/cassandra/pages/managing/operating/compaction/tombstones.adoc index 9e0dcb6f7879..e48c02d209df 100644 --- a/doc/modules/cassandra/pages/managing/operating/compaction/tombstones.adoc +++ b/doc/modules/cassandra/pages/managing/operating/compaction/tombstones.adoc @@ -69,22 +69,21 @@ This is basically the same as in the "Deletes without Tombstones" section. === Deletes without tombstones -Imagine a three node cluster which has the value [A] replicated to every -node.: +Imagine a three node cluster which has the value [A] replicated to every node: [source,none] ---- [A], [A], [A] ---- -If one of the nodes fails and and our delete operation only removes existing values, we can end up with a cluster that looks like: +If one of the nodes fails and our delete operation only removes existing values, we can end up with a cluster that looks like: [source,none] ---- [], [], [A] ---- -Then a repair operation would replace the value of [A] back onto the two nodes which are missing the value.: +Then a repair operation would replace the value of [A] back onto the two nodes which are missing the value: [source,none] ---- @@ -95,7 +94,7 @@ This would cause our data to be resurrected as a zombie even though it had been === Deletes with tombstones -Starting again with a three node cluster which has the value [A] replicated to every node.: +Starting again with a three node cluster which has the value [A] replicated to every node: [source,none] ---- diff --git a/doc/modules/cassandra/pages/managing/operating/security.adoc b/doc/modules/cassandra/pages/managing/operating/security.adoc index 1de19d86414d..783cac6d60c4 100644 --- a/doc/modules/cassandra/pages/managing/operating/security.adoc +++ b/doc/modules/cassandra/pages/managing/operating/security.adoc @@ -281,7 +281,7 @@ xref:cassandra:developing/cql/security.adoc#operation-roles[`CassandraRoleManage See also: `setting-credentials-for-internal-authentication`, xref:cassandra:developing/cql/security.adoc#create-role[`CREATE ROLE`], xref:cassandra:developing/cql/security.adoc#alter-role[`ALTER ROLE`], -xref:xref:cassandra:developing/cql/security.adoc#alter-keyspace[`ALTER KEYSPACE`] and +xref:cassandra:developing/cql/ddl.adoc#alter-keyspace-statement[`ALTER KEYSPACE`] and xref:cassandra:developing/cql/security.adoc#grant-permission[`GRANT PERMISSION`]. == Authorization @@ -411,7 +411,7 @@ If enabling remote connections, it is recommended to also use xref:cassandra:managing/operating/security.adoc#jmx-with-ssl[`SSL`] connections. Finally, after enabling auth and/or SSL, ensure that tools which use -JMX, such as xref:tools/nodetool/nodetools.adoc[`nodetool`] are correctly configured and working +JMX, such as xref:cassandra:managing/tools/nodetool/nodetool.adoc[`nodetool`] are correctly configured and working as expected. === Standard JMX Auth diff --git a/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc b/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc index 61ab25de3358..7143706058eb 100644 --- a/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc +++ b/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc @@ -22,7 +22,7 @@ of `cqlsh`. By default, `cqlsh` displays all timestamps with a UTC timezone. For Python 3.9 or higher, timestamps can be displayed in different timezones by modifying the -`timezone` option in xref:cassandra:developing/cql/tools/cqlsh.adoc#cqlshrc[cqlshrc] or by setting the environment +`timezone` option in xref:cassandra:managing/tools/cqlsh.adoc#cqlshrc[cqlshrc] or by setting the environment variable `TZ`. Python 3.8 or lower, however, will also require the installation of http://pytz.sourceforge.net/[pytz] library. diff --git a/doc/modules/cassandra/pages/new/index.adoc b/doc/modules/cassandra/pages/new/index.adoc index 5d36e59b73b5..d2516661967e 100644 --- a/doc/modules/cassandra/pages/new/index.adoc +++ b/doc/modules/cassandra/pages/new/index.adoc @@ -10,7 +10,7 @@ This section covers the new features in Apache Cassandra 5.0. * Trie SSTables: https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-25%3A+Trie-indexed+SSTable+format[CEP-25], https://issues.apache.org/jira/browse/CASSANDRA-18398[JIRA ticket] * JDK 17: xref:cassandra:reference/java17.adoc[Docs], https://issues.apache.org/jira/browse/CASSANDRA-16895[JIRA ticket] * More guardrails: https://github.com/apache/cassandra/blob/trunk/NEWS.txt[NEWS.txt] -* TTL and writetime on collections and UDTs: xref:cassandra:developing/cql/dml.html#writetime-and-ttl-function[Docs], https://issues.apache.org/jira/browse/CASSANDRA-8877[JIRA ticket] +* TTL and writetime on collections and UDTs: xref:cassandra:developing/cql/functions.adoc#writetime-and-ttl-functions[Docs], https://issues.apache.org/jira/browse/CASSANDRA-8877[JIRA ticket] * New vector data type: xref:cassandra:reference/vector-data-type.adoc[Docs], https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-30%3A+Approximate+Nearest+Neighbor%28ANN%29+Vector+Search+via+Storage-Attached+Indexes[CEP-30], https://issues.apache.org/jira/browse/CASSANDRA-18504[JIRA ticket] * New vector similarity functions: xref:cassandra:vector-search/overview.adoc[Docs], https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-30%3A+Approximate+Nearest+Neighbor%28ANN%29+Vector+Search+via+Storage-Attached+Indexes[CEP-30], https://issues.apache.org/jira/browse/CASSANDRA-18640[JIRA ticket] * Unified Compaction Strategy: xref:cassandra:managing/operating/compaction/ucs.adoc[Docs], https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-26%3A+Unified+Compaction+Strategy[CEP-26], https://issues.apache.org/jira/browse/CASSANDRA-18397[JIRA ticket] diff --git a/doc/modules/cassandra/pages/reference/cql-commands/alter-table.adoc b/doc/modules/cassandra/pages/reference/cql-commands/alter-table.adoc index 2c6071c52944..be4dff203927 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/alter-table.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/alter-table.adoc @@ -98,7 +98,7 @@ Enclose the value for a string property in single quotation marks. + Other table properties are set using a JSON map: `+option_name = { : [ , ... ] }+` + -See xref:reference:cql-commands/create-table.adoc#table_options[table_options] for more details. +See xref:cassandra:reference/cql-commands/create-table.adoc#table_options[table_options] for more details. == Usage notes @@ -190,7 +190,7 @@ To change an existing table's properties, use `ALTER TABLE` and `WITH`. You can specify a: * Single property name and value. -* Property map to set the names and values, as shown in the xref:cql-commands/alter-table.adoc#alter-compression[next section on compression and compaction]. +* Property map to set the names and values, as shown in the xref:cassandra:reference/cql-commands/alter-table.adoc#alter-compression[next section on compression and compaction]. For example, to add a comment to the xref:cassandra:cyclist_base-table.adoc[cyclist_base] table using WITH: @@ -204,7 +204,7 @@ Enclose a text property value in single quotation marks. [[alter-compression]] === Modifying compression and compaction -Use a property map to alter the xref:cassandra:comments-table.adoc[comments] table's compression or compaction setting: +Use a property map to alter the comments table's compression or compaction setting: [source,language-cql] ---- @@ -223,7 +223,7 @@ For strategies to minimize this disruption, see http://blog.alteroot.org/article === Changing caching -Set the number of rows per partition to store in the row cache for the xref:cassandra:comments-table.adoc[comments] table to 10 rows: +Set the number of rows per partition to store in the row cache for the comments table to 10 rows: [source,language-cql] ---- diff --git a/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc b/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc index 89ec71787679..b5f087671c3b 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc @@ -4,123 +4,111 @@ This section describes the Cassandra Query Language (CQL) commands supported by the {product} database. ''' -xref:reference/cql-commands/alter-keyspace.adoc[ALTER KEYSPACE] :: +xref:cassandra:developing/cql/ddl.adoc#alter-keyspace-statement[ALTER KEYSPACE] :: Changes keyspace replication strategy and enables or disables commit log. -xref:reference/cql-commands/alter-materialized-view.adoc[ALTER MATERIALIZED VIEW] :: +xref:cassandra:developing/cql/mvs.adoc#alter-materialized-view-statement[ALTER MATERIALIZED VIEW] :: Changes the table properties of a materialized view. -xref:reference/cql-commands/alter-role.adoc[ALTER ROLE] :: +xref:cassandra:developing/cql/security.adoc#alter-role-statement[ALTER ROLE] :: Changes password and sets superuser or login options. -xref:reference/cql-commands/alter-table.adoc[ALTER TABLE] :: +xref:cassandra:reference/cql-commands/alter-table.adoc[ALTER TABLE] :: Modifies the columns and properties of a table, or modify -xref:reference/cql-commands/alter-type.adoc[ALTER TYPE] :: +xref:cassandra:developing/cql/types.adoc#udts[ALTER TYPE] :: Modifies an existing user-defined type (UDT). -xref:reference/cql-commands/alter-user.adoc[ALTER USER (Deprecated)] :: +xref:cassandra:developing/cql/security.adoc#alter-user-statement[ALTER USER (Deprecated)] :: Deprecated. Alter existing user options. -xref:reference/cql-commands/batch.adoc[BATCH] :: +xref:cassandra:developing/cql/dml.adoc#batch_statement[BATCH] :: Applies multiple data modification language (DML) statements with atomicity and/or in isolation. -xref:reference/cql-commands/create-aggregate.adoc[CREATE AGGREGATE] :: +xref:cassandra:developing/cql/functions.adoc#create-aggregate-statement[CREATE AGGREGATE] :: Defines a user-defined aggregate. -xref:reference/cql-commands/create-custom-index.adoc[CREATE CUSTOM INDEX] :: +xref:cassandra:reference/cql-commands/create-custom-index.adoc[CREATE CUSTOM INDEX] :: Creates a storage-attached index. -xref:reference/cql-commands/create-function.adoc[CREATE FUNCTION] :: +xref:cassandra:developing/cql/functions.adoc#create-function-statement[CREATE FUNCTION] :: Creates custom function to execute user provided code. -xref:reference/cql-commands/create-index.adoc[CREATE INDEX] :: +xref:cassandra:reference/cql-commands/create-index.adoc[CREATE INDEX] :: Defines a new index for a single column of a table. -xref:reference/cql-commands/create-keyspace.adoc[CREATE KEYSPACE] :: +xref:cassandra:developing/cql/ddl.adoc#create-keyspace-statement[CREATE KEYSPACE] :: -xref:reference/cql-commands/create-materialized-view.adoc[CREATE MATERIALIZED VIEW] :: +xref:cassandra:developing/cql/mvs.adoc#create-materialized-view-statement[CREATE MATERIALIZED VIEW] :: Optimizes read requests and eliminates the need for multiple write requests by duplicating data from a base table. -xref:reference/cql-commands/create-role.adoc[CREATE ROLE] :: +xref:cassandra:developing/cql/security.adoc#create-role-statement[CREATE ROLE] :: Creates a cluster wide database object used for access control. -xref:reference/cql-commands/create-table.adoc[CREATE TABLE] :: +xref:cassandra:reference/cql-commands/create-table.adoc[CREATE TABLE] :: Creates a new table. -xref:reference/cql-commands/create-type.adoc[CREATE TYPE] :: +xref:cassandra:developing/cql/types.adoc#udts[CREATE TYPE] :: Creates a custom data type in the keyspace that contains one or more fields of related information. -xref:reference/cql-commands/create-user.adoc[CREATE USER (Deprecated)] :: +xref:cassandra:developing/cql/security.adoc#create-user-statement[CREATE USER (Deprecated)] :: Deprecated. Creates a new user. -xref:reference/cql-commands/delete.adoc[DELETE] :: +xref:cassandra:developing/cql/dml.adoc#delete_statement[DELETE] :: Removes data from one or more columns or removes the entire row -xref:reference/cql-commands/drop-aggregate.adoc[DROP AGGREGATE] :: +xref:cassandra:developing/cql/functions.adoc#drop-aggregate-statement[DROP AGGREGATE] :: Deletes a user-defined aggregate from a keyspace. -xref:reference/cql-commands/drop-function.adoc[DROP FUNCTION] :: +xref:cassandra:developing/cql/functions.adoc#drop-function-statement[DROP FUNCTION] :: Deletes a user-defined function (UDF) from a keyspace. -xref:reference/cql-commands/drop-index.adoc[DROP INDEX] :: +xref:cassandra:reference/cql-commands/drop-index.adoc[DROP INDEX] :: Removes an index from a table. -xref:reference/cql-commands/drop-keyspace.adoc[DROP KEYSPACE] :: +xref:cassandra:developing/cql/ddl.adoc#drop-keyspace-statement[DROP KEYSPACE] :: Removes the keyspace. -xref:reference/cql-commands/drop-materialized-view.adoc[DROP MATERIALIZED VIEW] :: +xref:cassandra:developing/cql/mvs.adoc#drop-materialized-view-statement[DROP MATERIALIZED VIEW] :: Removes the named materialized view. -xref:reference/cql-commands/drop-role.adoc[DROP ROLE] :: +xref:cassandra:developing/cql/security.adoc#drop-role-statement[DROP ROLE] :: Removes a role. -xref:reference/cql-commands/drop-table.adoc[DROP TABLE] :: +xref:cassandra:reference/cql-commands/drop-table.adoc[DROP TABLE] :: Removes the table. -xref:reference/cql-commands/drop-type.adoc[DROP TYPE] :: +xref:cassandra:developing/cql/types.adoc#udts[DROP TYPE] :: Drop a user-defined type. -xref:reference/cql-commands/drop-user.adoc[DROP USER (Deprecated)] :: +xref:cassandra:developing/cql/security.adoc#drop-user-statement[DROP USER (Deprecated)] :: Removes a user. -xref:reference/cql-commands/grant.adoc[GRANT] :: +xref:cassandra:developing/cql/security.adoc#grant-permission-statement[GRANT] :: Allow access to database resources. -xref:reference/cql-commands/insert.adoc[INSERT] :: +xref:cassandra:developing/cql/dml.adoc#insert-statement[INSERT] :: Inserts an entire row or upserts data into existing rows. -xref:reference/cql-commands/list-permissions.adoc[LIST PERMISSIONS] :: +xref:cassandra:developing/cql/security.adoc#list-permissions-statement[LIST PERMISSIONS] :: Lists permissions on resources. -xref:reference/cql-commands/list-roles.adoc[LIST ROLES] :: +xref:cassandra:developing/cql/security.adoc#list-roles-statement[LIST ROLES] :: Lists roles and shows superuser and login status. -xref:reference/cql-commands/list-users.adoc[LIST USERS (Deprecated)] :: +xref:cassandra:developing/cql/security.adoc#list-users-statement[LIST USERS (Deprecated)] :: Lists existing internal authentication users and their superuser status. -xref:reference/cql-commands/restrict.adoc[RESTRICT] :: -Denies the permission on a resource, even if the role is directly granted or inherits permissions. - -xref:reference/cql-commands/restrict-rows.adoc[RESTRICT ROWS] :: -Configures the column used for row-level access control. - -xref:reference/cql-commands/revoke.adoc[REVOKE] :: +xref:cassandra:developing/cql/security.adoc#revoke-permission-statement[REVOKE] :: Removes privileges on database objects from roles. -xref:reference/cql-commands/select.adoc[SELECT] :: +xref:cassandra:developing/cql/dml.adoc#select-statement[SELECT] :: Returns data from a table. -xref:reference/cql-commands/truncate.adoc[TRUNCATE] :: +xref:cassandra:developing/cql/ddl.adoc#truncate-statement[TRUNCATE] :: Removes all data from a table. -xref:reference/cql-commands/unrestrict.adoc[UNRESTRICT] :: -Removes a restriction from a role. - -xref:reference/cql-commands/unrestrict-rows.adoc[UNRESTRICT ROWS] :: -Removes the column definition for row-level access control. - -xref:reference/cql-commands/update.adoc[UPDATE] :: +xref:cassandra:developing/cql/dml.adoc#update-statement[UPDATE] :: Modifies one or more column values to a row in a table. -xref:reference/cql-commands/use.adoc[USE] :: -Selects the keyspace for the current client session. \ No newline at end of file +xref:cassandra:developing/cql/ddl.adoc#use-statement[USE] :: +Selects the keyspace for the current client session. diff --git a/doc/modules/cassandra/pages/reference/cql-commands/compact-subproperties.adoc b/doc/modules/cassandra/pages/reference/cql-commands/compact-subproperties.adoc index 3df664bbc172..91763fd0e790 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/compact-subproperties.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/compact-subproperties.adoc @@ -235,7 +235,7 @@ Default: `160` ==== The default value, 160 MB, may be inefficient and negatively impact database indexing and the queries that rely on indexes. For example, consider the benefit of using higher values for sstable_size_in_mb in tables that use (SAI) indexes. -For related information, see xref:developing:indexing/sai/configuring.adoc#saiConfigure__saiCompactionStrategies[Compaction strategies]. +For related information, see xref:cassandra:developing/cql/indexing/sai/operations/configuring.adoc#saiConfigure__saiCompactionStrategies[Compaction strategies]. ==== fanout_size:: diff --git a/doc/modules/cassandra/pages/reference/cql-commands/create-custom-index.adoc b/doc/modules/cassandra/pages/reference/cql-commands/create-custom-index.adoc index b4dc08566f0a..85c56721b7d4 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/create-custom-index.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/create-custom-index.adoc @@ -54,6 +54,8 @@ index_name:: Optional identifier for index. If no name is specified, the default used is `\_\_idx`. Enclose in quotes to use special characters or to preserve capitalization. ++ +include::cassandra:partial$index-naming.adoc[] column_name:: The name of the table column on which the SAI index is being defined. diff --git a/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc b/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc index ae95c33a1d50..5b191709e5bf 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc @@ -65,7 +65,7 @@ SAI returns `InvalidRequestException` if you try to define an index on a column == Optional parameters -[cols="1,3"] +[cols="1,3"] |=== | Parameter | Description @@ -75,6 +75,8 @@ SAI returns `InvalidRequestException` if you try to define an index on a column Enclose in quotes to use special characters or preserve capitalization. If no name is specified, {product} names the index as `\_\_idx`. +include::cassandra:partial$index-naming.adoc[] + | keyspace_name | Name of the keyspace that contains the table to index. If no name is specified, the current keyspace is used. @@ -248,7 +250,7 @@ Assume a cyclist table contains this map data where `nation` is the map key and ---- To index map keys, use the `KEYS` keyword and map name in nested parentheses in the CREATE INDEX statement. -To run a `SELECT` query on the table, use xref:cassandra:reference/cql-commands/select.adoc#filtering-on-collections[CONTAINS KEY] in `WHERE` clauses. +To run a `SELECT` query on the table, use xref:cassandra:developing/cql/dml.adoc#allow-filtering[CONTAINS KEY] in `WHERE` clauses. This query returns cyclist teams that have an entry for the year 2015. [tabs] diff --git a/doc/modules/cassandra/pages/reference/cql-commands/create-table-examples.adoc b/doc/modules/cassandra/pages/reference/cql-commands/create-table-examples.adoc index d3487202b56e..b037b68e2ae0 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/create-table-examples.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/create-table-examples.adoc @@ -46,9 +46,9 @@ Create the `race_winners` table that has a frozen user-defined type (UDT): include::cassandra:example$CQL/race_winners-table.cql[tag=usetype] ---- -See xref:developing/user-defined-type-create.adoc[Create a user-defined type] for information on Create UDTs. +See xref:cassandra:developing/cql/types.adoc#udts[Create a user-defined type] for information on Create UDTs. UDTs can be created unfrozen if only non-collection fields are used in the user-defined type creation. -If the table is created with an unfrozen UDT, then xref:developing/inserting/insert-user-defined-type.adoc[individual field values can be updated and deleted]. +If the table is created with an unfrozen UDT, then xref:cassandra:developing/cql/dml.adoc#update-statement[individual field values can be updated and deleted]. == Create a table with a CDC log diff --git a/doc/modules/cassandra/pages/reference/cql-commands/create-table.adoc b/doc/modules/cassandra/pages/reference/cql-commands/create-table.adoc index 69cb7cc7431a..5b21af328c4e 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/create-table.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/create-table.adoc @@ -67,6 +67,7 @@ include::cassandra:partial$compress-subproperties.adoc[] include::cassandra:partial$compact-subproperties.adoc[] +[[table_options]] == Optional parameters // Table Keywords @@ -88,7 +89,7 @@ If the column already contains data, it is indexed during the execution of this After an index has been created, it is automatically updated when data in the column changes. Indexing with the `CREATE INDEX` command can impact performance. -Before creating an index, be aware of when and xref:cassandra:developing/indexing/2i/2i-when-to-use.adoc#when-no-index[when not to create an index]. +Before creating an index, be aware of when and xref:cassandra:developing/cql/indexing/2i/2i-when-to-use.adoc#when-no-index[when not to create an index]. *Restriction:* Indexing counter columns is not supported. diff --git a/doc/modules/cassandra/pages/reference/index.adoc b/doc/modules/cassandra/pages/reference/index.adoc index 8e09e8f8d804..642b9e081058 100644 --- a/doc/modules/cassandra/pages/reference/index.adoc +++ b/doc/modules/cassandra/pages/reference/index.adoc @@ -1,7 +1,7 @@ = Reference * xref:reference/cql-commands/commands-toc.adoc[CQL commands] -* xref:developing/cql/cql_singlefile.html[CQL specification] +* xref:cassandra:developing/cql/cql_singlefile.adoc[CQL specification] * xref:reference/java17.adoc[Java 17] * xref:reference/native-protocol.adoc[Native Protocol specification] * xref:reference/sai-virtual-table-indexes.adoc[SAI virtual table] diff --git a/doc/modules/cassandra/pages/reference/static.adoc b/doc/modules/cassandra/pages/reference/static.adoc index d27adc76ea5f..96a9f7bf13ef 100644 --- a/doc/modules/cassandra/pages/reference/static.adoc +++ b/doc/modules/cassandra/pages/reference/static.adoc @@ -48,7 +48,7 @@ The table that does not have clustering columns has a one-row partition in which * A column designated to be the partition key cannot be static. ==== -You can do xref:developing/batch/batch-good-example.adoc[batch conditional updates to a static column]. +You can do xref:cassandra:developing/cql/batch/batch-good-example.adoc[batch conditional updates to a static column]. Use the `DISTINCT` keyword to select static columns. In this case, the database retrieves only the beginning (static column) of the partition. diff --git a/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc b/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc index d2e9a9b10a93..a9b9b3484eec 100644 --- a/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc +++ b/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc @@ -128,6 +128,6 @@ exhaust significant CPU capacitity with a "single" query. Once you have narrowed down the problem as much as possible (datacenter, rack , node), login to one of the nodes using SSH and proceed to debug -using xref:reading_logs.adoc[`logs`], xref:use_nodetooladoc[`nodetool`], and -xref:use_tools.adoc[`os tools`]. +using xref:cassandra:troubleshooting/reading_logs.adoc[`logs`], xref:cassandra:troubleshooting/use_nodetool.adoc[`nodetool`], and +xref:cassandra:troubleshooting/use_tools.adoc[`os tools`]. If you are not able to login you may still have access to `logs` and `nodetool` remotely. diff --git a/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc b/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc index 3f2f1a8ddcb4..d1a3a93cd55f 100644 --- a/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc +++ b/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc @@ -244,4 +244,4 @@ index b2c5b10..71b0a49 100644 Note that if you want more information than this tool provides, there are other live capture options available such as -xref:cassandra:developing/cql/troubleshooting/use_tools.adoc#packet-capture[`packet-capture`]. +xref:cassandra:troubleshooting/use_tools.adoc#packet-capture[`packet-capture`]. diff --git a/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc b/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc index ed72f5433d72..5b883cfd3ae6 100644 --- a/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc +++ b/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc @@ -208,7 +208,7 @@ when it syncs the commit log. This typically enters into the very high percentiles of write latency. Note that to get detailed latency breakdowns you will need a more -advanced tool such as xref:use_tools.adoc#bcc-tools[`bcc-tools`]. +advanced tool such as xref:cassandra:troubleshooting/use_tools.adoc#use-bcc-tools[`bcc-tools`]. === OS page Cache Usage @@ -232,8 +232,8 @@ Cassandra performance can suffer significantly. This is why Cassandra starts with a reasonably small amount of memory reserved for the heap. If you suspect that you are missing the OS page cache frequently you can -use advanced tools like xref:use_tools.adoc#use-bcc-tools[cachestat] or -xref:use_tools.adoc#use-vmtouch[vmtouch] to dive deeper. +use advanced tools like xref:cassandra:troubleshooting/use_tools.adoc#use-bcc-tools[cachestat] or +xref:cassandra:troubleshooting/use_tools.adoc#use-vmtouch[vmtouch] to dive deeper. === Network Latency and Reliability @@ -483,7 +483,7 @@ $ ./vmtouch /var/lib/cassandra/data/ In this case almost the entire dataset is hot in OS page Cache. Generally speaking the percentage doesn't really matter unless reads are -missing the cache (per e.g. xref:cassandra:developing/cql/troubleshooting/use_tools.adoc#use-bcc-tools[cachestat] in which case +missing the cache (per e.g. xref:cassandra:troubleshooting/use_tools.adoc#use-bcc-tools[cachestat] in which case having additional memory may help read performance. === CPU Flamegraphs @@ -544,6 +544,7 @@ $ cat cassandra_stacks | ./stackcollapse-perf.pl | grep -v cpu_idle | \ The resulting SVG is searchable, zoomable, and generally easy to introspect using a browser. +[[packet-capture]] === Packet Capture Sometimes you have to understand what queries a Cassandra node is diff --git a/doc/modules/cassandra/pages/vector-search/concepts.adoc b/doc/modules/cassandra/pages/vector-search/concepts.adoc index 05c97e4c1d02..ba194779043f 100644 --- a/doc/modules/cassandra/pages/vector-search/concepts.adoc +++ b/doc/modules/cassandra/pages/vector-search/concepts.adoc @@ -4,7 +4,7 @@ Vector Search is a new feature added to {cass-50}. It is a powerful technique for finding relevant content within large datasets and is particularly useful for AI applications. -Vector Search also makes use of xref:cassandra:developing/cql/indexing/sai/overview.adoc[Storage-Attached Indexes(SAI)], leveraging the new modularity of the latter feature. +Vector Search also makes use of xref:cassandra:developing/cql/indexing/sai/sai-overview.adoc[Storage-Attached Indexes(SAI)], leveraging the new modularity of the latter feature. Vector Search is the first instance of validating the extensibility of SAI. Data stored in a database is useful, but the context of that data is critical to applications. diff --git a/doc/modules/cassandra/partials/compact-subproperties.adoc b/doc/modules/cassandra/partials/compact-subproperties.adoc index 1cee84b7d296..1ca766211385 100644 --- a/doc/modules/cassandra/partials/compact-subproperties.adoc +++ b/doc/modules/cassandra/partials/compact-subproperties.adoc @@ -108,7 +108,7 @@ Default: `32` The compaction class `SizeTieredCompactionStrategy` (STCS) triggers a minor compaction when table meets the `min_threshold`. Minor compactions do not involve all the tables in a keyspace. See -xref:operating/compaction/stcs.adoc#stcs_options[SizeTieredCompactionStrategy (STCS)]. +xref:cassandra:managing/operating/compaction/stcs.adoc#stcs_options[SizeTieredCompactionStrategy (STCS)]. [NOTE] ==== @@ -151,7 +151,7 @@ Default: `50` (MB) [NOTE] ==== The `cold_reads_to_omit` property for -xref:operating/compaction/stcs.adoc#stcs_options[SizeTieredCompactionStrategy (STCS)] is no longer supported. +xref:cassandra:managing/operating/compaction/stcs.adoc#stcs_options[SizeTieredCompactionStrategy (STCS)] is no longer supported. ==== [[TWCS]] @@ -162,7 +162,7 @@ TWCS creates a new time window within each successive time period. During the active time window, TWCS compacts all SSTables flushed from memory into larger SSTables using STCS. At the end of the time period, all of these SSTables are compacted into a single SSTable. Then the next time window starts and the process repeats. -See xref:operating/compaction/twcs.adoc#twcs_options[TimeWindowCompactionStrategy (TWCS)]. +See xref:cassandra:managing/operating/compaction/twcs.adoc#twcs_options[TimeWindowCompactionStrategy (TWCS)]. [NOTE] ==== @@ -228,7 +228,7 @@ Default: `160` ==== The default value, 160 MB, may be inefficient and negatively impact database indexing and the queries that rely on indexes. For example, consider the benefit of using higher values for sstable_size_in_mb in tables that use (SAI) indexes. -For related information, see xref:developing/cql/indexing/sai/configuring.adoc#saiConfigure__saiCompactionStrategies[Compaction strategies]. +For related information, see xref:cassandra:developing/cql/indexing/sai/operations/configuring.adoc#saiConfigure__saiCompactionStrategies[Compaction strategies]. ==== ==== DateTieredCompactionStrategy (deprecated) diff --git a/doc/modules/cassandra/partials/index-naming.adoc b/doc/modules/cassandra/partials/index-naming.adoc new file mode 100644 index 000000000000..9e7977346387 --- /dev/null +++ b/doc/modules/cassandra/partials/index-naming.adoc @@ -0,0 +1,6 @@ +[WARNING] +==== +Index names are unique per keyspace. +You cannot use the same index name for two different indexes within a keyspace, regardless of which +table they are on. +==== diff --git a/doc/modules/cassandra/partials/primary-key-column.adoc b/doc/modules/cassandra/partials/primary-key-column.adoc index 9abe065f6418..d9f49bb08092 100644 --- a/doc/modules/cassandra/partials/primary-key-column.adoc +++ b/doc/modules/cassandra/partials/primary-key-column.adoc @@ -17,7 +17,7 @@ Use a unique name for each column in a table. To preserve case or use special characters, enclose the name in double-quotes. cql_type_definition :: Defines the type of data allowed in the column. -See xref:reference:data-types.adoc[CQL data type] or a xref:reference:user-defined-type.adoc[user-defined type]. +See xref:cassandra:developing/cql/types.adoc#native-types[CQL data type] or a xref:cassandra:developing/cql/types.adoc#udts[user-defined type]. *STATIC* :: Optional, the column has a single value. *PRIMARY KEY* :: diff --git a/doc/modules/cassandra/partials/table-column-definitions.adoc b/doc/modules/cassandra/partials/table-column-definitions.adoc index 2a5e3e40df5a..bb981226aadb 100644 --- a/doc/modules/cassandra/partials/table-column-definitions.adoc +++ b/doc/modules/cassandra/partials/table-column-definitions.adoc @@ -17,7 +17,7 @@ Use a unique name for each column in a table. To preserve case or use special characters, enclose the name in double-quotes. cql_type_definition :: Defines the type of data allowed in the column. -See xref:reference:data-types.adoc[CQL data type] or a xref:reference:user-defined-type.adoc[user-defined type]. +See xref:cassandra:developing/cql/types.adoc#native-types[CQL data type] or a xref:cassandra:developing/cql/types.adoc#udts[user-defined type]. *STATIC* :: Optional, the column has a single value. *PRIMARY KEY* :: diff --git a/doc/modules/cassandra/partials/table-properties.adoc b/doc/modules/cassandra/partials/table-properties.adoc index 9aa6f16d3da5..389dbe15bbbf 100644 --- a/doc/modules/cassandra/partials/table-properties.adoc +++ b/doc/modules/cassandra/partials/table-properties.adoc @@ -96,8 +96,8 @@ However, if you lower the `gc_grace_seconds` value, consider its interaction wit * *hint replays*: When a node goes down and then comes back up, other nodes replay the write operations (called xref:managing/operating/hints.adoc[hints]) that are queued for that node while it was unresponsive. The database does not replay hints older than gc_grace_seconds after creation. -The xref:managing/configuration/configuration/cass_yaml_file.adoc#max_hint_window[max_hint_window] setting in the -xref:managing/configuration/configuration/cass_yaml_file.adoc[cassandra.yaml] file sets the time limit (3 hours by default) for collecting hints for the unresponsive node. +The xref:cassandra:managing/configuration/cass_yaml_file.adoc#max_hint_window[max_hint_window] setting in the +xref:cassandra:managing/configuration/cass_yaml_file.adoc[cassandra.yaml] file sets the time limit (3 hours by default) for collecting hints for the unresponsive node. * *batch replays*: Like hint queues, xref:developing/cql/ddl.adoc#batch_statement[batch operations] store database mutations that are replayed in sequence. As with hints, the database does not replay a batched mutation older than gc_grace_seconds after creation. diff --git a/doc/modules/cassandra/partials/vector-search/vector_functions.adoc b/doc/modules/cassandra/partials/vector-search/vector_functions.adoc index daa4b2b8ce22..e73fc628b466 100644 --- a/doc/modules/cassandra/partials/vector-search/vector_functions.adoc +++ b/doc/modules/cassandra/partials/vector-search/vector_functions.adoc @@ -30,12 +30,38 @@ Examples: Examples: -`similarity_dot_product([0.1, 0.2], null)` -> `null` +`similarity_dot_product([0.447214, 0.894427], null)` -> `null` -`similarity_dot_product([0.1, 0.2], [0.1, 0.2])` -> `0.525` +`similarity_dot_product([0.447214, 0.894427], [0.447214, 0.894427])` -> `1` -`similarity_dot_product([0.1, 0.2], [-0.1, -0.2])` -> `0.475` +`similarity_dot_product([0.447214, 0.894427], [-0.447214, -0.894427])` -> `0` -`similarity_dot_product([0.1, 0.2], [0.9, 0.8])` -> `0.625` +`similarity_dot_product([0.447214, 0.894427], [-0.447214, 0.894427])` -> `0.8` + +`similarity_dot_product([0.447214, 0.894427], [0.447214, -0.894427])` -> `0.2` + +| `random_float_vector(int, float, float)` | Returns a new vector of floats with the specified dimension and where +all components will be in the specified min-max range. + +Examples: + +`random_float_vector(2, -1.0, 1.0)` -> `[-0.695395, -0.395755]` + +`random_float_vector(2, -1.0, 1.0)` -> `[-0.58795, 0.690014]` + +`random_float_vector(2, 0.0, 1.0)` -> `[0.423859, 0.630168]` + +`random_float_vector(2, 0.0, 1.0)` -> `[0.468159, 0.283808]` + +| `normalize_l2(vector)` | Applies L2 normalization to the input vector. +The result is a vector with the same direction but with a magnitude of 1. + +Examples: + +`normalize_l2([0.1])` -> `[1]` + +`normalize_l2([-0.7])` -> `[1]` + +`normalize_l2([3.0, 4.0])` -> `[0.6, 0.8]` |=== \ No newline at end of file diff --git a/doc/native_protocol_v4.1.spec b/doc/native_protocol_v4.1.spec new file mode 100644 index 000000000000..a10fd2404d8f --- /dev/null +++ b/doc/native_protocol_v4.1.spec @@ -0,0 +1,1212 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + CQL BINARY PROTOCOL v4.1 + + +Table of Contents + + 1. Overview + 2. Frame header + 2.1. version + 2.2. flags + 2.3. stream + 2.4. opcode + 2.5. length + 3. Notations + 4. Messages + 4.1. Requests + 4.1.1. STARTUP + 4.1.2. AUTH_RESPONSE + 4.1.3. OPTIONS + 4.1.4. QUERY + 4.1.5. PREPARE + 4.1.6. EXECUTE + 4.1.7. BATCH + 4.1.8. REGISTER + 4.2. Responses + 4.2.1. ERROR + 4.2.2. READY + 4.2.3. AUTHENTICATE + 4.2.4. SUPPORTED + 4.2.5. RESULT + 4.2.5.1. Void + 4.2.5.2. Rows + 4.2.5.3. Set_keyspace + 4.2.5.4. Prepared + 4.2.5.5. Schema_change + 4.2.6. EVENT + 4.2.7. AUTH_CHALLENGE + 4.2.8. AUTH_SUCCESS + 5. Compression + 6. Data Type Serialization Formats + 7. User Defined Type Serialization + 8. Result paging + 9. Error codes + 10. Changes from v4 + + +1. Overview + + The CQL binary protocol is a frame based protocol. Frames are defined as: + + 0 8 16 24 32 40 + +---------+---------+---------+---------+---------+ + | version | flags | stream | opcode | + +---------+---------+---------+---------+---------+ + | length | + +---------+---------+---------+---------+ + | | + . ... body ... . + . . + . . + +---------------------------------------- + + The protocol is big-endian (network byte order). + + Each frame contains a fixed size header (9 bytes) followed by a variable size + body. The header is described in Section 2. The content of the body depends + on the header opcode value (the body can in particular be empty for some + opcode values). The list of allowed opcodes is defined in Section 2.4 and the + details of each corresponding message are described Section 4. + + The protocol distinguishes two types of frames: requests and responses. Requests + are those frames sent by the client to the server. Responses are those frames sent + by the server to the client. Note, however, that the protocol supports server pushes + (events) so a response does not necessarily come right after a client request. + + Note to client implementors: client libraries should always assume that the + body of a given frame may contain more data than what is described in this + document. It will however always be safe to ignore the remainder of the frame + body in such cases. The reason is that this may enable extending the protocol + with optional features without needing to change the protocol version. + + + +2. Frame header + +2.1. version + + The version is a single byte that indicates both the direction of the message + (request or response) and the version of the protocol in use. The most + significant bit of version is used to define the direction of the message: + 0 indicates a request, 1 indicates a response. This can be useful for protocol + analyzers to distinguish the nature of the packet from the direction in which + it is moving. The rest of that byte is the protocol version (4 for the protocol + defined in this document). In other words, for this version of the protocol, + version will be one of: + 0x04 Request frame for this protocol version + 0x84 Response frame for this protocol version + + Please note that while every message ships with the version, only one version + of messages is accepted on a given connection. In other words, the first message + exchanged (STARTUP) sets the version for the connection for the lifetime of this + connection. + + This document describes version 4 of the protocol. For the changes made since + version 3, see Section 10. + + +2.2. flags + + Flags applying to this frame. The flags have the following meaning (described + by the mask that allows selecting them): + 0x01: Compression flag. If set, the frame body is compressed. The actual + compression to use should have been set up beforehand through the + Startup message (which thus cannot be compressed; Section 4.1.1). + 0x02: Tracing flag. For a request frame, this indicates the client requires + tracing of the request. Note that only QUERY, PREPARE and EXECUTE queries + support tracing. Other requests will simply ignore the tracing flag if + set. If a request supports tracing and the tracing flag is set, the response + to this request will have the tracing flag set and contain tracing + information. + If a response frame has the tracing flag set, its body contains + a tracing ID. The tracing ID is a [uuid] and is the first thing in + the frame body. + 0x04: Custom payload flag. For a request or response frame, this indicates + that a generic key-value custom payload for a custom QueryHandler + implementation is present in the frame. Such a custom payload is simply + ignored by the default QueryHandler implementation. + Currently, only QUERY, PREPARE, EXECUTE and BATCH requests support + payload. + Type of custom payload is [bytes map] (see below). If either or both + of the tracing and warning flags are set, the custom payload will follow + those indicated elements in the frame body. If neither are set, the custom + payload will be the first value in the frame body. + 0x08: Warning flag. The response contains warnings which were generated by the + server to go along with this response. + If a response frame has the warning flag set, its body will contain the + text of the warnings. The warnings are a [string list] and will be the + first value in the frame body if the tracing flag is not set, or directly + after the tracing ID if it is. + + The rest of flags is currently unused and ignored. + +2.3. stream + + A frame has a stream id (a [short] value). When sending request messages, this + stream id must be set by the client to a non-negative value (negative stream id + are reserved for streams initiated by the server; currently all EVENT messages + (section 4.2.6) have a streamId of -1). If a client sends a request message + with the stream id X, it is guaranteed that the stream id of the response to + that message will be X. + + This helps to enable the asynchronous nature of the protocol. If a client + sends multiple messages simultaneously (without waiting for responses), there + is no guarantee on the order of the responses. For instance, if the client + writes REQ_1, REQ_2, REQ_3 on the wire (in that order), the server might + respond to REQ_3 (or REQ_2) first. Assigning different stream ids to these 3 + requests allows the client to distinguish to which request a received answer + responds to. As there can only be 32768 different simultaneous streams, it is up + to the client to reuse stream id. + + Note that clients are free to use the protocol synchronously (i.e. wait for + the response to REQ_N before sending REQ_N+1). In that case, the stream id + can be safely set to 0. Clients should also feel free to use only a subset of + the 32768 maximum possible stream ids if it is simpler for its implementation. + +2.4. opcode + + An integer byte that distinguishes the actual message: + 0x00 ERROR + 0x01 STARTUP + 0x02 READY + 0x03 AUTHENTICATE + 0x05 OPTIONS + 0x06 SUPPORTED + 0x07 QUERY + 0x08 RESULT + 0x09 PREPARE + 0x0A EXECUTE + 0x0B REGISTER + 0x0C EVENT + 0x0D BATCH + 0x0E AUTH_CHALLENGE + 0x0F AUTH_RESPONSE + 0x10 AUTH_SUCCESS + + Messages are described in Section 4. + + (Note that there is no 0x04 message in this version of the protocol) + + +2.5. length + + A 4 byte integer representing the length of the body of the frame (note: + currently a frame is limited to 256MB in length). + + +3. Notations + + To describe the layout of the frame body for the messages in Section 4, we + define the following: + + [int] A 4 bytes integer + [long] A 8 bytes integer + [short] A 2 bytes unsigned integer + [string] A [short] n, followed by n bytes representing an UTF-8 + string. + [long string] An [int] n, followed by n bytes representing an UTF-8 string. + [uuid] A 16 bytes long uuid. + [string list] A [short] n, followed by n [string]. + [bytes] A [int] n, followed by n bytes if n >= 0. If n < 0, + no byte should follow and the value represented is `null`. + [value] A [int] n, followed by n bytes if n >= 0. + If n == -1 no byte should follow and the value represented is `null`. + If n == -2 no byte should follow and the value represented is + `not set` not resulting in any change to the existing value. + n < -2 is an invalid value and results in an error. + [short bytes] A [short] n, followed by n bytes if n >= 0. + + [option] A pair of where is a [short] representing + the option id and depends on that option (and can be + of size 0). The supported id (and the corresponding ) + will be described when this is used. + [option list] A [short] n, followed by n [option]. + [inet] An address (ip and port) to a node. It consists of one + [byte] n, that represents the address size, followed by n + [byte] representing the IP address (in practice n can only be + either 4 (IPv4) or 16 (IPv6)), following by one [int] + representing the port. + [consistency] A consistency level specification. This is a [short] + representing a consistency level with the following + correspondance: + 0x0000 ANY + 0x0001 ONE + 0x0002 TWO + 0x0003 THREE + 0x0004 QUORUM + 0x0005 ALL + 0x0006 LOCAL_QUORUM + 0x0007 EACH_QUORUM + 0x0008 SERIAL + 0x0009 LOCAL_SERIAL + 0x000A LOCAL_ONE + + [string map] A [short] n, followed by n pair where and + are [string]. + [string multimap] A [short] n, followed by n pair where is a + [string] and is a [string list]. + [bytes map] A [short] n, followed by n pair where is a + [string] and is a [bytes]. + + +4. Messages + + Dependant on the flags specified in the header, the layout of the message body must be: + [][][] + where: + - is a UUID tracing ID, present if this is a request message and the Tracing flag is set. + - is a string list of warnings (if this is a request message and the Warning flag is set. + - is bytes map for the serialised custom payload present if this is one of the message types + which support custom payloads (QUERY, PREPARE, EXECUTE and BATCH) and the Custom payload flag is set. + - as defined below through sections 4 and 5. + +4.1. Requests + + Note that outside of their normal responses (described below), all requests + can get an ERROR message (Section 4.2.1) as response. + +4.1.1. STARTUP + + Initialize the connection. The server will respond by either a READY message + (in which case the connection is ready for queries) or an AUTHENTICATE message + (in which case credentials will need to be provided using AUTH_RESPONSE). + + This must be the first message of the connection, except for OPTIONS that can + be sent before to find out the options supported by the server. Once the + connection has been initialized, a client should not send any more STARTUP + messages. + + The body is a [string map] of options. Possible options are: + - "CQL_VERSION": the version of CQL to use. This option is mandatory and + currently the only version supported is "3.0.0". Note that this is + different from the protocol version. + - "COMPRESSION": the compression algorithm to use for frames (See section 5). + This is optional; if not specified no compression will be used. + - "NO_COMPACT": whether or not connection has to be established in compatibility + mode. This mode will make all Thrift and Compact Tables to be exposed as if + they were CQL Tables. This is optional; if not specified, the option will + not be used. + - "THROW_ON_OVERLOAD": In case of server overloaded with too many requests, by default the server puts + back pressure on the client connection. Instead, the server can send an OverloadedException error message back to + the client if this option is set to true. + - "PAGE_UNIT": a list of supported page units. + + +4.1.2. AUTH_RESPONSE + + Answers a server authentication challenge. + + Authentication in the protocol is SASL based. The server sends authentication + challenges (a bytes token) to which the client answers with this message. Those + exchanges continue until the server accepts the authentication by sending a + AUTH_SUCCESS message after a client AUTH_RESPONSE. Note that the exchange + begins with the client sending an initial AUTH_RESPONSE in response to a + server AUTHENTICATE request. + + The body of this message is a single [bytes] token. The details of what this + token contains (and when it can be null/empty, if ever) depends on the actual + authenticator used. + + The response to a AUTH_RESPONSE is either a follow-up AUTH_CHALLENGE message, + an AUTH_SUCCESS message or an ERROR message. + + +4.1.3. OPTIONS + + Asks the server to return which STARTUP options are supported. The body of an + OPTIONS message should be empty and the server will respond with a SUPPORTED + message. + + +4.1.4. QUERY + + Performs a CQL query. The body of the message must be: + + where is a [long string] representing the query and + must be + [[name_1]...[name_n]][][][][] + where: + - is the [consistency] level for the operation. + - is a [byte] whose bits define the options for this query and + in particular influence what the remainder of the message contains. + A flag is set if the bit corresponding to its `mask` is set. Supported + flags are, given their mask: + 0x00000001: Values. If set, a [short] followed by [value] + values are provided. Those values are used for bound variables in + the query. Optionally, if the 0x40 flag is present, each value + will be preceded by a [string] name, representing the name of + the marker the value must be bound to. + 0x00000002: Skip_metadata. If set, the Result Set returned as a response + to the query (if any) will have the NO_METADATA flag (see + Section 4.2.5.2). + 0x00000004: Page_size. If set, is an [int] + controlling the desired page size of the result (in CQL3 rows or bytes). + See the section on paging (Section 8) for more details. + 0x00000008: With_paging_state. If set, should be present. + is a [bytes] value that should have been returned + in a result set (Section 4.2.5.2). The query will be + executed but starting from a given paging state. This is also to + continue paging on a different node than the one where it + started (See Section 8 for more details). + 0x00000010: With serial consistency. If set, should be + present. is the [consistency] level for the + serial phase of conditional updates. That consitency can only be + either SERIAL or LOCAL_SERIAL and if not present, it defaults to + SERIAL. This option will be ignored for anything else other than a + conditional update/insert. + 0x00000020: With default timestamp. If set, should be present. + is a [long] representing the default timestamp for the query + in microseconds (negative values are forbidden). This will + replace the server side assigned timestamp as default timestamp. + Note that a timestamp in the query itself will still override + this timestamp. This is entirely optional. + 0x00000040: With names for values. This only makes sense if the 0x01 flag is set and + is ignored otherwise. If present, the values from the 0x01 flag will + be preceded by a name (see above). Note that this is only useful for + QUERY requests where named bind markers are used; for EXECUTE statements, + since the names for the expected values was returned during preparation, + a client can always provide values in the right order without any names + and using this flag, while supported, is almost surely inefficient. + 0x40000000: When set, the is provided in bytes rather than in rows. + + + Note that the consistency is ignored by some queries (USE, CREATE, ALTER, + TRUNCATE, ...). + + The server will respond to a QUERY message with a RESULT message, the content + of which depends on the query. + + +4.1.5. PREPARE + + Prepare a query for later execution (through EXECUTE). The body consists of + the CQL query to prepare as a [long string]. + + The server will respond with a RESULT message with a `prepared` kind (0x0004, + see Section 4.2.5). + + +4.1.6. EXECUTE + + Executes a prepared query. The body of the message must be: + + where is the prepared query ID. It's the [short bytes] returned as a + response to a PREPARE message. As for , it has the exact + same definition as in QUERY (see Section 4.1.4). + + The response from the server will be a RESULT message. + + +4.1.7. BATCH + + Allows executing a list of queries (prepared or not) as a batch (note that + only DML statements are accepted in a batch). The body of the message must + be: + ...[][] + where: + - is a [byte] indicating the type of batch to use: + - If == 0, the batch will be "logged". This is equivalent to a + normal CQL3 batch statement. + - If == 1, the batch will be "unlogged". + - If == 2, the batch will be a "counter" batch (and non-counter + statements will be rejected). + - is a [byte] whose bits define the options for this query and + in particular influence what the remainder of the message contains. It is similar + to the from QUERY and EXECUTE methods, except that the 4 rightmost + bits must always be 0 as their corresponding options do not make sense for + Batch. A flag is set if the bit corresponding to its `mask` is set. Supported + flags are, given their mask: + 0x10: With serial consistency. If set, should be + present. is the [consistency] level for the + serial phase of conditional updates. That consistency can only be + either SERIAL or LOCAL_SERIAL and if not present, it defaults to + SERIAL. This option will be ignored for anything else other than a + conditional update/insert. + 0x20: With default timestamp. If set, should be present. + is a [long] representing the default timestamp for the query + in microseconds. This will replace the server side assigned + timestamp as default timestamp. Note that a timestamp in the query itself + will still override this timestamp. This is entirely optional. + 0x40: With names for values. If set, then all values for all must be + preceded by a [string] that have the same meaning as in QUERY + requests [IMPORTANT NOTE: this feature does not work and should not be + used. It is specified in a way that makes it impossible for the server + to implement. This will be fixed in a future version of the native + protocol. See https://issues.apache.org/jira/browse/CASSANDRA-10246 for + more details]. + - is a [short] indicating the number of following queries. + - ... are the queries to execute. A must be of the + form: + []...[] + where: + - is a [byte] indicating whether the following query is a prepared + one or not. value must be either 0 or 1. + - depends on the value of . If == 0, it should be + a [long string] query string (as in QUERY, the query string might contain + bind markers). Otherwise (that is, if == 1), it should be a + [short bytes] representing a prepared query ID. + - is a [short] indicating the number (possibly 0) of following values. + - is the optional name of the following . It must be present + if and only if the 0x40 flag is provided for the batch. + - is the [value] to use for bound variable i (of bound variable + if the 0x40 flag is used). + - is the [consistency] level for the operation. + - is only present if the 0x10 flag is set. In that case, + is the [consistency] level for the serial phase of + conditional updates. That consitency can only be either SERIAL or + LOCAL_SERIAL and if not present will defaults to SERIAL. This option will + be ignored for anything else other than a conditional update/insert. + + The server will respond with a RESULT message. + + +4.1.8. REGISTER + + Register this connection to receive some types of events. The body of the + message is a [string list] representing the event types to register for. See + section 4.2.6 for the list of valid event types. + + The response to a REGISTER message will be a READY message. + + Please note that if a client driver maintains multiple connections to a + Cassandra node and/or connections to multiple nodes, it is advised to + dedicate a handful of connections to receive events, but to *not* register + for events on all connections, as this would only result in receiving + multiple times the same event messages, wasting bandwidth. + + +4.2. Responses + + This section describes the content of the frame body for the different + responses. Please note that to make room for future evolution, clients should + support extra informations (that they should simply discard) to the one + described in this document at the end of the frame body. + +4.2.1. ERROR + + Indicates an error processing a request. The body of the message will be an + error code ([int]) followed by a [string] error message. Then, depending on + the exception, more content may follow. The error codes are defined in + Section 9, along with their additional content if any. + + +4.2.2. READY + + Indicates that the server is ready to process queries. This message will be + sent by the server either after a STARTUP message if no authentication is + required (if authentication is required, the server indicates readiness by + sending a AUTH_RESPONSE message). + + The body of a READY message is empty. + + +4.2.3. AUTHENTICATE + + Indicates that the server requires authentication, and which authentication + mechanism to use. + + The authentication is SASL based and thus consists of a number of server + challenges (AUTH_CHALLENGE, Section 4.2.7) followed by client responses + (AUTH_RESPONSE, Section 4.1.2). The initial exchange is however boostrapped + by an initial client response. The details of that exchange (including how + many challenge-response pairs are required) are specific to the authenticator + in use. The exchange ends when the server sends an AUTH_SUCCESS message or + an ERROR message. + + This message will be sent following a STARTUP message if authentication is + required and must be answered by a AUTH_RESPONSE message from the client. + + The body consists of a single [string] indicating the full class name of the + IAuthenticator in use. + + +4.2.4. SUPPORTED + + Indicates which startup options are supported by the server. This message + comes as a response to an OPTIONS message. + + The body of a SUPPORTED message is a [string multimap]. This multimap gives + for each of the supported STARTUP options, the list of supported values. + + +4.2.5. RESULT + + The result to a query (QUERY, PREPARE, EXECUTE or BATCH messages). + + The first element of the body of a RESULT message is an [int] representing the + `kind` of result. The rest of the body depends on the kind. The kind can be + one of: + 0x0001 Void: for results carrying no information. + 0x0002 Rows: for results to select queries, returning a set of rows. + 0x0003 Set_keyspace: the result to a `use` query. + 0x0004 Prepared: result to a PREPARE message. + 0x0005 Schema_change: the result to a schema altering query. + + The body for each kind (after the [int] kind) is defined below. + + +4.2.5.1. Void + + The rest of the body for a Void result is empty. It indicates that a query was + successful without providing more information. + + +4.2.5.2. Rows + + Indicates a set of rows. The rest of the body of a Rows result is: + + where: + - is composed of: + [][?...] + where: + - is an [int]. The bits of provides information on the + formatting of the remaining information. A flag is set if the bit + corresponding to its `mask` is set. Supported flags are, given their + mask: + 0x0001 Global_tables_spec: if set, only one table spec (keyspace + and table name) is provided as . If not + set, is not present. + 0x0002 Has_more_pages: indicates whether this is not the last + page of results and more should be retrieved. If set, the + will be present. The is a + [bytes] value that should be used in QUERY/EXECUTE to + continue paging and retrieve the remainder of the result for + this query (See Section 8 for more details). + 0x0004 No_metadata: if set, the is only composed of + these , the and optionally the + (depending on the Has_more_pages flag) but + no other information (so no nor ). + This will only ever be the case if this was requested + during the query (see QUERY and RESULT messages). + - is an [int] representing the number of columns selected + by the query that produced this result. It defines the number of + elements in and the number of elements for each row in . + - is present if the Global_tables_spec is set in + . It is composed of two [string] representing the + (unique) keyspace name and table name the columns belong to. + - specifies the columns returned in the query. There are + such column specifications that are composed of: + ()? + The initial and are two [string] and are only present + if the Global_tables_spec flag is not set. The is a + [string] and is an [option] that corresponds to the description + (what this description is depends a bit on the context: in results to + selects, this will be either the user chosen alias or the selection used + (often a colum name, but it can be a function call too). In results to + a PREPARE, this will be either the name of the corresponding bind variable + or the column name for the variable if it is "anonymous") and type of + the corresponding result. The option for is either a native + type (see below), in which case the option has no value, or a + 'custom' type, in which case the value is a [string] representing + the fully qualified class name of the type represented. Valid option + ids are: + 0x0000 Custom: the value is a [string], see above. + 0x0001 Ascii + 0x0002 Bigint + 0x0003 Blob + 0x0004 Boolean + 0x0005 Counter + 0x0006 Decimal + 0x0007 Double + 0x0008 Float + 0x0009 Int + 0x000B Timestamp + 0x000C Uuid + 0x000D Varchar + 0x000E Varint + 0x000F Timeuuid + 0x0010 Inet + 0x0011 Date + 0x0012 Time + 0x0013 Smallint + 0x0014 Tinyint + 0x0020 List: the value is an [option], representing the type + of the elements of the list. + 0x0021 Map: the value is two [option], representing the types of the + keys and values of the map + 0x0022 Set: the value is an [option], representing the type + of the elements of the set + 0x0030 UDT: the value is ... + where: + - is a [string] representing the keyspace name this + UDT is part of. + - is a [string] representing the UDT name. + - is a [short] representing the number of fields of + the UDT, and thus the number of pairs + following + - is a [string] representing the name of the + i_th field of the UDT. + - is an [option] representing the type of the + i_th field of the UDT. + 0x0031 Tuple: the value is ... where is a [short] + representing the number of values in the type, and + are [option] representing the type of the i_th component + of the tuple + + - is an [int] representing the number of rows present in this + result. Those rows are serialized in the part. + - is composed of ... where m is . + Each is composed of ... where n is + and where is a [bytes] representing the value + returned for the jth column of the ith row. In other words, + is composed of ( * ) [bytes]. + + +4.2.5.3. Set_keyspace + + The result to a `use` query. The body (after the kind [int]) is a single + [string] indicating the name of the keyspace that has been set. + + +4.2.5.4. Prepared + + The result to a PREPARE message. The body of a Prepared result is: + + where: + - is [short bytes] representing the prepared query ID. + - is composed of: + [...][?...] + where: + - is an [int]. The bits of provides information on the + formatting of the remaining information. A flag is set if the bit + corresponding to its `mask` is set. Supported masks and their flags + are: + 0x0001 Global_tables_spec: if set, only one table spec (keyspace + and table name) is provided as . If not + set, is not present. + - is an [int] representing the number of bind markers + in the prepared statement. It defines the number of + elements. + - is an [int] representing the number of + elements to follow. If this value is zero, at least one of the + partition key columns in the table that the statement acts on + did not have a corresponding bind marker (or the bind marker + was wrapped in a function call). + - is a short that represents the index of the bind marker + that corresponds to the partition key column in position i. + For example, a sequence of [2, 0, 1] indicates that the + table has three partition key columns; the full partition key + can be constructed by creating a composite of the values for + the bind markers at index 2, at index 0, and at index 1. + This allows implementations with token-aware routing to correctly + construct the partition key without needing to inspect table + metadata. + - is present if the Global_tables_spec is set in + . If present, it is composed of two [string]s. The first + [string] is the name of the keyspace that the statement acts on. + The second [string] is the name of the table that the columns + represented by the bind markers belong to. + - specifies the bind markers in the prepared statement. + There are such column specifications, each with the + following format: + ()? + The initial and are two [string] that are only + present if the Global_tables_spec flag is not set. The field + is a [string] that holds the name of the bind marker (if named), + or the name of the column, field, or expression that the bind marker + corresponds to (if the bind marker is "anonymous"). The + field is an [option] that represents the expected type of values for + the bind marker. See the Rows documentation (section 4.2.5.2) for + full details on the field. + + - is defined exactly the same as in the Rows + documentation (section 4.2.5.2). This describes the metadata for the + result set that will be returned when this prepared statement is executed. + Note that may be empty (have the No_metadata flag and + 0 columns, See section 4.2.5.2) and will be for any query that is not a + Select. In fact, there is never a guarantee that this will be non-empty, so + implementations should protect themselves accordingly. This result metadata + is an optimization that allows implementations to later execute the + prepared statement without requesting the metadata (see the Skip_metadata + flag in EXECUTE). Clients can safely discard this metadata if they do not + want to take advantage of that optimization. + + Note that the prepared query ID returned is global to the node on which the query + has been prepared. It can be used on any connection to that node + until the node is restarted (after which the query must be reprepared). + +4.2.5.5. Schema_change + + The result to a schema altering query (creation/update/drop of a + keyspace/table/index). The body (after the kind [int]) is the same + as the body for a "SCHEMA_CHANGE" event, so 3 strings: + + Please refer to section 4.2.6 below for the meaning of those fields. + + Note that a query to create or drop an index is considered to be a change + to the table the index is on. + + +4.2.6. EVENT + + An event pushed by the server. A client will only receive events for the + types it has REGISTERed to. The body of an EVENT message will start with a + [string] representing the event type. The rest of the message depends on the + event type. The valid event types are: + - "TOPOLOGY_CHANGE": events related to change in the cluster topology. + Currently, events are sent when new nodes are added to the cluster, and + when nodes are removed. The body of the message (after the event type) + consists of a [string] and an [inet], corresponding respectively to the + type of change ("NEW_NODE" or "REMOVED_NODE") followed by the address of + the new/removed node. + - "STATUS_CHANGE": events related to change of node status. Currently, + up/down events are sent. The body of the message (after the event type) + consists of a [string] and an [inet], corresponding respectively to the + type of status change ("UP" or "DOWN") followed by the address of the + concerned node. + - "SCHEMA_CHANGE": events related to schema change. After the event type, + the rest of the message will be where: + - is a [string] representing the type of changed involved. + It will be one of "CREATED", "UPDATED" or "DROPPED". + - is a [string] that can be one of "KEYSPACE", "TABLE", "TYPE", + "FUNCTION" or "AGGREGATE" and describes what has been modified + ("TYPE" stands for modifications related to user types, "FUNCTION" + for modifications related to user defined functions, "AGGREGATE" + for modifications related to user defined aggregates). + - depends on the preceding : + - If is "KEYSPACE", then will be a single [string] + representing the keyspace changed. + - If is "TABLE" or "TYPE", then + will be 2 [string]: the first one will be the keyspace + containing the affected object, and the second one will be the name + of said affected object (either the table, user type, function, or + aggregate name). + - If is "FUNCTION" or "AGGREGATE", multiple arguments follow: + - [string] keyspace containing the user defined function / aggregate + - [string] the function/aggregate name + - [string list] one string for each argument type (as CQL type) + + All EVENT messages have a streamId of -1 (Section 2.3). + + Please note that "NEW_NODE" and "UP" events are sent based on internal Gossip + communication and as such may be sent a short delay before the binary + protocol server on the newly up node is fully started. Clients are thus + advised to wait a short time before trying to connect to the node (1 second + should be enough), otherwise they may experience a connection refusal at + first. + +4.2.7. AUTH_CHALLENGE + + A server authentication challenge (see AUTH_RESPONSE (Section 4.1.2) for more + details). + + The body of this message is a single [bytes] token. The details of what this + token contains (and when it can be null/empty, if ever) depends on the actual + authenticator used. + + Clients are expected to answer the server challenge with an AUTH_RESPONSE + message. + +4.2.8. AUTH_SUCCESS + + Indicates the success of the authentication phase. See Section 4.2.3 for more + details. + + The body of this message is a single [bytes] token holding final information + from the server that the client may require to finish the authentication + process. What that token contains and whether it can be null depends on the + actual authenticator used. + + +5. Compression + + Frame compression is supported by the protocol, but then only the frame body + is compressed (the frame header should never be compressed). + + Before being used, client and server must agree on a compression algorithm to + use, which is done in the STARTUP message. As a consequence, a STARTUP message + must never be compressed. However, once the STARTUP frame has been received + by the server, messages can be compressed (including the response to the STARTUP + request). Frames do not have to be compressed, however, even if compression has + been agreed upon (a server may only compress frames above a certain size at its + discretion). A frame body should be compressed if and only if the compressed + flag (see Section 2.2) is set. + + As of version 2 of the protocol, the following compressions are available: + - lz4 (https://code.google.com/p/lz4/). In that, note that the first four bytes + of the body will be the uncompressed length (followed by the compressed + bytes). + - snappy (https://code.google.com/p/snappy/). This compression might not be + available as it depends on a native lib (server-side) that might not be + avaivable on some installations. + + +6. Data Type Serialization Formats + + This sections describes the serialization formats for all CQL data types + supported by Cassandra through the native protocol. These serialization + formats should be used by client drivers to encode values for EXECUTE + messages. Cassandra will use these formats when returning values in + RESULT messages. + + All values are represented as [bytes] in EXECUTE and RESULT messages. + The [bytes] format includes an int prefix denoting the length of the value. + For that reason, the serialization formats described here will not include + a length component. + + For legacy compatibility reasons, note that most non-string types support + "empty" values (i.e. a value with zero length). An empty value is distinct + from NULL, which is encoded with a negative length. + + As with the rest of the native protocol, all encodings are big-endian. + +6.1. ascii + + A sequence of bytes in the ASCII range [0, 127]. Bytes with values outside of + this range will result in a validation error. + +6.2 bigint + + An eight-byte two's complement integer. + +6.3 blob + + Any sequence of bytes. + +6.4 boolean + + A single byte. A value of 0 denotes "false"; any other value denotes "true". + (However, it is recommended that a value of 1 be used to represent "true".) + +6.5 date + + An unsigned integer representing days with epoch centered at 2^31. + (unix epoch January 1st, 1970). + A few examples: + 0: -5877641-06-23 + 2^31: 1970-1-1 + 2^32: 5881580-07-11 + +6.6 decimal + + The decimal format represents an arbitrary-precision number. It contains an + [int] "scale" component followed by a varint encoding (see section 6.17) + of the unscaled value. The encoded value represents "E<-scale>". + In other words, " * 10 ^ (-1 * )". + +6.7 double + + An 8 byte floating point number in the IEEE 754 binary64 format. + +6.8 float + + A 4 byte floating point number in the IEEE 754 binary32 format. + +6.9 inet + + A 4 byte or 16 byte sequence denoting an IPv4 or IPv6 address, respectively. + +6.10 int + + A 4 byte two's complement integer. + +6.11 list + + A [int] n indicating the number of elements in the list, followed by n + elements. Each element is [bytes] representing the serialized value. + +6.12 map + + A [int] n indicating the number of key/value pairs in the map, followed by + n entries. Each entry is composed of two [bytes] representing the key + and value. + +6.13 set + + A [int] n indicating the number of elements in the set, followed by n + elements. Each element is [bytes] representing the serialized value. + +6.14 smallint + + A 2 byte two's complement integer. + +6.15 text + + A sequence of bytes conforming to the UTF-8 specifications. + +6.16 time + + An 8 byte two's complement long representing nanoseconds since midnight. + Valid values are in the range 0 to 86399999999999 + +6.17 timestamp + + An 8 byte two's complement integer representing a millisecond-precision + offset from the unix epoch (00:00:00, January 1st, 1970). Negative values + represent a negative offset from the epoch. + +6.18 timeuuid + + A 16 byte sequence representing a version 1 UUID as defined by RFC 4122. + +6.19 tinyint + + A 1 byte two's complement integer. + +6.20 tuple + + A sequence of [bytes] values representing the items in a tuple. The encoding + of each element depends on the data type for that position in the tuple. + Null values may be represented by using length -1 for the [bytes] + representation of an element. + +6.21 uuid + + A 16 byte sequence representing any valid UUID as defined by RFC 4122. + +6.22 varchar + + An alias of the "text" type. + +6.23 varint + + A variable-length two's complement encoding of a signed integer. + + The following examples may help implementors of this spec: + + Value | Encoding + ------|--------- + 0 | 0x00 + 1 | 0x01 + 127 | 0x7F + 128 | 0x0080 + 129 | 0x0081 + -1 | 0xFF + -128 | 0x80 + -129 | 0xFF7F + + Note that positive numbers must use a most-significant byte with a value + less than 0x80, because a most-significant bit of 1 indicates a negative + value. Implementors should pad positive values that have a MSB >= 0x80 + with a leading 0x00 byte. + + +7. User Defined Types + + This section describes the serialization format for User defined types (UDT), + as described in section 4.2.5.2. + + A UDT value is composed of successive [bytes] values, one for each field of the UDT + value (in the order defined by the type). A UDT value will generally have one value + for each field of the type it represents, but it is allowed to have less values than + the type has fields. + + +8. Result paging + + The protocol allows for paging the result of queries. For that, the QUERY and + EXECUTE messages have a value that indicate the desired + page size in CQL3 rows or bytes. + + If a positive value is provided for , the result set of the + RESULT message returned for the query will contain at most the + first rows or bytes of the query result. If that first page of results + contains the full result set for the query, the RESULT message (of kind `Rows`) + will have the Has_more_pages flag *not* set. However, if some results are not + part of the first response, the Has_more_pages flag will be set and the result + will contain a value. In that case, the value + should be used in a QUERY or EXECUTE message (that has the *same* query as + the original one or the behavior is undefined) to retrieve the next page of + results. + + Only CQL3 queries that return a result set (RESULT message with a Rows `kind`) + support paging. For other type of queries, the value is + ignored. + + In the previous protocol versions the page size was always provided in rows. Since 4.1 + the page size can be provided in bytes as well. Whether the page size is specified in + rows or bytes is controlled by query flags (see section 4.1.4 for details). + + Note to client implementors: + - While can be as low as 1, it will likely be detrimental + to performance to pick a value too low. A value below 100 is probably too + low for most use cases. + - Clients should not rely on the actual size of the result set returned to + decide if there are more results to fetch or not. Instead, they should always + check the Has_more_pages flag (unless they did not enable paging for the query + obviously). Clients should also not assert that no result will have more than + results. While the current implementation always respects + the exact value of , we reserve the right to return + slightly smaller or bigger pages in the future for performance reasons. + - The is specific to a protocol version and drivers should not + send a returned by a node using the protocol v3 to query a node + using the protocol v4 for instance. + + +9. Error codes + + Let us recall that an ERROR message is composed of [...] + (see 4.2.1 for details). The supported error codes, as well as any additional + information the message may contain after the are described below: + 0x0000 Server error: something unexpected happened. This indicates a + server-side bug. + 0x000A Protocol error: some client message triggered a protocol + violation (for instance a QUERY message is sent before a STARTUP + one has been sent) + 0x0100 Authentication error: authentication was required and failed. The + possible reason for failing depends on the authenticator in use, + which may or may not include more detail in the accompanying + error message. + 0x1000 Unavailable exception. The rest of the ERROR message body will be + + where: + is the [consistency] level of the query that triggered + the exception. + is an [int] representing the number of nodes that + should be alive to respect + is an [int] representing the number of replicas that + were known to be alive when the request had been + processed (since an unavailable exception has been + triggered, there will be < ) + 0x1001 Overloaded: the request cannot be processed because the + coordinator node is overloaded + 0x1002 Is_bootstrapping: the request was a read request but the + coordinator node is bootstrapping + 0x1003 Truncate_error: error during a truncation error. + 0x1100 Write_timeout: Timeout exception during a write request. The rest + of the ERROR message body will be + + where: + is the [consistency] level of the query having triggered + the exception. + is an [int] representing the number of nodes having + acknowledged the request. + is an [int] representing the number of replicas whose + acknowledgement is required to achieve . + is a [string] that describe the type of the write + that timed out. The value of that string can be one + of: + - "SIMPLE": the write was a non-batched + non-counter write. + - "BATCH": the write was a (logged) batch write. + If this type is received, it means the batch log + has been successfully written (otherwise a + "BATCH_LOG" type would have been sent instead). + - "UNLOGGED_BATCH": the write was an unlogged + batch. No batch log write has been attempted. + - "COUNTER": the write was a counter write + (batched or not). + - "BATCH_LOG": the timeout occurred during the + write to the batch log when a (logged) batch + write was requested. + - "CAS": the timeout occured during the Compare And Set write/update. + - "VIEW": the timeout occured when a write involves + VIEW update and failure to acqiure local view(MV) + lock for key within timeout + - "CDC": the timeout occured when cdc_total_space_in_mb is + exceeded when doing a write to data tracked by cdc. + 0x1200 Read_timeout: Timeout exception during a read request. The rest + of the ERROR message body will be + + where: + is the [consistency] level of the query having triggered + the exception. + is an [int] representing the number of nodes having + answered the request. + is an [int] representing the number of replicas whose + response is required to achieve . Please note that + it is possible to have >= if + is false. Also in the (unlikely) + case where is achieved but the coordinator node + times out while waiting for read-repair acknowledgement. + is a single byte. If its value is 0, it means + the replica that was asked for data has not + responded. Otherwise, the value is != 0. + 0x1300 Read_failure: A non-timeout exception during a read request. The rest + of the ERROR message body will be + + where: + is the [consistency] level of the query having triggered + the exception. + is an [int] representing the number of nodes having + answered the request. + is an [int] representing the number of replicas whose + acknowledgement is required to achieve . + is an [int] representing the number of nodes that + experience a failure while executing the request. + is a single byte. If its value is 0, it means + the replica that was asked for data had not + responded. Otherwise, the value is != 0. + 0x1400 Function_failure: A (user defined) function failed during execution. + The rest of the ERROR message body will be + + where: + is the keyspace [string] of the failed function + is the name [string] of the failed function + [string list] one string for each argument type (as CQL type) of the failed function + 0x1500 Write_failure: A non-timeout exception during a write request. The rest + of the ERROR message body will be + + where: + is the [consistency] level of the query having triggered + the exception. + is an [int] representing the number of nodes having + answered the request. + is an [int] representing the number of replicas whose + acknowledgement is required to achieve . + is an [int] representing the number of nodes that + experience a failure while executing the request. + is a [string] that describes the type of the write + that failed. The value of that string can be one + of: + - "SIMPLE": the write was a non-batched + non-counter write. + - "BATCH": the write was a (logged) batch write. + If this type is received, it means the batch log + has been successfully written (otherwise a + "BATCH_LOG" type would have been sent instead). + - "UNLOGGED_BATCH": the write was an unlogged + batch. No batch log write has been attempted. + - "COUNTER": the write was a counter write + (batched or not). + - "BATCH_LOG": the failure occured during the + write to the batch log when a (logged) batch + write was requested. + - "CAS": the failure occured during the Compare And Set write/update. + - "VIEW": the failure occured when a write involves + VIEW update and failure to acqiure local view(MV) + lock for key within timeout + - "CDC": the failure occured when cdc_total_space_in_mb is + exceeded when doing a write to data tracked by cdc. + + 0x2000 Syntax_error: The submitted query has a syntax error. + 0x2100 Unauthorized: The logged user doesn't have the right to perform + the query. + 0x2200 Invalid: The query is syntactically correct but invalid. + 0x2300 Config_error: The query is invalid because of some configuration issue + 0x2400 Already_exists: The query attempted to create a keyspace or a + table that was already existing. The rest of the ERROR message + body will be where: + is a [string] representing either the keyspace that + already exists, or the keyspace in which the table that + already exists is. +
is a [string] representing the name of the table that + already exists. If the query was attempting to create a + keyspace,
will be present but will be the empty + string. + 0x2500 Unprepared: Can be thrown while a prepared statement tries to be + executed if the provided prepared statement ID is not known by + this host. The rest of the ERROR message body will be [short + bytes] representing the unknown ID. + +10. Changes from v4 + + * Query flags (Section 4.1.4) includes a new flag 0x40000000 which denotes that + the page size is specified in bytes rather than in rows. diff --git a/doc/scripts/cqlprotodoc.py b/doc/scripts/cqlprotodoc.py new file mode 100755 index 000000000000..8abedd997c90 --- /dev/null +++ b/doc/scripts/cqlprotodoc.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +"""Generate native-protocol HTML and asciidoc summary from .spec files.""" + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import sys +import re +import html +import io +from pathlib import Path +from typing import List + +_comment_re = re.compile(r'^#\s?(.*)$') +_empty_re = re.compile(r'^\s*$') +_title_re = re.compile(r'^\s+(.*)\s*$') +_heading_re = re.compile(r'^(?P\s*)(?P\d+(?:\.\d+)*)\.?\s+(?P[A-Za-z_].+)$') +_toc_entry_re = re.compile(r'^(?P<number>\d+(?:\.\d+)*)\.?\s+(?P<title>.+)$') +_url_re = re.compile(r'(https?://[^\s)]+)') +_URL_TRAILING_PUNCT = '.,;:!?' +_protocol_filename_re = re.compile(r'^native_protocol_v(\d+)\.(?:spec|html)$') + + +def _skip_blank(lines: List[str], idx: int) -> int: + while idx < len(lines) and _empty_re.match(lines[idx]): + idx += 1 + return idx + + +def parse_spec_file(path: Path) -> dict: + """Parse a native_protocol_v*.spec file into license, title, TOC, and sections.""" + text = path.read_text(encoding='utf-8') + lines = text.splitlines() + idx = 0 + + # License + license_lines = [] + while idx < len(lines): + m = _comment_re.match(lines[idx]) + if not m: + break + license_lines.append(m.group(1)) + idx += 1 + idx = _skip_blank(lines, idx) + + # Titles + m = _title_re.match(lines[idx]) if idx < len(lines) else None + if not m: + sys.exit(f"Parse error: missing or malformed title at line {idx + 1}") + title = m.group(1) + idx += 1 + idx = _skip_blank(lines, idx) + + # Table of Contents + if idx >= len(lines) or lines[idx] != "Table of Contents": + sys.exit(f"Parse error: expected 'Table of Contents' at line {idx + 1}") + idx += 1 + idx = _skip_blank(lines, idx) + + # TOC + toc = [] + while idx < len(lines) and lines[idx].strip(): + line = lines[idx].strip() + m = _toc_entry_re.match(line) + if not m: + sys.exit(f"Parse error: bad TOC entry at line {idx + 1}") + toc.append({'number': m.group('number'), 'title': m.group('title')}) + idx += 1 + idx = _skip_blank(lines, idx) + + # Sections distinguishing real headings from prose/list-items + sections = [] + current = None + for line in lines[idx:]: + m = _heading_re.match(line) + if m: + num = m.group('number') + sec_title = m.group('title').rstrip() + if '.' in num or m.group('indent') == '': + if current: + sections.append(current) + current = {'number': num, 'title': sec_title, 'body': []} + continue + if current: + current['body'].append(line) + + if current: + sections.append(current) + + return { + 'license': license_lines, + 'title': title, + 'toc': toc, + 'sections': sections, + } + + +def build_toc_tree(entries, nums): + """Build a nested TOC tree from flat entries; mark which numbers exist as sections.""" + root = {'children': []} + stack = [root] + for e in entries: + lvl = e['number'].count('.') + 1 + stack = stack[:lvl] + parent = stack[-1] + node = {'entry': e, 'exists': e['number'] in nums, 'children': []} + parent['children'].append(node) + stack.append(node) + return root['children'] + + +_section_multi_re = re.compile( + r'([sS]ections)(\s+\d+(?:\.\d+)*(?:,?\s+(?:and\s+)?\d+(?:\.\d+)*)+)' +) + + +_section_single_re = re.compile(r'([sS]ection (\d+(?:\.\d+)*))') +_num_re = re.compile(r'\d+(?:\.\d+)*') + + +def _linkify_section_refs(text: str) -> str: + def repl_multi(m): + return m.group(1) + _num_re.sub( + lambda n: f'<a href="#s{n.group(0)}">{n.group(0)}</a>', m.group(2)) + text = _section_multi_re.sub(repl_multi, text) + text = _section_single_re.sub( + lambda m: f'<a href="#s{m.group(2)}">{m.group(1)}</a>', text) + return text + + +def _linkify_url(m): + url = m.group(1) + trailing = '' + while url and url[-1] in _URL_TRAILING_PUNCT: + trailing = url[-1] + trailing + url = url[:-1] + return f'<a href="{url}">{url}</a>{trailing}' + + +def format_body(lines): + """Render section body lines as escaped HTML with URL and section-ref linkification.""" + text = "\n".join(lines) + if text.startswith('\n'): + text = text[1:] + escaped = html.escape(text) + with_urls = _url_re.sub(_linkify_url, escaped) + linked = _linkify_section_refs(with_urls) + # Transcode entity names to byte-match the previous (go) tools output. + linked = linked.replace('"', '"').replace(''', ''') + return '<pre>' + linked + '</pre>' + + +def build_sections(secs): + """Convert raw parsed sections into render-ready dicts with HTML body and heading level.""" + return [{ + 'number': s['number'], + 'title': s['title'], + 'level': s['number'].count('.') + 2, + 'body_html': format_body(s['body']) + } for s in secs] + + +def _render_toc(nodes, out, indent): + pad = ' ' * indent + for node in nodes: + num = node['entry']['number'] + node_title = html.escape(node['entry']['title']) + out.write(f'{pad}<li id="toc{num}">\n') + out.write(f'{pad} {num}\n') + if node['exists']: + out.write(f'{pad} <a href="#s{num}">{node_title}</a>\n') + else: + out.write(f'{pad} {node_title}\n') + if node['children']: + out.write(f'{pad} <ol>\n') + _render_toc(node['children'], out, indent + 2) + out.write(f'{pad} </ol>\n') + out.write(f'{pad}</li>\n') + + +def render_html(title, license_lines, toc_tree, sections): + """Render the full HTML document for a single protocol version.""" + out = io.StringIO() + t_esc = html.escape(title) + out.write('<!DOCTYPE html>\n') + out.write('<html>\n') + out.write('<head>\n') + out.write(' <meta charset="utf-8">\n') + out.write(f' <title>{t_esc}\n') + out.write(' \n') + out.write('\n') + out.write('\n') + for line in license_lines: + out.write(f' \n') + out.write(f'

{t_esc}

\n') + out.write('

Table of Contents

\n') + out.write(' \n') + for sec in sections: + lvl = sec['level'] + num = sec['number'] + sec_title = html.escape(sec['title']) + out.write(f' {num} {sec_title}\n') + out.write(f' {sec["body_html"]}\n') + out.write('\n') + out.write('\n') + return out.getvalue() + + +def main(): # pylint: disable=too-many-locals + """CLI entrypoint: render one HTML page per spec file plus an asciidoc summary.""" + parser = argparse.ArgumentParser( + description="Generate native-protocol HTML and asciidoc summary from .spec files." + ) + parser.add_argument( + '--spec-dir', type=Path, default=Path('.'), + help="Directory containing native_protocol_v*.spec files (default: cwd)." + ) + parser.add_argument( + '--attach-dir', type=Path, default=Path('modules/cassandra/attachments'), + help="Output directory for per-version HTML files." + ) + parser.add_argument( + '--summary-adoc', type=Path, + default=Path('modules/cassandra/pages/reference/native-protocol.adoc'), + help="Output path for the generated asciidoc summary." + ) + args = parser.parse_args() + + spec_dir = args.spec_dir + attach_dir = args.attach_dir + summary_adoc = args.summary_adoc + + if not spec_dir.is_dir(): + sys.exit(f"Spec directory does not exist: {spec_dir.resolve()}") + + attach_dir.mkdir(parents=True, exist_ok=True) + summary_adoc.parent.mkdir(parents=True, exist_ok=True) + + specs = sorted( + (p for p in spec_dir.glob('native_protocol_v*.spec') + if _protocol_filename_re.match(p.name)), + key=lambda p: int(_protocol_filename_re.match(p.name).group(1)), + reverse=True, + ) + if not specs: + sys.exit(f"No native_protocol_v*.spec files found in {spec_dir.resolve()}") + + for sp in specs: + version = _protocol_filename_re.match(sp.name).group(1) + hp = attach_dir / f'native_protocol_v{version}.html' + doc = parse_spec_file(sp) + toc_tree = build_toc_tree(doc['toc'], {s['number'] for s in doc['sections']}) + sections = build_sections(doc['sections']) + rendered = render_html(doc['title'], doc['license'], toc_tree, sections) + hp.write_text(rendered, encoding='utf-8') + print(f"-> {hp}") + + nav_js = """[source, js] +++++ + +++++ +""" + + html_files = sorted( + (p for p in attach_dir.glob('native_protocol_v*.html') + if _protocol_filename_re.match(p.name)), + key=lambda p: int(_protocol_filename_re.match(p.name).group(1)), + reverse=True, + ) + with summary_adoc.open('w', encoding='utf-8') as f: + f.write("= Native Protocol Versions\n") + f.write(":page-layout: default\n\n") + for file in html_files: + ver = _protocol_filename_re.match(file.name).group(1) + f.write(f"== Native Protocol Version {ver}\n\n") + f.write("[source, html]\n++++\n") + f.write(f"include::cassandra:attachment${file.name}[Version {ver}]\n") + f.write("++++\n\n") + f.write(nav_js) + print(f"-> {summary_adoc}") + + +if __name__ == '__main__': + main() diff --git a/doc/scripts/gen-antora-yml.py b/doc/scripts/gen-antora-yml.py new file mode 100644 index 000000000000..3f953f71ffe8 --- /dev/null +++ b/doc/scripts/gen-antora-yml.py @@ -0,0 +1,93 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +A script to generate doc/antora.yml from build metadata. +""" +import os +import subprocess +import re + +script_dir = os.path.dirname(os.path.abspath(__file__)) + +def get_version_from_build_xml(): + build_xml_path = os.path.join(os.path.dirname(os.path.dirname(script_dir)), 'build.xml') + + try: + with open(build_xml_path, 'r') as f: + content = f.read() + + match = re.search(r' 0 else '0' + minor = parts[1].split('-')[0] if len(parts) > 1 else '0' + version = f"{major}.{minor}" + + with open(output_path, 'w') as f: + f.write('# Auto-generated by gen-antora-yml.py\n') + f.write('# Do not edit manually - regenerated by `ant gen-asciidoc`\n') + f.write(f"name: Cassandra\n") + f.write(f"version: '{version}'\n") + f.write(f"display_version: '{version}'\n") + if not is_release: + f.write(f"prerelease: true\n") + f.write("asciidoc:\n") + f.write(" attributes:\n") + f.write(f" cass_url: http://cassandra.apache.org/\n") + f.write(f" cass-version: Cassandra {version}\n") + f.write(f" cassandra: Cassandra\n") + f.write(f" product: Apache Cassandra\n") + f.write("nav:\n") + f.write(f"- modules/ROOT/nav.adoc\n") + f.write(f"- modules/cassandra/nav.adoc\n") + +if __name__ == '__main__': + main() diff --git a/doc/scripts/gen-native-protocol-docs.sh b/doc/scripts/gen-native-protocol-docs.sh new file mode 100755 index 000000000000..17bef66e7415 --- /dev/null +++ b/doc/scripts/gen-native-protocol-docs.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +[ "x${SCRIPT_DIR:-}" != "x" ] || SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +[ "x${PYTOOL:-}" != "x" ] || PYTOOL="$SCRIPT_DIR/cqlprotodoc.py" + +echo "Processing native protocol specs..." +python3 "$PYTOOL" + +echo "Done" +exit 0 diff --git a/doc/scripts/process-native-protocol-specs-in-docker.sh b/doc/scripts/process-native-protocol-specs-in-docker.sh deleted file mode 100755 index 332310ab661e..000000000000 --- a/doc/scripts/process-native-protocol-specs-in-docker.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/sh -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -[ -f "../build.xml" ] || { echo "build.xml must exist (current directory needs to be doc/ in cassandra repo"; exit 1; } -[ -f "antora.yml" ] || { echo "antora.yml must exist (current directory needs to be doc/ in cassandra repo"; exit 1; } - -# Variables -GO_VERSION="1.23.1" -TMPDIR="${TMPDIR:-/tmp}" - -check_go_version() { - if command -v go &>/dev/null; then - local installed_version=$(go version | awk '{print $3}' | sed 's/go//') - if [ "$(printf '%s\n' "$GO_VERSION" "$installed_version" | sort -V | head -n1)" = "$GO_VERSION" ]; then - echo "Detected Go $installed_version (>= $GO_VERSION)" - return 0 - else - echo "Detected unsupported Go $installed_version (< $GO_VERSION), please update to supported version." - fi - else - echo "No Go installation detected, please install Go (>= $GO_VERSION)" - fi - return 1 -} - -if ! check_go_version; then - echo " Please install/upgrade Golang for 'ant gen-doc', or specify '-Dant.gen-doc.skip=true' to skip this step." - echo " For download and installation instructions see https://go.dev/doc/install" - exit 1 -fi - -# Step 1: Building the parser -echo "Building the cqlprotodoc..." -DIR="$(pwd)" -cd "${TMPDIR}" - -rm -rf "${TMPDIR}/cassandra-website" -git clone -n --depth=1 --filter=tree:0 https://github.com/apache/cassandra-website - -if [ $? != "0" ]; then - echo "Error occured while cloning https://github.com/apache/cassandra-website" - exit 1 -fi - -cd "${TMPDIR}/cassandra-website" -git sparse-checkout set --no-cone /cqlprotodoc -git checkout -cd "${TMPDIR}/cassandra-website/cqlprotodoc" -rm -rf "${TMPDIR}/cqlprotodoc" -go build -o "$TMPDIR"/cqlprotodoc - -# Step 2: Process the spec files using the parser -echo "Processing the .spec files..." -cd "${DIR}" -output_dir="modules/cassandra/attachments" -mkdir -p "${output_dir}" -"$TMPDIR"/cqlprotodoc . "${output_dir}" - -if ! ls ${output_dir}/native_protocol_v*.html > /dev/null 2>&1; then - echo "failed: No native_protocol_v*.html files generated in ${output_dir}" - exit 1 -fi - -# Step 4: Generate summary file -summary_file="modules/cassandra/pages/reference/native-protocol.adoc" - -# Write the header -echo "= Native Protocol Versions" > "$summary_file" -echo ":page-layout: default" >> "$summary_file" -echo >> "$summary_file" - -# Loop through the files from step 2 in reverse version order -for file in $(ls ${output_dir}/native_protocol_v*.html | sort -r | awk -F/ '{print $NF}'); do - version=$(echo "$file" | sed -E 's/native_protocol_v([0-9]+)\.html/\1/') - echo "== Native Protocol Version $version" >> "$summary_file" - echo >> "$summary_file" - echo "[source, html]" >> "$summary_file" - echo "++++" >> "$summary_file" - echo "include::cassandra:attachment\$$file[Version $version]" >> "$summary_file" - echo "++++" >> "$summary_file" - echo >> "$summary_file" -done - -# Navigation setup -echo "[source, js]" >> "$summary_file" -echo "++++" >> "$summary_file" -echo "" >> "$summary_file" - - -# Step 3: Cleanup - Remove the Cassandra and parser directories -echo "Cleaning up..." -cd "${DIR}" -rm -rf "${TMPDIR}/cassandra-website" "${TMPDIR}/cqlprotodoc" 2>/dev/null - -echo "Script completed successfully." diff --git a/ds/Jenkinsfile b/ds/Jenkinsfile new file mode 100644 index 000000000000..b840c9cd84d6 --- /dev/null +++ b/ds/Jenkinsfile @@ -0,0 +1,20 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This Jenkinsfile uses a shared library from https://github.com/riptano/jenkins-pipeline-lib +// The pipeline logic is defined in the library rather than in this file. + +@Library('ds-pipeline-lib') _ + +dsCassandraPRGate() diff --git a/ide/idea/vcs.xml b/ide/idea/vcs.xml index 81872fd3f150..8abf2cfaeca9 100644 --- a/ide/idea/vcs.xml +++ b/ide/idea/vcs.xml @@ -7,9 +7,17 @@ diff --git a/ide/idea/workspace.xml b/ide/idea/workspace.xml index 89528b240854..de7442fce22b 100644 --- a/ide/idea/workspace.xml +++ b/ide/idea/workspace.xml @@ -151,7 +151,9 @@ -Dcassandra.storagedir=$PROJECT_DIR$/data -Djava.library.path=$PROJECT_DIR$/lib/sigar-bin -Dlogback.configurationFile=file://$PROJECT_DIR$/conf/logback.xml + -Dcassandra.cluster_version_provider.min_stable_duration_ms=0 -XX:HeapDumpPath=build/test + -Dnet.bytebuddy.experimental=true -ea" />
keyspaceMapper) + { + if ((keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) || (names == null)) + return this; + + boolean changed = false; + List newNames = new ArrayList<>(names.size()); + for (ColumnSpecification cs : names) + { + ColumnSpecification newColumnSpecification = cs.withOverriddenKeyspace(keyspaceMapper); + newNames.add(newColumnSpecification); + if (newColumnSpecification != cs) + changed = true; + } + return changed ? new ResultMetadata(computeResultMetadataId(newNames), EnumSet.copyOf(flags), newNames, columnCount, pagingState) : this; + } + private static class Codec implements CBCodec { public ResultMetadata decode(ByteBuf body, ProtocolVersion version) @@ -430,7 +462,7 @@ public void encode(ResultMetadata m, ByteBuf dest, ProtocolVersion version) if (hasMorePages) CBUtil.writeValue(m.pagingState.serialize(version), dest); - if (version.isGreaterOrEqualTo(ProtocolVersion.V5) && metadataChanged) + if (version.isGreaterOrEqualTo(ProtocolVersion.V5) && metadataChanged) { assert !noMetadata : "MetadataChanged and NoMetadata are mutually exclusive flags"; CBUtil.writeBytes(m.getResultMetadataId().bytes, dest); @@ -578,6 +610,23 @@ public static PreparedMetadata fromPrepared(CQLStatement statement) return new PreparedMetadata(statement.getBindVariables(), statement.getPartitionKeyBindVariableIndexes()); } + public PreparedMetadata withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + if (keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) + return this; + + boolean changed = false; + List newNames = new ArrayList<>(names.size()); + for (ColumnSpecification cs : names) + { + ColumnSpecification newColumnSpecification = cs.withOverriddenKeyspace(keyspaceMapper); + newNames.add(newColumnSpecification); + if (newColumnSpecification != cs) + changed = true; + } + return changed ? new PreparedMetadata(EnumSet.copyOf(flags), newNames, partitionKeyBindIndexes == null ? null : Arrays.copyOf(partitionKeyBindIndexes, partitionKeyBindIndexes.length)) : this; + } + private static class Codec implements CBCodec { public PreparedMetadata decode(ByteBuf body, ProtocolVersion version) diff --git a/src/java/org/apache/cassandra/cql3/Sets.java b/src/java/org/apache/cassandra/cql3/Sets.java index 00d6870a9206..0e039cfe72b8 100644 --- a/src/java/org/apache/cassandra/cql3/Sets.java +++ b/src/java/org/apache/cassandra/cql3/Sets.java @@ -17,8 +17,6 @@ */ package org.apache.cassandra.cql3; -import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; - import java.nio.ByteBuffer; import java.util.Collections; import java.util.Comparator; @@ -26,7 +24,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -39,7 +36,6 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.db.marshal.MapType; -import org.apache.cassandra.db.marshal.ReversedType; import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; @@ -50,6 +46,8 @@ import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; +import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; + /** * Static helper methods and classes for sets. */ @@ -62,14 +60,9 @@ public static ColumnSpecification valueSpecOf(ColumnSpecification column) return new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier("value(" + column.name + ")", true), elementsType(column.type)); } - private static AbstractType unwrap(AbstractType type) - { - return type.isReversed() ? unwrap(((ReversedType) type).baseType) : type; - } - private static AbstractType elementsType(AbstractType type) { - return ((SetType) unwrap(type)).getElementsType(); + return ((SetType) type.unwrap()).getElementsType(); } /** @@ -134,8 +127,8 @@ public static String setToString(Iterable items, java.util.function.Funct public static SetType getExactSetTypeIfKnown(List items, java.util.function.Function> mapper) { - Optional> type = items.stream().map(mapper).filter(Objects::nonNull).findFirst(); - return type.isPresent() ? SetType.getInstance(type.get(), false) : null; + AbstractType type = Lists.getElementType(items, mapper); + return type != null ? SetType.getInstance(type, false) : null; } public static SetType getPreferredCompatibleType(List items, @@ -143,7 +136,7 @@ public static SetType getPreferredCompatibleType(List items, { Set> types = items.stream().map(mapper).filter(Objects::nonNull).collect(Collectors.toSet()); AbstractType type = AssignmentTestable.getCompatibleTypeIfKnown(types); - return type == null ? null : SetType.getInstance(type, false); + return type == null ? null : SetType.getInstance(type.freeze(), false); } public static class Literal extends Term.Raw @@ -185,7 +178,7 @@ public Term prepare(String keyspace, ColumnSpecification receiver) throws Invali private void validateAssignableTo(String keyspace, ColumnSpecification receiver) throws InvalidRequestException { - AbstractType type = unwrap(receiver.type); + AbstractType type = receiver.type.unwrap(); if (!(type instanceof SetType)) { @@ -367,6 +360,7 @@ public Adder(ColumnMetadata column, Term t) super(column, t); } + @Override public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException { assert column.type.isMultiCell() : "Attempted to add items to a frozen set"; @@ -426,6 +420,7 @@ public Discarder(ColumnMetadata column, Term t) super(column, t); } + @Override public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException { assert column.type.isMultiCell() : "Attempted to remove items from a frozen set"; @@ -451,6 +446,7 @@ public ElementDiscarder(ColumnMetadata column, Term k) super(column, k); } + @Override public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException { assert column.type.isMultiCell() : "Attempted to delete a single element in a frozen set"; diff --git a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java index cf1cb69066e6..08cf9457bf6a 100644 --- a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java +++ b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java @@ -22,6 +22,11 @@ import java.util.List; import java.util.Objects; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.db.marshal.VectorType; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.index.sai.analyzer.AnalyzerEqOperatorSupport; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.cql3.Term.Raw; @@ -32,6 +37,7 @@ import org.apache.cassandra.db.marshal.ListType; import org.apache.cassandra.db.marshal.MapType; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.service.ClientWarn; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; @@ -101,6 +107,11 @@ public static SingleColumnRelation createInRelation(ColumnIdentifier entity, Lis return new SingleColumnRelation(entity, null, Operator.IN, null, inValues); } + public static SingleColumnRelation createNotInRelation(ColumnIdentifier entity, List inValues) + { + return new SingleColumnRelation(entity, null, Operator.NOT_IN, null, inValues); + } + public ColumnIdentifier getEntity() { return entity; @@ -150,7 +161,7 @@ public String toCQLString() entityAsString = String.format("%s[%s]", entityAsString, mapKey); if (isIN()) - return String.format("%s IN %s", entityAsString, Tuples.tupleToString(inValues)); + return String.format("%s IN %s", entityAsString, inValues == null ? value : Tuples.tupleToString(inValues)); return String.format("%s %s %s", entityAsString, relationType, value); } @@ -179,18 +190,59 @@ public boolean equals(Object o) } @Override - protected Restriction newEQRestriction(TableMetadata table, VariableSpecifications boundNames) + protected Restriction newEQRestriction(TableMetadata table, VariableSpecifications boundNames, IndexHints indexHints) { ColumnMetadata columnDef = table.getExistingColumn(entity); if (mapKey == null) { Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); - return new SingleColumnRestriction.EQRestriction(columnDef, term); + // Leave the restriction as EQ if no analyzed index in backwards compatibility mode is present + IndexRegistry.EqBehaviorIndexes ebi = IndexRegistry.obtain(table).getEqBehavior(columnDef, indexHints); + // The primary key always has ambiguous EQ behavior and we have to defer to later logic to decide + // whether the EQ is analayzed or not. This is a legacy behavior that "does the right thing" when + // there is a fully restricted partition key or not. + if (ebi.behavior == IndexRegistry.EqBehavior.EQ || columnDef.isPrimaryKeyColumn()) + return new SingleColumnRestriction.EQRestriction(columnDef, term); + + // the index is configured to transform EQ into MATCH for backwards compatibility + if (ebi.behavior == IndexRegistry.EqBehavior.MATCH) + { + ClientWarn.instance.warn(String.format(AnalyzerEqOperatorSupport.EQ_RESTRICTION_ON_ANALYZED_WARNING, + columnDef.toString(), + Index.joinNames(ebi.matchIndexes)), + columnDef); + return new SingleColumnRestriction.AnalyzerMatchesRestriction(columnDef, term); + } + + // multiple indexes support EQ, this is unsupported + assert ebi.behavior == IndexRegistry.EqBehavior.AMBIGUOUS; + throw invalidRequest(AnalyzerEqOperatorSupport.EQ_AMBIGUOUS_ERROR, + columnDef.toString(), + Index.joinNames(ebi.matchIndexes), + Index.joinNames(ebi.eqIndexes), + ebi.matchIndexes.iterator().next().getIndexMetadata().name); } List receivers = toReceivers(columnDef); Term entryKey = toTerm(Collections.singletonList(receivers.get(0)), mapKey, table.keyspace, boundNames); Term entryValue = toTerm(Collections.singletonList(receivers.get(1)), value, table.keyspace, boundNames); - return new SingleColumnRestriction.ContainsRestriction(columnDef, entryKey, entryValue); + return new SingleColumnRestriction.ContainsRestriction(columnDef, entryKey, entryValue, false); + } + + @Override + protected Restriction newNEQRestriction(TableMetadata table, VariableSpecifications boundNames) + { + ColumnMetadata columnDef = table.getExistingColumn(entity); + if (mapKey == null) + { + Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); + MarkerOrTerms skippedValues = new MarkerOrTerms.Terms(Collections.singletonList(term)); + return SingleColumnRestriction.SliceRestriction.fromSkippedValues(columnDef, skippedValues); + } + + List receivers = toReceivers(columnDef); + Term entryKey = toTerm(Collections.singletonList(receivers.get(0)), mapKey, table.keyspace, boundNames); + Term entryValue = toTerm(Collections.singletonList(receivers.get(1)), value, table.keyspace, boundNames); + return new SingleColumnRestriction.ContainsRestriction(columnDef, entryKey, entryValue, true); } @Override @@ -202,14 +254,33 @@ protected Restriction newINRestriction(TableMetadata table, VariableSpecificatio if (terms == null) { Term term = toTerm(receivers, value, table.keyspace, boundNames); - return new SingleColumnRestriction.InRestrictionWithMarker(columnDef, (Lists.Marker) term); + return new SingleColumnRestriction.INRestriction(columnDef, new MarkerOrTerms.Marker((Lists.Marker) term)); } // An IN restrictions with only one element is the same than an EQ restriction if (terms.size() == 1) return new SingleColumnRestriction.EQRestriction(columnDef, terms.get(0)); - return new SingleColumnRestriction.InRestrictionWithValues(columnDef, terms); + return new SingleColumnRestriction.INRestriction(columnDef, new MarkerOrTerms.Terms(terms)); + } + + @Override + protected Restriction newNotINRestriction(TableMetadata table, VariableSpecifications boundNames) + { + ColumnMetadata columnDef = table.getExistingColumn(entity); + List receivers = toReceivers(columnDef); + List terms = toTerms(receivers, inValues, table.keyspace, boundNames); + MarkerOrTerms values; + if (terms == null) + { + Term term = toTerm(receivers, value, table.keyspace, boundNames); + values = new MarkerOrTerms.Marker((Lists.Marker) term); + } + else + { + values = new MarkerOrTerms.Terms(terms); + } + return SingleColumnRestriction.SliceRestriction.fromSkippedValues(columnDef, values); } @Override @@ -228,8 +299,15 @@ protected Restriction newSliceRestriction(TableMetadata table, throw invalidRequest("Slice restrictions are not supported on duration columns"); } - Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); - return new SingleColumnRestriction.SliceRestriction(columnDef, bound, inclusive, term); + if (mapKey == null) + { + Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); + return SingleColumnRestriction.SliceRestriction.fromBound(columnDef, bound, inclusive, term); + } + List receivers = toReceivers(columnDef); + Term entryKey = toTerm(Collections.singletonList(receivers.get(0)), mapKey, table.keyspace, boundNames); + Term entryValue = toTerm(Collections.singletonList(receivers.get(1)), value, table.keyspace, boundNames); + return new SingleColumnRestriction.MapSliceRestriction(columnDef, bound, inclusive, entryKey, entryValue); } @Override @@ -239,7 +317,17 @@ protected Restriction newContainsRestriction(TableMetadata table, { ColumnMetadata columnDef = table.getExistingColumn(entity); Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); - return new SingleColumnRestriction.ContainsRestriction(columnDef, term, isKey); + return new SingleColumnRestriction.ContainsRestriction(columnDef, term, isKey, false); + } + + @Override + protected Restriction newNotContainsRestriction(TableMetadata table, + VariableSpecifications boundNames, + boolean isKey) throws InvalidRequestException + { + ColumnMetadata columnDef = table.getExistingColumn(entity); + Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); + return new SingleColumnRestriction.ContainsRestriction(columnDef, term, isKey, true); } @Override @@ -264,6 +352,36 @@ protected Restriction newLikeRestriction(TableMetadata table, VariableSpecificat return new SingleColumnRestriction.LikeRestriction(columnDef, operator, term); } + @Override + protected Restriction newAnnRestriction(TableMetadata table, VariableSpecifications boundNames) + { + ColumnMetadata columnDef = table.getExistingColumn(entity); + if (!(columnDef.type instanceof VectorType)) + throw invalidRequest("ANN is only supported against DENSE FLOAT32 columns"); + Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); + return new SingleColumnRestriction.AnnRestriction(columnDef, term); + } + + @Override + protected Restriction newBm25Restriction(TableMetadata table, VariableSpecifications boundNames) + { + ColumnMetadata columnDef = table.getExistingColumn(entity); + Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); + return new SingleColumnRestriction.Bm25Restriction(columnDef, term); + } + + @Override + protected Restriction newAnalyzerMatchesRestriction(TableMetadata table, VariableSpecifications boundNames) + { + if (mapKey != null) + throw invalidRequest("%s can't be used with collections.", operator()); + + ColumnMetadata columnDef = table.getExistingColumn(entity); + Term term = toTerm(toReceivers(columnDef), value, table.keyspace, boundNames); + + return new SingleColumnRestriction.AnalyzerMatchesRestriction(columnDef, term); + } + /** * Returns the receivers for this relation. * @param columnDef the column definition @@ -276,13 +394,15 @@ private List toReceivers(ColumnMetadata columnDef checkFalse(isContainsKey() && !(receiver.type instanceof MapType), "Cannot use CONTAINS KEY on non-map column %s", receiver.name); checkFalse(isContains() && !(receiver.type.isCollection()), "Cannot use CONTAINS on non-collection column %s", receiver.name); + checkFalse(isNotContainsKey() && !(receiver.type instanceof MapType), "Cannot use NOT CONTAINS KEY on non-map column %s", receiver.name); + checkFalse(isNotContains() && !(receiver.type.isCollection()), "Cannot use NOT CONTAINS on non-collection column %s", receiver.name); if (mapKey != null) { checkFalse(receiver.type instanceof ListType, "Indexes on list entries (%s[index] = value) are not currently supported.", receiver.name); checkTrue(receiver.type instanceof MapType, "Column %s cannot be used as a map", receiver.name); checkTrue(receiver.type.isMultiCell(), "Map-entry equality predicates on frozen map column %s are not supported", receiver.name); - checkTrue(isEQ(), "Only EQ relations are supported on map entries"); + checkTrue(isEQ() || isNEQ() || isSlice(), "Only EQ, NEQ, and SLICE relations are supported on map entries"); } // Non-frozen UDTs don't support any operator @@ -300,11 +420,11 @@ private List toReceivers(ColumnMetadata columnDef receiver.type.asCQL3Type(), operator()); - if (isContainsKey() || isContains()) + if (isContainsKey() || isContains() || isNotContains() || isNotContainsKey()) { - receiver = makeCollectionReceiver(receiver, isContainsKey()); + receiver = makeCollectionReceiver(receiver, isContainsKey() || isNotContainsKey()); } - else if (receiver.type.isMultiCell() && mapKey != null && isEQ()) + else if (receiver.type.isMultiCell() && isMapEntryComparison()) { List receivers = new ArrayList<>(2); receivers.add(makeCollectionReceiver(receiver, true)); @@ -323,12 +443,12 @@ private static ColumnSpecification makeCollectionReceiver(ColumnSpecification re private boolean isLegalRelationForNonFrozenCollection() { - return isContainsKey() || isContains() || isMapEntryEquality(); + return isContainsKey() || isContains() || isNotContains() || isNotContainsKey() || isMapEntryComparison(); } - private boolean isMapEntryEquality() + private boolean isMapEntryComparison() { - return mapKey != null && isEQ(); + return mapKey != null && (isEQ() || isNEQ() || isSlice()); } private boolean canHaveOnlyOneValue() diff --git a/src/java/org/apache/cassandra/cql3/Term.java b/src/java/org/apache/cassandra/cql3/Term.java index c94b6141af0a..d19e1c099ce4 100644 --- a/src/java/org/apache/cassandra/cql3/Term.java +++ b/src/java/org/apache/cassandra/cql3/Term.java @@ -65,7 +65,7 @@ public interface Term * Whether or not that term contains at least one bind marker. * * Note that this is slightly different from being or not a NonTerminal, - * because calls to non pure functions will be NonTerminal (see #5616) + * because calls to non-deterministic functions will be NonTerminal (see #5616) * even if they don't have bind markers. */ public abstract boolean containsBindMarker(); @@ -151,15 +151,15 @@ public abstract class MultiColumnRaw extends Term.Raw /** * A terminal term, one that can be reduced to a byte buffer directly. - * + *

* This includes most terms that don't have a bind marker (an exception - * being delayed call for non pure function that are NonTerminal even + * being delayed call for non-deterministic function that are NonTerminal even * if they don't have bind markers). - * + *

* This can be only one of: * - a constant value * - a collection value - * + *

* Note that a terminal term will always have been type checked, and thus * consumer can (and should) assume so. */ @@ -190,10 +190,20 @@ public boolean isTerminal() */ public abstract ByteBuffer get(ProtocolVersion version) throws InvalidRequestException; + public ByteBuffer getVector(ProtocolVersion protocolVersion) throws InvalidRequestException + { + throw new InvalidRequestException("Doesn't support getVector"); + } + public ByteBuffer bindAndGet(QueryOptions options) throws InvalidRequestException { return get(options.getProtocolVersion()); } + + public ByteBuffer bindAndGetVector(QueryOptions options) throws InvalidRequestException + { + return getVector(options.getProtocolVersion()); + } } public abstract class MultiItemTerminal extends Terminal @@ -202,14 +212,14 @@ public abstract class MultiItemTerminal extends Terminal } /** - * A non terminal term, i.e. a term that can only be reduce to a byte buffer + * A non-terminal term, i.e. a term that can only be reduce to a byte buffer * at execution time. - * + *

* We have the following type of NonTerminal: * - marker for a constant value * - marker for a collection value (list, set, map) * - a function having bind marker - * - a non pure function (even if it doesn't have bind marker - see #5616) + * - a non-deterministic function (even if it doesn't have bind marker - see #5616) */ public abstract class NonTerminal implements Term { diff --git a/src/java/org/apache/cassandra/cql3/TokenRelation.java b/src/java/org/apache/cassandra/cql3/TokenRelation.java index 139c55d35862..3fa09f609ee8 100644 --- a/src/java/org/apache/cassandra/cql3/TokenRelation.java +++ b/src/java/org/apache/cassandra/cql3/TokenRelation.java @@ -25,6 +25,7 @@ import com.google.common.base.Joiner; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.cql3.Term.Raw; @@ -76,19 +77,30 @@ public List getInValues() } @Override - protected Restriction newEQRestriction(TableMetadata table, VariableSpecifications boundNames) + protected Restriction newEQRestriction(TableMetadata table, VariableSpecifications boundNames, IndexHints indexHints) { List columnDefs = getColumnDefinitions(table); Term term = toTerm(toReceivers(table, columnDefs), value, table.keyspace, boundNames); return new TokenRestriction.EQRestriction(table, columnDefs, term); } + @Override + protected Restriction newNEQRestriction(TableMetadata table, VariableSpecifications boundNames) + { + throw invalidRequest("%s cannot be used with the token function", operator()); + } + @Override protected Restriction newINRestriction(TableMetadata table, VariableSpecifications boundNames) { throw invalidRequest("%s cannot be used with the token function", operator()); } + protected Restriction newNotINRestriction(TableMetadata table, VariableSpecifications boundNames) + { + throw invalidRequest("%s cannot be used with the token function", operator()); + } + @Override protected Restriction newSliceRestriction(TableMetadata table, VariableSpecifications boundNames, @@ -106,6 +118,12 @@ protected Restriction newContainsRestriction(TableMetadata table, VariableSpecif throw invalidRequest("%s cannot be used with the token function", operator()); } + @Override + protected Restriction newNotContainsRestriction(TableMetadata table, VariableSpecifications boundNames, boolean isKey) + { + throw invalidRequest("%s cannot be used with the token function", operator()); + } + @Override protected Restriction newIsNotRestriction(TableMetadata table, VariableSpecifications boundNames) { @@ -118,6 +136,24 @@ protected Restriction newLikeRestriction(TableMetadata table, VariableSpecificat throw invalidRequest("%s cannot be used with the token function", operator); } + @Override + protected Restriction newAnnRestriction(TableMetadata table, VariableSpecifications boundNames) + { + throw invalidRequest("%s cannot be used for token relations", operator()); + } + + @Override + protected Restriction newBm25Restriction(TableMetadata table, VariableSpecifications boundNames) + { + throw invalidRequest("%s cannot be used for token relations", operator()); + } + + @Override + protected Restriction newAnalyzerMatchesRestriction(TableMetadata table, VariableSpecifications boundNames) + { + throw invalidRequest("%s cannot be used for token relations", operator()); + } + @Override protected Term toTerm(List receivers, Raw raw, diff --git a/src/java/org/apache/cassandra/cql3/Tuples.java b/src/java/org/apache/cassandra/cql3/Tuples.java index 60f963ce4fe2..53ce0a419347 100644 --- a/src/java/org/apache/cassandra/cql3/Tuples.java +++ b/src/java/org/apache/cassandra/cql3/Tuples.java @@ -24,11 +24,12 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.db.marshal.ListType; -import org.apache.cassandra.db.marshal.ReversedType; import org.apache.cassandra.db.marshal.TupleType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.MarshalException; @@ -94,7 +95,7 @@ public Term prepare(String keyspace, List receive throw new InvalidRequestException(String.format("Expected %d elements in value tuple, but got %d: %s", receivers.size(), elements.size(), this)); List values = new ArrayList<>(elements.size()); - List> types = new ArrayList<>(elements.size()); + ImmutableList.Builder> types = ImmutableList.builderWithExpectedSize(elements.size()); boolean allTerminal = true; for (int i = 0; i < elements.size(); i++) { @@ -105,7 +106,7 @@ public Term prepare(String keyspace, List receive values.add(t); types.add(receivers.get(i).type); } - DelayedValue value = new DelayedValue(new TupleType(types), values); + DelayedValue value = new DelayedValue(new TupleType(types.build()), values); return allTerminal ? value.bind(QueryOptions.DEFAULT) : value; } @@ -122,7 +123,7 @@ public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpeci @Override public AbstractType getExactTypeIfKnown(String keyspace) { - List> types = new ArrayList<>(elements.size()); + ImmutableList.Builder> types = ImmutableList.builderWithExpectedSize(elements.size()); for (Term.Raw term : elements) { AbstractType type = term.getExactTypeIfKnown(keyspace); @@ -130,7 +131,7 @@ public AbstractType getExactTypeIfKnown(String keyspace) return null; types.add(type); } - return new TupleType(types); + return new TupleType(types.build()); } public String getText() @@ -175,7 +176,7 @@ public List getElements() } /** - * Similar to Value, but contains at least one NonTerminal, such as a non-pure functions or bind marker. + * Similar to Value, but contains at least one NonTerminal, such as a non-deterministic functions or bind marker. */ public static class DelayedValue extends Term.NonTerminal { @@ -306,7 +307,7 @@ public Raw(int bindIndex) private static ColumnSpecification makeReceiver(List receivers) { - List> types = new ArrayList<>(receivers.size()); + ImmutableList.Builder> types = ImmutableList.builderWithExpectedSize(receivers.size()); StringBuilder inName = new StringBuilder("("); for (int i = 0; i < receivers.size(); i++) { @@ -319,7 +320,7 @@ private static ColumnSpecification makeReceiver(List receivers) throws InvalidRequestException { - List> types = new ArrayList<>(receivers.size()); + ImmutableList.Builder> types = ImmutableList.builderWithExpectedSize(receivers.size()); StringBuilder inName = new StringBuilder("in("); for (int i = 0; i < receivers.size(); i++) { @@ -363,8 +364,8 @@ private static ColumnSpecification makeInReceiver(List getExactTypeIfKnown(String keyspace) @@ -455,7 +456,7 @@ public static String tupleToString(Iterable items, java.util.function.Fun public static TupleType getExactTupleTypeIfKnown(List items, java.util.function.Function> mapper) { - List> types = new ArrayList<>(items.size()); + ImmutableList.Builder> types = ImmutableList.builderWithExpectedSize(items.size()); for (T item : items) { AbstractType type = mapper.apply(item); @@ -463,7 +464,7 @@ public static TupleType getExactTupleTypeIfKnown(List items, return null; types.add(type); } - return new TupleType(types); + return new TupleType(types.build()); } /** @@ -518,13 +519,11 @@ public static AssignmentTestable.TestResult testTupleAssignment(ColumnSpecificat public static boolean checkIfTupleType(AbstractType tuple) { - return (tuple instanceof TupleType) || - (tuple instanceof ReversedType && ((ReversedType) tuple).baseType instanceof TupleType); - + return tuple.unwrap() instanceof TupleType; } public static TupleType getTupleType(AbstractType tuple) { - return (tuple instanceof ReversedType ? ((TupleType) ((ReversedType) tuple).baseType) : (TupleType)tuple); + return (TupleType) tuple.unwrap(); } } diff --git a/src/java/org/apache/cassandra/cql3/UTName.java b/src/java/org/apache/cassandra/cql3/UTName.java index c8567977bdb0..8d4655a80f50 100644 --- a/src/java/org/apache/cassandra/cql3/UTName.java +++ b/src/java/org/apache/cassandra/cql3/UTName.java @@ -18,6 +18,7 @@ package org.apache.cassandra.cql3; import java.nio.ByteBuffer; +import java.util.function.UnaryOperator; public class UTName { @@ -40,6 +41,12 @@ public void setKeyspace(String keyspace) this.ksName = keyspace; } + public void updateKeyspaceIfDefined(UnaryOperator update) + { + if (hasKeyspace()) + setKeyspace(update.apply(getKeyspace())); + } + public String getKeyspace() { return ksName; diff --git a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java index a0201c500a39..deb2f6f8c1ee 100644 --- a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java +++ b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java @@ -40,7 +40,23 @@ import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.ReadExecutionController; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.ByteType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.DoubleType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.ShortType; +import org.apache.cassandra.db.marshal.TimestampType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.ComplexColumnData; @@ -56,6 +72,11 @@ /** a utility for doing internal cql-based queries */ public abstract class UntypedResultSet implements Iterable { + public Stream stream() + { + return StreamSupport.stream(spliterator(), false); + } + public static UntypedResultSet create(ResultSet rs) { return new FromResultSet(rs); @@ -66,7 +87,7 @@ public static UntypedResultSet create(List> results) return new FromResultList(results); } - public static UntypedResultSet create(SelectStatement select, QueryPager pager, int pageSize) + public static UntypedResultSet create(SelectStatement select, QueryPager pager, PageSize pageSize) { return new FromPager(select, pager, pageSize); } @@ -80,7 +101,7 @@ public static UntypedResultSet create(SelectStatement select, ConsistencyLevel cl, ClientState clientState, QueryPager pager, - int pageSize) + PageSize pageSize) { return new FromDistributedPager(select, cl, clientState, pager, pageSize); } @@ -90,11 +111,6 @@ public boolean isEmpty() return size() == 0; } - public Stream stream() - { - return StreamSupport.stream(spliterator(), false); - } - public abstract int size(); public abstract Row one(); @@ -189,10 +205,10 @@ private static class FromPager extends UntypedResultSet { private final SelectStatement select; private final QueryPager pager; - private final int pageSize; + private final PageSize pageSize; private final List metadata; - private FromPager(SelectStatement select, QueryPager pager, int pageSize) + private FromPager(SelectStatement select, QueryPager pager, PageSize pageSize) { this.select = select; this.pager = pager; @@ -250,13 +266,14 @@ private static class FromDistributedPager extends UntypedResultSet private final ConsistencyLevel cl; private final ClientState clientState; private final QueryPager pager; - private final int pageSize; + private final PageSize pageSize; private final List metadata; private FromDistributedPager(SelectStatement select, ConsistencyLevel cl, ClientState clientState, - QueryPager pager, int pageSize) + QueryPager pager, + PageSize pageSize) { this.select = select; this.cl = cl; @@ -401,6 +418,11 @@ public double getDouble(String column) return DoubleType.instance.compose(data.get(column)); } + public float getFloat(String column) + { + return FloatType.instance.compose(data.get(column)); + } + public ByteBuffer getBytes(String column) { return data.get(column); @@ -449,6 +471,14 @@ public long getLong(String column) return LongType.instance.compose(data.get(column)); } + // this function will return the default value if the row doesn't have that column or the column data is null + // This function is used to avoid the nullpointerexception + public long getLong(String column, long ifNull) + { + ByteBuffer bytes = data.get(column); + return bytes == null ? ifNull : LongType.instance.compose(bytes); + } + public Set getSet(String column, AbstractType type) { ByteBuffer raw = data.get(column); diff --git a/src/java/org/apache/cassandra/cql3/Vectors.java b/src/java/org/apache/cassandra/cql3/Vectors.java index 152dd579364e..320069d2b3a6 100644 --- a/src/java/org/apache/cassandra/cql3/Vectors.java +++ b/src/java/org/apache/cassandra/cql3/Vectors.java @@ -184,7 +184,7 @@ public List getElements() } /** - * Basically similar to a Value, but with some non-pure function (that need + * Basically similar to a Value, but with some non-deterministic function (that need * to be evaluated at execution time) in it. */ public static class DelayedValue extends Term.NonTerminal diff --git a/src/java/org/apache/cassandra/cql3/WhereClause.java b/src/java/org/apache/cassandra/cql3/WhereClause.java index dc1a7cfde055..1659e2d6aa87 100644 --- a/src/java/org/apache/cassandra/cql3/WhereClause.java +++ b/src/java/org/apache/cassandra/cql3/WhereClause.java @@ -17,30 +17,30 @@ */ package org.apache.cassandra.cql3; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import org.antlr.runtime.RecognitionException; import org.apache.cassandra.cql3.restrictions.CustomIndexExpression; -import static java.lang.String.join; - -import static com.google.common.collect.Iterables.concat; -import static com.google.common.collect.Iterables.transform; - +/** + * This is a parsed representation of the expression following the WHERE element + * in a CQL statement. It is parsed into an arbitrary sized expression tree consisting + * of ExpressionElement elements. + */ public final class WhereClause { - private static final WhereClause EMPTY = new WhereClause(new Builder()); + private static final WhereClause EMPTY = new WhereClause(new AndElement(Collections.emptyList())); - public final List relations; - public final List expressions; + private final ExpressionElement rootElement; - private WhereClause(Builder builder) + private WhereClause(ExpressionElement rootElement) { - relations = builder.relations.build(); - expressions = builder.expressions.build(); + this.rootElement = rootElement; } public static WhereClause empty() @@ -50,26 +50,45 @@ public static WhereClause empty() public boolean containsCustomExpressions() { - return !expressions.isEmpty(); + return rootElement.containsCustomExpressions(); + } + + public ExpressionElement root() + { + return rootElement; } /** * Renames identifiers in all relations + * * @param from the old identifier - * @param to the new identifier - * @return a new WhereClause with with "from" replaced by "to" in all relations + * @param to the new identifier + * @return a new WhereClause with "from" replaced by "to" in all relations */ public WhereClause renameIdentifier(ColumnIdentifier from, ColumnIdentifier to) { - WhereClause.Builder builder = new WhereClause.Builder(); - - relations.stream() - .map(r -> r.renameIdentifier(from, to)) - .forEach(builder::add); + return new WhereClause(rootElement.rename(from, to)); + } - expressions.forEach(builder::add); + /** + * Allows mutation of the relations held within the where clause element + * hierarchy + * + * @param relationMutator the relation mutator + * @return a new WhereClause with the relations mutated + */ + public WhereClause mutateRelations(Function relationMutator) + { + return new WhereClause(rootElement.mutate(relationMutator)); + } - return builder.build(); + /** + * @return a new WhereClause with the expression tree transforemd into conjuntive form + * @see ExpressionElement#conjunctiveForm() + */ + public WhereClause conjunctiveForm() + { + return new WhereClause(rootElement.conjunctiveForm()); } public static WhereClause parse(String cql) throws RecognitionException @@ -90,9 +109,7 @@ public String toString() */ public String toCQLString() { - return join(" AND ", - concat(transform(relations, Relation::toCQLString), - transform(expressions, CustomIndexExpression::toCQLString))); + return rootElement.toString(); } @Override @@ -105,13 +122,13 @@ public boolean equals(Object o) return false; WhereClause wc = (WhereClause) o; - return relations.equals(wc.relations) && expressions.equals(wc.expressions); + return rootElement.toString().equals(wc.rootElement.toString()); } @Override public int hashCode() { - return Objects.hash(relations, expressions); + return Objects.hash(rootElement); } /** @@ -121,7 +138,7 @@ public int hashCode() */ public boolean containsTokenRelations() { - for (Relation rel : relations) + for (Relation rel : rootElement.relations()) { if (rel.onToken()) return true; @@ -129,26 +146,509 @@ public boolean containsTokenRelations() return false; } + /** + * This receives fragments from the parse operation and builds them into the final WhereClause. + * + * The received fragments are: + *
    + *
  • add(Relation) - adds a new relation to the current ParseState
  • + *
  • add(CustomIndexExpression) - adds a new custom index expression to the current ParseState
  • + *
  • startEnclosure - responds to a '(' and pushes the current ParseState onto the precedence stack
  • + *
  • endEnclosure - responds to a ')' and pulls the ParseState associated with the + * matching startEnclosure. It will pull any intermediate precedence states off the stack until it + * reaches the matching enclosure state
  • + *
  • setCurrentOperator - changes the operator in the ParseState. If this new operator is + * of a higher precedence than the current operator, the last expression is popped from the ParseState and + * the state is pushed onto the precedence stack
  • + *
  • build - always the last call. This builds the resultant ExpressionTree from the + * precedence stack and the current ParseState
  • + *
+ */ public static final class Builder { - ImmutableList.Builder relations = new ImmutableList.Builder<>(); - ImmutableList.Builder expressions = new ImmutableList.Builder<>(); + private final Deque precedenceStack = new ArrayDeque<>(); + private ParseState parseState = new ParseState(); + + public void add(Relation relation) + { + parseState.push(new RelationElement(relation)); + } + + public void add(CustomIndexExpression customIndexExpression) + { + parseState.push(new CustomIndexExpressionElement(customIndexExpression)); + } + + public void startEnclosure() + { + pushStack(PushState.ENCLOSURE); + } + + public void endEnclosure() + { + do + { + ExpressionElement expression = generate(); + parseState = precedenceStack.pop(); + parseState.push(expression); + } + while (parseState.enclosure == PushState.PRECEDENCE); + } + + public void setCurrentOperator(String value) + { + Operator operator = Operator.valueOf(value.toUpperCase()); + if (parseState.isChangeOfOperator(operator)) + { + if (parseState.higherPrecedence(operator)) + { + // Where we have a = 1 OR b = 1 AND c = 1. When the operator changes to AND + // we need to pop b = 1 from the parseState, push the parseState containing + // a = 1 OR and then add b = 1 to the new parseState + ExpressionElement last = parseState.pop(); + pushStack(PushState.PRECEDENCE); + parseState.push(last); + } + else + { + ExpressionElement element = generate(); + if (!precedenceStack.isEmpty() && precedenceStack.peek().enclosure == PushState.PRECEDENCE) + parseState = precedenceStack.pop(); + else + parseState.clear(); + parseState.push(element); + } + } + parseState.operator = operator; + } + + public WhereClause build() + { + while (!precedenceStack.isEmpty()) + { + ExpressionElement expression = generate(); + parseState = precedenceStack.pop(); + parseState.push(expression); + } + return new WhereClause(generate()); + } + + private void pushStack(PushState enclosure) + { + parseState.enclosure = enclosure; + precedenceStack.push(parseState); + parseState = new ParseState(); + } + + private ExpressionElement generate() + { + if (parseState.size() == 1) + return parseState.pop(); + return parseState.asContainer(); + } + } + + /** + * Represents the state of the parsing operation at a point of enclosure or precedence change. + */ + public static class ParseState + { + Operator operator = Operator.NONE; + PushState enclosure = PushState.NONE; + Deque expressionElements = new ArrayDeque<>(); + + void push(ExpressionElement element) + { + expressionElements.add(element); + } + + ExpressionElement pop() + { + return expressionElements.removeLast(); + } + + int size() + { + return expressionElements.size(); + } + + ParseState clear() + { + expressionElements.clear(); + return this; + } + + boolean isChangeOfOperator(Operator operator) + { + return this.operator != operator && expressionElements.size() > 1; + } + + boolean higherPrecedence(Operator operator) + { + return operator.compareTo(this.operator) > 0; + } + + ContainerElement asContainer() + { + return operator == Operator.OR + ? new OrElement(expressionElements) + : new AndElement(expressionElements); + } + } + + enum Operator + { + NONE, OR, AND; + + public String joinValue() + { + return " " + name() + " "; + } + } + + /** + * This is the reason why the ParseState was pushed onto the precedence stack. + */ + enum PushState + { + NONE, PRECEDENCE, ENCLOSURE + } + + public static abstract class ExpressionElement + { + public List operations() + { + return Collections.emptyList(); + } + + public boolean isDisjunction() + { + return false; + } + + public List relations() + { + return Collections.emptyList(); + } - public Builder add(Relation relation) + public List expressions() + { + return Collections.emptyList(); + } + + /** + * Returns true if the given function f evaluates to true on any of the expression tree nodes. + */ + public abstract boolean exists(Predicate f); + + /** + * Returns true if this expression tree contains more than one relation. + */ + public final boolean isCompound() + { + return exists(e -> e instanceof ContainerElement && ((ContainerElement) e).children.size() > 1); + } + + /** + * Returns true if this expression tree contains a CustomIndexExpressionElement node. + */ + public final boolean containsCustomExpressions() + { + return exists(CustomIndexExpressionElement.class::isInstance); + } + + public ExpressionElement rename(ColumnIdentifier from, ColumnIdentifier to) { - relations.add(relation); return this; } - public Builder add(CustomIndexExpression expression) + /** + * Collapses expression tree levels of the same type to form a semantically equivalent, + * but simpler form of this tree. + * + * Collapsing is possible because OR and AND operations are associative. + * + *

+ * Examples: + *

+         * AND(a, AND(b, c))      -> AND(a, b, c)
+         * OR(OR(a, b), OR(c, d)) -> OR(a, b, c, d)
+         * AND(a, OR(b, c))       -> AND(a, OR(b, c))
+         * 
+ *

+ * + * @return a new tree; this tree is left unmodified + */ + public ExpressionElement flatten() { - expressions.add(expression); return this; } - public WhereClause build() + /** + * Creates a new tree that is a conjunctive form of this tree, semantically equivalent to this tree. + * The root of the conjunctive form is always an AndElement. + * + * The result tree is flattened so that nested conjunctions are lifted up to become the direct + * children of the root element. If the original tree does not have a top-level AndElement, + * an AndElement is inserted at the top, and a flattened original tree becomes its only child. + * + *

+ * Examples: + *

+         * a = 1                                 -> AND(a = 1)
+         * AND()                                 -> AND()
+         * AND(a = 1, b = 2)                     -> AND(a = 1, b = 2)
+         * AND(a = 1, AND(b = 2, c = 3))         -> AND(a = 1, b = 2, c = 3)
+         * OR(a = 1, b = 2)                      -> AND(OR(a = 1, b = 2))
+         * OR(a = 1, OR(b = 2, c = 3))           -> AND(OR(a = 1, b = 2, c = 3))
+         * 
+ *

+ * + * @return a new tree; this tree is left unmodified + */ + public final AndElement conjunctiveForm() { - return new WhereClause(this); + ExpressionElement flattened = this.flatten(); + return flattened instanceof AndElement + ? (AndElement) flattened + : new AndElement(Lists.newArrayList(flattened)); + } + + protected ExpressionElement mutate(Function relationMutator) + { + return this; + } + } + + public static abstract class VariableElement extends ExpressionElement + { + @Override + public boolean exists(Predicate f) + { + return f.test(this); + } + } + + public static class RelationElement extends VariableElement + { + private final Relation relation; + + public RelationElement(Relation relation) + { + this.relation = relation; + } + + @Override + public List relations() + { + return Lists.newArrayList(relation); + } + + @Override + public ExpressionElement rename(ColumnIdentifier from, ColumnIdentifier to) + { + return new RelationElement(relation.renameIdentifier(from, to)); + } + + @Override + public String toString() + { + return relation.toString(); + } + + @Override + protected ExpressionElement mutate(Function relationMutator) + { + return new RelationElement(relationMutator.apply(relation)); + } + } + + public static class CustomIndexExpressionElement extends VariableElement + { + private final CustomIndexExpression customIndexExpression; + + public CustomIndexExpressionElement(CustomIndexExpression customIndexExpression) + { + this.customIndexExpression = customIndexExpression; + } + + @Override + public List expressions() + { + return Lists.newArrayList(customIndexExpression); + } + + @Override + public String toString() + { + return customIndexExpression.toString(); + } + } + + public static abstract class ContainerElement extends ExpressionElement + { + protected final List children; + + protected ContainerElement(Collection children) + { + this.children = new ArrayList<>(children.size()); + this.children.addAll(children); + } + + /** + * Returns a new container of the same type with new children copied from the given collection + */ + protected abstract ContainerElement withChildren(Collection children); + + @Override + protected ExpressionElement mutate(Function relationMutator) + { + List newChildren = children.stream() + .map(c -> c.mutate(relationMutator)) + .collect(Collectors.toList()); + + return this.withChildren(newChildren); + } + + protected abstract Operator operator(); + + protected abstract String emptyValue(); + + @Override + public List operations() + { + return children.stream() + .filter(c -> (c instanceof ContainerElement)) + .map(r -> ((ContainerElement) r)) + .collect(Collectors.toList()); + } + + @Override + public List relations() + { + return children.stream() + .filter(c -> (c instanceof RelationElement)) + .map(r -> (((RelationElement) r).relation)) + .collect(Collectors.toList()); + } + + @Override + public List expressions() + { + return children.stream() + .filter(c -> (c instanceof CustomIndexExpressionElement)) + .map(r -> (((CustomIndexExpressionElement) r).customIndexExpression)) + .collect(Collectors.toList()); + } + + @Override + public boolean exists(Predicate f) + { + return f.test(this) || children.stream().anyMatch(f); + } + + @Override + public ExpressionElement rename(ColumnIdentifier from, ColumnIdentifier to) + { + List newChildren = children + .stream() + .map(c -> c.rename(from, to)) + .collect(Collectors.toList()); + + return this.withChildren(newChildren); + } + + @Override + public ExpressionElement flatten() + { + List newChildren = new ArrayList<>(); + for (ExpressionElement child: children) + { + ExpressionElement flattened = child.flatten(); + newChildren.add(flattened); + + if (flattened instanceof ContainerElement) + { + ContainerElement ce = (ContainerElement) flattened; + if (ce.operator() == this.operator()) + { + newChildren.remove(newChildren.size() - 1); + newChildren.addAll(ce.children); + } + } + } + + return this.withChildren(newChildren); + } + + @Override + public String toString() + { + if (children.isEmpty()) + return emptyValue(); + + return children + .stream() + .map(c -> children.size() > 1 && c.isCompound() ? '(' + c.toString() + ')' : c.toString()) + .collect(Collectors.joining(operator().joinValue())); + } + } + + public static class AndElement extends ContainerElement + { + public AndElement(Collection children) + { + super(children); + } + + @Override + protected AndElement withChildren(Collection children) + { + return new AndElement(children); + } + + @Override + protected Operator operator() + { + return Operator.AND; + } + + @Override + protected String emptyValue() + { + return "TRUE"; + } + } + + public static class OrElement extends ContainerElement + { + public OrElement(Collection children) + { + super(children); + } + + @Override + protected OrElement withChildren(Collection children) + { + return new OrElement(children); + } + + @Override + protected Operator operator() + { + return Operator.OR; + } + + @Override + protected String emptyValue() + { + return "FALSE"; + } + + @Override + public boolean isDisjunction() + { + return true; } } } diff --git a/src/java/org/apache/cassandra/cql3/conditions/AbstractConditions.java b/src/java/org/apache/cassandra/cql3/conditions/AbstractConditions.java index 0e2646effd4e..e2c3e2976f64 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/AbstractConditions.java +++ b/src/java/org/apache/cassandra/cql3/conditions/AbstractConditions.java @@ -17,8 +17,12 @@ */ package org.apache.cassandra.cql3.conditions; +import java.util.Collections; import java.util.List; +import java.util.Set; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.functions.Function; @@ -32,11 +36,18 @@ public void addFunctionsTo(List functions) { } + @Override public Iterable getColumns() { return null; } + @Override + public Set getAnalyzedColumns(IndexRegistry indexRegistry, IndexHints indexHints) + { + return Collections.emptySet(); + } + public boolean isEmpty() { return false; diff --git a/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java b/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java index 2f6a9c572c7a..c09cd7ff146b 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java +++ b/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java @@ -41,6 +41,8 @@ */ public abstract class ColumnCondition { + public static final String ANALYZER_MATCHES_ERROR = "LWT Conditions do not support the : operator"; + public final ColumnMetadata column; public final Operator operator; private final Terms terms; @@ -255,7 +257,7 @@ else if (otherValue == null) // the condition value is not null, so only NEQ can return true return operator == Operator.NEQ; } - return operator.isSatisfiedBy(type, otherValue, value); + return operator.isSatisfiedBy(type, otherValue, value); // We don't use any analyzers in LWT, see CNDB-11658 } } @@ -666,7 +668,12 @@ private ByteBuffer rowValue(Row row) return cell == null ? null : cell.buffer(); } - Cell cell = getCell(row, column); + // getCell returns Cell, which requires a method call to properly convert. + return getCellBuffer(getCell(row, column), userType); + } + + private ByteBuffer getCellBuffer(Cell cell, UserType userType) + { return cell == null ? null : userType.split(ByteBufferAccessor.instance, cell.buffer())[userType.fieldPosition(field)]; @@ -829,6 +836,10 @@ public ColumnCondition prepare(String keyspace, ColumnMetadata receiver, TableMe if (receiver.type instanceof CounterColumnType) throw invalidRequest("Conditions on counters are not supported"); + // Analyzer matches operator is only supported on SAI indexes for now + if (operator == Operator.ANALYZER_MATCHES) + throw invalidRequest(ANALYZER_MATCHES_ERROR); + if (collectionElement != null) { if (!(receiver.type.isCollection())) diff --git a/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java b/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java index 35d4a9570f47..ea0a8b9bb4d0 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java +++ b/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java @@ -20,14 +20,19 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Set; + +import com.google.common.collect.Iterators; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.CQL3CasRequest; import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -36,7 +41,7 @@ * A set of ColumnConditions. * */ -public final class ColumnConditions extends AbstractConditions +public final class ColumnConditions extends AbstractConditions implements Iterable { /** * The conditions on regular columns. @@ -72,9 +77,30 @@ public boolean appliesToRegularColumns() @Override public Collection getColumns() { - return Stream.concat(columnConditions.stream(), staticConditions.stream()) - .map(e -> e.column) - .collect(Collectors.toList()); + List columns = new ArrayList<>(size()); + + for (ColumnCondition condition : this) + { + columns.add(condition.column); + } + + return columns; + } + + @Override + public Set getAnalyzedColumns(IndexRegistry indexRegistry, IndexHints indexHints) + { + Set analyzedColumns = new HashSet<>(); + + for (ColumnCondition condition : this) + { + if (indexRegistry.getAnalyzerFor(condition.column, condition.operator, null, indexHints).isPresent()) + { + analyzedColumns.add(condition.column); + } + } + + return analyzedColumns; } @Override @@ -83,6 +109,17 @@ public boolean isEmpty() return columnConditions.isEmpty() && staticConditions.isEmpty(); } + @Override + public Iterator iterator() + { + return Iterators.concat(columnConditions.iterator(), staticConditions.iterator()); + } + + public int size() + { + return columnConditions.size() + staticConditions.size(); + } + /** * Adds the conditions to the specified CAS request. * @@ -103,8 +140,7 @@ public void addConditionsTo(CQL3CasRequest request, @Override public void addFunctionsTo(List functions) { - columnConditions.forEach(p -> p.addFunctionsTo(functions)); - staticConditions.forEach(p -> p.addFunctionsTo(functions)); + iterator().forEachRemaining(p -> p.addFunctionsTo(functions)); } /** diff --git a/src/java/org/apache/cassandra/cql3/conditions/Conditions.java b/src/java/org/apache/cassandra/cql3/conditions/Conditions.java index 1a202dff0db2..6875db7b1832 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/Conditions.java +++ b/src/java/org/apache/cassandra/cql3/conditions/Conditions.java @@ -18,11 +18,16 @@ package org.apache.cassandra.cql3.conditions; import java.util.List; +import java.util.Set; + +import javax.annotation.Nullable; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.CQL3CasRequest; import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.ColumnMetadata; /** @@ -56,8 +61,14 @@ public interface Conditions * Returns the column definitions to which apply the conditions. * @return the column definitions to which apply the conditions. */ + @Nullable Iterable getColumns(); + /** + * @return the column definitions of the conditions supported by a {@link org.apache.cassandra.index.Index.Analyzer}. + */ + Set getAnalyzedColumns(IndexRegistry indexRegistry, IndexHints indexHints); + /** * Checks if this Conditions is empty. * @return true if this Conditions is empty, false otherwise. diff --git a/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java b/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java index 1b62daddbb96..3b52d6cfd755 100644 --- a/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java @@ -21,16 +21,14 @@ import java.util.List; import com.google.common.base.Objects; +import org.apache.commons.lang3.text.StrBuilder; import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CQL3Type.Tuple; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.commons.lang3.text.StrBuilder; - import static java.util.stream.Collectors.toList; /** @@ -68,7 +66,7 @@ public List argumentsList() { return argTypes().stream() .map(AbstractType::asCQL3Type) - .map(CQL3Type::toString) + .map(CQL3Type::toSchemaString) .collect(toList()); } @@ -105,7 +103,7 @@ public final AssignmentTestable.TestResult testAssignment(String keyspace, Colum // We should ignore the fact that the receiver type is frozen in our comparison as functions do not support // frozen types for return type AbstractType returnType = returnType(); - if (receiver.type.isFreezable() && !receiver.type.isMultiCell()) + if (!receiver.type.isMultiCell()) returnType = returnType.freeze(); if (receiver.type.equals(returnType)) @@ -159,8 +157,7 @@ public String elementName() */ protected String toCqlString(AbstractType type) { - return type.isTuple() ? ((Tuple) type.asCQL3Type()).toString(false) - : type.asCQL3Type().toString(); + return type.asCQL3Type().toString(); } @Override diff --git a/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java b/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java index 9942869a5fb5..5bc810a8b2f6 100644 --- a/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java +++ b/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.transport.ProtocolVersion; @@ -65,7 +66,9 @@ public static void addFunctionsTo(NativeFunctions functions) functions.add(new FunctionFactory("max", FunctionParameter.anyType(true)) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) { AbstractType type = argTypes.get(0); return type.isCounter() ? maxFunctionForCounter : makeMaxFunction(type); @@ -76,7 +79,9 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("min", FunctionParameter.anyType(true)) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) { AbstractType type = argTypes.get(0); return type.isCounter() ? minFunctionForCounter : makeMinFunction(type); diff --git a/src/java/org/apache/cassandra/cql3/functions/CollectionFcts.java b/src/java/org/apache/cassandra/cql3/functions/CollectionFcts.java index a3bc4726f9cc..06c56c8f82df 100644 --- a/src/java/org/apache/cassandra/cql3/functions/CollectionFcts.java +++ b/src/java/org/apache/cassandra/cql3/functions/CollectionFcts.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableList; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.CollectionType; @@ -49,7 +50,7 @@ public static void addFunctionsTo(NativeFunctions functions) functions.add(new FunctionFactory("map_keys", FunctionParameter.anyMap()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, List> argTypes, AbstractType receiverType) { return makeMapKeysFunction(name.name, (MapType) argTypes.get(0)); } @@ -58,7 +59,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("map_values", FunctionParameter.anyMap()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args,List> argTypes, AbstractType receiverType) { return makeMapValuesFunction(name.name, (MapType) argTypes.get(0)); } @@ -67,7 +68,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("collection_count", FunctionParameter.anyCollection()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args,List> argTypes, AbstractType receiverType) { return makeCollectionCountFunction(name.name, (CollectionType) argTypes.get(0)); } @@ -76,7 +77,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("collection_min", FunctionParameter.setOrList()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args,List> argTypes, AbstractType receiverType) { return makeCollectionMinFunction(name.name, (CollectionType) argTypes.get(0)); } @@ -85,7 +86,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("collection_max", FunctionParameter.setOrList()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args,List> argTypes, AbstractType receiverType) { return makeCollectionMaxFunction(name.name, (CollectionType) argTypes.get(0)); } @@ -94,7 +95,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("collection_sum", FunctionParameter.numericSetOrList()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args,List> argTypes, AbstractType receiverType) { return makeCollectionSumFunction(name.name, (CollectionType) argTypes.get(0)); } @@ -103,7 +104,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A functions.add(new FunctionFactory("collection_avg", FunctionParameter.numericSetOrList()) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args,List> argTypes, AbstractType receiverType) { return makeCollectionAvgFunction(name.name, (CollectionType) argTypes.get(0)); } @@ -121,7 +122,7 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A */ private static NativeScalarFunction makeMapKeysFunction(String name, MapType inputType) { - SetType outputType = SetType.getInstance(inputType.getKeysType(), false); + SetType outputType = SetType.getInstance((AbstractType) inputType.getKeysType().freeze(), false); return new NativeScalarFunction(name, outputType, inputType) { @@ -149,7 +150,7 @@ public ByteBuffer execute(Arguments arguments) */ private static NativeScalarFunction makeMapValuesFunction(String name, MapType inputType) { - ListType outputType = ListType.getInstance(inputType.getValuesType(), false); + ListType outputType = ListType.getInstance((AbstractType) inputType.getValuesType().freeze(), false); return new NativeScalarFunction(name, outputType, inputType) { diff --git a/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java b/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java index 356003e8e82a..7075913e8816 100644 --- a/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java +++ b/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java @@ -22,6 +22,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.QueryOptions; @@ -97,7 +98,9 @@ private Factory(String name) } @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) { if (receiverType == null) throw new InvalidRequestException(format("%s() cannot be used in the selection clause of a SELECT statement", name.name)); diff --git a/src/java/org/apache/cassandra/cql3/functions/Function.java b/src/java/org/apache/cassandra/cql3/functions/Function.java index 5af03b2fd356..94532f6c2f8e 100644 --- a/src/java/org/apache/cassandra/cql3/functions/Function.java +++ b/src/java/org/apache/cassandra/cql3/functions/Function.java @@ -34,36 +34,37 @@ public interface Function extends AssignmentTestable * A marker buffer used to represent function parameters that cannot be resolved at some stage of CQL processing. * This is used for partial function application in particular. */ - public static final ByteBuffer UNRESOLVED = ByteBuffer.allocate(0); + ByteBuffer UNRESOLVED = ByteBuffer.allocate(0); - public FunctionName name(); - public List> argTypes(); - public AbstractType returnType(); + FunctionName name(); + List> argTypes(); + AbstractType returnType(); /** * Checks whether the function is a native/hard coded one or not. * * @return {@code true} if the function is a native/hard coded one, {@code false} otherwise. */ - public boolean isNative(); + boolean isNative(); /** - * Checks whether the function is a pure function (as in doesn't depend on, nor produces side effects) or not. + * Checks whether the function is a deterministic function (as in given a particular input, will always produce the + * same output) or not. * - * @return {@code true} if the function is a pure function, {@code false} otherwise. + * @return {@code true} if the function is a deterministic function, {@code false} otherwise. */ - public boolean isPure(); + boolean isDeterministic(); /** * Checks whether the function is an aggregate function or not. * * @return {@code true} if the function is an aggregate function, {@code false} otherwise. */ - public boolean isAggregate(); + boolean isAggregate(); - public void addFunctionsTo(List functions); + void addFunctionsTo(List functions); - public boolean referencesUserType(ByteBuffer name); + boolean referencesUserType(ByteBuffer name); /** * Returns the name of the function to use within a ResultSet. @@ -71,7 +72,7 @@ public interface Function extends AssignmentTestable * @param columnNames the names of the columns used to call the function * @return the name of the function to use within a ResultSet */ - public String columnName(List columnNames); + String columnName(List columnNames); /** * Creates some new input arguments for this function. @@ -81,7 +82,7 @@ public interface Function extends AssignmentTestable */ Arguments newArguments(ProtocolVersion version); - public default Optional compare(Function other) + default Optional compare(Function other) { throw new UnsupportedOperationException(); } diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionFactory.java b/src/java/org/apache/cassandra/cql3/functions/FunctionFactory.java index 90a2c69d77f9..e1a8fae45e4c 100644 --- a/src/java/org/apache/cassandra/cql3/functions/FunctionFactory.java +++ b/src/java/org/apache/cassandra/cql3/functions/FunctionFactory.java @@ -85,10 +85,7 @@ public NativeFunction getOrCreateFunction(List arg String receiverKeyspace, String receiverTable) { - // validate the number of arguments - int numArgs = args.size(); - if (numArgs < numMandatoryParameters || numArgs > numParameters) - throw invalidNumberOfArgumentsException(); + validateNumberOfArguments(args.size()); // Do a first pass trying to infer the types of the arguments individually, without any context about the types // of the other arguments. We don't do any validation during this first pass. @@ -102,7 +99,9 @@ public NativeFunction getOrCreateFunction(List arg // Do a second pass trying to infer the types of the arguments considering the types of other inferred types. // We can validate the inferred types during this second pass. - for (int i = 0; i < args.size(); i++) + // This is done in reverse order to favour a left-to-right reading, so arguments on the right have to match + // arguments on the left. + for (int i = args.size() - 1; i >= 0; i--) { AssignmentTestable arg = args.get(i); FunctionParameter parameter = parameters.get(i); @@ -111,29 +110,33 @@ public NativeFunction getOrCreateFunction(List arg throw new InvalidRequestException(String.format("Cannot infer type of argument %s in call to " + "function %s: use type casts to disambiguate", arg, this)); - parameter.validateType(name, arg, type); + parameter.validateType(this, arg, type); type = type.udfType(); types.set(i, type); } - return doGetOrCreateFunction(types, receiverType); + return doGetOrCreateFunction(args, types, receiverType); } - public InvalidRequestException invalidNumberOfArgumentsException() + protected void validateNumberOfArguments(int numArgs) { - return new InvalidRequestException("Invalid number of arguments for function " + this); + if (numArgs < numMandatoryParameters || numArgs > numParameters) + throw new InvalidRequestException("Invalid number of arguments for function " + this); } /** * Returns a function compatible with the specified signature. * + * @param args the arguments in the function call for which the function is going to be built * @param argTypes the types of the function arguments * @param receiverType the expected return type of the function * @return a function compatible with the specified signature, or {@code null} if this cannot create a function for * the supplied arguments but there might be another factory with the same {@link #name()} able to do it. */ @Nullable - protected abstract NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType); + protected abstract NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType); @Override public String toString() diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionParameter.java b/src/java/org/apache/cassandra/cql3/functions/FunctionParameter.java index 708e4fdd7d33..92f1bd0286c0 100644 --- a/src/java/org/apache/cassandra/cql3/functions/FunctionParameter.java +++ b/src/java/org/apache/cassandra/cql3/functions/FunctionParameter.java @@ -20,14 +20,15 @@ import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.selection.Selectable; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.ListType; import org.apache.cassandra.db.marshal.MapType; import org.apache.cassandra.db.marshal.NumberType; @@ -62,7 +63,7 @@ default AbstractType inferType(String keyspace, return arg.getCompatibleTypeIfKnown(keyspace); } - void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType); + void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType); /** * @return whether this parameter is optional @@ -92,9 +93,9 @@ public AbstractType inferType(String keyspace, } @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { - wrapped.validateType(name, arg, argType); + wrapped.validateType(factory, arg, argType); } @Override @@ -116,14 +117,32 @@ public String toString() */ static FunctionParameter string() { - return fixed(CQL3Type.Native.TEXT, CQL3Type.Native.VARCHAR, CQL3Type.Native.ASCII); + return fixed("string", CQL3Type.Native.TEXT, CQL3Type.Native.VARCHAR, CQL3Type.Native.ASCII); } /** - * @param types the accepted data types + * @return a function parameter definition that accepts values that can be interpreted as floats + */ + static FunctionParameter float32() + { + return fixed("float", CQL3Type.Native.FLOAT, CQL3Type.Native.DOUBLE, CQL3Type.Native.INT, CQL3Type.Native.BIGINT); + } + + /** + * @param type the accepted data type * @return a function parameter definition that accepts values of a specific data type */ - static FunctionParameter fixed(CQL3Type... types) + static FunctionParameter fixed(CQL3Type type) + { + return fixed(type.toString(), type); + } + + /** + * @param name the name of the data type + * @param types the accepted data types + * @return a function parameter definition that accepts values of the specified data types + */ + static FunctionParameter fixed(String name, CQL3Type... types) { assert types.length > 0; @@ -140,21 +159,18 @@ public AbstractType inferType(String keyspace, } @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { if (Arrays.stream(types).allMatch(t -> argType.testAssignment(t.getType()) == NOT_ASSIGNABLE)) throw new InvalidRequestException(format("Function %s requires an argument of type %s, " + "but found argument %s of type %s", - name, this, arg, argType.asCQL3Type())); + factory, this, arg, argType.asCQL3Type())); } @Override public String toString() { - if (types.length == 1) - return types[0].toString(); - - return '[' + Arrays.stream(types).map(Object::toString).collect(Collectors.joining("|")) + ']'; + return name; } }; } @@ -178,7 +194,7 @@ public AbstractType inferType(String keyspace, } @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { // nothing to do here, all types are accepted } @@ -218,9 +234,9 @@ public AbstractType inferType(String keyspace, } @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { - parameter.validateType(name, arg, argType); + parameter.validateType(factory, arg, argType); } @Override @@ -240,12 +256,12 @@ static FunctionParameter anyCollection() return new FunctionParameter() { @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { if (!argType.isCollection()) throw new InvalidRequestException(format("Function %s requires a collection argument, " + "but found argument %s of type %s", - name, arg, argType.asCQL3Type())); + factory, arg, argType.asCQL3Type())); } @Override @@ -264,7 +280,7 @@ static FunctionParameter setOrList() return new FunctionParameter() { @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { if (argType.isCollection()) { @@ -275,7 +291,7 @@ public void validateType(FunctionName name, AssignmentTestable arg, AbstractType throw new InvalidRequestException(format("Function %s requires a set or list argument, " + "but found argument %s of type %s", - name, arg, argType.asCQL3Type())); + factory, arg, argType.asCQL3Type())); } @Override @@ -295,7 +311,7 @@ static FunctionParameter numericSetOrList() return new FunctionParameter() { @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { AbstractType elementType = null; if (argType.isCollection()) @@ -314,7 +330,7 @@ else if (collectionType.kind == CollectionType.Kind.LIST) if (!(elementType instanceof NumberType)) throw new InvalidRequestException(format("Function %s requires a numeric set/list argument, " + "but found argument %s of type %s", - name, arg, argType.asCQL3Type())); + factory, arg, argType.asCQL3Type())); } @Override @@ -334,12 +350,12 @@ static FunctionParameter anyMap() return new FunctionParameter() { @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { if (!argType.isUDT() && !(argType instanceof MapType)) throw new InvalidRequestException(format("Function %s requires a map argument, " + "but found argument %s of type %s", - name, arg, argType.asCQL3Type())); + factory, arg, argType.asCQL3Type())); } @Override @@ -373,7 +389,7 @@ public AbstractType inferType(String keyspace, } @Override - public void validateType(FunctionName name, AssignmentTestable arg, AbstractType argType) + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) { if (argType.isVector()) { @@ -390,7 +406,7 @@ else if (argType instanceof ListType) // if it's terminal it will be a list throw new InvalidRequestException(format("Function %s requires a %s vector argument, " + "but found argument %s of type %s", - name, type, arg, argType.asCQL3Type())); + factory, type, arg, argType.asCQL3Type())); } @Override @@ -400,4 +416,56 @@ public String toString() } }; } + + /** + * @param name the name of the function parameter + * @param type the accepted type of literal + * @param inferredType the inferred type of the literal + * @return a function parameter definition that accepts a specific literal type + */ + static FunctionParameter literal(String name, Constants.Type type, AbstractType inferredType) + { + return new FunctionParameter() + { + @Override + public AbstractType inferType(String keyspace, + AssignmentTestable arg, + @Nullable AbstractType receiverType, + @Nullable List> inferredTypes) + { + return inferredType; + } + + @Override + public void validateType(FunctionFactory factory, AssignmentTestable arg, AbstractType argType) + { + if (arg instanceof Selectable.WithTerm) + arg = ((Selectable.WithTerm) arg).rawTerm; + + if (!(arg instanceof Constants.Literal)) + throw invalidArgumentException(factory, arg); + + Constants.Literal literal = (Constants.Literal) arg; + if (literal.type != type) + throw invalidArgumentException(factory, arg); + } + + private InvalidRequestException invalidArgumentException(FunctionFactory factory, AssignmentTestable arg) + { + throw new InvalidRequestException(format("Function %s requires a %s argument, but found %s", + factory, this, arg)); + } + + @Override + public String toString() + { + return name; + } + }; + } + + static FunctionParameter literalInteger() + { + return literal("literal_int", Constants.Type.INTEGER, Int32Type.instance); + } } diff --git a/src/java/org/apache/cassandra/cql3/functions/IndexFcts.java b/src/java/org/apache/cassandra/cql3/functions/IndexFcts.java new file mode 100644 index 000000000000..143eebd2fe83 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/functions/IndexFcts.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cql3.functions; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.google.common.base.Charsets; + +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.sai.analyzer.JSONAnalyzerParser; +import org.apache.cassandra.index.sai.analyzer.LuceneAnalyzer; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.lucene.analysis.Analyzer; + +public abstract class IndexFcts +{ + public static void addFunctionsTo(NativeFunctions functions) + { + functions.add(new SAIAnalyzeFunction()); + } + + /** + * CQL native function to get the tokens produced for given text value and the analyzer defined by the given JSON options. + */ + private static class SAIAnalyzeFunction extends NativeScalarFunction + { + private static final String NAME = "sai_analyze"; + private static final ListType returnType = ListType.getInstance(UTF8Type.instance, false); + + private SAIAnalyzeFunction() + { + super(NAME, returnType, UTF8Type.instance, UTF8Type.instance); + } + + @Override + public ByteBuffer execute(Arguments arguments) throws InvalidRequestException + { + if (arguments.get(0) == null) + return null; + String text = arguments.get(0); + + if (arguments.get(1) == null) + throw new InvalidRequestException("Function " + name + " requires a non-null json_analyzer parameter (2nd argument)"); + String json = arguments.get(1); + + LuceneAnalyzer luceneAnalyzer = null; + List tokens = new ArrayList<>(); + try (Analyzer analyzer = JSONAnalyzerParser.parse(json).left) + { + luceneAnalyzer = new LuceneAnalyzer(UTF8Type.instance, analyzer, new HashMap<>()); + + ByteBuffer toAnalyze = ByteBuffer.wrap(text.getBytes(Charsets.UTF_8)); + luceneAnalyzer.reset(toAnalyze); + ByteBuffer analyzed; + + while (luceneAnalyzer.hasNext()) + { + analyzed = luceneAnalyzer.next(); + tokens.add(ByteBufferUtil.string(analyzed, Charsets.UTF_8)); + } + } + catch (Exception ex) + { + throw new InvalidRequestException("Function " + name + " unable to analyze text=" + text + " json_analyzer=" + json, ex); + } + finally + { + if (luceneAnalyzer != null) + { + luceneAnalyzer.end(); + } + } + + return returnType.decompose(tokens); + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java b/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java index e51b9cbfc6dc..ba4c03581cfb 100644 --- a/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java @@ -40,6 +40,8 @@ import com.google.common.io.ByteStreams; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; +import org.eclipse.jdt.internal.compiler.lookup.ModuleBinding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,8 +61,6 @@ import org.eclipse.jdt.internal.compiler.env.INameEnvironment; import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; -import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; -import org.eclipse.jdt.internal.compiler.lookup.ModuleBinding; import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; public final class JavaBasedUDFunction extends UDFunction @@ -186,10 +186,17 @@ protected URLConnection openConnection(URL u) private static final Pattern patternJavaDriver = Pattern.compile("com\\.datastax\\.driver\\.core\\."); - JavaBasedUDFunction(FunctionName name, List argNames, List> argTypes, - AbstractType returnType, boolean calledOnNullInput, String body) + JavaBasedUDFunction(FunctionName name, + List argNames, + List> argTypes, + AbstractType returnType, + boolean calledOnNullInput, + String body, + boolean deterministic, + boolean monotonic, + List monotonicOn) { - super(name, argNames, argTypes, returnType, calledOnNullInput, "java", body); + super(name, argNames, argTypes, returnType, calledOnNullInput, "java", body, deterministic, monotonic, monotonicOn); // put each UDF in a separate package to prevent cross-UDF code access String pkgName = BASE_PACKAGE + '.' + generateClassName(name, 'p'); diff --git a/src/java/org/apache/cassandra/cql3/functions/NativeFunction.java b/src/java/org/apache/cassandra/cql3/functions/NativeFunction.java index 3437a8d1586f..af45668e6f93 100644 --- a/src/java/org/apache/cassandra/cql3/functions/NativeFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/NativeFunction.java @@ -41,9 +41,9 @@ public final boolean isNative() } @Override - public boolean isPure() + public boolean isDeterministic() { - // Most of our functions are pure, the other ones should override this + // Most of our functions are deterministic, the other ones should override this return true; } diff --git a/src/java/org/apache/cassandra/cql3/functions/NativeFunctions.java b/src/java/org/apache/cassandra/cql3/functions/NativeFunctions.java index 2100fe3f898e..bda9da58fd39 100644 --- a/src/java/org/apache/cassandra/cql3/functions/NativeFunctions.java +++ b/src/java/org/apache/cassandra/cql3/functions/NativeFunctions.java @@ -47,6 +47,7 @@ public class NativeFunctions MathFcts.addFunctionsTo(this); MaskingFcts.addFunctionsTo(this); VectorFcts.addFunctionsTo(this); + IndexFcts.addFunctionsTo(this); } }; diff --git a/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java b/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java index e492f758f10f..3ae0607f7a2e 100644 --- a/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java @@ -17,9 +17,6 @@ */ package org.apache.cassandra.cql3.functions; -import java.nio.ByteBuffer; -import java.util.List; - import org.apache.cassandra.db.marshal.AbstractType; /** @@ -41,17 +38,4 @@ public final boolean isAggregate() { return false; } - - /** - * Checks if a partial application of the function is monotonic. - * - *

A function is monotonic if it is either entirely nonincreasing or nondecreasing.

- * - * @param partialParameters the input parameters used to create the partial application of the function - * @return {@code true} if the partial application of the function is monotonic {@code false} otherwise. - */ - protected boolean isPartialApplicationMonotonic(List partialParameters) - { - return isMonotonic(); - } } diff --git a/src/java/org/apache/cassandra/cql3/functions/PartiallyAppliedScalarFunction.java b/src/java/org/apache/cassandra/cql3/functions/PartiallyAppliedScalarFunction.java index 7a5e5fb71ff3..3fa2484f446f 100644 --- a/src/java/org/apache/cassandra/cql3/functions/PartiallyAppliedScalarFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/PartiallyAppliedScalarFunction.java @@ -46,14 +46,13 @@ final class PartiallyAppliedScalarFunction extends NativeScalarFunction implemen @Override public boolean isMonotonic() { - return function.isNative() ? ((NativeScalarFunction) function).isPartialApplicationMonotonic(partialParameters) - : function.isMonotonic(); + return function.isPartialApplicationMonotonic(partialParameters); } @Override - public boolean isPure() + public boolean isDeterministic() { - return function.isPure(); + return function.isDeterministic(); } @Override diff --git a/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java b/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java index 986242a4135c..ce80c22e3be0 100644 --- a/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java @@ -28,16 +28,16 @@ */ public interface ScalarFunction extends Function { - public boolean isCalledOnNullInput(); + boolean isCalledOnNullInput(); /** * Checks if the function is monotonic. - * - *

A function is monotonic if it is either entirely nonincreasing or nondecreasing given an ordered set of inputs.

+ *

+ * A function is monotonic if it is either entirely nonincreasing or nondecreasing given an ordered set of inputs. * * @return {@code true} if the function is monotonic {@code false} otherwise. */ - public default boolean isMonotonic() + default boolean isMonotonic() { return false; } @@ -49,7 +49,7 @@ public default boolean isMonotonic() * @return the result of applying this function to the arguments * @throws InvalidRequestException if this function cannot not be applied to the arguments */ - public ByteBuffer execute(Arguments arguments) throws InvalidRequestException; + ByteBuffer execute(Arguments arguments) throws InvalidRequestException; /** * Does a partial application of the function. That is, given only some of the arguments of the function provided, @@ -69,7 +69,7 @@ public default boolean isMonotonic() * @param partialArguments a list of input arguments for the function where some arguments can be {@link #UNRESOLVED}. * The input must be of size {@code this.argsType().size()}. For convenience, it is * allowed both to pass a list with all arguments being {@link #UNRESOLVED} (the function is - * then returned directly) and with none of them unresolved (in which case, if the function is pure, + * then returned directly) and with none of them unresolved (in which case, if the function is deterministic, * it is computed and a dummy no-arg function returning the result is returned). * @return a function corresponding to the partial application of this function to the arguments of * {@code partialArguments} that are not {@link #UNRESOLVED}. @@ -86,7 +86,7 @@ default ScalarFunction partialApplication(ProtocolVersion protocolVersion, List< if (unresolvedCount == argTypes().size()) return this; - if (isPure() && unresolvedCount == 0) + if (isDeterministic() && unresolvedCount == 0) { Arguments arguments = newArguments(protocolVersion); for (int i = 0, m = partialArguments.size(); i < m; i++) @@ -103,4 +103,16 @@ default ScalarFunction partialApplication(ProtocolVersion protocolVersion, List< return new PartiallyAppliedScalarFunction(this, partialArguments, unresolvedCount); } + + /** + * Checks if a partial application of the function is monotonic. + * + *

A function is monotonic if it is either entirely nonincreasing or nondecreasing.

+ * @param partialParameters the input parameters used to create the partial application of the function + * @return {@code true} if the partial application of the function is monotonic {@code false} otherwise. + */ + default boolean isPartialApplicationMonotonic(List partialParameters) + { + return isMonotonic(); + } } diff --git a/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java b/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java index eb547f0f625d..b175d0ce3056 100644 --- a/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java +++ b/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java @@ -77,7 +77,7 @@ public ByteBuffer execute(Arguments arguments) } @Override - public boolean isPure() + public boolean isDeterministic() { return false; // as it returns non-identical results for identical arguments } @@ -294,7 +294,7 @@ protected FloorFunction(AbstractType returnType, } @Override - protected boolean isPartialApplicationMonotonic(List partialParameters) + public boolean isPartialApplicationMonotonic(List partialParameters) { return partialParameters.get(0) == UNRESOLVED && partialParameters.get(1) != UNRESOLVED @@ -461,7 +461,7 @@ protected void validateDuration(Duration duration) public static final NativeScalarFunction floorTime = new NativeScalarFunction("floor", TimeType.instance, TimeType.instance, DurationType.instance) { @Override - protected boolean isPartialApplicationMonotonic(List partialParameters) + public boolean isPartialApplicationMonotonic(List partialParameters) { return partialParameters.get(0) == UNRESOLVED && partialParameters.get(1) != UNRESOLVED; } diff --git a/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java b/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java index a1182a9c9e57..37bbda0fb414 100644 --- a/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java +++ b/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.cql3.functions; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -90,7 +91,9 @@ public Factory(String name) } @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) { return ToJsonFct.getInstance(name.name, argTypes); } diff --git a/src/java/org/apache/cassandra/cql3/functions/TokenFct.java b/src/java/org/apache/cassandra/cql3/functions/TokenFct.java index dd163b6b6da3..839cb6555d53 100644 --- a/src/java/org/apache/cassandra/cql3/functions/TokenFct.java +++ b/src/java/org/apache/cassandra/cql3/functions/TokenFct.java @@ -25,7 +25,7 @@ import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.db.CBuilder; +import org.apache.cassandra.db.ClusteringBuilder; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.transport.ProtocolVersion; @@ -59,7 +59,7 @@ private static AbstractType[] getKeyTypes(TableMetadata metadata) public ByteBuffer execute(Arguments arguments) throws InvalidRequestException { - CBuilder builder = CBuilder.create(metadata.partitionKeyAsClusteringComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(metadata.partitionKeyAsClusteringComparator()); for (int i = 0; i < arguments.size(); i++) { ByteBuffer bb = arguments.get(i); @@ -96,7 +96,9 @@ public NativeFunction getOrCreateFunction(List arg } @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) { throw new AssertionError("Should be unreachable"); } diff --git a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java index 2b15c8d9355e..642cbd0be2b3 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java @@ -18,7 +18,11 @@ package org.apache.cassandra.cql3.functions; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import com.google.common.base.Objects; import com.google.common.collect.Lists; @@ -31,6 +35,7 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.Difference; +import org.apache.cassandra.schema.Types; import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.ProtocolVersion; @@ -52,13 +57,15 @@ public class UDAggregate extends UserFunction implements AggregateFunction protected final ByteBuffer initcond; private final ScalarFunction stateFunction; private final ScalarFunction finalFunction; + private final boolean deterministic; public UDAggregate(FunctionName name, List> argTypes, AbstractType returnType, ScalarFunction stateFunc, ScalarFunction finalFunc, - ByteBuffer initcond) + ByteBuffer initcond, + boolean deterministic) { super(name, argTypes, returnType); this.stateFunction = stateFunc; @@ -67,6 +74,7 @@ public UDAggregate(FunctionName name, this.resultType = UDFDataType.wrap(returnType, false); this.stateType = stateFunc != null ? UDFDataType.wrap(stateFunc.returnType(), false) : null; this.initcond = initcond; + this.deterministic = deterministic; } public static UDAggregate create(Collection functions, @@ -76,7 +84,8 @@ public static UDAggregate create(Collection functions, FunctionName stateFunc, FunctionName finalFunc, AbstractType stateType, - ByteBuffer initcond) + ByteBuffer initcond, + boolean deterministic) { List> stateTypes = new ArrayList<>(argTypes.size() + 1); stateTypes.add(stateType); @@ -87,7 +96,8 @@ public static UDAggregate create(Collection functions, returnType, findFunction(name, functions, stateFunc, stateTypes), null == finalFunc ? null : findFunction(name, functions, finalFunc, finalTypes), - initcond); + initcond, + deterministic); } private static UDFunction findFunction(FunctionName udaName, Collection functions, FunctionName name, List> arguments) @@ -98,10 +108,10 @@ private static UDFunction findFunction(FunctionName udaName, Collection new ConfigurationException(String.format("Unable to find function %s referenced by UDA %s", name, udaName))); } - public boolean isPure() + @Override + public boolean isDeterministic() { - // Right now, we have no way to check if an UDA is pure. Due to that we consider them as non pure to avoid any risk. - return false; + return deterministic; } @Override @@ -135,7 +145,31 @@ public UDAggregate withUpdatedUserType(Collection udfs, UserType udt returnType.withUpdatedUserType(udt), findFunction(name, udfs, stateFunction.name(), stateFunction.argTypes()), null == finalFunction ? null : findFunction(name, udfs, finalFunction.name(), finalFunction.argTypes()), - initcond); + initcond, + deterministic); + } + + public UDAggregate withNewKeyspace(String newKeyspace, Collection udfs, Types types) + { + return new UDAggregate(new FunctionName(newKeyspace, name.name), + withUpdatedUserTypes(argTypes, types), + returnType.withUpdatedUserTypes(types), + findFunction(name, + udfs, + new FunctionName(newKeyspace, stateFunction.name().name), + withUpdatedUserTypes(stateFunction.argTypes(), types)), + null == finalFunction ? null + : findFunction(name, + udfs, + new FunctionName(newKeyspace, finalFunction.name().name), + withUpdatedUserTypes(finalFunction.argTypes(), types)), + initcond, + deterministic); + } + + private List> withUpdatedUserTypes(List> argTypes, Types types) + { + return Lists.newArrayList(transform(argTypes, t -> t.withUpdatedUserTypes(types))); } @Override @@ -372,6 +406,10 @@ public String toCqlString(boolean withInternals, boolean ifNotExists) .append("INITCOND ") .append(stateType().asCQL3Type().toCQLLiteral(initialCondition())); + if (deterministic) + builder.newLine() + .append("DETERMINISTIC"); + return builder.append(";") .toString(); } diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java index 538d80e9923f..47e9de78d73a 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java @@ -27,8 +27,8 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; // checkstyle: permit this import import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; // checkstyle: permit this import import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -41,6 +41,7 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.concurrent.ImmediateExecutor; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.ColumnIdentifier; @@ -50,7 +51,9 @@ import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.FunctionExecutionException; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.Difference; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.ProtocolVersion; @@ -75,6 +78,10 @@ public abstract class UDFunction extends UserFunction implements ScalarFunction protected final String language; protected final String body; + protected final boolean deterministic; + protected final boolean monotonic; + protected final List monotonicOn; + protected final List argumentTypes; protected final UDFDataType resultType; protected final boolean calledOnNullInput; @@ -99,6 +106,8 @@ public abstract class UDFunction extends UserFunction implements ScalarFunction "com/google/common/reflect/TypeToken", "java/io/IOException.class", "java/io/Serializable.class", + "java/io/ObjectOutputStream.class", + "java/io/ObjectInputStream.class", "java/lang/", "java/math/", "java/net/InetAddress.class", @@ -209,13 +218,19 @@ protected UDFunction(FunctionName name, AbstractType returnType, boolean calledOnNullInput, String language, - String body) + String body, + boolean deterministic, + boolean monotonic, + List monotonicOn) { super(name, argTypes, returnType); assert new HashSet<>(argNames).size() == argNames.size() : "duplicate argument names"; this.argNames = argNames; this.language = language; this.body = body; + this.deterministic = deterministic; + this.monotonic = monotonic; + this.monotonicOn = monotonicOn; this.argumentTypes = UDFDataType.wrap(argTypes, !calledOnNullInput); this.resultType = UDFDataType.wrap(returnType, !calledOnNullInput); this.calledOnNullInput = calledOnNullInput; @@ -234,15 +249,18 @@ public static UDFunction tryCreate(FunctionName name, AbstractType returnType, boolean calledOnNullInput, String language, - String body) + String body, + boolean deterministic, + boolean monotonic, + List monotonicOn) { try { - return create(name, argNames, argTypes, returnType, calledOnNullInput, language, body); + return create(name, argNames, argTypes, returnType, calledOnNullInput, language, body, deterministic, monotonic, monotonicOn); } catch (InvalidRequestException e) { - return createBrokenFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body, e); + return createBrokenFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body, deterministic, monotonic, monotonicOn, e); } } @@ -252,11 +270,14 @@ public static UDFunction create(FunctionName name, AbstractType returnType, boolean calledOnNullInput, String language, - String body) + String body, + boolean deterministic, + boolean monotonic, + List monotonicOn) { assertUdfsEnabled(language); - return new JavaBasedUDFunction(name, argNames, argTypes, returnType, calledOnNullInput, body); + return new JavaBasedUDFunction(name, argNames, argTypes, returnType, calledOnNullInput, body, deterministic, monotonic, monotonicOn); } /** @@ -275,9 +296,12 @@ public static UDFunction createBrokenFunction(FunctionName name, boolean calledOnNullInput, String language, String body, + boolean deterministic, + boolean monotonic, + List monotonicOn, InvalidRequestException reason) { - return new UDFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body) + return new UDFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body, deterministic, monotonic, monotonicOn) { protected ExecutorService executor() { @@ -341,8 +365,17 @@ public String toCqlString(boolean withInternals, boolean ifNotExists) .append(" ON NULL INPUT") .newLine() .append("RETURNS ") - .append(toCqlString(returnType())) - .newLine() + .append(toCqlString(returnType())); + + if (deterministic) + builder.newLine().append("DETERMINISTIC"); + + if (monotonic) + builder.newLine().append("MONOTONIC"); + else if (!monotonicOn.isEmpty()) + builder.newLine().append("MONOTONIC ON ").append(monotonicOn.get(0).toCQLString()); + + builder.newLine() .append("LANGUAGE ") .append(language()) .newLine() @@ -354,10 +387,40 @@ public String toCqlString(boolean withInternals, boolean ifNotExists) } @Override - public boolean isPure() + public boolean isDeterministic() { - // Right now, we have no way to check if an UDF is pure. Due to that we consider them as non pure to avoid any risk. - return false; + return deterministic; + } + + @Override + public boolean isMonotonic() + { + return monotonic; + } + + public List monotonicOn() + { + return monotonicOn; + } + + @Override + public boolean isPartialApplicationMonotonic(List partialParameters) + { + assert partialParameters.size() == argNames.size(); + if (!monotonic) + { + for (int i = 0; i < partialParameters.size(); i ++) + { + ByteBuffer partialParameter = partialParameters.get(i); + if (partialParameter == Function.UNRESOLVED) + { + ColumnIdentifier unresolvedArgumentName = argNames.get(i); + if (!monotonicOn.contains(unresolvedArgumentName)) + return false; + } + } + } + return true; } @Override @@ -426,6 +489,10 @@ public final Object executeForAggregate(Object state, Arguments arguments) public static void assertUdfsEnabled(String language) { + if (CassandraRelevantProperties.DISABLE_USER_DEFINED_FUNCTIONS.getBoolean()) + throw new InvalidRequestException("User-defined functions are disabled. " + + "The system property cassandra.disable_user_defined_functions is set to true."); + if (!DatabaseDescriptor.enableUserDefinedFunctions()) throw new InvalidRequestException("User-defined functions are disabled in cassandra.yaml - set user_defined_functions_enabled=true to enable"); if (!"java".equalsIgnoreCase(language)) @@ -622,7 +689,24 @@ public UDFunction withUpdatedUserType(UserType udt) returnType.withUpdatedUserType(udt), calledOnNullInput, language, - body); + body, + deterministic, + monotonic, + monotonicOn); + } + + public UDFunction withNewKeyspace(String newKeyspace, Types types) + { + return tryCreate(new FunctionName(newKeyspace, name.name), + argNames, + Lists.newArrayList(transform(argTypes, t -> t.withUpdatedUserTypes(types))), + returnType.withUpdatedUserTypes(types), + calledOnNullInput, + language, + body, + deterministic, + monotonic, + monotonicOn); } @Override diff --git a/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java b/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java index 3e90deb68340..77a71c2fb4b4 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java +++ b/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java @@ -37,5 +37,11 @@ public ByteBuffer execute(Arguments arguments) { return UUIDSerializer.instance.serialize(UUID.randomUUID()); } + + @Override + public boolean isDeterministic() + { + return false; // since UUIDs are generated randomly and so function calls are not deterministic + } }; } diff --git a/src/java/org/apache/cassandra/cql3/functions/VectorFcts.java b/src/java/org/apache/cassandra/cql3/functions/VectorFcts.java index ae219a529bc5..427d428a6a93 100644 --- a/src/java/org/apache/cassandra/cql3/functions/VectorFcts.java +++ b/src/java/org/apache/cassandra/cql3/functions/VectorFcts.java @@ -20,14 +20,24 @@ import java.nio.ByteBuffer; import java.util.List; +import java.util.Random; +import io.github.jbellis.jvector.vector.ArrayVectorFloat; +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.VectorUtil; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.NumberType; import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.transport.ProtocolVersion; -import io.github.jbellis.jvector.vector.VectorSimilarityFunction; + +import static java.lang.String.format; +import static org.apache.cassandra.index.sai.disk.vector.VectorValidation.isEffectivelyZero; public class VectorFcts { @@ -36,10 +46,12 @@ public static void addFunctionsTo(NativeFunctions functions) functions.add(createSimilarityFunctionFactory("similarity_cosine", VectorSimilarityFunction.COSINE, false)); functions.add(createSimilarityFunctionFactory("similarity_euclidean", VectorSimilarityFunction.EUCLIDEAN, true)); functions.add(createSimilarityFunctionFactory("similarity_dot_product", VectorSimilarityFunction.DOT_PRODUCT, true)); + functions.add(new RandomFloatVectorFunctionFactory()); + functions.add(new NormalizeL2FunctionFactory()); } private static FunctionFactory createSimilarityFunctionFactory(String name, - VectorSimilarityFunction vectorSimilarityFunction, + VectorSimilarityFunction luceneFunction, boolean supportsZeroVectors) { return new FunctionFactory(name, @@ -48,14 +60,16 @@ private static FunctionFactory createSimilarityFunctionFactory(String name, { @Override @SuppressWarnings("unchecked") - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) { // check that all arguments have the same vector dimensions VectorType firstArgType = (VectorType) argTypes.get(0); int dimensions = firstArgType.dimension; if (!argTypes.stream().allMatch(t -> ((VectorType) t).dimension == dimensions)) throw new InvalidRequestException("All arguments must have the same vector dimensions"); - return createSimilarityFunction(name.name, firstArgType, vectorSimilarityFunction, supportsZeroVectors); + return createSimilarityFunction(name.name, firstArgType, luceneFunction, supportsZeroVectors); } }; } @@ -65,6 +79,7 @@ private static NativeFunction createSimilarityFunction(String name, VectorSimilarityFunction f, boolean supportsZeroVectors) { + var vts = VectorizationProvider.getInstance().getVectorTypeSupport(); return new NativeScalarFunction(name, FloatType.instance, type, type) { @Override @@ -81,25 +96,147 @@ public ByteBuffer execute(Arguments arguments) throws InvalidRequestException if (arguments.containsNulls()) return null; - float[] v1 = arguments.get(0); - float[] v2 = arguments.get(1); + var v1 = vts.createFloatVector(arguments.get(0)); + var v2 = vts.createFloatVector(arguments.get(1)); - if (!supportsZeroVectors) - { - if (isAllZero(v1) || isAllZero(v2)) - throw new InvalidRequestException("Function " + name + " doesn't support all-zero vectors."); - } + if (!supportsZeroVectors && (isEffectivelyZero(v1) || isEffectivelyZero(v2))) + throw new InvalidRequestException("Function " + name + " doesn't support all-zero vectors."); return FloatType.instance.decompose(f.compare(v1, v2)); } + }; + } + + /** + * CQL native function create a random float vector of a certain dimension. + * All the components of the vector will be random floats between the specified min and max values. + */ + private static class RandomFloatVectorFunctionFactory extends FunctionFactory + { + private static final String NAME = "random_float_vector"; + + private RandomFloatVectorFunctionFactory() + { + super(NAME, FunctionParameter.literalInteger(), FunctionParameter.float32(), FunctionParameter.float32()); + } - private boolean isAllZero(float[] v) + @Override + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) + { + // Get the vector type from the dimension argument. We need to do this here assuming that the argument is a + // literal, so we know the dimension of the return type before actually executing the function. + int dimension = Integer.parseInt(args.get(0).toString()); + VectorType type = VectorType.getInstance(FloatType.instance, dimension); + + final NumberType minType = (NumberType) argTypes.get(1); + final NumberType maxType = (NumberType) argTypes.get(2); + + return new NativeScalarFunction(name.name, type, Int32Type.instance, minType, maxType) { - for (float f : v) - if (f != 0) - return false; - return true; - } - }; + private final Random random = new Random(); + + @Override + public Arguments newArguments(ProtocolVersion version) + { + return new FunctionArguments(version, + (v, b) -> Int32Type.instance.compose(b), + (v, b) -> { + if (b == null || !b.hasRemaining()) + throw new InvalidRequestException(format("Min argument of function %s must not be null", + RandomFloatVectorFunctionFactory.this)); + return minType.compose(b).floatValue(); + }, + (v, b) -> { + if (b == null || !b.hasRemaining()) + throw new InvalidRequestException(format("Max argument of function %s must not be null", + RandomFloatVectorFunctionFactory.this)); + return maxType.compose(b).floatValue(); + }); + } + + @Override + public ByteBuffer execute(Arguments arguments) + { + // get the min argument + float min = arguments.get(1); + if (!Float.isFinite(min)) + throw new InvalidRequestException("Min value must be finite"); + + // get the max argument + float max = arguments.get(2); + if (!Float.isFinite(max)) + throw new InvalidRequestException("Max value must be finite"); + if (max <= min) + throw new InvalidRequestException("Max value must be greater than min value"); + + // generate the random vector within the range defined by min and max + float[] vector = new float[dimension]; + for (int i = 0; i < dimension; i++) + { + // promote to double to avoid overflow with large (absolute value) min and/or max + double dmin = min; + double dmax = max; + vector[i] = (float) (dmin + random.nextDouble() * (dmax - dmin)); + } + + return type.getSerializer().serializeFloatArray(vector); + } + }; + } + } + + /** + * CQL native function to normalize a vector using L2 normalization. + */ + private static class NormalizeL2FunctionFactory extends FunctionFactory + { + private static final String NAME = "normalize_l2"; + + public NormalizeL2FunctionFactory() + { + super(NAME, FunctionParameter.vector(CQL3Type.Native.FLOAT)); + } + + @Override + @SuppressWarnings("unchecked") + protected NativeFunction doGetOrCreateFunction(List args, + List> argTypes, + AbstractType receiverType) + { + var vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + VectorType vectorType = (VectorType) argTypes.get(0); + + return new NativeScalarFunction(name.name, vectorType, vectorType) + { + @Override + public Arguments newArguments(ProtocolVersion version) + { + return new FunctionArguments(version, + (v, b) -> { + if (b == null || !b.hasRemaining()) + return null; + return vectorType.getSerializer().deserializeFloatArray(b); + }); + } + + @Override + public ByteBuffer execute(Arguments arguments) + { + // get the vector argument + var arg0 = arguments.get(0); + if (arg0 == null) + return null; + var vector = vts.createFloatVector(arg0); + + // normalize + VectorUtil.l2normalize(vector); + + // serialize the normalized vector + return vectorType.getSerializer().serializeFloatArray(((ArrayVectorFloat) vector).get()); + } + }; + } } } diff --git a/src/java/org/apache/cassandra/cql3/functions/masking/DefaultMaskingFunction.java b/src/java/org/apache/cassandra/cql3/functions/masking/DefaultMaskingFunction.java index e5c0ee2e7d6b..75e402368b4e 100644 --- a/src/java/org/apache/cassandra/cql3/functions/masking/DefaultMaskingFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/masking/DefaultMaskingFunction.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.functions.Arguments; import org.apache.cassandra.cql3.functions.FunctionArguments; import org.apache.cassandra.cql3.functions.FunctionFactory; @@ -70,7 +71,7 @@ public static FunctionFactory factory() return new MaskingFunction.Factory(NAME, FunctionParameter.anyType(false)) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, List> argTypes, AbstractType receiverType) { return new DefaultMaskingFunction(name, argTypes.get(0)); } diff --git a/src/java/org/apache/cassandra/cql3/functions/masking/HashMaskingFunction.java b/src/java/org/apache/cassandra/cql3/functions/masking/HashMaskingFunction.java index 3291b156f0e1..142353ca866c 100644 --- a/src/java/org/apache/cassandra/cql3/functions/masking/HashMaskingFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/masking/HashMaskingFunction.java @@ -28,6 +28,7 @@ import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; import org.apache.cassandra.cql3.functions.Arguments; @@ -136,7 +137,7 @@ public static FunctionFactory factory() FunctionParameter.optional(FunctionParameter.fixed(CQL3Type.Native.TEXT))) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, List> argTypes, AbstractType receiverType) { switch (argTypes.size()) { @@ -145,7 +146,8 @@ protected NativeFunction doGetOrCreateFunction(List> argTypes, A case 2: return new HashMaskingFunction(name, argTypes.get(0), true); default: - throw invalidNumberOfArgumentsException(); + throw new InvalidRequestException("Invalid number of arguments for function " + this); +// throw invalidNumberOfArgumentsException(); } } }; diff --git a/src/java/org/apache/cassandra/cql3/functions/masking/NullMaskingFunction.java b/src/java/org/apache/cassandra/cql3/functions/masking/NullMaskingFunction.java index 830a1c3b5484..046fd0e44fbd 100644 --- a/src/java/org/apache/cassandra/cql3/functions/masking/NullMaskingFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/masking/NullMaskingFunction.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.functions.Arguments; import org.apache.cassandra.cql3.functions.FunctionArguments; import org.apache.cassandra.cql3.functions.FunctionFactory; @@ -65,7 +66,7 @@ public static FunctionFactory factory() return new MaskingFunction.Factory(NAME, FunctionParameter.anyType(false)) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, List> argTypes, AbstractType receiverType) { return new NullMaskingFunction(name, argTypes.get(0)); } diff --git a/src/java/org/apache/cassandra/cql3/functions/masking/PartialMaskingFunction.java b/src/java/org/apache/cassandra/cql3/functions/masking/PartialMaskingFunction.java index 8f5a794b8e85..a7163d0bed3e 100644 --- a/src/java/org/apache/cassandra/cql3/functions/masking/PartialMaskingFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/masking/PartialMaskingFunction.java @@ -27,6 +27,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; import org.apache.cassandra.cql3.functions.Arguments; @@ -179,7 +180,8 @@ public static Collection factories() .collect(Collectors.toSet()); } - private static FunctionFactory factory(Kind kind) + @VisibleForTesting + public static FunctionFactory factory(Kind kind) { return new MaskingFunction.Factory(kind.name(), FunctionParameter.string(), @@ -189,7 +191,7 @@ private static FunctionFactory factory(Kind kind) { @Override @SuppressWarnings("unchecked") - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, List> argTypes, AbstractType receiverType) { AbstractType inputType = (AbstractType) argTypes.get(0); return new PartialMaskingFunction(name, kind, inputType, argTypes.size() == 4); diff --git a/src/java/org/apache/cassandra/cql3/functions/masking/ReplaceMaskingFunction.java b/src/java/org/apache/cassandra/cql3/functions/masking/ReplaceMaskingFunction.java index 373743daf4c5..4f667a7f9a5a 100644 --- a/src/java/org/apache/cassandra/cql3/functions/masking/ReplaceMaskingFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/masking/ReplaceMaskingFunction.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.functions.Arguments; import org.apache.cassandra.cql3.functions.FunctionArguments; import org.apache.cassandra.cql3.functions.FunctionFactory; @@ -67,7 +68,7 @@ public static FunctionFactory factory() FunctionParameter.sameAs(0, true, FunctionParameter.anyType(true))) { @Override - protected NativeFunction doGetOrCreateFunction(List> argTypes, AbstractType receiverType) + protected NativeFunction doGetOrCreateFunction(List args, List> argTypes, AbstractType receiverType) { AbstractType replacedType = argTypes.get(0); AbstractType replacementType = argTypes.get(1); diff --git a/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java index ddd9601c834d..6d08d11193bc 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java @@ -19,17 +19,17 @@ import java.util.*; -import javax.annotation.Nullable; - -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.statements.Bound; import org.apache.cassandra.db.*; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.btree.BTreeSet; @@ -46,73 +46,24 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper */ private final ClusteringComparator comparator; - /** - * true if filtering is allowed for this restriction, false otherwise - */ - private final boolean allowFiltering; - - public ClusteringColumnRestrictions(TableMetadata table, boolean allowFiltering) - { - this(table.comparator, new RestrictionSet(), allowFiltering); - } - private ClusteringColumnRestrictions(ClusteringComparator comparator, - RestrictionSet restrictionSet, - boolean allowFiltering) + RestrictionSet restrictionSet) { super(restrictionSet); this.comparator = comparator; - this.allowFiltering = allowFiltering; - } - - public ClusteringColumnRestrictions mergeWith(Restriction restriction, @Nullable IndexRegistry indexRegistry) throws InvalidRequestException - { - SingleRestriction newRestriction = (SingleRestriction) restriction; - RestrictionSet newRestrictionSet = restrictions.addRestriction(newRestriction); - - if (!isEmpty() && !allowFiltering && (indexRegistry == null || !newRestriction.hasSupportingIndex(indexRegistry))) - { - SingleRestriction lastRestriction = restrictions.lastRestriction(); - assert lastRestriction != null; - - ColumnMetadata lastRestrictionStart = lastRestriction.getFirstColumn(); - ColumnMetadata newRestrictionStart = restriction.getFirstColumn(); - - checkFalse(lastRestriction.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position(), - "Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", - newRestrictionStart.name, - lastRestrictionStart.name); - - if (newRestrictionStart.position() < lastRestrictionStart.position() && newRestriction.isSlice()) - throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", - restrictions.nextColumn(newRestrictionStart).name, - newRestrictionStart.name); - } - - return new ClusteringColumnRestrictions(this.comparator, newRestrictionSet, allowFiltering); - } - - private boolean hasMultiColumnSlice() - { - for (SingleRestriction restriction : restrictions) - { - if (restriction.isMultiColumn() && restriction.isSlice()) - return true; - } - return false; } public NavigableSet> valuesAsClustering(QueryOptions options, ClientState state) throws InvalidRequestException { - MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN()); - for (SingleRestriction r : restrictions) + MultiClusteringBuilder builder = MultiClusteringBuilder.create(comparator); + for (SingleRestriction restriction : restrictions()) { - r.appendTo(builder, options); + restriction.appendTo(builder, options); if (hasIN() && Guardrails.inSelectCartesianProduct.enabled(state)) Guardrails.inSelectCartesianProduct.guard(builder.buildSize(), "clustering key", false, state); - if (builder.hasMissingElements()) + if (builder.buildIsEmpty()) break; } return builder.build(); @@ -120,65 +71,28 @@ public NavigableSet> valuesAsClustering(QueryOptions options, Clie public NavigableSet> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException { - MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN() || hasMultiColumnSlice()); + MultiClusteringBuilder builder = MultiClusteringBuilder.create(comparator); int keyPosition = 0; - for (SingleRestriction r : restrictions) + for (SingleRestriction restriction : restrictions()) { - if (handleInFilter(r, keyPosition)) + if (handleInFilter(restriction, keyPosition)) break; - if (r.isSlice()) - { - r.appendBoundTo(builder, bound, options); - return builder.buildBoundForSlice(bound.isStart(), - r.isInclusive(bound), - r.isInclusive(bound.reverse()), - r.getColumnDefs()); - } - - r.appendBoundTo(builder, bound, options); + restriction.appendBoundTo(builder, bound, options); - if (builder.hasMissingElements()) + if (builder.buildIsEmpty()) return BTreeSet.empty(comparator); - keyPosition = r.getLastColumn().position() + 1; - } - - // Everything was an equal (or there was nothing) - return builder.buildBound(bound.isStart(), true); - } + // We allow slice restriction only on the last clustering column restricted by the query. + // Any further column restrictions must be handled by indexes or filtering. + if (restriction.isSlice()) + break; - /** - * Checks if any of the underlying restriction is a CONTAINS or CONTAINS KEY. - * - * @return true if any of the underlying restriction is a CONTAINS or CONTAINS KEY, - * false otherwise - */ - public boolean hasContains() - { - for (SingleRestriction restriction : restrictions) - { - if (restriction.isContains()) - return true; + keyPosition = restriction.getLastColumn().position() + 1; } - return false; - } - /** - * Checks if any of the underlying restriction is a slice restrictions. - * - * @return true if any of the underlying restriction is a slice restrictions, - * false otherwise - */ - public boolean hasSlice() - { - for (SingleRestriction restriction : restrictions) - { - if (restriction.isSlice()) - return true; - } - return false; + return builder.buildBound(bound.isStart()); } /** @@ -191,7 +105,7 @@ public boolean needFiltering() { int position = 0; - for (SingleRestriction restriction : restrictions) + for (SingleRestriction restriction : restrictions()) { if (handleInFilter(restriction, position)) return true; @@ -203,23 +117,25 @@ public boolean needFiltering() } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) throws InvalidRequestException + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) throws InvalidRequestException { int position = 0; - for (SingleRestriction restriction : restrictions) + for (SingleRestriction restriction : restrictions()) { // We ignore all the clustering columns that can be handled by slices. - if (handleInFilter(restriction, position) || restriction.hasSupportingIndex(indexRegistry)) + if (handleInFilter(restriction, position) || restriction.hasSupportingIndex(indexRegistry, indexHints)) { - restriction.addToRowFilter(filter, indexRegistry, options); - continue; + restriction.addToRowFilter(filter, indexRegistry, options, annOptions, indexHints); } - - if (!restriction.isSlice()) + else if (!restriction.isSlice()) + { position = restriction.getLastColumn().position() + 1; + } } } @@ -227,4 +143,75 @@ private boolean handleInFilter(SingleRestriction restriction, int index) { return restriction.isContains() || restriction.isLIKE() || index != restriction.getFirstColumn().position(); } + + public static ClusteringColumnRestrictions.Builder builder(TableMetadata table, boolean allowFiltering) + { + return new Builder(table, allowFiltering, null, IndexHints.NONE); + } + + public static ClusteringColumnRestrictions.Builder builder(TableMetadata table, + boolean allowFiltering, + IndexRegistry indexRegistry, + IndexHints indexHints) + { + return new Builder(table, allowFiltering, indexRegistry, indexHints); + } + + public static class Builder + { + private final TableMetadata table; + private final boolean allowFiltering; + private final IndexRegistry indexRegistry; + private final IndexHints indexHints; + + private final RestrictionSet.Builder restrictions = RestrictionSet.builder(); + + private Builder(TableMetadata table, boolean allowFiltering, IndexRegistry indexRegistry, IndexHints indexHints) + { + this.table = table; + this.allowFiltering = allowFiltering; + this.indexRegistry = indexRegistry; + this.indexHints = indexHints; + } + + public ClusteringColumnRestrictions.Builder addRestriction(Restriction restriction) + { + return addRestriction(restriction, false); + } + + public ClusteringColumnRestrictions.Builder addRestriction(Restriction restriction, boolean isDisjunction) + { + SingleRestriction newRestriction = (SingleRestriction) restriction; + boolean isEmpty = restrictions.isEmpty(); + + if (!isEmpty && !allowFiltering && (indexRegistry == null || !newRestriction.hasSupportingIndex(indexRegistry, indexHints))) + { + SingleRestriction lastRestriction = restrictions.lastRestriction(); + ColumnMetadata lastRestrictionStart = lastRestriction.getFirstColumn(); + ColumnMetadata newRestrictionStart = newRestriction.getFirstColumn(); + restrictions.addRestriction(newRestriction, isDisjunction); + + checkFalse(lastRestriction.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position(), + "Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", + newRestrictionStart.name, + lastRestrictionStart.name); + + if (newRestrictionStart.position() < lastRestrictionStart.position() && newRestriction.isSlice()) + throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", + restrictions.nextColumn(newRestrictionStart).name, + newRestrictionStart.name); + } + else + { + restrictions.addRestriction(newRestriction, isDisjunction); + } + + return this; + } + + public ClusteringColumnRestrictions build() + { + return new ClusteringColumnRestrictions(table.comparator, restrictions.build()); + } + } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java b/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java index 569418b3f4b8..b36565586501 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java @@ -25,7 +25,7 @@ import org.apache.cassandra.index.Index; import org.apache.cassandra.schema.TableMetadata; -public class CustomIndexExpression +public class CustomIndexExpression implements ExternalRestriction { private final ColumnIdentifier valueColId = new ColumnIdentifier("custom index expression", false); @@ -47,7 +47,7 @@ public void prepareValue(TableMetadata table, AbstractType expressionType, Va value.collectMarkerSpecification(boundNames); } - public void addToRowFilter(RowFilter filter, TableMetadata table, QueryOptions options) + public void addToRowFilter(RowFilter.Builder filter, TableMetadata table, QueryOptions options) { filter.addCustomIndexExpression(table, table.indexes diff --git a/src/java/org/apache/cassandra/cql3/restrictions/ExternalRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/ExternalRestriction.java new file mode 100644 index 000000000000..c0ace7d1ef15 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/restrictions/ExternalRestriction.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.cql3.restrictions; + +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.schema.TableMetadata; + +public interface ExternalRestriction +{ + public void addToRowFilter(RowFilter.Builder filter, TableMetadata table, QueryOptions options); + + /** + * Returns whether this restriction would need filtering if the specified index group were used. + * + * @param indexGroup an index group + * @return {@code true} if this would need filtering if {@code indexGroup} were used, {@code false} otherwise + */ + public boolean needsFiltering(Index.Group indexGroup); +} diff --git a/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java index fe01e4188744..3a81558f1b39 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java @@ -19,68 +19,117 @@ package org.apache.cassandra.cql3.restrictions; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -public class IndexRestrictions +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +public final class IndexRestrictions { + /** + * The empty {@code IndexRestrictions}. + */ + private static final IndexRestrictions EMPTY_RESTRICTIONS = new IndexRestrictions(Collections.EMPTY_LIST, Collections.EMPTY_LIST); + public static final String INDEX_NOT_FOUND = "Invalid index expression, index %s not found for %s"; public static final String INVALID_INDEX = "Target index %s cannot be used to query %s"; public static final String CUSTOM_EXPRESSION_NOT_SUPPORTED = "Index %s does not support custom expressions"; public static final String NON_CUSTOM_INDEX_IN_EXPRESSION = "Only CUSTOM indexes may be used in custom index expressions, %s is not valid"; public static final String MULTIPLE_EXPRESSIONS = "Multiple custom index expressions in a single query are not supported"; - private final List regularRestrictions = new ArrayList<>(); - private final List customExpressions = new ArrayList<>(); + private final List regularRestrictions; + private final List externalRestrictions; + + private IndexRestrictions(List regularRestrictions, List externalExpressions) + { + this.regularRestrictions = regularRestrictions; + this.externalRestrictions = externalExpressions; + } - public void add(Restrictions restrictions) + /** + * Returns an empty {@code IndexRestrictions}. + * @return an empty {@code IndexRestrictions} + */ + public static IndexRestrictions of() { - regularRestrictions.add(restrictions); + return EMPTY_RESTRICTIONS; } - public void add(CustomIndexExpression expression) + /** + * Creates a new {@code IndexRestrictions.Builder} instance. + * @return a new {@code IndexRestrictions.Builder} instance. + */ + public static Builder builder() { - customExpressions.add(expression); + return new IndexRestrictions.Builder(); } public boolean isEmpty() { - return regularRestrictions.isEmpty() && customExpressions.isEmpty(); + return regularRestrictions.isEmpty() && externalRestrictions.isEmpty(); } + /** + * Returns the regular restrictions. + * @return the regular restrictions + */ public List getRestrictions() { return regularRestrictions; } - public List getCustomIndexExpressions() + /** + * Returns the external restrictions. + * @return the external restrictions + */ + public List getExternalExpressions() { - return customExpressions; + return externalRestrictions; + } + + /** + * Returns the number of restrictions in external expression and regular restrictions. + * @return Returns the number of restrictions in external expression and regular restrictions. + */ + private int numOfSupportedRestrictions() + { + int numberOfRestrictions = getExternalExpressions().size(); + for (Restrictions restrictions : getRestrictions()) + numberOfRestrictions += restrictions.size(); + + return numberOfRestrictions; } /** * Returns whether these restrictions would need filtering if the specified index registry were used. * * @param indexRegistry an index registry + * @param indexHints the user-provided index hints, which might exclude some indexes or explicitly expect some + * indexes requested by the user + * @param hasClusteringColumnRestrictions {@code true} if there are restricted clustering columns + * @param hasMultipleContains {@code true} if there are multiple "contains" restrictions * @return {@code true} if this would need filtering if {@code indexRegistry} were used, {@code false} otherwise */ - public boolean needsFiltering(IndexRegistry indexRegistry) + public boolean needFiltering(IndexRegistry indexRegistry, + IndexHints indexHints, + boolean hasClusteringColumnRestrictions, + boolean hasMultipleContains) { - if (isEmpty()) - return false; + // We need filtering if any clustering columns have restrictions that are not supported + // by their indexes. + if (numOfSupportedRestrictions() == 0) + return hasClusteringColumnRestrictions; for (Index.Group group : indexRegistry.listIndexGroups()) - { - if (!needsFiltering(group)) + if (!needFiltering(group, indexHints, hasMultipleContains)) return false; - } return true; } @@ -89,21 +138,35 @@ public boolean needsFiltering(IndexRegistry indexRegistry) * Returns whether these restrictions would need filtering if the specified index group were used. * * @param indexGroup an index group + * @param indexHints the user-provided index hints, which might exclude some indexes + * @param hasMultipleContains {@code true} if there are multiple "contains" restrictions * @return {@code true} if this would need filtering if {@code indexGroup} were used, {@code false} otherwise */ - private boolean needsFiltering(Index.Group indexGroup) + private boolean needFiltering(Index.Group indexGroup, IndexHints indexHints, boolean hasMultipleContains) { + if (hasMultipleContains && !indexGroup.supportsMultipleContains()) + return true; + for (Restrictions restrictions : regularRestrictions) - { - if (restrictions.needsFiltering(indexGroup)) + if (restrictions.needsFiltering(indexGroup, indexHints)) return true; - } - for (CustomIndexExpression restriction : customExpressions) - { + for (ExternalRestriction restriction : externalRestrictions) if (restriction.needsFiltering(indexGroup)) return true; - } + + return false; + } + + public boolean indexBeingUsed(Index.Group indexGroup, IndexHints indexHints) + { + for (Restrictions restrictions : regularRestrictions) + if (!restrictions.needsFiltering(indexGroup, indexHints)) + return true; + + for (ExternalRestriction restriction : externalRestrictions) + if (!restriction.needsFiltering(indexGroup)) + return true; return false; } @@ -120,17 +183,75 @@ static InvalidRequestException indexNotFound(QualifiedName indexName, TableMetad static InvalidRequestException nonCustomIndexInExpression(QualifiedName indexName) { - return new InvalidRequestException(String.format(NON_CUSTOM_INDEX_IN_EXPRESSION, indexName.getName())); + return invalidRequest(NON_CUSTOM_INDEX_IN_EXPRESSION, indexName.getName()); } static InvalidRequestException customExpressionNotSupported(QualifiedName indexName) { - return new InvalidRequestException(String.format(CUSTOM_EXPRESSION_NOT_SUPPORTED, indexName.getName())); + return invalidRequest(CUSTOM_EXPRESSION_NOT_SUPPORTED, indexName.getName()); } - - @Override - public String toString() + + /** + * Builder for IndexRestrictions. + */ + public static final class Builder { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + /** + * Builder for the regular restrictions. + */ + private List regularRestrictions = new ArrayList<>(); + + /** + * Builder for the custom expressions. + */ + private List externalRestrictions = new ArrayList<>(); + + private Builder() {} + + /** + * Adds the specified restrictions. + * + * @param restrictions the restrictions to add + * @return this {@code Builder} + */ + public Builder add(Restrictions restrictions) + { + regularRestrictions.add(restrictions); + return this; + } + + /** + * Adds the restrictions and custom expressions from the specified {@code IndexRestrictions}. + * + * @param restrictions the restrictions and custom expressions to add + * @return this {@code Builder} + */ + public Builder add(IndexRestrictions restrictions) + { + regularRestrictions.addAll(restrictions.regularRestrictions); + externalRestrictions.addAll(restrictions.externalRestrictions); + return this; + } + + /** + * Adds the specified external expression. + * + * @param restriction the external expression to add + * @return this {@code Builder} + */ + public Builder add(ExternalRestriction restriction) + { + externalRestrictions.add(restriction); + return this; + } + + /** + * Builds a new {@code IndexRestrictions} instance + * @return a new {@code IndexRestrictions} instance + */ + public IndexRestrictions build() + { + return new IndexRestrictions(Collections.unmodifiableList(regularRestrictions), Collections.unmodifiableList(externalRestrictions)); + } } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java index 5ad6dabd4c3a..58805597ce28 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java @@ -19,6 +19,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; @@ -28,24 +29,27 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import org.apache.cassandra.cql3.AbstractMarker; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.MarkerOrTerms; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.Term; -import org.apache.cassandra.cql3.Terms; -import org.apache.cassandra.cql3.Tuples; import org.apache.cassandra.cql3.Term.Terminal; +import org.apache.cassandra.cql3.Tuples; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.Bound; -import org.apache.cassandra.db.MultiCBuilder; +import org.apache.cassandra.db.MultiClusteringBuilder; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.serializers.ListSerializer; +import org.apache.cassandra.serializers.CollectionSerializer; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; -import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; @@ -123,51 +127,35 @@ protected final String getColumnsInCommons(Restriction otherRestriction) } @Override - public final boolean hasSupportingIndex(IndexRegistry indexRegistry) + public final boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { - for (Index index : indexRegistry.listIndexes()) - if (isSupportedBy(index)) - return true; - + for (Index index : indexRegistry.listNotExcludedIndexes(indexHints)) + if (isSupportingIndex(index)) + return true; return false; } @Override - public final Index findSupportingIndex(IndexRegistry indexRegistry) - { - for (Index index : indexRegistry.listIndexes()) - if (isSupportedBy(index)) - return index; - return null; - } - - @Override - public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan) - { - for (Index index : indexQueryPlan.getIndexes()) - if (isSupportedBy(index)) - return index; - return null; - } - - @Override - public boolean needsFiltering(Index.Group indexGroup) + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { for (ColumnMetadata column : columnDefs) - { - if (!isSupportedBy(indexGroup, column)) + if (!isSupportedBy(indexGroup, indexHints, column)) return true; - } + return false; } - private boolean isSupportedBy(Index.Group indexGroup, ColumnMetadata column) + private boolean isSupportedBy(Index.Group indexGroup, IndexHints indexHints, ColumnMetadata column) { for (Index index : indexGroup.getIndexes()) { + if (indexHints.excludes(index)) + continue; + if (isSupportedBy(index, column)) return true; } + return false; } @@ -178,13 +166,12 @@ private boolean isSupportedBy(Index.Group indexGroup, ColumnMetadata column) * @return true this type of restriction is supported by the specified index, * false otherwise. */ - private boolean isSupportedBy(Index index) + private boolean isSupportingIndex(Index index) { for (ColumnMetadata column : columnDefs) - { if (isSupportedBy(index, column)) return true; - } + return false; } @@ -192,12 +179,12 @@ private boolean isSupportedBy(Index index) public static class EQRestriction extends MultiColumnRestriction { - protected final Term value; + protected final Term term; - public EQRestriction(List columnDefs, Term value) + public EQRestriction(List columnDefs, Term term) { super(columnDefs); - this.value = value; + this.term = term; } @Override @@ -209,22 +196,34 @@ public boolean isEQ() @Override public void addFunctionsTo(List functions) { - value.addFunctionsTo(functions); + term.addFunctionsTo(functions); } @Override public String toString() { - return String.format("EQ(%s)", value); + return String.format("EQ(%s)", term); } @Override public SingleRestriction doMergeWith(SingleRestriction otherRestriction) { + if (otherRestriction instanceof SliceRestriction) + { + SingleRestriction thisAsSlice = this.toSliceRestriction(); + return thisAsSlice.mergeWith(otherRestriction); + } throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal", getColumnsInCommons(otherRestriction)); } + private SingleRestriction toSliceRestriction() + { + SliceRestriction start = SliceRestriction.fromBound(columnDefs, Bound.START, true, this.term); + SliceRestriction end = SliceRestriction.fromBound(columnDefs, Bound.END, true, this.term); + return start.mergeWith(end); + } + @Override protected boolean isSupportedBy(Index index, ColumnMetadata column) { @@ -232,22 +231,27 @@ protected boolean isSupportedBy(Index index, ColumnMetadata column) } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { - Tuples.Value t = ((Tuples.Value) value.bind(options)); + Tuples.Value t = ((Tuples.Value) term.bind(options)); List values = t.getElements(); for (int i = 0, m = values.size(); i < m; i++) { - builder.addElementToAll(values.get(i)); - checkFalse(builder.containsNull(), "Invalid null value for column %s", columnDefs.get(i).name); + ColumnMetadata column = columnDefs.get(i); + builder.extend(MultiClusteringBuilder.ClusteringElements.point(values.get(i)), Collections.singletonList(column)); + checkFalse(builder.containsNull(), "Invalid null value for column %s", column.name); } return builder; } @Override - public final void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) + public final void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - Tuples.Value t = ((Tuples.Value) value.bind(options)); + Tuples.Value t = ((Tuples.Value) term.bind(options)); List values = t.getElements(); for (int i = 0, m = columnDefs.size(); i < m; i++) @@ -258,24 +262,33 @@ public final void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, } } - public abstract static class INRestriction extends MultiColumnRestriction + public static class INRestriction extends MultiColumnRestriction { - public INRestriction(List columnDefs) + private final MarkerOrTerms terms; + private final Collection columnIdentifiers; + + public INRestriction(List columnDefs, MarkerOrTerms terms) { super(columnDefs); + this.terms = terms; + this.columnIdentifiers = ColumnMetadata.toIdentifiers(columnDefs); } /** * {@inheritDoc} */ @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { - List> splitInValues = splitValues(options); - builder.addAllElementsToAll(splitInValues); + List> values = terms.bindAndGetTuples(options, columnIdentifiers); + List elements = new ArrayList<>(values.size()); + for (List value: values) + elements.add(MultiClusteringBuilder.ClusteringElements.point(value)); + + builder.extend(elements, columnDefs); if (builder.containsNull()) - throw invalidRequest("Invalid null value in condition for columns: %s", ColumnMetadata.toIdentifiers(columnDefs)); + throw invalidRequest("Invalid null value in condition for columns: %s", columnIdentifiers); return builder; } @@ -299,20 +312,22 @@ protected boolean isSupportedBy(Index index, ColumnMetadata column) } @Override - public final void addToRowFilter(RowFilter filter, + public final void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { // If the relation is of the type (c) IN ((x),(y),(z)) then it is equivalent to // c IN (x, y, z) and we can perform filtering if (getColumnDefs().size() == 1) { - List> splitValues = splitValues(options); + List> splitValues = terms.bindAndGetTuples(options, columnIdentifiers); List values = new ArrayList<>(splitValues.size()); for (List splitValue : splitValues) values.add(splitValue.get(0)); - ByteBuffer buffer = ListSerializer.pack(values, values.size()); + ByteBuffer buffer = CollectionSerializer.pack(values, ByteBufferAccessor.instance, values.size()); filter.add(getFirstColumn(), Operator.IN, buffer); } else @@ -321,96 +336,43 @@ public final void addToRowFilter(RowFilter filter, } } - protected abstract List> splitValues(QueryOptions options); - } - - /** - * An IN restriction that has a set of terms for in values. - * For example: "SELECT ... WHERE (a, b, c) IN ((1, 2, 3), (4, 5, 6))" or "WHERE (a, b, c) IN (?, ?)" - */ - public static class InRestrictionWithValues extends INRestriction - { - protected final List values; - - public InRestrictionWithValues(List columnDefs, List values) - { - super(columnDefs); - this.values = values; - } - @Override public void addFunctionsTo(List functions) { - Terms.addFunctions(values, functions); + terms.addFunctionsTo(functions); } @Override public String toString() { - return String.format("IN(%s)", values); - } - - @Override - protected List> splitValues(QueryOptions options) - { - List> buffers = new ArrayList<>(values.size()); - for (Term value : values) - { - Term.MultiItemTerminal term = (Term.MultiItemTerminal) value.bind(options); - buffers.add(term.getElements()); - } - return buffers; + return String.format("IN(%s)", terms); } } - /** - * An IN restriction that uses a single marker for a set of IN values that are tuples. - * For example: "SELECT ... WHERE (a, b, c) IN ?" - */ - public static class InRestrictionWithMarker extends INRestriction + + public static class SliceRestriction extends MultiColumnRestriction { - protected final AbstractMarker marker; + private final TermSlice slice; + private final List skippedValues; // values passed in NOT IN - public InRestrictionWithMarker(List columnDefs, AbstractMarker marker) + SliceRestriction(List columnDefs, TermSlice slice, List skippedValues) { super(columnDefs); - this.marker = marker; - } - - @Override - public void addFunctionsTo(List functions) - { - } - - @Override - public String toString() - { - return "IN ?"; - } - - @Override - protected List> splitValues(QueryOptions options) - { - Tuples.InMarker inMarker = (Tuples.InMarker) marker; - Tuples.InValue inValue = inMarker.bind(options); - checkNotNull(inValue, "Invalid null value for IN restriction"); - return inValue.getSplitValues(); + assert slice != null; + assert skippedValues != null; + this.slice = slice; + this.skippedValues = skippedValues; } - } - public static class SliceRestriction extends MultiColumnRestriction - { - private final TermSlice slice; - - public SliceRestriction(List columnDefs, Bound bound, boolean inclusive, Term term) + public static MultiColumnRestriction.SliceRestriction fromBound(List columnDefs, Bound bound, boolean inclusive, Term term) { - this(columnDefs, TermSlice.newInstance(bound, inclusive, term)); + TermSlice slice = TermSlice.newInstance(bound, inclusive, term); + return new MultiColumnRestriction.SliceRestriction(columnDefs, slice, Collections.emptyList()); } - SliceRestriction(List columnDefs, TermSlice slice) + public static MultiColumnRestriction.SliceRestriction fromSkippedValues(List columnDefs, MarkerOrTerms skippedValues) { - super(columnDefs); - this.slice = slice; + return new MultiColumnRestriction.SliceRestriction(columnDefs, TermSlice.UNBOUNDED, Collections.singletonList(skippedValues)); } @Override @@ -420,22 +382,55 @@ public boolean isSlice() } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { throw new UnsupportedOperationException(); } @Override - public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options) + public MultiClusteringBuilder appendBoundTo(MultiClusteringBuilder builder, Bound bound, QueryOptions options) + { + List toAdd = new ArrayList<>(); + addSliceBounds(bound, options, toAdd); + addSkippedValues(bound, options, toAdd); + return builder.extend(toAdd, columnDefs); + } + + /** + * Generates a list of clustering bounds based on this slice bounds and adds them to the toAdd list. + * Clustering bounds used for the table range scan might not be equal to this slice bounds. + * This method has to generate the TOP/BOTTOM bounds if this slice is unbounded on any side. + * It is also possible to generate multiple bounds, if clustering columns have mixed order. + * Does not guarantee order of results, but does not generate duplicates. + * + * @param bound the type of bounds to generate (start or end) + * @param options needed to get the actual values bound to markers + * @param toAdd receiver of the result + */ + private void addSliceBounds(Bound bound, QueryOptions options, List toAdd) { + // Stores the direction of sorting of the current processed column. + // Used to detect when the next processed column has different direction of sorting than the last one. + // If clustering columns are all sorted in the same direction (doesn't matter if ASC or DESC, but must be + // the same for all), we can just need to generate only one boolean reversed = getFirstColumn().isReversedType(); EnumMap> componentBounds = new EnumMap<>(Bound.class); componentBounds.put(Bound.START, componentBounds(Bound.START, options)); componentBounds.put(Bound.END, componentBounds(Bound.END, options)); - List> toAdd = new ArrayList<>(); - List values = new ArrayList<>(); + // We will pick a prefix of bounds from `componentBounds` into this array, either start or end bounds + // depending on the column clustering direction. + List values = Collections.emptyList(); + + // Tracks whether the last bound added to `values` is inclusive. + // We must start from true, because if there are no bounds at all (unbounded slice), + // we must not restrict the clusterings added by other restrictions. + boolean inclusive = true; + + // Number of components in the last element added to the toAdd collection. + // Used to avoid adding the same composite multiple times. + int sizeOfLastElement = -1; for (int i = 0, m = columnDefs.size(); i < m; i++) { @@ -446,50 +441,90 @@ public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOpti if (reversed != column.isReversedType()) { reversed = column.isReversedType(); - // As we are switching direction we need to add the current composite - toAdd.add(values); - - // The new bound side has no value for this component. just stop - if (!hasComponent(b, i, componentBounds)) - continue; - - // The other side has still some components. We need to end the slice that we have just open. - if (hasComponent(b.reverse(), i, componentBounds)) - toAdd.add(values); - // We need to rebuild where we are in this bound side - values = new ArrayList(); + // In the following comments, assume: + // c1 - previous column (== columnDefs.get(i - 1)) + // c2 - current column (== columnDefs.get(i)) + // x1 - the last bound stored in values (values == [..., x1]) + // x2 - the bound of c2 - List vals = componentBounds.get(b); - - int n = Math.min(i, vals.size()); - for (int j = 0; j < n; j++) + // Only try to add the current composite if we haven't done it already, to avoid duplicates. + if (values.size() > sizeOfLastElement) { - ByteBuffer v = checkNotNull(vals.get(j), - "Invalid null value in condition for column %s", - columnDefs.get(j).name); - values.add(v); + sizeOfLastElement = values.size(); + + // note that b.reverse() matches the bound of the last component added to the `values` + if (hasComponent(b.reverse(), i, componentBounds)) + { + // (c1, c2) <= (x1, x2) ----> (c1 < x1) || (c1 = x1) && (c2 <= x2) + // (c1, c2) >= (x1, x2) ----> (c1 > x1) || (c1 = x1) && (c2 >= x2) + // (c1, c2) < (x1, x2) ----> (c1 < x1) || (c1 = x1) && (c2 < x2) + // (c1, c2) > (x1, x2) ----> (c1 > x1) || (c1 = x1) && (c2 > x2) + // ^^^^^^^^^ + toAdd.add(MultiClusteringBuilder.ClusteringElements.bound(values, bound, false)); + + // Now add the other side of the union: + // (c1, c2) <= (x1, x2) ----> (c1 < x1) || (c1 = x1) && (c2 < x2) + // (c1, c2) >= (x1, x2) ----> (c1 > x1) || (c1 = x1) && (c2 > x2) + // ^^^^^^^^^ + // The other side has still some components. We need to end the slice that we have just open. + // Note that (c2 > x2) will be added by the call to an opposite bound. + toAdd.add(MultiClusteringBuilder.ClusteringElements.point(values)); + } + else + { + // The new bound side has no value for this component. Just add current composite as-is. + // No value means min or max, depending on the direction of the comparison. + // (c1, c2) <= (x1, no value) ----> (c1 <= x1) + // (c1, c2) >= (x1, no value) ----> (c1 >= x1) + // (c1, c2) < (x1, no value) ----> (c1 < x1) + // (c1, c2) > (x1, no value) ----> (c1 > x1) + toAdd.add(MultiClusteringBuilder.ClusteringElements.bound(values, bound, inclusive)); + } } } - if (!hasComponent(b, i, componentBounds)) - continue; - - ByteBuffer v = checkNotNull(componentBounds.get(b).get(i), "Invalid null value in condition for column %s", columnDefs.get(i).name); - values.add(v); + if (hasComponent(b, i, componentBounds)) + { + values = componentBounds.get(b).subList(0, i + 1); + inclusive = isInclusive(b); + } } - toAdd.add(values); - if (bound.isEnd()) - Collections.reverse(toAdd); + if (values.size() > sizeOfLastElement) + toAdd.add(MultiClusteringBuilder.ClusteringElements.bound(values, bound, inclusive)); + } + - return builder.addAllElementsToAll(toAdd); + + /** + * Generates a list of clustering bounds that exclude the skipped values. + * I.e. for skipped elements (s1, s2, ..., sN), generates the slices + * (BOTTOM, s1), (s1, s2), ..., (s(N-1), sN), (sN, TOP) and returns the list of + * their start or end bounds depending on the selected `bound` param. + * + * @param bound which bound of the slices we want to generate + * @param options needed to get the actual values bound to markers + * @param toAdd receiver of the result + */ + private void addSkippedValues(Bound bound, QueryOptions options, List toAdd) + { + for (MarkerOrTerms markerOrTerms : skippedValues) + { + for (List tuple: markerOrTerms.bindAndGetTuples(options, ColumnMetadata.toIdentifiers(columnDefs))) + { + MultiClusteringBuilder.ClusteringElements element = MultiClusteringBuilder.ClusteringElements.bound(tuple, bound, false); + toAdd.add(element); + } + } } @Override protected boolean isSupportedBy(Index index, ColumnMetadata column) { - return slice.isSupportedBy(column, index); + boolean supportsSlice = slice.isSupportedBy(column, index); + boolean supportsNeq = index.supportsExpression(column, Operator.NEQ); + return supportsSlice || !skippedValues.isEmpty() && supportsNeq; } @Override @@ -536,13 +571,20 @@ public SingleRestriction doMergeWith(SingleRestriction otherRestriction) SliceRestriction otherSlice = (SliceRestriction) otherRestriction; List newColumnDefs = columnDefs.size() >= otherSlice.columnDefs.size() ? columnDefs : otherSlice.columnDefs; - return new SliceRestriction(newColumnDefs, slice.merge(otherSlice.slice)); + int sizeHint = skippedValues.size() + otherSlice.skippedValues.size(); + List newSkippedValues = new ArrayList<>(sizeHint); + newSkippedValues.addAll(skippedValues); + newSkippedValues.addAll(otherSlice.skippedValues); + TermSlice newSlice = slice.merge(otherSlice.slice); + return new SliceRestriction(newColumnDefs, newSlice, newSkippedValues); } @Override - public final void addToRowFilter(RowFilter filter, + public final void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { throw invalidRequest("Multi-column slice restrictions cannot be used for filtering."); } @@ -550,7 +592,7 @@ public final void addToRowFilter(RowFilter filter, @Override public String toString() { - return "SLICE" + slice; + return String.format("SLICE{%s, NOT IN %s}", slice, skippedValues); } /** @@ -565,14 +607,22 @@ private List componentBounds(Bound b, QueryOptions options) if (!slice.hasBound(b)) return Collections.emptyList(); - Terminal terminal = slice.bound(b).bind(options); + List bounds = bindTuple(slice.bound(b).bind(options), options); - if (terminal instanceof Tuples.Value) - { - return ((Tuples.Value) terminal).getElements(); - } + assert bounds.size() <= columnDefs.size(); + int hasNullAt = bounds.indexOf(null); + if (hasNullAt != -1) + throw new InvalidRequestException(String.format( + "Invalid null value in condition for column %s", columnDefs.get(hasNullAt).name)); + + return bounds; + } - return Collections.singletonList(terminal.get(options.getProtocolVersion())); + private static List bindTuple(Terminal terminal, QueryOptions options) + { + return terminal instanceof Tuples.Value + ? ((Tuples.Value) terminal).getElements() + : Collections.singletonList(terminal.get(options.getProtocolVersion())); } private boolean hasComponent(Bound b, int index, EnumMap> componentBounds) @@ -620,18 +670,22 @@ protected boolean isSupportedBy(Index index, ColumnMetadata column) } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { throw new UnsupportedOperationException("Cannot use IS NOT NULL restriction for slicing"); } @Override - public final void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) + public final void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { throw new UnsupportedOperationException("Secondary indexes do not support IS NOT NULL restrictions"); } } - + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java index 822452979ebf..3447ca6460a0 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.statements.Bound; @@ -31,7 +32,7 @@ */ interface PartitionKeyRestrictions extends Restrictions { - public PartitionKeyRestrictions mergeWith(Restriction restriction); + public PartitionKeyRestrictions mergeWith(Restriction restriction, IndexRegistry indexRegistry); public List values(QueryOptions options, ClientState state); diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java index 914681d691cc..f7edd1f70b3a 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java @@ -20,20 +20,21 @@ import java.nio.ByteBuffer; import java.util.*; -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.statements.Bound; import org.apache.cassandra.db.ClusteringComparator; -import org.apache.cassandra.db.ClusteringPrefix; -import org.apache.cassandra.db.MultiCBuilder; +import org.apache.cassandra.db.MultiClusteringBuilder; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; /** * A set of single restrictions on the partition key. - *

This class can only contains SingleRestriction instances. Token restrictions will be handled by + *

This class can only contain SingleRestriction instances. Token restrictions will be handled by * TokenRestriction class or by the TokenFilter class if the query contains a mix of token * restrictions and single column restrictions on the partition key. */ @@ -44,74 +45,66 @@ final class PartitionKeySingleRestrictionSet extends RestrictionSetWrapper imple */ private final ClusteringComparator comparator; - public PartitionKeySingleRestrictionSet(ClusteringComparator comparator) + private PartitionKeySingleRestrictionSet(RestrictionSet restrictionSet, ClusteringComparator comparator) { - super(new RestrictionSet()); + super(restrictionSet); this.comparator = comparator; } - private PartitionKeySingleRestrictionSet(PartitionKeySingleRestrictionSet restrictionSet, - SingleRestriction restriction) - { - super(restrictionSet.restrictions.addRestriction(restriction)); - this.comparator = restrictionSet.comparator; - } - - private List toByteBuffers(SortedSet clusterings) - { - List l = new ArrayList<>(clusterings.size()); - for (ClusteringPrefix clustering : clusterings) - { - // Can not use QueryProcessor.validateKey here to validate each column as that validates that empty are not allowed - // but composite partition keys actually allow empty! - clustering.validate(); - l.add(clustering.serializeAsPartitionKey()); - } - return l; - } - @Override - public PartitionKeyRestrictions mergeWith(Restriction restriction) + public PartitionKeyRestrictions mergeWith(Restriction restriction, IndexRegistry indexRegistry) { if (restriction.isOnToken()) { if (isEmpty()) return (PartitionKeyRestrictions) restriction; - return new TokenFilter(this, (TokenRestriction) restriction); + return TokenFilter.create(this, (TokenRestriction) restriction); } - return new PartitionKeySingleRestrictionSet(this, (SingleRestriction) restriction); + Builder builder = PartitionKeySingleRestrictionSet.builder(comparator); + List restrictions = restrictions(); + for (int i = 0; i < restrictions.size(); i++) + { + SingleRestriction r = restrictions.get(i); + builder.addRestriction(r); + } + return builder.addRestriction(restriction) + .build(indexRegistry); } @Override public List values(QueryOptions options, ClientState state) { - MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN()); - for (SingleRestriction r : restrictions) + MultiClusteringBuilder builder = MultiClusteringBuilder.create(comparator); + List restrictions = restrictions(); + for (int i = 0; i < restrictions.size(); i++) { + SingleRestriction r = restrictions.get(i); r.appendTo(builder, options); if (hasIN() && Guardrails.inSelectCartesianProduct.enabled(state)) Guardrails.inSelectCartesianProduct.guard(builder.buildSize(), "partition key", false, state); - if (builder.hasMissingElements()) + if (builder.buildIsEmpty()) break; } - return toByteBuffers(builder.build()); + return builder.buildSerializedPartitionKeys(); } @Override public List bounds(Bound bound, QueryOptions options) { - MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN()); - for (SingleRestriction r : restrictions) + MultiClusteringBuilder builder = MultiClusteringBuilder.create(comparator); + List restrictions = restrictions(); + for (int i = 0; i < restrictions.size(); i++) { + SingleRestriction r = restrictions.get(i); r.appendBoundTo(builder, bound, options); - if (builder.hasMissingElements()) + if (builder.buildIsEmpty()) return Collections.emptyList(); } - return toByteBuffers(builder.buildBound(bound.isStart(), true)); + return builder.buildSerializedPartitionKeys(); } @Override @@ -131,13 +124,17 @@ public boolean isInclusive(Bound b) } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - for (SingleRestriction restriction : restrictions) + List restrictions = restrictions(); + for (int i = 0; i < restrictions.size(); i++) { - restriction.addToRowFilter(filter, indexRegistry, options); + SingleRestriction r = restrictions.get(i); + r.addToRowFilter(filter, indexRegistry, options, annOptions, indexHints); } } @@ -157,9 +154,68 @@ public boolean hasUnrestrictedPartitionKeyComponents(TableMetadata table) return size() < table.partitionKeyColumns().size(); } - @Override - public boolean hasSlice() + public static Builder builder(ClusteringComparator clusteringComparator) { - return restrictions.hasSlice(); + return new Builder(clusteringComparator); + } + + public static final class Builder + { + private final ClusteringComparator clusteringComparator; + + private final List restrictions = new ArrayList<>(); + + private Builder(ClusteringComparator clusteringComparator) + { + this.clusteringComparator = clusteringComparator; + } + + public Builder addRestriction(Restriction restriction) + { + restrictions.add(restriction); + return this; + } + + public PartitionKeyRestrictions build(IndexRegistry indexRegistry) + { + return build(indexRegistry, false); + } + + public PartitionKeyRestrictions build(IndexRegistry indexRegistry, boolean isDisjunction) + { + RestrictionSet.Builder restrictionSet = RestrictionSet.builder(); + + for (int i = 0; i < restrictions.size(); i++) + { + Restriction restriction = restrictions.get(i); + + // restrictions on tokens are handled in a special way + if (restriction.isOnToken()) + return buildWithTokens(restrictionSet, i, indexRegistry); + + restrictionSet.addRestriction((SingleRestriction) restriction, isDisjunction); + } + + return buildPartitionKeyRestrictions(restrictionSet); + } + + private PartitionKeyRestrictions buildWithTokens(RestrictionSet.Builder restrictionSet, int i, IndexRegistry indexRegistry) + { + PartitionKeyRestrictions merged = buildPartitionKeyRestrictions(restrictionSet); + + for (; i < restrictions.size(); i++) + { + Restriction restriction = restrictions.get(i); + + merged = merged.mergeWith(restriction, indexRegistry); + } + + return merged; + } + + private PartitionKeySingleRestrictionSet buildPartitionKeyRestrictions(RestrictionSet.Builder restrictionSet) + { + return new PartitionKeySingleRestrictionSet(restrictionSet.build(), clusteringComparator); + } } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java b/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java index 7a774bf0d58e..8b0b5c31a344 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java @@ -19,6 +19,8 @@ import java.util.List; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.index.Index; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.QueryOptions; @@ -31,7 +33,11 @@ */ public interface Restriction { - public default boolean isOnToken() + /** + * Check if the restriction is on a partition key + * @return true if the restriction is on a partition key, false + */ + default boolean isOnToken() { return false; } @@ -40,19 +46,19 @@ public default boolean isOnToken() * Returns the definition of the first column. * @return the definition of the first column. */ - public ColumnMetadata getFirstColumn(); + ColumnMetadata getFirstColumn(); /** * Returns the definition of the last column. * @return the definition of the last column. */ - public ColumnMetadata getLastColumn(); + ColumnMetadata getLastColumn(); /** * Returns the column definitions in position order. * @return the column definitions in position order. */ - public List getColumnDefs(); + List getColumnDefs(); /** * Adds all functions (native and user-defined) used by any component of the restriction @@ -65,32 +71,21 @@ public default boolean isOnToken() * Check if the restriction is on indexed columns. * * @param indexRegistry the index registry + * @param indexHints the user-provided index hints, which might exclude some indexes or explicitly expect some + * indexes requested by the user * @return true if the restriction is on indexed columns, false */ - boolean hasSupportingIndex(IndexRegistry indexRegistry); - - /** - * Find first supporting index for current restriction - * - * @param indexRegistry the index registry - * @return index if the restriction is on indexed columns, null - */ - Index findSupportingIndex(IndexRegistry indexRegistry); - - /** - * Find the first supporting index for the current restriction from an {@link Index.QueryPlan}. - * @param indexQueryPlan the index query plan - * @return index if the restriction is on indexed columns, null - */ - Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan); + boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints); /** * Returns whether this restriction would need filtering if the specified index group were used. * * @param indexGroup an index group + * @param indexHints the user-provided index hints, which might exclude some indexes or explicitly expect some + * indexes requested by the user * @return {@code true} if this would need filtering if {@code indexGroup} were used, {@code false} otherwise */ - boolean needsFiltering(Index.Group indexGroup); + boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints); /** * Adds to the specified row filter the expressions corresponding to this Restriction. @@ -98,8 +93,12 @@ public default boolean isOnToken() * @param filter the row filter to add expressions to * @param indexRegistry the index registry * @param options the query options + * @param annOptions the query ANN options + * @param indexHints the index hints */ - public void addToRowFilter(RowFilter filter, - IndexRegistry indexRegistry, - QueryOptions options); + void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints); } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java index 750b728295c8..a012c5107151 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java @@ -17,393 +17,494 @@ */ package org.apache.cassandra.cql3.restrictions; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.NavigableSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; -import com.google.common.collect.AbstractIterator; - -import org.apache.cassandra.index.Index; -import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction.ContainsRestriction; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.cassandra.schema.ColumnMetadata; /** * Sets of column restrictions. * *

This class is immutable.

*/ -final class RestrictionSet implements Restrictions, Iterable +public abstract class RestrictionSet implements Restrictions { /** * The comparator used to sort the Restrictions. */ - private static final Comparator COLUMN_DEFINITION_COMPARATOR = new Comparator() + private static final Comparator COLUMN_DEFINITION_COMPARATOR = Comparator.comparingInt(ColumnMetadata::position).thenComparing(column -> column.name.bytes); + + private static final class EmptyRestrictionSet extends RestrictionSet { + private static final EmptyRestrictionSet INSTANCE = new EmptyRestrictionSet(); + + private EmptyRestrictionSet() + { + } + @Override - public int compare(ColumnMetadata column, ColumnMetadata otherColumn) + public void addToRowFilter(RowFilter.Builder rowFilter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - int value = Integer.compare(column.position(), otherColumn.position()); - return value != 0 ? value : column.name.bytes.compareTo(otherColumn.name.bytes); + // nothing to do here, since there are no restrictions } - }; - private static final TreeMap EMPTY = new TreeMap<>(COLUMN_DEFINITION_COMPARATOR); + @Override + public List getColumnDefs() + { + return Collections.EMPTY_LIST; + } - /** - * The restrictions per column. - */ - private final TreeMap restrictions; + @Override + public void addFunctionsTo(List functions) + { + } - /** - * {@code true} if it contains multi-column restrictions, {@code false} otherwise. - */ - private final boolean hasMultiColumnRestrictions; + @Override + public boolean isEmpty() + { + return true; + } - private final boolean hasIn; - private final boolean hasContains; - private final boolean hasSlice; - private final boolean hasAnn; - private final boolean hasOnlyEqualityRestrictions; + @Override + public int size() + { + return 0; + } - public RestrictionSet() - { - this(EMPTY, false, - false, - false, - false, - false, - true); - } + @Override + public boolean hasRestrictionFor(ColumnMetadata.Kind kind) + { + return false; + } - private RestrictionSet(TreeMap restrictions, - boolean hasMultiColumnRestrictions, - boolean hasIn, - boolean hasContains, - boolean hasSlice, - boolean hasAnn, - boolean hasOnlyEqualityRestrictions) - { - this.restrictions = restrictions; - this.hasMultiColumnRestrictions = hasMultiColumnRestrictions; - this.hasIn = hasIn; - this.hasContains = hasContains; - this.hasSlice = hasSlice; - this.hasAnn = hasAnn; - this.hasOnlyEqualityRestrictions = hasOnlyEqualityRestrictions; - } + @Override + public Set getRestrictions(ColumnMetadata columnDef) + { + return Collections.emptySet(); + } - @Override - public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) throws InvalidRequestException - { - for (Restriction restriction : restrictions.values()) - restriction.addToRowFilter(filter, indexRegistry, options); - } + @Override + public boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) + { + return false; + } - @Override - public boolean needsFiltering(Index.Group indexGroup) - { - for (SingleRestriction restriction : restrictions.values()) + @Override + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { - if (restriction.needsFiltering(indexGroup)) - return true; + return false; } - return false; - } - @Override - public List getColumnDefs() - { - return new ArrayList<>(restrictions.keySet()); - } + @Override + public ColumnMetadata getFirstColumn() + { + return null; + } - /** - * @return a direct reference to the key set from {@link #restrictions} with no defenseive copying - */ - @Override - public Collection getColumnDefinitions() - { - return restrictions.keySet(); - } + @Override + public ColumnMetadata getLastColumn() + { + return null; + } - @Override - public void addFunctionsTo(List functions) - { - for (Restriction restriction : this) - restriction.addFunctionsTo(functions); - } + @Override + public SingleRestriction lastRestriction() + { + return null; + } - @Override - public boolean isEmpty() - { - return restrictions.isEmpty(); - } + @Override + public boolean hasMultipleContains() + { + return false; + } - @Override - public int size() - { - return restrictions.size(); - } + @Override + public List restrictions() + { + return Collections.EMPTY_LIST; + } - /** - * Checks if one of the restrictions applies to a column of the specific kind. - * @param kind the column kind - * @return {@code true} if one of the restrictions applies to a column of the specific kind, {@code false} otherwise. - */ - public boolean hasRestrictionFor(ColumnMetadata.Kind kind) - { - for (ColumnMetadata column : restrictions.keySet()) + @Override + public boolean hasMultiColumnSlice() { - if (column.kind == kind) - return true; + return false; } - return false; } - /** - * Adds the specified restriction to this set of restrictions. - * - * @param restriction the restriction to add - * @return the new set of restrictions - */ - public RestrictionSet addRestriction(SingleRestriction restriction) + private static final class DefaultRestrictionSet extends RestrictionSet { - // RestrictionSet is immutable so we need to clone the restrictions map. - TreeMap newRestrictions = new TreeMap<>(this.restrictions); - - boolean newHasIn = hasIn || restriction.isIN(); - boolean newHasContains = hasContains || restriction.isContains(); - boolean newHasSlice = hasSlice || restriction.isSlice(); - boolean newHasAnn = hasAnn || restriction.isANN(); - boolean newHasOnlyEqualityRestrictions = hasOnlyEqualityRestrictions && (restriction.isEQ() || restriction.isIN()); - - return new RestrictionSet(mergeRestrictions(newRestrictions, restriction), - hasMultiColumnRestrictions || restriction.isMultiColumn(), - newHasIn, - newHasContains, - newHasSlice, - newHasAnn, - newHasOnlyEqualityRestrictions); - } - private TreeMap mergeRestrictions(TreeMap restrictions, - SingleRestriction restriction) - { - Collection columnDefs = restriction.getColumnDefs(); - Set existingRestrictions = getRestrictions(columnDefs); + /** + * The keys from the 'restrictions' parameter to the + */ + private final List restrictionsKeys; + /** + * The values as returned from {@link #restrictions()}. + */ + private final List restrictionsValues; + private final Multimap restrictionsMap; + private final int hasBitmap; + private final int restrictionForKindBitmap; + private static final int maskHasContains = 1; + private static final int maskHasSlice = 2; + private static final int maskHasIN = 4; + private static final int maskHasOnlyEqualityRestrictions = 8; + private static final int maskHasMultiColumnSlice = 16; + private static final int maskHasMultipleContains = 32; + + private DefaultRestrictionSet(Multimap restrictions, + boolean hasMultiColumnRestrictions) + { + this.restrictionsKeys = new ArrayList<>(restrictions.keySet()); + restrictionsKeys.sort(COLUMN_DEFINITION_COMPARATOR); + + List sortedRestrictions = new ArrayList<>(); + + int numberOfContains = 0; + int restrictionForBitmap = 0; + int bitmap = maskHasOnlyEqualityRestrictions; + + SingleRestriction previous = null; + for (int i = 0; i < restrictionsKeys.size(); i++) + { + ColumnMetadata col = restrictionsKeys.get(i); + Collection columnRestrictions = restrictions.get(col); + + for (SingleRestriction singleRestriction : columnRestrictions) + { + if (singleRestriction.isContains()) + { + bitmap |= maskHasContains; + ContainsRestriction contains = (ContainsRestriction) singleRestriction; + numberOfContains += (contains.numberOfValues() + contains.numberOfKeys() + contains.numberOfEntries()); + } + + if (hasMultiColumnRestrictions) + { + if (singleRestriction.equals(previous)) + continue; + previous = singleRestriction; + } + + restrictionForBitmap |= 1 << col.kind.ordinal(); + + sortedRestrictions.add(singleRestriction); + + if (singleRestriction.isSlice()) + { + bitmap |= maskHasSlice; + if (singleRestriction.isMultiColumn()) + bitmap |= maskHasMultiColumnSlice; + } + + if (singleRestriction.isIN()) + bitmap |= maskHasIN; + else if (!singleRestriction.isEQ()) + bitmap &= ~maskHasOnlyEqualityRestrictions; + } + } + this.hasBitmap = bitmap | (numberOfContains > 1 ? maskHasMultipleContains : 0); + this.restrictionForKindBitmap = restrictionForBitmap; + + this.restrictionsValues = Collections.unmodifiableList(sortedRestrictions); + this.restrictionsMap = restrictions; + } - if (existingRestrictions.isEmpty()) + @Override + public void addToRowFilter(RowFilter.Builder rowFilter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) throws InvalidRequestException { - for (ColumnMetadata columnDef : columnDefs) - restrictions.put(columnDef, restriction); + for (SingleRestriction restriction : restrictionsMap.values()) + rowFilter.addAllAsConjunction(b -> restriction.addToRowFilter(b, indexRegistry, options, annOptions, indexHints)); } - else + + @Override + public List getColumnDefs() { - for (SingleRestriction existing : existingRestrictions) - { - SingleRestriction newRestriction = mergeRestrictions(existing, restriction); + return restrictionsKeys; + } - for (ColumnMetadata columnDef : columnDefs) - restrictions.put(columnDef, newRestriction); - } + @Override + public void addFunctionsTo(List functions) + { + for (int i = 0; i < restrictionsValues.size(); i++) + restrictionsValues.get(i).addFunctionsTo(functions); } - return restrictions; - } + @Override + public boolean isEmpty() + { + return false; + } - @Override - public Set getRestrictions(ColumnMetadata columnDef) - { - Restriction existing = restrictions.get(columnDef); - return existing == null ? Collections.emptySet() : Collections.singleton(existing); - } + @Override + public int size() + { + return restrictionsKeys.size(); + } - /** - * Returns all the restrictions applied to the specified columns. - * - * @param columnDefs the column definitions - * @return all the restrictions applied to the specified columns - */ - private Set getRestrictions(Collection columnDefs) - { - Set set = new HashSet<>(); - for (ColumnMetadata columnDef : columnDefs) + @Override + public boolean hasRestrictionFor(ColumnMetadata.Kind kind) { - SingleRestriction existing = restrictions.get(columnDef); - if (existing != null) - set.add(existing); + return 0 != (restrictionForKindBitmap & 1 << kind.ordinal()); } - return set; - } - @Override - public boolean hasSupportingIndex(IndexRegistry indexRegistry) - { - for (Restriction restriction : restrictions.values()) + @Override + public Set getRestrictions(ColumnMetadata columnDef) { - if (restriction.hasSupportingIndex(indexRegistry)) - return true; + return restrictionsMap.get(columnDef).stream().map(r -> ((Restriction)r)).collect(Collectors.toSet()); } - return false; - } - @Override - public Index findSupportingIndex(IndexRegistry indexRegistry) - { - for (SingleRestriction restriction : restrictions.values()) + @Override + public boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { - Index index = restriction.findSupportingIndex(indexRegistry); - if (index != null) - return index; + for (SingleRestriction restriction : restrictionsMap.values()) + if (restriction.hasSupportingIndex(indexRegistry, indexHints)) + return true; + return false; } - return null; - } - @Override - public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan) - { - for (SingleRestriction restriction : restrictions.values()) + @Override + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { - Index index = restriction.findSupportingIndexFromQueryPlan(indexQueryPlan); - if (index != null) - return index; + for (SingleRestriction restriction : restrictionsMap.values()) + if (restriction.needsFiltering(indexGroup, indexHints)) + return true; + + return false; } - return null; - } - /** - * Returns the column after the specified one. - * - * @param columnDef the column for which the next one need to be found - * @return the column after the specified one. - */ - ColumnMetadata nextColumn(ColumnMetadata columnDef) - { - return restrictions.tailMap(columnDef, false).firstKey(); - } + @Override + public ColumnMetadata getFirstColumn() + { + return this.restrictionsKeys.get(0); + } - @Override - public ColumnMetadata getFirstColumn() - { - return isEmpty() ? null : this.restrictions.firstKey(); - } + @Override + public ColumnMetadata getLastColumn() + { + return this.restrictionsKeys.get(this.restrictionsKeys.size() - 1); + } - @Override - public ColumnMetadata getLastColumn() - { - return isEmpty() ? null : this.restrictions.lastKey(); + @Override + public SingleRestriction lastRestriction() + { + return this.restrictionsValues.get(this.restrictionsValues.size() - 1); + } + + @Override + public boolean hasMultipleContains() + { + return 0 != (hasBitmap & maskHasMultipleContains); + } + + @Override + public List restrictions() + { + return restrictionsValues; + } + + @Override + public boolean hasIN() + { + return 0 != (hasBitmap & maskHasIN); + } + + @Override + public boolean hasContains() + { + return 0 != (hasBitmap & maskHasContains); + } + + @Override + public boolean hasSlice() + { + return 0 != (hasBitmap & maskHasSlice); + } + + @Override + public boolean hasMultiColumnSlice() + { + return 0 != (hasBitmap & maskHasMultiColumnSlice); + } + + @Override + public boolean hasOnlyEqualityRestrictions() + { + return 0 != (hasBitmap & maskHasOnlyEqualityRestrictions); + } } + /** + * Checks if one of the restrictions applies to a column of the specific kind. + * @param kind the column kind + * @return {@code true} if one of the restrictions applies to a column of the specific kind, {@code false} otherwise. + */ + public abstract boolean hasRestrictionFor(ColumnMetadata.Kind kind); + /** * Returns the last restriction. - * - * @return the last restriction. */ - SingleRestriction lastRestriction() - { - return isEmpty() ? null : this.restrictions.lastEntry().getValue(); - } + public abstract SingleRestriction lastRestriction(); /** - * Merges the two specified restrictions. + * Checks if the restrictions contains multiple contains, contains key, or map[key] = value. * - * @param restriction the first restriction - * @param otherRestriction the second restriction - * @return the merged restriction - * @throws InvalidRequestException if the two restrictions cannot be merged + * @return true if the restrictions contain multiple contains, contains key, or , + * map[key] = value; false otherwise */ - private static SingleRestriction mergeRestrictions(SingleRestriction restriction, - SingleRestriction otherRestriction) - { - return restriction == null ? otherRestriction - : restriction.mergeWith(otherRestriction); - } + public abstract boolean hasMultipleContains(); - @Override - public Iterator iterator() - { - Iterator iterator = restrictions.values().iterator(); - return hasMultiColumnRestrictions ? new DistinctIterator<>(iterator) : iterator; - } + public abstract List restrictions(); /** - * Checks if any of the underlying restriction is an IN. - * @return true if any of the underlying restriction is an IN, false otherwise + * Checks if the restrictions contains multiple contains, contains key, or map[key] = value. + * + * @return true if the restrictions contains multiple contains, contains key, or , + * map[key] = value; false otherwise */ - public final boolean hasIN() - { - return hasIn; - } + public abstract boolean hasMultiColumnSlice(); - public boolean hasContains() + public static Builder builder() { - return hasContains; + return new Builder(); } - public final boolean hasSlice() + public static final class Builder { - return hasSlice; - } + private final Multimap newRestrictions = ArrayListMultimap.create(); + private boolean multiColumn = false; - public boolean hasAnn() - { - return hasAnn; - } + private ColumnMetadata lastRestrictionColumn; + private SingleRestriction lastRestriction; - /** - * Checks if all of the underlying restrictions are EQ or IN restrictions. - * - * @return true if all of the underlying restrictions are EQ or IN restrictions, - * false otherwise - */ - public final boolean hasOnlyEqualityRestrictions() - { - return hasOnlyEqualityRestrictions; - } + private Builder() + { + } - /** - * {@code Iterator} decorator that removes duplicates in an ordered one. - * - * @param the iterator element type. - */ - private static final class DistinctIterator extends AbstractIterator - { - /** - * The decorated iterator. - */ - private final Iterator iterator; + public void addRestriction(SingleRestriction restriction, boolean isDisjunction) + { + List columnDefs = restriction.getColumnDefs(); - /** - * The previous element. - */ - private E previous; + if (isDisjunction) + { + // If this restriction is part of a disjunction query then we don't want + // to merge the restrictions, we just add the new restriction + addRestrictionForColumns(columnDefs, restriction, null); + } + else + { + // ANDed together restrictions against the same columns should be merged. + Set existingRestrictions = getRestrictions(newRestrictions, columnDefs); - public DistinctIterator(Iterator iterator) - { - this.iterator = iterator; + // merge the new restriction into an existing one. note that there is only ever a single + // restriction (per column), UNLESS one is ORDER BY BM25 and the other is MATCH. + for (var existing : existingRestrictions) + { + // shouldMerge exists for the BM25/MATCH case + if (existing.shouldMerge(restriction)) + { + var merged = existing.mergeWith(restriction); + addRestrictionForColumns(merged.getColumnDefs(), merged, Set.of(existing)); + return; + } + } + + // no existing restrictions that we should merge the new one with, add a new one + addRestrictionForColumns(columnDefs, restriction, null); + } } - protected E computeNext() + private void addRestrictionForColumns(List columnDefs, + SingleRestriction restriction, + @Nullable Set replacedRestrictions) { - while(iterator.hasNext()) + for (int i = 0; i < columnDefs.size(); i++) { - E next = iterator.next(); - if (!next.equals(previous)) + ColumnMetadata column = columnDefs.get(i); + if (lastRestrictionColumn == null || COLUMN_DEFINITION_COMPARATOR.compare(lastRestrictionColumn, column) < 0) { - previous = next; - return next; + lastRestrictionColumn = column; + lastRestriction = restriction; } + // If the restriction is a merger of new restriction and existing restrictions then + // we need to remove the existing restrictions for the column before adding it + if (replacedRestrictions != null) + { + for (SingleRestriction r : replacedRestrictions) + newRestrictions.remove(column, r); + } + + newRestrictions.put(column, restriction); } - return endOfData(); + + multiColumn |= restriction.isMultiColumn(); + } + + private static Set getRestrictions(Multimap restrictions, + List columnDefs) + { + Set set = new HashSet<>(); + for (int i = 0; i < columnDefs.size(); i++) + { + Collection existing = restrictions.get(columnDefs.get(i)); + if (!existing.isEmpty()) + set.addAll(existing); + } + return set; + } + + public RestrictionSet build() + { + return isEmpty() ? EmptyRestrictionSet.INSTANCE : new DefaultRestrictionSet(newRestrictions, multiColumn); + } + + public boolean isEmpty() + { + return newRestrictions.isEmpty(); + } + + public SingleRestriction lastRestriction() + { + return lastRestriction; + } + + public ColumnMetadata nextColumn(ColumnMetadata columnDef) + { + // This method is only invoked in the statement-preparation-phase to construct an error message. + NavigableSet columns = new TreeSet<>(COLUMN_DEFINITION_COMPARATOR); + columns.addAll(newRestrictions.keySet()); + return columns.tailSet(columnDef, false).first(); } - } - - @Override - public String toString() - { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java index 049d287cfd5d..d49684767fb1 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java @@ -17,13 +17,11 @@ */ package org.apache.cassandra.cql3.restrictions; -import java.util.Collection; import java.util.List; import java.util.Set; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.index.Index; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.QueryOptions; @@ -42,34 +40,28 @@ class RestrictionSetWrapper implements Restrictions */ protected final RestrictionSet restrictions; - public RestrictionSetWrapper(RestrictionSet restrictions) + RestrictionSetWrapper(RestrictionSet restrictions) { this.restrictions = restrictions; } - public void addToRowFilter(RowFilter filter, + @Override + public void addToRowFilter(RowFilter.Builder rowFilter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - restrictions.addToRowFilter(filter, indexRegistry, options); + restrictions.addToRowFilter(rowFilter, indexRegistry, options, annOptions, indexHints); } + @Override public List getColumnDefs() { return restrictions.getColumnDefs(); } @Override - public Collection getColumnDefinitions() - { - return restrictions.getColumnDefinitions(); - } - - public RestrictionSet getRestrictionSet() - { - return restrictions; - } - public void addFunctionsTo(List functions) { restrictions.addFunctionsTo(functions); @@ -80,72 +72,67 @@ public boolean isEmpty() return restrictions.isEmpty(); } - public int size() + public List restrictions() { - return restrictions.size(); + return restrictions.restrictions(); } - public boolean hasSupportingIndex(IndexRegistry indexRegistry) + public int size() { - return restrictions.hasSupportingIndex(indexRegistry); + return restrictions.size(); } @Override - public Index findSupportingIndex(IndexRegistry indexRegistry) + public boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { - return restrictions.findSupportingIndex(indexRegistry); + return restrictions.hasSupportingIndex(indexRegistry, indexHints); } @Override - public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan) + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { - return restrictions.findSupportingIndexFromQueryPlan(indexQueryPlan); + return restrictions.needsFiltering(indexGroup, indexHints); } @Override - public boolean needsFiltering(Index.Group indexGroup) - { - return restrictions.needsFiltering(indexGroup); - } - public ColumnMetadata getFirstColumn() { return restrictions.getFirstColumn(); } + @Override public ColumnMetadata getLastColumn() { return restrictions.getLastColumn(); } + @Override public boolean hasIN() { return restrictions.hasIN(); } + @Override public boolean hasContains() { return restrictions.hasContains(); } + @Override public boolean hasSlice() { return restrictions.hasSlice(); } + @Override public boolean hasOnlyEqualityRestrictions() { return restrictions.hasOnlyEqualityRestrictions(); } + @Override public Set getRestrictions(ColumnMetadata columnDef) { return restrictions.getRestrictions(columnDef); } - - @Override - public String toString() - { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); - } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java index 0ad7530e7698..13ca1413e9ff 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java @@ -62,18 +62,28 @@ default Collection getColumnDefinitions() * Checks if any of the underlying restriction is an IN. * @return true if any of the underlying restriction is an IN, false otherwise */ - public boolean hasIN(); + default public boolean hasIN() + { + return false; + } /** * Checks if any of the underlying restrictions is a CONTAINS / CONTAINS KEY restriction. * @return true if any of the underlying restrictions is CONTAINS, false otherwise */ - public boolean hasContains(); + default public boolean hasContains() + { + return false; + } + /** * Checks if any of the underlying restrictions is a slice. * @return true if any of the underlying restrictions is a slice, false otherwise */ - public boolean hasSlice(); + default public boolean hasSlice() + { + return false; + } /** * Checks if all of the underlying restrictions are EQ or IN restrictions. @@ -81,5 +91,8 @@ default Collection getColumnDefinitions() * @return true if all of the underlying restrictions are EQ or IN restrictions, * false otherwise */ - public boolean hasOnlyEqualityRestrictions(); + default public boolean hasOnlyEqualityRestrictions() + { + return true; + } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java index d499bdce3889..ae08d8d657ac 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java @@ -20,24 +20,34 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.serializers.ListSerializer; -import org.apache.cassandra.cql3.*; -import org.apache.cassandra.cql3.Term.Terminal; +import javax.annotation.Nullable; + +import org.apache.cassandra.cql3.MarkerOrTerms; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.Terms; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.Bound; -import org.apache.cassandra.db.MultiCBuilder; +import org.apache.cassandra.db.MultiClusteringBuilder; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.serializers.ListSerializer; +import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; +import static com.google.common.base.Preconditions.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkBindValueSet; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; -import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; @@ -72,41 +82,28 @@ public ColumnMetadata getLastColumn() } @Override - public boolean hasSupportingIndex(IndexRegistry indexRegistry) + public boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { - for (Index index : indexRegistry.listIndexes()) - if (isSupportedBy(index)) - return true; - - return false; + return findSupportingIndex(indexRegistry, indexHints) != null; } - @Override - public Index findSupportingIndex(IndexRegistry indexRegistry) + @Nullable + public Index findSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { - for (Index index : indexRegistry.listIndexes()) - if (isSupportedBy(index)) - return index; - - return null; - } - - @Override - public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan) - { - for (Index index : indexQueryPlan.getIndexes()) - if (isSupportedBy(index)) - return index; - - return null; + return indexHints.getBestIndexFor(indexRegistry.listIndexes(), this::isSupportedBy, isContains()).orElse(null); } @Override - public boolean needsFiltering(Index.Group indexGroup) + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { for (Index index : indexGroup.getIndexes()) + { + if (indexHints.excludes(index)) + continue; + if (isSupportedBy(index)) return false; + } return true; } @@ -154,18 +151,20 @@ boolean canBeConvertedToMultiColumnRestriction() public static final class EQRestriction extends SingleColumnRestriction { - private final Term value; + public static final String CANNOT_BE_MERGED_ERROR = "%s cannot be restricted by more than one relation if it includes an Equal"; + + private final Term term; - public EQRestriction(ColumnMetadata columnDef, Term value) + public EQRestriction(ColumnMetadata columnDef, Term term) { super(columnDef); - this.value = value; + this.term = term; } @Override public void addFunctionsTo(List functions) { - value.addFunctionsTo(functions); + term.addFunctionsTo(functions); } @Override @@ -177,21 +176,24 @@ public boolean isEQ() @Override MultiColumnRestriction toMultiColumnRestriction() { - return new MultiColumnRestriction.EQRestriction(Collections.singletonList(columnDef), value); + return new MultiColumnRestriction.EQRestriction(Collections.singletonList(columnDef), term); } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - filter.add(columnDef, Operator.EQ, value.bindAndGet(options)); + filter.add(columnDef, Operator.EQ, term.bindAndGet(options)); } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { - builder.addElementToAll(value.bindAndGet(options)); + List element = Collections.singletonList(MultiClusteringBuilder.ClusteringElements.point(term.bindAndGet(options))); + builder.extend(element, getColumnDefs()); checkFalse(builder.containsNull(), "Invalid null value in condition for column %s", columnDef.name); checkFalse(builder.containsUnset(), "Invalid unset value for column %s", columnDef.name); return builder; @@ -200,13 +202,13 @@ public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) @Override public String toString() { - return String.format("EQ(%s)", value); + return String.format("EQ(%s)", term); } @Override public SingleRestriction doMergeWith(SingleRestriction otherRestriction) { - throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal", columnDef.name); + throw invalidRequest(CANNOT_BE_MERGED_ERROR, columnDef.name); } @Override @@ -216,11 +218,15 @@ protected boolean isSupportedBy(Index index) } } - public static abstract class INRestriction extends SingleColumnRestriction + public static class INRestriction extends SingleColumnRestriction { - public INRestriction(ColumnMetadata columnDef) + public static final String CANNOT_BE_MERGED_ERROR = "%s cannot be restricted by more than one relation if it includes a IN"; + protected final MarkerOrTerms terms; + + public INRestriction(ColumnMetadata columnDef, MarkerOrTerms terms) { super(columnDef); + this.terms = terms; } @Override @@ -232,121 +238,87 @@ public final boolean isIN() @Override public final SingleRestriction doMergeWith(SingleRestriction otherRestriction) { - throw invalidRequest("%s cannot be restricted by more than one relation if it includes a IN", columnDef.name); + throw invalidRequest(CANNOT_BE_MERGED_ERROR, columnDef.name); } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + MultiColumnRestriction toMultiColumnRestriction() { - builder.addEachElementToAll(getValues(options)); + throw new UnsupportedOperationException("Cannot convert to multicolumn restriction"); + } + + @Override + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) + { + List values = this.terms.bindAndGet(options, columnDef.name); + List elements = new ArrayList<>(values.size()); + for (ByteBuffer value: values) + elements.add(MultiClusteringBuilder.ClusteringElements.point(value)); + builder.extend(elements, getColumnDefs()); checkFalse(builder.containsNull(), "Invalid null value in condition for column %s", columnDef.name); checkFalse(builder.containsUnset(), "Invalid unset value for column %s", columnDef.name); return builder; } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - List values = getValues(options); + List values = this.terms.bindAndGet(options, columnDef.name); + for (ByteBuffer v : values) + { + checkNotNull(v, "Invalid null value for column %s", columnDef.name); + checkBindValueSet(v, "Invalid unset value for column %s", columnDef.name); + } ByteBuffer buffer = ListSerializer.pack(values, values.size()); filter.add(columnDef, Operator.IN, buffer); } - @Override - protected final boolean isSupportedBy(Index index) - { - return index.supportsExpression(columnDef, Operator.IN); - } - - protected abstract List getValues(QueryOptions options); - } - - public static class InRestrictionWithValues extends INRestriction - { - protected final List values; - - public InRestrictionWithValues(ColumnMetadata columnDef, List values) - { - super(columnDef); - this.values = values; - } - - @Override - MultiColumnRestriction toMultiColumnRestriction() - { - return new MultiColumnRestriction.InRestrictionWithValues(Collections.singletonList(columnDef), values); - } - @Override public void addFunctionsTo(List functions) { - Terms.addFunctions(values, functions); + terms.addFunctionsTo(functions); } @Override - protected List getValues(QueryOptions options) + public final boolean isSupportedBy(Index index) { - List buffers = new ArrayList<>(values.size()); - for (Term value : values) - buffers.add(value.bindAndGet(options)); - return buffers; + return index.supportsExpression(columnDef, Operator.IN); } @Override public String toString() { - return String.format("IN(%s)", values); + return String.format("IN(%s)", terms); } } - public static class InRestrictionWithMarker extends INRestriction + public static class SliceRestriction extends SingleColumnRestriction { - protected final AbstractMarker marker; + private final TermSlice slice; + private final List skippedValues; // values passed in NOT IN - public InRestrictionWithMarker(ColumnMetadata columnDef, AbstractMarker marker) + private SliceRestriction(ColumnMetadata columnDef, TermSlice slice, List skippedValues) { super(columnDef); - this.marker = marker; - } - - @Override - public void addFunctionsTo(List functions) - { - } - - @Override - MultiColumnRestriction toMultiColumnRestriction() - { - return new MultiColumnRestriction.InRestrictionWithMarker(Collections.singletonList(columnDef), marker); - } - - @Override - protected List getValues(QueryOptions options) - { - Terminal term = marker.bind(options); - checkNotNull(term, "Invalid null value for column %s", columnDef.name); - checkFalse(term == Constants.UNSET_VALUE, "Invalid unset value for column %s", columnDef.name); - Term.MultiItemTerminal lval = (Term.MultiItemTerminal) term; - return lval.getElements(); + assert slice != null; + assert skippedValues != null; + this.slice = slice; + this.skippedValues = skippedValues; } - @Override - public String toString() + public static SliceRestriction fromBound(ColumnMetadata columnDef, Bound bound, boolean inclusive, Term term) { - return "IN ?"; + TermSlice slice = TermSlice.newInstance(bound, inclusive, term); + return new SliceRestriction(columnDef, slice, Collections.emptyList()); } - } - public static class SliceRestriction extends SingleColumnRestriction - { - private final TermSlice slice; - - public SliceRestriction(ColumnMetadata columnDef, Bound bound, boolean inclusive, Term term) + public static SliceRestriction fromSkippedValues(ColumnMetadata columnDef, MarkerOrTerms skippedValues) { - super(columnDef); - slice = TermSlice.newInstance(bound, inclusive, term); + return new SliceRestriction(columnDef, TermSlice.UNBOUNDED, Collections.singletonList(skippedValues)); } @Override @@ -358,7 +330,7 @@ public void addFunctionsTo(List functions) @Override MultiColumnRestriction toMultiColumnRestriction() { - return new MultiColumnRestriction.SliceRestriction(Collections.singletonList(columnDef), slice); + return new MultiColumnRestriction.SliceRestriction(Collections.singletonList(columnDef), slice, skippedValues); } @Override @@ -368,7 +340,7 @@ public boolean isSlice() } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { throw new UnsupportedOperationException(); } @@ -380,17 +352,31 @@ public boolean hasBound(Bound b) } @Override - public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options) + public MultiClusteringBuilder appendBoundTo(MultiClusteringBuilder builder, Bound bound, QueryOptions options) { Bound b = bound.reverseIfNeeded(getFirstColumn()); - if (!hasBound(b)) - return builder; - - ByteBuffer value = slice.bound(b).bindAndGet(options); - checkBindValueSet(value, "Invalid unset value for column %s", columnDef.name); - return builder.addElementToAll(value); + List toAdd = new ArrayList<>(skippedValues.size() + 1); + if (hasBound(b)) + { + ByteBuffer value = slice.bound(b).bindAndGet(options); + checkBindValueSet(value, "Invalid unset value for column %s", columnDef.name); + toAdd.add(MultiClusteringBuilder.ClusteringElements.bound(value, bound, slice.isInclusive(b))); + } + else + { + toAdd.add(bound.isStart() ? MultiClusteringBuilder.ClusteringElements.BOTTOM : MultiClusteringBuilder.ClusteringElements.TOP); + } + for (MarkerOrTerms markerOrTerms : skippedValues) + { + for (ByteBuffer value: markerOrTerms.bindAndGet(options, columnDef.name)) + { + checkBindValueSet(value, "Invalid unset value for column %s", columnDef.name); + toAdd.add(MultiClusteringBuilder.ClusteringElements.bound(value, bound, false)); + } + } + return builder.extend(toAdd, getColumnDefs()); } @Override @@ -414,58 +400,236 @@ public SingleRestriction doMergeWith(SingleRestriction otherRestriction) checkFalse(hasBound(Bound.END) && otherSlice.hasBound(Bound.END), "More than one restriction was found for the end bound on %s", columnDef.name); - return new SliceRestriction(columnDef, slice.merge(otherSlice.slice)); + List newSkippedValues = new ArrayList<>(skippedValues.size() + otherSlice.skippedValues.size()); + newSkippedValues.addAll(skippedValues); + newSkippedValues.addAll(otherSlice.skippedValues); + return new SliceRestriction(columnDef, slice.merge(otherSlice.slice), newSkippedValues); } @Override - public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { for (Bound b : Bound.values()) if (hasBound(b)) filter.add(columnDef, slice.getIndexOperator(b), slice.bound(b).bindAndGet(options)); + + for (MarkerOrTerms markerOrTerms : skippedValues) + { + for (ByteBuffer value : markerOrTerms.bindAndGet(options, columnDef.name)) + filter.add(columnDef, Operator.NEQ, value); + } } @Override protected boolean isSupportedBy(Index index) { - return slice.isSupportedBy(columnDef, index); + boolean supportsSlice = slice.isSupportedBy(columnDef, index); + boolean supportsNeq = index.supportsExpression(columnDef, Operator.NEQ); + return supportsSlice || !skippedValues.isEmpty() && supportsNeq; } @Override public String toString() { - return String.format("SLICE%s", slice); + return String.format("SLICE{%s, NOT IN %s}", slice, skippedValues); } - private SliceRestriction(ColumnMetadata columnDef, TermSlice slice) + } + + /** + * One or more slice restrictions on a column's map entries. + * For a map column of type map<text,int> with name m, here are some examples of valid restrictions: + * One restriction: m['a'] > 1 + * Restrictions on different keys: m['a'] > 1 AND m['b'] < 2 + * Restrictions on same key: m['a'] > 0 AND m['a'] < 2 + */ + public static class MapSliceRestriction extends SingleColumnRestriction + { + // Left is the map's key and right is the slice on the map's value. + private final List> slices; + + public MapSliceRestriction(ColumnMetadata columnDef, Bound bound, boolean inclusive, Term key, Term value) { super(columnDef); - this.slice = slice; + slices = new ArrayList<>(); + slices.add(Pair.create(key, TermSlice.newInstance(bound, inclusive, value))); + } + + private MapSliceRestriction(ColumnMetadata columnDef, List> slices) + { + super(columnDef); + this.slices = slices; + } + + @Override + public void addFunctionsTo(List functions) + { + slices.forEach(slice -> { + slice.left.addFunctionsTo(functions); + slice.right.addFunctionsTo(functions); + }); + } + + @Override + MultiColumnRestriction toMultiColumnRestriction() + { + throw new UnsupportedOperationException(); + } + + @Override + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) + { + // MapSliceRestrictions are not supported on clustering columns. + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasBound(Bound b) + { + // Because a MapSliceRestriction can have multiple slices, we cannot implement this method. + throw new UnsupportedOperationException("Bounds not well defined for map slice restrictions"); + } + + @Override + public boolean isInclusive(Bound b) + { + throw new UnsupportedOperationException(); + } + + @Override + public SingleRestriction doMergeWith(SingleRestriction otherRestriction) + { + checkTrue(otherRestriction instanceof SingleColumnRestriction.MapSliceRestriction, + "Column \"%s\" cannot be restricted by both an inequality relation and \"%s\"", + columnDef.name, otherRestriction); + + MapSliceRestriction otherMapSlice = ((SingleColumnRestriction.MapSliceRestriction) otherRestriction); + // Because the keys are not necessarily bound, we defer on making assertions about boundary violations + // until we create the row filter. + ArrayList> newSlices = new ArrayList<>(slices); + newSlices.addAll(otherMapSlice.slices); + return new MapSliceRestriction(columnDef, newSlices); + } + + @Override + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) + { + HashMap map = new HashMap(); + // First, we iterate through to verify that none of the slices create invalid ranges. + // We can only do this now because this is the point when we can bind the map's key and + // correctly compare them. + for (Pair pair : slices) + { + final ByteBuffer key = pair.left.bindAndGet(options); + final TermSlice otherSlice = pair.right(); + map.compute(key, (k, slice) -> { + if (slice == null) + return otherSlice; + + // Validate that the bounds do not conflict + checkFalse(slice.hasBound(Bound.START) && otherSlice.hasBound(Bound.START), + "More than one restriction was found for the start bound on %s", columnDef.name); + checkFalse(slice.hasBound(Bound.END) && otherSlice.hasBound(Bound.END), + "More than one restriction was found for the end bound on %s", columnDef.name); + return slice.merge(otherSlice); + }); + } + // Now we can add the filters. + for (Map.Entry entry : map.entrySet()) + { + TermSlice slice = entry.getValue(); + Term start = slice.bound(Bound.START); + if (start != null) + filter.addMapComparison(columnDef, + entry.getKey(), + slice.isInclusive(Bound.START) ? Operator.GTE : Operator.GT, + start.bindAndGet(options)); + Term end = slice.bound(Bound.END); + if (end != null) + filter.addMapComparison(columnDef, + entry.getKey(), + slice.isInclusive(Bound.END) ? Operator.LTE : Operator.LT, + end.bindAndGet(options)); + } + } + + @Override + protected boolean isSupportedBy(Index index) + { + for (Pair slice : slices) + if (!slice.right().isSupportedBy(columnDef, index)) + return false; + return true; + } + + @Override + public String toString() + { + return String.format("MAP_SLICE %s", slices); } } - // This holds CONTAINS, CONTAINS_KEY, and map[key] = value restrictions because we might want to have any combination of them. + // This holds CONTAINS, CONTAINS_KEY, NOT CONTAINS, NOT CONTAINS KEY and map[key] = value restrictions because we might want to have any combination of them. public static final class ContainsRestriction extends SingleColumnRestriction { + public static final String MULTIPLE_INDEXES_WARNING = "Multiple indexes found for CONTAINS restriction on %s. " + + "Using not-analyzed index %s. You can use index hints to " + + "specify which index to use, as in SELECT ... WITH included_indexes={...}."; + private final List values = new ArrayList<>(); // for CONTAINS + private final List negativeValues = new ArrayList<>(); // for NOT_CONTAINS private final List keys = new ArrayList<>(); // for CONTAINS_KEY + private final List negativeKeys = new ArrayList<>(); // for NOT_CONTAINS_KEY private final List entryKeys = new ArrayList<>(); // for map[key] = value private final List entryValues = new ArrayList<>(); // for map[key] = value + private final List negativeEntryKeys = new ArrayList<>(); // for map[key] != value + private final List negativeEntryValues = new ArrayList<>(); // for map[key] != value + + public ContainsRestriction(ColumnMetadata columnDef, Term t, boolean isKey, boolean isNot) + { + super(columnDef); + if (isNot) + { + if (isKey) + negativeKeys.add(t); + else + negativeValues.add(t); + } + else + { + if (isKey) + keys.add(t); + else + values.add(t); + } + } - public ContainsRestriction(ColumnMetadata columnDef, Term t, boolean isKey) + public ContainsRestriction(ColumnMetadata columnDef, Term mapKey, Term mapValue, boolean isNot) { super(columnDef); - if (isKey) - keys.add(t); + if (isNot) + { + negativeEntryKeys.add(mapKey); + negativeEntryValues.add(mapValue); + } else - values.add(t); + { + entryKeys.add(mapKey); + entryValues.add(mapValue); + } } - public ContainsRestriction(ColumnMetadata columnDef, Term mapKey, Term mapValue) + private ContainsRestriction(ColumnMetadata columnDef) { super(columnDef); - entryKeys.add(mapKey); - entryValues.add(mapValue); } @Override @@ -481,7 +645,7 @@ boolean canBeConvertedToMultiColumnRestriction() } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { throw new UnsupportedOperationException(); } @@ -500,7 +664,6 @@ public SingleRestriction doMergeWith(SingleRestriction otherRestriction) columnDef.name); SingleColumnRestriction.ContainsRestriction newContains = new ContainsRestriction(columnDef); - copyKeysAndValues(this, newContains); copyKeysAndValues((ContainsRestriction) otherRestriction, newContains); @@ -508,18 +671,33 @@ public SingleRestriction doMergeWith(SingleRestriction otherRestriction) } @Override - public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { for (ByteBuffer value : bindAndGet(values, options)) filter.add(columnDef, Operator.CONTAINS, value); for (ByteBuffer key : bindAndGet(keys, options)) filter.add(columnDef, Operator.CONTAINS_KEY, key); + for (ByteBuffer value : bindAndGet(negativeValues, options)) + filter.add(columnDef, Operator.NOT_CONTAINS, value); + for (ByteBuffer key : bindAndGet(negativeKeys, options)) + filter.add(columnDef, Operator.NOT_CONTAINS_KEY, key); List eks = bindAndGet(entryKeys, options); List evs = bindAndGet(entryValues, options); assert eks.size() == evs.size(); for (int i = 0; i < eks.size(); i++) - filter.addMapEquality(columnDef, eks.get(i), Operator.EQ, evs.get(i)); + filter.addMapComparison(columnDef, eks.get(i), Operator.EQ, evs.get(i)); + + List neks = bindAndGet(negativeEntryKeys, options); + List nevs = bindAndGet(negativeEntryValues, options); + assert neks.size() == nevs.size(); + for (int i = 0; i < neks.size(); i++) + filter.addMapComparison(columnDef, neks.get(i), Operator.NEQ, nevs.get(i)); + } @Override @@ -533,13 +711,21 @@ protected boolean isSupportedBy(Index index) if (numberOfKeys() > 0) supported |= index.supportsExpression(columnDef, Operator.CONTAINS_KEY); + if (numberOfNegativeValues() > 0) + supported |= index.supportsExpression(columnDef, Operator.NOT_CONTAINS); + + if (numberOfNegativeKeys() > 0) + supported |= index.supportsExpression(columnDef, Operator.NOT_CONTAINS_KEY); + if (numberOfEntries() > 0) supported |= index.supportsExpression(columnDef, Operator.EQ); + if (numberOfNegativeEntries() > 0) + supported |= index.supportsExpression(columnDef, Operator.NEQ); + return supported; } - @Override public boolean needsFiltering(Index.Group indexGroup) { // multiple contains might require filtering on some indexes, since that is equivalent to a disjunction (or) @@ -559,16 +745,66 @@ public int numberOfValues() return values.size(); } + public int numberOfNegativeValues() + { + return negativeValues.size(); + } + public int numberOfKeys() { return keys.size(); } + public int numberOfNegativeKeys() + { + return negativeKeys.size(); + } + public int numberOfEntries() { return entryKeys.size(); } + public int numberOfNegativeEntries() + { + return negativeEntryKeys.size(); + } + + @Override + public Index findSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) + { + Index bestIndex = super.findSupportingIndex(indexRegistry, indexHints); + + // If there is no index, or the best index is explicitly included by the user-provided hints, + // we don't need to do anything but return the best index. + if (bestIndex == null || indexHints.includes(bestIndex)) + return bestIndex; + + // If there are multiple supporting indexes, we prefer those without an analyzer (see CNDB-13925). + // This is done by the call to findSupportingIndex() above, but we also check it here to throw a client warning. + boolean hasNotAnalyzedIndex = false; + boolean hasAnalyzedIndex = false; + for (Index index : indexRegistry.listNotExcludedIndexes(indexHints)) + { + if (isSupportedBy(index)) + { + if (index.isAnalyzed()) + hasAnalyzedIndex = true; + else + hasNotAnalyzedIndex = true; + } + } + + // We use a client warning key so the warning is emitted just once per query. + if (hasNotAnalyzedIndex && hasAnalyzedIndex) + { + String msg = String.format(MULTIPLE_INDEXES_WARNING, columnDef.name, bestIndex.getIndexMetadata().name); + ClientWarn.instance.warn(msg, "multiple_indexes_for_contains_on_" + columnDef.name); + } + + return bestIndex; + } + @Override public void addFunctionsTo(List functions) { @@ -576,12 +812,18 @@ public void addFunctionsTo(List functions) Terms.addFunctions(keys, functions); Terms.addFunctions(entryKeys, functions); Terms.addFunctions(entryValues, functions); + + Terms.addFunctions(negativeValues, functions); + Terms.addFunctions(negativeKeys, functions); + Terms.addFunctions(negativeEntryKeys, functions); + Terms.addFunctions(negativeEntryValues, functions); } @Override public String toString() { - return String.format("CONTAINS(values=%s, keys=%s, entryKeys=%s, entryValues=%s)", values, keys, entryKeys, entryValues); + return String.format("CONTAINS(values=%s, keys=%s, entryKeys=%s, entryValues=%s)", + values, keys, entryKeys, entryValues); } @Override @@ -591,7 +833,7 @@ public boolean hasBound(Bound b) } @Override - public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options) + public MultiClusteringBuilder appendBoundTo(MultiClusteringBuilder builder, Bound bound, QueryOptions options) { throw new UnsupportedOperationException(); } @@ -626,17 +868,49 @@ private static List bindAndGet(List terms, QueryOptions option private static void copyKeysAndValues(ContainsRestriction from, ContainsRestriction to) { to.values.addAll(from.values); + to.negativeValues.addAll(from.negativeValues); to.keys.addAll(from.keys); + to.negativeKeys.addAll(from.negativeKeys); to.entryKeys.addAll(from.entryKeys); to.entryValues.addAll(from.entryValues); + to.negativeEntryKeys.addAll(from.negativeEntryKeys); + to.negativeEntryValues.addAll(from.negativeEntryValues); + } - private ContainsRestriction(ColumnMetadata columnDef) + public Index findSupportingIndex(IndexRegistry indexRegistry) { - super(columnDef); + // if there are multiple supporting indexes, we prefer those without an analyzer (see CNDB-13925) + Index notAnalyzedIndex = null; + Index analyzedIndex = null; + for (Index index : indexRegistry.listIndexes()) + { + if (isSupportedBy(index)) + { + if (index.getAnalyzer(null).isPresent()) + analyzedIndex = index; + else + notAnalyzedIndex = index; + } + } + + if (notAnalyzedIndex != null) + { + // We prefer the not analyzed index, but if there was also an analyzed index, we warn the user. + // We use a client warning key so the warning is emitted just once per query. + if (analyzedIndex != null) + { + String msg = String.format(MULTIPLE_INDEXES_WARNING, columnDef.name, notAnalyzedIndex.getIndexMetadata().name); + ClientWarn.instance.warn(msg, "multiple_indexes_for_contains_on_" + columnDef.name); + } + return notAnalyzedIndex; + } + + return analyzedIndex; } } + public static final class IsNotNullRestriction extends SingleColumnRestriction { public IsNotNullRestriction(ColumnMetadata columnDef) @@ -662,15 +936,17 @@ MultiColumnRestriction toMultiColumnRestriction() } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { throw new UnsupportedOperationException("Secondary indexes do not support IS NOT NULL restrictions"); } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { throw new UnsupportedOperationException("Cannot use IS NOT NULL restriction for slicing"); } @@ -713,12 +989,6 @@ public void addFunctionsTo(List functions) value.addFunctionsTo(functions); } - @Override - public boolean isEQ() - { - return false; - } - @Override public boolean isLIKE() { @@ -738,21 +1008,23 @@ MultiColumnRestriction toMultiColumnRestriction() } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { Pair operation = makeSpecific(value.bindAndGet(options)); // there must be a suitable INDEX for LIKE_XXX expressions RowFilter.SimpleExpression expression = filter.add(columnDef, operation.left, operation.right); - indexRegistry.getBestIndexFor(expression) + indexRegistry.getBestIndexFor(expression, indexHints) .orElseThrow(() -> invalidRequest("%s is only supported on properly indexed columns", expression)); } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { // LIKE can be used with clustering columns, but as it doesn't // represent an actual clustering value, it can't be used in a @@ -824,14 +1096,117 @@ else if (ByteBufferUtil.startsWith(value, LIKE_WILDCARD)) return Pair.create(operator, newValue); } } + + /** + * For now, index based ordering is represented as a restriction. + */ + public static final class OrderRestriction extends SingleColumnRestriction + { + private final SingleColumnRestriction otherRestriction; + private final Operator direction; + + public OrderRestriction(ColumnMetadata columnDef, Operator direction) + { + this(columnDef, null, direction); + } + + private OrderRestriction(ColumnMetadata columnDef, SingleColumnRestriction otherRestriction, Operator direction) + { + super(columnDef); + this.otherRestriction = otherRestriction; + this.direction = direction; + + if (direction != Operator.ORDER_BY_ASC && direction != Operator.ORDER_BY_DESC) + throw new IllegalArgumentException("Ordering restriction must be ASC or DESC"); + } + + public Operator getDirection() + { + return direction; + } + + @Override + public void addFunctionsTo(List functions) + { + if (otherRestriction != null) + otherRestriction.addFunctionsTo(functions); + } + + @Override + MultiColumnRestriction toMultiColumnRestriction() + { + throw new UnsupportedOperationException(); + } + + @Override + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) + { + filter.add(columnDef, direction, ByteBufferUtil.EMPTY_BYTE_BUFFER); + if (otherRestriction != null) + otherRestriction.addToRowFilter(filter, indexRegistry, options, annOptions, indexHints); + } + + @Override + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return String.format("ORDER BY %s %s", columnDef.name, direction); + } + + @Override + public SingleRestriction doMergeWith(SingleRestriction otherRestriction) + { + if (!(otherRestriction instanceof SingleColumnRestriction)) + throw invalidRequest("%s cannot be restricted by both ORDER BY and %s", + columnDef.name, + otherRestriction.toString()); + var otherSingleColumnRestriction = (SingleColumnRestriction) otherRestriction; + if (this.otherRestriction == null) + return new OrderRestriction(columnDef, otherSingleColumnRestriction, direction); + var mergedOtherRestriction = this.otherRestriction.doMergeWith(otherSingleColumnRestriction); + return new OrderRestriction(columnDef, (SingleColumnRestriction) mergedOtherRestriction, direction); + } + + @Override + protected boolean isSupportedBy(Index index) + { + return index.supportsExpression(columnDef, direction) + && (otherRestriction == null || otherRestriction.isSupportedBy(index)); + } + + @Override + public boolean isIndexBasedOrdering() + { + return true; + } + } + public static final class AnnRestriction extends SingleColumnRestriction { private final Term value; + // This is the only kind of restriction that can be merged into an AnnRestriction because all Ann + // are on vector columns, and the only other valid restriction on vector columns is BOUNDED_ANN. + private final BoundedAnnRestriction boundedAnnRestriction; public AnnRestriction(ColumnMetadata columnDef, Term value) + { + this(columnDef, value, null); + } + + private AnnRestriction(ColumnMetadata columnDef, Term value, BoundedAnnRestriction boundedAnnRestriction) { super(columnDef); this.value = value; + this.boundedAnnRestriction = boundedAnnRestriction; } public ByteBuffer value(QueryOptions options) @@ -840,11 +1215,93 @@ public ByteBuffer value(QueryOptions options) } @Override - public boolean isANN() + public void addFunctionsTo(List functions) + { + value.addFunctionsTo(functions); + if (boundedAnnRestriction != null) + boundedAnnRestriction.addFunctionsTo(functions); + } + + @Override + MultiColumnRestriction toMultiColumnRestriction() + { + throw new UnsupportedOperationException(); + } + + @Override + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) + { + filter.addANNExpression(columnDef, value.bindAndGet(options), annOptions); + if (boundedAnnRestriction != null) + boundedAnnRestriction.addToRowFilter(filter, indexRegistry, options, annOptions, indexHints); + } + + @Override + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return String.format("ANN(%s)", value); + } + + @Override + public SingleRestriction doMergeWith(SingleRestriction otherRestriction) + { + if (otherRestriction.isIndexBasedOrdering()) + throw invalidRequest("%s cannot be restricted by multiple ANN restrictions", columnDef.name); + + if (!otherRestriction.isBoundedAnn()) + throw invalidRequest("%s cannot be restricted by both BOUNDED_ANN and %s", columnDef.name, otherRestriction.toString()); + + if (boundedAnnRestriction == null) + return new AnnRestriction(columnDef, value, (BoundedAnnRestriction) otherRestriction); + + var mergedAnnRestriction = boundedAnnRestriction.doMergeWith(otherRestriction); + return new AnnRestriction(columnDef, value, (BoundedAnnRestriction) mergedAnnRestriction); + } + + @Override + protected boolean isSupportedBy(Index index) + { + return index.supportsExpression(columnDef, Operator.ANN) && (boundedAnnRestriction == null || boundedAnnRestriction.isSupportedBy(index)); + } + + @Override + public boolean isIndexBasedOrdering() { return true; } + @Override + public boolean isBoundedAnn() + { + return boundedAnnRestriction != null; + } + } + + public static final class Bm25Restriction extends SingleColumnRestriction + { + private final Term value; + + public Bm25Restriction(ColumnMetadata columnDef, Term value) + { + super(columnDef); + this.value = value; + } + + public ByteBuffer value(QueryOptions options) + { + return value.bindAndGet(options); + } + @Override public void addFunctionsTo(List functions) { @@ -858,15 +1315,22 @@ MultiColumnRestriction toMultiColumnRestriction() } @Override - public void addToRowFilter(RowFilter filter, + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, - QueryOptions options) + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - filter.add(columnDef, Operator.ANN, value.bindAndGet(options)); + var index = findSupportingIndex(indexRegistry, indexHints); + var valueBytes = value.bindAndGet(options); + var terms = index.getAnalyzer(valueBytes).get().queriedTokens(); + if (terms.isEmpty()) + throw invalidRequest("BM25 query must contain at least one term (perhaps your analyzer is discarding tokens you didn't expect)"); + filter.add(columnDef, Operator.BM25, valueBytes); } @Override - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) { throw new UnsupportedOperationException(); } @@ -874,19 +1338,211 @@ public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options) @Override public String toString() { - return String.format("ANN(%s)", value); + return String.format("BM25(%s)", value); } @Override public SingleRestriction doMergeWith(SingleRestriction otherRestriction) { - throw invalidRequest("%s cannot be restricted by more than one relation in an ANN ordering", columnDef.name); + throw invalidRequest("%s cannot be restricted by both BM25 and %s", columnDef.name, otherRestriction.toString()); + } + + @Override + protected boolean isSupportedBy(Index index) + { + return index.supportsExpression(columnDef, Operator.BM25); + } + + @Override + public boolean isIndexBasedOrdering() + { + return true; + } + + @Override + public boolean shouldMerge(SingleRestriction other) + { + // we don't want to merge MATCH restrictions with ORDER BY BM25 + // so shouldMerge = false for that scenario, and true for others + // (because even though we can't meaningfully merge with others, we want doMergeWith to be called to throw) + // + // (Note that because ORDER BY is processed before WHERE, we only need this check in the BM25 class) + return !other.isAnalyzerMatches(); + } + } + + /** + * A Bounded ANN Restriction is one that uses a similarity score as the limiting factor for ANN instead of a number + * of results. + */ + public static final class BoundedAnnRestriction extends SingleColumnRestriction + { + private final Term value; + private final Term distance; + private final boolean isInclusive; + + public BoundedAnnRestriction(ColumnMetadata columnDef, Term value, Term distance, boolean isInclusive) + { + super(columnDef); + this.value = value; + this.distance = distance; + this.isInclusive = isInclusive; + } + + @Override + public void addFunctionsTo(List functions) + { + value.addFunctionsTo(functions); + distance.addFunctionsTo(functions); + } + + @Override + MultiColumnRestriction toMultiColumnRestriction() + { + // only used by partition and clustering restrictions + throw new UnsupportedOperationException(); + } + + @Override + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) + { + filter.addGeoDistanceExpression(columnDef, value.bindAndGet(options), isInclusive ? Operator.LTE : Operator.LT, distance.bindAndGet(options)); + } + + @Override + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return String.format("BOUNDED_ANN(%s)", value); + } + + @Override + public SingleRestriction doMergeWith(SingleRestriction otherRestriction) + { + if (!otherRestriction.isBoundedAnn()) + throw invalidRequest("%s cannot be restricted by both BOUNDED_ANN and %s", columnDef.name, otherRestriction.toString()); + throw invalidRequest("%s cannot be restricted by multiple BOUNDED_ANN restrictions", columnDef.name, otherRestriction.toString()); + } + + @Override + protected boolean isSupportedBy(Index index) + { + return index.supportsExpression(columnDef, Operator.BOUNDED_ANN); + } + + @Override + public boolean isBoundedAnn() + { + return true; + } + } + + public static final class AnalyzerMatchesRestriction extends SingleColumnRestriction + { + public static final String CANNOT_BE_MERGED_ERROR = "%s cannot be restricted by other operators if it includes analyzer match (:)"; + public static final String CANNOT_BE_RESTRICTED_BY_CLUSTERING_ERROR = + "Cannot restrict column '%s' by analyzer match (:) because it is a clustering column. Equals (=) can be used " + + "instead of match, but it will produce incomplete results due to clustering column post filtering."; + + private final List values; + + public AnalyzerMatchesRestriction(ColumnMetadata columnDef, Term value) + { + this(columnDef, Collections.singletonList(value)); + } + + public AnalyzerMatchesRestriction(ColumnMetadata columnDef, List values) + { + super(columnDef); + // If we don't fail here, we would alternatively fail with the call to this::appendTo, which produces + // an unhelpful error message. + if (columnDef.isClusteringColumn()) + throw invalidRequest(CANNOT_BE_RESTRICTED_BY_CLUSTERING_ERROR, columnDef.name); + this.values = values; + } + + @Override + public boolean isAnalyzerMatches() + { + return true; + } + + List getValues() + { + return values; + } + + @Override + public void addFunctionsTo(List functions) + { + for (Term value : values) + { + value.addFunctionsTo(functions); + } + } + + @Override + MultiColumnRestriction toMultiColumnRestriction() + { + throw new UnsupportedOperationException(); + } + + @Override + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) + { + for (Term value : values) + { + filter.add(columnDef, Operator.ANALYZER_MATCHES, value.bindAndGet(options)); + } + } + + @Override + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return String.format("ANALYZER_MATCHES(%s)", values); + } + + /** + * Merges this restriction with another restriction. Only called for conjuctive restrictions. + */ + @Override + public SingleRestriction doMergeWith(SingleRestriction otherRestriction) + { + if (!otherRestriction.isAnalyzerMatches()) + throw invalidRequest(CANNOT_BE_MERGED_ERROR, columnDef.name); + + List otherValues = otherRestriction instanceof AnalyzerMatchesRestriction + ? ((AnalyzerMatchesRestriction) otherRestriction).getValues() + : List.of(((EQRestriction) otherRestriction).term); + List newValues = new ArrayList<>(values.size() + otherValues.size()); + newValues.addAll(values); + newValues.addAll(otherValues); + return new AnalyzerMatchesRestriction(columnDef, newValues); } @Override protected boolean isSupportedBy(Index index) { - return index.supportsExpression(columnDef, Operator.ANN); + return index.supportsExpression(columnDef, Operator.ANALYZER_MATCHES); } } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java index 8f4e2b5de4bb..0bc9e161f509 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java @@ -19,7 +19,7 @@ import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.statements.Bound; -import org.apache.cassandra.db.MultiCBuilder; +import org.apache.cassandra.db.MultiClusteringBuilder; /** * A single restriction/clause on one or multiple column. @@ -36,22 +36,22 @@ public default boolean isEQ() return false; } - public default boolean isLIKE() + public default boolean isAnalyzerMatches() { return false; } - public default boolean isIN() + public default boolean isLIKE() { return false; } - public default boolean isContains() + public default boolean isIN() { return false; } - public default boolean isANN() + public default boolean isContains() { return false; } @@ -74,6 +74,16 @@ public default boolean isMultiColumn() return false; } + public default boolean isIndexBasedOrdering() + { + return false; + } + + public default boolean isBoundedAnn() + { + return false; + } + /** * Checks if the specified bound is set or not. * @param b the bound type @@ -113,7 +123,7 @@ public default boolean isInclusive(Bound b) * @param options the query options * @return the MultiCBuilder */ - public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options); + public MultiClusteringBuilder appendTo(MultiClusteringBuilder builder, QueryOptions options); /** * Appends the values of the SingleRestriction for the specified bound to the specified builder. @@ -123,8 +133,20 @@ public default boolean isInclusive(Bound b) * @param options the query options * @return the MultiCBuilder */ - public default MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options) + public default MultiClusteringBuilder appendBoundTo(MultiClusteringBuilder builder, Bound bound, QueryOptions options) { return appendTo(builder, options); } + + /** + * @return true if the other restriction should be merged with this one. + * This is NOT for preventing illegal combinations of restrictions, e.g. + * a=1 AND a=2; that is handled by mergeWith. Instead, this is for the case + * where we want two completely different semantics against the same column. + * Currently the only such case is BM25 with MATCH. + */ + default boolean shouldMerge(SingleRestriction other) + { + return true; + } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java index adc4e65c4823..a4f4d25a05da 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java @@ -18,38 +18,65 @@ package org.apache.cassandra.cql3.restrictions; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.NavigableSet; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.common.collect.Streams; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.Ordering; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.Relation; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.cql3.statements.SelectOptions; import org.apache.cassandra.cql3.statements.StatementType; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.FloatType; -import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; import org.apache.cassandra.db.virtual.VirtualTable; -import org.apache.cassandra.dht.*; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.dht.ExcludingBounds; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.IncludingExcludingBounds; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.IntegerType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.index.NoopIndex; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.btree.BTreeSet; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_ENABLE_GENERAL_ORDER_BY; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; @@ -57,8 +84,10 @@ /** * The restrictions corresponding to the relations specified on the where-clause of CQL query. */ -public final class StatementRestrictions +public class StatementRestrictions { + public static final boolean ENABLE_SAI_GENERAL_ORDER_BY = SAI_ENABLE_GENERAL_ORDER_BY.getBoolean(); + private static final String ALLOW_FILTERING_MESSAGE = "Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. "; @@ -69,55 +98,88 @@ public final class StatementRestrictions "Executing this query despite the performance unpredictability with ALLOW FILTERING has been disabled " + "by the allow_filtering_enabled property in cassandra.yaml"; - public static final String ANN_REQUIRES_INDEX_MESSAGE = "ANN ordering by vector requires the column to be indexed"; + public static final String HAS_UNSUPPORTED_INDEX_RESTRICTION_MESSAGE_SINGLE = + "Column '%s' has an index but does not support the operators specified in the query. " + + "If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING"; - public static final String VECTOR_INDEXES_ANN_ONLY_MESSAGE = "Vector indexes only support ANN queries"; + public static final String HAS_UNSUPPORTED_INDEX_RESTRICTION_MESSAGE_MULTI = + "Columns %s have indexes but do not support the operators specified in the query. " + + "If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING"; - public static final String ANN_ONLY_SUPPORTED_ON_VECTOR_MESSAGE = "ANN ordering is only supported on float vector indexes"; + public static final String INDEX_DOES_NOT_SUPPORT_LIKE_MESSAGE = "Index on column %s does not support LIKE restrictions."; - public static final String ANN_REQUIRES_INDEXED_FILTERING_MESSAGE = "ANN ordering by vector requires all restricted column(s) to be indexed"; + public static final String INDEX_DOES_NOT_SUPPORT_ANALYZER_MATCHES_MESSAGE = "Index on column %s does not support ':' restrictions."; - /** - * The type of statement - */ - private final StatementType type; + public static final String INDEX_DOES_NOT_SUPPORT_DISJUNCTION = + "An index involved in this query does not support disjunctive queries using the OR operator"; + + public static final String RESTRICTION_REQUIRES_INDEX_MESSAGE = "%s restriction is only supported on properly indexed columns. %s is not valid."; + + public static final String PARTITION_KEY_RESTRICTION_MUST_BE_TOP_LEVEL = + "Restriction on partition key column %s must not be nested under OR operator"; + + public static final String GEO_DISTANCE_REQUIRES_INDEX_MESSAGE = "GEO_DISTANCE requires the vector column to be indexed"; + public static final String BM25_ORDERING_REQUIRES_ANALYZED_INDEX_MESSAGE = "BM25 ordering on column %s requires an analyzed index"; + public static final String BM25_ORDERING_REQUIRES_REGULAR_COLUMN_MESSAGE = "BM25 ordering on %s column %s is not supported. " + + "Only regular columns are supported."; + public static final String NON_CLUSTER_ORDERING_REQUIRES_INDEX_MESSAGE = + "Ordering on non-clustering column %s requires the column to be indexed with a non-analyzed index."; + public static final String NON_CLUSTER_ORDERING_REQUIRES_ALL_RESTRICTED_NON_PARTITION_KEY_COLUMNS_INDEXED_MESSAGE = + "Ordering on non-clustering column requires each restricted column to be indexed except for fully-specified partition keys"; + + public static final String VECTOR_INDEX_PRESENT_NOT_SUPPORT_GEO_DISTANCE_MESSAGE = + "Vector index present, but configuration does not support GEO_DISTANCE queries. GEO_DISTANCE requires similarity_function 'euclidean'"; + public static final String VECTOR_INDEXES_UNSUPPORTED_OP_MESSAGE = "Vector indexes only support ANN and GEO_DISTANCE queries"; + public static final String ANN_OPTIONS_WITHOUT_ORDER_BY_ANN = "ANN options specified without ORDER BY ... ANN OF ..."; + + public static final String INDEX_WITH_IN_ON_PK_MESSAGE = "Select on indexed columns and with IN clause for the PRIMARY KEY are not supported"; /** * The Column Family meta data */ public final TableMetadata table; + /** + * The index hints, needed to validate {@code ALLOW FILTERING}. + */ + protected final IndexHints indexHints; + /** * Restrictions on partitioning columns */ - private PartitionKeyRestrictions partitionKeyRestrictions; + protected final PartitionKeyRestrictions partitionKeyRestrictions; /** * Restrictions on clustering columns */ - private ClusteringColumnRestrictions clusteringColumnsRestrictions; + private final ClusteringColumnRestrictions clusteringColumnsRestrictions; /** * Restriction on non-primary key columns (i.e. secondary index restrictions) */ - private RestrictionSet nonPrimaryKeyRestrictions; + private final RestrictionSet nonPrimaryKeyRestrictions; - private Set notNullColumns; + private final ImmutableSet notNullColumns; /** * The restrictions used to build the row filter */ - private final IndexRestrictions filterRestrictions = new IndexRestrictions(); + private final IndexRestrictions filterRestrictions; + + /** + * true if these restrictions form part of an OR query, false otherwise + */ + private boolean isDisjunction; /** * true if the secondary index need to be queried, false otherwise */ - private boolean usesSecondaryIndexing; + protected boolean usesSecondaryIndexing; /** * Specify if the query will return a range of partition keys. */ - private boolean isKeyRange; + protected boolean isKeyRange; /** * true if nonPrimaryKeyRestrictions contains restriction on a regular column, @@ -125,238 +187,679 @@ public final class StatementRestrictions */ private boolean hasRegularColumnsRestrictions; + private final List children; + /** * Creates a new empty StatementRestrictions. * - * @param type the type of statement * @param table the column family meta data * @return a new empty StatementRestrictions. */ - public static StatementRestrictions empty(StatementType type, TableMetadata table) + public static StatementRestrictions empty(TableMetadata table) { - return new StatementRestrictions(type, table, false); + return new StatementRestrictions(table, IndexHints.NONE, false); } - private StatementRestrictions(StatementType type, TableMetadata table, boolean allowFiltering) + private StatementRestrictions(TableMetadata table, IndexHints indexHints, boolean allowFiltering) { - this.type = type; this.table = table; - this.partitionKeyRestrictions = new PartitionKeySingleRestrictionSet(table.partitionKeyAsClusteringComparator()); - this.clusteringColumnsRestrictions = new ClusteringColumnRestrictions(table, allowFiltering); - this.nonPrimaryKeyRestrictions = new RestrictionSet(); - this.notNullColumns = new HashSet<>(); + this.indexHints = indexHints; + this.partitionKeyRestrictions = PartitionKeySingleRestrictionSet.builder(table.partitionKeyAsClusteringComparator()) + .build(IndexRegistry.obtain(table)); + this.clusteringColumnsRestrictions = ClusteringColumnRestrictions.builder(table, allowFiltering).build(); + this.nonPrimaryKeyRestrictions = RestrictionSet.builder().build(); + this.notNullColumns = ImmutableSet.of(); + this.filterRestrictions = IndexRestrictions.of(); + this.children = Collections.emptyList(); } - public StatementRestrictions(ClientState state, - StatementType type, - TableMetadata table, - WhereClause whereClause, - VariableSpecifications boundNames, - List orderings, - boolean selectsOnlyStaticColumns, - boolean allowFiltering, - boolean forView) + /** + * Adds the following restrictions to the index restrictions. + * + * @param restrictions the restrictions to add to the index restrictions + * @return a new {@code StatementRestrictions} with the new index restrictions + */ + public StatementRestrictions addIndexRestrictions(Restrictions restrictions) { - this(state, type, table, whereClause, boundNames, orderings, selectsOnlyStaticColumns, type.allowUseOfSecondaryIndices(), allowFiltering, forView); + IndexRestrictions newIndexRestrictions = IndexRestrictions.builder() + .add(filterRestrictions) + .add(restrictions) + .build(); + + return new StatementRestrictions(table, + indexHints, + partitionKeyRestrictions, + clusteringColumnsRestrictions, + nonPrimaryKeyRestrictions, + notNullColumns, + isDisjunction, + usesSecondaryIndexing, + isKeyRange, + hasRegularColumnsRestrictions, + newIndexRestrictions, + children); } - /* - * We want to override allowUseOfSecondaryIndices flag from the StatementType for MV statements - * to avoid initing the Keyspace and SecondaryIndexManager. + /** + * Adds the following external restrictions (mostly custom and user index expressions) to the index restrictions. + * + * @param restrictions the restrictions to add to the index restrictions + * @return a new {@code StatementRestrictions} with the new index restrictions */ - public StatementRestrictions(ClientState state, - StatementType type, - TableMetadata table, - WhereClause whereClause, - VariableSpecifications boundNames, - List orderings, - boolean selectsOnlyStaticColumns, - boolean allowUseOfSecondaryIndices, - boolean allowFiltering, - boolean forView) + public StatementRestrictions addExternalRestrictions(Iterable restrictions) { - this(type, table, allowFiltering); - - final IndexRegistry indexRegistry = type.allowUseOfSecondaryIndices() ? IndexRegistry.obtain(table) : null; - - /* - * WHERE clause. For a given entity, rules are: - * - EQ relation conflicts with anything else (including a 2nd EQ) - * - Can't have more than one LT(E) relation (resp. GT(E) relation) - * - IN relation are restricted to row keys (for now) and conflicts with anything else (we could - * allow two IN for the same entity but that doesn't seem very useful) - * - The value_alias cannot be restricted in any way (we don't support wide rows with indexed value - * in CQL so far) - * - CONTAINS and CONTAINS_KEY cannot be used with UPDATE or DELETE - */ - for (Relation relation : whereClause.relations) - { - if ((relation.isContains() || relation.isContainsKey()) && (type.isUpdate() || type.isDelete())) - { - throw invalidRequest("Cannot use %s with %s", type, relation.operator()); - } + IndexRestrictions.Builder newIndexRestrictions = IndexRestrictions.builder().add(filterRestrictions); + + for (ExternalRestriction restriction : restrictions) + newIndexRestrictions.add(restriction); + + return new StatementRestrictions(table, + indexHints, + partitionKeyRestrictions, + clusteringColumnsRestrictions, + nonPrimaryKeyRestrictions, + notNullColumns, + isDisjunction, + usesSecondaryIndexing, + isKeyRange, + hasRegularColumnsRestrictions, + newIndexRestrictions.build(), + children); + } - if (relation.operator() == Operator.IS_NOT) - { - if (!forView) - throw new InvalidRequestException("Unsupported restriction: " + relation); + private StatementRestrictions(TableMetadata table, + IndexHints indexHints, + PartitionKeyRestrictions partitionKeyRestrictions, + ClusteringColumnRestrictions clusteringColumnsRestrictions, + RestrictionSet nonPrimaryKeyRestrictions, + ImmutableSet notNullColumns, + boolean isDisjunction, + boolean usesSecondaryIndexing, + boolean isKeyRange, + boolean hasRegularColumnsRestrictions, + IndexRestrictions filterRestrictions, + List children) + { + this.table = table; + this.indexHints = indexHints; + this.partitionKeyRestrictions = partitionKeyRestrictions; + this.clusteringColumnsRestrictions = clusteringColumnsRestrictions; + this.nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictions; + this.notNullColumns = notNullColumns; + this.filterRestrictions = filterRestrictions; + this.isDisjunction = isDisjunction; + this.usesSecondaryIndexing = usesSecondaryIndexing; + this.isKeyRange = isKeyRange; + this.hasRegularColumnsRestrictions = hasRegularColumnsRestrictions; + this.children = children; + } - this.notNullColumns.addAll(relation.toRestriction(table, boundNames).getColumnDefs()); - } - else if (relation.isLIKE()) - { - Restriction restriction = relation.toRestriction(table, boundNames); + public static StatementRestrictions create(ClientState state, + StatementType type, + TableMetadata table, + WhereClause whereClause, + VariableSpecifications boundNames, + List orderings, + IndexHints indexHints, + boolean selectsOnlyStaticColumns, + boolean allowFiltering, + boolean forView) + { + return new Builder(state, + type, + table, + whereClause, + boundNames, + orderings, + indexHints, + selectsOnlyStaticColumns, + type.allowUseOfSecondaryIndices(), + allowFiltering, + forView).build(); + } - if (!type.allowUseOfSecondaryIndices() || !restriction.hasSupportingIndex(indexRegistry)) - throw new InvalidRequestException(String.format("LIKE restriction is only supported on properly " + - "indexed columns. %s is not valid.", - relation)); + public static StatementRestrictions create(ClientState state, + StatementType type, + TableMetadata table, + WhereClause whereClause, + VariableSpecifications boundNames, + List orderings, + IndexHints indexHints, + boolean selectsOnlyStaticColumns, + boolean allowUseOfSecondaryIndices, + boolean allowFiltering, + boolean forView) + { + return new Builder(state, + type, + table, + whereClause, + boundNames, + orderings, + indexHints, + selectsOnlyStaticColumns, + allowUseOfSecondaryIndices, + allowFiltering, + forView).build(); + } - addRestriction(restriction, indexRegistry); - } - else - { - addRestriction(relation.toRestriction(table, boundNames), indexRegistry); - } + /** + * Build a StatementRestrictions from a WhereClause for a given + * StatementType, TableMetadata and VariableSpecifications + *

+ * The validation rules for whether the StatementRestrictions are valid depend on a + * number of considerations, including whether indexes are being used and whether filtering is being + * used. + */ + public static class Builder + { + private final ClientState state; + private final StatementType type; + private final TableMetadata table; + private final WhereClause whereClause; + private final VariableSpecifications boundNames; + + private final List orderings; + private final IndexHints indexHints; + private final boolean selectsOnlyStaticColumns; + private final boolean allowUseOfSecondaryIndices; + private final boolean allowFiltering; + private final boolean forView; + + public Builder(ClientState state, + StatementType type, + TableMetadata table, + WhereClause whereClause, + VariableSpecifications boundNames, + List orderings, + IndexHints indexHints, + boolean selectsOnlyStaticColumns, + boolean allowUseOfSecondaryIndices, + boolean allowFiltering, + boolean forView) + { + this.state = state; + this.type = type; + this.table = table; + this.whereClause = whereClause; + this.boundNames = boundNames; + this.orderings = orderings; + this.indexHints = indexHints; + this.selectsOnlyStaticColumns = selectsOnlyStaticColumns; + this.allowUseOfSecondaryIndices = allowUseOfSecondaryIndices; + this.allowFiltering = allowFiltering; + this.forView = forView; } - // ORDER BY clause. - // Some indexes can be used for ordering. - nonPrimaryKeyRestrictions = addOrderingRestrictions(orderings, nonPrimaryKeyRestrictions); + public StatementRestrictions build() + { + IndexRegistry indexRegistry = null; - hasRegularColumnsRestrictions = nonPrimaryKeyRestrictions.hasRestrictionFor(ColumnMetadata.Kind.REGULAR); + // We want to avoid opening the keyspace during view construction + // since we're parsing these for restore and the base table or keyspace might not exist in the current schema. + if (allowUseOfSecondaryIndices && type.allowUseOfSecondaryIndices()) + indexRegistry = IndexRegistry.obtain(table); - boolean hasQueriableClusteringColumnIndex = false; - boolean hasQueriableIndex = false; + WhereClause.AndElement root = whereClause.root().conjunctiveForm(); + return doBuild(root, indexRegistry, 0); + } - if (allowUseOfSecondaryIndices) + /** + * Processes the WHERE clause expression tree recursively and assigns the restrictions to different sets + * based on the columns they are applied to. + * + * @param element root of the tree + * @param indexRegistry the index registry for the queried table + * @param nestingLevel recursion depth needed to reject the restrictions that + * are not allowed to be nested (e.g. partition key restrictions) + */ + StatementRestrictions doBuild(WhereClause.ExpressionElement element, + IndexRegistry indexRegistry, + int nestingLevel) { - if (whereClause.containsCustomExpressions()) - processCustomIndexExpressions(whereClause.expressions, boundNames, indexRegistry); - - hasQueriableClusteringColumnIndex = clusteringColumnsRestrictions.hasSupportingIndex(indexRegistry); - hasQueriableIndex = !filterRestrictions.getCustomIndexExpressions().isEmpty() - || hasQueriableClusteringColumnIndex - || partitionKeyRestrictions.hasSupportingIndex(indexRegistry) - || nonPrimaryKeyRestrictions.hasSupportingIndex(indexRegistry); - } + assert element instanceof WhereClause.AndElement || nestingLevel > 0: + "Root of the WHERE clause expression tree must be a conjunction"; + + PartitionKeySingleRestrictionSet.Builder partitionKeyRestrictionSet = PartitionKeySingleRestrictionSet.builder(table.partitionKeyAsClusteringComparator()); + ClusteringColumnRestrictions.Builder clusteringColumnsRestrictionSet = ClusteringColumnRestrictions.builder(table, allowFiltering, indexRegistry, indexHints); + RestrictionSet.Builder nonPrimaryKeyRestrictionSet = RestrictionSet.builder(); + ImmutableSet.Builder notNullColumnsBuilder = ImmutableSet.builder(); + + + // ORDER BY clause. We add it first because orderings are not really restrictions + // and by adding first, we ensure that merging restrictions works as expected. + // The long term solution will break ordering out into its own abstraction. + if (nestingLevel == 0) + addOrderingRestrictions(orderings, indexRegistry, nonPrimaryKeyRestrictionSet); + + /* + * WHERE clause. For a given entity, rules are: + * - EQ relation conflicts with anything else (including a 2nd EQ) + * - Can't have more than one LT(E) relation (resp. GT(E) relation) + * - IN relation are restricted to row keys (for now) and conflicts with anything else (we could + * allow two IN for the same entity but that doesn't seem very useful) + * - The value_alias cannot be restricted in any way (we don't support wide rows with indexed value + * in CQL so far) + * - CONTAINS and CONTAINS_KEY cannot be used with UPDATE or DELETE + */ + for (Relation relation : element.relations()) + { + if ((relation.isContains() || relation.isContainsKey() || relation.isNotContains() || relation.isNotContainsKey()) + && (type.isUpdate() || type.isDelete())) + { + throw invalidRequest("Cannot use %s with %s", type, relation.operator()); + } - // At this point, the select statement if fully constructed, but we still have a few things to validate - processPartitionKeyRestrictions(state, hasQueriableIndex, allowFiltering, forView); + if (relation.operator() == Operator.IS_NOT) + { + if (!forView) + throw invalidRequest("Unsupported restriction: %s", relation); - // Some but not all of the partition key columns have been specified; - // hence we need turn these restrictions into a row filter. - if (usesSecondaryIndexing || partitionKeyRestrictions.needFiltering(table)) - filterRestrictions.add(partitionKeyRestrictions); + notNullColumnsBuilder.addAll(relation.toRestriction(table, boundNames, indexHints).getColumnDefs()); + } + else + { + Restriction restriction = relation.toRestriction(table, boundNames, indexHints); - if (selectsOnlyStaticColumns && hasClusteringColumnsRestrictions()) - { - // If the only updated/deleted columns are static, then we don't need clustering columns. - // And in fact, unless it is an INSERT, we reject if clustering colums are provided as that - // suggest something unintended. For instance, given: - // CREATE TABLE t (k int, v int, s int static, PRIMARY KEY (k, v)) - // it can make sense to do: - // INSERT INTO t(k, v, s) VALUES (0, 1, 2) - // but both - // UPDATE t SET s = 3 WHERE k = 0 AND v = 1 - // DELETE v FROM t WHERE k = 0 AND v = 1 - // sounds like you don't really understand what your are doing. - if (type.isDelete() || type.isUpdate()) - throw invalidRequest("Invalid restrictions on clustering columns since the %s statement modifies only static columns", - type); - if (type.isSelect()) - throw invalidRequest("Cannot restrict clustering columns when selecting only static columns"); - } + if (relation.isLIKE() && (!type.allowUseOfSecondaryIndices() || !restriction.hasSupportingIndex(indexRegistry, indexHints))) + { + if (getColumnsWithUnsupportedIndexRestrictions(table, indexHints, ImmutableList.of(restriction)).isEmpty()) + { + throw invalidRequest(RESTRICTION_REQUIRES_INDEX_MESSAGE, relation.operator(), relation.toString()); + } + else + { + throw invalidRequest(StatementRestrictions.INDEX_DOES_NOT_SUPPORT_LIKE_MESSAGE, restriction.getFirstColumn()); + } + } + if (relation.operator() == Operator.ANALYZER_MATCHES) + { + if (!type.allowUseOfSecondaryIndices()) + { + throw invalidRequest("Invalid query. %s does not support use of secondary indices, but %s restriction requires a secondary index.", type.name(), relation.toString()); + } + if (!restriction.hasSupportingIndex(indexRegistry, indexHints)) + { + if (getColumnsWithUnsupportedIndexRestrictions(table, indexHints, ImmutableList.of(restriction)).isEmpty()) + { + throw invalidRequest(RESTRICTION_REQUIRES_INDEX_MESSAGE, relation.operator(), relation.toString()); + } + else + { + throw invalidRequest(StatementRestrictions.INDEX_DOES_NOT_SUPPORT_ANALYZER_MATCHES_MESSAGE, restriction.getFirstColumn()); + } + } + } + + ColumnMetadata def = restriction.getFirstColumn(); + if (def.isPartitionKey()) + { + // All partition key restrictions must be a part of the top-level AND operation. + // The read path filtering logic is currently unable to filter rows based on + // partition key restriction that is a part of a complex expression involving disjunctions. + // ALLOW FILTERING does not cut it, as RowFilter can't handle ORed partition + // key restrictions properly. + if (nestingLevel > 0) + throw invalidRequest(StatementRestrictions.PARTITION_KEY_RESTRICTION_MUST_BE_TOP_LEVEL, def); + + partitionKeyRestrictionSet.addRestriction(restriction); + } + // If a clustering column restriction is nested (under OR operator), + // we can't treat it as a real clustering column, + // but instead we treat it as a regular column and use + // index (if we have one) or use row filtering on it; hence we require nestingLevel == 0 check here + else if (def.isClusteringColumn() && nestingLevel == 0) + { + // If a clustering column restriction is nested (under OR operator), + // we can't treat it as a real clustering column, + // but instead we treat it as a regular column and use + // index (if we have one) or use row filtering on it; hence we require nestingLevel == 0 check here + clusteringColumnsRestrictionSet.addRestriction(restriction); + } + else + { + nonPrimaryKeyRestrictionSet.addRestriction((SingleRestriction) restriction, element.isDisjunction()); + } + } + } - processClusteringColumnsRestrictions(hasQueriableIndex, - selectsOnlyStaticColumns, - forView, - allowFiltering); + PartitionKeyRestrictions partitionKeyRestrictions = partitionKeyRestrictionSet.build(indexRegistry); + ClusteringColumnRestrictions clusteringColumnsRestrictions = clusteringColumnsRestrictionSet.build(); + RestrictionSet nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictionSet.build(); + ImmutableSet notNullColumns = notNullColumnsBuilder.build(); + boolean hasRegularColumnsRestrictions = nonPrimaryKeyRestrictions.hasRestrictionFor(ColumnMetadata.Kind.REGULAR); + boolean usesSecondaryIndexing = false; + boolean isKeyRange = false; - // Covers indexes on the first clustering column (among others). - if (isKeyRange && hasQueriableClusteringColumnIndex) - usesSecondaryIndexing = true; + boolean hasQueryableClusteringColumnIndex = false; + boolean hasQueryableIndex = false; - if (usesSecondaryIndexing || clusteringColumnsRestrictions.needFiltering()) - filterRestrictions.add(clusteringColumnsRestrictions); + IndexRestrictions.Builder filterRestrictionsBuilder = IndexRestrictions.builder(); - // Even if usesSecondaryIndexing is false at this point, we'll still have to use one if - // there is restrictions not covered by the PK. - if (!nonPrimaryKeyRestrictions.isEmpty()) - { - if (!type.allowNonPrimaryKeyInWhereClause()) + if (allowUseOfSecondaryIndices) { - Collection nonPrimaryKeyColumns = - ColumnMetadata.toIdentifiers(nonPrimaryKeyRestrictions.getColumnDefs()); + if (element.containsCustomExpressions()) + { + CustomIndexExpression customExpression = prepareCustomIndexExpression(element.expressions(), + boundNames, + indexRegistry); + filterRestrictionsBuilder.add(customExpression); + } - throw invalidRequest("Non PRIMARY KEY columns found in where clause: %s ", - Joiner.on(", ").join(nonPrimaryKeyColumns)); + hasQueryableClusteringColumnIndex = clusteringColumnsRestrictions.hasSupportingIndex(indexRegistry, indexHints); + hasQueryableIndex = element.containsCustomExpressions() + || hasQueryableClusteringColumnIndex + || partitionKeyRestrictions.hasSupportingIndex(indexRegistry, indexHints) + || nonPrimaryKeyRestrictions.hasSupportingIndex(indexRegistry, indexHints); } - Optional annRestriction = Streams.stream(nonPrimaryKeyRestrictions).filter(SingleRestriction::isANN).findFirst(); - if (annRestriction.isPresent()) + // At this point, the select statement if fully constructed, but we still have a few things to validate + if (!type.allowPartitionKeyRanges()) { - // If there is an ANN restriction then it must be for a vector column, and it must have an index - ColumnMetadata annColumn = annRestriction.get().getFirstColumn(); - - if (!annColumn.type.isVector() || !(((VectorType)annColumn.type).elementType instanceof FloatType)) - throw invalidRequest(StatementRestrictions.ANN_ONLY_SUPPORTED_ON_VECTOR_MESSAGE); - if (indexRegistry == null || indexRegistry.listIndexes().stream().noneMatch(i -> i.dependsOn(annColumn))) - throw invalidRequest(StatementRestrictions.ANN_REQUIRES_INDEX_MESSAGE); - // We do not allow ANN queries using partition key restrictions that need filtering - if (partitionKeyRestrictions.needFiltering(table)) - throw invalidRequest(StatementRestrictions.ANN_REQUIRES_INDEXED_FILTERING_MESSAGE); - // We do not allow ANN query filtering using non-indexed columns - List nonAnnColumns = Streams.stream(nonPrimaryKeyRestrictions) - .filter(r -> !r.isANN()) - .map(Restriction::getFirstColumn) - .collect(Collectors.toList()); - Collection clusteringColumns = clusteringColumnsRestrictions.getColumnDefinitions(); - if (!nonAnnColumns.isEmpty() || !clusteringColumns.isEmpty()) + checkFalse(partitionKeyRestrictions.isOnToken(), + "The token function cannot be used in WHERE clauses for %s statements", type); + + if (partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(table)) + throw invalidRequest("Some partition key parts are missing: %s", + Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents(partitionKeyRestrictions))); + + // slice query + checkFalse(partitionKeyRestrictions.hasSlice(), + "Only EQ and IN relation are supported on the partition key (unless you use the token() function)" + + " for %s statements", type); + } + else + { + // If there are no partition restrictions or there's only token restriction, we have to set a key range + if (partitionKeyRestrictions.isOnToken()) + isKeyRange = true; + + if (partitionKeyRestrictions.isEmpty() && partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(table)) { - List nonIndexedColumns = Stream.concat(nonAnnColumns.stream(), clusteringColumns.stream()) - .filter(c -> indexRegistry.listIndexes().stream().noneMatch(i -> i.dependsOn(c))) - .collect(Collectors.toList()); + isKeyRange = true; + usesSecondaryIndexing = hasQueryableIndex; + } - if (!nonIndexedColumns.isEmpty()) - { - // restrictions on non-clustering columns, or clusterings that still need filtering, are invalid - if (!clusteringColumns.containsAll(nonIndexedColumns) - || partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(table) - || clusteringColumnsRestrictions.needFiltering()) - throw invalidRequest(StatementRestrictions.ANN_REQUIRES_INDEXED_FILTERING_MESSAGE); - } + // If there is a queryable index, no special condition is required on the other restrictions. + // But we still need to know 2 things: + // - If we don't have a queryable index, is the query ok + // - Is it queryable without 2ndary index, which is always more efficient + // If a component of the partition key is restricted by a relation, all preceding + // components must have a EQ. Only the last partition key component can be in IN relation. + // If partition key restrictions exist and this is a disjunction then we may need filtering + if (partitionKeyRestrictions.needFiltering(table) || (!partitionKeyRestrictions.isEmpty() && element.isDisjunction())) + { + if (!allowFiltering && !forView && !hasQueryableIndex && requiresAllowFilteringIfNotSpecified(table)) + throw new InvalidRequestException(allowFilteringMessage(state)); + + isKeyRange = true; + usesSecondaryIndexing = hasQueryableIndex; } } - else + + // Some but not all of the partition key columns have been specified or they form part of a disjunction; + // hence we need turn these restrictions into a row filter. + if (usesSecondaryIndexing || partitionKeyRestrictions.needFiltering(table) || element.isDisjunction()) + filterRestrictionsBuilder.add(partitionKeyRestrictions); + + if (selectsOnlyStaticColumns && !clusteringColumnsRestrictions.isEmpty()) { - // We do not support indexed vector restrictions that are not part of an ANN ordering - Optional vectorColumn = nonPrimaryKeyRestrictions.getColumnDefs() - .stream() - .filter(c -> c.type.isVector()) - .findFirst(); - if (vectorColumn.isPresent() && indexRegistry.listIndexes().stream().anyMatch(i -> i.dependsOn(vectorColumn.get()))) - throw invalidRequest(StatementRestrictions.VECTOR_INDEXES_ANN_ONLY_MESSAGE); + // If the only updated/deleted columns are static, then we don't need clustering columns. + // And in fact, unless it is an INSERT, we reject if clustering colums are provided as that + // suggest something unintended. For instance, given: + // CREATE TABLE t (k int, v int, s int static, PRIMARY KEY (k, v)) + // it can make sense to do: + // INSERT INTO t(k, v, s) VALUES (0, 1, 2) + // but both + // UPDATE t SET s = 3 WHERE k = 0 AND v = 1 + // DELETE v FROM t WHERE k = 0 AND v = 1 + // sounds like you don't really understand what you are doing. + if (type.isDelete() || type.isUpdate()) + throw invalidRequest("Invalid restrictions on clustering columns since the %s statement modifies only static columns", + type); } - if (hasQueriableIndex) + // Now process and validate the clustering column restrictions + checkFalse(!type.allowClusteringColumnSlices() && clusteringColumnsRestrictions.hasSlice(), + "Slice restrictions are not supported on the clustering columns in %s statements", type); + + if (!type.allowClusteringColumnSlices() + && (!table.isCompactTable() || (table.isCompactTable() && clusteringColumnsRestrictions.isEmpty()))) { - usesSecondaryIndexing = true; + if (!selectsOnlyStaticColumns && (table.clusteringColumns().size() != clusteringColumnsRestrictions.size())) + throw invalidRequest("Some clustering keys are missing: %s", + Joiner.on(", ").join(getUnrestrictedClusteringColumns(clusteringColumnsRestrictions))); } else { - if (!allowFiltering && requiresAllowFilteringIfNotSpecified()) - throw invalidRequest(allowFilteringMessage(state)); + checkFalse(clusteringColumnsRestrictions.hasContains() && !hasQueryableIndex && !allowFiltering, + "Clustering columns can only be restricted with CONTAINS with a secondary index or filtering"); + + if (!clusteringColumnsRestrictions.isEmpty() && clusteringColumnsRestrictions.needFiltering()) + { + if (hasQueryableIndex || forView) + { + usesSecondaryIndexing = true; + } + else if (!allowFiltering) + { + List clusteringColumns = table.clusteringColumns(); + List restrictedColumns = clusteringColumnsRestrictions.getColumnDefs(); + + for (int i = 0, m = restrictedColumns.size(); i < m; i++) + { + ColumnMetadata clusteringColumn = clusteringColumns.get(i); + ColumnMetadata restrictedColumn = restrictedColumns.get(i); + + if (!clusteringColumn.equals(restrictedColumn)) + { + throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted", + restrictedColumn.name, + clusteringColumn.name); + } + } + } + } + } + + // Covers indexes on the first clustering column (among others). + if (isKeyRange && hasQueryableClusteringColumnIndex) + usesSecondaryIndexing = true; + + // Because an ANN queries limit the result set based within the SAI, clustering column restrictions + // must be added to the filter restrictions. + if (orderings.stream().anyMatch(o -> o.expression.hasNonClusteredOrdering())) + usesSecondaryIndexing = true; + + if (usesSecondaryIndexing || clusteringColumnsRestrictions.needFiltering()) + filterRestrictionsBuilder.add(clusteringColumnsRestrictions); + + // Even if usesSecondaryIndexing is false at this point, we'll still have to use one if + // there is restrictions not covered by the PK. + if (!nonPrimaryKeyRestrictions.isEmpty()) + { + Iterable columnRestrictions = allColumnRestrictions(clusteringColumnsRestrictions, nonPrimaryKeyRestrictions); + + if (!type.allowNonPrimaryKeyInWhereClause()) + { + Collection nonPrimaryKeyColumns = + ColumnMetadata.toIdentifiers(nonPrimaryKeyRestrictions.getColumnDefs()); + + throw invalidRequest("Non PRIMARY KEY columns found in where clause: %s ", + Joiner.on(", ").join(nonPrimaryKeyColumns)); + } + if (hasQueryableIndex) + usesSecondaryIndexing = true; + else + { + Optional vectorColumn = nonPrimaryKeyRestrictions.getColumnDefs().stream().filter(c -> c.type.isVector()).findFirst(); + if (vectorColumn.isPresent()) + { + var vc = vectorColumn.get(); + var hasIndex = indexRegistry.listNotExcludedIndexes(indexHints).stream().anyMatch(i -> i.dependsOn(vc)); + var isBoundedANN = nonPrimaryKeyRestrictions.restrictions().stream().anyMatch(SingleRestriction::isBoundedAnn); + var isIndexBasedOrdering = nonPrimaryKeyRestrictions.restrictions().stream().anyMatch(SingleRestriction::isIndexBasedOrdering); + if (hasIndex) + { + if (isBoundedANN) + throw invalidRequest(StatementRestrictions.VECTOR_INDEX_PRESENT_NOT_SUPPORT_GEO_DISTANCE_MESSAGE); + else + throw invalidRequest(StatementRestrictions.VECTOR_INDEXES_UNSUPPORTED_OP_MESSAGE, vc); + } + else + { + // We check if ANN vector column has index earlier, so we only need to for bounded ann here + if (isBoundedANN) + throw invalidRequest(StatementRestrictions.GEO_DISTANCE_REQUIRES_INDEX_MESSAGE); + else if (isIndexBasedOrdering) + throw invalidRequest(StatementRestrictions.NON_CLUSTER_ORDERING_REQUIRES_INDEX_MESSAGE); + } + } + + if (!allowFiltering) + throwRequiresAllowFilteringError(table, indexHints, columnRestrictions, state); + } + filterRestrictionsBuilder.add(nonPrimaryKeyRestrictions); + } + + if (usesSecondaryIndexing) + checkFalse(partitionKeyRestrictions.hasIN(), INDEX_WITH_IN_ON_PK_MESSAGE); + + ImmutableList.Builder children = ImmutableList.builder(); + + for (WhereClause.ContainerElement container : element.operations()) + children.add(doBuild(container, indexRegistry, nestingLevel + 1)); + + return new StatementRestrictions(table, + indexHints, + partitionKeyRestrictions, + clusteringColumnsRestrictions, + nonPrimaryKeyRestrictions, + notNullColumns, + element.isDisjunction(), + usesSecondaryIndexing, + isKeyRange, + hasRegularColumnsRestrictions, + filterRestrictionsBuilder.build(), + children.build()); + } + + /** + * This is a hack to push ordering down to indexes. + * Indexes are selected based on RowFilter only, so we need to turn orderings into restrictions + * so they end up in the row filter. + * + * @param orderings orderings from the select statement + * @param indexRegistry used to check if the ordering is supported by an index + * @param receiver target restriction builder to receive the additional restrictions + */ + private void addOrderingRestrictions(List orderings, IndexRegistry indexRegistry, RestrictionSet.Builder receiver) + { + List indexOrderings = orderings.stream().filter(o -> o.expression.hasNonClusteredOrdering()).collect(Collectors.toList()); + + if (indexOrderings.size() > 1) + throw new InvalidRequestException("Cannot specify more than one ordering column when using SAI indexes"); + else if (indexOrderings.size() == 1) + { + if (orderings.size() > 1) + throw new InvalidRequestException("Cannot combine clustering column ordering with non-clustering column ordering"); + Ordering ordering = indexOrderings.get(0); + // TODO remove the instanceof with Ordering.Ann.USE_SYNTHETIC_SCORE. + if (ordering.direction != Ordering.Direction.ASC && (ordering.expression.isScored() || ordering.expression instanceof Ordering.Ann)) + throw new InvalidRequestException("Descending ANN ordering is not supported"); + if (!ENABLE_SAI_GENERAL_ORDER_BY && ordering.expression instanceof Ordering.SingleColumn) + throw new InvalidRequestException("SAI based ORDER BY on non-vector column is not supported"); + + SingleRestriction restriction = ordering.expression.toRestriction(); + + ColumnMetadata column = restriction.getFirstColumn(); + + if (!restriction.hasSupportingIndex(indexRegistry, indexHints)) + { + var type = column.type.asCQL3Type().getType(); + // This is a slight hack, but once we support a way to order these types, we can remove it. + if (type instanceof IntegerType || type instanceof DecimalType) + throw new InvalidRequestException(String.format("SAI based ordering on column %s of type %s is not supported", + column, + column.type.asCQL3Type())); + if (ordering.expression instanceof Ordering.Bm25) + throw new InvalidRequestException(String.format(BM25_ORDERING_REQUIRES_ANALYZED_INDEX_MESSAGE, column)); + else + throw new InvalidRequestException(String.format(NON_CLUSTER_ORDERING_REQUIRES_INDEX_MESSAGE, column)); + } + + if (ordering.expression instanceof Ordering.Bm25 && !column.isRegular()) + throw new InvalidRequestException(String.format(BM25_ORDERING_REQUIRES_REGULAR_COLUMN_MESSAGE, + column.kind.name().toLowerCase().replace("_", " "), + column.name)); + + receiver.addRestriction(restriction, false); } + } + + private CustomIndexExpression prepareCustomIndexExpression(List expressions, + VariableSpecifications boundNames, + IndexRegistry indexRegistry) + { + if (expressions.size() > 1) + throw new InvalidRequestException(IndexRestrictions.MULTIPLE_EXPRESSIONS); + + CustomIndexExpression expression = expressions.get(0); + + QualifiedName name = expression.targetIndex; + + if (name.hasKeyspace() && !name.getKeyspace().equals(table.keyspace)) + throw IndexRestrictions.invalidIndex(expression.targetIndex, table); + + if (!table.indexes.has(expression.targetIndex.getName())) + throw IndexRestrictions.indexNotFound(expression.targetIndex, table); + + Index index = indexRegistry.getIndex(table.indexes.get(expression.targetIndex.getName()).orElseThrow()); + if (!index.getIndexMetadata().isCustom()) + throw IndexRestrictions.nonCustomIndexInExpression(expression.targetIndex); + + AbstractType expressionType = index.customExpressionValueType(); + if (expressionType == null) + throw IndexRestrictions.customExpressionNotSupported(expression.targetIndex); - filterRestrictions.add(nonPrimaryKeyRestrictions); + expression.prepareValue(table, expressionType, boundNames); + return expression; } - if (usesSecondaryIndexing) - validateSecondaryIndexSelections(); + /** + * Returns the partition key components that are not restricted. + * @return the partition key components that are not restricted. + */ + private Collection getPartitionKeyUnrestrictedComponents(PartitionKeyRestrictions partitionKeyRestrictions) + { + List list = new ArrayList<>(table.partitionKeyColumns()); + list.removeAll(partitionKeyRestrictions.getColumnDefs()); + return ColumnMetadata.toIdentifiers(list); + } + + /** + * Returns the clustering columns that are not restricted. + * @return the clustering columns that are not restricted. + */ + private Collection getUnrestrictedClusteringColumns(ClusteringColumnRestrictions clusteringColumnsRestrictions) + { + List missingClusteringColumns = new ArrayList<>(table.clusteringColumns()); + missingClusteringColumns.removeAll(clusteringColumnsRestrictions.getColumnDefs()); + return ColumnMetadata.toIdentifiers(missingClusteringColumns); + } } - public boolean requiresAllowFilteringIfNotSpecified() + public IndexRestrictions filterRestrictions() + { + return filterRestrictions; + } + + public List children() + { + return children; + } + + public static boolean requiresAllowFilteringIfNotSpecified(TableMetadata table) { if (!table.isVirtual()) return true; @@ -366,15 +869,40 @@ public boolean requiresAllowFilteringIfNotSpecified() return !tableNullable.allowFilteringImplicitly(); } - private void addRestriction(Restriction restriction, IndexRegistry indexRegistry) + public boolean hasIndxBasedOrdering() { - ColumnMetadata def = restriction.getFirstColumn(); - if (def.isPartitionKey()) - partitionKeyRestrictions = partitionKeyRestrictions.mergeWith(restriction); - else if (def.isClusteringColumn()) - clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction, indexRegistry); + return nonPrimaryKeyRestrictions.restrictions().stream().anyMatch(SingleRestriction::isIndexBasedOrdering); + } + + public void throwRequiresAllowFilteringError(TableMetadata table, ClientState state) + { + if (hasIndxBasedOrdering()) + throw invalidRequest(StatementRestrictions.NON_CLUSTER_ORDERING_REQUIRES_ALL_RESTRICTED_NON_PARTITION_KEY_COLUMNS_INDEXED_MESSAGE); + + throwRequiresAllowFilteringError(table, indexHints, allColumnRestrictions(clusteringColumnsRestrictions, nonPrimaryKeyRestrictions), state); + } + + private static void throwRequiresAllowFilteringError(TableMetadata table, IndexHints indexHints, Iterable columnRestrictions, ClientState state) + { + Set unsupported = getColumnsWithUnsupportedIndexRestrictions(table, indexHints, columnRestrictions); + if (unsupported.isEmpty()) + { + if (requiresAllowFilteringIfNotSpecified(table)) + throw invalidRequest(allowFilteringMessage(state)); + } else - nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictions.addRestriction((SingleRestriction) restriction); + { + // If there's an index on these columns but the restriction is not supported on this index, throw a more specific error message + if (unsupported.size() == 1) + throw invalidRequest(String.format(StatementRestrictions.HAS_UNSUPPORTED_INDEX_RESTRICTION_MESSAGE_SINGLE, unsupported.iterator().next())); + else + throw invalidRequest(String.format(StatementRestrictions.HAS_UNSUPPORTED_INDEX_RESTRICTION_MESSAGE_MULTI, unsupported)); + } + } + + public void throwsRequiresIndexSupportingDisjunctionError() + { + throw invalidRequest(StatementRestrictions.INDEX_DOES_NOT_SUPPORT_DISJUNCTION); } public void addFunctionsTo(List functions) @@ -382,6 +910,9 @@ public void addFunctionsTo(List functions) partitionKeyRestrictions.addFunctionsTo(functions); clusteringColumnsRestrictions.addFunctionsTo(functions); nonPrimaryKeyRestrictions.addFunctionsTo(functions); + + for (StatementRestrictions child : children) + child.addFunctionsTo(functions); } // may be used by QueryHandler implementations @@ -407,20 +938,23 @@ public Set nonPKRestrictedColumns(boolean includeNotNullRestrict if (includeNotNullRestrictions) { - for (ColumnMetadata def : notNullColumns) + for (ColumnMetadata def : notNullColumns()) { if (!def.isPrimaryKeyColumn()) columns.add(def); } } + for (StatementRestrictions child : children) + columns.addAll(child.nonPKRestrictedColumns(includeNotNullRestrictions)); + return columns; } /** * @return the set of columns that have an IS NOT NULL restriction on them */ - public Set notNullColumns() + public ImmutableSet notNullColumns() { return notNullColumns; } @@ -430,10 +964,17 @@ public Set notNullColumns() */ public boolean isRestricted(ColumnMetadata column) { - if (notNullColumns.contains(column)) + if (notNullColumns().contains(column)) + return true; + + if (getRestrictions(column.kind).getColumnDefs().contains(column)) return true; - return getRestrictions(column.kind).getColumnDefs().contains(column); + for (StatementRestrictions child : children) + if (child.isRestricted(column)) + return true; + + return false; } /** @@ -454,7 +995,7 @@ public boolean keyIsInRelation() */ public boolean isKeyRange() { - return this.isKeyRange; + return isKeyRange; } /** @@ -494,7 +1035,7 @@ else if (column.kind == ColumnMetadata.Kind.CLUSTERING) { if (hasClusteringColumnsRestrictions()) { - for (SingleRestriction restriction : clusteringColumnsRestrictions.getRestrictionSet()) + for (SingleRestriction restriction : clusteringColumnsRestrictions.restrictions()) { if (restriction.isEqualityBased()) { @@ -512,7 +1053,7 @@ else if (restriction.getFirstColumn().name.equals(column.name)) } else if (hasNonPrimaryKeyRestrictions()) { - for (SingleRestriction restriction : nonPrimaryKeyRestrictions) + for (SingleRestriction restriction : nonPrimaryKeyRestrictions.restrictions()) if (restriction.getFirstColumn().name.equals(column.name) && restriction.isEqualityBased()) return true; } @@ -520,17 +1061,13 @@ else if (hasNonPrimaryKeyRestrictions()) return false; } - public boolean isTopK() - { - return nonPrimaryKeyRestrictions.hasAnn(); - } /** * Returns the Restrictions for the specified type of columns. * * @param kind the column type * @return the Restrictions for the specified type of columns */ - private Restrictions getRestrictions(ColumnMetadata.Kind kind) + protected Restrictions getRestrictions(ColumnMetadata.Kind kind) { switch (kind) { @@ -547,84 +1084,14 @@ private Restrictions getRestrictions(ColumnMetadata.Kind kind) */ public boolean usesSecondaryIndexing() { - return this.usesSecondaryIndexing; - } - - /** - * This is a hack to push ordering down to indexes. - * Indexes are selected based on RowFilter only, so we need to turn orderings into restrictions - * so they end up in the row filter. - * - * @param orderings orderings from the select statement - * @return the {@link RestrictionSet} with the added orderings - */ - private RestrictionSet addOrderingRestrictions(List orderings, RestrictionSet restrictionSet) - { - List annOrderings = orderings.stream().filter(o -> o.expression.hasNonClusteredOrdering()).collect(Collectors.toList()); - - if (annOrderings.size() > 1) - throw new InvalidRequestException("Cannot specify more than one ANN ordering"); - else if (annOrderings.size() == 1) - { - if (orderings.size() > 1) - throw new InvalidRequestException("ANN ordering does not support any other ordering"); - Ordering annOrdering = annOrderings.get(0); - if (annOrdering.direction != Ordering.Direction.ASC) - throw new InvalidRequestException("Descending ANN ordering is not supported"); - SingleRestriction restriction = annOrdering.expression.toRestriction(); - return restrictionSet.addRestriction(restriction); - } - return restrictionSet; - } - - private static Iterable allColumnRestrictions(ClusteringColumnRestrictions clusteringColumnsRestrictions, RestrictionSet nonPrimaryKeyRestrictions) - { - return Iterables.concat(clusteringColumnsRestrictions.getRestrictionSet(), nonPrimaryKeyRestrictions); - } - - private void processPartitionKeyRestrictions(ClientState state, boolean hasQueriableIndex, boolean allowFiltering, boolean forView) - { - if (!type.allowPartitionKeyRanges()) - { - checkFalse(partitionKeyRestrictions.isOnToken(), - "The token function cannot be used in WHERE clauses for %s statements", type); - - if (partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(table)) - throw invalidRequest("Some partition key parts are missing: %s", - Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents())); - - // slice query - checkFalse(partitionKeyRestrictions.hasSlice(), - "Only EQ and IN relation are supported on the partition key (unless you use the token() function)" - + " for %s statements", type); - } - else - { - // If there are no partition restrictions or there's only token restriction, we have to set a key range - if (partitionKeyRestrictions.isOnToken()) - isKeyRange = true; - - if (partitionKeyRestrictions.isEmpty() && partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(table)) - { - isKeyRange = true; - usesSecondaryIndexing = hasQueriableIndex; - } + if (usesSecondaryIndexing) + return true; - // If there is a queriable index, no special condition is required on the other restrictions. - // But we still need to know 2 things: - // - If we don't have a queriable index, is the query ok - // - Is it queriable without 2ndary index, which is always more efficient - // If a component of the partition key is restricted by a relation, all preceding - // components must have a EQ. Only the last partition key component can be in IN relation. - if (partitionKeyRestrictions.needFiltering(table)) - { - if (!allowFiltering && !forView && !hasQueriableIndex && requiresAllowFilteringIfNotSpecified()) - throw new InvalidRequestException(allowFilteringMessage(state)); + for (StatementRestrictions child: children) + if (child.usesSecondaryIndexing) + return true; - isKeyRange = true; - usesSecondaryIndexing = hasQueriableIndex; - } - } + return false; } public boolean hasPartitionKeyRestrictions() @@ -641,17 +1108,6 @@ public boolean hasNonPrimaryKeyRestrictions() return !nonPrimaryKeyRestrictions.isEmpty(); } - /** - * Returns the partition key components that are not restricted. - * @return the partition key components that are not restricted. - */ - private Collection getPartitionKeyUnrestrictedComponents() - { - List list = new ArrayList<>(table.partitionKeyColumns()); - list.removeAll(partitionKeyRestrictions.getColumnDefs()); - return ColumnMetadata.toIdentifiers(list); - } - /** * Checks if the restrictions on the partition key are token restrictions. * @@ -674,74 +1130,6 @@ public boolean clusteringKeyRestrictionsHasIN() return clusteringColumnsRestrictions.hasIN(); } - /** - * Processes the clustering column restrictions. - * - * @param hasQueriableIndex true if some of the queried data are indexed, false otherwise - * @param selectsOnlyStaticColumns true if the selected or modified columns are all statics, - * false otherwise. - */ - private void processClusteringColumnsRestrictions(boolean hasQueriableIndex, - boolean selectsOnlyStaticColumns, - boolean forView, - boolean allowFiltering) - { - checkFalse(!type.allowClusteringColumnSlices() && clusteringColumnsRestrictions.hasSlice(), - "Slice restrictions are not supported on the clustering columns in %s statements", type); - - if (!type.allowClusteringColumnSlices() - && (!table.isCompactTable() || (table.isCompactTable() && !hasClusteringColumnsRestrictions()))) - { - if (!selectsOnlyStaticColumns && hasUnrestrictedClusteringColumns()) - throw invalidRequest("Some clustering keys are missing: %s", - Joiner.on(", ").join(getUnrestrictedClusteringColumns())); - } - else - { - checkFalse(clusteringColumnsRestrictions.hasContains() && !hasQueriableIndex && !allowFiltering, - "Clustering columns can only be restricted with CONTAINS with a secondary index or filtering"); - - if (hasClusteringColumnsRestrictions() && clusteringColumnsRestrictions.needFiltering()) - { - if (hasQueriableIndex || forView) - { - usesSecondaryIndexing = true; - } - else if (!allowFiltering) - { - List clusteringColumns = table.clusteringColumns(); - List restrictedColumns = new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs()); - - for (int i = 0, m = restrictedColumns.size(); i < m; i++) - { - ColumnMetadata clusteringColumn = clusteringColumns.get(i); - ColumnMetadata restrictedColumn = restrictedColumns.get(i); - - if (!clusteringColumn.equals(restrictedColumn)) - { - throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted", - restrictedColumn.name, - clusteringColumn.name); - } - } - } - } - - } - - } - - /** - * Returns the clustering columns that are not restricted. - * @return the clustering columns that are not restricted. - */ - private Collection getUnrestrictedClusteringColumns() - { - List missingClusteringColumns = new ArrayList<>(table.clusteringColumns()); - missingClusteringColumns.removeAll(new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs())); - return ColumnMetadata.toIdentifiers(missingClusteringColumns); - } - /** * Checks if some clustering columns are not restricted. * @return true if some clustering columns are not restricted, false otherwise. @@ -751,54 +1139,32 @@ private boolean hasUnrestrictedClusteringColumns() return table.clusteringColumns().size() != clusteringColumnsRestrictions.size(); } - private void processCustomIndexExpressions(List expressions, - VariableSpecifications boundNames, - IndexRegistry indexRegistry) + public RowFilter getRowFilter(IndexRegistry indexRegistry, QueryOptions options, ClientState clientState, SelectOptions selectOptions) { - if (expressions.size() > 1) - throw new InvalidRequestException(IndexRestrictions.MULTIPLE_EXPRESSIONS); - - CustomIndexExpression expression = expressions.get(0); - - QualifiedName name = expression.targetIndex; - - if (name.hasKeyspace() && !name.getKeyspace().equals(table.keyspace)) - throw IndexRestrictions.invalidIndex(expression.targetIndex, table); - - if (!table.indexes.has(expression.targetIndex.getName())) - throw IndexRestrictions.indexNotFound(expression.targetIndex, table); + boolean hasAnnOptions = selectOptions.hasANNOptions(); - Index index = indexRegistry.getIndex(table.indexes.get(expression.targetIndex.getName()).get()); - if (!index.getIndexMetadata().isCustom()) - throw IndexRestrictions.nonCustomIndexInExpression(expression.targetIndex); - - AbstractType expressionType = index.customExpressionValueType(); - if (expressionType == null) - throw IndexRestrictions.customExpressionNotSupported(expression.targetIndex); - - expression.prepareValue(table, expressionType, boundNames); - - filterRestrictions.add(expression); - } + if (filterRestrictions.isEmpty() && children.isEmpty()) + { + if (hasAnnOptions) + throw new InvalidRequestException(ANN_OPTIONS_WITHOUT_ORDER_BY_ANN); - public RowFilter getRowFilter(IndexRegistry indexRegistry, QueryOptions options) - { - if (filterRestrictions.isEmpty()) return RowFilter.none(); + } // If there is only one replica, we don't need reconciliation at any consistency level. boolean needsReconciliation = !table.isVirtual() && options.getConsistency().needsReconciliation() && Keyspace.open(table.keyspace).getReplicationStrategy().getReplicationFactor().allReplicas > 1; - RowFilter filter = RowFilter.create(needsReconciliation); - for (Restrictions restrictions : filterRestrictions.getRestrictions()) - restrictions.addToRowFilter(filter, indexRegistry, options); + ANNOptions annOptions = selectOptions.parseANNOptions(); + + RowFilter.Builder filterBuilder = new RowFilter.Builder(needsReconciliation, indexRegistry, indexHints); + RowFilter rowFilter = filterBuilder.buildFromRestrictions(this, table, options, clientState, annOptions); - for (CustomIndexExpression expression : filterRestrictions.getCustomIndexExpressions()) - expression.addToRowFilter(filter, table, options); + if (hasAnnOptions && !rowFilter.hasANN()) + throw new InvalidRequestException(ANN_OPTIONS_WITHOUT_ORDER_BY_ANN); - return filter; + return rowFilter; } /** @@ -849,7 +1215,7 @@ private AbstractBounds getPartitionKeyBounds(IPartitioner p, { // Deal with unrestricted partition key components (special-casing is required to deal with 2i queries on the // first component of a composite partition key) queries that filter on the partition key. - if (partitionKeyRestrictions.needFiltering(table)) + if (partitionKeyRestrictions.needFiltering(table) || isDisjunction) return new Range<>(p.getMinimumToken().minKeyBound(), p.getMinimumToken().maxKeyBound()); ByteBuffer startKeyBytes = getPartitionKeyBound(Bound.START, options); @@ -864,13 +1230,13 @@ private AbstractBounds getPartitionKeyBounds(IPartitioner p, if (partitionKeyRestrictions.isInclusive(Bound.START)) { return partitionKeyRestrictions.isInclusive(Bound.END) - ? new Bounds<>(startKey, finishKey) - : new IncludingExcludingBounds<>(startKey, finishKey); + ? new Bounds<>(startKey, finishKey) + : new IncludingExcludingBounds<>(startKey, finishKey); } return partitionKeyRestrictions.isInclusive(Bound.END) - ? new Range<>(startKey, finishKey) - : new ExcludingBounds<>(startKey, finishKey); + ? new Range<>(startKey, finishKey) + : new ExcludingBounds<>(startKey, finishKey); } private AbstractBounds getPartitionKeyBoundsForTokenRestrictions(IPartitioner p, @@ -924,6 +1290,21 @@ public boolean hasClusteringColumnsRestrictions() return !clusteringColumnsRestrictions.isEmpty(); } + /** + * Checks if the query has any cluster column restrictions that do not also have a supporting index. + * @param table the table metadata + * @return true if the query has any cluster column restrictions that do not also have a supporting index, + * false otherwise. + */ + public boolean hasClusterColumnRestrictionWithoutSupportingIndex(TableMetadata table) + { + IndexRegistry registry = IndexRegistry.obtain(table); + for (Restriction restriction : clusteringColumnsRestrictions.restrictions()) + if (!restriction.hasSupportingIndex(registry, indexHints)) + return true; + return false; + } + /** * Returns the requested clustering columns. * @@ -954,6 +1335,11 @@ public NavigableSet> getClusteringColumnsBounds(Bound b, Quer return clusteringColumnsRestrictions.boundsAsClustering(b, options); } + public boolean isDisjunction() + { + return isDisjunction; + } + /** * Checks if the query returns a range of columns. * @@ -961,41 +1347,106 @@ public NavigableSet> getClusteringColumnsBounds(Bound b, Quer */ public boolean isColumnRange() { - int numberOfClusteringColumns = table.clusteringColumns().size(); - if (table.isStaticCompactTable()) - { - // For static compact tables we want to ignore the fake clustering column (note that if we weren't special casing, - // this would mean a 'SELECT *' on a static compact table would query whole partitions, even though we'll only return - // the static part as far as CQL is concerned. This is thus mostly an optimization to use the query-by-name path). - numberOfClusteringColumns = 0; - } - + // For static compact tables we want to ignore the fake clustering column (note that if we weren't special casing, + // this would mean a 'SELECT *' on a static compact table would query whole partitions, even though we'll only return + // the static part as far as CQL is concerned. This is thus mostly an optimization to use the query-by-name path). + int numberOfClusteringColumns = table.isStaticCompactTable() ? 0 : table.clusteringColumns().size(); // it is a range query if it has at least one the column alias for which no relation is defined or is not EQ or IN. return clusteringColumnsRestrictions.size() < numberOfClusteringColumns || !clusteringColumnsRestrictions.hasOnlyEqualityRestrictions(); } /** - * Checks if the query need to use filtering. + * Checks if the query needs to use filtering. + * * @return true if the query need to use filtering, false otherwise. */ public boolean needFiltering(TableMetadata table) { IndexRegistry indexRegistry = IndexRegistry.obtain(table); - if (filterRestrictions.needsFiltering(indexRegistry)) + boolean hasClusteringColumnRestrictions = !clusteringColumnsRestrictions.isEmpty(); + boolean hasMultipleContains = nonPrimaryKeyRestrictions.hasMultipleContains(); + if (filterRestrictions.needFiltering(indexRegistry, indexHints, hasClusteringColumnRestrictions, hasMultipleContains)) return true; - int numberOfRestrictions = filterRestrictions.getCustomIndexExpressions().size(); - for (Restrictions restrictions : filterRestrictions.getRestrictions()) - numberOfRestrictions += restrictions.size(); + for (StatementRestrictions child : children) + if (child.needFiltering(table)) + return true; - return numberOfRestrictions == 0 && !clusteringColumnsRestrictions.isEmpty(); + return false; } - private void validateSecondaryIndexSelections() + public boolean needsDisjunctionSupport(TableMetadata table) { - checkFalse(keyIsInRelation(), - "Select on indexed columns and with IN clause for the PRIMARY KEY are not supported"); + boolean containsDisjunction = isDisjunction || !children.isEmpty(); + + if (!containsDisjunction) + return false; + + IndexRegistry indexRegistry = IndexRegistry.obtain(table); + + // Check if there's at least one index group that can be used AND supports disjunctions + boolean hasIndexSupportingDisjunction = false; + boolean hasIndexBeingUsed = false; + + for (Index.Group group : indexRegistry.listIndexGroups()) + { + if (filterRestrictions.indexBeingUsed(group, indexHints)) + { + hasIndexBeingUsed = true; + if (group.supportsDisjunction()) + { + hasIndexSupportingDisjunction = true; + break; + } + } + } + + // If we have at least one index that can be used and supports disjunctions, we're good + if (hasIndexBeingUsed && !hasIndexSupportingDisjunction) + return true; + + for (StatementRestrictions child : children) + if (child.needsDisjunctionSupport(table)) + return true; + + return false; + } + + private static Iterable allColumnRestrictions(ClusteringColumnRestrictions clusteringColumnsRestrictions, RestrictionSet nonPrimaryKeyRestrictions) + { + return Iterables.concat(clusteringColumnsRestrictions.restrictions(), nonPrimaryKeyRestrictions.restrictions()); + } + + private static Set getColumnsWithUnsupportedIndexRestrictions(TableMetadata table, IndexHints indexHints, Iterable restrictions) + { + IndexRegistry indexRegistry = IndexRegistry.obtain(table); + if (indexRegistry.listIndexes().isEmpty()) + return Collections.emptySet(); + + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (Restriction restriction : restrictions) + { + if (!restriction.hasSupportingIndex(indexRegistry, indexHints)) + { + for (Index index : indexRegistry.listNotExcludedIndexes(indexHints)) + { + // If a column restriction has an index which was not picked up by hasSupportingIndex, it means it's an unsupported restriction + for (ColumnMetadata column : restriction.getColumnDefs()) + { + if (index.dependsOn(column)) + { + if (index instanceof NoopIndex) + throw invalidRequest(((NoopIndex) index).getUnsupportedMessage()); + builder.add(column); + } + } + } + } + } + + return builder.build(); } /** diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java b/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java index 100fcef64b5d..64606c66ef12 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java @@ -28,6 +28,12 @@ final class TermSlice { + /** + * Represents a slice with no bounds. + * Can be merged with any other slice. + */ + public static final TermSlice UNBOUNDED = new TermSlice(null, false, null, false); + /** * The slice boundaries. */ @@ -108,21 +114,16 @@ public boolean isInclusive(Bound b) */ public TermSlice merge(TermSlice otherSlice) { - if (hasBound(Bound.START)) - { - assert !otherSlice.hasBound(Bound.START); - - return new TermSlice(bound(Bound.START), - isInclusive(Bound.START), - otherSlice.bound(Bound.END), - otherSlice.isInclusive(Bound.END)); - } - assert !otherSlice.hasBound(Bound.END); - - return new TermSlice(otherSlice.bound(Bound.START), - otherSlice.isInclusive(Bound.START), - bound(Bound.END), - isInclusive(Bound.END)); + assert !(hasBound(Bound.START) && otherSlice.hasBound(Bound.START)); + assert !(hasBound(Bound.END) && otherSlice.hasBound(Bound.END)); + + TermSlice sliceForStart = hasBound(Bound.START) ? this : otherSlice; + TermSlice sliceForEnd = hasBound(Bound.END) ? this : otherSlice; + + return new TermSlice(sliceForStart.bound(Bound.START), + sliceForStart.isInclusive(Bound.START), + sliceForEnd.bound(Bound.END), + sliceForEnd.isInclusive(Bound.END)); } @Override diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java b/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java index 74ad7b51a4ce..78d1e07f7db9 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java @@ -18,25 +18,30 @@ package org.apache.cassandra.cql3.restrictions; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import com.google.common.collect.BoundType; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; -import org.apache.cassandra.index.Index; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; import static org.apache.cassandra.cql3.statements.Bound.END; import static org.apache.cassandra.cql3.statements.Bound.START; @@ -47,36 +52,108 @@ *

If all partition key columns have non-token restrictions and do not need filtering, they take precedence * when calculating bounds, incusiveness etc (see CASSANDRA-12149).

*/ -final class TokenFilter implements PartitionKeyRestrictions +abstract class TokenFilter implements PartitionKeyRestrictions { /** * The decorated restriction */ - private final PartitionKeyRestrictions restrictions; + final PartitionKeyRestrictions restrictions; /** * The restriction on the token */ - private final TokenRestriction tokenRestriction; + final TokenRestriction tokenRestriction; /** * Partitioner to manage tokens, extracted from tokenRestriction metadata. */ private final IPartitioner partitioner; - public boolean hasIN() + static TokenFilter create(PartitionKeyRestrictions restrictions, TokenRestriction tokenRestriction) { - return isOnToken() ? false : restrictions.hasIN(); + boolean onToken = restrictions.needFiltering(tokenRestriction.metadata) || restrictions.size() < tokenRestriction.size(); + return onToken ? new TokenFilter.OnToken(restrictions, tokenRestriction) + : new TokenFilter.NotOnToken(restrictions, tokenRestriction); } - public boolean hasContains() + private TokenFilter(PartitionKeyRestrictions restrictions, TokenRestriction tokenRestriction) { - return isOnToken() ? false : restrictions.hasContains(); + this.restrictions = restrictions; + this.tokenRestriction = tokenRestriction; + this.partitioner = tokenRestriction.metadata.partitioner; } - public boolean hasOnlyEqualityRestrictions() + private static final class OnToken extends TokenFilter { - return isOnToken() ? false : restrictions.hasOnlyEqualityRestrictions(); + private OnToken(PartitionKeyRestrictions restrictions, TokenRestriction tokenRestriction) + { + super(restrictions, tokenRestriction); + } + + @Override + public boolean isOnToken() + { + return true; + } + + @Override + public boolean isInclusive(Bound bound) + { + return tokenRestriction.isInclusive(bound); + } + + @Override + public boolean hasBound(Bound bound) + { + return tokenRestriction.hasBound(bound); + } + + @Override + public List bounds(Bound bound, QueryOptions options) throws InvalidRequestException + { + return tokenRestriction.bounds(bound, options); + } + } + + private static final class NotOnToken extends TokenFilter + { + private NotOnToken(PartitionKeyRestrictions restrictions, TokenRestriction tokenRestriction) + { + super(restrictions, tokenRestriction); + } + + @Override + public boolean isInclusive(Bound bound) + { + return restrictions.isInclusive(bound); + } + + @Override + public boolean hasBound(Bound bound) + { + return restrictions.hasBound(bound); + } + + @Override + public List bounds(Bound bound, QueryOptions options) throws InvalidRequestException + { + return restrictions.bounds(bound, options); + } + + public boolean hasIN() + { + return restrictions.hasIN(); + } + + public boolean hasContains() + { + return restrictions.hasContains(); + } + + public boolean hasOnlyEqualityRestrictions() + { + return restrictions.hasOnlyEqualityRestrictions(); + } } @Override @@ -96,12 +173,6 @@ public boolean isOnToken() return needFiltering(tokenRestriction.metadata) || restrictions.size() < tokenRestriction.size(); } - public TokenFilter(PartitionKeyRestrictions restrictions, TokenRestriction tokenRestriction) - { - this.restrictions = restrictions; - this.tokenRestriction = tokenRestriction; - this.partitioner = tokenRestriction.metadata.partitioner; - } @Override public List values(QueryOptions options, ClientState state) throws InvalidRequestException @@ -110,30 +181,12 @@ public List values(QueryOptions options, ClientState state) throws I } @Override - public PartitionKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException + public PartitionKeyRestrictions mergeWith(Restriction restriction, IndexRegistry indexRegistry) throws InvalidRequestException { if (restriction.isOnToken()) - return new TokenFilter(restrictions, (TokenRestriction) tokenRestriction.mergeWith(restriction)); + return TokenFilter.create(restrictions, (TokenRestriction) tokenRestriction.mergeWith(restriction, indexRegistry)); - return new TokenFilter(restrictions.mergeWith(restriction), tokenRestriction); - } - - @Override - public boolean isInclusive(Bound bound) - { - return isOnToken() ? tokenRestriction.isInclusive(bound) : restrictions.isInclusive(bound); - } - - @Override - public boolean hasBound(Bound bound) - { - return isOnToken() ? tokenRestriction.hasBound(bound) : restrictions.hasBound(bound); - } - - @Override - public List bounds(Bound bound, QueryOptions options) throws InvalidRequestException - { - return isOnToken() ? tokenRestriction.bounds(bound, options) : restrictions.bounds(bound, options); + return TokenFilter.create(restrictions.mergeWith(restriction, indexRegistry), tokenRestriction); } /** @@ -275,33 +328,25 @@ public void addFunctionsTo(List functions) } @Override - public boolean hasSupportingIndex(IndexRegistry indexRegistry) - { - return restrictions.hasSupportingIndex(indexRegistry); - } - - @Override - public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) - { - restrictions.addToRowFilter(filter, indexRegistry, options); - } - - @Override - public Index findSupportingIndex(IndexRegistry indexRegistry) + public boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { - return restrictions.findSupportingIndex(indexRegistry); + return restrictions.hasSupportingIndex(indexRegistry, indexHints); } @Override - public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan) + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { - return restrictions.findSupportingIndexFromQueryPlan(indexQueryPlan); + return restrictions.needsFiltering(indexGroup, indexHints); } @Override - public boolean needsFiltering(Index.Group indexGroup) + public void addToRowFilter(RowFilter.Builder filter, + IndexRegistry indexRegistry, + QueryOptions options, + ANNOptions annOptions, + IndexHints indexHints) { - return restrictions.needsFiltering(indexGroup); + restrictions.addToRowFilter(filter, indexRegistry, options, annOptions, indexHints); } @Override diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java index bf23b336b6c3..772bebe46bc9 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java @@ -22,6 +22,8 @@ import com.google.common.base.Joiner; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.index.Index; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; @@ -118,33 +120,21 @@ public ColumnMetadata getLastColumn() } @Override - public boolean hasSupportingIndex(IndexRegistry indexRegistry) + public boolean hasSupportingIndex(IndexRegistry indexRegistry, IndexHints indexHints) { return false; } @Override - public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) + public boolean needsFiltering(Index.Group indexGroup, IndexHints indexHints) { - throw new UnsupportedOperationException("Index expression cannot be created for token restriction"); - } - - @Override - public Index findSupportingIndex(IndexRegistry indexRegistry) - { - return null; - } - - @Override - public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan) - { - return null; + return false; } @Override - public boolean needsFiltering(Index.Group indexGroup) + public void addToRowFilter(RowFilter.Builder filter, IndexRegistry indexRegistry, QueryOptions options, ANNOptions annOptions, IndexHints indexHints) { - return false; + throw new UnsupportedOperationException("Index expression cannot be created for token restriction"); } @Override @@ -170,10 +160,10 @@ protected final String getColumnNamesAsString() } @Override - public final PartitionKeyRestrictions mergeWith(Restriction otherRestriction) throws InvalidRequestException + public final PartitionKeyRestrictions mergeWith(Restriction otherRestriction, IndexRegistry indexRegistry) throws InvalidRequestException { if (!otherRestriction.isOnToken()) - return new TokenFilter(toPartitionKeyRestrictions(otherRestriction), this); + return TokenFilter.create(toPartitionKeyRestrictions(otherRestriction, indexRegistry), this); return doMergeWith((TokenRestriction) otherRestriction); } @@ -191,12 +181,14 @@ public final PartitionKeyRestrictions mergeWith(Restriction otherRestriction) th * @return a PartitionKeyRestrictions * @throws InvalidRequestException if a problem occurs while converting the restriction */ - private PartitionKeyRestrictions toPartitionKeyRestrictions(Restriction restriction) throws InvalidRequestException + private PartitionKeyRestrictions toPartitionKeyRestrictions(Restriction restriction, IndexRegistry indexRegistry) throws InvalidRequestException { if (restriction instanceof PartitionKeyRestrictions) return (PartitionKeyRestrictions) restriction; - return new PartitionKeySingleRestrictionSet(metadata.partitionKeyAsClusteringComparator()).mergeWith(restriction); + return PartitionKeySingleRestrictionSet.builder(metadata.partitionKeyAsClusteringComparator()) + .addRestriction(restriction) + .build(indexRegistry); } public static final class EQRestriction extends TokenRestriction diff --git a/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java b/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java index f7853aee1fe3..b37fbeb232c5 100644 --- a/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java +++ b/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java @@ -208,8 +208,8 @@ private Selector createScalarSelector(QueryOptions options, ScalarFunction funct // We have some terminal arguments, do a partial application ScalarFunction partialFunction = function.partialApplication(version, terminalArgs); - // If all the arguments are terminal and the function is pure we can reduce to a simple value. - if (terminalCount == argSelectors.size() && fun.isPure()) + // If all the arguments are terminal and the function is deterministic we can reduce to a simple value. + if (terminalCount == argSelectors.size() && fun.isDeterministic()) { Arguments arguments = partialFunction.newArguments(version); return new TermSelector(partialFunction.execute(arguments), partialFunction.returnType()); diff --git a/src/java/org/apache/cassandra/cql3/selection/ColumnFilterFactory.java b/src/java/org/apache/cassandra/cql3/selection/ColumnFilterFactory.java index 63fa0520101e..00225cca4108 100644 --- a/src/java/org/apache/cassandra/cql3/selection/ColumnFilterFactory.java +++ b/src/java/org/apache/cassandra/cql3/selection/ColumnFilterFactory.java @@ -38,9 +38,21 @@ abstract class ColumnFilterFactory */ abstract ColumnFilter newInstance(List selectors); - public static ColumnFilterFactory wildcard(TableMetadata table) + public static ColumnFilterFactory wildcard(TableMetadata table, Set orderingColumns) { - return new PrecomputedColumnFilter(ColumnFilter.all(table)); + ColumnFilter cf; + if (orderingColumns.isEmpty()) + { + cf = ColumnFilter.all(table); + } + else + { + ColumnFilter.Builder builder = ColumnFilter.selectionBuilder(); + builder.addAll(table.regularAndStaticColumns()); + builder.addAll(orderingColumns); + cf = builder.build(); + } + return new PrecomputedColumnFilter(cf); } public static ColumnFilterFactory fromColumns(TableMetadata table, diff --git a/src/java/org/apache/cassandra/cql3/selection/ElementsSelector.java b/src/java/org/apache/cassandra/cql3/selection/ElementsSelector.java index 4644ba2ec815..a6f8024f837c 100644 --- a/src/java/org/apache/cassandra/cql3/selection/ElementsSelector.java +++ b/src/java/org/apache/cassandra/cql3/selection/ElementsSelector.java @@ -29,7 +29,10 @@ import org.apache.cassandra.cql3.selection.SimpleSelector.SimpleSelectorFactory; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.io.util.DataInputPlus; @@ -78,9 +81,7 @@ public static AbstractType valueType(CollectionType type) private static CollectionType getCollectionType(Selector selected) { - AbstractType type = selected.getType(); - if (type instanceof ReversedType) - type = ((ReversedType) type).baseType; + AbstractType type = selected.getType().unwrap(); assert type instanceof MapType || type instanceof SetType : "this shouldn't have passed validation in Selectable"; @@ -421,7 +422,7 @@ public void addFetchedColumns(ColumnFilter.Builder builder) protected ByteBuffer extractSelection(ByteBuffer collection) { - return type.getSerializer().getSliceFromSerialized(collection, from, to, type.nameComparator(), type.isFrozenCollection()); + return type.getSerializer().getSliceFromSerialized(collection, from, to, type.nameComparator(), !type.isMultiCell()); } @Override diff --git a/src/java/org/apache/cassandra/cql3/selection/ListSelector.java b/src/java/org/apache/cassandra/cql3/selection/ListSelector.java index a777bc5feb7f..163496b963f1 100644 --- a/src/java/org/apache/cassandra/cql3/selection/ListSelector.java +++ b/src/java/org/apache/cassandra/cql3/selection/ListSelector.java @@ -33,6 +33,7 @@ import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.serializers.CollectionSerializer; import org.apache.cassandra.transport.ProtocolVersion; @@ -102,7 +103,9 @@ public ByteBuffer getOutput(ProtocolVersion protocolVersion) { buffers.add(elements.get(i).getOutput(protocolVersion)); } - return CollectionSerializer.pack(buffers, buffers.size()); + return type.isVector() + ? ((VectorType) type).decomposeRaw(buffers) + : CollectionSerializer.pack(buffers, buffers.size()); } public void reset() diff --git a/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java b/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java index 9ab5ca0370cd..698c38141cf8 100644 --- a/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java +++ b/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java @@ -18,7 +18,6 @@ package org.apache.cassandra.cql3.selection; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.List; import org.apache.cassandra.cql3.ResultSet; @@ -34,7 +33,8 @@ public final class ResultSetBuilder { - private final ResultSet resultSet; + private final ResultMetadata metadata; + private final SortedRowsBuilder rows; /** * As multiple thread can access a Selection instance each ResultSetBuilder will use @@ -57,17 +57,21 @@ public final class ResultSetBuilder */ private Selector.InputRow inputRow; + private boolean hasResults = false; + private int readRowsSize = 0; + private long size = 0; private boolean sizeWarningEmitted = false; public ResultSetBuilder(ResultMetadata metadata, Selectors selectors, boolean unmask) { - this(metadata, selectors, unmask, null); + this(metadata, selectors, unmask, null, SortedRowsBuilder.create()); } - public ResultSetBuilder(ResultMetadata metadata, Selectors selectors, boolean unmask, GroupMaker groupMaker) + public ResultSetBuilder(ResultMetadata metadata, Selectors selectors, boolean unmask, GroupMaker groupMaker, SortedRowsBuilder rows) { - this.resultSet = new ResultSet(metadata.copy(), new ArrayList<>()); + this.metadata = metadata.copy(); + this.rows = rows; this.selectors = selectors; this.groupMaker = groupMaker; this.unmask = unmask; @@ -130,15 +134,10 @@ public void newRow(ProtocolVersion protocolVersion, DecoratedKey partitionKey, C if (inputRow != null) { selectors.addInputRow(inputRow); + inputRow.reset(!selectors.hasProcessing()); if (isNewAggregate) { - resultSet.addRow(getOutputRow()); - inputRow.reset(!selectors.hasProcessing()); - selectors.reset(); - } - else - { - inputRow.reset(!selectors.hasProcessing()); + addRow(); } } else @@ -159,15 +158,22 @@ public ResultSet build() if (inputRow != null) { selectors.addInputRow(inputRow); - resultSet.addRow(getOutputRow()); inputRow.reset(!selectors.hasProcessing()); - selectors.reset(); + addRow(); } // For aggregates we need to return a row even it no records have been found - if (resultSet.isEmpty() && groupMaker != null && groupMaker.returnAtLeastOneRow()) - resultSet.addRow(getOutputRow()); - return resultSet; + if (!hasResults && groupMaker != null && groupMaker.returnAtLeastOneRow()) + { + addRow(); + } + + return new ResultSet(metadata, rows.build()); + } + + public int readRowsSize() + { + return readRowsSize; } private List getOutputRow() @@ -176,4 +182,19 @@ private List getOutputRow() addSize(row); return row; } + + private void addRow() + { + List row = getOutputRow(); + selectors.reset(); + + hasResults = true; + for (int i = 0, isize = row.size(); i < isize; i++) + { + ByteBuffer value = row.get(i); + readRowsSize += value != null ? value.remaining() : 0; + } + + rows.add(row); + } } diff --git a/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java b/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java index 6df2b85b088d..f0171bb9663f 100644 --- a/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java +++ b/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.functions.ScalarFunction; import org.apache.cassandra.transport.ProtocolVersion; @@ -64,6 +65,11 @@ public ByteBuffer getOutput(ProtocolVersion protocolVersion) @Override public void validateForGroupBy() { + checkTrue(fun.isNative() || !DatabaseDescriptor.enableUserDefinedFunctionsThreads(), + "User defined functions are not supported in the GROUP BY clause when asynchronous UDF execution " + + "is enabled. Asynchronous UDF execution can be disabled by setting the configuration property " + + "'enable_user_defined_functions_threads' to false in cassandra.yaml, with the security risks " + + "described in the yaml file."); checkTrue(fun.isMonotonic(), "Only monotonic functions are supported in the GROUP BY clause. Got: %s ", fun); for (int i = 0, m = argSelectors.size(); i < m; i++) argSelectors.get(i).validateForGroupBy(); diff --git a/src/java/org/apache/cassandra/cql3/selection/Selectable.java b/src/java/org/apache/cassandra/cql3/selection/Selectable.java index 3319bb3b4e48..5d7ddfdde297 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selectable.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selectable.java @@ -18,14 +18,47 @@ */ package org.apache.cassandra.cql3.selection; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; -import org.apache.cassandra.cql3.*; -import org.apache.cassandra.cql3.functions.*; +import org.apache.cassandra.cql3.AssignmentTestable; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.FieldIdentifier; +import org.apache.cassandra.cql3.Lists; +import org.apache.cassandra.cql3.Maps; +import org.apache.cassandra.cql3.Sets; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.Tuples; +import org.apache.cassandra.cql3.UserTypes; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.Vectors; +import org.apache.cassandra.cql3.functions.AggregateFcts; +import org.apache.cassandra.cql3.functions.CastFcts; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.cql3.functions.FunctionResolver; +import org.apache.cassandra.cql3.functions.OperationFcts; import org.apache.cassandra.cql3.selection.Selector.Factory; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.DurationType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.TupleType; +import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; @@ -36,7 +69,7 @@ public interface Selectable extends AssignmentTestable { - public Selector.Factory newSelectorFactory(TableMetadata table, AbstractType expectedType, List defs, VariableSpecifications boundNames); + Selector.Factory newSelectorFactory(TableMetadata table, AbstractType expectedType, List defs, VariableSpecifications boundNames); /** * The type of the {@code Selectable} if it can be infered. @@ -48,14 +81,14 @@ public interface Selectable extends AssignmentTestable * literals, the exact type is not inferrable since they are valid for many * different types and so this will return {@code null} too). */ - public AbstractType getExactTypeIfKnown(String keyspace); + AbstractType getExactTypeIfKnown(String keyspace); /** * Checks if this {@code Selectable} select columns matching the specified predicate. * @return {@code true} if this {@code Selectable} select columns matching the specified predicate, * {@code false} otherwise. */ - public boolean selectColumns(Predicate predicate); + boolean selectColumns(Predicate predicate); /** * Checks if the specified Selectables select columns matching the specified predicate. @@ -63,7 +96,7 @@ public interface Selectable extends AssignmentTestable * @return {@code true} if the specified Selectables select columns matching the specified predicate, {@code false} otherwise. */ - public static boolean selectColumns(List selectables, Predicate predicate) + static boolean selectColumns(List selectables, Predicate predicate) { for (Selectable selectable : selectables) { @@ -77,21 +110,21 @@ public static boolean selectColumns(List selectables, Predicate type = getExactTypeIfKnown(keyspace); return type == null ? TestResult.NOT_ASSIGNABLE : type.testAssignment(keyspace, receiver); } @Override - public default AbstractType getCompatibleTypeIfKnown(String keyspace) + default AbstractType getCompatibleTypeIfKnown(String keyspace) { return getExactTypeIfKnown(keyspace); } @@ -118,12 +151,29 @@ default ColumnSpecification specForElementOrSlice(Selectable selected, ColumnSpe } } - public interface Raw + /** + * Checks that this {@code Selectable} is or can be converted into the specified type. + * @param table the table schema + * @param type the expected type + * @throws InvalidRequestException if the {@code Selectable} can not be converted into the specified type + */ + default void validateType(TableMetadata table, AbstractType type) { - public Selectable prepare(TableMetadata table); + ColumnSpecification receiver = new ColumnSpecification(table.keyspace, + table.name, + new ColumnIdentifier(toString(), true), + type); + + if (!testAssignment(table.keyspace, receiver).isAssignable()) + throw invalidRequest("%s is not of the expected type: %s", this, type.asCQL3Type()); } - public static class WithTerm implements Selectable + interface Raw + { + Selectable prepare(TableMetadata table); + } + + class WithTerm implements Selectable { /** * The names given to unamed bind markers found in selection. In selection clause, we often don't have a good @@ -138,7 +188,7 @@ public static class WithTerm implements Selectable */ private static final ColumnIdentifier bindMarkerNameInSelection = new ColumnIdentifier("[selection]", true); - private final Term.Raw rawTerm; + public final Term.Raw rawTerm; public WithTerm(Term.Raw rawTerm) { @@ -179,6 +229,8 @@ public Selector.Factory newSelectorFactory(TableMetadata table, AbstractType type = expectedType; if (type == null) throw new InvalidRequestException("Cannot infer type for term " + this + " in selection clause (try using a cast to force a type)"); + + validateType(table, type); } // The fact we default the name to "[selection]" inconditionally means that any bind marker in a @@ -230,7 +282,7 @@ public Selectable prepare(TableMetadata table) } } - public static class WritetimeOrTTL implements Selectable + class WritetimeOrTTL implements Selectable { // The order of the variants in the Kind enum matters as they are used in ser/deser public enum Kind @@ -297,7 +349,7 @@ public Selector.Factory newSelectorFactory(TableMetadata table, public AbstractType getExactTypeIfKnown(String keyspace) { AbstractType type = kind.returnType; - return column.type.isMultiCell() && !kind.aggregatesMultiCell() ? ListType.getInstance(type, false) : type; + return column.type.isMultiCell() && !kind.aggregatesMultiCell() ? ListType.getInstance(type.freeze(), false) : type; } @Override @@ -327,7 +379,7 @@ public WritetimeOrTTL prepare(TableMetadata table) } } - public static class WithFunction implements Selectable + class WithFunction implements Selectable { public final Function function; public final List args; @@ -425,7 +477,7 @@ public Selectable prepare(TableMetadata table) } } - public static class WithCast implements Selectable + class WithCast implements Selectable { private final CQL3Type type; private final Selectable arg; @@ -497,7 +549,7 @@ public WithCast prepare(TableMetadata table) /** * Represents the selection of the field of a UDT (eg. t.f). */ - public static class WithFieldSelection implements Selectable + class WithFieldSelection implements Selectable { public final Selectable selected; public final FieldIdentifier field; @@ -549,7 +601,7 @@ public Selector.Factory newSelectorFactory(TableMetadata table, AbstractType public AbstractType getExactTypeIfKnown(String keyspace) { AbstractType selectedType = selected.getExactTypeIfKnown(keyspace); - if (selectedType == null || !(selectedType instanceof UserType)) + if (!(selectedType instanceof UserType)) return null; UserType ut = (UserType) selectedType; @@ -589,7 +641,7 @@ public WithFieldSelection prepare(TableMetadata table) *

The parser cannot differentiate between a single element between parentheses or a single element tuple. * By consequence, we are forced to wait until the type is known to be able to differentiate them.

*/ - public static class BetweenParenthesesOrWithTuple implements Selectable + class BetweenParenthesesOrWithTuple implements Selectable { /** * The tuple elements or the element between the parentheses @@ -623,6 +675,7 @@ public Factory newSelectorFactory(TableMetadata cfm, if (type == null) throw invalidRequest("Cannot infer type for term %s in selection clause (try using a cast to force a type)", this); + validateType(cfm, type); } if (selectables.size() == 1 && !type.isTuple()) @@ -659,7 +712,7 @@ private Factory newTupleSelectorFactory(TableMetadata cfm, VariableSpecifications boundNames) { SelectorFactories factories = createFactoriesAndCollectColumnDefinitions(selectables, - tupleType.allTypes(), + tupleType.subTypes, cfm, defs, boundNames); @@ -862,18 +915,6 @@ public AbstractType getCompatibleTypeIfKnown(String keyspace) { return Lists.getPreferredCompatibleType(selectables, p -> p.getCompatibleTypeIfKnown(keyspace)); } - - @Override - public boolean selectColumns(Predicate predicate) - { - return Selectable.selectColumns(selectables, predicate); - } - - @Override - public String toString() - { - return Lists.listToString(selectables); - } } public static class WithVector extends WithArrayLiteral @@ -931,24 +972,12 @@ public AbstractType getCompatibleTypeIfKnown(String keyspace) { return Vectors.getPreferredCompatibleType(selectables, p -> p.getCompatibleTypeIfKnown(keyspace)); } - - @Override - public boolean selectColumns(Predicate predicate) - { - return Selectable.selectColumns(selectables, predicate); - } - - @Override - public String toString() - { - return Lists.listToString(selectables); - } } /** * Selectable for literal Sets. */ - public static class WithSet implements Selectable + class WithSet implements Selectable { /** * The set elements @@ -979,6 +1008,7 @@ public Factory newSelectorFactory(TableMetadata cfm, if (type == null) throw invalidRequest("Cannot infer type for term %s in selection clause (try using a cast to force a type)", this); + validateType(cfm, type); } // The parser treats empty Maps as Sets so if the type is a MapType we know that the Map is empty @@ -1050,7 +1080,7 @@ public Selectable prepare(TableMetadata cfm) * {@code ColumnIdentifier} is equivalent to a {@code FieldIdentifier} from a syntax point of view. * By consequence, we are forced to wait until the type is known to be able to differentiate them.

*/ - public static class WithMapOrUdt implements Selectable + class WithMapOrUdt implements Selectable { /** * The column family metadata. We need to store them to be able to build the proper data once the type has been @@ -1089,6 +1119,7 @@ public Factory newSelectorFactory(TableMetadata cfm, if (type == null) throw invalidRequest("Cannot infer type for term %s in selection clause (try using a cast to force a type)", this); + validateType(cfm, type); } if (type.isUDT()) @@ -1234,7 +1265,7 @@ public Selectable prepare(TableMetadata cfm) /** * Selectable for type hints (e.g. (int) ?). */ - public static class WithTypeHint implements Selectable + class WithTypeHint implements Selectable { /** @@ -1335,9 +1366,7 @@ public Raw( CQL3Type.Raw typeRaw, Selectable.Raw raw) public Selectable prepare(TableMetadata cfm) { Selectable selectable = raw.prepare(cfm); - AbstractType type = this.typeRaw.prepare(cfm.keyspace).getType(); - if (type.isFreezable()) - type = type.freeze(); + AbstractType type = this.typeRaw.prepare(cfm.keyspace).getType().freeze(); return new WithTypeHint(typeRaw.toString(), type, selectable); } } @@ -1348,7 +1377,7 @@ public Selectable prepare(TableMetadata cfm) * identifier have the same syntax. By consequence, we need to wait until the type is known to create the proper * Object: {@code ColumnMetadata} or {@code FieldIdentifier}. */ - public static final class RawIdentifier implements Selectable.Raw + final class RawIdentifier implements Selectable.Raw { private final String text; @@ -1403,7 +1432,7 @@ public String toString() /** * Represents the selection of an element of a collection (eg. c[x]). */ - public static class WithElementSelection implements Selectable + class WithElementSelection implements Selectable { public final Selectable selected; // Note that we can't yet prepare the Term.Raw yet as we need the ColumnSpecificiation corresponding to Selectable, which @@ -1430,28 +1459,24 @@ public Selector.Factory newSelectorFactory(TableMetadata cfm, AbstractType ex Selector.Factory factory = selected.newSelectorFactory(cfm, null, defs, boundNames); ColumnSpecification receiver = factory.getColumnSpecification(cfm); - AbstractType type = receiver.type; - if (receiver.isReversedType()) - { - type = ((ReversedType) type).baseType; - } + AbstractType type = receiver.type.unwrap(); if (!(type instanceof CollectionType)) throw new InvalidRequestException(String.format("Invalid element selection: %s is of type %s is not a collection", selected, type.asCQL3Type())); - ColumnSpecification boundSpec = specForElementOrSlice(selected, receiver, ((CollectionType) type).kind, "Element"); + ColumnSpecification boundSpec = specForElementOrSlice(selected, receiver, ((CollectionType) type).kind, "Element"); Term elt = element.prepare(cfm.keyspace, boundSpec); elt.collectMarkerSpecification(boundNames); - return ElementsSelector.newElementFactory(toString(), factory, (CollectionType)type, elt); + return ElementsSelector.newElementFactory(toString(), factory, (CollectionType)type, elt); } public AbstractType getExactTypeIfKnown(String keyspace) { AbstractType selectedType = selected.getExactTypeIfKnown(keyspace); - if (selectedType == null || !(selectedType instanceof CollectionType)) + if (!(selectedType instanceof CollectionType)) return null; - return ElementsSelector.valueType((CollectionType) selectedType); + return ElementsSelector.valueType((CollectionType) selectedType); } @Override @@ -1487,7 +1512,7 @@ public String toString() /** * Represents the selection of a slice of a collection (eg. c[x..y]). */ - public static class WithSliceSelection implements Selectable + class WithSliceSelection implements Selectable { public final Selectable selected; // Note that we can't yet prepare the Term.Raw yet as we need the ColumnSpecificiation corresponding to Selectable, which @@ -1517,15 +1542,11 @@ public Selector.Factory newSelectorFactory(TableMetadata cfm, AbstractType ex Selector.Factory factory = selected.newSelectorFactory(cfm, expectedType, defs, boundNames); ColumnSpecification receiver = factory.getColumnSpecification(cfm); - AbstractType type = receiver.type; - if (receiver.isReversedType()) - { - type = ((ReversedType) type).baseType; - } + AbstractType type = receiver.type.unwrap(); if (!(type instanceof CollectionType)) throw new InvalidRequestException(String.format("Invalid slice selection: %s of type %s is not a collection", selected, type.asCQL3Type())); - ColumnSpecification boundSpec = specForElementOrSlice(selected, receiver, ((CollectionType) type).kind, "Slice"); + ColumnSpecification boundSpec = specForElementOrSlice(selected, receiver, ((CollectionType) type).kind, "Slice"); // If from or to are null, this means the user didn't provide on in the syntax (we had c[x..] or c[..x]). // The equivalent of doing this when preparing values would be to use UNSET. @@ -1533,13 +1554,13 @@ public Selector.Factory newSelectorFactory(TableMetadata cfm, AbstractType ex Term t = to == null ? Constants.UNSET_VALUE : to.prepare(cfm.keyspace, boundSpec); f.collectMarkerSpecification(boundNames); t.collectMarkerSpecification(boundNames); - return ElementsSelector.newSliceFactory(toString(), factory, (CollectionType)type, f, t); + return ElementsSelector.newSliceFactory(toString(), factory, (CollectionType) type, f, t); } public AbstractType getExactTypeIfKnown(String keyspace) { AbstractType selectedType = selected.getExactTypeIfKnown(keyspace); - if (selectedType == null || !(selectedType instanceof CollectionType)) + if (!(selectedType instanceof CollectionType)) return null; return selectedType; diff --git a/src/java/org/apache/cassandra/cql3/selection/Selection.java b/src/java/org/apache/cassandra/cql3/selection/Selection.java index da87f2619a3c..7d8bb1d267e1 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selection.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selection.java @@ -45,10 +45,24 @@ public abstract class Selection private static final Predicate STATIC_COLUMN_FILTER = (column) -> column.isStatic(); private final TableMetadata table; + + // Full list of columns needed for processing the query, including selected columns, ordering columns, + // and columns needed for restrictions. Wildcard columns are fully materialized here. + // + // This also includes synthetic columns, because unlike all the other not-physical-columns selectables, they are + // computed on the replica instead of the coordinator and so, like physical columns, they need to be sent back + // as part of the result. private final List columns; + + // maps ColumnSpecifications (columns, function calls, aliases) to the columns backing them private final SelectionColumnMapping columnMapping; + + // metadata matching the ColumnSpcifications protected final ResultSet.ResultMetadata metadata; + + // creates a ColumnFilter that breaks columns into `queried` and `fetched` protected final ColumnFilterFactory columnFilterFactory; + protected final boolean isJson; // Columns used to order the result set for JSON queries with post ordering. @@ -134,9 +148,19 @@ public ResultSet.ResultMetadata getResultMetadata() public static Selection wildcard(TableMetadata table, boolean isJson, boolean returnStaticContentOnPartitionWithNoRows) { + return wildcard(table, Collections.emptySet(), isJson, returnStaticContentOnPartitionWithNoRows); + } + + public static Selection wildcard(TableMetadata table, Set orderingColumns, boolean isJson, boolean returnStaticContentOnPartitionWithNoRows) + { + // Add all table columns, but skip orderingColumns: List all = new ArrayList<>(table.columns().size()); Iterators.addAll(all, table.allColumnsInSelectOrder()); - return new SimpleSelection(table, all, Collections.emptySet(), true, isJson, returnStaticContentOnPartitionWithNoRows); + + Set newOrderingColumns = new HashSet<>(orderingColumns); + all.forEach(newOrderingColumns::remove); + + return new SimpleSelection(table, all, newOrderingColumns, true, isJson, returnStaticContentOnPartitionWithNoRows); } public static Selection wildcardWithGroupByOrMaskedColumns(TableMetadata table, @@ -344,14 +368,14 @@ private static List rowToJson(List row, return Arrays.asList(jsonRow); } - public static interface Selectors + public interface Selectors { /** * Returns the {@code ColumnFilter} corresponding to those selectors * * @return the {@code ColumnFilter} corresponding to those selectors */ - public ColumnFilter getColumnFilter(); + ColumnFilter getColumnFilter(); /** * Checks if this Selectors perform some processing @@ -363,19 +387,19 @@ public static interface Selectors * Checks if one of the selectors perform some aggregations. * @return {@code true} if one of the selectors perform some aggregations, {@code false} otherwise. */ - public boolean isAggregate(); + boolean isAggregate(); /** * Returns the number of fetched columns * @return the number of fetched columns */ - public int numberOfFetchedColumns(); + int numberOfFetchedColumns(); /** * Checks if one of the selectors collect TTLs. * @return {@code true} if one of the selectors collect TTLs, {@code false} otherwise. */ - public boolean collectTTLs(); + boolean collectTTLs(); /** * Checks if one of the selectors collects write timestamps. @@ -390,9 +414,9 @@ public static interface Selectors */ public void addInputRow(InputRow input); - public List getOutputRow(); + List getOutputRow(); - public void reset(); + void reset(); } // Special cased selection for when only columns are selected. @@ -411,7 +435,7 @@ public SimpleSelection(TableMetadata table, selectedColumns, orderingColumns, SelectionColumnMapping.simpleMapping(selectedColumns), - isWildcard ? ColumnFilterFactory.wildcard(table) + isWildcard ? ColumnFilterFactory.wildcard(table, orderingColumns) : ColumnFilterFactory.fromColumns(table, selectedColumns, orderingColumns, Collections.emptySet(), returnStaticContentOnPartitionWithNoRows), isWildcard, isJson); diff --git a/src/java/org/apache/cassandra/cql3/selection/SortedRowsBuilder.java b/src/java/org/apache/cassandra/cql3/selection/SortedRowsBuilder.java new file mode 100644 index 000000000000..45de6d77b5e0 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/selection/SortedRowsBuilder.java @@ -0,0 +1,250 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cql3.selection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import com.google.common.math.IntMath; + +import org.apache.cassandra.utils.TopKSelector; + +import static org.apache.cassandra.db.filter.DataLimits.NO_LIMIT; + +/** + * Builds a list of query result rows applying the specified order, limit and offset. + */ +public abstract class SortedRowsBuilder +{ + public final int limit; + public final int offset; + public final int fetchLimit; // limit + offset, saturated to Integer.MAX_VALUE + + @SuppressWarnings("UnstableApiUsage") + private SortedRowsBuilder(int limit, int offset) + { + assert limit > 0 && offset >= 0; + this.limit = limit; + this.offset = offset; + this.fetchLimit = IntMath.saturatedAdd(limit, offset); + } + + /** + * Adds the specified row to this builder. The row might be ignored if it's over the specified limit and offset. + * + * @param row the row to add + */ + public abstract void add(List row); + + /** + * @return a list of query result rows based on the specified order, limit and offset. + */ + public abstract List> build(); + + /** + * Returns a new row builder that keeps insertion order. + * + * @return a rows builder that keeps insertion order. + */ + public static SortedRowsBuilder create() + { + return new WithInsertionOrder(Integer.MAX_VALUE, 0); + } + + /** + * Returns a new row builder that keeps insertion order. + * + * @param limit the query limit + * @param offset the query offset + * @return a rows builder that keeps insertion order. + */ + public static SortedRowsBuilder create(int limit, int offset) + { + return new WithInsertionOrder(limit, offset); + } + + /** + * Returns a new row builder that orders the added rows based on the specified {@link Comparator}. + * + * @param limit the query limit + * @param offset the query offset + * @param comparator the comparator to use for ordering + * @return a rows builder that orders results based on a comparator. + */ + public static SortedRowsBuilder create(int limit, int offset, Comparator> comparator) + { + return new WithHybridSort(limit, offset, comparator); + } + + /** + * {@link SortedRowsBuilder} that keeps insertion order. + *

+ * It keeps at most {@code limit} rows in memory. + */ + private static class WithInsertionOrder extends SortedRowsBuilder + { + private final List> rows = new ArrayList<>(); + private int toSkip = offset; + + private WithInsertionOrder(int limit, int offset) + { + super(limit, offset); + } + + @Override + public void add(List row) + { + if (toSkip-- <= 0 && rows.size() < limit) + rows.add(row); + } + + @Override + public List> build() + { + return rows; + } + } + + /** + * {@link SortedRowsBuilder} that orders rows based on the provided comparator. + *

+ * It simply stores all the rows in a list, and sorts and trims it when {@link #build()} is called. As such, it can + * consume a bunch of resources if the number of rows is high. However, it has good performance for cases where the + * number of rows is close to {@code limit + offset}, as it's the case of partition-directed queries. + */ + public static class WithListSort extends SortedRowsBuilder + { + private final List> rows = new ArrayList<>(); + private final Comparator> comparator; + + private WithListSort(int limit, + int offset, + Comparator> comparator) + { + super(limit, offset); + this.comparator = comparator; + } + + @Override + public void add(List row) + { + rows.add(row); + } + + @Override + public List> build() + { + rows.sort(comparator); + return rows.subList(Math.min(offset, rows.size()), + Math.min(fetchLimit, rows.size())); + } + } + + /** + * {@link SortedRowsBuilder} that orders rows based on the provided comparator. + *

+ * It uses a heap to keep at most {@code limit + offset} rows in memory. + */ + public static class WithHeapSort extends SortedRowsBuilder + { + private final TopKSelector> heap; + + private WithHeapSort(int limit, int offset, Comparator> comparator) + { + super(limit, offset); + this.heap = new TopKSelector<>(comparator, fetchLimit); + } + + @Override + public void add(List row) + { + heap.add(row); + } + + public void addAll(Iterable> rows) + { + heap.addAll(rows); + } + + @Override + public List> build() + { + return heap.getSliced(offset); + } + } + + /** + * {@link SortedRowsBuilder} that tries to combine the benefits of {@link WithListSort} and {@link WithHeapSort}. + *

+ * {@link WithListSort} is faster for the first rows, but then it becomes slower than {@link WithHeapSort} as the + * number of rows grows. Also, {@link WithHeapSort} has constant {@code limit + offset} memory usage, whereas + * {@link WithListSort} memory usage grows linearly with the number of added rows. + *

+ * This uses a {@link WithListSort} to sort the first {@code (limit + offset) * }{@link #SWITCH_FACTOR} rows, + * and then it switches to a {@link WithHeapSort} if more rows are added. + *

+ * It keeps at most {@link #SWITCH_FACTOR} {@code * (limit + offset)} rows in memory. + */ + public static class WithHybridSort extends SortedRowsBuilder + { + /** + * Factor of {@code limit + offset} at which we switch from list to heap. + */ + public static final int SWITCH_FACTOR = 4; + + private final int threshold; // at what number of rows we switch from list to heap, -1 means no switch + + private WithListSort list; + private WithHeapSort heap; + + @SuppressWarnings("UnstableApiUsage") + private WithHybridSort(int limit, int offset, Comparator> comparator) + { + super(limit, offset); + this.list = new WithListSort(limit, offset, comparator); + + // The heap approach is only useful when the limit is smaller than the number of collected rows. + // If there is no limit we will return all the collected rows, so we can simply use the list approach. + this.threshold = limit == NO_LIMIT ? -1 : IntMath.saturatedMultiply(fetchLimit, SWITCH_FACTOR); + } + + @Override + public void add(List row) + { + // start using the heap if the list is full + if (list != null && threshold > 0 && list.rows.size() >= threshold) + { + heap = new WithHeapSort(limit, offset, list.comparator); + heap.addAll(list.rows); + list = null; + } + + if (list != null) + list.add(row); + else + heap.add(row); + } + + @Override + public List> build() + { + return list != null ? list.build() : heap.build(); + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java b/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java index 9be0b45d6ff4..637a918eef29 100644 --- a/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java +++ b/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java @@ -70,7 +70,7 @@ protected String getColumnName() protected AbstractType getReturnType() { AbstractType type = kind.returnType; - return isMultiCell && !kind.aggregatesMultiCell() ? ListType.getInstance(type, false) : type; + return isMultiCell && !kind.aggregatesMultiCell() ? ListType.getInstance(type.freeze(), false) : type; } @Override @@ -117,6 +117,7 @@ public void addFetchedColumns(ColumnFilter.Builder builder) }; } + @Override public void addFetchedColumns(ColumnFilter.Builder builder) { selected.addFetchedColumns(builder); @@ -146,11 +147,13 @@ public void addInput(InputRow input) } } + @Override public ByteBuffer getOutput(ProtocolVersion protocolVersion) { return current; } + @Override public void reset() { selected.reset(); @@ -158,10 +161,11 @@ public void reset() current = null; } + @Override public AbstractType getType() { AbstractType type = kind.returnType; - return isMultiCell ? ListType.getInstance(type, false) : type; + return isMultiCell ? ListType.getInstance(type.freeze(), false) : type; } @Override diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java index e42dc654f3c0..3fcffc5c1360 100644 --- a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java @@ -27,6 +27,7 @@ import org.apache.cassandra.exceptions.*; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.CassandraVersion; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import static org.apache.cassandra.cql3.statements.RequestValidations.*; @@ -54,6 +55,7 @@ public AlterRoleStatement(RoleName name, RoleOptions opts, DCPermissions dcPermi this.ifExists = ifExists; } + @Override public void validate(ClientState state) throws RequestValidationException { opts.validate(); @@ -129,7 +131,7 @@ public ResultMessage execute(ClientState state) throws RequestValidationExceptio if (dcPermissions != null) DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions); - if (cidrPermissions != null) + if (cidrPermissions != null && DatabaseDescriptor.getStorageCompatibilityMode().major >= CassandraVersion.CASSANDRA_5_0.major) DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions); return null; diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java index a70a8891a607..3bfcabe5c1ea 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -18,8 +18,20 @@ package org.apache.cassandra.cql3.statements; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashMultiset; @@ -31,28 +43,47 @@ import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; +import org.apache.cassandra.cql3.Attributes; +import org.apache.cassandra.cql3.BatchQueryOptions; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.ResultSet; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.*; -import org.apache.cassandra.db.*; import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.RowIterator; -import org.apache.cassandra.exceptions.*; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.metrics.BatchMetrics; import org.apache.cassandra.metrics.ClientRequestSizeMetrics; -import org.apache.cassandra.service.*; -import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.messages.ResultMessage; -import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Pair; import static java.util.function.Predicate.isEqual; - import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; /** @@ -65,6 +96,7 @@ public enum Type LOGGED, UNLOGGED, COUNTER } + private final String rawCQLStatement; public final Type type; private final VariableSpecifications bindVariables; private final List statements; @@ -82,10 +114,6 @@ public enum Type private static final Logger logger = LoggerFactory.getLogger(BatchStatement.class); - private static final String UNLOGGED_BATCH_WARNING = "Unlogged batch covering {} partitions detected " + - "against table{} {}. You should use a logged batch for " + - "atomicity, or asynchronous writes for performance."; - private static final String LOGGED_BATCH_LOW_GCGS_WARNING = "Executing a LOGGED BATCH on table{} {}, configured with a " + "gc_grace_seconds of 0. The gc_grace_seconds is used to TTL " + "batchlog entries, so setting gc_grace_seconds too low on " + @@ -101,8 +129,10 @@ public enum Type * @param statements the list of statements in the batch * @param attrs additional attributes for statement (CL, timestamp, timeToLive) */ - public BatchStatement(Type type, VariableSpecifications bindVariables, List statements, Attributes attrs) + public BatchStatement(String queryString, Type type, VariableSpecifications bindVariables, + List statements, Attributes attrs) { + this.rawCQLStatement = queryString; this.type = type; this.bindVariables = bindVariables; this.statements = statements; @@ -136,6 +166,12 @@ public BatchStatement(Type type, VariableSpecifications bindVariables, List getBindVariables() { @@ -255,8 +291,12 @@ private boolean isLogged() // The batch itself will be validated in either Parsed#prepare() - for regular CQL3 batches, // or in QueryProcessor.processBatch() - for native protocol batches. + @Override public void validate(ClientState state) throws InvalidRequestException { + if (isLogged()) + Guardrails.loggedBatchEnabled.ensureEnabled(state); + for (ModificationStatement statement : statements) statement.validate(state); } @@ -328,16 +368,15 @@ public List getMutations(ClientState state, * * @param mutations - the batch mutations. */ - private static void verifyBatchSize(Collection mutations) throws InvalidRequestException + private static void verifyBatchSize(Collection mutations, ClientState clientState) throws InvalidRequestException { // We only warn for batch spanning multiple mutations (#10876) if (mutations.size() <= 1) return; - long warnThreshold = DatabaseDescriptor.getBatchSizeWarnThreshold(); long size = IMutation.dataSize(mutations); - if (size > warnThreshold) + if (Guardrails.batchSize.triggersOn(size, clientState)) { Set tableNames = new HashSet<>(); for (IMutation mutation : mutations) @@ -346,27 +385,11 @@ private static void verifyBatchSize(Collection mutations) t tableNames.add(update.metadata().toString()); } - long failThreshold = DatabaseDescriptor.getBatchSizeFailThreshold(); - - String format = "Batch for {} is of size {}, exceeding specified threshold of {} by {}.{}"; - if (size > failThreshold) - { - Tracing.trace(format, tableNames, FBUtilities.prettyPrintMemory(size), FBUtilities.prettyPrintMemory(failThreshold), - FBUtilities.prettyPrintMemory(size - failThreshold), " (see batch_size_fail_threshold)"); - logger.error(format, tableNames, FBUtilities.prettyPrintMemory(size), FBUtilities.prettyPrintMemory(failThreshold), - FBUtilities.prettyPrintMemory(size - failThreshold), " (see batch_size_fail_threshold)"); - throw new InvalidRequestException("Batch too large"); - } - else if (logger.isWarnEnabled()) - { - logger.warn(format, tableNames, FBUtilities.prettyPrintMemory(size), FBUtilities.prettyPrintMemory(warnThreshold), - FBUtilities.prettyPrintMemory(size - warnThreshold), ""); - } - ClientWarn.instance.warn(MessageFormatter.arrayFormat(format, new Object[] {tableNames, size, warnThreshold, size - warnThreshold, ""}).getMessage()); + Guardrails.batchSize.guard(size, tableNames.toString(), false, clientState); } } - private void verifyBatchType(Collection mutations) + private void verifyBatchType(Collection mutations, ClientState clientState) { if (!isLogged() && mutations.size() > 1) { @@ -385,13 +408,9 @@ private void verifyBatchType(Collection mutations) // CASSANDRA-11529: log only if we have more than a threshold of keys, this was also suggested in the // original ticket that introduced this warning, CASSANDRA-9282 - if (keySet.size() > DatabaseDescriptor.getUnloggedBatchAcrossPartitionsWarnThreshold()) + if (Guardrails.unloggedBatchAcrossPartitions.triggersOn(keySet.size(), clientState)) { - NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.MINUTES, UNLOGGED_BATCH_WARNING, - keySet.size(), tableNames.size() == 1 ? "" : "s", tableNames); - - ClientWarn.instance.warn(MessageFormatter.arrayFormat(UNLOGGED_BATCH_WARNING, new Object[]{keySet.size(), - tableNames.size() == 1 ? "" : "s", tableNames}).getMessage()); + Guardrails.unloggedBatchAcrossPartitions.guard(keySet.size(), tableNames.toString(), false, clientState); } } } @@ -407,17 +426,23 @@ public ResultMessage execute(QueryState queryState, BatchQueryOptions options, D long timestamp = options.getTimestamp(queryState); long nowInSeconds = options.getNowInSeconds(queryState); - if (options.getConsistency() == null) + ConsistencyLevel cl = options.getConsistency(); + if (cl == null) throw new InvalidRequestException("Invalid empty consistency level"); - if (options.getSerialConsistency() == null) + + if (options.getSerialConsistency(queryState) == null) throw new InvalidRequestException("Invalid empty serial consistency level"); ClientState clientState = queryState.getClientState(); - Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency()), + Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency(queryState)), clientState); for (int i = 0; i < statements.size(); i++ ) - statements.get(i).validateDiskUsage(options.forStatement(i), clientState); + { + ModificationStatement statement = statements.get(i); + statement.validateConsistency(cl, clientState); + statement.validateDiskUsage(options.forStatement(i), clientState); + } if (hasConditions) return executeWithConditions(options, queryState, requestTime); @@ -426,35 +451,53 @@ public ResultMessage execute(QueryState queryState, BatchQueryOptions options, D executeInternalWithoutCondition(queryState, options, requestTime); else executeWithoutConditions(getMutations(clientState, options, false, timestamp, nowInSeconds, requestTime), - options.getConsistency(), requestTime); + clientState, options.getConsistency(), requestTime); + + ResultMessage result = new ResultMessage.Void(); + RequestSensors sensors = RequestTracker.instance.get(); + Map tableMetadataById = statements.stream() + .map(ModificationStatement::metadata) + .collect(Collectors.toMap(metadata -> metadata.id, Function.identity(), (existing, replacement) -> existing)); + for (TableMetadata metadata : tableMetadataById.values()) + { + Context context = Context.from(metadata); + SensorsCustomParams.addSensorToCQLResponse(result, options.wrapped.getProtocolVersion(), sensors, context, org.apache.cassandra.sensors.Type.WRITE_BYTES); + } - return new ResultMessage.Void(); + return result; } - private void executeWithoutConditions(List mutations, ConsistencyLevel cl, Dispatcher.RequestTime requestTime) throws RequestExecutionException, RequestValidationException + private void executeWithoutConditions(List mutations, + ClientState clientState, + ConsistencyLevel cl, + Dispatcher.RequestTime requestTime) throws RequestExecutionException, RequestValidationException { if (mutations.isEmpty()) return; - verifyBatchSize(mutations); - verifyBatchType(mutations); + verifyBatchSize(mutations, clientState); + verifyBatchType(mutations, clientState); - updatePartitionsPerBatchMetrics(mutations.size()); + updatePerBatchMetrics(mutations); boolean mutateAtomic = (isLogged() && mutations.size() > 1); - StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic, requestTime); + StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic, requestTime, clientState); ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations); } - private void updatePartitionsPerBatchMetrics(int updatedPartitions) + private void updatePerBatchMetrics(Collection mutations) { - if (isLogged()) { - metrics.partitionsPerLoggedBatch.update(updatedPartitions); - } else if (isCounter()) { - metrics.partitionsPerCounterBatch.update(updatedPartitions); - } else { - metrics.partitionsPerUnloggedBatch.update(updatedPartitions); + int nrUpdatedPartitions = mutations.size(); + int nrUpdatedColumns = 0; + for (IMutation mutation : mutations) + { + for (PartitionUpdate update : mutation.getPartitionUpdates()) + { + for (Row row : update.rows()) + nrUpdatedColumns += row.columns().size(); + } } + metrics.update(type, nrUpdatedPartitions, nrUpdatedColumns); } private ResultMessage executeWithConditions(BatchQueryOptions options, QueryState state, Dispatcher.RequestTime requestTime) @@ -470,7 +513,7 @@ private ResultMessage executeWithConditions(BatchQueryOptions options, QueryStat tableName, casRequest.key, casRequest, - options.getSerialConsistency(), + options.getSerialConsistency(state), options.getConsistency(), state.getClientState(), options.getNowInSeconds(state), @@ -608,7 +651,7 @@ public String toString() return String.format("BatchStatement(type=%s, statements=%s)", type, statements); } - public static class Parsed extends QualifiedStatement + public static class Parsed extends RawKeyspaceAwareStatement { private final Type type; private final Attributes.Raw attrs; @@ -616,45 +659,35 @@ public static class Parsed extends QualifiedStatement public Parsed(Type type, Attributes.Raw attrs, List parsedStatements) { - super(null); this.type = type; this.attrs = attrs; this.parsedStatements = parsedStatements; } - // Not doing this in the constructor since we only need this for prepared statements - @Override - public boolean isFullyQualified() + private void setKeyspace(ClientState state) throws InvalidRequestException { for (ModificationStatement.Parsed statement : parsedStatements) - if (!statement.isFullyQualified()) - return false; - - return true; + statement.setKeyspace(state); } - @Override - public void setKeyspace(ClientState state) throws InvalidRequestException + public void setKeyspace(Function convertKeyspace) throws InvalidRequestException { for (ModificationStatement.Parsed statement : parsedStatements) - statement.setKeyspace(state); + statement.setKeyspace(convertKeyspace.apply(statement)); } @Override - public String keyspace() + public BatchStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - return null; - } + setKeyspace(state); - public BatchStatement prepare(ClientState state) - { List statements = new ArrayList<>(parsedStatements.size()); - parsedStatements.forEach(s -> statements.add(s.prepare(state, bindVariables))); + parsedStatements.forEach(s -> statements.add(s.prepare(state, bindVariables, keyspaceMapper))); Attributes prepAttrs = attrs.prepare("[batch]", "[batch]"); prepAttrs.collectMarkerSpecification(bindVariables); - BatchStatement batchStatement = new BatchStatement(type, bindVariables, statements, prepAttrs); + BatchStatement batchStatement = new BatchStatement(rawCQLStatement, type, bindVariables, statements, prepAttrs); batchStatement.validate(); return batchStatement; diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java index 521cd2afa6e2..18a4cff1ebe9 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java @@ -36,10 +36,10 @@ import org.apache.cassandra.db.commitlog.CommitLogSegment; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.virtual.VirtualMutation; -import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.StorageCompatibilityMode; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; @@ -98,7 +98,7 @@ public PartitionUpdate.Builder getPartitionUpdateBuilder(TableMetadata metadata, { RegularAndStaticColumns columns = updatedColumns.get(metadata.id); assert columns != null; - upd = new PartitionUpdate.Builder(metadata, dk, columns, perPartitionKeyCounts.get(metadata.id).count(dk.getKey())); + upd = PartitionUpdate.builder(metadata, dk, columns, perPartitionKeyCounts.get(metadata.id).count(dk.getKey())); mut.add(upd); } return upd; @@ -146,7 +146,7 @@ public List toMutations(ClientState state) { IMutation mutation = builder.build(); mutation.validateIndexedColumns(state); - mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); + mutation.validateSize(StorageCompatibilityMode.current().storageMessagingVersion(), CommitLogSegment.ENTRY_OVERHEAD_SIZE); ms.add(mutation); } } diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java index 9671592c16b6..4802602b3262 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java @@ -233,9 +233,10 @@ private RegularAndStaticColumns updatedColumns() return builder.build(); } + @Override public PartitionUpdate makeUpdates(FilteredPartition current, ClientState clientState, Ballot ballot) throws InvalidRequestException { - PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata, key, updatedColumns(), conditions.size()); + PartitionUpdate.Builder updateBuilder = PartitionUpdate.builder(metadata, key, updatedColumns(), conditions.size()); long timeUuidNanos = 0; for (RowUpdate upd : updates) timeUuidNanos = upd.applyUpdates(current, updateBuilder, clientState, ballot.msb(), timeUuidNanos); diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java index d6e0a1298cde..2634d55debd3 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java @@ -37,6 +37,11 @@ public class CreateRoleStatement extends AuthenticationStatement final CIDRPermissions cidrPermissions; private final boolean ifNotExists; + public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions, boolean ifNotExists) + { + this(name, options, dcPermissions, null, ifNotExists); + } + public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions, CIDRPermissions cidrPermissions, boolean ifNotExists) { @@ -57,6 +62,7 @@ public void authorize(ClientState state) throws UnauthorizedException } } + @Override public void validate(ClientState state) throws RequestValidationException { opts.validate(); @@ -93,7 +99,7 @@ public ResultMessage execute(ClientState state) throws RequestExecutionException DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions); } - if (cidrPermissions != null) + if (cidrPermissions != null && !DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5)) DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions); grantPermissionsToCreator(state); diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java index 7a50a9b015bd..249bc17c8853 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -17,12 +17,21 @@ */ package org.apache.cassandra.cql3.statements; -import java.util.Collections; import java.util.List; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.Attributes; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Operation; +import org.apache.cassandra.cql3.Operations; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.UpdateParameters; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; import org.apache.cassandra.cql3.conditions.ColumnCondition; import org.apache.cassandra.cql3.conditions.Conditions; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; @@ -34,8 +43,6 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.Pair; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; @@ -45,14 +52,15 @@ */ public class DeleteStatement extends ModificationStatement { - private DeleteStatement(VariableSpecifications bindVariables, + private DeleteStatement(String queryString, + VariableSpecifications bindVariables, TableMetadata cfm, Operations operations, StatementRestrictions restrictions, Conditions conditions, Attributes attrs) { - super(StatementType.DELETE, bindVariables, cfm, operations, restrictions, conditions, attrs); + super(queryString, StatementType.DELETE, bindVariables, cfm, operations, restrictions, conditions, attrs); } @Override @@ -163,15 +171,10 @@ protected ModificationStatement prepareInternal(ClientState state, operations.add(op); } - StatementRestrictions restrictions = newRestrictions(state, - metadata, - bindVariables, - operations, - whereClause, - conditions, - Collections.emptyList()); + StatementRestrictions restrictions = newRestrictions(state, metadata, bindVariables, operations, whereClause, conditions); - DeleteStatement stmt = new DeleteStatement(bindVariables, + DeleteStatement stmt = new DeleteStatement(rawCQLStatement, + bindVariables, metadata, operations, restrictions, diff --git a/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java b/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java index 1b3c5fbbb0d9..18cd624a5f4f 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java @@ -159,14 +159,17 @@ public ResultMessage executeLocally(QueryState state, QueryOptions options) // long offset = getOffset(pagingState, schema.getVersion()); - int pageSize = options.getPageSize(); + PageSize pageSize = options.getPageSize(); + + if (pageSize.isDefined() && pageSize.getUnit() != PageSize.PageUnit.ROWS) + throw new InvalidRequestException("Paging in bytes is not supported for describe statement. Please specify the page size in rows."); Stream stream = describe(state.getClientState(), keyspaces); if (offset > 0L) stream = stream.skip(offset); - if (pageSize > 0) - stream = stream.limit(pageSize); + if (pageSize.isDefined()) + stream = stream.limit(pageSize.getSize()); List> rows = stream.map(e -> toRow(e, includeInternalDetails)) .collect(Collectors.toList()); @@ -174,8 +177,10 @@ public ResultMessage executeLocally(QueryState state, QueryOptions options) ResultSet.ResultMetadata resultMetadata = new ResultSet.ResultMetadata(metadata(state.getClientState())); ResultSet result = new ResultSet(resultMetadata, rows); - if (pageSize > 0 && rows.size() == pageSize) - result.metadata.setHasMorePages(getPagingState(offset + pageSize, schema.getVersion())); + if (pageSize.isDefined() && rows.size() == pageSize.getSize()) + { + result.metadata.setHasMorePages(getPagingState(offset + pageSize.getSize(), schema.getVersion())); + } return new ResultMessage.Rows(result); } diff --git a/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java index 13ba54a52d9d..0b86d20bbf78 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java @@ -51,6 +51,7 @@ public void authorize(ClientState state) throws UnauthorizedException throw new UnauthorizedException("Only superusers can drop a role with superuser status"); } + @Override public void validate(ClientState state) throws RequestValidationException { // validate login here before authorize to avoid leaking user existence to anonymous users. @@ -75,7 +76,8 @@ public ResultMessage execute(ClientState state) throws RequestValidationExceptio DatabaseDescriptor.getAuthorizer().revokeAllFrom(role); DatabaseDescriptor.getAuthorizer().revokeAllOn(role); DatabaseDescriptor.getNetworkAuthorizer().drop(role); - DatabaseDescriptor.getCIDRAuthorizer().dropCidrPermissionsForRole(role); + if (!DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5)) + DatabaseDescriptor.getCIDRAuthorizer().dropCidrPermissionsForRole(role); return null; } diff --git a/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java index 4b5aa601e2ab..b95b85061b0c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java @@ -64,6 +64,7 @@ public ListPermissionsStatement(Set permissions, IResource resource, this.grantee = grantee.hasName()? RoleResource.role(grantee.getName()) : null; } + @Override public void validate(ClientState state) throws RequestValidationException { // a check to ensure the existence of the user isn't being leaked by user existence check. diff --git a/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java index 8a75f8a6c36a..fe4e97986e88 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java @@ -67,6 +67,7 @@ public ListRolesStatement(RoleName grantee, boolean recursive) this.recursive = recursive; } + @Override public void validate(ClientState state) throws UnauthorizedException, InvalidRequestException { state.ensureNotAnonymous(); diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index a538348ceab5..30b1181fb017 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -18,7 +18,20 @@ package org.apache.cassandra.cql3.statements; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.StringJoiner; +import java.util.function.UnaryOperator; import com.google.common.collect.HashMultiset; import com.google.common.collect.Iterables; @@ -26,16 +39,26 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.ReplicaLayout; +import org.apache.cassandra.cql3.Attributes; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.Operation; +import org.apache.cassandra.cql3.Operations; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.ResultSet; +import org.apache.cassandra.cql3.UpdateParameters; +import org.apache.cassandra.cql3.Validation; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.index.sai.analyzer.AnalyzerEqOperatorSupport; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.ViewMetadata; -import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.conditions.ColumnCondition; import org.apache.cassandra.cql3.conditions.ColumnConditions; import org.apache.cassandra.cql3.conditions.Conditions; @@ -44,15 +67,46 @@ import org.apache.cassandra.cql3.selection.ResultSetBuilder; import org.apache.cassandra.cql3.selection.Selection; import org.apache.cassandra.cql3.selection.Selection.Selectors; -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.filter.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringBuilder; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.SinglePartitionReadQuery; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.BooleanType; -import org.apache.cassandra.db.partitions.*; +import org.apache.cassandra.db.partitions.FilteredPartition; +import org.apache.cassandra.db.partitions.Partition; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.PartitionIterators; +import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.RowIterator; -import org.apache.cassandra.db.view.View; -import org.apache.cassandra.exceptions.*; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.exceptions.UnauthorizedException; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaLayout; import org.apache.cassandra.metrics.ClientRequestSizeMetrics; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster; @@ -82,7 +136,9 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa public static final String CUSTOM_EXPRESSIONS_NOT_ALLOWED = "Custom index expressions cannot be used in WHERE clauses for UPDATE or DELETE statements"; - private static final ColumnIdentifier CAS_RESULT_COLUMN = new ColumnIdentifier("[applied]", false); + public static final ColumnIdentifier CAS_RESULT_COLUMN = new ColumnIdentifier("[applied]", false); + + private final String rawCQLStatement; protected final StatementType type; @@ -103,7 +159,8 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa private final RegularAndStaticColumns requiresRead; - public ModificationStatement(StatementType type, + public ModificationStatement(String queryString, + StatementType type, VariableSpecifications bindVariables, TableMetadata metadata, Operations operations, @@ -111,6 +168,7 @@ public ModificationStatement(StatementType type, Conditions conditions, Attributes attrs) { + this.rawCQLStatement = queryString; this.type = type; this.bindVariables = bindVariables; this.metadata = metadata; @@ -158,6 +216,12 @@ public ModificationStatement(StatementType type, this.requiresRead = requiresReadBuilder.build(); } + @Override + public String getRawCQLStatement() + { + return rawCQLStatement; + } + @Override public List getBindVariables() { @@ -252,22 +316,13 @@ public void authorize(ClientState state) throws InvalidRequestException, Unautho if (hasConditions()) state.ensureTablePermission(metadata, Permission.SELECT); - // MV updates need to get the current state from the table, and might update the views - // Require Permission.SELECT on the base table, and Permission.MODIFY on the views - Iterator views = View.findAll(keyspace(), table()).iterator(); - if (views.hasNext()) - { - state.ensureTablePermission(metadata, Permission.SELECT); - do - { - state.ensureTablePermission(views.next().metadata, Permission.MODIFY); - } while (views.hasNext()); - } + // Modification on base table with MV should skip SELECT access control to base table and WRITE access control to view table. for (Function function : getFunctions()) state.ensurePermission(Permission.EXECUTE, function); } + @Override public void validate(ClientState state) throws InvalidRequestException { checkFalse(hasConditions() && attrs.isTimestampSet(), "Cannot provide custom timestamp for conditional updates"); @@ -278,8 +333,19 @@ public void validate(ClientState state) throws InvalidRequestException checkFalse(isVirtual() && attrs.isTimeToLiveSet(), "Expiring columns are not supported by virtual tables"); checkFalse(isVirtual() && hasConditions(), "Conditional updates are not supported by virtual tables"); - if (attrs.isTimestampSet()) + // there are system queries with USING TIMESTAMP, e.g. SchemaKeyspace#saveSystemKeyspacesSchema + if (SchemaConstants.isUserKeyspace(metadata.keyspace) && attrs.isTimestampSet()) Guardrails.userTimestampsEnabled.ensureEnabled(state); + + // Warn but otherwise accept conditions on analyzed columns. The analyzers won't be used (see CNDB-11658). + IndexRegistry indexRegistry = IndexRegistry.obtain(metadata); + Set analyzedColumns = conditions.getAnalyzedColumns(indexRegistry, IndexHints.NONE); + if (!analyzedColumns.isEmpty()) + { + StringJoiner joiner = new StringJoiner(", "); + analyzedColumns.forEach(c -> joiner.add(c.name.toString())); + ClientWarn.instance.warn(String.format(AnalyzerEqOperatorSupport.LWT_CONDITION_ON_ANALYZED_WARNING, joiner)); + } } public void validateDiskUsage(QueryOptions options, ClientState state) @@ -380,7 +446,7 @@ public NavigableSet> createClustering(QueryOptions options, Client throws InvalidRequestException { if (appliesOnlyToStaticColumns() && !restrictions.hasClusteringColumnsRestrictions()) - return FBUtilities.singleton(CBuilder.STATIC_BUILDER.build(), metadata().comparator); + return FBUtilities.singleton(ClusteringBuilder.STATIC_BUILDER.build(), metadata().comparator); return restrictions.getClusteringColumns(options, state); } @@ -418,6 +484,7 @@ public boolean requiresRead() private Map readRequiredLists(Collection partitionKeys, ClusteringIndexFilter filter, + ClientState state, DataLimits limits, boolean local, ConsistencyLevel cl, @@ -457,7 +524,7 @@ private Map readRequiredLists(Collection pa } } - try (PartitionIterator iter = group.execute(cl, null, requestTime)) + try (PartitionIterator iter = group.execute(cl, state, requestTime)) { return asMaterializedMap(iter); } @@ -494,7 +561,7 @@ public ResultMessage execute(QueryState queryState, QueryOptions options, Dispat if (options.getConsistency() == null) throw new InvalidRequestException("Invalid empty consistency level"); - Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency()), + Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency(queryState)), queryState.getClientState()); return hasConditions() @@ -509,10 +576,7 @@ private ResultMessage executeWithoutCondition(QueryState queryState, QueryOption return executeInternalWithoutCondition(queryState, options, requestTime); ConsistencyLevel cl = options.getConsistency(); - if (isCounter()) - cl.validateCounterForWrite(metadata()); - else - cl.validateForWrite(); + validateConsistency(cl, queryState.getClientState()); validateDiskUsage(options, queryState.getClientState()); validateTimestamp(queryState, options); @@ -526,7 +590,7 @@ private ResultMessage executeWithoutCondition(QueryState queryState, QueryOption requestTime); if (!mutations.isEmpty()) { - StorageProxy.mutateWithTriggers(mutations, cl, false, requestTime); + StorageProxy.mutateWithTriggers(mutations, cl, false, requestTime, queryState.getClientState()); if (!SchemaConstants.isSystemKeyspace(metadata.keyspace)) ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations); @@ -535,6 +599,14 @@ private ResultMessage executeWithoutCondition(QueryState queryState, QueryOption return null; } + public void validateConsistency(ConsistencyLevel cl, ClientState clientState) + { + if (isCounter()) + cl.validateCounterForWrite(metadata(), clientState); + else + cl.validateForWrite(metadata().keyspace, clientState); + } + private ResultMessage executeWithCondition(QueryState queryState, QueryOptions options, Dispatcher.RequestTime requestTime) { CQL3CasRequest request = makeCasRequest(queryState, options); @@ -543,7 +615,7 @@ private ResultMessage executeWithCondition(QueryState queryState, QueryOptions o table(), request.key, request, - options.getSerialConsistency(), + options.getSerialConsistency(queryState), options.getConsistency(), queryState.getClientState(), options.getNowInSeconds(queryState), @@ -710,9 +782,11 @@ static RowIterator casInternal(ClientState state, CQL3CasRequest request, long t SinglePartitionReadQuery readCommand = request.readCommand(nowInSeconds); FilteredPartition current; try (ReadExecutionController executionController = readCommand.executionController(); - PartitionIterator iter = readCommand.executeInternal(executionController)) + PartitionIterator iter = readCommand.executeInternal(executionController); + RowIterator row = PartitionIterators.getOnlyElement(iter, readCommand);) { - current = FilteredPartition.create(PartitionIterators.getOnlyElement(iter, readCommand)); + // FilteredPartition consumes the row but does not close the iterator + current = FilteredPartition.create(row); } if (!request.appliesTo(current)) @@ -759,6 +833,12 @@ final void addUpdates(UpdatesCollector collector, long nowInSeconds, Dispatcher.RequestTime requestTime) { + if (type == StatementType.DELETE && !metadata.isVirtual()) + { + ColumnFamilyStore cfs = Keyspace.openAndGetStore(metadata); + cfs.metric.deleteRequests.inc(); + } + if (hasSlices()) { Slices slices = createSlices(options); @@ -873,6 +953,7 @@ private UpdateParameters makeUpdateParameters(Collection keys, Map lists = readRequiredLists(keys, filter, + state, limits, local, options.getConsistency(), @@ -920,7 +1001,7 @@ public static Slices toSlices(ClusteringComparator comparator, SortedSet { protected final StatementType type; private final Attributes.Raw attrs; @@ -943,16 +1024,19 @@ protected Parsed(QualifiedName name, this.ifExists = ifExists; } - public ModificationStatement prepare(ClientState state) + @Override + public ModificationStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - return prepare(state, bindVariables); + setKeyspace(state); + return prepare(state, bindVariables, keyspaceMapper); } - public ModificationStatement prepare(ClientState state, VariableSpecifications bindVariables) + public ModificationStatement prepare(ClientState state, VariableSpecifications bindVariables, UnaryOperator keyspaceMapper) { - TableMetadata metadata = Schema.instance.validateTable(keyspace(), name()); + String ks = keyspaceMapper.apply(keyspace()); + TableMetadata metadata = Schema.instance.validateTable(ks, name()); - Attributes preparedAttributes = attrs.prepare(keyspace(), name()); + Attributes preparedAttributes = attrs.prepare(ks, name()); preparedAttributes.collectMarkerSpecification(bindVariables); Conditions preparedConditions = prepareConditions(metadata, bindVariables); @@ -1032,19 +1116,31 @@ protected abstract ModificationStatement prepareInternal(ClientState state, * @param conditions the conditions * @return the restrictions */ + /** + * @deprecated Use the version with ClientState parameter instead + */ + @Deprecated(since = "5.0") + protected StatementRestrictions newRestrictions(TableMetadata metadata, + VariableSpecifications boundNames, + Operations operations, + WhereClause where, + Conditions conditions) + { + throw new UnsupportedOperationException("This method is deprecated. Use the version with ClientState parameter."); + } + protected StatementRestrictions newRestrictions(ClientState state, TableMetadata metadata, VariableSpecifications boundNames, Operations operations, WhereClause where, - Conditions conditions, - List orderings) + Conditions conditions) { if (where.containsCustomExpressions()) throw new InvalidRequestException(CUSTOM_EXPRESSIONS_NOT_ALLOWED); boolean applyOnlyToStaticColumns = appliesOnlyToStaticColumns(operations, conditions); - return new StatementRestrictions(state, type, metadata, where, boundNames, orderings, applyOnlyToStaticColumns, false, false); + return StatementRestrictions.create(state, type, metadata, where, boundNames, Collections.emptyList(), IndexHints.NONE, applyOnlyToStaticColumns, false, false); } public List> getConditions() diff --git a/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java b/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java index e809a27a45e9..5d8297e28257 100644 --- a/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java @@ -26,6 +26,7 @@ import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.service.ClientState; + import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -42,6 +43,7 @@ protected PermissionsManagementStatement(Set permissions, IResource this.grantee = RoleResource.role(grantee.getName()); } + @Override public void validate(ClientState state) throws RequestValidationException { // validate login here before authorize to avoid leaking user existence to anonymous users. diff --git a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java index 65ec8fca67c6..09a002cb0a9a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java +++ b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java @@ -17,37 +17,65 @@ */ package org.apache.cassandra.cql3.statements; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; + +import com.codahale.metrics.Clock; +import org.apache.cassandra.cql3.QualifiedName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.exceptions.SyntaxException; -import static java.lang.String.format; public class PropertyDefinitions { + public static final String MULTIPLE_DEFINITIONS_ERROR = "Multiple definitions for property '%s'"; private static final Pattern POSITIVE_PATTERN = Pattern.compile("(1|true|yes)"); private static final Pattern NEGATIVE_PATTERN = Pattern.compile("(0|false|no)"); - + private static final Map OBSOLETE_PROPERTY_LAST_LOG_TIMES = new ConcurrentHashMap<>(); + private static final long OBSOLETE_PROPERTY_LOG_INTERVAL_MS = 30_000; + protected static final Logger logger = LoggerFactory.getLogger(PropertyDefinitions.class); protected final Map properties = new HashMap<>(); + // Wrapper around System.currentTimeMillis() to simplify unit testing. + private final Clock clock; + + @VisibleForTesting + PropertyDefinitions(Clock clock) + { + this.clock = clock; + } + + public PropertyDefinitions() { + this.clock = Clock.defaultClock(); + } public void addProperty(String name, String value) throws SyntaxException { - if (properties.put(name, value) != null) - throw new SyntaxException(format("Multiple definitions for property '%s'", name)); + if (properties.putIfAbsent(name, value) != null) + throw new SyntaxException(String.format(MULTIPLE_DEFINITIONS_ERROR, name)); } public void addProperty(String name, Map value) throws SyntaxException { - if (properties.put(name, value) != null) - throw new SyntaxException(format("Multiple definitions for property '%s'", name)); + if (properties.putIfAbsent(name, value) != null) + throw new SyntaxException(String.format(MULTIPLE_DEFINITIONS_ERROR, name)); + } + + public void addProperty(String name, Set value) throws SyntaxException + { + if (properties.putIfAbsent(name, value) != null) + throw new SyntaxException(String.format(MULTIPLE_DEFINITIONS_ERROR, name)); } public void validate(Set keywords, Set obsolete) throws SyntaxException @@ -58,54 +86,62 @@ public void validate(Set keywords, Set obsolete) throws SyntaxEx continue; if (obsolete.contains(name)) - logger.warn("Ignoring obsolete property {}", name); + { + long now = clock.getTime(); + Long lastLogged = OBSOLETE_PROPERTY_LAST_LOG_TIMES.putIfAbsent(name, now); + + if (lastLogged == null || (now - lastLogged) >= OBSOLETE_PROPERTY_LOG_INTERVAL_MS) + { + logger.warn("Ignoring obsolete property {}", name); + } + } else - throw new SyntaxException(format("Unknown property '%s'", name)); + throw new SyntaxException(String.format("Unknown property '%s'", name)); } } - /** - * Returns the name of all the properties that are updated by this object. - */ - public Set updatedProperties() - { - return properties.keySet(); - } - - public void removeProperty(String name) + @Nullable + protected String getSimple(String name) throws SyntaxException { - properties.remove(name); - } - - public boolean hasProperty(String name) - { - return properties.containsKey(name); + Object val = properties.get(name); + if (val == null) + return null; + if (!(val instanceof String)) + throw new SyntaxException(String.format("Invalid value for property '%s'. It should be a string", name)); + return (String)val; } - protected String getString(String name) throws SyntaxException + @Nullable + @SuppressWarnings("unchecked") + public Set getQualifiedNames(String name) throws SyntaxException { Object val = properties.get(name); if (val == null) return null; - if (!(val instanceof String)) - throw new SyntaxException(format("Invalid value for property '%s'. It should be a string", name)); - return (String)val; + if (val instanceof Map && ((Map)val).isEmpty()) // to solve the ambiguity between empty map and empty set + return Collections.emptySet(); + if (!(val instanceof Set)) + throw new SyntaxException(String.format("Invalid value for property '%s'. It should be a set of identifiers.", name)); + return (Set) val; } - protected Map getMap(String name) throws SyntaxException + @Nullable + @SuppressWarnings("unchecked") + public Map getMap(String name) throws SyntaxException { Object val = properties.get(name); if (val == null) return null; + if (val instanceof Set && ((Set)val).isEmpty()) // to solve the ambiguity between empty map and empty set + return Collections.emptyMap(); if (!(val instanceof Map)) - throw new SyntaxException(format("Invalid value for property '%s'. It should be a map.", name)); + throw new SyntaxException(String.format("Invalid value for property '%s'. It should be a map.", name)); return (Map)val; } - public boolean getBoolean(String key, boolean defaultValue) throws SyntaxException + public Boolean hasProperty(String name) { - String value = getString(key); - return value != null ? parseBoolean(key, value) : defaultValue; + return properties.containsKey(name); } public static boolean parseBoolean(String key, String value) throws SyntaxException @@ -120,51 +156,85 @@ public static boolean parseBoolean(String key, String value) throws SyntaxExcept else if (NEGATIVE_PATTERN.matcher(lowerCasedValue).matches()) return false; - throw new SyntaxException(format("Invalid boolean value %s for '%s'. " + - "Positive values can be '1', 'true' or 'yes'. " + - "Negative values can be '0', 'false' or 'no'.", - value, key)); + throw new SyntaxException(String.format("Invalid boolean value %s for '%s'. " + + "Positive values can be '1', 'true' or 'yes'. " + + "Negative values can be '0', 'false' or 'no'.", + value, key)); } - public int getInt(String key, int defaultValue) throws SyntaxException + // Return a property value, typed as a Boolean + public Boolean getBoolean(String key, Boolean defaultValue) throws SyntaxException { - String value = getString(key); - return value != null ? parseInt(key, value) : defaultValue; + String value = getSimple(key); + return (value == null) ? defaultValue : parseBoolean(key, value); } - public static int parseInt(String key, String value) throws SyntaxException + // Return a property value, typed as a double + public double getDouble(String key, double defaultValue) throws SyntaxException { - if (null == value) - throw new IllegalArgumentException("value argument can't be null"); - - try + String value = getSimple(key); + if (value == null) { - return Integer.parseInt(value); + return defaultValue; } - catch (NumberFormatException e) + else { - throw new SyntaxException(format("Invalid integer value %s for '%s'", value, key)); + try + { + return Double.parseDouble(value); + } + catch (NumberFormatException e) + { + throw new SyntaxException(String.format("Invalid double value %s for '%s'", value, key)); + } } } - public double getDouble(String key, double defaultValue) throws SyntaxException + // Return a property value, typed as an Integer + public Integer getInt(String key, Integer defaultValue) throws SyntaxException { - String value = getString(key); - return value != null ? parseDouble(key, value) : defaultValue; + String value = getSimple(key); + return toInt(key, value, defaultValue); } - public static double parseDouble(String key, String value) throws SyntaxException + public static Integer toInt(String key, String value, Integer defaultValue) throws SyntaxException { - if (null == value) - throw new IllegalArgumentException("value argument can't be null"); - - try + if (value == null) { - return Double.parseDouble(value); + return defaultValue; } - catch (NumberFormatException e) + else { - throw new SyntaxException(format("Invalid double value %s for '%s'", value, key)); + try + { + return Integer.valueOf(value); + } + catch (NumberFormatException e) + { + throw new SyntaxException(String.format("Invalid integer value %s for '%s'", value, key)); + } } } -} + + /** + * Returns the name of all the properties that are updated by this object. + */ + public Set updatedProperties() + { + return properties.keySet(); + } + + public void removeProperty(String name) + { + properties.remove(name); + } + + public Object getProperty(String name) + { + Object ret = properties.get(name); + if (ret == null) + throw new SyntaxException(String.format("Invalid value for property '%s'. It should not be null.", name)); + + return ret; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/cql3/statements/QualifiedStatement.java b/src/java/org/apache/cassandra/cql3/statements/QualifiedStatement.java index 4ed41d168888..b7df8e60a161 100644 --- a/src/java/org/apache/cassandra/cql3/statements/QualifiedStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/QualifiedStatement.java @@ -27,7 +27,7 @@ /** * Abstract class for statements that work on sub-keyspace level (tables, views, indexes, functions, etc.) */ -public abstract class QualifiedStatement extends CQLStatement.Raw +public abstract class QualifiedStatement extends RawKeyspaceAwareStatement { final QualifiedName qualifiedName; @@ -72,7 +72,7 @@ public String name() { return qualifiedName.getName(); } - + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/cql3/statements/RawKeyspaceAwareStatement.java b/src/java/org/apache/cassandra/cql3/statements/RawKeyspaceAwareStatement.java new file mode 100644 index 000000000000..9117085b0db2 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/RawKeyspaceAwareStatement.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.cql3.statements; + +import java.util.function.UnaryOperator; + +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.service.ClientState; + +/** + * A super class for raw (parsed) statements which supports keyspace override during preparation. + * + * @param a type of the statement produced by preparation of this raw statement + */ +public abstract class RawKeyspaceAwareStatement extends CQLStatement.Raw +{ + /** + * Produces a prepared statement of type {@link R} without overriding keyspace. + */ + @Override + public final R prepare(ClientState state) + { + return prepare(state, Constants.IDENTITY_STRING_MAPPER); + } + + /** + * Produces a prepared statement of type {@link R}, optionally overriding keyspace name in the produced + * statement. The keyspace name is overridden using the provided mapping function in the statement and all + * contained objects which refer to some keyspace. + */ + public abstract R prepare(ClientState state, UnaryOperator keyspaceMapper); +} diff --git a/src/java/org/apache/cassandra/cql3/statements/RoleManagementStatement.java b/src/java/org/apache/cassandra/cql3/statements/RoleManagementStatement.java index a5274dd73834..e383e1ba6531 100644 --- a/src/java/org/apache/cassandra/cql3/statements/RoleManagementStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/RoleManagementStatement.java @@ -25,6 +25,7 @@ import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.service.ClientState; + import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -44,6 +45,7 @@ public void authorize(ClientState state) throws UnauthorizedException super.checkPermission(state, Permission.AUTHORIZE, role); } + @Override public void validate(ClientState state) throws RequestValidationException { state.ensureNotAnonymous(); diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectOptions.java b/src/java/org/apache/cassandra/cql3/statements/SelectOptions.java new file mode 100644 index 000000000000..e26fbe31a0a0 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/SelectOptions.java @@ -0,0 +1,104 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cql3.statements; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; + +/** + * {@code WITH option1=... AND option2=...} options for SELECT statements. + */ +public class SelectOptions extends PropertyDefinitions +{ + public static final SelectOptions EMPTY = new SelectOptions(); + public static final String ANN_OPTIONS = "ann_options"; + public static final String INCLUDED_INDEXES = "included_indexes"; + public static final String EXCLUDED_INDEXES = "excluded_indexes"; + + private static final Set keywords = ImmutableSet.of(ANN_OPTIONS, INCLUDED_INDEXES, EXCLUDED_INDEXES); + + /** + * Validates all the {@code SELECT} options. + * + * @param state the query state + * @param limit the {@code SELECT} query user-provided limit + * @param indexRegistry the index registry for the queried table + * @param indexQueryPlan the index query plan for the query, if any + * @throws InvalidRequestException if any of the options are invalid + */ + public void validate(ClientState state, + TableMetadata table, + int limit, + IndexRegistry indexRegistry, + @Nullable Index.QueryPlan indexQueryPlan) throws RequestValidationException + { + validate(keywords, Collections.emptySet()); + parseANNOptions().validate(state, table.keyspace, limit); + parseIndexHints(table, indexRegistry).validate(indexQueryPlan); + } + + /** + * Parse the ANN Options. Does not validate values of the options or whether peers will be able to process them. + * + * @return the ANN options within these options, or {@link ANNOptions#NONE} if no options are present + * @throws InvalidRequestException if the ANN options are invalid + */ + public ANNOptions parseANNOptions() throws RequestValidationException + { + Map options = getMap(ANN_OPTIONS); + + return options == null + ? ANNOptions.NONE + : ANNOptions.fromMap(options); + } + + /** + * @return {@code true} if these options contain ANN options, {@code false} otherwise + */ + public boolean hasANNOptions() + { + return properties.containsKey(ANN_OPTIONS); + } + + /** + * Parse the {@link IndexHints}, performing query-independent validation. Query-dependent validation should be done + * later, when the query plan is built, by calling {@link IndexHints#validate(Index.QueryPlan)}. + * + * @return the parsed index hints, {@link IndexHints#NONE} if no hints are present, or they are empty + * @throws InvalidRequestException if the index hints are invalid + */ + public IndexHints parseIndexHints(TableMetadata table, IndexRegistry indexRegistry) throws RequestValidationException + { + Set included = getQualifiedNames(INCLUDED_INDEXES); + Set excluded = getQualifiedNames(EXCLUDED_INDEXES); + return IndexHints.fromCQLNames(included, excluded, table, indexRegistry); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index f72befd22be9..25c32951b4b4 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -18,10 +18,24 @@ package org.apache.cassandra.cql3.statements; import java.nio.ByteBuffer; -import java.util.*; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; import java.util.concurrent.TimeUnit; - +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import com.google.common.annotations.VisibleForTesting; @@ -29,24 +43,42 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.google.common.math.IntMath; +import org.apache.cassandra.cql3.Ordering; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.restrictions.SingleRestriction; -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.index.Index; +import org.apache.cassandra.config.DataStorageSpec; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.ResultSet; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; +import org.apache.cassandra.cql3.restrictions.ExternalRestriction; +import org.apache.cassandra.cql3.restrictions.Restrictions; +import org.apache.cassandra.cql3.restrictions.StatementRestrictions; +import org.apache.cassandra.db.guardrails.GuardrailsConfigProvider; +import org.apache.cassandra.cql3.selection.SortedRowsBuilder; +import org.apache.cassandra.sensors.SensorsCustomParams; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; -import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.functions.Function; -import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.cql3.selection.RawSelector; import org.apache.cassandra.cql3.selection.ResultSetBuilder; import org.apache.cassandra.cql3.selection.Selectable; @@ -54,20 +86,53 @@ import org.apache.cassandra.cql3.selection.Selection; import org.apache.cassandra.cql3.selection.Selection.Selectors; import org.apache.cassandra.cql3.selection.Selector; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DataRange; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.PartitionRangeReadQuery; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.ReadQuery; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.SinglePartitionReadQuery; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; import org.apache.cassandra.db.aggregation.AggregationSpecification; import org.apache.cassandra.db.aggregation.GroupMaker; -import org.apache.cassandra.db.filter.*; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.FloatType; import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.db.view.View; import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.exceptions.*; -import org.apache.cassandra.metrics.ClientRequestSizeMetrics; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.ReadSizeAbortException; +import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.metrics.ClientRequestSizeMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetricsProvider; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; @@ -83,21 +148,19 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.NoSpamLogger; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import static java.lang.String.format; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; +import static org.apache.cassandra.db.filter.DataLimits.NO_LIMIT; import static org.apache.cassandra.utils.ByteBufferUtil.UNSET_BYTE_BUFFER; /** * Encapsulates a completely parsed SELECT query, including the target * column family, expression, result count, and ordering clause. - *

+ *

* A number of public methods here are only used internally. However, * many of these are made accessible for the benefit of custom * QueryHandler implementations, so before reducing their accessibility @@ -110,24 +173,25 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement { private static final Logger logger = LoggerFactory.getLogger(SelectStatement.class); private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(SelectStatement.logger, 1, TimeUnit.MINUTES); - - public static final int DEFAULT_PAGE_SIZE = 10000; - public static final String TOPK_CONSISTENCY_LEVEL_ERROR = "Top-K queries can only be run with consistency level ONE/LOCAL_ONE. Consistency level %s was used."; - public static final String TOPK_LIMIT_ERROR = "Top-K queries must have a limit specified and the limit must be less than the query page size"; - public static final String TOPK_PARTITION_LIMIT_ERROR = "Top-K queries do not support per-partition limits"; + public static final String USAGE_WARNING_PAGE_WEIGHT = "Applied page weight limit of "; public static final String TOPK_AGGREGATION_ERROR = "Top-K queries can not be run with aggregation"; + public static final String TOPK_CONSISTENCY_LEVEL_ERROR = "Top-K queries can only be run with consistency level ONE/LOCAL_ONE. Consistency level %s was used."; public static final String TOPK_CONSISTENCY_LEVEL_WARNING = "Top-K queries can only be run with consistency level ONE " + "/ LOCAL_ONE / NODE_LOCAL. Consistency level %s was requested. " + "Downgrading the consistency level to %s."; - public static final String TOPK_PAGE_SIZE_WARNING = "Top-K queries do not support paging and the page size is set to %d, " + - "which is less than LIMIT %d. The page size has been set to %d to match the LIMIT."; + public static final String TOPK_OFFSET_ERROR = "Top-K queries cannot be run with an offset. Offset was set to %d."; + + private static final int NO_OFFSET = -1; // sentinel value meaning no offset has been explicitly requested + private final String rawCQLStatement; public final VariableSpecifications bindVariables; public final TableMetadata table; public final Parameters parameters; private final Selection selection; private final Term limit; private final Term perPartitionLimit; + private final Term offset; + private final SelectOptions selectOptions; private final StatementRestrictions restrictions; @@ -150,7 +214,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement false, false); - public SelectStatement(TableMetadata table, + public SelectStatement(String queryString, + TableMetadata table, VariableSpecifications bindVariables, Parameters parameters, Selection selection, @@ -159,8 +224,11 @@ public SelectStatement(TableMetadata table, AggregationSpecification.Factory aggregationSpecFactory, ColumnComparator> orderingComparator, Term limit, - Term perPartitionLimit) + Term perPartitionLimit, + Term offset, + SelectOptions selectOptions) { + this.rawCQLStatement = queryString; this.table = table; this.bindVariables = bindVariables; this.selection = selection; @@ -171,6 +239,14 @@ public SelectStatement(TableMetadata table, this.parameters = parameters; this.limit = limit; this.perPartitionLimit = perPartitionLimit; + this.offset = offset; + this.selectOptions = selectOptions; + } + + @Override + public String getRawCQLStatement() + { + return rawCQLStatement; } @Override @@ -222,16 +298,19 @@ public ColumnFilter queriedColumns() // queried data through processColumnFamily. static SelectStatement forSelection(TableMetadata table, Selection selection) { - return new SelectStatement(table, + return new SelectStatement(null, + table, VariableSpecifications.empty(), defaultParameters, selection, - StatementRestrictions.empty(StatementType.SELECT, table), + StatementRestrictions.empty(table), false, null, null, null, - null); + null, + null, + SelectOptions.EMPTY); } public ResultSet.ResultMetadata getResultMetadata() @@ -271,73 +350,140 @@ public void authorize(ClientState state) throws InvalidRequestException, Unautho } } + @Override public void validate(ClientState state) throws InvalidRequestException { if (parameters.allowFiltering && !SchemaConstants.isSystemKeyspace(table.keyspace)) Guardrails.allowFilteringEnabled.ensureEnabled(state); } + /** + * Adds the specified restrictions to the index restrictions. + * + * @param indexRestrictions the index restrictions to add + * @return a new {@code SelectStatement} instance with the added index restrictions + */ + @SuppressWarnings("unused") // this is used by DSE and CNDB to add authorization restrictions + public SelectStatement addIndexRestrictions(Restrictions indexRestrictions) + { + return new SelectStatement(rawCQLStatement, + table, + bindVariables, + parameters, + selection, + restrictions.addIndexRestrictions(indexRestrictions), + isReversed, + aggregationSpecFactory, + orderingComparator, + limit, + perPartitionLimit, + offset, + selectOptions); + } + + /** + * Adds the specified external restrictions to the index restrictions. + * + * @param indexRestrictions the index restrictions to add + * @return a new {@code SelectStatement} instance with the added index restrictions + */ + public SelectStatement addIndexRestrictions(Iterable indexRestrictions) + { + return new SelectStatement(rawCQLStatement, + table, + bindVariables, + parameters, + selection, + restrictions.addExternalRestrictions(indexRestrictions), + isReversed, + aggregationSpecFactory, + orderingComparator, + limit, + perPartitionLimit, + offset, + selectOptions); + } + + private void validateQueryOptions(QueryState queryState, QueryOptions options) + { + if (SchemaConstants.isUserKeyspace(table.keyspace)) + Guardrails.readConsistencyLevels.guard(EnumSet.of(options.getConsistency()), queryState.getClientState()); + + PageSize pageSize = options.getPageSize(); + pageSize.guard(table(), queryState.getClientState()); + } + + /** + * Returns whether the paging can be skipped based on the user limits and the page size - that is, if the user limit + * is provided and is lower than the page size, it means that we will only return at most one page and thus paging + * is unnecessary in this case. That applies to the page size defined in rows - if the page size is defined in bytes + * we cannot say anything about the relation beteween the user rows limit and the page size. + */ + private boolean canSkipPaging(DataLimits userLimits, PageSize pageSize, boolean topK) + { + return !pageSize.isDefined() || + pageSize.getUnit() == PageSize.PageUnit.ROWS && !pageSize.isCompleted(userLimits.count(), PageSize.PageUnit.ROWS) || + topK; + } + + @Override public ResultMessage.Rows execute(QueryState state, QueryOptions options, Dispatcher.RequestTime requestTime) + { + return execute(state, options, null, requestTime); + } + + /** + * Common implementation of {@link #execute(QueryState, QueryOptions, Dispatcher.RequestTime)} and + * {@link #executeWithReadQuery(QueryState, QueryOptions, ReadQuery, Dispatcher.RequestTime)}: the two differ + * only in where the {@link ReadQuery} to read comes from. + * + * @param externalQuery the caller-supplied query to read, or {@code null} to build one from this statement's + * own restrictions with {@code getQuery(...)} + */ + private ResultMessage.Rows execute(QueryState state, + QueryOptions options, + @Nullable ReadQuery externalQuery, + Dispatcher.RequestTime requestTime) { ConsistencyLevel cl = options.getConsistency(); checkNotNull(cl, "Invalid empty consistency level"); cl.validateForRead(); - Guardrails.readConsistencyLevels.guard(EnumSet.of(cl), state.getClientState()); + validateQueryOptions(state, options); long nowInSec = options.getNowInSeconds(state); int userLimit = getLimit(options); int userPerPartitionLimit = getPerPartitionLimit(options); - int pageSize = options.getPageSize(); + int userOffset = getOffset(options); + PageSize pageSize = options.getPageSize(); boolean unmask = !table.hasMaskedColumns() || state.getClientState().hasTablePermission(table, Permission.UNMASK); Selectors selectors = selection.newSelectors(options); AggregationSpecification aggregationSpec = getAggregationSpec(options); - DataLimits limit = getDataLimits(userLimit, userPerPartitionLimit, pageSize, aggregationSpec); - - // Handle additional validation for topK queries - if (restrictions.isTopK()) + ReadQuery query; + if (externalQuery == null) { - checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR); - - // We aren't going to allow SERIAL at all, so we can error out on those. - checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL || - options.getConsistency() == ConsistencyLevel.SERIAL, - String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency())); - - if (options.getConsistency().needsReconciliation()) - { - ConsistencyLevel supplied = options.getConsistency(); - ConsistencyLevel downgrade = supplied.isDatacenterLocal() ? ConsistencyLevel.LOCAL_ONE : ConsistencyLevel.ONE; - - options = QueryOptions.withConsistencyLevel(options, downgrade); - - ClientWarn.instance.warn(String.format(TOPK_CONSISTENCY_LEVEL_WARNING, supplied, downgrade)); - } - - checkFalse(limit.isUnlimited(), TOPK_LIMIT_ERROR); - - checkFalse(limit.perPartitionCount() != DataLimits.NO_LIMIT, TOPK_PARTITION_LIMIT_ERROR); - - if (pageSize > 0 && pageSize < limit.count()) - { - int oldPageSize = pageSize; - pageSize = limit.count(); - limit = getDataLimits(userLimit, userPerPartitionLimit, pageSize, aggregationSpec); - options = QueryOptions.withPageSize(options, pageSize); - ClientWarn.instance.warn(String.format(TOPK_PAGE_SIZE_WARNING, oldPageSize, limit.count(), pageSize)); - } + query = getQuery(options, state.getClientState(), selectors.getColumnFilter(), + nowInSec, userLimit, userPerPartitionLimit, userOffset, aggregationSpec); + } + else + { + // getQuery(...) validates the query it builds before returning it; do the same for the supplied one. + query = externalQuery; + query.validateSelectOptions(selectOptions, state.getClientState()); + query.maybeValidateIndexes(); } - - ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(), nowInSec, limit); if (options.isReadThresholdsEnabled()) query.trackWarnings(); ResultMessage.Rows rows; - if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize) || query.isTopK())) + if (query.limits().isGroupByLimit() && pageSize != null && pageSize.isDefined() && pageSize.getUnit() == PageSize.PageUnit.BYTES) + throw new InvalidRequestException("Paging in bytes cannot be specified for aggregation queries"); + + if (aggregationSpec == null && canSkipPaging(query.limits(), pageSize, query.isTopK())) { - rows = execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, null, requestTime, unmask); + rows = execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, userOffset, null, requestTime, unmask); } else { @@ -350,6 +496,7 @@ public ResultMessage.Rows execute(QueryState state, QueryOptions options, Dispat pageSize, nowInSec, userLimit, + userOffset, aggregationSpec, requestTime, unmask); @@ -360,6 +507,56 @@ public ResultMessage.Rows execute(QueryState state, QueryOptions options, Dispat return rows; } + /** + * Executes this {@code SELECT} against a caller-supplied {@link ReadQuery} instead of the one this + * statement would build from its own {@code WHERE} restrictions, returning a single page of results. + *

+ * This is the entry point for components that resolve which partitions to read (and in + * what order) out of band — for example a custom {@link org.apache.cassandra.cql3.QueryHandler} + * that obtains the matching primary keys from an external index and, in the desired order, assembles a + * {@link org.apache.cassandra.db.SinglePartitionReadCommand.Group}. Result order is entirely the query's: + * this method pages the supplied {@code query} through {@link #getPager(ReadQuery, QueryOptions)}, and a + * {@link org.apache.cassandra.service.pager.MultiPartitionPager} yields partitions in the order of the + * {@link org.apache.cassandra.db.SinglePartitionReadCommand.Group}'s commands — not token order — across + * page boundaries. + *

+ * Apart from substituting {@code query} for {@link #getQuery(QueryOptions, ClientState, ColumnFilter, long, int, int, int, AggregationSpecification)}, + * this runs the very same code as {@link #execute(QueryState, QueryOptions, Dispatcher.RequestTime)} — both + * delegate to {@link #execute(QueryState, QueryOptions, ReadQuery, Dispatcher.RequestTime)}: consistency + * validation, guardrails ({@link #validateQueryOptions}, {@code pageSize.guard}), the per-query validation + * {@code getQuery} applies to the query it builds ({@link ReadQuery#validateSelectOptions(SelectOptions, ClientState)}, + * {@link ReadQuery#maybeValidateIndexes()}), read-threshold tracking + * ({@link ReadQuery#trackWarnings()}), selection/projection, user {@code LIMIT}/{@code OFFSET}, aggregation, + * dynamic-data masking, the single-shot fast path when paging can be skipped, read metrics/sensors, and page + * continuation via {@link ResultSet.ResultMetadata#setHasMorePages}. + *

+ * Caller contract: + *

    + *
  • {@code query} must read partitions of this statement's {@link #table}; build it with this + * statement's data limits and column filter (see {@link #getQuery(QueryOptions, long)}) so + * storage-level and result-level limits agree. Passing a query over a different table, or one whose + * shape disagrees with this statement's aggregation/selection, is undefined.
  • + *
  • Top-K queries are not supported through this entry point.
  • + *
+ * This method holds no state across calls and reads only immutable statement fields, so — like + * {@link #execute(QueryState, QueryOptions, Dispatcher.RequestTime)} — it is safe to call concurrently on a + * single shared (prepared) {@code SelectStatement} instance, one {@code query} per call. It relies on the + * same request-scoped thread-locals as the normal read path ({@link ClientWarn}, + * {@link org.apache.cassandra.sensors.RequestTracker}), so callers must invoke it on a request thread that + * has them set up (as the native-transport dispatch path does). + */ + public ResultMessage.Rows executeWithReadQuery(QueryState state, + QueryOptions options, + ReadQuery query, + Dispatcher.RequestTime requestTime) + { + Objects.requireNonNull(query, "query"); + // Top-K needs the specific validation/CL-downgrade handling in getQuery, which this entry point bypasses. + checkFalse(query.isTopK(), "Top-K queries are not supported by executeWithReadQuery"); + + return execute(state, options, query, requestTime); + } + public AggregationSpecification getAggregationSpec(QueryOptions options) { return aggregationSpecFactory == null ? null : aggregationSpecFactory.newInstance(options); @@ -374,7 +571,7 @@ public ReadQuery getQuery(QueryOptions options, long nowInSec) throws RequestVal nowInSec, getLimit(options), getPerPartitionLimit(options), - options.getPageSize(), + getOffset(options), getAggregationSpec(options)); } @@ -384,34 +581,52 @@ public ReadQuery getQuery(QueryOptions options, long nowInSec, int userLimit, int perPartitionLimit, - int pageSize, + int userOffset, AggregationSpecification aggregationSpec) { - DataLimits limit = getDataLimits(userLimit, perPartitionLimit, pageSize, aggregationSpec); + IndexRegistry indexRegistry = IndexRegistry.obtain(table); + RowFilter rowFilter = getRowFilter(options, state, indexRegistry); + DataLimits dataLimits = getDataLimits(state, userLimit, perPartitionLimit, userOffset, aggregationSpec); - return getQuery(options, state, columnFilter, nowInSec, limit); - } + if (restrictions.isKeyRange() && restrictions.usesSecondaryIndexing() && !SchemaConstants.isLocalSystemKeyspace(table.keyspace)) + Guardrails.nonPartitionRestrictedIndexQueryEnabled.ensureEnabled(state); - public ReadQuery getQuery(QueryOptions options, - ClientState state, - ColumnFilter columnFilter, - long nowInSec, - DataLimits limit) - { - RowFilter rowFilter = getRowFilter(options, state); + ReadQuery query = restrictions.isKeyRange() + ? getRangeCommand(options, state, columnFilter, rowFilter, dataLimits, nowInSec, indexRegistry) + : getSliceCommands(options, state, columnFilter, rowFilter, dataLimits, nowInSec, indexRegistry); - if (restrictions.isKeyRange()) + // Handle additional validation for topK queries + if (query.isTopK()) { - if (restrictions.usesSecondaryIndexing() && !SchemaConstants.isLocalSystemKeyspace(table.keyspace)) - Guardrails.nonPartitionRestrictedIndexQueryEnabled.ensureEnabled(state); + // We aren't going to allow SERIAL at all, so we can error out on those. + checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL || + options.getConsistency() == ConsistencyLevel.SERIAL, + String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency())); - return getRangeCommand(options, state, columnFilter, rowFilter, limit, nowInSec); + // Consistency levels with more than one replica are downgraded to ONE/LOCAL_ONE. + if (options.getConsistency() != ConsistencyLevel.ONE && + options.getConsistency() != ConsistencyLevel.LOCAL_ONE && + options.getConsistency() != ConsistencyLevel.NODE_LOCAL) + { + ConsistencyLevel supplied = options.getConsistency(); + ConsistencyLevel downgrade = supplied.isDatacenterLocal() ? ConsistencyLevel.LOCAL_ONE : ConsistencyLevel.ONE; + options.updateConsistency(downgrade); + ClientWarn.instance.warn(String.format(TOPK_CONSISTENCY_LEVEL_WARNING, supplied, downgrade)); + } + + // We don't support offset for top-k queries. + checkFalse(userOffset != NO_OFFSET, String.format(TOPK_OFFSET_ERROR, userOffset)); + + // We don't support aggregation for top-k queries because we don't support paging. + checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR); } - if (restrictions.usesSecondaryIndexing() && !rowFilter.isStrict()) - return getRangeCommand(options, state, columnFilter, rowFilter, limit, nowInSec); + query.validateSelectOptions(selectOptions, state); - return getSliceCommands(options, state, columnFilter, rowFilter, limit, nowInSec); + // If there's a secondary index that the command can use, have it validate the request parameters. + query.maybeValidateIndexes(); + + return query; } private ResultMessage.Rows execute(ReadQuery query, @@ -420,13 +635,14 @@ private ResultMessage.Rows execute(ReadQuery query, Selectors selectors, long nowInSec, int userLimit, + int userOffset, AggregationSpecification aggregationSpec, Dispatcher.RequestTime requestTime, boolean unmask) { try (PartitionIterator data = query.execute(options.getConsistency(), state, requestTime)) { - return processResults(data, options, selectors, nowInSec, userLimit, aggregationSpec, unmask, state); + return processResults(data, options, selectors, nowInSec, userLimit, userOffset, aggregationSpec, unmask, state); } } @@ -466,24 +682,33 @@ public PagingState state() return pager.state(); } - public abstract PartitionIterator fetchPage(int pageSize, Dispatcher.RequestTime requestTime); + public abstract PartitionIterator fetchPage(PageSize pageSize, Dispatcher.RequestTime requestTime); + + public abstract PartitionIterator readAll(PageSize pageSize, Dispatcher.RequestTime requestTime); public static class NormalPager extends Pager { private final ConsistencyLevel consistency; private final ClientState clientState; - private NormalPager(QueryPager pager, ConsistencyLevel consistency, ClientState clientState) + private NormalPager(QueryPager pager, ConsistencyLevel consistency, ClientState queryState) { super(pager); this.consistency = consistency; - this.clientState = clientState; + this.clientState = queryState; } - public PartitionIterator fetchPage(int pageSize, Dispatcher.RequestTime requestTime) + @Override + public PartitionIterator fetchPage(PageSize pageSize, Dispatcher.RequestTime requestTime) { return pager.fetchPage(pageSize, consistency, clientState, requestTime); } + + @Override + public PartitionIterator readAll(PageSize pageSize, Dispatcher.RequestTime requestTime) + { + return pager.readAll(pageSize, consistency, clientState, requestTime); + } } public static class InternalPager extends Pager @@ -496,10 +721,17 @@ private InternalPager(QueryPager pager, ReadExecutionController executionControl this.executionController = executionController; } - public PartitionIterator fetchPage(int pageSize, Dispatcher.RequestTime requestTime) + @Override + public PartitionIterator fetchPage(PageSize pageSize, Dispatcher.RequestTime requestTime) { return pager.fetchPageInternal(pageSize, executionController); } + + @Override + public PartitionIterator readAll(PageSize pageSize, Dispatcher.RequestTime requestTime) + { + return pager.readAllInternal(pageSize, executionController); + } } } @@ -507,14 +739,15 @@ private ResultMessage.Rows execute(QueryState state, Pager pager, QueryOptions options, Selectors selectors, - int pageSize, + PageSize pageSize, long nowInSec, int userLimit, + int userOffset, AggregationSpecification aggregationSpec, Dispatcher.RequestTime requestTime, boolean unmask) { - Guardrails.pageSize.guard(pageSize, table(), false, state.getClientState()); + pageSize.guard(table(), state.getClientState()); if (aggregationSpecFactory != null) { @@ -534,16 +767,28 @@ else if (restrictions.keyIsInRelation()) // We can't properly do post-query ordering if we page (see #6722) // For GROUP BY or aggregation queries we always page internally even if the user has turned paging off - checkFalse(pageSize > 0 && needsPostQueryOrdering(), + checkFalse(pageSize.isDefined() && needsPostQueryOrdering(), "Cannot page queries with both ORDER BY and a IN restriction on the partition key;" + " you must either remove the ORDER BY or the IN and sort client side, or disable paging for this query"); + // If the query has an offset we silently ignore user-facing paging and return all the rows specified by the + // limit/offset constraints in one go, since regular key-based paging is not supported when using limit/offest + // paging. However, we still use the query fetch size to internally page the rows. We do that to avoid loading + // in memory all the rows that will be discarded by the offset. Key-based paging is also disabled if the offset + // is explicitly set to zero. ResultMessage.Rows msg; - try (PartitionIterator page = pager.fetchPage(pageSize, requestTime)) + try (PartitionIterator partitions = userOffset == NO_OFFSET + ? pager.fetchPage(pageSize, requestTime) + : pager.readAll(pageSize, requestTime)) { - msg = processResults(page, options, selectors, nowInSec, userLimit, aggregationSpec, unmask, state.getClientState()); + msg = processResults(partitions, options, selectors, nowInSec, userLimit, userOffset, aggregationSpec, unmask, state.getClientState()); } + RequestSensors sensors = RequestTracker.instance.get(); + Context context = Context.from(this.table); + Type sensorType = Type.READ_BYTES; + SensorsCustomParams.addSensorToCQLResponse(msg, options.getProtocolVersion(), sensors, context, sensorType); + // Please note that the isExhausted state of the pager only gets updated when we've closed the page, so this // shouldn't be moved inside the 'try' above. if (!pager.isExhausted() && !pager.pager.isTopK()) @@ -563,14 +808,16 @@ private ResultMessage.Rows processResults(PartitionIterator partitions, Selectors selectors, long nowInSec, int userLimit, + int userOffset, AggregationSpecification aggregationSpec, boolean unmask, ClientState state) throws RequestValidationException { - ResultSet rset = process(partitions, options, selectors, nowInSec, userLimit, aggregationSpec, unmask, state); + ResultSet rset = process(partitions, options, selectors, nowInSec, userLimit, userOffset, aggregationSpec, unmask, state); return new ResultMessage.Rows(rset); } + @Override public ResultMessage.Rows executeLocally(QueryState state, QueryOptions options) throws RequestExecutionException, RequestValidationException { return executeInternal(state, options, options.getNowInSeconds(state), Dispatcher.RequestTime.forImmediateExecution()); @@ -583,7 +830,8 @@ public ResultMessage.Rows executeInternal(QueryState state, { int userLimit = getLimit(options); int userPerPartitionLimit = getPerPartitionLimit(options); - int pageSize = options.getPageSize(); + int userOffset = getOffset(options); + PageSize pageSize = options.getPageSize(); boolean unmask = state.getClientState().hasTablePermission(table, Permission.UNMASK); Selectors selectors = selection.newSelectors(options); @@ -594,16 +842,16 @@ public ResultMessage.Rows executeInternal(QueryState state, nowInSec, userLimit, userPerPartitionLimit, - pageSize, + userOffset, aggregationSpec); try (ReadExecutionController executionController = query.executionController()) { - if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize) || query.isTopK())) + if (aggregationSpec == null && canSkipPaging(query.limits(), pageSize, query.isTopK())) { try (PartitionIterator data = query.executeInternal(executionController)) { - return processResults(data, options, selectors, nowInSec, userLimit, null, unmask, state.getClientState()); + return processResults(data, options, selectors, nowInSec, userLimit, userOffset, null, unmask, state.getClientState()); } } @@ -616,33 +864,36 @@ public ResultMessage.Rows executeInternal(QueryState state, pageSize, nowInSec, userLimit, + userOffset, aggregationSpec, requestTime, unmask); } } - private QueryPager getPager(ReadQuery query, QueryOptions options) + @VisibleForTesting + public QueryPager getPager(ReadQuery query, QueryOptions options) { QueryPager pager = query.getPager(options.getPagingState(), options.getProtocolVersion()); if (aggregationSpecFactory == null || query.isEmpty()) return pager; - return new AggregationQueryPager(pager, query.limits()); + return new AggregationQueryPager(pager, DatabaseDescriptor.getAggregationSubPageSize(), query.limits(), DatabaseDescriptor.getAggregationRpcTimeout(TimeUnit.NANOSECONDS)); } public Map> executeRawInternal(QueryOptions options, ClientState state, long nowInSec) throws RequestExecutionException, RequestValidationException { int userLimit = getLimit(options); int userPerPartitionLimit = getPerPartitionLimit(options); - if (options.getPageSize() > 0) + int userOffset = getOffset(options); + if (options.getPageSize().isDefined()) throw new IllegalStateException(); if (aggregationSpecFactory != null) throw new IllegalStateException(); Selectors selectors = selection.newSelectors(options); - ReadQuery query = getQuery(options, state, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, Integer.MAX_VALUE, null); + ReadQuery query = getQuery(options, state, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, userOffset, null); Map> result = Collections.emptyMap(); try (ReadExecutionController executionController = query.executionController()) @@ -680,7 +931,7 @@ public ResultSet process(PartitionIterator partitions, long nowInSec, boolean un { QueryOptions options = QueryOptions.DEFAULT; Selectors selectors = selection.newSelectors(options); - return process(partitions, options, selectors, nowInSec, getLimit(options), getAggregationSpec(options), unmask, state); + return process(partitions, options, selectors, nowInSec, getLimit(options), getOffset(options), getAggregationSpec(options), unmask, state); } @Override @@ -711,7 +962,8 @@ public StatementRestrictions getRestrictions() } private ReadQuery getSliceCommands(QueryOptions options, ClientState state, ColumnFilter columnFilter, - RowFilter rowFilter, DataLimits limit, long nowInSec) + RowFilter rowFilter, DataLimits limit, long nowInSec, + IndexRegistry indexRegistry) { Collection keys = restrictions.getPartitionKeys(options, state); if (keys.isEmpty()) @@ -736,9 +988,6 @@ private ReadQuery getSliceCommands(QueryOptions options, ClientState state, Colu SinglePartitionReadQuery.Group group = SinglePartitionReadQuery.createGroup(table, nowInSec, columnFilter, rowFilter, limit, decoratedKeys, filter); - // If there's a secondary index that the commands can use, have it validate the request parameters. - group.maybeValidateIndex(); - return group; } @@ -775,7 +1024,7 @@ public SinglePartitionReadCommand internalReadForView(DecoratedKey key, long now ClientState state = ClientState.forInternalCalls(); ColumnFilter columnFilter = selection.newSelectors(options).getColumnFilter(); ClusteringIndexFilter filter = makeClusteringIndexFilter(options, state, columnFilter); - RowFilter rowFilter = getRowFilter(options, state); + RowFilter rowFilter = getRowFilter(options, state, IndexRegistry.EMPTY); return SinglePartitionReadCommand.create(table, nowInSec, columnFilter, rowFilter, DataLimits.NONE, key, filter); } @@ -784,11 +1033,14 @@ public SinglePartitionReadCommand internalReadForView(DecoratedKey key, long now */ public RowFilter rowFilterForInternalCalls() { - return getRowFilter(QueryOptions.forInternalCalls(Collections.emptyList()), ClientState.forInternalCalls()); + return getRowFilter(QueryOptions.forInternalCalls(Collections.emptyList()), + ClientState.forInternalCalls(), + IndexRegistry.EMPTY); } private ReadQuery getRangeCommand(QueryOptions options, ClientState state, ColumnFilter columnFilter, - RowFilter rowFilter, DataLimits limit, long nowInSec) + RowFilter rowFilter, DataLimits limit, long nowInSec, + IndexRegistry indexRegistry) { ClusteringIndexFilter clusteringIndexFilter = makeClusteringIndexFilter(options, state, columnFilter); if (clusteringIndexFilter == null) @@ -800,13 +1052,7 @@ private ReadQuery getRangeCommand(QueryOptions options, ClientState state, Colum if (keyBounds == null) return ReadQuery.empty(table); - ReadQuery command = - PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter)); - - // If there's a secondary index that the command can use, have it validate the request parameters. - command.maybeValidateIndex(); - - return command; + return PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter)); } private ClusteringIndexFilter makeClusteringIndexFilter(QueryOptions options, ClientState state, ColumnFilter columnFilter) @@ -823,6 +1069,11 @@ private ClusteringIndexFilter makeClusteringIndexFilter(QueryOptions options, Cl return new ClusteringIndexSliceFilter(Slices.ALL, false); } + if (restrictions.isDisjunction()) + { + return new ClusteringIndexSliceFilter(Slices.ALL, false); + } + if (restrictions.isColumnRange()) { Slices slices = makeSlices(options); @@ -878,46 +1129,68 @@ public Slices makeSlices(QueryOptions options) return builder.build(); } - private DataLimits getDataLimits(int userLimit, + private DataLimits getDataLimits(ClientState clientState, + int userLimit, int perPartitionLimit, - int pageSize, + int userOffset, AggregationSpecification aggregationSpec) { - int cqlRowLimit = DataLimits.NO_LIMIT; - int cqlPerPartitionLimit = DataLimits.NO_LIMIT; + assert userOffset == NO_OFFSET || userLimit != NO_LIMIT : "Cannot use OFFSET without LIMIT"; + + if (userOffset != NO_OFFSET) + Guardrails.offsetRows.guard(userOffset, "Select query", false, clientState); + + int fetchLimit = userLimit == NO_LIMIT || userOffset == NO_OFFSET ? userLimit : IntMath.saturatedAdd(userLimit, userOffset); + int cqlRowLimit = NO_LIMIT; + int cqlPerPartitionLimit = NO_LIMIT; // If we do post ordering we need to get all the results sorted before we can trim them. if (aggregationSpec != AggregationSpecification.AGGREGATE_EVERYTHING) { - // If we aren't need post-query ordering but we are doing index ordering (currently ANN only) then - // we do need to use the user limit. - if (!needsPostQueryOrdering() || needIndexOrdering()) - cqlRowLimit = userLimit; + if (!needsToSkipUserLimit()) + cqlRowLimit = fetchLimit; cqlPerPartitionLimit = perPartitionLimit; } - // Group by and aggregation queries will always be paged internally to avoid OOM. - // If the user provided a pageSize we'll use that to page internally (because why not), otherwise we use our default - if (pageSize <= 0) - pageSize = DEFAULT_PAGE_SIZE; + DataLimits limits = null; // Aggregation queries work fine on top of the group by paging but to maintain // backward compatibility we need to use the old way. if (aggregationSpec != null && aggregationSpec != AggregationSpecification.AGGREGATE_EVERYTHING) { if (parameters.isDistinct) - return DataLimits.distinctLimits(cqlRowLimit); - - return DataLimits.groupByLimits(cqlRowLimit, - cqlPerPartitionLimit, - pageSize, - aggregationSpec); + limits = DataLimits.distinctLimits(cqlRowLimit); + else + limits = DataLimits.groupByLimits(cqlRowLimit, + cqlPerPartitionLimit, + NO_LIMIT, + NO_LIMIT, + aggregationSpec); + } + else + { + if (parameters.isDistinct) + limits = cqlRowLimit == NO_LIMIT ? DataLimits.DISTINCT_NONE : DataLimits.distinctLimits(cqlRowLimit); + else + limits = DataLimits.cqlLimits(cqlRowLimit, cqlPerPartitionLimit); } - if (parameters.isDistinct) - return cqlRowLimit == DataLimits.NO_LIMIT ? DataLimits.DISTINCT_NONE : DataLimits.distinctLimits(cqlRowLimit); + if (!limits.isGroupByLimit()) + { + // if the user does not specify any limit, it means there is no limit - when the guardrail is defined, we + // want to limit the number of rows returned to the user + if (Guardrails.pageWeight.enabled(clientState)) + { + DataStorageSpec.IntBytesBound pageWeightFailThreshold = GuardrailsConfigProvider.instance.getOrCreate(clientState).getPageWeightFailThreshold(); + int bytesLimit = pageWeightFailThreshold == null ? NO_LIMIT : pageWeightFailThreshold.toBytes(); + String limitStr = USAGE_WARNING_PAGE_WEIGHT + FBUtilities.prettyPrintMemory(bytesLimit); + ClientWarn.instance.warn(limitStr); + logger.trace(limitStr); + limits = limits.forPaging(PageSize.inBytes(bytesLimit)); + } + } - return DataLimits.cqlLimits(cqlRowLimit, cqlPerPartitionLimit); + return limits; } /** @@ -946,7 +1219,7 @@ public int getPerPartitionLimit(QueryOptions options) private int getLimit(Term limit, QueryOptions options) { - int userLimit = DataLimits.NO_LIMIT; + int userLimit = NO_LIMIT; if (limit != null) { @@ -969,6 +1242,31 @@ private int getLimit(Term limit, QueryOptions options) return userLimit; } + public int getOffset(QueryOptions options) + { + int userOffset = NO_OFFSET; + + if (offset != null) + { + ByteBuffer b = checkNotNull(offset.bindAndGet(options), "Invalid null value of offset"); + // treat UNSET limit value as zero + if (b != UNSET_BYTE_BUFFER) + { + try + { + Int32Type.instance.validate(b); + userOffset = Int32Type.instance.compose(b); + checkTrue(userOffset >= 0, "Offset must be positive"); + } + catch (MarshalException e) + { + throw new InvalidRequestException("Invalid offset value"); + } + } + } + return userOffset; + } + private NavigableSet> getRequestedRows(QueryOptions options, ClientState state) throws InvalidRequestException { // Note: getRequestedColumns don't handle static columns, but due to CASSANDRA-5762 @@ -983,7 +1281,17 @@ private NavigableSet> getRequestedRows(QueryOptions options, Clien public RowFilter getRowFilter(QueryOptions options, ClientState state) throws InvalidRequestException { IndexRegistry indexRegistry = IndexRegistry.obtain(table); - RowFilter filter = restrictions.getRowFilter(indexRegistry, options); + RowFilter filter = restrictions.getRowFilter(indexRegistry, options, state, selectOptions); + + if (filter.needsReconciliation() && filter.isMutableIntersection() && restrictions.needFiltering(table)) + Guardrails.intersectFilteringQueryEnabled.ensureEnabled(state); + + return filter; + } + + public RowFilter getRowFilter(QueryOptions options, ClientState state, IndexRegistry indexRegistry) throws InvalidRequestException + { + RowFilter filter = restrictions.getRowFilter(indexRegistry, options, state, selectOptions); if (filter.needsReconciliation() && filter.isMutableIntersection() && restrictions.needFiltering(table)) Guardrails.intersectFilteringQueryEnabled.ensureEnabled(state); @@ -996,12 +1304,14 @@ private ResultSet process(PartitionIterator partitions, Selectors selectors, long nowInSec, int userLimit, + int userOffset, AggregationSpecification aggregationSpec, boolean unmask, ClientState state) throws InvalidRequestException { GroupMaker groupMaker = aggregationSpec == null ? null : aggregationSpec.newGroupMaker(); - ResultSetBuilder result = new ResultSetBuilder(getResultMetadata(), selectors, unmask, groupMaker); + SortedRowsBuilder rows = sortedRowsBuilder(userLimit, userOffset == NO_OFFSET ? 0 : userOffset); + ResultSetBuilder result = new ResultSetBuilder(getResultMetadata(), selectors, unmask, groupMaker, rows); while (partitions.hasNext()) { @@ -1011,13 +1321,10 @@ private ResultSet process(PartitionIterator partitions, } } + // maybeWarn requires the result set to be built ResultSet cqlRows = result.build(); maybeWarn(result, options); - orderResults(cqlRows, options, state); - - cqlRows.trim(userLimit); - return cqlRows; } @@ -1075,7 +1382,8 @@ private void maybeFail(ResultSetBuilder result, QueryOptions options) // to work around this, treat the coordinator as the only response we care about and mark it failed ReadSizeAbortException exception = new ReadSizeAbortException(clientMsg, options.getConsistency(), 0, 1, true, ImmutableMap.of(FBUtilities.getBroadcastAddressAndPort(), RequestFailureReason.READ_SIZE)); - StorageProxy.recordReadRegularAbort(options.getConsistency(), exception); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(options.getKeyspace()); + StorageProxy.recordReadRegularAbort(options.getConsistency(), exception, metrics); throw exception; } } @@ -1113,7 +1421,7 @@ public void processPartition(RowIterator partition, QueryOptions options, Result result.add(partition.staticRow().getColumnData(def), nowInSec); break; default: - result.add((ByteBuffer)null); + result.add(null); } } } @@ -1142,17 +1450,27 @@ public void processPartition(RowIterator partition, QueryOptions options, Result case CLUSTERING: result.add(row.clustering().bufferAt(def.position())); break; + case SYNTHETIC: + // treat as REGULAR case REGULAR: result.add(row.getColumnData(def), nowInSec); break; case STATIC: result.add(staticRow.getColumnData(def), nowInSec); break; + default: + throw new AssertionError(); } } } } + private boolean needsToSkipUserLimit() + { + // if post query ordering is required, and it's not ordered by an index + return needsPostQueryOrdering() && !needIndexOrdering(); + } + private boolean needsPostQueryOrdering() { // We need post-query ordering only for queries with IN on the partition key and an ORDER BY or index restriction reordering @@ -1165,35 +1483,42 @@ private boolean needIndexOrdering() } /** - * Orders results when multiple keys are selected (using IN). - *

- * In the case of ANN ordering the rows are first ordered in index column order and then by primary key. + * Orders results when multiple keys are selected (using IN) */ - private void orderResults(ResultSet cqlRows, QueryOptions options, ClientState state) + public SortedRowsBuilder sortedRowsBuilder(int limit, int offset) { - if (cqlRows.size() == 0 || !needsPostQueryOrdering()) - return; + assert (orderingComparator != null) == needsPostQueryOrdering() + : String.format("orderingComparator: %s, needsPostQueryOrdering: %s", + orderingComparator, needsPostQueryOrdering()); - Comparator> comparator = orderingComparator.prepareFor(table, getRowFilter(options, state), options); - if (comparator != null) - cqlRows.rows.sort(comparator); + if (orderingComparator == null || orderingComparator.indexOrdering()) + { + return SortedRowsBuilder.create(limit, offset); + } + else + { + return SortedRowsBuilder.create(limit, offset, orderingComparator); + } } - public static class RawStatement extends QualifiedStatement + public static class RawStatement extends QualifiedStatement { public final Parameters parameters; public final List selectClause; public final WhereClause whereClause; public final Term.Raw limit; public final Term.Raw perPartitionLimit; - private ClientState state; + public final Term.Raw offset; + public final SelectOptions options; public RawStatement(QualifiedName cfName, Parameters parameters, List selectClause, WhereClause whereClause, Term.Raw limit, - Term.Raw perPartitionLimit) + Term.Raw perPartitionLimit, + Term.Raw offset, + SelectOptions options) { super(cfName); this.parameters = parameters; @@ -1201,31 +1526,49 @@ public RawStatement(QualifiedName cfName, this.whereClause = whereClause; this.limit = limit; this.perPartitionLimit = perPartitionLimit; + this.offset = offset; + this.options = options; } - public SelectStatement prepare(ClientState state) + @Override + public SelectStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - // Cache locally for use by Guardrails - this.state = state; - return prepare(state, false); + setKeyspace(state); + return prepare(state, false, keyspaceMapper); } - public SelectStatement prepare(ClientState state, boolean forView) throws InvalidRequestException + public SelectStatement prepare(ClientState state, boolean forView, UnaryOperator keyspaceMapper) throws InvalidRequestException { - TableMetadata table = Schema.instance.validateTable(keyspace(), name()); + String ks = keyspaceMapper.apply(keyspace()); + TableMetadata table = Schema.instance.validateTable(ks, name()); + + // Besides actual restrictions (where clauses), prepareRestrictions will include the user-provided index hints, + // which are needed to determine what indexes to use for the query and to validate whether filtering is needed. + IndexHints indexHints = options.parseIndexHints(table, IndexRegistry.obtain(table)); List selectables = RawSelector.toSelectables(selectClause, table); boolean containsOnlyStaticColumns = selectOnlyStaticColumns(table, selectables); + // Besides actual restrictions (where clauses), prepareRestrictions will include pseudo-restrictions + // on indexed columns to allow pushing ORDER BY into the index; see StatementRestrictions::addOrderingRestrictions. + // Therefore, we don't want to convert an ANN Ordering column into a +score column until after that. List orderings = getOrderings(table); - StatementRestrictions restrictions = prepareRestrictions(state, table, bindVariables, orderings, containsOnlyStaticColumns, forView); + + StatementRestrictions restrictions = prepareRestrictions( + state, table, bindVariables, orderings, indexHints, containsOnlyStaticColumns, forView); // If we order post-query, the sorted column needs to be in the ResultSet for sorting, // even if we don't ultimately ship them to the client (CASSANDRA-4911). Map orderingColumns = getOrderingColumns(orderings); + // +score column for ANN/BM25 + var scoreOrdering = getScoreOrdering(orderings); + assert scoreOrdering == null || orderingColumns.isEmpty() : "can't have both scored ordering and column ordering"; + if (scoreOrdering != null) + orderingColumns = scoreOrdering; Set resultSetOrderingColumns = getResultSetOrdering(restrictions, orderingColumns); - Selection selection = prepareSelection(table, + Selection selection = prepareSelection(state, + table, selectables, bindVariables, resultSetOrderingColumns, @@ -1253,16 +1596,19 @@ public SelectStatement prepare(ClientState state, boolean forView) throws Invali if (!orderingColumns.isEmpty()) { assert !forView; - verifyOrderingIsAllowed(restrictions, orderingColumns); + verifyOrderingIsAllowed(table, restrictions, orderingColumns); orderingComparator = getOrderingComparator(selection, restrictions, orderingColumns); - isReversed = isReversed(table, orderingColumns, restrictions); + isReversed = isReversed(table, orderingColumns); if (isReversed && orderingComparator != null) orderingComparator = orderingComparator.reverse(); - } + } - checkNeedsFiltering(table, restrictions); + checkNeedsFiltering(table, restrictions, state); - return new SelectStatement(table, + checkDisjunctionIsSupported(table, restrictions); + + return new SelectStatement(rawCQLStatement, + table, bindVariables, parameters, selection, @@ -1270,8 +1616,25 @@ public SelectStatement prepare(ClientState state, boolean forView) throws Invali isReversed, aggregationSpecFactory, orderingComparator, - prepareLimit(bindVariables, limit, keyspace(), limitReceiver()), - prepareLimit(bindVariables, perPartitionLimit, keyspace(), perPartitionLimitReceiver())); + prepareLimit(bindVariables, limit, ks, limitReceiver()), + prepareLimit(bindVariables, perPartitionLimit, ks, perPartitionLimitReceiver()), + prepareLimit(bindVariables, offset, ks, offsetReceiver()), + options); + } + + private Map getScoreOrdering(List orderings) + { + if (orderings.isEmpty()) + return null; + + var expr = orderings.get(0).expression; + if (!expr.isScored()) + return null; + + // Create synthetic score column + ColumnMetadata sourceColumn = expr.getColumn(); + var cm = ColumnMetadata.syntheticScoreColumn(sourceColumn, FloatType.instance); + return Map.of(cm, orderings.get(0)); } private Set getResultSetOrdering(StatementRestrictions restrictions, Map orderingColumns) @@ -1281,7 +1644,8 @@ private Set getResultSetOrdering(StatementRestrictions restricti return Collections.emptySet(); } - private Selection prepareSelection(TableMetadata table, + private Selection prepareSelection(ClientState state, + TableMetadata table, List selectables, VariableSpecifications boundNames, Set resultSetOrderingColumns, @@ -1299,7 +1663,7 @@ private Selection prepareSelection(TableMetadata table, { return hasGroupBy || table.hasMaskedColumns() ? Selection.wildcardWithGroupByOrMaskedColumns(table, boundNames, resultSetOrderingColumns, isJson, returnStaticContentOnPartitionWithNoRows) - : Selection.wildcard(table, isJson, returnStaticContentOnPartitionWithNoRows); + : Selection.wildcard(table, resultSetOrderingColumns, isJson, returnStaticContentOnPartitionWithNoRows); } return Selection.fromSelectors(table, @@ -1341,20 +1705,21 @@ private Map getOrderingColumns(List ordering if (orderings.isEmpty()) return Collections.emptyMap(); - Map orderingColumns = new LinkedHashMap<>(); - for (Ordering ordering : orderings) - { - ColumnMetadata column = ordering.expression.getColumn(); - orderingColumns.put(column, ordering); - } - return orderingColumns; + return orderings.stream() + .filter(ordering -> !ordering.expression.isScored()) + .collect(Collectors.toMap(ordering -> ordering.expression.getColumn(), + ordering -> ordering, + (a, b) -> { + throw new IllegalStateException("Duplicate keys"); + }, + LinkedHashMap::new)); } private List getOrderings(TableMetadata table) { return parameters.orderings.stream() - .map(o -> o.bind(table, bindVariables)) - .collect(Collectors.toList()); + .map(o -> o.bind(table, bindVariables)) + .collect(Collectors.toList()); } /** @@ -1362,6 +1727,8 @@ private List getOrderings(TableMetadata table) * * @param metadata the column family meta data * @param boundNames the variable specifications + * @param orderings the orderings + * @param indexHints the index hints * @param selectsOnlyStaticColumns {@code true} if the query select only static columns, {@code false} otherwise. * @return the restrictions * @throws InvalidRequestException if a problem occurs while building the restrictions @@ -1370,18 +1737,20 @@ private StatementRestrictions prepareRestrictions(ClientState state, TableMetadata metadata, VariableSpecifications boundNames, List orderings, + IndexHints indexHints, boolean selectsOnlyStaticColumns, boolean forView) throws InvalidRequestException { - return new StatementRestrictions(state, + return StatementRestrictions.create(state, StatementType.SELECT, - metadata, - whereClause, - boundNames, - orderings, - selectsOnlyStaticColumns, - parameters.allowFiltering, - forView); + metadata, + whereClause, + boundNames, + orderings, + indexHints, + selectsOnlyStaticColumns, + parameters.allowFiltering, + forView); } /** Returns a Term for the limit or null if no limit is set */ @@ -1396,12 +1765,28 @@ private Term prepareLimit(VariableSpecifications boundNames, Term.Raw limit, return prepLimit; } - private static void verifyOrderingIsAllowed(StatementRestrictions restrictions, Map orderingColumns) throws InvalidRequestException + private static void verifyOrderingIsAllowed(TableMetadata table, StatementRestrictions restrictions, Map orderingColumns) throws InvalidRequestException { if (orderingColumns.values().stream().anyMatch(o -> o.expression.hasNonClusteredOrdering())) return; - checkFalse(restrictions.usesSecondaryIndexing(), "ORDER BY with 2ndary indexes is not supported, except for ANN queries."); + + checkFalse(restrictions.usesSecondaryIndexing(), "ORDER BY with 2ndary indexes is not supported."); checkFalse(restrictions.isKeyRange(), "ORDER BY is only supported when the partition key is restricted by an EQ or an IN."); + + // check that clustering columns are valid + int i = 0; + for (var entry : orderingColumns.entrySet()) + { + ColumnMetadata def = entry.getKey(); + checkTrue(def.isClusteringColumn(), + "Order by is currently only supported on indexed columns and the clustered columns of the PRIMARY KEY, got %s", def.name); + while (i != def.position()) + { + checkTrue(restrictions.isColumnRestrictedByEq(table.clusteringColumns().get(i++)), + "Ordering by clustered columns must follow the declared order in the PRIMARY KEY"); + } + i++; + } } private static void validateDistinctSelection(TableMetadata metadata, @@ -1435,7 +1820,7 @@ private static void validateDistinctSelection(TableMetadata metadata, * @param metadata the table metadata * @param selection the selection * @param restrictions the restrictions - * @param isDistinct true if the query is a DISTINCT one. + * @param isDistinct true if the query is a DISTINCT one. * @return the {@code AggregationSpecification.Factory} used to make the aggregates */ private AggregationSpecification.Factory getAggregationSpecFactory(TableMetadata metadata, @@ -1529,14 +1914,15 @@ private void validateGroupByFunction(WithFunction withFunction) private ColumnComparator> getOrderingComparator(Selection selection, StatementRestrictions restrictions, - Map orderingColumns) throws InvalidRequestException + Map orderingColumns) + throws InvalidRequestException { for (Map.Entry e : orderingColumns.entrySet()) { if (e.getValue().expression.hasNonClusteredOrdering()) { Preconditions.checkState(orderingColumns.size() == 1); - return new IndexColumnComparator(e.getValue().expression.toRestriction(), selection.getOrderingIndex(e.getKey())); + return new IndexColumnComparator(); } } @@ -1551,37 +1937,35 @@ private ColumnComparator> getOrderingComparator(Selection selec idToSort.add(selection.getOrderingIndex(orderingColumn)); sorters.add(orderingColumn.type); } + return idToSort.size() == 1 ? new SingleColumnComparator(idToSort.get(0), sorters.get(0)) : new CompositeComparator(sorters, idToSort); } - private boolean isReversed(TableMetadata table, Map orderingColumns, StatementRestrictions restrictions) throws InvalidRequestException + private boolean isReversed(TableMetadata table, Map orderingColumns) throws InvalidRequestException { + // Nonclustered ordering handles descending logic through ScoreOrderedResultRetriever and TKP if (orderingColumns.values().stream().anyMatch(o -> o.expression.hasNonClusteredOrdering())) return false; - Boolean[] reversedMap = new Boolean[table.clusteringColumns().size()]; - int i = 0; - for (Map.Entry entry : orderingColumns.entrySet()) + + Boolean[] clusteredMap = new Boolean[table.clusteringColumns().size()]; + for (var entry : orderingColumns.entrySet()) { ColumnMetadata def = entry.getKey(); Ordering ordering = entry.getValue(); - boolean reversed = ordering.direction == Ordering.Direction.DESC; - - checkTrue(def.isClusteringColumn(), - "Order by is currently only supported on the clustered columns of the PRIMARY KEY, got %s", def.name); - - while (i != def.position()) - { - checkTrue(restrictions.isColumnRestrictedByEq(table.clusteringColumns().get(i++)), - "Order by currently only supports the ordering of columns following their declared order in the PRIMARY KEY"); - } - i++; - reversedMap[def.position()] = (reversed != def.isReversedType()); + // We defined ANN OF to be ASC ordering, as in, "order by near-ness". But since score goes from + // 0 (worst) to 1 (closest), we need to reverse the ordering for the comparator when we're sorting + // by synthetic +score column. + boolean cqlReversed = ordering.direction == Ordering.Direction.DESC; + if (def.position() == ColumnMetadata.NO_POSITION) + return ordering.expression.isScored() || cqlReversed; + else + clusteredMap[def.position()] = (cqlReversed != def.isReversedType()); } - // Check that all boolean in reversedMap, if set, agrees + // Check that all boolean in clusteredMap, if set, agrees Boolean isReversed = null; - for (Boolean b : reversedMap) + for (Boolean b : clusteredMap) { // Column on which order is specified can be in any order if (b == null) @@ -1598,16 +1982,43 @@ private boolean isReversed(TableMetadata table, Map or return isReversed; } + /** + * This verifies that if the expression contains a disjunction - "value = 1 or value = 2" or "value in (1, 2)" + * the indexes involved in the query support disjunction. + */ + private void checkDisjunctionIsSupported(TableMetadata table, StatementRestrictions restrictions) throws InvalidRequestException + { + if (!parameters.allowFiltering && + restrictions.usesSecondaryIndexing() && + restrictions.needsDisjunctionSupport(table)) + { + restrictions.throwsRequiresIndexSupportingDisjunctionError(); + } + } + /** If ALLOW FILTERING was not specified, this verifies that it is not needed */ - private void checkNeedsFiltering(TableMetadata table, StatementRestrictions restrictions) throws InvalidRequestException + private void checkNeedsFiltering(TableMetadata table, StatementRestrictions restrictions, ClientState state) throws InvalidRequestException { + if (parameters.allowFiltering && restrictions.hasIndxBasedOrdering()) + { + // ANN queries do not currently work correctly when filtering is required, so + // we fail even though ALLOW FILTERING was passed + if (restrictions.needFiltering(table)) + throw invalidRequest(StatementRestrictions.NON_CLUSTER_ORDERING_REQUIRES_ALL_RESTRICTED_NON_PARTITION_KEY_COLUMNS_INDEXED_MESSAGE); + } // non-key-range non-indexed queries cannot involve filtering underneath if (!parameters.allowFiltering && (restrictions.isKeyRange() || restrictions.usesSecondaryIndexing())) { // We will potentially filter data if the row filter is not the identity and there isn't any index group // supporting all the expressions in the filter. - if (restrictions.requiresAllowFilteringIfNotSpecified()) - checkFalse(restrictions.needFiltering(table), StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE); + if (restrictions.needFiltering(table)) + { + restrictions.throwRequiresAllowFilteringError(table, state); + } + if (restrictions.hasClusteringColumnsRestrictions() + && restrictions.hasIndxBasedOrdering() + && restrictions.hasClusterColumnRestrictionWithoutSupportingIndex(table)) + restrictions.throwRequiresAllowFilteringError(table, state); } } @@ -1621,6 +2032,11 @@ private ColumnSpecification perPartitionLimitReceiver() return new ColumnSpecification(keyspace(), name(), new ColumnIdentifier("[per_partition_limit]", true), Int32Type.instance); } + private ColumnSpecification offsetReceiver() + { + return new ColumnSpecification(keyspace(), name(), new ColumnIdentifier("[offset]", true), Int32Type.instance); + } + @Override public String toString() { @@ -1678,14 +2094,6 @@ public boolean indexOrdering() { return false; } - - /** - * Produces a prepared {@link ColumnComparator} for current table and query-options - */ - public Comparator prepareFor(TableMetadata table, RowFilter rowFilter, QueryOptions options) - { - return this; - } } private static class ReversedColumnComparator extends ColumnComparator @@ -1702,6 +2110,12 @@ public int compare(T o1, T o2) { return wrapped.compare(o2, o1); } + + @Override + public boolean indexOrdering() + { + return wrapped.indexOrdering(); + } } /** @@ -1726,35 +2140,12 @@ public int compare(List a, List b) private static class IndexColumnComparator extends ColumnComparator> { - private final SingleRestriction restriction; - private final int columnIndex; - - public IndexColumnComparator(SingleRestriction restriction, int columnIndex) - { - this.restriction = restriction; - this.columnIndex = columnIndex; - } - @Override public boolean indexOrdering() { return true; } - @Override - public Comparator> prepareFor(TableMetadata table, RowFilter rowFilter, QueryOptions options) - { - if (table.indexes.isEmpty() || rowFilter.isEmpty()) - return this; - - Index.QueryPlan indexQueryPlan = Keyspace.openAndGetStore(table).indexManager.getBestIndexQueryPlanFor(rowFilter); - - Index index = restriction.findSupportingIndexFromQueryPlan(indexQueryPlan); - assert index != null; - Comparator comparator = index.getPostQueryOrdering(restriction, options); - return (a, b) -> compare(comparator, a.get(columnIndex), b.get(columnIndex)); - } - @Override public int compare(List o1, List o2) { @@ -1792,7 +2183,7 @@ public int compare(List a, List b) return 0; } } - + @Override public String toString() { @@ -1836,7 +2227,7 @@ private String asCQL(QueryOptions options, ClientState state) ColumnFilter columnFilter = selection.newSelectors(options).getColumnFilter(); StringBuilder sb = new StringBuilder(); - sb.append("SELECT ").append(queriedColumns().toCQLString()); + sb.append("SELECT ").append(queriedColumns().toCQLString(Redaction.NONE)); sb.append(" FROM ").append(table.keyspace).append('.').append(table.name); if (restrictions.isKeyRange() || restrictions.usesSecondaryIndexing()) { @@ -1866,7 +2257,7 @@ private String asCQL(QueryOptions options, ClientState state) sb.append(" AND "); } if (!dataRange.isUnrestricted(table)) - sb.append(dataRange.toCQLString(table, rowFilter)); + sb.append(dataRange.toCQLString(table, rowFilter, Redaction.NONE)); } } else @@ -1890,7 +2281,7 @@ private String asCQL(QueryOptions options, ClientState state) { sb.append(" = "); if (compoundPk) sb.append('('); - DataRange.appendKeyString(sb, table.partitionKeyType, Iterables.getOnlyElement(keys)); + DataRange.appendKeyString(sb, table.partitionKeyType, Iterables.getOnlyElement(keys), Redaction.NONE); if (compoundPk) sb.append(')'); } else @@ -1903,7 +2294,7 @@ private String asCQL(QueryOptions options, ClientState state) sb.append(", "); if (compoundPk) sb.append('('); - DataRange.appendKeyString(sb, table.partitionKeyType, key); + DataRange.appendKeyString(sb, table.partitionKeyType, key, Redaction.NONE); if (compoundPk) sb.append(')'); first = false; } @@ -1915,12 +2306,12 @@ private String asCQL(QueryOptions options, ClientState state) if (!rowFilter.isEmpty()) sb.append(" AND ").append(rowFilter); - String filterString = filter.toCQLString(table, rowFilter); + String filterString = filter.toCQLString(table, rowFilter, Redaction.NONE); if (!filterString.isEmpty()) sb.append(" AND ").append(filterString); } - DataLimits limits = getDataLimits(getLimit(options), getPerPartitionLimit(options), options.getPageSize(), getAggregationSpec(options)); + DataLimits limits = getDataLimits(state, getLimit(options), getPerPartitionLimit(options), getOffset(options), getAggregationSpec(options)); if (limits != DataLimits.NONE) sb.append(' ').append(limits); return sb.toString(); diff --git a/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java index 5ff299eb88d4..3a5fa9ce7a2a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java @@ -34,9 +34,9 @@ import org.apache.cassandra.db.commitlog.CommitLogSegment; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.virtual.VirtualMutation; -import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.StorageCompatibilityMode; /** * Utility class to collect updates. @@ -83,7 +83,7 @@ public PartitionUpdate.Builder getPartitionUpdateBuilder(TableMetadata metadata, PartitionUpdate.Builder builder = puBuilders.get(dk.getKey()); if (builder == null) { - builder = new PartitionUpdate.Builder(metadata, dk, updatedColumns, perPartitionKeyCounts.count(dk.getKey())); + builder = PartitionUpdate.builder(metadata, dk, updatedColumns, perPartitionKeyCounts.count(dk.getKey())); puBuilders.put(dk.getKey(), builder); } return builder; @@ -109,7 +109,7 @@ else if (metadata.isCounter()) mutation = new Mutation(builder.build()); mutation.validateIndexedColumns(state); - mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); + mutation.validateSize(StorageCompatibilityMode.current().storageMessagingVersion(), CommitLogSegment.ENTRY_OVERHEAD_SIZE); ms.add(mutation); } diff --git a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java index 2d1d58c7aa23..982c4c428347 100644 --- a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java @@ -17,17 +17,27 @@ */ package org.apache.cassandra.cql3.statements; +import java.lang.reflect.InvocationTargetException; import java.util.concurrent.TimeoutException; +import java.util.function.UnaryOperator; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; -import org.apache.cassandra.exceptions.*; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.TruncateException; +import org.apache.cassandra.exceptions.UnauthorizedException; +import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; @@ -36,19 +46,36 @@ import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.messages.ResultMessage; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.TRUNCATE_STATEMENT_PROVIDER; -public class TruncateStatement extends QualifiedStatement implements CQLStatement +public class TruncateStatement implements CQLStatement, CQLStatement.SingleKeyspaceCqlStatement { - public TruncateStatement(QualifiedName name) + private final String rawCQLStatement; + private final QualifiedName qualifiedName; + + public TruncateStatement(String queryString, QualifiedName name) + { + this.rawCQLStatement = queryString; + this.qualifiedName = name; + } + + @Override + public String getRawCQLStatement() { - super(name); + return rawCQLStatement; } - public TruncateStatement prepare(ClientState state) + @Override + public String keyspace() { - return this; + return qualifiedName.getKeyspace(); + } + + public String name() + { + return qualifiedName.getName(); } public void authorize(ClientState state) throws InvalidRequestException, UnauthorizedException @@ -56,6 +83,7 @@ public void authorize(ClientState state) throws InvalidRequestException, Unautho state.ensureTablePermission(keyspace(), name(), Permission.MODIFY); } + @Override public void validate(ClientState state) throws InvalidRequestException { Schema.instance.validateTable(keyspace(), name()); @@ -68,6 +96,9 @@ public ResultMessage execute(QueryState state, QueryOptions options, Dispatcher. try { TableMetadata metaData = Schema.instance.getTableMetadata(keyspace(), name()); + if (metaData == null) + throw new InvalidRequestException(String.format("Unknown keyspace/table %s.%s", keyspace(), name())); + if (metaData.isView()) throw new InvalidRequestException("Cannot TRUNCATE materialized view directly; must truncate base table instead"); @@ -77,10 +108,10 @@ public ResultMessage execute(QueryState state, QueryOptions options, Dispatcher. } else { - StorageProxy.truncateBlocking(keyspace(), name()); + doTruncateBlocking(); } } - catch (UnavailableException | TimeoutException e) + catch (UnavailableException | TimeoutException | InvalidRequestException e) { throw new TruncateException(e); } @@ -92,6 +123,9 @@ public ResultMessage executeLocally(QueryState state, QueryOptions options) try { TableMetadata metaData = Schema.instance.getTableMetadata(keyspace(), name()); + if (metaData == null) + throw new InvalidRequestException(String.format("Unknown keyspace/table %s.%s", keyspace(), name())); + if (metaData.isView()) throw new InvalidRequestException("Cannot TRUNCATE materialized view directly; must truncate base table instead"); @@ -117,6 +151,11 @@ private void executeForVirtualTable(TableId id) VirtualKeyspaceRegistry.instance.getTableNullable(id).truncate(); } + protected void doTruncateBlocking() throws TimeoutException + { + StorageProxy.instance.truncateBlocking(keyspace(), name()); + } + @Override public String toString() { @@ -128,4 +167,57 @@ public AuditLogContext getAuditLogContext() { return new AuditLogContext(AuditLogEntryType.TRUNCATE, keyspace(), name()); } + + public static final class Raw extends QualifiedStatement + { + public Raw(QualifiedName name) + { + super(name); + } + + @Override + public TruncateStatement prepare(ClientState state, UnaryOperator keyspaceMapper) + { + setKeyspace(state); + String ks = keyspaceMapper.apply(keyspace()); + QualifiedName qual = qualifiedName; + if (!ks.equals(qual.getKeyspace())) + qual = new QualifiedName(ks, qual.getName()); + return provider.createTruncateStatement(rawCQLStatement, qual); + } + } + + private static TruncateStatementProvider getProviderFromProperty() + { + try + { + return (TruncateStatementProvider)FBUtilities.classForName(TRUNCATE_STATEMENT_PROVIDER.getString(), + "Truncate statement provider") + .getConstructor().newInstance(); + } + catch (NoSuchMethodException | IllegalAccessException | InstantiationException | + InvocationTargetException e) + { + throw new RuntimeException("Unable to find a truncate statement provider with name " + + TRUNCATE_STATEMENT_PROVIDER.getString(), e); + } + } + + private static final TruncateStatementProvider provider = TRUNCATE_STATEMENT_PROVIDER.isPresent() ? + getProviderFromProperty() : + new DefaultTruncateStatementProvider(); + + public static interface TruncateStatementProvider + { + public TruncateStatement createTruncateStatement(String rawCQLStatement, QualifiedName qual); + } + + public static final class DefaultTruncateStatementProvider implements TruncateStatementProvider + { + @Override + public TruncateStatement createTruncateStatement(String rawCQLStatement, QualifiedName qual) + { + return new TruncateStatement(rawCQLStatement, qual); + } + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index d3f605a00865..2d890f188c33 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -21,22 +21,45 @@ import java.util.Collections; import java.util.List; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.Attributes; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Json; +import org.apache.cassandra.cql3.Operation; +import org.apache.cassandra.cql3.Operations; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.SingleColumnRelation; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.UpdateParameters; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; import org.apache.cassandra.cql3.conditions.ColumnCondition; import org.apache.cassandra.cql3.conditions.Conditions; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.sensors.SensorsCustomParams; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import static org.apache.cassandra.cql3.statements.RequestValidations.checkContainsNoDuplicates; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; @@ -49,7 +72,8 @@ public class UpdateStatement extends ModificationStatement { private static final Constants.Value EMPTY = new Constants.Value(ByteBufferUtil.EMPTY_BYTE_BUFFER); - private UpdateStatement(StatementType type, + private UpdateStatement(String queryString, + StatementType type, VariableSpecifications bindVariables, TableMetadata metadata, Operations operations, @@ -57,7 +81,7 @@ private UpdateStatement(StatementType type, Conditions conditions, Attributes attrs) { - super(type, bindVariables, metadata, operations, restrictions, conditions, attrs); + super(queryString, type, bindVariables, metadata, operations, restrictions, conditions, attrs); } @Override @@ -110,6 +134,23 @@ public void addUpdateForKey(PartitionUpdate.Builder update, Slice slice, UpdateP throw new UnsupportedOperationException(); } + @Override + public ResultMessage execute(QueryState state, QueryOptions options, Dispatcher.RequestTime requestTime) + { + ResultMessage result = super.execute(state, options, requestTime); + + if (result == null) + result = new ResultMessage.Void(); + + RequestSensors sensors = RequestTracker.instance.get(); + Context context = Context.from(this.metadata()); + SensorsCustomParams.addSensorToCQLResponse(result, options.getProtocolVersion(), sensors, context, Type.WRITE_BYTES); + // CAS updates incorporate read sensors + SensorsCustomParams.addSensorToCQLResponse(result, options.getProtocolVersion(), sensors, context, Type.READ_BYTES); + + return result; + } + public static class ParsedInsert extends ModificationStatement.Parsed { private final List columnNames; @@ -178,17 +219,19 @@ protected ModificationStatement prepareInternal(ClientState state, boolean applyOnlyToStaticColumns = !hasClusteringColumnsSet && appliesOnlyToStaticColumns(operations, conditions); - StatementRestrictions restrictions = new StatementRestrictions(state, - type, - metadata, - whereClause.build(), - bindVariables, - Collections.emptyList(), - applyOnlyToStaticColumns, - false, - false); - - return new UpdateStatement(type, + StatementRestrictions restrictions = StatementRestrictions.create(state, + type, + metadata, + whereClause.build(), + bindVariables, + Collections.emptyList(), + IndexHints.NONE, + applyOnlyToStaticColumns, + false, + false); + + return new UpdateStatement(rawCQLStatement, + type, bindVariables, metadata, operations, @@ -249,17 +292,19 @@ protected ModificationStatement prepareInternal(ClientState state, boolean applyOnlyToStaticColumns = !hasClusteringColumnsSet && appliesOnlyToStaticColumns(operations, conditions); - StatementRestrictions restrictions = new StatementRestrictions(state, - type, - metadata, - whereClause.build(), - bindVariables, - Collections.emptyList(), - applyOnlyToStaticColumns, - false, - false); - - return new UpdateStatement(type, + StatementRestrictions restrictions = StatementRestrictions.create(state, + type, + metadata, + whereClause.build(), + bindVariables, + Collections.emptyList(), + IndexHints.NONE, + applyOnlyToStaticColumns, + false, + false); + + return new UpdateStatement(rawCQLStatement, + type, bindVariables, metadata, operations, @@ -322,10 +367,10 @@ protected ModificationStatement prepareInternal(ClientState state, bindVariables, operations, whereClause, - conditions, - Collections.emptyList()); + conditions); - return new UpdateStatement(type, + return new UpdateStatement(rawCQLStatement, + type, bindVariables, metadata, operations, diff --git a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java index b3819b5cd26b..1dcf904a0231 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java @@ -17,6 +17,9 @@ */ package org.apache.cassandra.cql3.statements; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.config.DatabaseDescriptor; @@ -29,12 +32,10 @@ import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; -public class UseStatement extends CQLStatement.Raw implements CQLStatement +public class UseStatement extends CQLStatement.Raw implements CQLStatement.SingleKeyspaceCqlStatement { private final String keyspace; @@ -73,7 +74,7 @@ public ResultMessage executeLocally(QueryState state, QueryOptions options) thro // but for some unit tests we need to set the keyspace (e.g. for tests with DROP INDEX) return execute(state, options, Dispatcher.RequestTime.forImmediateExecution()); } - + @Override public String toString() { @@ -86,6 +87,7 @@ public AuditLogContext getAuditLogContext() return new AuditLogContext(AuditLogEntryType.USE_KEYSPACE, keyspace); } + @Override public String keyspace() { return keyspace; diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java index b8a27af3bab5..98fd6f9d8d9a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java @@ -19,7 +19,9 @@ import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -27,7 +29,7 @@ import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; @@ -50,22 +52,38 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_ALTER_RF_DURING_RANGE_MOVEMENT; import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_UNSAFE_TRANSIENT_CHANGES; -public final class AlterKeyspaceStatement extends AlterSchemaStatement +public final class AlterKeyspaceStatement extends AlterSchemaStatement implements AlterSchemaStatement.WithKeyspaceAttributes { private static final boolean allow_alter_rf_during_range_movement = ALLOW_ALTER_RF_DURING_RANGE_MOVEMENT.getBoolean(); private static final boolean allow_unsafe_transient_changes = ALLOW_UNSAFE_TRANSIENT_CHANGES.getBoolean(); - private final HashSet clientWarnings = new HashSet<>(); private final KeyspaceAttributes attrs; private final boolean ifExists; - public AlterKeyspaceStatement(String keyspaceName, KeyspaceAttributes attrs, boolean ifExists) + public AlterKeyspaceStatement(String queryString, String keyspaceName, KeyspaceAttributes attrs, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.attrs = attrs; this.ifExists = ifExists; } + public Object getAttribute(String key) + { + return attrs.getProperty(key); + } + + public void overrideAttribute(String oldKey, String newKey, String newValue) + { + attrs.removeProperty(oldKey); + attrs.addProperty(newKey, newValue); + } + + public void overrideAttribute(String oldKey, String newKey, Map newValue) + { + attrs.removeProperty(oldKey); + attrs.addProperty(newKey, newValue); + } + public Keyspaces apply(Keyspaces schema) { attrs.validate(); @@ -86,7 +104,7 @@ public Keyspaces apply(Keyspaces schema) if (newKeyspace.params.replication.klass.equals(LocalStrategy.class)) throw ire("Unable to use given strategy class: LocalStrategy is reserved for internal use."); - newKeyspace.params.validate(keyspaceName, state); + newKeyspace.validate(state); validateNoRangeMovements(); validateTransientReplication(keyspace.createReplicationStrategy(), newKeyspace.createReplicationStrategy()); @@ -109,6 +127,7 @@ public void authorize(ClientState client) @Override Set clientWarnings(KeyspacesDiff diff) { + HashSet clientWarnings = new HashSet<>(); if (diff.isEmpty()) return clientWarnings; @@ -204,7 +223,7 @@ public String toString() return String.format("%s (%s)", getClass().getSimpleName(), keyspaceName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final String keyspaceName; private final KeyspaceAttributes attrs; @@ -217,9 +236,10 @@ public Raw(String keyspaceName, KeyspaceAttributes attrs, boolean ifExists) this.ifExists = ifExists; } - public AlterKeyspaceStatement prepare(ClientState state) + @Override + public AlterKeyspaceStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - return new AlterKeyspaceStatement(keyspaceName, attrs, ifExists); + return new AlterKeyspaceStatement(rawCQLStatement, keyspaceMapper.apply(keyspaceName), attrs, ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java index ca4babc20439..81a0379ed1d0 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java @@ -17,7 +17,9 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.Map; import java.util.Set; +import java.util.function.Function; import com.google.common.collect.ImmutableSet; @@ -40,16 +42,24 @@ import static org.apache.cassandra.schema.KeyspaceMetadata.validateKeyspaceName; -abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspaceCqlStatement, SchemaTransformation +public abstract class AlterSchemaStatement implements CQLStatement.SingleKeyspaceCqlStatement, SchemaTransformation { - protected final String keyspaceName; // name of the keyspace affected by the statement + private final String rawCQLStatement; + protected String keyspaceName; // name of the keyspace affected by the statement protected ClientState state; - protected AlterSchemaStatement(String keyspaceName) + protected AlterSchemaStatement(String queryString, String keyspaceName) { + this.rawCQLStatement = queryString; this.keyspaceName = keyspaceName; } + @Override + public String getRawCQLStatement() + { + return rawCQLStatement; + } + public void validate(ClientState state) { // validation is performed while executing the statement, in apply() @@ -70,6 +80,11 @@ public String keyspace() return keyspaceName; } + public void overrideKeyspace(Function overrideKeyspace) + { + this.keyspaceName = overrideKeyspace.apply(keyspaceName); + } + public ResultMessage executeLocally(QueryState state, QueryOptions options) { return execute(state, true); @@ -164,4 +179,13 @@ static InvalidRequestException ire(String format, Object... args) { return new InvalidRequestException(String.format(format, args)); } + + public static interface WithKeyspaceAttributes + { + Object getAttribute(String key); + + void overrideAttribute(String oldKey, String newKey, String newValue); + + void overrideAttribute(String oldKey, String newKey, Map newValue); + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java index 6db30c1b4609..7de58cfd15f4 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -26,7 +26,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; - +import java.util.function.UnaryOperator; import javax.annotation.Nullable; import com.google.common.base.Splitter; @@ -40,10 +40,11 @@ import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.cql3.functions.masking.ColumnMask; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.AbstractType; @@ -55,6 +56,7 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.DroppedColumn; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Keyspaces; @@ -64,11 +66,13 @@ import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.schema.Views; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.NoSpamLogger; import static java.lang.String.format; @@ -85,9 +89,9 @@ public abstract class AlterTableStatement extends AlterSchemaStatement private final boolean ifExists; protected ClientState state; - public AlterTableStatement(String keyspaceName, String tableName, boolean ifExists) + public AlterTableStatement(String queryString, String keyspaceName, String tableName, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.ifExists = ifExists; } @@ -122,6 +126,18 @@ public Keyspaces apply(Keyspaces schema) return schema.withAddedOrUpdated(apply(keyspace, table)); } + // CNDB-14199: the method is needed for CNDB + public boolean containsDateRangeTypeColumn() + { + // Classes that need this method exposed have to override it + return false; + } + + public ResultMessage execute(QueryState state, boolean locally) + { + return super.execute(state, locally); + } + SchemaChange schemaChangeEvent(KeyspacesDiff diff) { return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, tableName); @@ -152,9 +168,9 @@ public String toString() */ public static class AlterColumn extends AlterTableStatement { - AlterColumn(String keyspaceName, String tableName, boolean ifTableExists) + AlterColumn(String queryString, String keyspaceName, String tableName, boolean ifTableExists) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); } public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) @@ -173,14 +189,15 @@ public static class MaskColumn extends AlterTableStatement private final ColumnMask.Raw rawMask; private final boolean ifColumnExists; - MaskColumn(String keyspaceName, + MaskColumn(String queryString, + String keyspaceName, String tableName, ColumnIdentifier columnName, @Nullable ColumnMask.Raw rawMask, boolean ifTableExists, boolean ifColumnExists) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); this.columnName = columnName; this.rawMask = rawMask; this.ifColumnExists = ifColumnExists; @@ -256,14 +273,15 @@ private static class Column this.isStatic = isStatic; this.mask = mask; } + } private final Collection newColumns; private final boolean ifColumnNotExists; - private AddColumns(String keyspaceName, String tableName, Collection newColumns, boolean ifTableExists, boolean ifColumnNotExists) + private AddColumns(String queryString, String keyspaceName, String tableName, Collection newColumns, boolean ifTableExists, boolean ifColumnNotExists) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); this.newColumns = newColumns; this.ifColumnNotExists = ifColumnNotExists; } @@ -334,9 +352,9 @@ private void addColumn(KeyspaceMetadata keyspace, { throw ire("Cannot add a column '%s' of type %s, incompatible with previously dropped column '%s' of type %s", name, - type.asCQL3Type(), + type.asCQL3Type().toSchemaString(), name, - droppedColumn.type.asCQL3Type()); + droppedColumn.type.asCQL3Type().toSchemaString()); } if (droppedColumn.isStatic() != isStatic) @@ -370,6 +388,18 @@ private void addColumn(KeyspaceMetadata keyspace, } } } + + @Override + public boolean containsDateRangeTypeColumn() + { + for (AddColumns.Column column : newColumns) + { + if (column.type.isDateRange()) + return true; + } + + return false; + } } /** @@ -383,9 +413,9 @@ private static class DropColumns extends AlterTableStatement private final boolean ifColumnExists; private final Long timestamp; - private DropColumns(String keyspaceName, String tableName, Set removedColumns, boolean ifTableExists, boolean ifColumnExists, Long timestamp) + private DropColumns(String queryString, String keyspaceName, String tableName, Set removedColumns, boolean ifTableExists, boolean ifColumnExists, Long timestamp) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); this.removedColumns = removedColumns; this.ifColumnExists = ifColumnExists; this.timestamp = timestamp; @@ -411,14 +441,6 @@ private void dropColumn(KeyspaceMetadata keyspace, TableMetadata table, ColumnId if (currentColumn.isPrimaryKeyColumn()) throw ire("Cannot drop PRIMARY KEY column %s", column); - /* - * Cannot allow dropping top-level columns of user defined types that aren't frozen because we cannot convert - * the type into an equivalent tuple: we only support frozen tuples currently. And as such we cannot persist - * the correct type in system_schema.dropped_columns. - */ - if (currentColumn.type.isUDT() && currentColumn.type.isMultiCell()) - throw ire("Cannot drop non-frozen column %s of user type %s", column, currentColumn.type.asCQL3Type()); - // TODO: some day try and find a way to not rely on Keyspace/IndexManager/Index to find dependent indexes Set dependentIndexes = Keyspace.openAndGetStore(table).indexManager.getDependentIndexes(currentColumn); if (!dependentIndexes.isEmpty()) @@ -452,9 +474,9 @@ private static class RenameColumns extends AlterTableStatement private final Map renamedColumns; private final boolean ifColumnsExists; - private RenameColumns(String keyspaceName, String tableName, Map renamedColumns, boolean ifTableExists, boolean ifColumnsExists) + private RenameColumns(String queryString, String keyspaceName, String tableName, Map renamedColumns, boolean ifTableExists, boolean ifColumnsExists) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); this.renamedColumns = renamedColumns; this.ifColumnsExists = ifColumnsExists; } @@ -525,9 +547,9 @@ private static class AlterOptions extends AlterTableStatement { private final TableAttributes attrs; - private AlterOptions(String keyspaceName, String tableName, TableAttributes attrs, boolean ifTableExists) + private AlterOptions(String queryString, String keyspaceName, String tableName, TableAttributes attrs, boolean ifTableExists) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); this.attrs = attrs; } @@ -568,7 +590,11 @@ public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) if (!params.compression.isEnabled()) Guardrails.uncompressedTablesEnabled.ensureEnabled(state); - return keyspace.withSwapped(keyspace.tables.withSwapped(table.withSwapped(params))); + TableMetadata.Builder builder = table.unbuild().params(params); + for (DroppedColumn.Raw record : attrs.droppedColumnRecords()) + builder.recordColumnDrop(record.prepare(keyspaceName, tableName, keyspace.types)); + + return keyspace.withSwapped(keyspace.tables.withSwapped(builder.build())); } } @@ -580,9 +606,9 @@ private static class DropCompactStorage extends AlterTableStatement { private static final Logger logger = LoggerFactory.getLogger(AlterTableStatement.class); private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 5L, TimeUnit.MINUTES); - private DropCompactStorage(String keyspaceName, String tableName, boolean ifTableExists) + private DropCompactStorage(String queryString, String keyspaceName, String tableName, boolean ifTableExists) { - super(keyspaceName, tableName, ifTableExists); + super(queryString, keyspaceName, tableName, ifTableExists); } public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) @@ -620,7 +646,7 @@ private void validateCanDropCompactStorage() Set preC15897nodes = new HashSet<>(); Set with2xSStables = new HashSet<>(); Splitter onComma = Splitter.on(',').omitEmptyStrings().trimResults(); - for (InetAddressAndPort node : StorageService.instance.getTokenMetadata().getAllEndpoints()) + for (InetAddressAndPort node : StorageService.instance.getTokenMetadataForKeyspace(keyspaceName).getAllEndpoints()) { if (MessagingService.instance().versions.knows(node) && MessagingService.instance().versions.getRaw(node) < MessagingService.VERSION_40) @@ -672,7 +698,7 @@ private void validateCanDropCompactStorage() } } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private enum Kind { @@ -701,7 +727,7 @@ private enum Kind // DROP private final Set droppedColumns = new HashSet<>(); - private Long timestamp = null; // will use execution timestamp if not provided by query + private Long dropTimestamp = null; // will use execution timestamp if not provided by query // RENAME private final Map renamedColumns = new HashMap<>(); @@ -715,20 +741,24 @@ public Raw(QualifiedName name, boolean ifTableExists) this.ifTableExists = ifTableExists; } - public AlterTableStatement prepare(ClientState state) + @Override + public AlterTableStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); String tableName = name.getName(); switch (kind) { - case ALTER_COLUMN: return new AlterColumn(keyspaceName, tableName, ifTableExists); - case MASK_COLUMN: return new MaskColumn(keyspaceName, tableName, maskedColumn, rawMask, ifTableExists, ifColumnExists); - case ADD_COLUMNS: return new AddColumns(keyspaceName, tableName, addedColumns, ifTableExists, ifColumnNotExists); - case DROP_COLUMNS: return new DropColumns(keyspaceName, tableName, droppedColumns, ifTableExists, ifColumnExists, timestamp); - case RENAME_COLUMNS: return new RenameColumns(keyspaceName, tableName, renamedColumns, ifTableExists, ifColumnExists); - case ALTER_OPTIONS: return new AlterOptions(keyspaceName, tableName, attrs, ifTableExists); - case DROP_COMPACT_STORAGE: return new DropCompactStorage(keyspaceName, tableName, ifTableExists); + case ALTER_COLUMN: return new AlterColumn(rawCQLStatement, keyspaceName, tableName, ifTableExists); + case MASK_COLUMN: return new MaskColumn(rawCQLStatement, keyspaceName, tableName, maskedColumn, rawMask, ifTableExists, ifColumnExists); + case ADD_COLUMNS: + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + addedColumns.forEach(c -> c.type.forEachUserType(utName -> utName.updateKeyspaceIfDefined(keyspaceMapper))); + return new AddColumns(rawCQLStatement, keyspaceName, tableName, addedColumns, ifTableExists, ifColumnNotExists); + case DROP_COLUMNS: return new DropColumns(rawCQLStatement, keyspaceName, tableName, droppedColumns, ifTableExists, ifColumnExists, dropTimestamp); + case RENAME_COLUMNS: return new RenameColumns(rawCQLStatement, keyspaceName, tableName, renamedColumns, ifTableExists, ifColumnExists); + case ALTER_OPTIONS: return new AlterOptions(rawCQLStatement, keyspaceName, tableName, attrs, ifTableExists); + case DROP_COMPACT_STORAGE: return new DropCompactStorage(rawCQLStatement, keyspaceName, tableName, ifTableExists); } throw new AssertionError(); @@ -773,9 +803,9 @@ public void dropCompactStorage() kind = Kind.DROP_COMPACT_STORAGE; } - public void timestamp(long timestamp) + public void dropTimestamp(long timestamp) { - this.timestamp = timestamp; + this.dropTimestamp = timestamp; } public void rename(ColumnIdentifier from, ColumnIdentifier to) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java index 40bca4aac991..f4bde39f8acc 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java @@ -22,11 +22,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.UnaryOperator; + +import com.google.common.collect.ImmutableList; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.FieldIdentifier; +import org.apache.cassandra.cql3.UTName; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; @@ -37,6 +44,7 @@ import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import org.apache.cassandra.utils.Collections3; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.filter; @@ -44,7 +52,6 @@ import static java.lang.String.join; import static java.util.function.Predicate.isEqual; import static java.util.stream.Collectors.toList; - import static org.apache.cassandra.utils.ByteBufferUtil.bytes; public abstract class AlterTypeStatement extends AlterSchemaStatement @@ -52,9 +59,9 @@ public abstract class AlterTypeStatement extends AlterSchemaStatement protected final String typeName; protected final boolean ifExists; - public AlterTypeStatement(String keyspaceName, String typeName, boolean ifExists) + public AlterTypeStatement(String queryString, String keyspaceName, String typeName, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.ifExists = ifExists; this.typeName = typeName; } @@ -84,7 +91,13 @@ public Keyspaces apply(Keyspaces schema) return schema; } - return schema.withAddedOrUpdated(keyspace.withUpdatedUserType(apply(keyspace, type))); + UserType updated = apply(keyspace, type); + CreateTypeStatement.validate(updated); + + KeyspaceMetadata newKeyspace = keyspace.withUpdatedUserType(updated); + newKeyspace.validate(state); + + return schema.withAddedOrUpdated(newKeyspace); } abstract UserType apply(KeyspaceMetadata keyspace, UserType type); @@ -100,6 +113,13 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, typeName); } + // CNDB-14199: the method is needed for CNDB + public boolean containsDateRangeTypeColumn() + { + // Classes that need this method exposed have to override it + return false; + } + private static final class AddField extends AlterTypeStatement { private final FieldIdentifier fieldName; @@ -108,9 +128,10 @@ private static final class AddField extends AlterTypeStatement private ClientState state; - private AddField(String keyspaceName, String typeName, FieldIdentifier fieldName, CQL3Type.Raw type, boolean ifExists, boolean ifFieldNotExists) + private AddField(String queryString, String keyspaceName, String typeName, + FieldIdentifier fieldName, CQL3Type.Raw type, boolean ifExists, boolean ifFieldNotExists) { - super(keyspaceName, typeName, ifExists); + super(queryString, keyspaceName, typeName, ifExists); this.fieldName = fieldName; this.ifFieldNotExists = ifFieldNotExists; this.type = type; @@ -155,12 +176,17 @@ UserType apply(KeyspaceMetadata keyspace, UserType userType) Guardrails.fieldsPerUDT.guard(userType.size() + 1, userType.getNameAsString(), false, state); type.validate(state, "Field " + fieldName); - List fieldNames = new ArrayList<>(userType.fieldNames()); fieldNames.add(fieldName); - List> fieldTypes = new ArrayList<>(userType.fieldTypes()); fieldTypes.add(fieldType); - + ImmutableList fieldNames = Collections3.withAppended(userType.fieldNames(), fieldName); + ImmutableList> fieldTypes = Collections3.withAppended(userType.fieldTypes(), fieldType); return new UserType(keyspaceName, userType.name, fieldNames, fieldTypes, true); } + @Override + public boolean containsDateRangeTypeColumn() + { + return type.isDateRange(); + } + private static Collection findTablesReferencingTypeInPartitionKey(KeyspaceMetadata keyspace, UserType userType) { Collection tables = new ArrayList<>(); @@ -176,9 +202,10 @@ private static final class RenameFields extends AlterTypeStatement private final Map renamedFields; private final boolean ifFieldExists; - private RenameFields(String keyspaceName, String typeName, Map renamedFields, boolean ifExists, boolean ifFieldExists) + private RenameFields(String queryString, String keyspaceName, String typeName, + Map renamedFields, boolean ifExists, boolean ifFieldExists) { - super(keyspaceName, typeName, ifExists); + super(queryString, keyspaceName, typeName, ifExists); this.ifFieldExists = ifFieldExists; this.renamedFields = renamedFields; } @@ -225,9 +252,9 @@ UserType apply(KeyspaceMetadata keyspace, UserType userType) private static final class AlterField extends AlterTypeStatement { - private AlterField(String keyspaceName, String typeName, boolean ifExists) + private AlterField(String queryString, String keyspaceName, String typeName, boolean ifExists) { - super(keyspaceName, typeName, ifExists); + super(queryString, keyspaceName, typeName, ifExists); } UserType apply(KeyspaceMetadata keyspace, UserType userType) @@ -236,7 +263,7 @@ UserType apply(KeyspaceMetadata keyspace, UserType userType) } } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private enum Kind { @@ -263,16 +290,20 @@ public Raw(UTName name, boolean ifExists) this.name = name; } - public AlterTypeStatement prepare(ClientState state) + @Override + public AlterTypeStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); String typeName = name.getStringTypeName(); switch (kind) { - case ADD_FIELD: return new AddField(keyspaceName, typeName, newFieldName, newFieldType, ifExists, ifFieldNotExists); - case RENAME_FIELDS: return new RenameFields(keyspaceName, typeName, renamedFields, ifExists, ifFieldExists); - case ALTER_FIELD: return new AlterField(keyspaceName, typeName, ifExists); + case ADD_FIELD: + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + newFieldType.forEachUserType(utName -> utName.updateKeyspaceIfDefined(keyspaceMapper)); + return new AddField(rawCQLStatement, keyspaceName, typeName, newFieldName, newFieldType, ifExists, ifFieldNotExists); + case RENAME_FIELDS: return new RenameFields(rawCQLStatement, keyspaceName, typeName, renamedFields, ifExists, ifFieldExists); + case ALTER_FIELD: return new AlterField(rawCQLStatement, keyspaceName, typeName, ifExists); } throw new AssertionError(); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java index 7e707f476bed..157bc1d43fd6 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java @@ -17,14 +17,20 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -37,9 +43,10 @@ public final class AlterViewStatement extends AlterSchemaStatement private ClientState state; private final boolean ifExists; - public AlterViewStatement(String keyspaceName, String viewName, TableAttributes attrs, boolean ifExists) + public AlterViewStatement(String queryString, String keyspaceName, String viewName, + TableAttributes attrs, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.viewName = viewName; this.attrs = attrs; this.ifExists = ifExists; @@ -85,7 +92,7 @@ public Keyspaces apply(Keyspaces schema) if (params.defaultTimeToLive > 0) { throw ire("Forbidden default_time_to_live detected for a materialized view. " + - "Data in a materialized view always expire at the same time than " + + "Data in a materialized view always expires at the same time as " + "the corresponding data in the parent table. default_time_to_live " + "must be set to zero, see CASSANDRA-12868 for more information"); } @@ -117,7 +124,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, viewName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName name; private final TableAttributes attrs; @@ -130,10 +137,11 @@ public Raw(QualifiedName name, TableAttributes attrs, boolean ifExists) this.ifExists = ifExists; } - public AlterViewStatement prepare(ClientState state) + @Override + public AlterViewStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); - return new AlterViewStatement(keyspaceName, name.getName(), attrs, ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + return new AlterViewStatement(rawCQLStatement, keyspaceName, name.getName(), attrs, ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java index eb9f33a94959..9a63746c202d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; @@ -30,33 +31,36 @@ import org.apache.cassandra.auth.FunctionResource; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.Terms; import org.apache.cassandra.cql3.functions.FunctionName; import org.apache.cassandra.cql3.functions.ScalarFunction; import org.apache.cassandra.cql3.functions.UDAggregate; import org.apache.cassandra.cql3.functions.UDFunction; import org.apache.cassandra.cql3.functions.UserFunction; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Iterables.transform; import static java.lang.String.format; import static java.lang.String.join; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; -import static com.google.common.collect.Iterables.concat; -import static com.google.common.collect.Iterables.transform; - public final class CreateAggregateStatement extends AlterSchemaStatement { private final String aggregateName; @@ -67,8 +71,10 @@ public final class CreateAggregateStatement extends AlterSchemaStatement private final Term.Raw rawInitialValue; private final boolean orReplace; private final boolean ifNotExists; + private final boolean deterministic; - public CreateAggregateStatement(String keyspaceName, + public CreateAggregateStatement(String queryString, + String keyspaceName, String aggregateName, List rawArgumentTypes, CQL3Type.Raw rawStateType, @@ -76,9 +82,10 @@ public CreateAggregateStatement(String keyspaceName, FunctionName finalFunctionName, Term.Raw rawInitialValue, boolean orReplace, - boolean ifNotExists) + boolean ifNotExists, + boolean deterministic) { - super(keyspaceName); + super(queryString, keyspaceName); this.aggregateName = aggregateName; this.rawArgumentTypes = rawArgumentTypes; this.rawStateType = rawStateType; @@ -87,6 +94,7 @@ public CreateAggregateStatement(String keyspaceName, this.rawInitialValue = rawInitialValue; this.orReplace = orReplace; this.ifNotExists = ifNotExists; + this.deterministic = deterministic; } public Keyspaces apply(Keyspaces schema) @@ -201,7 +209,8 @@ public Keyspaces apply(Keyspaces schema) returnType, (ScalarFunction) stateFunction, (ScalarFunction) finalFunction, - initialValue); + initialValue, + deterministic); UserFunction existingAggregate = keyspace.userFunctions.find(aggregate.name(), argumentTypes).orElse(null); if (null != existingAggregate) @@ -215,7 +224,7 @@ public Keyspaces apply(Keyspaces schema) if (!orReplace) throw ire("Aggregate '%s' already exists", aggregateName); - if (!returnType.isCompatibleWith(existingAggregate.returnType())) + if (!returnType.isCompatibleWith(existingAggregate.returnType())) // shouldn't this condition be opposite direction? existingAggregate.returnType().isCompatibleWith(returnType)? { throw ire("Cannot replace aggregate '%s', the new return type %s isn't compatible with the return type %s of existing function", aggregateName, @@ -299,7 +308,7 @@ private String finalFunctionString() return format("%s(%s)", finalFunctionName, rawStateType); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final FunctionName aggregateName; private final List rawArgumentTypes; @@ -309,6 +318,7 @@ public static final class Raw extends CQLStatement.Raw private final Term.Raw rawInitialValue; private final boolean orReplace; private final boolean ifNotExists; + private final boolean deterministic; public Raw(FunctionName aggregateName, List rawArgumentTypes, @@ -317,7 +327,8 @@ public Raw(FunctionName aggregateName, String finalFunctionName, Term.Raw rawInitialValue, boolean orReplace, - boolean ifNotExists) + boolean ifNotExists, + boolean deterministic) { this.aggregateName = aggregateName; this.rawArgumentTypes = rawArgumentTypes; @@ -327,13 +338,21 @@ public Raw(FunctionName aggregateName, this.rawInitialValue = rawInitialValue; this.orReplace = orReplace; this.ifNotExists = ifNotExists; + this.deterministic = deterministic; } - public CreateAggregateStatement prepare(ClientState state) + @Override + public CreateAggregateStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = aggregateName.hasKeyspace() ? aggregateName.keyspace : state.getKeyspace(); + String keyspaceName = keyspaceMapper.apply(aggregateName.hasKeyspace() ? aggregateName.keyspace : state.getKeyspace()); + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + { + rawArgumentTypes.forEach(t -> t.forEachUserType(name -> name.updateKeyspaceIfDefined(keyspaceMapper))); + rawStateType.forEachUserType(name -> name.updateKeyspaceIfDefined(keyspaceMapper)); + } - return new CreateAggregateStatement(keyspaceName, + return new CreateAggregateStatement(rawCQLStatement, + keyspaceName, aggregateName.name, rawArgumentTypes, rawStateType, @@ -341,7 +360,8 @@ public CreateAggregateStatement prepare(ClientState state) null != finalFunctionName ? new FunctionName(keyspaceName, finalFunctionName) : null, rawInitialValue, orReplace, - ifNotExists); + ifNotExists, + deterministic); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java index f04ae37cd52b..c729c1f9c5ab 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java @@ -20,25 +20,29 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; -import org.apache.cassandra.auth.*; +import org.apache.cassandra.auth.FunctionResource; +import org.apache.cassandra.auth.IResource; +import org.apache.cassandra.auth.Permission; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.functions.FunctionName; import org.apache.cassandra.cql3.functions.UDFunction; import org.apache.cassandra.cql3.functions.UserFunction; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -57,8 +61,12 @@ public final class CreateFunctionStatement extends AlterSchemaStatement private final String body; private final boolean orReplace; private final boolean ifNotExists; + private final boolean deterministic; + private final boolean monotonic; + private final List monotonicOn; - public CreateFunctionStatement(String keyspaceName, + public CreateFunctionStatement(String queryString, + String keyspaceName, String functionName, List argumentNames, List rawArgumentTypes, @@ -67,9 +75,12 @@ public CreateFunctionStatement(String keyspaceName, String language, String body, boolean orReplace, - boolean ifNotExists) + boolean ifNotExists, + boolean deterministic, + boolean monotonic, + List monotonicOn) { - super(keyspaceName); + super(queryString, keyspaceName); this.functionName = functionName; this.argumentNames = argumentNames; this.rawArgumentTypes = rawArgumentTypes; @@ -79,6 +90,10 @@ public CreateFunctionStatement(String keyspaceName, this.body = body; this.orReplace = orReplace; this.ifNotExists = ifNotExists; + this.deterministic = deterministic; + this.monotonic = monotonic; + this.monotonicOn = monotonicOn; + } // TODO: replace affected aggregates !! @@ -92,9 +107,14 @@ public Keyspaces apply(Keyspaces schema) if (!FunctionName.isNameValid(functionName)) throw ire("Function name '%s' is invalid", functionName); - if (new HashSet<>(argumentNames).size() != argumentNames.size()) + HashSet argumentNamesSet = new HashSet<>(argumentNames); + + if (argumentNamesSet.size() != argumentNames.size()) throw ire("Duplicate argument names for given function %s with argument names %s", functionName, argumentNames); + if (!argumentNamesSet.containsAll(monotonicOn)) + throw ire("Monotony should be declared on one of the arguments, '%s' is not an argument", monotonicOn.get(0)); + rawArgumentTypes.stream() .filter(raw -> !raw.isImplicitlyFrozen() && raw.isFrozen()) .findFirst() @@ -120,7 +140,10 @@ public Keyspaces apply(Keyspaces schema) returnType, calledOnNullInput, language, - body); + body, + deterministic, + monotonic, + monotonicOn); UserFunction existingFunction = keyspace.userFunctions.find(function.name(), argumentTypes).orElse(null); if (null != existingFunction) @@ -204,7 +227,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, functionName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final FunctionName name; private final List argumentNames; @@ -215,6 +238,9 @@ public static final class Raw extends CQLStatement.Raw private final String body; private final boolean orReplace; private final boolean ifNotExists; + private final boolean deterministic; + private final boolean monotonic; + private final List monotonicOn; public Raw(FunctionName name, List argumentNames, @@ -224,7 +250,10 @@ public Raw(FunctionName name, String language, String body, boolean orReplace, - boolean ifNotExists) + boolean ifNotExists, + boolean deterministic, + boolean monotonic, + List monotonicOn) { this.name = name; this.argumentNames = argumentNames; @@ -235,13 +264,24 @@ public Raw(FunctionName name, this.body = body; this.orReplace = orReplace; this.ifNotExists = ifNotExists; + this.deterministic = deterministic; + this.monotonic = monotonic; + this.monotonicOn = monotonicOn; } - public CreateFunctionStatement prepare(ClientState state) + @Override + public CreateFunctionStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.keyspace : state.getKeyspace(); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.keyspace : state.getKeyspace()); + + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + { + rawArgumentTypes.forEach(t -> t.forEachUserType(name -> name.updateKeyspaceIfDefined(keyspaceMapper))); + rawReturnType.forEachUserType(name -> name.updateKeyspaceIfDefined(keyspaceMapper)); + } - return new CreateFunctionStatement(keyspaceName, + return new CreateFunctionStatement(rawCQLStatement, + keyspaceName, name.name, argumentNames, rawArgumentTypes, @@ -250,7 +290,10 @@ public CreateFunctionStatement prepare(ClientState state) language, body, orReplace, - ifNotExists); + ifNotExists, + deterministic, + monotonic, + monotonicOn); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java index 946becb1db9d..03c52074b565 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java @@ -17,40 +17,58 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.*; - -import com.google.common.base.Strings; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.StreamSupport; + +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.cql3.statements.schema.IndexTarget.Type; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.guardrails.Threshold; import org.apache.cassandra.db.marshal.MapType; import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.internal.CassandraIndex; -import org.apache.cassandra.index.sasi.SASIIndex; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import org.apache.cassandra.utils.FBUtilities; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Iterables.tryFind; +import static org.apache.cassandra.config.CassandraRelevantProperties.INDEX_UNKNOWN_IGNORE; public final class CreateIndexStatement extends AlterSchemaStatement { - public static final String SASI_INDEX_DISABLED = "SASI indexes are disabled. Enable in cassandra.yaml to use."; + private static final Logger logger = LoggerFactory.getLogger(CreateIndexStatement.class); + public static final String KEYSPACE_DOES_NOT_EXIST = "Keyspace '%s' doesn't exist"; public static final String TABLE_DOES_NOT_EXIST = "Table '%s' doesn't exist"; public static final String COUNTER_TABLES_NOT_SUPPORTED = "Secondary indexes on counter tables aren't supported"; @@ -88,15 +106,31 @@ public final class CreateIndexStatement extends AlterSchemaStatement private final boolean ifNotExists; private ClientState state; - - public CreateIndexStatement(String keyspaceName, + private static final String DSE_INDEX_WARNING = "Index %s was not created. DSE custom index (%s) is not " + + "supported. Consult the docs on alternatives (SAI indexes, " + + "Secondary Indexes)."; + private static final String UNKNOWN_INDEX_WARNING = "Index %s was not created. Unknown custom index (%s) is not " + + "supported. Consult the docs on alternatives (SAI indexes, " + + "Secondary Indexes)."; + + @VisibleForTesting + public static final Set DSE_INDEXES = ImmutableSet.of( + "com.datastax.bdp.cassandra.index.solr.SolrSecondaryIndex", + "com.datastax.bdp.cassandra.index.solr.ThriftSolrSecondaryIndex", + "com.datastax.bdp.cassandra.index.solr.Cql3SolrSecondaryIndex", + "com.datastax.bdp.search.solr.ThriftSolrSecondaryIndex", + "com.datastax.bdp.search.solr.Cql3SolrSecondaryIndex" + ); + + public CreateIndexStatement(String queryString, + String keyspaceName, String tableName, String indexName, List rawIndexTargets, IndexAttributes attrs, boolean ifNotExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.indexName = indexName; this.rawIndexTargets = rawIndexTargets; @@ -109,19 +143,33 @@ public void validate(ClientState state) { super.validate(state); + // Check the length of a valid index name. + // Non-valid indexes are validated in IndexMetadata#validate. + if (!state.isInternal + && SchemaConstants.isValidCharsName(indexName) + && indexName.length() > SchemaConstants.INDEX_NAME_LENGTH) + + throw ire("Index name shouldn't be more than %s characters long (got %s chars for %s)", + SchemaConstants.INDEX_NAME_LENGTH, indexName.length(), indexName); + // save the query state to use it for guardrails validation in #apply this.state = state; } public Keyspaces apply(Keyspaces schema) { + if (isDseIndexCreateStatement()) + { + // DSE indexes are not supported. The index is not created, the attempt is ignored (doesn't cause error), + // a meaningful warning is returned instead. + return schema; + } + + attrs.maybeApplyDefaultIndex(); attrs.validate(); Guardrails.createSecondaryIndexesEnabled.ensureEnabled("Creating secondary indexes", state); - if (attrs.isCustom && attrs.customClass.equals(SASIIndex.class.getName()) && !DatabaseDescriptor.getSASIIndexesEnabled()) - throw new InvalidRequestException(SASI_INDEX_DISABLED); - KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire(KEYSPACE_DOES_NOT_EXIST, keyspaceName); @@ -144,17 +192,9 @@ public Keyspaces apply(Keyspaces schema) if (table.isView()) throw ire(MATERIALIZED_VIEWS_NOT_SUPPORTED); - if (Keyspace.open(table.keyspace).getReplicationStrategy().hasTransientReplicas()) + if (keyspace.createReplicationStrategy().hasTransientReplicas()) throw new InvalidRequestException(TRANSIENTLY_REPLICATED_KEYSPACE_NOT_SUPPORTED); - // guardrails to limit number of secondary indexes per table. - Guardrails.secondaryIndexesPerTable.guard(table.indexes.size() + 1, - Strings.isNullOrEmpty(indexName) - ? String.format("on table %s", table.name) - : String.format("%s on table %s", indexName, table.name), - false, - state); - List indexTargets = Lists.newArrayList(transform(rawIndexTargets, t -> t.prepare(table))); if (indexTargets.isEmpty() && !attrs.isCustom) @@ -181,6 +221,34 @@ public Keyspaces apply(Keyspaces schema) IndexMetadata index = IndexMetadata.fromIndexTargets(indexTargets, name, kind, options); + String className = index.getIndexClassName(); + if (isUnknownCustomIndexCreateStatement() && INDEX_UNKNOWN_IGNORE.getBoolean()) + { + logger.error("Cannot find index type {}, but '{}' is true so ignoring index {} creation", + className, INDEX_UNKNOWN_IGNORE.getKey(), indexName); + return schema; + } + + IndexGuardrails guardRails = IndexGuardrails.forClassName(className); + String indexDescription = indexName == null ? String.format("on table %s", table.name) : String.format("%s on table %s", indexName, table.name); + + // Guardrail to limit number of secondary indexes (per table) + if (guardRails.hasPerTableThreshold()) + { + long indexesOnSameTable = table.indexes.stream().filter(other -> className.equals(other.getIndexClassName())).count(); + guardRails.perTableThreshold.guard(indexesOnSameTable + 1, indexDescription,false, state); + } + + // Guardrail to limit number of secondary indexes (total) + if (guardRails.hasTotalThreshold()) + { + long indexesOnAllTables = StreamSupport.stream(Keyspace.all().spliterator(), false).flatMap(ks -> ks.getColumnFamilyStores().stream()) + .flatMap(ks -> ks.indexManager.listIndexes().stream()) + .map(i -> i.getIndexMetadata().getIndexClassName()) + .filter(className::equals).count(); + guardRails.totalThreshold.guard(indexesOnAllTables + 1, indexDescription, false, state); + } + // check to disallow creation of an index which duplicates an existing one in all but name IndexMetadata equalIndex = tryFind(table.indexes, i -> i.equalsWithoutName(index)).orNull(); if (null != equalIndex) @@ -200,8 +268,10 @@ public Keyspaces apply(Keyspaces schema) @Override Set clientWarnings(KeyspacesDiff diff) { - if (attrs.isCustom && attrs.customClass.equals(SASIIndex.class.getName())) - return ImmutableSet.of(SASIIndex.USAGE_WARNING); + if (isDseIndexCreateStatement()) + return ImmutableSet.of(String.format(DSE_INDEX_WARNING, indexName, attrs.customClass)); + if (isUnknownCustomIndexCreateStatement() && INDEX_UNKNOWN_IGNORE.getBoolean()) + return ImmutableSet.of(String.format(UNKNOWN_INDEX_WARNING, indexName, attrs.customClass)); return ImmutableSet.of(); } @@ -214,6 +284,26 @@ private void validateCustomIndexColumnName(String name) throw ire(TOO_LONG_CUSTOM_INDEX_TARGET, name, SchemaConstants.NAME_LENGTH); } + private boolean isDseIndexCreateStatement() + { + return DSE_INDEXES.contains(attrs.customClass); + } + + private boolean isUnknownCustomIndexCreateStatement() + { + try + { + // mimic what IndexMetadata.validate(..) does + if (attrs.isCustom) + FBUtilities.classForName(IndexMetadata.expandAliases(attrs.customClass), "custom indexer"); + return false; + } + catch (ConfigurationException ex) + { + return true; + } + } + private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, IndexTarget target) { ColumnMetadata column = table.getColumn(target.column); @@ -223,10 +313,6 @@ private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, I AbstractType baseType = column.type.unwrap(); - // TODO: this check needs to be removed with CASSANDRA-20235 - if ((kind == IndexMetadata.Kind.CUSTOM)) - validateCustomIndexColumnName(target.column.toString()); - if (column.type.referencesDuration()) { if (column.type.isCollection()) @@ -253,10 +339,10 @@ private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, I if (column.isPartitionKey() && table.partitionKeyColumns().size() == 1) throw ire(ONLY_PARTITION_KEY, column); - if (baseType.isFrozenCollection() && target.type != Type.FULL) + if (baseType.isCollection() && !baseType.isMultiCell() && target.type != Type.FULL) throw ire(CREATE_ON_FROZEN_COLUMN, target.type, column, column.name.toCQLString()); - if (!baseType.isFrozenCollection() && target.type == Type.FULL) + if ((!baseType.isCollection() || baseType.isMultiCell) && target.type == Type.FULL) throw ire(FULL_ON_FROZEN_COLLECTIONS); if (!baseType.isCollection() && target.type != Type.SIMPLE) @@ -271,9 +357,10 @@ private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, I private String generateIndexName(KeyspaceMetadata keyspace, List targets) { + assert keyspace.name.equals(keyspaceName); String baseName = targets.size() == 1 - ? IndexMetadata.generateDefaultIndexName(tableName, targets.get(0).column) - : IndexMetadata.generateDefaultIndexName(tableName); + ? IndexMetadata.generateDefaultIndexName(tableName, targets.get(0).column) + : IndexMetadata.generateDefaultIndexName(tableName, null); return keyspace.findAvailableIndexName(baseName); } @@ -298,7 +385,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, indexName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName tableName; private final QualifiedName indexName; @@ -319,7 +406,8 @@ public Raw(QualifiedName tableName, this.ifNotExists = ifNotExists; } - public CreateIndexStatement prepare(ClientState state) + @Override + public CreateIndexStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() @@ -330,7 +418,7 @@ public CreateIndexStatement prepare(ClientState state) if (indexName.hasKeyspace() && !keyspaceName.equals(indexName.getKeyspace())) throw ire(KEYSPACE_DOES_NOT_MATCH_INDEX, keyspaceName, tableName); - + // Set the configured default 2i implementation if one isn't specified with USING: if (attrs.customClass == null) { @@ -340,19 +428,62 @@ public CreateIndexStatement prepare(ClientState state) // However, operators may require an implementation be specified throw ire(MUST_SPECIFY_INDEX_IMPLEMENTATION); } - + // If we explicitly specify the index type "legacy_local_table", we can just clear the custom class, and the - // non-custom 2i creation process will begin. Otherwise, if an index type has been specified with + // non-custom 2i creation process will begin. Otherwise, if an index type has been specified with // USING, make sure the appropriate custom index is created. if (attrs.customClass != null) { - if (!attrs.isCustom && attrs.customClass.equalsIgnoreCase(CassandraIndex.NAME)) + boolean isLegacyLocalTable = attrs.customClass.equalsIgnoreCase(CassandraIndex.NAME); + if (isLegacyLocalTable) attrs.customClass = null; else attrs.isCustom = true; } - return new CreateIndexStatement(keyspaceName, tableName.getName(), indexName.getName(), rawIndexTargets, attrs, ifNotExists); + return new CreateIndexStatement(rawCQLStatement, keyspaceMapper.apply(keyspaceName), tableName.getName(), + indexName.getName(), rawIndexTargets, attrs, ifNotExists); + } + } + + enum IndexGuardrails + { + LEGACY(Guardrails.secondaryIndexesPerTable, null), + SAI(Guardrails.saiIndexesPerTable, Guardrails.saiIndexesTotal), + TRUSTED(Guardrails.trustedIndexesPerTable, null), + UNKNOWN(null, null); + + final Threshold perTableThreshold; + final Threshold totalThreshold; + + IndexGuardrails(Threshold perTableThreshold, Threshold totalThreshold) + { + this.perTableThreshold = perTableThreshold; + this.totalThreshold = totalThreshold; + } + + boolean hasPerTableThreshold() + { + return perTableThreshold != null; + } + + boolean hasTotalThreshold() + { + return totalThreshold != null; } + + static IndexGuardrails forClassName(String className) + { + switch (className) + { + case "org.apache.cassandra.index.internal.CassandraIndex": + return IndexGuardrails.LEGACY; + case "org.apache.cassandra.index.sai.StorageAttachedIndex": + return IndexGuardrails.SAI; + default: + return IndexMetadata.isTrustedIndexImplementation(className) ? IndexGuardrails.TRUSTED : IndexGuardrails.UNKNOWN; + } + } + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java index 13d52b1e156c..9d7ddaaade0b 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java @@ -18,10 +18,11 @@ package org.apache.cassandra.cql3.statements.schema; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.function.UnaryOperator; import com.google.common.collect.ImmutableSet; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +32,7 @@ import org.apache.cassandra.auth.FunctionResource; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.locator.LocalStrategy; @@ -45,21 +46,38 @@ import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; -public final class CreateKeyspaceStatement extends AlterSchemaStatement +public final class CreateKeyspaceStatement extends AlterSchemaStatement implements AlterSchemaStatement.WithKeyspaceAttributes { private static final Logger logger = LoggerFactory.getLogger(CreateKeyspaceStatement.class); private final KeyspaceAttributes attrs; private final boolean ifNotExists; - private final HashSet clientWarnings = new HashSet<>(); - public CreateKeyspaceStatement(String keyspaceName, KeyspaceAttributes attrs, boolean ifNotExists) + public CreateKeyspaceStatement(String queryString, String keyspaceName, + KeyspaceAttributes attrs, boolean ifNotExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.attrs = attrs; this.ifNotExists = ifNotExists; } + public Object getAttribute(String key) + { + return attrs.getProperty(key); + } + + public void overrideAttribute(String oldKey, String newKey, String newValue) + { + attrs.removeProperty(oldKey); + attrs.addProperty(newKey, newValue); + } + + public void overrideAttribute(String oldKey, String newKey, Map newValue) + { + attrs.removeProperty(oldKey); + attrs.addProperty(newKey, newValue); + } + public Keyspaces apply(Keyspaces schema) { attrs.validate(); @@ -79,11 +97,11 @@ public Keyspaces apply(Keyspaces schema) } KeyspaceMetadata keyspace = KeyspaceMetadata.create(keyspaceName, attrs.asNewKeyspaceParams()); + keyspace.validate(state); if (keyspace.params.replication.klass.equals(LocalStrategy.class)) throw ire("Unable to use given strategy class: LocalStrategy is reserved for internal use."); - keyspace.params.validate(keyspaceName, state); return schema.withAddedOrUpdated(keyspace); } @@ -123,7 +141,18 @@ public void validate(ClientState state) Guardrails.keyspaces.guard(Schema.instance.getUserKeyspaces().size() + 1, keyspaceName, false, state); } - public static final class Raw extends CQLStatement.Raw + Set clientWarnings(KeyspacesDiff diff) + { + HashSet clientWarnings = new HashSet<>(); + if (attrs.hasProperty("graph_engine")) + { + clientWarnings.add("The unsupported graph property 'graph_engine' was ignored."); + } + + return clientWarnings; + } + + public static final class Raw extends RawKeyspaceAwareStatement { public final String keyspaceName; private final KeyspaceAttributes attrs; @@ -136,9 +165,10 @@ public Raw(String keyspaceName, KeyspaceAttributes attrs, boolean ifNotExists) this.ifNotExists = ifNotExists; } - public CreateKeyspaceStatement prepare(ClientState state) + @Override + public CreateKeyspaceStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - return new CreateKeyspaceStatement(keyspaceName, attrs, ifNotExists); + return new CreateKeyspaceStatement(rawCQLStatement, keyspaceMapper.apply(keyspaceName), attrs, ifNotExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java index 747837f30556..71954cc1beea 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java @@ -17,15 +17,23 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; - import javax.annotation.Nullable; import com.google.common.collect.ImmutableSet; - import org.apache.commons.lang3.StringUtils; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,23 +42,44 @@ import org.apache.cassandra.auth.DataResource; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.CQLFragmentParser; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.CqlParser; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.UTName; import org.apache.cassandra.cql3.functions.masking.ColumnMask; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.guardrails.UserKeyspaceFilter; +import org.apache.cassandra.db.guardrails.UserKeyspaceFilterProvider; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.CounterColumnType; +import org.apache.cassandra.db.marshal.EmptyType; +import org.apache.cassandra.db.marshal.ReversedType; +import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.exceptions.AlreadyExistsException; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.schema.DroppedColumn; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.schema.Types; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; -import static java.util.Comparator.comparing; - import static com.google.common.collect.Iterables.concat; +import static java.util.Comparator.comparing; public final class CreateTableStatement extends AlterSchemaStatement { @@ -68,7 +97,8 @@ public final class CreateTableStatement extends AlterSchemaStatement private final boolean ifNotExists; private final boolean useCompactStorage; - public CreateTableStatement(String keyspaceName, + public CreateTableStatement(String queryString, + String keyspaceName, String tableName, Map rawColumns, Set staticColumns, @@ -79,7 +109,7 @@ public CreateTableStatement(String keyspaceName, boolean ifNotExists, boolean useCompactStorage) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.rawColumns = rawColumns; @@ -94,6 +124,23 @@ public CreateTableStatement(String keyspaceName, this.useCompactStorage = useCompactStorage; } + public boolean isCompactStorage() + { + return useCompactStorage; + } + + // CNDB-14199: the method is needed for CNDB + public boolean containsDateRangeTypeColumn() + { + for (ColumnProperties.Raw columnType : rawColumns.values()) + { + if (columnType.rawType.isDateRange()) + return true; + } + + return false; + } + public Keyspaces apply(Keyspaces schema) { KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); @@ -128,18 +175,28 @@ public void validate(ClientState state) { super.validate(state); + if (!state.isInternal && tableName.length() > SchemaConstants.NAME_LENGTH - keyspaceName.length()) + throw ire("Table name is too long, it needs to fit %s characters (got table name of %s chars for %s.%s)", + SchemaConstants.NAME_LENGTH - keyspaceName.length(), tableName.length(), keyspaceName, tableName); + // Guardrail on table properties Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state); + // Guardrail on counter + if (rawColumns.values().stream().anyMatch(t -> t.rawType.isCounter())) + Guardrails.counterEnabled.ensureEnabled(state); + // Guardrail on columns per table Guardrails.columnsPerTable.guard(rawColumns.size(), tableName, false, state); // Guardrail on number of tables if (Guardrails.tables.enabled(state)) { + UserKeyspaceFilter userKeyspaceFilter = UserKeyspaceFilterProvider.instance.get(state); int totalUserTables = Schema.instance.getUserKeyspaces() .stream() .map(Keyspace::open) + .filter(userKeyspaceFilter::filter) .mapToInt(keyspace -> keyspace.getColumnFamilyStores().size()) .sum(); Guardrails.tables.guard(totalUserTables + 1, tableName, false, state); @@ -190,20 +247,6 @@ public TableMetadata.Builder builder(Types types) Map columns = new TreeMap<>(comparing(o -> o.bytes)); rawColumns.forEach((column, properties) -> columns.put(column, properties.prepare(keyspaceName, tableName, column, types))); - // check for nested non-frozen UDTs or collections in a non-frozen UDT - columns.forEach((column, properties) -> - { - AbstractType type = properties.type; - if (type.isUDT() && type.isMultiCell()) - { - ((UserType) type).fieldTypes().forEach(field -> - { - if (field.isMultiCell()) - throw ire("Non-frozen UDTs with nested non-frozen collections are not supported"); - }); - } - }); - /* * Deal with PRIMARY KEY columns */ @@ -218,22 +261,6 @@ public TableMetadata.Builder builder(Types types) if (!primaryKeyColumns.add(column)) throw ire("Duplicate column '%s' in PRIMARY KEY clause for table '%s'", column, tableName); - AbstractType type = properties.type; - if (type.isMultiCell()) - { - CQL3Type cqlType = properties.cqlType; - if (type.isCollection()) - throw ire("Invalid non-frozen collection type %s for PRIMARY KEY column '%s'", cqlType, column); - else - throw ire("Invalid non-frozen user-defined type %s for PRIMARY KEY column '%s'", cqlType, column); - } - - if (type.isCounter()) - throw ire("counter type is not supported for PRIMARY KEY column '%s'", column); - - if (type.referencesDuration()) - throw ire("duration type is not supported for PRIMARY KEY column '%s'", column); - if (staticColumns.contains(column)) throw ire("Static column '%s' cannot be part of the PRIMARY KEY", column); }); @@ -295,11 +322,6 @@ public TableMetadata.Builder builder(Types types) boolean hasCounters = rawColumns.values().stream().anyMatch(c -> c.rawType.isCounter()); if (hasCounters) { - // We've handled anything that is not a PRIMARY KEY so columns only contains NON-PK columns. So - // if it's a counter table, make sure we don't have non-counter types - if (columns.values().stream().anyMatch(t -> !t.type.isCounter())) - throw ire("Cannot mix counter and non counter columns in the same table"); - if (params.defaultTimeToLive > 0) throw ire("Cannot set %s on a table with counters", TableParams.Option.DEFAULT_TIME_TO_LIVE); } @@ -341,6 +363,8 @@ public TableMetadata.Builder builder(Types types) builder.addRegularColumn(column, properties.type, properties.mask); }); } + for (DroppedColumn.Raw record : attrs.droppedColumnRecords()) + builder.recordColumnDrop(record.prepare(keyspaceName, tableName, types)); return builder; } @@ -419,6 +443,39 @@ else if (!builder.hasRegularColumns()) } } + @Override + public Set clientWarnings(KeyspacesDiff diff) + { + ImmutableSet.Builder warnings = ImmutableSet.builder(); + + if (attrs.hasUnsupportedDseCompaction()) + { + Map compactionOptions = attrs.getMap(TableParams.Option.COMPACTION.toString()); + String strategy = compactionOptions.get(CompactionParams.Option.CLASS.toString()); + warnings.add(String.format("The given compaction strategy (%s) is not supported. ", strategy) + + "The compaction strategy parameter was overridden with the default " + + String.format("(%s). ", CompactionParams.DEFAULT.klass().getCanonicalName()) + + "Inspect your schema and adjust other table properties if needed."); + } + + if (attrs.hasProperty("nodesync")) + { + warnings.add("The unsupported 'nodesync' table option was ignored."); + } + + if (attrs.hasProperty("dse_vertex_label_property")) + { + warnings.add("The unsupported graph table property was ignored (VERTEX LABEL)."); + } + + if (attrs.hasProperty("dse_edge_label_property")) + { + warnings.add("The unsupported graph table property was ignored (EDGE LABEL)."); + } + + return warnings.build(); + } + private static class DefaultNames { private static final String DEFAULT_CLUSTERING_NAME = "column"; @@ -457,14 +514,19 @@ public String defaultCompactValueName() } public static TableMetadata.Builder parse(String cql, String keyspace) + { + return parse(cql, keyspace, Types.none()); + } + + public static TableMetadata.Builder parse(String cql, String keyspace, Types types) { return CQLFragmentParser.parseAny(CqlParser::createTableStatement, cql, "CREATE TABLE") - .keyspace(keyspace) - .prepare(null) // works around a messy ClientState/QueryProcessor class init deadlock - .builder(Types.none()); + .keyspace(keyspace) + .prepare(null) // works around a messy ClientState/QueryProcessor class init deadlock + .builder(types); } - public final static class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName name; private final boolean ifNotExists; @@ -485,14 +547,19 @@ public Raw(QualifiedName name, boolean ifNotExists) this.ifNotExists = ifNotExists; } - public CreateTableStatement prepare(ClientState state) + @Override + public CreateTableStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + rawColumns.values().forEach(t -> t.forEachUserType(utName -> utName.updateKeyspaceIfDefined(keyspaceMapper))); if (null == partitionKeyColumns) throw ire("No PRIMARY KEY specifed for table '%s' (exactly one required)", name); - return new CreateTableStatement(keyspaceName, + return new CreateTableStatement(rawCQLStatement, + keyspaceName, name.getName(), rawColumns, staticColumns, @@ -613,6 +680,11 @@ public ColumnProperties prepare(String keyspace, String table, ColumnIdentifier ColumnMask mask = rawMask == null ? null : rawMask.prepare(keyspace, table, column, type); return new ColumnProperties(type, cqlType, mask); } + + public void forEachUserType(Consumer keyspaceMapper) + { + rawType.forEachUserType(keyspaceMapper); + } } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java index e85ffd80aecd..39d7242223e1 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java @@ -17,17 +17,22 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QualifiedName; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TriggerMetadata; import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.triggers.TriggerExecutor; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import org.apache.cassandra.triggers.TriggerExecutor; public final class CreateTriggerStatement extends AlterSchemaStatement { @@ -36,9 +41,10 @@ public final class CreateTriggerStatement extends AlterSchemaStatement private final String triggerClass; private final boolean ifNotExists; - public CreateTriggerStatement(String keyspaceName, String tableName, String triggerName, String triggerClass, boolean ifNotExists) + public CreateTriggerStatement(String queryString, String keyspaceName, String tableName, + String triggerName, String triggerClass, boolean ifNotExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.triggerName = triggerName; this.triggerClass = triggerClass; @@ -101,7 +107,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, triggerName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName tableName; private final String triggerName; @@ -116,10 +122,12 @@ public Raw(QualifiedName tableName, String triggerName, String triggerClass, boo this.ifNotExists = ifNotExists; } - public CreateTriggerStatement prepare(ClientState state) + @Override + public CreateTriggerStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace(); - return new CreateTriggerStatement(keyspaceName, tableName.getName(), triggerName, triggerClass, ifNotExists); + String keyspaceName = keyspaceMapper.apply(tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace()); + return new CreateTriggerStatement(rawCQLStatement, keyspaceName, tableName.getName(), + triggerName, triggerClass, ifNotExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java index d76c8089f60a..b05048c7ab5d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java @@ -17,15 +17,22 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.CQLFragmentParser; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.CqlParser; import org.apache.cassandra.cql3.FieldIdentifier; import org.apache.cassandra.cql3.UTName; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; @@ -38,9 +45,8 @@ import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; -import static org.apache.cassandra.utils.ByteBufferUtil.bytes; - import static java.util.stream.Collectors.toList; +import static org.apache.cassandra.utils.ByteBufferUtil.bytes; public final class CreateTypeStatement extends AlterSchemaStatement { @@ -49,13 +55,14 @@ public final class CreateTypeStatement extends AlterSchemaStatement private final List rawFieldTypes; private final boolean ifNotExists; - public CreateTypeStatement(String keyspaceName, + public CreateTypeStatement(String queryString, + String keyspaceName, String typeName, List fieldNames, List rawFieldTypes, boolean ifNotExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.typeName = typeName; this.fieldNames = fieldNames; this.rawFieldTypes = rawFieldTypes; @@ -95,21 +102,14 @@ public Keyspaces apply(Keyspaces schema) if (!usedNames.add(name)) throw ire("Duplicate field name '%s' in type '%s'", name, typeName); - for (CQL3Type.Raw type : rawFieldTypes) - { - if (type.isCounter()) - throw ire("A user type cannot contain counters"); - - if (type.isUDT() && !type.isFrozen()) - throw ire("A user type cannot contain non-frozen UDTs"); - } - List> fieldTypes = rawFieldTypes.stream() .map(t -> t.prepare(keyspaceName, keyspace.types).getType()) .collect(toList()); UserType udt = new UserType(keyspaceName, bytes(typeName), fieldNames, fieldTypes, true); + validate(udt); + return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.types.with(udt))); } @@ -134,7 +134,76 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, typeName); } - public static final class Raw extends CQLStatement.Raw + // CNDB-14199: the method is needed for CNDB + public boolean containsDateRangeTypeColumn() + { + for (CQL3Type.Raw fieldType : rawFieldTypes) + { + if (fieldType.isDateRange()) + return true; + } + + return false; + } + + public static UserType parse(String cql, String keyspace) + { + return parse(cql, keyspace, Types.none()); + } + + public static UserType parse(String cql, String keyspace, Types userTypes) + { + return CQLFragmentParser.parseAny(CqlParser::createTypeStatement, cql, "CREATE TYPE") + .keyspace(keyspace) + .prepare(null) // works around a messy ClientState/QueryProcessor class init deadlock + .createType(userTypes); + } + + /** + * Build the {@link UserType} this statement creates. + * + * @param existingTypes the user-types existing in the keyspace in which the type is created (and thus on which + * the created type may depend on). + * @return the created type. + */ + private UserType createType(Types existingTypes) + { + List> fieldTypes = rawFieldTypes.stream() + .map(t -> t.prepare(keyspaceName, existingTypes).getType()) + .collect(toList()); + UserType type = new UserType(keyspaceName, bytes(typeName), fieldNames, fieldTypes, true); + validate(type); + return type; + } + + /** + * Ensures that the created User-Defined Type (UDT) is valid and allowed. + *

+ * Note: Most type validation is performed through {@link AbstractType#validateForColumn} because almost no type + * is intrinsically invalid unless used as a column type. For instance, while we don't declare a column with a + * {@code set} type, there is no reason to forbid a UDF in a SELECT clause that takes two separate + * counter values and puts them in a set. Thus, {@code set} is not intrinsically invalid, and this applies + * to almost all validation in {@link AbstractType#validateForColumn}. + *

+ * However, since UDTs are created separately from their use, it makes sense for user-friendliness to be a bit + * more restrictive: if a UDT cannot ever be used as a column type, it is almost certainly a user error. Waiting + * until the type is used to throw an error might be annoying. Therefore, we do not allow creating types that + * cannot ever be used as column types, even if this is an arbitrary limitation in some ways (e.g., a user may + * "legitimately" want to create a type solely for use as the return type of a UDF, similar to the {@code set} + * example above, but we disallow that). + * + * @param type the User-Defined Type to validate + * @throws IllegalArgumentException if the UDT contains counters, as counters are always disallowed in UDTs + */ + static void validate(UserType type) + { + // The only thing that is always disallowed is the use of counters within a UDT. Anything else might be acceptable, + // though possibly only if the type is used frozen. + if (type.referencesCounter()) + throw ire("A user type cannot contain counters"); + } + + public static final class Raw extends RawKeyspaceAwareStatement { private final UTName name; private final boolean ifNotExists; @@ -148,10 +217,21 @@ public Raw(UTName name, boolean ifNotExists) this.ifNotExists = ifNotExists; } - public CreateTypeStatement prepare(ClientState state) + public Raw keyspace(String keyspace) + { + name.setKeyspace(keyspace); + return this; + } + + @Override + public CreateTypeStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); - return new CreateTypeStatement(keyspaceName, name.getStringTypeName(), fieldNames, rawFieldTypes, ifNotExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + rawFieldTypes.forEach(t -> t.forEachUserType(utName -> utName.updateKeyspaceIfDefined(keyspaceMapper))); + return new CreateTypeStatement(rawCQLStatement, keyspaceName, + name.getStringTypeName(), fieldNames, + rawFieldTypes, ifNotExists); } public void addField(FieldIdentifier name, CQL3Type.Raw type) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java index 05629e00fdeb..494655ddabcc 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java @@ -17,7 +17,13 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -27,29 +33,38 @@ import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.cql3.selection.RawSelector; import org.apache.cassandra.cql3.selection.Selectable; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.cql3.statements.StatementType; import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ReversedType; import org.apache.cassandra.db.view.View; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; -import static java.lang.String.join; - import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; +import static java.lang.String.join; import static org.apache.cassandra.config.CassandraRelevantProperties.MV_ALLOW_FILTERING_NONKEY_COLUMNS_UNSAFE; public final class CreateViewStatement extends AlterSchemaStatement @@ -70,7 +85,8 @@ public final class CreateViewStatement extends AlterSchemaStatement private ClientState state; - public CreateViewStatement(String keyspaceName, + public CreateViewStatement(String queryString, + String keyspaceName, String tableName, String viewName, @@ -85,7 +101,7 @@ public CreateViewStatement(String keyspaceName, boolean ifNotExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.viewName = viewName; @@ -273,17 +289,17 @@ public Keyspaces apply(Keyspaces schema) if (whereClause.containsCustomExpressions()) throw ire("WHERE clause for materialized view '%s' cannot contain custom index expressions", viewName); - StatementRestrictions restrictions = - new StatementRestrictions(state, - StatementType.SELECT, - table, - whereClause, - VariableSpecifications.empty(), - Collections.emptyList(), - false, - false, - true, - true); + StatementRestrictions restrictions = StatementRestrictions.create(state, + StatementType.SELECT, + table, + whereClause, + VariableSpecifications.empty(), + Collections.emptyList(), + IndexHints.NONE, + false, + false, + true, + true); List nonRestrictedPrimaryKeyColumns = Lists.newArrayList(filter(primaryKeyColumns, name -> !restrictions.isRestricted(table.getColumn(name)))); @@ -365,7 +381,7 @@ private AbstractType getType(ColumnMetadata column) boolean reverse = !clusteringOrder.get(column.name); if (type.isReversed() && !reverse) - return ((ReversedType) type).baseType; + return type.unwrap(); if (!type.isReversed() && reverse) return ReversedType.getInstance(type); @@ -390,7 +406,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, viewName); } - public final static class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName tableName; private final QualifiedName viewName; @@ -414,7 +430,8 @@ public Raw(QualifiedName tableName, QualifiedName viewName, List ra this.ifNotExists = ifNotExists; } - public CreateViewStatement prepare(ClientState state) + @Override + public CreateViewStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { String keyspaceName = viewName.hasKeyspace() ? viewName.getKeyspace() : state.getKeyspace(); @@ -427,7 +444,8 @@ public CreateViewStatement prepare(ClientState state) if (null == partitionKeyColumns) throw ire("No PRIMARY KEY specifed for view '%s' (exactly one required)", viewName); - return new CreateViewStatement(keyspaceName, + return new CreateViewStatement(rawCQLStatement, + keyspaceMapper.apply(keyspaceName), tableName.getName(), viewName.getName(), diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java index d83fbbf97f5b..03f2d2f48c7d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.apache.cassandra.audit.AuditLogContext; @@ -27,23 +28,27 @@ import org.apache.cassandra.auth.FunctionResource; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.functions.FunctionName; import org.apache.cassandra.cql3.functions.UDAggregate; import org.apache.cassandra.cql3.functions.UserFunction; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; +import static com.google.common.collect.Iterables.transform; import static java.lang.String.format; import static java.lang.String.join; import static java.util.stream.Collectors.toList; -import static com.google.common.collect.Iterables.transform; - public final class DropAggregateStatement extends AlterSchemaStatement { private final String aggregateName; @@ -51,13 +56,14 @@ public final class DropAggregateStatement extends AlterSchemaStatement private final boolean argumentsSpeficied; private final boolean ifExists; - public DropAggregateStatement(String keyspaceName, + public DropAggregateStatement(String queryString, + String keyspaceName, String aggregateName, List arguments, boolean argumentsSpeficied, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.aggregateName = aggregateName; this.arguments = arguments; this.argumentsSpeficied = argumentsSpeficied; @@ -152,7 +158,7 @@ private List> prepareArgumentTypes(Types types) .collect(toList()); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final FunctionName name; private final List arguments; @@ -170,10 +176,14 @@ public Raw(FunctionName name, this.ifExists = ifExists; } - public DropAggregateStatement prepare(ClientState state) + @Override + public DropAggregateStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.keyspace : state.getKeyspace(); - return new DropAggregateStatement(keyspaceName, name.name, arguments, argumentsSpecified, ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.keyspace : state.getKeyspace()); + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + arguments.forEach(t -> t.forEachUserType(name -> name.updateKeyspaceIfDefined(keyspaceMapper))); + return new DropAggregateStatement(rawCQLStatement, keyspaceName, name.name, + arguments, argumentsSpecified, ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java index af822063226a..a8fa189581de 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.apache.cassandra.audit.AuditLogContext; @@ -27,24 +28,28 @@ import org.apache.cassandra.auth.FunctionResource; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.functions.FunctionName; import org.apache.cassandra.cql3.functions.UDFunction; import org.apache.cassandra.cql3.functions.UserFunction; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; +import static com.google.common.collect.Iterables.transform; import static java.lang.String.format; import static java.lang.String.join; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; -import static com.google.common.collect.Iterables.transform; - public final class DropFunctionStatement extends AlterSchemaStatement { private final String functionName; @@ -52,13 +57,14 @@ public final class DropFunctionStatement extends AlterSchemaStatement private final boolean argumentsSpeficied; private final boolean ifExists; - public DropFunctionStatement(String keyspaceName, + public DropFunctionStatement(String queryString, + String keyspaceName, String functionName, Collection arguments, boolean argumentsSpeficied, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.functionName = functionName; this.arguments = arguments; this.argumentsSpeficied = argumentsSpeficied; @@ -169,7 +175,7 @@ private List> prepareArgumentTypes(Types types) .collect(toList()); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final FunctionName name; private final List arguments; @@ -187,10 +193,14 @@ public Raw(FunctionName name, this.ifExists = ifExists; } - public DropFunctionStatement prepare(ClientState state) + @Override + public DropFunctionStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.keyspace : state.getKeyspace(); - return new DropFunctionStatement(keyspaceName, name.name, arguments, argumentsSpecified, ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.keyspace : state.getKeyspace()); + if (keyspaceMapper != Constants.IDENTITY_STRING_MAPPER) + arguments.forEach(t -> t.forEachUserType(name -> name.updateKeyspaceIfDefined(keyspaceMapper))); + return new DropFunctionStatement(rawCQLStatement, keyspaceName, name.name, arguments, + argumentsSpecified, ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java index 24b372d8c3c4..445643bb3831 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java @@ -17,14 +17,20 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QualifiedName; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; +import org.apache.cassandra.schema.Diff; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -35,9 +41,10 @@ public final class DropIndexStatement extends AlterSchemaStatement private final String indexName; private final boolean ifExists; - public DropIndexStatement(String keyspaceName, String indexName, boolean ifExists) + public DropIndexStatement(String queryString, String keyspaceName, String indexName, + boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.indexName = indexName; this.ifExists = ifExists; } @@ -94,7 +101,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, indexName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName name; private final boolean ifExists; @@ -105,10 +112,11 @@ public Raw(QualifiedName name, boolean ifExists) this.ifExists = ifExists; } - public DropIndexStatement prepare(ClientState state) + @Override + public DropIndexStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); - return new DropIndexStatement(keyspaceName, name.getName(), ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + return new DropIndexStatement(rawCQLStatement, keyspaceName, name.getName(), ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java index 47e514a527fe..4c2212569776 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java @@ -17,10 +17,12 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; @@ -32,9 +34,9 @@ public final class DropKeyspaceStatement extends AlterSchemaStatement { private final boolean ifExists; - public DropKeyspaceStatement(String keyspaceName, boolean ifExists) + public DropKeyspaceStatement(String queryString, String keyspaceName, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.ifExists = ifExists; } @@ -72,7 +74,7 @@ public String toString() return String.format("%s (%s)", getClass().getSimpleName(), keyspaceName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final String keyspaceName; private final boolean ifExists; @@ -83,9 +85,10 @@ public Raw(String keyspaceName, boolean ifExists) this.ifExists = ifExists; } - public DropKeyspaceStatement prepare(ClientState state) + @Override + public DropKeyspaceStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - return new DropKeyspaceStatement(keyspaceName, ifExists); + return new DropKeyspaceStatement(rawCQLStatement, keyspaceMapper.apply(keyspaceName), ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java index 78c98be3a70c..6266607b0638 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java @@ -17,32 +17,37 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; -import static java.lang.String.join; - import static com.google.common.collect.Iterables.isEmpty; import static com.google.common.collect.Iterables.transform; +import static java.lang.String.join; public final class DropTableStatement extends AlterSchemaStatement { private final String tableName; private final boolean ifExists; - public DropTableStatement(String keyspaceName, String tableName, boolean ifExists) + public DropTableStatement(String queryString, String keyspaceName, String tableName, + boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.ifExists = ifExists; } @@ -100,7 +105,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, tableName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName name; private final boolean ifExists; @@ -111,10 +116,11 @@ public Raw(QualifiedName name, boolean ifExists) this.ifExists = ifExists; } - public DropTableStatement prepare(ClientState state) + @Override + public DropTableStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); - return new DropTableStatement(keyspaceName, name.getName(), ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + return new DropTableStatement(rawCQLStatement, keyspaceName, name.getName(), ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java index 967e56834f09..99bad41fa83a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java @@ -17,12 +17,17 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QualifiedName; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TriggerMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -34,9 +39,10 @@ public final class DropTriggerStatement extends AlterSchemaStatement private final String triggerName; private final boolean ifExists; - public DropTriggerStatement(String keyspaceName, String tableName, String triggerName, boolean ifExists) + public DropTriggerStatement(String queryString, String keyspaceName, String tableName, + String triggerName, boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.tableName = tableName; this.triggerName = triggerName; this.ifExists = ifExists; @@ -87,7 +93,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, triggerName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName tableName; private final String triggerName; @@ -100,10 +106,12 @@ public Raw(QualifiedName tableName, String triggerName, boolean ifExists) this.ifExists = ifExists; } - public DropTriggerStatement prepare(ClientState state) + @Override + public DropTriggerStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace(); - return new DropTriggerStatement(keyspaceName, tableName.getName(), triggerName, ifExists); + String keyspaceName = keyspaceMapper.apply(tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace()); + return new DropTriggerStatement(rawCQLStatement, keyspaceName, tableName.getName(), + triggerName, ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java index 97830c882aff..8a8eb8d43083 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java @@ -18,28 +18,27 @@ package org.apache.cassandra.cql3.statements.schema; import java.nio.ByteBuffer; +import java.util.function.UnaryOperator; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.UTName; import org.apache.cassandra.cql3.functions.UserFunction; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; -import org.apache.cassandra.transport.Event.SchemaChange; - -import static java.lang.String.join; import static com.google.common.collect.Iterables.isEmpty; import static com.google.common.collect.Iterables.transform; - +import static java.lang.String.join; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; public final class DropTypeStatement extends AlterSchemaStatement @@ -47,9 +46,10 @@ public final class DropTypeStatement extends AlterSchemaStatement private final String typeName; private final boolean ifExists; - public DropTypeStatement(String keyspaceName, String typeName, boolean ifExists) + public DropTypeStatement(String queryString, String keyspaceName, String typeName, + boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.typeName = typeName; this.ifExists = ifExists; } @@ -134,7 +134,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, typeName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final UTName name; private final boolean ifExists; @@ -145,10 +145,12 @@ public Raw(UTName name, boolean ifExists) this.ifExists = ifExists; } - public DropTypeStatement prepare(ClientState state) + @Override + public DropTypeStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); - return new DropTypeStatement(keyspaceName, name.getStringTypeName(), ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + return new DropTypeStatement(rawCQLStatement, keyspaceName, name.getStringTypeName(), + ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java index 2c73717546c7..fd5e32178401 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java @@ -17,13 +17,18 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.function.UnaryOperator; + import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QualifiedName; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.cql3.statements.RawKeyspaceAwareStatement; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -34,9 +39,10 @@ public final class DropViewStatement extends AlterSchemaStatement private final String viewName; private final boolean ifExists; - public DropViewStatement(String keyspaceName, String viewName, boolean ifExists) + public DropViewStatement(String queryString, String keyspaceName, String viewName, + boolean ifExists) { - super(keyspaceName); + super(queryString, keyspaceName); this.viewName = viewName; this.ifExists = ifExists; } @@ -83,7 +89,7 @@ public String toString() return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, viewName); } - public static final class Raw extends CQLStatement.Raw + public static final class Raw extends RawKeyspaceAwareStatement { private final QualifiedName name; private final boolean ifExists; @@ -94,10 +100,11 @@ public Raw(QualifiedName name, boolean ifExists) this.ifExists = ifExists; } - public DropViewStatement prepare(ClientState state) + @Override + public DropViewStatement prepare(ClientState state, UnaryOperator keyspaceMapper) { - String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); - return new DropViewStatement(keyspaceName, name.getName(), ifExists); + String keyspaceName = keyspaceMapper.apply(name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace()); + return new DropViewStatement(rawCQLStatement, keyspaceName, name.getName(), ifExists); } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/IndexAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/IndexAttributes.java index f30c502ca8b7..431529c5408d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/IndexAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/IndexAttributes.java @@ -24,6 +24,8 @@ import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.SyntaxException; +import static org.apache.cassandra.config.CassandraRelevantProperties.DEFAULT_INDEX_CLASS; + public class IndexAttributes extends PropertyDefinitions { private static final String KW_OPTIONS = "options"; @@ -39,6 +41,19 @@ public class IndexAttributes extends PropertyDefinitions keywords.add(KW_OPTIONS); } + public void maybeApplyDefaultIndex() + { + String defaultIndexClass = DEFAULT_INDEX_CLASS.getString(); + if (defaultIndexClass == null) + return; + + if (!isCustom && customClass == null) + { + isCustom = true; + customClass = defaultIndexClass; + } + } + public void validate() throws RequestValidationException { validate(keywords, obsoleteKeywords); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java index d4d5b984b3c3..189d2aecd571 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java @@ -38,7 +38,7 @@ public final class KeyspaceAttributes extends PropertyDefinitions for (Option option : Option.values()) validBuilder.add(option.toString()); validKeywords = validBuilder.build(); - obsoleteKeywords = ImmutableSet.of(); + obsoleteKeywords = ImmutableSet.of("graph_engine"); } public void validate() diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java index 93d477c8470d..3b6ab81733df 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java @@ -17,17 +17,28 @@ */ package org.apache.cassandra.cql3.statements.schema; +import java.util.Collection; +import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.functions.types.utils.Bytes; import org.apache.cassandra.cql3.statements.PropertyDefinitions; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.schema.AutoRepairParams; import org.apache.cassandra.schema.CachingParams; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.schema.DroppedColumn; import org.apache.cassandra.schema.MemtableParams; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableParams; @@ -41,8 +52,19 @@ public final class TableAttributes extends PropertyDefinitions { public static final String ID = "id"; - private static final Set validKeywords; - private static final Set obsoleteKeywords; + public static final Set validKeywords; + private static final Set obsoleteKeywords = ImmutableSet.of( + "nodesync", + "dse_vertex_label_property", + "dse_edge_label_property" + ); + + private static final Set UNSUPPORTED_DSE_COMPACTION_STRATEGIES = ImmutableSet.of( + "org.apache.cassandra.db.compaction.TieredCompactionStrategy", + "TieredCompactionStrategy", + "org.apache.cassandra.db.compaction.MemoryOnlyStrategy", + "MemoryOnlyStrategy" + ); static { @@ -51,15 +73,32 @@ public final class TableAttributes extends PropertyDefinitions validBuilder.add(option.toString()); validBuilder.add(ID); validKeywords = validBuilder.build(); - obsoleteKeywords = ImmutableSet.of(); } + private final Map droppedColumnRecords = new HashMap<>(); + public void validate() { validate(validKeywords, obsoleteKeywords); + + if (hasOption(AUTO_REPAIR) && !CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean()) + throw new ConfigurationException("auto_repair option is not supported unless auto-repair is enabled with -Dcassandra.autorepair.enable=true"); + build(TableParams.builder()).validate(); } + public void addDroppedColumnRecord(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, long timestamp) + { + DroppedColumn.Raw newRecord = new DroppedColumn.Raw(name, type, isStatic, timestamp); + if (droppedColumnRecords.put(name, newRecord) != null) + throw new InvalidRequestException(String.format("Cannot have multiple dropped column record for column %s", name)); + } + + public Collection droppedColumnRecords() + { + return droppedColumnRecords.values(); + } + TableParams asNewTableParams() { return build(TableParams.builder()); @@ -74,7 +113,7 @@ TableParams asAlteredTableParams(TableParams previous) public TableId getId() throws ConfigurationException { - String id = getString(ID); + String id = getSimple(ID); try { return id != null ? TableId.fromString(id) : null; @@ -95,6 +134,24 @@ public static Set allKeywords() return Sets.union(validKeywords, obsoleteKeywords); } + /** + * Returs `true` if this attributes instance has a COMPACTION option with a recognized unsupported compaction + * strategy class (coming from DSE). `false` otherwise. + */ + boolean hasUnsupportedDseCompaction() + { + if (hasOption(Option.COMPACTION)) + { + Map compactionOptions = getMap(Option.COMPACTION); + String strategy = compactionOptions.get(CompactionParams.Option.CLASS.toString()); + return UNSUPPORTED_DSE_COMPACTION_STRATEGIES.contains(strategy); + } + else + { + return false; + } + } + private TableParams build(TableParams.Builder builder) { if (hasOption(ALLOW_AUTO_SNAPSHOT)) @@ -108,22 +165,80 @@ private TableParams build(TableParams.Builder builder) if (hasOption(COMMENT)) builder.comment(getString(COMMENT)); - - if (hasOption(COMPACTION)) - builder.compaction(CompactionParams.fromMap(getMap(COMPACTION))); + + if (hasOption(Option.COMPACTION)) + { + if (hasUnsupportedDseCompaction()) + builder.compaction(CompactionParams.DEFAULT); + else + builder.compaction(CompactionParams.fromMap(getMap(Option.COMPACTION))); + } if (hasOption(COMPRESSION)) builder.compression(CompressionParams.fromMap(getMap(COMPRESSION))); if (hasOption(MEMTABLE)) - builder.memtable(MemtableParams.get(getString(MEMTABLE))); + { + // Handle deserialization of Astra/CC 4.0 schema with memtable option as a map + if (properties.get(MEMTABLE.toString()) instanceof Map) + { + String memtableClass = getMap(MEMTABLE) + .entrySet() + .stream() + .filter(e -> e.getKey().equals("class")) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (memtableClass == null) + { + builder.memtable(MemtableParams.get(null)); + } + // Only process as a known memtable if it's in the standard package or is a short name (no package qualifier) + else if (memtableClass.startsWith("org.apache.cassandra.db.memtable.") || !memtableClass.contains(".")) + { + // Extract short class name for comparison against known types + String shortClassName = memtableClass.contains(".") + ? memtableClass.substring(memtableClass.lastIndexOf('.') + 1) + : memtableClass; + + if ("SkipListMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("skiplist")); + else if ("PersistentMemoryMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("persistent_memory")); + else if ("TrieMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("trie")); + else if ("TrieMemtableStage1".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("trie")); + else if ("ShardedSkipListMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("skiplist_sharded")); + else + // Unknown short name or unknown class in standard package - use as configuration key + builder.memtable(MemtableParams.get(shortClassName)); + } + else + { + // Custom fully qualified class name from a different package. + builder.memtable(MemtableParams.get(memtableClass)); + } + } + else + builder.memtable(MemtableParams.get(getString(MEMTABLE))); + } if (hasOption(DEFAULT_TIME_TO_LIVE)) builder.defaultTimeToLive(getInt(DEFAULT_TIME_TO_LIVE)); + // extensions in CQL are strings, but are stored as a frozen map + if (hasOption(EXTENSIONS)) + builder.extensions(getMap(EXTENSIONS) + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> Bytes.fromHexString(entry.getValue())))); + if (hasOption(GC_GRACE_SECONDS)) builder.gcGraceSeconds(getInt(GC_GRACE_SECONDS)); - + if (hasOption(INCREMENTAL_BACKUPS)) builder.incrementalBackups(getBoolean(INCREMENTAL_BACKUPS.toString(), true)); @@ -151,6 +266,9 @@ private TableParams build(TableParams.Builder builder) if (hasOption(READ_REPAIR)) builder.readRepair(ReadRepairStrategy.fromString(getString(READ_REPAIR))); + if (hasOption(Option.AUTO_REPAIR)) + builder.automatedRepair(AutoRepairParams.fromMap(getMap(Option.AUTO_REPAIR))); + return builder.build(); } @@ -161,7 +279,7 @@ public boolean hasOption(Option option) private String getString(Option option) { - String value = getString(option.toString()); + String value = getSimple(option.toString()); if (value == null) throw new IllegalStateException(format("Option '%s' is absent", option)); return value; @@ -182,11 +300,19 @@ private boolean getBoolean(Option option) private int getInt(Option option) { - return parseInt(option.toString(), getString(option)); + return toInt(option.toString(), getString(option), null); } private double getDouble(Option option) { - return parseDouble(option.toString(), getString(option)); + String value = getString(option); + try + { + return Double.parseDouble(value); + } + catch (NumberFormatException e) + { + throw new SyntaxException(String.format("Invalid double value %s for '%s'", value, option)); + } } } diff --git a/src/java/org/apache/cassandra/crypto/IKeyProvider.java b/src/java/org/apache/cassandra/crypto/IKeyProvider.java new file mode 100644 index 000000000000..a6660cde2e89 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/IKeyProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + + +import javax.crypto.SecretKey; + +/** + * Interface for objects managing cryptographic secret keys + * used for encryption and decryption of CFs. + */ +public interface IKeyProvider +{ + /** + * Returns a key for the given cipher algorithm and key strength. + * If the key for the given algorithm and length is requested for the first time, it should be created. + * If the key is requested for the second time or more, always the same key should be returned. + * + * @param cipherName name of the JCE cipher, optionally with mode and padding + * @param keyStrength key length in bits + * @return a valid secret key, never returns null + * @throws KeyAccessException when the key exists but could not be retrieved, e.g. from the disk or external storage + * @throws KeyGenerationException when invalid cipherName was given, or keyStrength does not match the algorithm + */ + SecretKey getSecretKey(String cipherName, int keyStrength) throws KeyAccessException, KeyGenerationException; +} diff --git a/src/java/org/apache/cassandra/crypto/IKeyProviderFactory.java b/src/java/org/apache/cassandra/crypto/IKeyProviderFactory.java new file mode 100644 index 000000000000..2120266881c9 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/IKeyProviderFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Factory for obtaining secret key providers. + * Instances implementing IKeyProviderFactory are created by reflection by calling a default constructor. + */ +public interface IKeyProviderFactory +{ + /** + * Returns a key provider configured with the given options. + * It is allowed to return the same instance for the same options. + * @param options options dependent on the actual key provider type + * @return secret key provider + * @throws IOException if key provider could not be contacted + */ + IKeyProvider getKeyProvider(Map options) throws IOException; + + /** + * @return a list of options supported by getKeyProvider + */ + Set supportedOptions(); + +} diff --git a/src/java/org/apache/cassandra/crypto/IMultiKeyProvider.java b/src/java/org/apache/cassandra/crypto/IMultiKeyProvider.java new file mode 100644 index 000000000000..e8c460a13292 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/IMultiKeyProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +import java.nio.ByteBuffer; +import javax.crypto.SecretKey; + +/** + * Interface for key providers which may use multiple keys to encrypt data and store some header data + * in their encrypted chunks to identify which key was used. + */ +public interface IMultiKeyProvider extends IKeyProvider +{ + + /** + * Writes header data to the given buffer identifying the key it returns, which + * needs to be used to encrypt the data. + * + * Caller should assume that the byte buffer passed in will be mutated, and is + * responsible for updating any offset/length values it's using based on the state + * of the output ByteBuffer after this call returns. + * + * @param cipherName name of the JCE cipher, optionally with mode and padding + * @param keyStrength key length in bits + * @param output byte buffer to write header data to. Limit must be >= size returned by headerLength + * @return key instance identified by header data to be used for encryption + * @throws KeyAccessException when the key exists but could not be retrieved, e.g. from the disk or external storage + * @throws KeyGenerationException when invalid cipherName was given, or keyStrength does not match the algorithm + */ + SecretKey writeHeader(String cipherName, int keyStrength, ByteBuffer output) throws KeyAccessException, KeyGenerationException; + + /** + * Reads header data from the given buffer to determine which key to return, which + * needs to be used to decrypt the data. + * + * Caller should assume that the byte buffer passed in will be mutated, and is + * responsible for updating any offset/length values it's using based on the state + * of the input ByteBuffer after this call returns. + * + * @param cipherName name of the JCE cipher, optionally with mode and padding + * @param keyStrength key length in bits + * @param input byte buffer to write header data to. Limit must be >= size returned by headerLength + * @return key instance identified by header data to be used for encryption + * @throws KeyAccessException when the key exists but could not be retrieved, e.g. from the disk or external storage + * @throws KeyGenerationException when invalid cipherName was given, or keyStrength does not match the algorithm + */ + SecretKey readHeader(String cipherName, int keyStrength, ByteBuffer input) throws KeyAccessException, KeyGenerationException; + + /** + * @return size of header data to be written. + * @throws KeyAccessException when the key exists but could not be retrieved, e.g. from the disk or external storage + * @throws KeyGenerationException when invalid cipherName was given, or keyStrength does not match the algorithm + */ + int headerLength(); +} diff --git a/src/java/org/apache/cassandra/crypto/KeyAccessException.java b/src/java/org/apache/cassandra/crypto/KeyAccessException.java new file mode 100644 index 000000000000..0bb01aed07d8 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/KeyAccessException.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + + +public class KeyAccessException extends Exception +{ + public KeyAccessException() + { + } + + public KeyAccessException(String message) + { + super(message); + } + + public KeyAccessException(String message, Throwable cause) + { + super(message, cause); + } + + public KeyAccessException(Throwable cause) + { + super(cause); + } +} diff --git a/src/java/org/apache/cassandra/crypto/KeyGenerationException.java b/src/java/org/apache/cassandra/crypto/KeyGenerationException.java new file mode 100644 index 000000000000..002e5632e7f4 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/KeyGenerationException.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +public class KeyGenerationException extends Exception +{ + public KeyGenerationException() + { + } + + public KeyGenerationException(String message) + { + super(message); + } + + public KeyGenerationException(String message, Throwable cause) + { + super(message, cause); + } + + public KeyGenerationException(Throwable cause) + { + super(cause); + } +} diff --git a/src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProvider.java b/src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProvider.java new file mode 100644 index 000000000000..9dfd5c7e4f1f --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProvider.java @@ -0,0 +1,168 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.concurrent.ConcurrentMap; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import com.google.common.collect.Maps; +import java.util.Base64; + +import org.apache.cassandra.io.util.FileUtils; + +import static java.nio.file.StandardOpenOption.APPEND; +import static org.apache.cassandra.crypto.LocalSystemKey.KEY_DEFAULT_PERMISSIONS; + +public class LocalFileSystemKeyProvider implements IKeyProvider +{ + private static final SecureRandom RANDOM = new SecureRandom(); + + private final Path keyPath; + private final ConcurrentMap keys = Maps.newConcurrentMap(); + + public LocalFileSystemKeyProvider(Path keyPath) throws IOException + { + if (keyPath.getParent() == null) + { + throw new IllegalArgumentException("The key path must be absolute"); + } + + if (Files.exists(keyPath)) + { + this.keyPath = keyPath; + } + else + { + Files.createDirectories(keyPath.getParent()); + this.keyPath = Files.createFile(keyPath, KEY_DEFAULT_PERMISSIONS); + } + + loadKeys(); + } + + @Override + public SecretKey getSecretKey(String cipherName, int keyStrength) throws KeyGenerationException + { + try + { + String mapKey = getMapKey(cipherName, keyStrength); + SecretKey secretKey = keys.get(mapKey); + if (secretKey == null) + { + secretKey = generateNewKey(cipherName, keyStrength); + checkKey(cipherName, secretKey); + SecretKey previous = keys.putIfAbsent(mapKey, secretKey); + if (previous == null) + appendKey(cipherName, keyStrength, secretKey); + else + secretKey = previous; + } + return secretKey; + } + catch (IOException e) + { + throw new KeyGenerationException("Could not write secret key: " + e.getMessage(), e); + } + catch (NoSuchAlgorithmException e) + { + throw new KeyGenerationException("Failed to generate secret key: " + e.getMessage(), e); + } + } + + private void checkKey(String cipherName, SecretKey secretKey) throws KeyGenerationException + { + try + { + Cipher cipher = Cipher.getInstance(cipherName); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, RANDOM); + } + catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException e) + { + throw new KeyGenerationException("Error generating secret key: " + e.getMessage(), e); + } + } + + private SecretKey generateNewKey(String cipherName, int keyStrength) throws NoSuchAlgorithmException + { + KeyGenerator kgen = KeyGenerator.getInstance(getKeyType(cipherName)); + kgen.init(keyStrength, RANDOM); + return kgen.generateKey(); + } + + private String getMapKey(String cipherName, int keyStrength) + { + return cipherName + ":" + keyStrength; + } + + private synchronized void appendKey(String cipherName, int keyStrength, SecretKey key) throws IOException + { + PrintStream ps = null; + try + { + ps = new PrintStream(Files.newOutputStream(keyPath, APPEND)); + ps.println(cipherName + ":" + keyStrength + ":" + Base64.getEncoder().encodeToString(key.getEncoded())); + } + finally + { + FileUtils.closeQuietly(ps); + } + } + + private synchronized void loadKeys() throws IOException + { + keys.clear(); + BufferedReader is = null; + try + { + is = Files.newBufferedReader(keyPath); + String line; + while ((line = is.readLine()) != null) + { + String[] fields = line.split(":"); + String cipherName = fields[0]; + int keyStrength = Integer.parseInt(fields[1]); + byte[] key = Base64.getDecoder().decode(fields[2]); + keys.put(getMapKey(cipherName, keyStrength), new SecretKeySpec(key, getKeyType(cipherName))); + } + } + finally + { + FileUtils.closeQuietly(is); + } + } + + private String getKeyType(String cipherName) + { + return cipherName.replaceAll("/.*", ""); + } + + String getFileName() + { + return keyPath.toAbsolutePath().toString(); + } +} diff --git a/src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProviderFactory.java b/src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProviderFactory.java new file mode 100644 index 000000000000..2df62babf118 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProviderFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +import com.google.common.collect.Maps; + +import org.apache.cassandra.config.OptionMap; +import org.apache.cassandra.io.util.File; + + +public class LocalFileSystemKeyProviderFactory implements IKeyProviderFactory +{ + // Allows keyProvider reusing and guarantees at most one key provider per file: + private static final ConcurrentMap keyProviders = Maps.newConcurrentMap(); + + public static final String SECRET_KEY_FILE = "secret_key_file"; + private static final String DEFAULT_SECRET_KEY_FILE = "/etc/cassandra/conf/data_encryption_keys"; + + @Override + public IKeyProvider getKeyProvider(Map options) throws IOException + { + OptionMap optionMap = new OptionMap(options); + Path secretKeyPath = new File(optionMap.get(SECRET_KEY_FILE, DEFAULT_SECRET_KEY_FILE)).toPath(); + + LocalFileSystemKeyProvider kp = keyProviders.get(secretKeyPath); + if (kp == null) + { + kp = new LocalFileSystemKeyProvider(secretKeyPath); + LocalFileSystemKeyProvider previous = keyProviders.putIfAbsent(secretKeyPath, kp); + if (previous != null) + kp = previous; + } + return kp; + } + + @Override + public Set supportedOptions() + { + return Collections.singleton(SECRET_KEY_FILE); + } +} diff --git a/src/java/org/apache/cassandra/crypto/LocalSystemKey.java b/src/java/org/apache/cassandra/crypto/LocalSystemKey.java new file mode 100644 index 000000000000..24f2ee39e33d --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/LocalSystemKey.java @@ -0,0 +1,88 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.EnumSet; +import java.util.Set; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import java.util.Base64; + +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileUtils; + +import static java.nio.file.StandardOpenOption.APPEND; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public class LocalSystemKey +{ + static final FileAttribute> KEY_DEFAULT_PERMISSIONS = PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_READ, OWNER_WRITE)); + private static final SecureRandom RANDOM = new SecureRandom(); + + public static Path createKey(String path, String cipherName, int keyStrength) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException { + return createKey(null, path, cipherName, keyStrength); + } + + public static Path createKey(Path directory, String keyPath, String cipherName, int keyStrength) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException + { + Path targetDirectory = directory != null ? directory : new File(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).toPath(); + Path fullKeyPath = targetDirectory.resolve(keyPath); + + KeyGenerator keyGen = KeyGenerator.getInstance(getKeyType(cipherName)); + keyGen.init(keyStrength, RANDOM); + SecretKey key = keyGen.generateKey(); + + return storeKey(fullKeyPath, cipherName, keyStrength, key); + } + + static Path storeKey(Path keyPath, String cipherName, int keyStrength, SecretKey key) throws NoSuchAlgorithmException, NoSuchPaddingException, IOException + { + // validate the ciphername + Cipher.getInstance(cipherName); + Files.createDirectories(keyPath.getParent()); + Path createdKeyPath = Files.createFile(keyPath, KEY_DEFAULT_PERMISSIONS); + + PrintStream ps = null; + try + { + ps = new PrintStream(Files.newOutputStream(keyPath, APPEND)); + ps.println(cipherName + ":" + keyStrength + ":" + Base64.getEncoder().encodeToString(key.getEncoded())); + } + finally + { + FileUtils.closeQuietly(ps); + } + return createdKeyPath; + } + + protected static String getKeyType(String cipherName) + { + return cipherName.replaceAll("/.*", ""); + } +} diff --git a/src/java/org/apache/cassandra/crypto/TDEConfiguration.java b/src/java/org/apache/cassandra/crypto/TDEConfiguration.java new file mode 100644 index 000000000000..8c332e010a79 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/TDEConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +public class TDEConfiguration +{ + public final String systemKeyDirectory; + + public TDEConfiguration(String systemKeyDirectory) + { + this.systemKeyDirectory = systemKeyDirectory; + } +} diff --git a/src/java/org/apache/cassandra/crypto/TDEConfigurationProvider.java b/src/java/org/apache/cassandra/crypto/TDEConfigurationProvider.java new file mode 100644 index 000000000000..a62a21a97869 --- /dev/null +++ b/src/java/org/apache/cassandra/crypto/TDEConfigurationProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crypto; + +import com.google.common.annotations.VisibleForTesting; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY; + +public class TDEConfigurationProvider +{ + private static String systemKeyDirectoryProperty = SYSTEM_KEY_DIRECTORY.getString(); + + public static TDEConfiguration getConfiguration() + { + //TODO replace with reading the system key directory from config file + return new TDEConfiguration(systemKeyDirectoryProperty); + } + + @VisibleForTesting + public static void setSystemKeyDirectoryProperty(String value) + { + systemKeyDirectoryProperty = value; + } +} diff --git a/src/java/org/apache/cassandra/db/AbstractCompactionController.java b/src/java/org/apache/cassandra/db/AbstractCompactionController.java index db533ee870f3..25f773536d7e 100644 --- a/src/java/org/apache/cassandra/db/AbstractCompactionController.java +++ b/src/java/org/apache/cassandra/db/AbstractCompactionController.java @@ -20,6 +20,7 @@ import java.util.function.LongPredicate; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.schema.CompactionParams; @@ -28,30 +29,20 @@ */ public abstract class AbstractCompactionController implements AutoCloseable { - public final ColumnFamilyStore cfs; + public final CompactionRealm realm; public final long gcBefore; public final CompactionParams.TombstoneOption tombstoneOption; - public AbstractCompactionController(final ColumnFamilyStore cfs, final long gcBefore, CompactionParams.TombstoneOption tombstoneOption) + protected AbstractCompactionController(final CompactionRealm realm, final long gcBefore, CompactionParams.TombstoneOption tombstoneOption) { - assert cfs != null; - this.cfs = cfs; + assert realm != null; + this.realm = realm; this.gcBefore = gcBefore; this.tombstoneOption = tombstoneOption; } public abstract boolean compactingRepaired(); - public String getKeyspace() - { - return cfs.getKeyspaceName(); - } - - public String getColumnFamily() - { - return cfs.name; - } - public Iterable shadowSources(DecoratedKey key, boolean tombstoneOnly) { return null; diff --git a/src/java/org/apache/cassandra/db/AbstractReadQuery.java b/src/java/org/apache/cassandra/db/AbstractReadQuery.java index 448069cfca10..64b667c6dfa9 100644 --- a/src/java/org/apache/cassandra/db/AbstractReadQuery.java +++ b/src/java/org/apache/cassandra/db/AbstractReadQuery.java @@ -17,13 +17,20 @@ */ package org.apache.cassandra.db; +import java.util.Set; + import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.cql3.statements.SelectOptions; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.monitoring.MonitorableImpl; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.TableMetadata; /** @@ -54,9 +61,10 @@ public TableMetadata metadata() } // Monitorable interface + @Override public String name() { - return toCQLString(); + return toCQLString(Redaction.REDACT); } @Override @@ -90,32 +98,66 @@ public ColumnFilter columnFilter() } /** - * Recreate the CQL string corresponding to this query. + * Recreates the CQL string corresponding to this query. + *

+ * If the {@code redact} parameter is set to {@code true}, the query string will be redacted, replacing any specific + * column values with '?'. If set to {@code false}, the query string will not be redacted, and it might expose the + * queried column values which might contain sensitive data. The latter will be problematic if the query string ends + * up in logs or any other unprotected place. Therefore, non-redaction should only be used for debugging purposes or + * to present the query string to the same end user that created the query. *

* Note that in general the returned string will not be exactly the original user string, first - * because there isn't always a single syntax for a given query, but also because we don't have + * because there isn't always a single syntax for a given query, but also because we don't have * all the information needed (we know the non-PK columns queried but not the PK ones as internally - * we query them all). So this shouldn't be relied too strongly, but this should be good enough for - * debugging purpose which is what this is for. + * we query them all). So this shouldn't be relied upon too strongly, but this should be good enough for + * debugging purposes which is what this is for. + * + * @param redaction whether to redact the queried column values. */ - public String toCQLString() + + public String toUnredactedCQLString() { - StringBuilder sb = new StringBuilder().append("SELECT ") - .append(columnFilter().toCQLString()) - .append(" FROM ") - .append(ColumnIdentifier.maybeQuote(metadata().keyspace)) - .append('.') - .append(ColumnIdentifier.maybeQuote(metadata().name)); - appendCQLWhereClause(sb); + return toCQLString(Redaction.NONE); + } + + public String toRedactedCQLString() + { + return toCQLString(Redaction.REDACT); + } + + public String toCQLString(Redaction redaction) + { + CqlBuilder builder = new CqlBuilder(); + builder.append("SELECT ").append(columnFilter().toCQLString(redaction)); + builder.append(" FROM ").append(ColumnIdentifier.maybeQuote(metadata().keyspace)) + .append('.') + .append(ColumnIdentifier.maybeQuote(metadata().name)); + + appendCQLWhereClause(builder, redaction); if (limits() != DataLimits.NONE) - sb.append(' ').append(limits()); + builder.append(' ').append(limits().toCQLString()); // ALLOW FILTERING might not be strictly necessary - sb.append(" ALLOW FILTERING"); - - return sb.toString(); + builder.append(" ALLOW FILTERING"); + + builder.appendOptions(b -> { + IndexHints indexHints = rowFilter().indexHints; + Set included = IndexMetadata.toNames(indexHints.included); + Set excluded = IndexMetadata.toNames(indexHints.excluded); + b.append(SelectOptions.INCLUDED_INDEXES, included) + .append(SelectOptions.EXCLUDED_INDEXES, excluded) + .append(SelectOptions.ANN_OPTIONS, rowFilter().annOptions().toCQLString()); + }); + + // The limits used by the subsequent queries of paging don't have a clear direct translation into CQL. + // However, they change the meaning of the query, and it can be useful to identify them in logs. + // That's why here we append a fragment of invalid CQL syntanx, at the end of the query. + if (limits.isPagingContinuation()) + builder.append(" [paging continuation]"); + + return builder.toString(); } - protected abstract void appendCQLWhereClause(StringBuilder sb); -} \ No newline at end of file + protected abstract void appendCQLWhereClause(CqlBuilder builder, Redaction redaction); +} diff --git a/src/java/org/apache/cassandra/db/ArrayClustering.java b/src/java/org/apache/cassandra/db/ArrayClustering.java index b04910c434cb..9b98b7563fe3 100644 --- a/src/java/org/apache/cassandra/db/ArrayClustering.java +++ b/src/java/org/apache/cassandra/db/ArrayClustering.java @@ -32,7 +32,7 @@ public ArrayClustering(byte[]... values) public long unsharedHeapSize() { - if (this == ByteArrayAccessor.factory.clustering() || this == ByteArrayAccessor.factory.staticClustering()) + if (this == ByteArrayAccessor.factory.clustering()) return 0; long arrayRefSize = ObjectSizes.sizeOfArray(values); long elementsSize = 0; @@ -43,7 +43,7 @@ public long unsharedHeapSize() public long unsharedHeapSizeExcludingData() { - if (this == ByteArrayAccessor.factory.clustering() || this == ByteArrayAccessor.factory.staticClustering()) + if (this == ByteArrayAccessor.factory.clustering()) return 0; return EMPTY_SIZE + ObjectSizes.sizeOfArray(values); } diff --git a/src/java/org/apache/cassandra/db/CBuilder.java b/src/java/org/apache/cassandra/db/CBuilder.java deleted file mode 100644 index 7b28684344b1..000000000000 --- a/src/java/org/apache/cassandra/db/CBuilder.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.db; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.List; - -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.ByteBufferAccessor; -import org.apache.cassandra.db.marshal.ValueAccessor; - -/** - * Allows to build ClusteringPrefixes, either Clustering or ClusteringBound. - */ -public abstract class CBuilder -{ - public static CBuilder STATIC_BUILDER = new CBuilder() - { - public int count() - { - return 0; - } - - public int remainingCount() - { - return 0; - } - - public ClusteringComparator comparator() - { - throw new UnsupportedOperationException(); - } - - public CBuilder add(T value, ValueAccessor accessor) - { - throw new UnsupportedOperationException(); - } - - public CBuilder add(Object value) - { - throw new UnsupportedOperationException(); - } - - public Clustering build() - { - return Clustering.STATIC_CLUSTERING; - } - - public ClusteringBound buildBound(boolean isStart, boolean isInclusive) - { - throw new UnsupportedOperationException(); - } - - public Clustering buildWith(List newValues) - { - throw new UnsupportedOperationException(); - } - - public ClusteringBound buildBoundWith(List newValues, boolean isStart, boolean isInclusive) - { - throw new UnsupportedOperationException(); - } - }; - - public static CBuilder create(ClusteringComparator comparator) - { - return new ArrayBackedBuilder(comparator); - } - - public abstract int count(); - public abstract int remainingCount(); - public abstract ClusteringComparator comparator(); - public final CBuilder add(ByteBuffer value) - { - return add(value, ByteBufferAccessor.instance); - } - public final CBuilder add(ClusteringPrefix prefix, int i) - { - return add(prefix.get(i), prefix.accessor()); - } - public abstract CBuilder add(V value, ValueAccessor accessor); - public abstract CBuilder add(Object value); - public abstract Clustering build(); - public abstract ClusteringBound buildBound(boolean isStart, boolean isInclusive); - public abstract Clustering buildWith(List newValues); - public abstract ClusteringBound buildBoundWith(List newValues, boolean isStart, boolean isInclusive); - - private static class ArrayBackedBuilder extends CBuilder - { - private final ClusteringComparator type; - private final ByteBuffer[] values; - private int size; - private boolean built; - - public ArrayBackedBuilder(ClusteringComparator type) - { - this.type = type; - this.values = new ByteBuffer[type.size()]; - } - - public int count() - { - return size; - } - - public int remainingCount() - { - return values.length - size; - } - - public ClusteringComparator comparator() - { - return type; - } - - public CBuilder add(V value, ValueAccessor accessor) - { - if (isDone()) - throw new IllegalStateException(); - values[size++] = accessor.toBuffer(value); - return this; - } - - public CBuilder add(Object value) - { - return add(((AbstractType)type.subtype(size)).decompose(value)); - } - - private boolean isDone() - { - return remainingCount() == 0 || built; - } - - public Clustering build() - { - // We don't allow to add more element to a builder that has been built so - // that we don't have to copy values. - built = true; - - // Currently, only dense table can leave some clustering column out (see #7990) - return size == 0 ? Clustering.EMPTY : Clustering.make(values); - } - - public ClusteringBound buildBound(boolean isStart, boolean isInclusive) - { - // We don't allow to add more element to a builder that has been built so - // that we don't have to copy values (even though we have to do it in most cases). - built = true; - - if (size == 0) - return isStart ? BufferClusteringBound.BOTTOM : BufferClusteringBound.TOP; - - return BufferClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), - size == values.length ? values : Arrays.copyOfRange(values, 0, size)); - } - - public Clustering buildWith(List newValues) - { - assert size + newValues.size() <= type.size(); - ByteBuffer[] buffers = Arrays.copyOf(values, type.size()); - int newSize = size; - for (ByteBuffer value : newValues) - buffers[newSize++] = value; - - return Clustering.make(buffers); - } - - public ClusteringBound buildBoundWith(List newValues, boolean isStart, boolean isInclusive) - { - ByteBuffer[] buffers = Arrays.copyOf(values, size + newValues.size()); - int newSize = size; - for (ByteBuffer value : newValues) - buffers[newSize++] = value; - - return BufferClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), buffers); - } - } -} diff --git a/src/java/org/apache/cassandra/db/CassandraKeyspaceWriteHandler.java b/src/java/org/apache/cassandra/db/CassandraKeyspaceWriteHandler.java index ef9d0d137778..b1bb09048a3c 100644 --- a/src/java/org/apache/cassandra/db/CassandraKeyspaceWriteHandler.java +++ b/src/java/org/apache/cassandra/db/CassandraKeyspaceWriteHandler.java @@ -39,7 +39,7 @@ public CassandraKeyspaceWriteHandler(Keyspace keyspace) } @Override - public WriteContext beginWrite(Mutation mutation, boolean makeDurable) throws RequestExecutionException + public WriteContext beginWrite(Mutation mutation, WriteOptions writeOptions) throws RequestExecutionException { OpOrder.Group group = null; try @@ -48,7 +48,7 @@ public WriteContext beginWrite(Mutation mutation, boolean makeDurable) throws Re // write the mutation to the commitlog and memtables CommitLogPosition position = null; - if (makeDurable) + if (writeOptions.shouldWriteCommitLog(mutation.getKeyspaceName())) { position = addToCommitLog(mutation); } diff --git a/src/java/org/apache/cassandra/db/Clusterable.java b/src/java/org/apache/cassandra/db/Clusterable.java index 118b2724f503..8963f32f619d 100644 --- a/src/java/org/apache/cassandra/db/Clusterable.java +++ b/src/java/org/apache/cassandra/db/Clusterable.java @@ -20,8 +20,11 @@ /** * Common class for objects that are identified by a clustering prefix, and can be thus sorted by a * {@link ClusteringComparator}. + * + * Note that clusterings can have mixed accessors (necessary because the static clustering is always of ByteBuffer + * accessor) and thus the accessor type cannot be set here. */ -public interface Clusterable +public interface Clusterable { - public ClusteringPrefix clustering(); + public ClusteringPrefix clustering(); } diff --git a/src/java/org/apache/cassandra/db/Clustering.java b/src/java/org/apache/cassandra/db/Clustering.java index 426d3279f97d..04b1aaeeeabc 100644 --- a/src/java/org/apache/cassandra/db/Clustering.java +++ b/src/java/org/apache/cassandra/db/Clustering.java @@ -24,6 +24,7 @@ import org.apache.cassandra.cache.IMeasurableMemory; import org.apache.cassandra.db.marshal.ByteArrayAccessor; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.marshal.AbstractType; @@ -50,7 +51,7 @@ public default Clustering clone(ByteBufferCloner cloner) ByteBuffer[] newValues = new ByteBuffer[size()]; for (int i = 0; i < size(); i++) { - ByteBuffer val = accessor().toBuffer(get(i)); + ByteBuffer val = bufferAt(i); newValues[i] = val == null ? null : cloner.clone(val); } return new BufferClustering(newValues); @@ -79,13 +80,14 @@ public default String toString(TableMetadata metadata) return sb.toString(); } - public default String toCQLString(TableMetadata metadata) + default String toCQLString(TableMetadata metadata, Redaction redaction) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < size(); i++) { ColumnMetadata c = metadata.clusteringColumns().get(i); - sb.append(i == 0 ? "" : ", ").append(c.type.toCQLString(bufferAt(i))); + ByteBuffer value = bufferAt(i); + sb.append(i == 0 ? "" : ", ").append(c.type.toCQLString(value, redaction)); } return sb.toString(); } diff --git a/src/java/org/apache/cassandra/db/ClusteringBound.java b/src/java/org/apache/cassandra/db/ClusteringBound.java index 4afdfe628504..2214b424b2ab 100644 --- a/src/java/org/apache/cassandra/db/ClusteringBound.java +++ b/src/java/org/apache/cassandra/db/ClusteringBound.java @@ -111,7 +111,7 @@ static ClusteringBound exclusiveEndOf(ClusteringPrefix from) static ClusteringBound create(ClusteringComparator comparator, boolean isStart, boolean isInclusive, Object... values) { - CBuilder builder = CBuilder.create(comparator); + ClusteringBuilder builder = ClusteringBuilder.create(comparator); for (Object val : values) { if (val instanceof ByteBuffer) @@ -135,4 +135,4 @@ default ClusteringBound asEndBound() assert isEnd(); return this; } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/ClusteringBoundary.java b/src/java/org/apache/cassandra/db/ClusteringBoundary.java index 9e0a87c2efab..df35a5d1bb14 100644 --- a/src/java/org/apache/cassandra/db/ClusteringBoundary.java +++ b/src/java/org/apache/cassandra/db/ClusteringBoundary.java @@ -49,4 +49,4 @@ default ClusteringBound asEndBound() { return closeBound(false); } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/ClusteringBuilder.java b/src/java/org/apache/cassandra/db/ClusteringBuilder.java new file mode 100644 index 000000000000..6c448dd9d25b --- /dev/null +++ b/src/java/org/apache/cassandra/db/ClusteringBuilder.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.db.marshal.ValueAccessor; + +/** + * Allows to build ClusteringPrefixes, either Clustering or ClusteringBound. + */ +public abstract class ClusteringBuilder +{ + public static ClusteringBuilder STATIC_BUILDER = new ClusteringBuilder() + { + public int count() + { + return 0; + } + + public int remainingCount() + { + return 0; + } + + public ClusteringComparator comparator() + { + throw new UnsupportedOperationException(); + } + + public ClusteringBuilder add(T value, ValueAccessor accessor) + { + throw new UnsupportedOperationException(); + } + + public ClusteringBuilder add(Object value) + { + throw new UnsupportedOperationException(); + } + + public Clustering build() + { + return Clustering.STATIC_CLUSTERING; + } + + public ClusteringBound buildBound(boolean isStart, boolean isInclusive) + { + throw new UnsupportedOperationException(); + } + + public Clustering buildWith(List newValues) + { + throw new UnsupportedOperationException(); + } + + public ClusteringBound buildBoundWith(List newValues, boolean isStart, boolean isInclusive) + { + throw new UnsupportedOperationException(); + } + }; + + public static ClusteringBuilder create(ClusteringComparator comparator) + { + return new ArrayBackedBuilder(comparator); + } + + public abstract int count(); + public abstract int remainingCount(); + public abstract ClusteringComparator comparator(); + public final ClusteringBuilder add(ByteBuffer value) + { + return add(value, ByteBufferAccessor.instance); + } + public final ClusteringBuilder add(ClusteringPrefix prefix, int i) + { + return add(prefix.get(i), prefix.accessor()); + } + public abstract ClusteringBuilder add(V value, ValueAccessor accessor); + public abstract ClusteringBuilder add(Object value); + public abstract Clustering build(); + public abstract ClusteringBound buildBound(boolean isStart, boolean isInclusive); + public abstract Clustering buildWith(List newValues); + public abstract ClusteringBound buildBoundWith(List newValues, boolean isStart, boolean isInclusive); + + private static class ArrayBackedBuilder extends ClusteringBuilder + { + private final ClusteringComparator type; + private final ByteBuffer[] values; + private int size; + private boolean built; + + public ArrayBackedBuilder(ClusteringComparator type) + { + this.type = type; + this.values = new ByteBuffer[type.size()]; + } + + public int count() + { + return size; + } + + public int remainingCount() + { + return values.length - size; + } + + public ClusteringComparator comparator() + { + return type; + } + + public ClusteringBuilder add(V value, ValueAccessor accessor) + { + if (isDone()) + throw new IllegalStateException(); + values[size++] = accessor.toBuffer(value); + return this; + } + + public ClusteringBuilder add(Object value) + { + return add(((AbstractType)type.subtype(size)).decompose(value)); + } + + private boolean isDone() + { + return remainingCount() == 0 || built; + } + + public Clustering build() + { + // We don't allow to add more element to a builder that has been built so + // that we don't have to copy values. + built = true; + + // Currently, only dense table can leave some clustering column out (see #7990) + return size == 0 ? Clustering.EMPTY : Clustering.make(values); + } + + public ClusteringBound buildBound(boolean isStart, boolean isInclusive) + { + // We don't allow to add more element to a builder that has been built so + // that we don't have to copy values (even though we have to do it in most cases). + built = true; + + if (size == 0) + return isStart ? BufferClusteringBound.BOTTOM : BufferClusteringBound.TOP; + + return BufferClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), + size == values.length ? values : Arrays.copyOfRange(values, 0, size)); + } + + public Clustering buildWith(List newValues) + { + assert size + newValues.size() <= type.size(); + ByteBuffer[] buffers = Arrays.copyOf(values, type.size()); + int newSize = size; + for (ByteBuffer value : newValues) + buffers[newSize++] = value; + + return Clustering.make(buffers); + } + + public ClusteringBound buildBoundWith(List newValues, boolean isStart, boolean isInclusive) + { + ByteBuffer[] buffers = Arrays.copyOf(values, size + newValues.size()); + int newSize = size; + for (ByteBuffer value : newValues) + buffers[newSize++] = value; + + return BufferClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), buffers); + } + } +} diff --git a/src/java/org/apache/cassandra/db/ClusteringComparator.java b/src/java/org/apache/cassandra/db/ClusteringComparator.java index 2949130707fe..00f48b1506c2 100644 --- a/src/java/org/apache/cassandra/db/ClusteringComparator.java +++ b/src/java/org/apache/cassandra/db/ClusteringComparator.java @@ -26,20 +26,19 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ValueAccessor; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.sstable.format.big.IndexInfo; import org.apache.cassandra.serializers.MarshalException; - -import org.apache.cassandra.io.sstable.IndexInfo; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; import static org.apache.cassandra.utils.bytecomparable.ByteSource.EXCLUDED; import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT; -import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT_EMPTY; -import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT_EMPTY_REVERSED; import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT_NULL; +import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT_NULL_REVERSED; +import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_CLUSTERING_NULL; import static org.apache.cassandra.utils.bytecomparable.ByteSource.TERMINATOR; /** @@ -57,8 +56,15 @@ public class ClusteringComparator implements Comparator private final Comparator indexReverseComparator; private final Comparator reverseComparator; - private final Comparator rowComparator = (r1, r2) -> compare((ClusteringPrefix) r1.clustering(), - (ClusteringPrefix) r2.clustering()); + private final Comparator rowComparator = new Comparator<>() + { + @Override + public int compare(Row r1, Row r2) + { + return ClusteringComparator.this.compare((ClusteringPrefix) r1.clustering(), + (ClusteringPrefix) r2.clustering()); + } + }; public ClusteringComparator(AbstractType... clusteringTypes) { @@ -70,11 +76,34 @@ public ClusteringComparator(Iterable> clusteringTypes) // copy the list to ensure despatch is monomorphic this.clusteringTypes = ImmutableList.copyOf(clusteringTypes); - this.indexComparator = (o1, o2) -> ClusteringComparator.this.compare((ClusteringPrefix) o1.lastName, - (ClusteringPrefix) o2.lastName); - this.indexReverseComparator = (o1, o2) -> ClusteringComparator.this.compare((ClusteringPrefix) o1.firstName, - (ClusteringPrefix) o2.firstName); - this.reverseComparator = (c1, c2) -> ClusteringComparator.this.compare(c2, c1); + this.indexComparator = new Comparator<>() + { + @Override + public int compare(IndexInfo o1, IndexInfo o2) + { + return ClusteringComparator.this.compare((ClusteringPrefix) o1.lastName, + (ClusteringPrefix) o2.lastName); + } + }; + + this.indexReverseComparator = new Comparator<>() + { + @Override + public int compare(IndexInfo o1, IndexInfo o2) + { + return ClusteringComparator.this.compare((ClusteringPrefix) o1.firstName, + (ClusteringPrefix) o2.firstName); + } + }; + + this.reverseComparator = new Comparator<>() + { + @Override + public int compare(Clusterable o1, Clusterable o2) + { + return ClusteringComparator.this.compare(o2, o1); + } + }; for (AbstractType type : clusteringTypes) type.checkComparable(); // this should already be enforced by TableMetadata.Builder.addColumn, but we check again for other constructors } @@ -121,7 +150,7 @@ public Clustering make(Object... values) if (values.length != size()) throw new IllegalArgumentException(String.format("Invalid number of components, expecting %d but got %d", size(), values.length)); - CBuilder builder = CBuilder.create(this); + ClusteringBuilder builder = ClusteringBuilder.create(this); for (Object val : values) { if (val instanceof ByteBuffer) @@ -196,34 +225,6 @@ public int compareComponent(int i, ClusteringPrefix v1, ClusteringP return compareComponent(i, v1.get(i), v1.accessor(), v2.get(i), v2.accessor()); } - /** - * Returns whether this clustering comparator is compatible with the provided one, - * that is if the provided one can be safely replaced by this new one. - * - * @param previous the previous comparator that we want to replace and test - * compatibility with. - * - * @return whether {@code previous} can be safely replaced by this comparator. - */ - public boolean isCompatibleWith(ClusteringComparator previous) - { - if (this == previous) - return true; - - // Extending with new components is fine, shrinking is not - if (size() < previous.size()) - return false; - - for (int i = 0; i < previous.size(); i++) - { - AbstractType tprev = previous.subtype(i); - AbstractType tnew = subtype(i); - if (!tnew.isCompatibleWith(tprev)) - return false; - } - return true; - } - /** * Validates the provided prefix for corrupted data. * @@ -296,6 +297,7 @@ public int next() { if (current != null) { + // Process bytes of the current component. int b = current.next(); if (b > END_OF_STREAM) return b; @@ -303,24 +305,26 @@ public int next() } int sz = src.size(); - if (srcnum == sz) + if (srcnum == sz) // already produced the Kind byte, we are done return END_OF_STREAM; ++srcnum; if (srcnum == sz) return src.kind().asByteComparableValue(version); + else + return advanceToComponent(src.get(srcnum)); + } - final V nextComponent = src.get(srcnum); - // We can have a null as the clustering component (this is a relic of COMPACT STORAGE, but also - // can appear in indexed partitions with no rows but static content), + private int advanceToComponent(V nextComponent) + { if (nextComponent == null) { - if (version != Version.LEGACY) - return NEXT_COMPONENT_NULL; // always sorts before non-nulls, including for reversed types + if (version == Version.OSS50) + return NEXT_CLUSTERING_NULL; // always sorts before non-nulls, including for reversed types else { // legacy version did not permit nulls in clustering keys and treated these as null values - return subtype(srcnum).isReversed() ? NEXT_COMPONENT_EMPTY_REVERSED : NEXT_COMPONENT_EMPTY; + return nextComponentNull(subtype(srcnum).isReversed()); } } @@ -328,13 +332,18 @@ public int next() // and also null values for some types (e.g. int, varint but not text) that are encoded as empty // buffers. if (current == null) - return subtype(srcnum).isReversed() ? NEXT_COMPONENT_EMPTY_REVERSED : NEXT_COMPONENT_EMPTY; + return nextComponentNull(subtype(srcnum).isReversed()); return NEXT_COMPONENT; } }; } + private int nextComponentNull(boolean isReversed) + { + return isReversed ? NEXT_COMPONENT_NULL_REVERSED : NEXT_COMPONENT_NULL; + } + public String toString() { return src.clusteringString(subtypes()); @@ -344,14 +353,31 @@ public String toString() /** * Produces a clustering from the given byte-comparable value. The method will throw an exception if the value * does not correctly encode a clustering of this type, including if it encodes a position before or after a - * clustering (i.e. a bound/boundary). + * clustering (i.e. a bound/boundary). Uses the OSS50 version of the byte-comparable encoding. * * @param accessor Accessor to use to construct components. * @param comparable The clustering encoded as a byte-comparable sequence. */ - public Clustering clusteringFromByteComparable(ValueAccessor accessor, ByteComparable comparable) + public Clustering clusteringFromByteComparable(ValueAccessor accessor, ByteComparable comparable) + { + return clusteringFromByteComparable(accessor, comparable, ByteComparable.Version.OSS50); + } + + /** + * Produces a clustering from the given byte-comparable value. The method will throw an exception if the value + * does not correctly encode a clustering of this type, including if it encodes a position before or after a + * clustering (i.e. a bound/boundary). Uses the OSS50 version of the byte-comparable encoding. + * + * @param accessor Accessor to use to construct components. Because this will be used to construct individual + * arrays/buffers for each component, it may be sensible to use an accessor that allocates larger + * buffers in advance. + * @param comparable The clustering encoded as a byte-comparable sequence. + * @param version The version of the byte-comparable encoding. + */ + public Clustering clusteringFromByteComparable(ValueAccessor accessor, + ByteComparable comparable, + ByteComparable.Version version) { - ByteComparable.Version version = ByteComparable.Version.OSS50; ByteSource.Peekable orderedBytes = ByteSource.peekable(comparable.asComparableBytes(version)); if (orderedBytes == null) return null; @@ -364,7 +390,7 @@ public Clustering clusteringFromByteComparable(ValueAccessor accessor, assert size() == 0 : "Terminator should be after " + size() + " components, got 0"; return accessor.factory().clustering(); case EXCLUDED: - return accessor.factory().staticClustering(); + return Clustering.STATIC_CLUSTERING; default: // continue with processing } @@ -376,11 +402,11 @@ public Clustering clusteringFromByteComparable(ValueAccessor accessor, { switch (sep) { - case NEXT_COMPONENT_NULL: + case NEXT_CLUSTERING_NULL: components[cc] = null; break; - case NEXT_COMPONENT_EMPTY: - case NEXT_COMPONENT_EMPTY_REVERSED: + case NEXT_COMPONENT_NULL: + case NEXT_COMPONENT_NULL_REVERSED: components[cc] = subtype(cc).fromComparableBytes(accessor, null, version); break; case NEXT_COMPONENT: @@ -428,11 +454,11 @@ public ClusteringBound boundFromByteComparable(ValueAccessor accessor, { switch (sep) { - case NEXT_COMPONENT_NULL: + case NEXT_CLUSTERING_NULL: components[cc] = null; break; - case NEXT_COMPONENT_EMPTY: - case NEXT_COMPONENT_EMPTY_REVERSED: + case NEXT_COMPONENT_NULL: + case NEXT_COMPONENT_NULL_REVERSED: components[cc] = subtype(cc).fromComparableBytes(accessor, null, version); break; case NEXT_COMPONENT: @@ -485,11 +511,11 @@ public ClusteringBoundary boundaryFromByteComparable(ValueAccessor acc { switch (sep) { - case NEXT_COMPONENT_NULL: + case NEXT_CLUSTERING_NULL: components[cc] = null; break; - case NEXT_COMPONENT_EMPTY: - case NEXT_COMPONENT_EMPTY_REVERSED: + case NEXT_COMPONENT_NULL: + case NEXT_COMPONENT_NULL_REVERSED: components[cc] = subtype(cc).fromComparableBytes(accessor, null, version); break; case NEXT_COMPONENT: diff --git a/src/java/org/apache/cassandra/db/ClusteringPrefix.java b/src/java/org/apache/cassandra/db/ClusteringPrefix.java index 02f9330b430b..121d0563db65 100644 --- a/src/java/org/apache/cassandra/db/ClusteringPrefix.java +++ b/src/java/org/apache/cassandra/db/ClusteringPrefix.java @@ -19,17 +19,19 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.util.*; +import java.util.List; +import java.util.Objects; import java.util.function.ToIntFunction; import org.apache.cassandra.cache.IMeasurableMemory; -import org.apache.cassandra.config.*; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ByteArrayAccessor; import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.db.marshal.CompositeType; import org.apache.cassandra.db.marshal.ValueAccessor; -import org.apache.cassandra.db.rows.*; -import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredSerializer; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; @@ -52,7 +54,7 @@ * 3) {@code ClusteringBoundary} represents the threshold between two adjacent range tombstones. * See those classes for more details. */ -public interface ClusteringPrefix extends IMeasurableMemory, Clusterable +public interface ClusteringPrefix extends IMeasurableMemory, Clusterable { public static final Serializer serializer = new Serializer(); @@ -358,11 +360,11 @@ default int dataSize() default ByteBuffer serializeAsPartitionKey() { if (size() == 1) - return accessor().toBuffer(get(0)); + return bufferAt(0); ByteBuffer[] values = new ByteBuffer[size()]; for (int i = 0; i < size(); i++) - values[i] = accessor().toBuffer(get(i)); + values[i] = bufferAt(i); return CompositeType.build(ByteBufferAccessor.instance, values); } diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java index 99ee013024d6..db5af1d31ba1 100644 --- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java +++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java @@ -19,23 +19,22 @@ import java.io.IOException; import java.io.PrintStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -44,10 +43,14 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.openmbean.CompositeData; @@ -68,6 +71,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.common.primitives.Longs; import com.google.common.util.concurrent.RateLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,20 +82,31 @@ import org.apache.cassandra.cache.RowCacheSentinel; import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.concurrent.FutureTask; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DurationSpec; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; -import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.AbstractCompactionTask; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.db.compaction.CompactionStrategy; +import org.apache.cassandra.db.compaction.CompactionStrategyContainer; +import org.apache.cassandra.db.compaction.CompactionStrategyFactory; import org.apache.cassandra.db.compaction.CompactionStrategyManager; +import org.apache.cassandra.db.compaction.CompactionStrategyOptions; import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.TableOperation; +import org.apache.cassandra.db.compaction.unified.Environment; +import org.apache.cassandra.db.compaction.unified.RealEnvironment; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.SSTableIntervalTree; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.db.lifecycle.View; @@ -101,18 +116,17 @@ import org.apache.cassandra.db.partitions.CachedPartition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.repair.CassandraTableRepairManager; -import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.db.streaming.CassandraStreamManager; import org.apache.cassandra.db.view.TableViews; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Bounds; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.StartupException; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.index.internal.CassandraIndex; import org.apache.cassandra.index.transactions.UpdateTransaction; @@ -126,21 +140,23 @@ import org.apache.cassandra.io.sstable.SSTableId; import org.apache.cassandra.io.sstable.SSTableIdFactory; import org.apache.cassandra.io.sstable.SSTableMultiWriter; +import org.apache.cassandra.io.sstable.StorageHandler; +import org.apache.cassandra.io.sstable.filter.BloomFilterTracker; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.metrics.KeyspaceMetrics; import org.apache.cassandra.metrics.Sampler; import org.apache.cassandra.metrics.Sampler.Sample; import org.apache.cassandra.metrics.Sampler.SamplerType; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.TopPartitionTracker; import org.apache.cassandra.repair.TableRepairManager; -import org.apache.cassandra.repair.consistent.admin.CleanupSummary; import org.apache.cassandra.repair.consistent.admin.PendingStat; -import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.CompactionParams.TombstoneOption; import org.apache.cassandra.schema.CompressionParams; @@ -151,6 +167,10 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.StorageService; @@ -162,6 +182,7 @@ import org.apache.cassandra.service.snapshot.TableSnapshot; import org.apache.cassandra.streaming.TableStreamManager; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.DefaultValue; import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.FBUtilities; @@ -169,29 +190,35 @@ import org.apache.cassandra.utils.JsonUtils; import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.OverlapIterator; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.WrappedRunnable; import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.concurrent.Refs; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import static java.lang.String.format; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLED_AUTO_COMPACTION_PROPERTY; +import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYSTEM; import static org.apache.cassandra.config.DatabaseDescriptor.getFlushWriters; import static org.apache.cassandra.db.commitlog.CommitLogPosition.NONE; +import static org.apache.cassandra.schema.SchemaConstants.FILENAME_LENGTH; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.FBUtilities.now; import static org.apache.cassandra.utils.Throwables.maybeFail; import static org.apache.cassandra.utils.Throwables.merge; +import static org.apache.cassandra.utils.Throwables.perform; import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch; -public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner, SSTable.Owner +public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner, SSTable.Owner, CompactionRealm { private static final Logger logger = LoggerFactory.getLogger(ColumnFamilyStore.class); - /* We keep a pool of threads for each data directory, size of each pool is memtable_flush_writers. When flushing we start a Flush runnable in the flushExecutor. Flush calculates how to split the @@ -200,20 +227,20 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner are finished. By having flushExecutor size the same size as each of the perDiskflushExecutors we make sure we can have that many flushes going at the same time. */ - private static final ExecutorPlus flushExecutor = DatabaseDescriptor.isDaemonInitialized() + private static final ExecutorPlus flushExecutor = DatabaseDescriptor.enableMemtableAndCommitLog() ? executorFactory().withJmxInternal().pooled("MemtableFlushWriter", getFlushWriters()) : null; // post-flush executor is single threaded to provide guarantee that any flush Future on a CF will never return until prior flushes have completed - private static final ExecutorPlus postFlushExecutor = DatabaseDescriptor.isDaemonInitialized() + private static final ExecutorPlus postFlushExecutor = DatabaseDescriptor.enableMemtableAndCommitLog() ? executorFactory().withJmxInternal().sequential("MemtablePostFlush") : null; - private static final ExecutorPlus reclaimExecutor = DatabaseDescriptor.isDaemonInitialized() + private static final ExecutorPlus reclaimExecutor = DatabaseDescriptor.enableMemtableAndCommitLog() ? executorFactory().withJmxInternal().sequential("MemtableReclaimMemory") : null; - private static final PerDiskFlushExecutors perDiskflushExecutors = DatabaseDescriptor.isDaemonInitialized() + private static final PerDiskFlushExecutors perDiskflushExecutors = DatabaseDescriptor.enableMemtableAndCommitLog() ? new PerDiskFlushExecutors(DatabaseDescriptor.getFlushWriters(), DatabaseDescriptor.getNonLocalSystemKeyspacesDataFileLocations(), DatabaseDescriptor.useSpecificLocationForLocalSystemData()) @@ -245,7 +272,13 @@ public enum FlushReason ANTICOMPACTION, SCHEMA_CHANGE, OWNED_RANGES_CHANGE, - UNIT_TESTS // explicitly requested flush needed for a test + UNIT_TESTS, // explicitly requested flush needed for a test + /** Flush performed to a remote storage. Used by remote commit log replay */ + REMOTE_REPLAY, + BATCHLOG_REPLAY, + TRIE_LIMIT, + INDEX_MEMTABLE_LIMIT, + INDEX_MEMTABLE_PERIOD_EXPIRED, } private static final String[] COUNTER_NAMES = new String[]{"table", "count", "error", "value"}; @@ -284,7 +317,39 @@ public enum FlushReason /** @deprecated See CASSANDRA-9448 */ @Deprecated(since = "3.0") private final String oldMBeanName; - private volatile boolean valid = true; + + public enum STATUS + { + /** + * Initial status when CFS is created + */ + VALID, + /** + * When table is invalidated with unloading data + */ + INVALID_UNLOADED, + /** + * When table is invalidated with dropping data + */ + INVALID_DROPPED; + + /** + * @return true if CFS is not invalidated + */ + public boolean isValid() + { + return this == VALID; + } + + /** + * @return true if CFS is invalidated and sstables should be dropped locally and remotely + */ + public boolean isInvalidAndShouldDropData() + { + return this == INVALID_DROPPED; + } + } + private volatile STATUS status = STATUS.VALID; private volatile Memtable.Factory memtableFactory; @@ -293,7 +358,7 @@ public enum FlushReason * * We synchronize on the Tracker to ensure isolation when we want to make sure * that the memtable we're acting on doesn't change out from under us. I.e., flush - * syncronizes on it to make sure it can submit on both executors atomically, + * synchronizes on it to make sure it can submit on both executors atomically, * so anyone else who wants to make sure flush doesn't interfere should as well. */ private final Tracker data; @@ -304,6 +369,7 @@ public enum FlushReason /* This is used to generate the next index for a SSTable */ private final Supplier sstableIdGenerator; + private final StorageHandler storageHandler; public final SecondaryIndexManager indexManager; public final TableViews viewManager; @@ -312,11 +378,12 @@ public enum FlushReason private volatile DefaultValue maxCompactionThreshold; private volatile DefaultValue crcCheckChance; - private final CompactionStrategyManager compactionStrategyManager; + private final CompactionStrategyFactory strategyFactory; + private volatile CompactionStrategyContainer strategyContainer; private final Directories directories; - public final TableMetrics metric; + public volatile TableMetrics metric; public volatile long sampleReadLatencyMicros; public volatile long additionalWriteLatencyMicros; @@ -333,6 +400,12 @@ public enum FlushReason // Tombtone partitions that ignore the gc_grace_seconds during compaction private final Set partitionKeySetIgnoreGcGrace = ConcurrentHashMap.newKeySet(); + /** The local ranges are used by the {@link DiskBoundaryManager} to create the disk boundaries but can also be + * used independently. They are created lazily and invalidated whenever {@link this#invalidateLocalRangesAndDiskBoundaries()} + * is called. + */ + private volatile SortedLocalRanges localRanges; + @VisibleForTesting final DiskBoundaryManager diskBoundaryManager = new DiskBoundaryManager(); private volatile ShardBoundaries cachedShardBoundaries = null; @@ -362,6 +435,15 @@ TablePaxosRepairHistory get() private final PaxosRepairHistoryLoader paxosRepairHistory = new PaxosRepairHistoryLoader(); + // BloomFilterTracker is updated from corresponding {@link SSTableReader}s. Metrics are queried via CFS instance. + private final BloomFilterTracker bloomFilterTracker = BloomFilterTracker.createMeterTracker(); + + private final RequestTracker requestTracker = RequestTracker.instance; + + private final ReentrantLock longRunningSerializedOperationsLock = new ReentrantLock(); + + private final List> initialBuilds = new LinkedList<>(); + public static void shutdownPostFlushExecutor() throws InterruptedException { postFlushExecutor.shutdown(); @@ -376,6 +458,11 @@ public static void shutdownExecutorsAndWait(long timeout, TimeUnit unit) throws ExecutorUtils.shutdownAndWait(timeout, unit, executors); } + public boolean isReadyToServeData() + { + return storageHandler.isReady(); + } + public void reload() { // metadata object has been mutated directly. make all the members jibe with new settings. @@ -383,7 +470,7 @@ public void reload() // only update these runtime-modifiable settings if they have not been modified. if (!minCompactionThreshold.isModified()) for (ColumnFamilyStore cfs : concatWithIndexes()) - cfs.minCompactionThreshold = new DefaultValue(metadata().params.compaction.minCompactionThreshold()); + cfs.minCompactionThreshold = new DefaultValue<>(metadata().params.compaction.minCompactionThreshold()); if (!maxCompactionThreshold.isModified()) for (ColumnFamilyStore cfs : concatWithIndexes()) cfs.maxCompactionThreshold = new DefaultValue(metadata().params.compaction.maxCompactionThreshold()); @@ -391,42 +478,72 @@ public void reload() for (ColumnFamilyStore cfs : concatWithIndexes()) cfs.crcCheckChance = new DefaultValue(metadata().params.crcCheckChance); - compactionStrategyManager.maybeReloadParamsFromSchema(metadata().params.compaction); + reloadCompactionStrategy(metadata().params.compaction, CompactionStrategyContainer.ReloadReason.METADATA_CHANGE); indexManager.reload(); memtableFactory = metadata().params.memtable.factory(); - if (DatabaseDescriptor.isDaemonInitialized()) + if (DatabaseDescriptor.enableMemtableAndCommitLog()) switchMemtableOrNotify(FlushReason.SCHEMA_CHANGE, Memtable::metadataUpdated); + + if (metric.metricsAggregation != TableMetrics.MetricsAggregation.fromMetadata(metadata())) + { // Reload the metrics if histogram aggregation has changed + metric.release(); // release first because of those static tables containing metric names + metric = new TableMetrics(this, memtableFactory.createMemtableMetrics(metadata)); + } + } + + /** + * Reload the compaction strategy using the given compaction parameters and reason. + */ + private void reloadCompactionStrategy(CompactionParams compactionParams, CompactionStrategyContainer.ReloadReason reason) + { + CompactionStrategyContainer previous = strategyContainer; + strategyContainer = strategyFactory.reload(strategyContainer, compactionParams, reason, storageHandler.enableAutoCompaction()); + if (strategyContainer != previous) + { + getTracker().subscribe(strategyContainer); + if (previous != null) + getTracker().unsubscribe(previous); + } } public static Runnable getBackgroundCompactionTaskSubmitter() { - return () -> { - for (Keyspace keyspace : Keyspace.all()) - for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) - CompactionManager.instance.submitBackground(cfs); - }; + return () -> CompactionManager.instance.submitBackground(ImmutableSet.copyOf(all())); + } + + @VisibleForTesting + public CompactionStrategyFactory getCompactionFactory() + { + return strategyFactory; + } + + @Override + public CompactionParams getCompactionParams() + { + return strategyContainer.getCompactionParams(); } + @Override public Map getCompactionParameters() { - return compactionStrategyManager.getCompactionParams().asMap(); + return getCompactionParams().asMap(); } + @Override public String getCompactionParametersJson() { return JsonUtils.writeAsJsonString(getCompactionParameters()); } + @Override public void setCompactionParameters(Map options) { try { - CompactionParams compactionParams = CompactionParams.fromMap(options); - compactionParams.validate(); - compactionStrategyManager.overrideLocalParams(compactionParams); + reloadCompactionStrategy(CompactionParams.fromMap(options), CompactionStrategyContainer.ReloadReason.JMX_REQUEST); } catch (Throwable t) { @@ -436,21 +553,25 @@ public void setCompactionParameters(Map options) } } + @Override public void setCompactionParametersJson(String options) { setCompactionParameters(JsonUtils.fromJsonMap(options)); } + @Override public Map getCompressionParameters() { return metadata.getLocal().params.compression.asMap(); } + @Override public String getCompressionParametersJson() { return JsonUtils.writeAsJsonString(getCompressionParameters()); } + @Override public void setCompressionParameters(Map opts) { try @@ -465,6 +586,7 @@ public void setCompressionParameters(Map opts) } } + @Override public void setCompressionParametersJson(String options) { setCompressionParameters(JsonUtils.fromJsonMap(options)); @@ -485,7 +607,6 @@ public ColumnFamilyStore(Keyspace keyspace, this.keyspace = keyspace; this.metadata = metadata; - this.directories = directories; name = columnFamilyName; minCompactionThreshold = new DefaultValue<>(metadata.get().params.compaction.minCompactionThreshold()); maxCompactionThreshold = new DefaultValue<>(metadata.get().params.compaction.maxCompactionThreshold()); @@ -496,50 +617,69 @@ public ColumnFamilyStore(Keyspace keyspace, additionalWriteLatencyMicros = DatabaseDescriptor.getWriteRpcTimeout(TimeUnit.MICROSECONDS) / 2; memtableFactory = metadata.get().params.memtable.factory(); - logger.info("Initializing {}.{}", getKeyspaceName(), name); + logger.debug("Initializing {}.{}", getKeyspaceName(), name); // Create Memtable and its metrics object only on online Memtable initialMemtable = null; TableMetrics.ReleasableMetric memtableMetrics = null; - if (DatabaseDescriptor.isDaemonInitialized()) + if (DatabaseDescriptor.enableMemtableAndCommitLog()) { initialMemtable = createMemtable(new AtomicReference<>(CommitLog.instance.getCurrentPosition())); memtableMetrics = memtableFactory.createMemtableMetrics(metadata); + data = new Tracker(this, initialMemtable, loadSSTables); + } + else + { + data = new Tracker(this, null, false); } - data = new Tracker(this, initialMemtable, loadSSTables); // Note that this needs to happen before we load the first sstables, or the global sstable tracker will not // be notified on the initial loading. data.subscribe(StorageService.instance.sstablesTracker); + /** + * When creating a CFS offline we change the default logic needed by CASSANDRA-8671 + * and link the passed directories to be picked up by the compaction strategy + */ + if (offline) + this.directories = directories; + else + this.directories = new Directories(metadata.get()); + + storageHandler = StorageHandler.create(this, metadata, directories, data); + logger.debug("Initialized storage handler with {} for {}.{}", storageHandler.getClass().getSimpleName(), keyspace.getName(), name); + Collection sstables = null; // scan for sstables corresponding to this cf and load them - if (data.loadsstables) - { - Directories.SSTableLister sstableFiles = directories.sstableLister(Directories.OnTxnErr.IGNORE).skipTemporary(true); - sstables = SSTableReader.openAll(this, sstableFiles.list().entrySet(), metadata); - data.addInitialSSTablesWithoutUpdatingSize(sstables); - } + if (loadSSTables) + sstables = storageHandler.loadInitialSSTables(); // compaction strategy should be created after the CFS has been prepared - compactionStrategyManager = new CompactionStrategyManager(this); - - if (maxCompactionThreshold.value() <= 0 || minCompactionThreshold.value() <=0) + this.strategyFactory = new CompactionStrategyFactory(this); + this.strategyContainer = strategyFactory.reload(null, + metadata.get().params.compaction, + CompactionStrategyContainer.ReloadReason.FULL, + storageHandler.enableAutoCompaction()); + getTracker().subscribe(strategyContainer); + + if (!strategyContainer.isEnabled() || DISABLED_AUTO_COMPACTION_PROPERTY.getBoolean()) { - logger.warn("Disabling compaction strategy by setting compaction thresholds to 0 is deprecated, set the compaction option 'enabled' to 'false' instead."); - this.compactionStrategyManager.disable(); + logger.info("Strategy driven background compactions for {} are disabled: strategy container={}, {}={}", + metadata, strategyContainer.isEnabled(), DISABLED_AUTO_COMPACTION_PROPERTY.getKey(), + DISABLED_AUTO_COMPACTION_PROPERTY.getBoolean()); + this.strategyContainer.disable(); } // create the private ColumnFamilyStores for the secondary column indexes indexManager = new SecondaryIndexManager(this); for (IndexMetadata info : metadata.get().indexes) { - indexManager.addIndex(info, true); + initialBuilds.add(indexManager.addIndex(info, true)); } metric = new TableMetrics(this, memtableMetrics); - if (data.loadsstables) + if (data.loadsstables && sstables != null) { data.updateInitialSSTableSize(sstables); } @@ -570,6 +710,20 @@ public ColumnFamilyStore(Keyspace keyspace, topPartitions = new TopPartitionTracker(metadata()); } + /** + * Waits for the completion of index builds created during CFS initialization. + *

+ * This method blocks until all initial index builds have been completed or the timeout expires. Note that this method + * will throw if initial build tasks failed. + * + * @param timeout the maximum time to wait before timing out + * @param unit the time unit of the timeout argument + */ + public void awaitInitialIndexBuilds(long timeout, TimeUnit unit) + { + FBUtilities.waitOnFutures(initialBuilds, timeout, unit); + } + public static String getTableMBeanName(String ks, String name, boolean isIndex) { return String.format("org.apache.cassandra.db:type=%s,keyspace=%s,table=%s", @@ -588,8 +742,8 @@ public void updateSpeculationThreshold() { try { - sampleReadLatencyMicros = metadata().params.speculativeRetry.calculateThreshold(metric.coordinatorReadLatency, sampleReadLatencyMicros); - additionalWriteLatencyMicros = metadata().params.additionalWritePolicy.calculateThreshold(metric.coordinatorWriteLatency, additionalWriteLatencyMicros); + sampleReadLatencyMicros = metadata().params.speculativeRetry.calculateThreshold(metric.coordinatorReadLatency.tableOrKeyspaceTimer(), sampleReadLatencyMicros); + additionalWriteLatencyMicros = metadata().params.additionalWritePolicy.calculateThreshold(metric.coordinatorWriteLatency.tableOrKeyspaceTimer(), additionalWriteLatencyMicros); } catch (Throwable e) { @@ -612,16 +766,46 @@ public TableRepairManager getRepairManager() return repairManager; } + @Override + public Environment makeUCSEnvironment() + { + return new RealEnvironment(this); + } + public TableMetadata metadata() { return metadata.get(); } + @Override + public TableMetadataRef metadataRef() + { + return metadata; + } + + @Override + public TableMetrics metrics() + { + return metric; + } + + @Override + public AbstractReplicationStrategy getKeyspaceReplicationStrategy() + { + return keyspace.getReplicationStrategy(); + } + + @Override public Directories getDirectories() { return directories; } + public StorageHandler getStorageHandler() + { + return storageHandler; + } + @Override public List getDataPaths() throws IOException { @@ -656,7 +840,7 @@ public boolean streamFromMemtable() public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, TimeUUID pendingRepair, boolean isTransient, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker) { - return createSSTableMultiWriter(descriptor, keyCount, repairedAt, pendingRepair, isTransient, null, 0, header, lifecycleNewTracker); + return createSSTableMultiWriter(descriptor, keyCount, repairedAt, pendingRepair, isTransient, IntervalSet.empty(), 0, header, lifecycleNewTracker); } public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, TimeUUID pendingRepair, boolean isTransient, IntervalSet commitLogPositions, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker) @@ -666,12 +850,13 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long k public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, TimeUUID pendingRepair, boolean isTransient, IntervalSet commitLogPositions, int sstableLevel, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker) { - return getCompactionStrategyManager().createSSTableMultiWriter(descriptor, keyCount, repairedAt, pendingRepair, isTransient, commitLogPositions, sstableLevel, header, indexManager.listIndexGroups(), lifecycleNewTracker); + return getCompactionStrategy().createSSTableMultiWriter(descriptor, keyCount, repairedAt, pendingRepair, isTransient, commitLogPositions, sstableLevel, header, indexManager.listIndexGroups(), lifecycleNewTracker); } + @Override public boolean supportsEarlyOpen() { - return compactionStrategyManager.supportsEarlyOpen(); + return strategyContainer.supportsEarlyOpen(); } /** call when dropping or renaming a CF. Performs mbean housekeeping and invalidates CFS to other operations */ @@ -687,8 +872,14 @@ public void invalidate(boolean expectMBean) public void invalidate(boolean expectMBean, boolean dropData) { + if (logger.isTraceEnabled()) + { + logger.trace("Invalidating CFS {}, status: {}, expectMBean: {}, dropData: {}", + metadata.name, status, expectMBean, dropData); + } + // disable and cancel in-progress compactions before invalidating - valid = false; + status = dropData ? STATUS.INVALID_DROPPED : STATUS.INVALID_UNLOADED; try { @@ -704,20 +895,41 @@ public void invalidate(boolean expectMBean, boolean dropData) } } - compactionStrategyManager.shutdown(); + strategyContainer.shutdown(); // Do not remove truncation records for index CFs, given they have the same ID as their backing/base tables. if (!metadata.get().isIndex()) SystemKeyspace.removeTruncationRecord(metadata.id); - if (dropData) - { - data.dropSSTables(); - LifecycleTransaction.waitForDeletions(); - } - indexManager.dropAllIndexes(dropData); + storageHandler.runWithReloadingDisabled(() -> { + if (status.isInvalidAndShouldDropData()) + { + data.dropSSTables(); + + indexManager.dropAllIndexes(dropData); + } + else + { + // In CNDB, because of multi-tenancy, we might just unload a CFS without deleting the data as + // a tenant can be moved to a different set of nodes, which will then need to read data from remote storage + data.unloadSSTables(); + + indexManager.unloadAllIndexes(); + } + + storageHandler.unload(); + + // wait for sstable GlobalTidy to complete + if (!status.isValid()) + { + LifecycleTransaction.waitForDeletions(); // just in case an index had a reference on the sstable + } + }); invalidateCaches(); + if (logger.isTraceEnabled()) + logger.trace("CFS {} invalidated", metadata.name); + if (topPartitions != null) topPartitions.close(); } @@ -817,7 +1029,7 @@ public static void scrubDataDirectories(TableMetadata metadata) throws StartupE // cleanup incomplete saved caches Pattern tmpCacheFilePattern = Pattern.compile(metadata.keyspace + '-' + metadata.name + "-(Key|Row)Cache.*\\.tmp$"); - File dir = new File(DatabaseDescriptor.getSavedCachesLocation()); + File dir = DatabaseDescriptor.getSavedCachesLocation(); if (dir.exists()) { @@ -852,6 +1064,7 @@ public static void loadNewSSTables(String ksName, String cfName) /** @deprecated See CASSANDRA-6719 */ @Deprecated(since = "4.0") + @Override public void loadNewSSTables() { @@ -859,10 +1072,11 @@ public void loadNewSSTables() sstableImporter.importNewSSTables(options); } + /** + * #{@inheritDoc} + */ @Override - public List importNewSSTables(Set srcPaths, boolean resetLevel, boolean clearRepaired, - boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches, - boolean extendedVerify, boolean copyData) + public synchronized List importNewSSTables(Set srcPaths, boolean resetLevel, boolean clearRepaired, boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches, boolean extendedVerify, boolean copyData) { return sstableImporter.importNewSSTables(SSTableImporter.Options.options(srcPaths) .resetLevel(resetLevel) @@ -875,9 +1089,7 @@ public List importNewSSTables(Set srcPaths, boolean resetLevel, } @Override - public List importNewSSTables(Set srcPaths, boolean resetLevel, boolean clearRepaired, - boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches, - boolean extendedVerify) + public List importNewSSTables(Set srcPaths, boolean resetLevel, boolean clearRepaired, boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches, boolean extendedVerify) { return sstableImporter.importNewSSTables(SSTableImporter.Options.options(srcPaths) .resetLevel(resetLevel) @@ -938,37 +1150,32 @@ public static void rebuildSecondaryIndex(String ksName, String cfName, String... cfs.indexManager.rebuildIndexesBlocking(Sets.newHashSet(Arrays.asList(idxNames))); } - public AbstractCompactionStrategy createCompactionStrategyInstance(CompactionParams compactionParams) - { - try - { - Constructor constructor = - compactionParams.klass().getConstructor(ColumnFamilyStore.class, Map.class); - return constructor.newInstance(this, compactionParams.options()); - } - catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) - { - throw new RuntimeException(e); - } - } - /** @deprecated See CASSANDRA-9448 */ @Deprecated(since = "3.0") + @Override public String getColumnFamilyName() { return getTableName(); } + @Override public String getTableName() { return name; } + @Override public String getKeyspaceName() { return keyspace.getName(); } + public KeyspaceMetrics getKeyspaceMetrics() + { + return keyspace.metric; + } + + @Override public Descriptor newSSTableDescriptor(File directory) { return newSSTableDescriptor(directory, DatabaseDescriptor.getSelectedSSTableFormat().getLatestVersion()); @@ -996,11 +1203,14 @@ public Descriptor newSSTableDescriptor(File directory, Version version) */ private void switchMemtableOrNotify(FlushReason reason, Consumer elseNotify) { - Memtable currentMemtable = data.getView().getCurrentMemtable(); - if (currentMemtable.shouldSwitch(reason)) - switchMemtableIfCurrent(currentMemtable, reason); - else - elseNotify.accept(currentMemtable); + if (!data.getView().liveMemtables.isEmpty()) + { + Memtable currentMemtable = data.getView().getCurrentMemtable(); + if (currentMemtable.shouldSwitch(reason)) + switchMemtableIfCurrent(currentMemtable, reason); + else + elseNotify.accept(currentMemtable); + } } /** @@ -1049,7 +1259,7 @@ private void logFlush(FlushReason reason) for (ColumnFamilyStore indexCfs : indexManager.getAllIndexColumnFamilyStores()) indexCfs.getTracker().getView().getCurrentMemtable().addMemoryUsageTo(usage); - logger.info("Enqueuing flush of {}.{}, Reason: {}, Usage: {}", getKeyspaceName(), name, reason, usage); + logger.debug("Enqueuing flush of {}.{}, Reason: {}, Usage: {}", getKeyspaceName(), name, reason, usage); } @@ -1063,11 +1273,18 @@ public Future forceFlush(FlushReason reason) { synchronized (data) { - Memtable current = data.getView().getCurrentMemtable(); - for (ColumnFamilyStore cfs : concatWithIndexes()) - if (!cfs.data.getView().getCurrentMemtable().isClean()) - return flushMemtable(current, reason); - return waitForFlushes(); + if (!data.getView().liveMemtables.isEmpty()) + { + Memtable current = data.getView().getCurrentMemtable(); + for (ColumnFamilyStore cfs : concatWithIndexes()) + if (!cfs.data.getView().getCurrentMemtable().isClean()) + return flushMemtable(current, reason); + return waitForFlushes(); + } + else + { + return ImmediateFuture.success(CommitLogPosition.NONE); + } } } @@ -1128,6 +1345,7 @@ private PostFlush(Memtable mainMemtable) this.mainMemtable = mainMemtable; } + @Override public CommitLogPosition call() { try @@ -1223,6 +1441,7 @@ private Flush(boolean truncate) postFlushTask = new FutureTask<>(postFlush); } + @Override public void run() { if (logger.isTraceEnabled()) @@ -1256,18 +1475,25 @@ public void run() } catch (Throwable t) { - JVMStabilityInspector.inspectThrowable(t); postFlush.flushFailure = t; + JVMStabilityInspector.inspectThrowable(t); } + finally + { + if (logger.isTraceEnabled()) + logger.trace("Flush task {}@{} signaling post flush task", hashCode(), name); - if (logger.isTraceEnabled()) - logger.trace("Flush task {}@{} signaling post flush task", hashCode(), name); - - // signal the post-flush we've done our work - postFlush.latch.decrement(); + // signal the post-flush we've done our work + postFlush.latch.decrement(); - if (logger.isTraceEnabled()) - logger.trace("Flush task task {}@{} finished", hashCode(), name); + if (logger.isTraceEnabled()) + { + if (postFlush.flushFailure != null) + logger.trace("Flush task task {}@{} failed", hashCode(), name); + else + logger.trace("Flush task task {}@{} finished successfully", hashCode(), name); + } + } } public Collection flushMemtable(ColumnFamilyStore cfs, Memtable memtable, boolean flushNonCf2i) @@ -1277,17 +1503,24 @@ public Collection flushMemtable(ColumnFamilyStore cfs, Memtable m if (memtable.isClean() || truncate) { - cfs.replaceFlushed(memtable, Collections.emptyList()); - reclaim(memtable); - return Collections.emptyList(); + try + { + cfs.replaceFlushed(memtable, Collections.emptyList(), Optional.empty()); + return Collections.emptyList(); + } + finally + { + if (!cfs.getTracker().getView().flushingMemtables.contains(memtable)) + reclaim(memtable); + } } - + long start = Clock.Global.nanoTime(); List> futures = new ArrayList<>(); long totalBytesOnDisk = 0; long maxBytesOnDisk = 0; long minBytesOnDisk = Long.MAX_VALUE; List sstables = new ArrayList<>(); - try (LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.FLUSH)) + try (LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.FLUSH, metadata)) { List flushRunnables = null; List flushResults = null; @@ -1310,12 +1543,25 @@ public Collection flushMemtable(ColumnFamilyStore cfs, Memtable m if (flushNonCf2i) indexManager.flushAllNonCFSBackedIndexesBlocking(memtable); + // It may be worthwhile to add an early abort mechanism here if one of the futures throws. + // In such a case this code will run the other threads to completion and only then abort the operation. flushResults = Lists.newArrayList(FBUtilities.waitOnFutures(futures)); } catch (Throwable t) { + logger.error("Flushing {} failed with error", memtable.toString(), t); t = Flushing.abortRunnables(flushRunnables, t); + + // wait for any flush runnables that were submitted (after aborting they should complete immediately) + // this ensures that the writers are aborted by FlushRunnable.writeSortedContents(), in the worst + // case we'll repeat the same exception twice if the initial exception was thrown whilst waiting + // on a future + t = perform(t, () -> FBUtilities.waitOnFutures(futures)); + + //finally abort the transaction t = txn.abort(t); + + // and re-throw Throwables.throwIfUnchecked(t); throw new RuntimeException(t); } @@ -1328,7 +1574,7 @@ public Collection flushMemtable(ColumnFamilyStore cfs, Memtable m SSTableMultiWriter writer = writerIterator.next(); if (writer.getBytesWritten() > 0) { - writer.setOpenResult(true).prepareToCommit(); + writer.prepareToCommit(); } else { @@ -1336,6 +1582,15 @@ public Collection flushMemtable(ColumnFamilyStore cfs, Memtable m writerIterator.remove(); } } + + // This can throw on remote storage, e.g. if a file cannot be uploaded + txn.prepareToCommit(); + + // Open the underlying readers, the one that will be returned below by `finished()`. + // Currently needs to be called before commit, because committing will close a certain number + // of resources used by the writers which are accessed to open the readers. + for (SSTableMultiWriter writer : flushResults) + writer.openResult(storageHandler); } catch (Throwable t) { @@ -1346,13 +1601,12 @@ public Collection flushMemtable(ColumnFamilyStore cfs, Memtable m throw new RuntimeException(t); } - txn.prepareToCommit(); - Throwable accumulate = null; + for (SSTableMultiWriter writer : flushResults) { accumulate = writer.commit(accumulate); - metric.flushSizeOnDisk.update(writer.getOnDiskBytesWritten()); + metric.flushSizeOnDisk().update(writer.getOnDiskBytesWritten()); } maybeFail(txn.commit(accumulate)); @@ -1372,16 +1626,25 @@ public Collection flushMemtable(ColumnFamilyStore cfs, Memtable m } } } + metric.memTableFlushCompleted(Clock.Global.nanoTime() - start); + + cfs.replaceFlushed(memtable, sstables, Optional.of(txn.opId())); + } + finally + { + if (!cfs.getTracker().getView().flushingMemtables.contains(memtable)) + reclaim(memtable); + } + cfs.strategyFactory.getCompactionLogger().flush(sstables); + if (logger.isTraceEnabled()) + { + logger.trace("Flushed to {} ({} sstables, {}), biggest {}, smallest {}", + sstables, + sstables.size(), + FBUtilities.prettyPrintMemory(totalBytesOnDisk), + FBUtilities.prettyPrintMemory(maxBytesOnDisk), + FBUtilities.prettyPrintMemory(minBytesOnDisk)); } - cfs.replaceFlushed(memtable, sstables); - reclaim(memtable); - cfs.compactionStrategyManager.compactionLogger.flush(sstables); - logger.debug("Flushed to {} ({} sstables, {}), biggest {}, smallest {}", - sstables, - sstables.size(), - FBUtilities.prettyPrintMemory(totalBytesOnDisk), - FBUtilities.prettyPrintMemory(maxBytesOnDisk), - FBUtilities.prettyPrintMemory(minBytesOnDisk)); return sstables; } @@ -1392,6 +1655,7 @@ private void reclaim(final Memtable memtable) readBarrier.issue(); postFlushTask.addListener(new WrappedRunnable() { + @Override public void runMayThrow() { readBarrier.await(); @@ -1454,29 +1718,52 @@ public Iterable getIndexMemtables() cfs -> cfs.getTracker().getView().getCurrentMemtable()); } + @Override + public SecondaryIndexManager getIndexManager() + { + return indexManager; + } + /** * Insert/Update the column family for this key. * Caller is responsible for acquiring Keyspace.switchLock + * * @param update to be applied * @param context write context for current update - * @param updateIndexes whether secondary indexes should be updated + * @param updateIndexes whether secondary indexes should be updated. + * When {@code false} this write is treated as a nested write: it skips the + * memtable pool's room-wait gate ({@link org.apache.cassandra.utils.memory.MemtableAllocator#awaitRoomToStart}) + * and goes straight to {@link Memtable#putNested} instead of {@link Memtable#put}. + * Callers that pass {@code false} are already executing inside an enclosing mutation + * (e.g. a legacy 2i index write initiated from {@code indexer.onInserted()} under the + * base table's memtable-internal locks) and must not block for room, + * because doing so would deadlock the flush write-barrier (CASSANDRA-21019). + * As a consequence, a nested write may allocate slightly beyond the gated limit. */ public void apply(PartitionUpdate update, CassandraWriteContext context, boolean updateIndexes) { long start = nanoTime(); + metric.writeRequests.inc(); OpOrder.Group opGroup = context.getGroup(); CommitLogPosition commitLogPosition = context.getPosition(); try { Memtable mt = data.getMemtableFor(opGroup, commitLogPosition); UpdateTransaction indexer = newUpdateTransaction(update, context, updateIndexes, mt); - long timeDelta = mt.put(update, indexer, opGroup); + + // updateIndexes == false identifies the nested index-table write performed + // from within an enclosing mutation (CassandraTableWriteHandler.write via CassandraIndex) + long timeDelta = updateIndexes ? mt.put(update, indexer, opGroup) + : mt.putNested(update, indexer, opGroup); + DecoratedKey key = update.partitionKey(); invalidateCachedPartition(key); metric.topWritePartitionFrequency.addSample(key.getKey(), 1); + int dataSize = update.dataSize(); if (metric.topWritePartitionSize.isEnabled()) // dont compute datasize if not needed - metric.topWritePartitionSize.addSample(key.getKey(), update.dataSize()); + metric.topWritePartitionSize.addSample(key.getKey(), dataSize); + metric.bytesInserted.inc(dataSize); StorageHook.instance.reportWrite(metadata.id, update); metric.writeLatency.addNano(nanoTime() - start); // CASSANDRA-11117 - certain resolution paths on memtable put can result in very @@ -1486,18 +1773,32 @@ public void apply(PartitionUpdate update, CassandraWriteContext context, boolean // to update. if(timeDelta < Long.MAX_VALUE) metric.colUpdateTimeDeltaHistogram.update(Math.min(18165375903306L, timeDelta)); + + if (!isIndex()) + { + RequestSensors sensors = requestTracker.get(); + if (sensors != null) + { + Context puContext = Context.from(this.metadata.get()); + sensors.registerSensor(puContext, Type.WRITE_BYTES); + sensors.incrementSensor(puContext, Type.WRITE_BYTES, dataSize); + } + } } catch (RuntimeException e) { - String message = e.getMessage() + " for ks: " + keyspace.getName() + ", table: " + name; - if (e instanceof InvalidRequestException) - throw new InvalidRequestException(message, e); - - throw new RuntimeException(message, e); + { + throw new InvalidRequestException(e.getMessage() + + " for ks: " + + keyspace.getName() + ", table: " + name, e); + } + throw new RuntimeException(e.getMessage() + + " for ks: " + + getKeyspaceName() + ", table: " + name, e); } } - + private UpdateTransaction newUpdateTransaction(PartitionUpdate update, CassandraWriteContext context, boolean updateIndexes, Memtable memtable) { return updateIndexes @@ -1505,52 +1806,6 @@ private UpdateTransaction newUpdateTransaction(PartitionUpdate update, Cassandra : UpdateTransaction.NO_OP; } - public static class VersionedLocalRanges extends ArrayList - { - public final long ringVersion; - - public VersionedLocalRanges(long ringVersion, int initialSize) - { - super(initialSize); - this.ringVersion = ringVersion; - } - } - - public VersionedLocalRanges localRangesWeighted() - { - if (!SchemaConstants.isLocalSystemKeyspace(getKeyspaceName()) - && getPartitioner() == StorageService.instance.getTokenMetadata().partitioner) - { - DiskBoundaryManager.VersionedRangesAtEndpoint versionedLocalRanges = DiskBoundaryManager.getVersionedLocalRanges(this); - Set> localRanges = versionedLocalRanges.rangesAtEndpoint.ranges(); - long ringVersion = versionedLocalRanges.ringVersion; - - if (!localRanges.isEmpty()) - { - VersionedLocalRanges weightedRanges = new VersionedLocalRanges(ringVersion, localRanges.size()); - for (Range r : localRanges) - { - // WeightedRange supports only unwrapped ranges as it relies - // on right - left == num tokens equality - for (Range u: r.unwrap()) - weightedRanges.add(new Splitter.WeightedRange(1.0, u)); - } - weightedRanges.sort(Comparator.comparing(Splitter.WeightedRange::left)); - return weightedRanges; - } - else - { - return fullWeightedRange(ringVersion, getPartitioner()); - } - } - else - { - // Local tables need to cover the full token range and don't care about ring changes. - // We also end up here if the table's partitioner is not the database's, which can happen in tests. - return fullWeightedRange(RING_VERSION_IRRELEVANT, getPartitioner()); - } - } - @Override public ShardBoundaries localRangeSplits(int shardCount) { @@ -1562,35 +1817,28 @@ public ShardBoundaries localRangeSplits(int shardCount) if (shardBoundaries == null || shardBoundaries.shardCount() != shardCount || (shardBoundaries.ringVersion != RING_VERSION_IRRELEVANT && - shardBoundaries.ringVersion != StorageService.instance.getTokenMetadata().getRingVersion())) + shardBoundaries.ringVersion != keyspace.getReplicationStrategy().getTokenMetadata().getRingVersion())) { - VersionedLocalRanges weightedRanges = localRangesWeighted(); - - List boundaries = getPartitioner().splitter().get().splitOwnedRanges(shardCount, weightedRanges, false); - shardBoundaries = new ShardBoundaries(boundaries.subList(0, boundaries.size() - 1), - weightedRanges.ringVersion); + SortedLocalRanges localRanges = getLocalRanges(); + List positions = localRanges.split(shardCount); + shardBoundaries = new ShardBoundaries(positions.subList(0, positions.size() - 1), + localRanges.getRingVersion()); cachedShardBoundaries = shardBoundaries; - logger.debug("Memtable shard boundaries for {}.{}: {}", getKeyspaceName(), getTableName(), boundaries); + logger.debug("Memtable shard boundaries for {}.{}: {}", keyspace.getName(), getTableName(), positions); } return shardBoundaries; } - @VisibleForTesting - public static VersionedLocalRanges fullWeightedRange(long ringVersion, IPartitioner partitioner) - { - VersionedLocalRanges ranges = new VersionedLocalRanges(ringVersion, 1); - ranges.add(new Splitter.WeightedRange(1.0, new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken()))); - return ranges; - } - /** * @param sstables * @return sstables whose key range overlaps with that of the given sstables, not including itself. * (The given sstables may or may not overlap with each other.) */ - public Collection getOverlappingLiveSSTables(Iterable sstables) + @Override + public Set getOverlappingLiveSSTables(Iterable sstables) { - logger.trace("Checking for sstables overlapping {}", sstables); + if (logger.isTraceEnabled()) + logger.trace("Checking for sstables overlapping {}", sstables); // a normal compaction won't ever have an empty sstables list, but we create a skeleton // compaction controller for streaming, and that passes an empty list. @@ -1599,11 +1847,11 @@ public Collection getOverlappingLiveSSTables(Iterable sortedByFirst = Lists.newArrayList(sstables); - sortedByFirst.sort(SSTableReader.firstKeyComparator); + List sortedByFirst = Lists.newArrayList(sstables); + sortedByFirst.sort(CompactionSSTable.firstKeyComparator); List> bounds = new ArrayList<>(); - DecoratedKey first = null, last = null; + PartitionPosition first = null, last = null; /* normalize the intervals covered by the sstables assume we have sstables like this (brackets representing first/last key in the sstable); @@ -1613,7 +1861,7 @@ public Collection getOverlappingLiveSSTables(Iterable getOverlappingLiveSSTables(Iterable results = new HashSet<>(); + Set overlaps = new HashSet<>(); for (AbstractBounds bound : bounds) - Iterables.addAll(results, view.liveSSTablesInBounds(bound.left, bound.right)); + Iterables.addAll(overlaps, view.liveSSTablesInBounds(bound.left, bound.right)); - return Sets.difference(results, ImmutableSet.copyOf(sstables)); + for (CompactionSSTable sstable : sstables) + overlaps.remove(sstable); + return overlaps; } /** @@ -1651,7 +1901,7 @@ public Refs getAndReferenceOverlappingLiveSSTables(Iterable overlapped = getOverlappingLiveSSTables(sstables); + Set overlapped = getOverlappingLiveSSTables(sstables); Refs refs = Refs.tryRef(overlapped); if (refs != null) return refs; @@ -1667,15 +1917,22 @@ public Refs getAndReferenceOverlappingLiveSSTables(Iterable sstables) { - data.addSSTables(sstables); + addSSTables(sstables, OperationType.UNKNOWN); + } + + public void addSSTables(Collection sstables, OperationType operationType) + { + data.addSSTables(sstables, operationType); CompactionManager.instance.submitBackground(this); } @@ -1692,11 +1949,12 @@ public void addSSTables(Collection sstables) * @param operation Operation type * @return Expected file size of SSTable after compaction */ + @Override public long getExpectedCompactedFileSize(Iterable sstables, OperationType operation) { if (operation != OperationType.CLEANUP || isIndex()) { - return SSTableReader.getTotalBytes(sstables); + return CompactionSSTable.getTotalDataBytes(sstables); } // cleanup size estimation only counts bytes for keys local to this node @@ -1716,24 +1974,6 @@ public long getExpectedCompactedFileSize(Iterable sstables, Opera return expectedFileSize; } - /* - * Find the maximum size file in the list . - */ - public SSTableReader getMaxSizeFile(Iterable sstables) - { - long maxSize = 0L; - SSTableReader maxFile = null; - for (SSTableReader sstable : sstables) - { - if (sstable.onDiskLength() > maxSize) - { - maxSize = sstable.onDiskLength(); - maxFile = sstable; - } - } - return maxFile; - } - public CompactionManager.AllSSTableOpStatus forceCleanup(int jobs) throws ExecutionException, InterruptedException { return CompactionManager.instance.performCleanup(ColumnFamilyStore.this, jobs); @@ -1829,18 +2069,29 @@ public void markObsolete(Collection sstables, OperationType compa maybeFail(data.dropSSTables(Predicates.in(sstables), compactionType, null)); } - void replaceFlushed(Memtable memtable, Collection sstables) + /** + * Beware, this code doesn't have noexcept guarantees + */ + void replaceFlushed(Memtable memtable, Collection sstables, Optional operationId) { - data.replaceFlushed(memtable, sstables); + data.replaceFlushed(memtable, sstables, operationId); if (sstables != null && !sstables.isEmpty()) CompactionManager.instance.submitBackground(this); } public boolean isValid() { - return valid; + return status.isValid(); } + /** + * @return status of the current column family store + */ + public STATUS status() + { + return status; + } + /** * Package protected for access from the CompactionManager. */ @@ -1849,27 +2100,82 @@ public Tracker getTracker() return data; } + + /** + * Convenience method for getting the set of live sstables associated with this ColumnFamilyStore. Note that this + * will also contain any early-opened sstables. + * @return the tracker's current view's {@link SSTableSet#LIVE} sstables + */ + @Override public Set getLiveSSTables() { - return data.getView().liveSSTables(); + return data.getLiveSSTables(); } + @Override public Iterable getSSTables(SSTableSet sstableSet) { return data.getView().select(sstableSet); } - public Iterable getUncompactingSSTables() + public Iterable getNoncompactingSSTables() { - return data.getUncompacting(); + return data.getNoncompacting(); } - public Map getPendingRepairStats() + @Override + public Iterable getNoncompactingSSTables(Iterable candidates) { - Map builders = new HashMap<>(); - for (SSTableReader sstable : getLiveSSTables()) - { - TimeUUID session = sstable.getPendingRepair(); + return data.getNoncompacting(candidates); + } + + @Override + public Set getCompactingSSTables() + { + return data.getCompacting(); + } + + @Override + public Iterable getAllMemtables() + { + return data.getView().getAllMemtables(); + } + + @Override + public OpOrder readOrdering() + { + return readOrdering; + } + + @Override + public int getMemtableFlushPeriodInMs() + { + int flushPeriodInMs = metadata().params.memtableFlushPeriodInMs; + flushPeriodInMs = pickSmallerFlushPeriod(flushPeriodInMs, CassandraRelevantProperties.FLUSH_PERIOD_IN_MILLIS.getInt()); + + // When creating CFS, indexManager is initialized after memtable, we need to handle null here; Later when SAI + // is initialized, SAI will force flush to create new memtable with proper flush period. + if (indexManager == null) + return flushPeriodInMs; + + for (Index index : indexManager.listIndexes()) + flushPeriodInMs = pickSmallerFlushPeriod(flushPeriodInMs, index.getFlushPeriodInMs()); + return flushPeriodInMs; + } + + private static int pickSmallerFlushPeriod(int period1, int period2) + { + if (period1 > 0 && period2 > 0) + return Math.min(period1, period2); + return period1 > 0 ? period1 : period2; + } + + public Map getPendingRepairStats() + { + Map builders = new HashMap<>(); + for (SSTableReader sstable : getLiveSSTables()) + { + TimeUUID session = sstable.getPendingRepair(); if (session == null) continue; @@ -1887,29 +2193,6 @@ public Map getPendingRepairStats() return stats; } - /** - * promotes (or demotes) data attached to an incremental repair session that has either completed successfully, - * or failed - * - * @return session ids whose data could not be released - */ - public CleanupSummary releaseRepairData(Collection sessions, boolean force) - { - if (force) - { - Predicate predicate = sst -> { - TimeUUID session = sst.getPendingRepair(); - return session != null && sessions.contains(session); - }; - return runWithCompactionsDisabled(() -> compactionStrategyManager.releaseRepairData(sessions), - predicate, OperationType.STREAM, false, true, true); - } - else - { - return compactionStrategyManager.releaseRepairData(sessions); - } - } - public boolean isFilterFullyCoveredBy(ClusteringIndexFilter filter, DataLimits limits, CachedPartition cached, @@ -2003,16 +2286,19 @@ public ViewFragment select(Function> filter) } // WARNING: this returns the set of LIVE sstables only, which may be only partially written + @Override public List getSSTablesForKey(String key) { return getSSTablesForKey(key, false); } + @Override public List getSSTablesForKey(String key, boolean hexFormat) { return withSSTablesForKey(key, hexFormat, SSTableReader::getFilename); } + @Override public Map> getSSTablesForKeyWithLevel(String key, boolean hexFormat) { List> ssts = withSSTablesForKey(key, hexFormat, sstr -> Pair.create(sstr.getSSTableLevel(), sstr.getFilename())); @@ -2118,11 +2404,13 @@ public ClusteringComparator getComparator() return metadata().comparator; } + @Override public TableSnapshot snapshotWithoutMemtable(String snapshotName) { return snapshotWithoutMemtable(snapshotName, now()); } + @Override public TableSnapshot snapshotWithoutMemtable(String snapshotName, Instant creationTime) { return snapshotWithoutMemtable(snapshotName, null, false, null, null, creationTime); @@ -2138,6 +2426,8 @@ public TableSnapshot snapshotWithoutMemtable(String snapshotName, Predicate SchemaConstants.FILENAME_LENGTH) + { + throw new IllegalArgumentException(format("Snapshot name must not be more than %d characters long for " + + "resolved snapshot name (got %d characters for \"%s\")", + FILENAME_LENGTH, snapshotName.length(), snapshotName)); + } + + // Allowed characters are a conservative subset of the AWS S3 "Safe characters" set + // (https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines): + // 0-9 a-z A-Z - _ . + // plus '+', which is not an S3 "Safe character" but can legitimately appear in system + // snapshot names via version build metadata (e.g. an upgrade snapshot "-upgrade-5.0.4+build-..."). + // The remaining S3-safe characters (! * ' ( )) are intentionally excluded as they are + // shell-significant and error-prone in paths, and the path separator '/' is excluded too, + // which is what blocks traversal attempts such as "../../mysnapshot" + if (!Pattern.compile("[a-zA-Z0-9_.+-]+").matcher(snapshotName).matches()) + { + throw new IllegalArgumentException("Snapshot name contains illegal characters: " + snapshotName); + } + } + protected TableSnapshot createSnapshot(String tag, boolean ephemeral, DurationSpec.IntSecondsBound ttl, Set sstables, Instant creationTime) { Set snapshotDirs = sstables.stream() .map(s -> Directories.getSnapshotDirectory(s.descriptor, tag).toAbsolute()) @@ -2445,6 +2773,7 @@ public void invalidateCachedPartition(RowCacheKey key) CacheService.instance.rowCache.remove(key); } + @Override public void invalidateCachedPartition(DecoratedKey key) { if (!isRowCacheEnabled()) @@ -2453,18 +2782,18 @@ public void invalidateCachedPartition(DecoratedKey key) invalidateCachedPartition(new RowCacheKey(metadata(), key)); } - public ClockAndCount getCachedCounter(ByteBuffer partitionKey, Clustering clustering, ColumnMetadata column, CellPath path) + public ClockAndCount getCachedCounter(CounterCacheKey key) { if (CacheService.instance.counterCache.getCapacity() == 0L) // counter cache disabled. return null; - return CacheService.instance.counterCache.get(CounterCacheKey.create(metadata(), partitionKey, clustering, column, path)); + return CacheService.instance.counterCache.get(key); } - public void putCachedCounter(ByteBuffer partitionKey, Clustering clustering, ColumnMetadata column, CellPath path, ClockAndCount clockAndCount) + public void putCachedCounter(CounterCacheKey key, ClockAndCount clockAndCount) { if (CacheService.instance.counterCache.getCapacity() == 0L) // counter cache disabled. return; - CacheService.instance.counterCache.put(CounterCacheKey.create(metadata(), partitionKey, clustering, column, path), clockAndCount); + CacheService.instance.counterCache.put(key, clockAndCount); } public void forceMajorCompaction() @@ -2472,11 +2801,23 @@ public void forceMajorCompaction() forceMajorCompaction(false); } + @Override public void forceMajorCompaction(boolean splitOutput) { CompactionManager.instance.performMaximal(this, splitOutput); } + @Override + public void forceMajorCompaction(int parallelism) + { + CompactionManager.instance.performMaximal(this, false, parallelism); + } + + public void forceMajorCompaction(boolean splitOutput, int parallelism) + { + CompactionManager.instance.performMaximal(this, splitOutput, parallelism); + } + @Override public void forceCompactionForTokenRange(Collection> tokenRanges) throws ExecutionException, InterruptedException { @@ -2533,6 +2874,7 @@ public void forceCompactionKeysIgnoringGcGrace(String... partitionKeysIgnoreGcGr } } + @Override public boolean shouldIgnoreGcGraceForKey(DecoratedKey dk) { return partitionKeySetIgnoreGcGrace.contains(dk); @@ -2582,7 +2924,7 @@ public void writeAndAddMemtableRanges(TimeUUID repairSessionID, { try { - Collection sstables = memtableContent.finish(true); + Collection sstables = memtableContent.finish(true, storageHandler); try (Refs sstableReferences = Refs.ref(sstables)) { // This moves all references to placeIntoRefs, clearing sstableReferences @@ -2593,10 +2935,10 @@ public void writeAndAddMemtableRanges(TimeUUID repairSessionID, for (SSTableReader rdr : sstables) { rdr.selfRef().release(); - logger.info("Memtable ranges (keys {} size {}) written in {}", - rdr.estimatedKeys(), - rdr.getDataChannel().size(), - rdr); + logger.debug("Memtable ranges (keys {} size {}) written in {}", + rdr.estimatedKeys(), + rdr.getDataChannel().size(), + rdr); } } catch (Throwable t) @@ -2661,26 +3003,33 @@ private SSTableMultiWriter writeMemtableRanges(Supplier) () -> { cfs.data.reset(memtableFactory.create(new AtomicReference<>(CommitLogPosition.NONE), cfs.metadata, cfs)); + cfs.reloadCompactionStrategy(metadata().params.compaction, CompactionStrategyContainer.ReloadReason.FULL); return null; - }, OperationType.P0, true, false); + }, OperationType.P0, true, false, TableOperation.StopTrigger.UNIT_TESTS); } } @@ -2704,6 +3054,12 @@ public void truncateBlockingWithoutSnapshot() truncateBlocking(true); } + @FunctionalInterface + interface AdaptiveLogger + { + void log(String template, Object... args); + } + /** * Truncate deletes the entire column family's data with no expensive tombstone creation * @param noSnapshot if {@code true} no snapshot will be taken @@ -2722,13 +3078,21 @@ private void truncateBlocking(boolean noSnapshot) // beginning if we restart before they [the CL segments] are discarded for // normal reasons post-truncate. To prevent this, we store truncation // position in the System keyspace. - logger.info("Truncating {}.{}", getKeyspaceName(), name); + AdaptiveLogger log = truncateLogger(); + + log.log("Truncating {}.{}", getKeyspaceName(), name); viewManager.stopBuild(); final long truncatedAt; final CommitLogPosition replayAfter; + // This is a no-op on local storage, but on remote storage where compaction runs offline, this + // ensures that any live sstables created by the compaction process before it was interrupted, + // will actually be obsoleted by one of the writers - since all writers must be running for + // a truncate to run, then at least one writer per token range will load sstables created by compaction + storageHandler.reloadSSTables(StorageHandler.ReloadReason.TRUNCATION); + if (!noSnapshot && ((keyspace.getMetadata().params.durableWrites && !memtableWritesAreDurable()) // need to clear dirty regions || isAutoSnapshotEnabled())) @@ -2754,36 +3118,46 @@ private void truncateBlocking(boolean noSnapshot) Runnable truncateRunnable = new Runnable() { + @Override public void run() { - logger.info("Truncating {}.{} with truncatedAt={}", getKeyspaceName(), getTableName(), truncatedAt); + log.log("Truncating {}.{} with truncatedAt={}", getKeyspaceName(), getTableName(), truncatedAt); // since truncation can happen at different times on different nodes, we need to make sure // that any repairs are aborted, otherwise we might clear the data on one node and then // stream in data that is actually supposed to have been deleted ActiveRepairService.instance().abort((prs) -> prs.getTableIds().contains(metadata.id), "Stopping parent sessions {} due to truncation of tableId="+metadata.id); - data.notifyTruncated(truncatedAt); + data.notifyTruncated(replayAfter, truncatedAt); - if (!noSnapshot && isAutoSnapshotEnabled()) - snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, SNAPSHOT_TRUNCATE_PREFIX), DatabaseDescriptor.getAutoSnapshotTtl()); + if (!noSnapshot && isAutoSnapshotEnabled()) + snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, SNAPSHOT_TRUNCATE_PREFIX), DatabaseDescriptor.getAutoSnapshotTtl()); - discardSSTables(truncatedAt); + discardSSTables(truncatedAt); - indexManager.truncateAllIndexesBlocking(truncatedAt); - viewManager.truncateBlocking(replayAfter, truncatedAt); + indexManager.truncateAllIndexesBlocking(truncatedAt); + viewManager.truncateBlocking(replayAfter, truncatedAt); - SystemKeyspace.saveTruncationRecord(ColumnFamilyStore.this, truncatedAt, replayAfter); + SystemKeyspace.saveTruncationRecord(metadata.id, truncatedAt, replayAfter); logger.trace("cleaning out row cache"); invalidateCaches(); } }; - runWithCompactionsDisabled(FutureTask.callable(truncateRunnable), OperationType.P0, true, true); + storageHandler.runWithReloadingDisabled(() -> { + runWithCompactionsDisabled(FutureTask.callable(truncateRunnable), OperationType.P0, true, true, AbstractTableOperation.StopTrigger.TRUNCATE); + }); viewManager.build(); + log.log("Truncate of {}.{} is complete", getKeyspaceName(), name); + } - logger.info("Truncate of {}.{} is complete", getKeyspaceName(), name); + private AdaptiveLogger truncateLogger() + { + if (keyspace.getName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME)) + return logger::debug; + else + return logger::info; } /** @@ -2810,9 +3184,10 @@ public void unloadCf() FBUtilities.waitOnFuture(dumpMemtable()); } - public V runWithCompactionsDisabled(Callable callable, OperationType operationType, boolean interruptValidation, boolean interruptViews) + @Override + public V runWithCompactionsDisabled(Callable callable, OperationType operationType, boolean interruptValidation, boolean interruptViews, TableOperation.StopTrigger trigger) { - return runWithCompactionsDisabled(callable, (sstable) -> true, operationType, interruptValidation, interruptViews, true); + return runWithCompactionsDisabled(callable, (sstable) -> true, operationType, interruptValidation, interruptViews, true, trigger); } /** @@ -2824,55 +3199,70 @@ public V runWithCompactionsDisabled(Callable callable, OperationType oper * @param interruptViews if we should interrupt view compactions * @param interruptIndexes if we should interrupt compactions on indexes. NOTE: if you set this to true your sstablePredicate * must be able to handle LocalPartitioner sstables! + * @param trigger the cause for interrupting compactions */ - public V runWithCompactionsDisabled(Callable callable, Predicate sstablesPredicate, OperationType operationType, boolean interruptValidation, boolean interruptViews, boolean interruptIndexes) + public V runWithCompactionsDisabled(Callable callable, + Predicate sstablesPredicate, + OperationType operationType, + boolean interruptValidation, + boolean interruptViews, + boolean interruptIndexes, + TableOperation.StopTrigger trigger) { // synchronize so that concurrent invocations don't re-enable compactions partway through unexpectedly, // and so we only run one major compaction at a time - synchronized (this) + longRunningSerializedOperationsLock.lock(); + try { - logger.debug("Cancelling in-progress compactions for {}", metadata.name); - Iterable toInterruptFor = interruptIndexes - ? concatWithIndexes() - : Collections.singleton(this); - - toInterruptFor = interruptViews - ? Iterables.concat(toInterruptFor, viewManager.allViewsCfs()) - : toInterruptFor; + logger.debug("Started cancelling in-progress compactions for {}", metadata.name); + Iterable toInterruptFor = concatWith(interruptIndexes, interruptViews); Iterable toInterruptForMetadata = Iterables.transform(toInterruptFor, ColumnFamilyStore::metadata); try (CompactionManager.CompactionPauser pause = CompactionManager.instance.pauseGlobalCompaction(); CompactionManager.CompactionPauser pausedStrategies = pauseCompactionStrategies(toInterruptFor)) { - List uninterruptibleTasks = CompactionManager.instance.getCompactionsMatching(toInterruptForMetadata, - (info) -> info.getTaskType().priority <= operationType.priority); - if (!uninterruptibleTasks.isEmpty()) + List uninterruptibleOps = CompactionManager.instance.getCompactionsMatching(toInterruptForMetadata, + sstablesPredicate, + (progress) -> progress.operationType().priority <= operationType.priority); + if (!uninterruptibleOps.isEmpty()) { logger.info("Unable to cancel in-progress compactions, since they're running with higher or same priority: {}. You can abort these operations using `nodetool stop`.", - uninterruptibleTasks.stream().map((compaction) -> String.format("%s@%s (%s)", - compaction.getCompactionInfo().getTaskType(), - compaction.getCompactionInfo().getTable(), - compaction.getCompactionInfo().getTaskId())) + uninterruptibleOps.stream().map((compaction) -> String.format("%s@%s (%s)", + compaction.getProgress().operationType(), + compaction.getProgress().metadata().name, + compaction.getProgress().operationId())) .collect(Collectors.joining(","))); return null; } + Collection uninterruptibleTasks = CompactionManager.instance.active.getScheduledTasksMatching(toInterruptFor, + sstablesPredicate, + task -> task.getCompactionType().priority <= operationType.priority); + if (!uninterruptibleTasks.isEmpty()) + { + logger.info("Unable to cancel {} scheduled compactions with higher or same priority. You can abort these operations using `nodetool stop`.", uninterruptibleTasks.size()); + return null; + } + + // We have checked that there are no operations with overriding priority and can now stop all the tasks + // we find satisfying the sstables predicate. If new higher-priority tasks happen to appear in-between, + // we will still stop them; any task that appears at this point is in violation of the compaction pause + // we are operating under and is okay to cancel. + + // Cancel scheduled compactions matching predicate. This must be done first because tasks progress from + // scheduled to active. + CompactionManager.instance.active.cancelScheduledTasksAffecting(toInterruptFor, sstablesPredicate, trigger); + // interrupt in-progress compactions - CompactionManager.instance.interruptCompactionForCFs(toInterruptFor, sstablesPredicate, interruptValidation); + CompactionManager.instance.interruptCompactionForCFs(toInterruptFor, sstablesPredicate, interruptValidation, trigger); + CompactionManager.instance.waitForCessation(toInterruptFor, sstablesPredicate); // doublecheck that we finished, instead of timing out - for (ColumnFamilyStore cfs : toInterruptFor) - { - if (cfs.getTracker().getCompacting().stream().anyMatch(sstablesPredicate)) - { - logger.warn("Unable to cancel in-progress compactions for {}. " + - "Perhaps there is an unusually large row in progress somewhere, or the system is simply overloaded.", - metadata.name); - return null; - } - } + if (!allCompactionsFinished(toInterruptFor, sstablesPredicate)) + return null; + logger.trace("Compactions successfully cancelled"); // run our task @@ -2885,7 +3275,33 @@ public V runWithCompactionsDisabled(Callable callable, Predicate cfss, Predicate sstablesPredicate) + { + for (ColumnFamilyStore cfs : cfss) + { + List compactingSatisfyingPredicate = cfs.getCompactingSSTables().stream().filter(sstablesPredicate).collect(Collectors.toList()); + if (!compactingSatisfyingPredicate.isEmpty()) + { + logger.warn("Unable to cancel in-progress compactions for {}.{}. Perhaps there is an unusually " + + "large row in progress somewhere, or the system is simply overloaded.", metadata.keyspace, metadata.name); + logger.debug("SSTables in in-flight operations: {}", compactingSatisfyingPredicate); + logger.debug("Operations involving these sstables: {}", CompactionManager.instance.getOperationsInvolving(List.of(cfs.metadata()), sstablesPredicate)); + return false; + } + } + + return true; } private static CompactionManager.CompactionPauser pauseCompactionStrategies(Iterable toPause) @@ -2896,7 +3312,7 @@ private static CompactionManager.CompactionPauser pauseCompactionStrategies(Iter for (ColumnFamilyStore cfs : toPause) { successfullyPaused.ensureCapacity(successfullyPaused.size() + 1); // to avoid OOM:ing after pausing the strategies - cfs.getCompactionStrategyManager().pause(); + cfs.getCompactionStrategy().pause(); successfullyPaused.add(cfs); } return () -> maybeFail(resumeAll(null, toPause)); @@ -2914,7 +3330,7 @@ private static Throwable resumeAll(Throwable accumulate, Iterable T withAllSSTables(final OperationType operationType, Function op) + public T withAllSSTables(final OperationType operationType, TableOperation.StopTrigger trigger, Function op) { Callable callable = () -> { assert data.getCompacting().isEmpty() : data.getCompacting(); - Iterable sstables = getLiveSSTables(); - sstables = AbstractCompactionStrategy.filterSuspectSSTables(sstables); + Iterable sstables = Iterables.filter(getLiveSSTables(), sstable -> !sstable.isMarkedSuspect()); LifecycleTransaction modifier = data.tryModify(sstables, operationType); assert modifier != null: "something marked things compacting while compactions are disabled"; return modifier; }; - try (LifecycleTransaction compacting = runWithCompactionsDisabled(callable, operationType, false, false)) + try (LifecycleTransaction compacting = runWithCompactionsDisabled(callable, operationType, false, false, trigger)) { return op.apply(compacting); } @@ -2944,17 +3359,32 @@ public T withAllSSTables(final OperationType operationType, Function> futures = CompactionManager.instance.submitBackground(this); - if (waitForFutures) - FBUtilities.waitOnFutures(futures); + strategyContainer.enable(); + Future future = CompactionManager.instance.submitBackground(this); + if (waitForFuture) + FBUtilities.waitOnFuture(future); } + @Override public boolean isAutoCompactionDisabled() { - return !this.compactionStrategyManager.isEnabled(); + return !this.strategyContainer.isEnabled(); } - /* - JMX getters and setters for the Defaults. - - get/set minCompactionThreshold - - get/set maxCompactionThreshold - - get memsize - - get memops - - get/set memtime + public List getCandidatesForUpgrade() + { + Set compacting = getTracker().getCompacting(); + return getLiveSSTables().stream() + .filter(s -> !compacting.contains(s) && !s.descriptor.version.isLatestVersion()) + .sorted((o1, o2) -> { + File f1 = o1.descriptor.fileFor(Components.DATA); + File f2 = o2.descriptor.fileFor(Components.DATA); + return Longs.compare(f1.lastModified(), f2.lastModified()); + }).collect(Collectors.toList()); + } + + public SortedLocalRanges getLocalRanges() + { + synchronized (this) + { + if (localRanges != null && !localRanges.isOutOfDate()) + return localRanges; + + localRanges = SortedLocalRanges.create(this); + return localRanges; + } + } + + /** + * Return the compaction strategy for this CFS. Even though internally the strategy container + * implements the strategy, we would like to just expose {@link CompactionStrategy} externally. + * This is not currently possible for the reasons explained in {@link this#getCompactionStrategyContainer()}, + * so we expose the container as well, but using a separate method, marked as deprecated. + * + * @return the compaction strategy for this CFS + */ + public CompactionStrategy getCompactionStrategy() + { + return strategyContainer; + } + + /** + * The reasons for exposing the compaction strategy container are the following: + * + * - Unit tests + * - Repair + * + * Eventually we would like to only expose the {@link CompactionStrategy}, so for new code call + * {@link this#getCompactionStrategy()} instead. + * + * @return the compaction strategy container */ + /** @deprecated See STAR-13 */ + @Deprecated(since = "unknown") + @VisibleForTesting + public CompactionStrategyContainer getCompactionStrategyContainer() + { + return strategyContainer; + } - public CompactionStrategyManager getCompactionStrategyManager() + /** + * This option determines if tombstones should only be removed when the sstable has been repaired. + * Because this option was introduced in patch releases (I'm guessing), the compaction parameters were + * abused. Eventually this option should be moved out of the compaction parameters. TODO: move it + * to the new compaction strategy interface. + * + * @return true if tombstones can only be removed if the sstable has been repaired + */ + @Override + public boolean onlyPurgeRepairedTombstones() { - return compactionStrategyManager; + // Here we need to ask the CSM for the parameters in case they were changed over JMX without changing the schema, + // for now the CSM has the up-to-date copy of the params + CompactionParams params = strategyContainer.getCompactionParams(); + return Boolean.parseBoolean(params.options().get(CompactionStrategyOptions.ONLY_PURGE_REPAIRED_TOMBSTONES)); } + @Override public void setCrcCheckChance(double crcCheckChance) { try @@ -3020,6 +3511,7 @@ public Double getCrcCheckChance() return crcCheckChance.value(); } + @Override public void setCompactionThresholds(int minThreshold, int maxThreshold) { validateCompactionThresholds(minThreshold, maxThreshold); @@ -3029,22 +3521,26 @@ public void setCompactionThresholds(int minThreshold, int maxThreshold) CompactionManager.instance.submitBackground(this); } + @Override public int getMinimumCompactionThreshold() { return minCompactionThreshold.value(); } + @Override public void setMinimumCompactionThreshold(int minCompactionThreshold) { validateCompactionThresholds(minCompactionThreshold, maxCompactionThreshold.value()); this.minCompactionThreshold.set(minCompactionThreshold); } + @Override public int getMaximumCompactionThreshold() { return maxCompactionThreshold.value(); } + @Override public void setMaximumCompactionThreshold(int maxCompactionThreshold) { validateCompactionThresholds(minCompactionThreshold.value(), maxCompactionThreshold); @@ -3064,6 +3560,17 @@ private void validateCompactionThresholds(int minThreshold, int maxThreshold) // End JMX get/set. + @Override + public boolean isCompactionActive() + { + return getCompactionStrategyContainer().isActive(); + } + + public long getMaxSSTableBytes() + { + return getCompactionStrategy().getMaxSSTableBytes(); + } + public int getMeanEstimatedCellPerPartitionCount() { long sum = 0; @@ -3090,7 +3597,7 @@ public double getMeanPartitionSize() return count > 0 ? sum * 1.0 / count : 0; } - public int getMeanRowCount() + public int getMeanRowsPerPartition() { long totalRows = 0; long totalPartitions = 0; @@ -3103,6 +3610,7 @@ public int getMeanRowCount() return totalPartitions > 0 ? (int) (totalRows / totalPartitions) : 0; } + @Override public long estimateKeys() { long n = 0; @@ -3111,6 +3619,7 @@ public long estimateKeys() return n; } + @Override public IPartitioner getPartitioner() { return metadata().partitioner; @@ -3121,6 +3630,67 @@ public DecoratedKey decorateKey(ByteBuffer key) return getPartitioner().decorateKey(key); } + @Override + public BloomFilterTracker getBloomFilterTracker() + { + return bloomFilterTracker; + } + + public long getBloomFilterFalsePositiveCount() + { + return bloomFilterTracker.getFalsePositiveCount(); + } + + public long getBloomFilterTruePositiveCount() + { + return bloomFilterTracker.getTruePositiveCount(); + } + + public long getBloomFilterTrueNegativeCount() + { + return bloomFilterTracker.getTrueNegativeCount(); + } + + public double getRecentBloomFilterFalsePositiveRate() + { + return bloomFilterTracker.getRecentFalsePositiveRate(); + } + + public double getRecentBloomFilterTruePositiveRate() + { + return bloomFilterTracker.getRecentTruePositiveRate(); + } + + public double getRecentBloomFilterTrueNegativeRate() + { + return bloomFilterTracker.getRecentTrueNegativeRate(); + } + + public long getLazyBloomFilterHitCount() + { + return bloomFilterTracker.getLazyBloomFilterHitCount(); + } + + public long getLoadedBloomFilterHitCount() + { + return bloomFilterTracker.getLoadedBloomFilterHitCount(); + } + + public long getPassThroughBloomFilterHitCount() + { + return bloomFilterTracker.getPassThroughBloomFilterHitCount(); + } + + public long getReadRequests() + { + return metric == null ? 0 : metric.readRequests.getCount(); + } + + public long getBytesInserted() + { + return metric == null ? 0 : metric.bytesInserted.getCount(); + } + /** true if this CFS contains secondary index data */ public boolean isIndex() { @@ -3128,12 +3698,27 @@ public boolean isIndex() } public Iterable concatWithIndexes() + { + return concatWith(true, false); + } + + public Iterable concatWith(boolean includeIndexes, boolean includeViews) { // we return the main CFS first, which we rely on for simplicity in switchMemtable(), for getting the // latest commit log segment position - return Iterables.concat(Collections.singleton(this), indexManager.getAllIndexColumnFamilyStores()); + Set mainCFS = Collections.singleton(this); + if (includeIndexes && includeViews) + return Iterables.concat(mainCFS, + indexManager.getAllIndexColumnFamilyStores(), + viewManager.allViewsCfs()); + if (includeIndexes) + return Iterables.concat(mainCFS, indexManager.getAllIndexColumnFamilyStores()); + if (includeViews) + return Iterables.concat(mainCFS, viewManager.allViewsCfs()); + return mainCFS; } + @Override public List getBuiltIndexes() { return indexManager.getBuiltIndexNames(); @@ -3142,37 +3727,40 @@ public List getBuiltIndexes() @Override public int getUnleveledSSTables() { - return compactionStrategyManager.getUnleveledSSTables(); + if (strategyContainer instanceof CompactionStrategyManager) + return ((CompactionStrategyManager) strategyContainer).getUnleveledSSTables(); + else + return 0; } @Override public int[] getSSTableCountPerLevel() { - return compactionStrategyManager.getSSTableCountPerLevel(); + return strategyContainer.getSSTableCountPerLevel(); } @Override public long[] getPerLevelSizeBytes() { - return compactionStrategyManager.getPerLevelSizeBytes(); + return strategyContainer.getPerLevelSizeBytes(); } @Override public boolean isLeveledCompaction() { - return compactionStrategyManager.isLeveledCompaction(); + return strategyContainer.isLeveledCompaction(); } @Override public int[] getSSTableCountPerTWCSBucket() { - return compactionStrategyManager.getSSTableCountPerTWCSBucket(); + return strategyContainer.getSSTableCountPerTWCSBucket(); } @Override public int getLevelFanoutSize() { - return compactionStrategyManager.getLevelFanoutSize(); + return strategyContainer.getLevelFanoutSize(); } public static class ViewFragment @@ -3202,6 +3790,7 @@ public void release() refs.release(); } + @Override public void close() { refs.release(); @@ -3252,6 +3841,7 @@ public boolean isTableIncrementalBackupsEnabled() public void discardSSTables(long truncatedAt) { assert data.getCompacting().isEmpty() : data.getCompacting(); + AdaptiveLogger log = truncateLogger(); List truncatedSSTables = new ArrayList<>(); int keptSSTables = 0; @@ -3264,15 +3854,31 @@ public void discardSSTables(long truncatedAt) else { keptSSTables++; - logger.info("Truncation is keeping {} maxDataAge={} truncatedAt={}", sstable, sstable.maxDataAge, truncatedAt); + log.log("Truncation is keeping {} maxDataAge={} truncatedAt={}", sstable, sstable.maxDataAge, truncatedAt); } } if (!truncatedSSTables.isEmpty()) { - logger.info("Truncation is dropping {} sstables and keeping {} due to sstable.maxDataAge > truncatedAt", truncatedSSTables.size(), keptSSTables); - markObsolete(truncatedSSTables, OperationType.UNKNOWN); + log.log("Truncation is dropping {} sstables and keeping {} due to sstable.maxDataAge > truncatedAt", truncatedSSTables.size(), keptSSTables); + markObsolete(truncatedSSTables, OperationType.TRUNCATE_TABLE); + } + } + + /** + * Discard sstables that matches given filter with provided operation type + */ + public void discardSSTables(Iterable sstables, Predicate filter, OperationType operationType) + { + List discarded = new ArrayList<>(); + for (SSTableReader sstable : sstables) + { + if (filter.apply(sstable)) + discarded.add(sstable); } + + if (!discarded.isEmpty()) + markObsolete(discarded, operationType); } @Override @@ -3343,11 +3949,23 @@ public static TableMetrics metricsFor(TableId tableId) return Objects.requireNonNull(getIfExists(tableId)).metric; } - /** - * Grabs the global first/last tokens among sstables and returns the range of data directories that start/end with those tokens. - * - * This is done to avoid grabbing the disk boundaries for every sstable in case of huge compactions. - */ + @Nullable + public static TableMetrics metricsForIfPresent(TableId tableId) + { + ColumnFamilyStore cfs = getIfExists(tableId); + return cfs == null ? null : cfs.metric; + } + + // Used by CNDB + public long getMemtablesLiveSize() + { + long liveSize = 0L; + for (Memtable memtable : data.getView().getAllMemtables()) + liveSize += memtable.getLiveDataSize(); + return liveSize; + } + + @Override public List getDirectoriesForFiles(Set sstables) { Directories.DataDirectory[] writeableLocations = directories.getWriteableLocations(); @@ -3373,13 +3991,20 @@ public List getDirectoriesForFiles(Set sstables) return diskBoundaries.getDisksInBounds(first, last).stream().map(directories::getLocationForDisk).collect(Collectors.toList()); } + @Override public DiskBoundaries getDiskBoundaries() { return diskBoundaryManager.getDiskBoundaries(this); } - public void invalidateLocalRanges() + public void invalidateLocalRangesAndDiskBoundaries() { + synchronized (this) + { + if (localRanges != null) + localRanges.invalidate(); + } + diskBoundaryManager.invalidate(); switchMemtableOrNotify(FlushReason.OWNED_RANGES_CHANGE, Memtable::localRangesUpdated); @@ -3405,17 +4030,39 @@ public boolean getNeverPurgeTombstones() void onTableDropped() { indexManager.markAllIndexesRemoved(); + if (logger.isTraceEnabled()) + logger.trace("CFS {} is being dropped: indexes removed", name); - CompactionManager.instance.interruptCompactionForCFs(concatWithIndexes(), (sstable) -> true, true); + CompactionManager.instance.interruptCompactionForCFs(concatWithIndexes(), (sstable) -> true, true, TableOperation.StopTrigger.DROP_TABLE); + if (logger.isTraceEnabled()) + logger.trace("CFS {} is being dropped: compactions stopped", name); if (isAutoSnapshotEnabled()) snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, ColumnFamilyStore.SNAPSHOT_DROP_PREFIX), DatabaseDescriptor.getAutoSnapshotTtl()); - CommitLog.instance.forceRecycleAllSegments(Collections.singleton(metadata.id)); + if (getTracker().isDummy()) + { + // offline services (e.g. standalone compactor) don't have Memtables or CommitLog. An attempt to flush would + // throw an exception + logger.debug("Memtables and CommitLog are disabled; not recycling or flushing {}", metadata); + } + else + { + if (!UNSAFE_SYSTEM.getBoolean()) + { + if (logger.isTraceEnabled()) + logger.trace("Recycling CL segments for dropping {}", metadata); + CommitLog.instance.forceRecycleAllSegments(Collections.singleton(metadata.id)); + } + } - compactionStrategyManager.shutdown(); + if (logger.isTraceEnabled()) + logger.trace("Dropping CFS {}: shutting down compaction strategy", name); + strategyContainer.shutdown(); // wait for any outstanding reads/writes that might affect the CFS + if (logger.isTraceEnabled()) + logger.trace("Dropping CFS {}: waiting for read and write barriers", name); Keyspace.writeOrder.awaitNewBarrier(); readOrdering.awaitNewBarrier(); } @@ -3447,7 +4094,7 @@ private static final class PerDiskFlushExecutors private final boolean useSpecificExecutorForSystemKeyspaces; public PerDiskFlushExecutors(int flushWriters, - String[] locationsForNonSystemKeyspaces, + File[] locationsForNonSystemKeyspaces, boolean useSpecificLocationForSystemKeyspaces) { ExecutorPlus[] flushExecutors = createPerDiskFlushWriters(locationsForNonSystemKeyspaces.length, flushWriters); @@ -3574,4 +4221,186 @@ public TableMetrics getMetrics() { return metric; } + + private static void verifyMetadata(SSTableReader sstable, long repairedAt, TimeUUID pendingRepair, boolean isTransient) + { + if (!Objects.equals(pendingRepair, sstable.getPendingRepair())) + throw new IllegalStateException(String.format("Failed setting pending repair to %s on %s (pending repair is %s)", pendingRepair, sstable, sstable.getPendingRepair())); + if (repairedAt != sstable.getRepairedAt()) + throw new IllegalStateException(String.format("Failed setting repairedAt to %d on %s (repairedAt is %d)", repairedAt, sstable, sstable.getRepairedAt())); + if (isTransient != sstable.isTransient()) + throw new IllegalStateException(String.format("Failed setting isTransient to %b on %s (isTransient is %b)", isTransient, sstable, sstable.isTransient())); + } + + /** + * This method is exposed for testing only + * NotThreadSafe + */ + @VisibleForTesting + public int mutateRepaired(Collection sstables, long repairedAt, TimeUUID pendingRepair, boolean isTransient) throws IOException + { + Set changed = new HashSet<>(); + try + { + for (SSTableReader sstable: sstables) + { + sstable.mutateRepairedAndReload(repairedAt, pendingRepair, isTransient); + verifyMetadata(sstable, repairedAt, pendingRepair, isTransient); + changed.add(sstable); + } + } + finally + { + // if there was an exception mutating repairedAt, we should still notify for the + // sstables that we were able to modify successfully before releasing the lock + getTracker().notifySSTableRepairedStatusChanged(changed); + } + return changed.size(); + } + + /** + * Mutates sstable repairedAt times and notifies listeners of the change with the writeLock held. Prevents races + * with other processes between when the metadata is changed and when sstables are moved between strategies. + */ + public int mutateRepaired(@Nullable final ReentrantReadWriteLock.WriteLock writeLock, + Collection sstables, + long repairedAt, + TimeUUID pendingRepair, + boolean isTransient) throws IOException + { + if (writeLock == null) + return mutateRepaired(sstables, repairedAt, pendingRepair, isTransient); + + writeLock.lock(); + try + { + return mutateRepaired(sstables, repairedAt, pendingRepair, isTransient); + } + finally + { + writeLock.unlock(); + } + } + + @Override + public int mutateRepairedWithLock(Collection sstables, long repairedAt, TimeUUID pendingRepair, boolean isTransient) throws IOException + { + return mutateRepaired(getCompactionStrategyContainer().getWriteLock(), sstables, repairedAt, pendingRepair, isTransient); + } + + @Override + public void repairSessionCompleted(TimeUUID sessionID) + { + getCompactionStrategyContainer().repairSessionCompleted(sessionID); + } + + public boolean hasPendingRepairSSTables(TimeUUID sessionID) + { + return Iterables.any(data.getLiveSSTables(), pendingRepairPredicate(sessionID)); + } + + public Set getPendingRepairSSTables(TimeUUID sessionID) + { + return Sets.filter(data.getLiveSSTables(), pendingRepairPredicate(sessionID)); + } + + public static Predicate pendingRepairPredicate(@Nonnull TimeUUID sessionID) + { + return sstable -> sstable.getPendingRepair() != null && sessionID.equals(sstable.getPendingRepair()); + } + + @Override + public LifecycleTransaction tryModify(Iterable ssTableReaders, + OperationType operationType, + TimeUUID id) + { + return data.tryModify(Iterables.transform(ssTableReaders, SSTableReader.class::cast), operationType, id); + } + + @Override + public CompactionRealm.OverlapTracker getOverlapTracker(Iterable sources) + { + return new OverlapTracker(sources); + } + + class OverlapTracker implements CompactionRealm.OverlapTracker + { + final Iterable compacting; + private Refs overlappingSSTables; + private OverlapIterator overlapIterator; + + OverlapTracker(Iterable compacting) + { + this.compacting = compacting; + collectOverlaps(); + } + + @Override + public Collection overlaps() + { + return overlappingSSTables; + } + + @Override + public Collection overlaps(DecoratedKey key) + { + overlapIterator.update(key); + return overlapIterator.overlaps(); + } + + @Override + public Iterable openSelectedOverlappingSSTables(DecoratedKey key, + Predicate filter, + Function transformation) + { + overlapIterator.update(key); + + Iterable overlaps = overlapIterator.overlaps(); + Iterable transformed = Iterables.transform(overlaps, sstable -> filter.apply(sstable) + ? transformation.apply(sstable) + : null); + return Iterables.filter(transformed, Predicates.notNull()); + } + + @Override + public void close() + { + overlapIterator = null; + overlappingSSTables.release(); + } + + @Override + public boolean maybeRefresh() + { + for (CompactionSSTable reader : overlappingSSTables) + { + if (reader.isMarkedCompacted()) + { + close(); + collectOverlaps(); + return true; + } + } + return false; + } + + public void refreshOverlaps() + { + if (this.overlappingSSTables != null) + close(); + collectOverlaps(); + } + + private void collectOverlaps() + { + if (compacting == null) + overlappingSSTables = Refs.tryRef(Collections.emptyList()); + else + overlappingSSTables = getAndReferenceOverlappingLiveSSTables(compacting); + this.overlapIterator = new OverlapIterator<>(SSTableIntervalTree.buildIntervals(overlappingSSTables)); + + if (logger.isTraceEnabled()) + logger.trace("Refreshed overlaps: {}", overlappingSSTables); + } + } } diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java b/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java index 7d7b9e58eebc..83b69712aeea 100644 --- a/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java +++ b/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java @@ -52,6 +52,15 @@ public interface ColumnFamilyStoreMBean */ public void forceMajorCompaction(boolean splitOutput) throws ExecutionException, InterruptedException; + /** + * force a major compaction of this column family + * + * @param permittedParallelism The maximum number of compaction threads that can be used by the operation. + * If 0, the operation can use all available threads. + * If <0, the default parallelism will be used. + */ + public void forceMajorCompaction(int permittedParallelism) throws ExecutionException, InterruptedException; + /** * Forces a major compaction of specified token ranges in this column family. *

@@ -251,13 +260,14 @@ public List importNewSSTables(Set srcPaths, /** @deprecated See CASSANDRA-6719 */ @Deprecated(since = "4.0") public void loadNewSSTables(); + /** * @return the number of SSTables in L0. Always return 0 if Leveled compaction is not enabled. */ public int getUnleveledSSTables(); /** - * @return sstable count for each level. null unless leveled compaction is used. + * @return sstable count for each level. empty unless leveled or unified compaction is used. * array index corresponds to level(int[0] is for level 0, ...). */ public int[] getSSTableCountPerLevel(); @@ -280,7 +290,7 @@ public List importNewSSTables(Set srcPaths, public int[] getSSTableCountPerTWCSBucket(); /** - * @return sstable fanout size for level compaction strategy. + * @return sstable fanout size for level or unified compaction strategies. Default LCS fanout size otherwise. */ public int getLevelFanoutSize(); diff --git a/src/java/org/apache/cassandra/db/Columns.java b/src/java/org/apache/cassandra/db/Columns.java index 275d000dd369..32d7217bb1d0 100644 --- a/src/java/org/apache/cassandra/db/Columns.java +++ b/src/java/org/apache/cassandra/db/Columns.java @@ -19,7 +19,11 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.util.*; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; @@ -28,6 +32,7 @@ import net.nicoulaj.compilecommand.annotations.DontInline; import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.rows.ColumnData; @@ -36,6 +41,7 @@ import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.serializers.AbstractTypeSerializer; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.SearchIterator; @@ -76,6 +82,11 @@ public class Columns extends AbstractCollection implements Colle private final Object[] columns; private final int complexIdx; // Index of the first complex column + /** + * The columns passed to this constructor MUST BE SORTED with natural order - this is not checked in the constructor! + * The constructor remains private to ensure that this invariant is maintained - all the methods that call it + * ensure that the columns are properly sorted. + */ private Columns(Object[] columns, int complexIdx) { assert complexIdx <= BTree.size(columns); @@ -456,39 +467,121 @@ public String toString() public static class Serializer { + AbstractTypeSerializer typeSerializer = new AbstractTypeSerializer(); + public void serialize(Columns columns, DataOutputPlus out) throws IOException { - out.writeUnsignedVInt32(columns.size()); + int regularCount = 0; + int syntheticCount = 0; + + // Count regular and synthetic columns for (ColumnMetadata column : columns) - ByteBufferUtil.writeWithVIntLength(column.name.bytes, out); + { + if (column.isSynthetic()) + syntheticCount++; + else + regularCount++; + } + + // Jam the two counts into a single value to avoid massive backwards compatibility issues + long packedCount = getPackedCount(syntheticCount, regularCount); + out.writeUnsignedVInt(packedCount); + + // First pass - write synthetic columns with their full metadata + for (ColumnMetadata column : columns) + { + if (column.isSynthetic()) + { + ByteBufferUtil.writeWithVIntLength(column.name.bytes, out); + ByteBufferUtil.writeWithVIntLength(column.sythenticSourceColumn.bytes, out); + typeSerializer.serialize(column.type, out); + } + } + + // Second pass - write regular columns + for (ColumnMetadata column : columns) + { + if (!column.isSynthetic()) + ByteBufferUtil.writeWithVIntLength(column.name.bytes, out); + } + } + + private static long getPackedCount(int syntheticCount, int regularCount) + { + // Left shift of 20 gives us over 1M regular columns, and up to 4 synthetic columns + // before overflowing to a 4th byte. + return ((long) syntheticCount << 20) | regularCount; } public long serializedSize(Columns columns) { - long size = TypeSizes.sizeofUnsignedVInt(columns.size()); + int regularCount = 0; + int syntheticCount = 0; + long size = 0; + + // Count and calculate sizes for (ColumnMetadata column : columns) - size += ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes); - return size; + { + if (column.isSynthetic()) + { + syntheticCount++; + size += ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes); + size += ByteBufferUtil.serializedSizeWithVIntLength(column.sythenticSourceColumn.bytes); + size += typeSerializer.serializedSize(column.type); + } + else + { + regularCount++; + size += ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes); + } + } + + return TypeSizes.sizeofUnsignedVInt(getPackedCount(syntheticCount, regularCount)) + + size; } public Columns deserialize(DataInputPlus in, TableMetadata metadata) throws IOException { - int length = in.readUnsignedVInt32(); try (BTree.FastBuilder builder = BTree.fastBuilder()) { - for (int i = 0; i < length; i++) + long packedCount = in.readUnsignedVInt() ; + int regularCount = (int) (packedCount & 0xFFFFF); + int syntheticCount = (int) (packedCount >> 20); + + // First pass - synthetic columns + for (int i = 0; i < syntheticCount; i++) + { + ByteBuffer name = ByteBufferUtil.readWithVIntLength(in); + ByteBuffer sourceColumnName = ByteBufferUtil.readWithVIntLength(in); + AbstractType type = typeSerializer.deserialize(in); + + if (!name.equals(ColumnMetadata.SYNTHETIC_SCORE_ID.bytes)) + throw new IllegalStateException("Unknown synthetic column " + UTF8Type.instance.getString(name)); + + ColumnMetadata sourceColumn = metadata.getColumn(sourceColumnName); + if (sourceColumn == null) + { + // If we don't find the definition, it could be we have data for a dropped column + sourceColumn = metadata.getDroppedColumn(name); + if (sourceColumn == null) + throw new RuntimeException("Unknown column " + UTF8Type.instance.getString(name) + " during deserialization of " + metadata.keyspace + '.' + metadata.name); + } + + ColumnMetadata column = ColumnMetadata.syntheticScoreColumn(sourceColumn, type); + builder.add(column); + } + + // Second pass - regular columns + for (int i = 0; i < regularCount; i++) { ByteBuffer name = ByteBufferUtil.readWithVIntLength(in); ColumnMetadata column = metadata.getColumn(name); if (column == null) { - // If we don't find the definition, it could be we have data for a dropped column, and we shouldn't - // fail deserialization because of that. So we grab a "fake" ColumnMetadata that ensure proper - // deserialization. The column will be ignore later on anyway. + // If we don't find the definition, it could be we have data for a dropped column column = metadata.getDroppedColumn(name); - if (column == null) - throw new RuntimeException("Unknown column " + UTF8Type.instance.getString(name) + " during deserialization"); + throw new RuntimeException("Unknown column " + UTF8Type.instance.getString(name) + " during deserialization of " + metadata.keyspace + '.' + metadata.name); } builder.add(column); } @@ -502,7 +595,7 @@ public Columns deserialize(DataInputPlus in, TableMetadata metadata) throws IOEx */ public void serializeSubset(Collection columns, Columns superset, DataOutputPlus out) throws IOException { - /** + /* * We weight this towards small sets, and sets where the majority of items are present, since * we expect this to mostly be used for serializing result sets. * @@ -581,6 +674,50 @@ else if (superset.size() >= 64) } } + /** + * Deserialize a columns subset, placing the selected columns in the given array and returning the number of + * columns. + * + * @param superset the full list of columns + * @param in file from which the subset should be read + * @param placeInto An array where the selected columns will be placed, in the same order as superset. Must + * be at least superset.length long. + * @return the number of items placed in the target array, <= superset.length. + * @throws IOException + */ + public int deserializeSubset(ColumnMetadata[] superset, + DataInputPlus in, + ColumnMetadata[] placeInto) + throws IOException + { + long encoded = in.readUnsignedVInt(); + if (encoded == 0L) + { + // this is wasteful, but we don't expect to be called in this case (rows will have a flag set that + // bypasses this path). + System.arraycopy(superset, 0, placeInto, 0, superset.length); + return superset.length; + } + else if (superset.length >= 64) + { + return deserializeLargeSubset(in, superset, (int) encoded, placeInto); + } + else + { + int count = 0; + for (ColumnMetadata column : superset) + { + if ((encoded & 1) == 0) + placeInto[count++] = column; + + encoded >>>= 1; + } + if (encoded != 0) + throw new IOException("Invalid Columns subset bytes; too many bits set:" + Long.toBinaryString(encoded)); + return count; + } + } + // encodes a 1 bit for every *missing* column, on the assumption presence is more common, // and because this is consistent with encoding 0 to represent all present private static long encodeBitmap(Collection columns, Columns superset, int supersetCount) @@ -663,7 +800,18 @@ private Columns deserializeLargeSubset(DataInputPlus in, Columns superset, int d int skipped = 0; while (true) { - int nextMissingIndex = skipped < delta ? in.readUnsignedVInt32() : supersetCount; + int nextMissingIndex; + if (skipped < delta) + { + nextMissingIndex = (int) in.readUnsignedVInt32(); + if (nextMissingIndex >= supersetCount) + throw new IOException("Invalid Columns subset bytes; encoded not existing column: " + nextMissingIndex); + } + else + { + nextMissingIndex = supersetCount; + } + while (idx < nextMissingIndex) { ColumnMetadata def = iter.next(); @@ -681,6 +829,44 @@ private Columns deserializeLargeSubset(DataInputPlus in, Columns superset, int d } } + @DontInline + private int deserializeLargeSubset(DataInputPlus in, + ColumnMetadata[] superset, + int delta, + ColumnMetadata[] placeInto) + throws IOException + { + int supersetCount = superset.length; + int columnCount = supersetCount - delta; + + int count = 0; + if (columnCount < supersetCount / 2) + { + for (int i = 0 ; i < columnCount ; i++) + { + int idx = (int) in.readUnsignedVInt(); + placeInto[count++] = superset[idx]; + } + } + else + { + int idx = 0; + int skipped = 0; + while (true) + { + int nextMissingIndex = skipped < delta ? (int)in.readUnsignedVInt() : supersetCount; + while (idx < nextMissingIndex) + placeInto[count++] = superset[idx++]; + + if (idx == supersetCount) + break; + idx++; + skipped++; + } + } + return count; + } + @DontInline private int serializeLargeSubsetSize(Collection columns, int columnCount, Columns superset, int supersetCount) { @@ -714,6 +900,5 @@ private int serializeLargeSubsetSize(Collection columns, int col } return size; } - } } diff --git a/src/java/org/apache/cassandra/db/ConsistencyLevel.java b/src/java/org/apache/cassandra/db/ConsistencyLevel.java index 7c21c1287a7a..7f1a6bdeba49 100644 --- a/src/java/org/apache/cassandra/db/ConsistencyLevel.java +++ b/src/java/org/apache/cassandra/db/ConsistencyLevel.java @@ -17,10 +17,12 @@ */ package org.apache.cassandra.db; - import java.util.Locale; +import javax.annotation.Nullable; + import com.carrotsearch.hppc.ObjectIntHashMap; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.InOurDc; import org.apache.cassandra.schema.TableMetadata; @@ -28,8 +30,11 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.NetworkTopologyStrategy; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.QueryState; import org.apache.cassandra.transport.ProtocolException; +import static org.apache.cassandra.db.guardrails.Guardrails.CONFIG_PROVIDER; import static org.apache.cassandra.locator.Replicas.addToCountPerDc; public enum ConsistencyLevel @@ -47,6 +52,8 @@ public enum ConsistencyLevel LOCAL_ONE (10, true), NODE_LOCAL (11, true); + public static final boolean THREE_MEANS_ALL_BUT_ONE = CassandraRelevantProperties.THREE_MEANS_ALL_BUT_ONE.getBoolean(); + // Used by the binary protocol public final int code; private final boolean isDCLocal; @@ -83,6 +90,16 @@ public static ConsistencyLevel fromCode(int code) return codeIdx[code]; } + @Override + public String toString() + { + if (this == THREE && THREE_MEANS_ALL_BUT_ONE) + { + return "THREE (ALL_BUT_ONE)"; + } + return super.toString(); + } + public static ConsistencyLevel fromString(String str) { return valueOf(str.toUpperCase(Locale.US)); @@ -93,6 +110,12 @@ public static int quorumFor(AbstractReplicationStrategy replicationStrategy) return (replicationStrategy.getReplicationFactor().allReplicas / 2) + 1; } + static int allButOneFor(AbstractReplicationStrategy replicationStrategy) + { + int rf = replicationStrategy.getReplicationFactor().fullReplicas; + return rf <= 1 ? rf : rf - 1; + } + public static int localQuorumFor(AbstractReplicationStrategy replicationStrategy, String dc) { return (replicationStrategy instanceof NetworkTopologyStrategy) @@ -142,6 +165,10 @@ public int blockFor(AbstractReplicationStrategy replicationStrategy) case TWO: return 2; case THREE: + if (THREE_MEANS_ALL_BUT_ONE) + { + return allButOneFor(replicationStrategy); + } return 3; case QUORUM: case SERIAL: @@ -214,7 +241,7 @@ public void validateForRead() throws InvalidRequestException } } - public void validateForWrite() throws InvalidRequestException + public void validateForWrite(String keyspaceName, ClientState clientState) throws InvalidRequestException { switch (this) { @@ -225,7 +252,7 @@ public void validateForWrite() throws InvalidRequestException } // This is the same than validateForWrite really, but we include a slightly different error message for SERIAL/LOCAL_SERIAL - public void validateForCasCommit(AbstractReplicationStrategy replicationStrategy) throws InvalidRequestException + public void validateForCasCommit(AbstractReplicationStrategy replicationStrategy, String keyspaceName, ClientState clientState) throws InvalidRequestException { switch (this) { @@ -238,7 +265,7 @@ public void validateForCasCommit(AbstractReplicationStrategy replicationStrategy } } - public void validateForCas() throws InvalidRequestException + public void validateForCas(String keyspaceName, ClientState clientState) throws InvalidRequestException { if (!isSerialConsistency()) throw new InvalidRequestException("Invalid consistency for conditional update. Must be one of SERIAL or LOCAL_SERIAL"); @@ -249,7 +276,7 @@ public boolean isSerialConsistency() return this == SERIAL || this == LOCAL_SERIAL; } - public void validateCounterForWrite(TableMetadata metadata) throws InvalidRequestException + public void validateCounterForWrite(TableMetadata metadata, ClientState clientState) throws InvalidRequestException { if (this == ConsistencyLevel.ANY) throw new InvalidRequestException("Consistency level ANY is not yet supported for counter table " + metadata.name); @@ -259,7 +286,7 @@ public void validateCounterForWrite(TableMetadata metadata) throws InvalidReques } /** - * With a replication factor greater than one, reads that contact more than one replica will require + * With a replication factor greater than one, reads that contact more than one replica will require * reconciliation of the individual replica results at the coordinator. * * @return true if reads at this consistency level require merging at the coordinator @@ -275,4 +302,22 @@ private void requireNetworkTopologyStrategy(AbstractReplicationStrategy replicat throw new InvalidRequestException(String.format("consistency level %s not compatible with replication strategy (%s)", this, replicationStrategy.getClass().getName())); } + + /** + * Returns the strictest consistency level allowed by Guardrails. + * + * @param state the query state, used to skip the guardrails check if the query is internal or is done by a superuser. + * @return the strictest allowed serial consistency level + * @throws InvalidRequestException if all serial consistency level are disallowed + */ + public static ConsistencyLevel defaultSerialConsistency(@Nullable QueryState state) throws InvalidRequestException + { + ClientState clientState = state == null ? null : state.getClientState(); + if (DatabaseDescriptor.getRawConfig() == null || !CONFIG_PROVIDER.getOrCreate(clientState).getWriteConsistencyLevelsDisallowed().contains(ConsistencyLevel.SERIAL)) + return ConsistencyLevel.SERIAL; + else if (!CONFIG_PROVIDER.getOrCreate(clientState).getWriteConsistencyLevelsDisallowed().contains(ConsistencyLevel.LOCAL_SERIAL)) + return ConsistencyLevel.LOCAL_SERIAL; + + throw new InvalidRequestException("Serial consistency levels are disallowed by disallowedWriteConsistencies Guardrail"); + } } diff --git a/src/java/org/apache/cassandra/db/CounterMutation.java b/src/java/org/apache/cassandra/db/CounterMutation.java index ed64e0aad7d1..2ae8e805a713 100644 --- a/src/java/org/apache/cassandra/db/CounterMutation.java +++ b/src/java/org/apache/cassandra/db/CounterMutation.java @@ -18,49 +18,109 @@ package org.apache.cassandra.db; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.function.Supplier; +import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Objects; +import com.google.common.base.Supplier; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; -import com.google.common.util.concurrent.Striped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Histogram; +import org.apache.cassandra.cache.CounterCacheKey; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.counters.CounterLockManager; import org.apache.cassandra.db.marshal.ByteBufferAccessor; -import org.apache.cassandra.db.rows.*; -import org.apache.cassandra.db.filter.*; -import org.apache.cassandra.db.partitions.*; import org.apache.cassandra.db.context.CounterContext; +import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.metrics.DefaultNameFactory; +import org.apache.cassandra.metrics.LatencyMetrics; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.tracing.Tracing; -import org.apache.cassandra.utils.*; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.CounterId; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.btree.BTreeSet; - -import static java.util.concurrent.TimeUnit.*; +import org.apache.cassandra.utils.concurrent.Future; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_10; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_11; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_12; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_20; import static org.apache.cassandra.net.MessagingService.VERSION_40; import static org.apache.cassandra.net.MessagingService.VERSION_50; +import static org.apache.cassandra.net.MessagingService.VERSION_DSE_68; import static org.apache.cassandra.utils.Clock.Global.nanoTime; + public class CounterMutation implements IMutation { + private static final Logger logger = LoggerFactory.getLogger(CounterMutation.class); + private static final NoSpamLogger nospamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.SECONDS); + public static final CounterMutationSerializer serializer = new CounterMutationSerializer(); - private static final Striped LOCKS = Striped.lazyWeakLock(DatabaseDescriptor.getConcurrentCounterWriters() * 1024); + /** + * This metric tracks the number of timeouts that occurred because the locks could not be + * acquired within DatabaseDescriptor.getCounterWriteRpcTimeout(). + */ + public static final Counter lockTimeout = Metrics.counter(DefaultNameFactory.createMetricName("Counter", "lock_timeout", null)); + + /** + * This metric tracks how long it took to acquire all the locks + * that must be acquired before applying the counter mutation. + */ + public static final LatencyMetrics lockAcquireTime = new LatencyMetrics("Counter", "lock_acquire_time"); + + /** + * This metric tracks the number of locks that must be acquired before applying the counter + * mutation. A mutation normally has one partition only, unless it comes from a batch, + * where the same partition key is used across different tables. + * For each partition, we need to acquire one lock for each column on each row. + * The locks are striped, see {@link CounterMutation#LOCKS} for details. + */ + public static final Histogram locksPerUpdate = Metrics.histogram(DefaultNameFactory + .createMetricName("Counter", + "locks_per_update", + null), + false); + + private static final String LOCK_TIMEOUT_MESSAGE = "Failed to acquire locks for counter mutation on keyspace {} for longer than {} millis, giving up"; + private static final String LOCK_TIMEOUT_TRACE = "Failed to acquire locks for counter mutation for longer than {} millis, giving up"; private final Mutation mutation; private final ConsistencyLevel consistency; + public CounterMutation(Mutation mutation, ConsistencyLevel consistency) { this.mutation = mutation; @@ -72,6 +132,11 @@ public String getKeyspaceName() return mutation.getKeyspaceName(); } + public Keyspace getKeyspace() + { + return mutation.getKeyspace(); + } + public Collection getTableIds() { return mutation.getTableIds(); @@ -131,13 +196,17 @@ public Mutation applyCounterMutation() throws WriteTimeoutException Mutation.PartitionUpdateCollector resultBuilder = new Mutation.PartitionUpdateCollector(getKeyspaceName(), key()); Keyspace keyspace = Keyspace.open(getKeyspaceName()); - List locks = new ArrayList<>(); + List lockHandles = new ArrayList<>(); Tracing.trace("Acquiring counter locks"); + + long clock = FBUtilities.timestampMicros(); + CounterId counterId = CounterId.getLocalId(); + try { - grabCounterLocks(keyspace, locks); + grabCounterLocks(keyspace, lockHandles); for (PartitionUpdate upd : getPartitionUpdates()) - resultBuilder.add(processModifications(upd)); + resultBuilder.add(processModifications(upd, clock, counterId)); Mutation result = resultBuilder.build(); result.apply(); @@ -145,35 +214,94 @@ public Mutation applyCounterMutation() throws WriteTimeoutException } finally { - for (Lock lock : locks) - lock.unlock(); + // iterate over all locks in reverse order and unlock them + for (int i = lockHandles.size() - 1; i >= 0; i--) + lockHandles.get(i).release(); } } + /** + * Applies the counter mutation with the provided time and {@link CounterId}. As opposed to + * {@link #applyCounterMutation()} this method doesn't acquire cell-level locks. + *

+ * This method is used in CDC counter write path (CNDB). + *

+ * The time and counter values are evaluated and propagated to all replicas by CDC Service. The replicas + * use this method to apply the mutation locally without locks. The locks are not needed in the CDC + * path as all the writes to the same partition are serialized by CDC Service. + */ + public Future applyCounterMutationWithoutLocks(long systemClockMicros, CounterId counterId) + { + Mutation.PartitionUpdateCollector resultBuilder = new Mutation.PartitionUpdateCollector(getKeyspaceName(), key()); + for (PartitionUpdate upd : getPartitionUpdates()) + resultBuilder.add(processModifications(upd, systemClockMicros, counterId)); + + Mutation mutatation = resultBuilder.build(); + return mutatation.applyFuture(WriteOptions.DEFAULT).map(o -> mutatation); + } + public void apply() { applyCounterMutation(); } - private void grabCounterLocks(Keyspace keyspace, List locks) throws WriteTimeoutException + private int countDistinctLocks(Iterable sortedLocks) { + CounterLockManager.LockHandle prev = null; + int counter = 0; + for(CounterLockManager.LockHandle l: sortedLocks) + { + if (prev != l) + counter++; + prev = l; + } + return counter; + } + + @VisibleForTesting + public void grabCounterLocks(Keyspace keyspace, List lockHandles) throws WriteTimeoutException + { + assert lockHandles.isEmpty(); long startTime = nanoTime(); AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - for (Lock lock : LOCKS.bulkGet(getCounterLockKeys())) + List sortedLockHandles = CounterLockManager.instance.grabLocks(getCounterLockKeys()); + // always return all the locks to the caller, this way they can be released even in case of errors + lockHandles.addAll(sortedLockHandles); + locksPerUpdate.update(countDistinctLocks(sortedLockHandles)); + try { - long timeout = getTimeout(NANOSECONDS) - (nanoTime() - startTime); - try - { - if (!lock.tryLock(timeout, NANOSECONDS)) - throw new WriteTimeoutException(WriteType.COUNTER, consistency(), 0, consistency().blockFor(replicationStrategy)); - locks.add(lock); - } - catch (InterruptedException e) + for (CounterLockManager.LockHandle lockHandle : sortedLockHandles) { - throw new WriteTimeoutException(WriteType.COUNTER, consistency(), 0, consistency().blockFor(replicationStrategy)); + long timeout = getTimeout(NANOSECONDS) - (nanoTime() - startTime); + try + { + if (!lockHandle.tryLock(timeout, NANOSECONDS)) + handleLockTimeoutAndThrow(replicationStrategy); + + } + catch (InterruptedException e) + { + handleLockTimeoutAndThrow(replicationStrategy); + } } } + finally + { + lockAcquireTime.addNano(Clock.Global.nanoTime() - startTime); + } + } + + private void handleLockTimeoutAndThrow(AbstractReplicationStrategy replicationStrategy) + { + lockTimeout.inc(); + + nospamLogger.error(LOCK_TIMEOUT_MESSAGE, + getKeyspaceName(), + DatabaseDescriptor.getCounterWriteRpcTimeout(MILLISECONDS)); + Tracing.trace(LOCK_TIMEOUT_TRACE, DatabaseDescriptor.getCounterWriteRpcTimeout(MILLISECONDS)); + + throw new WriteTimeoutException(WriteType.COUNTER, consistency(), 0, consistency().blockFor(replicationStrategy)); } /** @@ -181,19 +309,19 @@ private void grabCounterLocks(Keyspace keyspace, List locks) throws WriteT * Striped#bulkGet() depends on Object#hashCode(), so here we make sure that the cf id and the partition key * all get to be part of the hashCode() calculation. */ - private Iterable getCounterLockKeys() + private Iterable getCounterLockKeys() { - return Iterables.concat(Iterables.transform(getPartitionUpdates(), new Function>() + return Iterables.concat(Iterables.transform(getPartitionUpdates(), new Function>() { - public Iterable apply(final PartitionUpdate update) + public Iterable apply(final PartitionUpdate update) { - return Iterables.concat(Iterables.transform(update, new Function>() + return Iterables.concat(Iterables.transform(update.rows(), new Function>() { - public Iterable apply(final Row row) + public Iterable apply(final Row row) { - return Iterables.concat(Iterables.transform(row, new Function() + return Iterables.concat(Iterables.transform(row, new Function() { - public Object apply(final ColumnData data) + public Integer apply(final ColumnData data) { return Objects.hashCode(update.metadata().id, key(), row.clustering(), data.column()); } @@ -204,64 +332,84 @@ public Object apply(final ColumnData data) })); } - private PartitionUpdate processModifications(PartitionUpdate changes) + private PartitionUpdate processModifications(PartitionUpdate changes, + long systemClockMicros, + CounterId counterId) { ColumnFamilyStore cfs = Keyspace.open(getKeyspaceName()).getColumnFamilyStore(changes.metadata().id); - List marks = changes.collectCounterMarks(); + List> marks = changes.collectCounterMarks().stream() + .map(mark -> Pair.create(mark, cacheKeyForMark(cfs, mark))) + .collect(Collectors.toList()); if (CacheService.instance.counterCache.getCapacity() != 0) { Tracing.trace("Fetching {} counter values from cache", marks.size()); - updateWithCurrentValuesFromCache(marks, cfs); + updateWithCurrentValuesFromCache(marks, cfs, systemClockMicros, counterId); if (marks.isEmpty()) return changes; } Tracing.trace("Reading {} counter values from the CF", marks.size()); - updateWithCurrentValuesFromCFS(marks, cfs); + updateWithCurrentValuesFromCFS(marks, cfs, systemClockMicros, counterId); // What's remain is new counters - for (PartitionUpdate.CounterMark mark : marks) - updateWithCurrentValue(mark, ClockAndCount.BLANK, cfs); + for (Pair mark : marks) + updateWithCurrentValue(mark, ClockAndCount.BLANK, cfs, systemClockMicros, counterId); return changes; } - private void updateWithCurrentValue(PartitionUpdate.CounterMark mark, ClockAndCount currentValue, ColumnFamilyStore cfs) + private CounterCacheKey cacheKeyForMark(ColumnFamilyStore cfs, PartitionUpdate.CounterMark mark) + { + return CounterCacheKey.create(cfs.metadata(), key().getKey(), mark.clustering(), mark.column(), mark.path()); + } + + private void updateWithCurrentValue(Pair mark, + ClockAndCount currentValue, + ColumnFamilyStore cfs, + long systemClockMicros, + CounterId counterId) { - long clock = Math.max(FBUtilities.timestampMicros(), currentValue.clock + 1L); - long count = currentValue.count + CounterContext.instance().total(mark.value(), ByteBufferAccessor.instance); + long clock = Math.max(systemClockMicros, currentValue.clock + 1L); + long count = currentValue.count + CounterContext.instance().total(mark.left.value(), ByteBufferAccessor.instance); - mark.setValue(CounterContext.instance().createGlobal(CounterId.getLocalId(), clock, count)); + mark.left.setValue(CounterContext.instance().createGlobal(counterId, clock, count)); // Cache the newly updated value - cfs.putCachedCounter(key().getKey(), mark.clustering(), mark.column(), mark.path(), ClockAndCount.create(clock, count)); + cfs.putCachedCounter(mark.right, ClockAndCount.create(clock, count)); } // Returns the count of cache misses. - private void updateWithCurrentValuesFromCache(List marks, ColumnFamilyStore cfs) + private void updateWithCurrentValuesFromCache(List> marks, + ColumnFamilyStore cfs, + long systemClockMicros, + CounterId counterId) { - Iterator iter = marks.iterator(); + Iterator> iter = marks.iterator(); while (iter.hasNext()) { - PartitionUpdate.CounterMark mark = iter.next(); - ClockAndCount cached = cfs.getCachedCounter(key().getKey(), mark.clustering(), mark.column(), mark.path()); + Pair mark = iter.next(); + ClockAndCount cached = cfs.getCachedCounter(mark.right); if (cached != null) { - updateWithCurrentValue(mark, cached, cfs); + updateWithCurrentValue(mark, cached, cfs, systemClockMicros, counterId); iter.remove(); } } } // Reads the missing current values from the CFS. - private void updateWithCurrentValuesFromCFS(List marks, ColumnFamilyStore cfs) + private void updateWithCurrentValuesFromCFS(List> marks, + ColumnFamilyStore cfs, + long systemClockMicros, + CounterId counterId) { ColumnFilter.Builder builder = ColumnFilter.selectionBuilder(); BTreeSet.Builder> names = BTreeSet.builder(cfs.metadata().comparator); - for (PartitionUpdate.CounterMark mark : marks) + for (Pair markAndKey : marks) { + PartitionUpdate.CounterMark mark = markAndKey.left; if (mark.clustering() != Clustering.STATIC_CLUSTERING) names.add(mark.clustering()); if (mark.path() == null) @@ -273,18 +421,18 @@ private void updateWithCurrentValuesFromCFS(List ma long nowInSec = FBUtilities.nowInSeconds(); ClusteringIndexNamesFilter filter = new ClusteringIndexNamesFilter(names.build(), false); SinglePartitionReadCommand cmd = SinglePartitionReadCommand.create(cfs.metadata(), nowInSec, key(), builder.build(), filter); - PeekingIterator markIter = Iterators.peekingIterator(marks.iterator()); + PeekingIterator> markIter = Iterators.peekingIterator(marks.iterator()); try (ReadExecutionController controller = cmd.executionController(); RowIterator partition = UnfilteredRowIterators.filter(cmd.queryMemtableAndDisk(cfs, controller), nowInSec)) { - updateForRow(markIter, partition.staticRow(), cfs); + updateForRow(markIter, partition.staticRow(), cfs, systemClockMicros, counterId); while (partition.hasNext()) { if (!markIter.hasNext()) return; - updateForRow(markIter, partition.next(), cfs); + updateForRow(markIter, partition.next(), cfs, systemClockMicros, counterId); } } } @@ -299,11 +447,15 @@ private int compare(Clustering c1, Clustering c2, ColumnFamilyStore cfs) return cfs.getComparator().compare(c1, c2); } - private void updateForRow(PeekingIterator markIter, Row row, ColumnFamilyStore cfs) + private void updateForRow(PeekingIterator> markIter, + Row row, + ColumnFamilyStore cfs, + long systemClockMicros, + CounterId counterId) { int cmp = 0; // If the mark is before the row, we have no value for this mark, just consume it - while (markIter.hasNext() && (cmp = compare(markIter.peek().clustering(), row.clustering(), cfs)) < 0) + while (markIter.hasNext() && (cmp = compare(markIter.peek().left().clustering(), row.clustering(), cfs)) < 0) markIter.next(); if (!markIter.hasNext()) @@ -311,17 +463,19 @@ private void updateForRow(PeekingIterator markIter, while (cmp == 0) { - PartitionUpdate.CounterMark mark = markIter.next(); + Pair markAndKey = markIter.next(); + PartitionUpdate.CounterMark mark = markAndKey.left; Cell cell = mark.path() == null ? row.getCell(mark.column()) : row.getCell(mark.column(), mark.path()); if (cell != null) { - updateWithCurrentValue(mark, CounterContext.instance().getLocalClockAndCount(cell.buffer()), cfs); + ClockAndCount localClockAndCount = CounterContext.instance().getLocalClockAndCount(cell.buffer()); + updateWithCurrentValue(markAndKey, localClockAndCount, cfs, systemClockMicros, counterId); markIter.remove(); } if (!markIter.hasNext()) return; - cmp = compare(markIter.peek().clustering(), row.clustering(), cfs); + cmp = compare(markIter.peek().left().clustering(), row.clustering(), cfs); } } @@ -332,6 +486,11 @@ public long getTimeout(TimeUnit unit) private int serializedSize40; private int serializedSize50; + private int serializedSizeDS10; + private int serializedSizeDS11; + private int serializedSizeDS12; + private int serializedSizeDS20; + private int serializedSizeDSE68; public int serializedSize(int version) { @@ -345,6 +504,26 @@ public int serializedSize(int version) if (serializedSize50 == 0) serializedSize50 = (int) serializer.serializedSize(this, VERSION_50); return serializedSize50; + case VERSION_DS_10: + if (serializedSizeDS10 == 0) + serializedSizeDS10 = (int) serializer.serializedSize(this, VERSION_DS_10); + return serializedSizeDS10; + case VERSION_DS_11: + if (serializedSizeDS11 == 0) + serializedSizeDS11 = (int) serializer.serializedSize(this, VERSION_DS_11); + return serializedSizeDS11; + case VERSION_DS_12: + if (serializedSizeDS12 == 0) + serializedSizeDS12 = (int) serializer.serializedSize(this, VERSION_DS_12); + return serializedSizeDS12; + case VERSION_DS_20: + if (serializedSizeDS20 == 0) + serializedSizeDS20 = (int) serializer.serializedSize(this, VERSION_DS_20); + return serializedSizeDS20; + case VERSION_DSE_68: + if (serializedSizeDSE68 == 0) + serializedSizeDSE68 = (int) serializer.serializedSize(this, VERSION_DSE_68); + return serializedSizeDSE68; default: throw new IllegalStateException("Unknown serialization version: " + version); } diff --git a/src/java/org/apache/cassandra/db/CounterMutationCallback.java b/src/java/org/apache/cassandra/db/CounterMutationCallback.java new file mode 100644 index 000000000000..648997637b6f --- /dev/null +++ b/src/java/org/apache/cassandra/db/CounterMutationCallback.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.SensorsCustomParams; + +/** + * A counter mutation callback that encapsulates {@link RequestSensors} and replica count + */ +public class CounterMutationCallback implements Runnable +{ + private final Message requestMessage; + private final InetAddressAndPort respondToAddress; + private final RequestSensors sensors; + private int replicaCount = 0; + + public CounterMutationCallback(Message requestMessage, InetAddressAndPort respondToAddress, RequestSensors sensors) + { + this.requestMessage = requestMessage; + this.respondToAddress = respondToAddress; + this.sensors = sensors; + } + + /** + * Sets replica count including the local one. + */ + public void setReplicaCount(Integer replicaCount) + { + this.replicaCount = replicaCount; + } + + @Override + public void run() + { + Message.Builder responseBuilder = requestMessage.emptyResponseBuilder(); + int replicaMultiplier = replicaCount == 0 ? + 1 : // replica count was not explicitly set (default). At the bare minimum, we should send the response accomodating for the local replica (aka. mutation leader) sensor values + replicaCount; + SensorsCustomParams.addSensorsToInternodeResponse(sensors, s -> s.getValue() * replicaMultiplier, responseBuilder); + MessagingService.instance().send(responseBuilder.build(), respondToAddress); + } +} diff --git a/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java b/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java index 3c38497150e5..fcfa31351fee 100644 --- a/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java +++ b/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java @@ -17,13 +17,23 @@ */ package org.apache.cassandra.db; +import java.util.Collection; +import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.transport.Dispatcher; @@ -39,6 +49,19 @@ protected void applyMutation(final Message message, InetAddress final CounterMutation cm = message.payload; logger.trace("Applying forwarded {}", cm); + // Initialize the sensor and set ExecutorLocals + RequestSensors requestSensors = SensorsFactory.instance.createRequestSensors(message.payload.getKeyspaceName()); + Collection tables = message.payload.getPartitionUpdates().stream().map(PartitionUpdate::metadata).collect(Collectors.toSet()); + RequestTracker.instance.set(requestSensors); + + // Initialize internode bytes with the inbound message size: + for (TableMetadata tm : tables) + { + Context context = Context.from(tm); + requestSensors.registerSensor(context, Type.INTERNODE_BYTES); + requestSensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version) / tables.size()); + } + String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); // We should not wait for the result of the write in this thread, // otherwise we could have a distributed deadlock between replicas @@ -49,7 +72,7 @@ protected void applyMutation(final Message message, InetAddress // it's own in that case. StorageProxy.applyCounterMutationOnLeader(cm, localDataCenter, - () -> MessagingService.instance().send(message.emptyResponse(), respondToAddress), + new CounterMutationCallback(message, message.from(), requestSensors), Dispatcher.RequestTime.forImmediateExecution()); } } diff --git a/src/java/org/apache/cassandra/db/DataRange.java b/src/java/org/apache/cassandra/db/DataRange.java index 9912ac56e919..e1ff2cc0710f 100644 --- a/src/java/org/apache/cassandra/db/DataRange.java +++ b/src/java/org/apache/cassandra/db/DataRange.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.ByteBuffer; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.filter.*; @@ -281,56 +282,79 @@ public DataRange forSubRange(AbstractBounds range) return new DataRange(range, clusteringIndexFilter); } + /** + * Whether this range queries a single partition. That happens for partition queries using secondary indexes, which + * are internally mapped to range commands using a single-key data range. + * + * @return {@code true} if this range queries a single partition, {@code false} otherwise + */ + public boolean isSinglePartition() + { + return keyRange.inclusiveLeft() && + keyRange.inclusiveRight() && + keyRange.left instanceof DecoratedKey && + keyRange.right instanceof DecoratedKey && + keyRange.left.equals(keyRange.right); + } + public String toString(TableMetadata metadata) { return String.format("range=%s pfilter=%s", keyRange.getString(metadata.partitionKeyType), clusteringIndexFilter.toString(metadata)); } - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction) { if (isUnrestricted(metadata)) - return rowFilter.toCQLString(); - - StringBuilder sb = new StringBuilder(); + return rowFilter.toCQLString(redaction); - boolean needAnd = false; - if (!startKey().isMinimum()) + if (isSinglePartition()) { - appendClause(startKey(), sb, metadata, true, keyRange.isStartInclusive()); - needAnd = true; + /* + * Single partition queries using an index are internally mapped to range commands where the start and end + * key are the same. If that is the case, we want to print the query as an equality on the partition key + * rather than a token range, as if it was a partition query, for better readability. + */ + return ((DecoratedKey) startKey()).toCQLString(metadata, redaction); } - if (!stopKey().isMinimum()) + else { - if (needAnd) - sb.append(" AND "); - appendClause(stopKey(), sb, metadata, false, keyRange.isEndInclusive()); - needAnd = true; + StringBuilder builder = new StringBuilder(); + if (!startKey().isMinimum()) + { + appendCQLClause(startKey(), builder, metadata, true, keyRange.isStartInclusive(), redaction); + } + if (!stopKey().isMinimum()) + { + if (builder.length() > 0) + builder.append(" AND "); + appendCQLClause(stopKey(), builder, metadata, false, keyRange.isEndInclusive(), redaction); + } + return builder.toString(); } - - String filterString = clusteringIndexFilter.toCQLString(metadata, rowFilter); - if (!filterString.isEmpty()) - sb.append(needAnd ? " AND " : "").append(filterString); - - return sb.toString(); } - private void appendClause(PartitionPosition pos, StringBuilder sb, TableMetadata metadata, boolean isStart, boolean isInclusive) + private void appendCQLClause(PartitionPosition pos, + StringBuilder builder, + TableMetadata metadata, + boolean isStart, + boolean isInclusive, + Redaction redaction) { - sb.append("token("); - sb.append(ColumnMetadata.toCQLString(metadata.partitionKeyColumns())); - sb.append(") "); + builder.append("token("); + builder.append(ColumnMetadata.toCQLString(metadata.partitionKeyColumns())); + builder.append(") "); if (pos instanceof DecoratedKey) { - sb.append(getOperator(isStart, isInclusive)).append(" "); - sb.append("token("); - appendKeyString(sb, metadata.partitionKeyType, ((DecoratedKey)pos).getKey()); - sb.append(")"); + builder.append(getOperator(isStart, isInclusive)).append(" "); + builder.append("token("); + appendKeyString(builder, metadata.partitionKeyType, ((DecoratedKey)pos).getKey(), redaction); + builder.append(')'); } else { Token.KeyBound keyBound = (Token.KeyBound) pos; - sb.append(getOperator(isStart, isStart == keyBound.isMinimumBound)).append(" "); - sb.append(keyBound.getToken()); + builder.append(getOperator(isStart, isStart == keyBound.isMinimumBound)).append(' '); + builder.append(redaction == Redaction.REDACT ? "?" : keyBound.getToken()); } } @@ -341,18 +365,20 @@ private static String getOperator(boolean isStart, boolean isInclusive) : (isInclusive ? "<=" : "<"); } - public static void appendKeyString(StringBuilder sb, AbstractType type, ByteBuffer key) + // TODO: this is reused in SinglePartitionReadCommand but this should not really be here. Ideally + // we need a more "native" handling of composite partition keys. + public static void appendKeyString(StringBuilder builder, AbstractType type, ByteBuffer key, Redaction redaction) { if (type instanceof CompositeType) { CompositeType ct = (CompositeType)type; ByteBuffer[] values = ct.split(key); - for (int i = 0; i < ct.types.size(); i++) - sb.append(i == 0 ? "" : ", ").append(ct.types.get(i).toCQLString(values[i])); + for (int i = 0; i < ct.subTypes().size(); i++) + builder.append(i == 0 ? "" : ", ").append(ct.subTypes().get(i).toCQLString(values[i], redaction)); } else { - sb.append(type.toCQLString(key)); + builder.append(type.toCQLString(key, redaction)); } } diff --git a/src/java/org/apache/cassandra/db/DecoratedKey.java b/src/java/org/apache/cassandra/db/DecoratedKey.java index 03d6374112a4..8b8ef04e5e51 100644 --- a/src/java/org/apache/cassandra/db/DecoratedKey.java +++ b/src/java/org/apache/cassandra/db/DecoratedKey.java @@ -24,6 +24,7 @@ import java.util.function.BiFunction; import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.dht.Token.KeyBound; @@ -76,6 +77,7 @@ public boolean equals(Object obj) return ByteBufferUtil.compareUnsigned(getKey(), other.getKey()) == 0; // we compare faster than BB.equals for array backed BB } + @Override public int compareTo(PartitionPosition pos) { if (this == pos) @@ -106,7 +108,7 @@ public ByteSource asComparableBytes(Version version) { // Note: In the legacy version one encoding could be a prefix of another as the escaping is only weakly // prefix-free (see ByteSourceTest.testDecoratedKeyPrefixes()). - // The OSS50 version avoids this by adding a terminator. + // The OSS41 and 50 versions avoids this by adding a terminator. return ByteSource.withTerminatorMaybeLegacy(version, ByteSource.END_OF_STREAM, token.asComparableBytes(version), @@ -163,28 +165,29 @@ public String toString() /** * Returns a CQL representation of this key. * - * @param metadata the metadata of the table that this key belogs to + * @param metadata the table metadata + * @param redaction whether to redact the key value, as in "k1 = ? AND k2 = ?". * @return a CQL representation of this key */ - public String toCQLString(TableMetadata metadata) + public String toCQLString(TableMetadata metadata, Redaction redaction) { List columns = metadata.partitionKeyColumns(); if (columns.size() == 1) - return toCQLString(columns.get(0), getKey()); + return toCQLString(columns.get(0), getKey(), redaction); ByteBuffer[] values = ((CompositeType) metadata.partitionKeyType).split(getKey()); StringJoiner joiner = new StringJoiner(" AND "); for (int i = 0; i < columns.size(); i++) - joiner.add(toCQLString(columns.get(i), values[i])); + joiner.add(toCQLString(columns.get(i), values[i], redaction)); return joiner.toString(); } - private static String toCQLString(ColumnMetadata metadata, ByteBuffer key) + private static String toCQLString(ColumnMetadata metadata, ByteBuffer key, Redaction redaction) { - return String.format("%s = %s", metadata.name.toCQLString(), metadata.type.toCQLString(key)); + return String.format("%s = %s", metadata.name.toCQLString(), metadata.type.toCQLString(key, redaction)); } public Token getToken() @@ -232,6 +235,14 @@ static T fromByteComparable(ByteComparable byteComparab return decoratedKeyFactory.apply(token, keyBytes); } + public static byte[] keyFromByteComparable(ByteComparable byteComparable, + Version version, + IPartitioner partitioner) + { + return keyFromByteSource(ByteSource.peekable(byteComparable.asComparableBytes(version)), version, partitioner); + } + + public static byte[] keyFromByteSource(ByteSource.Peekable peekableByteSource, Version version, IPartitioner partitioner) diff --git a/src/java/org/apache/cassandra/db/DeletionInfo.java b/src/java/org/apache/cassandra/db/DeletionInfo.java index bbc4eee95056..ecdea45cd4d5 100644 --- a/src/java/org/apache/cassandra/db/DeletionInfo.java +++ b/src/java/org/apache/cassandra/db/DeletionInfo.java @@ -18,6 +18,7 @@ package org.apache.cassandra.db; import java.util.Iterator; +import java.util.SortedSet; import org.apache.cassandra.cache.IMeasurableMemory; import org.apache.cassandra.db.rows.EncodingStats; @@ -52,6 +53,8 @@ public interface DeletionInfo extends IMeasurableMemory public Iterator rangeIterator(Slice slice, boolean reversed); + public Iterator rangeIterator(SortedSet> names, boolean isRevered); + public RangeTombstone rangeCovering(Clustering name); public void collectStats(EncodingStats.Collector collector); @@ -72,4 +75,6 @@ public interface DeletionInfo extends IMeasurableMemory public MutableDeletionInfo mutableCopy(); public DeletionInfo clone(ByteBufferCloner cloner); + + public RangeTombstoneList copyRanges(ByteBufferCloner cloner); } diff --git a/src/java/org/apache/cassandra/db/DeletionTime.java b/src/java/org/apache/cassandra/db/DeletionTime.java index 5970fbb042a4..5c8153886604 100644 --- a/src/java/org/apache/cassandra/db/DeletionTime.java +++ b/src/java/org/apache/cassandra/db/DeletionTime.java @@ -45,7 +45,7 @@ public class DeletionTime implements Comparable, IMeasurableMemory */ public static final DeletionTime LIVE = new DeletionTime(Long.MIN_VALUE, Long.MAX_VALUE); - private static final Serializer serializer = new Serializer(); + public static final Serializer serializer = new Serializer(); private static final Serializer legacySerializer = new LegacySerializer(); private final long markedForDeleteAt; @@ -143,7 +143,7 @@ public final int hashCode() @Override public String toString() { - return String.format("deletedAt=%d, localDeletion=%d", markedForDeleteAt(), localDeletionTime()); + return this == LIVE ? "LIVE" : String.format("deletedAt=%d, localDeletion=%d", markedForDeleteAt(), localDeletionTime()); } public int compareTo(DeletionTime dt) @@ -160,6 +160,11 @@ public boolean supersedes(DeletionTime dt) return markedForDeleteAt() > dt.markedForDeleteAt() || (markedForDeleteAt() == dt.markedForDeleteAt() && localDeletionTime() > dt.localDeletionTime()); } + public static DeletionTime merge(DeletionTime d1, DeletionTime d2) + { + return d2.supersedes(d1) ? d2 : d1; + } + public boolean deletes(LivenessInfo info) { return deletes(info.timestamp()); diff --git a/src/java/org/apache/cassandra/db/Directories.java b/src/java/org/apache/cassandra/db/Directories.java index 1bcb8eefe946..fe47fdf7fc3f 100644 --- a/src/java/org/apache/cassandra/db/Directories.java +++ b/src/java/org/apache/cassandra/db/Directories.java @@ -45,8 +45,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; @@ -67,10 +69,12 @@ import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.SSTableId; import org.apache.cassandra.io.sstable.SSTableIdFactory; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileStoreUtils; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.snapshot.SnapshotManifest; @@ -133,29 +137,28 @@ public class Directories * the details if it does not. * * @param dir File object of the directory. - * @param dataDir String representation of the directory's location * @return status representing Cassandra's RWX permissions to the supplied folder location. */ - public static boolean verifyFullPermissions(File dir, String dataDir) + public static boolean verifyFullPermissions(File dir) { if (!dir.isDirectory()) { - logger.error("Not a directory {}", dataDir); + logger.error("Not a directory {}", dir); return false; } else if (!FileAction.hasPrivilege(dir, FileAction.X)) { - logger.error("Doesn't have execute permissions for {} directory", dataDir); + logger.error("Doesn't have execute permissions for {} directory", dir); return false; } else if (!FileAction.hasPrivilege(dir, FileAction.R)) { - logger.error("Doesn't have read permissions for {} directory", dataDir); + logger.error("Doesn't have read permissions for {} directory", dir); return false; } else if (dir.exists() && !FileAction.hasPrivilege(dir, FileAction.W)) { - logger.error("Doesn't have write permissions for {} directory", dataDir); + logger.error("Doesn't have write permissions for {} directory", dir); return false; } @@ -212,9 +215,19 @@ public Directories(final TableMetadata metadata) this(metadata, dataDirectories.getDataDirectoriesFor(metadata)); } + public Directories(final KeyspaceMetadata ksMetadata, final TableMetadata metadata) + { + this(ksMetadata, metadata, dataDirectories.getDataDirectoriesFor(metadata)); + } + public Directories(final TableMetadata metadata, Collection paths) { - this(metadata, paths.toArray(new DataDirectory[paths.size()])); + this(null, metadata, paths.toArray(new DataDirectory[paths.size()])); + } + + public Directories(final TableMetadata metadata, DataDirectory[] paths) + { + this(null, metadata, paths); } /** @@ -223,10 +236,11 @@ public Directories(final TableMetadata metadata, Collection paths * * @param metadata metadata of ColumnFamily */ - public Directories(final TableMetadata metadata, DataDirectory[] paths) + public Directories(@Nullable KeyspaceMetadata ksMetadata, final TableMetadata metadata, DataDirectory[] dirs) { this.metadata = metadata; - this.paths = paths; + this.paths = StorageProvider.instance.createDataDirectories(ksMetadata, metadata, dirs); + ImmutableMap.Builder canonicalPathsBuilder = ImmutableMap.builder(); String indexNameWithDot = metadata.getIndexNameWithDot(); @@ -295,13 +309,33 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths) { File destFile = new File(dataPath, indexFile.name()); logger.trace("Moving index file {} to {}", indexFile, destFile); - FileUtils.renameWithConfirm(indexFile, destFile); + indexFile.move(destFile); } } } canonicalPathToDD = canonicalPathsBuilder.build(); } + /** + * A special constructor used to mock SSTables for CNDB tests. + * + * This constructor fixes the data path and path to whichever directory is passed in. No other manipulations + * to the data paths are performed, unlike in the other constructors. The directory should therefore already + * contain information related to the keyspace and table, whether it is local or remote. + */ + @VisibleForTesting + public Directories(final TableMetadata metadata, Path directory) + { + ImmutableMap.Builder canonicalPathsBuilder = ImmutableMap.builder(); + + this.metadata = metadata; + this.paths = new DataDirectory[] { new DataDirectory(directory) }; + this.dataPaths = new File[] { paths[0].location }; + + canonicalPathsBuilder.put(dataPaths[0].toCanonical().toPath(), paths[0]); + this.canonicalPathToDD = canonicalPathsBuilder.build(); + } + /** * Returns SSTable location which is inside given data directory. * @@ -329,6 +363,12 @@ public DataDirectory getDataDirectoryForFile(Descriptor descriptor) return null; } + /** + * This method looks for the file name passed in and resolves it into a descriptor + * if the file exists. + * + * @return a descriptor for the file name passed in + */ public Descriptor find(String filename) { for (File dir : dataPaths) @@ -340,6 +380,19 @@ public Descriptor find(String filename) return null; } + /** + * This method resolves the filename against the specified directory number, whether + * the file exists or not. + * + * @return a descriptor for the passed in filename + */ + public Descriptor resolve(String filename, int dirNumber) + { + Preconditions.checkArgument(dirNumber < dataPaths.length, "Invalid dir number: " + dirNumber); + File dir = dataPaths[dirNumber]; + return Descriptor.fromFileWithComponent(dir, filename, true).left; + } + /** * Basically the same as calling {@link #getWriteableLocationAsFile(long)} with an unknown size ({@code -1L}), * which may return any allowed directory - even a data directory that has no usable space. @@ -789,6 +842,20 @@ public long getRawSize() return FileUtils.folderSize(location); } + // Used by CNDB + @VisibleForTesting + public long getTotalSpace() + { + return PathUtils.tryGetSpace(location.toPath(), FileStore::getTotalSpace); + } + + // Used by CNDB + @VisibleForTesting + public long getSpaceUsed() + { + return getTotalSpace() - getAvailableSpace(); + } + @Override public boolean equals(Object o) { @@ -830,17 +897,17 @@ public static final class DataDirectories implements Iterable private final DataDirectory[] nonLocalSystemKeyspacesDirectories; - public DataDirectories(String[] locationsForNonSystemKeyspaces, String[] locationsForSystemKeyspace) + public DataDirectories(File[] locationsForNonSystemKeyspaces, File[] locationsForSystemKeyspace) { nonLocalSystemKeyspacesDirectories = toDataDirectories(locationsForNonSystemKeyspaces); localSystemKeyspaceDataDirectories = toDataDirectories(locationsForSystemKeyspace); } - private static DataDirectory[] toDataDirectories(String... locations) + private static DataDirectory[] toDataDirectories(File... locations) { DataDirectory[] directories = new DataDirectory[locations.length]; for (int i = 0; i < locations.length; ++i) - directories[i] = new DataDirectory(new File(locations[i])); + directories[i] = new DataDirectory(locations[i]); return directories; } @@ -951,7 +1018,10 @@ public enum FileType TEMPORARY, /** A transaction log file (contains information on final and temporary files). */ - TXN_LOG; + TXN_LOG, + + /** An sstable file that was marked for deletion */ + OBSOLETE; } /** @@ -1119,6 +1189,7 @@ private BiPredicate getFilter(boolean includeForeignTables) switch (type) { case TXN_LOG: + case OBSOLETE: return false; case TEMPORARY: if (skipTemporary) @@ -1160,7 +1231,7 @@ private BiPredicate getFilter(boolean includeForeignTables) return false; default: - throw new AssertionError(); + throw new AssertionError("unexpected file type: " + type + " for file " + file); } }; } @@ -1386,6 +1457,18 @@ public List getCFDirectories() return result; } + /** + * Returns all data paths without checking if they are directories. + * This is useful in contexts where the isDirectory() check could cause + * deadlocks or unnecessary blocking I/O, such as with remote storage paths. + * + * This method is needed by CNDB, please do not remove. + */ + public List getCFDirectoriesUnchecked() + { + return Arrays.asList(dataPaths); + } + /** * Initializes the sstable unique identifier generator using a provided builder for this instance of directories. * If the id builder needs that, sstables in these directories are listed to provide the existing identifiers to diff --git a/src/java/org/apache/cassandra/db/DisallowedDirectories.java b/src/java/org/apache/cassandra/db/DisallowedDirectories.java index e666bad78599..2f0e354a0c70 100644 --- a/src/java/org/apache/cassandra/db/DisallowedDirectories.java +++ b/src/java/org/apache/cassandra/db/DisallowedDirectories.java @@ -27,6 +27,7 @@ import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.util.File; import org.apache.cassandra.utils.MBeanWrapper; @@ -84,6 +85,9 @@ public void markUnwritable(String path) */ public static File maybeMarkUnreadable(File path) { + if (!DatabaseDescriptor.supportsBlacklistingDirectory()) + return null; + File directory = getDirectory(path); if (instance.unreadableDirectories.add(directory)) { @@ -102,6 +106,9 @@ public static File maybeMarkUnreadable(File path) */ public static File maybeMarkUnwritable(File path) { + if (!DatabaseDescriptor.supportsBlacklistingDirectory()) + return null; + File directory = getDirectory(path); if (instance.unwritableDirectories.add(directory)) { diff --git a/src/java/org/apache/cassandra/db/DiskBoundaries.java b/src/java/org/apache/cassandra/db/DiskBoundaries.java index 7fe10f4c1336..c19b031366a0 100644 --- a/src/java/org/apache/cassandra/db/DiskBoundaries.java +++ b/src/java/org/apache/cassandra/db/DiskBoundaries.java @@ -21,34 +21,48 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import javax.annotation.Nullable; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.service.StorageService; public class DiskBoundaries { - public final List directories; - public final ImmutableList positions; - final long ringVersion; + @Nullable public final List directories; + /** + * End-inclusive list of boundaries between directories. + * I.e. directories[0] covers [min, positions[0]] + * directories[1] covers (positions[0], positions[1]] + * ... + * directories[last] covers (positions[last-1], positions[last]==max] + */ + @Nullable private final ImmutableList positions; + public final SortedLocalRanges localRanges; final int directoriesVersion; private final ColumnFamilyStore cfs; private volatile boolean isInvalid = false; - public DiskBoundaries(ColumnFamilyStore cfs, Directories.DataDirectory[] directories, int diskVersion) + public DiskBoundaries(ColumnFamilyStore cfs, + @Nullable Directories.DataDirectory[] directories, + SortedLocalRanges localRanges, + int diskVersion) { - this(cfs, directories, null, -1, diskVersion); + this(cfs, directories, null, localRanges, diskVersion); } - @VisibleForTesting - public DiskBoundaries(ColumnFamilyStore cfs, Directories.DataDirectory[] directories, List positions, long ringVersion, int diskVersion) + public DiskBoundaries(ColumnFamilyStore cfs, + @Nullable Directories.DataDirectory[] directories, + @Nullable List positions, + SortedLocalRanges localRanges, + int diskVersion) { this.directories = directories == null ? null : ImmutableList.copyOf(directories); this.positions = positions == null ? null : ImmutableList.copyOf(positions); - this.ringVersion = ringVersion; + this.localRanges = localRanges; this.directoriesVersion = diskVersion; this.cfs = cfs; } @@ -60,17 +74,17 @@ public boolean equals(Object o) DiskBoundaries that = (DiskBoundaries) o; - if (ringVersion != that.ringVersion) return false; - if (directoriesVersion != that.directoriesVersion) return false; - if (!directories.equals(that.directories)) return false; - return positions != null ? positions.equals(that.positions) : that.positions == null; + return Objects.equals(localRanges, that.localRanges) && + directoriesVersion == that.directoriesVersion && + Objects.equals(directories, that.directories) && + Objects.equals(positions, that.positions); } public int hashCode() { int result = directories != null ? directories.hashCode() : 0; result = 31 * result + (positions != null ? positions.hashCode() : 0); - result = 31 * result + (int) (ringVersion ^ (ringVersion >>> 32)); + result = 31 * result + localRanges.hashCode(); result = 31 * result + directoriesVersion; return result; } @@ -80,7 +94,7 @@ public String toString() return "DiskBoundaries{" + "directories=" + directories + ", positions=" + positions + - ", ringVersion=" + ringVersion + + ", localRanges=" + localRanges.toString() + ", directoriesVersion=" + directoriesVersion + '}'; } @@ -92,9 +106,9 @@ public boolean isOutOfDate() { if (isInvalid) return true; + int currentDiskVersion = DisallowedDirectories.getDirectoriesVersion(); - long currentRingVersion = StorageService.instance.getTokenMetadata().getRingVersion(); - return currentDiskVersion != directoriesVersion || (ringVersion != -1 && currentRingVersion != ringVersion); + return currentDiskVersion != directoriesVersion || localRanges.isOutOfDate(); } public void invalidate() @@ -102,16 +116,15 @@ public void invalidate() this.isInvalid = true; } - public int getDiskIndex(SSTableReader sstable) + public int getDiskIndexFromKey(CompactionSSTable sstable) { if (positions == null) { - return getBoundariesFromSSTableDirectory(sstable.descriptor); + return getBoundariesFromSSTableDirectory(sstable.getDescriptor()); } - int pos = Collections.binarySearch(positions, sstable.getFirst()); - assert pos < 0; // boundaries are .minkeybound and .maxkeybound so they should never be equal - return -pos - 1; + int pos = Collections.binarySearch(positions, sstable.getFirst().getToken()); + return pos >= 0 ? pos : -pos - 1; // disk boundaries are end-inclusive } /** @@ -131,7 +144,7 @@ public int getBoundariesFromSSTableDirectory(Descriptor descriptor) public Directories.DataDirectory getCorrectDiskForSSTable(SSTableReader sstable) { - return directories.get(getDiskIndex(sstable)); + return directories.get(getDiskIndexFromKey(sstable)); } public Directories.DataDirectory getCorrectDiskForKey(DecoratedKey key) @@ -139,29 +152,40 @@ public Directories.DataDirectory getCorrectDiskForKey(DecoratedKey key) if (positions == null) return null; - return directories.get(getDiskIndex(key)); + return directories.get(getDiskIndexFromKey(key)); } public boolean isInCorrectLocation(SSTableReader sstable, Directories.DataDirectory currentLocation) { - int diskIndex = getDiskIndex(sstable); - PartitionPosition diskLast = positions.get(diskIndex); - return directories.get(diskIndex).equals(currentLocation) && sstable.getLast().compareTo(diskLast) <= 0; + int diskIndex = getDiskIndexFromKey(sstable); + Token diskLast = positions.get(diskIndex); + return directories.get(diskIndex).equals(currentLocation) && sstable.last.getToken().compareTo(diskLast) <= 0; } - private int getDiskIndex(DecoratedKey key) + /** + * Return the number of boundaries. If this instance was created with token boundaries (positions) then this + * is the number of boundaries. If this instance was created without boundaries but only with directories, then + * this is the number of directories. + * + * @return the number of boundaries. + */ + public int getNumBoundaries() { - int pos = Collections.binarySearch(positions, key); - assert pos < 0; - return -pos - 1; + return positions == null ? directories.size() : positions.size(); + } + + private int getDiskIndexFromKey(DecoratedKey key) + { + int pos = Collections.binarySearch(positions, key.getToken()); + return pos >= 0 ? pos : -pos - 1; // disk boundaries are end-inclusive } public List getDisksInBounds(DecoratedKey first, DecoratedKey last) { if (positions == null || first == null || last == null) return directories; - int firstIndex = getDiskIndex(first); - int lastIndex = getDiskIndex(last); + int firstIndex = getDiskIndexFromKey(first); + int lastIndex = getDiskIndexFromKey(last); return directories.subList(firstIndex, lastIndex + 1); } @@ -171,4 +195,27 @@ public boolean isEquivalentTo(DiskBoundaries oldBoundaries) Objects.equals(positions, oldBoundaries.positions) && Objects.equals(directories, oldBoundaries.directories); } + + /** + * Return the local sorted ranges, which contain the local ranges for this node, sorted. + * See {@link SortedLocalRanges}. + * + * @return the local ranges, see {@link SortedLocalRanges}. + */ + public SortedLocalRanges getLocalRanges() + { + return localRanges; + } + + /** + * Returns a non-modifiable list of the disk boundary positions. This will be null if the token space is not split + * for the disks, this is not normally the case). + * + * Extracted as a method (instead of direct access to the final field) to permit mocking in tests. + */ + @Nullable + public List getPositions() + { + return positions; + } } diff --git a/src/java/org/apache/cassandra/db/DiskBoundaryManager.java b/src/java/org/apache/cassandra/db/DiskBoundaryManager.java index 7857d0cff888..2ce5299078e3 100644 --- a/src/java/org/apache/cassandra/db/DiskBoundaryManager.java +++ b/src/java/org/apache/cassandra/db/DiskBoundaryManager.java @@ -18,8 +18,6 @@ package org.apache.cassandra.db; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import org.slf4j.Logger; @@ -27,14 +25,8 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.RangesAtEndpoint; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.PendingRangeCalculatorService; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; public class DiskBoundaryManager { @@ -43,18 +35,21 @@ public class DiskBoundaryManager public DiskBoundaries getDiskBoundaries(ColumnFamilyStore cfs) { - if (!cfs.getPartitioner().splitter().isPresent()) - return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), DisallowedDirectories.getDirectoriesVersion()); if (diskBoundaries == null || diskBoundaries.isOutOfDate()) { synchronized (this) { if (diskBoundaries == null || diskBoundaries.isOutOfDate()) { - logger.debug("Refreshing disk boundary cache for {}.{}", cfs.getKeyspaceName(), cfs.getTableName()); + logger.debug("Refreshing disk boundary cache for {}.{}", cfs.keyspace.getName(), cfs.getTableName()); + SortedLocalRanges localRanges = cfs.getLocalRanges(); + DiskBoundaries oldBoundaries = diskBoundaries; - diskBoundaries = getDiskBoundaryValue(cfs); - logger.debug("Updating boundaries from {} to {} for {}.{}", oldBoundaries, diskBoundaries, cfs.getKeyspaceName(), cfs.getTableName()); + diskBoundaries = !cfs.getPartitioner().splitter().isPresent() + ? new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), localRanges, DisallowedDirectories.getDirectoriesVersion()) + : getDiskBoundaryValue(cfs, localRanges); + + logger.debug("Updating boundaries from {} to {} for {}.{}", oldBoundaries, diskBoundaries, cfs.keyspace.getName(), cfs.getTableName()); } } } @@ -67,43 +62,9 @@ public void invalidate() diskBoundaries.invalidate(); } - static class VersionedRangesAtEndpoint - { - public final RangesAtEndpoint rangesAtEndpoint; - public final long ringVersion; - - VersionedRangesAtEndpoint(RangesAtEndpoint rangesAtEndpoint, long ringVersion) - { - this.rangesAtEndpoint = rangesAtEndpoint; - this.ringVersion = ringVersion; - } - } - - public static VersionedRangesAtEndpoint getVersionedLocalRanges(ColumnFamilyStore cfs) - { - RangesAtEndpoint localRanges; - - long ringVersion; - TokenMetadata tmd; - do - { - tmd = StorageService.instance.getTokenMetadata(); - ringVersion = tmd.getRingVersion(); - localRanges = getLocalRanges(cfs, tmd); - logger.debug("Got local ranges {} (ringVersion = {})", localRanges, ringVersion); - } - while (ringVersion != tmd.getRingVersion()); // if ringVersion is different here it means that - // it might have changed before we calculated localRanges - recalculate - - return new VersionedRangesAtEndpoint(localRanges, ringVersion); - } - private static DiskBoundaries getDiskBoundaryValue(ColumnFamilyStore cfs) + private static DiskBoundaries getDiskBoundaryValue(ColumnFamilyStore cfs, SortedLocalRanges localRanges) { - VersionedRangesAtEndpoint rangesAtEndpoint = getVersionedLocalRanges(cfs); - RangesAtEndpoint localRanges = rangesAtEndpoint.rangesAtEndpoint; - long ringVersion = rangesAtEndpoint.ringVersion; - int directoriesVersion; Directories.DataDirectory[] dirs; do @@ -113,31 +74,11 @@ private static DiskBoundaries getDiskBoundaryValue(ColumnFamilyStore cfs) } while (directoriesVersion != DisallowedDirectories.getDirectoriesVersion()); // if directoriesVersion has changed we need to recalculate - if (localRanges == null || localRanges.isEmpty()) - return new DiskBoundaries(cfs, dirs, null, ringVersion, directoriesVersion); - - List positions = getDiskBoundaries(localRanges, cfs.getPartitioner(), dirs); - - return new DiskBoundaries(cfs, dirs, positions, ringVersion, directoriesVersion); - } + if (localRanges == null || localRanges.getRanges().isEmpty()) + return new DiskBoundaries(cfs, dirs, null, localRanges, directoriesVersion); - private static RangesAtEndpoint getLocalRanges(ColumnFamilyStore cfs, TokenMetadata tmd) - { - RangesAtEndpoint localRanges; - if (StorageService.instance.isBootstrapMode() - && !StorageService.isReplacingSameAddress()) // When replacing same address, the node marks itself as UN locally - { - PendingRangeCalculatorService.instance.blockUntilFinished(); - localRanges = tmd.getPendingRanges(cfs.getKeyspaceName(), FBUtilities.getBroadcastAddressAndPort()); - } - else - { - // Reason we use use the future settled TMD is that if we decommission a node, we want to stream - // from that node to the correct location on disk, if we didn't, we would put new files in the wrong places. - // We do this to minimize the amount of data we need to move in rebalancedisks once everything settled - localRanges = cfs.keyspace.getReplicationStrategy().getAddressReplicas(tmd.cloneAfterAllSettled(), FBUtilities.getBroadcastAddressAndPort()); - } - return localRanges; + List positions = getDiskBoundaries(localRanges.getRanges(), cfs.getPartitioner(), dirs); + return new DiskBoundaries(cfs, dirs, positions, localRanges, directoriesVersion); } /** @@ -149,32 +90,15 @@ private static RangesAtEndpoint getLocalRanges(ColumnFamilyStore cfs, TokenMetad * * The final entry in the returned list will always be the partitioner maximum tokens upper key bound */ - private static List getDiskBoundaries(RangesAtEndpoint replicas, IPartitioner partitioner, Directories.DataDirectory[] dataDirectories) + private static List getDiskBoundaries(List weightedRanges, IPartitioner partitioner, Directories.DataDirectory[] dataDirectories) { assert partitioner.splitter().isPresent(); Splitter splitter = partitioner.splitter().get(); - boolean dontSplitRanges = DatabaseDescriptor.getNumTokens() > 1; - - List weightedRanges = new ArrayList<>(replicas.size()); - // note that Range.sort unwraps any wraparound ranges, so we need to sort them here - for (Range r : Range.sort(replicas.onlyFull().ranges())) - weightedRanges.add(new Splitter.WeightedRange(1.0, r)); - - for (Range r : Range.sort(replicas.onlyTransient().ranges())) - weightedRanges.add(new Splitter.WeightedRange(0.1, r)); + Splitter.SplitType splitType = DatabaseDescriptor.getNumTokens() > 1 ? Splitter.SplitType.PREFER_WHOLE : Splitter.SplitType.ALWAYS_SPLIT; - weightedRanges.sort(Comparator.comparing(Splitter.WeightedRange::left)); - - List boundaries = splitter.splitOwnedRanges(dataDirectories.length, weightedRanges, dontSplitRanges); - // If we can't split by ranges, split evenly to ensure utilisation of all disks - if (dontSplitRanges && boundaries.size() < dataDirectories.length) - boundaries = splitter.splitOwnedRanges(dataDirectories.length, weightedRanges, false); - - List diskBoundaries = new ArrayList<>(); - for (int i = 0; i < boundaries.size() - 1; i++) - diskBoundaries.add(boundaries.get(i).maxKeyBound()); - diskBoundaries.add(partitioner.getMaximumToken().maxKeyBound()); - return diskBoundaries; + List boundaries = splitter.splitOwnedRanges(dataDirectories.length, weightedRanges, splitType).boundaries; + assert boundaries.size() == dataDirectories.length : "Wrong number of boundaries for directories: " + boundaries.size(); + return boundaries; } } diff --git a/src/java/org/apache/cassandra/db/IMutation.java b/src/java/org/apache/cassandra/db/IMutation.java index 1998e2c0353c..c392d9b50f25 100644 --- a/src/java/org/apache/cassandra/db/IMutation.java +++ b/src/java/org/apache/cassandra/db/IMutation.java @@ -38,6 +38,7 @@ public interface IMutation String toString(boolean shallow); Collection getPartitionUpdates(); Supplier hintOnFailure(); + Keyspace getKeyspace(); default void validateIndexedColumns(ClientState state) { diff --git a/src/java/org/apache/cassandra/db/Keyspace.java b/src/java/org/apache/cassandra/db/Keyspace.java index 05b354a74bad..98fdd28e25bd 100644 --- a/src/java/org/apache/cassandra/db/Keyspace.java +++ b/src/java/org/apache/cassandra/db/Keyspace.java @@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; +import java.util.function.Supplier; import java.util.stream.Stream; import com.google.common.annotations.VisibleForTesting; @@ -47,11 +48,14 @@ import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.repair.CassandraKeyspaceRepairManager; import org.apache.cassandra.db.view.ViewManager; +import org.apache.cassandra.exceptions.InternalRequestExecutionException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.exceptions.UnknownKeyspaceException; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.SecondaryIndexManager; -import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.metrics.KeyspaceMetrics; import org.apache.cassandra.repair.KeyspaceRepairManager; @@ -67,11 +71,12 @@ import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.OpOrder; -import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import org.apache.cassandra.utils.concurrent.Promise; +import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -106,6 +111,9 @@ public class Keyspace //Keyspaces in the case of Views (batchlog of view mutations) public static final OpOrder writeOrder = new OpOrder(); + // Set during draining to indicate that no more mutations should be accepted + private volatile OpOrder.Barrier writeBarrier = null; + /* ColumnFamilyStore per column family */ private final ConcurrentMap columnFamilyStores = new ConcurrentHashMap<>(); @@ -152,24 +160,27 @@ public static Keyspace open(String keyspaceName) } // to only be used by org.apache.cassandra.tools.Standalone* classes - public static Keyspace openWithoutSSTables(String keyspaceName) + public static Keyspace openWithoutSSTables(String keyspaceName) throws UnknownKeyspaceException { return open(keyspaceName, Schema.instance, false); } - public static Keyspace open(String keyspaceName, SchemaProvider schema, boolean loadSSTables) + public static Keyspace open(String keyspaceName, SchemaProvider schema, boolean loadSSTables) throws UnknownKeyspaceException { - return schema.maybeAddKeyspaceInstance(keyspaceName, () -> new Keyspace(keyspaceName, schema, loadSSTables)); + return schema.maybeAddKeyspaceInstance(keyspaceName, () -> { + logger.debug("New instance created for keyspace {}", keyspaceName); + return new Keyspace(keyspaceName, schema, loadSSTables); + }); } public static ColumnFamilyStore openAndGetStore(TableMetadataRef tableRef) { - return open(tableRef.keyspace).getColumnFamilyStore(tableRef.id); + return open(tableRef.keyspace).getColumnFamilyStore(tableRef.get()); } public static ColumnFamilyStore openAndGetStore(TableMetadata table) { - return open(table.keyspace).getColumnFamilyStore(table.id); + return open(table.keyspace).getColumnFamilyStore(table); } public static ColumnFamilyStore openAndGetStoreIfExists(TableMetadata table) @@ -217,14 +228,33 @@ public ColumnFamilyStore getColumnFamilyStore(String cfName) TableMetadata table = schema.getTableMetadata(getName(), cfName); if (table == null) throw new IllegalArgumentException(String.format("Unknown keyspace/cf pair (%s.%s)", getName(), cfName)); - return getColumnFamilyStore(table.id); + return getColumnFamilyStore(table); + } + + public ColumnFamilyStore getColumnFamilyStore(TableMetadata table) + { + return getColumnFamilyStore(table.id, + () -> String.format("Cannot find table %s.%s with id %s, it may have been dropped", + getName(), table.name, table.id)); } public ColumnFamilyStore getColumnFamilyStore(TableId id) + { + return getColumnFamilyStore(id, + () -> String.format("Cannot find table with id %s in keyspace %s, it may have been dropped", + id, getName())); + } + + private ColumnFamilyStore getColumnFamilyStore(TableId id, Supplier errorMsg) { ColumnFamilyStore cfs = columnFamilyStores.get(id); if (cfs == null) - throw new IllegalArgumentException("Unknown CF " + id); + { + // We log a more detailed error message here rather than complicating the client facing exception message + logger.error(errorMsg.get()); + throw new IllegalArgumentException("Cannot find table, it may have been dropped. Table id" + id); + } + return cfs; } @@ -330,12 +360,13 @@ public Stream getAllSnapshots() return getColumnFamilyStores().stream().flatMap(cfs -> cfs.listSnapshots().values().stream()); } - private Keyspace(String keyspaceName, SchemaProvider schema, boolean loadSSTables) + private Keyspace(String keyspaceName, SchemaProvider schema, boolean loadSSTables) throws UnknownKeyspaceException { this.schema = schema; metadata = schema.getKeyspaceMetadata(keyspaceName); - assert metadata != null : "Unknown keyspace " + keyspaceName; - + if (metadata == null) + throw new UnknownKeyspaceException(keyspaceName); + if (metadata.isVirtual()) throw new IllegalStateException("Cannot initialize Keyspace with virtual metadata " + keyspaceName); createReplicationStrategy(metadata); @@ -376,12 +407,12 @@ public static Keyspace mockKS(KeyspaceMetadata metadata) private void createReplicationStrategy(KeyspaceMetadata ksm) { - logger.info("Creating replication strategy " + ksm.name + " params " + ksm.params); + logger.debug("Creating replication strategy " + ksm.name + " params " + ksm.params); replicationStrategy = ksm.createReplicationStrategy(); if (!ksm.params.replication.equals(replicationParams)) { logger.debug("New replication settings for keyspace {} - invalidating disk boundary caches", ksm.name); - columnFamilyStores.values().forEach(ColumnFamilyStore::invalidateLocalRanges); + columnFamilyStores.values().forEach(ColumnFamilyStore::invalidateLocalRangesAndDiskBoundaries); } replicationParams = ksm.params.replication; } @@ -391,10 +422,18 @@ public void dropCf(TableId tableId, boolean dropData) { ColumnFamilyStore cfs = columnFamilyStores.remove(tableId); if (cfs == null) + { + logger.debug("No CFS found when trying to drop table {}, {}", tableId, schema.getTableMetadata(tableId).name); return; + } cfs.onTableDropped(); + + if (logger.isTraceEnabled()) + logger.trace("Dropping CFS {}: unloading CFS", cfs.name); unloadCf(cfs, dropData); + if (logger.isTraceEnabled()) + logger.trace("Dropping CFS {}: completed", cfs.name); } /** @@ -407,11 +446,35 @@ public void unload(boolean dropData) metric.release(); } - // disassociate a cfs from this keyspace instance. + /** + * Unload the column family. For online services, it will also flush beforehand. + * + * Because this method is called by schema operations, it will not throw in case + * of failures, but just log an error. + * + * @param cfs the table to unload + * @param dropData true when data should also be dropped + */ private void unloadCf(ColumnFamilyStore cfs, boolean dropData) { - cfs.unloadCf(); - cfs.invalidate(true, dropData); + logger.debug("Unloading column family store for table {} with dropData={}", cfs.metadata, dropData); + + Throwable err = null; + + // offline services (e.g. standalone compactor) don't have Memtables or CommitLog. An attempt to flush would + // throw an exception + if (!cfs.getTracker().isDummy()) + err = Throwables.perform(err, () -> cfs.unloadCf()); + + err = Throwables.perform(err, () -> cfs.invalidate(true, dropData)); + + if (err != null) + { + logger.error("Failed to unload {}:", cfs.metadata(), err); + JVMStabilityInspector.inspectThrowable(err); + } + + logger.debug("Column family store has been unloaded for table {} with dropData={}", cfs.metadata, dropData); } /** @@ -448,6 +511,9 @@ public KeyspaceWriteHandler getWriteHandler() */ public void initCf(TableMetadataRef metadata, boolean loadSSTables) { + logger.debug("Initializing column family store for table {} with loadSSTables={}", + metadata, loadSSTables); + ColumnFamilyStore cfs = columnFamilyStores.get(metadata.id); if (cfs == null) @@ -467,28 +533,19 @@ public void initCf(TableMetadataRef metadata, boolean loadSSTables) assert cfs.name.equals(metadata.name); cfs.reload(); } - } - - public Future applyFuture(Mutation mutation, boolean writeCommitLog, boolean updateIndexes) - { - return applyInternal(mutation, writeCommitLog, updateIndexes, true, true, new AsyncPromise<>()); - } - public Future applyFuture(Mutation mutation, boolean writeCommitLog, boolean updateIndexes, boolean isDroppable, - boolean isDeferrable) - { - return applyInternal(mutation, writeCommitLog, updateIndexes, isDroppable, isDeferrable, new AsyncPromise<>()); + logger.debug("Column family store initialized for table {} with loadSSTables={}", + metadata, loadSSTables); } - public void apply(Mutation mutation, boolean writeCommitLog, boolean updateIndexes) + public Future applyFuture(Mutation mutation, WriteOptions writeOptions) { - apply(mutation, writeCommitLog, updateIndexes, true); + return applyInternal(mutation, writeOptions, true, new AsyncPromise<>()); } - public void apply(final Mutation mutation, - final boolean writeCommitLog) + public Future applyFuture(Mutation mutation, WriteOptions writeOptions, boolean isDeferrable) { - apply(mutation, writeCommitLog, true, true); + return applyInternal(mutation, writeOptions, isDeferrable, new AsyncPromise<>()); } /** @@ -496,43 +553,51 @@ public void apply(final Mutation mutation, * Otherwise there is a race condition where ALL mutation workers are beeing blocked ending * in a complete deadlock of the mutation stage. See CASSANDRA-12689. * - * @param mutation the row to write. Must not be modified after calling apply, since commitlog append - * may happen concurrently, depending on the CL Executor type. - * @param makeDurable if true, don't return unless write has been made durable - * @param updateIndexes false to disable index updates (used by CollationController "defragmenting") - * @param isDroppable true if this should throw WriteTimeoutException if it does not acquire lock within write_request_timeout + * @param mutation the row to write. Must not be modified after calling apply, since commitlog append + * may happen concurrently, depending on the CL Executor type. + * @param writeOptions describes desired write properties + */ + public void apply(Mutation mutation, WriteOptions writeOptions) + { + applyInternal(mutation, writeOptions, false, null); + } + + /** + * Close this keyspace to further mutations, called when draining or shutting down. + * + * A final write barrier is issued and returned. After this barrier is set, new mutations + * will be rejected, see {@link Keyspace#applyInternal(Mutation, WriteOptions, boolean, Promise)}. */ - public void apply(final Mutation mutation, - final boolean makeDurable, - boolean updateIndexes, - boolean isDroppable) + public OpOrder.Barrier stopMutations() { - applyInternal(mutation, makeDurable, updateIndexes, isDroppable, false, null); + assert writeBarrier == null : "Keyspace has already been closed to mutations"; + writeBarrier = writeOrder.newBarrier(); + writeBarrier.issue(); + return writeBarrier; } /** * This method appends a row to the global CommitLog, then updates memtables and indexes. * - * @param mutation the row to write. Must not be modified after calling apply, since commitlog append - * may happen concurrently, depending on the CL Executor type. - * @param makeDurable if true, don't return unless write has been made durable - * @param updateIndexes false to disable index updates (used by CollationController "defragmenting") - * @param isDroppable true if this should throw WriteTimeoutException if it does not acquire lock within write_request_timeout - * @param isDeferrable true if caller is not waiting for future to complete, so that future may be deferred + * @param mutation the row to write. Must not be modified after calling apply, since commitlog append + * may happen concurrently, depending on the CL Executor type. + * @param writeOptions describes desired write properties + * @param isDeferrable true if caller is not waiting for future to complete, so that future may be deferred */ - private Future applyInternal(final Mutation mutation, - final boolean makeDurable, - boolean updateIndexes, - boolean isDroppable, - boolean isDeferrable, - Promise future) + private Future applyInternal(Mutation mutation, + WriteOptions writeOptions, + boolean isDeferrable, + Promise future) { if (TEST_FAIL_WRITES && metadata.name.equals(TEST_FAIL_WRITES_KS)) throw new RuntimeException("Testing write failures"); + if (writeBarrier != null) + return failDueToWriteBarrier(mutation, future); + Lock[] locks = null; - boolean requiresViewUpdate = updateIndexes && viewManager.updatesAffectView(Collections.singleton(mutation), false); + boolean requiresViewUpdate = writeOptions.requiresViewUpdate(viewManager, mutation); if (requiresViewUpdate) { @@ -559,7 +624,7 @@ private Future applyInternal(final Mutation mutation, if (lock == null) { //throw WTE only if request is droppable - if (isDroppable && (approxTime.isAfter(mutation.approxCreatedAtNanos + DatabaseDescriptor.getWriteRpcTimeout(NANOSECONDS)))) + if (writeOptions.isDroppable && (approxTime.isAfter(mutation.approxCreatedAtNanos + DatabaseDescriptor.getWriteRpcTimeout(NANOSECONDS)))) { for (int j = 0; j < i; j++) locks[j].unlock(); @@ -581,9 +646,9 @@ else if (isDeferrable) locks[j].unlock(); // This view update can't happen right now. so rather than keep this thread busy - // we will re-apply ourself to the queue and try again later + // we will re-apply ourselve to the queue and try again later Stage.MUTATION.execute(() -> - applyInternal(mutation, makeDurable, true, isDroppable, true, future) + applyInternal(mutation, writeOptions, true, future) ); return future; } @@ -616,13 +681,13 @@ else if (isDeferrable) long acquireTime = currentTimeMillis() - mutation.viewLockAcquireStart.get(); // Metrics are only collected for droppable write operations // Bulk non-droppable operations (e.g. commitlog replay, hint delivery) are not measured - if (isDroppable) + if (writeOptions.isDroppable) { for(TableId tableId : tableIds) columnFamilyStores.get(tableId).metric.viewLockAcquireTime.update(acquireTime, MILLISECONDS); } } - try (WriteContext ctx = getWriteHandler().beginWrite(mutation, makeDurable)) + try (WriteContext ctx = getWriteHandler().beginWrite(mutation, writeOptions)) { for (PartitionUpdate upd : mutation.getPartitionUpdates()) { @@ -639,7 +704,7 @@ else if (isDeferrable) try { Tracing.trace("Creating materialized view mutations from base table replica"); - viewManager.forTable(upd.metadata().id).pushViewReplicaUpdates(upd, makeDurable, baseComplete); + viewManager.forTable(upd.metadata().id).pushViewReplicaUpdates(upd, writeOptions, baseComplete); } catch (Throwable t) { @@ -650,7 +715,7 @@ else if (isDeferrable) } } - cfs.getWriteHandler().write(upd, ctx, updateIndexes); + cfs.getWriteHandler().write(upd, ctx, writeOptions.updateIndexes); if (requiresViewUpdate) baseComplete.set(currentTimeMillis()); @@ -672,6 +737,17 @@ else if (isDeferrable) } } + private Promise failDueToWriteBarrier(Mutation mutation, Promise future) + { + assert writeBarrier != null : "Expected non-null write barrier"; + + logger.error("Attempted to apply mutation "+ mutation+" after final write barrier", new Throwable()); + BarrierRejectionException exception = new BarrierRejectionException("Keyspace closed to new mutations"); + if (future != null) + future.setFailure(exception); + throw exception; + } + public AbstractReplicationStrategy getReplicationStrategy() { return replicationStrategy; @@ -782,4 +858,18 @@ public String getName() { return metadata.name; } + + public static class BarrierRejectionException extends RejectException implements InternalRequestExecutionException + { + public BarrierRejectionException(String msg) + { + super(msg); + } + + @Override + public RequestFailureReason getReason() + { + return RequestFailureReason.UNKNOWN; + } + } } diff --git a/src/java/org/apache/cassandra/db/KeyspaceWriteHandler.java b/src/java/org/apache/cassandra/db/KeyspaceWriteHandler.java index 19cca7243210..81205b1a866f 100644 --- a/src/java/org/apache/cassandra/db/KeyspaceWriteHandler.java +++ b/src/java/org/apache/cassandra/db/KeyspaceWriteHandler.java @@ -22,8 +22,9 @@ public interface KeyspaceWriteHandler { - // mutation can be null if makeDurable is false - WriteContext beginWrite(Mutation mutation, boolean makeDurable) throws RequestExecutionException; + // mutation can be null if writeOptions.writeCommitLog is false + WriteContext beginWrite(Mutation mutation, WriteOptions writeOptions) throws RequestExecutionException; + WriteContext createContextForIndexing(); WriteContext createContextForRead(); } diff --git a/src/java/org/apache/cassandra/db/LivenessInfo.java b/src/java/org/apache/cassandra/db/LivenessInfo.java index 168473add552..9bf480aa87d3 100644 --- a/src/java/org/apache/cassandra/db/LivenessInfo.java +++ b/src/java/org/apache/cassandra/db/LivenessInfo.java @@ -231,6 +231,11 @@ public boolean supersedes(LivenessInfo other) return isExpiring(); } + public static LivenessInfo merge(LivenessInfo a, LivenessInfo b) + { + return b.supersedes(a) ? b : a; + } + protected boolean isExpired() { return false; @@ -263,7 +268,7 @@ public LivenessInfo withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, @Override public String toString() { - return String.format("[ts=%d]", timestamp); + return timestamp == NO_TIMESTAMP ? "[ts=EMPTY]" : String.format("[ts=%d]", timestamp); } @Override diff --git a/src/java/org/apache/cassandra/db/MultiCBuilder.java b/src/java/org/apache/cassandra/db/MultiCBuilder.java deleted file mode 100644 index 435e418eb3a7..000000000000 --- a/src/java/org/apache/cassandra/db/MultiCBuilder.java +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.db; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.NavigableSet; - -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.btree.BTreeSet; - -/** - * Builder that allow to build multiple Clustering/ClusteringBound at the same time. - */ -public abstract class MultiCBuilder -{ - /** - * The table comparator. - */ - protected final ClusteringComparator comparator; - - /** - * The number of clustering elements that have been added. - */ - protected int size; - - /** - * true if the clusterings have been build, false otherwise. - */ - protected boolean built; - - /** - * true if the clusterings contains some null elements. - */ - protected boolean containsNull; - - /** - * true if the composites contains some unset elements. - */ - protected boolean containsUnset; - - /** - * true if some empty collection have been added. - */ - protected boolean hasMissingElements; - - protected MultiCBuilder(ClusteringComparator comparator) - { - this.comparator = comparator; - } - - /** - * Creates a new empty {@code MultiCBuilder}. - */ - public static MultiCBuilder create(ClusteringComparator comparator, boolean forMultipleValues) - { - return forMultipleValues - ? new MultiClusteringBuilder(comparator) - : new OneClusteringBuilder(comparator); - } - - /** - * Adds the specified element to all the clusterings. - *

- * If this builder contains 2 clustering: A-B and A-C a call to this method to add D will result in the clusterings: - * A-B-D and A-C-D. - *

- * - * @param value the value of the next element - * @return this MulitCBuilder - */ - public abstract MultiCBuilder addElementToAll(ByteBuffer value); - - /** - * Adds individually each of the specified elements to the end of all of the existing clusterings. - *

- * If this builder contains 2 clusterings: A-B and A-C a call to this method to add D and E will result in the 4 - * clusterings: A-B-D, A-B-E, A-C-D and A-C-E. - *

- * - * @param values the elements to add - * @return this CompositeBuilder - */ - public abstract MultiCBuilder addEachElementToAll(List values); - - /** - * Adds individually each of the specified list of elements to the end of all of the existing composites. - *

- * If this builder contains 2 composites: A-B and A-C a call to this method to add [[D, E], [F, G]] will result in the 4 - * composites: A-B-D-E, A-B-F-G, A-C-D-E and A-C-F-G. - *

- * - * @param values the elements to add - * @return this CompositeBuilder - */ - public abstract MultiCBuilder addAllElementsToAll(List> values); - - protected void checkUpdateable() - { - if (!hasRemaining() || built) - throw new IllegalStateException("this builder cannot be updated anymore"); - } - - /** - * Returns the number of elements that can be added to the clusterings. - * - * @return the number of elements that can be added to the clusterings. - */ - public int remainingCount() - { - return comparator.size() - size; - } - - /** - * Returns the current number of results when {@link #build()} is called - * - * @return the current number of build results - */ - public abstract int buildSize(); - - /** - * Checks if the clusterings contains null elements. - * - * @return true if the clusterings contains null elements, false otherwise. - */ - public boolean containsNull() - { - return containsNull; - } - - /** - * Checks if the clusterings contains unset elements. - * - * @return true if the clusterings contains unset elements, false otherwise. - */ - public boolean containsUnset() - { - return containsUnset; - } - - /** - * Checks if some empty list of values have been added - * @return true if the clusterings have some missing elements, false otherwise. - */ - public boolean hasMissingElements() - { - return hasMissingElements; - } - - /** - * Builds the clusterings. - * - * @return the clusterings - */ - public abstract NavigableSet> build(); - - /** - * Builds the ClusteringBounds for slice restrictions. - * - * @param isStart specify if the bound is a start one - * @param isInclusive specify if the bound is inclusive or not - * @param isOtherBoundInclusive specify if the other bound is inclusive or not - * @param columnDefs the columns of the slice restriction - * @return the ClusteringBounds - */ - public abstract NavigableSet> buildBoundForSlice(boolean isStart, - boolean isInclusive, - boolean isOtherBoundInclusive, - List columnDefs); - - /** - * Builds the ClusteringBounds - * - * @param isStart specify if the bound is a start one - * @param isInclusive specify if the bound is inclusive or not - * @return the ClusteringBounds - */ - public abstract NavigableSet> buildBound(boolean isStart, boolean isInclusive); - - /** - * Checks if some elements can still be added to the clusterings. - * - * @return true if it is possible to add more elements to the clusterings, false otherwise. - */ - public boolean hasRemaining() - { - return remainingCount() > 0; - } - - /** - * Specialization of MultiCBuilder when we know only one clustering/bound is created. - */ - private static class OneClusteringBuilder extends MultiCBuilder - { - /** - * The elements of the clusterings - */ - private final ByteBuffer[] elements; - - public OneClusteringBuilder(ClusteringComparator comparator) - { - super(comparator); - this.elements = new ByteBuffer[comparator.size()]; - } - - public MultiCBuilder addElementToAll(ByteBuffer value) - { - checkUpdateable(); - - if (value == null) - containsNull = true; - if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) - containsUnset = true; - - elements[size++] = value; - return this; - } - - public MultiCBuilder addEachElementToAll(List values) - { - if (values.isEmpty()) - { - hasMissingElements = true; - return this; - } - - assert values.size() == 1; - - return addElementToAll(values.get(0)); - } - - public MultiCBuilder addAllElementsToAll(List> values) - { - if (values.isEmpty()) - { - hasMissingElements = true; - return this; - } - - assert values.size() == 1; - return addEachElementToAll(values.get(0)); - } - - @Override - public int buildSize() - { - return hasMissingElements ? 0 : 1; - } - - public NavigableSet> build() - { - built = true; - - if (hasMissingElements) - return BTreeSet.empty(comparator); - - return BTreeSet.of(comparator, size == 0 ? Clustering.EMPTY : Clustering.make(elements)); - } - - @Override - public NavigableSet> buildBoundForSlice(boolean isStart, - boolean isInclusive, - boolean isOtherBoundInclusive, - List columnDefs) - { - return buildBound(isStart, columnDefs.get(0).isReversedType() ? isOtherBoundInclusive : isInclusive); - } - - public NavigableSet> buildBound(boolean isStart, boolean isInclusive) - { - built = true; - - if (hasMissingElements) - return BTreeSet.empty(comparator); - - if (size == 0) - return BTreeSet.of(comparator, isStart ? BufferClusteringBound.BOTTOM : BufferClusteringBound.TOP); - - ByteBuffer[] newValues = size == elements.length - ? elements - : Arrays.copyOf(elements, size); - - return BTreeSet.of(comparator, BufferClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), newValues)); - } - } - - /** - * MultiCBuilder implementation actually supporting the creation of multiple clustering/bound. - */ - private static class MultiClusteringBuilder extends MultiCBuilder - { - /** - * The elements of the clusterings - */ - private final List> elementsList = new ArrayList<>(); - - public MultiClusteringBuilder(ClusteringComparator comparator) - { - super(comparator); - } - - public MultiCBuilder addElementToAll(ByteBuffer value) - { - checkUpdateable(); - - if (elementsList.isEmpty()) - elementsList.add(new ArrayList<>()); - - if (value == null) - containsNull = true; - else if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) - containsUnset = true; - - for (int i = 0, m = elementsList.size(); i < m; i++) - elementsList.get(i).add(value); - - size++; - return this; - } - - public MultiCBuilder addEachElementToAll(List values) - { - checkUpdateable(); - - if (elementsList.isEmpty()) - elementsList.add(new ArrayList<>()); - - if (values.isEmpty()) - { - hasMissingElements = true; - } - else - { - for (int i = 0, m = elementsList.size(); i < m; i++) - { - List oldComposite = elementsList.remove(0); - - for (int j = 0, n = values.size(); j < n; j++) - { - List newComposite = new ArrayList<>(oldComposite); - elementsList.add(newComposite); - - ByteBuffer value = values.get(j); - - if (value == null) - containsNull = true; - if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) - containsUnset = true; - - newComposite.add(values.get(j)); - } - } - } - size++; - return this; - } - - public MultiCBuilder addAllElementsToAll(List> values) - { - checkUpdateable(); - - if (elementsList.isEmpty()) - elementsList.add(new ArrayList<>()); - - if (values.isEmpty()) - { - hasMissingElements = true; - } - else - { - for (int i = 0, m = elementsList.size(); i < m; i++) - { - List oldComposite = elementsList.remove(0); - - for (int j = 0, n = values.size(); j < n; j++) - { - List newComposite = new ArrayList<>(oldComposite); - elementsList.add(newComposite); - - List value = values.get(j); - - if (value.contains(null)) - containsNull = true; - if (value.contains(ByteBufferUtil.UNSET_BYTE_BUFFER)) - containsUnset = true; - - newComposite.addAll(value); - } - } - size += values.get(0).size(); - } - return this; - } - - @Override - public int buildSize() - { - return hasMissingElements ? 0 : elementsList.size(); - } - - public NavigableSet> build() - { - built = true; - - if (hasMissingElements) - return BTreeSet.empty(comparator); - - CBuilder builder = CBuilder.create(comparator); - - if (elementsList.isEmpty()) - return BTreeSet.of(builder.comparator(), builder.build()); - - BTreeSet.Builder> set = BTreeSet.builder(builder.comparator()); - for (int i = 0, m = elementsList.size(); i < m; i++) - { - List elements = elementsList.get(i); - set.add(builder.buildWith(elements)); - } - return set.build(); - } - - public NavigableSet> buildBoundForSlice(boolean isStart, - boolean isInclusive, - boolean isOtherBoundInclusive, - List columnDefs) - { - built = true; - - if (hasMissingElements) - return BTreeSet.empty(comparator); - - CBuilder builder = CBuilder.create(comparator); - - if (elementsList.isEmpty()) - return BTreeSet.of(comparator, builder.buildBound(isStart, isInclusive)); - - // Use a TreeSet to sort and eliminate duplicates - BTreeSet.Builder> set = BTreeSet.builder(comparator); - - // The first column of the slice might not be the first clustering column (e.g. clustering_0 = ? AND (clustering_1, clustering_2) >= (?, ?) - int offset = columnDefs.get(0).position(); - - for (int i = 0, m = elementsList.size(); i < m; i++) - { - List elements = elementsList.get(i); - - // Handle the no bound case - if (elements.size() == offset) - { - set.add(builder.buildBoundWith(elements, isStart, true)); - continue; - } - - // In the case of mixed order columns, we will have some extra slices where the columns change directions. - // For example: if we have clustering_0 DESC and clustering_1 ASC a slice like (clustering_0, clustering_1) > (1, 2) - // will produce 2 slices: [BOTTOM, 1) and (1.2, 1] - // So, the END bound will return 2 bounds with the same values 1 - ColumnMetadata lastColumn = columnDefs.get(columnDefs.size() - 1); - if (elements.size() <= lastColumn.position() && i < m - 1 && elements.equals(elementsList.get(i + 1))) - { - set.add(builder.buildBoundWith(elements, isStart, false)); - set.add(builder.buildBoundWith(elementsList.get(i++), isStart, true)); - continue; - } - - // Handle the normal bounds - ColumnMetadata column = columnDefs.get(elements.size() - 1 - offset); - set.add(builder.buildBoundWith(elements, isStart, column.isReversedType() ? isOtherBoundInclusive : isInclusive)); - } - return set.build(); - } - - public NavigableSet> buildBound(boolean isStart, boolean isInclusive) - { - built = true; - - if (hasMissingElements) - return BTreeSet.empty(comparator); - - CBuilder builder = CBuilder.create(comparator); - - if (elementsList.isEmpty()) - return BTreeSet.of(comparator, builder.buildBound(isStart, isInclusive)); - - // Use a TreeSet to sort and eliminate duplicates - BTreeSet.Builder> set = BTreeSet.builder(comparator); - - for (int i = 0, m = elementsList.size(); i < m; i++) - { - List elements = elementsList.get(i); - set.add(builder.buildBoundWith(elements, isStart, isInclusive)); - } - return set.build(); - } - } -} diff --git a/src/java/org/apache/cassandra/db/MultiClusteringBuilder.java b/src/java/org/apache/cassandra/db/MultiClusteringBuilder.java new file mode 100644 index 000000000000..49e880f46439 --- /dev/null +++ b/src/java/org/apache/cassandra/db/MultiClusteringBuilder.java @@ -0,0 +1,501 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.NavigableSet; +import java.util.TreeSet; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.UniqueComparator; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.btree.BTreeSet; + +/** + * Builder that allows to build multiple {@link Clustering}/{@link ClusteringBound} at the same time. + * Builds a set of clusterings incrementally, by computing cartesian products of + * sets of values present in each statement restriction. The typical use of this builder is as follows: + *
    + *
  1. Call {@link MultiClusteringBuilder#extend(ClusteringElements, List)} or {@link MultiClusteringBuilder#extend(List, List)} + * method once per each restriction. Slice restrictons, if they exist, must be added last. + *
  2. Finally, call {@link MultiClusteringBuilder#build()} or {@link MultiClusteringBuilder#buildBound(boolean)} method + * to obtain the set of clusterings / clustering bounds.
  3. + *
+ *

+ * Important: When dealing with slices, you likely want the number of start and end bounds to match. + * If some columns are restricted from one side only, you can use the special {@link ClusteringElements#BOTTOM} or + * {@link ClusteringElements#TOP} values to generate a proper clustering bound for the "unbounded" + * side of the restriction. + *

+ *

Example

+ *

+ * + * Imagine we have a CQL query with multiple restrictions joined by AND: + *

+ * SELECT * FROM tab
+ * WHERE a IN (a1, a2)
+ *   AND b IN (b1, b2, b3)
+ *   AND c > c1
+ * 
+ *

+ * We need to generate a list of clustering bounds that will be used to fetch proper contiguous chunks of the partition. + * + *

+ * The builder initial state is a single empty clustering, denoted by the {@code ROOT} constant, + * which is a natural zero element of cartesian set multiplication. This significantly simplifies the logic. + *

+ * point: ()
+ * 
+ * + * After adding the IN restriction on column {@code a} we get 2 points: + *
+ * point: (a1)
+ * point: (a2)
+ * 
+ * + * Next when we add the IN restrction on column {@code b}, we get a cartesian product of all values + * of {@code a} with all values of {@code b}: + *
+ * point: (a1, b1)
+ * point: (a1, b2)
+ * point: (a1, b3)
+ * point: (a2, b1)
+ * point: (a2, b2)
+ * point: (a2, b3)
+ * 
+ * + * Finally, we add the slice of column {@code c} by specifying the lower and upper bound + * (we use {@code TOP} for the upper bound), and we get the final set of clustering bounds: + *
+ * excl start: (a1, b1, c1)
+ * incl end:   (a1, b1)
+ * excl start: (a1, b2, c1)
+ * incl end:   (a1, b2)
+ * excl start: (a1, b3, c1)
+ * incl end:   (a1, b3)
+ * excl start: (a2, b1, c1)
+ * incl end:   (a2, b1)
+ * excl start: (a2, b2, c1)
+ * incl end:   (a2, b2)
+ * excl start: (a2, b3, c1)
+ * incl end:   (a2, b3)
+ * 
+ */ +public class MultiClusteringBuilder +{ + /** + * Represents a building block of a clustering. + * Either a point or a bound. + * Can consist of multiple column values. + * + *

+ * For bounds, it additionally stores the inclusiveness of a bound and whether it is start or end, so that + * it is possible to mix bounds of different inclusiveness. + */ + public static class ClusteringElements + { + public enum Kind + { + POINT, INCL_START, EXCL_START, INCL_END, EXCL_END + } + + public static final ClusteringElements BOTTOM = new ClusteringElements(Collections.emptyList(), Kind.INCL_START); + public static final ClusteringElements TOP = new ClusteringElements(Collections.emptyList(), Kind.INCL_END); + public static final ClusteringElements ROOT = new ClusteringElements(Collections.emptyList(), Kind.POINT); + + final List values; + final Kind kind; + + + private ClusteringElements(List values, Kind kind) + { + this.values = values; + this.kind = kind; + } + + public static ClusteringElements point(ByteBuffer value) + { + return point(Collections.singletonList(value)); + } + + public static ClusteringElements point(List values) + { + return new ClusteringElements(values, Kind.POINT); + } + + public static ClusteringElements bound(ByteBuffer value, Bound bound, boolean inclusive) + { + return bound(Collections.singletonList(value), bound, inclusive); + } + + public static ClusteringElements bound(List values, Bound bound, boolean inclusive) + { + Kind kind; + if (bound.isStart()) + kind = (inclusive ? Kind.INCL_START : Kind.EXCL_START); + else + kind = (inclusive ? Kind.INCL_END : Kind.EXCL_END); + return new ClusteringElements(values, kind); + } + + public boolean isBound() + { + return kind != Kind.POINT; + } + + public boolean isStart() + { + return kind == ClusteringElements.Kind.EXCL_START || + kind == ClusteringElements.Kind.INCL_START; + } + + public boolean isInclusive() + { + return kind == Kind.INCL_START || + kind == Kind.INCL_END || + kind == Kind.POINT; + } + + public String toString() + { + return "Element{" + + "kind=" + kind + + ", value=" + values + + '}'; + } + } + + /** + * The table comparator. + */ + private final ClusteringComparator comparator; + + /** + * Columns corresponding to the already added elements. + */ + private final List columns = new ArrayList<>(); + + /** + * The elements of the clusterings. + */ + private List clusterings = Collections.singletonList(ClusteringElements.ROOT); + + + /** + * true if the clusterings have been build, false otherwise. + */ + private boolean built; + + /** + * true if the clusterings contains some null elements. + */ + private boolean containsNull; + + /** + * true if the composites contains some unset elements. + */ + private boolean containsUnset; + + /** + * true if the composites contains some slice bound elements. + */ + private boolean containsSliceBound; + + + private MultiClusteringBuilder(ClusteringComparator comparator) + { + this.comparator = comparator; + } + + /** + * Creates a new empty {@code MultiCBuilder}. + */ + public static MultiClusteringBuilder create(ClusteringComparator comparator) + { + return new MultiClusteringBuilder(comparator); + } + + protected void checkUpdateable() + { + if (!hasRemaining() || built) + throw new IllegalStateException("This builder cannot be updated anymore"); + if (containsSliceBound) + throw new IllegalStateException("Cannot extend clustering that contains a slice bound"); + } + + /** + * Returns the number of elements that can be added to the clusterings. + * + * @return the number of elements that can be added to the clusterings. + */ + public int remainingCount() + { + return comparator.size() - columns.size(); + } + + /** + * Checks if the clusterings contains null elements. + * + * @return true if the clusterings contains null elements, false otherwise. + */ + public boolean containsNull() + { + return containsNull; + } + + /** + * Checks if the clusterings contains unset elements. + * + * @return true if the clusterings contains unset elements, false otherwise. + */ + public boolean containsUnset() + { + return containsUnset; + } + + /** + * Returns the current number of results when {@link #build()} is called + * + * @return the current number of build results + */ + public int buildSize() + { + return clusterings.size(); + } + + /** + * Returns true if the current number of build results is zero. + */ + public boolean buildIsEmpty() + { + return clusterings.isEmpty(); + } + + /** + * Checks if some elements can still be added to the clusterings. + * + * @return true if it is possible to add more elements to the clusterings, false otherwise. + */ + public boolean hasRemaining() + { + return remainingCount() > 0; + } + + /** + * Extends each clustering with the given element(s). + * + *

+ * If this builder contains 2 composites: A-B and A-C a call to this method to add D will result in the + * clusterings A-B-D and A-C-D. + *

+ * + * @param suffix the element to add + * @param suffixColumns column definitions in the element; must match the subsequent comparator subtypes + * @return this CompositeBuilder + */ + public final MultiClusteringBuilder extend(ClusteringElements suffix, List suffixColumns) + { + return extend(Collections.singletonList(suffix), suffixColumns); + } + + /** + * Adds individually each of the specified elements to the end of all the existing clusterings. + * The number of result clusterings is the product of the number of current clusterings and the number + * of elements added. + * + *

+ * If this builder contains 2 composites: A-B and A-C a call to this method to add D and E will result in the 4 + * clusterings: A-B-D, A-B-E, A-C-D and A-C-E. + *

+ * + *

+ * Added elements can be composites as well. + * If this builder contains 2 composites: A-B and A-C a call to this method to add [[D, E], [F, G]] will result in + * 4 composites: A-B-D-E, A-B-F-G, A-C-D-E and A-C-F-G. + *

+ * + * @param suffixes the elements to add + * @param suffixColumns column definitions in each element; must match the subsequent comparator subtypes + * @return this CompositeBuilder + */ + public MultiClusteringBuilder extend(List suffixes, List suffixColumns) + { + checkUpdateable(); + + for (int i = 0; i < suffixColumns.size(); i++) + { + AbstractType expectedType = comparator.subtype(columns.size() + i); + AbstractType actualType = suffixColumns.get(i).type; + if (!actualType.equals(expectedType)) + { + throw new IllegalStateException( + String.format("Unexpected column type %s != %s.", actualType, expectedType)); + } + } + + for (ClusteringElements suffix: suffixes) + { + if (suffix.kind != ClusteringElements.Kind.POINT) + containsSliceBound = true; + if (suffix.values.contains(null)) + containsNull = true; + // Cannot use `value.contains(UNSET_BYTE_BUFFER)` + // because UNSET_BYTE_BUFFER.equals(EMPTY_BYTE_BUFFER) but UNSET_BYTE_BUFFER != EMPTY_BYTE_BUFFER + if (suffix.values.stream().anyMatch(b -> b == ByteBufferUtil.UNSET_BYTE_BUFFER)) + containsUnset = true; + } + + this.clusterings = columns.isEmpty() ? suffixes : cartesianProduct(clusterings, suffixes); + this.columns.addAll(suffixColumns); + + assert columns.size() <= comparator.size(); + return this; + } + + private static ArrayList cartesianProduct(List prefixes, List suffixes) + { + ArrayList newElements = new ArrayList<>(prefixes.size() * suffixes.size()); + for (ClusteringElements prefix: prefixes) + { + for (ClusteringElements suffix: suffixes) + { + List newValue = new ArrayList<>(prefix.values.size() + suffix.values.size()); + newValue.addAll(prefix.values); + newValue.addAll(suffix.values); + newElements.add(new ClusteringElements(newValue, suffix.kind)); + } + } + assert newElements.size() == prefixes.size() * suffixes.size(); + return newElements; + } + + /** + * Builds the clusterings. + * This cannot be used if slice restrictions were added. + */ + public NavigableSet> build() + { + built = true; + + ClusteringBuilder builder = ClusteringBuilder.create(comparator); + BTreeSet.Builder> set = BTreeSet.builder(builder.comparator()); + for (ClusteringElements element: clusterings) + { + assert element.kind == ClusteringElements.Kind.POINT : String.format("Not a point: %s", element); + if (!element.values.isEmpty()) + set.add(builder.buildWith(element.values)); + else + set.add(Clustering.EMPTY); + } + return set.build(); + } + + /** + * Builds the ClusteringBounds for slice restrictions. + * The number of start bounds equals the number of end bounds. + * + * @param isStart if true, start bounds are returned, otherwise end bounds are returned + */ + public NavigableSet> buildBound(boolean isStart) + { + built = true; + ClusteringBuilder builder = ClusteringBuilder.create(comparator); + + // Use UniqueComparator to allow duplicates. + // We deal with start bounds and end bounds separately, so it is a bad idea to lose duplicates, + // as this would cause the number of start bounds differ from the number of end bounds, if accidentally + // two bounds on one end collide but their corresponding bounds on the other end do not. + BTreeSet.Builder> set = BTreeSet.builder(new UniqueComparator<>(comparator)); + for (ClusteringElements element: clusterings) + { + if (element.isBound() && element.isStart() != isStart) + continue; + + org.apache.cassandra.db.ClusteringBound bound = element.values.isEmpty() + ? builder.buildBound(isStart, element.isInclusive()) + : builder.buildBoundWith(element.values, isStart, element.isInclusive()); + + set.add(bound); + } + return set.build(); + } + + /** + * Builds the serialized partition keys. + * + * @return the serialized partition keys + */ + public List buildSerializedPartitionKeys() + { + built = true; + + if (clusterings.isEmpty()) + return Collections.emptyList(); + + if (clusterings.get(0) == ClusteringElements.ROOT) + return ImmutableList.of(ByteBufferUtil.EMPTY_BYTE_BUFFER); + + // Use a TreeSet here to return the values in comparator sorted order + TreeSet set = comparator.size() == 1 + ? new TreeSet<>(comparator.subtype(0)) + : new TreeSet<>(CompositeType.getInstance(comparator.subtypes())); + + for (ClusteringElements c: clusterings) + set.add(c.values.size() == 1 ? c.values.get(0) : toComposite(c.values)); + + return new ArrayList<>(set); + } + + protected static ByteBuffer toComposite(ByteBuffer[] components) + { + int sum = 0; + for (ByteBuffer v : components) + { + sum += v == null ? 0 : v.remaining(); + } + if (sum > FBUtilities.MAX_UNSIGNED_SHORT) + throw new InvalidRequestException(String.format("Key length of %d is longer than maximum of %d", + sum, + FBUtilities.MAX_UNSIGNED_SHORT)); + + return CompositeType.build(ByteBufferAccessor.instance, components); + } + + private ByteBuffer toComposite(List elements) + { + ByteBuffer[] tmp = new ByteBuffer[elements.size()]; + for (int i = 0, m = elements.size(); i < m; i++) + { + tmp[i] = elements.get(i); + } + return toComposite(tmp); + } + +} + diff --git a/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java b/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java new file mode 100644 index 000000000000..0b03b8d4ccfa --- /dev/null +++ b/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java @@ -0,0 +1,73 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.schema.TableMetadata; + +/** + * A {@code ReadQuery} for multiple partitions, restricted by one of more data ranges. + */ +public interface MultiPartitionReadQuery extends ReadQuery +{ + List ranges(); + + default void appendCQLWhereClause(CqlBuilder builder, Redaction redaction) + { + // Append the data ranges. + TableMetadata metadata = metadata(); + boolean hasRanges = appendRanges(builder, redaction); + + // Append the clustering index filter and the row filter. + String filter = ranges().get(0).clusteringIndexFilter.toCQLString(metadata, rowFilter(), redaction); + builder.appendRestrictions(filter, hasRanges); + } + + private boolean appendRanges(CqlBuilder builder, Redaction redaction) + { + List ranges = ranges(); + if (ranges.size() == 1) + { + DataRange range = ranges.get(0); + if (range.isUnrestricted(metadata())) + return false; + + String rangeString = range.toCQLString(metadata(), rowFilter(), redaction); + if (!rangeString.isEmpty()) + { + builder.append(" WHERE ").append(rangeString); + return true; + } + } + else + { + builder.append(" WHERE ").append('('); + for (int i = 0; i < ranges.size(); i++) + { + if (i > 0) + builder.append(" OR "); + builder.append(ranges.get(i).toCQLString(metadata(), rowFilter(), redaction)); + } + builder.append(')'); + return true; + } + return false; + } +} diff --git a/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java b/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java new file mode 100644 index 000000000000..c2ed61d961fe --- /dev/null +++ b/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java @@ -0,0 +1,477 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.metrics.TableMetrics; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.pager.PagingState; +import org.apache.cassandra.service.pager.QueryPager; +import org.apache.cassandra.service.reads.ReadCallback; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.transport.ProtocolVersion; + +/** + * Used by {@code EndpointGroupingCoordinator} to query all involved ranges on a given replica at once. + * + * Note: digest is not supported because each replica is responsible for different token ranges, there is no point on + * sending digest. + */ +public class MultiRangeReadCommand extends ReadCommand implements MultiPartitionReadQuery +{ + protected static final SelectionDeserializer selectionDeserializer = new Deserializer(); + + private final List dataRanges; + + private MultiRangeReadCommand(boolean isDigest, + int digestVersion, + boolean acceptsTransient, + TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + List dataRanges, + Index.QueryPlan indexQueryPlan, + boolean trackWarnings) + { + super(Kind.MULTI_RANGE, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, null); + + assert dataRanges.size() > 0; + this.dataRanges = dataRanges; + } + + /** + * + * @param command current partition range command + * @param ranges token ranges to be queried on specific endpoint + * @param isRangeContinuation whether it's querying the first range in the batch + * @return multi-range read command for specific endpoint + */ + @VisibleForTesting + public static MultiRangeReadCommand create(PartitionRangeReadCommand command, List> ranges, boolean isRangeContinuation) + { + List dataRanges = new ArrayList<>(ranges.size()); + for (AbstractBounds range : ranges) + dataRanges.add(command.dataRange().forSubRange(range)); + + return new MultiRangeReadCommand(command.isDigestQuery(), + command.digestVersion(), + command.acceptsTransient(), + command.metadata(), + command.nowInSec(), + command.columnFilter(), + command.rowFilter(), + isRangeContinuation ? command.limits() : command.limits().withoutState(), + dataRanges, + command.indexQueryPlan(), + false); + } + + /** + * @param subrangeHandlers handlers for all vnode ranges replicated in current endpoint. + * @return multi-range read command for specific endpoint + */ + public static MultiRangeReadCommand create(List> subrangeHandlers) + { + assert !subrangeHandlers.isEmpty(); + + PartitionRangeReadCommand command = (PartitionRangeReadCommand) subrangeHandlers.get(0).command(); + List dataRanges = new ArrayList<>(subrangeHandlers.size()); + boolean trackWarnings = false; + for (ReadCallback handler : subrangeHandlers) + { + dataRanges.add(((PartitionRangeReadCommand) handler.command()).dataRange()); + trackWarnings |= handler.command().isTrackingWarnings(); + } + + return new MultiRangeReadCommand(command.isDigestQuery(), + command.digestVersion(), + command.acceptsTransient(), + command.metadata(), + command.nowInSec(), + command.columnFilter(), + command.rowFilter(), + command.limits(), + dataRanges, + command.indexQueryPlan(), + trackWarnings); + } + + // we need to override this method to return instances of MultiRangeReadResponse that don't mess with the serializer + @Override + public ReadResponse createEmptyResponse() + { + UnfilteredPartitionIterator iterator = EmptyIterators.unfilteredPartition(metadata()); + + return isDigestQuery() + ? ReadResponse.createDigestResponse(iterator, this) + : MultiRangeReadResponse.createDataResponse(iterator, this); + } + + @Override + public boolean isSinglePartition() + { + return dataRanges.size() == 1 && dataRanges.get(0).isSinglePartition(); + } + + /** + * @return all token ranges to be queried + */ + @Override + public List ranges() + { + return dataRanges; + } + + @Override + public String loggableTokens() + { + StringBuilder loggableTokens = new StringBuilder(); + boolean first = true; + for (DataRange dataRange : dataRanges) + { + if (first) + first = false; + else + loggableTokens.append(", "); + loggableTokens.append(loggableTokens(dataRange)); + } + return loggableTokens.toString(); + } + + private StringBuilder loggableTokens(DataRange dataRange) + { + return new StringBuilder() + .append("token range: ") + .append(dataRange.keyRange.inclusiveLeft() ? '[' : '(') + .append(dataRange.keyRange.left.getToken().toString()) + .append(", ") + .append(dataRange.keyRange.right.getToken().toString()) + .append(dataRange.keyRange.inclusiveRight() ? ']' : ')'); + } + + @Override + protected void serializeSelection(DataOutputPlus out, int version) throws IOException + { + int rangeCount = dataRanges.size(); + out.writeInt(rangeCount); + + for (DataRange range : dataRanges) + DataRange.serializer.serialize(range, out, version, metadata()); + } + + @Override + protected long selectionSerializedSize(int version) + { + int rangeCount = dataRanges.size(); + long size = TypeSizes.sizeof(rangeCount); + + for (DataRange range : dataRanges) + size += DataRange.serializer.serializedSize(range, version, metadata()); + + return size; + } + + @Override + public boolean isLimitedToOnePartition() + { + if (dataRanges.size() != 1) + return false; + + DataRange dataRange = dataRanges.get(0); + return dataRange.keyRange() instanceof Bounds + && dataRange.startKey().kind() == PartitionPosition.Kind.ROW_KEY + && dataRange.startKey().equals(dataRange.stopKey()); + } + + @Override + public boolean isRangeRequest() + { + return false; + } + + @Override + public ReadCommand withUpdatedLimit(DataLimits newLimits) + { + return new MultiRangeReadCommand(isDigestQuery(), + digestVersion(), + acceptsTransient(), + metadata(), + nowInSec(), + columnFilter(), + rowFilter(), + newLimits, + dataRanges, + indexQueryPlan(), + isTrackingWarnings()); + } + + @Override + public long getTimeout(TimeUnit unit) + { + return DatabaseDescriptor.getRangeRpcTimeout(unit); + } + + @Override + public ReadResponse createResponse(UnfilteredPartitionIterator iterator, RepairedDataInfo rdi) + { + assert !isDigestQuery(); + return MultiRangeReadResponse.createDataResponse(iterator, this); + } + + @Override + public ClusteringIndexFilter clusteringIndexFilter(DecoratedKey key) + { + for (DataRange dataRange : ranges()) + { + if (dataRange.keyRange().contains(key)) + return dataRange.clusteringIndexFilter(key); + } + + throw new IllegalArgumentException(key + " is not in data ranges " + dataRanges.stream().map(r -> r.toString(metadata())).collect(Collectors.toList())); + } + + @Override + public ReadCommand copy() + { + return new MultiRangeReadCommand(isDigestQuery(), + digestVersion(), + acceptsTransient(), + metadata(), + nowInSec(), + columnFilter(), + rowFilter(), + limits(), + dataRanges, + indexQueryPlan(), + isTrackingWarnings()); + } + + @Override + protected ReadCommand copyAsTransientQuery() + { + return new MultiRangeReadCommand(false, + 0, + true, + metadata(), + nowInSec(), + columnFilter(), + rowFilter(), + limits(), + dataRanges, + indexQueryPlan(), + isTrackingWarnings()); + } + + @Override + protected ReadCommand copyAsDigestQuery() + { + throw new UnsupportedOperationException(); + } + + @Override + public UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadExecutionController executionController) + { + return UnfilteredPartitionIterators.concat(dataRanges.stream() + .map(this::toPartitionRangeReadCommand) + .map(command -> command.queryStorage(cfs, executionController)) + .collect(Collectors.toList())); + } + + @Override + protected boolean intersects(SSTableReader sstable) + { + return dataRanges.stream().anyMatch(dataRange -> dataRange.clusteringIndexFilter.intersects(sstable.metadata().comparator, sstable.getSSTableMetadata().coveredClustering)); + } + + @Override + public UnfilteredPartitionIterator searchStorage(Index.Searcher searcher, ReadExecutionController controller) + { + if (indexQueryPlan.supportsMultiRangeReadCommand()) + { + // SAI supports fetching multiple ranges at once + return super.searchStorage(searcher, controller); + } + else + { + // search each subrange separately as they don't support MultiRangeReadCommand + return UnfilteredPartitionIterators.concat(dataRanges.stream() + .map(this::toPartitionRangeReadCommand) + .map(command -> command.searchStorage(searcher, controller)) + .collect(Collectors.toList())); + } + } + + private PartitionRangeReadCommand toPartitionRangeReadCommand(DataRange dataRange) + { + return PartitionRangeReadCommand.create(metadata(), nowInSec(), columnFilter(), rowFilter(), limits(), dataRange, indexQueryPlan(), isTrackingWarnings()); + } + + @Override + public boolean isReversed() + { + return ranges().get(0).isReversed(); + } + + @Override + protected void recordReadLatency(TableMetrics metric, long latencyNanos) + { + metric.rangeLatency.addNano(latencyNanos); + } + + @Override + protected void recordReadRequest(TableMetrics metric) + { + metric.rangeRequests.inc(); + } + + @Override + public Verb verb() + { + return Verb.MULTI_RANGE_REQ; + } + + @Override + public void appendCQLWhereClause(CqlBuilder builder, Redaction redaction) + { + MultiPartitionReadQuery.super.appendCQLWhereClause(builder, redaction); + } + + @Override + public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState, final Dispatcher.RequestTime requestTime) throws RequestExecutionException + { + // MultiRangeReadCommand should only be executed on the replica side + throw new UnsupportedOperationException(); + } + + @Override + public DataRange dataRange() + { + throw new UnsupportedOperationException(); + } + + @Override + public QueryPager getPager(PagingState pagingState, ProtocolVersion protocolVersion) + { + // MultiRangeReadCommand should only be executed at replica side" + throw new UnsupportedOperationException(); + } + + @Override + public boolean selectsKey(DecoratedKey key) + { + for (DataRange dataRange : ranges()) + { + if (!dataRange.contains(key)) + continue; + + return rowFilter().partitionKeyRestrictionsAreSatisfiedBy(key, metadata().partitionKeyType); + } + + return false; + } + + @Override + public boolean selectsClustering(DecoratedKey key, Clustering clustering) + { + if (clustering == Clustering.STATIC_CLUSTERING) + return !columnFilter().fetchedColumns().statics.isEmpty(); + + for (DataRange dataRange : ranges()) + { + if (!dataRange.keyRange().contains(key) || !dataRange.clusteringIndexFilter(key).selects(clustering)) + continue; + + if (rowFilter().clusteringKeyRestrictionsAreSatisfiedBy(clustering)) + return true; + } + + return false; + } + + @Override + public boolean selectsFullPartition() + { + return metadata().isStaticCompactTable() || + (ranges().stream().allMatch(DataRange::selectsAllPartition) && !rowFilter().hasExpressionOnClusteringOrRegularColumns()); + } + + private static class Deserializer extends SelectionDeserializer + { + @Override + public ReadCommand deserialize(DataInputPlus in, + int version, + boolean isDigest, + int digestVersion, + boolean acceptsTransient, + TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + Index.QueryPlan indexQueryPlan) + throws IOException + { + int rangeCount = in.readInt(); + + List ranges = new ArrayList<>(rangeCount); + for (int i = 0; i < rangeCount; i++) + ranges.add(DataRange.serializer.deserialize(in, version, metadata)); + + return new MultiRangeReadCommand(isDigest, + digestVersion, + acceptsTransient, + metadata, + nowInSec, + columnFilter, + rowFilter, + limits, + ranges, + indexQueryPlan, + false); + } + } +} diff --git a/src/java/org/apache/cassandra/db/MultiRangeReadResponse.java b/src/java/org/apache/cassandra/db/MultiRangeReadResponse.java new file mode 100644 index 000000000000..71416f39d770 --- /dev/null +++ b/src/java/org/apache/cassandra/db/MultiRangeReadResponse.java @@ -0,0 +1,423 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.NoSuchElementException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * It's used to store response of multi-range read request from a given endpoint, + * {@link ReadResponse} of subrange can be extracted via {@link #subrangeResponse(MultiRangeReadCommand, AbstractBounds)}; + */ +public abstract class MultiRangeReadResponse extends ReadResponse +{ + protected static final Logger logger = LoggerFactory.getLogger(MultiRangeReadResponse.class); + + public static final IVersionedSerializer serializer = new Serializer(); + + private MultiRangeReadResponse() + { + } + + /** + * @param data results of multiple ranges + * @param command current multi-range read command + * @return multi-range read response + */ + static ReadResponse createDataResponse(UnfilteredPartitionIterator data, MultiRangeReadCommand command) + { + return new LocalDataResponse(data, command); + } + + /** + * @param command current multi-range read command + * @param range target subrange + * @return response corresponding to the given range + */ + public abstract ReadResponse subrangeResponse(MultiRangeReadCommand command, AbstractBounds range); + + @Override + public ByteBuffer digest(ReadCommand command) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDigestResponse() + { + return false; + } + + @Override + public ByteBuffer repairedDataDigest() + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRepairedDigestConclusive() + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean mayIncludeRepairedDigest() + { + throw new UnsupportedOperationException(); + } + + @Override + public String toDebugString(ReadCommand command, DecoratedKey key) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean supportsResponseSizeTracking() + { + return false; + } + + /** + * A local response that is not meant to be serialized or used for caching remote endpoint's multi-range response. + */ + private static class LocalResponse extends MultiRangeReadResponse + { + private final RangeBoundPartitionIterator iterator; + + LocalResponse(UnfilteredPartitionIterator response) + { + this.iterator = new RangeBoundPartitionIterator(response); + } + + @Override + public UnfilteredPartitionIterator makeIterator(ReadCommand command) + { + throw new UnsupportedOperationException(); + } + + @Override + public ReadResponse subrangeResponse(MultiRangeReadCommand command, AbstractBounds range) + { + // deliver already cached content without deserialization. + return new LocalSubrangeResponse(iterator, range); + } + + class RangeBoundPartitionIterator + { + private final UnfilteredPartitionIterator iterator; + private UnfilteredRowIterator next = null; + + RangeBoundPartitionIterator(UnfilteredPartitionIterator iterator) + { + this.iterator = iterator; + } + + public boolean hasNext(AbstractBounds range) + { + if (next != null) + return range.contains(next.partitionKey()); + + if (iterator.hasNext()) + { + next = iterator.next(); + if (range.contains(next.partitionKey())) + return true; + } + return false; + } + + public UnfilteredRowIterator next() + { + if (next != null) + { + UnfilteredRowIterator result = next; + next = null; + return result; + } + throw new NoSuchElementException(); + } + } + } + + private static class LocalSubrangeResponse extends ReadResponse + { + private final LocalResponse.RangeBoundPartitionIterator iterator; + private final AbstractBounds range; + + LocalSubrangeResponse(LocalResponse.RangeBoundPartitionIterator iterator, AbstractBounds range) + { + this.iterator = iterator; + this.range = range; + } + + @Override + public UnfilteredPartitionIterator makeIterator(ReadCommand command) + { + return new AbstractUnfilteredPartitionIterator() + { + @Override + public TableMetadata metadata() + { + return command.metadata(); + } + + @Override + public boolean hasNext() + { + return iterator.hasNext(range); + } + + @Override + public UnfilteredRowIterator next() + { + return iterator.next(); + } + }; + } + + @Override + public ByteBuffer digest(ReadCommand command) + { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuffer repairedDataDigest() + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRepairedDigestConclusive() + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean mayIncludeRepairedDigest() + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDigestResponse() + { + return false; + } + + @Override + public boolean supportsResponseSizeTracking() + { + return false; + } + } + + /** + * A local response that needs to be serialized, i.e. sent to another node. The iterator + * is serialized by the build method and can be closed as soon as this response has been created. + */ + private static class LocalDataResponse extends DataResponse + { + private LocalDataResponse(UnfilteredPartitionIterator iterator, MultiRangeReadCommand command) + { + super(build(iterator, command.columnFilter()), MessagingService.current_version, DeserializationHelper.Flag.FROM_REMOTE); + } + + private static ByteBuffer build(UnfilteredPartitionIterator iterator, ColumnFilter selection) + { + try (DataOutputBuffer buffer = new DataOutputBuffer()) + { + UnfilteredPartitionIterators.serializerForIntraNode().serialize(iterator, selection, buffer, MessagingService.current_version); + return buffer.buffer(); + } + catch (IOException e) + { + // We're serializing in memory so this shouldn't happen + throw new RuntimeException(e); + } + } + } + + /** + * A response received from a remove node. We keep the response serialized in the byte buffer. + */ + private static class RemoteDataResponse extends DataResponse + { + RemoteDataResponse(ByteBuffer data, + int dataSerializationVersion) + { + super(data, dataSerializationVersion, DeserializationHelper.Flag.FROM_REMOTE); + } + } + + /** + * The command base class for local or remote responses that stay serialized in a byte buffer, + * the data. + */ + static abstract class DataResponse extends MultiRangeReadResponse + { + // The response, serialized in the current messaging version + private final ByteBuffer data; + private final int dataSerializationVersion; + private final DeserializationHelper.Flag flag; + + private MultiRangeReadResponse.LocalResponse cached; + + DataResponse(ByteBuffer data, + int dataSerializationVersion, + DeserializationHelper.Flag flag) + { + this.data = data; + this.dataSerializationVersion = dataSerializationVersion; + this.flag = flag; + } + + public UnfilteredPartitionIterator makeIterator(ReadCommand command) + { + try (DataInputBuffer in = new DataInputBuffer(data, true)) + { + // Note that the command parameter shadows the 'command' field and this is intended because + // the later can be null (for RemoteDataResponse as those are created in the serializers and + // those don't have easy access to the command). This is also why we need the command as parameter here. + return UnfilteredPartitionIterators.serializerForIntraNode().deserialize(in, + dataSerializationVersion, + command.metadata(), + command.columnFilter(), + flag); + } + catch (IOException e) + { + // We're deserializing in memory so this shouldn't happen + throw new RuntimeException(e); + } + } + + public ByteBuffer repairedDataDigest() + { + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + } + + @Override + public boolean isRepairedDigestConclusive() + { + return true; + } + + @Override + public boolean mayIncludeRepairedDigest() + { + return dataSerializationVersion >= MessagingService.VERSION_40; + } + + @Override + public ReadResponse subrangeResponse(MultiRangeReadCommand command, AbstractBounds range) + { + if (cached == null) + { + try (DataInputBuffer in = new DataInputBuffer(data, true)) + { + @SuppressWarnings("resource") // The close operation is a noop for a deserialized UPI + UnfilteredPartitionIterator iterator = UnfilteredPartitionIterators.serializerForIntraNode() + .deserialize(in, + dataSerializationVersion, + command.metadata(), + command.columnFilter(), + flag); + cached = new LocalResponse(iterator); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + return cached.subrangeResponse(command, range); + } + } + + /** + * A copy of {@code ReadResponse.Serializer} that doesn't support a digest response + */ + private static class Serializer implements IVersionedSerializer + { + public void serialize(ReadResponse response, DataOutputPlus out, int version) throws IOException + { + ByteBuffer digest = ByteBufferUtil.EMPTY_BYTE_BUFFER; + ByteBufferUtil.writeWithVIntLength(digest, out); + if (version >= MessagingService.VERSION_40) + { + ByteBufferUtil.writeWithVIntLength(response.repairedDataDigest(), out); + out.writeBoolean(response.isRepairedDigestConclusive()); + } + ByteBuffer data = ((DataResponse)response).data; + ByteBufferUtil.writeWithVIntLength(data, out); + } + + public ReadResponse deserialize(DataInputPlus in, int version) throws IOException + { + ByteBuffer digest = ByteBufferUtil.readWithVIntLength(in); + assert !digest.hasRemaining(); + + if (version >= MessagingService.VERSION_40) + { + ByteBufferUtil.readWithVIntLength(in); + in.readBoolean(); + } + ByteBuffer data = ByteBufferUtil.readWithVIntLength(in); + return new RemoteDataResponse(data, version); + } + + public long serializedSize(ReadResponse response, int version) + { + ByteBuffer digest = ByteBufferUtil.EMPTY_BYTE_BUFFER; + long size = ByteBufferUtil.serializedSizeWithVIntLength(digest); + + if (version >= MessagingService.VERSION_40) + { + size += ByteBufferUtil.serializedSizeWithVIntLength(response.repairedDataDigest()); + size += 1; + } + assert version >= MessagingService.VERSION_30; + ByteBuffer data = ((DataResponse)response).data; + size += ByteBufferUtil.serializedSizeWithVIntLength(data); + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java index c8d9fd18116d..09abf33b48e3 100644 --- a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java +++ b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.Iterator; +import java.util.SortedSet; import com.google.common.base.Objects; @@ -32,7 +33,7 @@ */ public class MutableDeletionInfo implements DeletionInfo { - private static final long EMPTY_SIZE = ObjectSizes.measure(new MutableDeletionInfo(0, 0)); + protected static final long EMPTY_SIZE = ObjectSizes.measure(new MutableDeletionInfo(0, 0)); /** * This represents a deletion of the entire partition. We can't represent this within the RangeTombstoneList, so it's @@ -85,12 +86,17 @@ public MutableDeletionInfo mutableCopy() @Override public MutableDeletionInfo clone(ByteBufferCloner cloner) + { + return new MutableDeletionInfo(partitionDeletion, copyRanges(cloner)); + } + + @Override + public RangeTombstoneList copyRanges(ByteBufferCloner cloner) { RangeTombstoneList rangesCopy = null; if (ranges != null) - rangesCopy = ranges.clone(cloner); - - return new MutableDeletionInfo(partitionDeletion, rangesCopy); + rangesCopy = ranges.clone(cloner); + return rangesCopy; } /** @@ -160,6 +166,11 @@ public Iterator rangeIterator(Slice slice, boolean reversed) return ranges == null ? Collections.emptyIterator() : ranges.iterator(slice, reversed); } + public Iterator rangeIterator(SortedSet> names, boolean reversed) + { + return ranges == null ? Collections.emptyIterator() : ranges.iterator(names, reversed); + } + public RangeTombstone rangeCovering(Clustering name) { return ranges == null ? null : ranges.search(name); @@ -167,7 +178,7 @@ public RangeTombstone rangeCovering(Clustering name) public int dataSize() { - int size = TypeSizes.sizeof(partitionDeletion.markedForDeleteAt()); + int size = (int) DeletionTime.serializer.serializedSize(partitionDeletion); // small enough so cast is okay return size + (ranges == null ? 0 : ranges.dataSize()); } diff --git a/src/java/org/apache/cassandra/db/Mutation.java b/src/java/org/apache/cassandra/db/Mutation.java index edfa8ee9fa58..2c3aa230483f 100644 --- a/src/java/org/apache/cassandra/db/Mutation.java +++ b/src/java/org/apache/cassandra/db/Mutation.java @@ -18,7 +18,13 @@ package org.apache.cassandra.db; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -44,17 +50,24 @@ import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; import org.apache.cassandra.service.AbstractWriteResponseHandler; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.concurrent.Future; import static org.apache.cassandra.net.MessagingService.VERSION_40; import static org.apache.cassandra.net.MessagingService.VERSION_50; +import static org.apache.cassandra.net.MessagingService.VERSION_DSE_68; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_10; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_11; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_12; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_20; public class Mutation implements IMutation, Supplier { - public static final MutationSerializer serializer = new MutationSerializer(); + public static final MutationSerializer serializer = new MutationSerializer(PartitionUpdate.serializer); // todo this is redundant // when we remove it, also restore SerializationsTest.testMutationRead to not regenerate new Mutations each test @@ -70,6 +83,7 @@ public class Mutation implements IMutation, Supplier final AtomicLong viewLockAcquireStart = new AtomicLong(0); private final boolean cdcEnabled; + private final RequestTracker requestTracker; private static final int SERIALIZATION_VERSION_COUNT = MessagingService.Version.values().length; // Contains serialized representations of this mutation. @@ -97,6 +111,7 @@ public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap modifications) @@ -134,6 +149,11 @@ public String getKeyspaceName() return keyspaceName; } + public Keyspace getKeyspace() + { + return Keyspace.open(keyspaceName); + } + public Collection getTableIds() { return modifications.keySet(); @@ -232,31 +252,29 @@ public static Mutation merge(List mutations) if (updates.isEmpty()) continue; - modifications.put(table, updates.size() == 1 ? updates.get(0) : PartitionUpdate.merge(updates)); + modifications.put(table, PartitionUpdate.merge(updates)); updates.clear(); } return new Mutation(ks, key, modifications.build(), approxTime.now()); } - public Future applyFuture() + public Future applyFuture(WriteOptions writeOptions) { Keyspace ks = Keyspace.open(keyspaceName); - return ks.applyFuture(this, Keyspace.open(keyspaceName).getMetadata().params.durableWrites, true); + return ks.applyFuture(this, writeOptions, true).addListener(f -> { + RequestSensors sensors = requestTracker.get(); + if (sensors != null) + sensors.syncAllSensors(); + }); } - private void apply(Keyspace keyspace, boolean durableWrites, boolean isDroppable) + public void apply(WriteOptions writeOptions) { - keyspace.apply(this, durableWrites, true, isDroppable); - } + Keyspace.open(keyspaceName).apply(this, writeOptions); - public void apply(boolean durableWrites, boolean isDroppable) - { - apply(Keyspace.open(keyspaceName), durableWrites, isDroppable); - } - - public void apply(boolean durableWrites) - { - apply(durableWrites, true); + RequestSensors sensors = requestTracker.get(); + if (sensors != null) + sensors.syncAllSensors(); } /* @@ -265,13 +283,12 @@ public void apply(boolean durableWrites) */ public void apply() { - Keyspace keyspace = Keyspace.open(keyspaceName); - apply(keyspace, keyspace.getMetadata().params.durableWrites, true); + apply(WriteOptions.DEFAULT); } public void applyUnsafe() { - apply(false); + apply(WriteOptions.DEFAULT_WITHOUT_COMMITLOG); } public long getTimeout(TimeUnit unit) @@ -322,6 +339,11 @@ public String toString(boolean shallow) private int serializedSize40; private int serializedSize50; + private int serializedSizeDS10; + private int serializedSizeDS11; + private int serializedSizeDS12; + private int serializedSizeDS20; + private int serializedSizeDSE68; public int serializedSize(int version) { @@ -335,7 +357,26 @@ public int serializedSize(int version) if (serializedSize50 == 0) serializedSize50 = (int) serializer.serializedSize(this, VERSION_50); return serializedSize50; - + case VERSION_DS_10: + if (serializedSizeDS10 == 0) + serializedSizeDS10 = (int) serializer.serializedSize(this, VERSION_DS_10); + return serializedSizeDS10; + case VERSION_DS_11: + if (serializedSizeDS11 == 0) + serializedSizeDS11 = (int) serializer.serializedSize(this, VERSION_DS_11); + return serializedSizeDS11; + case VERSION_DS_12: + if (serializedSizeDS12 == 0) + serializedSizeDS12 = (int) serializer.serializedSize(this, VERSION_DS_12); + return serializedSizeDS12; + case VERSION_DS_20: + if (serializedSizeDS20 == 0) + serializedSizeDS20 = (int) serializer.serializedSize(this, VERSION_DS_20); + return serializedSizeDS20; + case VERSION_DSE_68: + if (serializedSizeDSE68 == 0) + serializedSizeDSE68 = (int) serializer.serializedSize(this, VERSION_DSE_68); + return serializedSizeDSE68; default: throw new IllegalStateException("Unknown serialization version: " + version); } @@ -409,9 +450,16 @@ public interface SimpleBuilder public static class MutationSerializer implements IVersionedSerializer { + private final PartitionUpdate.PartitionUpdateSerializer partitionUpdateSerializer; + + public MutationSerializer(PartitionUpdate.PartitionUpdateSerializer partitionUpdateSerializer) + { + this.partitionUpdateSerializer = partitionUpdateSerializer; + } + public void serialize(Mutation mutation, DataOutputPlus out, int version) throws IOException { - serialization(mutation, version).serialize(PartitionUpdate.serializer, mutation, out, version); + serialization(mutation, version).serialize(partitionUpdateSerializer, mutation, out, version); } /** @@ -444,7 +492,7 @@ private Serialization serialization(Mutation mutation, int version) if (serialization == null) { serialization = new SizeOnlyCacheableSerialization(); - long serializedSize = serialization.serializedSize(PartitionUpdate.serializer, mutation, version); + long serializedSize = serialization.serializedSize(partitionUpdateSerializer, mutation, version); // Excessively large mutation objects cause GC pressure and huge allocations when serialized. // so we only cache serialized mutations when they are below the defined limit. @@ -452,7 +500,7 @@ private Serialization serialization(Mutation mutation, int version) { try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get()) { - serializeInternal(PartitionUpdate.serializer, mutation, dob, version); + serializeInternal(partitionUpdateSerializer, mutation, dob, version); serialization = new CachedSerialization(dob.toByteArray()); } catch (IOException e) @@ -495,7 +543,7 @@ public Mutation deserialize(DataInputPlus in, int version, DeserializationHelper int size = teeIn.readUnsignedVInt32(); assert size > 0; - PartitionUpdate update = PartitionUpdate.serializer.deserialize(teeIn, version, flag); + PartitionUpdate update = partitionUpdateSerializer.deserialize(teeIn, version, flag); if (size == 1) { m = new Mutation(update); @@ -508,7 +556,7 @@ public Mutation deserialize(DataInputPlus in, int version, DeserializationHelper modifications.put(update.metadata().id, update); for (int i = 1; i < size; ++i) { - update = PartitionUpdate.serializer.deserialize(teeIn, version, flag); + update = partitionUpdateSerializer.deserialize(teeIn, version, flag); modifications.put(update.metadata().id, update); } m = new Mutation(update.metadata().keyspace, dk, modifications.build(), approxTime.now()); @@ -529,7 +577,7 @@ public Mutation deserialize(DataInputPlus in, int version) throws IOException public long serializedSize(Mutation mutation, int version) { - return serialization(mutation, version).serializedSize(PartitionUpdate.serializer, mutation, version); + return serialization(mutation, version).serializedSize(partitionUpdateSerializer, mutation, version); } } diff --git a/src/java/org/apache/cassandra/db/MutationVerbHandler.java b/src/java/org/apache/cassandra/db/MutationVerbHandler.java index 6704febf07cb..c533c98d3932 100644 --- a/src/java/org/apache/cassandra/db/MutationVerbHandler.java +++ b/src/java/org/apache/cassandra/db/MutationVerbHandler.java @@ -17,10 +17,26 @@ */ package org.apache.cassandra.db; +import java.util.Collection; +import java.util.stream.Collectors; + +import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.net.*; +import org.apache.cassandra.net.ForwardingInfo; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.ParamType; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.MonotonicClock; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.apache.cassandra.db.commitlog.CommitLogSegment.ENTRY_OVERHEAD_SIZE; @@ -30,10 +46,14 @@ public class MutationVerbHandler extends AbstractMutationVerbHandler { public static final MutationVerbHandler instance = new MutationVerbHandler(); - private void respond(Message respondTo, InetAddressAndPort respondToAddress) + private void respond(RequestSensors requestSensors, Message respondToMessage, InetAddressAndPort respondToAddress) { Tracing.trace("Enqueuing response to {}", respondToAddress); - MessagingService.instance().send(respondTo.emptyResponse(), respondToAddress); + + Message.Builder response = respondToMessage.emptyResponseBuilder(); + // no need to calculate outbound internode bytes because the response is NoPayload + SensorsCustomParams.addSensorsToInternodeResponse(requestSensors, response); + MessagingService.instance().send(response.build(), respondToAddress); } private void failed() @@ -52,6 +72,12 @@ public void doVerb(Message message) } message.payload.validateSize(MessagingService.current_version, ENTRY_OVERHEAD_SIZE); + if (MonotonicClock.Global.approxTime.now() > message.expiresAtNanos()) + { + Tracing.trace("Discarding mutation from {} (timed out)", message.from()); + MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS); + return; + } // Check if there were any forwarding headers in this message ForwardingInfo forwardTo = message.forwardTo(); @@ -72,23 +98,36 @@ public void doVerb(Message message) @Override protected void applyMutation(Message message, InetAddressAndPort respondToAddress) { - message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress), wto -> failed()); + // Initialize the sensor and set ExecutorLocals + RequestSensors requestSensors = SensorsFactory.instance.createRequestSensors(message.payload.getKeyspaceName()); + RequestTracker.instance.set(requestSensors); + + // Initialize internode bytes with the inbound message size: + Collection tables = message.payload.getPartitionUpdates().stream().map(PartitionUpdate::metadata).collect(Collectors.toList()); + for (TableMetadata tm : tables) + { + Context context = Context.from(tm); + requestSensors.registerSensor(context, Type.INTERNODE_BYTES); + requestSensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version) / tables.size()); + } + + message.payload.applyFuture(WriteOptions.DEFAULT).addCallback(o -> respond(requestSensors, message, respondToAddress), wto -> failed()); } private static void forwardToLocalNodes(Message originalMessage, ForwardingInfo forwardTo) { Message.Builder builder = - Message.builder(originalMessage) - .withParam(ParamType.RESPOND_TO, originalMessage.from()) - .withoutParam(ParamType.FORWARD_TO); + Message.builder(originalMessage) + .withParam(ParamType.RESPOND_TO, originalMessage.from()) + .withoutParam(ParamType.FORWARD_TO); // reuse the same Message if all ids are identical (as they will be for 4.0+ node originated messages) Message message = builder.build(); forwardTo.forEach((id, target) -> - { - Tracing.trace("Enqueuing forwarded write to {}", target); - MessagingService.instance().send(message, target); - }); + { + Tracing.trace("Enqueuing forwarded write to {}", target); + MessagingService.instance().send(message, target); + }); } } diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java index 095e2507f4c9..1fe67e52a4ff 100644 --- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java +++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java @@ -23,13 +23,17 @@ import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.lifecycle.View; -import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.partitions.CachedPartition; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; @@ -49,8 +53,6 @@ import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.metrics.TableMetrics; -import org.apache.cassandra.net.Verb; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.tracing.Tracing; @@ -122,6 +124,28 @@ private static PartitionRangeReadCommand create(boolean isDigest, trackWarnings); } + public static PartitionRangeReadCommand create(TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + DataRange dataRange, + Index.QueryPlan indexQueryPlan, + boolean trackWarnings) + { + return new PartitionRangeReadCommand(false, + 0, + false, + metadata, + nowInSec, + columnFilter, + rowFilter, + limits, + dataRange, + indexQueryPlan, + trackWarnings); + } + public static PartitionRangeReadCommand create(TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -165,6 +189,12 @@ public static PartitionRangeReadCommand allDataRead(TableMetadata metadata, long false); } + @Override + public boolean isSinglePartition() + { + return dataRange.isSinglePartition(); + } + public ClusteringIndexFilter clusteringIndexFilter(DecoratedKey key) { return dataRange.clusteringIndexFilter(key); @@ -299,17 +329,22 @@ public boolean isReversed() return dataRange.isReversed(); } + @Override public PartitionIterator execute(ConsistencyLevel consistency, ClientState state, Dispatcher.RequestTime requestTime) throws RequestExecutionException { - return StorageProxy.getRangeSlice(this, consistency, requestTime); + return StorageProxy.getRangeSlice(this, consistency, requestTime, state); } - protected void recordLatency(TableMetrics metric, long latencyNanos) + protected void recordReadLatency(TableMetrics metric, long latencyNanos) { metric.rangeLatency.addNano(latencyNanos); } - @VisibleForTesting + protected void recordReadRequest(TableMetrics metric) + { + metric.rangeRequests.inc(); + } + public UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadExecutionController controller) { ColumnFamilyStore.ViewFragment view = cfs.select(View.selectLive(dataRange().keyRange())); @@ -319,11 +354,15 @@ public UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, Rea InputCollector inputCollector = iteratorsForRange(view, controller); try { + // avoid iterating over the memtable if we purge all tombstones + boolean useMinLocalDeletionTime = cfs.onlyPurgeRepairedTombstones(); + SSTableReadsListener readCountUpdater = newReadCountUpdater(); for (Memtable memtable : view.memtables) { UnfilteredPartitionIterator iter = memtable.partitionIterator(columnFilter(), dataRange(), readCountUpdater); - controller.updateMinOldestUnrepairedTombstone(memtable.getMinLocalDeletionTime()); + if (useMinLocalDeletionTime) + controller.updateMinOldestUnrepairedTombstone(memtable.getMinLocalDeletionTime()); inputCollector.addMemtableIterator(RTBoundValidator.validate(iter, RTBoundValidator.Stage.MEMTABLE, false)); } @@ -439,11 +478,10 @@ public Verb verb() return Verb.RANGE_REQ; } - protected void appendCQLWhereClause(StringBuilder sb) + @Override + public void appendCQLWhereClause(CqlBuilder builder, Redaction redaction) { - String filterString = dataRange().toCQLString(metadata(), rowFilter()); - if (!filterString.isEmpty()) - sb.append(" WHERE ").append(filterString); + PartitionRangeReadQuery.super.appendCQLWhereClause(builder, redaction); } @Override @@ -455,18 +493,6 @@ public String loggableTokens() (dataRange.keyRange.inclusiveRight() ? ']' : ')'); } - /** - * Allow to post-process the result of the query after it has been reconciled on the coordinator - * but before it is passed to the CQL layer to return the ResultSet. - * - * See CASSANDRA-8717 for why this exists. - */ - public PartitionIterator postReconciliationProcessing(PartitionIterator result) - { - Index.QueryPlan queryPlan = indexQueryPlan(); - return queryPlan == null ? result : queryPlan.postProcessor(this).apply(result); - } - @Override public String toString() { diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java b/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java index d91930125d6f..f05499c1c8a9 100644 --- a/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java +++ b/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java @@ -17,6 +17,8 @@ */ package org.apache.cassandra.db; +import java.util.List; + import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; @@ -29,7 +31,7 @@ /** * A {@code ReadQuery} for a range of partitions. */ -public interface PartitionRangeReadQuery extends ReadQuery +public interface PartitionRangeReadQuery extends MultiPartitionReadQuery { static ReadQuery create(TableMetadata table, long nowInSec, @@ -41,6 +43,15 @@ static ReadQuery create(TableMetadata table, return PartitionRangeReadCommand.create(table, nowInSec, columnFilter, rowFilter, limits, dataRange); } + + DataRange dataRange(); + + @Override + default List ranges() + { + return List.of(dataRange()); + } + /** * Creates a new {@code PartitionRangeReadQuery} with the updated limits. * @@ -87,4 +98,5 @@ default boolean selectsFullPartition() return dataRange().selectsAllPartition() && !rowFilter().hasExpressionOnClusteringOrRegularColumns(); } + } diff --git a/src/java/org/apache/cassandra/db/RangeTombstoneList.java b/src/java/org/apache/cassandra/db/RangeTombstoneList.java index 8b8cee2d39bd..b50802115f44 100644 --- a/src/java/org/apache/cassandra/db/RangeTombstoneList.java +++ b/src/java/org/apache/cassandra/db/RangeTombstoneList.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Iterator; +import java.util.SortedSet; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.utils.AbstractIterator; @@ -333,6 +334,14 @@ private RangeTombstone rangeTombstone(int idx) return new RangeTombstone(Slice.make(starts[idx], ends[idx]), DeletionTime.buildUnsafeWithUnsignedInteger(markedAts[idx], delTimesUnsignedIntegers[idx])); } + /** + * Return range tombstone with give clustering and recorded deletion time. + */ + private RangeTombstone rangeTombstone(int idx, Clustering clustering) + { + return new RangeTombstone(Slice.make(clustering), DeletionTime.buildUnsafeWithUnsignedInteger(markedAts[idx], delTimesUnsignedIntegers[idx])); + } + private RangeTombstone rangeTombstoneWithNewStart(int idx, ClusteringBound newStart) { return new RangeTombstone(Slice.make(newStart, ends[idx]), DeletionTime.buildUnsafeWithUnsignedInteger(markedAts[idx], delTimesUnsignedIntegers[idx])); @@ -382,6 +391,36 @@ protected RangeTombstone computeNext() }; } + public Iterator iterator(SortedSet> names, boolean isReversed) + { + return new AbstractIterator() { + + int startIdx = 0; + int endIdx = size; + Iterator> iterator = names.iterator(); + + @Override + protected RangeTombstone computeNext() + { + int idx = -1; + Clustering clustering = null; + + while (idx < 0 && iterator.hasNext()) + { + clustering = iterator.next(); + idx = searchInternal(clustering, startIdx, endIdx); + + if (isReversed) + endIdx = (idx < 0 ? -idx - 2 : idx) + 1; // exclusive + else + startIdx = idx < 0 ? -idx - 1 : idx; + } + + return idx < 0 ? endOfData() : rangeTombstone(idx, clustering); + } + }; + } + public Iterator iterator(final Slice slice, boolean reversed) { return reversed ? reverseIterator(slice) : forwardIterator(slice); diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java index b4be5c764652..8235b5c7375a 100644 --- a/src/java/org/apache/cassandra/db/ReadCommand.java +++ b/src/java/org/apache/cassandra/db/ReadCommand.java @@ -18,12 +18,17 @@ package org.apache.cassandra.db; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.LongPredicate; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.function.Supplier; import javax.annotation.Nullable; @@ -32,21 +37,42 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import io.netty.util.concurrent.FastThreadLocal; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.netty.util.concurrent.FastThreadLocal; -import org.apache.cassandra.config.*; -import org.apache.cassandra.db.filter.*; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.cql3.statements.SelectOptions; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.DataStorageSpec; import org.apache.cassandra.db.transform.BasePartitions; import org.apache.cassandra.db.transform.BaseRows; +import org.apache.cassandra.db.guardrails.Threshold; import org.apache.cassandra.exceptions.QueryCancelledException; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.net.MessageFlag; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.ParamType; import org.apache.cassandra.net.Verb; -import org.apache.cassandra.db.partitions.*; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.LocalReadSizeTooLargeException; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.PurgeFunction; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.db.transform.RTBoundCloser; import org.apache.cassandra.db.transform.RTBoundValidator; import org.apache.cassandra.db.transform.RTBoundValidator.Stage; @@ -54,6 +80,7 @@ import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.exceptions.UnknownIndexException; import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.DataInputPlus; @@ -67,8 +94,10 @@ import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.SchemaProvider; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.read.TrackingRowIterator; import org.apache.cassandra.service.ActiveRepairService; -import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.ClientState; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.CassandraUInt; import org.apache.cassandra.transport.Dispatcher; @@ -78,6 +107,7 @@ import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.filter; + import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.db.partitions.UnfilteredPartitionIterators.MergeListener.NOOP; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; @@ -111,7 +141,9 @@ public abstract class ReadCommand extends AbstractReadQuery protected final DataRange dataRange; @Nullable - private final Index.QueryPlan indexQueryPlan; + protected final Index.QueryPlan indexQueryPlan; + + private Supplier executionInfoSupplier = ExecutionInfo.EMPTY_SUPPLIER; protected static abstract class SelectionDeserializer { @@ -131,7 +163,8 @@ public abstract ReadCommand deserialize(DataInputPlus in, protected enum Kind { SINGLE_PARTITION (SinglePartitionReadCommand.selectionDeserializer), - PARTITION_RANGE (PartitionRangeReadCommand.selectionDeserializer); + PARTITION_RANGE (PartitionRangeReadCommand.selectionDeserializer), + MULTI_RANGE (MultiRangeReadCommand.selectionDeserializer); private final SelectionDeserializer selectionDeserializer; @@ -265,24 +298,52 @@ public boolean isTrackingWarnings() * * @return index query plan chosen for this query */ + @Override @Nullable public Index.QueryPlan indexQueryPlan() { return indexQueryPlan; } + /** + * @return {@code true} if this command uses index-based filtering, {@code false} otherwise + */ + public boolean usesIndexFiltering() + { + return indexQueryPlan != null && indexQueryPlan.usesIndexFiltering(); + } + @Override public boolean isTopK() { return indexQueryPlan != null && indexQueryPlan.isTopK(); } + /** + * @return {@code true} if this command is a BM25 request, {@code false} otherwise + */ + public boolean isBM25() + { + return indexQueryPlan != null && indexQueryPlan.isBM25(); + } + + /** + * @return {@code true} if this command only queries a single partition, {@code false} otherwise. + */ + public abstract boolean isSinglePartition(); + @VisibleForTesting public Index.Searcher indexSearcher() { return indexQueryPlan == null ? null : indexQueryPlan.searcherFor(this); } + @Override + public ExecutionInfo executionInfo() + { + return executionInfoSupplier.get(); + } + /** * The clustering index filter this command to use for the provided key. *

@@ -354,7 +415,8 @@ public ReadCommand copyAsDigestQuery(Iterable replicas) protected abstract ReadCommand copyAsDigestQuery(); - protected abstract UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadExecutionController executionController); + @VisibleForTesting + public abstract UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadExecutionController executionController); /** * Whether the underlying {@code ClusteringIndexFilter} is reversed or not. @@ -377,10 +439,20 @@ public ReadResponse createResponse(UnfilteredPartitionIterator iterator, Repaire public ReadResponse createEmptyResponse() { UnfilteredPartitionIterator iterator = EmptyIterators.unfilteredPartition(metadata()); - + return isDigestQuery() - ? ReadResponse.createDigestResponse(iterator, this) - : ReadResponse.createDataResponse(iterator, this, RepairedDataInfo.NO_OP_REPAIRED_DATA_INFO); + ? ReadResponse.createDigestResponse(iterator, this) + : ReadResponse.createDataResponse(iterator, this, RepairedDataInfo.NO_OP_REPAIRED_DATA_INFO); + } + + public DataLimits.Counter createLimitedCounter(boolean assumeLiveData) + { + return limits().newCounter(nowInSec(), assumeLiveData, selectsFullPartition(), metadata().enforceStrictLiveness()).onlyCount(); + } + + public DataLimits.Counter createUnlimitedCounter(boolean assumeLiveData) + { + return DataLimits.NONE.newCounter(nowInSec(), assumeLiveData, selectsFullPartition(), metadata().enforceStrictLiveness()); } long indexSerializedSize(int version) @@ -390,6 +462,13 @@ long indexSerializedSize(int version) : 0; } + public Index getIndex(ColumnFamilyStore cfs) + { + return null != indexQueryPlan + ? indexQueryPlan.getFirst() + : null; + } + static Index.QueryPlan findIndexQueryPlan(TableMetadata table, RowFilter rowFilter) { if (table.indexes.isEmpty() || rowFilter.isEmpty()) @@ -400,14 +479,8 @@ static Index.QueryPlan findIndexQueryPlan(TableMetadata table, RowFilter rowFilt return cfs.indexManager.getBestIndexQueryPlanFor(rowFilter); } - /** - * If the index manager for the CFS determines that there's an applicable - * 2i that can be used to execute this command, call its (optional) - * validation method to check that nothing in this command's parameters - * violates the implementation specific validation rules. - */ @Override - public void maybeValidateIndex() + public void maybeValidateIndexes() { if (null != indexQueryPlan) { @@ -415,6 +488,12 @@ public void maybeValidateIndex() } } + @Override + public void validateSelectOptions(SelectOptions selectOptions, ClientState state) + { + selectOptions.validate(state, metadata(), limits().count(), IndexRegistry.obtain(metadata()), indexQueryPlan); + } + /** * Executes this command on the local host. * @@ -431,9 +510,8 @@ public UnfilteredPartitionIterator executeLocally(ReadExecutionController execut try { ColumnFamilyStore cfs = Keyspace.openAndGetStore(metadata()); - Index.QueryPlan indexQueryPlan = indexQueryPlan(); - Index.Searcher searcher = null; + if (indexQueryPlan != null) { cfs.indexManager.checkQueryability(indexQueryPlan); @@ -449,7 +527,13 @@ public UnfilteredPartitionIterator executeLocally(ReadExecutionController execut .collect(Collectors.joining(","))); } - UnfilteredPartitionIterator iterator = (null == searcher) ? queryStorage(cfs, executionController) : searcher.search(executionController); + Context context = Context.from(this); + var storageTarget = (null == searcher) ? queryStorage(cfs, executionController) + : searchStorage(searcher, executionController); + // Prepare the monitorable execution info, which will be null if it's deferred to the index + ReadCommandExecutionInfo executionInfo = setupExecutionInfo(searcher); + + UnfilteredPartitionIterator iterator = Transformation.apply(storageTarget, new TrackingRowIterator(context)); iterator = RTBoundValidator.validate(iterator, Stage.MERGED, false); try @@ -457,8 +541,11 @@ public UnfilteredPartitionIterator executeLocally(ReadExecutionController execut iterator = withQuerySizeTracking(iterator); iterator = maybeSlowDownForTesting(iterator); iterator = withQueryCancellation(iterator); + iterator = withReadObserver(iterator); iterator = RTBoundValidator.validate(withoutPurgeableTombstones(iterator, cfs, executionController), Stage.PURGED, false); iterator = withMetricsRecording(iterator, cfs.metric, startTimeNanos); + if (executionInfo != null) + iterator = executionInfo.countFetched(iterator, nowInSec()); // If we've used a 2ndary index, we know the result already satisfy the primary expression used, so // no point in checking it again. @@ -491,8 +578,13 @@ public UnfilteredPartitionIterator executeLocally(ReadExecutionController execut iterator = limits().filter(iterator, nowInSec(), selectsFullPartition()); } - // because of the above, we need to append an aritifical end bound if the source iterator was stopped short by a counter. - return RTBoundCloser.close(iterator); + // because of the above, we need to append an artifical end bound if the source iterator was stopped short by a counter. + iterator = RTBoundCloser.close(iterator); + + if (executionInfo != null) + iterator = executionInfo.countReturned(iterator, nowInSec()); + + return iterator; } catch (RuntimeException | Error e) { @@ -506,18 +598,101 @@ public UnfilteredPartitionIterator executeLocally(ReadExecutionController execut } } - protected abstract void recordLatency(TableMetrics metric, long latencyNanos); + public UnfilteredPartitionIterator withReadObserver(UnfilteredPartitionIterator partitions) + { + ReadObserver observer = ReadObserverFactory.instance.create(this.metadata()); + + // skip if observer is disabled + if (observer == ReadObserver.NO_OP) + return partitions; + + class ReadObserverTransformation extends Transformation + { + @Override + protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) + { + observer.onPartition(partition.partitionKey(), partition.partitionLevelDeletion()); + return Transformation.apply(partition, this); + } + + @Override + protected Row applyToStatic(Row row) + { + if (!row.isEmpty()) + observer.onStaticRow(row); + return row; + } + + @Override + protected Row applyToRow(Row row) + { + observer.onUnfiltered(row); + return row; + } + + @Override + protected RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker) + { + observer.onUnfiltered(marker); + return marker; + } + + @Override + protected void onClose() + { + observer.onComplete(); + } + } + + return Transformation.apply(partitions, new ReadObserverTransformation()); + } + + public UnfilteredPartitionIterator searchStorage(Index.Searcher searcher, ReadExecutionController executionController) + { + return searcher.search(executionController); + } + + protected abstract void recordReadRequest(TableMetrics metric); + protected abstract void recordReadLatency(TableMetrics metric, long latencyNanos); public ReadExecutionController executionController(boolean trackRepairedStatus) { return ReadExecutionController.forCommand(this, trackRepairedStatus); } + /** + * Allow to post-process the result of the query after it has been reconciled on the coordinator + * but before it is passed to the CQL layer to return the ResultSet. + * + * See CASSANDRA-8717 for why this exists. + */ + public PartitionIterator postReconciliationProcessing(PartitionIterator result) + { + return indexQueryPlan == null ? result : indexQueryPlan.postProcessor(this).apply(result); + } + + @Override + public PartitionIterator executeInternal(ReadExecutionController controller) + { + return postReconciliationProcessing(UnfilteredPartitionIterators.filter(executeLocally(controller), nowInSec())); + } + public ReadExecutionController executionController() { return ReadExecutionController.forCommand(this, false); } + /** + * Whether tombstone guardrail ({@link Guardrails#scannedTombstones} should be respected for this query. + * + * @return {@code true} if the tombstone thresholds should be respected for the query. If {@code false}, no + * tombstone warning will ever be logged, and the query will never fail due to tombstones. + */ + protected boolean shouldRespectTombstoneThresholds() + { + return !SchemaConstants.isLocalSystemKeyspace(ReadCommand.this.metadata().keyspace); + } + /** * Wraps the provided iterator so that metrics on what is scanned by the command are recorded. * This also log warning/trow TombstoneOverwhelmingException if appropriate. @@ -526,19 +701,28 @@ private UnfilteredPartitionIterator withMetricsRecording(UnfilteredPartitionIter { class MetricRecording extends Transformation { - private final int failureThreshold = DatabaseDescriptor.getTombstoneFailureThreshold(); - private final int warningThreshold = DatabaseDescriptor.getTombstoneWarnThreshold(); - - private final boolean respectTombstoneThresholds = !SchemaConstants.isLocalSystemKeyspace(ReadCommand.this.metadata().keyspace); private final boolean enforceStrictLiveness = metadata().enforceStrictLiveness(); private int liveRows = 0; private int lastReportedLiveRows = 0; - private int tombstones = 0; - private int lastReportedTombstones = 0; + private final Threshold.GuardedCounter tombstones = createTombstoneCounter(); + private long lastReportedTombstones = 0; private DecoratedKey currentKey; + private Threshold.GuardedCounter createTombstoneCounter() + { + Threshold guardrail = shouldRespectTombstoneThresholds() + ? Guardrails.scannedTombstones + : Threshold.NEVER_TRIGGERED; + return guardrail.newCounter(() -> ReadCommand.this.toCQLString(Redaction.REDACT), false, null); + } + + private MetricRecording() + { + recordReadRequest(metric); + } + @Override public UnfilteredRowIterator applyToPartition(UnfilteredRowIterator iter) { @@ -587,18 +771,18 @@ public RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker) private void countTombstone(ClusteringPrefix clustering) { - ++tombstones; - if (tombstones > failureThreshold && respectTombstoneThresholds) + try + { + tombstones.add(1); + } + catch (InvalidRequestException e) { - String query = ReadCommand.this.toCQLString(); - Tracing.trace("Scanned over {} tombstones for query {}; query aborted (see tombstone_failure_threshold)", failureThreshold, query); metric.tombstoneFailures.inc(); - if (trackWarnings) - { - MessageParams.remove(ParamType.TOMBSTONE_WARNING); - MessageParams.add(ParamType.TOMBSTONE_FAIL, tombstones); - } - throw new TombstoneOverwhelmingException(tombstones, query, ReadCommand.this.metadata(), currentKey, clustering); + throw new TombstoneOverwhelmingException(tombstones.get(), + ReadCommand.this.toCQLString(Redaction.REDACT), + ReadCommand.this.metadata(), + currentKey, + clustering); } } @@ -606,7 +790,7 @@ private void countTombstone(ClusteringPrefix clustering) protected void onPartitionClose() { int lr = liveRows - lastReportedLiveRows; - int ts = tombstones - lastReportedTombstones; + long ts = tombstones.get() - lastReportedTombstones; if (lr > 0) metric.topReadPartitionRowCount.addSample(currentKey.getKey(), lr); @@ -615,38 +799,18 @@ protected void onPartitionClose() metric.topReadPartitionTombstoneCount.addSample(currentKey.getKey(), ts); lastReportedLiveRows = liveRows; - lastReportedTombstones = tombstones; + lastReportedTombstones = tombstones.get(); } @Override public void onClose() { - recordLatency(metric, nanoTime() - startTimeNanos); - - metric.tombstoneScannedHistogram.update(tombstones); - metric.liveScannedHistogram.update(liveRows); - - boolean warnTombstones = tombstones > warningThreshold && respectTombstoneThresholds; - if (warnTombstones) - { - String msg = String.format( - "Read %d live rows and %d tombstone cells for query %1.512s; token %s (see tombstone_warn_threshold)", - liveRows, tombstones, ReadCommand.this.toCQLString(), currentKey.getToken()); - if (trackWarnings) - MessageParams.add(ParamType.TOMBSTONE_WARNING, tombstones); - else - ClientWarn.instance.warn(msg); - if (tombstones < failureThreshold) - { - metric.tombstoneWarnings.inc(); - } + recordReadLatency(metric, nanoTime() - startTimeNanos); - logger.warn(msg); - } + metric.incLiveRows(liveRows); + metric.incTombstones(tombstones.get(), tombstones.checkAndTriggerWarning()); - Tracing.trace("Read {} live rows and {} tombstone cells{}", - liveRows, tombstones, - (warnTombstones ? " (see tombstone_warn_threshold)" : "")); + Tracing.trace("Read {} live rows and {} tombstone ones", liveRows, tombstones.get()); } } @@ -715,7 +879,7 @@ private void addSize(long size) if (failBytes != -1 && this.sizeInBytes >= failBytes) { String msg = String.format("Query %s attempted to read %d bytes but max allowed is %s; query aborted (see local_read_size_fail_threshold)", - ReadCommand.this.toCQLString(), this.sizeInBytes, failThreshold); + ReadCommand.this.toCQLString(Redaction.REDACT), this.sizeInBytes, failThreshold); Tracing.trace(msg); MessageParams.remove(ParamType.LOCAL_READ_SIZE_WARN); MessageParams.add(ParamType.LOCAL_READ_SIZE_FAIL, this.sizeInBytes); @@ -801,6 +965,7 @@ private UnfilteredPartitionIterator withQueryCancellation(UnfilteredPartitionIte /** * A transformation used for simulating slow queries by tests. */ + @VisibleForTesting private static class DelayInjector extends Transformation { @Override @@ -859,12 +1024,10 @@ protected boolean hasPartitionLevelDeletions(SSTableReader sstable) public abstract Verb verb(); - protected abstract void appendCQLWhereClause(StringBuilder sb); - // Skip purgeable tombstones. We do this because it's safe to do (post-merge of the memtable and sstable at least), it // can save us some bandwith, and avoid making us throw a TombstoneOverwhelmingException for purgeable tombstones (which // are to some extend an artefact of compaction lagging behind and hence counting them is somewhat unintuitive). - protected UnfilteredPartitionIterator withoutPurgeableTombstones(UnfilteredPartitionIterator iterator, + protected UnfilteredPartitionIterator withoutPurgeableTombstones(UnfilteredPartitionIterator iterator, ColumnFamilyStore cfs, ReadExecutionController controller) { @@ -873,7 +1036,7 @@ class WithoutPurgeableTombstones extends PurgeFunction public WithoutPurgeableTombstones() { super(nowInSec(), cfs.gcBefore(nowInSec()), controller.oldestUnrepairedTombstone(), - cfs.getCompactionStrategyManager().onlyPurgeRepairedTombstones(), + cfs.onlyPurgeRepairedTombstones(), iterator.metadata().enforceStrictLiveness()); } @@ -890,12 +1053,6 @@ protected LongPredicate getPurgeEvaluator() */ public abstract String loggableTokens(); - // Monitorable interface - public String name() - { - return toCQLString(); - } - InputCollector iteratorsForPartition(ColumnFamilyStore.ViewFragment view, ReadExecutionController controller) { final BiFunction, RepairedDataInfo, UnfilteredRowIterator> merge = @@ -953,7 +1110,7 @@ static class InputCollector { this.repairedDataInfo = controller.getRepairedDataInfo(); this.isTrackingRepairedStatus = controller.isTrackingRepairedStatus(); - + if (isTrackingRepairedStatus) { for (SSTableReader sstable : view.sstables) @@ -1140,7 +1297,7 @@ public void serialize(ReadCommand command, DataOutputPlus out, int version) thro if (command.isDigestQuery()) out.writeUnsignedVInt32(command.digestVersion()); command.metadata().id.serialize(out); - out.writeInt(version >= MessagingService.VERSION_50 ? CassandraUInt.fromLong(command.nowInSec()) : (int) command.nowInSec()); + out.writeInt(MessagingService.Version.supportsExtendedDeletionTime(version) ? CassandraUInt.fromLong(command.nowInSec()) : (int) command.nowInSec()); ColumnFilter.serializer.serialize(command.columnFilter(), out, version); RowFilter.serializer.serialize(command.rowFilter(), out, version); DataLimits.serializer.serialize(command.limits(), out, version, command.metadata().comparator); @@ -1163,17 +1320,31 @@ public ReadCommand deserialize(DataInputPlus in, int version) throws IOException // better complain loudly than doing the wrong thing. if (isForThrift(flags)) throw new IllegalStateException("Received a command with the thrift flag set. " - + "This means thrift is in use in a mixed 3.0/3.X and 4.0+ cluster, " - + "which is unsupported. Make sure to stop using thrift before " - + "upgrading to 4.0"); + + "This means thrift is in use in a mixed 3.0/3.X and 4.0+ cluster, " + + "which is unsupported. Make sure to stop using thrift before " + + "upgrading to 4.0"); boolean hasIndex = hasIndex(flags); int digestVersion = isDigest ? in.readUnsignedVInt32() : 0; boolean needsReconciliation = needsReconciliation(flags); TableMetadata metadata = schema.getExistingTableMetadata(TableId.deserialize(in)); - long nowInSec = version >= MessagingService.VERSION_50 ? CassandraUInt.toLong(in.readInt()) : in.readInt(); + long nowInSec = MessagingService.Version.supportsExtendedDeletionTime(version) ? CassandraUInt.toLong(in.readInt()) : in.readInt(); ColumnFilter columnFilter = ColumnFilter.serializer.deserialize(in, version, metadata); + + // add synthetic columns to the tablemetadata so we can serialize them in our response + var tmb = metadata.unbuild(); + for (var it = columnFilter.fetchedColumns().regulars.simpleColumns(); it.hasNext(); ) + { + var c = it.next(); + // synthetic columns sort first, so when we hit the first non-synthetic, we're done + if (!c.isSynthetic()) + break; + assert c.sythenticSourceColumn != null; + tmb.addColumn(c); + } + metadata = tmb.build(); + RowFilter rowFilter = RowFilter.serializer.deserialize(in, version, metadata, needsReconciliation); DataLimits limits = DataLimits.serializer.deserialize(in, version, metadata); @@ -1181,16 +1352,19 @@ public ReadCommand deserialize(DataInputPlus in, int version) throws IOException if (hasIndex) { IndexMetadata index = deserializeIndexMetadata(in, version, metadata); - Index.Group indexGroup = Keyspace.openAndGetStore(metadata).indexManager.getIndexGroup(index); - if (indexGroup != null) - indexQueryPlan = indexGroup.queryPlanFor(rowFilter); + if (index != null) + { + Index.Group indexGroup = Keyspace.openAndGetStore(metadata).indexManager.getIndexGroup(index); + if (indexGroup != null) + indexQueryPlan = indexGroup.queryPlanFor(rowFilter); + } } return kind.selectionDeserializer.deserialize(in, version, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan); } - private IndexMetadata deserializeIndexMetadata(DataInputPlus in, int version, TableMetadata metadata) throws IOException + private @Nullable IndexMetadata deserializeIndexMetadata(DataInputPlus in, int version, TableMetadata metadata) throws IOException { try { @@ -1220,4 +1394,28 @@ public long serializedSize(ReadCommand command, int version) + command.indexSerializedSize(version); } } + + @Nullable + private ReadCommandExecutionInfo setupExecutionInfo(Index.Searcher searcher) + { + // if we have a searcher, it may use its own custom execution info instead of the generic one + if (searcher != null) + { + Supplier searcherExecutionInfoSupplier = searcher.monitorableExecutionInfo(); + if (searcherExecutionInfoSupplier != null) + { + executionInfoSupplier = searcherExecutionInfoSupplier; + return null; + } + } + + // if execution info is disabled, return null so we will keep using the default empty supplier + if (!CassandraRelevantProperties.MONITORING_EXECUTION_INFO_ENABLED.getBoolean()) + return null; + + // otherwise, create and use the generic execution info + ReadCommandExecutionInfo commandExecutionInfo = new ReadCommandExecutionInfo(); + executionInfoSupplier = () -> commandExecutionInfo; + return commandExecutionInfo; + } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/ReadCommandExecutionInfo.java b/src/java/org/apache/cassandra/db/ReadCommandExecutionInfo.java new file mode 100644 index 000000000000..577edd6d8633 --- /dev/null +++ b/src/java/org/apache/cassandra/db/ReadCommandExecutionInfo.java @@ -0,0 +1,146 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.db.monitoring.Monitorable; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.index.Index; + +/** + * A custom {@link Monitorable.ExecutionInfo} implementation for {@link ReadCommand}, to be used unless there is an + * {@link Index.Searcher} with its own custom implementation. + *

+ * It holds and prints the number of partitions, rows and tombstones fetched and returned by the command. + *

+ * Deleted partitions are considered as a partition tombstone. + * Deleted rows and range tombstone markers are considered as row tombstones. + */ +@NotThreadSafe +class ReadCommandExecutionInfo implements Monitorable.ExecutionInfo +{ + private long partitionsFetched = 0; + private long partitionsReturned = 0; + private long partitionTombstones = 0; + private long rowsFetched = 0; + private long rowsReturned = 0; + private long rowTombstones = 0; + + /** + * Counts the number of fetched partitions and rows in the specified iterator. + * + * @param partitions the iterator of fetched partitions to count + * @param nowInSec the command's time in seconds, used to evaluate whether a partition/row is alive + * @return the same iterator + */ + UnfilteredPartitionIterator countFetched(UnfilteredPartitionIterator partitions, long nowInSec) + { + Transformation rowCounter = new Transformation<>() { + @Override + protected Row applyToRow(Row row) + { + if (row.hasLiveData(nowInSec, false)) + rowsFetched++; + return row; + } + }; + return Transformation.apply(partitions, new Transformation<>() { + @Override + protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) + { + if (!partition.partitionLevelDeletion().deletes(nowInSec)) + partitionsFetched++; + return Transformation.apply(partition, rowCounter); + } + }); + } + + /** + * Counts the number of fetched partitions, rows and tombstones in the specified iterator. + * + * @param partitions the iterator of returned partitions to count + * @param nowInSec the command's time in seconds, used to evaluate whether a partition/row is alive + * @return the same iterator + */ + UnfilteredPartitionIterator countReturned(UnfilteredPartitionIterator partitions, long nowInSec) + { + Transformation rowCounter = new Transformation<>() { + @Override + protected Row applyToRow(Row row) + { + if (row.hasLiveData(nowInSec, false)) + rowsReturned++; + else + rowTombstones++; + return row; + } + + @Override + protected RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker) + { + rowTombstones++; + return marker; + } + }; + return Transformation.apply(partitions, new Transformation<>() { + @Override + protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) + { + if (partition.partitionLevelDeletion().deletes(nowInSec)) + partitionTombstones++; + else + partitionsReturned++; + return Transformation.apply(partition, rowCounter); + } + }); + } + + @Override + public String toLogString(boolean unique) + { + StringBuilder sb = new StringBuilder("\n"); + sb.append(INDENT); + sb.append(unique ? "Fetched/returned/tombstones:" : "Slowest fetched/returned/tombstones:"); + append(sb, "partitions", + partitionsFetched, + partitionsReturned, + partitionTombstones); + append(sb, "rows", + rowsFetched, + rowsReturned, + rowTombstones); + return sb.toString(); + } + + private static void append(StringBuilder sb, String name, long fetched, long returned, long tombstones) + { + sb.append('\n') + .append(DOUBLE_INDENT) + .append(name) + .append(": ") + .append(fetched) + .append('/') + .append(returned) + .append('/') + .append(tombstones); + } +} diff --git a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java index 58d45c998f3f..8153b48fb097 100644 --- a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java +++ b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java @@ -19,10 +19,13 @@ import java.util.concurrent.TimeUnit; +import com.google.common.base.Preconditions; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -31,7 +34,14 @@ import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.RequestTracker; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.NoSpamLogger; @@ -47,9 +57,21 @@ public class ReadCommandVerbHandler implements IVerbHandler public void doVerb(Message message) { - if (StorageService.instance.isBootstrapMode()) + TableMetadata metadata = message.payload.metadata(); + if (metadata.isVirtual()) + { + if (StorageService.instance.isBootstrapMode()) + { + throw new RuntimeException("Cannot service reads while bootstrapping!"); + } + } + else { - throw new RuntimeException("Cannot service reads while bootstrapping!"); + ColumnFamilyStore cfs = Keyspace.openAndGetStore(metadata); + if (!cfs.isReadyToServeData()) + { + throw new RuntimeException("Cannot service reads while bootstrapping!"); + } } ReadCommand command = message.payload; @@ -80,6 +102,16 @@ public void doVerb(Message message) validateTransientStatus(message); MessageParams.reset(); + // Initialize the sensor and set ExecutorLocals + RequestSensors requestSensors = SensorsFactory.instance.createRequestSensors(command.metadata().keyspace); + Context context = Context.from(command); + requestSensors.registerSensor(context, Type.READ_BYTES); + RequestTracker.instance.set(requestSensors); + + // Initialize internode bytes with the inbound message size: + requestSensors.registerSensor(context, Type.INTERNODE_BYTES); + requestSensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + long timeout = message.expiresAtNanos() - message.createdAtNanos(); command.setMonitoringTime(message.createdAtNanos(), message.isCrossNode(), timeout, DatabaseDescriptor.getSlowQueryTimeout(NANOSECONDS)); @@ -94,7 +126,10 @@ public void doVerb(Message message) } catch (RejectException e) { - if (!command.isTrackingWarnings()) + // TombstoneOverwhelmingException should always be propagated as a failure, + // even when tracking warnings, as it indicates a guardrail violation that + // must be reported to the client + if (!command.isTrackingWarnings() || e instanceof TombstoneOverwhelmingException) throw e; // make sure to log as the exception is swallowed @@ -109,19 +144,25 @@ public void doVerb(Message message) } catch (AssertionError t) { - throw new AssertionError(String.format("Caught an error while trying to process the command: %s", command.toCQLString()), t); + throw new AssertionError(String.format("Caught an error while trying to process the command: %s", command.toRedactedCQLString()), t); } catch (QueryCancelledException e) { logger.debug("Query cancelled (timeout)", e); response = null; - assert !command.isCompleted() : "Read marked as completed despite being aborted by timeout to table " + command.metadata(); + Preconditions.checkState(!command.isCompleted(), "Read marked as completed despite being aborted by timeout to table %s", command.metadata()); } if (command.complete()) { + Message.Builder replyBuilder = message.responseWithBuilder(response); + int size = replyBuilder.currentPayloadSize(MessagingService.current_version); + requestSensors.incrementSensor(context, Type.INTERNODE_BYTES, size); + requestSensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(requestSensors, replyBuilder); + Tracing.trace("Enqueuing response to {}", message.from()); - Message reply = message.responseWith(response); + Message reply = replyBuilder.build(); reply = MessageParams.addToMessage(reply); MessagingService.instance().send(reply, message.from()); } @@ -141,8 +182,10 @@ private void validateTransientStatus(Message message) if (command instanceof SinglePartitionReadCommand) token = ((SinglePartitionReadCommand) command).partitionKey().getToken(); - else + else if (command instanceof PartitionRangeReadCommand) token = ((PartitionRangeReadCommand) command).dataRange().keyRange().right.getToken(); + else + return; Replica replica = Keyspace.open(command.metadata().keyspace) .getReplicationStrategy() @@ -150,6 +193,7 @@ private void validateTransientStatus(Message message) if (replica == null) { + // it's fine for serverless which unloads stale sstables, SEE VECTOR-30 if (command.isTopK()) return; diff --git a/src/java/org/apache/cassandra/db/ReadExecutionController.java b/src/java/org/apache/cassandra/db/ReadExecutionController.java index 8a62ea390d3e..994c55a1d464 100644 --- a/src/java/org/apache/cassandra/db/ReadExecutionController.java +++ b/src/java/org/apache/cassandra/db/ReadExecutionController.java @@ -22,9 +22,16 @@ import com.google.common.annotations.VisibleForTesting; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Snapshot; import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.index.Index; +import org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.context.OperationContext; +import org.apache.cassandra.service.context.OperationContextTracker; +import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.MonotonicClock; import org.apache.cassandra.utils.concurrent.OpOrder; @@ -49,6 +56,8 @@ public class ReadExecutionController implements AutoCloseable private final RepairedDataInfo repairedDataInfo; private long oldestUnrepairedTombstone = Long.MAX_VALUE; + private final Histogram sstablesScannedPerRowRead; + ReadExecutionController(ReadCommand command, OpOrder.Group baseOp, TableMetadata baseMetadata, @@ -67,6 +76,11 @@ public class ReadExecutionController implements AutoCloseable this.command = command; this.createdAtNanos = createdAtNanos; + // This is expensive to create, and since this is on the query hot path, we must only make it when we need it. + this.sstablesScannedPerRowRead = Tracing.isTracing() + ? new Histogram(new DecayingEstimatedHistogramReservoir(true)) + : null; + if (trackRepairedStatus) { DataLimits.Counter repairedReadCount = command.limits().newCounter(command.nowInSec(), @@ -79,6 +93,9 @@ public class ReadExecutionController implements AutoCloseable { repairedDataInfo = RepairedDataInfo.NO_OP_REPAIRED_DATA_INFO; } + + if (Tracing.isTracing()) + Tracing.instance.setRangeQuery(isRangeCommand()); } public boolean isRangeCommand() @@ -133,6 +150,8 @@ static ReadExecutionController forCommand(ReadCommand command, boolean trackRepa long createdAtNanos = baseCfs.metric.topLocalReadQueryTime.isEnabled() ? clock.now() : NO_SAMPLING; + OperationContextTracker.start(OperationContext.FACTORY.forRead(command, baseCfs)); + if (indexCfs == null) return new ReadExecutionController(command, baseCfs.readOrdering.start(), baseCfs.metadata(), null, null, createdAtNanos, trackRepairedStatus); @@ -166,6 +185,7 @@ static ReadExecutionController forCommand(ReadCommand command, boolean trackRepa if (indexController != null) indexController.close(); } + OperationContextTracker.endCurrent(); throw e; } } @@ -207,9 +227,21 @@ public void close() } } + OperationContextTracker.endCurrent(); + if (createdAtNanos != NO_SAMPLING) addSample(); - } + + if (sstablesScannedPerRowRead != null) + { + Snapshot sstablesHistogram = sstablesScannedPerRowRead.getSnapshot(); + Tracing.trace("Scanned {} rows; average {} sstables scanned per row with stdev {} and max {}", + sstablesScannedPerRowRead.getCount(), + sstablesHistogram.getMean(), + sstablesHistogram.getStdDev(), + sstablesHistogram.getMax()); + } +} public boolean isTrackingRepairedStatus() { @@ -227,7 +259,7 @@ public boolean isRepairedDataDigestConclusive() { return repairedDataInfo.isConclusive(); } - + public RepairedDataInfo getRepairedDataInfo() { return repairedDataInfo; @@ -235,10 +267,18 @@ public RepairedDataInfo getRepairedDataInfo() private void addSample() { - String cql = command.toCQLString(); + String cql = command.toCQLString(Redaction.REDACT); int timeMicros = (int) Math.min(TimeUnit.NANOSECONDS.toMicros(clock.now() - createdAtNanos), Integer.MAX_VALUE); ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(baseMetadata.id); if (cfs != null) cfs.metric.topLocalReadQueryTime.addSample(cql, timeMicros); } + + public void updateSstablesIteratedPerRow(int mergedSSTablesIterated) + { + if (sstablesScannedPerRowRead != null) + { + sstablesScannedPerRowRead.update(mergedSSTablesIterated); + } + } } diff --git a/src/java/org/apache/cassandra/db/ReadObserver.java b/src/java/org/apache/cassandra/db/ReadObserver.java new file mode 100644 index 000000000000..65767af435fe --- /dev/null +++ b/src/java/org/apache/cassandra/db/ReadObserver.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db; + +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; + +/** + * An interface that allows to capture what local data has been read + *

+ * This is used by CNDB remote file cache warmup strategy to track access pattern + */ +public interface ReadObserver +{ + ReadObserver NO_OP = new ReadObserver() {}; + + /** + * Called on every partition read + * + * @param partitionKey the partition key + * @param deletionTime partition deletion time + */ + default void onPartition(DecoratedKey partitionKey, DeletionTime deletionTime) {} + + /** + * Called on every static row read. + * + * @param staticRow static row of the partition + */ + default void onStaticRow(Row staticRow) {} + + /** + * Called on every unfiltered read. + * + * @param unfiltered either row or range tombstone. + */ + default void onUnfiltered(Unfiltered unfiltered) {} + + /** + * Called on read request completion + */ + default void onComplete() {} +} diff --git a/src/java/org/apache/cassandra/db/ReadObserverFactory.java b/src/java/org/apache/cassandra/db/ReadObserverFactory.java new file mode 100644 index 000000000000..a0539eb70eeb --- /dev/null +++ b/src/java/org/apache/cassandra/db/ReadObserverFactory.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_READ_OBSERVER_FACTORY; + + +/** + * Provides custom factory that creates a {@link ReadObserver} instance per read request + */ +public interface ReadObserverFactory +{ + ReadObserverFactory instance = CUSTOM_READ_OBSERVER_FACTORY.getString() == null ? + new ReadObserverFactory() {} : + FBUtilities.construct(CassandraRelevantProperties.CUSTOM_READ_OBSERVER_FACTORY.getString(), "custom read observer factory"); + + default ReadObserver create(TableMetadata table) + { + return ReadObserver.NO_OP; + } +} diff --git a/src/java/org/apache/cassandra/db/ReadQuery.java b/src/java/org/apache/cassandra/db/ReadQuery.java index ee383b963194..28d73f8095c6 100644 --- a/src/java/org/apache/cassandra/db/ReadQuery.java +++ b/src/java/org/apache/cassandra/db/ReadQuery.java @@ -17,11 +17,15 @@ */ package org.apache.cassandra.db; +import javax.annotation.Nullable; + +import org.apache.cassandra.cql3.statements.SelectOptions; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.partitions.*; import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.index.Index; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.pager.QueryPager; @@ -134,8 +138,8 @@ default DataRange dataRange() /** * Starts a new read operation. *

- * This must be called before {@link #executeInternal} and passed to it to protect the read. - * The returned object must be closed on all path and it is thus strongly advised to + * This must be called before {@link #executeInternal(ReadExecutionController)} and passed to it to protect the read. + * The returned object must be closed on all paths, and it is thus strongly advised to * use it in a try-with-ressource construction. * * @return a newly started execution controller for this {@code ReadQuery}. @@ -256,7 +260,7 @@ public default boolean isEmpty() * validation method to check that nothing in this query's parameters * violates the implementation specific validation rules. */ - default void maybeValidateIndex() + default void maybeValidateIndexes() { } @@ -265,13 +269,28 @@ default void trackWarnings() } /** - * The query is a top-k query if the query has an {@link org.apache.cassandra.index.Index.QueryPlan} that - * supports top-k ordering. - * - * @return {@code true} if this is a top-k query + * Validates the specified {@link SelectOptions} against this query. + */ + default void validateSelectOptions(SelectOptions selectOptions, ClientState state) + { + } + + /** + * @return true given read query is a top-k request */ default boolean isTopK() { return false; } + + /** + * Index query plan chosen for this query. Can be null. + * + * @return index query plan chosen for this query + */ + @Nullable + default Index.QueryPlan indexQueryPlan() + { + return null; + } } diff --git a/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java b/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java index a34be9d9c1ac..20956448bf28 100644 --- a/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java +++ b/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java @@ -38,7 +38,7 @@ public void doVerb(Message message) throws IOException @Override void applyMutation(Message message, InetAddressAndPort respondToAddress) { - message.payload.apply(); + message.payload.apply(WriteOptions.FOR_READ_REPAIR); MessagingService.instance().send(message.emptyResponse(), respondToAddress); } } diff --git a/src/java/org/apache/cassandra/db/ReadResponse.java b/src/java/org/apache/cassandra/db/ReadResponse.java index a9e2cec4a725..61527a7ac8f2 100644 --- a/src/java/org/apache/cassandra/db/ReadResponse.java +++ b/src/java/org/apache/cassandra/db/ReadResponse.java @@ -80,6 +80,18 @@ public static ReadResponse createDigestResponse(UnfilteredPartitionIterator data public abstract boolean isDigestResponse(); + /** + * Indicates whether this response type supports response size tracking for metrics. + * Some response types (like MultiRangeReadResponse) may not support payload size calculation + * and will throw UnsupportedOperationException when attempting to serialize for size calculation. + * + * @return true if this response supports size tracking, false otherwise + */ + public boolean supportsResponseSizeTracking() + { + return true; + } + /** * Creates a string of the requested partition in this read response suitable for debugging. */ diff --git a/src/java/org/apache/cassandra/db/RegularAndStaticColumns.java b/src/java/org/apache/cassandra/db/RegularAndStaticColumns.java index b6da183d013f..55533eda0e97 100644 --- a/src/java/org/apache/cassandra/db/RegularAndStaticColumns.java +++ b/src/java/org/apache/cassandra/db/RegularAndStaticColumns.java @@ -163,7 +163,7 @@ public Builder add(ColumnMetadata c) } else { - assert c.isRegular(); + assert c.isRegular() || c.isSynthetic(); if (regularColumns == null) regularColumns = BTree.builder(naturalOrder()); regularColumns.add(c); @@ -197,7 +197,7 @@ public Builder addAll(RegularAndStaticColumns columns) public RegularAndStaticColumns build() { - return new RegularAndStaticColumns(staticColumns == null ? Columns.NONE : Columns.from(staticColumns), + return new RegularAndStaticColumns(staticColumns == null ? Columns.NONE : Columns.from(staticColumns), regularColumns == null ? Columns.NONE : Columns.from(regularColumns)); } } diff --git a/src/java/org/apache/cassandra/db/RepairedDataInfo.java b/src/java/org/apache/cassandra/db/RepairedDataInfo.java index 1f03654d25c7..347e2faf7a3d 100644 --- a/src/java/org/apache/cassandra/db/RepairedDataInfo.java +++ b/src/java/org/apache/cassandra/db/RepairedDataInfo.java @@ -39,7 +39,7 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime; @NotThreadSafe -class RepairedDataInfo +public class RepairedDataInfo { public static final RepairedDataInfo NO_OP_REPAIRED_DATA_INFO = new RepairedDataInfo(null) { @@ -332,7 +332,7 @@ private static class RepairedDataPurger extends PurgeFunction super(nowInSec, cfs.gcBefore(nowInSec), oldestUnrepairedTombstone, - cfs.getCompactionStrategyManager().onlyPurgeRepairedTombstones(), + cfs.onlyPurgeRepairedTombstones(), cfs.metadata.get().enforceStrictLiveness()); } diff --git a/src/java/org/apache/cassandra/db/SSTableImporter.java b/src/java/org/apache/cassandra/db/SSTableImporter.java index 8ad79003d37b..a3b50a951ff6 100644 --- a/src/java/org/apache/cassandra/db/SSTableImporter.java +++ b/src/java/org/apache/cassandra/db/SSTableImporter.java @@ -28,14 +28,18 @@ import java.util.UUID; import com.google.common.annotations.VisibleForTesting; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.TargetParser; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.IVerifier; @@ -44,6 +48,9 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.OutputHandler; @@ -79,6 +86,15 @@ synchronized List importNewSSTables(Options options) UUID importID = UUID.randomUUID(); logger.info("[{}] Loading new SSTables for {}/{}: {}", importID, cfs.getKeyspaceName(), cfs.getTableName(), options); + try + { + abortIfDraining(); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + List> listers = getSSTableListers(options.srcPaths); Set currentDescriptors = new HashSet<>(); @@ -107,23 +123,39 @@ synchronized List importNewSSTables(Options options) Index.Group saiIndexGroup = cfs.indexManager.getIndexGroup(StorageAttachedIndexGroup.GROUP_KEY); if (saiIndexGroup != null) { - IndexDescriptor indexDescriptor = IndexDescriptor.create(descriptor, - cfs.getPartitioner(), - cfs.metadata().comparator); - String keyspace = cfs.getKeyspaceName(); String table = cfs.getTableName(); - if (!indexDescriptor.isPerSSTableIndexBuildComplete()) + SSTableReader reader = SSTableReader.open(cfs, descriptor); + StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs); + if (group == null) + throw new IllegalStateException(String.format("Missing SAI index group to import for SSTable %s on %s.%s", + descriptor.toString(), + keyspace, + table)); + + IndexDescriptor indexDescriptor = group.descriptorFor(reader); + if (!indexDescriptor.perSSTableComponents().isComplete()) throw new IllegalStateException(String.format("Missing SAI index to import for SSTable %s on %s.%s", - indexDescriptor.sstableDescriptor.toString(), + descriptor.toString(), keyspace, table)); for (Index index : saiIndexGroup.getIndexes()) { - IndexIdentifier indexIdentifier = new IndexIdentifier(keyspace, table, index.getIndexMetadata().name); - if (!indexDescriptor.isPerColumnIndexBuildComplete(indexIdentifier)) + TableMetadata tableMetadata = cfs.metadata(); + IndexMetadata indexMetadata = index.getIndexMetadata(); + Pair target = TargetParser.parse(tableMetadata, indexMetadata); + IndexContext indexContext = new IndexContext(tableMetadata.keyspace, + tableMetadata.name, + tableMetadata.id, + tableMetadata.partitionKeyType, + tableMetadata.comparator, + target.left, + target.right, + indexMetadata, + cfs); + if (!indexDescriptor.perIndexComponents(indexContext).isComplete()) throw new IllegalStateException(String.format("Missing SAI index to import for index %s on %s.%s", index.getIndexMetadata().name, keyspace, @@ -227,7 +259,7 @@ synchronized List importNewSSTables(Options options) if (!cfs.indexManager.validateSSTableAttachedIndexes(newSSTables, false, options.validateIndexChecksum)) cfs.indexManager.buildSSTableAttachedIndexesBlocking(newSSTables); - cfs.getTracker().addSSTables(newSSTables); + cfs.getTracker().addSSTables(newSSTables, OperationType.UNKNOWN); for (SSTableReader reader : newSSTables) { if (options.invalidateCaches && cfs.isRowCacheEnabled()) @@ -302,7 +334,7 @@ private File getTargetDirectory(String srcPath, Descriptor descriptor, Set> getSSTableListers(Set srcPaths; - private final boolean resetLevel; - private final boolean clearRepaired; - private final boolean verifySSTables; - private final boolean verifyTokens; - private final boolean invalidateCaches; - private final boolean extendedVerify; - private final boolean copyData; - private final boolean failOnMissingIndex; - public final boolean validateIndexChecksum; + final Set srcPaths; + final boolean resetLevel; + final boolean clearRepaired; + final boolean verifySSTables; + final boolean verifyTokens; + final boolean invalidateCaches; + final boolean extendedVerify; + final boolean copyData; + final boolean failOnMissingIndex; + final boolean validateIndexChecksum; public Options(Set srcPaths, boolean resetLevel, boolean clearRepaired, boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches, diff --git a/src/java/org/apache/cassandra/db/SerializationHeader.java b/src/java/org/apache/cassandra/db/SerializationHeader.java index 841f7b305198..8fc93a64aa54 100644 --- a/src/java/org/apache/cassandra/db/SerializationHeader.java +++ b/src/java/org/apache/cassandra/db/SerializationHeader.java @@ -19,15 +19,29 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CompositeType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.exceptions.InvalidColumnTypeException; import org.apache.cassandra.exceptions.UnknownColumnException; +import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.sstable.metadata.IMetadataComponentSerializer; @@ -42,6 +56,8 @@ public class SerializationHeader { + private final static Logger logger = LoggerFactory.getLogger(SerializationHeader.class); + public static final Serializer serializer = new Serializer(); private final boolean isForSSTable; @@ -82,7 +98,7 @@ public static SerializationHeader make(TableMetadata metadata, Collection> staticColumns = new LinkedHashMap<>(); - Map> regularColumns = new LinkedHashMap<>(); + LinkedHashMap> staticColumns = new LinkedHashMap<>(); + LinkedHashMap> regularColumns = new LinkedHashMap<>(); for (ColumnMetadata column : columns.statics) staticColumns.put(column.name.bytes, column.type); for (ColumnMetadata column : columns.regulars) @@ -272,14 +299,14 @@ public static class Component extends MetadataComponent { private final AbstractType keyType; private final List> clusteringTypes; - private final Map> staticColumns; - private final Map> regularColumns; + private final LinkedHashMap> staticColumns; + private final LinkedHashMap> regularColumns; private final EncodingStats stats; private Component(AbstractType keyType, List> clusteringTypes, - Map> staticColumns, - Map> regularColumns, + LinkedHashMap> staticColumns, + LinkedHashMap> regularColumns, EncodingStats stats) { this.keyType = keyType; @@ -289,12 +316,186 @@ private Component(AbstractType keyType, this.stats = stats; } + /** + * Only exposed for {@link org.apache.cassandra.io.sstable.SSTableHeaderFix}. + */ + public static Component buildComponentForTools(AbstractType keyType, + List> clusteringTypes, + LinkedHashMap> staticColumns, + LinkedHashMap> regularColumns, + EncodingStats stats) + { + return new Component(keyType, clusteringTypes, staticColumns, regularColumns, stats); + } + public MetadataType getType() { return MetadataType.HEADER; } - public SerializationHeader toHeader(TableMetadata metadata) throws UnknownColumnException + private static AbstractType validateAndMaybeFixColumnType(String description, + TableMetadata metadata, + ByteBuffer columnName, + AbstractType type, + boolean allowImplicitlyFrozenTuples, + boolean isForOfflineTool) + { + boolean dropped = metadata.getDroppedColumn(columnName) != null; + boolean isPrimaryKeyColumn = Iterables.any(metadata.primaryKeyColumns(), cd -> cd.name.bytes.equals(columnName)); + + try + { + type.validateForColumn(columnName, isPrimaryKeyColumn, metadata.isCounter(), dropped, isForOfflineTool); + return type; + } + catch (InvalidColumnTypeException e) + { + // Always try to fix tuple types regardless of allowImplicitlyFrozenTuples. Tuples are + // always implicitly frozen in CQL, so a multi-cell tuple indicates old data that needs fixing. + // The allowImplicitlyFrozenTuples flag controls whether to allow implicit freezing for OTHER types + // (like UDTs), but tuples must always be frozen to ensure consistent column ordering. + boolean shouldTryFix = allowImplicitlyFrozenTuples || isForOfflineTool || type.isTuple(); + AbstractType fixed = shouldTryFix ? tryFix(type, columnName, isPrimaryKeyColumn, metadata.isCounter(), dropped, isForOfflineTool) : null; + if (fixed == null) + { + // We don't know how to fix. We throw an error here because reading such table may result in corruption + String msg = String.format("Error reading SSTable header %s, the type for column %s in %s is %s, which is invalid (%s); " + + "The type could not be automatically fixed.", + description, ColumnIdentifier.toCQLString(columnName), metadata, type.asCQL3Type().toSchemaString(), + e.getMessage()); + throw new IllegalArgumentException(msg, e); + } + else + { + // For dropped tuple columns, use the schema's dropped column type to determine the correct + // isMultiCell. We cannot rely on the SSTable header's isMultiCell because old SSTable formats + // (e.g. C41) may incorrectly record plain tuples (TupleType, not UserType) as multi-cell + // even though they were always implicitly frozen (single-cell) in CQL. The schema's dropped + // column type is the authoritative source because it preserves the original column type at + // drop time: plain tuples are recorded + // as frozen> (single-cell), while non-frozen UDTs are recorded as the UDT type + // (multi-cell). If the schema entry is missing, we keep the tryFix default (frozen for tuples). + if (dropped && fixed.isTuple()) + { + ColumnMetadata droppedColumn = metadata.getDroppedColumn(columnName); + if (droppedColumn != null && droppedColumn.type.isMultiCell() != fixed.isMultiCell()) + { + logger.debug("Adjusting dropped column {} isMultiCell from {} to {} to match schema", + ColumnIdentifier.toCQLString(columnName), fixed.isMultiCell(), droppedColumn.type.isMultiCell()); + fixed = fixed.with(fixed.subTypes(), droppedColumn.type.isMultiCell()); + } + } + logger.debug("Error reading SSTable header {}, the type for column {} in {} is {}, which is " + + "invalid ({}); The type has been automatically fixed to {}, but please contact " + + "support if this is incorrect", + description, ColumnIdentifier.toCQLString(columnName), metadata, type.asCQL3Type().toSchemaString(), + e.getMessage(), fixed.asCQL3Type().toSchemaString()); + return fixed; + } + } + } + + /** + * Attempts to return a "fixed" (and thus valid) version of the type. Doing is so is only possible in restrained + * case where we know why the type is invalid and are confident we know what it should be. + * + * @return if we know how to auto-magically fix the invalid type that triggered this exception, the hopefully + * fixed version of said type. Otherwise, {@code null}. + */ + public static AbstractType tryFix(AbstractType invalidType, ByteBuffer name, boolean isPrimaryKeyColumn, boolean isCounterTable, boolean isDroppedColumn, boolean isForOfflineTool) + { + AbstractType fixed = tryFixInternal(invalidType, isPrimaryKeyColumn, isDroppedColumn); + if (fixed != null) + { + try + { + // Make doubly sure the fixed type is valid before returning it. + fixed.validateForColumn(name, isPrimaryKeyColumn, isCounterTable, isDroppedColumn, isForOfflineTool); + return fixed; + } + catch (InvalidColumnTypeException e2) + { + // Continue as if we hadn't been able to fix, since we haven't + } + } + return null; + } + + private static AbstractType tryFixInternal(AbstractType invalidType, boolean isPrimaryKeyColumn, boolean isDroppedColumn) + { + if (isPrimaryKeyColumn) + { + // The only issue we have a fix to in that case if the type is not frozen; we can then just freeze it. + if (invalidType.isMultiCell()) + return invalidType.freeze(); + } + else + { + // Here again, it's mainly issues of frozen-ness that are fixable, namely multi-cell types that either: + // - are plain tuples (TupleType, not UserType) which _should_ be frozen. In which case we freeze it. + // - has non-frozen subtypes. In which case, we just freeze all subtypes. + if (invalidType.isMultiCell()) + { + // For tuples, default to frozen (isMultiCell=false) since plain tuples (TupleType) in CQL are + // always implicitly frozen. For dropped columns, validateAndMaybeFixColumnType will adjust + // the isMultiCell to match the schema's dropped column type, which preserves the original + // frozen status from before the column was dropped. + boolean isMultiCell = !invalidType.isTuple(); + return invalidType.with(AbstractType.freeze(invalidType.subTypes()), isMultiCell); + } + + } + // In other case, we don't know how to fix (at least somewhat auto-magically) and will have to fail. + return null; + } + + private static AbstractType validateAndMaybeFixPartitionKeyType(String descriptor, + TableMetadata metadata, + AbstractType fullType, + boolean allowImplicitlyFrozenTuples, + boolean isForOfflineTool) + { + List pkColumns = metadata.partitionKeyColumns(); + int pkCount = pkColumns.size(); + + if (pkCount == 1) + return validateAndMaybeFixColumnType(descriptor, metadata, pkColumns.get(0).name.bytes, fullType, allowImplicitlyFrozenTuples, isForOfflineTool); + + List> subTypes = fullType.subTypes(); + assert fullType instanceof CompositeType && subTypes.size() == pkCount + : String.format("In %s, got %s as table %s partition key type but partition key is %s", + descriptor, fullType, metadata, pkColumns); + + return CompositeType.getInstance(validateAndMaybeFixPKTypes(descriptor, metadata, pkColumns, subTypes, allowImplicitlyFrozenTuples, isForOfflineTool)); + } + + private static List> validateAndMaybeFixPKTypes(String descriptor, + TableMetadata table, + List pkColumns, + List> pkTypes, + boolean allowImplicitlyFrozenTuples, + boolean isForOfflineTool) + { + int count = pkTypes.size(); + List> updated = new ArrayList<>(count); + for (int i = 0; i < count; i++) + { + updated.add(validateAndMaybeFixColumnType(descriptor, + table, + pkColumns.get(i).name.bytes, + pkTypes.get(i), + allowImplicitlyFrozenTuples, + isForOfflineTool)); + } + return updated; + } + + public SerializationHeader toHeader(Descriptor descriptor, TableMetadata metadata) throws UnknownColumnException + { + return toHeader(descriptor.toString(), metadata, descriptor.version, false); + } + + public SerializationHeader toHeader(String descriptor, TableMetadata metadata, Version sstableVersion, boolean isForOfflineTool) throws UnknownColumnException { Map> typeMap = new HashMap<>(staticColumns.size() + regularColumns.size()); @@ -305,8 +506,9 @@ public SerializationHeader toHeader(TableMetadata metadata) throws UnknownColumn for (Map.Entry> e : map.entrySet()) { ByteBuffer name = e.getKey(); - AbstractType other = typeMap.put(name, e.getValue()); - if (other != null && !other.equals(e.getValue())) + AbstractType type = validateAndMaybeFixColumnType(descriptor, metadata, name, e.getValue(), sstableVersion.hasImplicitlyFrozenTuples(), isForOfflineTool); + AbstractType other = typeMap.put(name, type); + if (other != null && !other.equals(type)) throw new IllegalStateException("Column " + name + " occurs as both regular and static with types " + other + "and " + e.getValue()); ColumnMetadata column = metadata.getColumn(name); @@ -319,15 +521,39 @@ public SerializationHeader toHeader(TableMetadata metadata) throws UnknownColumn // If we don't find the definition, it could be we have data for a dropped column, and we shouldn't // fail deserialization because of that. So we grab a "fake" ColumnDefinition that ensure proper - // deserialization. The column will be ignore later on anyway. - column = metadata.getDroppedColumn(name, isStatic); - if (column == null) + // deserialization. The column will be ignored later on anyway. + ColumnMetadata droppedColumn = metadata.getDroppedColumn(name, isStatic); + if (droppedColumn == null) throw new UnknownColumnException("Unknown column " + UTF8Type.instance.getString(name) + " during deserialization"); + + // Use the SSTable's validated type for the dropped column metadata instead of the + // schema's type. This is critical because column ordering depends on isComplex() which depends + // on the type's isMultiCell(). The SSTable's type has been validated and adjusted above + // (via tryFix + schema-based isMultiCell correction for tuples), so it reflects the correct + // frozen status for deserialization. Using the schema's type directly could have a different + // isMultiCell, causing column order mismatches and data corruption. + // We must also expand user types since droppedColumn() asserts that the type has no UDT refs. + // expandUserTypes() preserves the isMultiCell() property which is what we need. + AbstractType expandedType = type.expandUserTypes(); + column = ColumnMetadata.droppedColumn(droppedColumn.ksName, + droppedColumn.cfName, + droppedColumn.name, + expandedType, + droppedColumn.kind, + droppedColumn.getMask()); } builder.add(column); } } + AbstractType keyType = validateAndMaybeFixPartitionKeyType(descriptor, metadata, this.keyType, sstableVersion.hasImplicitlyFrozenTuples(), isForOfflineTool); + List> clusteringTypes = validateAndMaybeFixPKTypes(descriptor, + metadata, + metadata.clusteringColumns(), + this.clusteringTypes, + sstableVersion.hasImplicitlyFrozenTuples(), + isForOfflineTool); + return new SerializationHeader(true, keyType, clusteringTypes, builder.build(), stats, typeMap); } @@ -382,6 +608,28 @@ public EncodingStats getEncodingStats() { return stats; } + + @SuppressWarnings("unused") + public Component withMigratedKeyspaces(Map keyspaceMapping) + { + if (keyspaceMapping.isEmpty()) + return this; + + AbstractType newKeyType = keyType.overrideKeyspace(ks -> keyspaceMapping.getOrDefault(ks, ks)); + List> clusteringTypes = this.clusteringTypes.stream().map(t -> t.overrideKeyspace(ks -> keyspaceMapping.getOrDefault(ks, ks))).collect(Collectors.toList()); + LinkedHashMap> staticColumns = this.staticColumns.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().overrideKeyspace(ks -> keyspaceMapping.getOrDefault(ks, ks)), + (a, b) -> { throw new IllegalArgumentException("Duplicate key"); }, + LinkedHashMap::new)); + LinkedHashMap> regularColumns = this.regularColumns.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().overrideKeyspace(ks -> keyspaceMapping.getOrDefault(ks, ks)), + (a, b) -> { throw new IllegalArgumentException("Duplicate key"); }, + LinkedHashMap::new)); + return new Component(newKeyType, clusteringTypes, staticColumns, regularColumns, stats); + } + } public static class Serializer implements IMetadataComponentSerializer @@ -467,8 +715,8 @@ public Component deserialize(Version version, DataInputPlus in) throws IOExcepti AbstractType keyType = typeSerializer.deserialize(in); List> clusteringTypes = typeSerializer.deserializeList(in); - Map> staticColumns = readColumnsWithType(in); - Map> regularColumns = readColumnsWithType(in); + LinkedHashMap> staticColumns = readColumnsWithType(in); + LinkedHashMap> regularColumns = readColumnsWithType(in); return new Component(keyType, clusteringTypes, staticColumns, regularColumns, stats); } @@ -507,10 +755,10 @@ private long sizeofColumnsWithTypes(Map> columns) return size; } - private Map> readColumnsWithType(DataInputPlus in) throws IOException + private LinkedHashMap> readColumnsWithType(DataInputPlus in) throws IOException { int length = in.readUnsignedVInt32(); - Map> typeMap = new LinkedHashMap<>(length); + LinkedHashMap> typeMap = new LinkedHashMap<>(length); for (int i = 0; i < length; i++) { ByteBuffer name = ByteBufferUtil.readWithVIntLength(in); @@ -519,4 +767,4 @@ private Map> readColumnsWithType(DataInputPlus in) t return typeMap; } } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/SimpleBuilders.java b/src/java/org/apache/cassandra/db/SimpleBuilders.java index 3564eb1f100a..5466c7b2856d 100644 --- a/src/java/org/apache/cassandra/db/SimpleBuilders.java +++ b/src/java/org/apache/cassandra/db/SimpleBuilders.java @@ -223,7 +223,7 @@ public PartitionUpdate build() // Note that rowBuilders.size() could include the static column so could be 1 off the really need capacity // of the final PartitionUpdate, but as that's just a sizing hint, we'll live. - PartitionUpdate.Builder update = new PartitionUpdate.Builder(metadata, key, columns.build(), rowBuilders.size()); + PartitionUpdate.Builder update = PartitionUpdate.builder(metadata, key, columns.build(), rowBuilders.size()); update.addPartitionDeletion(partitionDeletion); if (rangeBuilders != null) diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java index 06122f027e8c..159995f0b122 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java @@ -35,7 +35,10 @@ import org.apache.cassandra.cache.IRowCacheEntry; import org.apache.cassandra.cache.RowCacheKey; import org.apache.cassandra.cache.RowCacheSentinel; +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; @@ -44,6 +47,7 @@ import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.View; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.partitions.CachedBTreePartition; import org.apache.cassandra.db.partitions.CachedPartition; @@ -81,6 +85,7 @@ import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.btree.BTreeSet; @@ -226,7 +231,22 @@ public static SinglePartitionReadCommand create(TableMetadata metadata, limits, partitionKey, clusteringIndexFilter, - findIndexQueryPlan(metadata, rowFilter)); + findIndexQueryPlan(metadata, rowFilter, clusteringIndexFilter)); + } + + private static Index.QueryPlan findIndexQueryPlan(TableMetadata table, + RowFilter rowFilter, + ClusteringIndexFilter clusteringIndexFilter) + { + // We can skip the indexes if the query is for a specific set of primary keys, there is no ordering, + // and there are no hints telling us otherwise. + if (CassandraRelevantProperties.SKIP_INDEXES_ON_FULL_PRIMARY_KEYS.getBoolean() + && clusteringIndexFilter instanceof ClusteringIndexNamesFilter + && rowFilter.indexHints.included.isEmpty() + && !rowFilter.hasOrdering()) + return null; + + return findIndexQueryPlan(table, rowFilter); } /** @@ -470,21 +490,32 @@ public SinglePartitionReadCommand forPaging(Clustering lastReturned, DataLimi return cmd; } + @Override + public boolean isSinglePartition() + { + return true; + } + @Override public PartitionIterator execute(ConsistencyLevel consistency, ClientState state, Dispatcher.RequestTime requestTime) throws RequestExecutionException { if (clusteringIndexFilter.isEmpty(metadata().comparator)) return EmptyIterators.partition(); - return StorageProxy.read(Group.one(this), consistency, requestTime); + return StorageProxy.read(Group.one(this), consistency, state, requestTime); } - protected void recordLatency(TableMetrics metric, long latencyNanos) + protected void recordReadLatency(TableMetrics metric, long latencyNanos) { metric.readLatency.addNano(latencyNanos); } - protected UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadExecutionController executionController) + protected void recordReadRequest(TableMetrics metric) + { + metric.readRequests.inc(); + } + + public UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadExecutionController executionController) { // skip the row cache and go directly to sstables/memtable if repaired status of // data is being tracked. This is only requested after an initial digest mismatch @@ -530,7 +561,7 @@ private UnfilteredRowIterator getThroughCache(ColumnFamilyStore cfs, ReadExecuti cfs.metric.rowCacheHit.inc(); Tracing.trace("Row cache hit"); UnfilteredRowIterator unfilteredRowIterator = clusteringIndexFilter().getUnfilteredRowIterator(columnFilter(), cachedPartition); - cfs.metric.updateSSTableIterated(0); + cfs.metric.updateSSTableIterated(0, 0, 0); return unfilteredRowIterator; } @@ -658,28 +689,37 @@ public Unfiltered next() public UnfilteredRowIterator queryMemtableAndDisk(ColumnFamilyStore cfs, ReadExecutionController executionController) { assert executionController != null && executionController.validForReadOn(cfs); - Tracing.trace("Executing single-partition query on {}", cfs.name); + if (Tracing.traceSinglePartitions()) + Tracing.trace("Executing single-partition query on {}; stage READ pending: {}, active: {}", cfs.name, Stage.READ.getPendingTaskCount(), Stage.READ.getActiveTaskCount()); Tracing.trace("Acquiring sstable references"); ColumnFamilyStore.ViewFragment view = cfs.select(View.select(SSTableSet.LIVE, partitionKey())); - return queryMemtableAndDiskInternal(cfs, view, null, executionController); + return queryMemtableAndDisk(cfs, view, null, executionController); } public UnfilteredRowIterator queryMemtableAndDisk(ColumnFamilyStore cfs, ColumnFamilyStore.ViewFragment view, - Function>> rowTransformer, + Function>> rowTransformer, ReadExecutionController executionController) { assert executionController != null && executionController.validForReadOn(cfs); - Tracing.trace("Executing single-partition query on {}", cfs.name); + if (Tracing.traceSinglePartitions()) + Tracing.trace("Executing single-partition query on {}; stage READ pending: {}, active: {}", cfs.name, Stage.READ.getPendingTaskCount(), Stage.READ.getActiveTaskCount()); + + return queryMemtableAndDiskInternal(cfs, view, rowTransformer, executionController, Clock.Global.nanoTime()); + } - return queryMemtableAndDiskInternal(cfs, view, rowTransformer, executionController); + private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs, ReadExecutionController controller, long startTimeNanos) + { + var view = cfs.select(View.select(SSTableSet.LIVE, partitionKey())); + return queryMemtableAndDiskInternal(cfs, view, null, controller, startTimeNanos); } private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs, ColumnFamilyStore.ViewFragment view, - Function>> rowTransformer, - ReadExecutionController controller) + Function>> rowTransformer, + ReadExecutionController controller, + long startTimeNanos) { /* * We have 2 main strategies: @@ -703,9 +743,12 @@ private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs && !queriesMulticellType() && !controller.isTrackingRepairedStatus()) { - return queryMemtableAndSSTablesInTimestampOrder(cfs, view, rowTransformer, (ClusteringIndexNamesFilter)clusteringIndexFilter(), controller); + return queryMemtableAndSSTablesInTimestampOrder(cfs, view, rowTransformer, (ClusteringIndexNamesFilter)clusteringIndexFilter(), controller, startTimeNanos); } + if (Tracing.traceSinglePartitions()) + Tracing.trace("Acquiring sstable references"); + view.sstables.sort(SSTableReader.maxTimestampDescending); ClusteringIndexFilter filter = clusteringIndexFilter(); long minTimestamp = Long.MAX_VALUE; @@ -777,21 +820,15 @@ private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs continue; } + UnfilteredRowIterator iter; if (intersects || hasRequiredStatics) { if (!sstable.isRepaired()) controller.updateMinOldestUnrepairedTombstone(sstable.getMinLocalDeletionTime()); // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator - UnfilteredRowIterator iter = intersects ? makeRowIteratorWithLowerBound(cfs, sstable, metricsCollector) + iter = intersects ? makeRowIteratorWithLowerBound(cfs, sstable, metricsCollector) : makeRowIteratorWithSkippedNonStaticContent(cfs, sstable, metricsCollector); - - if (rowTransformer != null) - iter = Transformation.apply(iter, rowTransformer.apply(sstable.getId())); - - inputCollector.addSSTableIterator(sstable, iter); - mostRecentPartitionTombstone = Math.max(mostRecentPartitionTombstone, - iter.partitionLevelDeletion().markedForDeleteAt()); } else { @@ -803,7 +840,7 @@ private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs // an iterator figure out that (see `StatsMetadata.hasPartitionLevelDeletions`) // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator - UnfilteredRowIterator iter = makeRowIteratorWithSkippedNonStaticContent(cfs, sstable, metricsCollector); + iter = makeRowIteratorWithSkippedNonStaticContent(cfs, sstable, metricsCollector); // if the sstable contains a partition delete, then we must include it regardless of whether it // shadows any other data seen locally as we can't guarantee that other replicas have seen it @@ -812,22 +849,26 @@ private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs if (!sstable.isRepaired()) controller.updateMinOldestUnrepairedTombstone(sstable.getMinLocalDeletionTime()); - if (rowTransformer != null) - iter = Transformation.apply(iter, rowTransformer.apply(sstable.getId())); - - inputCollector.addSSTableIterator(sstable, iter); includedDueToTombstones++; - mostRecentPartitionTombstone = Math.max(mostRecentPartitionTombstone, - iter.partitionLevelDeletion().markedForDeleteAt()); } else { iter.close(); + iter = null; } } + if (iter != null) + { + + if (rowTransformer != null) + iter = Transformation.apply(iter, rowTransformer.apply(sstable.getId())); + inputCollector.addSSTableIterator(sstable, iter); + mostRecentPartitionTombstone = Math.max(mostRecentPartitionTombstone, + iter.partitionLevelDeletion().markedForDeleteAt()); + } } - if (Tracing.isTracing()) + if (Tracing.traceSinglePartitions()) Tracing.trace("Skipped {}/{} non-slice-intersecting sstables, included {} due to tombstones", nonIntersectingSSTables, view.sstables.size(), includedDueToTombstones); @@ -837,7 +878,7 @@ private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs StorageHook.instance.reportRead(cfs.metadata().id, partitionKey()); List iterators = inputCollector.finalizeIterators(cfs, nowInSec(), controller.oldestUnrepairedTombstone()); - return withSSTablesIterated(iterators, cfs.metric, metricsCollector); + return withSSTablesIterated(iterators, controller, view.sstables.size(), cfs.metric, metricsCollector, startTimeNanos); } catch (RuntimeException | Error e) { @@ -905,28 +946,41 @@ private UnfilteredRowIterator makeRowIteratorWithSkippedNonStaticContent(ColumnF * would cause all iterators to be initialized and hence all sstables to be accessed. */ private UnfilteredRowIterator withSSTablesIterated(List iterators, + ReadExecutionController controller, + int totalIntersectingSSTables, TableMetrics metrics, - SSTableReadMetricsCollector metricsCollector) + SSTableReadMetricsCollector metricsCollector, + long startTimeNanos) { UnfilteredRowIterator merged = UnfilteredRowIterators.merge(iterators); - if (!merged.isEmpty()) + return withSSTablesIterated(merged, controller, totalIntersectingSSTables, metrics, metricsCollector, startTimeNanos); + } + + private UnfilteredRowIterator withSSTablesIterated(UnfilteredRowIterator iterator, + ReadExecutionController controller, + int totalIntersectingSSTables, + TableMetrics metrics, + SSTableReadMetricsCollector metricsCollector, + long startTimeNanos) + { + if (!iterator.isEmpty()) { - DecoratedKey key = merged.partitionKey(); + DecoratedKey key = iterator.partitionKey(); metrics.topReadPartitionFrequency.addSample(key.getKey(), 1); metrics.topReadPartitionSSTableCount.addSample(key.getKey(), metricsCollector.getMergedSSTables()); } class UpdateSstablesIterated extends Transformation { - public void onPartitionClose() - { - int mergedSSTablesIterated = metricsCollector.getMergedSSTables(); - metrics.updateSSTableIterated(mergedSSTablesIterated); - Tracing.trace("Merged data from memtables and {} sstables", mergedSSTablesIterated); - } + public void onPartitionClose() + { + int mergedSSTablesIterated = metricsCollector.getMergedSSTables(); + metrics.updateSSTableIterated(mergedSSTablesIterated, totalIntersectingSSTables, Clock.Global.nanoTime() - startTimeNanos); + controller.updateSstablesIteratedPerRow(mergedSSTablesIterated); + } } - return Transformation.apply(merged, new UpdateSstablesIterated()); + return Transformation.apply(iterator, new UpdateSstablesIterated()); } private boolean queriesMulticellType() @@ -948,12 +1002,17 @@ private boolean queriesMulticellType() * no collection or counters are included). * This method assumes the filter is a {@code ClusteringIndexNamesFilter}. */ - private UnfilteredRowIterator queryMemtableAndSSTablesInTimestampOrder(ColumnFamilyStore cfs, ColumnFamilyStore.ViewFragment view, Function>> rowTransformer, ClusteringIndexNamesFilter filter, ReadExecutionController controller) + private UnfilteredRowIterator queryMemtableAndSSTablesInTimestampOrder(ColumnFamilyStore cfs, ColumnFamilyStore.ViewFragment view, Function>> rowTransformer, ClusteringIndexNamesFilter filter, ReadExecutionController controller, long startTimeNanos) { + if (Tracing.traceSinglePartitions()) + Tracing.trace("Acquiring sstable references"); + ImmutableBTreePartition result = null; SSTableReadMetricsCollector metricsCollector = new SSTableReadMetricsCollector(); - Tracing.trace("Merging memtable contents"); + if (Tracing.traceSinglePartitions()) + Tracing.trace("Merging memtable contents"); + for (Memtable memtable : view.memtables) { try (UnfilteredRowIterator iter = memtable.rowIterator(partitionKey, filter.getSlices(metadata()), columnFilter(), isReversed(), metricsCollector)) @@ -1039,14 +1098,14 @@ private UnfilteredRowIterator queryMemtableAndSSTablesInTimestampOrder(ColumnFam UnfilteredRowIterator wrapped = rowTransformer != null ? Transformation.apply(iter, rowTransformer.apply(sstable.getId())) : iter; result = add(RTBoundValidator.validate(wrapped, RTBoundValidator.Stage.SSTABLE, false), - result, - filter, - sstable.isRepaired(), - controller); + result, + filter, + sstable.isRepaired(), + controller); } } - cfs.metric.updateSSTableIterated(metricsCollector.getMergedSSTables()); + cfs.metric.updateSSTableIterated(metricsCollector.getMergedSSTables(), view.sstables.size(), Clock.Global.nanoTime() - startTimeNanos); if (result == null || result.isEmpty()) return EmptyIterators.unfilteredRow(metadata(), partitionKey(), false); @@ -1056,7 +1115,9 @@ private UnfilteredRowIterator queryMemtableAndSSTablesInTimestampOrder(ColumnFam cfs.metric.topReadPartitionSSTableCount.addSample(key.getKey(), metricsCollector.getMergedSSTables()); StorageHook.instance.reportRead(cfs.metadata.id, partitionKey()); - return result.unfilteredIterator(columnFilter(), Slices.ALL, clusteringIndexFilter().isReversed()); + UnfilteredRowIterator iterator = result.unfilteredIterator(columnFilter(), Slices.ALL, clusteringIndexFilter().isReversed()); + return withSSTablesIterated(iterator, controller, view.sstables.size(), cfs.metric, metricsCollector, startTimeNanos); + } private ImmutableBTreePartition add(UnfilteredRowIterator iter, ImmutableBTreePartition result, ClusteringIndexNamesFilter filter, boolean isRepaired, ReadExecutionController controller) @@ -1223,17 +1284,9 @@ public Verb verb() } @Override - protected void appendCQLWhereClause(StringBuilder sb) + public void appendCQLWhereClause(CqlBuilder builder, Redaction redaction) { - sb.append(" WHERE ").append(partitionKey().toCQLString(metadata())); - - String filterString = clusteringIndexFilter().toCQLString(metadata(), rowFilter()); - if (!filterString.isEmpty()) - { - if (!clusteringIndexFilter().selectsAllPartition() || !rowFilter().isEmpty()) - sb.append(" AND "); - sb.append(filterString); - } + SinglePartitionReadQuery.super.appendCQLWhereClause(builder, redaction); } @Override @@ -1311,7 +1364,12 @@ public static Group create(List commands, DataLimits public PartitionIterator execute(ConsistencyLevel consistency, ClientState state, Dispatcher.RequestTime requestTime) throws RequestExecutionException { - return StorageProxy.read(this, consistency, requestTime); + return StorageProxy.read(this, consistency, state, requestTime); + } + + public PartitionIterator postReconciliationProcessing(PartitionIterator result) + { + return queries.isEmpty() ? result : queries.get(0).postReconciliationProcessing(result); } } @@ -1366,6 +1424,12 @@ private static final class SSTableReadMetricsCollector implements SSTableReadsLi */ private int mergedSSTables; + @Override + public void onSSTablePartitionIndexAccessed(SSTableReader sstable) + { + sstable.incrementIndexReadCount(); + } + @Override public void onSSTableSelected(SSTableReader sstable, SelectionReason reason) { @@ -1429,4 +1493,4 @@ public ReadExecutionController executionController(boolean trackRepairedStatus) return executionController(); } } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java b/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java index 5409cde8c493..846e64881596 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java @@ -26,14 +26,18 @@ import org.apache.commons.lang3.tuple.Pair; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.cql3.statements.SelectOptions; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.pager.MultiPartitionPager; import org.apache.cassandra.service.pager.PagingState; import org.apache.cassandra.service.pager.QueryPager; @@ -154,6 +158,19 @@ default boolean selectsClustering(DecoratedKey key, Clustering clustering) return rowFilter().clusteringKeyRestrictionsAreSatisfiedBy(clustering); } + default void appendCQLWhereClause(CqlBuilder builder, Redaction redaction) + { + builder.append(" WHERE "); + + // Append the partition key restrictions. + TableMetadata metadata = metadata(); + builder.append(partitionKey().toCQLString(metadata, redaction)); + + // Append the clustering index filter and the row filter. + String filter = clusteringIndexFilter().toCQLString(metadata(), rowFilter(), redaction); + builder.appendRestrictions(filter, true); + } + /** * Groups multiple single partition read queries. */ @@ -163,6 +180,7 @@ abstract class Group implements ReadQuery private final DataLimits limits; private final long nowInSec; private final boolean selectsFullPartitions; + private final boolean isTopK; public Group(List queries, DataLimits limits) { @@ -172,15 +190,21 @@ public Group(List queries, DataLimits limits) T firstQuery = queries.get(0); this.nowInSec = firstQuery.nowInSec(); this.selectsFullPartitions = firstQuery.selectsFullPartition(); - for (int i = 1; i < queries.size(); i++) - assert queries.get(i).nowInSec() == nowInSec; + this.isTopK = firstQuery.isTopK(); + + for (T query : queries) + { + assert query.nowInSec() == nowInSec; + assert query.selectsFullPartition() == selectsFullPartitions; + assert query.isTopK() == isTopK; + } } @Override - public void maybeValidateIndex() + public void maybeValidateIndexes() { for (ReadQuery query : queries) - query.maybeValidateIndex(); + query.maybeValidateIndexes(); } public long nowInSec() @@ -204,6 +228,21 @@ public boolean selectsFullPartition() return selectsFullPartitions; } + @Override + public boolean isTopK() + { + return isTopK; + } + + @Override + public void validateSelectOptions(SelectOptions selectOptions, ClientState state) + { + for (T query : queries) + { + query.validateSelectOptions(selectOptions, state); + } + } + public ReadExecutionController executionController() { // Note that the only difference between the queries in a group must be the partition key on which diff --git a/src/java/org/apache/cassandra/db/Slice.java b/src/java/org/apache/cassandra/db/Slice.java index 1fc60ba1f727..c5216f13cef8 100644 --- a/src/java/org/apache/cassandra/db/Slice.java +++ b/src/java/org/apache/cassandra/db/Slice.java @@ -84,7 +84,7 @@ public static Slice make(ClusteringBound start, ClusteringBound end) public static Slice make(ClusteringComparator comparator, Object... values) { - CBuilder builder = CBuilder.create(comparator); + ClusteringBuilder builder = ClusteringBuilder.create(comparator); for (Object val : values) { if (val instanceof ByteBuffer) diff --git a/src/java/org/apache/cassandra/db/Slices.java b/src/java/org/apache/cassandra/db/Slices.java index bae83d5980fb..05fa2d215efd 100644 --- a/src/java/org/apache/cassandra/db/Slices.java +++ b/src/java/org/apache/cassandra/db/Slices.java @@ -25,8 +25,10 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Iterators; +import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.marshal.AbstractType; @@ -149,7 +151,15 @@ public ClusteringBound end() */ public abstract boolean intersects(Slice slice); - public abstract String toCQLString(TableMetadata metadata, RowFilter rowFilter); + /** + * Returns a CQL string representing this slice and the specified {@link RowFilter}. + * + * @param metadata the table metadata + * @param rowFilter a row filter + * @param redaction whether to redact the slice column values + * @return a CQL string representing this slice and the specified {@link RowFilter} + */ + public abstract String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction); /** * Checks if this Slices is empty. @@ -541,9 +551,9 @@ public String toString() } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction) { - StringBuilder sb = new StringBuilder(); + CqlBuilder sb = new CqlBuilder(); // In CQL, condition are expressed by column, so first group things that way, // i.e. for each column, we create a list of what each slice contains on that column @@ -593,7 +603,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) if (values.size() == 1) { - sb.append(" = ").append(column.type.toCQLString(first.startValue)); + sb.append(" = ").append(column.type.toCQLString(first.startValue, redaction)); rowFilter = rowFilter.without(column, Operator.EQ, first.startValue); } else @@ -602,7 +612,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) int j = 0; for (ByteBuffer value : values) { - sb.append(j++ == 0 ? "" : ", ").append(column.type.toCQLString(value)); + sb.append(j++ == 0 ? "" : ", ").append(column.type.toCQLString(value, redaction)); rowFilter = rowFilter.without(column, Operator.EQ, value); } sb.append(")"); @@ -626,7 +636,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) else operator = first.startInclusive ? Operator.GTE : Operator.GT; sb.append(' ').append(operator).append(' ') - .append(column.type.toCQLString(first.startValue)); + .append(column.type.toCQLString(first.startValue, redaction)); rowFilter = rowFilter.without(column, operator, first.startValue); } if (first.endValue != null) @@ -640,19 +650,22 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) else operator = first.endInclusive ? Operator.LTE : Operator.LT; sb.append(' ').append(operator).append(' ') - .append(column.type.toCQLString(first.endValue)); + .append(column.type.toCQLString(first.endValue, redaction)); rowFilter = rowFilter.without(column, operator, first.endValue); } } - } - if (!rowFilter.isEmpty()) - { - if (needAnd) - sb.append(" AND "); - sb.append(rowFilter.toCQLString()); + // Remove index restrictions for this clustering column from the row filter, so we don't print them twice. + // The row filter can contain expressions copying the clustering filter restrictions, because indexed + // clustering key restrictions are added to the row filter at the CQL layer for easier consumption + // downstream. However, due to CQL validation the row filter won't contain additional expressions for + // columns that are included in the clustering filter, besided the aformentioned copies. + rowFilter = rowFilter.withoutFirstLevelExpression(column); } + // Append the row filter. + sb.append(rowFilter, true, redaction); + return sb.toString(); } @@ -775,9 +788,9 @@ public String toString() } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction) { - return rowFilter.toCQLString(); + return rowFilter.toCQLString(redaction); } } @@ -852,7 +865,7 @@ public String toString() } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction) { return ""; } diff --git a/src/java/org/apache/cassandra/db/SortedLocalRanges.java b/src/java/org/apache/cassandra/db/SortedLocalRanges.java new file mode 100644 index 000000000000..0cb922c5b644 --- /dev/null +++ b/src/java/org/apache/cassandra/db/SortedLocalRanges.java @@ -0,0 +1,269 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Splitter; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.service.PendingRangeCalculatorService; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.FBUtilities; + +/** + * This class contains the local ranges for a given table, sorted. At least one range is always present. + */ +public class SortedLocalRanges +{ + private static final Logger logger = LoggerFactory.getLogger(SortedLocalRanges.class); + + private final CompactionRealm realm; + private final long ringVersion; + private final List ranges; + private final Map> splits; + + private volatile boolean valid; + + public SortedLocalRanges(CompactionRealm realm, long ringVersion, List ranges) + { + this.realm = realm; + this.ringVersion = ringVersion; + + if (ranges == null) + { + IPartitioner partitioner = realm.getPartitioner(); + var range = new Splitter.WeightedRange(1.0, + new Range<>(partitioner.getMinimumToken(), + partitioner.getMinimumToken())); + this.ranges = List.of(range); + } + else if (ranges.isEmpty()) + { + this.ranges = ranges; + } + else + { + List sortedRanges = new ArrayList<>(ranges.size()); + for (Splitter.WeightedRange range : ranges) + { + for (Range unwrapped : range.range().unwrap()) + { + sortedRanges.add(new Splitter.WeightedRange(range.weight(), unwrapped)); + } + } + assert !sortedRanges.isEmpty() : "Got empty ranges unwrapping " + ranges; + sortedRanges.sort(Comparator.comparing(Splitter.WeightedRange::left)); + + this.ranges = sortedRanges; + } + this.splits = new ConcurrentHashMap<>(); + this.valid = true; + } + + /** + * Create a set of sorted local ranges based on the current token metadata and ring version. + * + * This method should preferably only be called by {@link ColumnFamilyStore} because later on, + * ranges may need invalidating, see {@link this#invalidate()} and so a reference must be + * kept to ranges that are passed around, and current cfs does this. + */ + static SortedLocalRanges create(ColumnFamilyStore cfs) + { + // If the table's partitioner differs from the system partitioner, the TokenMetadata + // ranges will contain incompatible token types. Fall back to a full range using the + // table's own partitioner. This can happen when CQLSSTableWriter overrides the partitioner. + if (cfs.getPartitioner() != DatabaseDescriptor.getPartitioner()) + return new SortedLocalRanges(cfs, 0, null); + + RangesAtEndpoint localRanges; + List weightedRanges; + long ringVersion; + TokenMetadata tmd; + + do + { + tmd = cfs.keyspace.getReplicationStrategy().getTokenMetadata(); + ringVersion = tmd.getRingVersion(); + localRanges = getLocalRanges(cfs, tmd); + + weightedRanges = new ArrayList<>(localRanges.size()); + for (Range r : localRanges.onlyFull().ranges()) + weightedRanges.add(new Splitter.WeightedRange(1.0, r)); + + for (Range r : localRanges.onlyTransient().ranges()) + weightedRanges.add(new Splitter.WeightedRange(0.1, r)); + + if (logger.isTraceEnabled()) + logger.trace("Got local ranges {} (ringVersion = {})", localRanges, ringVersion); + } + while (ringVersion != tmd.getRingVersion()); // if ringVersion is different here it means that + // it might have changed before we calculated localRanges - recalculate + + return new SortedLocalRanges(cfs, ringVersion, weightedRanges); + } + + private static RangesAtEndpoint getLocalRanges(ColumnFamilyStore cfs, TokenMetadata tmd) + { + RangesAtEndpoint localRanges; + if (StorageService.instance.isBootstrapMode() + && !StorageService.isReplacingSameAddress()) // When replacing same address, the node marks itself as UN locally + { + PendingRangeCalculatorService.instance.blockUntilFinished(); + localRanges = tmd.getPendingRanges(cfs.keyspace.getName(), FBUtilities.getBroadcastAddressAndPort()); + } + else + { + // Reason we use use the future settled TMD is that if we decommission a node, we want to stream + // from that node to the correct location on disk, if we didn't, we would put new files in the wrong places. + // We do this to minimize the amount of data we need to move in rebalancedisks once everything settled + localRanges = cfs.keyspace.getReplicationStrategy().getAddressReplicas(tmd.cloneAfterAllSettled(), FBUtilities.getBroadcastAddressAndPort()); + } + return localRanges; + } + + @VisibleForTesting + public static SortedLocalRanges forTesting(CompactionRealm realm, List ranges) + { + return new SortedLocalRanges(realm, 0, ranges); + } + + public static SortedLocalRanges forTestingFull(CompactionRealm realm) + { + return forTesting(realm, null); + } + + /** + * check if the given disk boundaries are out of date due not being set or to having too old diskVersion/ringVersion + */ + public boolean isOutOfDate() + { + return !valid || ringVersion != realm.getKeyspaceReplicationStrategy().getTokenMetadata().getRingVersion(); + } + + public void invalidate() + { + this.valid = false; + } + + public List getRanges() + { + return ranges; + } + + public long getRingVersion() + { + return ringVersion; + } + + /** + * Split the local ranges into the given number of parts. + * + * @param numParts the number of parts to split into + * + * @return a list of positions into which the local ranges were split + */ + public List split(int numParts) + { + return splits.computeIfAbsent(numParts, this::doSplit); + } + + private List doSplit(int numParts) + { + Splitter splitter = realm.getPartitioner().splitter().orElse(null); + + List boundaries; + if (splitter == null) + { + logger.debug("Could not split local ranges into {} parts for {}.{} (no splitter)", numParts, realm.getKeyspaceName(), realm.getTableName()); + boundaries = ranges.stream().map(Splitter.WeightedRange::right).collect(Collectors.toList()); + } + else + { + logger.debug("Splitting local ranges into {} parts for {}.{}", numParts, realm.getKeyspaceName(), realm.getTableName()); + boundaries = splitter.splitOwnedRanges(numParts, ranges, Splitter.SplitType.ALWAYS_SPLIT).boundaries; + } + + logger.debug("Boundaries for {}.{}: {} ({} splits)", realm.getKeyspaceName(), realm.getTableName(), boundaries, boundaries.size()); + return boundaries; + } + + /** + * Returns the intersection of this list with the given range. + */ + public List subrange(Range range) + { + return ranges.stream() + .map(r -> { + Range subRange = r.range().intersectionNonWrapping(range); + return subRange == null ? null : new Splitter.WeightedRange(r.weight(), subRange); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SortedLocalRanges that = (SortedLocalRanges) o; + if (ringVersion != that.ringVersion) + return false; + + if (!realm.equals(that.realm)) + return false; + + return ranges.equals(that.ranges); + } + + public int hashCode() + { + int result = realm.hashCode(); + result = 31 * result + Long.hashCode(ringVersion); + result = 31 * result + ranges.hashCode(); + return result; + } + + public String toString() + { + return "LocalRanges{" + + "table=" + realm.getKeyspaceName() + "." + realm.getTableName() + + ", ring version=" + ringVersion + + ", num ranges=" + ranges.size() + '}'; + } + + public CompactionRealm getRealm() + { + return realm; + } +} diff --git a/src/java/org/apache/cassandra/db/StorageHook.java b/src/java/org/apache/cassandra/db/StorageHook.java index f5fdec6a563e..0abd0f05ed7f 100644 --- a/src/java/org/apache/cassandra/db/StorageHook.java +++ b/src/java/org/apache/cassandra/db/StorageHook.java @@ -55,7 +55,7 @@ static StorageHook createHook() 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() diff --git a/src/java/org/apache/cassandra/db/SystemKeyspace.java b/src/java/org/apache/cassandra/db/SystemKeyspace.java index a7318fb2dee2..f29b4e2a5624 100644 --- a/src/java/org/apache/cassandra/db/SystemKeyspace.java +++ b/src/java/org/apache/cassandra/db/SystemKeyspace.java @@ -20,7 +20,6 @@ import java.io.IOError; import java.io.IOException; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; @@ -43,19 +42,20 @@ import javax.management.openmbean.TabularData; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; +import org.apache.commons.lang3.ObjectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryHandler.Prepared; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; @@ -69,7 +69,6 @@ import org.apache.cassandra.db.marshal.TimeUUIDType; import org.apache.cassandra.db.marshal.TupleType; import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.db.marshal.UUIDType; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Rows; @@ -80,16 +79,16 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.SSTableId; import org.apache.cassandra.io.sstable.SequenceBasedSSTableId; -import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.io.util.RebufferingInputStream; -import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.metrics.RestorableMeter; import org.apache.cassandra.metrics.TopPartitionTracker; -import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.nodes.INodeInfo; +import org.apache.cassandra.nodes.IPeerInfo; +import org.apache.cassandra.nodes.Nodes; +import org.apache.cassandra.nodes.TruncationRecord; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; @@ -101,8 +100,11 @@ import org.apache.cassandra.schema.Types; import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.schema.Views; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.paxos.Ballot; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.Commit.Accepted; import org.apache.cassandra.service.paxos.Commit.AcceptedWithTTL; @@ -118,6 +120,7 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MD5Digest; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.StorageCompatibilityMode; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Future; @@ -125,6 +128,8 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static org.apache.cassandra.config.CassandraRelevantProperties.PERSIST_PREPARED_STATEMENTS; +import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYSTEM; import static org.apache.cassandra.config.Config.PaxosStatePurging.legacy; import static org.apache.cassandra.config.DatabaseDescriptor.paxosStatePurging; import static org.apache.cassandra.cql3.QueryProcessor.PREPARED_STATEMENT_CACHE_SIZE_BYTES; @@ -157,7 +162,7 @@ private SystemKeyspace() public static final String PEERS_V2 = "peers_v2"; public static final String PEER_EVENTS_V2 = "peer_events_v2"; public static final String COMPACTION_HISTORY = "compaction_history"; - public static final String SSTABLE_ACTIVITY_V2 = "sstable_activity_v2"; // v2 has modified generation column type (v1 - int, v2 - blob), see CASSANDRA-17048 + public static final String SSTABLE_ACTIVITY_V2 = "sstable_activity_v2"; // v2 has modified generation column type (v1 - int, v2 - text), see CASSANDRA-17048 public static final String TABLE_ESTIMATES = "table_estimates"; public static final String TABLE_ESTIMATES_TYPE_PRIMARY = "primary"; public static final String TABLE_ESTIMATES_TYPE_LOCAL_PRIMARY = "local_primary"; @@ -238,6 +243,7 @@ private SystemKeyspace() .compaction(CompactionParams.lcs(emptyMap())) .indexes(PaxosUncommittedIndex.indexes()) .build(); + private static final Context PaxosContext = Context.from(Paxos); private static final TableMetadata BuiltIndexes = parse(BUILT_INDEXES, @@ -260,7 +266,8 @@ private SystemKeyspace() + "WITH COMMENT='Last successful paxos repairs by range'") .build(); - private static final TableMetadata Local = + // Used by CNDB + public static final TableMetadata Local = parse(LOCAL, "information about the local node", "CREATE TABLE %s (" @@ -288,7 +295,8 @@ private SystemKeyspace() ).recordDeprecatedSystemColumn("thrift_version", UTF8Type.instance) .build(); - private static final TableMetadata PeersV2 = + // Used by CNDB + public static final TableMetadata PeersV2 = parse(PEERS_V2, "information about known peers in the cluster", "CREATE TABLE %s (" @@ -327,12 +335,27 @@ private SystemKeyspace() + "columnfamily_name text," + "compacted_at timestamp," + "keyspace_name text," - + "rows_merged map," + + "rows_merged map," // Note that we currently store partitions, not rows! + "compaction_properties frozen>," + "PRIMARY KEY ((id)))") .defaultTimeToLive((int) TimeUnit.DAYS.toSeconds(7)) .build(); + private static final TableMetadata CompactionHistoryLegacy = + parse(COMPACTION_HISTORY, + "week-long compaction history", + "CREATE TABLE %s (" + + "id timeuuid," + + "bytes_in bigint," + + "bytes_out bigint," + + "columnfamily_name text," + + "compacted_at timestamp," + + "keyspace_name text," + + "rows_merged map," + + "PRIMARY KEY ((id)))") + .defaultTimeToLive((int) TimeUnit.DAYS.toSeconds(7)) + .build(); + private static final TableMetadata LegacySSTableActivity = parse(LEGACY_SSTABLE_ACTIVITY, "historic sstable read rates", @@ -473,7 +496,8 @@ private SystemKeyspace() /** @deprecated See CASSANDRA-7544 */ @Deprecated(since = "4.0") - private static final TableMetadata LegacyPeers = + // Used by CNDB + public static final TableMetadata LegacyPeers = parse(LEGACY_PEERS, "information about known peers in the cluster", "CREATE TABLE %s (" @@ -549,7 +573,7 @@ private static Tables tables() LegacyPeers, PeerEventsV2, LegacyPeerEvents, - CompactionHistory, + DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major) ? CompactionHistoryLegacy : CompactionHistory, LegacySSTableActivity, SSTableActivity, LegacySizeEstimates, @@ -565,8 +589,6 @@ private static Tables tables() TopPartitions); } - private static volatile Map> truncationRecords; - public enum BootstrapState { NEEDS_BOOTSTRAP, @@ -583,38 +605,20 @@ public static void persistLocalMetadata() @VisibleForTesting public static void persistLocalMetadata(Supplier nodeIdSupplier) { - String req = "INSERT INTO system.%s (" + - "key," + - "cluster_name," + - "release_version," + - "cql_version," + - "native_protocol_version," + - "data_center," + - "rack," + - "partitioner," + - "rpc_address," + - "rpc_port," + - "broadcast_address," + - "broadcast_port," + - "listen_address," + - "listen_port" + - ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - executeOnceInternal(format(req, LOCAL), - LOCAL, - DatabaseDescriptor.getClusterName(), - FBUtilities.getReleaseVersionString(), - QueryProcessor.CQL_VERSION.toString(), - String.valueOf(ProtocolVersion.CURRENT.asInt()), - snitch.getLocalDatacenter(), - snitch.getLocalRack(), - DatabaseDescriptor.getPartitioner().getClass().getName(), - FBUtilities.getJustBroadcastNativeAddress(), - DatabaseDescriptor.getNativeTransportPort(), - FBUtilities.getJustBroadcastAddress(), - DatabaseDescriptor.getStoragePort(), - FBUtilities.getJustLocalAddress(), - DatabaseDescriptor.getStoragePort()); + Nodes.local().update(info -> { + info.setClusterName(DatabaseDescriptor.getClusterName()); + info.setReleaseVersion(SystemKeyspace.CURRENT_VERSION); + info.setCqlVersion(QueryProcessor.CQL_VERSION); + info.setNativeProtocolVersion(ProtocolVersion.CURRENT); + info.setBroadcastAddressAndPort(FBUtilities.getBroadcastAddressAndPort()); + info.setDataCenter(DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter()); + info.setRack(DatabaseDescriptor.getEndpointSnitch().getLocalRack()); + info.setPartitionerClass(DatabaseDescriptor.getPartitioner().getClass()); + info.setNativeTransportAddressAndPort(InetAddressAndPort.getByAddressOverrideDefaults(DatabaseDescriptor.getRpcAddress(), DatabaseDescriptor.getNativeTransportPort())); + info.setBroadcastAddressAndPort(FBUtilities.getBroadcastAddressAndPort()); + info.setListenAddressAndPort(FBUtilities.getLocalAddressAndPort()); + return info; + }, true, true); // We should store host ID as soon as possible in the system.local table and flush that table to disk so that // we can be sure that those changes are stored in sstable and not in the commit log (see CASSANDRA-18153). @@ -630,22 +634,38 @@ public static void updateCompactionHistory(TimeUUID taskId, long compactedAt, long bytesIn, long bytesOut, - Map rowsMerged, + Map partitionsMerged, Map compactionProperties) { // don't write anything when the history table itself is compacted, since that would in turn cause new compactions if (ksname.equals("system") && cfname.equals(COMPACTION_HISTORY)) return; - String req = "INSERT INTO system.%s (id, keyspace_name, columnfamily_name, compacted_at, bytes_in, bytes_out, rows_merged, compaction_properties) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; - executeInternal(format(req, COMPACTION_HISTORY), - taskId, - ksname, - cfname, - ByteBufferUtil.bytes(compactedAt), - bytesIn, - bytesOut, - rowsMerged, - compactionProperties); + // For historical reasons (pre 3.0 refactor) we call the final field rows_merged but we actually store partitions! + if (DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major)) + { + String req = "INSERT INTO system.%s (id, keyspace_name, columnfamily_name, compacted_at, bytes_in, bytes_out, rows_merged) VALUES (?, ?, ?, ?, ?, ?, ?)"; + executeInternal(format(req, COMPACTION_HISTORY), + taskId, + ksname, + cfname, + ByteBufferUtil.bytes(compactedAt), + bytesIn, + bytesOut, + partitionsMerged); + } + else + { + String req = "INSERT INTO system.%s (id, keyspace_name, columnfamily_name, compacted_at, bytes_in, bytes_out, rows_merged, compaction_properties) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + executeInternal(format(req, COMPACTION_HISTORY), + taskId, + ksname, + cfname, + ByteBufferUtil.bytes(compactedAt), + bytesIn, + bytesOut, + partitionsMerged, + compactionProperties); + } } public static TabularData getCompactionHistory() throws OpenDataException @@ -746,142 +766,57 @@ public static Map, Pair> getViewBuildStatus(String ksn return status; } - public static synchronized void saveTruncationRecord(ColumnFamilyStore cfs, long truncatedAt, CommitLogPosition position) + public static void saveTruncationRecord(TableId tableId, long truncatedAt, CommitLogPosition position) { - String req = "UPDATE system.%s SET truncated_at = truncated_at + ? WHERE key = '%s'"; - executeInternal(format(req, LOCAL, LOCAL), truncationAsMapEntry(cfs, truncatedAt, position)); - truncationRecords = null; - forceBlockingFlush(LOCAL); + Nodes.local().update(info -> info.addTruncationRecord(tableId.asUUID(), new TruncationRecord(position, truncatedAt)), true); } /** * This method is used to remove information about truncation time for specified column family */ - public static synchronized void removeTruncationRecord(TableId id) - { - Pair truncationRecord = getTruncationRecord(id); - if (truncationRecord == null) - return; - - String req = "DELETE truncated_at[?] from system.%s WHERE key = '%s'"; - executeInternal(format(req, LOCAL, LOCAL), id.asUUID()); - truncationRecords = null; - forceBlockingFlush(LOCAL); - } - - private static Map truncationAsMapEntry(ColumnFamilyStore cfs, long truncatedAt, CommitLogPosition position) + public static void removeTruncationRecord(TableId id) { - try (DataOutputBuffer out = DataOutputBuffer.scratchBuffer.get()) - { - CommitLogPosition.serializer.serialize(position, out); - out.writeLong(truncatedAt); - return singletonMap(cfs.metadata.id.asUUID(), out.asNewBuffer()); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + Nodes.local().update(info -> info.removeTruncationRecord(id.asUUID()), true); } public static CommitLogPosition getTruncatedPosition(TableId id) { - Pair record = getTruncationRecord(id); - return record == null ? null : record.left; + TruncationRecord record = Nodes.local().get().getTruncationRecords().get(id.asUUID()); + return record != null ? record.position : null; } public static long getTruncatedAt(TableId id) { - Pair record = getTruncationRecord(id); - return record == null ? Long.MIN_VALUE : record.right; - } - - private static synchronized Pair getTruncationRecord(TableId id) - { - if (truncationRecords == null) - truncationRecords = readTruncationRecords(); - return truncationRecords.get(id); - } - - private static Map> readTruncationRecords() - { - UntypedResultSet rows = executeInternal(format("SELECT truncated_at FROM system.%s WHERE key = '%s'", LOCAL, LOCAL)); - - Map> records = new HashMap<>(); - - if (!rows.isEmpty() && rows.one().has("truncated_at")) - { - Map map = rows.one().getMap("truncated_at", UUIDType.instance, BytesType.instance); - for (Map.Entry entry : map.entrySet()) - records.put(TableId.fromUUID(entry.getKey()), truncationRecordFromBlob(entry.getValue())); - } - - return records; - } - - private static Pair truncationRecordFromBlob(ByteBuffer bytes) - { - try (RebufferingInputStream in = new DataInputBuffer(bytes, true)) - { - return Pair.create(CommitLogPosition.serializer.deserialize(in), in.available() > 0 ? in.readLong() : Long.MIN_VALUE); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + TruncationRecord record = Nodes.local().get().getTruncationRecords().get(id.asUUID()); + return record != null ? record.truncatedAt : Long.MIN_VALUE; } /** * Record tokens being used by another node */ - public static synchronized void updateTokens(InetAddressAndPort ep, Collection tokens) + public static void updateTokens(InetAddressAndPort ep, Collection tokens) { if (ep.equals(FBUtilities.getBroadcastAddressAndPort())) return; - String req = "INSERT INTO system.%s (peer, tokens) VALUES (?, ?)"; - executeInternal(String.format(req, LEGACY_PEERS), ep.getAddress(), tokensAsSet(tokens)); - req = "INSERT INTO system.%s (peer, peer_port, tokens) VALUES (?, ?, ?)"; - executeInternal(String.format(req, PEERS_V2), ep.getAddress(), ep.getPort(), tokensAsSet(tokens)); + Nodes.peers().update(ep, peer -> peer.setTokens(tokens), false); } - public static synchronized boolean updatePreferredIP(InetAddressAndPort ep, InetAddressAndPort preferred_ip) + public static boolean updatePreferredIP(InetAddressAndPort ep, InetAddressAndPort preferredIP) { - if (preferred_ip.equals(getPreferredIP(ep))) + if (preferredIP.equals(getPreferredIP(ep))) return false; - String req = "INSERT INTO system.%s (peer, preferred_ip) VALUES (?, ?)"; - executeInternal(String.format(req, LEGACY_PEERS), ep.getAddress(), preferred_ip.getAddress()); - req = "INSERT INTO system.%s (peer, peer_port, preferred_ip, preferred_port) VALUES (?, ?, ?, ?)"; - executeInternal(String.format(req, PEERS_V2), ep.getAddress(), ep.getPort(), preferred_ip.getAddress(), preferred_ip.getPort()); - forceBlockingFlush(LEGACY_PEERS, PEERS_V2); + Nodes.peers().update(ep, info -> info.setPreferredAddressAndPort(preferredIP), true); return true; } - public static synchronized void updatePeerInfo(InetAddressAndPort ep, String columnName, Object value) - { - if (ep.equals(FBUtilities.getBroadcastAddressAndPort())) - return; - - String req = "INSERT INTO system.%s (peer, %s) VALUES (?, ?)"; - executeInternal(String.format(req, LEGACY_PEERS, columnName), ep.getAddress(), value); - //This column doesn't match across the two tables - if (columnName.equals("rpc_address")) - { - columnName = "native_address"; - } - req = "INSERT INTO system.%s (peer, peer_port, %s) VALUES (?, ?, ?)"; - executeInternal(String.format(req, PEERS_V2, columnName), ep.getAddress(), ep.getPort(), value); - } - - public static synchronized void updatePeerNativeAddress(InetAddressAndPort ep, InetAddressAndPort address) + public static void updatePeerNativeAddress(InetAddressAndPort ep, InetAddressAndPort address) { if (ep.equals(FBUtilities.getBroadcastAddressAndPort())) return; - String req = "INSERT INTO system.%s (peer, rpc_address) VALUES (?, ?)"; - executeInternal(String.format(req, LEGACY_PEERS), ep.getAddress(), address.getAddress()); - req = "INSERT INTO system.%s (peer, peer_port, native_address, native_port) VALUES (?, ?, ?, ?)"; - executeInternal(String.format(req, PEERS_V2), ep.getAddress(), ep.getPort(), address.getAddress(), address.getPort()); + Nodes.peers().update(ep, info -> info.setNativeTransportAddressAndPort(address), false); } @@ -894,63 +829,31 @@ public static synchronized void updateHintsDropped(InetAddressAndPort ep, TimeUU executeInternal(String.format(req, PEER_EVENTS_V2), timePeriod, value, ep.getAddress(), ep.getPort()); } - public static synchronized void updateSchemaVersion(UUID version) - { - String req = "INSERT INTO system.%s (key, schema_version) VALUES ('%s', ?)"; - executeInternal(format(req, LOCAL, LOCAL), version); - } - - private static Set tokensAsSet(Collection tokens) + public static void updateSchemaVersion(UUID version) { - if (tokens.isEmpty()) - return Collections.emptySet(); - Token.TokenFactory factory = StorageService.instance.getTokenFactory(); - Set s = new HashSet<>(tokens.size()); - for (Token tk : tokens) - s.add(factory.toString(tk)); - return s; - } - - private static Collection deserializeTokens(Collection tokensStrings) - { - Token.TokenFactory factory = StorageService.instance.getTokenFactory(); - List tokens = new ArrayList<>(tokensStrings.size()); - for (String tk : tokensStrings) - tokens.add(factory.fromString(tk)); - return tokens; + Nodes.local().update(info -> info.setSchemaVersion(version), false); } /** * Remove stored tokens being used by another node */ - public static synchronized void removeEndpoint(InetSocketAddress ep) + public static void removeEndpoint(InetAddressAndPort ep) { - String req = "DELETE FROM system.%s WHERE peer = ?"; - executeInternal(String.format(req, LEGACY_PEERS), ep.getAddress()); - req = String.format("DELETE FROM system.%s WHERE peer = ? AND peer_port = ?", PEERS_V2); - executeInternal(req, ep.getAddress(), ep.getPort()); - forceBlockingFlush(LEGACY_PEERS, PEERS_V2); + Nodes.peers().remove(ep, true, false); } /** * This method is used to update the System Keyspace with the new tokens for this node */ - public static synchronized void updateTokens(Collection tokens) + public static void updateTokens(Collection tokens) { assert !tokens.isEmpty() : "removeEndpoint should be used instead"; - - Collection savedTokens = getSavedTokens(); - if (tokens.containsAll(savedTokens) && tokens.size() == savedTokens.size()) - return; - - String req = "INSERT INTO system.%s (key, tokens) VALUES ('%s', ?)"; - executeInternal(format(req, LOCAL, LOCAL), tokensAsSet(tokens)); - forceBlockingFlush(LOCAL); + Nodes.getInstance().getLocal().update(info -> info.setTokens(tokens), true); } public static void forceBlockingFlush(String ...cfnames) { - if (!DatabaseDescriptor.isUnsafeSystem()) + if (!UNSAFE_SYSTEM.getBoolean()) { List> futures = new ArrayList<>(); @@ -971,15 +874,7 @@ public static void forceBlockingFlush(String ...cfnames) public static SetMultimap loadTokens() { SetMultimap tokenMap = HashMultimap.create(); - for (UntypedResultSet.Row row : executeInternal("SELECT peer, peer_port, tokens FROM system." + PEERS_V2)) - { - InetAddress address = row.getInetAddress("peer"); - Integer port = row.getInt("peer_port"); - InetAddressAndPort peer = InetAddressAndPort.getByAddressOverrideDefaults(address, port); - if (row.has("tokens")) - tokenMap.putAll(peer, deserializeTokens(row.getSet("tokens", UTF8Type.instance))); - } - + Nodes.peers().get().filter(IPeerInfo::isExisting).forEach(info -> tokenMap.putAll(info.getPeerAddressAndPort(), info.getTokens())); return tokenMap; } @@ -989,18 +884,7 @@ public static SetMultimap loadTokens() */ public static Map loadHostIds() { - Map hostIdMap = new HashMap<>(); - for (UntypedResultSet.Row row : executeInternal("SELECT peer, peer_port, host_id FROM system." + PEERS_V2)) - { - InetAddress address = row.getInetAddress("peer"); - Integer port = row.getInt("peer_port"); - InetAddressAndPort peer = InetAddressAndPort.getByAddressOverrideDefaults(address, port); - if (row.has("host_id")) - { - hostIdMap.put(peer, row.getUUID("host_id")); - } - } - return hostIdMap; + return Nodes.peers().get().filter(IPeerInfo::isExisting).collect(Collectors.toMap(IPeerInfo::getPeerAddressAndPort, INodeInfo::getHostId)); } /** @@ -1011,38 +895,27 @@ public static Map loadHostIds() */ public static InetAddressAndPort getPreferredIP(InetAddressAndPort ep) { - Preconditions.checkState(DatabaseDescriptor.isDaemonInitialized()); // Make sure being used as a daemon, not a tool - - String req = "SELECT preferred_ip, preferred_port FROM system.%s WHERE peer=? AND peer_port = ?"; - UntypedResultSet result = executeInternal(String.format(req, PEERS_V2), ep.getAddress(), ep.getPort()); - if (!result.isEmpty() && result.one().has("preferred_ip")) - { - UntypedResultSet.Row row = result.one(); - return InetAddressAndPort.getByAddressOverrideDefaults(row.getInetAddress("preferred_ip"), row.getInt("preferred_port")); - } - return ep; + IPeerInfo info = Nodes.peers().get(ep); + if (info != null && info.getPreferredAddressAndPort() != null && info.isExisting()) + return info.getPreferredAddressAndPort(); + else + return ep; } /** * Return a map of IP addresses containing a map of dc and rack info */ - public static Map> loadDcRackInfo() + public static Map> loadDcRackInfo() { - Map> result = new HashMap<>(); - for (UntypedResultSet.Row row : executeInternal("SELECT peer, peer_port, data_center, rack from system." + PEERS_V2)) - { - InetAddress address = row.getInetAddress("peer"); - Integer port = row.getInt("peer_port"); - InetAddressAndPort peer = InetAddressAndPort.getByAddressOverrideDefaults(address, port); - if (row.has("data_center") && row.has("rack")) - { - Map dcRack = new HashMap<>(); - dcRack.put("data_center", row.getString("data_center")); - dcRack.put("rack", row.getString("rack")); - result.put(peer, dcRack); - } - } - return result; + return Nodes.peers() + .get() + .filter(p -> p.getDataCenter() != null && p.getRack() != null && p.isExisting()) + .collect(Collectors.toMap(IPeerInfo::getPeerAddressAndPort, p -> { + Map dcRack = new HashMap<>(); + dcRack.put("data_center", p.getDataCenter()); + dcRack.put("rack", p.getRack()); + return dcRack; + })); } /** @@ -1054,25 +927,12 @@ public static Map> loadDcRackInfo() */ public static CassandraVersion getReleaseVersion(InetAddressAndPort ep) { - try - { - if (FBUtilities.getBroadcastAddressAndPort().equals(ep)) - { - return CURRENT_VERSION; - } - String req = "SELECT release_version FROM system.%s WHERE peer=? AND peer_port=?"; - UntypedResultSet result = executeInternal(String.format(req, PEERS_V2), ep.getAddress(), ep.getPort()); - if (result != null && result.one().has("release_version")) - { - return new CassandraVersion(result.one().getString("release_version")); - } - // version is unknown - return null; - } - catch (IllegalArgumentException e) + if (FBUtilities.getBroadcastAddressAndPort().equals(ep)) + return CURRENT_VERSION; + else { - // version string cannot be parsed - return null; + IPeerInfo peer = Nodes.peers().get(ep); + return peer != null && peer.isExisting() ? peer.getReleaseVersion() : null; } } @@ -1099,11 +959,8 @@ public static void checkHealth() throws ConfigurationException } ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(LOCAL); - String req = "SELECT cluster_name FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - - if (result.isEmpty() || !result.one().has("cluster_name")) - { + String savedClusterName = Nodes.local().get().getClusterName(); + if (savedClusterName == null) { // this is a brand new node if (!cfs.getLiveSSTables().isEmpty()) throw new ConfigurationException("Found system keyspace files, but they couldn't be loaded!"); @@ -1111,23 +968,18 @@ public static void checkHealth() throws ConfigurationException // no system files. this is a new node. return; } - - String savedClusterName = result.one().getString("cluster_name"); if (!DatabaseDescriptor.getClusterName().equals(savedClusterName)) throw new ConfigurationException("Saved cluster name " + savedClusterName + " != configured name " + DatabaseDescriptor.getClusterName()); } public static Collection getSavedTokens() { - String req = "SELECT tokens FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - return result.isEmpty() || !result.one().has("tokens") - ? Collections.emptyList() - : deserializeTokens(result.one().getSet("tokens", UTF8Type.instance)); + return Nodes.local().get().getTokens(); } public static int incrementAndGetGeneration() { + // gossip generation is specific to Gossip thus it is not handled by Nodes.Local String req = "SELECT gossip_generation FROM system.%s WHERE key='%s'"; UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); @@ -1165,13 +1017,7 @@ public static int incrementAndGetGeneration() public static BootstrapState getBootstrapState() { - String req = "SELECT bootstrapped FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - - if (result.isEmpty() || !result.one().has("bootstrapped")) - return BootstrapState.NEEDS_BOOTSTRAP; - - return BootstrapState.valueOf(result.one().getString("bootstrapped")); + return ObjectUtils.firstNonNull(Nodes.local().get().getBootstrapState(), BootstrapState.NEEDS_BOOTSTRAP); } public static boolean bootstrapComplete() @@ -1191,12 +1037,7 @@ public static boolean wasDecommissioned() public static void setBootstrapState(BootstrapState state) { - if (getBootstrapState() == state) - return; - - String req = "INSERT INTO system.%s (key, bootstrapped) VALUES ('%s', ?)"; - executeInternal(format(req, LOCAL, LOCAL), state.name()); - forceBlockingFlush(LOCAL); + Nodes.local().update(info -> info.setBootstrapState(state), true); } public static boolean isIndexBuilt(String keyspaceName, String indexName) @@ -1235,14 +1076,7 @@ public static List getBuiltIndexes(String keyspaceName, Set inde */ public static UUID getLocalHostId() { - String req = "SELECT host_id FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - - // Look up the Host UUID (return it if found) - if (result != null && !result.isEmpty() && result.one().has("host_id")) - return result.one().getUUID("host_id"); - - return null; + return Nodes.local().get().getHostId(); } /** @@ -1269,12 +1103,9 @@ private static synchronized UUID getOrInitializeLocalHostId(Supplier nodeI /** * Sets the local host ID explicitly. Should only be called outside of SystemTable when replacing a node. */ - public static synchronized UUID setLocalHostId(UUID hostId) + public static UUID setLocalHostId(UUID hostId) { - String req = "INSERT INTO system.%s (key, host_id) VALUES ('%s', ?)"; - executeInternal(format(req, LOCAL, LOCAL), hostId); - forceBlockingFlush(LOCAL); - return hostId; + return Nodes.local().update(info -> info.setHostId(hostId), false).getHostId(); } /** @@ -1282,13 +1113,7 @@ public static synchronized UUID setLocalHostId(UUID hostId) */ public static UUID getSchemaVersion() { - String req = "SELECT schema_version FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - - if (!result.isEmpty() && result.one().has("schema_version")) - return result.one().getUUID("schema_version"); - - return null; + return Nodes.local().get().getSchemaVersion(); } /** @@ -1296,14 +1121,7 @@ public static UUID getSchemaVersion() */ public static String getRack() { - String req = "SELECT rack FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - - // Look up the Rack (return it if found) - if (!result.isEmpty() && result.one().has("rack")) - return result.one().getString("rack"); - - return null; + return Nodes.local().get().getRack(); } /** @@ -1311,14 +1129,7 @@ public static String getRack() */ public static String getDatacenter() { - String req = "SELECT data_center FROM system.%s WHERE key='%s'"; - UntypedResultSet result = executeInternal(format(req, LOCAL, LOCAL)); - - // Look up the Data center (return it if found) - if (!result.isEmpty() && result.one().has("data_center")) - return result.one().getString("data_center"); - - return null; + return Nodes.local().get().getDataCenter(); } /** @@ -1328,8 +1139,15 @@ public static String getDatacenter() */ public static PaxosState.Snapshot loadPaxosState(DecoratedKey partitionKey, TableMetadata metadata, long nowInSec) { + // Track bytes read from the Paxos system table for the commit that initiated Paxos + registerPaxosSensor(Type.READ_BYTES); + String cql = "SELECT * FROM system." + PAXOS + " WHERE row_key = ? AND cf_id = ?"; List results = QueryProcessor.executeInternalRawWithNow(nowInSec, cql, partitionKey.getKey(), metadata.id.asUUID()).get(partitionKey); + + // transfer bytes read off of Paxos system table to the user table for the commit that initiated Paxos + transferPaxosSensorBytes(metadata, Type.READ_BYTES); + if (results == null || results.isEmpty()) { Committed noneCommitted = Committed.none(partitionKey, metadata); @@ -1386,21 +1204,21 @@ public static void savePaxosWritePromise(DecoratedKey key, TableMetadata metadat if (paxosStatePurging() == legacy) { String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET in_progress_ballot = ? WHERE row_key = ? AND cf_id = ?"; - executeInternal(cql, + trackPaxosBytes(metadata, () -> executeInternal(cql, ballot.unixMicros(), legacyPaxosTtlSec(metadata), ballot, key.getKey(), - metadata.id.asUUID()); + metadata.id.asUUID())); } else { String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET in_progress_ballot = ? WHERE row_key = ? AND cf_id = ?"; - executeInternal(cql, + trackPaxosBytes(metadata, () -> executeInternal(cql, ballot.unixMicros(), ballot, key.getKey(), - metadata.id.asUUID()); + metadata.id.asUUID())); } } @@ -1409,57 +1227,59 @@ public static void savePaxosReadPromise(DecoratedKey key, TableMetadata metadata if (paxosStatePurging() == legacy) { String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET in_progress_read_ballot = ? WHERE row_key = ? AND cf_id = ?"; - executeInternal(cql, + trackPaxosBytes(metadata, () -> executeInternal(cql, ballot.unixMicros(), legacyPaxosTtlSec(metadata), ballot, key.getKey(), - metadata.id.asUUID()); + metadata.id.asUUID())); } else { String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET in_progress_read_ballot = ? WHERE row_key = ? AND cf_id = ?"; - executeInternal(cql, + trackPaxosBytes(metadata, () -> executeInternal(cql, ballot.unixMicros(), ballot, key.getKey(), - metadata.id.asUUID()); + metadata.id.asUUID())); } } public static void savePaxosProposal(Commit proposal) { + int storageVersion = StorageCompatibilityMode.current().storageMessagingVersion(); if (proposal instanceof AcceptedWithTTL) { long localDeletionTime = ((Commit.AcceptedWithTTL) proposal).localDeletionTime; int ttlInSec = legacyPaxosTtlSec(proposal.update.metadata()); long nowInSec = localDeletionTime - ttlInSec; String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = ?, proposal = ?, proposal_version = ? WHERE row_key = ? AND cf_id = ?"; - executeInternalWithNowInSec(cql, + trackPaxosBytes(proposal, () -> executeInternalWithNowInSec(cql, nowInSec, proposal.ballot.unixMicros(), ttlInSec, proposal.ballot, - PartitionUpdate.toBytes(proposal.update, MessagingService.current_version), - MessagingService.current_version, + PartitionUpdate.toBytes(proposal.update, storageVersion), + storageVersion, proposal.update.partitionKey().getKey(), - proposal.update.metadata().id.asUUID()); + proposal.update.metadata().id.asUUID())); } else { String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET proposal_ballot = ?, proposal = ?, proposal_version = ? WHERE row_key = ? AND cf_id = ?"; - executeInternal(cql, + trackPaxosBytes(proposal, () -> executeInternal(cql, proposal.ballot.unixMicros(), proposal.ballot, - PartitionUpdate.toBytes(proposal.update, MessagingService.current_version), - MessagingService.current_version, + PartitionUpdate.toBytes(proposal.update, storageVersion), + storageVersion, proposal.update.partitionKey().getKey(), - proposal.update.metadata().id.asUUID()); + proposal.update.metadata().id.asUUID())); } } public static void savePaxosCommit(Commit commit) { + int storageVersion = StorageCompatibilityMode.current().storageMessagingVersion(); // We always erase the last proposal (with the commit timestamp to no erase more recent proposal in case the commit is old) // even though that's really just an optimization since SP.beginAndRepairPaxos will exclude accepted proposal older than the mrc. if (commit instanceof Commit.CommittedWithTTL) @@ -1468,26 +1288,26 @@ public static void savePaxosCommit(Commit commit) int ttlInSec = legacyPaxosTtlSec(commit.update.metadata()); long nowInSec = localDeletionTime - ttlInSec; String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ? WHERE row_key = ? AND cf_id = ?"; - executeInternalWithNowInSec(cql, + trackPaxosBytes(commit, () -> executeInternalWithNowInSec(cql, nowInSec, commit.ballot.unixMicros(), ttlInSec, commit.ballot, - PartitionUpdate.toBytes(commit.update, MessagingService.current_version), - MessagingService.current_version, + PartitionUpdate.toBytes(commit.update, storageVersion), + storageVersion, commit.update.partitionKey().getKey(), - commit.update.metadata().id.asUUID()); + commit.update.metadata().id.asUUID())); } else { String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ? WHERE row_key = ? AND cf_id = ?"; - executeInternal(cql, + trackPaxosBytes(commit, () -> executeInternal(cql, commit.ballot.unixMicros(), commit.ballot, - PartitionUpdate.toBytes(commit.update, MessagingService.current_version), - MessagingService.current_version, + PartitionUpdate.toBytes(commit.update, storageVersion), + storageVersion, commit.update.partitionKey().getKey(), - commit.update.metadata().id.asUUID()); + commit.update.metadata().id.asUUID())); } } @@ -1521,6 +1341,52 @@ public static PaxosRepairHistory loadPaxosRepairHistory(String keyspace, String return PaxosRepairHistory.fromTupleBufferList(points); } + /** + * Decorates a paxos comit consumer with methods to track bytes written to the Paxos system table under the context of the user table that initiated Paxos. + */ + private static void trackPaxosBytes(TableMetadata metadata, Runnable paxosCommitConsumer) + { + // Track bytes written to the Paxos system table for the commit that initiated Paxos + registerPaxosSensor(Type.WRITE_BYTES); + paxosCommitConsumer.run(); + // transfer bytes written to the Paxos system table to the user table for the commit that initiated Paxos + transferPaxosSensorBytes(metadata, Type.WRITE_BYTES); + } + + /** + * Decorates a paxos comit consumer with methods to track bytes written to the Paxos system table under the context of the user table that initiated Paxos. + */ + private static void trackPaxosBytes(Commit commit, Runnable paxosCommitConsumer) + { + // Track bytes written to the Paxos system table for the commit that initiated Paxos + registerPaxosSensor(Type.WRITE_BYTES); + paxosCommitConsumer.run(); + // transfer bytes written to the Paxos system table to the user table for the commit that initiated Paxos + transferPaxosSensorBytes(commit.update.metadata(), Type.WRITE_BYTES); + } + + private static void registerPaxosSensor(Type type) + { + RequestSensors sensors = RequestTracker.instance.get(); + if (sensors != null) + { + sensors.registerSensor(PaxosContext, type); + } + } + + /** + * Populates sensor values of a given {@link Type} associated with the user commit that initiated Paxos. + */ + private static void transferPaxosSensorBytes(TableMetadata targetSensorMetadata, Type type) + { + RequestSensors sensors = RequestTracker.instance.get(); + if (sensors != null) + sensors.getSensor(PaxosContext, type).ifPresent(paxosSensor -> { + sensors.incrementSensor(Context.from(targetSensorMetadata), type, paxosSensor.getValue()); + sensors.syncAllSensors(); + }); + } + /** * Returns a RestorableMeter tracking the average read rate of a particular SSTable, restoring the last-seen rate * from values in system.sstable_activity if present. @@ -1533,12 +1399,12 @@ public static RestorableMeter getSSTableReadMeter(String keyspace, String table, UntypedResultSet results = readSSTableActivity(keyspace, table, id); if (results.isEmpty()) - return new RestorableMeter(); + return RestorableMeter.createWithDefaultRates(); UntypedResultSet.Row row = results.one(); double m15rate = row.getDouble("rate_15m"); double m120rate = row.getDouble("rate_120m"); - return new RestorableMeter(m15rate, m120rate); + return RestorableMeter.builder().withM15Rate(m15rate).withM120Rate(m120rate).build(); } @VisibleForTesting @@ -1599,7 +1465,7 @@ public static void updateSizeEstimates(String keyspace, String table, Map byteBufferToRange(ByteBuffer rawRange, IPartitioner public static void writePreparedStatement(String loggedKeyspace, MD5Digest key, String cql, long timestamp) { - executeInternal(format("INSERT INTO %s (logged_keyspace, prepared_id, query_string) VALUES (?, ?, ?) USING TIMESTAMP ?", - PreparedStatements.toString()), - loggedKeyspace, key.byteBuffer(), cql, timestamp); - logger.debug("stored prepared statement for logged keyspace '{}': '{}'", loggedKeyspace, cql); + if (PERSIST_PREPARED_STATEMENTS.getBoolean()) + { + executeInternal(format("INSERT INTO %s (logged_keyspace, prepared_id, query_string) VALUES (?, ?, ?) USING TIMESTAMP ?", + PreparedStatements.toString()), + loggedKeyspace, key.byteBuffer(), cql, timestamp); + logger.debug("stored prepared statement for logged keyspace '{}': '{}'", loggedKeyspace, cql); + } + else + logger.debug("not persisting prepared statement for logged keyspace '{}': '{}'", loggedKeyspace, cql); } public static void removePreparedStatement(MD5Digest key) @@ -1899,7 +1769,7 @@ public static int loadPreparedStatements(TriFunction onLoaded, int pageSize) { String query = String.format("SELECT prepared_id, logged_keyspace, query_string FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, PREPARED_STATEMENTS); - UntypedResultSet resultSet = executeOnceInternalWithPaging(query, pageSize); + UntypedResultSet resultSet = executeOnceInternalWithPaging(query, new PageSize(pageSize, PageSize.PageUnit.ROWS)); int counter = 0; // As the cache size may be briefly exceeded before statements are evicted, we allow loading 110% the cache size @@ -1985,7 +1855,7 @@ public static TopPartitionTracker.StoredTopPartitions getTopPartitions(TableMeta return TopPartitionTracker.StoredTopPartitions.EMPTY; List topPartitions = new ArrayList<>(top.size()); - TupleType tupleType = new TupleType(Lists.newArrayList(UTF8Type.instance, LongType.instance)); + TupleType tupleType = new TupleType(ImmutableList.of(UTF8Type.instance, LongType.instance)); for (ByteBuffer bb : top) { ByteBuffer[] components = tupleType.split(ByteBufferAccessor.instance, bb); diff --git a/src/java/org/apache/cassandra/db/SystemKeyspaceMigrator41.java b/src/java/org/apache/cassandra/db/SystemKeyspaceMigrator41.java index ab9f01f94500..7431615ccd94 100644 --- a/src/java/org/apache/cassandra/db/SystemKeyspaceMigrator41.java +++ b/src/java/org/apache/cassandra/db/SystemKeyspaceMigrator41.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.db.marshal.BytesType; @@ -52,6 +53,7 @@ public class SystemKeyspaceMigrator41 { private static final Logger logger = LoggerFactory.getLogger(SystemKeyspaceMigrator41.class); + private static final PageSize DEFAULT_PAGE_SIZE = PageSize.inRows(1000); private SystemKeyspaceMigrator41() { @@ -162,10 +164,33 @@ static void migrateSSTableActivity() }) ); } - + @VisibleForTesting static void migrateCompactionHistory() { + if (DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major)) + { + migrateTable(false, + SystemKeyspace.COMPACTION_HISTORY, + SystemKeyspace.COMPACTION_HISTORY, + new String[]{ "id", + "bytes_in", + "bytes_out", + "columnfamily_name", + "compacted_at", + "keyspace_name", + "rows_merged" }, + row -> Collections.singletonList(new Object[]{ row.getTimeUUID("id"), + row.has("bytes_in") ? row.getLong("bytes_in") : null, + row.has("bytes_out") ? row.getLong("bytes_out") : null, + row.has("columnfamily_name") ? row.getString("columnfamily_name") : null, + row.has("compacted_at") ? row.getTimestamp("compacted_at") : null, + row.has("keyspace_name") ? row.getString("keyspace_name") : null, + row.has("rows_merged") ? row.getMap("rows_merged", Int32Type.instance, LongType.instance) : null }) + ); + return; + } + migrateTable(false, SystemKeyspace.COMPACTION_HISTORY, SystemKeyspace.COMPACTION_HISTORY, @@ -177,7 +202,7 @@ static void migrateCompactionHistory() "keyspace_name", "rows_merged", "compaction_properties" }, - row -> Collections.singletonList(new Object[]{ row.getTimeUUID("id") , + row -> Collections.singletonList(new Object[]{ row.getTimeUUID("id"), row.has("bytes_in") ? row.getLong("bytes_in") : null, row.has("bytes_out") ? row.getLong("bytes_out") : null, row.has("columnfamily_name") ? row.getString("columnfamily_name") : null, @@ -191,7 +216,7 @@ static void migrateCompactionHistory() /** * Perform table migration by reading data from the old table, converting it, and adding to the new table. * If oldName and newName are same, it means data in the table will be refreshed. - * + * * @param truncateIfExists truncate the existing table if it exists before migration; if it is disabled * and the new table is not empty and oldName is not equal to newName, no migration is performed * @param oldName old table name @@ -217,10 +242,10 @@ static void migrateTable(boolean truncateIfExists, String oldName, String newNam String insert = String.format("INSERT INTO %s.%s (%s) VALUES (%s)", SchemaConstants.SYSTEM_KEYSPACE_NAME, newName, StringUtils.join(columns, ", "), StringUtils.repeat("?", ", ", columns.length)); - UntypedResultSet rows = QueryProcessor.executeInternal(query); + UntypedResultSet rows = QueryProcessor.executeInternalWithPaging(query, DEFAULT_PAGE_SIZE); assert rows != null : String.format("Migrating rows from legacy %s to %s was not done as returned rows from %s are null!", oldName, newName, oldName); - + int transferred = 0; logger.info("Migrating rows from legacy {} to {}", oldName, newName); for (UntypedResultSet.Row row : rows) diff --git a/src/java/org/apache/cassandra/db/TableWriteHandler.java b/src/java/org/apache/cassandra/db/TableWriteHandler.java index 7aa709fe5281..a5a211ba8da8 100644 --- a/src/java/org/apache/cassandra/db/TableWriteHandler.java +++ b/src/java/org/apache/cassandra/db/TableWriteHandler.java @@ -22,5 +22,17 @@ public interface TableWriteHandler { + /** + * Apply {@code update} to the table's memtable. + * + * @param update the partition update to write + * @param context write context for the current mutation + * @param updateIndexes whether secondary indexes should be updated. + * When {@code false} this is a nested write (e.g. a legacy 2i index-table write + * issued from {@code indexer.onInserted()} under the base table's memtable-internal locks). + * Nested writes bypass the memtable pool's room-wait gate to avoid deadlocking the flush + * write-barrier; as a consequence they may allocate slightly beyond the gated limit + * (CASSANDRA-21019). + */ void write(PartitionUpdate update, WriteContext context, boolean updateIndexes); } diff --git a/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java b/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java index 856b27c0a3a0..665d2beea3e5 100644 --- a/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java +++ b/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java @@ -169,4 +169,5 @@ public void skipNext() throws IOException UnfilteredSerializer.serializer.skipRowBody(in); } } + } diff --git a/src/java/org/apache/cassandra/db/WriteOptions.java b/src/java/org/apache/cassandra/db/WriteOptions.java new file mode 100644 index 000000000000..f84cdd696c47 --- /dev/null +++ b/src/java/org/apache/cassandra/db/WriteOptions.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db; + +import java.util.Collections; + +import org.apache.cassandra.db.view.ViewManager; +import org.apache.cassandra.streaming.StreamOperation; + +public enum WriteOptions +{ + /** + * Default write options for client initiated requests. + */ + DEFAULT(null, true, true, null, true), + + /** + * Does not persist commit log but updates indexes and is droppable. Used for tests and + * when commit log persistence is not required. + */ + DEFAULT_WITHOUT_COMMITLOG(false, true, true, null, true), + + /** + * Same as default, except it does not perform paired view replication since it's delayed write + */ + FOR_READ_REPAIR(null, true, true, null, false), + + /** + * Streaming with CDC always needs to write to commit log. It's also not droppable since it's not a client-initiated request. + * + * The difference from this to {@link this#FOR_STREAMING} is that we can safely skip updating views when updating + * sstables through the commit log, since we can ensure view sstables will be streamed from other replicas. + */ + FOR_BOOTSTRAP_STREAMING(true, true, false, false, false), + + /** + * Streaming with CDC always needs to write to commit log. It's also not droppable since it's not a client-initiated request. + */ + FOR_STREAMING(true, true, false, null, false), + + /** + * Streaming with MVs does not need to write to commit log, since it can be recovered on crash. It's also not + * droppable since it's not a client-initiated request. + */ + FOR_STREAMING_WITH_MV(false, true, false, null, false), + + /** + * Commit log replay obviously does not need to write to the commit log. + * It's also not droppable since it's not a client-initiated request. + */ + FOR_COMMITLOG_REPLAY(false, true, false, null, false), + + /** + * Paxos commit must write to commit log, independent of keyspace settings. + * + * It can also used paired view replication, since we can ensure it's the first time the + * mutation is written. + */ + FOR_PAXOS_COMMIT(true, true, true, null, true), + + /** + * View rebuild uses paired view replication since all nodes will build the view simultaneously + */ + FOR_VIEW_BUILD(true, true, false, null, true), + + /** + * For use on SecondaryIndexTest + */ + SKIP_INDEXES_AND_COMMITLOG(false, false, false, null, false), + + /** + * Batchlog replay uses default settings but does not perform paired view replications for view writes. + */ + FOR_BATCH_REPLAY(null, true, true, null, false), + + /** + * Batchlog replay but done by CNDB, so without using the commit log. + */ + FOR_BATCH_CNDB_REPLAY(false, true, true, null, false), + /** + * Hint replay uses default settings but does not perform paired view replications for view writes. + */ + FOR_HINT_REPLAY(null, true, true, null, false); + + + /** + * Disable index updates (used by CollationController "defragmenting") + */ + public final boolean updateIndexes; + /** + * Should this update Materialized Views? Used by {@link this#FOR_BOOTSTRAP_STREAMING} to skip building + * views when receiving from streaming. + * + * When unset, it will only perform view updates when {@link this#updateIndexes} is true and there are views + * in the table being written to. + */ + public final Boolean updateViews; + /** + * Throws WriteTimeoutException if write does not acquire lock within write_request_timeout_in_ms + */ + public final boolean isDroppable; + /** + * Whether paired view replication should be used for view writes. + * + * This is only the case for {@link this#DEFAULT}, {@link this#DEFAULT_WITHOUT_COMMITLOG} + * and {@link this#FOR_VIEW_BUILD}. + */ + public final boolean usePairedViewReplication; + /** + * Whether the write should be appened to the commit log. A null value means default keyspace settings are used. + */ + private final Boolean writeCommitLog; + + WriteOptions(Boolean writeCommitLog, boolean updateIndexes, boolean isDroppable, Boolean updateViews, + boolean usePairedViewReplication) + { + this.writeCommitLog = writeCommitLog; + this.updateIndexes = updateIndexes; + this.isDroppable = isDroppable; + this.usePairedViewReplication = usePairedViewReplication; + this.updateViews = updateViews; + } + + public static WriteOptions forStreaming(StreamOperation streamOperation, boolean cdcEnabled) + { + if (cdcEnabled) + return streamOperation == StreamOperation.BOOTSTRAP + ? FOR_BOOTSTRAP_STREAMING + : FOR_STREAMING; + + return FOR_STREAMING_WITH_MV; + } + + public boolean shouldWriteCommitLog(String keyspaceName) + { + return writeCommitLog != null + ? writeCommitLog + : Keyspace.open(keyspaceName).getMetadata().params.durableWrites; + } + + public boolean requiresViewUpdate(ViewManager viewManager, Mutation mutation) + { + return updateViews != null ? updateViews : + updateIndexes && viewManager.updatesAffectView(Collections.singleton(mutation), false); + } +} diff --git a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java index 549955dd180a..27c61f8142b5 100644 --- a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java +++ b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BooleanSupplier; @@ -60,6 +61,7 @@ import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.SYNCHRONIZED; import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; import static org.apache.cassandra.db.commitlog.CommitLogSegment.Allocation; +import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue; /** @@ -70,9 +72,15 @@ public abstract class AbstractCommitLogSegmentManager { static final Logger logger = LoggerFactory.getLogger(AbstractCommitLogSegmentManager.class); + /** + * The latest id to replay, which is also the base for the next id: kept separate for clarity. + */ + private volatile long replayLimitId = 0; + private volatile long idBase = 0; + /** * Segment that is ready to be used. The management thread fills this and blocks until consumed. - * + *

* A single management thread produces this, and consumers are already synchronizing to make sure other work is * performed atomically with consuming this. Volatile to make sure writes by the management thread become * visible (ordered/lazySet would suffice). Consumers (advanceAllocatingFrom and discardAvailableSegment) must @@ -82,17 +90,19 @@ public abstract class AbstractCommitLogSegmentManager private final WaitQueue segmentPrepared = newWaitQueue(); - /** Active segments, containing unflushed data. The tail of this queue is the one we allocate writes to */ + /** + * Active segments, containing unflushed data. The tail of this queue is the one we allocate writes to + */ private final ConcurrentLinkedQueue activeSegments = new ConcurrentLinkedQueue<>(); /** * The segment we are currently allocating commit log records to. - * + *

* Written by advanceAllocatingFrom which synchronizes on 'this'. Volatile to ensure reads get current value. */ private volatile CommitLogSegment allocatingFrom = null; - final String storageDirectory; + final File storageDirectory; /** * Tracks commitlog size, in multiples of the segment size. We need to do this so we can "promise" size @@ -102,6 +112,8 @@ public abstract class AbstractCommitLogSegmentManager */ private final AtomicLong size = new AtomicLong(); + public static volatile CommitLogSegmentHandler commitLogSegmentHandler = new CommitLogSegmentHandler(); + @VisibleForTesting Interruptible executor; private final CommitLog commitLog; @@ -112,10 +124,13 @@ public abstract class AbstractCommitLogSegmentManager private volatile SimpleCachedBufferPool bufferPool; - AbstractCommitLogSegmentManager(final CommitLog commitLog, String storageDirectory) + private final static AtomicInteger nextId = new AtomicInteger(1); + + AbstractCommitLogSegmentManager(final CommitLog commitLog, File storageDirectory) { this.commitLog = commitLog; this.storageDirectory = storageDirectory; + init(); } private CommitLogSegment.Builder createSegmentBuilder(CommitLog.Configuration config) @@ -138,6 +153,10 @@ else if (config.diskAccessMode == DiskAccessMode.mmap) { return new MemoryMappedSegment.MemoryMappedSegmentBuilder(this); } + else if (config.diskAccessMode == DiskAccessMode.standard) + { + return new UncompressedSegment.UncompressedSegmentBuilder(this); + } throw new AssertionError("Unsupported disk access mode: " + config.diskAccessMode); } @@ -147,6 +166,38 @@ CommitLog.Configuration getConfiguration() return commitLog.configuration; } + private void init() + { + AtomicLong id = new AtomicLong(); + FileUtils.listPaths(storageDirectory.toPath()).forEach(file -> { + long maxId = Long.MIN_VALUE; + String fileName = file.getFileName().toString(); + if (CommitLogDescriptor.isValid(fileName)) + maxId = Math.max(CommitLogDescriptor.fromFileName(fileName).id, maxId); + + id.set(maxId); + }); + replayLimitId = idBase = Math.max(currentTimeMillis(), id.get() + 1); + } + + long getNextId() + { + return idBase + nextId.getAndIncrement(); + } + + boolean shouldReplay(String name) + { + return CommitLogDescriptor.fromFileName(name).id < replayLimitId; + } + + /** + * FOR TESTING PURPOSES. + */ + void resetReplayLimit() + { + replayLimitId = getNextId(); + } + void start() { assert this.segmentBuilder == null; @@ -357,11 +408,19 @@ void awaitAvailableSegment(CommitLogSegment currentAllocatingFrom) void forceRecycleAll(Collection droppedTables) { List segmentsToRecycle = new ArrayList<>(activeSegments); + + if (segmentsToRecycle.isEmpty()) + { + logger.debug("No segments to recycle"); + return; + } + CommitLogSegment last = segmentsToRecycle.get(segmentsToRecycle.size() - 1); advanceAllocatingFrom(last); // wait for the commit log modifications - last.waitForModifications(); + if (last != null) + last.waitForModifications(); // make sure the writes have materialized inside of the memtables by waiting for all outstanding writes // to complete @@ -419,9 +478,12 @@ void archiveAndDiscard(final CommitLogSegment segment) */ void handleReplayedSegment(final File file) { - // (don't decrease managed size, since this was never a "live" segment) - logger.trace("(Unopened) segment {} is no longer needed and will be deleted now", file); - FileUtils.deleteWithConfirm(file); + handleReplayedSegment(file, false, false); + } + + void handleReplayedSegment(final File file, boolean hasInvalidMutations, boolean hasFailedMutations) + { + commitLogSegmentHandler.handleReplayedSegment(file, hasInvalidMutations, hasFailedMutations); } /** @@ -562,7 +624,7 @@ private void closeAndDeleteSegmentUnsafe(CommitLogSegment segment, boolean delet */ public void shutdown() { - executor.shutdownNow(); + executor.shutdown(); // Release the management thread and delete prepared segment. // Do not block as another thread may claim the segment (this can happen during unit test initialization). discardAvailableSegment(); @@ -613,7 +675,7 @@ public Collection getActiveSegments() */ CommitLogPosition getCurrentPosition() { - return allocatingFrom.getCurrentCommitLogPosition(); + return allocatingFrom != null ? allocatingFrom.getCurrentCommitLogPosition() : CommitLogPosition.NONE; } /** diff --git a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java index cd3eb56105d6..717d0eb57738 100644 --- a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java +++ b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java @@ -43,8 +43,6 @@ import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; import static org.apache.cassandra.concurrent.Interruptible.State.NORMAL; import static org.apache.cassandra.concurrent.Interruptible.State.SHUTTING_DOWN; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; -import static org.apache.cassandra.utils.MonotonicClock.Global.preciseTime; import static org.apache.cassandra.utils.concurrent.Semaphore.newSemaphore; import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue; @@ -59,7 +57,10 @@ public abstract class AbstractCommitLogService private volatile Interruptible executor; // all Allocations written before this time will be synced - protected volatile long lastSyncedAt = MonotonicClock.Global.preciseTime.now(); + protected volatile long lastSyncedAt; + + // set to true when there is any error sync-ing and set to false upon a successful sync + private volatile boolean syncError = false; // counts of total written, and pending, log messages private final AtomicLong written = new AtomicLong(0); @@ -83,6 +84,11 @@ public abstract class AbstractCommitLogService */ final long markerIntervalNanos; + /** + * Provides time related functions for commit log syncing scheduling. + */ + protected final MonotonicClock clock; + /** * A flag that callers outside of the sync thread can use to signal they want the commitlog segments * to be flushed to disk. Note: this flag is primarily to support commit log's batch mode, which requires @@ -98,9 +104,9 @@ public abstract class AbstractCommitLogService * * Subclasses may be notified when a sync finishes by using the syncComplete WaitQueue. */ - AbstractCommitLogService(final CommitLog commitLog, final String name, long syncIntervalMillis) + AbstractCommitLogService(final CommitLog commitLog, final String name, long syncIntervalMillis, MonotonicClock clock) { - this (commitLog, name, syncIntervalMillis, false); + this (commitLog, name, syncIntervalMillis, clock, false); } /** @@ -111,10 +117,12 @@ public abstract class AbstractCommitLogService * * @param markHeadersFaster true if the chained markers should be updated more frequently than on the disk sync bounds. */ - AbstractCommitLogService(final CommitLog commitLog, final String name, long syncIntervalMillis, boolean markHeadersFaster) + AbstractCommitLogService(final CommitLog commitLog, final String name, long syncIntervalMillis, MonotonicClock clock, boolean markHeadersFaster) { this.commitLog = commitLog; this.name = name; + this.clock = clock; + this.lastSyncedAt = clock.now(); final long markerIntervalMillis; if (syncIntervalMillis < 0) @@ -151,7 +159,7 @@ void start() throw new IllegalArgumentException(String.format("Commit log flush interval must be positive: %fms", syncIntervalNanos * 1e-6)); - SyncRunnable sync = new SyncRunnable(preciseTime); + SyncRunnable sync = new SyncRunnable(clock); executor = executorFactory().infiniteLoop(name, sync, SAFE, NON_DAEMON, SYNCHRONIZED); } @@ -175,7 +183,7 @@ public void run(Interruptible.State state) throws InterruptedException { // sync and signal long pollStarted = clock.now(); - boolean flushToDisk = lastSyncedAt + syncIntervalNanos <= pollStarted || state != NORMAL || syncRequested; + boolean flushToDisk = lastSyncedAt + syncIntervalNanos - pollStarted <= 0 || state != NORMAL || syncRequested; // synchronized to prevent thread interrupts while performing IO operations and also // clear interrupted status to prevent ClosedByInterruptException in CommitLog::sync synchronized (this) @@ -206,17 +214,20 @@ public void run(Interruptible.State state) throws InterruptedException } else { + syncError = false; long now = clock.now(); if (flushToDisk) maybeLogFlushLag(pollStarted, now); long wakeUpAt = pollStarted + markerIntervalNanos; - if (wakeUpAt > now) + if (wakeUpAt - now > 0) haveWork.tryAcquireUntil(1, wakeUpAt); } } catch (Throwable t) { + syncError = true; + if (!CommitLog.handleCommitError("Failed to persist commits to disk", t)) throw new TerminateException(); else // sleep for full poll-interval after an error, so we don't spam the log file @@ -235,7 +246,7 @@ boolean maybeLogFlushLag(long pollStarted, long now) // this is the timestamp by which we should have completed the flush long maxFlushTimestamp = pollStarted + syncIntervalNanos; - if (maxFlushTimestamp > now) + if (maxFlushTimestamp - now > 0) return false; // if we have lagged noticeably, update our lag counter @@ -246,7 +257,7 @@ boolean maybeLogFlushLag(long pollStarted, long now) syncCount = 1; totalSyncDuration = flushDuration; } - syncExceededIntervalBy += now - maxFlushTimestamp; + syncExceededIntervalBy += Math.abs(now - maxFlushTimestamp); lagCount++; if (firstLagAt > 0) @@ -258,7 +269,7 @@ boolean maybeLogFlushLag(long pollStarted, long now) MINUTES, "Out of {} commit log syncs over the past {}s with average duration of {}ms, {} have exceeded the configured commit interval by an average of {}ms", syncCount, - String.format("%.2f", (now - firstLagAt) * 1e-9d), + String.format("%.2f", Math.abs(now - firstLagAt) * 1e-9d), String.format("%.2f", totalSyncDuration * 1e-6d / syncCount), lagCount, String.format("%.2f", syncExceededIntervalBy * 1e-6d / lagCount)); @@ -309,7 +320,7 @@ public void shutdown() */ public void syncBlocking() { - long requestTime = nanoTime(); + long requestTime = clock.now(); requestExtraSync(); awaitSyncAt(requestTime, null); } @@ -319,12 +330,12 @@ void awaitSyncAt(long syncTime, Context context) do { WaitQueue.Signal signal = context != null ? syncComplete.register(context, Context::stop) : syncComplete.register(); - if (lastSyncedAt < syncTime) + if (lastSyncedAt - syncTime < 0) signal.awaitUninterruptibly(); else signal.cancel(); } - while (lastSyncedAt < syncTime); + while (lastSyncedAt - syncTime < 0); } public void awaitTermination() throws InterruptedException @@ -341,4 +352,6 @@ public long getPendingTasks() { return pending.get(); } + + public boolean getSyncError() { return syncError; } } diff --git a/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java index e913e678d0a5..0f818dafda6a 100644 --- a/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java +++ b/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java @@ -18,6 +18,7 @@ package org.apache.cassandra.db.commitlog; import static org.apache.cassandra.config.CassandraRelevantProperties.BATCH_COMMIT_LOG_SYNC_INTERVAL; +import org.apache.cassandra.utils.MonotonicClock; class BatchCommitLogService extends AbstractCommitLogService { @@ -28,9 +29,9 @@ class BatchCommitLogService extends AbstractCommitLogService */ private static final int POLL_TIME_MILLIS = BATCH_COMMIT_LOG_SYNC_INTERVAL.getInt(); - public BatchCommitLogService(CommitLog commitLog) + public BatchCommitLogService(CommitLog commitLog, MonotonicClock clock) { - super(commitLog, "COMMIT-LOG-WRITER", POLL_TIME_MILLIS); + super(commitLog, "COMMIT-LOG-WRITER", POLL_TIME_MILLIS, clock); } protected void maybeWaitForSync(CommitLogSegment.Allocation alloc) diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLog.java b/src/java/org/apache/cassandra/db/commitlog/CommitLog.java index 9b38336a04b3..2112439cce98 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLog.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLog.java @@ -27,6 +27,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -43,6 +45,8 @@ import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.exceptions.CDCWriteException; import org.apache.cassandra.io.FSWriteError; @@ -53,15 +57,18 @@ import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.PathUtils; import org.apache.cassandra.metrics.CommitLogMetrics; -import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.CompressionParams; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MBeanWrapper; +import org.apache.cassandra.utils.MonotonicClock; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.StorageCompatibilityMode; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import static org.apache.cassandra.db.ColumnFamilyStore.FlushReason.STARTUP; import static org.apache.cassandra.db.commitlog.CommitLogSegment.Allocation; import static org.apache.cassandra.db.commitlog.CommitLogSegment.ENTRY_OVERHEAD_SIZE; import static org.apache.cassandra.utils.Clock.Global.nanoTime; @@ -75,20 +82,25 @@ public class CommitLog implements CommitLogMBean { private static final Logger logger = LoggerFactory.getLogger(CommitLog.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 10, TimeUnit.SECONDS); public static final CommitLog instance = CommitLog.construct(); - private static final BiPredicate unmanagedFilesFilter = (dir, name) -> CommitLogDescriptor.isValid(name) && CommitLogSegment.shouldReplay(name); + private volatile AbstractCommitLogSegmentManager segmentManager; - final public AbstractCommitLogSegmentManager segmentManager; + private final BiPredicate unmanagedFilesFilter = (dir, name) -> CommitLogDescriptor.isValid(name) && segmentManager.shouldReplay(name); public final CommitLogArchiver archiver; public final CommitLogMetrics metrics; final AbstractCommitLogService executor; - + private Set segmentsWithInvalidMutations; + private Set segmentsWithFailedMutations; volatile Configuration configuration; private boolean started = false; + @VisibleForTesting + final MonotonicClock clock; + private static CommitLog construct() { CommitLog log = new CommitLog(CommitLogArchiver.construct(), DatabaseDescriptor.getCommitLogSegmentMgrProvider()); @@ -113,16 +125,18 @@ private static CommitLog construct() this.archiver = archiver; metrics = new CommitLogMetrics(); + this.clock = MonotonicClock.Global.preciseTime; + switch (DatabaseDescriptor.getCommitLogSync()) { case periodic: - executor = new PeriodicCommitLogService(this); + executor = new PeriodicCommitLogService(this, clock); break; case batch: - executor = new BatchCommitLogService(this); + executor = new BatchCommitLogService(this, clock); break; case group: - executor = new GroupCommitLogService(this); + executor = new GroupCommitLogService(this, clock); break; default: throw new IllegalArgumentException("Unknown commitlog service type: " + DatabaseDescriptor.getCommitLogSync()); @@ -167,24 +181,57 @@ public boolean hasFilesToReplay() public File[] getUnmanagedFiles() { - File[] files = new File(segmentManager.storageDirectory).tryList(unmanagedFilesFilter); + return getFilteredFiles(Optional.empty()); + } + + /** + * Returns segment files to replay according to the configured segment manager. + * + * @param filter An optional filter to apply to segment files returned by the segment manager. + * @return Segment files to replay, optionally filtered. + */ + public File[] getFilteredFiles(Optional> filter) + { + BiPredicate compositeFilter = (path, name) -> + filter.orElse((ignored1, ignored2) -> true).test(path, name) + && unmanagedFilesFilter.test(path, name); + + File[] files = segmentManager.storageDirectory.tryList(compositeFilter); if (files == null) return new File[0]; + return files; } /** - * Perform recovery on commit logs located in the directory specified by the config file. + * Updates the commit log storage directory and re-initializes the segment manager accordingly. + *

+ * Used by CNDB. * - * @return the number of mutations replayed + * @param commitLogLocation storage directory to update to + * @return this commit log with updated storage directory + */ + public CommitLog forPath(File commitLogLocation) + { + segmentManager = new CommitLogSegmentManagerStandard(this, commitLogLocation); + return this; + } + + /** + * Perform recovery on commit logs located in the directory specified by the config file, + * performing archive and restore before. + * The recovery is executed as a commit log read followed by a flush. + * + * @param flushReason the reason for flushing that fallows commit log reading, use + * {@link org.apache.cassandra.db.ColumnFamilyStore.FlushReason#STARTUP} when recovering on a + * node start. Use {@link org.apache.cassandra.db.ColumnFamilyStore.FlushReason#REMOTE_REPLAY} + * when replying commit logs to a remote storage. + * @return keyspaces and the corresponding number of partition updates * @throws IOException */ - public int recoverSegmentsOnDisk() throws IOException + public Map recoverSegmentsOnDiskWithArchive(ColumnFamilyStore.FlushReason flushReason) throws IOException { - // submit all files for this segment manager for archiving prior to recovery - CASSANDRA-6904 - // The files may have already been archived by normal CommitLog operation. This may cause errors in this - // archiving pass, which we should not treat as serious. - for (File file : getUnmanagedFiles()) + for (File file : getFilteredFiles(Optional.empty())) { archiver.maybeArchive(file.path(), file.name()); archiver.maybeWaitForArchiving(file.name()); @@ -194,8 +241,24 @@ public int recoverSegmentsOnDisk() throws IOException archiver.maybeRestoreArchive(); // List the files again as archiver may have added segments. - File[] files = getUnmanagedFiles(); - int replayed = 0; + return recoverSegmentsOnDiskNoArchive(flushReason, getFilteredFiles(Optional.empty())); + } + + /** + * Perform recovery on commit logs located in the directory specified by the config file, without archiving. + * The recovery is executed as a commit log read followed by a flush. + * + * @param flushReason the reason for flushing that fallows commit log reading, use + * {@link org.apache.cassandra.db.ColumnFamilyStore.FlushReason#STARTUP} when recovering on a + * node start. Use {@link org.apache.cassandra.db.ColumnFamilyStore.FlushReason#REMOTE_REPLAY} + * when replying commit logs to a remote storage. + * @param files THe segment files to recovery. + * @return keyspaces and the corresponding number of partition updates + * @throws IOException + */ + public Map recoverSegmentsOnDiskNoArchive(ColumnFamilyStore.FlushReason flushReason, File[] files) throws IOException + { + Map replayedKeyspaces = Collections.emptyMap(); if (files.length == 0) { logger.info("No commitlog files found; skipping replay"); @@ -205,36 +268,46 @@ public int recoverSegmentsOnDisk() throws IOException Arrays.sort(files, new CommitLogSegment.CommitLogSegmentFileComparator()); logger.info("Replaying {}", StringUtils.join(files, ", ")); long startTime = nanoTime(); - replayed = recoverFiles(files); + replayedKeyspaces = recoverFiles(flushReason, files); long endTime = nanoTime(); - logger.info("Log replay complete, {} replayed mutations in {} ms", replayed, + logger.info("Log replay complete, {} replayed mutations in {} ms", + replayedKeyspaces.values().stream().reduce(Integer::sum).orElse(0), TimeUnit.NANOSECONDS.toMillis(endTime - startTime)); for (File f : files) - segmentManager.handleReplayedSegment(f); + { + segmentManager.handleReplayedSegment(f, segmentsWithInvalidMutations.contains(f.name()), segmentsWithFailedMutations.contains(f.name())); + } } - return replayed; + return replayedKeyspaces; } /** - * Perform recovery on a list of commit log files. + * Perform recovery on a list of commit log files. The recovery is executed as a commit log read followed by a + * flush. * + * @param flushReason the reason for flushing that follows commit log reading * @param clogs the list of commit log files to replay - * @return the number of mutations replayed + * @return keyspaces and the corresponding number of partition updates */ - public int recoverFiles(File... clogs) throws IOException + @VisibleForTesting + public Map recoverFiles(ColumnFamilyStore.FlushReason flushReason, File... clogs) throws IOException { CommitLogReplayer replayer = CommitLogReplayer.construct(this, getLocalHostId()); replayer.replayFiles(clogs); - return replayer.blockForWrites(); + + Map res = replayer.blockForWrites(flushReason); + segmentsWithFailedMutations = replayer.getSegmentWithFailedMutations(); + segmentsWithInvalidMutations = replayer.getSegmentWithInvalidMutations(); + return res; } - public void recoverPath(String path) throws IOException + public void recoverPath(String path, boolean tolerateTruncation) throws IOException { CommitLogReplayer replayer = CommitLogReplayer.construct(this, getLocalHostId()); - replayer.replayPath(new File(path), false); - replayer.blockForWrites(); + replayer.replayPath(new File(PathUtils.getPath(path)), tolerateTruncation); + replayer.blockForWrites(STARTUP); } private static UUID getLocalHostId() @@ -247,7 +320,12 @@ private static UUID getLocalHostId() */ public void recover(String path) throws IOException { - recoverPath(path); + recoverPath(path, false); + } + + public void setCommitLogSegmentHandler(CommitLogSegmentHandler handler) + { + AbstractCommitLogSegmentManager.commitLogSegmentHandler = handler; } /** @@ -291,21 +369,41 @@ public void requestExtraSync() executor.requestExtraSync(); } + /** + * If there was an exception when sync-ing, and if the commit log failure policy is + * {@link Config.CommitFailurePolicy#fail_writes} then mutations will be rejected until + * the sync error is cleared, which happens after a successful sync. + * @return + */ + @VisibleForTesting + public boolean shouldRejectMutations() + { + return executor.getSyncError() && + DatabaseDescriptor.getCommitFailurePolicy() == Config.CommitFailurePolicy.fail_writes; + } + /** * Add a Mutation to the commit log. If CDC is enabled, this can fail. * * @param mutation the Mutation to add to the log - * @throws CDCWriteException */ public CommitLogPosition add(Mutation mutation) throws CDCWriteException { assert mutation != null; - mutation.validateSize(MessagingService.current_version, ENTRY_OVERHEAD_SIZE); + int storageVersion = StorageCompatibilityMode.current().storageMessagingVersion(); + mutation.validateSize(storageVersion, ENTRY_OVERHEAD_SIZE); + + if (shouldRejectMutations()) + { + String errorMsg = "Rejecting mutation due to a failure sync-ing commit log segments"; + noSpamLogger.error(errorMsg); + throw new FSWriteError(new IllegalStateException(errorMsg), segmentManager.allocatingFrom().getPath()); + } try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get()) { - Mutation.serializer.serialize(mutation, dob, MessagingService.current_version); + Mutation.serializer.serialize(mutation, dob, storageVersion); int size = dob.getLength(); int totalSize = size + ENTRY_OVERHEAD_SIZE; Allocation alloc = segmentManager.allocate(mutation, totalSize); @@ -510,10 +608,10 @@ synchronized public void shutdownBlocking() throws InterruptedException /** * FOR TESTING PURPOSES - * @return the number of files recovered + * @return keyspaces and the corresponding number of partition updates */ @VisibleForTesting - synchronized public int resetUnsafe(boolean deleteSegments) throws IOException + synchronized public Map resetUnsafe(boolean deleteSegments) throws IOException { stopUnsafe(deleteSegments); resetConfiguration(); @@ -551,9 +649,9 @@ synchronized public void stopUnsafe(boolean deleteSegments) throw new UncheckedInterruptedException(e); } segmentManager.stopUnsafe(deleteSegments); - CommitLogSegment.resetReplayLimit(); + segmentManager.resetReplayLimit(); if (DatabaseDescriptor.isCDCEnabled() && deleteSegments) - for (File f : new File(DatabaseDescriptor.getCDCLogLocation()).tryList()) + for (File f : DatabaseDescriptor.getCDCLogLocation().tryList()) f.delete(); } @@ -561,21 +659,21 @@ synchronized public void stopUnsafe(boolean deleteSegments) * FOR TESTING PURPOSES */ @VisibleForTesting - synchronized public int restartUnsafe() throws IOException + synchronized public Map restartUnsafe() throws IOException { started = false; - return start().recoverSegmentsOnDisk(); + return start().recoverSegmentsOnDiskWithArchive(ColumnFamilyStore.FlushReason.STARTUP); } public static long freeDiskSpace() { - return PathUtils.tryGetSpace(new File(DatabaseDescriptor.getCommitLogLocation()).toPath(), FileStore::getTotalSpace); + return PathUtils.tryGetSpace(DatabaseDescriptor.getCommitLogLocation().toPath(), FileStore::getTotalSpace); } @VisibleForTesting public static boolean handleCommitError(String message, Throwable t) { - JVMStabilityInspector.inspectCommitLogThrowable(t); + JVMStabilityInspector.inspectCommitLogThrowable(message, t); switch (DatabaseDescriptor.getCommitFailurePolicy()) { // Needed here for unit tests to not fail on default assertion @@ -587,6 +685,7 @@ public static boolean handleCommitError(String message, Throwable t) String errorMsg = String.format("%s. Commit disk failure policy is %s; terminating thread.", message, DatabaseDescriptor.getCommitFailurePolicy()); logger.error(addAdditionalInformationIfPossible(errorMsg), t); return false; + case fail_writes: case ignore: logger.error(addAdditionalInformationIfPossible(message), t); return true; @@ -614,6 +713,11 @@ private static String addAdditionalInformationIfPossible(String msg) return msg; } + public AbstractCommitLogSegmentManager getSegmentManager() + { + return segmentManager; + } + public static final class Configuration { /** diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java index c0bba5b1dabf..00ba0cf38b22 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java @@ -294,7 +294,7 @@ else if (fromHeader != null) descriptor = fromHeader; else descriptor = fromName; - if (descriptor.version > CommitLogDescriptor.current_version) + if (descriptor.version > CommitLogDescriptor.CURRENT_VERSION) throw new IllegalStateException("Unsupported commit log version: " + descriptor.version); if (descriptor.compression != null) diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java index 08bb189c908f..f5bcca38c39a 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java @@ -44,6 +44,7 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.utils.JsonUtils; +import org.apache.cassandra.utils.StorageCompatibilityMode; import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt; @@ -59,17 +60,27 @@ public class CommitLogDescriptor static final String COMPRESSION_PARAMETERS_KEY = "compressionParameters"; static final String COMPRESSION_CLASS_KEY = "compressionClass"; + /** + * the versions below ARE NOT the same thing as MessagingService versions + * see {@link #getMessagingVersion()} + */ // We don't support anything pre-3.0 public static final int VERSION_30 = 6; public static final int VERSION_40 = 7; public static final int VERSION_50 = 8; + // Stargazer 1.0 messaging + static final int VERSION_DS_10 = MessagingService.VERSION_DS_10; + static final int VERSION_DS_11 = MessagingService.VERSION_DS_11; + static final int VERSION_DS_12 = MessagingService.VERSION_DS_12; + static final int VERSION_DS_20 = MessagingService.VERSION_DS_20; + // For compatibility with CNDB + public static final int VERSION_DSE_68 = 680; /** * Increment this number if there is a changes in the commit log disc layout or MessagingVersion changes. - * Note: make sure to handle {@link #getMessagingVersion()} + * Note: make sure to handle {@link #currentVersion()} and {@link #getMessagingVersion()} */ - @VisibleForTesting - public static final int current_version = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_50; + public static final int CURRENT_VERSION = currentVersion(); final int version; public final long id; @@ -86,7 +97,19 @@ public CommitLogDescriptor(int version, long id, ParameterizedClass compression, public CommitLogDescriptor(long id, ParameterizedClass compression, EncryptionContext encryptionContext) { - this(current_version, id, compression, encryptionContext); + this(currentStorageVersion(), id, compression, encryptionContext); + } + + /** + * Returns the commit log version to use for new segments, respecting storage compatibility mode. + * When compatibility mode is set (e.g., HCD_1), this ensures commit logs are written in a format + * that older versions can read. + * + * @return the commit log version appropriate for the current storage compatibility mode + */ + public static int currentStorageVersion() + { + return currentVersion(StorageCompatibilityMode.current().storageMessagingVersion()); } public static void writeHeader(ByteBuffer out, CommitLogDescriptor descriptor) @@ -217,16 +240,64 @@ private static Matcher extactFromFileName(String name) public int getMessagingVersion() { - switch (version) + return getMessagingVersion(version); + } + + @VisibleForTesting + static int getMessagingVersion(int commitLogVersion) + { + switch (commitLogVersion) { case VERSION_30: - return MessagingService.VERSION_30; + return MessagingService.Version.VERSION_30.value; case VERSION_40: - return MessagingService.VERSION_40; + return MessagingService.Version.VERSION_40.value; case VERSION_50: - return MessagingService.VERSION_50; + return MessagingService.Version.VERSION_50.value; + case VERSION_DS_10: + return MessagingService.Version.VERSION_DS_10.value; + case VERSION_DS_11: + return MessagingService.Version.VERSION_DS_11.value; + case VERSION_DS_12: + return MessagingService.VERSION_DS_12; + case VERSION_DS_20: + return MessagingService.Version.VERSION_DS_20.value; + case VERSION_DSE_68: + return MessagingService.Version.VERSION_DSE_68.value; + default: + throw new IllegalStateException("Unknown commitlog version " + commitLogVersion); + } + } + + private static int currentVersion() + { + return currentVersion(MessagingService.current_version); + } + + @VisibleForTesting + static int currentVersion(int messagingVersion) + { + switch(messagingVersion) + { + case MessagingService.VERSION_30: + case MessagingService.VERSION_3014: + return VERSION_30; + case MessagingService.VERSION_40: + return VERSION_40; + case MessagingService.VERSION_50: + return VERSION_50; + case MessagingService.VERSION_DSE_68: + return VERSION_DSE_68; + case MessagingService.VERSION_DS_10: + return VERSION_DS_10; + case MessagingService.VERSION_DS_11: + return VERSION_DS_11; + case MessagingService.VERSION_DS_12: + return VERSION_DS_12; + case MessagingService.VERSION_DS_20: + return VERSION_DS_20; default: - throw new IllegalStateException("Unknown commitlog version " + version); + throw new IllegalStateException("Unknown messaging version " + messagingVersion); } } diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java index 3b3a21af3c56..628719881c1f 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java @@ -98,12 +98,6 @@ public String toString() ')'; } - public CommitLogPosition clone() - { - return new CommitLogPosition(segmentId, position); - } - - public static class CommitLogPositionSerializer implements ISerializer { public void serialize(CommitLogPosition clsp, DataOutputPlus out) throws IOException diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java index ee052354db81..503fa56bd6fc 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java @@ -21,6 +21,7 @@ import java.io.IOException; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.schema.TableId; public interface CommitLogReadHandler { @@ -73,4 +74,11 @@ class CommitLogReadException extends IOException * @param desc CommitLogDescriptor for mutation being processed */ void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc); + + /** + * Process an invalid mutation + * + * @param id table id corresponding to the invalid mutation + */ + void handleInvalidMutation(TableId id); } diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java index 451ee37595d0..f0d15c6ae271 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.db.commitlog; +import java.io.IOError; import java.nio.file.Files; import java.nio.file.Path; import java.io.*; @@ -56,7 +57,8 @@ public class CommitLogReader @VisibleForTesting public static final int ALL_MUTATIONS = -1; private final CRC32 checksum; - private final Map invalidMutations; + private final Map invalidMutations; // if we can't find a table for a mutation, we count it here + private final Set segmentsWithInvalidMutations; private byte[] buffer; @@ -64,9 +66,15 @@ public CommitLogReader() { checksum = new CRC32(); invalidMutations = new HashMap<>(); + segmentsWithInvalidMutations = new HashSet<>(); buffer = new byte[4096]; } + public Set getSegmentsWithInvalidMutations() + { + return segmentsWithInvalidMutations; + } + public Set> getInvalidMutations() { return invalidMutations.entrySet(); @@ -112,6 +120,19 @@ static List filterCommitLogFiles(File[] toFilter) // let recover deal with it filtered.add(file); } + catch (IOError e) + { + // Only handle file-not-found errors gracefully; let other IOErrors propagate + // as they may indicate corruption or serious I/O issues + if (e.getCause() instanceof java.nio.file.NoSuchFileException) + { + filtered.add(file); + } + else + { + throw e; + } + } } return filtered; @@ -444,9 +465,12 @@ protected void readMutation(CommitLogReadHandler handler, { i = new AtomicInteger(1); invalidMutations.put(ex.id, i); + segmentsWithInvalidMutations.add(desc.fileName()); } else i.incrementAndGet(); + + handler.handleInvalidMutation(ex.id); return; } catch (Throwable t) diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java index 8e26425ed544..420683f76cb4 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java @@ -29,20 +29,21 @@ import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Ordering; - -import org.apache.cassandra.io.util.File; +import com.google.common.util.concurrent.FutureCallback; +import org.apache.cassandra.concurrent.ImmediateExecutor; import org.apache.commons.lang3.StringUtils; - -import org.apache.cassandra.utils.concurrent.Future; -import org.cliffc.high_scale_lib.NonBlockingHashSet; +import org.apache.commons.lang3.tuple.Triple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,8 +53,10 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.WriteOptions; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.schema.Schema; @@ -61,14 +64,25 @@ import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.WrappedRunnable; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.Promise; +import org.jctools.maps.NonBlockingHashMap; +import org.jctools.maps.NonBlockingHashSet; import static java.lang.String.format; -import static org.apache.cassandra.config.CassandraRelevantProperties.COMMITLOG_IGNORE_REPLAY_ERRORS; -import static org.apache.cassandra.config.CassandraRelevantProperties.COMMITLOG_MAX_OUTSTANDING_REPLAY_BYTES; -import static org.apache.cassandra.config.CassandraRelevantProperties.COMMITLOG_MAX_OUTSTANDING_REPLAY_COUNT; -import static org.apache.cassandra.config.CassandraRelevantProperties.COMMIT_LOG_REPLAY_LIST; +import static org.apache.cassandra.config.CassandraRelevantProperties.*; +/** + * Replays commit logs (reads commit logs and flushes new sstables). + * + * Note that instances of this class are meant to be used for a single replay only. Do not reuse the same + * instance for another replay as internal accumulated state (keyspacesReplayed) is not + * reset before the replay. + */ public class CommitLogReplayer implements CommitLogReadHandler { @VisibleForTesting @@ -76,12 +90,14 @@ public class CommitLogReplayer implements CommitLogReadHandler @VisibleForTesting public static MutationInitiator mutationInitiator = new MutationInitiator(); private static final Logger logger = LoggerFactory.getLogger(CommitLogReplayer.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 10, TimeUnit.SECONDS); private static final int MAX_OUTSTANDING_REPLAY_COUNT = COMMITLOG_MAX_OUTSTANDING_REPLAY_COUNT.getInt(); - private final Set keyspacesReplayed; + private final Map keyspacesReplayed; private final Queue> futures; - private final AtomicInteger replayedCount; + private final Set segmentsWithFailedMutations; // mutations that failed to apply + private final Map> cfPersisted; private final CommitLogPosition globalPosition; @@ -97,15 +113,16 @@ public class CommitLogReplayer implements CommitLogReadHandler @VisibleForTesting protected CommitLogReader commitLogReader; + private volatile boolean replayed = false; + CommitLogReplayer(CommitLog commitLog, CommitLogPosition globalPosition, Map> cfPersisted, ReplayFilter replayFilter) { - this.keyspacesReplayed = new NonBlockingHashSet<>(); + this.keyspacesReplayed = new NonBlockingHashMap<>(); this.futures = new ArrayDeque<>(); - // count the number of replayed mutation. We don't really care about atomicity, but we need it to be a reference. - this.replayedCount = new AtomicInteger(); + this.segmentsWithFailedMutations = new NonBlockingHashSet<>(); this.cfPersisted = cfPersisted; this.globalPosition = globalPosition; this.replayFilter = replayFilter; @@ -182,20 +199,54 @@ public static CommitLogReplayer construct(CommitLog commitLog, UUID localHostId) cfPersisted.put(cfs.metadata.id, filter); } CommitLogPosition globalPosition = firstNotCovered(cfPersisted.values()); - logger.debug("Global replay position is {} from columnfamilies {}", globalPosition, FBUtilities.toString(cfPersisted)); + + // Limit the amount of column family data logged to prevent massive log lines + if (logger.isDebugEnabled()) + { + int maxColumnFamiliesToLog = 10; + int cfCount = cfPersisted.size(); + if (cfCount <= maxColumnFamiliesToLog) + { + logger.debug("Global replay position is {} from {} columnfamilies: {}", + globalPosition, cfCount, FBUtilities.toString(cfPersisted)); + } + else + { + // For large numbers of column families, just log the count and a sample + Map> sample = new HashMap<>(); + int count = 0; + for (Map.Entry> entry : cfPersisted.entrySet()) + { + if (count++ >= maxColumnFamiliesToLog) + break; + sample.put(entry.getKey(), entry.getValue()); + } + logger.debug("Global replay position is {} from {} columnfamilies (showing first {}): {}", + globalPosition, cfCount, maxColumnFamiliesToLog, FBUtilities.toString(sample)); + logger.debug("Use TRACE level to see all {} columnfamilies", cfCount); + logger.trace("Full columnfamilies list: {}", FBUtilities.toString(cfPersisted)); + } + } + return new CommitLogReplayer(commitLog, globalPosition, cfPersisted, replayFilter); } public void replayPath(File file, boolean tolerateTruncation) throws IOException { + Preconditions.checkArgument(!replayed, "CommitlogReplayer can only replay once"); + sawCDCMutation = false; commitLogReader.readCommitLogSegment(this, file, globalPosition, CommitLogReader.ALL_MUTATIONS, tolerateTruncation); if (sawCDCMutation) handleCDCReplayCompletion(file); + + replayed = true; } public void replayFiles(File[] clogs) throws IOException { + Preconditions.checkArgument(!replayed, "CommitlogReplayer can only replay once"); + List filteredLogs = CommitLogReader.filterCommitLogFiles(clogs); int i = 0; for (File file: filteredLogs) @@ -206,6 +257,8 @@ public void replayFiles(File[] clogs) throws IOException if (sawCDCMutation) handleCDCReplayCompletion(file); } + + replayed = true; } @@ -216,7 +269,7 @@ public void replayFiles(File[] clogs) throws IOException private void handleCDCReplayCompletion(File f) throws IOException { // Can only reach this point if CDC is enabled, thus we have a CDCSegmentManager - ((CommitLogSegmentManagerCDC)CommitLog.instance.segmentManager).addCDCSize(f.length()); + ((CommitLogSegmentManagerCDC)CommitLog.instance.getSegmentManager()).addCDCSize(f.length()); File dest = new File(DatabaseDescriptor.getCDCLogLocation(), f.name()); @@ -239,9 +292,10 @@ private void handleCDCReplayCompletion(File f) throws IOException /** * Flushes all keyspaces associated with this replayer in parallel, blocking until their flushes are complete. - * @return the number of mutations replayed + * @param flushReason the reason for flushing + * @return keyspaces and the corresponding number of partition updates */ - public int blockForWrites() + public Map blockForWrites(ColumnFamilyStore.FlushReason flushReason) { for (Map.Entry entry : commitLogReader.getInvalidMutations()) logger.warn("Skipped {} mutations from unknown (probably removed) CF with id {}", entry.getValue(), entry.getKey()); @@ -255,12 +309,28 @@ public int blockForWrites() boolean flushingSystem = false; List> futures = new ArrayList>(); - for (Keyspace keyspace : keyspacesReplayed) + for (Keyspace keyspace : keyspacesReplayed.keySet()) { if (keyspace.getName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME)) flushingSystem = true; - futures.addAll(keyspace.flush(ColumnFamilyStore.FlushReason.STARTUP)); + for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) + { + Future f = cfs.forceFlush(flushReason); + futures.add(f); + f.addCallback(new FutureCallback() + { + public void onSuccess(CommitLogPosition result) + { + mutationInitiator.onFlushed(cfs.metadata.id); + } + + public void onFailure(Throwable t) + { + // no-op + } + }, ImmediateExecutor.INSTANCE); + } } // also flush batchlog incase of any MV updates @@ -271,7 +341,7 @@ public int blockForWrites() FBUtilities.waitOnFutures(futures); - return replayedCount.get(); + return Collections.unmodifiableMap(Maps.transformValues(keyspacesReplayed, AtomicInteger::get)); } /* @@ -281,8 +351,15 @@ public int blockForWrites() @VisibleForTesting public static class MutationInitiator { + protected void onInvalidMutation(TableId id) + { + logger.debug("Invalid mutation detected for table id {}", id); + } + + protected void onFailedMutation(String keyspace, Collection tableIds) {} + protected Future initiateMutation(final Mutation mutation, - final long segmentId, + final CommitLogDescriptor desc, final int serializedSize, final int entryLocation, final CommitLogReplayer commitLogReplayer) @@ -304,6 +381,8 @@ public void runMayThrow() // or c) are part of a cf that was dropped. // Keep in mind that the cf.name() is suspect. do every thing based on the cfid instead. Mutation.PartitionUpdateCollector newPUCollector = null; + List> updatesAndPositions = new ArrayList<>(); + int replayedCount = 0; for (PartitionUpdate update : commitLogReplayer.replayFilter.filter(mutation)) { if (Schema.instance.getTableMetadata(update.metadata().id) == null) @@ -311,24 +390,78 @@ public void runMayThrow() // replay if current segment is newer than last flushed one or, // if it is the last known segment, if we are after the commit log segment position - if (commitLogReplayer.shouldReplay(update.metadata().id, new CommitLogPosition(segmentId, entryLocation))) + if (shouldReplay(update.metadata().id, commitLogReplayer, desc.id, entryLocation)) { if (newPUCollector == null) newPUCollector = new Mutation.PartitionUpdateCollector(mutation.getKeyspaceName(), mutation.key()); newPUCollector.add(update); - commitLogReplayer.replayedCount.incrementAndGet(); + replayedCount++; + updatesAndPositions.add(Triple.of(update, desc.id, entryLocation)); + } + else + { + onSkipped(update); } } if (newPUCollector != null) { assert !newPUCollector.isEmpty(); - Keyspace.open(newPUCollector.getKeyspaceName()).apply(newPUCollector.build(), false, true, false); - commitLogReplayer.keyspacesReplayed.add(keyspace); + Keyspace.open(newPUCollector.getKeyspaceName()).applyFuture(newPUCollector.build(), WriteOptions.FOR_COMMITLOG_REPLAY, false) + .addListener(() -> { + for (Triple updateAndPosition : updatesAndPositions) + onReplayed(updateAndPosition.getLeft(), updateAndPosition.getMiddle(), updateAndPosition.getRight()); + }); + + commitLogReplayer.keyspacesReplayed.computeIfAbsent(keyspace, k -> new AtomicInteger(0)) + .addAndGet(replayedCount); } } }; - return Stage.MUTATION.submit(runnable, serializedSize); + Promise returnFuture = new AsyncPromise<>(); + Future mutationFuture = Stage.MUTATION.submit(runnable, serializedSize);//.addCallback((integer, ex) -> { + mutationFuture.addListener(() -> + { + try + { + Integer result = mutationFuture.get(); + returnFuture.trySuccess(result); + } + catch (Throwable t) + { + noSpamLogger.warn("Failed applying mutation for keyspace {}", mutation.getKeyspaceName(), t); + onFailedMutation(mutation.getKeyspaceName(), mutation.getTableIds()); + commitLogReplayer.segmentsWithFailedMutations.add(desc.fileName()); + returnFuture.tryFailure(Throwables.unchecked(t.getCause())); + } + }, ImmediateExecutor.INSTANCE); + return returnFuture; + } + + /** + * Return true if mutation at given commitlog position should be replayed into memtable + */ + protected boolean shouldReplay(TableId tableId, CommitLogReplayer commitLogReplayer, long segmentId, int entryLocation) + { + return commitLogReplayer.shouldReplay(tableId, new CommitLogPosition(segmentId, entryLocation)); + } + + /** + * Called when a table is flushed successfully, including table without replayed mutation + */ + protected void onFlushed(TableId tableId) + { + // CNDB will override it to monitor flush status + } + + protected void onReplayed(PartitionUpdate update, long segmentId, int entryLocation) + { + // Override for test purposes + } + + protected void onSkipped(PartitionUpdate update) + { + // Override for test purposes } } @@ -353,7 +486,25 @@ public static IntervalSet persistedIntervals(Iterable sample = new ArrayList<>(); + int count = 0; + for (String sstable : skippedSSTables) + { + if (count++ >= maxSSTablesToLog) + break; + sample.add(sstable); + } + logger.debug("Ignored commitLogIntervals from {} sstables (showing first {}): {}", + skippedSSTables.size(), maxSSTablesToLog, sample); + logger.debug("Use TRACE level to see all {} skipped sstables", skippedSSTables.size()); + logger.trace("Full list of ignored sstables: {}", skippedSSTables); + } } if (truncatedAt != null) @@ -381,7 +532,7 @@ public static CommitLogPosition firstNotCovered(Collection filter(Mutation mutation); @@ -396,8 +547,18 @@ public static ReplayFilter create() { String replayList = COMMIT_LOG_REPLAY_LIST.getString(); + // If no replaylist is supplied an empty array of strings is used to replay everything. if (replayList == null) + { + String customReplayFilter = CUSTOM_REPLAY_FILTER_CLASS.getString(); + if (customReplayFilter != null) + return FBUtilities.construct(customReplayFilter, "custom_replay_filter"); return new AlwaysReplayFilter(); + } + else + { + logger.info("Commit log replay list set by cassandra.replayList property to: {}", replayList); + } Multimap toReplay = HashMultimap.create(); for (String rawPair : replayList.split(",")) @@ -496,7 +657,8 @@ public boolean includes(TableMetadataRef metadata) */ private boolean shouldReplay(TableId tableId, CommitLogPosition position) { - return !cfPersisted.get(tableId).contains(position); + IntervalSet intervalSet = cfPersisted.get(tableId); + return intervalSet == null || !intervalSet.contains(position); } protected boolean pointInTimeExceeded(Mutation fm) @@ -509,6 +671,25 @@ protected boolean pointInTimeExceeded(Mutation fm) return false; } + public Set getSegmentWithFailedMutations() + { + return segmentsWithFailedMutations; + } + + /** + * Get segments with invalid mutations. + * Invalid mutations are mutations for which the table can not be found. + */ + public Set getSegmentWithInvalidMutations() + { + return commitLogReader.getSegmentsWithInvalidMutations(); + } + + public void handleInvalidMutation(TableId id) + { + mutationInitiator.onInvalidMutation(id); + } + public void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc) { if (DatabaseDescriptor.isCDCEnabled() && m.trackedByCDC()) @@ -516,7 +697,7 @@ public void handleMutation(Mutation m, int size, int entryLocation, CommitLogDes pendingMutationBytes += size; futures.offer(mutationInitiator.initiateMutation(m, - desc.id, + desc, size, entryLocation, this)); diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java index b64f05b00fe9..41422518cfd1 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java @@ -54,7 +54,6 @@ import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.concurrent.WaitQueue; -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt; import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue; @@ -65,8 +64,6 @@ */ public abstract class CommitLogSegment { - private final static long idBase; - private CDCState cdcState = CDCState.PERMITTED; public enum CDCState { @@ -77,16 +74,14 @@ public enum CDCState final Object cdcStateLock = new Object(); private final static AtomicInteger nextId = new AtomicInteger(1); - private static long replayLimitId; static { long maxId = Long.MIN_VALUE; - for (File file : new File(DatabaseDescriptor.getCommitLogLocation()).tryList()) + for (File file : DatabaseDescriptor.getCommitLogLocation().tryList()) { if (CommitLogDescriptor.isValid(file.name())) maxId = Math.max(CommitLogDescriptor.fromFileName(file.name()).id, maxId); } - replayLimitId = idBase = Math.max(currentTimeMillis(), maxId + 1); } // The commit log entry overhead in bytes (int: length + int: head checksum + int: tail checksum) @@ -138,11 +133,6 @@ public enum CDCState public final CommitLogDescriptor descriptor; - static long getNextId() - { - return idBase + nextId.getAndIncrement(); - } - /** * Constructs a new segment file. */ @@ -150,7 +140,7 @@ static long getNextId() { this.manager = manager; - id = getNextId(); + id = manager.getNextId(); descriptor = new CommitLogDescriptor(id, manager.getConfiguration().getCompressorClass(), manager.getConfiguration().getEncryptionContext()); @@ -222,20 +212,6 @@ Allocation allocate(Mutation mutation, int size) } } - static boolean shouldReplay(String name) - { - return CommitLogDescriptor.fromFileName(name).id < replayLimitId; - } - - /** - * FOR TESTING PURPOSES. - */ - @VisibleForTesting - public static void resetReplayLimit() - { - replayLimitId = getNextId(); - } - // allocate bytes in the segment, or return -1 if not enough space private int allocate(int size) { diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentHandler.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentHandler.java new file mode 100644 index 000000000000..be2aa2f2db75 --- /dev/null +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentHandler.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.commitlog; + +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * After recovery of commit logs is performed, this class is responsible for handling the commit log files that were + * replayed. + */ +public class CommitLogSegmentHandler +{ + private static final Logger logger = LoggerFactory.getLogger(CommitLogSegmentHandler.class); + + public void handleReplayedSegment(final File file, boolean hasInvalidMutations, boolean hasFailedMutations) + { + if (!hasFailedMutations && !hasInvalidMutations) + { + // (don't decrease managed size, since this was never a "live" segment) + logger.trace("(Unopened) segment {} is no longer needed and will be deleted now", file); + FileUtils.deleteWithConfirm(file); + } + else + { + logger.debug("File {} should not be deleted as it contains invalid or failed mutations", file.name()); + } + } +} diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java index 7dfe7add8fc6..c26feac491ab 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java @@ -50,10 +50,10 @@ public class CommitLogSegmentManagerCDC extends AbstractCommitLogSegmentManager static final Logger logger = LoggerFactory.getLogger(CommitLogSegmentManagerCDC.class); private final CDCSizeTracker cdcSizeTracker; - public CommitLogSegmentManagerCDC(final CommitLog commitLog, String storageDirectory) + public CommitLogSegmentManagerCDC(final CommitLog commitLog, File storageDirectory) { super(commitLog, storageDirectory); - cdcSizeTracker = new CDCSizeTracker(this, new File(DatabaseDescriptor.getCDCLogLocation())); + cdcSizeTracker = new CDCSizeTracker(this, DatabaseDescriptor.getCDCLogLocation()); } @Override @@ -93,7 +93,7 @@ public long deleteOldLinkedCDCCommitLogSegment(long bytesToFree) if (bytesToFree <= 0) return 0; - File cdcDir = new File(DatabaseDescriptor.getCDCLogLocation()); + File cdcDir = DatabaseDescriptor.getCDCLogLocation(); Preconditions.checkState(cdcDir.isDirectory(), "The CDC directory does not exist."); File[] files = cdcDir.tryList(f -> CommitLogDescriptor.isValid(f.name())); if (files == null || files.length == 0) @@ -133,16 +133,18 @@ public long deleteOldLinkedCDCCommitLogSegment(long bytesToFree) private long deleteCDCFiles(File cdcLink, File cdcIndexFile) { long total = 0; + // Use deleteIfExists to tolerate a race where a concurrent actor (e.g. the test teardown + // or a CDC consumer) deletes the file between our exists() check and delete() call. if (cdcLink != null && cdcLink.exists()) { total += cdcLink.length(); - cdcLink.delete(); + cdcLink.deleteIfExists(); } if (cdcIndexFile != null && cdcIndexFile.exists()) { total += cdcIndexFile.length(); - cdcIndexFile.delete(); + cdcIndexFile.deleteIfExists(); } return total; } @@ -254,9 +256,9 @@ public CommitLogSegment createSegment() * @param file segment file that is no longer in use. */ @Override - void handleReplayedSegment(final File file) + void handleReplayedSegment(final File file, boolean hasInvalidMutations, boolean hasFailedMutations) { - super.handleReplayedSegment(file); + super.handleReplayedSegment(file, hasInvalidMutations, hasFailedMutations); // delete untracked cdc segment hard link files if their index files do not exist File cdcFile = new File(DatabaseDescriptor.getCDCLogLocation(), file.name()); diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java index 6ca662a3db9b..ab1aad5e3c87 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java @@ -19,11 +19,12 @@ package org.apache.cassandra.db.commitlog; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; public class CommitLogSegmentManagerStandard extends AbstractCommitLogSegmentManager { - public CommitLogSegmentManagerStandard(final CommitLog commitLog, String storageDirectory) + public CommitLogSegmentManagerStandard(final CommitLog commitLog, File storageDirectory) { super(commitLog, storageDirectory); } diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java index f58f5bab5f9b..a3d84df36163 100644 --- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java +++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java @@ -107,11 +107,13 @@ protected SyncSegment computeNext() } catch (CommitLogSegmentReader.SegmentReadException e) { + logger.debug("Error reading commit log", e); handleUnrecoverableError(e, !e.invalidCrc && tolerateTruncation); end = -1; // skip the remaining part of the corrupted log segment } catch (IOException e) { + logger.debug("Error reading commit log", e); boolean tolerateErrorsInSection = tolerateTruncation & segmenter.tolerateSegmentErrors(end, reader.length()); handleUnrecoverableError(e, tolerateErrorsInSection); end = -1; // skip the remaining part of the corrupted log segment @@ -134,15 +136,24 @@ protected SyncSegment computeNext() } catch (CommitLogSegmentReader.SegmentReadException e) { + logger.debug("Error reading commit log", e); handleUnrecoverableError(e, !e.invalidCrc && tolerateTruncation); // if no exception is thrown, the while loop will continue } catch (IOException e) { + logger.debug("Error reading commit log", e); boolean tolerateErrorsInSection = tolerateTruncation & segmenter.tolerateSegmentErrors(end, reader.length()); handleUnrecoverableError(e, tolerateErrorsInSection); // if no exception is thrown, the while loop will continue } + + // if we've not been able to read the sync marker, or the file is truncated, + // then return end of data, otherwise continue the loop + if (currentStart == end) + { + return endOfData(); + } } } } @@ -190,21 +201,21 @@ private int readSyncMarker(CommitLogDescriptor descriptor, int offset, RandomAcc { logger.warn("Skipping sync marker CRC check at position {} (end={}, calculated crc={}) of commit log {}." + "Using per-mutation CRC checks to ensure correctness...", - offset, end, crc.getValue(), reader.getPath()); + offset, end, crc.getValue(), reader.getFile()); return end; } if (end != 0 || filecrc != 0) { String msg = String.format("Encountered bad header at position %d of commit log %s, with invalid CRC. " + - "The end of segment marker should be zero.", offset, reader.getPath()); + "The end of segment marker should be zero.", offset, reader.getFile()); throw new SegmentReadException(msg, true); } return -1; } else if (end < offset || end > reader.length()) { - String msg = String.format("Encountered bad header at position %d of commit log %s, with bad position but valid CRC", offset, reader.getPath()); + String msg = String.format("Encountered bad header at position %d of commit log %s, with bad position but valid CRC", offset, reader.getFile()); throw new SegmentReadException(msg, false); } return end; @@ -328,7 +339,7 @@ public SyncSegment nextSegment(final int startPosition, final int nextSectionSta uncompressedBuffer = new byte[(int) (1.2 * uncompressedLength)]; int count = compressor.uncompress(compressedBuffer, 0, compressedLength, uncompressedBuffer, 0); nextLogicalStart += SYNC_MARKER_SIZE; - FileDataInput input = new FileSegmentInputStream(ByteBuffer.wrap(uncompressedBuffer, 0, count), reader.getPath(), nextLogicalStart); + FileDataInput input = new FileSegmentInputStream(ByteBuffer.wrap(uncompressedBuffer, 0, count), reader.getFile(), nextLogicalStart); nextLogicalStart += uncompressedLength; return new SyncSegment(input, startPosition, nextSectionStartPosition, (int)nextLogicalStart, tolerateSegmentErrors(nextSectionStartPosition, reader.length())); } @@ -374,7 +385,7 @@ public EncryptedSegmenter(CommitLogDescriptor descriptor, RandomAccessReader rea } catch (IOException ioe) { - throw new FSReadError(ioe, reader.getPath()); + throw new FSReadError(ioe, reader.getFile()); } chunkProvider = () -> { @@ -388,7 +399,7 @@ public EncryptedSegmenter(CommitLogDescriptor descriptor, RandomAccessReader rea } catch (IOException e) { - throw new FSReadError(e, reader.getPath()); + throw new FSReadError(e, reader.getFile()); } }; } @@ -399,7 +410,7 @@ public SyncSegment nextSegment(int startPosition, int nextSectionStartPosition) currentSegmentEndPosition = nextSectionStartPosition - 1; nextLogicalStart += SYNC_MARKER_SIZE; - FileDataInput input = new EncryptedFileSegmentInputStream(reader.getPath(), nextLogicalStart, 0, totalPlainTextLength, chunkProvider); + FileDataInput input = new EncryptedFileSegmentInputStream(reader.getFile(), nextLogicalStart, 0, totalPlainTextLength, chunkProvider); nextLogicalStart += totalPlainTextLength; return new SyncSegment(input, startPosition, nextSectionStartPosition, (int)nextLogicalStart, tolerateSegmentErrors(nextSectionStartPosition, reader.length())); } diff --git a/src/java/org/apache/cassandra/db/commitlog/DirectIOSegment.java b/src/java/org/apache/cassandra/db/commitlog/DirectIOSegment.java index fec799ce9d5a..b185c3eaf6fc 100644 --- a/src/java/org/apache/cassandra/db/commitlog/DirectIOSegment.java +++ b/src/java/org/apache/cassandra/db/commitlog/DirectIOSegment.java @@ -30,7 +30,6 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.compress.BufferType; -import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.SimpleCachedBufferPool; import org.apache.cassandra.utils.ByteBufferUtil; @@ -163,7 +162,7 @@ protected static class DirectIOSegmentBuilder extends CommitLogSegment.Builder public DirectIOSegmentBuilder(AbstractCommitLogSegmentManager segmentManager) { - this(segmentManager, FileUtils.getBlockSize(new File(segmentManager.storageDirectory))); + this(segmentManager, FileUtils.getBlockSize(segmentManager.storageDirectory)); } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java b/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java index 9da3d5041d43..171c138dce35 100644 --- a/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java +++ b/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import org.apache.cassandra.io.util.DataPosition; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileSegmentInputStream; @@ -42,7 +43,7 @@ public class EncryptedFileSegmentInputStream extends FileSegmentInputStream impl */ private int totalChunkOffset; - public EncryptedFileSegmentInputStream(String filePath, long segmentOffset, int position, int expectedLength, ChunkProvider chunkProvider) + public EncryptedFileSegmentInputStream(File filePath, long segmentOffset, int position, int expectedLength, ChunkProvider chunkProvider) { super(chunkProvider.nextChunk(), filePath, position); this.segmentOffset = segmentOffset; @@ -89,8 +90,8 @@ public void seek(long position) if (buffer == null || bufferPos < 0 || bufferPos > buffer.capacity()) throw new IllegalArgumentException( String.format("Unable to seek to position %d in %s (%d bytes) in partial mode", - position, - getPath(), + position, + getFile(), segmentOffset + expectedLength)); buffer.position((int) bufferPos); } diff --git a/src/java/org/apache/cassandra/db/commitlog/GroupCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/GroupCommitLogService.java index ad4448a3ded5..7364ff6fb171 100644 --- a/src/java/org/apache/cassandra/db/commitlog/GroupCommitLogService.java +++ b/src/java/org/apache/cassandra/db/commitlog/GroupCommitLogService.java @@ -19,6 +19,7 @@ package org.apache.cassandra.db.commitlog; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.utils.MonotonicClock; /** * A commitlog service that will block returning an ACK back to the a coordinator/client @@ -26,9 +27,9 @@ */ public class GroupCommitLogService extends AbstractCommitLogService { - public GroupCommitLogService(CommitLog commitLog) + public GroupCommitLogService(CommitLog commitLog, MonotonicClock clock) { - super(commitLog, "GROUP-COMMIT-LOG-WRITER", (int) DatabaseDescriptor.getCommitLogSyncGroupWindow()); + super(commitLog, "GROUP-COMMIT-LOG-WRITER", (int) DatabaseDescriptor.getCommitLogSyncGroupWindow(), clock); } protected void maybeWaitForSync(CommitLogSegment.Allocation alloc) diff --git a/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java b/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java index fb671130a176..1bdd7c13f8c7 100644 --- a/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java +++ b/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java @@ -25,13 +25,19 @@ import java.nio.file.StandardOpenOption; import net.openhft.chronicle.core.util.ThrowingFunction; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.SimpleCachedBufferPool; import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.SyncUtil; +import static org.apache.cassandra.config.CassandraRelevantProperties.COMMITLOG_SKIP_FILE_ADVICE; + /* * Memory-mapped segment. Maps the destination channel into an appropriately-sized memory-mapped buffer in which the * mutation threads write. On sync forces the buffer to disk. @@ -39,7 +45,11 @@ */ public class MemoryMappedSegment extends CommitLogSegment { - private final int fd; + @VisibleForTesting + final int fd; + + @VisibleForTesting + static boolean skipFileAdviseToFreePageCache = COMMITLOG_SKIP_FILE_ADVICE.getBoolean(); /** * Constructs a new segment file. @@ -51,7 +61,7 @@ public class MemoryMappedSegment extends CommitLogSegment int firstSync = buffer.position(); buffer.putInt(firstSync + 0, 0); buffer.putInt(firstSync + 4, 0); - fd = NativeLibrary.getfd(channel); + fd = NativeLibrary.instance.getfd(channel); } @Override @@ -96,7 +106,16 @@ protected void flush(int startMarker, int nextMarker) { throw new FSWriteError(e, getPath()); } - NativeLibrary.trySkipCache(fd, startMarker, nextMarker, logFile.absolutePath()); + + if (!skipFileAdviseToFreePageCache) + { + adviceOnFileToFreePageCache(fd, startMarker, nextMarker, logFile); + } + } + + void adviceOnFileToFreePageCache(int fd, int startMarker, int nextMarker, File logFile) + { + INativeLibrary.instance.trySkipCache(fd, startMarker, nextMarker, logFile.absolutePath()); } @Override diff --git a/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java index ae170a87d51e..5d5e14422e9b 100644 --- a/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java +++ b/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java @@ -20,23 +20,24 @@ import java.util.concurrent.TimeUnit; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.utils.MonotonicClock; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import static org.apache.cassandra.config.CassandraRelevantProperties.SYNC_LAG_FACTOR; class PeriodicCommitLogService extends AbstractCommitLogService { - private static final long blockWhenSyncLagsNanos = TimeUnit.MILLISECONDS.toNanos(DatabaseDescriptor.getPeriodicCommitLogSyncBlock()); + private static final long blockWhenSyncLagsNanos = (long) (TimeUnit.MILLISECONDS.toNanos(DatabaseDescriptor.getCommitLogSyncPeriod()) * SYNC_LAG_FACTOR.getDouble()); - public PeriodicCommitLogService(final CommitLog commitLog) + public PeriodicCommitLogService(final CommitLog commitLog, MonotonicClock clock) { - super(commitLog, "PERIODIC-COMMIT-LOG-SYNCER", DatabaseDescriptor.getCommitLogSyncPeriod(), + super(commitLog, "PERIODIC-COMMIT-LOG-SYNCER", DatabaseDescriptor.getCommitLogSyncPeriod(), clock, !(commitLog.configuration.useCompression() || commitLog.configuration.useEncryption())); } protected void maybeWaitForSync(CommitLogSegment.Allocation alloc) { - long expectedSyncTime = nanoTime() - blockWhenSyncLagsNanos; - if (lastSyncedAt < expectedSyncTime) + long expectedSyncTime = clock.now() - blockWhenSyncLagsNanos; + if (lastSyncedAt - expectedSyncTime < 0) { pending.incrementAndGet(); awaitSyncAt(expectedSyncTime, commitLog.metrics.waitingOnCommit.time()); diff --git a/src/java/org/apache/cassandra/db/commitlog/UncompressedSegment.java b/src/java/org/apache/cassandra/db/commitlog/UncompressedSegment.java new file mode 100644 index 000000000000..7f820df1fae7 --- /dev/null +++ b/src/java/org/apache/cassandra/db/commitlog/UncompressedSegment.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.commitlog; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import net.openhft.chronicle.core.util.ThrowingFunction; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.compress.BufferType; +import org.apache.cassandra.io.util.SimpleCachedBufferPool; +import org.apache.cassandra.utils.SyncUtil; + +/** + * Uncompressed commit log segment. Provides an in-memory buffer for the mutation threads. On sync writes anything + * unwritten to disk and waits for the writes to materialize. + * + * The format of the uncompressed commit log is as follows: + * - standard commit log header (as written by {@link CommitLogDescriptor#writeHeader(ByteBuffer, CommitLogDescriptor)}) + * - a series of 'sync segments' that are written every time the commit log is sync()'ed + * -- a sync section header, see {@link CommitLogSegment#writeSyncMarker(long, ByteBuffer, int, int, int)} + * -- a block of uncompressed data + */ +public class UncompressedSegment extends FileDirectSegment +{ + /** + * Constructs a new segment file. + */ + UncompressedSegment(AbstractCommitLogSegmentManager manager, ThrowingFunction channelFactory) + { + super(manager, channelFactory); + } + + @Override + synchronized void write(int startMarker, int nextMarker) + { + int contentStart = startMarker + SYNC_MARKER_SIZE; + int length = nextMarker - contentStart; + // The length may be 0 when the segment is being closed. + assert length > 0 || length == 0 && !isStillAllocating(); + + try + { + writeSyncMarker(id, buffer, startMarker, startMarker, nextMarker); + + ByteBuffer inputBuffer = buffer.duplicate(); + inputBuffer.limit(nextMarker).position(startMarker); + + // Only one thread can be here at a given time. + // Protected by synchronization on CommitLogSegment.sync(). + manager.addSize(inputBuffer.remaining()); + channel.write(inputBuffer); + lastWrittenPos = nextMarker; + assert channel.position() == nextMarker; + SyncUtil.force(channel, true); + } + catch (Exception e) + { + throw new FSWriteError(e, getPath()); + } + } + + @Override + public long onDiskSize() + { + return lastWrittenPos; + } + + protected static class UncompressedSegmentBuilder extends CommitLogSegment.Builder + { + public UncompressedSegmentBuilder(AbstractCommitLogSegmentManager segmentManager) + { + super(segmentManager); + } + + @Override + public UncompressedSegment build() + { + return new UncompressedSegment(segmentManager, + path -> FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)); + } + + @Override + public SimpleCachedBufferPool createBufferPool() + { + return new SimpleCachedBufferPool(DatabaseDescriptor.getCommitLogMaxCompressionBuffersInPool(), + DatabaseDescriptor.getCommitLogSegmentSize(), + BufferType.OFF_HEAP); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java index fd210d6d028f..ca96fae8f2ab 100644 --- a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java @@ -20,18 +20,18 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; @@ -43,53 +43,33 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.index.Index; import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.SSTableMultiWriter; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.SimpleSSTableMultiWriter; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.sstable.metadata.MetadataCollector; -import org.apache.cassandra.io.sstable.metadata.StatsMetadata; -import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.Overlaps; -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; - -/** - * Pluggable compaction strategy determines how SSTables get merged. - * - * There are two main goals: - * - perform background compaction constantly as needed; this typically makes a tradeoff between - * i/o done by compaction, and merging done at read time. - * - perform a full (maximum possible) compaction if requested by the user - */ -public abstract class AbstractCompactionStrategy +abstract class AbstractCompactionStrategy implements CompactionStrategy { - private static final Logger logger = LoggerFactory.getLogger(AbstractCompactionStrategy.class); + public static final Class CONTAINER_CLASS = CompactionStrategyManager.class; - protected static final float DEFAULT_TOMBSTONE_THRESHOLD = 0.2f; - // minimum interval needed to perform tombstone removal compaction in seconds, default 86400 or 1 day. - protected static final long DEFAULT_TOMBSTONE_COMPACTION_INTERVAL = 86400; - protected static final boolean DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION = false; - protected static final boolean DEFAULT_LOG_ALL_OPTION = false; + protected static final Logger logger = LoggerFactory.getLogger(AbstractCompactionStrategy.class); - protected static final String TOMBSTONE_THRESHOLD_OPTION = "tombstone_threshold"; - protected static final String TOMBSTONE_COMPACTION_INTERVAL_OPTION = "tombstone_compaction_interval"; - // disable range overlap check when deciding if an SSTable is candidate for tombstone compaction (CASSANDRA-6563) - protected static final String UNCHECKED_TOMBSTONE_COMPACTION_OPTION = "unchecked_tombstone_compaction"; - protected static final String LOG_ALL_OPTION = "log_all"; - protected static final String COMPACTION_ENABLED = "enabled"; - public static final String ONLY_PURGE_REPAIRED_TOMBSTONES = "only_purge_repaired_tombstones"; + private static int logCount = 0; - protected Map options; - - protected final ColumnFamilyStore cfs; - protected float tombstoneThreshold; - protected long tombstoneCompactionInterval; - protected boolean uncheckedTombstoneCompaction; - protected boolean disableTombstoneCompactions = false; - protected boolean logAll = true; + protected final CompactionStrategyOptions options; + /** The column family store should only be used when creating writers. However it is currently also used + * by legacy strategies and compaction tasks. + */ + protected final CompactionRealm realm; - private final Directories directories; + protected final CompactionLogger compactionLogger; + protected final Directories directories; + /** + * This class groups all the compaction tasks that are pending, submitted, in progress and completed. + */ + protected final BackgroundCompactions backgroundCompactions; /** * pause/resume/getNextBackgroundTask must synchronize. This guarantees that after pause completes, @@ -101,48 +81,58 @@ public abstract class AbstractCompactionStrategy * * See CASSANDRA-3430 */ - protected boolean isActive = false; + protected volatile boolean isActive = false; - protected AbstractCompactionStrategy(ColumnFamilyStore cfs, Map options) + protected AbstractCompactionStrategy(CompactionStrategyFactory factory, BackgroundCompactions backgroundCompactions, Map options) { - assert cfs != null; - this.cfs = cfs; - this.options = ImmutableMap.copyOf(options); + Preconditions.checkNotNull(factory); + Preconditions.checkNotNull(backgroundCompactions); + + this.realm = Objects.requireNonNull(factory.getRealm()); + this.compactionLogger = Objects.requireNonNull(factory.getCompactionLogger()); + this.options = new CompactionStrategyOptions(getClass(), options, false); + this.directories = Objects.requireNonNull(realm.getDirectories()); + this.backgroundCompactions = backgroundCompactions; + } - /* checks must be repeated here, as user supplied strategies might not call validateOptions directly */ + public CompactionStrategyOptions getOptions() + { + return options; + } - try - { - validateOptions(options); - String optionValue = options.get(TOMBSTONE_THRESHOLD_OPTION); - tombstoneThreshold = optionValue == null ? DEFAULT_TOMBSTONE_THRESHOLD : Float.parseFloat(optionValue); - optionValue = options.get(TOMBSTONE_COMPACTION_INTERVAL_OPTION); - tombstoneCompactionInterval = optionValue == null ? DEFAULT_TOMBSTONE_COMPACTION_INTERVAL : Long.parseLong(optionValue); - optionValue = options.get(UNCHECKED_TOMBSTONE_COMPACTION_OPTION); - uncheckedTombstoneCompaction = optionValue == null ? DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION : Boolean.parseBoolean(optionValue); - optionValue = options.get(LOG_ALL_OPTION); - logAll = optionValue == null ? DEFAULT_LOG_ALL_OPTION : Boolean.parseBoolean(optionValue); - } - catch (ConfigurationException e) - { - logger.warn("Error setting compaction strategy options ({}), defaults will be used", e.getMessage()); - tombstoneThreshold = DEFAULT_TOMBSTONE_THRESHOLD; - tombstoneCompactionInterval = DEFAULT_TOMBSTONE_COMPACTION_INTERVAL; - uncheckedTombstoneCompaction = DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION; - } + @Override + public CompactionLogger getCompactionLogger() + { + return compactionLogger; + } + + public CompactionRealm getRealm() { return realm; } - directories = cfs.getDirectories(); + // + // Compaction Observer + // + + @Override + public void onInProgress(CompactionProgress progress) + { + backgroundCompactions.onInProgress(progress); } - public Directories getDirectories() + @Override + public void onCompleted(TimeUUID id, Throwable err) { - return directories; + backgroundCompactions.onCompleted(this, id); } + // + // CompactionStrategy + // + /** * For internal, temporary suspension of background compactions so that we can do exceptional * things like truncate or major compaction */ + @Override public synchronized void pause() { isActive = false; @@ -152,6 +142,7 @@ public synchronized void pause() * For internal, temporary suspension of background compactions so that we can do exceptional * things like truncate or major compaction */ + @Override public synchronized void resume() { isActive = true; @@ -160,6 +151,7 @@ public synchronized void resume() /** * Performs any extra initialization required */ + @Override public void startup() { isActive = true; @@ -168,29 +160,43 @@ public void startup() /** * Releases any resources if this strategy is shutdown (when the CFS is reloaded after a schema change). */ + @Override public void shutdown() { isActive = false; } /** - * @param gcBefore throw away tombstones older than this - * - * @return the next background/minor compaction task to run; null if nothing to do. - * - * Is responsible for marking its sstables as compaction-pending. - */ - public abstract AbstractCompactionTask getNextBackgroundTask(final long gcBefore); - - /** - * @param gcBefore throw away tombstones older than this - * + * @param gcBefore throw away tombstones older than this + * @param permittedParallelism the maximum permitted parallelism for the operation * @return a compaction task that should be run to compact this columnfamilystore * as much as possible. Null if nothing to do. - * + *

* Is responsible for marking its sstables as compaction-pending. */ - public abstract Collection getMaximalTask(final long gcBefore, boolean splitOutput); + @Override + @SuppressWarnings("resource") + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) + { + Iterable filteredSSTables = Iterables.filter(getSSTables(), sstable -> !sstable.isMarkedSuspect()); + if (Iterables.isEmpty(filteredSSTables)) + return CompactionTasks.empty(); + LifecycleTransaction txn = realm.tryModify(filteredSSTables, OperationType.COMPACTION); + if (txn == null) + return CompactionTasks.empty(); + return CompactionTasks.create(Collections.singleton(createCompactionTask(gcBefore, txn, true, splitOutput))); + } + + @Override + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism, OperationType operationType) + { + CompactionTasks maximalTasks = getMaximalTasks(gcBefore, splitOutput, permittedParallelism); + for (AbstractCompactionTask task: maximalTasks) + { + task.setCompactionType(operationType); + } + return maximalTasks; + } /** * @param sstables SSTables to compact. Must be marked as compacting. @@ -201,320 +207,165 @@ public void shutdown() * * Is responsible for marking its sstables as compaction-pending. */ - public abstract AbstractCompactionTask getUserDefinedTask(Collection sstables, final long gcBefore); - - public AbstractCompactionTask getCompactionTask(LifecycleTransaction txn, final long gcBefore, long maxSSTableBytes) + @Override + @SuppressWarnings("resource") + public synchronized CompactionTasks getUserDefinedTasks(Collection sstables, long gcBefore) { - return new CompactionTask(cfs, txn, gcBefore); + assert !sstables.isEmpty(); // checked for by CM.submitUserDefined + + LifecycleTransaction modifier = realm.tryModify(sstables, OperationType.COMPACTION); + if (modifier == null) + { + logger.trace("Unable to mark {} for compaction; probably a background compaction got to it first. You can disable background compactions temporarily if this is a problem", sstables); + return CompactionTasks.empty(); + } + + return CompactionTasks.create(ImmutableList.of(createCompactionTask(gcBefore, modifier, false, false).setUserDefined(true))); } /** - * @return the number of background tasks estimated to still be needed for this columnfamilystore + * Create a compaction task for a maximal, user defined or background compaction without aggregates (legacy strategies). + * Background compactions for strategies that extend {@link LegacyAbstractCompactionStrategy.WithAggregates} will use + * {@link LegacyAbstractCompactionStrategy.WithAggregates#createCompactionTask(long, LifecycleTransaction, boolean, boolean)} instead. + * + * @param gcBefore tombstone threshold, older tombstones can be discarded + * @param txn the transaction containing the files to be compacted + * @param isMaximal set to true only when it's a maximal compaction + * @param splitOutput false except for maximal compactions and passed in by the user to indicate to SizeTieredCompactionStrategy to split the out, + * ignored otherwise + * + * @return a compaction task, see {@link AbstractCompactionTask} and sub-classes */ - public abstract int getEstimatedRemainingTasks(); + protected AbstractCompactionTask createCompactionTask(final long gcBefore, LifecycleTransaction txn, boolean isMaximal, boolean splitOutput) + { + return new CompactionTask(realm, txn, gcBefore, false, this); + } /** - * @return the estimated number of background tasks needed, assuming an additional number of SSTables + * Create a compaction task for operations that are not driven by the strategies. + * + * @param txn the transaction containing the files to be compacted + * @param gcBefore tombstone threshold, older tombstones can be discarded + * @param maxSSTableBytes the maximum size in bytes for an output sstables + * + * @return a compaction task, see {@link AbstractCompactionTask} and sub-classes */ - int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes) + @Override + public AbstractCompactionTask createCompactionTask(LifecycleTransaction txn, final long gcBefore, long maxSSTableBytes) { - return getEstimatedRemainingTasks() + (int)Math.ceil((double)additionalSSTables / cfs.getMaximumCompactionThreshold()); + return new CompactionTask(realm, txn, gcBefore, false, this); } /** - * @return size in bytes of the largest sstables for this strategy + * @return a list of the compaction aggregates, e.g. the levels or buckets. Note that legacy strategies that derive from + * {@link LeveledCompactionStrategy.WithSSTableList} will return an empty list. */ - public abstract long getMaxSSTableBytes(); + public Collection getAggregates() + { + return backgroundCompactions.getAggregates(); + } /** - * Filters SSTables that are to be excluded from the given collection - * - * @param originalCandidates The collection to check for excluded SSTables - * @return list of the SSTables with excluded ones filtered out + * @return the estimated number of background tasks needed, assuming an additional number of SSTables */ - public static List filterSuspectSSTables(Iterable originalCandidates) + int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes) { - List filtered = new ArrayList<>(); - for (SSTableReader sstable : originalCandidates) - { - if (!sstable.isMarkedSuspect()) - filtered.add(sstable); - } - return filtered; + return getEstimatedRemainingTasks() + (int)Math.ceil((double)additionalSSTables / realm.getMaximumCompactionThreshold()); } - - public ScannerList getScanners(Collection sstables, Range range) + @Override + public int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes, boolean isIncremental) throws IllegalArgumentException { - return range == null ? getScanners(sstables, (Collection>)null) : getScanners(sstables, Collections.singleton(range)); + return getEstimatedRemainingTasks(additionalSSTables, additionalBytes); } + /** - * Returns a list of KeyScanners given sstables and a range on which to scan. - * The default implementation simply grab one SSTableScanner per-sstable, but overriding this method - * allow for a more memory efficient solution if we know the sstable don't overlap (see - * LeveledCompactionStrategy for instance). + * @return the total number of background compactions, pending or in progress */ - public ScannerList getScanners(Collection sstables, Collection> ranges) + @Override + public int getTotalCompactions() { - ArrayList scanners = new ArrayList<>(); - try - { - for (SSTableReader sstable : sstables) - scanners.add(sstable.getScanner(ranges)); - } - catch (Throwable t) - { - ISSTableScanner.closeAllAndPropagate(scanners, t); - } - return new ScannerList(scanners); + return getEstimatedRemainingTasks() + backgroundCompactions.getCompactionsInProgress().size(); } - public String getName() + /** + * Return the statistics. Only strategies that implement {@link LegacyAbstractCompactionStrategy.WithAggregates} will provide non-empty statistics, + * the legacy strategies will always have empty statistics. + *

+ * @return statistics about this compaction picks. + */ + @Override + public List getStatistics() { - return getClass().getSimpleName(); + return ImmutableList.of(backgroundCompactions.getStatistics(this)); } - /** - * Replaces sstables in the compaction strategy - * - * Note that implementations must be able to handle duplicate notifications here (that removed are already gone and - * added have already been added) - * */ - public synchronized void replaceSSTables(Collection removed, Collection added) + public static Iterable nonSuspectAndNotIn(Iterable sstables, Set compacting) { - for (SSTableReader remove : removed) - removeSSTable(remove); - addSSTables(added); + return Iterables.filter(sstables, x -> !x.isMarkedSuspect() && !compacting.contains(x)); } - /** - * Adds sstable, note that implementations must handle duplicate notifications here (added already being in the compaction strategy) - */ - public abstract void addSSTable(SSTableReader added); + @Override + public int[] getSSTableCountPerLevel() + { + return new int[0]; + } - /** - * Adds sstables, note that implementations must handle duplicate notifications here (added already being in the compaction strategy) - */ - public synchronized void addSSTables(Iterable added) + @Override + public long[] getPerLevelSizeBytes() { - for (SSTableReader sstable : added) - addSSTable(sstable); + return new long[0]; } - /** - * Removes sstable from the strategy, implementations must be able to handle the sstable having already been removed. - */ - public abstract void removeSSTable(SSTableReader sstable); + @Override + public boolean isLeveledCompaction() + { + return false; + } - /** - * Removes sstables from the strategy, implementations must be able to handle the sstables having already been removed. - */ - public void removeSSTables(Iterable removed) + @Override + public int[] getSSTableCountPerTWCSBucket() { - for (SSTableReader sstable : removed) - removeSSTable(sstable); + return new int[0]; } - /** - * Returns the sstables managed by this strategy instance - */ - @VisibleForTesting - protected abstract Set getSSTables(); + @Override + public int getLevelFanoutSize() + { + return LeveledCompactionStrategy.DEFAULT_LEVEL_FANOUT_SIZE; // this makes no sense but it's the existing behaviour + } /** - * Called when the metadata has changed for an sstable - for example if the level changed - * - * Not called when repair status changes (which is also metadata), because this results in the - * sstable getting removed from the compaction strategy instance. + * Returns a list of KeyScanners given sstables and a range on which to scan. + * The default implementation simply grab one SSTableScanner per-sstable, but overriding this method + * allow for a more memory efficient solution if we know the sstable don't overlap (see + * LeveledCompactionStrategy for instance). */ - public void metadataChanged(StatsMetadata oldMetadata, SSTableReader sstable) + @Override + public ScannerList getScanners(Collection sstables, Collection> ranges) { + return ScannerList.of(sstables, ranges); } - public static class ScannerList implements AutoCloseable + @Override + public String getName() { - public final List scanners; - public ScannerList(List scanners) - { - this.scanners = scanners; - } - - public long getTotalBytesScanned() - { - long bytesScanned = 0L; - for (int i=0, isize=scanners.size(); i toCompact) + protected BackgroundCompactions getBackgroundCompactions() { - return getScanners(toCompact, (Collection>)null); + return backgroundCompactions; } - /** - * Check if given sstable is worth dropping tombstones at gcBefore. - * Check is skipped if tombstone_compaction_interval time does not elapse since sstable creation and returns false. - * - * @param sstable SSTable to check - * @param gcBefore time to drop tombstones - * @return true if given sstable's tombstones are expected to be removed - */ - protected boolean worthDroppingTombstones(SSTableReader sstable, long gcBefore) - { - if (disableTombstoneCompactions || CompactionController.NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || cfs.getNeverPurgeTombstones()) - return false; - // since we use estimations to calculate, there is a chance that compaction will not drop tombstones actually. - // if that happens we will end up in infinite compaction loop, so first we check enough if enough time has - // elapsed since SSTable created. - if (currentTimeMillis() < sstable.getDataCreationTime() + tombstoneCompactionInterval * 1000) - return false; - - double droppableRatio = sstable.getEstimatedDroppableTombstoneRatio(gcBefore); - if (droppableRatio <= tombstoneThreshold) - return false; - - //sstable range overlap check is disabled. See CASSANDRA-6563. - if (uncheckedTombstoneCompaction) - return true; - - Collection overlaps = cfs.getOverlappingLiveSSTables(Collections.singleton(sstable)); - if (overlaps.isEmpty()) - { - // there is no overlap, tombstones are safely droppable - return true; - } - else if (CompactionController.getFullyExpiredSSTables(cfs, Collections.singleton(sstable), overlaps, gcBefore).size() > 0) - { - return true; - } - else - { - // what percentage of columns do we expect to compact outside of overlap? - if (!sstable.isEstimationInformative()) - { - // we have too few samples to estimate correct percentage - return false; - } - // first, calculate estimated keys that do not overlap - long keys = sstable.estimatedKeys(); - Set> ranges = new HashSet<>(overlaps.size()); - for (SSTableReader overlap : overlaps) - ranges.add(new Range<>(overlap.getFirst().getToken(), overlap.getLast().getToken())); - long remainingKeys = keys - sstable.estimatedKeysForRanges(ranges); - // next, calculate what percentage of columns we have within those keys - long columns = sstable.getEstimatedCellPerPartitionCount().mean() * remainingKeys; - double remainingColumnsRatio = ((double) columns) / (sstable.getEstimatedCellPerPartitionCount().count() * sstable.getEstimatedCellPerPartitionCount().mean()); - - // return if we still expect to have droppable tombstones in rest of columns - return remainingColumnsRatio * droppableRatio > tombstoneThreshold; - } + public long getSkippedAggregatesDueToDiskSpace() + { + return backgroundCompactions.getSkippedAggregatesDueToDiskSpace(); } public static Map validateOptions(Map options) throws ConfigurationException { - String threshold = options.get(TOMBSTONE_THRESHOLD_OPTION); - if (threshold != null) - { - try - { - float thresholdValue = Float.parseFloat(threshold); - if (thresholdValue < 0) - { - throw new ConfigurationException(String.format("%s must be greater than 0, but was %f", TOMBSTONE_THRESHOLD_OPTION, thresholdValue)); - } - } - catch (NumberFormatException e) - { - throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", threshold, TOMBSTONE_THRESHOLD_OPTION), e); - } - } - - String interval = options.get(TOMBSTONE_COMPACTION_INTERVAL_OPTION); - if (interval != null) - { - try - { - long tombstoneCompactionInterval = Long.parseLong(interval); - if (tombstoneCompactionInterval < 0) - { - throw new ConfigurationException(String.format("%s must be greater than 0, but was %d", TOMBSTONE_COMPACTION_INTERVAL_OPTION, tombstoneCompactionInterval)); - } - } - catch (NumberFormatException e) - { - throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", interval, TOMBSTONE_COMPACTION_INTERVAL_OPTION), e); - } - } - - String unchecked = options.get(UNCHECKED_TOMBSTONE_COMPACTION_OPTION); - if (unchecked != null) - { - if (!unchecked.equalsIgnoreCase("true") && !unchecked.equalsIgnoreCase("false")) - throw new ConfigurationException(String.format("'%s' should be either 'true' or 'false', not '%s'", UNCHECKED_TOMBSTONE_COMPACTION_OPTION, unchecked)); - } - - String logAll = options.get(LOG_ALL_OPTION); - if (logAll != null) - { - if (!logAll.equalsIgnoreCase("true") && !logAll.equalsIgnoreCase("false")) - { - throw new ConfigurationException(String.format("'%s' should either be 'true' or 'false', not %s", LOG_ALL_OPTION, logAll)); - } - } - - String compactionEnabled = options.get(COMPACTION_ENABLED); - if (compactionEnabled != null) - { - if (!compactionEnabled.equalsIgnoreCase("true") && !compactionEnabled.equalsIgnoreCase("false")) - { - throw new ConfigurationException(String.format("enabled should either be 'true' or 'false', not %s", compactionEnabled)); - } - } - - Map uncheckedOptions = new HashMap(options); - uncheckedOptions.remove(TOMBSTONE_THRESHOLD_OPTION); - uncheckedOptions.remove(TOMBSTONE_COMPACTION_INTERVAL_OPTION); - uncheckedOptions.remove(UNCHECKED_TOMBSTONE_COMPACTION_OPTION); - uncheckedOptions.remove(LOG_ALL_OPTION); - uncheckedOptions.remove(COMPACTION_ENABLED); - uncheckedOptions.remove(ONLY_PURGE_REPAIRED_TOMBSTONES); - uncheckedOptions.remove(CompactionParams.Option.PROVIDE_OVERLAPPING_TOMBSTONES.toString()); - return uncheckedOptions; + return CompactionStrategyOptions.validateOptions(options); } /** @@ -522,17 +373,20 @@ public static Map validateOptions(Map options) t * anti-compaction to determine which SSTables should be anitcompacted * as a group. If a given compaction strategy creates sstables which * cannot be merged due to some constraint it must override this method. + * @param sstablesToGroup + * @return */ - public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) + @Override + public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) { int groupSize = 2; - List sortedSSTablesToGroup = new ArrayList<>(sstablesToGroup); - Collections.sort(sortedSSTablesToGroup, SSTableReader.firstKeyComparator); + List sortedSSTablesToGroup = new ArrayList<>(sstablesToGroup); + Collections.sort(sortedSSTablesToGroup, CompactionSSTable.firstKeyComparator); - Collection> groupedSSTables = new ArrayList<>(); - Collection currGroup = new ArrayList<>(groupSize); + Collection> groupedSSTables = new ArrayList<>(); + Collection currGroup = new ArrayList<>(groupSize); - for (SSTableReader sstable : sortedSSTablesToGroup) + for (CompactionSSTable sstable : sortedSSTablesToGroup) { currGroup.add(sstable); if (currGroup.size() == groupSize) @@ -547,11 +401,6 @@ public Collection> groupSSTablesForAntiCompaction(Coll return groupedSSTables; } - public CompactionLogger.Strategy strategyLogger() - { - return CompactionLogger.Strategy.none; - } - public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, @@ -568,16 +417,43 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, repairedAt, pendingRepair, isTransient, - cfs.metadata, + realm.metadataRef(), commitLogPositions, sstableLevel, header, indexGroups, - lifecycleNewTracker, cfs); + lifecycleNewTracker, + realm); } + @Override public boolean supportsEarlyOpen() { return true; } + + public void periodicReport() + { + logCount++; + CompactionLogger logger = this.getCompactionLogger(); + CompactionStrategyOptions options = this.getOptions(); + BackgroundCompactions backgroundCompactions = this.getBackgroundCompactions(); + int interval = options.getLogPeriodMinutes(); + boolean logAll = options.isLogAll(); + if (logger != null && logger.enabled() && logAll && logCount % interval == 0) + { + logCount = 0; + logger.statistics(this, "periodic", backgroundCompactions.getStatistics(this)); + } + } + + @Override + public Map getMaxOverlapsMap() + { + final Set liveSSTables = getSSTables(); + return ImmutableMap.of("all", Integer.toString(Overlaps.maxOverlap(liveSSTables, + CompactionSSTable.startsAfter, + CompactionSSTable.firstKeyComparator, + CompactionSSTable.lastKeyComparator))); + } } diff --git a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionTask.java index 40c4cb49e123..dec0cd1a4fe7 100644 --- a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionTask.java +++ b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionTask.java @@ -17,43 +17,94 @@ */ package org.apache.cassandra.db.compaction; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +import javax.annotation.Nullable; import com.google.common.base.Preconditions; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Directories; -import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.io.FSDiskFullWriteError; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.WrappedRunnable; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; + +import static com.google.common.base.Throwables.propagate; + public abstract class AbstractCompactionTask extends WrappedRunnable { - protected final ColumnFamilyStore cfs; - protected LifecycleTransaction transaction; + protected static final Logger logger = LoggerFactory.getLogger(AbstractCompactionTask.class); + + // See CNDB-10549 + static final boolean SKIP_REPAIR_STATE_CHECKING = + CassandraRelevantProperties.COMPACTION_SKIP_REPAIR_STATE_CHECKING.getBoolean(); + static final boolean SKIP_COMPACTING_STATE_CHECKING = + CassandraRelevantProperties.COMPACTION_SKIP_COMPACTING_STATE_CHECKING.getBoolean(); + + protected final CompactionRealm realm; + protected ILifecycleTransaction transaction; protected boolean isUserDefined; protected OperationType compactionType; + protected TableOperationObserver opObserver; + protected final List compObservers; + + private enum ExecutionState + { + CREATED, // Task is created and ready for execution, possibly waiting in a queue. + // If it is rejected, the rejecting thread must clean it up. + STARTED, // Task has started execution, but still hasn't entered the active operations. + // It still accepts cancellation requests that may or may not be honored by the executing thread. + ACTIVE, // Task has started execution and is listed in the active operations. + COMPLETE, // Task is complete, cleanup done or to be done by executing thread. + ABORT, // Task has been cancelled after entering STARTED state, before becoming ACTIVE + REJECTED, // Task has been rejected, either because of an error or cancelled before becoming active. + // Cleanup done or to be done by rejecting thread. + } + private final AtomicReference executionState = new AtomicReference<>(ExecutionState.CREATED); /** - * @param cfs + * @param realm * @param transaction the modifying managing the status of the sstables we're replacing */ - public AbstractCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction transaction) + protected AbstractCompactionTask(CompactionRealm realm, ILifecycleTransaction transaction) { - this.cfs = cfs; + this.realm = realm; this.transaction = transaction; this.isUserDefined = false; this.compactionType = OperationType.COMPACTION; - // enforce contract that caller should mark sstables compacting - Set compacting = transaction.tracker.getCompacting(); - for (SSTableReader sstable : transaction.originals()) - assert compacting.contains(sstable) : sstable.getFilename() + " is not correctly marked compacting"; + this.opObserver = TableOperationObserver.NOOP; + this.compObservers = new ArrayList<>(); + + try + { + if (!SKIP_COMPACTING_STATE_CHECKING && !transaction.isOffline()) + { + // enforce contract that caller should mark sstables compacting + var compacting = realm.getCompactingSSTables(); + for (SSTableReader sstable : transaction.originals()) + assert compacting.contains(sstable) : sstable.getFilename() + " is not correctly marked compacting"; + } + + validateSSTables(transaction.originals()); + } + catch (Throwable err) + { + propagate(cleanup(err)); + } - validateSSTables(transaction.originals()); + CompactionManager.instance.active.addTaskToScheduled(this); } /** @@ -61,7 +112,10 @@ public AbstractCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction transa */ private void validateSSTables(Set sstables) { - // do not allow to be compacted together + if (SKIP_REPAIR_STATE_CHECKING) + return; + + // do not allow sstables in different repair states to be compacted together if (!sstables.isEmpty()) { Iterator iter = sstables.iterator(); @@ -91,28 +145,138 @@ private void validateSSTables(Set sstables) } /** - * executes the task and unmarks sstables compacting + * Executes the task after setting a new observer, normally the observer is the + * compaction manager metrics. */ - public int execute(ActiveCompactionsTracker activeCompactions) + public void execute(TableOperationObserver observer) + { + setOpObserver(observer).execute(); + } + + /** Executes the task */ + public void execute() { + // Exit immediately if task is already rejected. Also change the state to STARTED so that a race with rejection + // cannot close our resources while we are trying to work; before this point rejecting thread is responsible for + // clean-up; after this passes, we are. + if (!executionState.compareAndSet(ExecutionState.CREATED, ExecutionState.STARTED)) + { + cancelledOnStart(); + return; + } + + Throwable t = null; try { - return executeInternal(activeCompactions); + executeInternal(); } - catch(FSDiskFullWriteError e) + catch (FSDiskFullWriteError e) { RuntimeException cause = new RuntimeException("Converted from FSDiskFullWriteError: " + e.getMessage()); cause.setStackTrace(e.getStackTrace()); + t = cause; throw new RuntimeException("Throwing new Runtime to bypass exception handler when disk is full", cause); } + catch (Throwable t1) + { + t = t1; + throw t1; + } finally { - transaction.close(); + // if executeInternal has not switched the task to active state, do it now to remove it from the + // scheduled set. + switchToActive(); + // Unless the task has been fully rejected before entering this function, clean it up. + if (executionState.getAndSet(ExecutionState.COMPLETE) != ExecutionState.REJECTED) + Throwables.maybeFail(cleanup(t)); + } + } + + public void cancelledOnStart() + { + // Called when the task starts after being cancelled. Normally nothing to do, overridden by tests. + } + + public Throwable rejected(Throwable t) + { + if (executionState.compareAndSet(ExecutionState.CREATED, ExecutionState.REJECTED)) + { + CompactionManager.instance.active.removeTaskFromScheduled(this); + logger.debug("Compaction {} rejected", transaction, t); + return cleanup(t); + } + else + { + // We have another chance to request abort if the task has not become ACTIVE yet. + // If this works, the executing thread is currently active, may honor the request and will clean up. + if (executionState.compareAndSet(ExecutionState.STARTED, ExecutionState.ABORT)) + { + CompactionManager.instance.active.removeTaskFromScheduled(this); + logger.debug("Compaction {} aborted", transaction, t); + } + // We are either already rejected, or racing with switching to active. + // In the latter case, the operation can now be cancelled through the active operations list. + return t; + } + } + + public boolean switchToActive() + { + boolean switched = executionState.compareAndSet(ExecutionState.STARTED, ExecutionState.ACTIVE); + if (switched) + CompactionManager.instance.active.removeTaskFromScheduled(this); + return switched; + } + + /** + * Reject/cancel the task if it affects any sstable that satisfies the given predicate. + */ + public boolean cancelIfAffects(CompactionRealm realm, Predicate sstablePredicate, TableOperation.StopTrigger trigger) + { + boolean affects = affectsAny(realm, sstablePredicate); + if (affects) + { + // Reject with an exception to notify observers task wasn't successful. + TimeUUID id = getTransaction().opId(); + Throwable err = rejected(new CompactionInterruptedException(id, trigger)); + if (err != null && !(err instanceof CompactionInterruptedException)) + logger.warn("Failed to reject task with id={}", id, err); + } + return affects; + } + + /** + * Returns true iff the task affects any sstable that satisfies the given predicate. + */ + public boolean affectsAny(CompactionRealm realm, Predicate sstablePredicate) + { + if (realm != this.realm) + return false; + + for (SSTableReader r : transaction.originals()) + { + if (sstablePredicate.test(r)) + return true; } + return false; + } + + protected Throwable cleanup(Throwable err) + { + final Throwable originalError = err; + for (CompactionObserver compObserver : compObservers) + err = Throwables.perform(err, () -> compObserver.onCompleted(transaction.opId(), originalError)); + + return Throwables.perform(err, () -> transaction.close()); + } + + protected void executeInternal() + { + run(); } - public abstract CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables); - protected abstract int executeInternal(ActiveCompactionsTracker activeCompactions); + // TODO Eventually these three setters should be passed in to the constructor. public AbstractCompactionTask setUserDefined(boolean isUserDefined) { @@ -120,12 +284,66 @@ public AbstractCompactionTask setUserDefined(boolean isUserDefined) return this; } + /** + * @return The type of compaction this task is performing. Used by CNDB. + */ + public OperationType getCompactionType() + { + return compactionType; + } + public AbstractCompactionTask setCompactionType(OperationType compactionType) { this.compactionType = compactionType; return this; } + /** + * Override the NO OP observer, this is normally overridden by the compaction metrics. + */ + public AbstractCompactionTask setOpObserver(TableOperationObserver opObserver) + { + this.opObserver = opObserver; + return this; + } + + public void addObserver(CompactionObserver compObserver) + { + compObservers.add(compObserver); + } + + /** + * Returns the space overhead of this compaction. This can be used to limit running compactions to they fit under + * a given space budget. Only implemented for the types of tasks used by the unified compaction strategy and used + * by CNDB. + */ + public abstract long getSpaceOverhead(); + + /** + * Allows subclasses to route the task to a dedicated executor instead of the shared compaction executor. + */ + @Nullable + public Executor getCustomExecutor() + { + return null; + } + + /** + * @return The compaction observers for this task. Used by CNDB. + */ + public List getCompObservers() + { + return compObservers; + } + + /** + * Return the transaction that this task is working on. Used by CNDB as well as tests. + */ + public ILifecycleTransaction getTransaction() + { + return transaction; + } + public String toString() { return "CompactionTask(" + transaction + ")"; diff --git a/src/java/org/apache/cassandra/db/compaction/AbstractStrategyHolder.java b/src/java/org/apache/cassandra/db/compaction/AbstractStrategyHolder.java index a1471c77468c..805661a026f7 100644 --- a/src/java/org/apache/cassandra/db/compaction/AbstractStrategyHolder.java +++ b/src/java/org/apache/cassandra/db/compaction/AbstractStrategyHolder.java @@ -27,7 +27,6 @@ import com.google.common.base.Preconditions; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; @@ -50,23 +49,23 @@ */ public abstract class AbstractStrategyHolder { - public static class TaskSupplier implements Comparable + public static class TasksSupplier implements Comparable { private final int numRemaining; - private final Supplier supplier; + private final Supplier> supplier; - TaskSupplier(int numRemaining, Supplier supplier) + TasksSupplier(int numRemaining, Supplier> supplier) { this.numRemaining = numRemaining; this.supplier = supplier; } - public AbstractCompactionTask getTask() + public Collection getTasks() { return supplier.get(); } - public int compareTo(TaskSupplier o) + public int compareTo(TasksSupplier o) { return o.numRemaining - numRemaining; } @@ -74,17 +73,17 @@ public int compareTo(TaskSupplier o) public static interface DestinationRouter { - int getIndexForSSTable(SSTableReader sstable); + int getIndexForSSTable(CompactionSSTable sstable); int getIndexForSSTableDirectory(Descriptor descriptor); } /** * Maps sstables to their token partition bucket */ - public static class GroupedSSTableContainer + public static class GroupedSSTableContainer { private final AbstractStrategyHolder holder; - private final Set[] groups; + private final Set[] groups; private GroupedSSTableContainer(AbstractStrategyHolder holder) { @@ -93,7 +92,7 @@ private GroupedSSTableContainer(AbstractStrategyHolder holder) groups = new Set[holder.numTokenPartitions]; } - void add(SSTableReader sstable) + void add(S sstable) { Preconditions.checkArgument(holder.managesSSTable(sstable), "this strategy holder doesn't manage %s", sstable); int idx = holder.router.getIndexForSSTable(sstable); @@ -108,10 +107,10 @@ public int numGroups() return groups.length; } - public Set getGroup(int i) + public Set getGroup(int i) { Preconditions.checkArgument(i >= 0 && i < groups.length); - Set group = groups[i]; + Set group = groups[i]; return group != null ? group : Collections.emptySet(); } @@ -129,13 +128,15 @@ boolean isEmpty() } } - protected final ColumnFamilyStore cfs; + protected final CompactionRealm realm; + protected final CompactionStrategyFactory strategyFactory; final DestinationRouter router; private int numTokenPartitions = -1; - AbstractStrategyHolder(ColumnFamilyStore cfs, DestinationRouter router) + AbstractStrategyHolder(CompactionRealm realm, CompactionStrategyFactory strategyFactory, DestinationRouter router) { - this.cfs = cfs; + this.realm = realm; + this.strategyFactory = strategyFactory; this.router = router; } @@ -161,34 +162,33 @@ final void setStrategy(CompactionParams params, int numTokenPartitions) */ public abstract boolean managesRepairedGroup(boolean isRepaired, boolean isPendingRepair, boolean isTransient); - public boolean managesSSTable(SSTableReader sstable) + public boolean managesSSTable(CompactionSSTable sstable) { return managesRepairedGroup(sstable.isRepaired(), sstable.isPendingRepair(), sstable.isTransient()); } - public abstract AbstractCompactionStrategy getStrategyFor(SSTableReader sstable); + public abstract LegacyAbstractCompactionStrategy getStrategyFor(CompactionSSTable sstable); - public abstract Iterable allStrategies(); + public abstract Iterable allStrategies(); - public abstract Collection getBackgroundTaskSuppliers(long gcBefore); + public abstract Collection getBackgroundTaskSuppliers(long gcBefore); - public abstract Collection getMaximalTasks(long gcBefore, boolean splitOutput); + public abstract Collection getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism); - public abstract Collection getUserDefinedTasks(GroupedSSTableContainer sstables, long gcBefore); + public abstract Collection getUserDefinedTasks(GroupedSSTableContainer sstables, long gcBefore); - public GroupedSSTableContainer createGroupedSSTableContainer() + public GroupedSSTableContainer createGroupedSSTableContainer() { - return new GroupedSSTableContainer(this); + return new GroupedSSTableContainer<>(this); } - public abstract void addSSTable(SSTableReader sstable); - public abstract void addSSTables(GroupedSSTableContainer sstables); + public abstract void addSSTables(GroupedSSTableContainer sstables); - public abstract void removeSSTables(GroupedSSTableContainer sstables); + public abstract void removeSSTables(GroupedSSTableContainer sstables); - public abstract void replaceSSTables(GroupedSSTableContainer removed, GroupedSSTableContainer added); + public abstract void replaceSSTables(GroupedSSTableContainer removed, GroupedSSTableContainer added); - public abstract List getScanners(GroupedSSTableContainer sstables, Collection> ranges); + public abstract List getScanners(GroupedSSTableContainer sstables, Collection> ranges); public abstract SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, @@ -202,13 +202,7 @@ public abstract SSTableMultiWriter createSSTableMultiWriter(Descriptor descripto Collection indexGroups, LifecycleNewTracker lifecycleNewTracker); - /** - * Return the directory index the given compaction strategy belongs to, or -1 - * if it's not held by this holder - */ - public abstract int getStrategyIndex(AbstractCompactionStrategy strategy); - - public abstract boolean containsSSTable(SSTableReader sstable); + public abstract boolean containsSSTable(CompactionSSTable sstable); public abstract int getEstimatedRemainingTasks(); } diff --git a/src/java/org/apache/cassandra/db/compaction/AbstractTableOperation.java b/src/java/org/apache/cassandra/db/compaction/AbstractTableOperation.java new file mode 100644 index 000000000000..85602202d374 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/AbstractTableOperation.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.TimeUUID; + +/** + * This is a base abstract implementing some default methods of {@link TableOperation}. + *

+ * In previous versions it used to be called CompactionInfo and CompactionInfo.Holder. + *

+ * This class implements serializable to allow structured info to be returned via JMX. + **/ +public abstract class AbstractTableOperation implements TableOperation +{ + private volatile boolean stopRequested = false; + private volatile StopTrigger trigger = StopTrigger.NONE; + + /** + * Interrupt the current operation if possible and if the predicate is true. + * + * @param trigger cause of compaction interruption + */ + @Override + public void stop(StopTrigger trigger) + { + this.stopRequested = true; + if (!this.trigger.isFinal()) + this.trigger = trigger; + } + + /** + * @return true if the operation has received a request to be interrupted. + */ + @Override + public boolean isStopRequested() + { + return stopRequested || (isGlobal() && CompactionManager.instance.isGlobalCompactionPaused()); + } + + /** + * Return true if the predicate for the given sstables holds, or if the operation + * does not consider any sstables, in which case it will always return true (the + * default behaviour). + */ + @Override + public boolean shouldStop(Predicate predicate) + { + Progress progress = getProgress(); + final Set sstables = progress.sstables(); + if (sstables.isEmpty()) + return true; + + return sstables.stream().anyMatch(predicate); + } + + /** + * @return cause of compaction interruption. + */ + @Override + public StopTrigger trigger() + { + return trigger; + } + + /** + * The progress information for an operation, refer to the description of the class properties. + */ + public static class OperationProgress implements Serializable, Progress + { + private static final long serialVersionUID = 3695381572726744816L; + + /** + * The table metadata + */ + private final TableMetadata metadata; + /** + * The type of operation + */ + private final OperationType operationType; + /** + * Normally the bytes processed so far by this operation, but depending on the unit it could mean something else, e.g. ranges or keys. + */ + private final long completed; + /** + * The total bytes that need to be processed, for example the size of the input files. Depending on the unit it could mean something else, e.g. ranges or keys. + */ + private final long total; + /** + * The unit for {@link this#completed} and for {@link this#total}. + */ + private final Unit unit; + /** + * A unique ID for this operation + */ + private final TimeUUID operationId; + /** + * A set of SSTables participating in this operation + */ + private final ImmutableSet sstables; + private final String targetDirectory; + + public OperationProgress(TableMetadata metadata, OperationType operationType, long bytesComplete, long totalBytes, TimeUUID operationId, Collection sstables, String targetDirectory) + { + this(metadata, operationType, bytesComplete, totalBytes, Unit.BYTES, operationId, sstables, targetDirectory); + } + + public OperationProgress(TableMetadata metadata, OperationType operationType, long bytesComplete, long totalBytes, TimeUUID operationId, Collection sstables) + { + this(metadata, operationType, bytesComplete, totalBytes, Unit.BYTES, operationId, sstables, null); + } + + public OperationProgress(TableMetadata metadata, OperationType operationType, long bytesComplete, long totalBytes, long totalBytesScanned, TimeUUID operationId, Collection sstables) + { + this(metadata, operationType, bytesComplete, totalBytes, Unit.BYTES, operationId, sstables, null); + } + + public OperationProgress(TableMetadata metadata, OperationType operationType, long completed, long total, Unit unit, TimeUUID operationId, Collection sstables, String targetDirectory) + { + this.operationType = operationType; + this.completed = completed; + this.total = total; + this.metadata = metadata; + this.unit = unit; + this.operationId = operationId; + this.sstables = ImmutableSet.copyOf(sstables); + this.targetDirectory = targetDirectory; + } + + /** + * @return A copy of this OperationProgress with updated progress. + */ + public OperationProgress forProgress(long complete, long total) + { + return new OperationProgress(metadata, operationType, complete, total, unit, operationId, sstables, targetDirectory); + } + + /** + * Special operation progress where we always need to cancel the compaction - for example ViewBuilderTask where we don't know + * the sstables at construction + */ + public static OperationProgress withoutSSTables(TableMetadata metadata, OperationType tasktype, long completed, long total, AbstractTableOperation.Unit unit, TimeUUID compactionId) + { + return withoutSSTables(metadata, tasktype, completed, total, unit, compactionId, null); + } + + /** + * Special operation progress where we always need to cancel the compaction - for example AutoSavingCache where we don't know + * the sstables at construction + */ + public static OperationProgress withoutSSTables(TableMetadata metadata, OperationType tasktype, long completed, long total, AbstractTableOperation.Unit unit, TimeUUID compactionId, String targetDirectory) + { + return new OperationProgress(metadata, tasktype, completed, total, unit, compactionId, ImmutableSet.of(), targetDirectory); + } + + @Override + public Optional keyspace() + { + return metadata != null ? Optional.of(metadata.keyspace) : Optional.empty(); + } + + @Override + public Optional table() + { + return metadata != null ? Optional.of(metadata.name) : Optional.empty(); + } + + @Override + public TableMetadata metadata() + { + return metadata; + } + + @Override + public long completed() + { + return completed; + } + + @Override + public long total() + { + return total; + } + + @Override + public OperationType operationType() + { + return operationType; + } + + @Override + public TimeUUID operationId() + { + return operationId; + } + + @Override + public Unit unit() + { + return unit; + } + + @Override + public Set sstables() + { + return sstables; + } + + @Override + public String targetDirectory() + { + if (targetDirectory == null) + return ""; + + try + { + return new File(targetDirectory).canonicalPath(); + } + catch (Throwable t) + { + throw new RuntimeException("Unable to resolve canonical path for " + targetDirectory); + } + } + + public String toString() + { + return progressToString(); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/ActiveCompactions.java b/src/java/org/apache/cassandra/db/compaction/ActiveCompactions.java deleted file mode 100644 index 4e238ad95d46..000000000000 --- a/src/java/org/apache/cassandra/db/compaction/ActiveCompactions.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.db.compaction; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.File; - -public class ActiveCompactions implements ActiveCompactionsTracker -{ - // a synchronized identity set of running tasks to their compaction info - private final Set compactions = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); - - public List getCompactions() - { - return new ArrayList<>(compactions); - } - - public void beginCompaction(CompactionInfo.Holder ci) - { - compactions.add(ci); - } - - public void finishCompaction(CompactionInfo.Holder ci) - { - compactions.remove(ci); - CompactionManager.instance.getMetrics().bytesCompacted.inc(ci.getCompactionInfo().getTotal()); - CompactionManager.instance.getMetrics().totalCompactionsCompleted.mark(); - } - - /** - * Get the estimated number of bytes remaining to write per sstable directory - */ - public Map estimatedRemainingWriteBytes() - { - synchronized (compactions) - { - Map writeBytesPerSSTableDir = new HashMap<>(); - for (CompactionInfo.Holder holder : compactions) - { - CompactionInfo compactionInfo = holder.getCompactionInfo(); - List directories = compactionInfo.getTargetDirectories(); - if (directories == null || directories.isEmpty()) - continue; - long remainingWriteBytesPerDataDir = compactionInfo.estimatedRemainingWriteBytes() / directories.size(); - for (File directory : directories) - writeBytesPerSSTableDir.merge(directory, remainingWriteBytesPerDataDir, Long::sum); - } - return writeBytesPerSSTableDir; - } - } - - /** - * Iterates over the active compactions and tries to find CompactionInfos with the given compactionType for the given sstable - * - * Number of entries in compactions should be small (< 10) but avoid calling in any time-sensitive context - */ - public Collection getCompactionsForSSTable(SSTableReader sstable, OperationType compactionType) - { - List toReturn = null; - synchronized (compactions) - { - for (CompactionInfo.Holder holder : compactions) - { - CompactionInfo compactionInfo = holder.getCompactionInfo(); - if (compactionInfo.getSSTables().contains(sstable) && compactionInfo.getTaskType() == compactionType) - { - if (toReturn == null) - toReturn = new ArrayList<>(); - toReturn.add(compactionInfo); - } - } - } - return toReturn; - } -} diff --git a/src/java/org/apache/cassandra/db/compaction/ActiveCompactionsTracker.java b/src/java/org/apache/cassandra/db/compaction/ActiveCompactionsTracker.java deleted file mode 100644 index c1bbbd8e67bf..000000000000 --- a/src/java/org/apache/cassandra/db/compaction/ActiveCompactionsTracker.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.db.compaction; - -public interface ActiveCompactionsTracker -{ - public void beginCompaction(CompactionInfo.Holder ci); - public void finishCompaction(CompactionInfo.Holder ci); - - public static final ActiveCompactionsTracker NOOP = new ActiveCompactionsTracker() - { - public void beginCompaction(CompactionInfo.Holder ci) - {} - - public void finishCompaction(CompactionInfo.Holder ci) - {} - }; -} diff --git a/src/java/org/apache/cassandra/db/compaction/ActiveOperations.java b/src/java/org/apache/cassandra/db/compaction/ActiveOperations.java new file mode 100644 index 000000000000..fbc7783185c4 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/ActiveOperations.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; + +import javax.annotation.concurrent.ThreadSafe; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.utils.NonThrowingCloseable; + +@ThreadSafe +public class ActiveOperations implements TableOperationObserver +{ + private static final Logger logger = LoggerFactory.getLogger(ActiveOperations.class); + + // The operations ordered by keyspace.table for all the operations that are currently in progress. + private static final Set operations = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + + // Compaction tasks that have been created but aren't executing yet. + private static final Set scheduledTasks = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + + // Keep registered listeners to be called onStart and close + private final List listeners = new CopyOnWriteArrayList<>(); + + public interface CompactionProgressListener + { + /** + * Called when compaction started + */ + default void onStarted(TableOperation.Progress progress) {} + + /** + * Called when compaction completed + */ + default void onCompleted(TableOperation.Progress progressOnCompleted) {} + } + + public void registerListener(CompactionProgressListener listener) + { + listeners.add(listener); + } + + public void unregisterListener(CompactionProgressListener listener) + { + listeners.remove(listener); + } + + /** + * @return all the table operations currently in progress. This is mostly compactions but it can include other + * operations too, basically any operation that calls {@link this#onOperationStart(TableOperation)}. + */ + public List getTableOperations() + { + ImmutableList.Builder builder = ImmutableList.builder(); + synchronized (operations) + { + builder.addAll(operations); + } + return builder.build(); + } + + @Override + public NonThrowingCloseable onOperationStart(TableOperation op) + { + TableOperation.Progress progress = op.getProgress(); + for (CompactionProgressListener listener : listeners) + { + try + { + listener.onStarted(progress); + } + catch (Throwable t) + { + String listenerName = listener.getClass().getName(); + logger.error("Unable to notify listener {} while trying to start compaction {} on table {}", + listenerName, progress.operationType(), progress.metadata(), t); + } + } + operations.add(op); + return () -> completeOperation(op); + } + + private void completeOperation(TableOperation op) + { + operations.remove(op); + TableOperation.Progress progressOnCompleted = op.getProgress(); + CompactionManager.instance.getMetrics().bytesCompacted.inc(progressOnCompleted.total()); + CompactionManager.instance.getMetrics().totalCompactionsCompleted.mark(); + + for (CompactionProgressListener listener : listeners) + { + try + { + listener.onCompleted(progressOnCompleted); + } + catch (Throwable t) + { + String listenerName = listener.getClass().getName(); + logger.error("Unable to notify listener {} while trying to complete compaction {} on table {}", + listenerName, progressOnCompleted.operationType(), progressOnCompleted.metadata(), t); + } + } + } + + /** + * Get the estimated number of bytes remaining to write per sstable directory + */ + public Map estimatedRemainingWriteBytes() + { + synchronized (operations) + { + Map writeBytesPerSSTableDir = new HashMap<>(); + for (TableOperation holder : operations) + { + TableOperation.Progress compactionInfo = holder.getProgress(); + List directories = compactionInfo.getTargetDirectories(); + if (directories == null || directories.isEmpty()) + continue; + long remainingWriteBytesPerDataDir = compactionInfo.estimatedRemainingWriteBytes() / directories.size(); + for (File directory : directories) + writeBytesPerSSTableDir.merge(directory, remainingWriteBytesPerDataDir, Long::sum); + } + return writeBytesPerSSTableDir; + } + } + + /** + * Iterates over the active operations and tries to find OperationProgresses with the given operation type for the given sstable + * + * Number of entries in operations should be small (< 10) but avoid calling in any time-sensitive context + */ + public Collection getOperationsForSSTable(SSTableReader sstable, OperationType operationType) + { + List toReturn = null; + + synchronized (operations) + { + for (TableOperation op : operations) + { + TableOperation.Progress progress = op.getProgress(); + if (progress.sstables().contains(sstable) && progress.operationType() == operationType) + { + if (toReturn == null) + toReturn = new ArrayList<>(); + toReturn.add(progress); + } + } + } + return toReturn; + } + + /** + * @return true if given table operation is still active + */ + public boolean isActive(TableOperation op) + { + return getTableOperations().contains(op); + } + + public void addTaskToScheduled(AbstractCompactionTask task) + { + scheduledTasks.add(task); + } + + public void removeTaskFromScheduled(AbstractCompactionTask task) + { + scheduledTasks.remove(task); + } + + public Collection getScheduledTasksMatching(Iterable cfss, Predicate predicate, Predicate taskPredicate) + { + List tasksCopy; + synchronized (scheduledTasks) + { + tasksCopy = new ArrayList<>(scheduledTasks); + } + + List matching = new ArrayList<>(tasksCopy.size()); + for (AbstractCompactionTask task : tasksCopy) + { + if (taskPredicate.test(task)) + { + for (ColumnFamilyStore cfs : cfss) + { + if (task.affectsAny(cfs, predicate)) + { + matching.add(task); + break; + } + } + } + } + return matching; + } + + public void cancelScheduledTasksAffecting(Iterable cfss, Predicate predicate, TableOperation.StopTrigger trigger) + { + Iterable tasksCopy; + synchronized (scheduledTasks) + { + tasksCopy = new ArrayList<>(scheduledTasks); + } + + for (AbstractCompactionTask task : tasksCopy) + for (ColumnFamilyStore cfs : cfss) + task.cancelIfAffects(cfs, predicate, trigger); + } + + @VisibleForTesting + List getScheduledTasks() + { + synchronized (scheduledTasks) + { + return new ArrayList<>(scheduledTasks); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/ArenaSelector.java b/src/java/org/apache/cassandra/db/compaction/ArenaSelector.java new file mode 100644 index 000000000000..f3a38f30620a --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/ArenaSelector.java @@ -0,0 +1,143 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Collectors; + +import org.apache.cassandra.db.DiskBoundaries; +import org.apache.cassandra.db.compaction.unified.Controller; + +/** + * Arena selector, used by UnifiedCompactionStrategy to distribute SSTables to separate compaction arenas. + * + * This is used to: + * - ensure that sstables that should not be compacted together (e.g. repaired with unrepaired) are separated + * - ensure that each disk's sstables are compacted separately + */ +public class ArenaSelector implements Comparator +{ + private final EquivClassSplitter[] classSplitters; + final Controller controller; + final DiskBoundaries diskBoundaries; + + public ArenaSelector(Controller controller, DiskBoundaries diskBoundaries) + { + this.controller = controller; + this.diskBoundaries = diskBoundaries; + + ArrayList ret = new ArrayList<>(2); + + ret.add(RepairEquivClassSplitter.INSTANCE); + + if (diskBoundaries.getNumBoundaries() > 1) + { + ret.add(new DiskIndexEquivClassSplitter()); + } + + classSplitters = ret.toArray(new EquivClassSplitter[0]); + } + + @Override + public int compare(CompactionSSTable o1, CompactionSSTable o2) + { + int res = 0; + for (int i = 0; res == 0 && i < classSplitters.length; i++) + res = classSplitters[i].compare(o1, o2); + return res; + } + + public String name(CompactionSSTable t) + { + return Arrays.stream(classSplitters) + .map(e -> e.name(t)) + .collect(Collectors.joining("-")); + } + + /** + * An equivalence class is a function that compares two sstables and returns 0 when they fall in the same class. + * For example, the repair status or disk index may define equivalence classes. See the concrete equivalence classes below. + */ + private interface EquivClassSplitter extends Comparator { + + @Override + int compare(CompactionSSTable a, CompactionSSTable b); + + /** Return a name that describes the equivalence class */ + String name(CompactionSSTable ssTableReader); + } + + /** + * Split sstables by their repair state: repaired, unrepaired, pending repair with a specific UUID (one group per pending repair). + */ + private static final class RepairEquivClassSplitter implements EquivClassSplitter + { + public static final EquivClassSplitter INSTANCE = new RepairEquivClassSplitter(); + + @Override + public int compare(CompactionSSTable a, CompactionSSTable b) + { + // This is the same as name(a).compareTo(name(b)) + int af = repairClassValue(a); + int bf = repairClassValue(b); + if (af != 0 || bf != 0) + return Integer.compare(af, bf); + return a.getPendingRepair().compareTo(b.getPendingRepair()); + } + + private static int repairClassValue(CompactionSSTable a) + { + if (a.isRepaired()) + return 1; + if (!a.isPendingRepair()) + return 2; + else + return 0; + } + + @Override + public String name(CompactionSSTable ssTableReader) + { + if (ssTableReader.isRepaired()) + return "repaired"; + else if (!ssTableReader.isPendingRepair()) + return "unrepaired"; + else + return "pending_repair_" + ssTableReader.getPendingRepair(); + } + } + + /** + * Group sstables by their disk index. + */ + private final class DiskIndexEquivClassSplitter implements EquivClassSplitter + { + @Override + public int compare(CompactionSSTable a, CompactionSSTable b) + { + return Integer.compare(diskBoundaries.getDiskIndexFromKey(a), diskBoundaries.getDiskIndexFromKey(b)); + } + + @Override + public String name(CompactionSSTable ssTableReader) + { + return "disk_" + diskBoundaries.getDiskIndexFromKey(ssTableReader); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/BackgroundCompactionRunner.java b/src/java/org/apache/cassandra/db/compaction/BackgroundCompactionRunner.java new file mode 100644 index 000000000000..0a2d4e4ae707 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/BackgroundCompactionRunner.java @@ -0,0 +1,546 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.io.IOError; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; // checkstyle: permit this import +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import org.apache.cassandra.concurrent.ScheduledExecutorPlus; +import org.apache.cassandra.concurrent.WrappedExecutorPlus; +import org.apache.cassandra.utils.concurrent.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.io.FSDiskFullWriteError; +import org.apache.cassandra.io.FSError; +import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.compress.CorruptBlockException; +import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.CorruptFileException; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.Throwables; + +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; + +public class BackgroundCompactionRunner implements Runnable +{ + private static final Logger logger = LoggerFactory.getLogger(BackgroundCompactionRunner.class); + + public static final String NO_SPACE_LEFT_MESSAGE = "No space left on device"; + + public enum RequestResult + { + /** + * when the compaction check was done and there were no compaction tasks for the CFS + */ + NOT_NEEDED, + + /** + * when the compaction was aborted for the CFS because the CF got dropped in the meantime + */ + ABORTED, + + /** + * compaction tasks were completed for the CFS + */ + COMPLETED + } + + private final ScheduledExecutorPlus checkExecutor; + + /** + * CFSs for which a compaction was requested mapped to the promise returned to the requesting code + */ + private final ConcurrentMap compactionRequests = new ConcurrentHashMap<>(); + + private final AtomicInteger ongoingUpgrades = new AtomicInteger(0); + + /** + * Tracks the number of currently requested compactions. Used to delay checking for new compactions until there's + * room in the executing threads. + */ + private final AtomicInteger ongoingCompactions = new AtomicInteger(0); + + private final Random random = new Random(); + + private final WrappedExecutorPlus compactionExecutor; + + private final ActiveOperations activeOperations; + + + BackgroundCompactionRunner(WrappedExecutorPlus compactionExecutor, ActiveOperations activeOperations) + { + this(compactionExecutor, executorFactory().scheduled("BackgroundTaskExecutor"), activeOperations); + } + + @VisibleForTesting + BackgroundCompactionRunner(WrappedExecutorPlus compactionExecutor, ScheduledExecutorPlus checkExecutor, ActiveOperations activeOperations) + { + this.compactionExecutor = compactionExecutor; + this.checkExecutor = checkExecutor; + this.activeOperations = activeOperations; + } + + /** + * This extends and behave like a {@link CompletableFuture}, with the exception that one cannot call + * {@link #cancel}, {@link #setSuccess} and {@link #setFailure} (they throw {@link UnsupportedOperationException}). + */ + public static class FutureRequestResult extends AsyncPromise + { + @Override + public Promise setSuccess(RequestResult t) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean cancel(boolean interruptIfRunning) + { + throw new UnsupportedOperationException(); + } + + @Override + public Promise setFailure(Throwable throwable) + { + throw new UnsupportedOperationException(); + } + + private void completeInternal(RequestResult t) + { + super.trySuccess(t); + } + + private void completeExceptionallyInternal(Throwable throwable) + { + super.tryFailure(throwable); + } + } + + /** + * Marks each CFS in a set for compaction. See {@link #markForCompactionCheck(ColumnFamilyStore)} for details. + */ + void markForCompactionCheck(Set cfss) + { + List results = cfss.stream() + .map(this::requestCompactionInternal) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!results.isEmpty() && !maybeScheduleNextCheck()) + { + logger.info("Executor has been shut down, background compactions check will not be scheduled"); + results.forEach(r -> r.completeInternal(RequestResult.ABORTED)); + } + } + + /** + * Marks a CFS for compaction. Since marked, it will become a possible candidate for compaction. The mark will be + * cleared when we actually run the compaction for CFS. + * + * @return a promise which will be completed when the mark is cleared. The returned future should not be cancelled or + * completed by the caller. + */ + Promise markForCompactionCheck(ColumnFamilyStore cfs) + { + FutureRequestResult p = requestCompactionInternal(cfs); + if (p == null) + return new AsyncPromise().setSuccess(RequestResult.ABORTED); + + if (!maybeScheduleNextCheck()) + { + logger.info("Executor has been shut down, background compactions check will not be scheduled"); + p.completeInternal(RequestResult.ABORTED); + } + return p; + } + + private FutureRequestResult requestCompactionInternal(ColumnFamilyStore cfs) + { + logger.trace("Requested background compaction for {}", cfs); + if (!cfs.isValid()) + { + logger.trace("Aborting compaction for dropped CF {}", cfs); + return null; + } + if (cfs.isAutoCompactionDisabled()) + { + logger.trace("Autocompaction is disabled"); + return null; + } + + return compactionRequests.computeIfAbsent(cfs, ignored -> new FutureRequestResult()); + } + + void shutdown() + { + checkExecutor.shutdown(); + compactionRequests.values().forEach(promise -> promise.completeInternal(RequestResult.ABORTED)); + // it's okay to complete a CompletableFuture more than one on race between request, run and shutdown + } + + @VisibleForTesting + int getOngoingCompactionsCount() + { + return ongoingCompactions.get(); + } + + @VisibleForTesting + int getOngoingUpgradesCount() + { + return ongoingUpgrades.get(); + } + + @VisibleForTesting + Set getMarkedCFSs() + { + return ImmutableSet.copyOf(compactionRequests.keySet()); + } + + @Override + public void run() + { + logger.trace("Running background compactions check"); + + // When the executor is fully occupied, we delay acting on this request until a thread is available. This + // helps make a better decision what exactly to compact (e.g. if we issue the request now we may select n + // sstables, while by the time this request actually has a thread to execute on more may have accumulated, + // and it may be better to compact all). + // Note that we make a request whenever a task completes and thus this method is guaranteed to run again + // when threads free up. + if (ongoingCompactions.get() >= compactionExecutor.getMaximumPoolSize()) + { + logger.trace("Background compaction threads are busy; delaying new compactions check until there are free threads"); + return; + } + + // We shuffle the CFSs for which the compaction was requested so that with each run we traverse those CFSs + // in different order and make each CFS have equal chance to be selected + ArrayList compactionRequestsList = new ArrayList<>(compactionRequests.keySet()); + Collections.shuffle(compactionRequestsList, random); + + for (ColumnFamilyStore cfs : compactionRequestsList) + { + if (ongoingCompactions.get() >= compactionExecutor.getMaximumPoolSize()) + { + logger.trace("Background compaction threads are busy; delaying new compactions check until there are free threads"); + return; + } + + FutureRequestResult promise = compactionRequests.remove(cfs); + assert promise != null : "Background compaction checker must be single-threaded"; + + if (promise.isDone()) + { + // A shutdown request may abort processing while we are still processing + try + { + assert promise.get() == RequestResult.ABORTED : "Background compaction checker must be single-threaded"; + } + catch (InterruptedException e) + { + throw new UncheckedInterruptedException(e); + } + catch (ExecutionException e) + { + throw new RuntimeException(e); + } + + logger.trace("The request for {} was aborted due to shutdown", cfs); + continue; + } + + if (!cfs.isValid()) + { + logger.trace("Aborting compaction for dropped CF {}", cfs); + promise.completeInternal(RequestResult.ABORTED); + continue; + } + + logger.trace("Running a background task check for {} with {}", cfs, cfs.getCompactionStrategy().getName()); + + CompletableFuture compactionTasks = startCompactionTasks(cfs); + if (compactionTasks == null) + compactionTasks = startUpgradeTasks(cfs); + + if (compactionTasks != null) + { + compactionTasks.handle((ignored, throwable) -> { + if (throwable != null) + { + handleCompactionError(throwable, cfs); + promise.completeExceptionallyInternal(throwable); + } + else + { + logger.trace("Finished compaction for {}", cfs); + promise.completeInternal(RequestResult.COMPLETED); + } + return null; + }); + + // The compaction strategy may return more subsequent tasks if we ask for them. Therefore, we request + // the compaction again on that CFS early (without waiting for the currently scheduled/started + // compaction tasks to finish). We can start them in the next check round if we have free slots + // in the compaction executor. + markForCompactionCheck(cfs); + } + else + { + promise.completeInternal(RequestResult.NOT_NEEDED); + } + } + } + + private boolean maybeScheduleNextCheck() + { + if (checkExecutor.getPendingTaskCount() == 0) + { + try + { + checkExecutor.execute(this); + } + catch (RejectedExecutionException ex) + { + if (checkExecutor.isShutdown()) + logger.info("Executor has been shut down, background compactions check will not be scheduled"); + else + logger.error("Failed to submit background compactions check", ex); + + return false; + } + } + + return true; + } + + private CompletableFuture startCompactionTasks(ColumnFamilyStore cfs) + { + // Check if we are in a paused state. If so, we shouldn't modify sstable lists until the pause is done. + if (!cfs.isCompactionActive()) + return null; + + Collection compactionTasks = cfs.getCompactionStrategy() + .getNextBackgroundTasks(CompactionManager.getDefaultGcBefore(cfs, FBUtilities.nowInSeconds())); + + // Re-check if we are in a paused state (this status may have changed between the time the next tasks call + // was initiated and now). If so, we shouldn't modify sstable lists until the pause is done. + if (cfs.isCompactionActive()) + { + + CompletableFuture[] compactionTaskFutures = startCompactionTasks(cfs, compactionTasks); + return compactionTaskFutures != null ? CompletableFuture.allOf(compactionTaskFutures) : null; + } + else + { + logger.debug("Background compactions not issued because compaction is not active."); + Throwable t = null; + for (var c : compactionTasks) + t = c.rejected(t); + Throwables.maybeFail(t); + // Note that there is still a race between the compaction pause in `runWithCompactionsDisabled` and task + // collection creating the transactions for the tasks above that can cause `runWithCompactionsDisabled` to + // fail if we have prepared a compaction task but not reached this point to reject it yet. + // This situation, however, should occur very rarely and will resolve itself quickly. + return null; + } + } + + CompletableFuture[] startCompactionTasks(ColumnFamilyStore cfs, Collection compactionTasks) + { + if (!compactionTasks.isEmpty()) + { + logger.debug("Running compaction tasks: {}", compactionTasks); + CompletableFuture[] arr = new CompletableFuture[compactionTasks.size()]; + int index = 0; + for (AbstractCompactionTask task : compactionTasks) + arr[index++] = startTask(cfs, task); + + return arr; + } + else + { + logger.trace("No compaction tasks for {}", cfs); + return null; + } + } + + private CompletableFuture startTask(ColumnFamilyStore cfs, AbstractCompactionTask task) + { + ongoingCompactions.incrementAndGet(); + try + { + Executor executor = task.getCustomExecutor() == null ? compactionExecutor : task.getCustomExecutor(); + return CompletableFuture.runAsync( + () -> { + try + { + task.execute(activeOperations); + } + finally + { + ongoingCompactions.decrementAndGet(); + + // Request a new round of checking for compactions. We do this for two reasons: + // - a task has completed and there may now be new compaction possibilities in this CFS, + // - a thread has freed up, and a new compaction task (from any CFS) can be scheduled on it + markForCompactionCheck(cfs); + } + }, executor); + } + catch (RejectedExecutionException ex) + { + ongoingCompactions.decrementAndGet(); + logger.debug("Background compaction task for {} was rejected", cfs); + return CompletableFuture.completedFuture(null); + } + } + + private CompletableFuture startUpgradeTasks(ColumnFamilyStore cfs) + { + AbstractCompactionTask upgradeTask = getUpgradeSSTableTask(cfs); + + if (upgradeTask != null) + { + logger.debug("Running upgrade task: {}", upgradeTask); + return startTask(cfs, upgradeTask).handle((ignored1, ignored2) -> { + ongoingUpgrades.decrementAndGet(); + return null; + }); + } + else + { + logger.trace("No upgrade tasks for {}", cfs); + return null; + } + } + + /** + * Finds the oldest (by modification date) non-latest-version sstable on disk and creates an upgrade task for it + */ + @VisibleForTesting + public AbstractCompactionTask getUpgradeSSTableTask(ColumnFamilyStore cfs) + { + logger.trace("Checking for upgrade tasks {}", cfs); + + if (!DatabaseDescriptor.automaticSSTableUpgrade()) + { + logger.trace("Automatic sstable upgrade is disabled - will not try to upgrade sstables of {}", cfs); + return null; + } + + if (ongoingUpgrades.incrementAndGet() <= DatabaseDescriptor.maxConcurrentAutoUpgradeTasks()) + { + List potentialUpgrade = cfs.getCandidatesForUpgrade(); + for (SSTableReader sstable : potentialUpgrade) + { + LifecycleTransaction txn = cfs.getTracker().tryModify(sstable, OperationType.UPGRADE_SSTABLES); + if (txn != null) + { + logger.debug("Found tasks for automatic sstable upgrade of {}", sstable); + return cfs.getCompactionStrategy().createCompactionTask(txn, Integer.MIN_VALUE, Long.MAX_VALUE); + } + } + } + else + { + logger.trace("Skipped upgrade task for {} because the limit {} of concurrent upgrade tasks has been reached", + cfs, DatabaseDescriptor.maxConcurrentAutoUpgradeTasks()); + } + + ongoingUpgrades.decrementAndGet(); + return null; + } + + public static void handleCompactionError(Throwable t, ColumnFamilyStore cfs) + { + t = Throwables.unwrapped(t); + + // FSDiskFullWriteErrors is thrown when checking disk space before starting flush or compaction task. They + // are expected to be recoverable because we haven't actually hit No-Space-Left error yet, so we don't explicitly + // trigger the disk failure policy because of them (see CASSANDRA-12385). + if (t instanceof IOError && !(t instanceof FSDiskFullWriteError)) + { + logger.error("Potentially unrecoverable error during background compaction of table {}", cfs, t); + // Strictly speaking it's also possible to hit a read-related IOError during compaction, although the + // chances for that are much lower than the chances for write-related IOError. If we want to handle that, + // we might have to rely on error message parsing... + t = t instanceof FSError ? t : new FSWriteError(t); + JVMStabilityInspector.inspectThrowable(t); + CompactionManager.instance.incrementFailed(); + } + // No-Space-Left IO exception is thrown by JDK when disk has reached its capacity. The key difference between this + // and the earlier case with `FSDiskFullWriteError` is that here we have definitively run out of disk space, and + // no further writes can be performed until disk space is freed, potentially leading to data corruption or + // system instability if not handled properly. We must trigger the disk failure policy. + else if (Throwables.isCausedBy(t, IOException.class) && t.toString().contains(NO_SPACE_LEFT_MESSAGE)) + { + logger.error("Encountered no space left error on {}", cfs, t); + // wrap it with FSWriteError so that JVMStabilityInspector can properly stop or die + t = t instanceof FSError ? t : new FSWriteError(t); + JVMStabilityInspector.inspectThrowable(t); + CompactionManager.instance.incrementFailed(); + } + else if (Throwables.isCausedBy(t, OutOfMemoryError.class)) + { + logger.error("Encountered out of memory error on {}", cfs, t); + JVMStabilityInspector.inspectThrowable(t); + CompactionManager.instance.incrementFailed(); + } + else if (Throwables.anyCauseMatches(t, err -> err instanceof CorruptBlockException + || err instanceof CorruptFileException + || err instanceof CorruptSSTableException)) + { + logger.error("Encountered corruption exception on {}", cfs, t); + JVMStabilityInspector.inspectThrowable(t); + CompactionManager.instance.incrementFailed(); + } + else if (t instanceof CompactionInterruptedException) + { + logger.warn(String.format("Aborting background compaction of %s due to interruption", cfs), Throwables.unwrapped(t)); + CompactionManager.instance.incrementAborted(); + } + else + { + logger.error("Exception during background compaction of table {}", cfs, t); + CompactionManager.instance.incrementFailed(); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/BackgroundCompactions.java b/src/java/org/apache/cassandra/db/compaction/BackgroundCompactions.java new file mode 100644 index 000000000000..0ee35967fda1 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/BackgroundCompactions.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.ExpMovingAverage; +import org.apache.cassandra.utils.MovingAverage; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.TimeUUID; + +/** + * A class for grouping the background compactions picked by a strategy, either pending or in progress. + * + * A compaction strategy has a {@link BackgroundCompactions} object as part of its state. Each + * {@link LegacyAbstractCompactionStrategy} instance has its {@link BackgroundCompactions}, and their lifespans are the + * same. In the case of {@link UnifiedCompactionStrategy} the new strategy instance inherits + * {@link BackgroundCompactions} from its predecessor. + */ +public class BackgroundCompactions +{ + private static final Logger logger = LoggerFactory.getLogger(BackgroundCompactions.class); + + /** The table metadata */ + private final TableMetadata metadata; + + /** The compaction aggregates with either pending or ongoing compactions, or both. This is a private map + * whose access needs to be synchronized. */ + private final TreeMap aggregatesMap; + + /** + * The current list of compaction aggregates, this list must be recreated every time the aggregates + * map is changed. + * + * We publish aggregates to a separate variable instead of calling {@code aggregatesMap.values()} so that reads + * that race with updates always observe a consistent snapshot. + */ + private volatile List aggregates; + + /** The ongoing compactions grouped by unique operation ID. */ + private final ConcurrentHashMap compactions = new ConcurrentHashMap<>(); + + /** + * Rate of progress (per thread) of recent compactions for the CFS. Used by the UnifiedCompactionStrategy to + * limit the number of running compactions to no more than what is sufficient to saturate the throughput limit. + * This needs to be a longer-running average to ensure that the rate limiter stalling a new thread can't cause + * the compaction rate to temporarily drop to levels that permit an extra thread. + */ + MovingAverage compactionRate = ExpMovingAverage.decayBy1000(); + + /** + * Track num of skipped compaction aggregates due to insufficient disk space + */ + private final AtomicLong skippedAggregatesDueToDiskSpace = new AtomicLong(0); + + BackgroundCompactions(CompactionRealm realm) + { + this.metadata = realm.metadata(); + this.aggregatesMap = new TreeMap<>(); + this.aggregates = ImmutableList.of(); + } + + /** + * Updates the list of pending compactions, while preserving the set of running ones. This is done + * by creating new aggregates with the pending aggregates but adding any existing aggregates with + * compactions in progress. If there is a matching pending aggregate then the existing compactions + * are transferred to it, otherwise the old aggregate is stripped of its pending compactios and then + * it is kept with the compactions in progress only. + * + * @param pending compaction aggregates with pending compactions + */ + synchronized void setPending(CompactionStrategy strategy, Collection pending) + { + if (pending == null) + throw new IllegalArgumentException("argument cannot be null"); + + if (logger.isTraceEnabled()) + logger.trace("Resetting pending aggregates for strategy {}/{}, received {} new aggregates", + strategy.getName(), strategy.hashCode(), pending.size()); + + // First remove the existing aggregates + aggregatesMap.clear(); + + // Then add all the pending aggregates + for (CompactionAggregate aggregate : pending) + { + CompactionAggregate prev = aggregatesMap.get(aggregate.getKey()); + if (logger.isTraceEnabled()) + logger.trace("Adding new pending aggregate: prev={}, current={}", prev, aggregate); + + if (prev == null) + aggregatesMap.put(aggregate.getKey(), aggregate); + else + aggregatesMap.put(aggregate.getKey(), prev.mergeWith(aggregate)); + } + + // Then add the old aggregates but only if they have ongoing compactions + for (CompactionAggregate oldAggregate : this.aggregates) + { + Collection compacting = oldAggregate.getInProgress(); + if (compacting.isEmpty()) + { + if (logger.isTraceEnabled()) + logger.trace("Existing aggregate {} has no in progress compactions, removing it", oldAggregate); + + continue; + } + + // See if we have a matching aggregate in the pending aggregates, if so add all the existing compactions to it + // otherwise strip the pending and selected compactions from the old one and keep it only with the compactions in progress + CompactionAggregate newAggregate; + CompactionAggregate matchingAggregate = oldAggregate.getMatching(aggregatesMap); + if (matchingAggregate != null) + { + // add the old compactions to the new aggregate + // the key will change slightly for STCS so remove it before adding it again + aggregatesMap.remove(matchingAggregate.getKey()); + newAggregate = matchingAggregate.withAdditionalCompactions(compacting); + + if (logger.isTraceEnabled()) + logger.trace("Removed matching aggregate {}", matchingAggregate); + } + else + { + // keep the old aggregate but only with the compactions already in progress and not yet completed + newAggregate = oldAggregate.withOnlyTheseCompactions(compacting); + + if (logger.isTraceEnabled()) + logger.trace("Keeping old aggregate but only with compactions {}", oldAggregate); + } + + if (logger.isTraceEnabled()) + logger.trace("Adding new aggregate with previous compactions {}", newAggregate); + + aggregatesMap.put(newAggregate.getKey(), newAggregate); + } + + // Publish the new aggregates + this.aggregates = ImmutableList.copyOf(aggregatesMap.values()); + + CompactionLogger compactionLogger = strategy.getCompactionLogger(); + if (compactionLogger != null && compactionLogger.enabled()) + { + // compactionLogger.statistics(strategy, "pending", getStatistics()); // too much noise + compactionLogger.pending(strategy, getEstimatedRemainingTasks()); + } + } + + void setSubmitted(CompactionStrategy strategy, TimeUUID id, CompactionAggregate aggregate) + { + if (id == null || aggregate == null) + throw new IllegalArgumentException("arguments cannot be null"); + + logger.debug("Submitting background compaction {} for {}.{}", id, metadata.keyspace, metadata.name); + CompactionPick compaction = aggregate.getSelected(); + + CompactionPick prev = compactions.put(id, compaction); + if (prev != null) + throw new IllegalArgumentException("Found existing compaction with same id: " + id); + + compaction.setSubmitted(id); + + synchronized (this) + { + CompactionAggregate existingAggregate = aggregate.getMatching(aggregatesMap); + boolean aggregatesMapChanged = false; + + if (existingAggregate == null) + { + if (logger.isTraceEnabled()) + logger.trace("Could not find aggregate for compaction using the one passed in: {}", aggregate); + + aggregatesMapChanged = true; + aggregatesMap.put(aggregate.getKey(), aggregate); + } + else + { + if (logger.isTraceEnabled()) + logger.trace("Found aggregate for compaction: {}", existingAggregate); + + Pair contains = existingAggregate.containsSameInstance(compaction); + if (!contains.left) + { + // add the compaction just submitted to the aggregate that was found if it doesn't already contain it + // (the same exact instance that is because when we set the progress in compactions we ideally would like + // the instance in the aggregates map to also be updated) + // because for STCS the key may change slightly, first remove the existing aggregate, before re-inserting it + aggregatesMapChanged = true; + aggregatesMap.remove(existingAggregate.getKey()); + CompactionAggregate newAggregate = existingAggregate.withReplacedCompaction(compaction, contains.right); + aggregatesMap.put(newAggregate.getKey(), newAggregate); + + if (logger.isTraceEnabled()) + logger.trace("Added compaction to existing aggregate: {} -> {}", existingAggregate, newAggregate); + } + else + { + if (logger.isTraceEnabled()) + logger.trace("Existing aggregate {} already had compaction", existingAggregate); + } + } + + // Publish the new aggregates if needed + if (aggregatesMapChanged) + this.aggregates = ImmutableList.copyOf(aggregatesMap.values()); + } + + CompactionLogger compactionLogger = strategy.getCompactionLogger(); + if (compactionLogger != null && compactionLogger.enabled()) + compactionLogger.statistics(strategy, "submitted", getStatistics(strategy)); + } + + public void onInProgress(CompactionProgress progress) + { + if (progress == null) + throw new IllegalArgumentException("argument cannot be null"); + + updateCompactionRate(progress); + + TimeUUID id = progress.operationId(); + CompactionPick compaction = compactions.computeIfAbsent(id, + uuid -> + CompactionPick.createWithUnknownParent(id, + progress.inSSTables())); + + logger.debug("Setting background compaction {} as in progress", id); + compaction.setProgress(progress); + } + + public void onCompleted(CompactionStrategy strategy, TimeUUID id) + { + if (id == null) + throw new IllegalArgumentException("argument cannot be null"); + + logger.debug("Removing compaction {}", id); + + // log the statistics before completing the compaction so that we see the stats for the + // compaction that just completed + CompactionLogger compactionLogger = strategy.getCompactionLogger(); + if (compactionLogger != null && compactionLogger.enabled()) + compactionLogger.statistics(strategy, "completed", getStatistics(strategy)); + + CompactionPick completed = compactions.remove(id); + if (completed != null) + { + CompactionProgress progress = completed.progress(); + updateCompactionRate(progress); + completed.setCompleted(); + } + + // We rely on setPending() to refresh the aggregates again even though in some cases it may not be + // called immediately (e.g. compactions disabled) + } + + private void updateCompactionRate(CompactionProgress progress) + { + if (progress != null) + { + final long durationInMillis = progress.durationInMillis(); + final long outputDiskSize = progress.outputDiskSize(); + if (durationInMillis > 0 && outputDiskSize > 0) + compactionRate.update(outputDiskSize * 1.e3 / durationInMillis); + } + } + + public void incrementSkippedAggregatesDueToDiskSpace() + { + skippedAggregatesDueToDiskSpace.incrementAndGet(); + } + + public long getSkippedAggregatesDueToDiskSpace() + { + return skippedAggregatesDueToDiskSpace.get(); + } + + public Collection getAggregates() + { + return aggregates; + } + + /** + * @return the number of background compactions estimated to still be needed + */ + public int getEstimatedRemainingTasks() + { + return CompactionAggregate.numEstimatedCompactions(aggregates); + } + + /** + * @return the compactions currently in progress + */ + public Collection getCompactionsInProgress() + { + return Collections.unmodifiableCollection(compactions.values()); + } + + /** + * @return the compaction with the given id, if it is currently in progress + */ + public CompactionPick getCompaction(TimeUUID id) + { + return compactions.get(id); + } + + /** + * @return the total number of background compactions, pending or in progress + */ + public int getTotalCompactions() + { + return compactions.size() + getEstimatedRemainingTasks(); + } + + /** + * Return the compaction statistics for this strategy. + * + * @return statistics about this compaction strategy. + */ + public CompactionStrategyStatistics getStatistics(CompactionStrategy strategy) + { + return CompactionAggregate.getStatistics(metadata, strategy, aggregates); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CleanupTask.java b/src/java/org/apache/cassandra/db/compaction/CleanupTask.java new file mode 100644 index 000000000000..2076d628bbed --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CleanupTask.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.repair.consistent.admin.CleanupSummary; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.TimeUUID; + +public class CleanupTask +{ + private static final Logger logger = LoggerFactory.getLogger(CleanupTask.class); + + private final CompactionRealm realm; + private final List> tasks; + + public CleanupTask(CompactionRealm realm, List> tasks) + { + this.realm = realm; + this.tasks = tasks; + } + + public CleanupSummary cleanup() + { + Set successful = new HashSet<>(); + Set unsuccessful = new HashSet<>(); + for (Pair pair : tasks) + { + TimeUUID session = pair.left; + RepairFinishedCompactionTask task = pair.right; + + if (task != null) + { + try + { + task.run(); + successful.add(session); + } + catch (Throwable t) + { + t = task.transaction.abort(t); + logger.error("Failed cleaning up " + session, t); + unsuccessful.add(session); + } + } + else + { + unsuccessful.add(session); + } + } + return new CleanupSummary(realm, successful, unsuccessful); + } + + public Throwable abort(Throwable accumulate) + { + for (Pair pair : tasks) + accumulate = pair.right.transaction.abort(accumulate); + return accumulate; + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionAggregate.java b/src/java/org/apache/cassandra/db/compaction/CompactionAggregate.java new file mode 100644 index 000000000000..82bca520f81e --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionAggregate.java @@ -0,0 +1,1099 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; + +/** + * A compaction aggregate is either a level in {@link LeveledCompactionStrategy} or a tier (bucket) in other + * compaction strategies. + *

+ * It contains a list of {@link CompactionPick}, which are the compactions either in progress or pending. + * It also contains a selected {@link CompactionPick}, which is a compaction about to be submitted. The submitted + * compaction is also part of the compactions. Lastly, it contains a set of all the sstables in this aggregate, + * regardless of whether they need compaction. + */ +public abstract class CompactionAggregate +{ + private static final Logger logger = LoggerFactory.getLogger(CompactionAggregate.class); + + /** The unique key that identifies this aggregate. */ + final Key key; + + /** The sstables in this aggregate, whether they are compaction candidates or not */ + final Set sstables; + + /** The compaction that was selected for this aggregate when it was created. It is also part of {@link this#compactions}. */ + final CompactionPick selected; + + /** The compactions that are part of this aggregate, they could be pending or in progress. */ + final LinkedHashSet compactions; + + CompactionAggregate(Key key, Iterable sstables, CompactionPick selected, Iterable pending) + { + if (sstables == null || selected == null || pending == null) + throw new IllegalArgumentException("Arguments cannot be null"); + + this.key = key; + this.sstables = new HashSet<>(); sstables.forEach(this.sstables::add); + this.selected = selected; + + // Here we want to keep the iteration order since normally pending compactions are ordered by a strategy + // and the selected compaction should be the first one + this.compactions = new LinkedHashSet<>(); + if (!selected.isEmpty()) + compactions.add(selected); + + for (CompactionPick p : pending) + { + if (p == null || p.isEmpty()) + throw new IllegalArgumentException("Pending compactions should be valid compactions"); + + compactions.add(p); + } + } + + public CompactionPick getSelected() + { + return selected; + } + + /** + * @return the total sstable size for all the compaction picks that are either pending or still in progress + */ + public long getPendingBytes() + { + long ret = 0; + for (CompactionPick comp : compactions) + { + if (!comp.submitted()) + ret += comp.totSizeInBytes(); + } + return ret; + } + + /** + * @return compactions that have not yet been submitted (no compaction id). + */ + public List getPending() + { + List ret = new ArrayList<>(compactions.size()); + for (CompactionPick comp : compactions) + { + if (!comp.submitted()) + ret.add(comp); + } + + return ret; + } + + /** + * @return compactions that have already been submitted (compaction id is available) and haven't completed yet + */ + public List getInProgress() + { + List ret = new ArrayList<>(compactions.size()); + for (CompactionPick comp : compactions) + { + if (comp.submitted() && !comp.completed()) + ret.add(comp); + } + + return ret; + } + + /** + * @return all the compactions we have + */ + public List getActive() + { + return new ArrayList<>(compactions); + } + + /** + * @return true if this aggregate has no compactions + */ + public boolean isEmpty() + { + return compactions.isEmpty(); + } + + /** + * Merge the pending compactions and the compactions in progress to create some aggregated statistics. + * + * @return the statistics for this compaction aggregate, see {@link CompactionAggregateStatistics}. + */ + public abstract CompactionAggregateStatistics getStatistics(); + + /** + * Calculates basic compaction statistics, common for all types of {@link CompactionAggregate}s. + * + * @param trackHotness Indicates whether aggregate (tier/bucket) hotness is relevant and should be calculated. + * If this is {@code false}, a default value of {@link Double#NaN} will be used to indicate + * that hotness hasn't been calculated. + * + * @return a new {@link CompactionAggregateStatistics} instance, containing all the common statistics for the + * different types of {@link CompactionAggregate}s (see above for the caveat about hotness). + */ + CompactionAggregateStatistics getCommonStatistics(boolean trackHotness) + { + int numCompactions = 0; + int numCompactionsInProgress = 0; + int numCandidateSSTables = 0; + int numCompactingSSTables = 0; + int numExpiredSSTables = 0; + long tot = 0; + long expiredTot = 0; + double hotness = trackHotness ? 0.0 : Double.NaN; + long read = 0; + long written = 0; + double readThroughput = 0; + double writeThroughput = 0; + + for (CompactionPick compaction : compactions) + { + if (compaction.completed()) + continue; + + numCompactions++; + numCandidateSSTables += compaction.sstables().size(); + numExpiredSSTables += compaction.expired().size(); + tot += compaction.sstables().stream().mapToLong(CompactionSSTable::uncompressedLength).reduce(0L, Long::sum); + expiredTot += compaction.expired().stream().mapToLong(CompactionSSTable::uncompressedLength).reduce(0L, Long::sum); + if (trackHotness) + hotness += compaction.hotness(); + + if (compaction.submitted()) + { + numCompactionsInProgress++; + numCompactingSSTables += compaction.sstables().size(); + } + + if (compaction.inProgress()) + { + final CompactionProgress progress = compaction.progress(); + read += progress.uncompressedBytesRead(); + written += progress.uncompressedBytesWritten(); + readThroughput += progress.readThroughput(); + writeThroughput += progress.writeThroughput(); + } + } + + return new CompactionAggregateStatistics(numCompactions, + numCompactionsInProgress, + sstables.size(), + numExpiredSSTables, + numCandidateSSTables, + numCompactingSSTables, + getTotSizeBytes(sstables), + tot, + expiredTot, + read, + written, + readThroughput, + writeThroughput, + hotness); + } + + /** + * @return the number of estimated compactions that are still pending. + */ + public int numEstimatedCompactions() + { + return getPending().size(); + } + + /** + * @return a key that ensures the uniqueness of an aggregate but also that allows identify future identical aggregates, + * e.g. when an aggregate is merged with an older aggregate that has still ongoing compactions like a level + * in LCS or a bucket in the unified strategy or STCS or a time window in TWCS + */ + public Key getKey() + { + return key; + } + + /** + * Return a matching aggregate from the map passed in or null. Normally this is just a matter of finding + * the key in the map but for STCS we need to look at the possible min and maximum average sizes and so + * {@link SizeTiered} overrides this method. + * + * @param others a map of other aggregates + * + * @return an aggregate with the same key or null + */ + @Nullable CompactionAggregate getMatching(NavigableMap others) + { + return others.get(getKey()); + } + + /** + * Create a copy of this aggregate with the new parameters + * + * @return a deep copy of this aggregate + */ + protected abstract CompactionAggregate clone(Iterable sstables, CompactionPick selected, Iterable compactions); + + /** + * Add expired sstables to the selected compaction pick and return a new compaction aggregate. + */ + CompactionAggregate withExpired(Collection expired) + { + return clone(Iterables.concat(sstables, expired), selected.withExpiredSSTables(expired), compactions); + } + + /** + * Check if this aggregate compactions contain the compaction passed in. Here we're looking for + * the exact same instance, not just a compaction that is equal to it. + * + * @param compaction the compaction to check if it can be found + * + * @return a pair containing the result on the left (true if the compaction is found, false otherwise), and + * a matching compaction on the right (any compaction that is equal, including the same instance). + */ + public Pair containsSameInstance(CompactionPick compaction) + { + List activeCompactions = getActive(); + int existingCompactionIdx = activeCompactions.indexOf(compaction); + CompactionPick existingCompaction = existingCompactionIdx == -1 ? null : activeCompactions.get(existingCompactionIdx); + boolean containsSameInstance = existingCompaction != null && existingCompaction == compaction; + return Pair.create(containsSameInstance, existingCompaction); + } + + /** + * Replace an existing compaction pick with a new one, this is used by CNDB because it creates new + * compactions from etcd. If the existing compaction is null, simply add the replacement. + */ + public CompactionAggregate withReplacedCompaction(CompactionPick replacement, @Nullable CompactionPick existing) + { + Preconditions.checkArgument(existing == null || this.compactions.contains(existing), "Expected existing to be part of compactions"); + if (existing == null) + return withAdditionalCompactions(ImmutableList.of(replacement)); + + List sstables = new ArrayList<>(this.sstables.size()); + LinkedHashSet compactions = new LinkedHashSet<>(this.compactions.size()); + for (CompactionPick comp : this.compactions) + { + if (comp == existing) + { + compactions.add(replacement); + sstables.addAll(replacement.sstables()); + } + else + { + compactions.add(comp); + sstables.addAll(comp.sstables()); + } + } + + return clone(sstables, existing == selected ? replacement : selected, compactions); + } + + /** + * Add existing compactions to our own compactions and return a new compaction aggregate + */ + public CompactionAggregate withAdditionalCompactions(Collection comps) + { + List added = comps.stream().flatMap(comp -> comp.sstables().stream()).collect(Collectors.toList()); + return clone(Iterables.concat(sstables, added), selected, Iterables.concat(compactions, comps)); + } + + /** + * Only keep the compactions passed in, strip everything else. + */ + public CompactionAggregate withOnlyTheseCompactions(Collection comps) + { + List retained = comps.stream().flatMap(comp -> comp.sstables().stream()).collect(Collectors.toList()); + return clone(retained, CompactionPick.EMPTY, comps); + } + + /** + * Merge an aggregate with another one with the same key. + */ + protected CompactionAggregate mergeWith(CompactionAggregate other) + { + return withAdditionalCompactions(other.compactions); + } + + @Override + public int hashCode() + { + return Objects.hash(sstables, selected, compactions); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + + if (!(obj instanceof CompactionAggregate)) + return false; + + CompactionAggregate that = (CompactionAggregate) obj; + return sstables.equals(that.sstables) && + selected.equals(that.selected) && + compactions.equals(that.compactions); + } + + /** + * Contains information about a levelled compaction aggregate, this is equivalent to a level in {@link LeveledCompactionStrategy}. + */ + public static final class Leveled extends CompactionAggregate + { + /** The current level number */ + final int level; + + /** The next level number */ + final int nextLevel; + + /** The score of this level as defined in {@link LeveledCompactionStrategy}. */ + final double score; + + /** The maximum size of each output sstable that will be produced by compaction, Long.MAX_VALUE if no maximum exists */ + final long maxSSTableBytes; + + /** + * How many more compactions this level is expected to perform. This is required because for LCS we cannot + * easily identify candidate sstables to put into the pending picks. + */ + final int pendingCompactions; + + /** The fanout size */ + final int fanout; + + Leveled(Iterable sstables, + CompactionPick selected, + Iterable compactions, + int level, + int nextLevel, + double score, + long maxSSTableBytes, + int pendingCompactions, + int fanout) + { + super(new Key(level), sstables, selected, compactions); + + this.level = level; + this.nextLevel = nextLevel; + this.score = score; + this.maxSSTableBytes = maxSSTableBytes; + this.pendingCompactions = pendingCompactions; + this.fanout = fanout; + } + + @Override + protected CompactionAggregate clone(Iterable sstables, CompactionPick selected, Iterable compactions) + { + return new Leveled(sstables, selected, compactions, level, nextLevel, score, maxSSTableBytes, pendingCompactions, fanout); + } + + @Override + public CompactionAggregateStatistics getStatistics() + { + CompactionAggregateStatistics stats = getCommonStatistics(false); + + long readLevel = 0L; + + for (CompactionPick compaction : compactions) + if (!compaction.completed() && compaction.inProgress()) + readLevel += compaction.progress().uncompressedBytesRead(level); + + return new LeveledCompactionStatistics(stats, level, score, pendingCompactions, readLevel); + } + + @Override + public int numEstimatedCompactions() + { + return pendingCompactions; + } + + @Override + public boolean isEmpty() + { + return super.isEmpty() && pendingCompactions == 0; + } + + @Override + public String toString() + { + return String.format("Level %d with %d sstables, %d compactions and %d pending", level, sstables.size(), compactions.size(), pendingCompactions); + } + } + + /** + * Create a level where we have a compaction candidate. + */ + static CompactionAggregate.Leveled createLeveled(Collection all, + Collection candidates, + int pendingCompactions, + long maxSSTableBytes, + int level, + int nextLevel, + double score, + int fanout) + { + return new Leveled(all, + CompactionPick.create(level, candidates), + ImmutableList.of(), + level, + nextLevel, + score, + maxSSTableBytes, + pendingCompactions, + fanout); + } + + /** + * Create a level when we only have estimated tasks. + */ + static CompactionAggregate.Leveled createLeveled(Collection all, + int pendingCompactions, + long maxSSTableBytes, + int level, + double score, + int fanout) + { + return new Leveled(all, + CompactionPick.EMPTY, + ImmutableList.of(), + level, + level + 1, + score, + maxSSTableBytes, + pendingCompactions, + fanout); + } + + /** + * Create a leveled aggregate when LCS is doing STCS on level 0 + */ + static CompactionAggregate.Leveled createLeveledForSTCS(Collection all, + CompactionPick pick, + int pendingCompactions, + double score, + int fanout) + { + return new Leveled(all, + pick, + ImmutableList.of(), + 0, + 0, + score, + Long.MAX_VALUE, + pendingCompactions, + fanout); + } + + /** + * Contains information about a size-tiered compaction aggregate, this is equivalent to a bucket in {@link SizeTieredCompactionStrategy}. + */ + public static final class SizeTiered extends CompactionAggregate + { + /** The total read hotness of the sstables in this tier, as defined by {@link CompactionSSTable#hotness()} */ + final double hotness; + + /** The average on disk size in bytes of the sstables in this tier */ + final long avgSizeBytes; + + /** The minimum on disk size in bytes for this tier, this is normally the avg size times the STCS bucket low and it is + * used to find compacting aggregates that are on the same tier. */ + final long minSizeBytes; + + /** The maximum on disk size in bytes for this tier, this is normally the avg size times the STCS bucket high and it is + * used to find compacting aggregates that are on the same tier. */ + final long maxSizeBytes; + + SizeTiered(Iterable sstables, + CompactionPick selected, + Iterable pending, + double hotness, + long avgSizeBytes, + long minSizeBytes, + long maxSizeBytes) + { + super(new Key(avgSizeBytes), sstables, selected, pending); + + this.hotness = hotness; + this.avgSizeBytes = avgSizeBytes; + this.minSizeBytes = minSizeBytes; + this.maxSizeBytes = maxSizeBytes; + } + + @Override + protected CompactionAggregate clone(Iterable sstables, CompactionPick selected, Iterable compactions) + { + return new SizeTiered(sstables, selected, compactions, getTotHotness(sstables), getAvgSizeBytes(sstables), minSizeBytes, maxSizeBytes); + } + + @Override + public CompactionAggregateStatistics getStatistics() + { + CompactionAggregateStatistics stats = getCommonStatistics(true); + + return new SizeTieredCompactionStatistics(stats, avgSizeBytes); + } + + @Override + @Nullable CompactionAggregate getMatching(NavigableMap others) + { + SortedMap subMap = others.subMap(new Key(minSizeBytes), new Key(maxSizeBytes)); + if (subMap.isEmpty()) + { + if (logger.isTraceEnabled()) + logger.trace("Found no matching aggregate for {}", + FBUtilities.prettyPrintMemory(avgSizeBytes)); + + return null; + } + + if (logger.isTraceEnabled()) + logger.trace("Found {} matching aggregates for {}", + subMap.size(), + FBUtilities.prettyPrintMemory(avgSizeBytes)); + + Key closest = null; + long minDiff = 0; + for (Key m : subMap.keySet()) + { + long diff = Math.abs(m.index - avgSizeBytes); + if (closest == null || diff < minDiff) + { + closest = m; + minDiff = diff; + } + } + + if (logger.isTraceEnabled()) + logger.trace("Using closest matching aggregate for {}: {}", + FBUtilities.prettyPrintMemory(avgSizeBytes), + FBUtilities.prettyPrintMemory(closest != null ? closest.index : -1)); + + return others.get(closest); + } + + @Override + public String toString() + { + return String.format("Size tiered %s/%s/%s with %d sstables, %d compactions", + FBUtilities.prettyPrintMemory(minSizeBytes), + FBUtilities.prettyPrintMemory(avgSizeBytes), + FBUtilities.prettyPrintMemory(maxSizeBytes), + sstables.size(), + compactions.size()); + } + } + + static CompactionAggregate createSizeTiered(Collection all, + CompactionPick selected, + List pending, + double hotness, + long avgSizeBytes, + long minSizeBytes, + long maxSizeBytes) + { + return new SizeTiered(all, selected, pending, hotness, avgSizeBytes, minSizeBytes, maxSizeBytes); + } + + /** + * Contains information about a size-tiered compaction aggregate, this is equivalent to a bucket in {@link SizeTieredCompactionStrategy}. + */ + public static final class TimeTiered extends CompactionAggregate + { + /** The timestamp of this aggregate */ + final long timestamp; + + TimeTiered(Iterable sstables, CompactionPick selected, Iterable pending, long timestamp) + { + super(new Key(timestamp), sstables, selected, pending); + this.timestamp = timestamp; + } + + @Override + protected CompactionAggregate clone(Iterable sstables, CompactionPick selected, Iterable compactions) + { + return new TimeTiered(sstables, selected, compactions, timestamp); + } + + @Override + public CompactionAggregateStatistics getStatistics() + { + CompactionAggregateStatistics stats = getCommonStatistics(true); + return new TimeTieredCompactionStatistics(stats, timestamp); + } + + @Override + public String toString() + { + return String.format("Time tiered %d with %d sstables, %d compactions", timestamp, sstables.size(), compactions.size()); + } + } + + static CompactionAggregate createTimeTiered(Collection sstables, long timestamp) + { + return new TimeTiered(sstables, CompactionPick.create(timestamp, sstables), ImmutableList.of(), timestamp); + } + + static CompactionAggregate createTimeTiered(Collection sstables, CompactionPick selected, List pending, long timestamp) + { + return new TimeTiered(sstables, selected, pending, timestamp); + } + + public static class UnifiedAggregate extends CompactionAggregate + { + /** The arena to which this level belongs */ + private final UnifiedCompactionStrategy.Arena arena; + + /** The level generated by the compaction strategy */ + private final UnifiedCompactionStrategy.Level level; + + private UnifiedCompactionStrategy.ShardingStats shardingStats; + + /** The maximum number of overlapping sstables in the level. */ + private final int maxOverlap; + + private int permittedParallelism; + + UnifiedAggregate(Iterable sstables, + int maxOverlap, + CompactionPick selected, + Iterable pending, + UnifiedCompactionStrategy.Arena arena, + UnifiedCompactionStrategy.Level level) + { + super(new ArenaedKey(arena, level.index), sstables, selected, pending); + this.maxOverlap = maxOverlap; + this.arena = arena; + this.level = level; + } + + public UnifiedCompactionStrategy.Arena getArena() + { + return arena; + } + + public void setShardingStats(UnifiedCompactionStrategy.ShardingStats shardingStats) + { + assert this.shardingStats == null; + this.shardingStats = shardingStats; + } + + public UnifiedCompactionStrategy.ShardingStats getShardingStats() + { + return shardingStats; + } + + @Override + public CompactionAggregateStatistics getStatistics() + { + CompactionAggregateStatistics stats = getCommonStatistics(false); + + return new UnifiedCompactionStatistics(stats, + level.index, + level.survivalFactor, + level.scalingParameter, + level.min, + level.max, + maxOverlap, + arena.name()); + } + + @Override + protected CompactionAggregate clone(Iterable sstables, CompactionPick selected, Iterable compactions) + { + return new UnifiedAggregate(sstables, maxOverlap, selected, compactions, arena, level); + } + + @Override + protected CompactionAggregate mergeWith(CompactionAggregate other) + { + return new UnifiedAggregate(Iterables.concat(sstables, other.sstables), + Math.max(maxOverlap, ((UnifiedAggregate) other).maxOverlap), + selected, + Iterables.concat(compactions, other.compactions), + arena, + level); + } + + public int bucketIndex() + { + return level.index; + } + + // used by CNDB, "bucket" name is historical + public double bucketMin() + { + return level.min; + } + + // used by CNDB, "bucket" name is historical + public double bucketMax() + { + return level.max; + } + + public int maxOverlap() + { + return maxOverlap; + } + + @Override + public String toString() + { + return String.format("Unified arena %s level %d with %d sstables (max overlap %d) and %d compactions", + arena.name(), + level.index, + sstables.size(), + maxOverlap, + compactions.size()); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + + if (!(obj instanceof UnifiedAggregate)) + return false; + + UnifiedAggregate that = (UnifiedAggregate) obj; + return sstables.equals(that.sstables) && + selected.equals(that.selected) && + compactions.equals(that.compactions) && + level.equals(that.level) && + arena.equals(that.arena); + // no need to compare maxOverlap, that's a feature of sstables + } + + @Override + public int hashCode() + { + return Objects.hash(sstables, selected, compactions, level, arena); + } + + public Range operationRange() + { + return null; + } + + public boolean keepOriginals() + { + return false; + } + + public void setPermittedParallelism(int parallelism) + { + this.permittedParallelism = parallelism; + } + + public int getPermittedParallelism() + { + return permittedParallelism; + } + } + + /** + * A unified compaction aggregate for compaction over a specified subrange of the given sources. This would be a + * part of a larger composite transaction over the same inputs sstables, thus a ranged aggregate's tasks cannot + * delete any of the input sstables, which needs to be done in addition to the execution of this aggregate. + * The intended use of this is to parallelize compactions over multiple nodes in CNDB. + * See RangedAggregatesTest for an example of how this would be used. + */ + public static class UnifiedWithRange extends UnifiedAggregate + { + private final Range operationRange; + + UnifiedWithRange(Iterable sstables, + int maxOverlap, + CompactionPick selected, + Iterable pending, + UnifiedCompactionStrategy.Arena arena, + UnifiedCompactionStrategy.Level level, + int permittedParallelism, + Range operationRange) + { + super(sstables, maxOverlap, selected, pending, arena, level); + this.operationRange = operationRange; + setPermittedParallelism(permittedParallelism); + } + + @Override + public Range operationRange() + { + return operationRange; + } + + @Override + public boolean keepOriginals() + { + return true; // if an aggregate is partial, the sources cannot be deleted as they are needed for the other parts + } + + @Override + public String toString() + { + return super.toString() + " range " + operationRange; + } + } + + public static UnifiedAggregate createUnified(Collection sstables, + int maxOverlap, + CompactionPick selected, + Iterable pending, + UnifiedCompactionStrategy.Arena arena, + UnifiedCompactionStrategy.Level level) + { + return new UnifiedAggregate(sstables, maxOverlap, selected, pending, arena, level); + } + + /** + * Create a ranged portion of the specified aggregate. To be used by CNDB to split compaction over nodes. + */ + public static UnifiedAggregate createUnifiedWithRange(UnifiedAggregate base, + Collection rangeSSTables, + Range range, + int permittedParallelism) + { + return new UnifiedWithRange(rangeSSTables, + base.maxOverlap, + CompactionPick.create(base.bucketIndex(), rangeSSTables), + Collections.emptySet(), + base.arena, + base.level, + permittedParallelism, + range); + } + + + + /** An aggregate that is created for a compaction issued only to drop tombstones */ + public static final class TombstoneAggregate extends CompactionAggregate + { + TombstoneAggregate(Iterable sstables, CompactionPick selected, Iterable pending) + { + super(new Key(-1), sstables, selected, pending); + } + + @Override + protected CompactionAggregate clone(Iterable sstables, CompactionPick selected, Iterable compactions) + { + return new TombstoneAggregate(sstables, selected, compactions); + } + + @Override + public CompactionAggregateStatistics getStatistics() + { + return getCommonStatistics(false); + } + + @Override + public String toString() + { + return String.format("Tombstones with %d sstables, %d compactions", sstables.size(), compactions.size()); + } + } + + static CompactionAggregate createForTombstones(CompactionSSTable sstable) + { + List sstables = ImmutableList.of(sstable); + CompactionPick comp = CompactionPick.create(-1, sstables); + return new TombstoneAggregate(sstables, comp, ImmutableList.of()); + } + + /** + * A key suitable for a strategy that has no arenas, that is a legacy strategy that is + * managed by CompactionStrategyManager. + */ + public static class Key implements Comparable + { + protected final long index; + + Key(long index) + { + this.index = index; + } + + @Override + public int compareTo(Key key) + { + return Long.compare(index, key.index); + } + + @Override + public String toString() + { + return Long.toString(index); + } + } + + /** + * A key suitable for a strategy using arenas, first it compares by arena, and then by level index. + */ + private static final class ArenaedKey extends Key + { + private final UnifiedCompactionStrategy.Arena arena; + + ArenaedKey(UnifiedCompactionStrategy.Arena arena, long index) + { + super(index); + this.arena = arena; + } + + @Override + public int compareTo(Key key) + { + if (key instanceof ArenaedKey) + { + ArenaedKey arenaedKey = (ArenaedKey) key; + + int ret = arena.compareTo(arenaedKey.arena); + if (ret != 0) + return ret; + } + + // either not arenaed or same arena + return Long.compare(index, key.index); + } + + @Override + public String toString() + { + return index + "-" + arena; + } + } + + /** + * Return the compaction statistics for this strategy and list of compactions that are either pending or in progress. + * + * @param aggregates the compaction aggregates + * + * @return the statistics about this compactions + */ + static CompactionStrategyStatistics getStatistics(TableMetadata metadata, + CompactionStrategy strategy, + Collection aggregates) + { + List statistics = new ArrayList<>(aggregates.size()); + + for (CompactionAggregate aggregate : aggregates) + statistics.add(aggregate.getStatistics()); + + return new CompactionStrategyStatistics(metadata, strategy.getClass().getSimpleName(), statistics); + } + + /** + * Return the number of compactions that are still pending; + * @param aggregates the compaction aggregates + * + * @return the number of compactions that are still pending (net yet submitted) + */ + static int numEstimatedCompactions(Collection aggregates) + { + int ret = 0; + for (CompactionAggregate aggregate : aggregates) + ret += aggregate.numEstimatedCompactions(); + + return ret; + } + + /** + * Given a sorted list of compactions, return the first selected pick. + * + * @param aggregates a sorted list of compaction aggregates from most interesting to least interesting, some may be empty + * + * @return the compaction pick of the first aggregate + */ + static CompactionPick getSelected(List aggregates) + { + return aggregates.isEmpty() ? CompactionPick.EMPTY : aggregates.get(0).getSelected(); + } + + /** + * Given a list of sstables, return their average size on disk. + * + * @param sstables the sstables + * @return average sstable size on disk or zero. + */ + static long getAvgSizeBytes(Iterable sstables) + { + long ret = 0; + long num = 0; + for (CompactionSSTable sstable : sstables) + { + ret += sstable.onDiskLength(); + num++; + } + + return num > 0 ? ret / num : 0; + } + + /** + * Given a list of sstables, return their total size on disk. + * + * @param sstables the sstables + * @return total sstable size on disk or zero. + */ + static long getTotSizeBytes(Iterable sstables) + { + long ret = 0; + for (CompactionSSTable sstable : sstables) + ret += sstable.onDiskLength(); + + return ret; + } + + /** + * Given a list of sstables, return their total read hotness. + * + * @param sstables the sstables + * @return total read hotness or zero. + */ + static double getTotHotness(Iterable sstables) + { + double ret = 0; + for (CompactionSSTable sstable : sstables) + ret += sstable.hotness(); + + return ret; + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionAggregateStatistics.java b/src/java/org/apache/cassandra/db/compaction/CompactionAggregateStatistics.java new file mode 100644 index 000000000000..35ca5d89873f --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionAggregateStatistics.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.io.Serializable; +import java.util.Collection; + +import com.google.common.collect.ImmutableList; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory; +import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemoryPerSecond; + +/** + * The statistics for a {@link CompactionAggregate}. + *

+ * It must be serializable for JMX and convertible to JSON for insights. The JSON + * properties are published to insights so changing them has a downstream impact. + */ +public class CompactionAggregateStatistics implements Serializable +{ + public static final String NO_SHARD = ""; + + protected static final Collection HEADER = ImmutableList.of("Tot. SSTables", + "Tot. size (bytes)", + "Compactions", + "Comp. SSTables", + "Read (bytes/sec)", + "Write (bytes/sec)", + "Tot. comp. size/Read/Written (bytes)"); + /** The number of compactions that are either pending or in progress */ + protected final int numCompactions; + + /** The number of compactions that are in progress */ + protected final int numCompactionsInProgress; + + /** The total number of sstables, whether they need compacting or not */ + protected final int numSSTables; + + /** The total number of expired sstables */ + protected final int numExpiredSSTables; + + /** The number of sstables that are compaction candidates */ + protected final int numCandidateSSTables; + + /** The number of sstables that are currently compacting */ + protected final int numCompactingSSTables; + + /** The size in bytes (on disk) of the total sstables */ + protected final long sizeInBytes; + + /** The total uncompressed size of the sstables selected for compaction */ + protected final long totBytesToCompact; + + /** The total uncompressed size of the expired sstables that are going to be dropped during compaction */ + protected final long totalBytesToDrop; + + /** The number of bytes read so far for the compactions here - read throughput is calculated based on this */ + protected final long readBytes; + + /** The number of bytes written so far for the compaction here - write throughput is calculated based on this */ + protected final long writtenBytes; + + /** The read throughput in bytes per second */ + protected final double readThroughput; + + /** The write throughput in bytes per second */ + protected final double writeThroughput; + + /** The hotness of this aggregate (where applicable) */ + protected final double hotness; + + CompactionAggregateStatistics(int numCompactions, + int numCompactionsInProgress, + int numSSTables, + int numExpiredSSTables, + int numCandidateSSTables, + int numCompactingSSTables, + long sizeInBytes, + long totBytesToCompact, + long totBytesToDrop, + long readBytes, + long writtenBytes, + double readThroughput, + double writeThroughput, + double hotness) + { + this.numCompactions = numCompactions; + this.numCompactionsInProgress = numCompactionsInProgress; + this.numCandidateSSTables = numCandidateSSTables; + this.numCompactingSSTables = numCompactingSSTables; + this.numSSTables = numSSTables; + this.numExpiredSSTables = numExpiredSSTables; + this.sizeInBytes = sizeInBytes; + this.totBytesToCompact = totBytesToCompact; + this.totalBytesToDrop = totBytesToDrop; + this.readBytes = readBytes; + this.writtenBytes = writtenBytes; + this.readThroughput = readThroughput; + this.writeThroughput = writeThroughput; + this.hotness = hotness; + } + + CompactionAggregateStatistics(CompactionAggregateStatistics base) + { + this.numCompactions = base.numCompactions; + this.numCompactionsInProgress = base.numCompactionsInProgress; + this.numCandidateSSTables = base.numCandidateSSTables; + this.numCompactingSSTables = base.numCompactingSSTables; + this.numExpiredSSTables = base.numExpiredSSTables; + this.numSSTables = base.numSSTables; + this.sizeInBytes = base.sizeInBytes; + this.totBytesToCompact = base.totBytesToCompact; + this.totalBytesToDrop = base.totalBytesToDrop; + this.readBytes = base.readBytes; + this.writtenBytes = base.writtenBytes; + this.readThroughput = base.readThroughput; + this.writeThroughput = base.writeThroughput; + this.hotness = base.hotness; + } + + /** The number of compactions that are either pending or in progress */ + @JsonProperty + public int numCompactions() + { + return numCompactions; + } + + /** The number of compactions that are in progress */ + @JsonProperty + public int numCompactionsInProgress() + { + return numCompactionsInProgress; + } + + /** The total number of sstables, whether they need compacting or not */ + @JsonProperty + public int numSSTables() + { + return numSSTables; + } + + /** The number of sstables that are part of this level */ + @JsonProperty + public int numCandidateSSTables() + { + return numCandidateSSTables; + } + + /** The number of sstables that are currently part of a compaction operation */ + @JsonProperty + public int numCompactingSSTables() + { + return numCompactingSSTables; + } + + /** The size in bytes (on disk) of the total sstables */ + public long sizeInBytes() + { + return sizeInBytes; + } + + /** The read throughput in bytes per second */ + @JsonProperty + public double readThroughput() + { + return readThroughput; + } + + /** The write throughput in bytes per second */ + @JsonProperty + public double writeThroughput() + { + return writeThroughput; + } + + /** The total uncompressed size of the sstables selected for compaction */ + @JsonProperty + public long tot() + { + return totBytesToCompact; + } + + /** The number of bytes read so far for the compactions here - read throughput is calculated based on this */ + @JsonProperty + public long read() + { + return readBytes; + } + + /** The number of bytes written so far for the compaction here - write throughput is calculated based on this */ + @JsonProperty + public long written() + { + return writtenBytes; + } + + /** The hotness of this aggregate (where applicable) */ + @JsonProperty + public double hotness() + { + return hotness; + } + + /** The name of the shard, empty if the compaction is not sharded (the default). */ + @JsonProperty + public String shard() + { + return NO_SHARD; + } + + @Override + public String toString() + { + return data().toString(); + } + + protected Collection header() + { + return HEADER; + } + + protected Collection data() + { + return ImmutableList.of(Integer.toString(numSSTables), + prettyPrintMemory(sizeInBytes), + Integer.toString(numCompactions()) + '/' + numCompactionsInProgress(), + Integer.toString(numCandidateSSTables()) + '/' + numCompactingSSTables(), + prettyPrintMemoryPerSecond((long) readThroughput()), + prettyPrintMemoryPerSecond((long) writeThroughput()), + prettyPrintMemory(totBytesToCompact) + '/' + prettyPrintMemory(readBytes) + '/' + prettyPrintMemory(writtenBytes)); + } + + protected String toString(long value) + { + return FBUtilities.prettyPrintMemory(value); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionController.java b/src/java/org/apache/cassandra/db/compaction/CompactionController.java index a9fcad73c971..872d7fb405cd 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionController.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionController.java @@ -17,37 +17,35 @@ */ package org.apache.cassandra.db.compaction; -import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.LongPredicate; +import java.util.function.UnaryOperator; + +import javax.annotation.Nullable; -import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.RateLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.AbstractCompactionController; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.schema.CompactionParams.TombstoneOption; -import org.apache.cassandra.utils.OverlapIterator; -import org.apache.cassandra.utils.concurrent.Refs; +import org.apache.cassandra.utils.concurrent.OpOrder; import static org.apache.cassandra.config.CassandraRelevantProperties.NEVER_PURGE_TOMBSTONES; -import static org.apache.cassandra.db.lifecycle.SSTableIntervalTree.buildIntervals; /** * Manage compaction options. @@ -58,79 +56,81 @@ public class CompactionController extends AbstractCompactionController static final boolean NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE = NEVER_PURGE_TOMBSTONES.getBoolean(); private final boolean compactingRepaired; - // note that overlapIterator and overlappingSSTables will be null if NEVER_PURGE_TOMBSTONES is set - this is a + // note that overlapTracker will be null if NEVER_PURGE_TOMBSTONES is set - this is a // good thing so that noone starts using them and thinks that if overlappingSSTables is empty, there // is no overlap. - private Refs overlappingSSTables; - private OverlapIterator overlapIterator; + @Nullable + private final CompactionRealm.OverlapTracker overlapTracker; + @Nullable private final Iterable compacting; + @Nullable private final RateLimiter limiter; private final long minTimestamp; - final Map openDataFiles = new HashMap<>(); + private final Map openDataFiles = new HashMap<>(); - protected CompactionController(ColumnFamilyStore cfs, long maxValue) + protected CompactionController(CompactionRealm realm, long maxValue) { - this(cfs, null, maxValue); + this(realm, null, maxValue); } - public CompactionController(ColumnFamilyStore cfs, Set compacting, long gcBefore) + public CompactionController(CompactionRealm realm, Set compacting, long gcBefore) { - this(cfs, compacting, gcBefore, null, - cfs.getCompactionStrategyManager().getCompactionParams().tombstoneOption()); + this(realm, compacting, gcBefore, null, realm.getCompactionParams().tombstoneOption()); } - public CompactionController(ColumnFamilyStore cfs, Set compacting, long gcBefore, RateLimiter limiter, TombstoneOption tombstoneOption) + public CompactionController(CompactionRealm realm, Set compacting, long gcBefore, RateLimiter limiter, TombstoneOption tombstoneOption) { //When making changes to the method, be aware that some of the state of the controller may still be uninitialized //(e.g. TWCS sets up the value of ignoreOverlaps() after this completes) - super(cfs, gcBefore, tombstoneOption); + super(realm, gcBefore, tombstoneOption); this.compacting = compacting; this.limiter = limiter; compactingRepaired = compacting != null && compacting.stream().allMatch(SSTableReader::isRepaired); this.minTimestamp = compacting != null && !compacting.isEmpty() // check needed for test ? compacting.stream().mapToLong(SSTableReader::getMinTimestamp).min().getAsLong() : 0; - refreshOverlaps(); - if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE) - logger.warn("You are running with -D{}=true, this is dangerous!", NEVER_PURGE_TOMBSTONES.getKey()); + + if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || realm.getNeverPurgeTombstones()) + { + overlapTracker = null; + if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE) + logger.warn("You are running with -D{}=true, this is dangerous!", NEVER_PURGE_TOMBSTONES.getKey()); + else + logger.debug("Not using overlaps for {}.{} - neverPurgeTombstones is enabled", realm.getKeyspaceName(), realm.getTableName()); + } + else + overlapTracker = realm.getOverlapTracker(compacting); + + logger.debug("Compaction controller created for {} with {} compacting sstables, {} overlapping sstables, tsOption={}, compactingRepaired={}", + realm.metadata(), compacting == null ? 0 : compacting.size(), overlapTracker == null ? 0 : overlapTracker.overlaps().size(), tombstoneOption, compactingRepaired()); } public void maybeRefreshOverlaps() { - if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE) - { - logger.debug("not refreshing overlaps - running with -D{}=true", NEVER_PURGE_TOMBSTONES.getKey()); - return; - } + if (overlapTracker != null && overlapTracker.maybeRefresh()) + closeDataFiles(); + } - if (cfs.getNeverPurgeTombstones()) + public void refreshOverlaps() + { + if (overlapTracker != null) { - logger.debug("not refreshing overlaps for {}.{} - neverPurgeTombstones is enabled", cfs.getKeyspaceName(), cfs.getTableName()); - return; + overlapTracker.refreshOverlaps(); + closeDataFiles(); } - - if (overlappingSSTables == null || overlappingSSTables.stream().anyMatch(SSTableReader::isMarkedCompacted)) - refreshOverlaps(); } - void refreshOverlaps() + void closeDataFiles() { - if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || cfs.getNeverPurgeTombstones()) - return; - - if (this.overlappingSSTables != null) - close(); - - if (compacting == null) - overlappingSSTables = Refs.tryRef(Collections.emptyList()); - else - overlappingSSTables = cfs.getAndReferenceOverlappingLiveSSTables(compacting); - this.overlapIterator = new OverlapIterator<>(buildIntervals(overlappingSSTables)); + FileUtils.closeQuietly(openDataFiles.values()); + openDataFiles.clear(); } - public Set getFullyExpiredSSTables() + public Set getFullyExpiredSSTables() { - return getFullyExpiredSSTables(cfs, compacting, overlappingSSTables, gcBefore, ignoreOverlaps()); + if (overlapTracker == null) + return Collections.emptySet(); + return getFullyExpiredSSTables(realm, compacting, c -> overlapTracker.overlaps(), gcBefore, ignoreOverlaps()); } /** @@ -143,95 +143,93 @@ public Set getFullyExpiredSSTables() * - if not droppable, remove from candidates * 4. return candidates. * - * @param cfStore + * @param realm * @param compacting we take the drop-candidates from this set, it is usually the sstables included in the compaction - * @param overlapping the sstables that overlap the ones in compacting. + * @param overlappingSupplier called on the compacting sstables to compute the set of sstables that overlap with them if needed * @param gcBefore * @param ignoreOverlaps don't check if data shadows/overlaps any data in other sstables * @return */ - public static Set getFullyExpiredSSTables(ColumnFamilyStore cfStore, - Iterable compacting, - Iterable overlapping, - long gcBefore, - boolean ignoreOverlaps) + public static + Set getFullyExpiredSSTables(CompactionRealm realm, + Iterable compacting, + UnaryOperator> overlappingSupplier, + long gcBefore, + boolean ignoreOverlaps) { - logger.trace("Checking droppable sstables in {}", cfStore); + logger.trace("Checking droppable sstables in {}", realm); - if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || compacting == null || cfStore.getNeverPurgeTombstones() || overlapping == null) + if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || compacting == null || realm.getNeverPurgeTombstones()) return Collections.emptySet(); - if (cfStore.getCompactionStrategyManager().onlyPurgeRepairedTombstones() && !Iterables.all(compacting, SSTableReader::isRepaired)) + if (realm.onlyPurgeRepairedTombstones() && !Iterables.all(compacting, CompactionSSTable::isRepaired)) return Collections.emptySet(); - if (ignoreOverlaps) + long minTimestamp; + if (!ignoreOverlaps) { - Set fullyExpired = new HashSet<>(); - for (SSTableReader candidate : compacting) - { - if (candidate.getMaxLocalDeletionTime() < gcBefore) - { - fullyExpired.add(candidate); - logger.trace("Dropping overlap ignored expired SSTable {} (maxLocalDeletionTime={}, gcBefore={})", - candidate, candidate.getMaxLocalDeletionTime(), gcBefore); - } - } - return fullyExpired; + var overlapping = overlappingSupplier.apply(compacting); + minTimestamp = Math.min(Math.min(minSurvivingTimestamp(overlapping, gcBefore), + minSurvivingTimestamp(compacting, gcBefore)), + minTimestamp(realm.getAllMemtables())); } - - List candidates = new ArrayList<>(); - long minTimestamp = Long.MAX_VALUE; - - for (SSTableReader sstable : overlapping) + else { - // Overlapping might include fully expired sstables. What we care about here is - // the min timestamp of the overlapping sstables that actually contain live data. - if (sstable.getMaxLocalDeletionTime() >= gcBefore) - minTimestamp = Math.min(minTimestamp, sstable.getMinTimestamp()); + minTimestamp = Long.MAX_VALUE; } - for (SSTableReader candidate : compacting) + // At this point, minTimestamp denotes the lowest timestamp of any relevant + // SSTable or Memtable that contains a constructive value. Any compacting sstable with only expired content that + // also has (getMaxTimestamp() < minTimestamp) serves no purpose anymore. + + Set expired = new HashSet<>(); + for (CompactionSSTable candidate : compacting) { - if (candidate.getMaxLocalDeletionTime() < gcBefore) - candidates.add(candidate); - else - minTimestamp = Math.min(minTimestamp, candidate.getMinTimestamp()); + if (candidate.getMaxLocalDeletionTime() < gcBefore && + candidate.getMaxTimestamp() < minTimestamp) + { + logger.trace("Dropping {}expired SSTable {} (maxLocalDeletionTime={}, gcBefore={})", + ignoreOverlaps ? "overlap ignored " : "", + candidate, candidate.getMaxLocalDeletionTime(), gcBefore); + expired.add(candidate); + } } + return expired; + } - for (Memtable memtable : cfStore.getTracker().getView().getAllMemtables()) + private static long minTimestamp(Iterable memtables) + { + long minTimestamp = Long.MAX_VALUE; + for (Memtable memtable : memtables) { if (memtable.getMinTimestamp() != Memtable.NO_MIN_TIMESTAMP) minTimestamp = Math.min(minTimestamp, memtable.getMinTimestamp()); } + return minTimestamp; + } - // At this point, minTimestamp denotes the lowest timestamp of any relevant - // SSTable or Memtable that contains a constructive value. candidates contains all the - // candidates with no constructive values. The ones out of these that have - // (getMaxTimestamp() < minTimestamp) serve no purpose anymore. - - Iterator iterator = candidates.iterator(); - while (iterator.hasNext()) + private static long minSurvivingTimestamp(Iterable ssTables, + long gcBefore) + { + long minTimestamp = Long.MAX_VALUE; + for (CompactionSSTable sstable : ssTables) { - SSTableReader candidate = iterator.next(); - if (candidate.getMaxTimestamp() >= minTimestamp) - { - iterator.remove(); - } - else - { - logger.trace("Dropping expired SSTable {} (maxLocalDeletionTime={}, gcBefore={})", - candidate, candidate.getMaxLocalDeletionTime(), gcBefore); - } + // Overlapping might include fully expired sstables. What we care about here is + // the min timestamp of the overlapping sstables that actually contain live data. + if (sstable.getMaxLocalDeletionTime() >= gcBefore) + minTimestamp = Math.min(minTimestamp, sstable.getMinTimestamp()); } - return new HashSet<>(candidates); + + return minTimestamp; } - public static Set getFullyExpiredSSTables(ColumnFamilyStore cfStore, - Iterable compacting, - Iterable overlapping, - long gcBefore) + public static + Set getFullyExpiredSSTables(CompactionRealm realm, + Iterable compacting, + UnaryOperator> overlappingSupplier, + long gcBefore) { - return getFullyExpiredSSTables(cfStore, compacting, overlapping, gcBefore, false); + return getFullyExpiredSSTables(realm, compacting, overlappingSupplier, gcBefore, false); } /** @@ -244,35 +242,52 @@ public static Set getFullyExpiredSSTables(ColumnFamilyStore cfSto @Override public LongPredicate getPurgeEvaluator(DecoratedKey key) { - if (NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || !compactingRepaired() || cfs.getNeverPurgeTombstones() || overlapIterator == null) + if (overlapTracker == null || !compactingRepaired()) return time -> false; - overlapIterator.update(key); - Set filteredSSTables = overlapIterator.overlaps(); - Iterable memtables = cfs.getTracker().getView().getAllMemtables(); + Collection filteredSSTables = overlapTracker.overlaps(key); + Iterable memtables = realm.getAllMemtables(); long minTimestampSeen = Long.MAX_VALUE; boolean hasTimestamp = false; - for (SSTableReader sstable: filteredSSTables) + // TODO: Evaluate if doing this in sort order to minimize mayContainAssumingKeyIsRange calls is a performance improvement. + for (CompactionSSTable sstable: filteredSSTables) { - if (sstable.mayContainAssumingKeyIsInRange(key)) + long sstableMinTimestamp = sstable.getMinTimestamp(); + // if we don't have bloom filter(bf_fp_chance=1.0 or filter file is missing), + // we check index file instead. + if (sstableMinTimestamp < minTimestampSeen && sstable.mayContainAssumingKeyIsInRange(key)) { - minTimestampSeen = Math.min(minTimestampSeen, sstable.getMinTimestamp()); + minTimestampSeen = sstableMinTimestamp; hasTimestamp = true; } } - for (Memtable memtable : memtables) + OpOrder.Group readGroup = null; + try { - if (memtable.getMinTimestamp() != Memtable.NO_MIN_TIMESTAMP) + for (Memtable memtable : memtables) { - if (memtable.rowIterator(key) != null) + long memtableMinTimestamp = memtable.getMinTimestamp(); + if (memtableMinTimestamp >= minTimestampSeen || memtableMinTimestamp == Memtable.NO_MIN_TIMESTAMP) + continue; + + if (readGroup == null) + readGroup = memtable.readOrdering().start(); // the read order is the same for all memtables of a CFS + + Partition partition = memtable.getPartition(key); + if (partition != null) { - minTimestampSeen = Math.min(minTimestampSeen, memtable.getMinTimestamp()); + minTimestampSeen = Math.min(minTimestampSeen, partition.stats().minTimestamp); hasTimestamp = true; } } } + finally + { + if (readGroup != null) + readGroup.close(); + } if (!hasTimestamp) return time -> true; @@ -285,39 +300,58 @@ public LongPredicate getPurgeEvaluator(DecoratedKey key) public void close() { - if (overlappingSSTables != null) - overlappingSSTables.release(); - - FileUtils.closeQuietly(openDataFiles.values()); - openDataFiles.clear(); + closeDataFiles(); + FileUtils.closeQuietly(overlapTracker); } public boolean compactingRepaired() { - return !cfs.getCompactionStrategyManager().onlyPurgeRepairedTombstones() || compactingRepaired; + return !realm.onlyPurgeRepairedTombstones() || compactingRepaired; } - boolean provideTombstoneSources() + boolean shouldProvideTombstoneSources() { - return tombstoneOption != TombstoneOption.NONE; + return tombstoneOption != TombstoneOption.NONE && compactingRepaired() && overlapTracker != null; } // caller must close iterators public Iterable shadowSources(DecoratedKey key, boolean tombstoneOnly) { - if (!provideTombstoneSources() || !compactingRepaired() || NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE || cfs.getNeverPurgeTombstones()) + if (!shouldProvideTombstoneSources()) return null; - overlapIterator.update(key); - return Iterables.filter(Iterables.transform(overlapIterator.overlaps(), - reader -> getShadowIterator(reader, key, tombstoneOnly)), - Predicates.notNull()); + + return overlapTracker.openSelectedOverlappingSSTables(key, + tombstoneOnly ? this::isTombstoneShadowSource + : this::isCellDataShadowSource, + sstable -> { + long position = sstable.getPosition(key, SSTableReader.Operator.EQ, false); + if (position < 0) + return null; + + return sstable.simpleIterator(openDataFiles.computeIfAbsent(sstable, + this::openDataFile), + key, + position, + tombstoneOnly); + }); + } + + // TODO verify this stuff + private boolean isTombstoneShadowSource(CompactionSSTable ssTable) + { + return isCellDataShadowSource(ssTable) && ssTable.mayHaveTombstones(); + } + + private boolean isCellDataShadowSource(CompactionSSTable ssTable) + { + return !ssTable.isMarkedSuspect() && ssTable.getMaxTimestamp() > minTimestamp; } private UnfilteredRowIterator getShadowIterator(SSTableReader reader, DecoratedKey key, boolean tombstoneOnly) { if (reader.isMarkedSuspect() || - reader.getMaxTimestamp() <= minTimestamp || - tombstoneOnly && !reader.mayHaveTombstones()) + reader.getMaxTimestamp() <= minTimestamp || + tombstoneOnly && !reader.mayHaveTombstones()) return null; long position = reader.getPosition(key, SSTableReader.Operator.EQ); if (position < 0) @@ -347,6 +381,6 @@ protected boolean ignoreOverlaps() private FileDataInput openDataFile(SSTableReader reader) { - return limiter != null ? reader.openDataReader(limiter) : reader.openDataReader(); + return limiter != null ? reader.openDataReader(limiter, ReadPattern.SEQUENTIAL) : reader.openDataReader(ReadPattern.SEQUENTIAL); } } diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionCursor.java b/src/java/org/apache/cassandra/db/compaction/CompactionCursor.java new file mode 100644 index 000000000000..89c9761bc45b --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionCursor.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.RateLimiter; + +import org.apache.cassandra.db.compaction.writers.SSTableDataSink; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.compaction.SortedStringTableCursor; +import org.apache.cassandra.io.sstable.compaction.IteratorFromCursor; +import org.apache.cassandra.io.sstable.compaction.PurgeCursor; +import org.apache.cassandra.io.sstable.compaction.SSTableCursor; +import org.apache.cassandra.io.sstable.compaction.SSTableCursorMerger; +import org.apache.cassandra.io.sstable.compaction.SkipEmptyDataCursor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.Clock; + +/** + * Counterpart to CompactionIterator. Maintains sstable cursors, applies limiter and produces metrics. In the future it + * should also pass information to observers and deal with expired tombstones and garbage-collection compactions. + */ +public class CompactionCursor implements SSTableCursorMerger.MergeListener, AutoCloseable +{ + private static final long MILLISECONDS_TO_UPDATE_PROGRESS = 1000; + + private final OperationType type; + private final CompactionController controller; + private final SSTableCursor cursor; + private final Row.Builder rowBuilder; + + private final long totalBytes; + private volatile long currentBytes; + private long currentProgressMillisSinceStartup; + + /** + * Merged frequency counters for partitions and rows (AKA histograms). + * The array index represents the number of sstables containing the row or partition minus one. So index 0 contains + * the number of rows or partitions coming from a single sstable (therefore copied rather than merged), index 1 contains + * the number of rows or partitions coming from two sstables and so forth. + */ + private final long[] mergedPartitionsHistogram; + private final long[] mergedRowsHistogram; + + public CompactionCursor(OperationType type, Collection readers, Range tokenRange, CompactionController controller, RateLimiter limiter, long nowInSec) + { + this.controller = controller; + this.type = type; + this.mergedPartitionsHistogram = new long[readers.size()]; + this.mergedRowsHistogram = new long[readers.size()]; + this.rowBuilder = BTreeRow.sortedBuilder(); + this.cursor = makeMergedAndPurgedCursor(readers, tokenRange, controller, limiter, nowInSec); + this.totalBytes = cursor.bytesTotal(); + this.currentBytes = 0; + this.currentProgressMillisSinceStartup = Clock.Global.currentTimeMillis(); + } + + private SSTableCursor makeMergedAndPurgedCursor(Collection readers, + Range tokenRange, + CompactionController controller, + RateLimiter limiter, + long nowInSec) + { + if (readers.isEmpty()) + return SSTableCursor.empty(); + + SSTableCursor merged = new SSTableCursorMerger(readers.stream() + .map(r -> new SortedStringTableCursor(r, tokenRange, limiter)) + .collect(Collectors.toList()), + metadata(), + this); + + if (Iterables.any(readers, SSTableReader::mayHaveTombstones)) + { + merged = new PurgeCursor(merged, controller, nowInSec); + merged = new SkipEmptyDataCursor(merged); + } + return merged; + } + + public SSTableCursor.Type copyOne(SSTableDataSink writer) throws IOException + { + boolean wasInitialized = true; + if (cursor.type() == SSTableCursor.Type.UNINITIALIZED) + { + cursor.advance(); + wasInitialized = false; + } + + switch (cursor.type()) + { + case ROW: + Row row = collectRow(); + if (!row.isEmpty()) + writer.addUnfiltered(row); + return SSTableCursor.Type.ROW; + case RANGE_TOMBSTONE: + writer.addUnfiltered(collectRangeTombstoneMarker()); + return SSTableCursor.Type.RANGE_TOMBSTONE; + case PARTITION: + if (wasInitialized) + writer.endPartition(); + maybeUpdateProgress(); + // The writer can reject a partition (e.g. due to long key). Loop until it accepts one. + while (!writer.startPartition(cursor.partitionKey(), cursor.partitionLevelDeletion())) + { + if (!skipToNextPartition()) + return SSTableCursor.Type.EXHAUSTED; + } + cursor.advance(); + return SSTableCursor.Type.PARTITION; + case EXHAUSTED: + if (wasInitialized) + writer.endPartition(); + updateProgress(Long.MAX_VALUE); + return SSTableCursor.Type.EXHAUSTED; + default: + throw new AssertionError(); + } + } + + private void maybeUpdateProgress() + { + long now = Clock.Global.currentTimeMillis(); + if (now - currentProgressMillisSinceStartup > MILLISECONDS_TO_UPDATE_PROGRESS) + updateProgress(now); + } + + private void updateProgress(long now) + { + currentBytes = cursor.bytesProcessed(); + currentProgressMillisSinceStartup = now; + } + + private Row collectRow() + { + return IteratorFromCursor.collectRow(cursor, rowBuilder); + } + + private Unfiltered collectRangeTombstoneMarker() + { + return IteratorFromCursor.collectRangeTombstoneMarker(cursor); + } + + private boolean skipToNextPartition() + { + while (true) + { + switch (cursor.advance()) + { + case EXHAUSTED: + return false; + case PARTITION: + return true; + default: + break; // continue loop + } + } + } + + /** + * @return A {@link TableOperation} backed by this iterator. This operation can be observed for progress + * and for interrupting provided that it is registered with a {@link TableOperationObserver}, normally the + * metrics in the compaction manager. The caller is responsible for registering the operation and checking + * {@link TableOperation#isStopRequested()}. + */ + public TableOperation createOperation(TableOperation.Progress progress) + { + return new AbstractTableOperation() { + + @Override + public Progress getProgress() + { + return progress; + } + + @Override + public boolean isGlobal() + { + return false; + } + }; + } + + public TableMetadata metadata() + { + return controller.realm.metadata(); + } + + long bytesRead() + { + // Note: This may be called from other threads. Reading the current positions in the sources is not safe as + // random access readers aren't thread-safe. To avoid problems we track the progress in the processing thread + // and store it in a volatile field. + return currentBytes; + } + + long totalBytes() + { + return totalBytes; + } + + long totalSourcePartitions() + { + return Arrays.stream(mergedPartitionsHistogram).reduce(0L, Long::sum); + } + + long totalSourceRows() + { + return Arrays.stream(mergedRowsHistogram).reduce(0L, Long::sum); + } + + long[] mergedPartitionsHistogram() + { + return mergedPartitionsHistogram; + } + + long[] mergedRowsHistogram() + { + return mergedRowsHistogram; + } + + public void onItem(SSTableCursor cursor, int numVersions) + { + switch (cursor.type()) + { + case PARTITION: + mergedPartitionsHistogram[numVersions - 1] += 1; + break; + case ROW: + mergedRowsHistogram[numVersions - 1] += 1; + break; + default: + break; + } + } + + public void remove() + { + throw new UnsupportedOperationException(); + } + + public void close() + { + cursor.close(); + } + + public String toString() + { + return String.format("%s: %s, (%d/%d)", type, metadata(), bytesRead(), totalBytes()); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java b/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java deleted file mode 100644 index 0bfc925a7d0d..000000000000 --- a/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.db.compaction; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; - -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableSet; - -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.TimeUUID; - -public final class CompactionInfo -{ - public static final String ID = "id"; - public static final String KEYSPACE = "keyspace"; - public static final String COLUMNFAMILY = "columnfamily"; - public static final String COMPLETED = "completed"; - public static final String TOTAL = "total"; - public static final String TASK_TYPE = "taskType"; - public static final String UNIT = "unit"; - public static final String COMPACTION_ID = "compactionId"; - public static final String SSTABLES = "sstables"; - public static final String TARGET_DIRECTORY = "targetDirectory"; - - private final TableMetadata metadata; - private final OperationType tasktype; - private final long completed; - private final long total; - private final Unit unit; - private final TimeUUID compactionId; - private final ImmutableSet sstables; - private final String targetDirectory; - - public CompactionInfo(TableMetadata metadata, OperationType tasktype, long completed, long total, Unit unit, TimeUUID compactionId, Collection sstables, String targetDirectory) - { - this.tasktype = tasktype; - this.completed = completed; - this.total = total; - this.metadata = metadata; - this.unit = unit; - this.compactionId = compactionId; - this.sstables = ImmutableSet.copyOf(sstables); - this.targetDirectory = targetDirectory; - } - - public CompactionInfo(TableMetadata metadata, OperationType tasktype, long completed, long total, TimeUUID compactionId, Collection sstables, String targetDirectory) - { - this(metadata, tasktype, completed, total, Unit.BYTES, compactionId, sstables, targetDirectory); - } - - public CompactionInfo(TableMetadata metadata, OperationType tasktype, long completed, long total, TimeUUID compactionId, Collection sstables) - { - this(metadata, tasktype, completed, total, Unit.BYTES, compactionId, sstables, null); - } - - /** - * Special compaction info where we always need to cancel the compaction - for example ViewBuilderTask where we don't know - * the sstables at construction - */ - public static CompactionInfo withoutSSTables(TableMetadata metadata, OperationType tasktype, long completed, long total, Unit unit, TimeUUID compactionId) - { - return withoutSSTables(metadata, tasktype, completed, total, unit, compactionId, null); - } - - /** - * Special compaction info where we always need to cancel the compaction - for example AutoSavingCache where we don't know - * the sstables at construction - */ - public static CompactionInfo withoutSSTables(TableMetadata metadata, OperationType tasktype, long completed, long total, Unit unit, TimeUUID compactionId, String targetDirectory) - { - return new CompactionInfo(metadata, tasktype, completed, total, unit, compactionId, ImmutableSet.of(), targetDirectory); - } - - /** @return A copy of this CompactionInfo with updated progress. */ - public CompactionInfo forProgress(long complete, long total) - { - return new CompactionInfo(metadata, tasktype, complete, total, unit, compactionId, sstables, targetDirectory); - } - - public Optional getKeyspace() - { - return Optional.ofNullable(metadata != null ? metadata.keyspace : null); - } - - public Optional getTable() - { - return Optional.ofNullable(metadata != null ? metadata.name : null); - } - - public TableMetadata getTableMetadata() - { - return metadata; - } - - public long getCompleted() - { - return completed; - } - - public long getTotal() - { - return total; - } - - public OperationType getTaskType() - { - return tasktype; - } - - public TimeUUID getTaskId() - { - return compactionId; - } - - public Unit getUnit() - { - return unit; - } - - public Set getSSTables() - { - return sstables; - } - - /** - * Get the directories this compaction could possibly write to. - * - * @return the directories that we might write to, or empty list if we don't know the metadata - * (like for index summary redistribution), or null if we don't have any disk boundaries - */ - public List getTargetDirectories() - { - if (metadata != null && !metadata.isIndex()) - { - ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(metadata.id); - if (cfs != null) - return cfs.getDirectoriesForFiles(sstables); - } - return Collections.emptyList(); - } - - public String targetDirectory() - { - if (targetDirectory == null) - return ""; - - try - { - return new File(targetDirectory).canonicalPath(); - } - catch (Throwable t) - { - throw new RuntimeException("Unable to resolve canonical path for " + targetDirectory); - } - } - - /** - * Note that this estimate is based on the amount of data we have left to read - it assumes input - * size == output size for a compaction, which is not really true, but should most often provide a worst case - * remaining write size. - */ - public long estimatedRemainingWriteBytes() - { - if (unit == Unit.BYTES && tasktype.writesData) - return getTotal() - getCompleted(); - return 0; - } - - @Override - public String toString() - { - if (metadata != null) - { - return String.format("%s(%s, %s / %s %s)@%s(%s, %s)", - tasktype, compactionId, completed, total, unit, - metadata.id, metadata.keyspace, metadata.name); - } - else - { - return String.format("%s(%s, %s / %s %s)", - tasktype, compactionId, completed, total, unit); - } - } - - public Map asMap() - { - Map ret = new HashMap(); - ret.put(ID, metadata != null ? metadata.id.toString() : ""); - ret.put(KEYSPACE, getKeyspace().orElse(null)); - ret.put(COLUMNFAMILY, getTable().orElse(null)); - ret.put(COMPLETED, Long.toString(completed)); - ret.put(TOTAL, Long.toString(total)); - ret.put(TASK_TYPE, tasktype.toString()); - ret.put(UNIT, unit.toString()); - ret.put(COMPACTION_ID, compactionId == null ? "" : compactionId.toString()); - ret.put(SSTABLES, Joiner.on(',').join(sstables)); - ret.put(TARGET_DIRECTORY, targetDirectory()); - return ret; - } - - boolean shouldStop(Predicate sstablePredicate) - { - if (sstables.isEmpty()) - { - return true; - } - return sstables.stream().anyMatch(sstablePredicate); - } - - public static abstract class Holder - { - private volatile boolean stopRequested = false; - public abstract CompactionInfo getCompactionInfo(); - - public void stop() - { - stopRequested = true; - } - - /** - * if this compaction involves several/all tables we can safely check globalCompactionsPaused - * in isStopRequested() below - */ - public abstract boolean isGlobal(); - - public boolean isStopRequested() - { - return stopRequested || (isGlobal() && CompactionManager.instance.isGlobalCompactionPaused()); - } - } - - public enum Unit - { - BYTES("bytes"), RANGES("token range parts"), KEYS("keys"); - - private final String name; - - Unit(String name) - { - this.name = name; - } - - @Override - public String toString() - { - return this.name; - } - - public static boolean isFileSize(String unit) - { - return BYTES.toString().equals(unit); - } - } -} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionInterruptedException.java b/src/java/org/apache/cassandra/db/compaction/CompactionInterruptedException.java index b9174ec262f8..d59d7a156c3d 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionInterruptedException.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionInterruptedException.java @@ -24,8 +24,10 @@ public class CompactionInterruptedException extends RuntimeException { private static final long serialVersionUID = -8651427062512310398L; - public CompactionInterruptedException(Object info) + public CompactionInterruptedException(Object info, TableOperation.StopTrigger trigger) { - super("Compaction interrupted: " + info); + super(String.format("Compaction interrupted due to %s: %s", + (trigger == null ? TableOperation.StopTrigger.NONE : trigger).toString().toLowerCase(), + info)); } } diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java b/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java index 00e3dee5af2a..4b22a4d41bca 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java @@ -18,6 +18,7 @@ package org.apache.cassandra.db.compaction; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -30,7 +31,6 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.AbstractCompactionController; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Columns; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionTime; @@ -38,7 +38,6 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.transform.DuplicateRowChecker; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.partitions.PurgeFunction; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; @@ -51,12 +50,14 @@ import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.db.rows.WrappingUnfilteredRowIterator; +import org.apache.cassandra.db.transform.DuplicateRowChecker; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.dht.Token; import org.apache.cassandra.index.transactions.CompactionTransaction; import org.apache.cassandra.index.transactions.IndexTransaction; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.metrics.TopPartitionTracker; import org.apache.cassandra.schema.CompactionParams.TombstoneOption; import org.apache.cassandra.schema.Schema; @@ -65,6 +66,7 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.paxos.PaxosRepairHistory; import org.apache.cassandra.service.paxos.uncommitted.PaxosRows; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import static java.util.concurrent.TimeUnit.MICROSECONDS; @@ -87,7 +89,7 @@ *

  • keep tracks of the compaction progress.
  • * */ -public class CompactionIterator extends CompactionInfo.Holder implements UnfilteredPartitionIterator +public class CompactionIterator implements UnfilteredPartitionIterator { private static final long UNFILTERED_TO_UPDATE_PROGRESS = 100; @@ -98,107 +100,176 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte private final long nowInSec; private final TimeUUID compactionId; private final long totalBytes; - private long bytesRead; - private long totalSourceCQLRows; + private volatile long[] bytesReadByLevel; // Keep targetDirectory for compactions, needed for `nodetool compactionstats` private volatile String targetDirectory; - /* - * counters for merged rows. - * array index represents (number of merged rows - 1), so index 0 is counter for no merge (1 row), - * index 1 is counter for 2 rows merged, and so on. + /** + * Merged frequency counters for partitions and rows (AKA histograms). + * The array index represents the number of sstables containing the row or partition minus one. So index 0 contains + * the number of rows or partitions coming from a single sstable (therefore copied rather than merged), index 1 contains + * the number of rows or partitions coming from two sstables and so forth. */ - private final long[] mergeCounters; + private final long[] mergedPartitionsHistogram; + private final long[] mergedRowsHistogram; private final UnfilteredPartitionIterator compacted; - private final ActiveCompactionsTracker activeCompactions; + private final TableOperation op; public CompactionIterator(OperationType type, List scanners, AbstractCompactionController controller, long nowInSec, TimeUUID compactionId) { - this(type, scanners, controller, nowInSec, compactionId, ActiveCompactionsTracker.NOOP, null); + this(type, scanners, controller, nowInSec, compactionId, null, null); } - public CompactionIterator(OperationType type, - List scanners, - AbstractCompactionController controller, - long nowInSec, - TimeUUID compactionId, - ActiveCompactionsTracker activeCompactions, - TopPartitionTracker.Collector topPartitionCollector) + @SuppressWarnings("resource") // We make sure to close mergedIterator in close() and CompactionIterator is itself an AutoCloseable + public CompactionIterator(OperationType type, List scanners, AbstractCompactionController controller, long nowInSec, TimeUUID compactionId, TopPartitionTracker.Collector topPartitionCollector, CompactionProgress progress) { this.controller = controller; this.type = type; this.scanners = scanners; this.nowInSec = nowInSec; this.compactionId = compactionId; - this.bytesRead = 0; + this.bytesReadByLevel = new long[LeveledGenerations.MAX_LEVEL_COUNT]; long bytes = 0; + long compressedBytes = 0; for (ISSTableScanner scanner : scanners) + { bytes += scanner.getLengthInBytes(); + compressedBytes += scanner.getCompressedLengthInBytes(); + } this.totalBytes = bytes; - this.mergeCounters = new long[scanners.size()]; + this.mergedPartitionsHistogram = new long[scanners.size()]; + this.mergedRowsHistogram = new long[scanners.size()]; // note that we leak `this` from the constructor when calling beginCompaction below, this means we have to get the sstables before // calling that to avoid a NPE. sstables = scanners.stream().map(ISSTableScanner::getBackingSSTables).flatMap(Collection::stream).collect(ImmutableSet.toImmutableSet()); - this.activeCompactions = activeCompactions == null ? ActiveCompactionsTracker.NOOP : activeCompactions; - this.activeCompactions.beginCompaction(this); // note that CompactionTask also calls this, but CT only creates CompactionIterator with a NOOP ActiveCompactions + op = createOperation(progress); UnfilteredPartitionIterator merged = scanners.isEmpty() - ? EmptyIterators.unfilteredPartition(controller.cfs.metadata()) + ? EmptyIterators.unfilteredPartition(controller.realm.metadata()) : UnfilteredPartitionIterators.merge(scanners, listener()); if (topPartitionCollector != null) // need to count tombstones before they are purged merged = Transformation.apply(merged, new TopPartitionTracker.TombstoneCounter(topPartitionCollector, nowInSec)); merged = Transformation.apply(merged, new GarbageSkipper(controller)); - Transformation purger = isPaxos(controller.cfs) && paxosStatePurging() != legacy + Transformation purger = isPaxos(controller.realm) && paxosStatePurging() != legacy ? new PaxosPurger(nowInSec) : new Purger(controller, nowInSec); merged = Transformation.apply(merged, purger); merged = DuplicateRowChecker.duringCompaction(merged, type); - compacted = Transformation.apply(merged, new AbortableUnfilteredPartitionTransformation(this)); + compacted = Transformation.apply(merged, new AbortableUnfilteredPartitionTransformation(op)); + } + + protected TableOperation createOperation(CompactionProgress progress) + { + return new AbstractTableOperation() { + + @Override + public Progress getProgress() + { + return progress != null + ? progress + : new AbstractTableOperation.OperationProgress(controller.realm.metadata(), type, bytesRead(), totalBytes, compactionId, sstables); + } + + @Override + public boolean isGlobal() + { + return false; + } + }; + } + + /** + * @return A {@link TableOperation} backed by this iterator. This operation can be observed for progress + * and for interrupting provided that it is registered with a {@link TableOperationObserver}, normally the + * metrics in the compaction manager. The caller is responsible for registering the operation and checking + * {@link TableOperation#isStopRequested()}. + */ + public TableOperation getOperation() + { + return op; } public TableMetadata metadata() { - return controller.cfs.metadata(); + return controller.realm.metadata(); } - public CompactionInfo getCompactionInfo() + public long bytesRead() { - return new CompactionInfo(controller.cfs.metadata(), - type, - bytesRead, - totalBytes, - compactionId, - sstables, - targetDirectory); + long bytesScanned = 0L; + for (ISSTableScanner scanner : scanners) + bytesScanned += scanner.getBytesScanned(); + + return bytesScanned; } - public boolean isGlobal() + long bytesRead(int level) { - return false; + return level >= 0 && level < bytesReadByLevel.length ? bytesReadByLevel[level] : 0; } - public void setTargetDirectory(final String targetDirectory) + long totalBytes() { - this.targetDirectory = targetDirectory; + return totalBytes; + } + + long totalSourcePartitions() + { + return Arrays.stream(mergedPartitionsHistogram).reduce(0L, Long::sum); + } + + long totalSourceRows() + { + return Arrays.stream(mergedRowsHistogram).reduce(0L, Long::sum); + } + + public long getTotalCompressedSize() + { + long compressedSize = 0; + for (ISSTableScanner scanner : scanners) + compressedSize += scanner.getCompressedLengthInBytes(); + + return compressedSize; + } + + public double getCompressionRatio() + { + double compressed = 0.0; + double uncompressed = 0.0; + + for (ISSTableScanner scanner : scanners) + { + compressed += scanner.getCompressedLengthInBytes(); + uncompressed += scanner.getLengthInBytes(); + } + + if (compressed == uncompressed || uncompressed == 0) + return MetadataCollector.NO_COMPRESSION_RATIO; + + return compressed / uncompressed; } - private void updateCounterFor(int rows) + long[] mergedPartitionsHistogram() { - assert rows > 0 && rows - 1 < mergeCounters.length; - mergeCounters[rows - 1] += 1; + return mergedPartitionsHistogram; } - public long[] getMergedRowCounts() + long[] mergedRowsHistogram() { - return mergeCounters; + return mergedRowsHistogram; } - public long getTotalSourceCQLRows() + public boolean isGlobal() { - return totalSourceCQLRows; + return false; + } + + public void setTargetDirectory(final String targetDirectory) + { + this.targetDirectory = targetDirectory; } private UnfilteredPartitionIterators.MergeListener listener() @@ -208,7 +279,7 @@ private UnfilteredPartitionIterators.MergeListener listener() private boolean rowProcessingNeeded() { return (type == OperationType.COMPACTION || type == OperationType.MAJOR_COMPACTION) - && controller.cfs.indexManager.handles(IndexTransaction.Type.COMPACTION); + && controller.realm.getIndexManager().handles(IndexTransaction.Type.COMPACTION); } @Override @@ -219,49 +290,18 @@ public boolean preserveOrder() public UnfilteredRowIterators.MergeListener getRowMergeListener(DecoratedKey partitionKey, List versions) { - int merged = 0; + int numVersions = 0; for (int i=0, isize=versions.size(); i 0; + mergedPartitionsHistogram[numVersions - 1] += 1; - CompactionIterator.this.updateCounterFor(merged); - - if (!rowProcessingNeeded()) - return null; - - Columns statics = Columns.NONE; - Columns regulars = Columns.NONE; - for (int i=0, isize=versions.size(); i 0 && numVersions - 1 < mergedRowsHistogram.length; + mergedRowsHistogram[numVersions - 1] += 1; + + if (indexTransaction != null) + { + indexTransaction.start(); + indexTransaction.onRowMerge(merged, versions); + indexTransaction.commit(); + } + } @Override @@ -286,17 +340,46 @@ public void close() {} }; } - private void updateBytesRead() + private CompactionTransaction getIndexTransaction(DecoratedKey partitionKey, List versions) { - long n = 0; - for (ISSTableScanner scanner : scanners) - n += scanner.getBytesScanned(); - bytesRead = n; + Columns statics = Columns.NONE; + Columns regulars = Columns.NONE; + for (int i=0, isize=versions.size(); i= 0 && level < bytesReadByLevel.length) + bytesReadByLevel[level] += n; + } + this.bytesReadByLevel = bytesReadByLevel; } public boolean hasNext() @@ -316,22 +399,17 @@ public void remove() public void close() { - try - { - compacted.close(); - } - finally - { - activeCompactions.finishCompaction(this); - } + updateBytesRead(); + + Throwables.maybeFail(Throwables.close(null, compacted)); } public String toString() { - return this.getCompactionInfo().toString(); + return String.format("%s: %s, (%d/%d)", type, metadata(), bytesRead(), totalBytes()); } - private class Purger extends PurgeFunction + class Purger extends PurgeFunction { private final AbstractCompactionController controller; @@ -343,8 +421,8 @@ private class Purger extends PurgeFunction private Purger(AbstractCompactionController controller, long nowInSec) { super(nowInSec, controller.gcBefore, controller.compactingRepaired() ? Long.MAX_VALUE : Integer.MIN_VALUE, - controller.cfs.getCompactionStrategyManager().onlyPurgeRepairedTombstones(), - controller.cfs.metadata.get().enforceStrictLiveness()); + controller.realm.onlyPurgeRepairedTombstones(), + controller.realm.metadata().enforceStrictLiveness()); this.controller = controller; } @@ -352,7 +430,7 @@ private Purger(AbstractCompactionController controller, long nowInSec) protected void onEmptyPartitionPostPurge(DecoratedKey key) { if (type == OperationType.COMPACTION) - controller.cfs.invalidateCachedPartition(key); + controller.realm.invalidateCachedPartition(key); } @Override @@ -365,7 +443,6 @@ protected void onNewPartition(DecoratedKey key) @Override protected void updateProgress() { - totalSourceCQLRows++; if ((++compactedUnfiltered) % UNFILTERED_TO_UPDATE_PROGRESS == 0) updateBytesRead(); } @@ -379,7 +456,7 @@ protected void updateProgress() @Override protected boolean shouldIgnoreGcGrace() { - return controller.cfs.shouldIgnoreGcGraceForKey(currentKey); + return controller.realm.shouldIgnoreGcGraceForKey(currentKey); } /* @@ -651,7 +728,7 @@ private PaxosPurger(long nowInSec) protected void onEmptyPartitionPostPurge(DecoratedKey key) { if (type == OperationType.COMPACTION) - controller.cfs.invalidateCachedPartition(key); + controller.realm.invalidateCachedPartition(key); } protected void updateProgress() @@ -709,40 +786,40 @@ protected Row applyToRow(Row row) private static class AbortableUnfilteredPartitionTransformation extends Transformation { private final AbortableUnfilteredRowTransformation abortableIter; + private final TableOperation op; - private AbortableUnfilteredPartitionTransformation(CompactionIterator iter) + private AbortableUnfilteredPartitionTransformation(TableOperation op) { - this.abortableIter = new AbortableUnfilteredRowTransformation(iter); + this.op = op; + this.abortableIter = new AbortableUnfilteredRowTransformation(op); } @Override protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) { - if (abortableIter.iter.isStopRequested()) - throw new CompactionInterruptedException(abortableIter.iter.getCompactionInfo()); + op.throwIfStopRequested(); return Transformation.apply(partition, abortableIter); } } private static class AbortableUnfilteredRowTransformation extends Transformation { - private final CompactionIterator iter; + private final TableOperation op; - private AbortableUnfilteredRowTransformation(CompactionIterator iter) + private AbortableUnfilteredRowTransformation(TableOperation op) { - this.iter = iter; + this.op = op; } public Row applyToRow(Row row) { - if (iter.isStopRequested()) - throw new CompactionInterruptedException(iter.getCompactionInfo()); + op.throwIfStopRequested(); return row; } } - private static boolean isPaxos(ColumnFamilyStore cfs) + private static boolean isPaxos(CompactionRealm realm) { - return cfs.name.equals(SystemKeyspace.PAXOS) && cfs.getKeyspaceName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME); + return realm.getTableName().equals(SystemKeyspace.PAXOS) && realm.getKeyspaceName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME); } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java b/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java index dd4983ddda6b..1089db5bcc99 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java @@ -18,15 +18,17 @@ package org.apache.cassandra.db.compaction; +import java.io.Closeable; import java.io.IOException; import java.io.OutputStreamWriter; -import java.lang.ref.WeakReference; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -36,6 +38,8 @@ import java.util.function.Consumer; import java.util.function.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.common.collect.MapMaker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,37 +49,29 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.cassandra.concurrent.ExecutorPlus; -import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ExecutorUtils; +import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.Throwables; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.config.CassandraRelevantProperties.LOG_DIR; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; +/** + * This is a Compaction logger that logs compaction events in a file called compactions.log. + * It was added by CASSANDRA-10805. + */ public class CompactionLogger { - public interface Strategy - { - JsonNode sstable(SSTableReader sstable); - - JsonNode options(); - - static Strategy none = new Strategy() - { - public JsonNode sstable(SSTableReader sstable) - { - return null; - } - - public JsonNode options() - { - return null; - } - }; - } + private static final DateTimeFormatter dateFormatter = DateTimeFormatter + .ofPattern("yyyy-MM-dd' 'HH:mm:ss.SSS") + .withZone(ZoneId.systemDefault() ); /** * This will produce the compaction strategy's starting information. @@ -88,8 +84,13 @@ public interface StrategySummary /** * This is an interface to allow writing to a different interface. */ - public interface Writer + public interface Writer extends Closeable { + /** + * @param toWrite This should be written out to the medium capturing the logs + */ + void write(String toWrite); + /** * This is used when we are already trying to write out the start of a * @param statement This should be written out to the medium capturing the logs @@ -104,141 +105,141 @@ public interface Writer * @param tag This is an identifier for a strategy; each strategy should have a distinct Object */ void write(JsonNode statement, StrategySummary summary, Object tag); - } - private interface CompactionStrategyAndTableFunction - { - JsonNode apply(AbstractCompactionStrategy strategy, SSTableReader sstable); + /** + * Closes the writer + */ + @Override + void close(); } private static final JsonNodeFactory json = JsonNodeFactory.instance; private static final Logger logger = LoggerFactory.getLogger(CompactionLogger.class); - private static final CompactionLogSerializer serializer = new CompactionLogSerializer(); - private final WeakReference cfsRef; - private final WeakReference csmRef; + + private static final ExecutorPlus loggerService = executorFactory().sequential("CompactionLogger"); + private static final CompactionLogSerializer jsonWriter = new CompactionLogSerializer("compaction", "log", loggerService); + + private final String keyspace; + private final String table; private final AtomicInteger identifier = new AtomicInteger(0); - private final Map compactionStrategyMapping = new MapMaker().weakKeys().makeMap(); + private final Map compactionStrategyMapping = new MapMaker().weakKeys().makeMap(); + private final Map> csvWriters = new MapMaker().makeMap(); private final AtomicBoolean enabled = new AtomicBoolean(false); - public CompactionLogger(ColumnFamilyStore cfs, CompactionStrategyManager csm) + CompactionLogger(TableMetadata metadata) { - csmRef = new WeakReference<>(csm); - cfsRef = new WeakReference<>(cfs); + this.keyspace = metadata.keyspace; + this.table = metadata.name; } - private void forEach(Consumer consumer) + void strategyCreated(CompactionStrategy strategy) { - CompactionStrategyManager csm = csmRef.get(); - if (csm == null) - return; - csm.getStrategies() - .forEach(l -> l.forEach(consumer)); + compactionStrategyMapping.computeIfAbsent(strategy, s -> String.valueOf(identifier.getAndIncrement())); } - private ArrayNode compactionStrategyMap(Function select) + /** + * Visit all the strategies. + * + * @param consumer a consumer function that receives all the strategies one by one + */ + private void visitStrategies(Consumer consumer) + { + compactionStrategyMapping.keySet().forEach(consumer); + } + + /** + * Rely on {@link this#visitStrategies(Consumer)} to visit all the strategies + * and add the properties extracted by the function passed in to a json node that is returned. + * + * @param select a function that given a strategy returns a json node + * + * @return a json node containing information on all the strategies returned by the strategy manager and the function passed in. + */ + private ArrayNode getStrategiesJsonNode(Function select) { ArrayNode node = json.arrayNode(); - forEach(acs -> node.add(select.apply(acs))); + visitStrategies(acs -> node.add(select.apply(acs))); return node; } - private ArrayNode sstableMap(Collection sstables, CompactionStrategyAndTableFunction csatf) + private ArrayNode sstableMap(Collection sstables) { - CompactionStrategyManager csm = csmRef.get(); ArrayNode node = json.arrayNode(); - if (csm == null) - return node; - sstables.forEach(t -> node.add(csatf.apply(csm.getCompactionStrategyFor(t), t))); + sstables.forEach(t -> node.add(describeSSTable(t))); return node; } - private String getId(AbstractCompactionStrategy strategy) + private String getId(CompactionStrategy strategy) { - return compactionStrategyMapping.computeIfAbsent(strategy, s -> String.valueOf(identifier.getAndIncrement())); + return compactionStrategyMapping.getOrDefault(strategy, "-1"); // there should always be a strategy because of strategyCreated() } - private JsonNode formatSSTables(AbstractCompactionStrategy strategy) + private JsonNode formatSSTables(CompactionStrategy strategy) { ArrayNode node = json.arrayNode(); - CompactionStrategyManager csm = csmRef.get(); - ColumnFamilyStore cfs = cfsRef.get(); - if (csm == null || cfs == null) - return node; - for (SSTableReader sstable : cfs.getLiveSSTables()) - { - if (csm.getCompactionStrategyFor(sstable) == strategy) - node.add(formatSSTable(strategy, sstable)); - } + for (CompactionSSTable sstable : strategy.getSSTables()) + node.add(formatSSTable(sstable)); + return node; } - private JsonNode formatSSTable(AbstractCompactionStrategy strategy, SSTableReader sstable) + private JsonNode formatSSTable(CompactionSSTable sstable) { ObjectNode node = json.objectNode(); - node.put("generation", sstable.descriptor.id.toString()); - node.put("version", sstable.descriptor.version.version); + node.put("generation", sstable.getDescriptor().id.toString()); + node.put("version", sstable.getDescriptor().version.version); node.put("size", sstable.onDiskLength()); - JsonNode logResult = strategy.strategyLogger().sstable(sstable); - if (logResult != null) - node.set("details", logResult); + + // The details are only relevant or available for some strategies, e.g. LCS or Date tiered but + // it doesn't hurt to log them all the time in order to simplify things + ObjectNode details = json.objectNode(); + details.put("level", sstable.getSSTableLevel()); + details.put("min_token", sstable.getFirst().getToken().toString()); + details.put("max_token", sstable.getLast().getToken().toString()); + details.put("min_timestamp", sstable.getMinTimestamp()); + details.put("max_timestamp", sstable.getMaxTimestamp()); + + node.put("details", details); + return node; } - private JsonNode startStrategy(AbstractCompactionStrategy strategy) + private JsonNode getStrategyDetails(CompactionStrategy strategy) { ObjectNode node = json.objectNode(); - CompactionStrategyManager csm = csmRef.get(); - if (csm == null) - return node; node.put("strategyId", getId(strategy)); node.put("type", strategy.getName()); node.set("tables", formatSSTables(strategy)); - node.put("repaired", csm.isRepaired(strategy)); - List folders = csm.getStrategyFolders(strategy); - ArrayNode folderNode = json.arrayNode(); - for (String folder : folders) - { - folderNode.add(folder); - } - node.set("folders", folderNode); - - JsonNode logResult = strategy.strategyLogger().options(); - if (logResult != null) - node.set("options", logResult); return node; } - private JsonNode shutdownStrategy(AbstractCompactionStrategy strategy) + private JsonNode getStrategyId(CompactionStrategy strategy) { ObjectNode node = json.objectNode(); node.put("strategyId", getId(strategy)); return node; } - private JsonNode describeSSTable(AbstractCompactionStrategy strategy, SSTableReader sstable) + private JsonNode describeSSTable(SSTableReader sstable) { ObjectNode node = json.objectNode(); - node.put("strategyId", getId(strategy)); - node.set("table", formatSSTable(strategy, sstable)); + node.put("table", formatSSTable(sstable)); return node; } - private void describeStrategy(ObjectNode node) + private void maybeAddSchemaAndTimeInfo(ObjectNode node) { - ColumnFamilyStore cfs = cfsRef.get(); - if (cfs == null) - return; - node.put("keyspace", cfs.getKeyspaceName()); - node.put("table", cfs.getTableName()); + node.put("keyspace", keyspace); + node.put("table", table); node.put("time", currentTimeMillis()); } - private JsonNode startStrategies() + private JsonNode getEventJsonNode() { ObjectNode node = json.objectNode(); node.put("type", "enable"); - describeStrategy(node); - node.set("strategies", compactionStrategyMap(this::startStrategy)); + maybeAddSchemaAndTimeInfo(node); + node.set("strategies", getStrategiesJsonNode(this::getStrategyDetails)); return node; } @@ -246,7 +247,7 @@ public void enable() { if (enabled.compareAndSet(false, true)) { - serializer.writeStart(startStrategies(), this); + jsonWriter.writeStart(getEventJsonNode(), this); } } @@ -256,70 +257,151 @@ public void disable() { ObjectNode node = json.objectNode(); node.put("type", "disable"); - describeStrategy(node); - node.set("strategies", compactionStrategyMap(this::shutdownStrategy)); - serializer.write(node, this::startStrategies, this); + maybeAddSchemaAndTimeInfo(node); + node.set("strategies", getStrategiesJsonNode(this::getStrategyId)); + jsonWriter.write(node, this::getEventJsonNode, this); + + visitStrategies(strategy -> csvWriters.computeIfPresent(strategy, (s, writers) -> { writers.values().forEach(Writer::close); return null; })); } } + public boolean enabled() + { + return enabled.get(); + } + public void flush(Collection sstables) { if (enabled.get()) { ObjectNode node = json.objectNode(); node.put("type", "flush"); - describeStrategy(node); - node.set("tables", sstableMap(sstables, this::describeSSTable)); - serializer.write(node, this::startStrategies, this); + maybeAddSchemaAndTimeInfo(node); + node.set("tables", sstableMap(sstables)); + jsonWriter.write(node, this::getEventJsonNode, this); } } - public void compaction(long startTime, Collection input, long endTime, Collection output) + public void compaction(long startTime, Collection input, Range tokenRange, long endTime, Collection output) { if (enabled.get()) { ObjectNode node = json.objectNode(); node.put("type", "compaction"); - describeStrategy(node); + maybeAddSchemaAndTimeInfo(node); node.put("start", String.valueOf(startTime)); node.put("end", String.valueOf(endTime)); - node.set("input", sstableMap(input, this::describeSSTable)); - node.set("output", sstableMap(output, this::describeSSTable)); - serializer.write(node, this::startStrategies, this); + node.set("input", sstableMap(input)); + node.set("output", sstableMap(output)); + if (tokenRange != null) + node.put("range", tokenRange.toString()); + jsonWriter.write(node, this::getEventJsonNode, this); } } - public void pending(AbstractCompactionStrategy strategy, int remaining) + public void pending(CompactionStrategy strategy, int remaining) { if (remaining != 0 && enabled.get()) { ObjectNode node = json.objectNode(); node.put("type", "pending"); - describeStrategy(node); + maybeAddSchemaAndTimeInfo(node); node.put("strategyId", getId(strategy)); node.put("pending", remaining); - serializer.write(node, this::startStrategies, this); + jsonWriter.write(node, this::getEventJsonNode, this); + } + } + + /** + * Write the strategy statistics formatted as CSV. + **/ + public void statistics(CompactionStrategy strategy, String event, CompactionStrategyStatistics statistics) + { + if (logger.isTraceEnabled()) + logger.trace("Compaction statistics for strategy {} and event {}: {}", strategy, event, statistics); + + if (!enabled.get()) + return; + + for (CompactionAggregateStatistics aggregateStatistics : statistics.aggregates()) + { + Writer writer = getCsvWriter(strategy, statistics.getHeader(), aggregateStatistics); + writer.write(String.join(",", Iterables.concat(ImmutableList.of(currentTime(), event), aggregateStatistics.data())) + System.lineSeparator()); } } + private Writer getCsvWriter(CompactionStrategy strategy, Collection header, CompactionAggregateStatistics statistics) + { + Map writers = csvWriters.get(strategy); + if (writers == null) + { + writers = new MapMaker().makeMap(); + if (csvWriters.putIfAbsent(strategy, writers) != null) + { + writers = csvWriters.get(strategy); + } + } + + String shard = statistics.shard(); + Writer writer = writers.get(shard); + if (writer != null) + return writer; + + String fileName = String.format("compaction-%s-%s-%s-%s", + strategy.getName(), + keyspace, + table, + getId(strategy)); + + if (!shard.isEmpty()) + fileName += '-' + shard; + + writer = new CompactionLogSerializer(fileName, "csv", loggerService); + if (writers.putIfAbsent(shard, writer) == null) + { + writer.write(String.join(",", Iterables.concat(ImmutableList.of("Timestamp", "Event"), header)) + System.lineSeparator()); + return writer; + } + else + { + writer.close(); + return writers.get(shard); + } + } + + private String currentTime() + { + return dateFormatter.format(Instant.ofEpochMilli(currentTimeMillis())); + } + private static class CompactionLogSerializer implements Writer { private static final String logDirectory = LOG_DIR.getString(); - private final ExecutorPlus loggerService = executorFactory().sequential("CompactionLogger"); // This is only accessed on the logger service thread, so it does not need to be thread safe - private final Set rolled = new HashSet<>(); + private final String fileName; + private final String fileExt; + private final ExecutorPlus loggerService; + private final Set rolled; private OutputStreamWriter stream; - private static OutputStreamWriter createStream() throws IOException + CompactionLogSerializer(String fileName, String fileExt, ExecutorPlus loggerService) + { + this.fileName = fileName; + this.fileExt = fileExt; + this.loggerService = loggerService; + this.rolled = new HashSet<>(); + } + + private OutputStreamWriter createStream() throws IOException { int count = 0; - Path compactionLog = new File(logDirectory, "compaction.log").toPath(); + Path compactionLog = new File(logDirectory, String.format("%s.%s", fileName, fileExt)).toPath(); if (Files.exists(compactionLog)) { Path tryPath = compactionLog; while (Files.exists(tryPath)) { - tryPath = new File(logDirectory, String.format("compaction-%d.log", count++)).toPath(); + tryPath = new File(logDirectory, String.format("%s-%d.%s", fileName, count++, fileExt)).toPath(); } Files.move(compactionLog, tryPath); } @@ -327,50 +409,77 @@ private static OutputStreamWriter createStream() throws IOException return new OutputStreamWriter(Files.newOutputStream(compactionLog, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)); } - private void writeLocal(String toWrite) + private interface ThrowingConsumer { - try - { - if (stream == null) - stream = createStream(); - stream.write(toWrite); - stream.flush(); - } - catch (IOException ioe) + void accept(T stream) throws IOException; + } + + private void performWrite(ThrowingConsumer writeTask) + { + loggerService.execute(() -> { - // We'll drop the change and log the error to the logger. - NoSpamLogger.log(logger, NoSpamLogger.Level.ERROR, 1, TimeUnit.MINUTES, - "Could not write to the log file: {}", ioe); - } + try + { + if (stream == null) + stream = createStream(); + + writeTask.accept(stream); + stream.flush(); + } + catch (IOException ioe) + { + // We'll drop the change and log the error to the logger. + NoSpamLogger.log(logger, NoSpamLogger.Level.ERROR, 1, TimeUnit.MINUTES, + "Could not write to the log file: {}", ioe); + } + }); + } + public void write(String toWrite) + { + performWrite(s -> s.write(toWrite)); } public void writeStart(JsonNode statement, Object tag) { final String toWrite = statement.toString() + System.lineSeparator(); - loggerService.execute(() -> { + performWrite(s -> { rolled.add(tag); - writeLocal(toWrite); + s.write(toWrite); }); } public void write(JsonNode statement, StrategySummary summary, Object tag) { final String toWrite = statement.toString() + System.lineSeparator(); - loggerService.execute(() -> { + performWrite(s -> { if (!rolled.contains(tag)) { - writeLocal(summary.getSummary().toString() + System.lineSeparator()); + s.write(toWrite); rolled.add(tag); } - writeLocal(toWrite); }); } + + public void close() + { + if (stream != null) + { + Throwable err = Throwables.close(null, stream); + if (err != null) + { + JVMStabilityInspector.inspectThrowable(err); + logger.error("Failed to close {}: {}", String.format("%s.%s", fileName, fileExt), err); + } + + stream = null; + } + } } public static void shutdownNowAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { - ExecutorUtils.shutdownNowAndWait(timeout, unit, serializer.loggerService); + ExecutorUtils.shutdownNowAndWait(timeout, unit, jsonWriter.loggerService); } } diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionManager.java index e16e46c17c5b..82c42d3cdf9e 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionManager.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionManager.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.db.compaction; +import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -29,6 +30,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; // checkstyle: permit this import import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; @@ -37,6 +39,7 @@ import java.util.function.BooleanSupplier; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.TabularData; @@ -46,23 +49,24 @@ import com.google.common.base.Predicates; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; -import com.google.common.collect.ConcurrentHashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; -import com.google.common.collect.Multiset; import com.google.common.collect.Sets; import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.Uninterruptibles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.openhft.chronicle.core.util.ThrowingSupplier; +import com.codahale.metrics.Meter; +import io.netty.util.concurrent.FastThreadLocal; import org.apache.cassandra.cache.AutoSavingCache; import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.concurrent.WrappedExecutorPlus; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; @@ -71,7 +75,7 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.compaction.CompactionInfo.Holder; +import org.apache.cassandra.db.compaction.BackgroundCompactionRunner.RequestResult; import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.lifecycle.SSTableIntervalTree; @@ -90,9 +94,12 @@ import org.apache.cassandra.io.sstable.IScrubber; import org.apache.cassandra.io.sstable.IVerifier; import org.apache.cassandra.io.sstable.SSTableRewriter; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.indexsummary.IndexSummaryRedistribution; +import org.apache.cassandra.io.sstable.indexsummary.IndexSummarySupport; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.File; @@ -103,6 +110,8 @@ import org.apache.cassandra.repair.NoSuchRepairSessionException; import org.apache.cassandra.schema.CompactionParams.TombstoneOption; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.StorageService; @@ -110,6 +119,7 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MBeanWrapper; +import org.apache.cassandra.utils.NonThrowingCloseable; import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; @@ -121,6 +131,7 @@ import static java.util.Collections.singleton; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.concurrent.FutureTask.callable; +import static org.apache.cassandra.config.CassandraRelevantProperties.COMPACTION_RATE_LIMIT_GRANULARITY_IN_KB; import static org.apache.cassandra.config.DatabaseDescriptor.getConcurrentCompactors; import static org.apache.cassandra.db.compaction.CompactionManager.CompactionExecutor.compactionThreadGroup; import static org.apache.cassandra.db.lifecycle.SSTableIntervalTree.buildSSTableIntervalTree; @@ -143,19 +154,34 @@ public class CompactionManager implements CompactionManagerMBean, ICompactionMan private static final Logger logger = LoggerFactory.getLogger(CompactionManager.class); public static final CompactionManager instance; - @VisibleForTesting - public final AtomicInteger currentlyBackgroundUpgrading = new AtomicInteger(0); - public static final int NO_GC = Integer.MIN_VALUE; public static final int GC_ALL = Integer.MAX_VALUE; + // A thread local that tells us if the current thread is owned by the compaction manager. Used + // by CounterContext to figure out if it should log a warning for invalid counter shards. + public static final FastThreadLocal isCompactionManager = new FastThreadLocal() + { + @Override + protected Boolean initialValue() + { + return false; + } + }; + private static final int ACQUIRE_GRANULARITY = COMPACTION_RATE_LIMIT_GRANULARITY_IN_KB.getInt(128) * 1024; + private static final String CONTROLLER_CONFIG_JSON_SUFFIX = "-controller-config.JSON"; + static { instance = new CompactionManager(); MBeanWrapper.instance.registerMBean(instance, MBEAN_OBJECT_NAME); - } + /*Schedule periodic reports to run every minute*/ + ScheduledExecutors.scheduledTasks.scheduleAtFixedRate(CompactionManager::periodicReports, 1, 1, TimeUnit.MINUTES); + + /*Store Controller Config for UCS every hour*/ + ScheduledExecutors.scheduledTasks.scheduleAtFixedRate(CompactionManager::storeControllerConfig, 10, 60, TimeUnit.MINUTES); + } private final CompactionExecutor executor = new CompactionExecutor(); private final ValidationExecutor validationExecutor = new ValidationExecutor(); private final CompactionExecutor cacheCleanupExecutor = new CacheCleanupExecutor(); @@ -168,16 +194,112 @@ public class CompactionManager implements CompactionManagerMBean, ICompactionMan private final CompactionMetrics metrics = new CompactionMetrics(executor, validationExecutor, viewBuildExecutor, secondaryIndexExecutor); - @VisibleForTesting - final Multiset compactingCF = ConcurrentHashMultiset.create(); + public final ActiveOperations active = new ActiveOperations(); + + private final BackgroundCompactionRunner backgroundCompactionRunner = new BackgroundCompactionRunner(executor, active); - public final ActiveCompactions active = new ActiveCompactions(); + // The length of time to wait for task cessation when we want to run with compactions disabled. + private static final int CESSATION_WAIT_SECONDS = CassandraRelevantProperties.CESSATION_WAIT_SECONDS.getInt(60); // used to temporarily pause non-strategy managed compactions (like index summary redistribution) private final AtomicInteger globalCompactionPauseCount = new AtomicInteger(0); private final RateLimiter compactionRateLimiter = RateLimiter.create(Double.MAX_VALUE); + protected static void periodicReports() + { + if (!Keyspace.isInitialized()) + return; + + for (String keyspace : Schema.instance.getKeyspaces()) + { + if (Schema.instance.getKeyspaceInstance(keyspace) != null) + { + for (ColumnFamilyStore cfs : Schema.instance.getKeyspaceInstance(keyspace).getColumnFamilyStores()) + { + CompactionStrategy strat = cfs.getCompactionStrategy(); + strat.periodicReport(); + } + } + } + } + + @VisibleForTesting + public static void storeControllerConfig() + { + /*Delete any controller-config.JSON files that correspond to a table that no longer exists*/ + if (!Keyspace.isInitialized()) + return; + + cleanupControllerConfig(); + + for (String keyspace : Schema.instance.getKeyspaces()) + { + //don't store config files for system tables + if (Schema.instance.getKeyspaceInstance(keyspace) != null && !SchemaConstants.isSystemKeyspace(keyspace)) + { + for (ColumnFamilyStore cfs : Schema.instance.getKeyspaceInstance(keyspace).getColumnFamilyStores()) + { + CompactionStrategy strat = cfs.getCompactionStrategy(); + if (strat instanceof UnifiedCompactionContainer) + { + UnifiedCompactionStrategy ucs = (UnifiedCompactionStrategy) ((UnifiedCompactionContainer) strat).getStrategies().get(0); + ucs.storeControllerConfig(); + } + } + } + } + } + + @VisibleForTesting + public static void cleanupControllerConfig() + { + Pattern fileNamePattern = Pattern.compile(CONTROLLER_CONFIG_JSON_SUFFIX, Pattern.LITERAL); + Pattern keyspaceNameSeparator = Pattern.compile("\\."); + File dir = DatabaseDescriptor.getMetadataDirectory(); + if (dir != null) + { + for (File file : dir.tryList()) + { + if (file.name().contains(CONTROLLER_CONFIG_JSON_SUFFIX)) + { + String[] names = keyspaceNameSeparator.split(fileNamePattern.matcher(file.name()).replaceAll("")); + if (names.length == 2) + { + try + { + //table exists so keep the file + Schema.instance.getKeyspaceInstance(names[0]).getColumnFamilyStore(names[1]); + } + catch (NullPointerException | IllegalArgumentException e) + { + //table does not exist so delete the file + logger.debug("Removing {} because it does not correspond to an existing table", file); + file.delete(); + } + catch (Throwable e) + { + logger.error("Encountered an exception while cleaning up the orphaned compaction settings file ({}) for {}.{}", file, names[0], names[1], e); + logger.error("Encountered an exception while cleaning up the orphaned compaction settings file ({}) for {}.{}", file, names[0], names[1]); + throw e; + } + } + else if (names.length == 3) // if keyspace/table names are long, we include table id as a 3rd component while the keyspace and table names are abbreviated + { + TableId tableId = TableId.fromHexString(names[2]); + //table exists so keep the file + if (Schema.instance.getTableMetadata(tableId) == null) + { + //table does not exist so delete the file + logger.debug("Removing {} because it does not correspond to an existing table", file); + file.delete(); + } + } + } + } + } + } + public CompactionMetrics getMetrics() { return metrics; @@ -222,45 +344,39 @@ public void setRateInBytes(final double throughputBytesPerSec) compactionRateLimiter.setRate(throughput); } + public Meter getCompactionThroughput() + { + return metrics.bytesCompactedThroughput; + } + /** - * Call this whenever a compaction might be needed on the given columnfamily. + * Call this whenever a compaction might be needed on the given column family store. * It's okay to over-call (within reason) if a call is unnecessary, it will * turn into a no-op in the bucketing/candidate-scan phase. */ - public List> submitBackground(final ColumnFamilyStore cfs) + public Future submitBackground(final ColumnFamilyStore cfs) { - if (cfs.isAutoCompactionDisabled()) - { - logger.trace("Autocompaction is disabled"); - return Collections.emptyList(); - } + return backgroundCompactionRunner.markForCompactionCheck(cfs); + } - /** - * If a CF is currently being compacted, and there are no idle threads, submitBackground should be a no-op; - * we can wait for the current compaction to finish and re-submit when more information is available. - * Otherwise, we should submit at least one task to prevent starvation by busier CFs, and more if there - * are idle threads stil. (CASSANDRA-4310) - */ - int count = compactingCF.count(cfs); - if (count > 0 && executor.getActiveTaskCount() >= executor.getMaximumPoolSize()) - { - logger.trace("Background compaction is still running for {}.{} ({} remaining). Skipping", - cfs.getKeyspaceName(), cfs.name, count); - return Collections.emptyList(); - } + public void submitBackground(Set cfss) + { + backgroundCompactionRunner.markForCompactionCheck(cfss); + } - logger.trace("Scheduling a background task check for {}.{} with {}", - cfs.getKeyspaceName(), - cfs.name, - cfs.getCompactionStrategyManager().getName()); + public int getOngoingBackgroundCompactionsCount() + { + return backgroundCompactionRunner.getOngoingCompactionsCount(); + } - List> futures = new ArrayList<>(1); - Future fut = executor.submitIfRunning(new BackgroundCompactionCandidate(cfs), "background task"); - if (!fut.isCancelled()) - futures.add(fut); - else - compactingCF.remove(cfs); - return futures; + public CompletableFuture[] startCompactionTasks(ColumnFamilyStore cfs, Collection tasks) + { + return backgroundCompactionRunner.startCompactionTasks(cfs, tasks); + } + + public int getOngoingBackgroundUpgradesCount() + { + return backgroundCompactionRunner.getOngoingUpgradesCount(); } public boolean isCompacting(Iterable cfses, Predicate sstablePredicate) @@ -274,7 +390,7 @@ public boolean isCompacting(Iterable cfses, Predicate { + return cfs.withAllSSTables(operationType, trigger, (compacting) -> { logger.info("Starting {} for {}.{}", operationType, cfs.getKeyspaceName(), cfs.getTableName()); List transactions = new ArrayList<>(); List> futures = new ArrayList<>(); @@ -528,7 +571,7 @@ public void execute(LifecycleTransaction input) { scrubOne(cfs, input, options, active); } - }, jobs, OperationType.SCRUB); + }, jobs, OperationType.SCRUB, TableOperation.StopTrigger.SCRUB); } public AllSSTableOpStatus performVerify(ColumnFamilyStore cfs, IVerifier.Options options) throws InterruptedException, ExecutionException @@ -547,7 +590,7 @@ public void execute(LifecycleTransaction input) { verifyOne(cfs, input.onlyOne(), options, active); } - }, 0, OperationType.VERIFY); + }, 0, OperationType.VERIFY, TableOperation.StopTrigger.VERIFY); } public AllSSTableOpStatus performSSTableRewrite(final ColumnFamilyStore cfs, @@ -589,7 +632,7 @@ public AllSSTableOpStatus performSSTableRewrite(final ColumnFamilyStore cfs, Pre public Iterable filterSSTables(LifecycleTransaction transaction) { List sortedSSTables = Lists.newArrayList(transaction.originals()); - Collections.sort(sortedSSTables, SSTableReader.sizeComparator.reversed()); + Collections.sort(sortedSSTables, CompactionSSTable.sizeComparator.reversed()); Iterator iter = sortedSSTables.iterator(); while (iter.hasNext()) { @@ -606,12 +649,12 @@ public Iterable filterSSTables(LifecycleTransaction transaction) @Override public void execute(LifecycleTransaction txn) { - AbstractCompactionTask task = cfs.getCompactionStrategyManager().getCompactionTask(txn, NO_GC, Long.MAX_VALUE); + AbstractCompactionTask task = cfs.getCompactionStrategy().createCompactionTask(txn, NO_GC, Long.MAX_VALUE); task.setUserDefined(true); task.setCompactionType(OperationType.UPGRADE_SSTABLES); task.execute(active); } - }, jobs, OperationType.UPGRADE_SSTABLES); + }, jobs, OperationType.UPGRADE_SSTABLES, TableOperation.StopTrigger.UPGRADE_SSTABLES); } public AllSSTableOpStatus performCleanup(final ColumnFamilyStore cfStore, int jobs) throws InterruptedException, ExecutionException @@ -663,7 +706,7 @@ public Iterable filterSSTables(LifecycleTransaction transaction) } logger.info("Skipping cleanup for {}/{} sstables for {}.{} since they are fully contained in owned ranges (full ranges: {}, transient ranges: {})", skippedSStables, totalSSTables, cfStore.getKeyspaceName(), cfStore.getTableName(), fullRanges, transientRanges); - sortedSSTables.sort(SSTableReader.sizeComparator); + sortedSSTables.sort(CompactionSSTable.sizeComparator); return sortedSSTables; } @@ -673,7 +716,7 @@ public void execute(LifecycleTransaction txn) throws IOException CleanupStrategy cleanupStrategy = CleanupStrategy.get(cfStore, allRanges, transientRanges, txn.onlyOne().isRepaired(), FBUtilities.nowInSeconds()); doCleanupOne(cfStore, txn, cleanupStrategy, replicas.ranges(), hasIndexes); } - }, jobs, OperationType.CLEANUP); + }, jobs, OperationType.CLEANUP, TableOperation.StopTrigger.CLEANUP); } public AllSSTableOpStatus performGarbageCollection(final ColumnFamilyStore cfStore, TombstoneOption tombstoneOption, int jobs) throws InterruptedException, ExecutionException @@ -686,7 +729,7 @@ public AllSSTableOpStatus performGarbageCollection(final ColumnFamilyStore cfSto public Iterable filterSSTables(LifecycleTransaction transaction) { List filteredSSTables = new ArrayList<>(); - if (cfStore.getCompactionStrategyManager().onlyPurgeRepairedTombstones()) + if (cfStore.onlyPurgeRepairedTombstones()) { // Copy originals to avoid ConcurrentModificationException when cancel() // modifies the underlying collection when calling `cancel(..)` @@ -713,34 +756,21 @@ public Iterable filterSSTables(LifecycleTransaction transaction) { filteredSSTables.addAll(transaction.originals()); } - - filteredSSTables.sort(SSTableReader.maxTimestampAscending); + Collections.sort(filteredSSTables, SSTableReader.maxTimestampAscending); return filteredSSTables; } @Override - public void execute(LifecycleTransaction txn) throws IOException + public void execute(LifecycleTransaction txn) { logger.debug("Garbage collecting {}", txn.originals()); - CompactionTask task = new CompactionTask(cfStore, txn, getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds())) - { - @Override - protected CompactionController getCompactionController(Set toCompact) - { - return new CompactionController(cfStore, toCompact, gcBefore, null, tombstoneOption); - } - - @Override - protected int getLevel() - { - return txn.onlyOne().getSSTableLevel(); - } - }; - task.setUserDefined(true); - task.setCompactionType(OperationType.GARBAGE_COLLECT); + AbstractCompactionTask task = CompactionTask.forGarbageCollection(cfStore, + txn, + getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds()), + tombstoneOption); task.execute(active); } - }, jobs, OperationType.GARBAGE_COLLECT); + }, jobs, OperationType.GARBAGE_COLLECT, TableOperation.StopTrigger.GARBAGE_COLLECT); } public AllSSTableOpStatus relocateSSTables(final ColumnFamilyStore cfs, int jobs) throws ExecutionException, InterruptedException @@ -786,7 +816,7 @@ public Iterable filterSSTables(LifecycleTransaction transaction) public Map> groupByDiskIndex(Set needsRelocation) { - return needsRelocation.stream().collect(Collectors.groupingBy((s) -> diskBoundaries.getDiskIndex(s))); + return needsRelocation.stream().collect(Collectors.groupingBy((s) -> diskBoundaries.getDiskIndexFromKey(s))); } private boolean inCorrectLocation(SSTableReader sstable) @@ -803,12 +833,12 @@ private boolean inCorrectLocation(SSTableReader sstable) public void execute(LifecycleTransaction txn) { logger.debug("Relocating {}", txn.originals()); - AbstractCompactionTask task = cfs.getCompactionStrategyManager().getCompactionTask(txn, NO_GC, Long.MAX_VALUE); + AbstractCompactionTask task = cfs.getCompactionStrategy().createCompactionTask(txn, NO_GC, Long.MAX_VALUE); task.setUserDefined(true); task.setCompactionType(OperationType.RELOCATE); task.execute(active); } - }, jobs, OperationType.RELOCATE); + }, jobs, OperationType.RELOCATE, TableOperation.StopTrigger.RELOCATE); } /** @@ -867,8 +897,8 @@ private static void mutateFullyContainedSSTables(ColumnFamilyStore cfs, Set fullyContainedSSTables = findSSTablesToAnticompact(sstableIterator, normalizedRanges, sessionID); - cfs.metric.bytesMutatedAnticompaction.inc(SSTableReader.getTotalBytes(fullyContainedSSTables)); - cfs.getCompactionStrategyManager().mutateRepaired(fullyContainedSSTables, UNREPAIRED_SSTABLE, sessionID, isTransient); + cfs.metric.bytesMutatedAnticompaction.mark(CompactionSSTable.getTotalDataBytes(fullyContainedSSTables)); + cfs.mutateRepaired(fullyContainedSSTables, UNREPAIRED_SSTABLE, sessionID, isTransient); // since we're just re-writing the sstable metdata for the fully contained sstables, we don't want // them obsoleted when the anti-compaction is complete. So they're removed from the transaction here txn.cancel(fullyContainedSSTables); @@ -904,7 +934,7 @@ public void performAnticompaction(ColumnFamilyStore cfs, } catch (NoSuchRepairSessionException e) { - throw new CompactionInterruptedException(e.getMessage()); + throw new CompactionInterruptedException(e.getMessage(), TableOperation.StopTrigger.ANTICOMPACTION); } Preconditions.checkArgument(!prs.isPreview(), "Cannot anticompact for previews"); Preconditions.checkArgument(!replicas.isEmpty(), "No ranges to anti-compact"); @@ -969,7 +999,7 @@ static Set findSSTablesToAnticompact(Iterator ssta // ranges are normalized - no wrap around - if first and last are contained we know that all tokens are contained in the range if (r.contains(sstable.getFirst().getToken()) && r.contains(sstable.getLast().getToken())) { - logger.info("{} SSTable {} fully contained in range {}, mutating repairedAt instead of anticompacting", PreviewKind.NONE.logPrefix(parentRepairSession), sstable, r); + logger.info("{} SSTable {} fully contained in range {}, mutating repairedAt to unrepaired instead of anticompacting", PreviewKind.NONE.logPrefix(parentRepairSession), sstable, r); fullyContainedSSTables.add(sstable); sstableIterator.remove(); break; @@ -988,17 +1018,45 @@ public void performMaximal(final ColumnFamilyStore cfStore, boolean splitOutput) FBUtilities.waitOnFutures(submitMaximal(cfStore, getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds()), splitOutput)); } + public void performMaximal(final ColumnFamilyStore cfStore, boolean splitOutput, int parallelism) + { + FBUtilities.waitOnFutures(submitMaximal(cfStore, getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds()), splitOutput, parallelism, active, OperationType.MAJOR_COMPACTION)); + } + public List> submitMaximal(final ColumnFamilyStore cfStore, final long gcBefore, boolean splitOutput) { - return submitMaximal(cfStore, gcBefore, splitOutput, OperationType.MAJOR_COMPACTION); + return submitMaximal(cfStore, gcBefore, splitOutput, active, OperationType.MAJOR_COMPACTION); } - public List> submitMaximal(final ColumnFamilyStore cfStore, final long gcBefore, boolean splitOutput, OperationType operationType) + public List> submitMaximal(final ColumnFamilyStore cfStore, + final long gcBefore, + boolean splitOutput, + TableOperationObserver obs, + OperationType operationType) { + return submitMaximal(cfStore, gcBefore, splitOutput, -1, obs, operationType); + } + + @VisibleForTesting + @SuppressWarnings("resource") // the tasks are executed in parallel on the executor, making sure that they get closed + public List> submitMaximal(final ColumnFamilyStore cfStore, + final long gcBefore, + boolean splitOutput, + int permittedParallelism, + TableOperationObserver obs, + OperationType operationType) + { + // The default parallelism is half the number of compaction threads to leave enough room for other compactions. + if (permittedParallelism < 0) + permittedParallelism = getCoreCompactorThreads() / 2; + else if (permittedParallelism == 0) + permittedParallelism = Integer.MAX_VALUE; + // here we compute the task off the compaction executor, so having that present doesn't // confuse runWithCompactionsDisabled -- i.e., we don't want to deadlock ourselves, waiting // for ourselves to finish/acknowledge cancellation before continuing. - CompactionTasks tasks = cfStore.getCompactionStrategyManager().getMaximalTasks(gcBefore, splitOutput, operationType); + + CompactionTasks tasks = cfStore.getCompactionStrategy().getMaximalTasks(gcBefore, splitOutput, permittedParallelism, operationType); if (tasks.isEmpty()) return Collections.emptyList(); @@ -1015,16 +1073,25 @@ public List> submitMaximal(final ColumnFamilyStore cfStore, final long { protected void runMayThrow() { - task.execute(active); + task.execute(obs); } }; Future fut = executor.submitIfRunning(runnable, "maximal task"); if (!fut.isCancelled()) futures.add(fut); + else + { + Throwable error = task.rejected(new RejectedExecutionException("rejected by executor")); + if (error != null) + futures.add(ImmediateFuture.failure(error)); + } } if (nonEmptyTasks > 1) - logger.info("Major compaction will not result in a single sstable - repaired and unrepaired data is kept separate and compaction runs per data_file_directory."); + logger.info("Major compaction of {}.{} will not result in a single sstable - " + + "repaired and unrepaired data is kept separate, compaction runs per data_file_directory, " + + "and some compaction strategies will construct multiple non-overlapping sstables.", + cfStore.getKeyspaceName(), cfStore.getTableName()); return futures; } @@ -1038,7 +1105,7 @@ public void forceCompaction(ColumnFamilyStore cfStore, Supplier refs = Refs.ref(Collections.singleton(sstable)); - CompactionIterator ci = new CompactionIterator(OperationType.CLEANUP, Collections.singletonList(scanner), controller, nowInSec, nextTimeUUID(), active, null)) + Refs refs = Refs.ref(singleton(sstable)); + CompactionIterator ci = new CompactionIterator(OperationType.CLEANUP, Collections.singletonList(scanner), controller, nowInSec, nextTimeUUID())) { StatsMetadata metadata = sstable.getSSTableMetadata(); writer.switchWriter(createWriter(cfs, compactionFileLocation, expectedBloomFilterSize, metadata.repairedAt, metadata.pendingRepair, metadata.isTransient, sstable, txn)); @@ -1468,9 +1522,8 @@ private void doCleanupOne(final ColumnFamilyStore cfs, long bytesScanned = scanner.getBytesScanned(); - compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio); - - lastBytesScanned = bytesScanned; + if (compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio)) + lastBytesScanned = bytesScanned; } } @@ -1495,9 +1548,22 @@ private void doCleanupOne(final ColumnFamilyStore cfs, } - static void compactionRateLimiterAcquire(RateLimiter limiter, long bytesScanned, long lastBytesScanned, double compressionRatio) + protected boolean compactionRateLimiterAcquire(RateLimiter limiter, long bytesScanned, long lastBytesScanned, double compressionRatio) { + if (DatabaseDescriptor.getCompactionThroughputMebibytesPerSecAsInt() == 0) + return false; + long lengthRead = (long) ((bytesScanned - lastBytesScanned) * compressionRatio) + 1; + // Acquire at 128k granularity. At worst we'll exceed the limit a bit, but acquire is quite expensive. + if (lengthRead < ACQUIRE_GRANULARITY) + return false; + + return actuallyAcquire(limiter, lengthRead); + } + + private boolean actuallyAcquire(RateLimiter limiter, long lengthRead) + { + metrics.bytesCompactedThroughput.mark(lengthRead); while (lengthRead >= Integer.MAX_VALUE) { limiter.acquire(Integer.MAX_VALUE); @@ -1507,6 +1573,7 @@ static void compactionRateLimiterAcquire(RateLimiter limiter, long bytesScanned, { limiter.acquire((int) lengthRead); } + return true; } private static abstract class CleanupStrategy @@ -1609,7 +1676,7 @@ public UnfilteredRowIterator cleanup(UnfilteredRowIterator partition) } } - public static SSTableWriter createWriter(ColumnFamilyStore cfs, + public static SSTableWriter createWriter(CompactionRealm cfs, File compactionFileLocation, long expectedBloomFilterSize, long repairedAt, @@ -1626,11 +1693,11 @@ public static SSTableWriter createWriter(ColumnFamilyStore cfs, .setRepairedAt(repairedAt) .setPendingRepair(pendingRepair) .setTransientSSTable(isTransient) - .setTableMetadataRef(cfs.metadata) + .setTableMetadataRef(cfs.metadataRef()) .setMetadataCollector(new MetadataCollector(cfs.metadata().comparator).sstableLevel(sstable.getSSTableLevel())) .setSerializationHeader(sstable.header) - .addDefaultComponents(cfs.indexManager.listIndexGroups()) - .setSecondaryIndexGroups(cfs.indexManager.listIndexGroups()) + .addDefaultComponents(cfs.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(cfs.getIndexManager().listIndexGroups()) .build(txn, cfs); } @@ -1681,7 +1748,7 @@ public static SSTableWriter createWriterForAntiCompaction(ColumnFamilyStore cfs, * @param cfs * @param txn a transaction over the repaired sstables to anticompact * @param ranges full and transient ranges to be placed into one of the new sstables. The repaired table will be tracked via - * the {@link org.apache.cassandra.io.sstable.metadata.StatsMetadata#pendingRepair} field. + * the {@link StatsMetadata#pendingRepair} field. * @param pendingRepair the repair session we're anti-compacting for * @param isCancelled function that indicates if active anti-compaction should be canceled */ @@ -1702,14 +1769,14 @@ private void doAntiCompaction(ColumnFamilyStore cfs, // repairedAt values for these, we still avoid anti-compacting already repaired sstables, as we currently don't // make use of any actual repairedAt value and splitting up sstables just for that is not worth it at this point. Set unrepairedSSTables = sstables.stream().filter((s) -> !s.isRepaired()).collect(Collectors.toSet()); - cfs.metric.bytesAnticompacted.inc(SSTableReader.getTotalBytes(unrepairedSSTables)); - Collection> groupedSSTables = cfs.getCompactionStrategyManager().groupSSTablesForAntiCompaction(unrepairedSSTables); + cfs.metric.bytesAnticompacted.mark(CompactionSSTable.getTotalDataBytes(unrepairedSSTables)); + Collection> groupedSSTables = cfs.getCompactionStrategy().groupSSTablesForAntiCompaction(unrepairedSSTables); // iterate over sstables to check if the full / transient / unrepaired ranges intersect them. int antiCompactedSSTableCount = 0; - for (Collection sstableGroup : groupedSSTables) + for (Collection sstableGroup : groupedSSTables) { - try (LifecycleTransaction groupTxn = txn.split(sstableGroup)) + try (LifecycleTransaction groupTxn = txn.split(Collections2.transform(sstableGroup, SSTableReader.class::cast))) { int antiCompacted = antiCompactGroup(cfs, ranges, groupTxn, pendingRepair, isCancelled); antiCompactedSSTableCount += antiCompacted; @@ -1783,55 +1850,55 @@ public void obsoleteOriginals() {} public void close() {} } - CompactionStrategyManager strategy = cfs.getCompactionStrategyManager(); + CompactionStrategy strategy = cfs.getCompactionStrategy(); try (SharedTxn sharedTxn = new SharedTxn(txn); SSTableRewriter fullWriter = SSTableRewriter.constructWithoutEarlyOpening(sharedTxn, false, groupMaxDataAge); SSTableRewriter transWriter = SSTableRewriter.constructWithoutEarlyOpening(sharedTxn, false, groupMaxDataAge); SSTableRewriter unrepairedWriter = SSTableRewriter.constructWithoutEarlyOpening(sharedTxn, false, groupMaxDataAge); - AbstractCompactionStrategy.ScannerList scanners = strategy.getScanners(txn.originals()); + ScannerList scanners = strategy.getScanners(txn.originals()); CompactionController controller = new CompactionController(cfs, sstableAsSet, getDefaultGcBefore(cfs, nowInSec)); - CompactionIterator ci = getAntiCompactionIterator(scanners.scanners, controller, nowInSec, nextTimeUUID(), active, isCancelled)) + CompactionIterator ci = getAntiCompactionIterator(scanners.scanners, controller, nowInSec, nextTimeUUID(), isCancelled)) { - int expectedBloomFilterSize = Math.max(cfs.metadata().params.minIndexInterval, (int)(SSTableReader.getApproximateKeyCount(sstableAsSet))); + TableOperation op = ci.getOperation(); + try (NonThrowingCloseable cls = active.onOperationStart(op)) + { + int expectedBloomFilterSize = Math.max(cfs.metadata().params.minIndexInterval, (int)(SSTableReader.getApproximateKeyCount(sstableAsSet))); - fullWriter.switchWriter(CompactionManager.createWriterForAntiCompaction(cfs, destination, expectedBloomFilterSize, UNREPAIRED_SSTABLE, pendingRepair, false, sstableAsSet, txn)); - transWriter.switchWriter(CompactionManager.createWriterForAntiCompaction(cfs, destination, expectedBloomFilterSize, UNREPAIRED_SSTABLE, pendingRepair, true, sstableAsSet, txn)); - unrepairedWriter.switchWriter(CompactionManager.createWriterForAntiCompaction(cfs, destination, expectedBloomFilterSize, UNREPAIRED_SSTABLE, NO_PENDING_REPAIR, false, sstableAsSet, txn)); + fullWriter.switchWriter(CompactionManager.createWriterForAntiCompaction(cfs, destination, expectedBloomFilterSize, UNREPAIRED_SSTABLE, pendingRepair, false, sstableAsSet, txn)); + transWriter.switchWriter(CompactionManager.createWriterForAntiCompaction(cfs, destination, expectedBloomFilterSize, UNREPAIRED_SSTABLE, pendingRepair, true, sstableAsSet, txn)); + unrepairedWriter.switchWriter(CompactionManager.createWriterForAntiCompaction(cfs, destination, expectedBloomFilterSize, UNREPAIRED_SSTABLE, NO_PENDING_REPAIR, false, sstableAsSet, txn)); - Predicate fullChecker = !ranges.onlyFull().isEmpty() ? new Range.OrderedRangeContainmentChecker(ranges.onlyFull().ranges()) : t -> false; - Predicate transChecker = !ranges.onlyTransient().isEmpty() ? new Range.OrderedRangeContainmentChecker(ranges.onlyTransient().ranges()) : t -> false; - double compressionRatio = scanners.getCompressionRatio(); - if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO) - compressionRatio = 1.0; + Predicate fullChecker = !ranges.onlyFull().isEmpty() ? new Range.OrderedRangeContainmentChecker(ranges.onlyFull().ranges()) : t -> false; + Predicate transChecker = !ranges.onlyTransient().isEmpty() ? new Range.OrderedRangeContainmentChecker(ranges.onlyTransient().ranges()) : t -> false; + double compressionRatio = scanners.getCompressionRatio(); + if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO) + compressionRatio = 1.0; - long lastBytesScanned = 0; - - while (ci.hasNext()) - { - try (UnfilteredRowIterator partition = ci.next()) + long lastBytesScanned = 0; + while (ci.hasNext()) { - Token token = partition.partitionKey().getToken(); - // if this row is contained in the full or transient ranges, append it to the appropriate sstable - if (fullChecker.test(token)) - { - fullWriter.append(partition); - ci.setTargetDirectory(fullWriter.currentWriter().getFilename()); - } - else if (transChecker.test(token)) + try (UnfilteredRowIterator partition = ci.next()) { - transWriter.append(partition); - ci.setTargetDirectory(transWriter.currentWriter().getFilename()); - } - else - { - // otherwise, append it to the unrepaired sstable - unrepairedWriter.append(partition); - ci.setTargetDirectory(unrepairedWriter.currentWriter().getFilename()); + Token token = partition.partitionKey().getToken(); + // if this row is contained in the full or transient ranges, append it to the appropriate sstable + if (fullChecker.test(token)) + { + fullWriter.append(partition); + } + else if (transChecker.test(token)) + { + transWriter.append(partition); + } + else + { + // otherwise, append it to the unrepaired sstable + unrepairedWriter.append(partition); + } + long bytesScanned = scanners.getTotalBytesScanned(); + if (compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio)) + lastBytesScanned = bytesScanned; } - long bytesScanned = scanners.getTotalBytesScanned(); - compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio); - lastBytesScanned = bytesScanned; } } @@ -1860,57 +1927,87 @@ else if (transChecker.test(token)) pendingRepair); return fullSSTables.size() + transSSTables.size() + unrepairedSSTables.size(); } - catch (Throwable e) + catch (CompactionInterruptedException e) { - if (e instanceof CompactionInterruptedException) + if (isCancelled.getAsBoolean()) { - if (isCancelled.getAsBoolean()) - { - logger.info("Anticompaction has been canceled for session {}", pendingRepair); - logger.trace(e.getMessage(), e); - } - else - { - logger.info("Anticompaction for session {} has been stopped by request.", pendingRepair); - } + logger.info("Anticompaction has been canceled for session {}", pendingRepair); + logger.trace(e.getMessage(), e); } else { - JVMStabilityInspector.inspectThrowable(e); - logger.error("Error anticompacting " + txn + " for " + pendingRepair, e); + logger.info("Anticompaction for session {} has been stopped by request.", pendingRepair); } throw e; } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.error("Error anticompacting " + txn + " for " + pendingRepair, e); + throw e; + } + } + + @VisibleForTesting + public static CompactionIterator getAntiCompactionIterator(List scanners, CompactionController controller, long nowInSec, TimeUUID timeUUID, BooleanSupplier isCancelled) + { + return new CompactionIterator(OperationType.ANTICOMPACTION, scanners, controller, nowInSec, timeUUID) { + @Override + public TableOperation createOperation(CompactionProgress progress) + { + return getAntiCompactionOperation(super.createOperation(progress), isCancelled); + } + }; } @VisibleForTesting - public static CompactionIterator getAntiCompactionIterator(List scanners, CompactionController controller, long nowInSec, TimeUUID timeUUID, ActiveCompactionsTracker activeCompactions, BooleanSupplier isCancelled) + public static TableOperation getAntiCompactionOperation(TableOperation compaction, BooleanSupplier isCancelled) { - return new CompactionIterator(OperationType.ANTICOMPACTION, scanners, controller, nowInSec, timeUUID, activeCompactions, null) + return new AbstractTableOperation() { + @Override + public boolean isGlobal() + { + return false; + } + + @Override + public Progress getProgress() + { + return compaction.getProgress(); + } + + @Override + public void stop(StopTrigger trigger) + { + compaction.stop(trigger); + } + + @Override public boolean isStopRequested() { - return super.isStopRequested() || isCancelled.getAsBoolean(); + return compaction.isStopRequested() || isCancelled.getAsBoolean(); + } + + @Override + public StopTrigger trigger() + { + return compaction.trigger(); } }; } @VisibleForTesting - Future submitIndexBuild(final SecondaryIndexBuilder builder, ActiveCompactionsTracker activeCompactions) + Future submitIndexBuild(final SecondaryIndexBuilder builder, TableOperationObserver activeCompactions) { Runnable runnable = new Runnable() { public void run() { - activeCompactions.beginCompaction(builder); - try + try (NonThrowingCloseable c = activeCompactions.onOperationStart(builder)) { builder.build(); } - finally - { - activeCompactions.finishCompaction(builder); - } } }; @@ -1930,7 +2027,7 @@ public Future submitCacheWrite(final AutoSavingCache.Writer writer) return submitCacheWrite(writer, active); } - Future submitCacheWrite(final AutoSavingCache.Writer writer, ActiveCompactionsTracker activeCompactions) + Future submitCacheWrite(final AutoSavingCache.Writer writer, TableOperationObserver activeCompactions) { Runnable runnable = new Runnable() { @@ -1938,20 +2035,15 @@ public void run() { if (!AutoSavingCache.flushInProgress.add(writer.cacheType())) { - logger.trace("Cache flushing was already in progress: skipping {}", writer.getCompactionInfo()); + logger.trace("Cache flushing was already in progress: skipping {}", writer.getProgress()); return; } try { - activeCompactions.beginCompaction(writer); - try + try (NonThrowingCloseable c = activeCompactions.onOperationStart(writer)) { writer.saveCache(); } - finally - { - activeCompactions.finishCompaction(writer); - } } finally { @@ -1963,16 +2055,17 @@ public void run() return executor.submitIfRunning(runnable, "cache write"); } - public T runAsActiveCompaction(Holder activeCompactionInfo, ThrowingSupplier callable) throws E + public > List runIndexSummaryRedistribution(IndexSummaryRedistribution redistribution) throws IOException { - active.beginCompaction(activeCompactionInfo); - try - { - return callable.get(); - } - finally + return runIndexSummaryRedistribution(redistribution, active); + } + + @VisibleForTesting + > List runIndexSummaryRedistribution(IndexSummaryRedistribution redistribution, TableOperationObserver activeCompactions) throws IOException + { + try(Closeable c = activeCompactions.onOperationStart(redistribution)) { - active.finishCompaction(activeCompactionInfo); + return redistribution.redistributeSummaries(); } } @@ -1989,24 +2082,19 @@ public Future submitViewBuilder(final ViewBuilderTask task) } @VisibleForTesting - Future submitViewBuilder(final ViewBuilderTask task, ActiveCompactionsTracker activeCompactions) + Future submitViewBuilder(final ViewBuilderTask task, TableOperationObserver activeCompactions) { return viewBuildExecutor.submitIfRunning(() -> { - activeCompactions.beginCompaction(task); - try + try(Closeable c = activeCompactions.onOperationStart(task)) { return task.call(); } - finally - { - activeCompactions.finishCompaction(task); - } }, "view build"); } public int getActiveCompactions() { - return active.getCompactions().size(); + return active.getTableOperations().size(); } public static boolean isCompactor(Thread thread) @@ -2141,6 +2229,11 @@ public void incrementAborted() metrics.compactionsAborted.inc(); } + public void incrementFailed() + { + metrics.totalCompactionsFailed.inc(); + } + public void incrementCompactionsReduced() { metrics.compactionsReduced.inc(); @@ -2159,23 +2252,34 @@ public SecondaryIndexExecutor() } } + public void incrementRemovedExpiredSSTables(long num) + { + metrics.removedExpiredSSTables.mark(num); + } + + public void incrementDeleteOnlyCompactions() + { + metrics.deleteOnlyCompactions.mark(); + } + + @Override public List> getCompactions() { - List compactionHolders = active.getCompactions(); - List> out = new ArrayList>(compactionHolders.size()); - for (CompactionInfo.Holder ci : compactionHolders) - out.add(ci.getCompactionInfo().asMap()); + List operationSources = active.getTableOperations(); + List> out = new ArrayList>(operationSources.size()); + for (TableOperation op : operationSources) + out.add(op.getProgress().asMap()); return out; } @Override public List getCompactionSummary() { - List compactionHolders = active.getCompactions(); - List out = new ArrayList(compactionHolders.size()); - for (CompactionInfo.Holder ci : compactionHolders) - out.add(ci.getCompactionInfo().toString()); + List operationSources = active.getTableOperations(); + List out = new ArrayList(operationSources.size()); + for (TableOperation ci : operationSources) + out.add(ci.getProgress().toString()); return out; } @@ -2217,21 +2321,21 @@ public long getCompletedTasks() public void stopCompaction(String type) { OperationType operation = OperationType.valueOf(type); - for (Holder holder : active.getCompactions()) + for (TableOperation operationSource : active.getTableOperations()) { - if (holder.getCompactionInfo().getTaskType() == operation) - holder.stop(); + if (operationSource.getProgress().operationType() == operation) + operationSource.stop(TableOperation.StopTrigger.USER_STOP); } } @Override public void stopCompactionById(String compactionId) { - for (Holder holder : active.getCompactions()) + for (TableOperation operationSource : active.getTableOperations()) { - TimeUUID holderId = holder.getCompactionInfo().getTaskId(); + TimeUUID holderId = operationSource.getProgress().operationId(); if (holderId != null && holderId.equals(TimeUUID.fromString(compactionId))) - holder.stop(); + operationSource.stop(TableOperation.StopTrigger.USER_STOP); } } @@ -2412,24 +2516,74 @@ public void setMaxConcurrentAutoUpgradeTasks(int value) } } - public List getCompactionsMatching(Iterable columnFamilies, Predicate predicate) + public List getCompactionsMatching(Iterable columnFamilies, Predicate sstablePredicate, Predicate progressPredicate) { Preconditions.checkArgument(columnFamilies != null, "Attempted to getCompactionsMatching in CompactionManager with no columnFamilies specified."); - List matched = new ArrayList<>(); + List matched = new ArrayList<>(); // consider all in-progress compactions - for (Holder holder : active.getCompactions()) + for (TableOperation holder : active.getTableOperations()) { - CompactionInfo info = holder.getCompactionInfo(); - if (info.getTableMetadata() == null || Iterables.contains(columnFamilies, info.getTableMetadata())) + TableOperation.Progress progress = holder.getProgress(); + if (progress.metadata() == null || Iterables.contains(columnFamilies, progress.metadata())) { - if (predicate.test(info)) + if (progressPredicate.test(progress) && holder.shouldStop(sstablePredicate)) matched.add(holder); } } return matched; } + /** + * Try to stop all of the compactions for given tables. + * + * Note that this method does not wait for all compactions to finish; you'll need to loop against + * isCompacting if you want that behavior. + * + * @param tables The tables to try to stop compaction upon. + * @param opPredicate Predicate to define which compaction operation to stop, based on its type. + * @param readerPredicate Predicate to define which compaction to stop based on candidate sstables. + * @param waitForInterruption whether to wait until interrupted compaction has fully stopped + * + * @return True if any compaction has been interrupted false otherwise. + */ + public boolean interruptCompactionFor(Iterable tables, Predicate opPredicate, Predicate readerPredicate, + boolean waitForInterruption, TableOperation.StopTrigger trigger) + { + assert tables != null; + + // interrupt in-progress compactions + Set interrupted = new HashSet<>(); + for (TableOperation operationSource : active.getTableOperations()) + { + TableOperation.Progress info = operationSource.getProgress(); + + if (Iterables.contains(tables, info.metadata()) && opPredicate.test(info.operationType())) + { + operationSource.stop(trigger); + interrupted.add(operationSource); + } + } + + if (waitForInterruption) + { + // wait at most 2 minutes + long start = nanoTime(); + long wait = TimeUnit.MINUTES.toNanos(2); + + for (TableOperation operation : interrupted) + { + while (active.isActive(operation) && nanoTime() - start < wait) + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + + if (active.isActive(operation)) + throw new RuntimeException(String.format("Compaction task (%s) didn't finish within 2 minutes", operation.getProgress())); + } + } + + return !interrupted.isEmpty(); + } + /** * Try to stop all of the compactions for given ColumnFamilies. * @@ -2439,39 +2593,79 @@ public List getCompactionsMatching(Iterable columnFamilie * @param columnFamilies The ColumnFamilies to try to stop compaction upon. * @param sstablePredicate the sstable predicate to match on * @param interruptValidation true if validation operations for repair should also be interrupted + * @return True if any compaction has been interrupted false otherwise. */ - public void interruptCompactionFor(Iterable columnFamilies, Predicate sstablePredicate, boolean interruptValidation) + public boolean interruptCompactionFor(Iterable columnFamilies, + Predicate sstablePredicate, + boolean interruptValidation, + TableOperation.StopTrigger trigger) { assert columnFamilies != null; // interrupt in-progress compactions - for (Holder compactionHolder : active.getCompactions()) + boolean interrupted = false; + for (TableOperation operationSource : active.getTableOperations()) { - CompactionInfo info = compactionHolder.getCompactionInfo(); - if ((info.getTaskType() == OperationType.VALIDATION) && !interruptValidation) + TableOperation.Progress info = operationSource.getProgress(); + if ((info.operationType() == OperationType.VALIDATION) && !interruptValidation) continue; - if (info.getTableMetadata() == null || Iterables.contains(columnFamilies, info.getTableMetadata())) + if (info.metadata() == null || Iterables.contains(columnFamilies, info.metadata())) + { + if (operationSource.shouldStop(sstablePredicate)) + { + operationSource.stop(trigger); + interrupted = true; + } + } + } + return interrupted; + } + + public Collection getOperationsInvolving(Iterable columnFamilies, + Predicate sstablePredicate) + { + List result = new ArrayList<>(); + for (TableOperation operationSource : active.getTableOperations()) + { + TableOperation.Progress info = operationSource.getProgress(); + + if (info.metadata() == null || Iterables.contains(columnFamilies, info.metadata())) { - if (info.shouldStop(sstablePredicate)) - compactionHolder.stop(); + for (SSTableReader ssTableReader : info.sstables()) + { + if (sstablePredicate.test(ssTableReader)) + { + result.add(info); + break; + } + } } } + return result; + } + + public boolean interruptCompactionFor(Iterable tables, TableOperation.StopTrigger trigger) + { + return interruptCompactionFor(tables, Predicates.alwaysTrue(), true, trigger); } - public void interruptCompactionForCFs(Iterable cfss, Predicate sstablePredicate, boolean interruptValidation) + public void interruptCompactionForCFs(Iterable cfss, + Predicate sstablePredicate, + boolean interruptValidation, + TableOperation.StopTrigger trigger) { List metadata = new ArrayList<>(); for (ColumnFamilyStore cfs : cfss) metadata.add(cfs.metadata()); - interruptCompactionFor(metadata, sstablePredicate, interruptValidation); + interruptCompactionFor(metadata, sstablePredicate, interruptValidation, trigger); } public void waitForCessation(Iterable cfss, Predicate sstablePredicate) { long start = nanoTime(); - long delay = TimeUnit.MINUTES.toNanos(1); + long delay = TimeUnit.SECONDS.toNanos(CESSATION_WAIT_SECONDS); while (nanoTime() - start < delay) { @@ -2483,14 +2677,14 @@ public void waitForCessation(Iterable cfss, Predicate getSSTableTasks() + public List getSSTableTasks() { - return active.getCompactions() + return active.getTableOperations() .stream() - .map(CompactionInfo.Holder::getCompactionInfo) - .filter(task -> task.getTaskType() != OperationType.COUNTER_CACHE_SAVE - && task.getTaskType() != OperationType.KEY_CACHE_SAVE - && task.getTaskType() != OperationType.ROW_CACHE_SAVE) + .map(TableOperation::getProgress) + .filter(progress -> progress.operationType() != OperationType.COUNTER_CACHE_SAVE + && progress.operationType() != OperationType.KEY_CACHE_SAVE + && progress.operationType() != OperationType.ROW_CACHE_SAVE) .collect(Collectors.toList()); } diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionObserver.java b/src/java/org/apache/cassandra/db/compaction/CompactionObserver.java new file mode 100644 index 000000000000..8de942a2d90d --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionObserver.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import org.apache.cassandra.utils.TimeUUID; + +import javax.annotation.Nullable; + +/** + * An observer of a compaction operation. It is notified when a compaction operation is started. + *

    + * It returns a closeable that is invoked when the compaction is finished. + *

    + * The progress can be queried at any time to obtain real-time updates of the compaction operation. + */ +public interface CompactionObserver +{ + CompactionObserver NO_OP = new CompactionObserver() + { + @Override + public void onInProgress(CompactionProgress progress) { } + + @Override + public void onCompleted(TimeUUID id, @Nullable Throwable error) { } + }; + + /** + * Indicates that a compaction has started. + *

    + * @param progress the compaction progress, it contains the unique id and real-time progress information + */ + void onInProgress(CompactionProgress progress); + + /** + * Indicates that a compaction with the given id has completed. + *

    + * @param id the id of the compaction + * @param error error if compaction failed with any exceptions; or null if completed successfully + */ + void onCompleted(TimeUUID id, @Nullable Throwable error); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionPick.java b/src/java/org/apache/cassandra/db/compaction/CompactionPick.java new file mode 100644 index 000000000000..17a6c8cff38a --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionPick.java @@ -0,0 +1,378 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.utils.TimeUUID; + +/** + * A set of sstables that were picked for compaction along with some other relevant properties. + *

    + * This is a list of sstables that should be compacted together after having been picked by a compaction strategy, + * for example from a bucket in {@link SizeTieredCompactionStrategy} or from a level in {@link LeveledCompactionStrategy}. + * Also, it contains other useful parameters such as a score that was assigned to this candidate (the read hotness or level + * score depending on the strategy) and the level, if applicable. + **/ +@NotThreadSafe +public class CompactionPick +{ + final static CompactionPick EMPTY = create(-1, Collections.emptyList(), 0); + + /** The key to the parent compaction aggregate, e.g. a level number or tier avg size, -1 if no parent */ + private final long parent; + + /** The sstables to be compacted */ + private final ImmutableSet sstables; + + /** Only expired sstables */ + private final ImmutableSet expired; + + /** The sum of all the sstable hotness scores */ + private final double hotness; + + /** The average size in bytes for the sstables in this compaction */ + private final long avgSizeInBytes; + + /** The total size on disk for the sstables in this compaction */ + private final long totSizeInBytes; + + /** The unique compaction id, this is available from the beginning and *MUST BE* used to create the transaction, + * when it is submitted */ + @Nonnull + private final TimeUUID id; + + /** The total space overhead for this compaction, including primary and secondary indexes. */ + private final long totalOverheadInBytes; + + /** This is set to true when the compaction is submitted */ + private volatile boolean submitted; + + /** The compaction progress, this is only available when compaction actually starts and will be null as long as + * the candidate is still pending execution, also some tasks cannot report a progress at all, e.g. {@link SingleSSTableLCSTask}. + * */ + @Nullable + private volatile CompactionProgress progress; + + /** Set to true when the compaction has completed */ + private volatile boolean completed; + + private CompactionPick(TimeUUID id, + long parent, + Collection compacting, + Collection expired, + double hotness, + long avgSizeInBytes, + long totSizeInBytes, + long totalOverheadInBytes) + { + this.id = Objects.requireNonNull(id); + this.parent = parent; + this.sstables = ImmutableSet.copyOf(compacting); + this.expired = ImmutableSet.copyOf(expired); + this.hotness = hotness; + this.avgSizeInBytes = avgSizeInBytes; + this.totSizeInBytes = totSizeInBytes; + this.totalOverheadInBytes = totalOverheadInBytes; + } + + /** + * Create a pending compaction candidate with the given id, and average hotness and size. + * This method will use the data file size as the space overhead and should not be used by the unified compaction + * strategy where the overhead can be configurable. + */ + public static CompactionPick create(TimeUUID id, + long parent, + Collection sstables, + Collection expired) + { + Collection nonExpiring = sstables.stream().filter(sstable -> !expired.contains(sstable)).collect(Collectors.toList()); + final long totSizeBytes = CompactionAggregate.getTotSizeBytes(nonExpiring); + return create(id, + parent, + sstables, + expired, + CompactionAggregate.getTotHotness(nonExpiring), + totSizeBytes / Math.max(nonExpiring.size(), 1), + totSizeBytes, + totSizeBytes); + } + + /** + * Create a pending compaction candidate calculating hotness and avg and total size. + */ + public static CompactionPick create(long parent, Collection sstables, Collection expired) + { + Collection nonExpiring = sstables.stream().filter(sstable -> !expired.contains(sstable)).collect(Collectors.toList()); + final long totSizeBytes = CompactionAggregate.getTotSizeBytes(nonExpiring); + return create(LifecycleTransaction.newId(), + parent, + sstables, + expired, + CompactionAggregate.getTotHotness(nonExpiring), + totSizeBytes / Math.max(nonExpiring.size(), 1), + totSizeBytes, + totSizeBytes); + } + + static CompactionPick create(long parent, Collection sstables) + { + return create(parent, sstables, Collections.emptyList()); + } + + static CompactionPick createWithUnknownParent(TimeUUID id, Collection sstables) + { + return create(id, -1, sstables, Collections.emptyList()); + } + + /** + * Create a pending compaction candidate calculating avg and total size. + * This method will use the data file size as the space overhead and should not be used by the unified compaction + * strategy where the overhead can be configurable. + */ + static CompactionPick create(long parent, Collection sstables, double hotness) + { + final long totSizeBytes = CompactionAggregate.getTotSizeBytes(sstables); + return create(LifecycleTransaction.newId(), + parent, + sstables, + Collections.emptyList(), + hotness, + totSizeBytes / Math.max(sstables.size(), 1), + totSizeBytes, + totSizeBytes); + } + + /** + * Create a pending compaction candidate with the given parameters. + */ + static CompactionPick create(TimeUUID id, + long parent, + Collection sstables, + Collection expired, + double hotness, + long avgSizeInBytes, + long totSizeInBytes, + long totalOverheadInBytes) + { + return new CompactionPick(id, parent, sstables, expired, hotness, avgSizeInBytes, totSizeInBytes, totalOverheadInBytes); + } + + public double hotness() + { + return hotness; + } + + public long avgSizeInBytes() + { + return avgSizeInBytes; + } + + public long totSizeInBytes() + { + return totSizeInBytes; + } + + public long totalOverheadInBytes() + { + return totalOverheadInBytes; + } + + public double overheadToDataRatio() + { + return totalOverheadInBytes / Math.max(totSizeInBytes, 1.0); + } + + public long parent() + { + return parent; + } + + public ImmutableSet sstables() + { + return sstables; + } + + public ImmutableSet expired() + { + return expired; + } + + public TimeUUID id() + { + return id; + } + + public CompactionProgress progress() + { + return progress; + } + + public boolean inProgress() + { + return progress != null; + } + + public boolean completed() + { + return completed; + } + + public boolean submitted() { return submitted; } + + public void setSubmitted(TimeUUID id) + { + if (id == null || !this.id.equals(id)) + throw new IllegalArgumentException("Id should have been " + this.id); + + this.submitted = true; + } + + /** + * Set the compaction progress, this means the compaction pick has started executing. + */ + public void setProgress(CompactionProgress progress) + { + if (progress == null) + throw new IllegalArgumentException("Progress cannot be null"); + + if (this.progress != null) + { + if (this.progress.operationId() == progress.operationId()) + return; + else + throw new IllegalStateException("Already compacting with different id"); + } + + if (!this.submitted()) + setSubmitted(progress.operationId()); + else if (this.id != progress.operationId()) + throw new IllegalStateException("Submitted with a different id"); + + this.progress = progress; + } + + public void setCompleted() + { + this.completed = true; + } + + /** + * Create new compaction pick similar to the one provided but with a new parent. + */ + CompactionPick withParent(long parent) + { + return new CompactionPick(id, + parent, + sstables, + expired, + hotness, + avgSizeInBytes, + totSizeInBytes, + totalOverheadInBytes); + } + + /** + * Add more sstables to the collection of sstables initially picked. + *

    + * This is currently used by {@link TimeWindowCompactionStrategy} to add expired sstables. + * + * @param expired the sstables to add + */ + CompactionPick withExpiredSSTables(Collection expired) + { + ImmutableSet newSSTables = ImmutableSet.builder() + .addAll(this.sstables) + .addAll(expired) + .build(); + ImmutableSet newExpired = ImmutableSet.builder() + .addAll(this.expired) + .addAll(expired) + .build(); + return new CompactionPick(id, + parent, + newSSTables, + newExpired, + hotness, + avgSizeInBytes, + totSizeInBytes, + totalOverheadInBytes); + } + + /** + * @return true if this compaction candidate is empty, that is it has no sstables to compact. + */ + boolean isEmpty() + { + return sstables.isEmpty(); + } + + boolean hasExpiredOnly() + { + return sstables.size() == expired.size(); + } + + @Override + public int hashCode() + { + return Objects.hash(id, parent, sstables, expired); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + + if (!(obj instanceof CompactionPick)) + return false; + + CompactionPick that = (CompactionPick) obj; + + // a pick is the same if the sstables are the same given that + // the other properties are derived from sstables and two + // picks are the same whether compaction has started or not so + // the progress and completed properties should not determine equality + return id.equals(that.id) + && parent == that.parent + && sstables.equals(that.sstables) + && expired.equals(that.expired); + } + + @Override + public String toString() + { + return String.format("Id: %s, Parent: %d, Hotness: %f, Avg size in bytes: %d, sstables: %s, expired: %s", + id, + parent, + hotness, + avgSizeInBytes, + sstables, + expired); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionProgress.java b/src/java/org/apache/cassandra/db/compaction/CompactionProgress.java new file mode 100644 index 000000000000..9425278df83b --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionProgress.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Clock; + +/** + * The progress information for a compaction operation. This adds compaction + * specific information to {@link TableOperation.Progress}. + */ +public interface CompactionProgress extends TableOperation.Progress +{ + /** + * The compaction strategy if available, otherwise null. + *

    + * The compaction strategy may not be available for some operations that use compaction task such + * as GC or sstable splitting. + * + * @return the compaction strategy when available or null. + */ + @Nullable + CompactionStrategy strategy(); + + /** + * @return input sstables + */ + Collection inSSTables(); + + /** + * @return output sstables + */ + Collection outSSTables(); + + /** + * @return Size on disk (compressed) of the input sstables. + */ + long inputDiskSize(); + + /** + * @return The uncompressed size of the input sstables. + */ + long inputUncompressedSize(); + + /** Same as {@link this#inputDiskSize()} except for LCS where it estimates + * the compressed size for number of keys that will be read from the input sstables, + * see {@link org.apache.cassandra.db.compaction.LeveledCompactionStrategy}. */ + long adjustedInputDiskSize(); + + /** + * @return Size on disk (compressed) of the output sstables. + */ + long outputDiskSize(); + + /** + * @return the number of bytes processed by the compaction iterator. For compressed or encrypted sstables, + * this is the number of bytes processed by the iterator after decompression, so this is the current + * position in the uncompressed sstable files. + */ + long uncompressedBytesRead(); + + /** + * @return the number of bytes processed by the compaction iterator for sstables on the specified level. + * For compressed or encrypted sstables, this is the number of bytes processed by the iterator after decompression, + * so this is the current position in the uncompressed sstable files. + */ + long uncompressedBytesRead(int level); + + /** + * @return the number of bytes that were written before compression is applied (uncompressed size). + */ + long uncompressedBytesWritten(); + + /** + * @return the start time of this operation in millis since the epoch, i.e. as {@link System#currentTimeMillis} + * would report it. + */ + long startTimeMillis(); + + /** + * @return the duration so far in milliseconds. + */ + default long durationInMillis() + { + return Clock.Global.currentTimeMillis() - startTimeMillis(); + } + + /** + * @return total number of partitions read + */ + long partitionsRead(); + + /** + * @return otal number of rows read + */ + long rowsRead(); + + /** + * The partitions histogram maps the number of sstables to the number of partitions that were merged with that number of input sstables. + * + * @return the partitions histogram + */ + long[] partitionsHistogram(); + + /** + * The rows histogram maps the number of sstables to the number of rows that were merged with that number of input sstables. + * + * @return the rows histogram + */ + long[] rowsHistogram(); + + /** + * @return the ratio of bytes before and after compaction, using the adjusted input and output disk sizes (uncompressed values). + */ + default double sizeRatio() + { + long estInputSizeBytes = adjustedInputDiskSize(); + if (estInputSizeBytes > 0) + return outputDiskSize() / (double) estInputSizeBytes; + + // this is a valid case, when there are no sstables to actually compact + // the previous code would return a NaN that would be logged as zero + return 0; + } + + default double readThroughput() + { + long durationMillis = durationInMillis(); + return durationMillis == 0 ? 0 : ((double) uncompressedBytesRead() / durationMillis) * TimeUnit.SECONDS.toMillis(1); + } + + default double writeThroughput() + { + long durationMillis = durationInMillis(); + return durationMillis == 0 ? 0 : ((double) uncompressedBytesWritten() / durationMillis) * TimeUnit.SECONDS.toMillis(1); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionRealm.java b/src/java/org/apache/cassandra/db/compaction/CompactionRealm.java new file mode 100644 index 000000000000..41f033ac23aa --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionRealm.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.io.IOException; +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.DiskBoundaries; +import org.apache.cassandra.db.compaction.unified.Environment; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.index.SecondaryIndexManager; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.metrics.TableMetrics; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.service.snapshot.TableSnapshot; +import org.apache.cassandra.utils.TimeUUID; + +/** + * An interface for supplying the CFS data relevant to compaction. This is implemented by {@link org.apache.cassandra.db.ColumnFamilyStore} and + * works together with the {@link CompactionSSTable} interface as an abstraction of the space where compaction + * strategies operate. + * + * ColumnFamilyStore uses its SSTableReaders (which are already open to serve reads) as the CompactionSSTable instances, + * but alternate implementations can choose to maintain lighter representations (e.g. metadata-only local versions of + * remote sstables) which need only be switched to readers when the compaction is selected for execution and locks the + * sstable copies using {@link #tryModify}. + */ +public interface CompactionRealm extends SSTableReader.Owner +{ + /** + * @return the UCS environment of this table. + */ + Environment makeUCSEnvironment(); + + /** + * @return a {@link ShardManager} for this specific compaction realm. If null is returned, UCS will build its own + * shard manager. + */ + default ShardManager buildShardManager() + { + return null; + } + + /** + * @return the schema metadata of this table. + */ + default TableMetadata metadata() + { + return metadataRef().get(); + } + + /** + * @return the schema metadata of this table as a reference, used for long-living objects to keep up-to-date with + * changes. + */ + TableMetadataRef metadataRef(); + + default String getTableName() + { + return metadata().name; + } + + default String getKeyspaceName() + { + return metadata().keyspace; + } + + AbstractReplicationStrategy getKeyspaceReplicationStrategy(); + + /** + * @return the partitioner used by this table. + */ + default IPartitioner getPartitioner() + { + return metadata().partitioner; + } + + /** + * @return the {@link Directories} backing this table. + */ + Directories getDirectories(); + + /** + * Grabs the global first/last tokens among sstables and returns the range of data directories that start/end with those tokens. + * + * This is done to avoid grabbing the disk boundaries for every sstable in case of huge compactions. + */ + List getDirectoriesForFiles(Set sstables); + + /** + * @return the {@link DiskBoundaries} that are currently applied to the directories backing table. + */ + DiskBoundaries getDiskBoundaries(); + + /** + * @return metrics object for the realm. This can be null during the initial construction of a compaction strategy, + * but should be set when the strategy is asked to select or run compactions. + */ + TableMetrics metrics(); + + /** + * Return the estimated partition count, used when the number of partitions in an sstable is not sufficient to give + * a sensible range estimation. + */ + default long estimatedPartitionCountInSSTables() + { + final long INITIAL_ESTIMATED_PARTITION_COUNT = 1 << 16; // If we don't yet have a count, use a sensible default. + if (metrics() == null) + return INITIAL_ESTIMATED_PARTITION_COUNT; + final Long estimation = metrics().estimatedPartitionCountInSSTablesCached.getValue(); + if (estimation == null || estimation == 0) + return INITIAL_ESTIMATED_PARTITION_COUNT; + return estimation; + } + + /** + * @return the secondary index manager, which is responsible for all secondary indexes. + */ + SecondaryIndexManager getIndexManager(); + + /** + * @return true if tombstones should be purged only from repaired sstables. + */ + boolean onlyPurgeRepairedTombstones(); + + /** + * @param sstables + * @return sstables whose key range overlaps with that of the given sstables, not including itself. + * (The given sstables may or may not overlap with each other.) + */ + Set getOverlappingLiveSSTables(Iterable sstables); + + /** + * @return true if compaction is operating and false if it has been stopped. + */ + boolean isCompactionActive(); + + /** + * @return the compaction parameters associated with this table. + */ + CompactionParams getCompactionParams(); + + /** + * @return true if the table is operating in a mode where no tombstones are allowed to be deleted. + */ + boolean getNeverPurgeTombstones(); + + /** + * @return the minimum compaction threshold for size-tiered compaction (also when used as helper in leveled and + * time-window compaction strategies). + */ + int getMinimumCompactionThreshold(); + /** + * @return the maximum compaction threshold for size-tiered compaction (also when used as helper in leveled and + * time-window compaction strategies). + */ + int getMaximumCompactionThreshold(); + + /** + * @return the write amplification (bytes flushed + bytes compacted / bytes flushed). + */ + default double getWA() + { + TableMetrics metric = metrics(); + if (metric == null) + return 0; + + double bytesCompacted = metric.compactionBytesWritten.getCount(); + double bytesFlushed = metric.bytesFlushed.getCount(); + return bytesFlushed <= 0 ? 0 : (bytesFlushed + bytesCompacted) / bytesFlushed; + } + + /** + * @return the level fanout factor for leveled compaction. + */ + int getLevelFanoutSize(); + /** + * @return true if the table and its compaction strategy support opening of incomplete compaction results early. + */ + boolean supportsEarlyOpen(); + /** + * @return the expected total size of the result of compacting the given sstables, taking into account ranges in + * the sstables that would be thrown away because they are no longer processed by this node. + */ + long getExpectedCompactedFileSize(Iterable sstables, OperationType operationType); + /** + * @return true if compaction should check if the result of an operation fits in the disk space and reduce its scope + * when it does not. + */ + boolean isCompactionDiskSpaceCheckEnabled(); + + /** + * @return all live memtables, or empty if no memtables are available. + */ + Iterable getAllMemtables(); + + /** + * @return the set of all live sstables. + */ + Set getLiveSSTables(); + /** + * @return the set of sstables which are currently compacting. + */ + Set getCompactingSSTables(); + + /** + * Return the subset of the given sstable set which is not currently compacting. + */ + Iterable getNoncompactingSSTables(Iterable sstables); + + /** + * Return the given subset of sstables, i.e. LIVE, NONCOMPACTING or CANONICAL. + */ + Iterable getSSTables(SSTableSet set); + + /** + * Invalidate the given key from local caches. + */ + void invalidateCachedPartition(DecoratedKey key); + + /** + * Construct a descriptor for a new sstable in the given location. + */ + Descriptor newSSTableDescriptor(File locationForDisk); + + /** + * Initiate a transaction to modify the given sstables and operation type, most often a compaction. + * The transaction will convert the given CompactionSSTable handles into open SSTableReaders. + */ + LifecycleTransaction tryModify(Iterable sstables, + OperationType operationType, + TimeUUID id); + + /** + * Initiate a transaction to modify the given sstables and operation type, most often a compaction. + * The transaction will convert the given CompactionSSTable handles into open SSTableReaders. + */ + default LifecycleTransaction tryModify(Iterable sstables, + OperationType operationType) + { + return tryModify(sstables, operationType, LifecycleTransaction.newId()); + } + + /** + * Create an overlap tracker for the given set of source sstables. The tracker is used to identify all sstables + * that overlap with the given sources, which is used to decide if tombstones or other data can be purged. + */ + OverlapTracker getOverlapTracker(Iterable sources); + + interface OverlapTracker extends AutoCloseable + { + /** + * @return all sstables that overlap with the given source set. + */ + Collection overlaps(); + + /** + * @return the sstables whose span covers the given key. + */ + Collection overlaps(DecoratedKey key); + + /** + * Get all the sstables whose span covers the given key, open (i.e. convert to SSTableReader) the ones selected + * by the given filter, and collect the non-null results of applying the given transformation to the resulting + * SSTableReaders. + * Used to select shadow sources (i.e. sources of tombstones or data) for garbage-collecting compactions. + */ + Iterable openSelectedOverlappingSSTables(DecoratedKey key, + Predicate filter, + Function transformation); + + /** + * Refresh the overlapping sstables to reflect compactions applied to any of them. + * Done to avoid holding on to references of obsolete sstables, which will prevent them from being deleted. + */ + boolean maybeRefresh(); + + void refreshOverlaps(); + } + + /** + * Create a CFS snapshot with the given name. + */ + TableSnapshot snapshotWithoutMemtable(String snapshotId); + + /** + * Create a CFS snapshot with the given name and timestamp. + */ + TableSnapshot snapshotWithoutMemtable(String snapshotName, Instant creationTime); + + /** + * Change the repaired status of a set of sstables, usually to reflect a completed repair operation. + */ + int mutateRepairedWithLock(Collection originals, long repairedAt, TimeUUID pendingRepair, boolean isTransient) throws IOException; + + /** + * Signal that a repair session has completed. + */ + void repairSessionCompleted(TimeUUID sessionID); + + boolean shouldIgnoreGcGraceForKey(DecoratedKey dk); + + /** + * Run an operation with concurrent compactions being stopped. + */ + V runWithCompactionsDisabled(Callable callable, OperationType operationType, boolean interruptValidation, boolean interruptViews, TableOperation.StopTrigger trigger); +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionSSTable.java b/src/java/org/apache/cassandra/db/compaction/CompactionSSTable.java new file mode 100644 index 000000000000..2587594b76a5 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionSSTable.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.function.BiPredicate; + +import javax.annotation.Nullable; + +import com.google.common.collect.Ordering; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.sstable.SSTableIdFactory; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Interval; +import org.apache.cassandra.utils.TimeUUID; + +/** + * An SSTable abstraction used by compaction. Implemented by {@link SSTableReader} and provided by + * {@link CompactionRealm} instances. + * + * This abstraction is used to select the sstables to compact. When a compaction is initiated using + * {@link CompactionRealm#tryModify}, the compaction operation receives the SSTableReaders corresponding to the passed + * CompactionSSTables. + */ +public interface CompactionSSTable +{ + // Note: please do not replace with Comparator.comparing, this code can be on a hot path. + Comparator maxTimestampDescending = (o1, o2) -> Long.compare(o2.getMaxTimestamp(), o1.getMaxTimestamp()); + Comparator maxTimestampAscending = (o1, o2) -> Long.compare(o1.getMaxTimestamp(), o2.getMaxTimestamp()); + Comparator firstKeyComparator = (o1, o2) -> o1.getFirst().compareTo(o2.getFirst()); + Comparator lastKeyComparator = (o1, o2) -> o1.getLast().compareTo(o2.getLast()); + Ordering firstKeyOrdering = Ordering.from(firstKeyComparator); + Comparator sizeComparator = (o1, o2) -> Long.compare(o1.onDiskLength(), o2.onDiskLength()); + Comparator idComparator = (o1, o2) -> SSTableIdFactory.COMPARATOR.compare(o1.getId(), o2.getId()); + Comparator idReverseComparator = idComparator.reversed(); + BiPredicate startsAfter = (a, b) -> a.getFirst().compareTo(b.getLast()) > 0; + + /** + * @return the position of the first partition in the sstable + */ + PartitionPosition getFirst(); + + /** + * @return the position of the last partition in the sstable + */ + PartitionPosition getLast(); + + /** + * @return the bounds spanned by this sstable, from first to last keys. + */ + AbstractBounds getBounds(); + + Interval getInterval(); + + /** + * @return the length in bytes of the all on-disk components' file size for this SSTable. + */ + long onDiskComponentsSize(); + + /** + * @return the length in bytes of the on disk data file size for this SSTable. For compressed files, this is not the same + * thing as the data length (see {@link #uncompressedLength}) + */ + long onDiskLength(); + + /** + * @return the length in bytes of the data for this SSTable. For compressed files, this is not the same thing as the + * on disk size (see {@link #onDiskLength}) + */ + long uncompressedLength(); + + /** + * @return the fraction of the token space for which this sstable has content. In the simplest case this is just the + * size of the interval returned by {@link #getBounds()}, but the sstable may contain "holes" when the locally-owned + * range is not contiguous (e.g. with vnodes). + * As this is affected by the local ranges which can change, the token space fraction is calculated at the time of + * writing the sstable and stored with its metadata. + * For older sstables that do not contain this metadata field, this method returns NaN. + */ + double tokenSpaceCoverage(); + + /** + * @return the sum of the on-disk size of the given sstables. + */ + static long getTotalDataBytes(Iterable sstables) + { + long sum = 0; + for (CompactionSSTable sstable : sstables) + sum += sstable.onDiskLength(); + return sum; + } + + /* + * @return the total number of bytes in all on-disk components of the given sstables. + */ + static long getTotalOnDiskComponentsBytes(Iterable sstables) + { + long total = 0; + for (CompactionSSTable sstable : sstables) + total += sstable.onDiskComponentsSize(); + + // We estimate the compaction overhead to be the same as the all components size of the input sstables including SAI files + // This is because even though we have a cache, the output sstable data files will be on disk + // first, and only added to the cache at the end. We could improve flushed sstables, since we know that + // the output will be 1 / RF of the input size, but we don't have this information handy, and normally + // L0 sstables have a small overhead, the overhead is mostly significant for the sstables at the higher levels. + return total; + } + + /** + * @return the sum of the uncompressed size of the given sstables. + */ + static long getTotalUncompressedBytes(Iterable sstables) + { + long sum = 0; + for (CompactionSSTable sstable : sstables) + sum += sstable.uncompressedLength(); + + return sum; + } + + /** + * @return the smallest timestamp of all cells contained in this sstable. + */ + long getMinTimestamp(); + + /** + * @return the largest timestamp of all cells contained in this sstable. + */ + long getMaxTimestamp(); + + /** + * @return the smallest deletion time of all deletions contained in this sstable. + */ + long getMinLocalDeletionTime(); + + /** + * @return the larget deletion time of all deletions contained in this sstable. + */ + long getMaxLocalDeletionTime(); + + /** + * Called by {@link org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy} and other compaction strategies + * to determine the read hotness of this sstables, this method returna a "read hotness" which is calculated by + * looking at the last two hours read rate and dividing this number by the estimated number of keys. + *

    + * Note that some system tables do not have read meters, in which case this method will return zero. + * + * @return the last two hours read rate per estimated key + */ + double hotness(); + + /** + * @return true if this sstable was repaired by a repair service, false otherwise. + */ + boolean isRepaired(); + + /** + * @return the time of repair when isRepaired is true, otherwise UNREPAIRED_SSTABLE. + */ + long getRepairedAt(); + + /** + * @return true if this sstable is pending repair, false otherwise. + */ + boolean isPendingRepair(); + + /** + * @return the id of the repair session when isPendingRepair is true, otherwise null. + */ + @Nullable + TimeUUID getPendingRepair(); + + /** + * @return true if this sstable belongs to a transient range. + */ + boolean isTransient(); + + /** + * @return an estimate of the number of keys in this SSTable based on the index summary. + */ + long estimatedKeys(); + + /** + * @return the level of this sstable according to {@link LeveledCompactionStrategy}, zero for other strategies. + */ + int getSSTableLevel(); + + /** + * @return true if this sstable can take part into a compaction. + */ + boolean isSuitableForCompaction(); + + /** + * @return true if this sstable was marked for obsoletion by a compaction. + */ + boolean isMarkedCompacted(); + + /** + * @return true if this sstable is suspect, that is it was involved in an operation that failed, such + * as a write or read that resulted in {@link CorruptSSTableException}. + */ + boolean isMarkedSuspect(); + + /** + * Whether the sstable may contain tombstones or if it is guaranteed to not contain any. + *

    + * Note that having that method return {@code false} guarantees the sstable has no tombstones whatsoever (so no cell + * tombstone, no range tombstone maker and no expiring columns), but having it return {@code true} doesn't guarantee + * it contains any as it may simply have non-expired cells. + */ + boolean mayHaveTombstones(); + + /** + * The method verifies whether the sstable may contain the provided key. The method does approximation using + * Bloom filter if it is present and if it is not, performs accurate check in the index. + */ + boolean mayContainAssumingKeyIsInRange(DecoratedKey key); + + Descriptor getDescriptor(); + Path getFile(); + default String getColumnFamilyName() + { + return getDescriptor().cfname; + } + default String getKeyspaceName() + { + return getDescriptor().ksname; + } + default SSTableId getId() + { + return getDescriptor().id; + } + + /** + * @param component component to get timestamp. + * @return last modified time for given component. 0 if given component does not exist or IO error occurs. + */ + default long getCreationTimeFor(Component component) + { + return getDescriptor().fileFor(component).lastModified(); + } + + /** + * @return an estimate of the ratio of the tombstones present in the sstable that could be dropped for the given + * garbage collection threshold. + */ + double getEstimatedDroppableTombstoneRatio(long gcBefore); + + /** + * Changes the SSTable level as used by {@link LeveledCompactionStrategy}. + * @throws IOException + */ + void mutateLevelAndReload(int newLevel) throws IOException; + +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategy.java new file mode 100644 index 000000000000..aac6dc52ba29 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategy.java @@ -0,0 +1,239 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.commitlog.IntervalSet; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.SSTableMultiWriter; +import org.apache.cassandra.io.sstable.ScannerList; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; + +/** + * The common interface between legacy compaction strategies (those that extend {@link LegacyAbstractCompactionStrategy} + * and the new compaction strategy, {@link UnifiedCompactionStrategy}. + */ +public interface CompactionStrategy extends CompactionObserver +{ + /** + * @return the compaction logger optionally logs events in a csv file. + */ + CompactionLogger getCompactionLogger(); + + /** + * For internal, temporary suspension of background compactions so that we can do exceptional + * things like truncate or major compaction + */ + void pause(); + + /** + * For internal, temporary suspension of background compactions so that we can do exceptional + * things like truncate or major compaction + */ + void resume(); + + /** + * Performs any extra initialization required + */ + void startup(); + + /** + * Releases any resources if this strategy is shutdown (when the CFS is reloaded after a schema change). + */ + void shutdown(); + + /** + * @param gcBefore throw away tombstones older than this + * + * @return the next background/minor compaction tasks to run; empty if nothing to do. + * + * Is responsible for marking its sstables as compaction-pending. + */ + Collection getNextBackgroundTasks(long gcBefore); + + /** + * @param gcBefore throw away tombstones older than this + * @param splitOutput whether the output of the compaction should be split (only applicable to STCS) + * @param permittedParallelism the maximum number of tasks that can be run in parallel, if the operation can be + * parallelized (UCS with parallelize_output_shards enabled) + * @return compaction tasks that should be run to compact this table as much as possible. + *

    + * Is responsible for marking its sstables as compaction-pending. + */ + @SuppressWarnings("resource") + CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism); + + CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism, OperationType operationType); + + /** + * @param sstables SSTables to compact. Must be marked as compacting. + * @param gcBefore throw away tombstones older than this + * + * @return a compaction task corresponding to the requested sstables. + * Will not be null. (Will throw if user requests an invalid compaction.) + * + * Is responsible for marking its sstables as compaction-pending. + */ + @SuppressWarnings("resource") + CompactionTasks getUserDefinedTasks(Collection sstables, long gcBefore); + + /** + * Get the estimated remaining compactions. + * + * @return the number of background tasks estimated to still be needed for this strategy + */ + int getEstimatedRemainingTasks(); + + int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes, boolean isIncremental); + + /** + * Create a compaction task for the sstables in the transaction. + * + * @return a valid compaction task that can be executed. + */ + AbstractCompactionTask createCompactionTask(LifecycleTransaction txn, long gcBefore, long maxSSTableBytes); + + /** + * @return the total number of background compactions, pending or in progress + */ + int getTotalCompactions(); + + /** + * @return the level for the given transaction, if this strategy supports levels. Otherwise return -1. + */ + default int getLevel(ILifecycleTransaction txn) + { + return -1; + } + + /** + * Return the statistics. Not all strategies will provide non-empty statistics, + * the legacy strategies that do not support aggregates will return empty statistics. + *

    + * @return statistics about this compaction picks. + */ + List getStatistics(); + + /** + * @return size in bytes of the largest sstables for this strategy + */ + long getMaxSSTableBytes(); + + /** + * @return the number of sstables for each level, if this strategy supports levels. Otherwise return an empty array. + */ + int[] getSSTableCountPerLevel(); + + /** + * @return total size on disk for each level. null unless leveled compaction is used. + */ + long[] getPerLevelSizeBytes(); + + /** + * @return true if the table is using LeveledCompactionStrategy. false otherwise. + */ + boolean isLeveledCompaction(); + + /** + * @return sstable count for each bucket in TWCS. null unless time window compaction is used. + */ + int[] getSSTableCountPerTWCSBucket(); + + /** + * @return the level fanout size if applicable to this strategy. Otherwise return the default LCS fanout size. + */ + int getLevelFanoutSize(); + + /** + * Returns a list of KeyScanners given sstables and a range on which to scan. + * The default implementation simply grab one SSTableScanner per-sstable, but overriding this method + * allow for a more memory efficient solution if we know the sstable don't overlap (see + * LeveledCompactionStrategy for instance). + */ + ScannerList getScanners(Collection sstables, Collection> ranges); + + default ScannerList getScanners(Collection toCompact) + { + return getScanners(toCompact, null); + } + + /** + * @return the name of the strategy + */ + String getName(); + + /** + * Returns the sstables managed by the strategy + */ + Set getSSTables(); + + /** + * Group sstables that can be anti-compacted togetehr. + * @param sstablesToGroup + * @return + */ + Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup); + + /** + * Create an sstable writer that is suitable for the strategy. + */ + SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, + long keyCount, + long repairedAt, + TimeUUID pendingRepair, + boolean isTransient, + IntervalSet commitLogPositions, + int sstableLevel, + SerializationHeader header, + Collection indexGroups, + LifecycleNewTracker lifecycleNewTracker); + + /** + * @return true if the strategy supports early open + */ + boolean supportsEarlyOpen(); + + /** + * Return whether this strategy can be used with cursor compaction. + * Currently we report true for all strategies. + */ + default boolean supportsCursorCompaction() + { + return true; + } + + void periodicReport(); + + /** + * Returns a map of sstable regions (e.g. repaired, unrepaired, possibly combined with level information) to the + * maximum overlap between the sstables in the region. + */ + Map getMaxOverlapsMap(); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyContainer.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyContainer.java new file mode 100644 index 000000000000..46535665c989 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyContainer.java @@ -0,0 +1,192 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.cassandra.notifications.INotificationConsumer; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.utils.TimeUUID; + +/** + * A strategy container manages compaction strategies for a {@link ColumnFamilyStore}. + * + * This class is responsible for: + * - providing a single interface for possibly multiple active strategy instances - e.g. due to having + * multiple arenas for repaired, unrepaired, pending, transient SSTables. + * - updating or recreating the strategies when configuration change - e.g. compaction parameters + * or disk boundaries + */ +public interface CompactionStrategyContainer extends CompactionStrategy, INotificationConsumer +{ + /** + * Enable compaction. + */ + void enable(); + + /** + * Disable compaction. + */ + void disable(); + + /** + * @return {@code true} if compaction is enabled and running; e.g. if autocompaction has been disabled via nodetool + * or JMX, this should return {@code false}, even if the underlying compaction strategy hasn't been paused. + */ + boolean isEnabled(); + + /** + * @return {@code true} if compaction is running, i.e. if the underlying compaction strategy is not currently + * paused or being shut down. + */ + boolean isActive(); + + /** + * The reason for reloading + */ + enum ReloadReason + { + /** A new strategy container has been created. */ + FULL, + + /** A new strategy container has been reloaded due to table metadata changes, e.g. a schema change. */ + METADATA_CHANGE, + + /** A request over JMX to update the compaction parameters only locally, without changing the schema permanently. */ + JMX_REQUEST, + + /** The disk boundaries were updated, in this case the strategies may need to be recreated even if the params haven't changed */ + DISK_BOUNDARIES_UPDATED + } + + /** + * Reload the strategy container taking into account the state of the previous strategy container instance + * ({@code this}, in case we're not reloading after switching between containers), the new compaction parameters, + * and the reason for reloading. + *

    + * Depending on the reason, different actions are taken, for example the schema parameters are not updated over + * JMX and the decision on whether to enable or disable compaction depends only on the parameters over JMX, but + * also on the previous JMX directive in case of a full reload. Also, the disk boundaries are not updated over JMX. + *

    + * See the implementations of this method for more details. + * + * @param previous the strategy container instance which state needs to be inherited/taken into account, in many + * cases the same as {@code this}, but never {@code null}. + * @param compactionParams the new compaction parameters + * @param reason the reason for reloading + * + * @return existing or new container with updated parameters + */ + CompactionStrategyContainer reload(@Nonnull CompactionStrategyContainer previous, + CompactionParams compactionParams, + ReloadReason reason); + + /** + * @param params new compaction parameters + * @param reason the reason for reloading + * @return {@code true} if the compaction parameters should be updated on reload + */ + default boolean shouldReload(CompactionParams params, ReloadReason reason) + { + return reason != CompactionStrategyContainer.ReloadReason.METADATA_CHANGE || !params.equals(getMetadataCompactionParams()); + } + + /** + * Creates new {@link CompactionStrategyContainer} and loads its parameters + * + * This method is used by {@link CompactionStrategyFactory} to create a + * {@link CompactionStrategyContainer}s via reflection. + * + * @param previous the strategy container instance which state needs to be inherited/taken into account + * or {@code null} if there was no container to inherit from. + * @param strategyFactory the factory instance responsible for creating the CSM + * @param compactionParams the new compaction parameters + * @param reason the reason for creating a new container + * @param enableAutoCompaction true if auto compaction should be enabled + * + * @return a new {@link CompactionStrategyContainer} with newly loaded parameters + */ + static CompactionStrategyContainer create(@Nullable CompactionStrategyContainer previous, + CompactionStrategyFactory strategyFactory, + CompactionParams compactionParams, + CompactionStrategyContainer.ReloadReason reason, + boolean enableAutoCompaction) + { + throw new UnsupportedOperationException("Implementations of CompactionStrategyContainer must implement static create method"); + } + + /** + * Return the compaction parameters. These are not necessarily the same as the ones specified in the schema, they + * may have been overwritten over JMX. + * + * @return the compaction params currently active + */ + CompactionParams getCompactionParams(); + + /** + * Returns the compaction parameters set via metadata. + * + * This method is useful to decide if we should update the compaction strategy due to a + * metadata change such as a schema changed caused by an ALTER TABLE. + * + * If a user changes the local compaction strategy via JMX and then later ALTERs a compaction parameter, + * we will use the new compaction parameters but we will not override the JMX parameters if compaction + * was not changed by the ALTER. + * + * @return the compaction parameters set via metadata changes + */ + CompactionParams getMetadataCompactionParams(); + + /** + * This method is to keep compatibility with strategies baked by {@link CompactionStrategyManager} where + * there are multiple inner strategies handling sstables by repair status. + * + * @return all inner compaction strategies + */ + List getStrategies(); + + /** + * This method is to keep compatibility with strategies baked by {@link CompactionStrategyManager} where + * there are multiple inner strategies handling sstables by repair status. + * + * Note that if {@code isRepaired} is true, {@code pendingRepair} must be null. + * + * @param isRepaired will return strategies for repaired SSTables; must be {@code false} if + * {@code pendingRepair} is specified + * @param pendingRepair will return strategies for the given pending repair; must be {@code null} + * if {@code isRepaired} is true + * + * @return a list of inner strategies that match given parameters + */ + List getStrategies(boolean isRepaired, @Nullable TimeUUID pendingRepair); + + /** + * Called to clean up state when a repair session completes. + * + * @param sessionID repair session id. + */ + void repairSessionCompleted(TimeUUID sessionID); + + /** + * The method is for CompactionStrategyManager to use with {@link CompactionRealm#mutateRepairedWithLock}. + * UnifiedCompactionContainer does not need it. + */ + ReentrantReadWriteLock.WriteLock getWriteLock(); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyFactory.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyFactory.java new file mode 100644 index 000000000000..83d4aefe9ed5 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyFactory.java @@ -0,0 +1,188 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import javax.annotation.Nullable; + +import org.apache.cassandra.schema.CompactionParams; + +/** + * The factory for compaction strategies and their containers. + */ +public class CompactionStrategyFactory +{ + private final CompactionRealm realm; + private final CompactionLogger compactionLogger; + + public CompactionStrategyFactory(CompactionRealm realm) + { + this.realm = realm; + this.compactionLogger = new CompactionLogger(realm.metadata()); + } + + /** + * Reload the existing strategy container, possibly creating a new one if required. + * + * @param current the current strategy container, or {@code null} if this is the first time we're loading a + * compaction strategy + * @param compactionParams the new compaction parameters + * @param reason the reason for reloading + * @param enableAutoCompaction true if auto compaction should be enabled + * + * @return Either a new strategy container or the current one, but reloaded with the given compaction parameters. + */ + public CompactionStrategyContainer reload(@Nullable CompactionStrategyContainer current, + CompactionParams compactionParams, + CompactionStrategyContainer.ReloadReason reason, + boolean enableAutoCompaction) + { + // If we were called due to a metadata change but the compaction parameters are the same then + // don't reload since we risk overriding parameters set via JMX + if (current != null && !current.shouldReload(compactionParams, reason)) + return current; + + Class containerClass = containerForStrategy(compactionParams.klass()); + CompactionStrategyContainer ret; + + // if the strategy belongs to the same container, we can just reload + if (current != null && current.getClass().equals(containerClass)) + ret = current.reload(current, compactionParams, reason); + else + { + // otherwise we need to re-create the container + ret = createStrategyContainer(containerClass, current, compactionParams, reason, enableAutoCompaction); + } + + return ret; + } + + static boolean enableCompactionOnReload(@Nullable CompactionStrategyContainer previous, + CompactionParams compactionParams, + CompactionStrategyContainer.ReloadReason reason) + { + // If this is a JMX request, we only consider the params passed by it + if (reason == CompactionStrategyContainer.ReloadReason.JMX_REQUEST) + return compactionParams.isEnabled(); + // If the enabled state flag and the params of the previous container differ, compaction was forcefully + // enabled/disabled by JMX/nodetool, and we should inherit that setting through the enabled state flag + if (previous != null && previous.isEnabled() != previous.getCompactionParams().isEnabled()) + return previous.isEnabled(); + + return compactionParams.isEnabled(); + } + + /** + * Returns a {@link CompactionStrategyContainer} class for the given strategy class. + * + * We need this method to create correct container for the strategy, but also to distinguish + * between situations when a container should reloaded or recreated. + */ + private Class containerForStrategy(Class strategyClass) + { + Class containerClass; + try + { + Field containerClassField = strategyClass.getField("CONTAINER_CLASS"); + containerClass = (Class) containerClassField.get(null); + } + catch (IllegalAccessException | NoSuchFieldException e) + { + containerClass = CompactionStrategyManager.class; + } + + return containerClass; + } + + private CompactionStrategyContainer createStrategyContainer(Class containerClass, + CompactionStrategyContainer previous, + CompactionParams compactionParams, + CompactionStrategyContainer.ReloadReason reason, + boolean enableAutoCompaction) + { + CompactionStrategyContainer ret; + try + { + Method createMethod = containerClass.getMethod("create", + CompactionStrategyContainer.class, + CompactionStrategyFactory.class, + CompactionParams.class, + CompactionStrategyContainer.ReloadReason.class, + boolean.class); + ret = (CompactionStrategyContainer) createMethod.invoke(null, + previous, + this, + compactionParams, + reason, + enableAutoCompaction); + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) + { + ret = new CompactionStrategyManager(this, enableAutoCompaction); + ret.reload(previous, compactionParams, reason); + } + return ret; + } + + public CompactionLogger getCompactionLogger() + { + return compactionLogger; + } + + public CompactionRealm getRealm() + { + return realm; + } + + /** + * Creates a compaction strategy that is managed by {@link CompactionStrategyManager} and its strategy holders. + * These strategies must extend {@link LegacyAbstractCompactionStrategy}. + * + * @return an instance of the compaction strategy specified in the parameters so long as it extends {@link LegacyAbstractCompactionStrategy} + * @throws IllegalArgumentException if the params do not contain a strategy that extends {@link LegacyAbstractCompactionStrategy} + */ + LegacyAbstractCompactionStrategy createLegacyStrategy(CompactionParams compactionParams) + { + try + { + if (!LegacyAbstractCompactionStrategy.class.isAssignableFrom(compactionParams.klass())) + throw new IllegalArgumentException("Expected compaction params for legacy strategy: " + compactionParams); + + Constructor constructor = + compactionParams.klass().getConstructor(CompactionStrategyFactory.class, Map.class); + LegacyAbstractCompactionStrategy ret = (LegacyAbstractCompactionStrategy) constructor.newInstance(this, compactionParams.options()); + compactionLogger.strategyCreated(ret); + return ret; + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) + { + throw org.apache.cassandra.utils.Throwables.cleaned(e); + } + } + + /** + * Create a compaction strategy. This is only called by tiered storage so we forward to the legacy strategy. + */ + public CompactionStrategy createStrategy(CompactionParams compactionParams) + { + return createLegacyStrategy(compactionParams); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyHolder.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyHolder.java index becd3b954af1..1a12bcc77751 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyHolder.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyHolder.java @@ -25,7 +25,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; @@ -43,25 +42,25 @@ public class CompactionStrategyHolder extends AbstractStrategyHolder { - private final List strategies = new ArrayList<>(); + private final List strategies = new ArrayList<>(); private final boolean isRepaired; - public CompactionStrategyHolder(ColumnFamilyStore cfs, DestinationRouter router, boolean isRepaired) + public CompactionStrategyHolder(CompactionRealm realm, CompactionStrategyFactory strategyFactory, DestinationRouter router, boolean isRepaired) { - super(cfs, router); + super(realm, strategyFactory, router); this.isRepaired = isRepaired; } @Override public void startup() { - strategies.forEach(AbstractCompactionStrategy::startup); + strategies.forEach(CompactionStrategy::startup); } @Override public void shutdown() { - strategies.forEach(AbstractCompactionStrategy::shutdown); + strategies.forEach(CompactionStrategy::shutdown); } @Override @@ -69,7 +68,7 @@ public void setStrategyInternal(CompactionParams params, int numTokenPartitions) { strategies.clear(); for (int i = 0; i < numTokenPartitions; i++) - strategies.add(cfs.createCompactionStrategyInstance(params)); + strategies.add(strategyFactory.createLegacyStrategy(params)); } @Override @@ -89,43 +88,41 @@ public boolean managesRepairedGroup(boolean isRepaired, boolean isPendingRepair, } @Override - public AbstractCompactionStrategy getStrategyFor(SSTableReader sstable) + public LegacyAbstractCompactionStrategy getStrategyFor(CompactionSSTable sstable) { Preconditions.checkArgument(managesSSTable(sstable), "Attempting to get compaction strategy from wrong holder"); return strategies.get(router.getIndexForSSTable(sstable)); } @Override - public Iterable allStrategies() + public Iterable allStrategies() { return strategies; } @Override - public Collection getBackgroundTaskSuppliers(long gcBefore) + public Collection getBackgroundTaskSuppliers(long gcBefore) { - List suppliers = new ArrayList<>(strategies.size()); - for (AbstractCompactionStrategy strategy : strategies) - suppliers.add(new TaskSupplier(strategy.getEstimatedRemainingTasks(), () -> strategy.getNextBackgroundTask(gcBefore))); + List suppliers = new ArrayList<>(strategies.size()); + for (CompactionStrategy strategy : strategies) + suppliers.add(new TasksSupplier(strategy.getEstimatedRemainingTasks(), () -> strategy.getNextBackgroundTasks(gcBefore))); return suppliers; } @Override - public Collection getMaximalTasks(long gcBefore, boolean splitOutput) + public Collection getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) { List tasks = new ArrayList<>(strategies.size()); - for (AbstractCompactionStrategy strategy : strategies) + for (CompactionStrategy strategy : strategies) { - Collection task = strategy.getMaximalTask(gcBefore, splitOutput); - if (task != null) - tasks.addAll(task); + tasks.addAll(strategy.getMaximalTasks(gcBefore, splitOutput, permittedParallelism)); } return tasks; } @Override - public Collection getUserDefinedTasks(GroupedSSTableContainer sstables, long gcBefore) + public Collection getUserDefinedTasks(GroupedSSTableContainer sstables, long gcBefore) { List tasks = new ArrayList<>(strategies.size()); for (int i = 0; i < strategies.size(); i++) @@ -133,19 +130,13 @@ public Collection getUserDefinedTasks(GroupedSSTableCont if (sstables.isGroupEmpty(i)) continue; - tasks.add(strategies.get(i).getUserDefinedTask(sstables.getGroup(i), gcBefore)); + tasks.addAll(strategies.get(i).getUserDefinedTasks(sstables.getGroup(i), gcBefore)); } return tasks; } @Override - public void addSSTable(SSTableReader sstable) - { - getStrategyFor(sstable).addSSTable(sstable); - } - - @Override - public void addSSTables(GroupedSSTableContainer sstables) + public void addSSTables(GroupedSSTableContainer sstables) { Preconditions.checkArgument(sstables.numGroups() == strategies.size()); for (int i = 0; i < strategies.size(); i++) @@ -156,7 +147,7 @@ public void addSSTables(GroupedSSTableContainer sstables) } @Override - public void removeSSTables(GroupedSSTableContainer sstables) + public void removeSSTables(GroupedSSTableContainer sstables) { Preconditions.checkArgument(sstables.numGroups() == strategies.size()); for (int i = 0; i < strategies.size(); i++) @@ -167,7 +158,7 @@ public void removeSSTables(GroupedSSTableContainer sstables) } @Override - public void replaceSSTables(GroupedSSTableContainer removed, GroupedSSTableContainer added) + public void replaceSSTables(GroupedSSTableContainer removed, GroupedSSTableContainer added) { Preconditions.checkArgument(removed.numGroups() == strategies.size()); Preconditions.checkArgument(added.numGroups() == strategies.size()); @@ -189,7 +180,7 @@ public AbstractCompactionStrategy first() } @Override - public List getScanners(GroupedSSTableContainer sstables, Collection> ranges) + public List getScanners(GroupedSSTableContainer sstables, Collection> ranges) { List scanners = new ArrayList<>(strategies.size()); for (int i = 0; i < strategies.size(); i++) @@ -202,13 +193,13 @@ public List getScanners(GroupedSSTableContainer sstables, Colle return scanners; } - Collection> groupForAnticompaction(Iterable sstables) + Collection> groupForAnticompaction(Iterable sstables) { Preconditions.checkState(!isRepaired); - GroupedSSTableContainer group = createGroupedSSTableContainer(); + GroupedSSTableContainer group = this.createGroupedSSTableContainer(); sstables.forEach(group::add); - Collection> anticompactionGroups = new ArrayList<>(); + Collection> anticompactionGroups = new ArrayList<>(); for (int i = 0; i < strategies.size(); i++) { if (group.isGroupEmpty(i)) @@ -245,7 +236,7 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, Preconditions.checkArgument(pendingRepair == null, "CompactionStrategyHolder can't create sstable writer with pendingRepair id"); // to avoid creating a compaction strategy for the wrong pending repair manager, we get the index based on where the sstable is to be written - AbstractCompactionStrategy strategy = strategies.get(router.getIndexForSSTableDirectory(descriptor)); + CompactionStrategy strategy = strategies.get(router.getIndexForSSTableDirectory(descriptor)); return strategy.createSSTableMultiWriter(descriptor, keyCount, repairedAt, @@ -259,13 +250,7 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, } @Override - public int getStrategyIndex(AbstractCompactionStrategy strategy) - { - return strategies.indexOf(strategy); - } - - @Override - public boolean containsSSTable(SSTableReader sstable) + public boolean containsSSTable(CompactionSSTable sstable) { return Iterables.any(strategies, acs -> acs.getSSTables().contains(sstable)); } diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java index 010d4d77d253..36ae60677b93 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java @@ -18,7 +18,6 @@ package org.apache.cassandra.db.compaction; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -26,9 +25,9 @@ import java.util.Comparator; import java.util.ConcurrentModificationException; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -36,24 +35,21 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.common.primitives.Longs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.DiskBoundaries; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; -import org.apache.cassandra.db.compaction.AbstractStrategyHolder.TaskSupplier; -import org.apache.cassandra.db.compaction.PendingRepairManager.CleanupTask; +import org.apache.cassandra.db.compaction.AbstractStrategyHolder.TasksSupplier; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.lifecycle.SSTableSet; @@ -64,18 +60,13 @@ import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.SSTableMultiWriter; -import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.sstable.metadata.StatsMetadata; -import org.apache.cassandra.io.util.File; import org.apache.cassandra.notifications.INotification; -import org.apache.cassandra.notifications.INotificationConsumer; import org.apache.cassandra.notifications.SSTableAddedNotification; import org.apache.cassandra.notifications.SSTableDeletingNotification; import org.apache.cassandra.notifications.SSTableListChangedNotification; -import org.apache.cassandra.notifications.SSTableMetadataChanged; import org.apache.cassandra.notifications.SSTableRepairStatusChanged; -import org.apache.cassandra.repair.consistent.admin.CleanupSummary; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.utils.TimeUUID; @@ -99,17 +90,18 @@ * * Whenever the {@link DiskBoundaries} change, the compaction strategies must be reloaded, so in order to ensure * the compaction strategy placement reflect most up-to-date disk boundaries, call {@link this#maybeReloadDiskBoundaries()} - * before acquiring the read lock to acess the strategies. + * before acquiring the read lock to access the strategies. * */ -public class CompactionStrategyManager implements INotificationConsumer +public class CompactionStrategyManager implements CompactionStrategyContainer { private static final Logger logger = LoggerFactory.getLogger(CompactionStrategyManager.class); public final CompactionLogger compactionLogger; - private final ColumnFamilyStore cfs; + private final CompactionRealm realm; private final boolean partitionSSTablesByTokenRange; private final Supplier boundariesSupplier; + private final boolean enableAutoCompaction; /** * Performs mutual exclusion on the variables below @@ -134,13 +126,16 @@ public class CompactionStrategyManager implements INotificationConsumer private volatile boolean isActive = true; /* - We keep a copy of the schema compaction parameters here to be able to decide if we - should update the compaction strategy in maybeReload() due to an ALTER. + We keep a copy of the table metadata compaction parameters here to be able to decide if we + should update the compaction strategy due to a metadata change such as a schema changed + caused by an ALTER TABLE. - If a user changes the local compaction strategy and then later ALTERs a compaction parameter, - we will use the new compaction parameters. + If a user changes the local compaction strategy via JMX and then later ALTERs a compaction parameter, + we will use the new compaction parameters but we will not override the JMX parameters if compaction + was not changed by the ALTER. */ - private volatile CompactionParams schemaCompactionParams; + @SuppressWarnings("thread-safe") + private volatile CompactionParams metadataParams; private volatile boolean supportsEarlyOpen; private volatile int fanout; private volatile long maxSSTableSizeBytes; @@ -148,18 +143,24 @@ should update the compaction strategy in maybeReload() due to an ALTER. public static int TWCS_BUCKET_COUNT_MAX = 128; - public CompactionStrategyManager(ColumnFamilyStore cfs) + + public CompactionStrategyManager(CompactionStrategyFactory strategyFactory, boolean enableAutoCompaction) { - this(cfs, cfs::getDiskBoundaries, cfs.getPartitioner().splitter().isPresent()); + this(strategyFactory, + () -> strategyFactory.getRealm().getDiskBoundaries(), + strategyFactory.getRealm().getPartitioner().splitter().isPresent(), + enableAutoCompaction); } @VisibleForTesting - public CompactionStrategyManager(ColumnFamilyStore cfs, Supplier boundariesSupplier, - boolean partitionSSTablesByTokenRange) + public CompactionStrategyManager(CompactionStrategyFactory strategyFactory, + Supplier boundariesSupplier, + boolean partitionSSTablesByTokenRange, + boolean enableAutoCompaction) { AbstractStrategyHolder.DestinationRouter router = new AbstractStrategyHolder.DestinationRouter() { - public int getIndexForSSTable(SSTableReader sstable) + public int getIndexForSSTable(CompactionSSTable sstable) { return compactionStrategyIndexFor(sstable); } @@ -169,68 +170,82 @@ public int getIndexForSSTableDirectory(Descriptor descriptor) return compactionStrategyIndexForDirectory(descriptor); } }; - transientRepairs = new PendingRepairHolder(cfs, router, true); - pendingRepairs = new PendingRepairHolder(cfs, router, false); - repaired = new CompactionStrategyHolder(cfs, router, true); - unrepaired = new CompactionStrategyHolder(cfs, router, false); + + this.enableAutoCompaction = enableAutoCompaction; + realm = strategyFactory.getRealm(); + + transientRepairs = new PendingRepairHolder(realm, strategyFactory, router, true); + pendingRepairs = new PendingRepairHolder(realm, strategyFactory, router, false); + repaired = new CompactionStrategyHolder(realm, strategyFactory, router, true); + unrepaired = new CompactionStrategyHolder(realm, strategyFactory, router, false); holders = ImmutableList.of(transientRepairs, pendingRepairs, repaired, unrepaired); - cfs.getTracker().subscribe(this); - logger.trace("{} subscribed to the data tracker.", this); - this.cfs = cfs; - this.compactionLogger = new CompactionLogger(cfs, this); + compactionLogger = strategyFactory.getCompactionLogger(); this.boundariesSupplier = boundariesSupplier; this.partitionSSTablesByTokenRange = partitionSSTablesByTokenRange; currentBoundaries = boundariesSupplier.get(); - params = schemaCompactionParams = cfs.metadata().params.compaction; + params = realm.metadata().params.compaction; enabled = params.isEnabled(); - setStrategy(schemaCompactionParams); - startup(); + } + + public static CompactionStrategyContainer create(@Nullable CompactionStrategyContainer previous, + CompactionStrategyFactory strategyFactory, + CompactionParams compactionParams, + CompactionStrategyContainer.ReloadReason reason, + boolean enableAutoCompaction) + { + CompactionStrategyManager csm = new CompactionStrategyManager(strategyFactory, enableAutoCompaction); + csm.reload(previous != null ? previous : csm, compactionParams, reason); + return csm; } /** * Return the next background task * - * Returns a task for the compaction strategy that needs it the most (most estimated remaining tasks) - */ - public AbstractCompactionTask getNextBackgroundTask(long gcBefore) + * Legacy strategies will always return one task but we wrap this in a collection because new strategies + * might return multiple tasks. + * + * @return the task for the compaction strategy that needs it the most (most estimated remaining tasks) */ + @Override + public Collection getNextBackgroundTasks(long gcBefore) { maybeReloadDiskBoundaries(); readLock.lock(); try { if (!isEnabled()) - return null; + return ImmutableList.of(); int numPartitions = getNumTokenPartitions(); // first try to promote/demote sstables from completed repairs - AbstractCompactionTask repairFinishedTask; - repairFinishedTask = pendingRepairs.getNextRepairFinishedTask(); - if (repairFinishedTask != null) - return repairFinishedTask; + Collection repairFinishedTasks; + repairFinishedTasks = pendingRepairs.getNextRepairFinishedTasks(); + if (!repairFinishedTasks.isEmpty()) + return repairFinishedTasks; - repairFinishedTask = transientRepairs.getNextRepairFinishedTask(); - if (repairFinishedTask != null) - return repairFinishedTask; + repairFinishedTasks = transientRepairs.getNextRepairFinishedTasks(); + if (!repairFinishedTasks.isEmpty()) + return repairFinishedTasks; // sort compaction task suppliers by remaining tasks descending - List suppliers = new ArrayList<>(numPartitions * holders.size()); + List suppliers = new ArrayList<>(numPartitions * holders.size()); for (AbstractStrategyHolder holder : holders) suppliers.addAll(holder.getBackgroundTaskSuppliers(gcBefore)); Collections.sort(suppliers); - // return the first non-null task - for (TaskSupplier supplier : suppliers) + // return the first non-empty list, we could enhance it to return all tasks of all + // suppliers but this would change existing behavior + for (TasksSupplier supplier : suppliers) { - AbstractCompactionTask task = supplier.getTask(); - if (task != null) - return task; + Collection tasks = supplier.getTasks(); + if (!tasks.isEmpty()) + return tasks; } - return null; + return ImmutableList.of(); } finally { @@ -238,46 +253,25 @@ public AbstractCompactionTask getNextBackgroundTask(long gcBefore) } } - /** - * finds the oldest (by modification date) non-latest-version sstable on disk and creates an upgrade task for it - * @return - */ - @VisibleForTesting - AbstractCompactionTask findUpgradeSSTableTask() + @Override + public CompactionLogger getCompactionLogger() { - if (!isEnabled() || !DatabaseDescriptor.automaticSSTableUpgrade()) - return null; - Set compacting = cfs.getTracker().getCompacting(); - List potentialUpgrade = cfs.getLiveSSTables() - .stream() - .filter(s -> !compacting.contains(s) && !s.descriptor.version.isLatestVersion()) - .sorted((o1, o2) -> { - File f1 = o1.descriptor.fileFor(Components.DATA); - File f2 = o2.descriptor.fileFor(Components.DATA); - return Longs.compare(f1.lastModified(), f2.lastModified()); - }).collect(Collectors.toList()); - for (SSTableReader sstable : potentialUpgrade) - { - LifecycleTransaction txn = cfs.getTracker().tryModify(sstable, OperationType.UPGRADE_SSTABLES); - if (txn != null) - { - logger.debug("Running automatic sstable upgrade for {}", sstable); - return getCompactionStrategyFor(sstable).getCompactionTask(txn, Integer.MIN_VALUE, Long.MAX_VALUE); - } - } - return null; + return compactionLogger; } + @Override public boolean isEnabled() { - return enabled && isActive; + return enableAutoCompaction && enabled && isActive; } + @Override public boolean isActive() { return isActive; } + @Override public void resume() { writeLock.lock(); @@ -296,6 +290,7 @@ public void resume() * * Separate call from enable/disable to not have to save the enabled-state externally */ + @Override public void pause() { writeLock.lock(); @@ -310,19 +305,19 @@ public void pause() } - private void startup() + @Override + public void startup() { writeLock.lock(); try { - for (SSTableReader sstable : cfs.getSSTables(SSTableSet.CANONICAL)) + for (CompactionSSTable sstable : realm.getSSTables(SSTableSet.CANONICAL)) { - if (sstable.openReason != SSTableReader.OpenReason.EARLY) + if (sstable.isSuitableForCompaction()) compactionStrategyFor(sstable).addSSTable(sstable); } holders.forEach(AbstractStrategyHolder::startup); supportsEarlyOpen = repaired.first().supportsEarlyOpen(); - fanout = (repaired.first() instanceof LeveledCompactionStrategy) ? ((LeveledCompactionStrategy) repaired.first()).getLevelFanoutSize() : LeveledCompactionStrategy.DEFAULT_LEVEL_FANOUT_SIZE; maxSSTableSizeBytes = repaired.first().getMaxSSTableBytes(); name = repaired.first().getName(); } @@ -331,25 +326,23 @@ private void startup() writeLock.unlock(); } - if (repaired.first().logAll) + if (repaired.first().getOptions().isLogEnabled()) compactionLogger.enable(); } /** - * return the compaction strategy for the given sstable - * * returns differently based on the repaired status and which vnode the compaction strategy belongs to * @param sstable - * @return + * @return the compaction strategy for the given sstable */ - public AbstractCompactionStrategy getCompactionStrategyFor(SSTableReader sstable) + LegacyAbstractCompactionStrategy getCompactionStrategyFor(CompactionSSTable sstable) { maybeReloadDiskBoundaries(); return compactionStrategyFor(sstable); } @VisibleForTesting - AbstractCompactionStrategy compactionStrategyFor(SSTableReader sstable) + LegacyAbstractCompactionStrategy compactionStrategyFor(CompactionSSTable sstable) { // should not call maybeReloadDiskBoundaries because it may be called from within lock readLock.lock(); @@ -374,7 +367,7 @@ AbstractCompactionStrategy compactionStrategyFor(SSTableReader sstable) * @param sstable * @return */ - int compactionStrategyIndexFor(SSTableReader sstable) + int compactionStrategyIndexFor(CompactionSSTable sstable) { // should not call maybeReloadDiskBoundaries because it may be called from within lock readLock.lock(); @@ -385,7 +378,7 @@ int compactionStrategyIndexFor(SSTableReader sstable) if (!partitionSSTablesByTokenRange) return 0; - return currentBoundaries.getDiskIndex(sstable); + return currentBoundaries.getDiskIndexFromKey(sstable); } finally { @@ -430,33 +423,7 @@ PendingRepairHolder getTransientRepairsUnsafe() return transientRepairs; } - public boolean hasDataForPendingRepair(TimeUUID sessionID) - { - readLock.lock(); - try - { - return pendingRepairs.hasDataForSession(sessionID) || transientRepairs.hasDataForSession(sessionID); - } - finally - { - readLock.unlock(); - } - } - - @VisibleForTesting - public boolean hasPendingRepairSSTable(TimeUUID sessionID, SSTableReader sstable) - { - readLock.lock(); - try - { - return pendingRepairs.hasPendingRepairSSTable(sessionID, sstable) || transientRepairs.hasPendingRepairSSTable(sessionID, sstable); - } - finally - { - readLock.unlock(); - } - } - + @Override public void shutdown() { writeLock.lock(); @@ -473,19 +440,30 @@ public void shutdown() } /** - * Maybe reload the compaction strategies. Called after changing configuration. + * Checks if the disk boundaries changed and reloads the compaction strategies + * to reflect the most up-to-date disk boundaries. + *

    + * This is typically called before acquiring the {@link this#readLock} to ensure the most up-to-date + * disk locations and boundaries are used. + *

    + * This should *never* be called inside by a thread holding the {@link this#readLock}, since it + * will potentially acquire the {@link this#writeLock} to update the compaction strategies + * what can cause a deadlock. + *

    + * TODO: improve this to reload after receiving a notification rather than trying to reload on every operation */ - public void maybeReloadParamsFromSchema(CompactionParams params) + @VisibleForTesting + protected void maybeReloadDiskBoundaries() { - // compare the old schema configuration to the new one, ignore any locally set changes. - if (params.equals(schemaCompactionParams)) + if (!currentBoundaries.isOutOfDate()) return; writeLock.lock(); try { - if (!params.equals(schemaCompactionParams)) - reloadParamsFromSchema(params); + if (!currentBoundaries.isOutOfDate()) + return; + doReload(this, params, ReloadReason.DISK_BOUNDARIES_UPDATED); } finally { @@ -493,125 +471,67 @@ public void maybeReloadParamsFromSchema(CompactionParams params) } } - /** - * @param newParams new CompactionParams set in via CQL - */ - private void reloadParamsFromSchema(CompactionParams newParams) - { - logger.debug("Recreating compaction strategy for {}.{} - compaction parameters changed via CQL", - cfs.getKeyspaceName(), cfs.getTableName()); - - /* - * It's possible for compaction to be explicitly enabled/disabled - * via JMX when already enabled/disabled via params. In that case, - * if we now toggle enabled/disabled via params, we'll technically - * be overriding JMX-set value with params-set value. - */ - boolean enabledWithJMX = enabled && !shouldBeEnabled(); - boolean disabledWithJMX = !enabled && shouldBeEnabled(); - - schemaCompactionParams = newParams; - setStrategy(newParams); - - // enable/disable via JMX overrides CQL params, but please see the comment above - if (enabled && !shouldBeEnabled() && !enabledWithJMX) - disable(); - else if (!enabled && shouldBeEnabled() && !disabledWithJMX) - enable(); - - startup(); - } - - private void maybeReloadParamsFromJMX(CompactionParams params) + @Override + public CompactionStrategyContainer reload(@Nonnull CompactionStrategyContainer previous, CompactionParams newCompactionParams, ReloadReason reason) { - // compare the old local configuration to the new one, ignoring schema - if (params.equals(this.params)) - return; - writeLock.lock(); try { - if (!params.equals(this.params)) - reloadParamsFromJMX(params); + doReload(previous, newCompactionParams, reason); } finally { writeLock.unlock(); } + if (previous != this) + previous.shutdown(); + + return this; } - /** - * @param newParams new CompactionParams set via JMX - */ - private void reloadParamsFromJMX(CompactionParams newParams) + private void doReload(CompactionStrategyContainer previous, CompactionParams compactionParams, ReloadReason reason) { - logger.debug("Recreating compaction strategy for {}.{} - compaction parameters changed via JMX", - cfs.getKeyspaceName(), cfs.getTableName()); + boolean updateDiskBoundaries = currentBoundaries == null || currentBoundaries.isOutOfDate(); + boolean enabledOnReload = CompactionStrategyFactory.enableCompactionOnReload(previous, compactionParams, reason) && enableAutoCompaction; - setStrategy(newParams); + logger.debug("Recreating compaction strategy for {}.{}, reason: {}, params updated: {}, disk boundaries updated: {}, enabled: {}, params: {} -> {}, metadataParams: {}", + realm.getKeyspaceName(), realm.getTableName(), reason, !compactionParams.equals(params), updateDiskBoundaries, enabledOnReload, params, compactionParams, metadataParams); - // compaction params set via JMX override enable/disable via JMX - if (enabled && !shouldBeEnabled()) - disable(); - else if (!enabled && shouldBeEnabled()) - enable(); + if (updateDiskBoundaries) + currentBoundaries = boundariesSupplier.get(); - startup(); - } + int numPartitions = getNumTokenPartitions(); + for (AbstractStrategyHolder holder : holders) + holder.setStrategy(compactionParams, numPartitions); - /** - * Checks if the disk boundaries changed and reloads the compaction strategies - * to reflect the most up-to-date disk boundaries. - *

    - * This is typically called before acquiring the {@link this#readLock} to ensure the most up-to-date - * disk locations and boundaries are used. - *

    - * This should *never* be called inside by a thread holding the {@link this#readLock}, since it - * will potentially acquire the {@link this#writeLock} to update the compaction strategies - * what can cause a deadlock. - *

    - * TODO: improve this to reload after receiving a notification rather than trying to reload on every operation - */ - @VisibleForTesting - protected void maybeReloadDiskBoundaries() - { - if (!currentBoundaries.isOutOfDate()) - return; + params = compactionParams; - writeLock.lock(); - try - { - if (currentBoundaries.isOutOfDate()) - reloadDiskBoundaries(boundariesSupplier.get()); - } - finally - { - writeLock.unlock(); - } - } + // full reload or switch from a strategy not managed by CompactionStrategyManager + if (metadataParams == null || reason == ReloadReason.FULL) + metadataParams = realm.metadata().params.compaction; + else if (reason == ReloadReason.METADATA_CHANGE) + // metadataParams are aligned with compactionParams. We do not access TableParams.COMPACTION to avoid racing with + // concurrent ALTER TABLE metadata change. + metadataParams = compactionParams; + + // no-op for DISK_BOUNDARIES_UPDATED and JMX_REQUEST. DISK_BOUNDARIES_UPDATED does not change compaction params + // and JMX changes do not affect table metadata - /** - * @param newBoundaries new DiskBoundaries - potentially functionally equivalent to current ones - */ - private void reloadDiskBoundaries(DiskBoundaries newBoundaries) - { - DiskBoundaries oldBoundaries = currentBoundaries; - currentBoundaries = newBoundaries; - if (newBoundaries.isEquivalentTo(oldBoundaries)) + if (params.maxCompactionThreshold() <= 0 || params.minCompactionThreshold() <= 0) { - logger.debug("Not recreating compaction strategy for {}.{} - disk boundaries are equivalent", - cfs.getKeyspaceName(), cfs.getTableName()); - return; + logger.warn("Disabling compaction strategy by setting compaction thresholds to 0 is deprecated, set the compaction option 'enabled' to 'false' instead."); + disable(); } + else if (!enabledOnReload) + disable(); + else + enable(); - logger.debug("Recreating compaction strategy for {}.{} - disk boundaries are out of date", - cfs.getKeyspaceName(), cfs.getTableName()); - setStrategy(params); startup(); } - private Iterable getAllStrategies() + private Iterable getAllStrategies() { return Iterables.concat(Iterables.transform(holders, AbstractStrategyHolder::allStrategies)); } @@ -625,7 +545,7 @@ public int getUnleveledSSTables() if (repaired.first() instanceof LeveledCompactionStrategy) { int count = 0; - for (AbstractCompactionStrategy strategy : getAllStrategies()) + for (CompactionStrategy strategy : getAllStrategies()) count += ((LeveledCompactionStrategy) strategy).getLevelSize(0); return count; } @@ -637,11 +557,13 @@ public int getUnleveledSSTables() return 0; } + @Override public int getLevelFanoutSize() { - return fanout; + return repaired.first().getLevelFanoutSize(); } + @Override public int[] getSSTableCountPerLevel() { maybeReloadDiskBoundaries(); @@ -651,19 +573,22 @@ public int[] getSSTableCountPerLevel() if (repaired.first() instanceof LeveledCompactionStrategy) { int[] res = new int[LeveledGenerations.MAX_LEVEL_COUNT]; - for (AbstractCompactionStrategy strategy : getAllStrategies()) + for (CompactionStrategy strategy : getAllStrategies()) { int[] repairedCountPerLevel = ((LeveledCompactionStrategy) strategy).getAllLevelSize(); res = sumArrays(res, repairedCountPerLevel); } return res; } + else + { + return new int[0]; + } } finally { readLock.unlock(); } - return null; } public long[] getPerLevelSizeBytes() @@ -674,7 +599,7 @@ public long[] getPerLevelSizeBytes() if (repaired.first() instanceof LeveledCompactionStrategy) { long [] res = new long[LeveledGenerations.MAX_LEVEL_COUNT]; - for (AbstractCompactionStrategy strategy : getAllStrategies()) + for (CompactionStrategy strategy : getAllStrategies()) { long[] repairedCountPerLevel = ((LeveledCompactionStrategy) strategy).getAllLevelSizeBytes(); res = sumArrays(res, repairedCountPerLevel); @@ -760,13 +685,13 @@ else if (i < a.length) /** * Should only be called holding the readLock */ - private void handleFlushNotification(Iterable added) + private void handleFlushNotification(Iterable added) { - for (SSTableReader sstable : added) - getHolder(sstable).addSSTable(sstable); + for (CompactionSSTable sstable : added) + compactionStrategyFor(sstable).addSSTable(sstable); } - private int getHolderIndex(SSTableReader sstable) + private int getHolderIndex(CompactionSSTable sstable) { for (int i = 0; i < holders.size(); i++) { @@ -777,7 +702,7 @@ private int getHolderIndex(SSTableReader sstable) throw new IllegalStateException("No holder claimed " + sstable); } - private AbstractStrategyHolder getHolder(SSTableReader sstable) + private AbstractStrategyHolder getHolder(CompactionSSTable sstable) { for (AbstractStrategyHolder holder : holders) { @@ -819,15 +744,16 @@ ImmutableList getHolders() * * lives in matches the list index of the holder that's responsible for it */ - public List groupSSTables(Iterable sstables) + public + List> groupSSTables(Iterable sstables) { - List classified = new ArrayList<>(holders.size()); + List> classified = new ArrayList<>(holders.size()); for (AbstractStrategyHolder holder : holders) { classified.add(holder.createGroupedSSTableContainer()); } - for (SSTableReader sstable : sstables) + for (S sstable : sstables) { classified.get(getHolderIndex(sstable)).add(sstable); } @@ -838,10 +764,10 @@ public List groupSSTables(Iterable sstab /** * Should only be called holding the readLock */ - private void handleListChangedNotification(Iterable added, Iterable removed) + private void handleListChangedNotification(Iterable added, Iterable removed) { - List addedGroups = groupSSTables(added); - List removedGroups = groupSSTables(removed); + List> addedGroups = groupSSTables(added); + List> removedGroups = groupSSTables(removed); for (int i=0; i added, Iterab /** * Should only be called holding the readLock */ - private void handleRepairStatusChangedNotification(Iterable sstables) + private void handleRepairStatusChangedNotification(Iterable sstables) { - List groups = groupSSTables(sstables); + List> groups = groupSSTables(sstables); for (int i = 0; i < holders.size(); i++) { - GroupedSSTableContainer group = groups.get(i); + GroupedSSTableContainer group = groups.get(i); if (group.isEmpty()) continue; @@ -877,15 +803,7 @@ private void handleRepairStatusChangedNotification(Iterable sstab /** * Should only be called holding the readLock */ - private void handleMetadataChangedNotification(SSTableReader sstable, StatsMetadata oldMetadata) - { - compactionStrategyFor(sstable).metadataChanged(oldMetadata, sstable); - } - - /** - * Should only be called holding the readLock - */ - private void handleDeletingNotification(SSTableReader deleted) + private void handleDeletingNotification(CompactionSSTable deleted) { compactionStrategyFor(deleted).removeSSTable(deleted); } @@ -917,11 +835,6 @@ else if (notification instanceof SSTableDeletingNotification) { handleDeletingNotification(((SSTableDeletingNotification) notification).deleting); } - else if (notification instanceof SSTableMetadataChanged) - { - SSTableMetadataChanged lcNotification = (SSTableMetadataChanged) notification; - handleMetadataChangedNotification(lcNotification.sstable, lcNotification.oldMetadata); - } } finally { @@ -929,6 +842,7 @@ else if (notification instanceof SSTableMetadataChanged) } } + @Override public void enable() { writeLock.lock(); @@ -943,6 +857,7 @@ public void enable() } } + @Override public void disable() { writeLock.lock(); @@ -964,19 +879,19 @@ public void disable() * @param ranges * @return */ - public AbstractCompactionStrategy.ScannerList maybeGetScanners(Collection sstables, Collection> ranges) + private ScannerList maybeGetScanners(Collection sstables, Collection> ranges) { maybeReloadDiskBoundaries(); List scanners = new ArrayList<>(sstables.size()); readLock.lock(); try { - List sstableGroups = groupSSTables(sstables); + List> sstableGroups = groupSSTables(sstables); for (int i = 0; i < holders.size(); i++) { AbstractStrategyHolder holder = holders.get(i); - GroupedSSTableContainer group = sstableGroups.get(i); + GroupedSSTableContainer group = sstableGroups.get(i); scanners.addAll(holder.getScanners(group, ranges)); } } @@ -988,10 +903,11 @@ public AbstractCompactionStrategy.ScannerList maybeGetScanners(Collection sstables, Collection> ranges) + @Override + public ScannerList getScanners(Collection sstables, Collection> ranges) { while (true) { @@ -1006,12 +922,22 @@ public AbstractCompactionStrategy.ScannerList getScanners(Collection sstables) + @Override + public ScannerList getScanners(Collection sstables) { return getScanners(sstables, null); } - public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) + @Override + public Set getSSTables() + { + return getStrategies().stream() + .flatMap(strategy -> strategy.getSSTables().stream()) + .collect(Collectors.toSet()); + } + + @Override + public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) { maybeReloadDiskBoundaries(); readLock.lock(); @@ -1025,45 +951,46 @@ public Collection> groupSSTablesForAntiCompaction(Coll } } + @Override public long getMaxSSTableBytes() { return maxSSTableSizeBytes; } - public AbstractCompactionTask getCompactionTask(LifecycleTransaction txn, long gcBefore, long maxSSTableBytes) + @Override + public AbstractCompactionTask createCompactionTask(LifecycleTransaction txn, long gcBefore, long maxSSTableBytes) { maybeReloadDiskBoundaries(); readLock.lock(); try { validateForCompaction(txn.originals()); - return compactionStrategyFor(txn.originals().iterator().next()).getCompactionTask(txn, gcBefore, maxSSTableBytes); + return compactionStrategyFor(txn.originals().iterator().next()).createCompactionTask(txn, gcBefore, maxSSTableBytes); } finally { readLock.unlock(); } - } - private void validateForCompaction(Iterable input) + private void validateForCompaction(Iterable input) { readLock.lock(); try { - SSTableReader firstSSTable = Iterables.getFirst(input, null); + CompactionSSTable firstSSTable = Iterables.getFirst(input, null); assert firstSSTable != null; boolean repaired = firstSSTable.isRepaired(); int firstIndex = compactionStrategyIndexFor(firstSSTable); boolean isPending = firstSSTable.isPendingRepair(); - TimeUUID pendingRepair = firstSSTable.getSSTableMetadata().pendingRepair; - for (SSTableReader sstable : input) + TimeUUID pendingRepair = firstSSTable.getPendingRepair(); + for (CompactionSSTable sstable : input) { if (sstable.isRepaired() != repaired) throw new UnsupportedOperationException("You can't mix repaired and unrepaired data in a compaction"); if (firstIndex != compactionStrategyIndexFor(sstable)) throw new UnsupportedOperationException("You can't mix sstables from different directories in a compaction"); - if (isPending && !pendingRepair.equals(sstable.getSSTableMetadata().pendingRepair)) + if (isPending && !pendingRepair.equals(sstable.getPendingRepair())) throw new UnsupportedOperationException("You can't compact sstables from different pending repair sessions"); } } @@ -1073,20 +1000,27 @@ private void validateForCompaction(Iterable input) } } - public CompactionTasks getMaximalTasks(final long gcBefore, final boolean splitOutput, OperationType operationType) + @Override + public CompactionTasks getMaximalTasks(final long gcBefore, final boolean splitOutput, int permittedParallelism) + { + return this.getMaximalTasks(gcBefore, splitOutput, permittedParallelism, OperationType.MAJOR_COMPACTION); + } + + @Override + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism, OperationType operationType) { maybeReloadDiskBoundaries(); // runWithCompactionsDisabled cancels active compactions and disables them, then we are able // to make the repaired/unrepaired strategies mark their own sstables as compacting. Once the // sstables are marked the compactions are re-enabled - return cfs.runWithCompactionsDisabled(() -> { + return realm.runWithCompactionsDisabled(() -> { List tasks = new ArrayList<>(); readLock.lock(); try { for (AbstractStrategyHolder holder : holders) { - for (AbstractCompactionTask task: holder.getMaximalTasks(gcBefore, splitOutput)) + for (AbstractCompactionTask task: holder.getMaximalTasks(gcBefore, splitOutput, permittedParallelism)) { tasks.add(task.setCompactionType(operationType)); } @@ -1096,8 +1030,8 @@ public CompactionTasks getMaximalTasks(final long gcBefore, final boolean splitO { readLock.unlock(); } - return CompactionTasks.create(tasks); - }, operationType, false, false); + return CompactionTasks.create(CompositeCompactionTask.applyParallelismLimit(tasks, permittedParallelism)); + }, operationType, false, false, TableOperation.StopTrigger.COMPACTION); } /** @@ -1109,14 +1043,15 @@ public CompactionTasks getMaximalTasks(final long gcBefore, final boolean splitO * @param gcBefore gc grace period, throw away tombstones older than this * @return a list of compaction tasks corresponding to the sstables requested */ - public CompactionTasks getUserDefinedTasks(Collection sstables, long gcBefore) + @Override + public CompactionTasks getUserDefinedTasks(Collection sstables, long gcBefore) { maybeReloadDiskBoundaries(); List ret = new ArrayList<>(); readLock.lock(); try { - List groupedSSTables = groupSSTables(sstables); + List> groupedSSTables = groupSSTables(sstables); for (int i = 0; i < holders.size(); i++) { ret.addAll(holders.get(i).getUserDefinedTasks(groupedSSTables.get(i), gcBefore)); @@ -1129,21 +1064,13 @@ public CompactionTasks getUserDefinedTasks(Collection sstables, l } } + @Override public int getEstimatedRemainingTasks() { - maybeReloadDiskBoundaries(); - int tasks = 0; - readLock.lock(); - try - { - for (AbstractCompactionStrategy strategy : getAllStrategies()) - tasks += strategy.getEstimatedRemainingTasks(); - } - finally - { - readLock.unlock(); - } - return tasks; + return getStrategies(false).stream() + .flatMap(list -> list.stream()) + .mapToInt(CompactionStrategy::getEstimatedRemainingTasks) + .sum(); } public int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes, boolean isIncremental) @@ -1157,7 +1084,7 @@ public int getEstimatedRemainingTasks(int additionalSSTables, long additionalByt { int tasks = pendingRepairs.getEstimatedRemainingTasks(); - Iterable strategies; + Iterable strategies; if (isIncremental) { // Note that it is unlikely that we are behind in the pending strategies (as they only have a small fraction @@ -1186,19 +1113,32 @@ public int getEstimatedRemainingTasks(int additionalSSTables, long additionalByt } } - public boolean shouldBeEnabled() + @Override + public int getTotalCompactions() { - return params.isEnabled(); + return getStrategies(false).stream() + .flatMap(list -> list.stream()) + .mapToInt(CompactionStrategy::getTotalCompactions) + .sum(); } + @Override public String getName() { return name; } - public List> getStrategies() + @Override + public List getStrategies() { - maybeReloadDiskBoundaries(); + return getStrategies(true).stream().flatMap(List::stream).collect(Collectors.toList()); + } + + private List> getStrategies(boolean checkBoundaries) + { + if (checkBoundaries) + maybeReloadDiskBoundaries(); + readLock.lock(); try { @@ -1212,35 +1152,57 @@ public List> getStrategies() } } - public void overrideLocalParams(CompactionParams params) + @Override + public List getStrategies(boolean isRepaired, @Nullable TimeUUID pendingRepair) { - logger.info("Switching local compaction strategy from {} to {}", this.params, params); - maybeReloadParamsFromJMX(params); + readLock.lock(); + try + { + if (isRepaired) + return Lists.newArrayList(repaired.allStrategies()); + else if (pendingRepair != null) + return Lists.newArrayList(pendingRepairs.getStrategiesFor(pendingRepair)); + else + return Lists.newArrayList(unrepaired.allStrategies()); + } + finally + { + readLock.unlock(); + } } - private int getNumTokenPartitions() + /** + * @return the statistics for the compaction strategies that have compactions in progress or pending + */ + @Override + public List getStatistics() { - return partitionSSTablesByTokenRange ? currentBoundaries.directories.size() : 1; + return getStrategies(false).stream() + .flatMap(list -> list.stream()) + .filter(strategy -> strategy.getTotalCompactions() > 0) + .map(CompactionStrategy::getStatistics) + .flatMap(List::stream) + .collect(Collectors.toList()); } - private void setStrategy(CompactionParams params) + private int getNumTokenPartitions() { - int numPartitions = getNumTokenPartitions(); - for (AbstractStrategyHolder holder : holders) - holder.setStrategy(params, numPartitions); - this.params = params; + return partitionSSTablesByTokenRange && currentBoundaries != null ? currentBoundaries.directories.size() : 1; } + @Override public CompactionParams getCompactionParams() { return params; } - public boolean onlyPurgeRepairedTombstones() + @Override + public CompactionParams getMetadataCompactionParams() { - return Boolean.parseBoolean(params.options().get(AbstractCompactionStrategy.ONLY_PURGE_REPAIRED_TOMBSTONES)); + return metadataParams; } + @Override public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, @@ -1274,123 +1236,70 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, } } - public boolean isRepaired(AbstractCompactionStrategy strategy) + @Override + public boolean supportsEarlyOpen() { - return repaired.getStrategyIndex(strategy) >= 0; + return supportsEarlyOpen; } - public List getStrategyFolders(AbstractCompactionStrategy strategy) + @Override + public void periodicReport() { - readLock.lock(); - try + for (CompactionStrategy strat : getAllStrategies()) { - Directories.DataDirectory[] locations = cfs.getDirectories().getWriteableLocations(); - if (partitionSSTablesByTokenRange) - { - for (AbstractStrategyHolder holder : holders) - { - int idx = holder.getStrategyIndex(strategy); - if (idx >= 0) - return Collections.singletonList(locations[idx].location.absolutePath()); - } - } - List folders = new ArrayList<>(locations.length); - for (Directories.DataDirectory location : locations) - { - folders.add(location.location.absolutePath()); - } - return folders; - } - finally - { - readLock.unlock(); + strat.periodicReport(); } } - public boolean supportsEarlyOpen() + public ReentrantReadWriteLock.WriteLock getWriteLock() { - return supportsEarlyOpen; + return this.writeLock; } + /** + * This method is exposed for testing only + * @return the LocalSession sessionIDs of any pending repairs + */ @VisibleForTesting - List getPendingRepairManagers() + public Set pendingRepairs() { - maybeReloadDiskBoundaries(); - readLock.lock(); - try - { - return Lists.newArrayList(pendingRepairs.getManagers()); - } - finally - { - readLock.unlock(); - } + Set ids = new HashSet<>(); + pendingRepairs.getManagers().forEach(p -> ids.addAll(p.getSessions())); + return ids; } - /** - * Mutates sstable repairedAt times and notifies listeners of the change with the writeLock held. Prevents races - * with other processes between when the metadata is changed and when sstables are moved between strategies. - */ - public void mutateRepaired(Collection sstables, long repairedAt, TimeUUID pendingRepair, boolean isTransient) throws IOException + @Override + public void repairSessionCompleted(TimeUUID sessionID) { - if (sstables.isEmpty()) - return; - Set changed = new HashSet<>(); - - writeLock.lock(); - try - { - for (SSTableReader sstable: sstables) - { - sstable.mutateRepairedAndReload(repairedAt, pendingRepair, isTransient); - verifyMetadata(sstable, repairedAt, pendingRepair, isTransient); - changed.add(sstable); - } - } - finally - { - try - { - // if there was an exception mutating repairedAt, we should still notify for the - // sstables that we were able to modify successfully before releasing the lock - cfs.getTracker().notifySSTableRepairedStatusChanged(changed); - } - finally - { - writeLock.unlock(); - } - } + for (PendingRepairManager manager : pendingRepairs.getManagers()) + manager.removeSessionIfEmpty(sessionID); } - private static void verifyMetadata(SSTableReader sstable, long repairedAt, TimeUUID pendingRepair, boolean isTransient) + // + // CompactionObserver - because the strategies observe compactions, for CSM this is currently a no-op + // + + @Override + public void onInProgress(CompactionProgress progress) { - if (!Objects.equals(pendingRepair, sstable.getPendingRepair())) - throw new IllegalStateException(String.format("Failed setting pending repair to %s on %s (pending repair is %s)", pendingRepair, sstable, sstable.getPendingRepair())); - if (repairedAt != sstable.getRepairedAt()) - throw new IllegalStateException(String.format("Failed setting repairedAt to %d on %s (repairedAt is %d)", repairedAt, sstable, sstable.getRepairedAt())); - if (isTransient != sstable.isTransient()) - throw new IllegalStateException(String.format("Failed setting isTransient to %b on %s (isTransient is %b)", isTransient, sstable, sstable.isTransient())); + } - public CleanupSummary releaseRepairData(Collection sessions) + @Override + public void onCompleted(TimeUUID id, Throwable err) { - List cleanupTasks = new ArrayList<>(); - readLock.lock(); - try - { - for (PendingRepairManager prm : Iterables.concat(pendingRepairs.getManagers(), transientRepairs.getManagers())) - cleanupTasks.add(prm.releaseSessionData(sessions)); - } - finally - { - readLock.unlock(); - } - CleanupSummary summary = new CleanupSummary(cfs, Collections.emptySet(), Collections.emptySet()); + } + + @Override + public Map getMaxOverlapsMap() + { + Map result = new LinkedHashMap<>(); - for (CleanupTask task : cleanupTasks) - summary = CleanupSummary.add(summary, task.cleanup()); + for (AbstractStrategyHolder holder : holders) + for (LegacyAbstractCompactionStrategy strategy : holder.allStrategies()) + result.putAll(strategy.getMaxOverlapsMap()); - return summary; + return result; } } diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyOptions.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyOptions.java new file mode 100644 index 000000000000..dc097e8a3e85 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyOptions.java @@ -0,0 +1,502 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.utils.Throwables; + +import static java.lang.String.format; +import static org.apache.cassandra.config.CassandraRelevantProperties.DEFAULT_COMPACTION_COSTS_READ_MULTIPLIER; +import static org.apache.cassandra.config.CassandraRelevantProperties.DEFAULT_COMPACTION_LOGS; +import static org.apache.cassandra.config.CassandraRelevantProperties.DEFAULT_COMPACTION_LOG_MINUTES; + +/** + * This class contains all compaction options that are shared by all strategies. + */ +public class CompactionStrategyOptions +{ + public static final int DEFAULT_MIN_THRESHOLD = 4; + public static final int DEFAULT_MAX_THRESHOLD = 32; + private static final Logger logger = LoggerFactory.getLogger(CompactionStrategyOptions.class); + + public static final Map DEFAULT_THRESHOLDS = + ImmutableMap.of(CompactionParams.Option.MIN_THRESHOLD.toString(), Integer.toString(DEFAULT_MIN_THRESHOLD), + CompactionParams.Option.MAX_THRESHOLD.toString(), Integer.toString(DEFAULT_MAX_THRESHOLD)); + + public static final String ONLY_PURGE_REPAIRED_TOMBSTONES = "only_purge_repaired_tombstones"; + + public static final String DEFAULT_TOMBSTONE_THRESHOLD = "0.2"; + // minimum interval needed to perform tombstone removal compaction in seconds, default 86400 or 1 day. + public static final String DEFAULT_TOMBSTONE_COMPACTION_INTERVAL = "86400"; + public static final String DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION = "false"; + public static final String DEFAULT_LOG_TYPE_OPTION = DEFAULT_COMPACTION_LOGS.getString("none"); + public static final String DEFAULT_LOG_PERIOD_MINUTES_OPTION = DEFAULT_COMPACTION_LOG_MINUTES.getString("1"); + public static final String DEFAULT_READ_MULTIPLIER_OPTION = DEFAULT_COMPACTION_COSTS_READ_MULTIPLIER.getString("1.0"); + public static final String DEFAULT_WRITE_MULTIPLIER_OPTION = DEFAULT_COMPACTION_COSTS_READ_MULTIPLIER.getString("1.0"); + + public static final String TOMBSTONE_THRESHOLD_OPTION = "tombstone_threshold"; + public static final String TOMBSTONE_COMPACTION_INTERVAL_OPTION = "tombstone_compaction_interval"; + // disable range overlap check when deciding if an SSTable is candidate for tombstone compaction (CASSANDRA-6563) + public static final String UNCHECKED_TOMBSTONE_COMPACTION_OPTION = "unchecked_tombstone_compaction"; + public static final String LOG_ALL_OPTION = "log_all"; + public static final String LOG_TYPE_OPTION = "log"; + public static final String LOG_PERIOD_MINUTES_OPTION = "log_period_minutes"; + + /** The multipliers can be used by users if they wish to adjust the costs. We reduce the read costs because writes are batch processes (flush and compaction) + * and therefore the costs tend to be lower that for reads, so by reducing read costs we make the costs more comparable. + */ + public static final String READ_MULTIPLIER_OPTION = "costs_read_multiplier"; + public static final String WRITE_MULTIPLIER_OPTION = "costs_write_multiplier"; + public static final String COMPACTION_ENABLED = "enabled"; + + private final Class klass; + private final Map options; + private final float tombstoneThreshold; + private final long tombstoneCompactionInterval; + private final boolean uncheckedTombstoneCompaction; + private boolean disableTombstoneCompactions = false; + public enum LogType + { + NONE, EVENTS_ONLY, ALL; + } + private final LogType logType; + private final int logPeriodMinutes; + private final double readMultiplier; + private final double writeMultiplier; + + public CompactionStrategyOptions(Class klass, Map options, boolean throwOnInvalidOption) + { + this.klass = klass; + this.options = copyOptions(klass, options); + + boolean useDefault = false; + try + { + validate(); // will throw ConfigurationException if the options are invalid + } + catch (ConfigurationException e) + { + // when called from CompactionParams we throw but when called from AbstractCompactionStrategy we use defaults + // could probably not bother with the latter (?) + if (throwOnInvalidOption) + { + throw e; + } + else + { + logger.warn("Error setting compaction strategy options ({}), defaults will be used", e.getMessage()); + useDefault = true; + } + } + + tombstoneThreshold = Float.parseFloat(getOption(TOMBSTONE_THRESHOLD_OPTION, useDefault, DEFAULT_TOMBSTONE_THRESHOLD)); + tombstoneCompactionInterval = Long.parseLong(getOption(TOMBSTONE_COMPACTION_INTERVAL_OPTION, useDefault, DEFAULT_TOMBSTONE_COMPACTION_INTERVAL)); + uncheckedTombstoneCompaction = Boolean.parseBoolean(getOption(UNCHECKED_TOMBSTONE_COMPACTION_OPTION, useDefault, DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION)); + if (options.containsKey(LOG_ALL_OPTION)) + { + if (options.get(LOG_ALL_OPTION).equalsIgnoreCase("true")) + logType = LogType.ALL; + else + logType = LogType.NONE; + } + else + logType = LogType.valueOf(getOption(LOG_TYPE_OPTION, useDefault, DEFAULT_LOG_TYPE_OPTION).toUpperCase()); + logPeriodMinutes = Integer.parseInt(getOption(LOG_PERIOD_MINUTES_OPTION, useDefault, DEFAULT_LOG_PERIOD_MINUTES_OPTION)); + readMultiplier = Double.parseDouble(getOption(READ_MULTIPLIER_OPTION, useDefault, DEFAULT_READ_MULTIPLIER_OPTION)); + writeMultiplier = Double.parseDouble(getOption(WRITE_MULTIPLIER_OPTION, useDefault, DEFAULT_WRITE_MULTIPLIER_OPTION)); + } + + private Map copyOptions(Class klass, Map options) + { + Map newOptions = new HashMap<>(options); + + // For legacy compatibility reasons, for some compaction strategies we want to see the default min and max threshold + // in the compaction parameters that can be seen in CQL when retrieving the table from the schema tables so for + // these strategies we need to add these options when they have not been specified by the user + if (supportsThresholdParams(klass)) + { + newOptions.putIfAbsent(CompactionParams.Option.MIN_THRESHOLD.toString(), Integer.toString(DEFAULT_MIN_THRESHOLD)); + newOptions.putIfAbsent(CompactionParams.Option.MAX_THRESHOLD.toString(), Integer.toString(DEFAULT_MAX_THRESHOLD)); + } + + return newOptions; + } + + /** + * All strategies except {@link UnifiedCompactionStrategy} support the minimum and maximum thresholds + */ + @SuppressWarnings("unchecked") + public static boolean supportsThresholdParams(Class klass) + { + try + { + Map unrecognizedOptions = + (Map) klass.getMethod("validateOptions", Map.class) + .invoke(null, DEFAULT_THRESHOLDS); + + return unrecognizedOptions.isEmpty(); + } + catch (Exception e) + { + throw Throwables.cleaned(e); + } + } + + private String getOption(String optionName, boolean useDefault, String defaultValue) + { + if (useDefault) + return defaultValue; + + String optionValue = options.get(optionName); + if (optionValue == null) + return defaultValue; + + return optionValue; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("class", klass.getName()) + .add("options", options) + .toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof CompactionStrategyOptions)) + return false; + + CompactionStrategyOptions that = (CompactionStrategyOptions) o; + + return klass.equals(that.klass) && options.equals(that.options); + } + + @Override + public int hashCode() + { + return Objects.hash(klass, options); + } + + private Map validate() + { + try + { + // Each strategy currently implements a static validateOptions() method for custom validation, the default behavior + // is to simply call validateOptions() below, through AbstractCompactionStrategy.validateOptions(), we could simplify + // all this assuming we don't need to support any user-defined compaction strategy + Map unknownOptions = (Map) klass.getMethod("validateOptions", Map.class).invoke(null, options); + if (!unknownOptions.isEmpty()) + { + throw new ConfigurationException(format("Properties specified %s are not understood by %s", + unknownOptions.keySet(), + klass.getSimpleName())); + } + + return unknownOptions; + } + catch (NoSuchMethodException e) + { + logger.warn("Compaction strategy {} does not have a static validateOptions method. Validation ignored", klass.getName()); + } + catch (InvocationTargetException e) + { + if (e.getTargetException() instanceof ConfigurationException) + throw (ConfigurationException) e.getTargetException(); + + Throwable cause = e.getCause() == null + ? e + : e.getCause(); + + throw new ConfigurationException(format("%s.validateOptions() threw an error: %s %s", + klass.getName(), + cause.getClass().getName(), + cause.getMessage()), + e); + } + catch (IllegalAccessException e) + { + throw new ConfigurationException("Cannot access method validateOptions in " + klass.getName(), e); + } + + if (minCompactionThreshold() <= 0 || maxCompactionThreshold() <= 0) + { + throw new ConfigurationException("Disabling compaction by setting compaction thresholds to 0 has been removed," + + " set the compaction option 'enabled' to false instead."); + } + + if (minCompactionThreshold() <= 1) + { + throw new ConfigurationException(format("Min compaction threshold cannot be less than 2 (got %d)", + minCompactionThreshold())); + } + + if (minCompactionThreshold() > maxCompactionThreshold()) + { + throw new ConfigurationException(format("Min compaction threshold (got %d) cannot be greater than max compaction threshold (got %d)", + minCompactionThreshold(), + maxCompactionThreshold())); + } + + return options; + } + + public static Map validateOptions(Map options) throws ConfigurationException + { + String minThreshold = options.get(CompactionParams.Option.MIN_THRESHOLD.toString()); + if (minThreshold != null && !StringUtils.isNumeric(minThreshold)) + { + throw new ConfigurationException(format("Invalid value %s for '%s' compaction sub-option - must be an integer", + minThreshold, + CompactionParams.Option.MIN_THRESHOLD)); + } + + String maxThreshold = options.get(CompactionParams.Option.MAX_THRESHOLD.toString()); + if (maxThreshold != null && !StringUtils.isNumeric(maxThreshold)) + { + throw new ConfigurationException(format("Invalid value %s for '%s' compaction sub-option - must be an integer", + maxThreshold, + CompactionParams.Option.MAX_THRESHOLD)); + } + + String threshold = options.get(TOMBSTONE_THRESHOLD_OPTION); + if (threshold != null) + { + try + { + float thresholdValue = Float.parseFloat(threshold); + if (thresholdValue < 0) + { + throw new ConfigurationException(String.format("%s must be greater than 0, but was %f", TOMBSTONE_THRESHOLD_OPTION, thresholdValue)); + } + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", threshold, TOMBSTONE_THRESHOLD_OPTION), e); + } + } + + String interval = options.get(TOMBSTONE_COMPACTION_INTERVAL_OPTION); + if (interval != null) + { + try + { + long tombstoneCompactionInterval = Long.parseLong(interval); + if (tombstoneCompactionInterval < 0) + { + throw new ConfigurationException(String.format("%s must be greater than 0, but was %d", TOMBSTONE_COMPACTION_INTERVAL_OPTION, tombstoneCompactionInterval)); + } + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", interval, TOMBSTONE_COMPACTION_INTERVAL_OPTION), e); + } + } + + String unchecked = options.get(UNCHECKED_TOMBSTONE_COMPACTION_OPTION); + if (unchecked != null && !unchecked.equalsIgnoreCase("true") && !unchecked.equalsIgnoreCase("false")) + { + throw new ConfigurationException(String.format("'%s' should be either 'true' or 'false', not '%s'", UNCHECKED_TOMBSTONE_COMPACTION_OPTION, unchecked)); + } + + String logAll = options.get(LOG_ALL_OPTION); + if (logAll != null && !logAll.equalsIgnoreCase("true") && !logAll.equalsIgnoreCase("false")) + { + throw new ConfigurationException(String.format("'%s' should either be 'true' or 'false', not %s", LOG_ALL_OPTION, logAll)); + } + + String logType = options.get(LOG_TYPE_OPTION); + if (logType != null && !logType.equalsIgnoreCase("all") && !logType.equalsIgnoreCase("events_only") && !logType.equalsIgnoreCase("none")) + { + throw new ConfigurationException(String.format("'%s' should either be 'all' or 'events_only' or 'none', not %s", LOG_TYPE_OPTION, logType)); + } + + if (logAll != null && logType != null) + { + throw new ConfigurationException(String.format("Either '%s' or '%s' should be used, not both", LOG_ALL_OPTION, LOG_TYPE_OPTION)); + } + + String logPeriodMinutes = options.get(LOG_PERIOD_MINUTES_OPTION); + if (logPeriodMinutes != null) + { + try + { + long minutes = Integer.parseInt(logPeriodMinutes); + if (minutes < 1) + { + throw new ConfigurationException(String.format("%s must be greater than or equal to 1, but was %d", LOG_PERIOD_MINUTES_OPTION, minutes)); + } + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", logPeriodMinutes, LOG_PERIOD_MINUTES_OPTION), e); + } + } + + String readMultiplier = options.get(READ_MULTIPLIER_OPTION); + if (readMultiplier != null) + { + try + { + double multiplier = Double.parseDouble(readMultiplier); + if (!(multiplier > 0 && multiplier <= 1)) + { + throw new ConfigurationException(String.format("%s must be between 0 and 1, but was %d", READ_MULTIPLIER_OPTION, multiplier)); + } + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format("%s is not a parsable double (base10) for %s", readMultiplier, READ_MULTIPLIER_OPTION), e); + } + } + + String writeMultiplier = options.get(WRITE_MULTIPLIER_OPTION); + if (writeMultiplier != null) + { + try + { + double multiplier = Double.parseDouble(writeMultiplier); + if (!(multiplier > 0 && multiplier <= 1)) + { + throw new ConfigurationException(String.format("%s must be between 0 and 1, but was %d", WRITE_MULTIPLIER_OPTION, multiplier)); + } + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format("%s is not a parsable double (base10) for %s", writeMultiplier, WRITE_MULTIPLIER_OPTION), e); + } + } + + String compactionEnabled = options.get(COMPACTION_ENABLED); + if (compactionEnabled != null && !compactionEnabled.equalsIgnoreCase("true") && !compactionEnabled.equalsIgnoreCase("false")) + { + throw new ConfigurationException(String.format("enabled should either be 'true' or 'false', not %s", compactionEnabled)); + } + + Map uncheckedOptions = new HashMap<>(options); + uncheckedOptions.remove(TOMBSTONE_THRESHOLD_OPTION); + uncheckedOptions.remove(TOMBSTONE_COMPACTION_INTERVAL_OPTION); + uncheckedOptions.remove(UNCHECKED_TOMBSTONE_COMPACTION_OPTION); + uncheckedOptions.remove(LOG_ALL_OPTION); + uncheckedOptions.remove(LOG_TYPE_OPTION); + uncheckedOptions.remove(LOG_PERIOD_MINUTES_OPTION); + uncheckedOptions.remove(READ_MULTIPLIER_OPTION); + uncheckedOptions.remove(WRITE_MULTIPLIER_OPTION); + uncheckedOptions.remove(COMPACTION_ENABLED); + uncheckedOptions.remove(ONLY_PURGE_REPAIRED_TOMBSTONES); + uncheckedOptions.remove(CompactionParams.Option.PROVIDE_OVERLAPPING_TOMBSTONES.toString()); + return uncheckedOptions; + } + + public int minCompactionThreshold() + { + String threshold = options.get(CompactionParams.Option.MIN_THRESHOLD.toString()); + return threshold == null + ? DEFAULT_MIN_THRESHOLD + : Integer.parseInt(threshold); + } + + public int maxCompactionThreshold() + { + String threshold = options.get(CompactionParams.Option.MAX_THRESHOLD.toString()); + return threshold == null + ? DEFAULT_MAX_THRESHOLD + : Integer.parseInt(threshold); + } + + public Class klass() + { + return klass; + } + + public Map getOptions() + { + return options; + } + + public float getTombstoneThreshold() + { + return tombstoneThreshold; + } + + public long getTombstoneCompactionInterval() + { + return tombstoneCompactionInterval; + } + + public boolean isUncheckedTombstoneCompaction() + { + return uncheckedTombstoneCompaction; + } + + public boolean isDisableTombstoneCompactions() + { + return disableTombstoneCompactions; + } + + /** + * {@link TimeWindowCompactionStrategy} disable this parameter if other parameters aren't available. + */ + public void setDisableTombstoneCompactions(boolean disableTombstoneCompactions) + { + this.disableTombstoneCompactions = disableTombstoneCompactions; + } + + public boolean isLogEnabled() + { + return (logType == LogType.ALL || logType == LogType.EVENTS_ONLY); + } + + public boolean isLogAll() + { + return (logType == LogType.ALL); + } + + public int getLogPeriodMinutes() + { + return logPeriodMinutes; + } + + public double getReadMultiplier() + { + return readMultiplier; + } + + public double getWriteMultiplier() + { + return writeMultiplier; + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyStatistics.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyStatistics.java new file mode 100644 index 000000000000..307810b912ee --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyStatistics.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.apache.cassandra.schema.TableMetadata; + +/** + * The statistics for a compaction strategy, to be published over JMX and insights. + *

    + * Implements serializable to allow structured info to be returned via JMX. The JSON + * properties are published to insights so changing them has a downstream impact. + */ +public class CompactionStrategyStatistics implements Serializable +{ + private static final long serialVersionUID = 3695927592357744816L; + + private final String keyspace; + private final String table; + private final String strategy; + private final List aggregates; + + CompactionStrategyStatistics(TableMetadata metadata, + String strategy, + List aggregates) + { + this.keyspace = metadata.keyspace; + this.table = metadata.name; + this.strategy = strategy; + this.aggregates = new ArrayList<>(aggregates); + } + + public String keyspace() + { + return keyspace; + } + + public String table() + { + return table; + } + + @JsonProperty + public String strategy() + { + return strategy; + } + + @JsonProperty + public List aggregates() + { + return aggregates; + } + + @Override + public String toString() + { + StringBuilder ret = new StringBuilder(1024); + ret.append(keyspace) + .append('.') + .append(table) + .append('/') + .append(strategy) + .append('\n'); + + if (!aggregates.isEmpty()) + { + Collection header = aggregates.get(0).header(); // all headers are identical + int[] lengths = new int[header.size()]; // the max lengths of each column + Iterator it = header.iterator(); + + for (int i = 0; i < lengths.length; i++) + lengths[i] = it.next().length(); + + Map> rowsByShard = new LinkedHashMap<>(); + for (CompactionAggregateStatistics aggregate : aggregates) + { + String shard = aggregate.shard(); + List rows = rowsByShard.computeIfAbsent(shard, key -> new ArrayList<>(aggregates.size())); + String[] data = new String[header.size()]; + + it = aggregate.data().iterator(); + for (int i = 0; i < lengths.length; i++) + { + data[i] = it.next(); + if (data[i].length() > lengths[i]) + lengths[i] = data[i].length(); + } + + rows.add(data); + } + + for (Map.Entry> entry : rowsByShard.entrySet()) + { + // optional shard + if (!entry.getKey().isEmpty()) + ret.append("Shard/").append(entry.getKey()).append('\n'); + + // header + it = header.iterator(); + for (int i = 0; i < header.size(); i++) + ret.append(String.format("%-" + lengths[i] + "s\t", it.next())); + + ret.append('\n'); + + // rows + for (String[] row : entry.getValue()) + { + for (int i = 0; i < row.length; i++) + ret.append(String.format("%-" + lengths[i] + "s\t", row[i])); + + ret.append('\n'); + } + + ret.append('\n'); + } + } + + return ret.toString(); + } + + Collection getHeader() + { + return aggregates.isEmpty() ? ImmutableList.of() : aggregates.get(0).header(); + } + + Collection> getData() + { + return aggregates.stream().map(CompactionAggregateStatistics::data).collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionTask.java b/src/java/org/apache/cassandra/db/compaction/CompactionTask.java index 9566ef843047..a35807102488 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionTask.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionTask.java @@ -17,98 +17,191 @@ */ package org.apache.cassandra.db.compaction; +import java.io.Closeable; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; -import com.google.common.base.Predicate; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; import com.google.common.util.concurrent.RateLimiter; + import org.apache.commons.lang3.StringUtils; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.WriteContext; +import org.apache.cassandra.db.compaction.unified.UnifiedCompactionTask; import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; import org.apache.cassandra.db.compaction.writers.DefaultCompactionWriter; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.transactions.IndexTransaction; +import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Refs; +import static org.apache.cassandra.config.CassandraRelevantProperties.COMPACTION_HISTORY_ENABLED; +import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_CURSOR_COMPACTION; import static org.apache.cassandra.db.compaction.CompactionHistoryTabularData.COMPACTION_TYPE_PROPERTY; -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.FBUtilities.now; +import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory; +import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemoryPerSecond; public class CompactionTask extends AbstractCompactionTask { protected static final Logger logger = LoggerFactory.getLogger(CompactionTask.class); + protected final long gcBefore; protected final boolean keepOriginals; - protected static long totalBytesCompacted = 0; - private ActiveCompactionsTracker activeCompactions; - - public CompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, long gcBefore) + /** for trace logging purposes only */ + private static final AtomicLong totalBytesCompacted = new AtomicLong(); + + // The compaction strategy is not necessarily available for all compaction tasks (e.g. GC or sstable splitting) + @Nullable + private final CompactionStrategy strategy; + protected OperationTotals totals; + + public CompactionTask(CompactionRealm realm, + ILifecycleTransaction txn, + long gcBefore, + boolean keepOriginals, + @Nullable CompactionStrategy strategy) { - this(cfs, txn, gcBefore, false); + this(realm, txn, null, gcBefore, keepOriginals, strategy, strategy); } - public CompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, long gcBefore, boolean keepOriginals) + public CompactionTask(CompactionRealm realm, + ILifecycleTransaction txn, + OperationTotals totals, + long gcBefore, + boolean keepOriginals, + @Nullable CompactionStrategy strategy, + CompactionObserver observer) { - super(cfs, txn); + super(realm, txn); this.gcBefore = gcBefore; this.keepOriginals = keepOriginals; + this.strategy = strategy; + this.totals = totals; + + if (observer != null) + addObserver(observer); + + logger.debug("Created compaction task with id {} and strategy {}", txn.opIdString(), strategy); } - public static synchronized long addToTotalBytesCompacted(long bytesCompacted) + + /** + * Create a compaction task for deleted data collection. + */ + public static AbstractCompactionTask forGarbageCollection(CompactionRealm realm, + ILifecycleTransaction txn, + long gcBefore, + CompactionParams.TombstoneOption tombstoneOption) { - return totalBytesCompacted += bytesCompacted; + return new GarbageCollectionTask(realm, txn, gcBefore, tombstoneOption); } - protected int executeInternal(ActiveCompactionsTracker activeCompactions) + public static class GarbageCollectionTask extends CompactionTask { - this.activeCompactions = activeCompactions == null ? ActiveCompactionsTracker.NOOP : activeCompactions; - run(); - return transaction.originals().size(); + private final CompactionParams.TombstoneOption tombstoneOption; + + public GarbageCollectionTask(CompactionRealm realm, ILifecycleTransaction txn, long gcBefore, CompactionParams.TombstoneOption tombstoneOption) + { + super(realm, txn, gcBefore, false, null); + this.tombstoneOption = tombstoneOption; + setCompactionType(OperationType.GARBAGE_COLLECT); + setUserDefined(true); + } + + @Override + protected CompactionController getCompactionController(Set toCompact) + { + return new CompactionController(realm, toCompact, gcBefore, null, tombstoneOption); + } + + @Override + protected int getLevel() + { + return transaction.onlyOne().getSSTableLevel(); + } } + private static long addToTotalBytesCompacted(long bytesCompacted) + { + return totalBytesCompacted.addAndGet(bytesCompacted); + } + + /* + * Find the maximum size file in the list . + */ + private SSTableReader getMaxSizeFile(Iterable sstables) + { + long maxSize = 0L; + SSTableReader maxFile = null; + for (SSTableReader sstable : sstables) + { + if (sstable.onDiskLength() > maxSize) + { + maxSize = sstable.onDiskLength(); + maxFile = sstable; + } + } + return maxFile; + } + + @VisibleForTesting public boolean reduceScopeForLimitedSpace(Set nonExpiredSSTables, long expectedSize) { - if (partialCompactionsAcceptable() && transaction.originals().size() > 1) + if (partialCompactionsAcceptable() && nonExpiredSSTables.size() > 1) { // Try again w/o the largest one. - SSTableReader removedSSTable = cfs.getMaxSizeFile(nonExpiredSSTables); + SSTableReader removedSSTable = getMaxSizeFile(nonExpiredSSTables); logger.warn("insufficient space to compact all requested files. {}MiB required, {} for compaction {} - removing largest SSTable: {}", (float) expectedSize / 1024 / 1024, - StringUtils.join(transaction.originals(), ", "), - transaction.opId(), + StringUtils.join(nonExpiredSSTables, ", "), + transaction.opIdString(), removedSSTable); // Note that we have removed files that are still marked as compacting. // This suboptimal but ok since the caller will unmark all the sstables at the end. transaction.cancel(removedSSTable); + nonExpiredSSTables.remove(removedSSTable); return true; } return false; @@ -119,216 +212,747 @@ public boolean reduceScopeForLimitedSpace(Set nonExpiredSSTables, * which are properly serialized. * Caller is in charge of marking/unmarking the sstables as compacting. */ + @Override protected void runMayThrow() throws Exception { // The collection of sstables passed may be empty (but not null); even if // it is not empty, it may compact down to nothing if all rows are deleted. assert transaction != null; - if (transaction.originals().isEmpty()) + if (inputSSTables().isEmpty()) return; - // Note that the current compaction strategy, is not necessarily the one this task was created under. - // This should be harmless; see comments to CFS.maybeReloadCompactionStrategy. - CompactionStrategyManager strategy = cfs.getCompactionStrategyManager(); - if (DatabaseDescriptor.isSnapshotBeforeCompaction()) { Instant creationTime = now(); - cfs.snapshotWithoutMemtable(creationTime.toEpochMilli() + "-compact-" + cfs.name, creationTime); + realm.snapshotWithoutMemtable(creationTime.toEpochMilli() + "-compact-" + realm.getTableName(), creationTime); } - try (CompactionController controller = getCompactionController(transaction.originals())) + // The set of sstables given here may be later modified by buildCompactionCandidatesForAvailableDiskSpace() and + // the compaction iterators in CompactionController and OverlapTracker will reflect the updated set of sstables. + try (CompactionController controller = getCompactionController(inputSSTables()); + CompactionOperation operation = createCompactionOperation(controller, strategy)) { + // Mark the operation as active, rechecking that it has not been cancelled. + if (!switchToActive()) + throw new CompactionInterruptedException(operation.op.getProgress(), TableOperation.StopTrigger.NONE); + // If not, the operation is now in the active operations list and can be interrupted from there. + + operation.execute(); + } + } - final Set fullyExpiredSSTables = controller.getFullyExpiredSSTables(); + /** + * @return The token range that the operation should compact. This is usually null, but if we have a parallelizable + * multi-task operation (see {@link UnifiedCompactionStrategy#createAndAddTasks}), it will specify a subrange. + */ + protected Range tokenRange() + { + return null; + } - TimeUUID taskId = transaction.opId(); - // select SSTables to compact based on available disk space. - if (!buildCompactionCandidatesForAvailableDiskSpace(fullyExpiredSSTables, taskId)) + /** + * If this is a partial compaction, its progress reports are shared between tasks. This method returns the shared + * progress object. + */ + protected SharedCompactionProgress sharedProgress() + { + return null; + } + + /** + * @return The set of input sstables for this compaction. This must be a subset of the transaction originals and + * must reflect any removal of sstables from the originals set for correct overlap tracking. + * See {@link UnifiedCompactionTask} for an example. + */ + protected Set inputSSTables() + { + return transaction.originals(); + } + + /** + * @return True if the task should try to limit the operation size to the available space by removing sstables from + * the compacting set. This cannot be done if this is part of a multi-task operation with a shared transaction. + */ + protected boolean shouldReduceScopeForSpace() + { + return true; + } + + private CompactionOperation createCompactionOperation(CompactionController controller, CompactionStrategy strategy) + { + Set fullyExpiredSSTables = controller.getFullyExpiredSSTables(); + maybeNotifyIndexersAboutRowsInFullyExpiredSSTables(fullyExpiredSSTables); + + if (!fullyExpiredSSTables.isEmpty()) + { + logger.debug("Compaction {} dropping expired sstables: {}", transaction.opId().toString(), fullyExpiredSSTables); + fullyExpiredSSTables.forEach(reader -> { if (reader instanceof SSTableReader) transaction.obsolete((SSTableReader)reader); }); + } + Set actuallyCompact = new HashSet<>(inputSSTables()); + actuallyCompact.removeAll(fullyExpiredSSTables); + // select SSTables to compact based on available disk space. + if (shouldReduceScopeForSpace() && !buildCompactionCandidatesForAvailableDiskSpace(actuallyCompact, transaction.opId(), !fullyExpiredSSTables.isEmpty())) + { + // The set of sstables has changed (one or more were excluded due to limited available disk space). + // We need to recompute the overlaps between sstables. The iterators used in the compaction controller + // and tracker will reflect the changed set of sstables made by LifecycleTransaction.cancel(), + // so refreshing the overlaps will be based on the updated set of sstables. + controller.refreshOverlaps(); + } + + // Calculate the operation total sizes if not already set + if (totals == null) + totals = getOperationTotals(actuallyCompact, tokenRange()); + + // sanity check: sstables to compact is a subset of the transaction originals + assert transaction.originals().containsAll(actuallyCompact); + // sanity check: all sstables must belong to the same table + assert !Iterables.any(transaction.originals(), sstable -> !sstable.descriptor.cfname.equals(realm.getTableName())); + + + // Cursors currently don't support: + boolean compactByIterators = !ALLOW_CURSOR_COMPACTION.getBoolean() + || strategy != null && !strategy.supportsCursorCompaction() // strategy does not support it + || controller.shouldProvideTombstoneSources() // garbagecollect + || realm.getIndexManager().hasIndexes() // indexes + || realm.metadata().enforceStrictLiveness(); // strict liveness + + logger.debug("Compacting in {} by {}: {} {} {} {} {}", + realm.toString(), + compactByIterators ? "iterators" : "cursors", + ALLOW_CURSOR_COMPACTION.getBoolean() ? "" : "cursors disabled", + strategy == null ? "no table compaction strategy" + : !strategy.supportsCursorCompaction() ? "no cursor support" + : "", + controller.shouldProvideTombstoneSources() ? "tombstone sources" : "", + realm.getIndexManager().hasIndexes() ? "has indexes" : "", + realm.metadata().enforceStrictLiveness() ? "strict liveness" : ""); + + if (compactByIterators) + return new CompactionOperationIterator(controller, actuallyCompact, fullyExpiredSSTables.size()); + else + return new CompactionOperationCursor(controller, actuallyCompact, fullyExpiredSSTables.size()); + } + + public static class OperationTotals + { + public final long inputDiskSize; + public final long inputUncompressedSize; + + OperationTotals(long inputDiskSize, long inputUncompressedSize) + { + this.inputDiskSize = inputDiskSize; + this.inputUncompressedSize = inputUncompressedSize; + } + } + + public static OperationTotals getOperationTotals(Collection sstables, Range tokenRange) + { + long inputDiskSize = 0; + long inputUncompressedSize = 0; + if (tokenRange == null) + { + for (SSTableReader rdr : sstables) + { + inputUncompressedSize += rdr.uncompressedLength(); + inputDiskSize += rdr.onDiskLength(); + } + } + else + { + var rangeList = ImmutableList.of(tokenRange); + for (SSTableReader rdr : sstables) { - // The set of sstables has changed (one or more were excluded due to limited available disk space). - // We need to recompute the overlaps between sstables. - controller.refreshOverlaps(); + final List positionsForRanges = rdr.getPositionsForRanges(rangeList); + for (SSTableReader.PartitionPositionBounds pp : positionsForRanges) + inputUncompressedSize += pp.upperPosition - pp.lowerPosition; + inputDiskSize += rdr.onDiskSizeForPartitionPositions(positionsForRanges); } + } + return new OperationTotals(inputDiskSize, inputUncompressedSize); + } - // sanity check: all sstables must belong to the same cfs - assert !Iterables.any(transaction.originals(), new Predicate() + @Override + public long getSpaceOverhead() + { + // This value should be quick to return and never change. + // We can calculate the total number of bytes in the inputSSTables, but that's something that can change if + // we remove sstable because expired sstables or fitting under the available disk space. + // So we throw instead and let UnifiedCompactionStrategy override this method. + throw new UnsupportedOperationException("Unimplemented in base class."); + } + + /** + * The compaction operation is a special case of an {@link AbstractTableOperation} and takes care of executing the + * actual compaction and releasing any resources when the compaction is finished. + *

    + * This class also extends {@link AbstractTableOperation} for reporting compaction-specific progress information. + */ + public abstract class CompactionOperation implements AutoCloseable, CompactionProgress + { + final CompactionController controller; + final TimeUUID taskId; + final String taskIdString; + final RateLimiter limiter; + private final long startTimeMillis; + final Set actuallyCompact; + protected final int fullyExpiredSSTablesCount; + private final long inputDiskSize; + private final long inputUncompressedSize; + + // resources that are updated and may be read by another thread + volatile Collection newSStables; + volatile long totalKeysWritten; + volatile long estimatedKeys; + + // resources that are updated but only read by this thread + boolean completed; + long lastCheckObsoletion; + + // resources that need closing + Refs sstableRefs; + TableOperation op; + Closeable obsCloseable; + CompactionAwareWriter writer; + + /** + * Create a new compaction operation. + *

    + * + * @param controller the compaction controller is needed by the scanners and compaction iterator to manage options + * @param actuallyCompact the set of sstables to compact (excludes any fully expired ones) + * @param fullyExpiredSSTablesCount the number of fully expired sstables (used in metrics) + */ + private CompactionOperation(CompactionController controller, Set actuallyCompact, int fullyExpiredSSTablesCount) + { + this.controller = controller; + this.actuallyCompact = actuallyCompact; + this.taskId = transaction.opId(); + this.taskIdString = transaction.opIdString(); + + this.limiter = CompactionManager.instance.getRateLimiter(); + this.startTimeMillis = Clock.Global.currentTimeMillis(); + this.newSStables = Collections.emptyList(); + this.fullyExpiredSSTablesCount = fullyExpiredSSTablesCount; + this.totalKeysWritten = 0; + this.estimatedKeys = 0; + this.completed = false; + this.inputDiskSize = totals.inputDiskSize; + this.inputUncompressedSize = totals.inputUncompressedSize; + + Directories dirs = getDirectories(); + + try { - @Override - public boolean apply(SSTableReader sstable) + // resources that need closing, must be created last in case of exceptions and released if there is an exception in the c.tor + this.sstableRefs = Refs.ref(actuallyCompact); + this.op = initializeSource(tokenRange()); + this.writer = getCompactionAwareWriter(realm, dirs, actuallyCompact); + CompactionProgress progress = this; + var sharedProgress = sharedProgress(); + if (sharedProgress != null) { - return !sstable.descriptor.cfname.equals(cfs.name); + sharedProgress.addSubtask(this); + progress = sharedProgress; } - }); - // new sstables from flush can be added during a compaction, but only the compaction can remove them, - // so in our single-threaded compaction world this is a valid way of determining if we're compacting - // all the sstables (that existed when we started) - StringBuilder ssTableLoggerMsg = new StringBuilder("["); - for (SSTableReader sstr : transaction.originals()) + if (null != opObserver) + this.obsCloseable = opObserver.onOperationStart(op); + for (var obs : getCompObservers()) + obs.onInProgress(progress); + } + catch (Throwable t) { - ssTableLoggerMsg.append(String.format("%s:level=%d, ", sstr.getFilename(), sstr.getSSTableLevel())); + close(t); + throw new AssertionError(t); // unreachable (close will throw when t is not null). Added for static analysis. } - ssTableLoggerMsg.append("]"); + } + + abstract TableOperation initializeSource(Range tokenRange) throws Throwable; + + private void execute() + { + try + { + // new sstables from flush can be added during a compaction, but only the compaction can remove them, + // so in our single-threaded compaction world this is a valid way of determining if we're compacting + // all the sstables (that existed when we started) + if (logger.isDebugEnabled()) + { + debugLogCompactingMessage(taskIdString); + } - logger.info("Compacting ({}) {}", taskId, ssTableLoggerMsg); + estimatedKeys = writer.estimatedKeys(); - RateLimiter limiter = CompactionManager.instance.getRateLimiter(); - long start = nanoTime(); - long startTime = currentTimeMillis(); - long totalKeysWritten = 0; - long estimatedKeys = 0; - long inputSizeBytes; - long timeSpentWritingKeys; + execute0(); - maybeNotifyIndexersAboutRowsInFullyExpiredSSTables(fullyExpiredSSTables); + // point of no return + newSStables = writer.finish(); + + completed = true; + } + catch (Throwable t) + { + Throwables.maybeFail(onError(t)); + } + } - if (!fullyExpiredSSTables.isEmpty()) + private Throwable onError(Throwable e) + { + if (e instanceof AssertionError) { - logger.debug("Compaction {} dropping expired sstables: {}", transaction.opId().toString(), fullyExpiredSSTables); - fullyExpiredSSTables.forEach(transaction::obsolete); + // Add additional information to help operators. + AssertionError error = new AssertionError( + String.format("Illegal input has been generated, most probably due to corruption in the input sstables\n" + + "\t%s\n" + + "Try scrubbing the sstables by running\n" + + "\tnodetool scrub %s %s\n", + transaction.originals(), + realm.getKeyspaceName(), + realm.getTableName())); + error.addSuppressed(e); + return error; } - Set actuallyCompact = Sets.difference(transaction.originals(), fullyExpiredSSTables); - Collection newSStables; + return e; + } - long[] mergedRowCounts; - long totalSourceCQLRows; + void maybeStopOrUpdateState() + { + op.throwIfStopRequested(); - long nowInSec = FBUtilities.nowInSeconds(); - try (Refs refs = Refs.ref(actuallyCompact); - AbstractCompactionStrategy.ScannerList scanners = strategy.getScanners(actuallyCompact); - CompactionIterator ci = new CompactionIterator(compactionType, scanners.scanners, controller, nowInSec, taskId)) + long now = Clock.Global.nanoTime(); + if (now - lastCheckObsoletion > TimeUnit.MINUTES.toNanos(1L)) { - long lastCheckObsoletion = start; - inputSizeBytes = scanners.getTotalCompressedSize(); - double compressionRatio = scanners.getCompressionRatio(); - if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO) - compressionRatio = 1.0; + controller.maybeRefreshOverlaps(); + lastCheckObsoletion = now; + } + } - long lastBytesScanned = 0; + abstract void execute0(); - activeCompactions.beginCompaction(ci); - try (CompactionAwareWriter writer = getCompactionAwareWriter(cfs, getDirectories(), transaction, actuallyCompact)) - { - // Note that we need to re-check this flag after calling beginCompaction above to avoid a window - // where the compaction does not exist in activeCompactions but the CSM gets paused. - // We already have the sstables marked compacting here so CompactionManager#waitForCessation will - // block until the below exception is thrown and the transaction is cancelled. - if (!controller.cfs.getCompactionStrategyManager().isActive()) - throw new CompactionInterruptedException(ci.getCompactionInfo()); - estimatedKeys = writer.estimatedKeys(); - while (ci.hasNext()) - { - if (writer.append(ci.next())) - totalKeysWritten++; + // + // Closeable + // - ci.setTargetDirectory(writer.getSStableDirectory().path()); - long bytesScanned = scanners.getTotalBytesScanned(); + @Override + public void close() + { + close(null); + } - // Rate limit the scanners, and account for compression - CompactionManager.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio); + public void close(Throwable errorsSoFar) + { + Throwable err = Throwables.close(errorsSoFar, obsCloseable, writer, sstableRefs); + final long elapsedTimeMillis = Clock.Global.currentTimeMillis() - startTimeMillis; - lastBytesScanned = bytesScanned; + if (transaction.isOffline()) + { + if (completed) + { + // update basic metrics + realm.metrics().incBytesCompacted(adjustedInputDiskSize(), + outputDiskSize(), + elapsedTimeMillis); + } + Throwables.maybeFail(err); + return; + } - if (nanoTime() - lastCheckObsoletion > TimeUnit.MINUTES.toNanos(1L)) - { - controller.maybeRefreshOverlaps(); - lastCheckObsoletion = nanoTime(); - } - } - timeSpentWritingKeys = TimeUnit.NANOSECONDS.toMillis(nanoTime() - start); + if (completed) + { + boolean shouldSignalCompletion = true; + var sharedProgress = sharedProgress(); + if (sharedProgress != null) + shouldSignalCompletion = sharedProgress.completeSubtask(this); - // point of no return - newSStables = writer.finish(); - } - finally + if (shouldSignalCompletion) { - activeCompactions.finishCompaction(ci); - mergedRowCounts = ci.getMergedRowCounts(); - totalSourceCQLRows = ci.getTotalSourceCQLRows(); + if (COMPACTION_HISTORY_ENABLED.getBoolean()) + { + updateCompactionHistory(taskId, realm.getKeyspaceName(), realm.getTableName(), this, ImmutableMap.of(COMPACTION_TYPE_PROPERTY, compactionType.type)); + } + CompactionManager.instance.incrementRemovedExpiredSSTables(fullyExpiredSSTablesCount); + if (!transaction.originals().isEmpty() && actuallyCompact.isEmpty()) + // this CompactionOperation only deleted fully expired SSTables without compacting anything + CompactionManager.instance.incrementDeleteOnlyCompactions(); } + + if (logger.isDebugEnabled()) + debugLogCompactionSummaryInfo(taskIdString, elapsedTimeMillis, totalKeysWritten, newSStables, this); + if (logger.isTraceEnabled()) + traceLogCompactionSummaryInfo(totalKeysWritten, estimatedKeys, this); + if (strategy != null) + strategy.getCompactionLogger().compaction(startTimeMillis, + transaction.originals(), + tokenRange(), + Clock.Global.currentTimeMillis(), + newSStables); + + // update the metrics + realm.metrics().incBytesCompacted(adjustedInputDiskSize(), + outputDiskSize(), + elapsedTimeMillis); } - if (transaction.isOffline()) - return; + Throwables.maybeFail(err); + } - // log a bunch of statistics about the result and save to system table compaction_history - long durationInNano = nanoTime() - start; - long dTime = TimeUnit.NANOSECONDS.toMillis(durationInNano); - long startsize = inputSizeBytes; - long endsize = SSTableReader.getTotalBytes(newSStables); - double ratio = (double) endsize / (double) startsize; - - StringBuilder newSSTableNames = new StringBuilder(); - for (SSTableReader reader : newSStables) - newSSTableNames.append(reader.descriptor.baseFile()).append(","); - long totalSourceRows = 0; - for (int i = 0; i < mergedRowCounts.length; i++) - totalSourceRows += mergedRowCounts[i] * (i + 1); - - String mergeSummary = updateCompactionHistory(taskId, cfs.getKeyspaceName(), cfs.getTableName(), mergedRowCounts, startsize, endsize, - ImmutableMap.of(COMPACTION_TYPE_PROPERTY, compactionType.type)); - - logger.info(String.format("Compacted (%s) %d sstables to [%s] to level=%d. %s to %s (~%d%% of original) in %,dms. Read Throughput = %s, Write Throughput = %s, Row Throughput = ~%,d/s. %,d total partitions merged to %,d. Partition merge counts were {%s}. Time spent writing keys = %,dms", - taskId, - transaction.originals().size(), - newSSTableNames.toString(), - getLevel(), - FBUtilities.prettyPrintMemory(startsize), - FBUtilities.prettyPrintMemory(endsize), - (int) (ratio * 100), - dTime, - FBUtilities.prettyPrintMemoryPerSecond(startsize, durationInNano), - FBUtilities.prettyPrintMemoryPerSecond(endsize, durationInNano), - (int) totalSourceCQLRows / (TimeUnit.NANOSECONDS.toSeconds(durationInNano) + 1), - totalSourceRows, - totalKeysWritten, - mergeSummary, - timeSpentWritingKeys)); - if (logger.isTraceEnabled()) - { - logger.trace("CF Total Bytes Compacted: {}", FBUtilities.prettyPrintMemory(CompactionTask.addToTotalBytesCompacted(endsize))); - logger.trace("Actual #keys: {}, Estimated #keys:{}, Err%: {}", totalKeysWritten, estimatedKeys, ((double)(totalKeysWritten - estimatedKeys)/totalKeysWritten)); - } - cfs.getCompactionStrategyManager().compactionLogger.compaction(startTime, transaction.originals(), currentTimeMillis(), newSStables); + @Override + public Optional keyspace() + { + return Optional.of(metadata().keyspace); + } + + @Override + public Optional table() + { + return Optional.of(metadata().name); + } + + @Override + public TableMetadata metadata() + { + return realm.metadata(); + } + + @Override + public OperationType operationType() + { + return compactionType; + } + + @Override + public TimeUUID operationId() + { + return taskId; + } + + @Override + public TableOperation.Unit unit() + { + return TableOperation.Unit.BYTES; + } + + @Override + public Set sstables() + { + return transaction.originals(); + } + + @Override + public String toString() + { + return progressToString(); + } + + // + // CompactionProgress + // + + @Override + @Nullable + public CompactionStrategy strategy() + { + return CompactionTask.this.strategy; + } + + @Override + public Collection inSSTables() + { + // TODO should we use transaction.originals() and include the expired sstables? + // This would be more correct but all the metrics we get from CompactionIterator will not be compatible + return actuallyCompact; + } + + @Override + public Collection outSSTables() + { + return newSStables; + } + + @Override + public long inputDiskSize() + { + return inputDiskSize; + } + + /** + * @return the initial number of bytes for input sstables. For compressed or encrypted sstables, + * this is the number of bytes after decompression, so this is the uncompressed length of sstable files. + */ + public long total() + { + return inputUncompressedSize; + } - // update the metrics - cfs.metric.compactionBytesWritten.inc(endsize); + @Override + public long inputUncompressedSize() + { + return inputUncompressedSize; + } + + @Override + public long outputDiskSize() + { + return CompactionSSTable.getTotalDataBytes(newSStables); + } + + @Override + public long uncompressedBytesWritten() + { + return writer.bytesWritten(); + } + + @Override + public long startTimeMillis() + { + return startTimeMillis; } } - @Override - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, - Directories directories, - LifecycleTransaction transaction, - Set nonExpiredSSTables) + /** + * The compaction operation is a special case of an {@link AbstractTableOperation} and takes care of executing the + * actual compaction and releasing any resources when the compaction is finished. + *

    + * This class also extends {@link AbstractTableOperation} for reporting compaction-specific progress information. + */ + public final class CompactionOperationIterator extends CompactionOperation { - return new DefaultCompactionWriter(cfs, directories, transaction, nonExpiredSSTables, keepOriginals, getLevel()); + // resources that need closing + private ScannerList scanners; + private CompactionIterator compactionIterator; + + /** + * Create a new compaction operation. + *

    + * @param controller the compaction controller is needed by the scanners and compaction iterator to manage options + */ + CompactionOperationIterator(CompactionController controller, Set actuallyCompact, int fullyExpiredSSTablesCount) + { + super(controller, actuallyCompact, fullyExpiredSSTablesCount); + } + + @Override + TableOperation initializeSource(Range tokenRange) + { + var rangeList = tokenRange != null ? ImmutableList.of(tokenRange) : null; + this.scanners = strategy != null ? strategy.getScanners(actuallyCompact, rangeList) + : ScannerList.of(actuallyCompact, rangeList); + // We use `this` rather than `sharedProgress()` because the `TableOperation` tracks individual compactions. + this.compactionIterator = new CompactionIterator(compactionType, scanners.scanners, controller, FBUtilities.nowInSeconds(), taskId, null, this); + return compactionIterator.getOperation(); + } + + void execute0() + { + double compressionRatio = compactionIterator.getCompressionRatio(); + if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO) + compressionRatio = 1.0; + + long lastBytesScanned = 0; + + while (compactionIterator.hasNext()) + { + UnfilteredRowIterator partition = compactionIterator.next(); + if (writer.append(partition) != null) + totalKeysWritten++; + + long bytesScanned = compactionIterator.bytesRead(); + + // Rate limit the scanners, and account for compression + if (CompactionManager.instance.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio)) + lastBytesScanned = bytesScanned; + + maybeStopOrUpdateState(); + } + } + + @Override + public void close(Throwable errorsSoFar) + { + super.close(Throwables.close(errorsSoFar, compactionIterator, scanners)); + } + + /** + * @return the number of bytes read by the compaction iterator. For compressed or encrypted sstables, + * this is the number of bytes processed by the iterator after decompression, so this is the current + * position in the uncompressed sstable files. + */ + @Override + public long completed() + { + return compactionIterator.bytesRead(); + } + + @Override + public long adjustedInputDiskSize() + { + return compactionIterator.getTotalCompressedSize(); + } + + @Override + public long uncompressedBytesRead() + { + return compactionIterator.bytesRead(); + } + + @Override + public long uncompressedBytesRead(int level) + { + return compactionIterator.bytesRead(level); + } + + @Override + public long partitionsRead() + { + return compactionIterator.totalSourcePartitions(); + } + + @Override + public long rowsRead() + { + return compactionIterator.totalSourceRows(); + } + + @Override + public long[] partitionsHistogram() + { + return compactionIterator.mergedPartitionsHistogram(); + } + + @Override + public long[] rowsHistogram() + { + return compactionIterator.mergedRowsHistogram(); + } + } - public static String updateCompactionHistory(TimeUUID taskId, String keyspaceName, String columnFamilyName, long[] mergedRowCounts, long startSize, long endSize, Map compactionProperties) + /** + * Cursor version of the above. + */ + public final class CompactionOperationCursor extends CompactionOperation { - StringBuilder mergeSummary = new StringBuilder(mergedRowCounts.length * 10); - Map mergedRows = new HashMap<>(); - for (int i = 0; i < mergedRowCounts.length; i++) + // resources that need closing + private CompactionCursor compactionCursor; + + /** + * Create a new compaction operation. + *

    + * @param controller the compaction controller is needed by the scanners and compaction iterator to manage options + */ + CompactionOperationCursor(CompactionController controller, Set actuallyCompact, int fullyExpiredSSTablesCount) { - long count = mergedRowCounts[i]; - if (count == 0) - continue; + super(controller, actuallyCompact, fullyExpiredSSTablesCount); + } - int rows = i + 1; - mergeSummary.append(String.format("%d:%d, ", rows, count)); - mergedRows.put(rows, count); + @Override + TableOperation initializeSource(Range tokenRange) + { + this.compactionCursor = new CompactionCursor(compactionType, actuallyCompact, tokenRange, controller, limiter, FBUtilities.nowInSeconds()); + // We use `this` rather than `sharedProgress()` because the `TableOperation` tracks individual compactions. + return compactionCursor.createOperation(this); } - SystemKeyspace.updateCompactionHistory(taskId, keyspaceName, columnFamilyName, currentTimeMillis(), startSize, endSize, mergedRows, compactionProperties); - return mergeSummary.toString(); + + void execute0() + { + try + { + writeLoop: + while (true) + { + op.throwIfStopRequested(); + + switch (compactionCursor.copyOne(writer)) + { + case EXHAUSTED: + break writeLoop; + case PARTITION: + ++totalKeysWritten; + maybeStopOrUpdateState(); + break; + } + } + } + catch (IOException e) + { + throw new FSWriteError(e, writer.getCurrentFileName()); + } + } + + @Override + public void close(Throwable errorsSoFar) + { + super.close(Throwables.close(errorsSoFar, compactionCursor)); + } + + /** + * @return the number of bytes read by the compaction iterator. For compressed or encrypted sstables, + * this is the number of bytes processed by the iterator after decompression, so this is the current + * position in the uncompressed sstable files. + */ + @Override + public long completed() + { + return compactionCursor.bytesRead(); + } + + @Override + public long adjustedInputDiskSize() + { + return inputDiskSize(); + } + + @Override + public long uncompressedBytesRead() + { + return compactionCursor.bytesRead(); + } + + @Override + public long uncompressedBytesRead(int level) + { + // Cursors don't implement LCS per-level progress tracking. + return 0L; + } + + @Override + public long partitionsRead() + { + return compactionCursor.totalSourcePartitions(); + } + + @Override + public long rowsRead() + { + return compactionCursor.totalSourceRows(); + } + + @Override + public long[] partitionsHistogram() + { + return compactionCursor.mergedPartitionsHistogram(); + } + + @Override + public long[] rowsHistogram() + { + return compactionCursor.mergedRowsHistogram(); + } + } + + public CompactionAwareWriter getCompactionAwareWriter(CompactionRealm realm, + Directories directories, + Set nonExpiredSSTables) + { + return new DefaultCompactionWriter(realm, directories, transaction, nonExpiredSSTables, keepOriginals, getLevel()); } protected Directories getDirectories() { - return cfs.getDirectories(); + return realm.getDirectories(); } public static long getMinRepairedAt(Set actuallyCompact) @@ -352,7 +976,12 @@ public static TimeUUID getPendingRepair(Set sstables) ids.add(sstable.getSSTableMetadata().pendingRepair); if (ids.size() != 1) - throw new RuntimeException(String.format("Attempting to compact pending repair sstables with sstables from other repair, or sstables not pending repair: %s", ids)); + { + if (!SKIP_REPAIR_STATE_CHECKING) + throw new RuntimeException(String.format("Attempting to compact pending repair sstables with sstables from other repair, or sstables not pending repair: %s", ids)); + // otherwise we should continue but mark the result as unrepaired + return ActiveRepairService.NO_PENDING_REPAIR; + } return ids.iterator().next(); } @@ -374,23 +1003,21 @@ public static boolean getIsTransient(Set sstables) return isTransient; } - - /* + /** * Checks if we have enough disk space to execute the compaction. Drops the largest sstable out of the Task until * there's enough space (in theory) to handle the compaction. * * @return true if there is enough disk space to execute the complete compaction, false if some sstables are excluded. + * If SSTables are excluded, they are removed from the transaction as well as the nonExpiredSSTables set. */ - protected boolean buildCompactionCandidatesForAvailableDiskSpace(final Set fullyExpiredSSTables, TimeUUID taskId) + protected boolean buildCompactionCandidatesForAvailableDiskSpace(Set nonExpiredSSTables, TimeUUID taskId, boolean containsExpired) { - if(!cfs.isCompactionDiskSpaceCheckEnabled() && compactionType == OperationType.COMPACTION) + if(!realm.isCompactionDiskSpaceCheckEnabled() && compactionType == OperationType.COMPACTION) { logger.info("Compaction space check is disabled - trying to compact all sstables"); return true; } - final Set nonExpiredSSTables = Sets.difference(transaction.originals(), fullyExpiredSSTables); - CompactionStrategyManager strategy = cfs.getCompactionStrategyManager(); int sstablesRemoved = 0; while(!nonExpiredSSTables.isEmpty()) @@ -399,9 +1026,10 @@ protected boolean buildCompactionCandidatesForAvailableDiskSpace(final Set expectedNewWriteSize = new HashMap<>(); - List newCompactionDatadirs = cfs.getDirectoriesForFiles(nonExpiredSSTables); + List newCompactionDatadirs = realm.getDirectoriesForFiles(nonExpiredSSTables); long writeSizePerOutputDatadir = writeSize / Math.max(newCompactionDatadirs.size(), 1); for (File directory : newCompactionDatadirs) expectedNewWriteSize.put(directory, writeSizePerOutputDatadir); @@ -409,7 +1037,7 @@ protected boolean buildCompactionCandidatesForAvailableDiskSpace(final Set expectedWriteSize = CompactionManager.instance.active.estimatedRemainingWriteBytes(); // todo: abort streams if they block compactions - if (cfs.getDirectories().hasDiskSpaceForCompactionsAndStreams(expectedNewWriteSize, expectedWriteSize)) + if (realm.getDirectories().hasDiskSpaceForCompactionsAndStreams(expectedNewWriteSize, expectedWriteSize)) break; } catch (Exception e) @@ -423,18 +1051,20 @@ protected boolean buildCompactionCandidatesForAvailableDiskSpace(final Set 0 ) + // but we can still remove expired SSTables + if (partialCompactionsAcceptable() && containsExpired) { - // sanity check to make sure we compact only fully expired SSTables. - assert transaction.originals().equals(fullyExpiredSSTables); + for (SSTableReader rdr : nonExpiredSSTables) + transaction.cancel(rdr); + nonExpiredSSTables.clear(); + assert transaction.originals().size() > 0; break; } String msg = String.format("Not enough space for compaction (%s) of %s.%s, estimated sstables = %d, expected write size = %d", taskId, - cfs.getKeyspaceName(), - cfs.name, + realm.getKeyspaceName(), + realm.getTableName(), Math.max(1, writeSize / strategy.getMaxSSTableBytes()), writeSize); logger.warn(msg); @@ -463,7 +1093,7 @@ protected int getLevel() protected CompactionController getCompactionController(Set toCompact) { - return new CompactionController(cfs, toCompact, gcBefore); + return new CompactionController(realm, toCompact, gcBefore); } protected boolean partialCompactionsAcceptable() @@ -482,13 +1112,13 @@ public static long getMaxDataAge(Collection sstables) return max; } - private void maybeNotifyIndexersAboutRowsInFullyExpiredSSTables(Set fullyExpiredSSTables) + private void maybeNotifyIndexersAboutRowsInFullyExpiredSSTables(Set fullyExpiredSSTables) { if (fullyExpiredSSTables.isEmpty()) return; List indexes = new ArrayList<>(); - for (Index index : cfs.indexManager.listIndexes()) + for (Index index : realm.getIndexManager().listIndexes()) { if (index.notifyIndexerAboutRowsInFullyExpiredSSTables()) indexes.add(index); @@ -497,50 +1127,163 @@ private void maybeNotifyIndexersAboutRowsInFullyExpiredSSTables(Set indexers = new ArrayList<>(); - for (int i = 0; i < indexes.size(); i++) + try (UnfilteredRowIterator partition = scanner.next(); + WriteContext ctx = Keyspace.open(realm.metadata().keyspace).getWriteHandler().createContextForIndexing()) { - Index.Indexer indexer = indexes.get(i).indexerFor(partition.partitionKey(), - partition.columns(), - FBUtilities.nowInSeconds(), - ctx, - IndexTransaction.Type.COMPACTION, - null); - - if (indexer != null) - indexers.add(indexer); - } - - if (!indexers.isEmpty()) - { - for (Index.Indexer indexer : indexers) - indexer.begin(); + List indexers = new ArrayList<>(); + for (int i = 0; i < indexes.size(); i++) + { + Index.Indexer indexer = indexes.get(i).indexerFor(partition.partitionKey(), + partition.columns(), + FBUtilities.nowInSeconds(), + ctx, + IndexTransaction.Type.COMPACTION, + null); + + if (indexer != null) + indexers.add(indexer); + } - while (partition.hasNext()) + if (!indexers.isEmpty()) { - Unfiltered unfiltered = partition.next(); - if (unfiltered instanceof Row) + for (Index.Indexer indexer : indexers) + indexer.begin(); + + while (partition.hasNext()) { - for (Index.Indexer indexer : indexers) - indexer.removeRow((Row) unfiltered); + Unfiltered unfiltered = partition.next(); + if (unfiltered instanceof Row) + { + for (Index.Indexer indexer : indexers) + indexer.removeRow((Row) unfiltered); + } } - } - for (Index.Indexer indexer : indexers) - indexer.finish(); + for (Index.Indexer indexer : indexers) + indexer.finish(); + } } } } } } } + + private void debugLogCompactionSummaryInfo(String taskId, + long durationInMillis, + long totalKeysWritten, + Collection newSStables, + CompactionProgress progress) + { + // log a bunch of statistics about the result and save to system table compaction_history + long totalMergedPartitions = 0; + long[] mergedPartitionCounts = progress.partitionsHistogram(); + StringBuilder mergeSummary = new StringBuilder(mergedPartitionCounts.length * 10); + mergeSummary.append('{'); + for (int i = 0; i < mergedPartitionCounts.length; i++) + { + long mergedPartitionCount = mergedPartitionCounts[i]; + if (mergedPartitionCount != 0) + { + totalMergedPartitions += mergedPartitionCount * (i + 1); + mergeSummary.append(i).append(':').append(mergedPartitionCount).append(", "); + } + } + mergeSummary.append('}'); + + StringBuilder newSSTableNames = new StringBuilder(newSStables.size() * 100); + for (SSTableReader reader : newSStables) + newSSTableNames.append(reader.descriptor.baseFileUri()).append(','); + long durationInNano = TimeUnit.MILLISECONDS.toNanos(durationInMillis); + int level = getLevel(); + if (level == 0 && strategy != null) + level = strategy.getLevel(transaction); + + logger.debug("Compacted ({}{}) {} sstables to [{}]{}. {} to {} (~{}% of original) in {}ms. " + + "Read Throughput = {}, Write Throughput = {}, Row Throughput = ~{}/s, Partition Throughput = ~{}/s." + + " {} total partitions merged to {}. Partition merge counts were {}.", + taskId, + tokenRange() != null ? " range " + tokenRange() : "", + transaction.originals().size(), + newSSTableNames, + level >= 0 ? " in level=" + level : "", + prettyPrintMemory(progress.adjustedInputDiskSize()), + prettyPrintMemory(progress.outputDiskSize()), + (int) (progress.sizeRatio() * 100), + durationInMillis, + prettyPrintMemoryPerSecond(progress.adjustedInputDiskSize(), durationInNano), + prettyPrintMemoryPerSecond(progress.outputDiskSize(), durationInNano), + (long) (progress.rowsRead() * 1.0e-3 / durationInMillis), + (long) (progress.partitionsRead() * 1.0e-3 / durationInMillis), + totalMergedPartitions, + totalKeysWritten, + mergeSummary); + } + + private void debugLogCompactingMessage(String taskId) + { + Set originals = transaction.originals(); + StringBuilder ssTableLoggerMsg = new StringBuilder(originals.size() * 100); + ssTableLoggerMsg.append("Compacting (").append(taskId); + if (tokenRange() != null) + ssTableLoggerMsg.append(" range ").append(tokenRange()); + ssTableLoggerMsg.append(") ["); + for (SSTableReader sstr : originals) + { + ssTableLoggerMsg.append(sstr.getFilename()); + if (sstr.getSSTableLevel() != 0) + ssTableLoggerMsg.append(":level=").append(sstr.getSSTableLevel()); + ssTableLoggerMsg.append(", "); + } + ssTableLoggerMsg.append(']'); + + logger.debug(ssTableLoggerMsg.toString()); + } + + + private static void updateCompactionHistory(TimeUUID id, + String keyspaceName, + String columnFamilyName, + CompactionProgress progress, + Map compactionProperties) + { + long[] mergedPartitionsHistogram = progress.partitionsHistogram(); + Map mergedPartitions = new HashMap<>(mergedPartitionsHistogram.length); + for (int i = 0; i < mergedPartitionsHistogram.length; i++) + { + long count = mergedPartitionsHistogram[i]; + if (count == 0) + continue; + + int rows = i + 1; + mergedPartitions.put(rows, count); + } + SystemKeyspace.updateCompactionHistory(id, + keyspaceName, + columnFamilyName, + Clock.Global.currentTimeMillis(), + progress.adjustedInputDiskSize(), + progress.outputDiskSize(), + mergedPartitions, + compactionProperties); + } + + private void traceLogCompactionSummaryInfo(long totalKeysWritten, + long estimatedKeys, + CompactionProgress progress) + { + logger.trace("CF Total Bytes Compacted: {}", prettyPrintMemory(addToTotalBytesCompacted(progress.outputDiskSize()))); + logger.trace("Actual #keys: {}, Estimated #keys:{}, Err%: {}", + totalKeysWritten, + estimatedKeys, + ((double) (totalKeysWritten - estimatedKeys) / totalKeysWritten)); + } } diff --git a/src/java/org/apache/cassandra/db/compaction/CompositeCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/CompositeCompactionTask.java new file mode 100644 index 000000000000..27620fa7128b --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CompositeCompactionTask.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Throwables; + +/// A composition of several compaction tasks into one. This object executes the given tasks sequentially and +/// is used to limit the parallelism of some compaction tasks that split into a large number of parallelizable ones +/// but should not be allowed to take all compaction executor threads. +public class CompositeCompactionTask extends AbstractCompactionTask +{ + @VisibleForTesting + final ArrayList tasks; + + public CompositeCompactionTask(AbstractCompactionTask first) + { + super(first.realm, first.realm.tryModify(Collections.emptyList(), OperationType.COMPACTION)); + tasks = new ArrayList<>(); + addTask(first); + } + + /// Add a task to the composition. + public CompositeCompactionTask addTask(AbstractCompactionTask task) + { + tasks.add(task); + return this; + } + + @Override + protected void runMayThrow() throws Exception + { + // Run all tasks in sequence, regardless if any of them fail. + Throwable accumulate = null; + for (AbstractCompactionTask task : tasks) + { + accumulate = Throwables.perform(accumulate, () -> task.execute(opObserver)); + // The previous operation may have completed due to a requested stop. We do not stop other tasks in our + // list if that is the case, because if the tasks are related, the [SharedTableOperation] will have already + // requested a stop from the other components as well. If we stopped the other tasks here, we may + // overrespond to a user's request to stop an individual operation. + // On the other hand, [CompactionManager] sometimes requests a stop of all ongoing operations e.g. to + // initiate a table drop. Such requests, however, do not affect tasks in the executor queue; as this class + // is acting similarly to an executor queue, we do not apply such stop requests to the remaining tasks + // either. + } + Throwables.maybeFail(accumulate); + } + + @Override + public Throwable rejected(Throwable t) + { + for (AbstractCompactionTask task : tasks) + t = task.rejected(t); + return super.rejected(t); + } + + @Override + public boolean cancelIfAffects(CompactionRealm realm, Predicate sstablePredicate, TableOperation.StopTrigger trigger) + { + // Leave cancellation to the individual tasks. + return false; + } + + @Override + public AbstractCompactionTask setUserDefined(boolean isUserDefined) + { + for (AbstractCompactionTask task : tasks) + task.setUserDefined(isUserDefined); + return super.setUserDefined(isUserDefined); + } + + @Override + public AbstractCompactionTask setCompactionType(OperationType compactionType) + { + for (AbstractCompactionTask task : tasks) + task.setCompactionType(compactionType); + return super.setCompactionType(compactionType); + } + + @Override + public void addObserver(CompactionObserver compObserver) + { + for (AbstractCompactionTask task : tasks) + task.addObserver(compObserver); + super.addObserver(compObserver); + } + + @Override + public String toString() + { + return "Composite " + tasks; + } + + @Override + public long getSpaceOverhead() + { + throw new UnsupportedOperationException("Cannot calculate space overhead for composite tasks"); + } + + /// Limit the parallelism of a list of compaction tasks by combining them into a smaller number of composite tasks. + /// This method assumes that the caller has preference for the tasks to be executed in order close to the order of + /// the input list. See [UnifiedCompactionStrategy#getMaximalTasks] for an example of how to use this method. + public static List applyParallelismLimit(List tasks, int parallelismLimit) + { + if (tasks.size() <= parallelismLimit || parallelismLimit <= 0) + return tasks; + + List result = new ArrayList<>(parallelismLimit); + int taskIndex = 0; + for (AbstractCompactionTask task : tasks) + { + if (result.size() < parallelismLimit) + result.add(task); + else + { + result.set(taskIndex, combineTasks(result.get(taskIndex), task)); + if (++taskIndex == parallelismLimit) + taskIndex = 0; + } + } + return result; + } + + /// Make a composite tasks that combines two tasks. If the former is already a composite task, the latter is added + /// to it. Otherwise, a new composite task is created. + public static CompositeCompactionTask combineTasks(AbstractCompactionTask task1, AbstractCompactionTask task2) + { + CompositeCompactionTask composite; + if (task1 instanceof CompositeCompactionTask) + composite = (CompositeCompactionTask) task1; + else + composite = new CompositeCompactionTask(task1); + return composite.addTask(task2); + } + +} diff --git a/src/java/org/apache/cassandra/db/compaction/DelegatingShardManager.java b/src/java/org/apache/cassandra/db/compaction/DelegatingShardManager.java new file mode 100644 index 000000000000..a9702e056cc7 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/DelegatingShardManager.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.function.IntFunction; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; + +/** + * A shard manager that delegates to a token generator for determining shard boundaries. + */ +public class DelegatingShardManager implements ShardManager +{ + private final IntFunction tokenGenerator; + private CompactionRealm realm; + + public DelegatingShardManager(IntFunction tokenGenerator, CompactionRealm realm) + { + this.tokenGenerator = tokenGenerator; + this.realm = realm; + } + + @Override + public double rangeSpanned(Range tableRange) + { + return tableRange.left.size(tableRange.right); + } + + @Override + public double localSpaceCoverage() + { + // This manager is global, so it owns the whole range. + return 1; + } + + @Override + public double shardSetCoverage() + { + // For now there are no disks defined, so this is the same as localSpaceCoverage + return 1; + } + + @Override + public double minimumPerPartitionSpan() + { + return localSpaceCoverage() / Math.max(1, realm.estimatedPartitionCountInSSTables()); + } + + @Override + public ShardTracker boundaries(int shardCount) + { + var tokens = tokenGenerator.apply(shardCount); + return new SimpleShardTracker(tokens); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/ExpirationTask.java b/src/java/org/apache/cassandra/db/compaction/ExpirationTask.java new file mode 100644 index 000000000000..08899ae9508d --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/ExpirationTask.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + + +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; + +/// SSTable expiration task. +/// +/// This is used when compaction identifies fully-expired SSTables that can be safely deleted. Executing the task +/// simply commits the associated transaction which has the effect of deleting the source SSTables. +public class ExpirationTask extends AbstractCompactionTask +{ + protected ExpirationTask(CompactionRealm realm, ILifecycleTransaction transaction) + { + super(realm, transaction); + } + + @Override + protected void runMayThrow() throws Exception + { + transaction.obsoleteOriginals(); + transaction.prepareToCommit(); + transaction.commit(); + CompactionManager.instance.incrementDeleteOnlyCompactions(); + } + + @Override + public long getSpaceOverhead() + { + return 0; // This is just deleting files, no overhead. + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/LegacyAbstractCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/LegacyAbstractCompactionStrategy.java new file mode 100644 index 000000000000..fee7aa9825b0 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/LegacyAbstractCompactionStrategy.java @@ -0,0 +1,354 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Clock; + +/** + * Pluggable compaction strategy determines how SSTables get merged. + * + * There are two main goals: + * - perform background compaction constantly as needed; this typically makes a tradeoff between + * i/o done by compaction, and merging done at read time. + * - perform a full (maximum possible) compaction if requested by the user + */ +abstract class LegacyAbstractCompactionStrategy extends AbstractCompactionStrategy +{ + protected LegacyAbstractCompactionStrategy(CompactionStrategyFactory factory, Map options) + { + super(factory, new BackgroundCompactions(factory.getRealm()), options); + assert factory != null; + } + + /** + * Helper base class for strategies that provide CompactionAggregates, implementing the typical + * getNextBackgroundTasks logic based on a getNextBackgroundAggregate method. + */ + protected static abstract class WithAggregates extends LegacyAbstractCompactionStrategy + { + protected WithAggregates(CompactionStrategyFactory factory, Map options) + { + super(factory, options); + } + + @Override + @SuppressWarnings("resource") + public Collection getNextBackgroundTasks(long gcBefore) + { + CompactionPick previous = null; + while (true) + { + CompactionAggregate compaction = getNextBackgroundAggregate(gcBefore); + if (compaction == null || compaction.isEmpty()) + return ImmutableList.of(); + + // Already tried acquiring references without success. It means there is a race with + // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager + if (compaction.getSelected().equals(previous)) + { + logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," + + "unless it happens frequently, in which case it must be reported. Will retry later.", + compaction.getSelected()); + return ImmutableList.of(); + } + + CompactionPick selected = compaction.getSelected(); + Preconditions.checkNotNull(selected); + + LifecycleTransaction transaction = realm.tryModify(selected.sstables(), + OperationType.COMPACTION, + selected.id()); + if (transaction != null) + { + backgroundCompactions.setSubmitted(this, transaction.opId(), compaction); + return ImmutableList.of(createCompactionTask(gcBefore, transaction, compaction)); + } + + // Getting references to the sstables failed. This may be because we tried to compact sstables that are + // no longer present (due to races in getting the notification), or because we still haven't + // received any replace notifications. Remove any non-live sstables we track and try again. + removeDeadSSTables(); + + previous = selected; + } + } + + /** + * Select the next compaction to perform. This method is typically synchronized. + */ + protected abstract CompactionAggregate getNextBackgroundAggregate(long gcBefore); + + protected AbstractCompactionTask createCompactionTask(final long gcBefore, LifecycleTransaction txn, CompactionAggregate compaction) + { + return new CompactionTask(realm, txn, gcBefore, false, this); + } + + /** + * Get the estimated remaining compactions. Strategies that implement {@link WithAggregates} can delegate this + * to {@link BackgroundCompactions} because they set the pending aggregates as background compactions but legacy + * strategies that do not support aggregates must implement this method. + *

    + * @return the number of background tasks estimated to still be needed for this strategy + */ + @Override + public int getEstimatedRemainingTasks() + { + return backgroundCompactions.getEstimatedRemainingTasks(); + } + } + + /** + * Helper base class for (older, deprecated) strategies that provide a list of tables to compact, implementing the + * typical getNextBackgroundTask logic based on a getNextBackgroundSSTables method. + */ + protected static abstract class WithSSTableList extends LegacyAbstractCompactionStrategy + { + protected WithSSTableList(CompactionStrategyFactory factory, Map options) + { + super(factory, options); + } + + @Override + @SuppressWarnings("resource") + public Collection getNextBackgroundTasks(long gcBefore) + { + List previousCandidate = null; + while (true) + { + List latestBucket = getNextBackgroundSSTables(gcBefore); + + if (latestBucket.isEmpty()) + return ImmutableList.of(); + + // Already tried acquiring references without success. It means there is a race with + // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager + if (latestBucket.equals(previousCandidate)) + { + logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," + + "unless it happens frequently, in which case it must be reported. Will retry later.", + latestBucket); + return ImmutableList.of(); + } + + LifecycleTransaction modifier = realm.tryModify(latestBucket, OperationType.COMPACTION); + if (modifier != null) + return ImmutableList.of(createCompactionTask(gcBefore, modifier, false, false)); + + // Getting references to the sstables failed. This may be because we tried to compact sstables that are + // no longer present (due to races in getting the notification), or because we still haven't + // received any replace notifications. Remove any non-live sstables we track and try again. + removeDeadSSTables(); + + previousCandidate = latestBucket; + } + } + + /** + * Select the next tables to compact. This method is typically synchronized. + * @return + */ + protected abstract List getNextBackgroundSSTables(final long gcBefore); + } + + /** + * Replaces sstables in the compaction strategy + * + * Note that implementations must be able to handle duplicate notifications here (that removed are already gone and + * added have already been added) + */ + public abstract void replaceSSTables(Collection removed, Collection added); + + /** + * Adds sstable, note that implementations must handle duplicate notifications here (added already being in the compaction strategy) + */ + abstract void addSSTable(CompactionSSTable added); + + /** + * Adds sstables, note that implementations must handle duplicate notifications here (added already being in the compaction strategy) + */ + public synchronized void addSSTables(Iterable added) + { + for (CompactionSSTable sstable : added) + addSSTable(sstable); + } + + /** + * Removes sstable from the strategy, implementations must be able to handle the sstable having already been removed. + */ + abstract void removeSSTable(CompactionSSTable sstable); + + /** + * Removes sstables from the strategy, implementations must be able to handle the sstables having already been removed. + */ + public void removeSSTables(Iterable removed) + { + for (CompactionSSTable sstable : removed) + removeSSTable(sstable); + } + + /** + * Remove any tracked sstable that is no longer in the live set. Note that because we get notifications after the + * tracker is modified, anything we know of must be already in the live set. If it is not, it has been removed + * from there, and we either haven't received the removal notification yet, or we did and we messed it up (i.e. + * we got it before the addition). The former is transient, but the latter can cause persistent problems, including + * fully stopping compaction. In any case, we should remove any such sstables. + * There is a special-case implementation of this in LeveledManifest. + */ + abstract void removeDeadSSTables(); + + void removeDeadSSTables(Iterable sstables) + { + synchronized (sstables) + { + int removed = 0; + Set liveSet = realm.getLiveSSTables(); + for (Iterator it = sstables.iterator(); it.hasNext(); ) + { + CompactionSSTable sstable = it.next(); + if (!liveSet.contains(sstable)) + { + it.remove(); + ++removed; + } + } + + if (removed > 0) + logger.debug("Removed {} dead sstables from the compactions tracked list.", removed); + } + } + + @Override + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) + { + removeDeadSSTables(); + return super.getMaximalTasks(gcBefore, splitOutput, permittedParallelism); + } + + @Override + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism, OperationType operationType) + { + removeDeadSSTables(); + return super.getMaximalTasks(gcBefore, splitOutput, permittedParallelism, operationType); + } + + + /** + * Select a table for tombstone-removing compaction from the given set. Returns null if no table is suitable. + */ + @Nullable + CompactionAggregate makeTombstoneCompaction(long gcBefore, + Iterable candidates, + Function, CompactionSSTable> selector) + { + List sstablesWithTombstones = new ArrayList<>(); + for (CompactionSSTable sstable : candidates) + { + if (worthDroppingTombstones(sstable, gcBefore)) + sstablesWithTombstones.add(sstable); + } + if (sstablesWithTombstones.isEmpty()) + return null; + + final CompactionSSTable sstable = selector.apply(sstablesWithTombstones); + return CompactionAggregate.createForTombstones(sstable); + } + + /** + * Check if given sstable is worth dropping tombstones at gcBefore. + * Check is skipped if tombstone_compaction_interval time does not elapse since sstable creation and returns false. + * + * @param sstable SSTable to check + * @param gcBefore time to drop tombstones + * @return true if given sstable's tombstones are expected to be removed + */ + protected boolean worthDroppingTombstones(CompactionSSTable sstable, long gcBefore) + { + if (options.isDisableTombstoneCompactions() + || CompactionController.NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE + || realm.getNeverPurgeTombstones()) + return false; + // since we use estimations to calculate, there is a chance that compaction will not drop tombstones actually. + // if that happens we will end up in infinite compaction loop, so first we check enough if enough time has + // elapsed since SSTable created. + if (Clock.Global.currentTimeMillis() < sstable.getCreationTimeFor(SSTableFormat.Components.DATA)+ options.getTombstoneCompactionInterval() * 1000) + return false; + + double droppableRatio = sstable.getEstimatedDroppableTombstoneRatio(gcBefore); + if (droppableRatio <= options.getTombstoneThreshold()) + return false; + + //sstable range overlap check is disabled. See CASSANDRA-6563. + if (options.isUncheckedTombstoneCompaction()) + return true; + + Set overlaps = realm.getOverlappingLiveSSTables(Collections.singleton(sstable)); + if (overlaps.isEmpty()) + { + // there is no overlap, tombstones are safely droppable + return true; + } + else if (CompactionController.getFullyExpiredSSTables(realm, Collections.singleton(sstable), c -> overlaps, gcBefore).size() > 0) + { + return true; + } + else + { + if (!(sstable instanceof SSTableReader)) + return false; // Correctly estimating percentage requires data that CompactionSSTable does not provide. + + SSTableReader reader = (SSTableReader) sstable; + // what percentage of columns do we expect to compact outside of overlap? + if (reader.isEstimationInformative()) + { + // we have too few samples to estimate correct percentage + return false; + } + // first, calculate estimated keys that do not overlap + long keys = reader.estimatedKeys(); + Set> ranges = new HashSet>(overlaps.size()); + for (CompactionSSTable overlap : overlaps) + ranges.add(new Range<>(overlap.getFirst().getToken(), overlap.getLast().getToken())); + long remainingKeys = keys - reader.estimatedKeysForRanges(ranges); + // next, calculate what percentage of columns we have within those keys + long columns = reader.getEstimatedCellPerPartitionCount().mean() * remainingKeys; + double remainingColumnsRatio = ((double) columns) / (reader.getEstimatedCellPerPartitionCount().count() * + reader.getEstimatedCellPerPartitionCount().mean()); + + // return if we still expect to have droppable tombstones in rest of columns + return remainingColumnsRatio * droppableRatio > options.getTombstoneThreshold(); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStatistics.java b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStatistics.java new file mode 100644 index 000000000000..15fd1b725470 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStatistics.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +/** + * The statistics for leveled compaction. + *

    + * Implements serializable to allow structured info to be returned via JMX. + */ +public class LeveledCompactionStatistics extends CompactionAggregateStatistics +{ + private static final Collection HEADER = ImmutableList.copyOf(Iterables.concat(ImmutableList.of("Level", "Score"), + CompactionAggregateStatistics.HEADER, + ImmutableList.of("Read: Tot/Prev/Next", + "Written: Tot/New", + "WA (tot_written/read_prev)"))); + + private static final long serialVersionUID = 3695927592357744816L; + + /** The current level */ + private final int level; + + /** The score of this level */ + private final double score; + + /** + * How many more compactions this level is expected to perform. This is required because for LCS we cannot + * easily identify candidate sstables to put into the pending picks. + */ + private final int pendingCompactions; + + /** + * Bytes read from the current level (N) during compaction between levels N and N+1. Note that {@link #readBytes} + * includes bytes read from both the current level (N) and the target level (N+1). + */ + private final long readLevel; + + /** + * Additional RocksDB metrics we may want to consider: + * Moved(GB): Bytes moved to level N+1 during compaction. In this case there is no IO other than updating the manifest to indicate that a file which used to be in level X is now in level Y + * Rd(MB/s): The rate at which data is read during compaction between levels N and N+1. This is (Read(GB) * 1024) / duration where duration is the time for which compactions are in progress from level N to N+1. + * Wr(MB/s): The rate at which data is written during compaction. See Rd(MB/s). + * Rn(cnt): Total files read from level N during compaction between levels N and N+1 + * Rnp1(cnt): Total files read from level N+1 during compaction between levels N and N+1 + * Wnp1(cnt): Total files written to level N+1 during compaction between levels N and N+1 + * Wnew(cnt): (Wnp1(cnt) - Rnp1(cnt)) -- Increase in file count as result of compaction between levels N and N+1 + * Comp(sec): Total time spent doing compactions between levels N and N+1 + * Comp(cnt): Total number of compactions between levels N and N+1 + * Avg(sec): Average time per compaction between levels N and N+1 + * Stall(sec): Total time writes were stalled because level N+1 was uncompacted (compaction score was high) + * Stall(cnt): Total number of writes stalled because level N+1 was uncompacted + * Avg(ms): Average time in milliseconds a write was stalled because level N+1 was uncompacted + * KeyIn: number of records compared during compaction + * KeyDrop: number of records dropped (not written out) during compaction + */ + + public LeveledCompactionStatistics(CompactionAggregateStatistics base, + int level, + double score, + int pendingCompactions, + long readLevel) + { + super(base); + this.level = level; + this.score = score; + this.pendingCompactions = pendingCompactions; + this.readLevel = readLevel; + } + + /** The number of compactions that are either pending or in progress */ + @Override + @JsonProperty + public int numCompactions() + { + return numCompactions + pendingCompactions; + } + + /** The current level */ + @JsonProperty + public int level() + { + return level; + } + + /** The score of a level is the level size in bytes of all its files dived by the ideal + * level size if applicable, or zero for tiered strategies */ + @JsonProperty + public double score() + { + return score; + } + + /** + * Bytes read from the current level (N) during compaction between levels N and N+1. Note that + * {@link #read()} includes bytes read from both the current level (N) and the target level (N+1). + */ @JsonProperty + public long readLevel() + { + return readLevel; + } + + /** Uncompressed bytes read from the next level (N+1) during compaction between levels N and N+1 */ + @JsonProperty + public long readNext() + { + return readBytes - readLevel; + } + + /** Uncompressed bytes written to level N+1, calculated as total bytes written - bytes read from N+1 */ + @JsonProperty + public long writtenNew() + { + return writtenBytes - readNext(); + } + + /** W-Amp: total bytes written divided by the bytes read from level N. */ + @JsonProperty + public double writeAmpl() + { + return readLevel() > 0 ? (double) writtenBytes / readLevel() : Double.NaN; + } + + @Override + protected Collection header() + { + return HEADER; + } + + @Override + protected Collection data() + { + List data = new ArrayList<>(HEADER.size()); + data.add(Integer.toString(level())); + data.add(String.format("%.3f", score())); + + data.addAll(super.data()); + + data.add(toString(read()) + '/' + toString(readLevel()) + '/' + toString(readNext())); + data.add(toString(written()) + '/' + toString(writtenNew())); + data.add(String.format("%.3f", writeAmpl())); + + return data; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java index a09dc7262a98..30f8ec39e2a9 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java @@ -19,39 +19,51 @@ import java.util.*; import java.math.BigInteger; - +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.*; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import com.google.common.primitives.Doubles; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.apache.cassandra.io.sstable.metadata.StatsMetadata; -import org.apache.cassandra.schema.CompactionParams; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.metadata.StatsMetadata; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.schema.TableMetadata; import static org.apache.cassandra.config.CassandraRelevantProperties.TOLERATE_SSTABLE_SIZE; import static org.apache.cassandra.db.compaction.LeveledGenerations.MAX_LEVEL_COUNT; -public class LeveledCompactionStrategy extends AbstractCompactionStrategy +public class LeveledCompactionStrategy extends LegacyAbstractCompactionStrategy.WithAggregates { private static final Logger logger = LoggerFactory.getLogger(LeveledCompactionStrategy.class); - private static final String SSTABLE_SIZE_OPTION = "sstable_size_in_mb"; + static final String SSTABLE_SIZE_OPTION = "sstable_size_in_mb"; private static final boolean tolerateSstableSize = TOLERATE_SSTABLE_SIZE.getBoolean(); - private static final String LEVEL_FANOUT_SIZE_OPTION = "fanout_size"; + static final String LEVEL_FANOUT_SIZE_OPTION = "fanout_size"; private static final String SINGLE_SSTABLE_UPLEVEL_OPTION = "single_sstable_uplevel"; public static final int DEFAULT_LEVEL_FANOUT_SIZE = 10; @@ -61,9 +73,9 @@ public class LeveledCompactionStrategy extends AbstractCompactionStrategy private final int levelFanoutSize; private final boolean singleSSTableUplevel; - public LeveledCompactionStrategy(ColumnFamilyStore cfs, Map options) + public LeveledCompactionStrategy(CompactionStrategyFactory factory, Map options) { - super(cfs, options); + super(factory, options); int configuredMaxSSTableSize = 160; int configuredLevelFanoutSize = DEFAULT_LEVEL_FANOUT_SIZE; boolean configuredSingleSSTableUplevel = false; @@ -77,10 +89,10 @@ public LeveledCompactionStrategy(ColumnFamilyStore cfs, Map opti { if (configuredMaxSSTableSize >= 1000) logger.warn("Max sstable size of {}MB is configured for {}.{}; having a unit of compaction this large is probably a bad idea", - configuredMaxSSTableSize, cfs.name, cfs.getTableName()); + configuredMaxSSTableSize, realm.getKeyspaceName(), realm.getTableName()); if (configuredMaxSSTableSize < 50) logger.warn("Max sstable size of {}MB is configured for {}.{}. Testing done for CASSANDRA-5727 indicates that performance improves up to 160MB", - configuredMaxSSTableSize, cfs.name, cfs.getTableName()); + configuredMaxSSTableSize, realm.getKeyspaceName(), realm.getTableName()); } } @@ -98,11 +110,11 @@ public LeveledCompactionStrategy(ColumnFamilyStore cfs, Map opti levelFanoutSize = configuredLevelFanoutSize; singleSSTableUplevel = configuredSingleSSTableUplevel; - manifest = new LeveledManifest(cfs, this.maxSSTableSizeInMiB, this.levelFanoutSize, localOptions); + manifest = new LeveledManifest(realm, this.maxSSTableSizeInMiB, this.levelFanoutSize, localOptions); logger.trace("Created {}", manifest); } - public int getLevelSize(int i) + int getLevelSize(int i) { return manifest.getLevelSize(i); } @@ -117,6 +129,12 @@ public long[] getAllLevelSizeBytes() return manifest.getAllLevelSizeBytes(); } + @Override + public int[] getSSTableCountPerLevel() + { + return manifest.getSSTableCountPerLevel(); + } + @Override public void startup() { @@ -124,95 +142,62 @@ public void startup() super.startup(); } - /** - * the only difference between background and maximal in LCS is that maximal is still allowed - * (by explicit user request) even when compaction is disabled. - */ - public AbstractCompactionTask getNextBackgroundTask(long gcBefore) + @Override + protected CompactionAggregate getNextBackgroundAggregate(long gcBefore) { - Collection previousCandidate = null; - while (true) - { - OperationType op; - LeveledManifest.CompactionCandidate candidate = manifest.getCompactionCandidates(); - if (candidate == null) - { - // if there is no sstable to compact in standard way, try compacting based on droppable tombstone ratio - SSTableReader sstable = findDroppableSSTable(gcBefore); - if (sstable == null) - { - logger.trace("No compaction necessary for {}", this); - return null; - } - candidate = new LeveledManifest.CompactionCandidate(Collections.singleton(sstable), - sstable.getSSTableLevel(), - getMaxSSTableBytes()); - op = OperationType.TOMBSTONE_COMPACTION; - } - else - { - op = OperationType.COMPACTION; - } - - // Already tried acquiring references without success. It means there is a race with - // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager - if (candidate.sstables.equals(previousCandidate)) - { - logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," + - "unless it happens frequently, in which case it must be reported. Will retry later.", - candidate.sstables); - return null; - } + CompactionAggregate.Leveled candidate = manifest.getCompactionCandidate(); + backgroundCompactions.setPending(this, manifest.getEstimatedTasks(candidate)); - LifecycleTransaction txn = cfs.getTracker().tryModify(candidate.sstables, OperationType.COMPACTION); - if (txn != null) - { - AbstractCompactionTask newTask; - if (!singleSSTableUplevel || op == OperationType.TOMBSTONE_COMPACTION || txn.originals().size() > 1) - newTask = new LeveledCompactionTask(cfs, txn, candidate.level, gcBefore, candidate.maxSSTableBytes, false); - else - newTask = new SingleSSTableLCSTask(cfs, txn, candidate.level); + if (candidate != null) + return candidate; - newTask.setCompactionType(op); - return newTask; - } - previousCandidate = candidate.sstables; - } + return findDroppableSSTable(gcBefore); } - public synchronized Collection getMaximalTask(long gcBefore, boolean splitOutput) + @Override + protected AbstractCompactionTask createCompactionTask(final long gcBefore, LifecycleTransaction txn, CompactionAggregate compaction) { - Iterable sstables = manifest.getSSTables(); + long maxxSSTableBytes; + int nextLevel; + OperationType op; - Iterable filteredSSTables = filterSuspectSSTables(sstables); - if (Iterables.isEmpty(sstables)) - return null; - LifecycleTransaction txn = cfs.getTracker().tryModify(filteredSSTables, OperationType.COMPACTION); - if (txn == null) - return null; - return Arrays.asList(new LeveledCompactionTask(cfs, txn, 0, gcBefore, getMaxSSTableBytes(), true)); + if (compaction instanceof CompactionAggregate.TombstoneAggregate) + { + op = OperationType.TOMBSTONE_COMPACTION; + nextLevel = Iterables.getOnlyElement(compaction.selected.sstables()).getSSTableLevel(); + maxxSSTableBytes = getMaxSSTableBytes(); // TODO: verify this is expected as it can split L0 tables + } + else + { + CompactionAggregate.Leveled candidate = (CompactionAggregate.Leveled) compaction; + op = OperationType.COMPACTION; + nextLevel = candidate.nextLevel; + maxxSSTableBytes = candidate.maxSSTableBytes; + } + + AbstractCompactionTask newTask; + if (!singleSSTableUplevel || op == OperationType.TOMBSTONE_COMPACTION || txn.originals().size() > 1) + newTask = new LeveledCompactionTask(this, txn, nextLevel, gcBefore, maxxSSTableBytes, false); + else + newTask = new SingleSSTableLCSTask(this, txn, nextLevel); + + newTask.setCompactionType(op); + return newTask; } + @Override - public AbstractCompactionTask getUserDefinedTask(Collection sstables, long gcBefore) + protected AbstractCompactionTask createCompactionTask(final long gcBefore, LifecycleTransaction txn, boolean isMaximal, boolean splitOutput) { - - if (sstables.isEmpty()) - return null; - - LifecycleTransaction transaction = cfs.getTracker().tryModify(sstables, OperationType.COMPACTION); - if (transaction == null) - { - logger.trace("Unable to mark {} for compaction; probably a background compaction got to it first. You can disable background compactions temporarily if this is a problem", sstables); - return null; - } + Collection sstables = txn.originals(); int level = sstables.size() > 1 ? 0 : sstables.iterator().next().getSSTableLevel(); - return new LeveledCompactionTask(cfs, transaction, level, gcBefore, level == 0 ? Long.MAX_VALUE : getMaxSSTableBytes(), false); + long maxSSTableBytes = (level == 0 && !isMaximal) ? Long.MAX_VALUE : getMaxSSTableBytes(); + return new LeveledCompactionTask(this, txn, level, gcBefore, maxSSTableBytes, isMaximal); } @Override - public AbstractCompactionTask getCompactionTask(LifecycleTransaction txn, long gcBefore, long maxSSTableBytes) + public AbstractCompactionTask createCompactionTask(LifecycleTransaction txn, long gcBefore, long maxSSTableBytes) { assert txn.originals().size() > 0; int level = -1; @@ -224,7 +209,7 @@ public AbstractCompactionTask getCompactionTask(LifecycleTransaction txn, long g if (level != sstable.getSSTableLevel()) level = 0; } - return new LeveledCompactionTask(cfs, txn, level, gcBefore, maxSSTableBytes, false); + return new LeveledCompactionTask(this, txn, level, gcBefore, maxSSTableBytes, false); } /** @@ -235,28 +220,28 @@ public AbstractCompactionTask getCompactionTask(LifecycleTransaction txn, long g * @return Groups of sstables from the same level */ @Override - public Collection> groupSSTablesForAntiCompaction(Collection ssTablesToGroup) + public Collection> groupSSTablesForAntiCompaction(Collection ssTablesToGroup) { int groupSize = 2; - Map> sstablesByLevel = new HashMap<>(); - for (SSTableReader sstable : ssTablesToGroup) + Map> sstablesByLevel = new HashMap<>(); + for (CompactionSSTable sstable : ssTablesToGroup) { Integer level = sstable.getSSTableLevel(); - Collection sstablesForLevel = sstablesByLevel.get(level); + Collection sstablesForLevel = sstablesByLevel.get(level); if (sstablesForLevel == null) { - sstablesForLevel = new ArrayList(); + sstablesForLevel = new ArrayList<>(); sstablesByLevel.put(level, sstablesForLevel); } sstablesForLevel.add(sstable); } - Collection> groupedSSTables = new ArrayList<>(); + Collection> groupedSSTables = new ArrayList<>(); - for (Collection levelOfSSTables : sstablesByLevel.values()) + for (Collection levelOfSSTables : sstablesByLevel.values()) { - Collection currGroup = new ArrayList<>(groupSize); - for (SSTableReader sstable : levelOfSSTables) + Collection currGroup = new ArrayList<>(groupSize); + for (CompactionSSTable sstable : levelOfSSTables) { currGroup.add(sstable); if (currGroup.size() == groupSize) @@ -273,19 +258,6 @@ public Collection> groupSSTablesForAntiCompaction(Coll } - public int getEstimatedRemainingTasks() - { - int n = manifest.getEstimatedTasks(); - cfs.getCompactionStrategyManager().compactionLogger.pending(this, n); - return n; - } - - @Override - int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes) - { - return manifest.getEstimatedTasks(additionalBytes); - } - public long getMaxSSTableBytes() { return maxSSTableSizeInMiB * 1024L * 1024L; @@ -298,7 +270,7 @@ public int getLevelFanoutSize() public ScannerList getScanners(Collection sstables, Collection> ranges) { - Set[] sstablesPerLevel = manifest.getSStablesPerLevelSnapshot(); + Set[] sstablesPerLevel = manifest.getSStablesPerLevelSnapshot(); Multimap byLevel = ArrayListMultimap.create(); for (SSTableReader sstable : sstables) @@ -336,7 +308,7 @@ public ScannerList getScanners(Collection sstables, Collection intersecting = LeveledScanner.intersecting(byLevel.get(level), ranges); if (!intersecting.isEmpty()) { - ISSTableScanner scanner = new LeveledScanner(cfs.metadata(), intersecting, ranges); + ISSTableScanner scanner = new LeveledScanner(realm.metadata(), intersecting, ranges, level); scanners.add(scanner); } } @@ -351,49 +323,65 @@ public ScannerList getScanners(Collection sstables, Collection removed, Collection added) + public void replaceSSTables(Collection removed, Collection added) { manifest.replace(removed, added); } - @Override - public void metadataChanged(StatsMetadata oldMetadata, SSTableReader sstable) + public void metadataChanged(StatsMetadata oldMetadata, CompactionSSTable sstable) { if (sstable.getSSTableLevel() != oldMetadata.sstableLevel) manifest.newLevel(sstable, oldMetadata.sstableLevel); } @Override - public void addSSTables(Iterable sstables) + public void addSSTables(Iterable sstables) { manifest.addSSTables(sstables); } @Override - public void addSSTable(SSTableReader added) + void removeDeadSSTables() + { + manifest.removeDeadSSTables(); + } + + @Override + public void addSSTable(CompactionSSTable added) { manifest.addSSTables(Collections.singleton(added)); } @Override - public void removeSSTable(SSTableReader sstable) + public int getLevel(ILifecycleTransaction txn) + { + CompactionPick pick = backgroundCompactions.getCompaction(txn.opId()); + if (pick != null) + return (int) pick.parent(); + + return -1; + } + + @Override + public void removeSSTable(CompactionSSTable sstable) { manifest.remove(sstable); } @Override - protected Set getSSTables() + public Set getSSTables() { return manifest.getSSTables(); } // Lazily creates SSTableBoundedScanner for sstable that are assumed to be from the // same level (e.g. non overlapping) - see #4142 - private static class LeveledScanner extends AbstractIterator implements ISSTableScanner + protected static class LeveledScanner extends AbstractIterator implements ISSTableScanner { private final TableMetadata metadata; private final Collection> ranges; private final List sstables; + private final int level; private final Iterator sstableIterator; private final long totalLength; private final long compressedLength; @@ -402,13 +390,14 @@ private static class LeveledScanner extends AbstractIterator sstables, Collection> ranges) + public LeveledScanner(TableMetadata metadata, Collection sstables, Collection> ranges, int level) { this.metadata = metadata; this.ranges = ranges; // add only sstables that intersect our range, and estimate how much data that involves this.sstables = new ArrayList<>(sstables.size()); + this.level = level; long length = 0; long cLength = 0; for (SSTableReader sstable : sstables) @@ -426,7 +415,7 @@ public LeveledScanner(TableMetadata metadata, Collection sstables totalLength = length; compressedLength = cLength; - Collections.sort(this.sstables, SSTableReader.firstKeyComparator); + Collections.sort(this.sstables, CompactionSSTable.firstKeyComparator); sstableIterator = this.sstables.iterator(); assert sstableIterator.hasNext(); // caller should check intersecting first SSTableReader currentSSTable = sstableIterator.next(); @@ -444,8 +433,7 @@ public static Collection intersecting(Collection s { for (SSTableReader sstable : sstables) { - Range sstableRange = new Range<>(sstable.getFirst().getToken(), sstable.getLast().getToken()); - if (range == null || sstableRange.intersects(range)) + if (range == null || range.intersects(sstable.getBounds())) filtered.add(sstable); } } @@ -512,63 +500,43 @@ public Set getBackingSSTables() { return ImmutableSet.copyOf(sstables); } + + public int level() + { + return level; + } } @Override public String toString() { - return String.format("LCS@%d(%s)", hashCode(), cfs.name); + return String.format("LCS@%d(%s)", hashCode(), realm.getTableName()); } - private SSTableReader findDroppableSSTable(final long gcBefore) + private CompactionAggregate findDroppableSSTable(final long gcBefore) { - level: + Comparator comparator = (o1, o2) -> { + double r1 = o1.getEstimatedDroppableTombstoneRatio(gcBefore); + double r2 = o2.getEstimatedDroppableTombstoneRatio(gcBefore); + return -1 * Doubles.compare(r1, r2); + }; + Function, CompactionSSTable> selector = list -> Collections.max(list, comparator); + Set compacting = realm.getCompactingSSTables(); + for (int i = manifest.getLevelCount(); i >= 0; i--) { - if (manifest.getLevelSize(i) == 0) - continue; - // sort sstables by droppable ratio in descending order - List tombstoneSortedSSTables = manifest.getLevelSorted(i, (o1, o2) -> { - double r1 = o1.getEstimatedDroppableTombstoneRatio(gcBefore); - double r2 = o2.getEstimatedDroppableTombstoneRatio(gcBefore); - return -1 * Doubles.compare(r1, r2); - }); - - Set compacting = cfs.getTracker().getCompacting(); - for (SSTableReader sstable : tombstoneSortedSSTables) - { - if (sstable.getEstimatedDroppableTombstoneRatio(gcBefore) <= tombstoneThreshold) - continue level; - else if (!compacting.contains(sstable) && !sstable.isMarkedSuspect() && worthDroppingTombstones(sstable, gcBefore)) - return sstable; - } + CompactionAggregate tombstoneAggregate = makeTombstoneCompaction(gcBefore, + nonSuspectAndNotIn(manifest.getLevel(i), compacting), + selector); + if (tombstoneAggregate != null) + return tombstoneAggregate; } return null; } - public CompactionLogger.Strategy strategyLogger() - { - return new CompactionLogger.Strategy() - { - public JsonNode sstable(SSTableReader sstable) - { - ObjectNode node = JsonNodeFactory.instance.objectNode(); - node.put("level", sstable.getSSTableLevel()); - node.put("min_token", sstable.getFirst().getToken().toString()); - node.put("max_token", sstable.getLast().getToken().toString()); - return node; - } - - public JsonNode options() - { - return null; - } - }; - } - public static Map validateOptions(Map options) throws ConfigurationException { - Map uncheckedOptions = AbstractCompactionStrategy.validateOptions(options); + Map uncheckedOptions = CompactionStrategyOptions.validateOptions(options); int ssSize; int fanoutSize; diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java index 8f5a70a84d16..87af9f2d8ae9 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java @@ -19,6 +19,7 @@ import java.util.Set; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; @@ -26,7 +27,7 @@ import org.apache.cassandra.db.compaction.writers.MajorLeveledCompactionWriter; import org.apache.cassandra.db.compaction.writers.MaxSSTableSizeWriter; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; public class LeveledCompactionTask extends CompactionTask { @@ -34,29 +35,36 @@ public class LeveledCompactionTask extends CompactionTask private final long maxSSTableBytes; private final boolean majorCompaction; - public LeveledCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, int level, long gcBefore, long maxSSTableBytes, boolean majorCompaction) + public LeveledCompactionTask(LeveledCompactionStrategy strategy, ILifecycleTransaction txn, int level, long gcBefore, long maxSSTableBytes, boolean majorCompaction) { - super(cfs, txn, gcBefore); + super(strategy.realm, txn, gcBefore, false, strategy); + this.level = level; + this.maxSSTableBytes = maxSSTableBytes; + this.majorCompaction = majorCompaction; + } + + public LeveledCompactionTask(ColumnFamilyStore cfs, ILifecycleTransaction txn, int level, long gcBefore, long maxSSTableBytes, boolean majorCompaction, @Nullable CompactionStrategy strategy) { + super(cfs, txn, gcBefore, false, strategy); this.level = level; this.maxSSTableBytes = maxSSTableBytes; this.majorCompaction = majorCompaction; } @Override - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, + public CompactionAwareWriter getCompactionAwareWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, Set nonExpiredSSTables) { if (majorCompaction) - return new MajorLeveledCompactionWriter(cfs, directories, txn, nonExpiredSSTables, maxSSTableBytes, false); - return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, maxSSTableBytes, getLevel(), false); + return new MajorLeveledCompactionWriter(realm, directories, transaction, nonExpiredSSTables, maxSSTableBytes, false); + return new MaxSSTableSizeWriter(realm, directories, transaction, nonExpiredSSTables, maxSSTableBytes, getLevel(), false); } @Override protected boolean partialCompactionsAcceptable() { - return level == 0; + // LCS allows removing L0 sstable from L0/L1 compaction task for limited disk space. It's handled in #reduceScopeForLimitedSpace + return level <= 1; } protected int getLevel() @@ -67,7 +75,7 @@ protected int getLevel() @Override public boolean reduceScopeForLimitedSpace(Set nonExpiredSSTables, long expectedSize) { - if (transaction.originals().size() > 1 && level <= 1) + if (nonExpiredSSTables.size() > 1 && level <= 1) { // Try again w/o the largest one. logger.warn("insufficient space to do L0 -> L{} compaction. {}MiB required, {} for compaction {}", @@ -77,7 +85,7 @@ public boolean reduceScopeForLimitedSpace(Set nonExpiredSSTables, .stream() .map(sstable -> String.format("%s (level=%s, size=%s)", sstable, sstable.getSSTableLevel(), sstable.onDiskLength())) .collect(Collectors.joining(",")), - transaction.opId()); + transaction.opIdString()); // Note that we have removed files that are still marked as compacting. // This suboptimal but ok since the caller will unmark all the sstables at the end. int l0SSTableCount = 0; @@ -94,12 +102,12 @@ public boolean reduceScopeForLimitedSpace(Set nonExpiredSSTables, // no point doing a L0 -> L{0,1} compaction if we have cancelled all L0 sstables if (largestL0SSTable != null && l0SSTableCount > 1) { - logger.info("Removing {} (level={}, size={}) from compaction {}", + logger.info("Removing {} (size={}) from compaction {}", largestL0SSTable, - largestL0SSTable.getSSTableLevel(), largestL0SSTable.onDiskLength(), - transaction.opId()); + transaction.opIdString()); transaction.cancel(largestL0SSTable); + nonExpiredSSTables.remove(largestL0SSTable); return true; } } diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java index 513e02aad99e..9f221df1fdcb 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java @@ -33,12 +33,9 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.io.sstable.SSTableIdFactory; -import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_STRICT_LCS_CHECKS; @@ -68,16 +65,16 @@ class LeveledGenerations * do allSSTables.get(instance_with_moved_starts) we will get the NORMAL sstable back, which we can then remove * from the TreeSet. */ - private final Map allSSTables = new HashMap<>(); - private final Set l0 = new HashSet<>(); + private final Map allSSTables = new HashMap<>(); + private final Set l0 = new HashSet<>(); private static long lastOverlapCheck = nanoTime(); // note that since l0 is broken out, levels[0] represents L1: - private final TreeSet [] levels = new TreeSet[MAX_LEVEL_COUNT - 1]; + private final TreeSet [] levels = new TreeSet[MAX_LEVEL_COUNT - 1]; - private static final Comparator nonL0Comparator = (o1, o2) -> { - int cmp = SSTableReader.firstKeyComparator.compare(o1, o2); + private static final Comparator nonL0Comparator = (o1, o2) -> { + int cmp = CompactionSSTable.firstKeyComparator.compare(o1, o2); if (cmp == 0) - cmp = SSTableIdFactory.COMPARATOR.compare(o1.descriptor.id, o2.descriptor.id); + cmp = CompactionSSTable.idComparator.compare(o1, o2); return cmp; }; @@ -87,7 +84,7 @@ class LeveledGenerations levels[i] = new TreeSet<>(nonL0Comparator); } - Set get(int level) + Set get(int level) { if (level > levelCount() - 1 || level < 0) throw new ArrayIndexOutOfBoundsException("Invalid generation " + level + " - maximum is " + (levelCount() - 1)); @@ -113,28 +110,13 @@ int levelCount() * * todo: group sstables per level, add all if level is currently empty, improve startup speed */ - void addAll(Iterable readers) + void addAll(Iterable readers) { logDistribution(); - for (SSTableReader sstable : readers) + for (CompactionSSTable sstable : readers) { assert sstable.getSSTableLevel() < levelCount() : "Invalid level " + sstable.getSSTableLevel() + " out of " + (levelCount() - 1); - int existingLevel = getLevelIfExists(sstable); - if (existingLevel != -1) - { - if (sstable.getSSTableLevel() != existingLevel) - { - logger.error("SSTable {} on the wrong level in the manifest - {} instead of {} as recorded in the sstable metadata, removing from level {}", sstable, existingLevel, sstable.getSSTableLevel(), existingLevel); - if (strictLCSChecksTest) - throw new AssertionError("SSTable not in matching level in manifest: "+sstable + ": "+existingLevel+" != " + sstable.getSSTableLevel()); - } - else - { - logger.info("Manifest already contains {} in level {} - replacing instance", sstable, existingLevel); - } - get(existingLevel).remove(sstable); - allSSTables.remove(sstable); - } + removeIfExists(sstable); allSSTables.put(sstable, sstable); if (sstable.getSSTableLevel() == 0) @@ -143,7 +125,7 @@ void addAll(Iterable readers) continue; } - TreeSet level = levels[sstable.getSSTableLevel() - 1]; + TreeSet level = levels[sstable.getSSTableLevel() - 1]; /* current level: |-----||----||----| |---||---| new sstable: |--| @@ -151,8 +133,8 @@ void addAll(Iterable readers) ^ after overlap if before.last >= newsstable.first or after.first <= newsstable.last */ - SSTableReader after = level.ceiling(sstable); - SSTableReader before = level.floor(sstable); + CompactionSSTable after = level.ceiling(sstable); + CompactionSSTable before = level.floor(sstable); if (before != null && before.getLast().compareTo(sstable.getFirst()) >= 0 || after != null && after.getFirst().compareTo(sstable.getLast()) <= 0) @@ -172,7 +154,7 @@ void addAll(Iterable readers) * * SSTable should not exist in the manifest */ - private void sendToL0(SSTableReader sstable) + private void sendToL0(CompactionSSTable sstable) { try { @@ -183,34 +165,53 @@ private void sendToL0(SSTableReader sstable) // Adding it to L0 and marking suspect is probably the best we can do here - it won't create overlap // and we won't pick it for later compactions. logger.error("Failed mutating sstable metadata for {} - adding it to L0 to avoid overlap. Marking suspect", sstable, e); - sstable.markSuspect(); } l0.add(sstable); } /** - * Tries to find the sstable in the levels without using the sstable-recorded level + * Tries to find the sstable in the levels without using the sstable-recorded level, and removes it if it does find + * it. * * Used to make sure we don't try to re-add an existing sstable */ - private int getLevelIfExists(SSTableReader sstable) + private void removeIfExists(CompactionSSTable sstable) { - for (int i = 0; i < levelCount(); i++) + for (int level = 0; level < levelCount(); level++) { - if (get(i).contains(sstable)) - return i; + if (get(level).contains(sstable)) + { + if (sstable.getSSTableLevel() != level) + { + logger.error("SSTable {} on the wrong level in the manifest - {} instead of {} as recorded in the sstable metadata, removing from level {}", + sstable, + level, + sstable.getSSTableLevel(), + level); + if (strictLCSChecksTest) + throw new AssertionError("SSTable not in matching level in manifest: " + sstable + ": " + level + " != " + + sstable.getSSTableLevel()); + } + else + { + logger.info("Manifest already contains {} in level {} - replacing instance", + sstable, + level); + } + get(level).remove(sstable); + allSSTables.remove(sstable); + } } - return -1; } - int remove(Collection readers) + int remove(Collection readers) { int minLevel = Integer.MAX_VALUE; - for (SSTableReader sstable : readers) + for (CompactionSSTable sstable : readers) { int level = sstable.getSSTableLevel(); minLevel = Math.min(minLevel, level); - SSTableReader versionInManifest = allSSTables.get(sstable); + CompactionSSTable versionInManifest = allSSTables.get(sstable); if (versionInManifest != null) { get(level).remove(versionInManifest); @@ -232,15 +233,15 @@ long[] getAllLevelSizeBytes() { long[] sums = new long[levelCount()]; for (int i = 0; i < sums.length; i++) - sums[i] = get(i).stream().map(SSTableReader::onDiskLength).reduce(0L, Long::sum); + sums[i] = get(i).stream().map(CompactionSSTable::onDiskLength).reduce(0L, Long::sum); return sums; } - Set allSSTables() + Set allSSTables() { - ImmutableSet.Builder builder = ImmutableSet.builder(); + ImmutableSet.Builder builder = ImmutableSet.builder(); builder.addAll(l0); - for (Set sstables : levels) + for (Set sstables : levels) builder.addAll(sstables); return builder.build(); } @@ -249,21 +250,21 @@ Set allSSTables() * given a level with sstables with first tokens [0, 10, 20, 30] and a lastCompactedSSTable with last = 15, we will * return an Iterator over [20, 30, 0, 10]. */ - Iterator wrappingIterator(int lvl, SSTableReader lastCompactedSSTable) + Iterator wrappingIterator(int lvl, CompactionSSTable lastCompactedSSTable) { assert lvl > 0; // only makes sense in L1+ - TreeSet level = levels[lvl - 1]; + TreeSet level = levels[lvl - 1]; if (level.isEmpty()) return Collections.emptyIterator(); if (lastCompactedSSTable == null) return level.iterator(); - PeekingIterator tail = Iterators.peekingIterator(level.tailSet(lastCompactedSSTable).iterator()); - SSTableReader pivot = null; + PeekingIterator tail = Iterators.peekingIterator(level.tailSet(lastCompactedSSTable).iterator()); + CompactionSSTable pivot = null; // then we need to make sure that the first token of the pivot is greater than the last token of the lastCompactedSSTable while (tail.hasNext()) { - SSTableReader potentialPivot = tail.peek(); + CompactionSSTable potentialPivot = tail.peek(); if (potentialPivot.getFirst().compareTo(lastCompactedSSTable.getLast()) > 0) { pivot = potentialPivot; @@ -284,22 +285,22 @@ void logDistribution() { for (int i = 0; i < levelCount(); i++) { - Set level = get(i); + Set level = get(i); if (!level.isEmpty()) { logger.trace("L{} contains {} SSTables ({}) in {}", i, level.size(), - FBUtilities.prettyPrintMemory(SSTableReader.getTotalBytes(level)), + FBUtilities.prettyPrintMemory(CompactionSSTable.getTotalDataBytes(level)), this); } } } } - Set[] snapshot() + Set[] snapshot() { - Set [] levelsCopy = new Set[levelCount()]; + Set [] levelsCopy = new Set[levelCount()]; for (int i = 0; i < levelCount(); i++) levelsCopy[i] = ImmutableSet.copyOf(get(i)); return levelsCopy; @@ -318,8 +319,8 @@ private void maybeVerifyLevels() lastOverlapCheck = nanoTime(); for (int i = 1; i < levelCount(); i++) { - SSTableReader prev = null; - for (SSTableReader sstable : get(i)) + CompactionSSTable prev = null; + for (CompactionSSTable sstable : get(i)) { // no overlap: assert prev == null || prev.getLast().compareTo(sstable.getFirst()) < 0; @@ -335,9 +336,9 @@ private void maybeVerifyLevels() } } - void newLevel(SSTableReader sstable, int oldLevel) + void newLevel(CompactionSSTable sstable, int oldLevel) { - SSTableReader versionInManifest = allSSTables.remove(sstable); + CompactionSSTable versionInManifest = allSSTables.remove(sstable); boolean removed = false; if (versionInManifest != null) removed = get(oldLevel).remove(versionInManifest); diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java index a8cafeba2218..918be7ef636f 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java @@ -17,7 +17,7 @@ */ package org.apache.cassandra.db.compaction; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicates; @@ -35,19 +34,16 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.Bounds; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.Pair; import static org.apache.cassandra.db.compaction.LeveledGenerations.MAX_LEVEL_COUNT; @@ -59,7 +55,13 @@ public class LeveledManifest * if we have more than MAX_COMPACTING_L0 sstables in L0, we will run a round of STCS with at most * cfs.getMaxCompactionThreshold() sstables. */ - private static final int MAX_COMPACTING_L0 = 32; + @VisibleForTesting + static final int MAX_COMPACTING_L0 = 32; + + /** + * The maximum number of sstables in L0 for calculating the maximum number of bytes in L0. + */ + static final int MAX_SSTABLES_L0 = 4; /** * If we go this many rounds without compacting @@ -68,36 +70,36 @@ public class LeveledManifest */ private static final int NO_COMPACTION_LIMIT = 25; - private final ColumnFamilyStore cfs; + private final CompactionRealm realm; private final LeveledGenerations generations; - private final SSTableReader[] lastCompactedSSTables; + private final CompactionSSTable[] lastCompactedSSTables; private final long maxSSTableSizeInBytes; private final SizeTieredCompactionStrategyOptions options; private final int [] compactionCounter; private final int levelFanoutSize; - LeveledManifest(ColumnFamilyStore cfs, int maxSSTableSizeInMB, int fanoutSize, SizeTieredCompactionStrategyOptions options) + LeveledManifest(CompactionRealm realm, int maxSSTableSizeInMB, int fanoutSize, SizeTieredCompactionStrategyOptions options) { - this.cfs = cfs; + this.realm = realm; this.maxSSTableSizeInBytes = maxSSTableSizeInMB * 1024L * 1024L; this.options = options; this.levelFanoutSize = fanoutSize; - lastCompactedSSTables = new SSTableReader[MAX_LEVEL_COUNT]; + lastCompactedSSTables = new CompactionSSTable[MAX_LEVEL_COUNT]; generations = new LeveledGenerations(); compactionCounter = new int[MAX_LEVEL_COUNT]; } - public static LeveledManifest create(ColumnFamilyStore cfs, int maxSSTableSize, int fanoutSize, List sstables) + public static LeveledManifest create(CompactionRealm realm, int maxSSTableSize, int fanoutSize, List sstables) { - return create(cfs, maxSSTableSize, fanoutSize, sstables, new SizeTieredCompactionStrategyOptions()); + return create(realm, maxSSTableSize, fanoutSize, sstables, new SizeTieredCompactionStrategyOptions()); } - public static LeveledManifest create(ColumnFamilyStore cfs, int maxSSTableSize, int fanoutSize, Iterable sstables, SizeTieredCompactionStrategyOptions options) + public static LeveledManifest create(CompactionRealm realm, int maxSSTableSize, int fanoutSize, Iterable sstables, SizeTieredCompactionStrategyOptions options) { - LeveledManifest manifest = new LeveledManifest(cfs, maxSSTableSize, fanoutSize, options); + LeveledManifest manifest = new LeveledManifest(realm, maxSSTableSize, fanoutSize, options); // ensure all SSTables are in the manifest manifest.addSSTables(sstables); @@ -113,16 +115,16 @@ void calculateLastCompactedKeys() { for (int i = 0; i < generations.levelCount() - 1; i++) { - Set level = generations.get(i + 1); + Set level = generations.get(i + 1); // this level is empty if (level.isEmpty()) continue; - SSTableReader sstableWithMaxModificationTime = null; + CompactionSSTable sstableWithMaxModificationTime = null; long maxModificationTime = Long.MIN_VALUE; - for (SSTableReader ssTableReader : level) + for (CompactionSSTable ssTableReader : level) { - long modificationTime = ssTableReader.getDataCreationTime(); + long modificationTime = ssTableReader.getCreationTimeFor(SSTableFormat.Components.DATA); if (modificationTime >= maxModificationTime) { sstableWithMaxModificationTime = ssTableReader; @@ -134,12 +136,12 @@ void calculateLastCompactedKeys() } } - public synchronized void addSSTables(Iterable readers) + public synchronized void addSSTables(Iterable readers) { generations.addAll(readers); } - public synchronized void replace(Collection removed, Collection added) + public synchronized void replace(Collection removed, Collection added) { assert !removed.isEmpty(); // use add() instead of promote when adding new sstables if (logger.isTraceEnabled()) @@ -159,17 +161,43 @@ public synchronized void replace(Collection removed, Collection sstables) + /** + * See {@link AbstractCompactionStrategy#removeDeadSSTables} + */ + public synchronized void removeDeadSSTables() + { + int removed = 0; + Set liveSet = realm.getLiveSSTables(); + + for (int i = 0; i < generations.levelCount(); i++) + { + Iterator it = generations.get(i).iterator(); + while (it.hasNext()) + { + CompactionSSTable sstable = it.next(); + if (!liveSet.contains(sstable)) + { + it.remove(); + ++removed; + } + } + } + + if (removed > 0) + logger.debug("Removed {} dead sstables from the compactions tracked list.", removed); + } + + private String toString(Collection sstables) { StringBuilder builder = new StringBuilder(); - for (SSTableReader sstable : sstables) + for (CompactionSSTable sstable : sstables) { - builder.append(sstable.descriptor.cfname) + builder.append(sstable.getColumnFamilyName()) .append('-') - .append(sstable.descriptor.id) + .append(sstable.getId()) .append("(L") .append(sstable.getSSTableLevel()) .append("), "); @@ -185,7 +213,7 @@ public long maxBytesForLevel(int level, long maxSSTableSizeInBytes) public static long maxBytesForLevel(int level, int levelFanoutSize, long maxSSTableSizeInBytes) { if (level == 0) - return 4L * maxSSTableSizeInBytes; + return MAX_SSTABLES_L0 * maxSSTableSizeInBytes; double bytes = Math.pow(levelFanoutSize, level) * maxSSTableSizeInBytes; if (bytes > Long.MAX_VALUE) throw new RuntimeException("At most " + Long.MAX_VALUE + " bytes may be in a compaction level; your maxSSTableSize must be absurdly high to compute " + bytes); @@ -196,17 +224,17 @@ public static long maxBytesForLevel(int level, int levelFanoutSize, long maxSSTa * @return highest-priority sstables to compact, and level to compact them to * If no compactions are necessary, will return null */ - public synchronized CompactionCandidate getCompactionCandidates() + synchronized CompactionAggregate.Leveled getCompactionCandidate() { // during bootstrap we only do size tiering in L0 to make sure // the streamed files can be placed in their original levels if (StorageService.instance.isBootstrapMode()) { - List mostInteresting = getSSTablesForSTCS(generations.get(0)); + CompactionPick mostInteresting = getSSTablesForSTCS(generations.get(0)); if (!mostInteresting.isEmpty()) { logger.info("Bootstrapping - doing STCS in L0"); - return new CompactionCandidate(mostInteresting, 0, Long.MAX_VALUE); + return getSTCSAggregate(mostInteresting); } return null; } @@ -240,17 +268,17 @@ public synchronized CompactionCandidate getCompactionCandidates() // Let's check that L0 is far enough behind to warrant STCS. // If it is, it will be used before proceeding any of higher level - CompactionCandidate l0Compaction = getSTCSInL0CompactionCandidate(); + CompactionAggregate.Leveled l0Compactions = getSTCSInL0CompactionCandidate(); for (int i = generations.levelCount() - 1; i > 0; i--) { - Set sstables = generations.get(i); + Set sstables = generations.get(i); if (sstables.isEmpty()) continue; // mostly this just avoids polluting the debug log with zero scores // we want to calculate score excluding compacting ones - Set sstablesInLevel = Sets.newHashSet(sstables); - Set remaining = Sets.difference(sstablesInLevel, cfs.getTracker().getCompacting()); - long remainingBytesForLevel = SSTableReader.getTotalBytes(remaining); + Set sstablesInLevel = Sets.newHashSet(sstables); + Set remaining = Sets.difference(sstablesInLevel, realm.getCompactingSSTables()); + long remainingBytesForLevel = CompactionSSTable.getTotalDataBytes(remaining); long maxBytesForLevel = maxBytesForLevel(i, maxSSTableSizeInBytes); double score = (double) remainingBytesForLevel / (double) maxBytesForLevel; logger.trace("Compaction score for level {} is {}", i, score); @@ -267,18 +295,20 @@ public synchronized CompactionCandidate getCompactionCandidates() } // before proceeding with a higher level, let's see if L0 is far enough behind to warrant STCS - if (l0Compaction != null) - return l0Compaction; + if (l0Compactions != null) + return l0Compactions; // L0 is fine, proceed with this level - Collection candidates = getCandidatesFor(i); + Collection candidates = getCandidatesFor(i); + int pendingCompactions = Math.max(0, getEstimatedPendingTasks(i) - 1); + if (!candidates.isEmpty()) { int nextLevel = getNextLevel(candidates); candidates = getOverlappingStarvedSSTables(nextLevel, candidates); if (logger.isTraceEnabled()) logger.trace("Compaction candidates for L{} are {}", i, toString(candidates)); - return new CompactionCandidate(candidates, nextLevel, maxSSTableSizeInBytes); + return CompactionAggregate.createLeveled(sstablesInLevel, candidates, pendingCompactions, maxSSTableSizeInBytes, i, nextLevel, score, levelFanoutSize); } else { @@ -288,44 +318,61 @@ public synchronized CompactionCandidate getCompactionCandidates() } // Higher levels are happy, time for a standard, non-STCS L0 compaction - if (generations.get(0).isEmpty()) + Set sstables = getLevel(0); + + if (sstables.isEmpty()) return null; - Collection candidates = getCandidatesFor(0); + Collection candidates = getCandidatesFor(0); if (candidates.isEmpty()) { // Since we don't have any other compactions to do, see if there is a STCS compaction to perform in L0; if // there is a long running compaction, we want to make sure that we continue to keep the number of SSTables // small in L0. - return l0Compaction; + return l0Compactions; } - return new CompactionCandidate(candidates, getNextLevel(candidates), maxSSTableSizeInBytes); + double l0Score = (double) CompactionSSTable.getTotalDataBytes(sstables) / (double) maxBytesForLevel(0, maxSSTableSizeInBytes); + int l0PendingCompactions = Math.max(0, getEstimatedPendingTasks(0) - 1); + return CompactionAggregate.createLeveled(sstables, candidates, l0PendingCompactions, maxSSTableSizeInBytes, 0, getNextLevel(candidates), l0Score, levelFanoutSize); } - private CompactionCandidate getSTCSInL0CompactionCandidate() + private CompactionAggregate.Leveled getSTCSInL0CompactionCandidate() { if (!DatabaseDescriptor.getDisableSTCSInL0() && generations.get(0).size() > MAX_COMPACTING_L0) { - List mostInteresting = getSSTablesForSTCS(generations.get(0)); + CompactionPick mostInteresting = getSSTablesForSTCS(getLevel(0)); if (!mostInteresting.isEmpty()) { logger.debug("L0 is too far behind, performing size-tiering there first"); - return new CompactionCandidate(mostInteresting, 0, Long.MAX_VALUE); + return getSTCSAggregate(mostInteresting); } } return null; } - private List getSSTablesForSTCS(Collection sstables) + private CompactionAggregate.Leveled getSTCSAggregate(CompactionPick compaction) + { + Set sstables = getLevel(0); + double score = (double) CompactionSSTable.getTotalDataBytes(sstables) / (double) maxBytesForLevel(0, maxSSTableSizeInBytes); + int remainingSSTables = sstables.size() - compaction.sstables().size(); + int pendingTasks = remainingSSTables > realm.getMinimumCompactionThreshold() + ? (int) Math.ceil(remainingSSTables / realm.getMaximumCompactionThreshold()) + : 0; + return CompactionAggregate.createLeveledForSTCS(sstables, compaction, pendingTasks, score, levelFanoutSize); + } + + private CompactionPick getSSTablesForSTCS(Collection sstables) { - Iterable candidates = cfs.getTracker().getUncompacting(sstables); - List> pairs = SizeTieredCompactionStrategy.createSSTableAndLengthPairs(AbstractCompactionStrategy.filterSuspectSSTables(candidates)); - List> buckets = SizeTieredCompactionStrategy.getBuckets(pairs, - options.bucketHigh, - options.bucketLow, - options.minSSTableSize); - return SizeTieredCompactionStrategy.mostInterestingBucket(buckets, - cfs.getMinimumCompactionThreshold(), cfs.getMaximumCompactionThreshold()); + Iterable candidates = realm.getNoncompactingSSTables(sstables); + + SizeTieredCompactionStrategy.SizeTieredBuckets sizeTieredBuckets; + sizeTieredBuckets = new SizeTieredCompactionStrategy.SizeTieredBuckets(candidates, + options, + realm.getMinimumCompactionThreshold(), + realm.getMaximumCompactionThreshold()); + sizeTieredBuckets.aggregate(); + + return CompactionAggregate.getSelected(sizeTieredBuckets.getAggregates()); } /** @@ -339,9 +386,9 @@ private List getSSTablesForSTCS(Collection sstable * @param candidates the original sstables to compact * @return */ - private Collection getOverlappingStarvedSSTables(int targetLevel, Collection candidates) + private Collection getOverlappingStarvedSSTables(int targetLevel, Collection candidates) { - Set withStarvedCandidate = new HashSet<>(candidates); + Set withStarvedCandidate = new HashSet<>(candidates); for (int i = generations.levelCount() - 1; i > 0; i--) compactionCounter[i]++; @@ -364,7 +411,7 @@ private Collection getOverlappingStarvedSSTables(int targetLevel, // contained within 0 -> 33 to the compaction PartitionPosition max = null; PartitionPosition min = null; - for (SSTableReader candidate : candidates) + for (CompactionSSTable candidate : candidates) { if (min == null || candidate.getFirst().compareTo(min) < 0) min = candidate.getFirst(); @@ -373,14 +420,14 @@ private Collection getOverlappingStarvedSSTables(int targetLevel, } if (min == null || max == null || min.equals(max)) // single partition sstables - we cannot include a high level sstable. return candidates; - Set compacting = cfs.getTracker().getCompacting(); + Set compacting = realm.getCompactingSSTables(); Range boundaries = new Range<>(min, max); - for (SSTableReader sstable : generations.get(i)) + for (CompactionSSTable sstable : generations.get(i)) { Range r = new Range<>(sstable.getFirst(), sstable.getLast()); if (boundaries.contains(r) && !compacting.contains(sstable)) { - logger.info("Adding high-level (L{}) {} to candidates", sstable.getSSTableLevel(), sstable); + logger.info("Adding high-level {} to candidates", sstable); withStarvedCandidate.add(sstable); return withStarvedCandidate; } @@ -398,6 +445,14 @@ public synchronized int getLevelSize(int i) return generations.get(i).size(); } + public synchronized int[] getSSTableCountPerLevel() + { + int[] counts = new int[getLevelCount()]; + for (int i = 0; i < counts.length; i++) + counts[i] = getLevel(i).size(); + return counts; + } + public synchronized int[] getAllLevelSize() { return generations.getAllLevelSize(); @@ -409,7 +464,7 @@ public synchronized long[] getAllLevelSizeBytes() } @VisibleForTesting - public synchronized int remove(SSTableReader reader) + public synchronized int remove(CompactionSSTable reader) { int level = reader.getSSTableLevel(); assert level >= 0 : reader + " not present in manifest: "+level; @@ -417,12 +472,12 @@ public synchronized int remove(SSTableReader reader) return level; } - public synchronized Set getSSTables() + public synchronized Set getSSTables() { return generations.allSSTables(); } - private static Set overlapping(Collection candidates, Iterable others) + private static Set overlapping(Collection candidates, Iterable others) { assert !candidates.isEmpty(); /* @@ -436,8 +491,8 @@ private static Set overlapping(Collection candidat * Thus, the correct approach is to pick sstables overlapping anything between the first key in all * the candidate sstables, and the last. */ - Iterator iter = candidates.iterator(); - SSTableReader sstable = iter.next(); + Iterator iter = candidates.iterator(); + CompactionSSTable sstable = iter.next(); Token first = sstable.getFirst().getToken(); Token last = sstable.getLast().getToken(); while (iter.hasNext()) @@ -449,7 +504,7 @@ private static Set overlapping(Collection candidat return overlapping(first, last, others); } - private static Set overlappingWithBounds(SSTableReader sstable, Map> others) + static Set overlappingWithBounds(CompactionSSTable sstable, Map> others) { return overlappingWithBounds(sstable.getFirst().getToken(), sstable.getLast().getToken(), others); } @@ -458,18 +513,18 @@ private static Set overlappingWithBounds(SSTableReader sstable, M * @return sstables from @param sstables that contain keys between @param start and @param end, inclusive. */ @VisibleForTesting - static Set overlapping(Token start, Token end, Iterable sstables) + static Set overlapping(Token start, Token end, Iterable sstables) { return overlappingWithBounds(start, end, genBounds(sstables)); } - private static Set overlappingWithBounds(Token start, Token end, Map> sstables) + private static Set overlappingWithBounds(Token start, Token end, Map> sstables) { assert start.compareTo(end) <= 0; - Set overlapped = new HashSet<>(); + Set overlapped = new HashSet<>(); Bounds promotedBounds = new Bounds<>(start, end); - for (Map.Entry> pair : sstables.entrySet()) + for (Map.Entry> pair : sstables.entrySet()) { if (pair.getValue().intersects(promotedBounds)) overlapped.add(pair.getKey()); @@ -477,10 +532,11 @@ private static Set overlappingWithBounds(Token start, Token end, return overlapped; } - private static Map> genBounds(Iterable ssTableReaders) + @VisibleForTesting + static Map> genBounds(Iterable ssTableReaders) { - Map> boundsMap = new HashMap<>(); - for (SSTableReader sstable : ssTableReaders) + Map> boundsMap = new HashMap<>(); + for (CompactionSSTable sstable : ssTableReaders) { boundsMap.put(sstable, new Bounds<>(sstable.getFirst().getToken(), sstable.getLast().getToken())); } @@ -488,24 +544,30 @@ private static Map> genBounds(Iterable * @return highest-priority sstables to compact for the given level. * If no compactions are possible (because of concurrent compactions or because some sstables are excluded * for prior failure), will return an empty list. Never returns null. + * + * @param level the level number + * @return highest-priority sstables to compact for the given level. */ - private Collection getCandidatesFor(int level) + private Collection getCandidatesFor(int level) { assert !generations.get(level).isEmpty(); logger.trace("Choosing candidates for L{}", level); - final Set compacting = cfs.getTracker().getCompacting(); + final Set compacting = realm.getCompactingSSTables(); if (level == 0) { - Set compactingL0 = getCompactingL0(); + Set compactingL0 = getCompactingL0(); PartitionPosition lastCompactingKey = null; PartitionPosition firstCompactingKey = null; - for (SSTableReader candidate : compactingL0) + for (CompactionSSTable candidate : compactingL0) { if (firstCompactingKey == null || candidate.getFirst().compareTo(firstCompactingKey) < 0) firstCompactingKey = candidate.getFirst(); @@ -526,40 +588,40 @@ private Collection getCandidatesFor(int level) // Note that we ignore suspect-ness of L1 sstables here, since if an L1 sstable is suspect we're // basically screwed, since we expect all or most L0 sstables to overlap with each L1 sstable. // So if an L1 sstable is suspect we can't do much besides try anyway and hope for the best. - Set candidates = new HashSet<>(); - Map> remaining = genBounds(Iterables.filter(generations.get(0), Predicates.not(SSTableReader::isMarkedSuspect))); + Set candidates = new HashSet<>(); + Map> remaining = genBounds(Iterables.filter(generations.get(0), Predicates.not(CompactionSSTable::isMarkedSuspect))); - for (SSTableReader sstable : ageSortedSSTables(remaining.keySet())) + for (CompactionSSTable sstable : ageSortedSSTables(remaining.keySet())) { if (candidates.contains(sstable)) continue; - Sets.SetView overlappedL0 = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, remaining)); + Sets.SetView overlappedL0 = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, remaining)); if (!Sets.intersection(overlappedL0, compactingL0).isEmpty()) continue; - for (SSTableReader newCandidate : overlappedL0) + for (CompactionSSTable newCandidate : overlappedL0) { if (firstCompactingKey == null || lastCompactingKey == null || overlapping(firstCompactingKey.getToken(), lastCompactingKey.getToken(), Collections.singleton(newCandidate)).size() == 0) candidates.add(newCandidate); remaining.remove(newCandidate); } - if (candidates.size() > cfs.getMaximumCompactionThreshold()) + if (candidates.size() > realm.getMaximumCompactionThreshold()) { // limit to only the cfs.getMaximumCompactionThreshold() oldest candidates - candidates = new HashSet<>(ageSortedSSTables(candidates).subList(0, cfs.getMaximumCompactionThreshold())); + candidates = new HashSet<>(ageSortedSSTables(candidates).subList(0, realm.getMaximumCompactionThreshold())); break; } } // leave everything in L0 if we didn't end up with a full sstable's worth of data - if (SSTableReader.getTotalBytes(candidates) > maxSSTableSizeInBytes) + if (CompactionSSTable.getTotalDataBytes(candidates) > maxSSTableSizeInBytes) { // add sstables from L1 that overlap candidates // if the overlapping ones are already busy in a compaction, leave it out. // TODO try to find a set of L0 sstables that only overlaps with non-busy L1 sstables - Set l1overlapping = overlapping(candidates, generations.get(1)); + Set l1overlapping = overlapping(candidates, generations.get(1)); if (Sets.intersection(l1overlapping, compacting).size() > 0) return Collections.emptyList(); if (!overlapping(candidates, compactingL0).isEmpty()) @@ -574,14 +636,14 @@ private Collection getCandidatesFor(int level) // look for a non-suspect keyspace to compact with, starting with where we left off last time, // and wrapping back to the beginning of the generation if necessary - Map> sstablesNextLevel = genBounds(generations.get(level + 1)); - Iterator levelIterator = generations.wrappingIterator(level, lastCompactedSSTables[level]); + Map> sstablesNextLevel = genBounds(generations.get(level + 1)); + Iterator levelIterator = generations.wrappingIterator(level, lastCompactedSSTables[level]); while (levelIterator.hasNext()) { - SSTableReader sstable = levelIterator.next(); - Set candidates = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, sstablesNextLevel)); + CompactionSSTable sstable = levelIterator.next(); + Set candidates = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, sstablesNextLevel)); - if (Iterables.any(candidates, SSTableReader::isMarkedSuspect)) + if (Iterables.any(candidates, CompactionSSTable::isMarkedSuspect)) continue; if (Sets.intersection(candidates, compacting).isEmpty()) return candidates; @@ -591,11 +653,11 @@ private Collection getCandidatesFor(int level) return Collections.emptyList(); } - private Set getCompactingL0() + private Set getCompactingL0() { - Set sstables = new HashSet<>(); - Set levelSSTables = new HashSet<>(generations.get(0)); - for (SSTableReader sstable : cfs.getTracker().getCompacting()) + Set sstables = new HashSet<>(); + Set levelSSTables = new HashSet<>(generations.get(0)); + for (CompactionSSTable sstable : realm.getCompactingSSTables()) { if (levelSSTables.contains(sstable)) sstables.add(sstable); @@ -604,12 +666,12 @@ private Set getCompactingL0() } @VisibleForTesting - List ageSortedSSTables(Collection candidates) + List ageSortedSSTables(Collection candidates) { - return ImmutableList.sortedCopyOf(SSTableReader.maxTimestampAscending, candidates); + return ImmutableList.sortedCopyOf(CompactionSSTable.maxTimestampAscending, candidates); } - public synchronized Set[] getSStablesPerLevelSnapshot() + public synchronized Set[] getSStablesPerLevelSnapshot() { return generations.snapshot(); } @@ -630,52 +692,79 @@ public synchronized int getLevelCount() return 0; } - public int getEstimatedTasks() + public synchronized List getEstimatedTasks(CompactionAggregate.Leveled selected) { - return getEstimatedTasks(0); - } + List ret = new ArrayList<>(generations.levelCount()); - int getEstimatedTasks(long additionalLevel0Bytes) - { - return getEstimatedTasks((level) -> SSTableReader.getTotalBytes(getLevel(level)) + (level == 0 ? additionalLevel0Bytes : 0)); + for (int i = generations.levelCount() - 1; i >= 0; i--) + { + Set sstables = generations.get(i); + + // do not log high levels that are empty, only log after we've found a non-empty level + if (sstables.isEmpty() && ret.isEmpty()) + continue; + + if (selected != null && selected.level == i) + { + ret.add(selected); + continue; // pending tasks already calculated by getCompactionCandidate() + } + + if (i == 0) + { // for L0 if it is too far behind then pick the STCS choice + CompactionAggregate l0Compactions = getSTCSInL0CompactionCandidate(); + if (l0Compactions != null) + { + ret.add(l0Compactions); + continue; + } + } + + int pendingTasks = getEstimatedPendingTasks(i); + double score = (double) CompactionSSTable.getTotalDataBytes(sstables) / (double) maxBytesForLevel(i, maxSSTableSizeInBytes); + ret.add(CompactionAggregate.createLeveled(sstables, pendingTasks, maxSSTableSizeInBytes, i, score, levelFanoutSize)); + } + + logger.trace("Estimating {} compactions to do for {}", ret.size(), realm.metadata()); + return ret; } - private synchronized int getEstimatedTasks(Function fnTotalSizeBytesByLevel) + /** + * @return the estimated number of LCS compactions for a given level with the given sstables. Because it compacts one sstable at + * a time, this number is determined as the number of bytes above the maximum divided the maximum sstable size in bytes. + * + * This is however incorrect for L0. If the STCS threshold has been exceeded, we simply divide by the max threshold, + * otherwise we currently use a very pessimistic estimate (no overlapping sstables). + */ + private int getEstimatedPendingTasks(int level) { - long tasks = 0; - long[] estimated = new long[generations.levelCount()]; + final Set sstables = getLevel(level); + if (sstables.isEmpty()) + return 0; - for (int i = generations.levelCount() - 1; i >= 0; i--) - { - // If there is 1 byte over TBL - (MBL * 1.001), there is still a task left, so we need to round up. - estimated[i] = (long)Math.ceil((double)Math.max(0L, fnTotalSizeBytesByLevel.apply(i) - (long)(maxBytesForLevel(i, maxSSTableSizeInBytes) * 1.001)) / (double)maxSSTableSizeInBytes); - tasks += estimated[i]; - } + final Set compacting = realm.getCompactingSSTables(); + final Set remaining = Sets.difference(Sets.newHashSet(sstables), compacting); - if (!DatabaseDescriptor.getDisableSTCSInL0() && generations.get(0).size() > cfs.getMaximumCompactionThreshold()) - { - int l0compactions = generations.get(0).size() / cfs.getMaximumCompactionThreshold(); - tasks += l0compactions; - estimated[0] += l0compactions; - } + if (level == 0 && !DatabaseDescriptor.getDisableSTCSInL0() && remaining.size() > MAX_COMPACTING_L0) + return remaining.size() / realm.getMaximumCompactionThreshold(); - logger.trace("Estimating {} compactions to do for {}.{}", - Arrays.toString(estimated), cfs.getKeyspaceName(), cfs.name); - return Ints.checkedCast(tasks); + // If there is 1 byte over TBL - (MBL * 1.001), there is still a task left, so we need to round up. + return Math.toIntExact((long) Math.ceil((Math.max(0L, CompactionSSTable.getTotalDataBytes(remaining) - + (maxBytesForLevel(level, maxSSTableSizeInBytes) * 1.001)) / (double) maxSSTableSizeInBytes))); } - public int getNextLevel(Collection sstables) + int getNextLevel(Collection sstables) { int maximumLevel = Integer.MIN_VALUE; int minimumLevel = Integer.MAX_VALUE; - for (SSTableReader sstable : sstables) + for (CompactionSSTable sstable : sstables) { maximumLevel = Math.max(sstable.getSSTableLevel(), maximumLevel); minimumLevel = Math.min(sstable.getSSTableLevel(), minimumLevel); } int newLevel; - if (minimumLevel == 0 && minimumLevel == maximumLevel && SSTableReader.getTotalBytes(sstables) < maxSSTableSizeInBytes) + if (minimumLevel == 0 && minimumLevel == maximumLevel && CompactionSSTable.getTotalDataBytes(sstables) < maxSSTableSizeInBytes) { newLevel = 0; } @@ -687,33 +776,19 @@ public int getNextLevel(Collection sstables) return newLevel; } - synchronized Set getLevel(int level) + synchronized Set getLevel(int level) { return ImmutableSet.copyOf(generations.get(level)); } - synchronized List getLevelSorted(int level, Comparator comparator) + synchronized List getLevelSorted(int level, Comparator comparator) { return ImmutableList.sortedCopyOf(comparator, generations.get(level)); } - synchronized void newLevel(SSTableReader sstable, int oldLevel) + synchronized void newLevel(CompactionSSTable sstable, int oldLevel) { generations.newLevel(sstable, oldLevel); lastCompactedSSTables[oldLevel] = sstable; } - - public static class CompactionCandidate - { - public final Collection sstables; - public final int level; - public final long maxSSTableBytes; - - public CompactionCandidate(Collection sstables, int level, long maxSSTableBytes) - { - this.sstables = sstables; - this.level = level; - this.maxSSTableBytes = maxSSTableBytes; - } - } } diff --git a/src/java/org/apache/cassandra/db/compaction/OperationType.java b/src/java/org/apache/cassandra/db/compaction/OperationType.java index 2a5ffc61e678..2a0ba1f704be 100644 --- a/src/java/org/apache/cassandra/db/compaction/OperationType.java +++ b/src/java/org/apache/cassandra/db/compaction/OperationType.java @@ -17,6 +17,16 @@ */ package org.apache.cassandra.db.compaction; +import com.google.common.base.Predicate; + +/** + * The types of operations that can be observed with {@link AbstractTableOperation} and tracked by + * {@link org.apache.cassandra.db.lifecycle.LifecycleTransaction}. + *

    + * Historically these operations have been broadly described as "compactions", even though they have + * nothing to do with actual compactions. Any operation that can report progress and that normally + * involves files, either for reading or writing, is a valid operation. + */ public enum OperationType { /** Each modification here should be also applied to {@link org.apache.cassandra.tools.nodetool.Stop#compactionType} */ @@ -49,10 +59,28 @@ public enum OperationType KEY_CACHE_SAVE("Key cache save", false, 6), ROW_CACHE_SAVE("Row cache save", false, 6), COUNTER_CACHE_SAVE("Counter cache save", false, 6), - INDEX_SUMMARY("Index summary redistribution", false, 6); + INDEX_SUMMARY("Index summary redistribution", false, 6), + // FIXME CNDB-11008: Review port of STAR-979 to review values of `writesData` and `priority` for the added operations below + RESTORE("Restore", false, 6), + // operations used for sstables on remote storage + REMOTE_RELOAD("Remote reload", false, 6, true), // reload locally sstables that already exist remotely + REMOTE_COMPACTION("Remote compaction", false, 6, true), // no longer used, kept for backward compatibility + REMOTE_RELOAD_FOR_REPAIR("Remote reload for repair", false, 6, true, false), // reload locally sstables that already exist remotely for repair + TRUNCATE_TABLE("Table truncated", false, 6), + DROP_TABLE("Table dropped", false, 6), + REMOVE_UNREADEABLE("Remove unreadable sstables", false, 6), + REGION_BOOTSTRAP("Region Bootstrap", false, 6), + REGION_DECOMMISSION("Region Decommission", false, 6), + REGION_REPAIR("Region Repair", false, 6), + SSTABLE_DISCARD("Local-only sstable discard", false, 6, true), + INITIAL_LOAD("Local-only sstable loading during node initialization", false, 6, true); public final String type; public final String fileName; + /** true if the transaction of this type should NOT be uploaded remotely */ + public final boolean localOnly; + /** true if the transaction should remove unfinished leftovers for CNDB */ + public final boolean removeTransactionLeftovers; /** * For purposes of calculating space for interim compactions in flight, whether or not this OperationType is expected @@ -67,11 +95,23 @@ public enum OperationType public final int priority; OperationType(String type, boolean writesData, int priority) + { + this(type, writesData, priority, false); + } + + OperationType(String type, boolean writesData, int priority, boolean localOnly) + { + this(type, writesData, priority, localOnly, true); + } + + OperationType(String type, boolean writesData, int priority, boolean localOnly, boolean removeTransactionLeftovers) { this.type = type; this.fileName = type.toLowerCase().replace(" ", ""); this.writesData = writesData; this.priority = priority; + this.localOnly = localOnly; + this.removeTransactionLeftovers = removeTransactionLeftovers; } public static OperationType fromFileName(String fileName) @@ -83,8 +123,20 @@ public static OperationType fromFileName(String fileName) throw new IllegalArgumentException("Invalid fileName for operation type: " + fileName); } + public boolean isCacheSave() + { + return this == COUNTER_CACHE_SAVE || this == KEY_CACHE_SAVE || this == ROW_CACHE_SAVE; + } + public String toString() { return type; } + + public static final Predicate EXCEPT_VALIDATIONS = o -> o != VALIDATION; + public static final Predicate COMPACTIONS_ONLY = o -> o == COMPACTION || o == TOMBSTONE_COMPACTION; + public static final Predicate REWRITES_SSTABLES = o -> o == COMPACTION || o == CLEANUP || o == SCRUB || + o == TOMBSTONE_COMPACTION || o == ANTICOMPACTION || + o == UPGRADE_SSTABLES || o == RELOCATE || + o == GARBAGE_COLLECT; } diff --git a/src/java/org/apache/cassandra/db/compaction/PendingRepairHolder.java b/src/java/org/apache/cassandra/db/compaction/PendingRepairHolder.java index 0c5d53c1d8a3..9d8672d28589 100644 --- a/src/java/org/apache/cassandra/db/compaction/PendingRepairHolder.java +++ b/src/java/org/apache/cassandra/db/compaction/PendingRepairHolder.java @@ -24,9 +24,9 @@ import java.util.List; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; @@ -47,9 +47,9 @@ public class PendingRepairHolder extends AbstractStrategyHolder private final List managers = new ArrayList<>(); private final boolean isTransient; - public PendingRepairHolder(ColumnFamilyStore cfs, DestinationRouter router, boolean isTransient) + public PendingRepairHolder(CompactionRealm realm, CompactionStrategyFactory strategyFactory, DestinationRouter router, boolean isTransient) { - super(cfs, router); + super(realm, strategyFactory, router); this.isTransient = isTransient; } @@ -70,7 +70,7 @@ public void setStrategyInternal(CompactionParams params, int numTokenPartitions) { managers.clear(); for (int i = 0; i < numTokenPartitions; i++) - managers.add(new PendingRepairManager(cfs, params, isTransient)); + managers.add(new PendingRepairManager(realm, strategyFactory, params, isTransient)); } @Override @@ -82,24 +82,24 @@ public boolean managesRepairedGroup(boolean isRepaired, boolean isPendingRepair, } @Override - public AbstractCompactionStrategy getStrategyFor(SSTableReader sstable) + public LegacyAbstractCompactionStrategy getStrategyFor(CompactionSSTable sstable) { Preconditions.checkArgument(managesSSTable(sstable), "Attempting to get compaction strategy from wrong holder"); return managers.get(router.getIndexForSSTable(sstable)).getOrCreate(sstable); } @Override - public Iterable allStrategies() + public Iterable allStrategies() { return Iterables.concat(Iterables.transform(managers, PendingRepairManager::getStrategies)); } - Iterable getStrategiesFor(TimeUUID session) + Iterable getStrategiesFor(TimeUUID session) { - List strategies = new ArrayList<>(managers.size()); + List strategies = new ArrayList<>(managers.size()); for (PendingRepairManager manager : managers) { - AbstractCompactionStrategy strategy = manager.get(session); + LegacyAbstractCompactionStrategy strategy = manager.get(session); if (strategy != null) strategies.add(strategy); } @@ -112,24 +112,22 @@ public Iterable getManagers() } @Override - public Collection getBackgroundTaskSuppliers(long gcBefore) + public Collection getBackgroundTaskSuppliers(long gcBefore) { - List suppliers = new ArrayList<>(managers.size()); + List suppliers = new ArrayList<>(managers.size()); for (PendingRepairManager manager : managers) - suppliers.add(new TaskSupplier(manager.getMaxEstimatedRemainingTasks(), () -> manager.getNextBackgroundTask(gcBefore))); + suppliers.add(new TasksSupplier(manager.getMaxEstimatedRemainingTasks(), () -> manager.getNextBackgroundTasks(gcBefore))); return suppliers; } @Override - public Collection getMaximalTasks(long gcBefore, boolean splitOutput) + public Collection getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) { List tasks = new ArrayList<>(managers.size()); for (PendingRepairManager manager : managers) { - Collection task = manager.getMaximalTasks(gcBefore, splitOutput); - if (task != null) - tasks.addAll(task); + tasks.addAll(manager.getMaximalTasks(gcBefore, splitOutput, permittedParallelism)); } return tasks; } @@ -149,38 +147,31 @@ public Collection getUserDefinedTasks(GroupedSSTableCont return tasks; } - @Override - public void addSSTable(SSTableReader sstable) - { - Preconditions.checkArgument(managesSSTable(sstable), "Attempting to add sstable from wrong holder"); - managers.get(router.getIndexForSSTable(sstable)).addSSTable(sstable); - } - - AbstractCompactionTask getNextRepairFinishedTask() + Collection getNextRepairFinishedTasks() { - List repairFinishedSuppliers = getRepairFinishedTaskSuppliers(); + List repairFinishedSuppliers = getRepairFinishedTaskSuppliers(); if (!repairFinishedSuppliers.isEmpty()) { Collections.sort(repairFinishedSuppliers); - for (TaskSupplier supplier : repairFinishedSuppliers) + for (TasksSupplier supplier : repairFinishedSuppliers) { - AbstractCompactionTask task = supplier.getTask(); - if (task != null) - return task; + Collection tasks = supplier.getTasks(); + if (!tasks.isEmpty()) + return tasks; } } - return null; + return ImmutableList.of(); } - private ArrayList getRepairFinishedTaskSuppliers() + private ArrayList getRepairFinishedTaskSuppliers() { - ArrayList suppliers = new ArrayList<>(managers.size()); + ArrayList suppliers = new ArrayList<>(managers.size()); for (PendingRepairManager manager : managers) { int numPending = manager.getNumPendingRepairFinishedTasks(); if (numPending > 0) { - suppliers.add(new TaskSupplier(numPending, manager::getNextRepairFinishedTask)); + suppliers.add(new TasksSupplier(numPending, manager::getNextRepairFinishedTasks)); } } @@ -227,7 +218,7 @@ public void replaceSSTables(GroupedSSTableContainer removed, GroupedSSTableConta } @Override - public List getScanners(GroupedSSTableContainer sstables, Collection> ranges) + public List getScanners(GroupedSSTableContainer sstables, Collection> ranges) { List scanners = new ArrayList<>(managers.size()); for (int i = 0; i < managers.size(); i++) @@ -257,7 +248,7 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, Preconditions.checkArgument(pendingRepair != null, "PendingRepairHolder can't create sstable writer without pendingRepair id"); // to avoid creating a compaction strategy for the wrong pending repair manager, we get the index based on where the sstable is to be written - AbstractCompactionStrategy strategy = managers.get(router.getIndexForSSTableDirectory(descriptor)).getOrCreate(pendingRepair); + CompactionStrategy strategy = managers.get(router.getIndexForSSTableDirectory(descriptor)).getOrCreate(pendingRepair); return strategy.createSSTableMultiWriter(descriptor, keyCount, repairedAt, @@ -270,24 +261,13 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, lifecycleNewTracker); } - @Override - public int getStrategyIndex(AbstractCompactionStrategy strategy) - { - for (int i = 0; i < managers.size(); i++) - { - if (managers.get(i).hasStrategy(strategy)) - return i; - } - return -1; - } - public boolean hasDataForSession(TimeUUID sessionID) { return Iterables.any(managers, prm -> prm.hasDataForSession(sessionID)); } @Override - public boolean containsSSTable(SSTableReader sstable) + public boolean containsSSTable(CompactionSSTable sstable) { return Iterables.any(managers, prm -> prm.containsSSTable(sstable)); } @@ -301,8 +281,8 @@ public int getEstimatedRemainingTasks() return tasks; } - public boolean hasPendingRepairSSTable(TimeUUID sessionID, SSTableReader sstable) + public int size() { - return Iterables.any(managers, prm -> prm.hasPendingRepairSSTable(sessionID, sstable)); + return managers.size(); } } diff --git a/src/java/org/apache/cassandra/db/compaction/PendingRepairManager.java b/src/java/org/apache/cassandra/db/compaction/PendingRepairManager.java index 7251c04dcf02..78d9d31f84cf 100644 --- a/src/java/org/apache/cassandra/db/compaction/PendingRepairManager.java +++ b/src/java/org/apache/cassandra/db/compaction/PendingRepairManager.java @@ -28,31 +28,26 @@ import java.util.Set; import java.util.stream.Collectors; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.common.collect.Maps; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Directories; -import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.repair.consistent.admin.CleanupSummary; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.TimeUUID; /** - * Companion to CompactionStrategyManager which manages the sstables marked pending repair. + * This class manages the sstables marked pending repair so that they can be assigned to legacy compaction + * strategies via the legacy strategy container or manager. * * SSTables are classified as pending repair by the anti-compaction performed at the beginning * of an incremental repair, or when they're streamed in with a pending repair id. This prevents @@ -63,10 +58,11 @@ class PendingRepairManager { private static final Logger logger = LoggerFactory.getLogger(PendingRepairManager.class); - private final ColumnFamilyStore cfs; + private final CompactionRealm realm; + private final CompactionStrategyFactory strategyFactory; private final CompactionParams params; private final boolean isTransient; - private volatile ImmutableMap strategies = ImmutableMap.of(); + private volatile ImmutableMap strategies = ImmutableMap.of(); /** * Indicates we're being asked to do something with an sstable that isn't marked pending repair @@ -79,34 +75,35 @@ public IllegalSSTableArgumentException(String s) } } - PendingRepairManager(ColumnFamilyStore cfs, CompactionParams params, boolean isTransient) + PendingRepairManager(CompactionRealm realm, CompactionStrategyFactory strategyFactory, CompactionParams params, boolean isTransient) { - this.cfs = cfs; + this.realm = realm; + this.strategyFactory = strategyFactory; this.params = params; this.isTransient = isTransient; } - private ImmutableMap.Builder mapBuilder() + private ImmutableMap.Builder mapBuilder() { return ImmutableMap.builder(); } - AbstractCompactionStrategy get(TimeUUID id) + LegacyAbstractCompactionStrategy get(TimeUUID id) { return strategies.get(id); } - AbstractCompactionStrategy get(SSTableReader sstable) + LegacyAbstractCompactionStrategy get(CompactionSSTable sstable) { assert sstable.isPendingRepair(); - return get(sstable.getSSTableMetadata().pendingRepair); + return get(sstable.getPendingRepair()); } - AbstractCompactionStrategy getOrCreate(TimeUUID id) + LegacyAbstractCompactionStrategy getOrCreate(TimeUUID id) { checkPendingID(id); assert id != null; - AbstractCompactionStrategy strategy = get(id); + LegacyAbstractCompactionStrategy strategy = get(id); if (strategy == null) { synchronized (this) @@ -115,8 +112,8 @@ AbstractCompactionStrategy getOrCreate(TimeUUID id) if (strategy == null) { - logger.debug("Creating {}.{} compaction strategy for pending repair: {}", cfs.metadata.keyspace, cfs.metadata.name, id); - strategy = cfs.createCompactionStrategyInstance(params); + logger.debug("Creating {}.{} compaction strategy for pending repair: {}", realm.getKeyspaceName(), realm.getTableName(), id); + strategy = strategyFactory.createLegacyStrategy(params); strategies = mapBuilder().putAll(strategies).put(id, strategy).build(); } } @@ -132,58 +129,57 @@ private static void checkPendingID(TimeUUID pendingID) } } - AbstractCompactionStrategy getOrCreate(SSTableReader sstable) + LegacyAbstractCompactionStrategy getOrCreate(CompactionSSTable sstable) { - return getOrCreate(sstable.getSSTableMetadata().pendingRepair); + return getOrCreate(sstable.getPendingRepair()); } - private synchronized void removeSessionIfEmpty(TimeUUID sessionID) + synchronized void removeSessionIfEmpty(TimeUUID sessionID) { if (!strategies.containsKey(sessionID) || !strategies.get(sessionID).getSSTables().isEmpty()) return; - logger.debug("Removing compaction strategy for pending repair {} on {}.{}", sessionID, cfs.metadata.keyspace, cfs.metadata.name); + logger.debug("Removing compaction strategy for pending repair {} on {}.{}", sessionID, realm.getKeyspaceName(), realm.getTableName()); strategies = ImmutableMap.copyOf(Maps.filterKeys(strategies, k -> !k.equals(sessionID))); } - synchronized void removeSSTable(SSTableReader sstable) + synchronized void removeSSTable(CompactionSSTable sstable) { - for (Map.Entry entry : strategies.entrySet()) + for (Map.Entry entry : strategies.entrySet()) { entry.getValue().removeSSTable(sstable); removeSessionIfEmpty(entry.getKey()); } } - - void removeSSTables(Iterable removed) + void removeSSTables(Iterable removed) { - for (SSTableReader sstable : removed) + for (CompactionSSTable sstable : removed) removeSSTable(sstable); } - synchronized void addSSTable(SSTableReader sstable) + synchronized void addSSTable(CompactionSSTable sstable) { Preconditions.checkArgument(sstable.isTransient() == isTransient); getOrCreate(sstable).addSSTable(sstable); } - void addSSTables(Iterable added) + void addSSTables(Iterable added) { - for (SSTableReader sstable : added) + for (CompactionSSTable sstable : added) addSSTable(sstable); } - synchronized void replaceSSTables(Set removed, Set added) + synchronized void replaceSSTables(Set removed, Set added) { if (removed.isEmpty() && added.isEmpty()) return; // left=removed, right=added - Map, Set>> groups = new HashMap<>(); - for (SSTableReader sstable : removed) + Map, Set>> groups = new HashMap<>(); + for (CompactionSSTable sstable : removed) { - TimeUUID sessionID = sstable.getSSTableMetadata().pendingRepair; + TimeUUID sessionID = sstable.getPendingRepair(); if (!groups.containsKey(sessionID)) { groups.put(sessionID, Pair.create(new HashSet<>(), new HashSet<>())); @@ -191,9 +187,9 @@ synchronized void replaceSSTables(Set removed, Set groups.get(sessionID).left.add(sstable); } - for (SSTableReader sstable : added) + for (CompactionSSTable sstable : added) { - TimeUUID sessionID = sstable.getSSTableMetadata().pendingRepair; + TimeUUID sessionID = sstable.getPendingRepair(); if (!groups.containsKey(sessionID)) { groups.put(sessionID, Pair.create(new HashSet<>(), new HashSet<>())); @@ -201,11 +197,11 @@ synchronized void replaceSSTables(Set removed, Set groups.get(sessionID).right.add(sstable); } - for (Map.Entry, Set>> entry : groups.entrySet()) + for (Map.Entry, Set>> entry : groups.entrySet()) { - AbstractCompactionStrategy strategy = getOrCreate(entry.getKey()); - Set groupRemoved = entry.getValue().left; - Set groupAdded = entry.getValue().right; + LegacyAbstractCompactionStrategy strategy = getOrCreate(entry.getKey()); + Set groupRemoved = entry.getValue().left; + Set groupAdded = entry.getValue().right; if (!groupRemoved.isEmpty()) strategy.replaceSSTables(groupRemoved, groupAdded); @@ -218,12 +214,12 @@ synchronized void replaceSSTables(Set removed, Set synchronized void startup() { - strategies.values().forEach(AbstractCompactionStrategy::startup); + strategies.values().forEach(CompactionStrategy::startup); } synchronized void shutdown() { - strategies.values().forEach(AbstractCompactionStrategy::shutdown); + strategies.values().forEach(CompactionStrategy::shutdown); } private int getEstimatedRemainingTasks(TimeUUID sessionID, AbstractCompactionStrategy strategy) @@ -244,7 +240,7 @@ int getEstimatedRemainingTasks() int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes) { int tasks = 0; - for (Map.Entry entry : strategies.entrySet()) + for (Map.Entry entry : strategies.entrySet()) { tasks += getEstimatedRemainingTasks(entry.getKey(), entry.getValue(), additionalSSTables, additionalBytes); } @@ -257,7 +253,7 @@ int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes) int getMaxEstimatedRemainingTasks() { int tasks = 0; - for (Map.Entry entry : strategies.entrySet()) + for (Map.Entry entry : strategies.entrySet()) { tasks = Math.max(tasks, getEstimatedRemainingTasks(entry.getKey(), entry.getValue())); } @@ -267,63 +263,13 @@ int getMaxEstimatedRemainingTasks() private RepairFinishedCompactionTask getRepairFinishedCompactionTask(TimeUUID sessionID) { Preconditions.checkState(canCleanup(sessionID)); - AbstractCompactionStrategy compactionStrategy = get(sessionID); + LegacyAbstractCompactionStrategy compactionStrategy = get(sessionID); if (compactionStrategy == null) return null; - Set sstables = compactionStrategy.getSSTables(); + Set sstables = compactionStrategy.getSSTables(); long repairedAt = ActiveRepairService.instance().consistent.local.getFinalSessionRepairedAt(sessionID); - LifecycleTransaction txn = cfs.getTracker().tryModify(sstables, OperationType.COMPACTION); - return txn == null ? null : new RepairFinishedCompactionTask(cfs, txn, sessionID, repairedAt); - } - - public static class CleanupTask - { - private final ColumnFamilyStore cfs; - private final List> tasks; - - public CleanupTask(ColumnFamilyStore cfs, List> tasks) - { - this.cfs = cfs; - this.tasks = tasks; - } - - public CleanupSummary cleanup() - { - Set successful = new HashSet<>(); - Set unsuccessful = new HashSet<>(); - for (Pair pair : tasks) - { - TimeUUID session = pair.left; - RepairFinishedCompactionTask task = pair.right; - - if (task != null) - { - try - { - task.run(); - successful.add(session); - } - catch (Throwable t) - { - t = task.transaction.abort(t); - logger.error("Failed cleaning up " + session, t); - unsuccessful.add(session); - } - } - else - { - unsuccessful.add(session); - } - } - return new CleanupSummary(cfs, successful, unsuccessful); - } - - public Throwable abort(Throwable accumulate) - { - for (Pair pair : tasks) - accumulate = pair.right.transaction.abort(accumulate); - return accumulate; - } + LifecycleTransaction txn = realm.tryModify(sstables, OperationType.COMPACTION); + return txn == null ? null : new RepairFinishedCompactionTask(realm, txn, sessionID, repairedAt, isTransient); } public CleanupTask releaseSessionData(Collection sessionIDs) @@ -336,7 +282,7 @@ public CleanupTask releaseSessionData(Collection sessionIDs) tasks.add(Pair.create(session, getRepairFinishedCompactionTask(session))); } } - return new CleanupTask(cfs, tasks); + return new CleanupTask(realm, tasks); } synchronized int getNumPendingRepairFinishedTasks() @@ -352,26 +298,29 @@ synchronized int getNumPendingRepairFinishedTasks() return count; } - synchronized AbstractCompactionTask getNextRepairFinishedTask() + synchronized Collection getNextRepairFinishedTasks() { for (TimeUUID sessionID : strategies.keySet()) { if (canCleanup(sessionID)) { - return getRepairFinishedCompactionTask(sessionID); + RepairFinishedCompactionTask task = getRepairFinishedCompactionTask(sessionID); + if (task != null) + return ImmutableList.of(task); + else + return ImmutableList.of(); } } - return null; + return ImmutableList.of(); } - synchronized AbstractCompactionTask getNextBackgroundTask(long gcBefore) + synchronized Collection getNextBackgroundTasks(long gcBefore) { if (strategies.isEmpty()) - return null; - + return ImmutableList.of(); Map numTasks = new HashMap<>(strategies.size()); ArrayList sessions = new ArrayList<>(strategies.size()); - for (Map.Entry entry : strategies.entrySet()) + for (Map.Entry entry : strategies.entrySet()) { if (canCleanup(entry.getKey())) { @@ -382,22 +331,22 @@ synchronized AbstractCompactionTask getNextBackgroundTask(long gcBefore) } if (sessions.isEmpty()) - return null; + return ImmutableList.of(); // we want the session with the most compactions at the head of the list sessions.sort((o1, o2) -> numTasks.get(o2) - numTasks.get(o1)); TimeUUID sessionID = sessions.get(0); - return get(sessionID).getNextBackgroundTask(gcBefore); + return get(sessionID).getNextBackgroundTasks(gcBefore); } - synchronized Collection getMaximalTasks(long gcBefore, boolean splitOutput) + synchronized Collection getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) { if (strategies.isEmpty()) - return null; + return ImmutableList.of(); List maximalTasks = new ArrayList<>(strategies.size()); - for (Map.Entry entry : strategies.entrySet()) + for (Map.Entry entry : strategies.entrySet()) { if (canCleanup(entry.getKey())) { @@ -405,15 +354,13 @@ synchronized Collection getMaximalTasks(long gcBefore, b } else { - Collection tasks = entry.getValue().getMaximalTask(gcBefore, splitOutput); - if (tasks != null) - maximalTasks.addAll(tasks); + maximalTasks.addAll(entry.getValue().getMaximalTasks(gcBefore, splitOutput, permittedParallelism)); } } - return !maximalTasks.isEmpty() ? maximalTasks : null; + return maximalTasks; } - Collection getStrategies() + Collection getStrategies() { return strategies.values(); } @@ -438,7 +385,7 @@ synchronized Set getScanners(Collection sstables Map> sessionSSTables = new HashMap<>(); for (SSTableReader sstable : sstables) { - TimeUUID sessionID = sstable.getSSTableMetadata().pendingRepair; + TimeUUID sessionID = sstable.getPendingRepair(); checkPendingID(sessionID); sessionSSTables.computeIfAbsent(sessionID, k -> new HashSet<>()).add(sstable); } @@ -458,7 +405,7 @@ synchronized Set getScanners(Collection sstables return scanners; } - public boolean hasStrategy(AbstractCompactionStrategy strategy) + public boolean hasStrategy(CompactionStrategy strategy) { return strategies.values().contains(strategy); } @@ -468,7 +415,7 @@ public synchronized boolean hasDataForSession(TimeUUID sessionID) return strategies.containsKey(sessionID); } - boolean containsSSTable(SSTableReader sstable) + boolean containsSSTable(CompactionSSTable sstable) { if (!sstable.isPendingRepair()) return false; @@ -477,91 +424,9 @@ boolean containsSSTable(SSTableReader sstable) return strategy != null && strategy.getSSTables().contains(sstable); } - public Collection createUserDefinedTasks(Collection sstables, long gcBefore) - { - Map> group = sstables.stream().collect(Collectors.groupingBy(s -> s.getSSTableMetadata().pendingRepair)); - return group.entrySet().stream().map(g -> strategies.get(g.getKey()).getUserDefinedTask(g.getValue(), gcBefore)).collect(Collectors.toList()); - } - - @VisibleForTesting - public synchronized boolean hasPendingRepairSSTable(TimeUUID sessionID, SSTableReader sstable) - { - AbstractCompactionStrategy strat = strategies.get(sessionID); - if (strat == null) - return false; - return strat.getSSTables().contains(sstable); - } - - /** - * promotes/demotes sstables involved in a consistent repair that has been finalized, or failed - */ - class RepairFinishedCompactionTask extends AbstractCompactionTask + public Collection createUserDefinedTasks(Collection sstables, long gcBefore) { - private final TimeUUID sessionID; - private final long repairedAt; - - RepairFinishedCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction transaction, TimeUUID sessionID, long repairedAt) - { - super(cfs, transaction); - this.sessionID = sessionID; - this.repairedAt = repairedAt; - } - - @VisibleForTesting - TimeUUID getSessionID() - { - return sessionID; - } - - protected void runMayThrow() throws Exception - { - boolean completed = false; - boolean obsoleteSSTables = isTransient && repairedAt > 0; - try - { - if (obsoleteSSTables) - { - logger.info("Obsoleting transient repaired sstables for {}", sessionID); - Preconditions.checkState(Iterables.all(transaction.originals(), SSTableReader::isTransient)); - transaction.obsoleteOriginals(); - } - else - { - logger.info("Moving {} from pending to repaired with repaired at = {} and session id = {}", transaction.originals(), repairedAt, sessionID); - cfs.getCompactionStrategyManager().mutateRepaired(transaction.originals(), repairedAt, ActiveRepairService.NO_PENDING_REPAIR, false); - } - completed = true; - } - finally - { - if (obsoleteSSTables) - { - transaction.finish(); - } - else - { - // we abort here because mutating metadata isn't guarded by LifecycleTransaction, so this won't roll - // anything back. Also, we don't want to obsolete the originals. We're only using it to prevent other - // compactions from marking these sstables compacting, and unmarking them when we're done - transaction.abort(); - } - if (completed) - { - removeSessionIfEmpty(sessionID); - } - } - } - - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables) - { - throw new UnsupportedOperationException(); - } - - protected int executeInternal(ActiveCompactionsTracker activeCompactions) - { - run(); - return transaction.originals().size(); - } + Map> group = sstables.stream().collect(Collectors.groupingBy(s -> s.getPendingRepair())); + return group.entrySet().stream().map(g -> strategies.get(g.getKey()).getUserDefinedTasks(g.getValue(), gcBefore)).flatMap(Collection::stream).collect(Collectors.toList()); } - } diff --git a/src/java/org/apache/cassandra/db/compaction/RepairFinishedCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/RepairFinishedCompactionTask.java new file mode 100644 index 000000000000..9e11df81780b --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/RepairFinishedCompactionTask.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.utils.TimeUUID; + +/** + * promotes/demotes sstables involved in a consistent repair that has been finalized, or failed + */ +public class RepairFinishedCompactionTask extends AbstractCompactionTask +{ + private static final Logger logger = LoggerFactory.getLogger(RepairFinishedCompactionTask.class); + + private final TimeUUID sessionID; + private final long repairedAt; + private final boolean isTransient; + + public RepairFinishedCompactionTask(CompactionRealm realm, + ILifecycleTransaction transaction, + TimeUUID sessionID, + long repairedAt, + boolean isTransient) + { + super(realm, transaction); + this.sessionID = sessionID; + this.repairedAt = repairedAt; + this.isTransient = isTransient; + } + + @VisibleForTesting + TimeUUID getSessionID() + { + return sessionID; + } + + protected void runMayThrow() throws Exception + { + boolean completed = false; + boolean obsoleteSSTables = isTransient && repairedAt > 0; + try + { + if (obsoleteSSTables) + { + logger.info("Obsoleting transient repaired sstables for {}", sessionID); + Preconditions.checkState(Iterables.all(transaction.originals(), SSTableReader::isTransient)); + transaction.obsoleteOriginals(); + } + else + { + logger.info("Moving {} from pending to repaired with repaired at = {} for session id = {}", transaction.originals(), repairedAt, sessionID); + realm.mutateRepairedWithLock(transaction.originals(), + repairedAt, + ActiveRepairService.NO_PENDING_REPAIR, + false); + realm.repairSessionCompleted(sessionID); + } + completed = true; + } + finally + { + if (obsoleteSSTables) + { + transaction.prepareToCommit(); + transaction.commit(); + } + else + { + // we abort here because mutating metadata isn't guarded by LifecycleTransaction, so this won't roll + // anything back. Also, we don't want to obsolete the originals. We're only using it to prevent other + // compactions from marking these sstables compacting, and unmarking them when we're done + transaction.abort(); + } + if (completed) + { + realm.repairSessionCompleted(sessionID); + } + } + } + + @Override + public long getSpaceOverhead() + { + return 0; // This is just metadata modification, no overhead. + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java b/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java index 6f68c340f0e5..9cad8f7ddbb2 100644 --- a/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java +++ b/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java @@ -17,36 +17,37 @@ */ package org.apache.cassandra.db.compaction; -import java.util.*; +import java.util.Set; import java.util.function.LongPredicate; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; import org.apache.cassandra.db.compaction.writers.MaxSSTableSizeWriter; -import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableReader; public class SSTableSplitter { - private final SplittingCompactionTask task; + private final AbstractCompactionTask task; - public SSTableSplitter(ColumnFamilyStore cfs, LifecycleTransaction transaction, int sstableSizeInMB) + public SSTableSplitter(CompactionRealm realm, LifecycleTransaction transaction, int sstableSizeInMB) { - this.task = new SplittingCompactionTask(cfs, transaction, sstableSizeInMB); + this.task = new SplittingCompactionTask(realm, transaction, sstableSizeInMB); } public void split() { - task.execute(ActiveCompactionsTracker.NOOP); + task.execute(); } - public static class SplittingCompactionTask extends CompactionTask + private static class SplittingCompactionTask extends CompactionTask { private final int sstableSizeInMiB; - public SplittingCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction transaction, int sstableSizeInMB) + public SplittingCompactionTask(CompactionRealm realm, LifecycleTransaction transaction, int sstableSizeInMB) { - super(cfs, transaction, CompactionManager.NO_GC, false); + super(realm, transaction, CompactionManager.NO_GC, false, null); this.sstableSizeInMiB = sstableSizeInMB; if (sstableSizeInMB <= 0) @@ -56,16 +57,15 @@ public SplittingCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction trans @Override protected CompactionController getCompactionController(Set toCompact) { - return new SplitController(cfs); + return new SplitController(realm); } @Override - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, + public CompactionAwareWriter getCompactionAwareWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, Set nonExpiredSSTables) { - return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, sstableSizeInMiB * 1024L * 1024L, 0, false); + return new MaxSSTableSizeWriter(realm, directories, transaction, nonExpiredSSTables, sstableSizeInMiB * 1024L * 1024L, 0, false); } @Override @@ -77,7 +77,7 @@ protected boolean partialCompactionsAcceptable() public static class SplitController extends CompactionController { - public SplitController(ColumnFamilyStore cfs) + public SplitController(CompactionRealm cfs) { super(cfs, CompactionManager.NO_GC); } diff --git a/src/java/org/apache/cassandra/db/compaction/ShardManager.java b/src/java/org/apache/cassandra/db/compaction/ShardManager.java index 6ea2cd72a84c..730d7367f54f 100644 --- a/src/java/org/apache/cassandra/db/compaction/ShardManager.java +++ b/src/java/org/apache/cassandra/db/compaction/ShardManager.java @@ -18,105 +18,132 @@ package org.apache.cassandra.db.compaction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.PriorityQueue; import java.util.Set; -import java.util.stream.Collectors; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.ObjIntConsumer; -import com.google.common.collect.ImmutableList; - -import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DiskBoundaries; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.SortedLocalRanges; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.SortingIterator; public interface ShardManager { - /** - * Single-partition, and generally sstables with very few partitions, can cover very small sections of the token - * space, resulting in very high densities. - * Additionally, sstables that have completely fallen outside of the local token ranges will end up with a zero - * coverage. - * To avoid problems with both we check if coverage is below the minimum, and replace it with 1. - */ - static final double MINIMUM_TOKEN_COVERAGE = Math.scalb(1.0, -48); + /// Single-partition, and generally sstables with very few partitions, can cover very small sections of the token + /// space, resulting in very high densities. + /// + /// When the number of partitions in an sstable is smaller than this threshold, we will use a per-partition minimum + /// span, calculated from the total number of partitions in this table. + long PER_PARTITION_SPAN_THRESHOLD = 100; + + /// Additionally, sstables that have completely fallen outside the local token ranges will end up with a zero + /// coverage. + /// + /// To avoid problems with this we check if coverage is below the minimum, and replace it using the per-partition + /// calculation. + double MINIMUM_TOKEN_COVERAGE = Math.scalb(1.0, -48); - static ShardManager create(ColumnFamilyStore cfs) + static ShardManager create(DiskBoundaries diskBoundaries, AbstractReplicationStrategy rs, boolean isReplicaAware) { - final ImmutableList diskPositions = cfs.getDiskBoundaries().positions; - ColumnFamilyStore.VersionedLocalRanges localRanges = cfs.localRangesWeighted(); - IPartitioner partitioner = cfs.getPartitioner(); + List diskPositions = diskBoundaries.getPositions(); + + SortedLocalRanges localRanges = diskBoundaries.getLocalRanges(); + IPartitioner partitioner = localRanges.getRealm().getPartitioner(); + // this should only happen in tests that change partitioners, but we don't want UCS to throw + // where other strategies work even if the situations are unrealistic. + if (localRanges.getRanges().isEmpty() || !localRanges.getRanges() + .get(0) + .range() + .left + .getPartitioner() + .equals(localRanges.getRealm().getPartitioner())) + localRanges = new SortedLocalRanges(localRanges.getRealm(), + localRanges.getRingVersion(), + null); + if (diskPositions != null && diskPositions.size() > 1) - return new ShardManagerDiskAware(localRanges, diskPositions.stream() - .map(PartitionPosition::getToken) - .collect(Collectors.toList())); + return new ShardManagerDiskAware(localRanges, diskPositions); else if (partitioner.splitter().isPresent()) - return new ShardManagerNoDisks(localRanges); + if (isReplicaAware) + return new ShardManagerReplicaAware(rs, localRanges.getRealm()); + else + return new ShardManagerNoDisks(localRanges); else return new ShardManagerTrivial(partitioner); } - boolean isOutOfDate(long ringVersion); - - /** - * The token range fraction spanned by the given range, adjusted for the local range ownership. - */ + /// The token range fraction spanned by the given range, adjusted for the local range ownership. double rangeSpanned(Range tableRange); - /** - * The total fraction of the token space covered by the local ranges. - */ + /// The total fraction of the token space covered by the local ranges. double localSpaceCoverage(); - /** - * The fraction of the token space covered by a shard set, i.e. the space that is split in the requested number of - * shards. - * If no disks are defined, this is the same as localSpaceCoverage(). Otherwise, it is the token coverage of a disk. - */ + /// The fraction of the token space covered by a shard set, i.e. the space that is split in the requested number of + /// shards. + /// + /// If no disks are defined, this is the same as localSpaceCoverage(). Otherwise, it is the token coverage of a disk. double shardSetCoverage(); - /** - * Construct a boundary/shard iterator for the given number of shards. - * - * Note: This does not offer a method of listing the shard boundaries it generates, just to advance to the - * corresponding one for a given token. The only usage for listing is currently in tests. Should a need for this - * arise, see {@link CompactionSimulationTest} for a possible implementation. - */ + /// The minimum token space share per partition that should be assigned to sstables with small numbers of partitions + /// or which have fallen outside the local token ranges. + double minimumPerPartitionSpan(); + + /// Construct a boundary/shard iterator for the given number of shards. + /// + /// If a list of the ranges for each shard is required instead, use [#getShardRanges]. ShardTracker boundaries(int shardCount); - static Range coveringRange(SSTableReader sstable) + static Range coveringRange(CompactionSSTable sstable) { return coveringRange(sstable.getFirst(), sstable.getLast()); } static Range coveringRange(PartitionPosition first, PartitionPosition last) { - // To include the token of last, the range's upper bound must be increased. - return new Range<>(first.getToken(), last.getToken().nextValidToken()); + // To include the token of left, the range's lower bound must be decreased. + return new Range<>(first.getToken().isMinimum() ? first.getToken() : first.getToken().prevValidToken(), last.getToken()); } - /** - * Return the token space share that the given SSTable spans, excluding any non-locally owned space. - * Returns a positive floating-point number between 0 and 1. - */ - default double rangeSpanned(SSTableReader rdr) + /// Return the token space share that the given SSTable spans, excluding any non-locally owned space. + /// Returns a positive floating-point number between 0 and 1. + default double rangeSpanned(CompactionSSTable rdr) { double reported = rdr.tokenSpaceCoverage(); + double span; if (reported > 0) // also false for NaN span = reported; else span = rangeSpanned(rdr.getFirst(), rdr.getLast()); - if (span >= MINIMUM_TOKEN_COVERAGE) + long partitionCount = rdr.estimatedKeys(); + return adjustSmallSpans(span, partitionCount); + } + + private double adjustSmallSpans(double span, long partitionCount) + { + if (partitionCount >= PER_PARTITION_SPAN_THRESHOLD && span >= MINIMUM_TOKEN_COVERAGE) return span; - // Too small ranges are expected to be the result of either a single-partition sstable or falling outside - // of the local token ranges. In these cases we substitute it with 1 because for them sharding and density - // tiering does not make sense. - return 1.0; // This will be chosen if span is NaN too. + // Too small ranges are expected to be the result of either an sstable with a very small number of partitions, + // or falling outside the local token ranges. In these cases we apply a per-partition minimum calculated from + // the number of partitions in the table. + double perPartitionMinimum = Math.min(partitionCount * minimumPerPartitionSpan(), 1.0); + return span > perPartitionMinimum ? span : perPartitionMinimum; } default double rangeSpanned(PartitionPosition first, PartitionPosition last) @@ -124,43 +151,231 @@ default double rangeSpanned(PartitionPosition first, PartitionPosition last) return rangeSpanned(ShardManager.coveringRange(first, last)); } - /** - * Return the density of an SSTable, i.e. its size divided by the covered token space share. - * This is an improved measure of the compaction age of an SSTable that grows both with STCS-like full-SSTable - * compactions (where size grows, share is constant), LCS-like size-threshold splitting (where size is constant - * but share shrinks), UCS-like compactions (where size may grow and covered shards i.e. share may decrease) - * and can reproduce levelling structure that corresponds to all, including their mixtures. - */ - default double density(SSTableReader rdr) + /// Return the density of an SSTable, i.e. its size divided by the covered token space share. + /// This is an improved measure of the compaction age of an SSTable that grows both with STCS-like full-SSTable + /// compactions (where size grows, share is constant), LCS-like size-threshold splitting (where size is constant + /// but share shrinks), UCS-like compactions (where size may grow and covered shards i.e. share may decrease) + /// and can reproduce levelling structure that corresponds to all, including their mixtures. + default double density(CompactionSSTable rdr) { return rdr.onDiskLength() / rangeSpanned(rdr); } - default int compareByDensity(SSTableReader a, SSTableReader b) + default double density(long onDiskLength, PartitionPosition min, PartitionPosition max, long approximatePartitionCount) { - return Double.compare(density(a), density(b)); + double span = rangeSpanned(min, max); + return onDiskLength / adjustSmallSpans(span, approximatePartitionCount); } - /** - * Estimate the density of the sstable that will be the result of compacting the given sources. - */ - default double calculateCombinedDensity(Set sstables) + + /// Seggregate the given sstables into the shard ranges that intersect sstables from the collection, and call + /// the given function on the intersecting sstable set, with access to the shard tracker from which information + /// about the shard can be recovered. + /// + /// If an operationRange is given, this method restricts the collection to the given range and assumes all sstables + /// cover at least some portion of that range. + private void assignSSTablesInShards(Collection sstables, + Range operationRange, + int numShardsForDensity, + BiConsumer, ShardTracker> consumer) { - if (sstables.isEmpty()) - return 0; - long onDiskLength = 0; - PartitionPosition min = null; - PartitionPosition max = null; - for (SSTableReader sstable : sstables) + var boundaries = boundaries(numShardsForDensity); + SortingIterator items = SortingIterator.create(CompactionSSTable.firstKeyComparator, sstables); + PriorityQueue active = new PriorityQueue<>(CompactionSSTable.lastKeyComparator); + // Advance inside the range. This will add all sstables that start before the end of the covering shard. + if (operationRange != null) + boundaries.advanceTo(operationRange.left.nextValidToken()); + while (items.hasNext() || !active.isEmpty()) { - onDiskLength += sstable.onDiskLength(); - min = min == null || min.compareTo(sstable.getFirst()) > 0 ? sstable.getFirst() : min; - max = max == null || max.compareTo(sstable.getLast()) < 0 ? sstable.getLast() : max; + if (active.isEmpty()) + { + boundaries.advanceTo(items.peek().getFirst().getToken()); + active.add(items.next()); + } + Token shardEnd = boundaries.shardEnd(); + if (operationRange != null && + !operationRange.right.isMinimum() && + shardEnd != null && + shardEnd.compareTo(operationRange.right) >= 0) + shardEnd = null; // Take all remaining sstables. + + while (items.hasNext() && (shardEnd == null || items.peek().getFirst().getToken().compareTo(shardEnd) <= 0)) + active.add(items.next()); + + consumer.accept(active, boundaries); + + while (!active.isEmpty() && (shardEnd == null || active.peek().getLast().getToken().compareTo(shardEnd) <= 0)) + active.poll(); + + if (!active.isEmpty()) // shardEnd must be non-null (otherwise the line above exhausts all) + boundaries.advanceTo(shardEnd.nextValidToken()); } - double span = rangeSpanned(min, max); - if (span >= MINIMUM_TOKEN_COVERAGE) - return onDiskLength / span; - else - return onDiskLength; + } + + /// Seggregate the given sstables into the shard ranges that intersect sstables from the collection, and call + /// the given function on the combination of each shard index and the intersecting sstable set. + /// + /// If an operationRange is given, this method restricts the collection to the given range and assumes all sstables + /// cover at least some portion of that range. + default void assignSSTablesToShardIndexes(Collection sstables, + Range operationRange, + int numShardsForDensity, + ObjIntConsumer> consumer) + { + assignSSTablesInShards(sstables, operationRange, numShardsForDensity, + (rangeSSTables, boundaries) -> consumer.accept(rangeSSTables, boundaries.shardIndex())); + } + + /// Seggregate the given sstables into the shard ranges that intersect sstables from the collection, and call + /// the given function on the combination of each shard range and the intersecting sstable set. + default List splitSSTablesInShards(Collection sstables, + int numShardsForDensity, + BiFunction, Range, T> maker) + { + return splitSSTablesInShards(sstables, null, numShardsForDensity, maker); + } + + /// Seggregate the given sstables into the shard ranges that intersect sstables from the collection, and call + /// the given function on the combination of each shard range and the intersecting sstable set. + /// + /// This version restricts the operation to the given token range, and assumes all sstables cover at least some + /// portion of that range. + default List splitSSTablesInShards(Collection sstables, + Range operationRange, + int numShardsForDensity, + BiFunction, Range, T> maker) + { + List tasks = new ArrayList<>(); + assignSSTablesInShards(sstables, operationRange, numShardsForDensity, (rangeSSTables, boundaries) -> { + final T result = maker.apply(rangeSSTables, boundaries.shardSpan()); + if (result != null) + tasks.add(result); + }); + return tasks; + } + + /// Seggregate the given sstables into the shard ranges that intersect sstables from the collection, and call + /// the given function on the combination of each shard range and the intersecting sstable set. + /// + /// This version restricts the operation to the given token range (which may be null) and accepts a parallelism + /// limit and will group shards together to fit within that limit. + default List splitSSTablesInShardsLimited(Collection sstables, + Range operationRange, + int numShardsForDensity, + int coveredShards, + int maxParallelism, + BiFunction, Range, T> maker) + { + if (coveredShards <= maxParallelism) + return splitSSTablesInShards(sstables, operationRange, numShardsForDensity, maker); + + var shards = splitSSTablesInShards(sstables, + operationRange, + numShardsForDensity, + (rangeSSTables, range) -> Pair.create(Set.copyOf(rangeSSTables), range)); + + return applyMaxParallelism(maxParallelism, maker, shards); + } + + private static List applyMaxParallelism(int maxParallelism, + BiFunction, Range, T> maker, + List, Range>> shards) + { + Iterator, Range>> iter = shards.iterator(); + List tasks = new ArrayList<>(maxParallelism); + int shardsRemaining = shards.size(); + int tasksRemaining = maxParallelism; + + if (shardsRemaining > tasksRemaining) + { + double totalSpan = shards.stream().map(Pair::right).mapToDouble(r -> r.left.size(r.right)).sum(); + double spanPerTask = totalSpan / maxParallelism; + + Set currentSSTables = new HashSet<>(); + Token rangeStart = null; + double currentSpan = 0; + + // While we have more shards to process than there are tasks, we need to bunch shards up into tasks. + while (shardsRemaining > tasksRemaining) + { + Pair, Range> pair = iter.next(); // shardsRemaining counts the shards so iter can't be exhausted at this point + Token currentStart = pair.right.left; + Token currentEnd = pair.right.right; + double span = currentStart.size(currentEnd); + + if (rangeStart == null) + rangeStart = currentStart; + + currentSSTables.addAll(pair.left); + currentSpan += span; + + // If there is only one task remaining, we should not issue it until we are processing the last shard. + // The latter condition is normally guaranteed, but floating point rounding has a very small chance of making the calculations wrong + if (currentSpan >= spanPerTask && tasksRemaining > 1) + { + tasks.add(maker.apply(currentSSTables, new Range<>(rangeStart, currentEnd))); + --tasksRemaining; + currentSSTables = new HashSet<>(); + rangeStart = null; + currentSpan = 0; + } + --shardsRemaining; + } + + // At this point there are as many tasks remaining as there are shards + // (this includes the case of issuing a task for the last shard when only one task remains). + + // Add any already collected sstables to the next task. + if (!currentSSTables.isEmpty()) + { + assert shardsRemaining > 0; + Pair, Range> pair = iter.next(); // shardsRemaining counts the shards so iter can't be exhausted at this point + currentSSTables.addAll(pair.left); + Token currentEnd = pair.right.right; + tasks.add(maker.apply(currentSSTables, new Range<>(rangeStart, currentEnd))); + --tasksRemaining; + --shardsRemaining; + } + assert shardsRemaining == tasksRemaining : shardsRemaining + " != " + tasksRemaining; + } + + // If we still have tasks and shards to process, produce one task for each shard. + while (iter.hasNext()) + { + Pair, Range> pair = iter.next(); // shardsRemaining counts the shards so iter can't be exhausted at this point + tasks.add(maker.apply(pair.left, pair.right)); + --tasksRemaining; + --shardsRemaining; + } + + assert tasks.size() == Math.min(maxParallelism, shards.size()) : tasks.size() + " != " + maxParallelism; + assert shardsRemaining == 0 : shardsRemaining + " != 0"; + return tasks; + } + + /// Return the number of shards that the given range of positions (start- and end-inclusive) spans. + default int coveredShardCount(PartitionPosition first, PartitionPosition last, int numShardsForDensity) + { + var boundaries = boundaries(numShardsForDensity); + boundaries.advanceTo(first.getToken()); + int firstShard = boundaries.shardIndex(); + boundaries.advanceTo(last.getToken()); + int lastShard = boundaries.shardIndex(); + return lastShard - firstShard + 1; + } + + /// Get the list of shard ranges for the given shard count. Useful for diagnostics and debugging. + default List> getShardRanges(int shardCount) + { + var boundaries = boundaries(shardCount); + var result = new ArrayList>(shardCount); + while (true) + { + result.add(boundaries.shardSpan()); + if (boundaries.shardEnd() == null) + break; + boundaries.advanceTo(boundaries.shardEnd().nextValidToken()); + } + return result; } } diff --git a/src/java/org/apache/cassandra/db/compaction/ShardManagerDiskAware.java b/src/java/org/apache/cassandra/db/compaction/ShardManagerDiskAware.java index 4f8aba283aba..afbbcc03dcbe 100644 --- a/src/java/org/apache/cassandra/db/compaction/ShardManagerDiskAware.java +++ b/src/java/org/apache/cassandra/db/compaction/ShardManagerDiskAware.java @@ -23,8 +23,8 @@ import javax.annotation.Nullable; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.SortedLocalRanges; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Token; @@ -39,14 +39,14 @@ public class ShardManagerDiskAware extends ShardManagerNoDisks private final int[] diskStartRangeIndex; private final List diskBoundaries; - public ShardManagerDiskAware(ColumnFamilyStore.VersionedLocalRanges localRanges, List diskBoundaries) + public ShardManagerDiskAware(SortedLocalRanges localRanges, List diskBoundaries) { super(localRanges); assert diskBoundaries != null && !diskBoundaries.isEmpty(); this.diskBoundaries = diskBoundaries; double position = 0; - final List ranges = localRanges; + final List ranges = localRanges.getRanges(); int diskIndex = 0; diskBoundaryPositions = new double[diskBoundaries.size()]; diskStartRangeIndex = new int[diskBoundaryPositions.length]; @@ -110,7 +110,7 @@ public class BoundaryTrackerDiskAware implements ShardTracker public BoundaryTrackerDiskAware(int countPerDisk) { this.countPerDisk = countPerDisk; - currentStart = localRanges.get(0).left(); + currentStart = localRanges.getRanges().get(0).left(); diskIndex = -1; } @@ -133,25 +133,37 @@ private Token getEndToken(double toPos) right = localRangePositions[++currentRange]; } - final Range range = localRanges.get(currentRange).range(); + final Range range = localRanges.getRanges().get(currentRange).range(); return currentStart.getPartitioner().split(range.left, range.right, (toPos - left) / (right - left)); } public Token shardStart() { + ensureInitialized(); return currentStart; } public Token shardEnd() { + ensureInitialized(); return currentEnd; } public Range shardSpan() { + ensureInitialized(); return new Range<>(currentStart, currentEnd != null ? currentEnd : currentStart.minValue()); } + private void ensureInitialized() + { + if (diskIndex < 0) + { + enterDisk(0); + setEndToken(); + } + } + public double shardSpanSize() { return shardStep; @@ -204,7 +216,7 @@ private void setEndToken() public int count() { - return countPerDisk; + return countPerDisk * diskBoundaryPositions.length; } /** @@ -231,7 +243,7 @@ public double rangeSpanned(PartitionPosition first, PartitionPosition last) public int shardIndex() { - return nextShardIndex - 1; + return diskIndex * countPerDisk + nextShardIndex - 1; } } } diff --git a/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java b/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java index 6174612a94aa..0d8d76d8e4c3 100644 --- a/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java +++ b/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java @@ -22,15 +22,15 @@ import javax.annotation.Nullable; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.SortedLocalRanges; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Token; public class ShardManagerNoDisks implements ShardManager { - final ColumnFamilyStore.VersionedLocalRanges localRanges; + final SortedLocalRanges localRanges; /** * Ending positions for the local token ranges, in covered token range; in other words, the accumulated share of @@ -39,11 +39,11 @@ public class ShardManagerNoDisks implements ShardManager */ final double[] localRangePositions; - public ShardManagerNoDisks(ColumnFamilyStore.VersionedLocalRanges localRanges) + public ShardManagerNoDisks(SortedLocalRanges localRanges) { this.localRanges = localRanges; double position = 0; - final List ranges = localRanges; + final List ranges = localRanges.getRanges(); localRangePositions = new double[ranges.size()]; for (int i = 0; i < localRangePositions.length; ++i) { @@ -53,12 +53,6 @@ public ShardManagerNoDisks(ColumnFamilyStore.VersionedLocalRanges localRanges) } } - public boolean isOutOfDate(long ringVersion) - { - return ringVersion != localRanges.ringVersion && - localRanges.ringVersion != ColumnFamilyStore.RING_VERSION_IRRELEVANT; - } - @Override public double rangeSpanned(Range tableRange) { @@ -69,7 +63,7 @@ public double rangeSpanned(Range tableRange) private double rangeSizeNonWrapping(Range tableRange) { double size = 0; - for (Splitter.WeightedRange range : localRanges) + for (Splitter.WeightedRange range : localRanges.getRanges()) { Range ix = range.range().intersectionNonWrapping(tableRange); // local and table ranges are non-wrapping if (ix == null) @@ -91,6 +85,11 @@ public double shardSetCoverage() return localSpaceCoverage(); } + public double minimumPerPartitionSpan() + { + return localSpaceCoverage() / Math.max(1, localRanges.getRealm().estimatedPartitionCountInSSTables()); + } + @Override public ShardTracker boundaries(int shardCount) { @@ -111,7 +110,7 @@ public BoundaryTracker(int count) { this.count = count; rangeStep = localSpaceCoverage() / count; - currentStart = localRanges.get(0).left(); + currentStart = localRanges.getRanges().get(0).left(); currentRange = 0; nextShardIndex = 1; if (nextShardIndex == count) @@ -130,7 +129,7 @@ private Token getEndToken(double toPos) right = localRangePositions[++currentRange]; } - final Range range = localRanges.get(currentRange).range(); + final Range range = localRanges.getRanges().get(currentRange).range(); return currentStart.getPartitioner().split(range.left, range.right, (toPos - left) / (right - left)); } diff --git a/src/java/org/apache/cassandra/db/compaction/ShardManagerReplicaAware.java b/src/java/org/apache/cassandra/db/compaction/ShardManagerReplicaAware.java new file mode 100644 index 000000000000..0bfafb0e28ef --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/ShardManagerReplicaAware.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.dht.tokenallocator.IsolatedTokenAllocator; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.TokenMetadata; + +/** + * A {@link ShardManager} implementation that aligns UCS and replica shards to limit the amount of sstables that are + * partially owned by replicas. It takes an {@link AbstractReplicationStrategy} as input and uses it to determine + * current and future replica token boundaries to use as sharding split points to ensure that for current and + * future states of the cluster, the generated sstable shard ranges will not span multiple nodes for sufficiently high + * levels of compaction. + *

    + * If more compaction requires more shards than the already allocated tokens can satisfy, use the + * {@link org.apache.cassandra.dht.tokenallocator.TokenAllocator} to allocate more tokens and then use those tokens + * as split points. This implementation relies on the fact that token allocation is deterministic after the first + * token has been selected. + */ +public class ShardManagerReplicaAware implements ShardManager +{ + private static final Logger logger = LoggerFactory.getLogger(ShardManagerReplicaAware.class); + public static final Token[] EMPTY_TOKENS = new Token[0]; + private final AbstractReplicationStrategy rs; + private final TokenMetadata tokenMetadata; + private final IPartitioner partitioner; + private final ConcurrentHashMap splitPointCache; + private final CompactionRealm realm; + + public ShardManagerReplicaAware(AbstractReplicationStrategy rs, CompactionRealm realm) + { + this.rs = rs; + // Clone the map to ensure it has a consistent view of the tokenMetadata. UCS creates a new instance of the + // ShardManagerTokenAware class when the token metadata changes. + this.tokenMetadata = rs.getTokenMetadata().cloneOnlyTokenMap(); + this.splitPointCache = new ConcurrentHashMap<>(); + this.partitioner = tokenMetadata.partitioner; + this.realm = realm; + } + + @Override + public double rangeSpanned(Range tableRange) + { + return tableRange.left.size(tableRange.right); + } + + @Override + public double localSpaceCoverage() + { + // This manager is global, so it owns the whole range. + return 1; + } + + @Override + public double shardSetCoverage() + { + // For now there are no disks defined, so this is the same as localSpaceCoverage + return 1; + } + + @Override + public double minimumPerPartitionSpan() + { + return localSpaceCoverage() / Math.max(1, realm.estimatedPartitionCountInSSTables()); + } + + @Override + public ShardTracker boundaries(int shardCount) + { + try + { + var splitPoints = splitPointCache.computeIfAbsent(shardCount, this::computeBoundaries); + return new SimpleShardTracker(splitPoints); + } + catch (Throwable t) + { + logger.error("Error creating shard boundaries", t); + throw t; + } + } + + private Token[] computeBoundaries(int shardCount) + { + logger.debug("Creating shard boundaries for {} shards", shardCount); + // Because sstables do not wrap around, we need shardCount - 1 splits. + var splitPointCount = shardCount - 1; + if (splitPointCount == 0) + return new Token[]{partitioner.getMinimumToken()}; + + // Copy array list. The current token allocation logic doesn't consider our copy of tokenMetadata, so + // modifying the sorted tokens here won't give us much benefit. + var sortedTokensList = new ArrayList<>(tokenMetadata.sortedTokens()); + if (splitPointCount > sortedTokensList.size()) + { + // Not enough tokens, allocate them. + int additionalSplits = splitPointCount - sortedTokensList.size(); + var newTokens = IsolatedTokenAllocator.allocateTokens(additionalSplits, rs); + sortedTokensList.addAll(newTokens); + sortedTokensList.sort(Token::compareTo); + } + + // Short circuit on equal. + if (sortedTokensList.size() == splitPointCount) + { + var sortedTokens = new Token[shardCount]; + sortedTokens[0] = partitioner.getMinimumToken(); + for (int i = 0; i < splitPointCount; i++) + sortedTokens[i + 1] = sortedTokensList.get(i); + return sortedTokens; + } + + var sortedTokens = sortedTokensList.toArray(EMPTY_TOKENS); + + // Get the ideal split points and then map them to their nearest neighbor. + var evenSplitPoints = computeUniformSplitPoints(splitPointCount); + var nodeAlignedSplitPoints = new Token[shardCount]; + nodeAlignedSplitPoints[0] = partitioner.getMinimumToken(); + + // UCS requires that the splitting points for a given density are also splitting points for + // all higher densities, so we pick from among the existing tokens. + int pos = 0; + for (int i = 0; i < evenSplitPoints.length; i++) + { + int min = pos; + int max = sortedTokens.length - evenSplitPoints.length + i; + Token value = evenSplitPoints[i]; + pos = Arrays.binarySearch(sortedTokens, min, max, value); + if (pos < 0) + pos = -pos - 1; + + if (pos == min) + { + // No left neighbor, so choose the right neighbor + nodeAlignedSplitPoints[i + 1] = sortedTokens[pos]; + pos++; + } + else if (pos == max) + { + // No right neighbor, so choose the left neighbor + // This also means that for all greater indexes we don't have a choice. + for (; i < evenSplitPoints.length; ++i) + nodeAlignedSplitPoints[i + 1] = sortedTokens[pos++ - 1]; + } + else + { + // Check the neighbors + Token leftNeighbor = sortedTokens[pos - 1]; + Token rightNeighbor = sortedTokens[pos]; + + // Choose the nearest neighbor. By convention, prefer left if value is midpoint, but don't + // choose the same token twice. + if (leftNeighbor.size(value) <= value.size(rightNeighbor)) + { + nodeAlignedSplitPoints[i + 1] = leftNeighbor; + // No need to bump pos because we decremented it to find the right split token. + } + else + { + nodeAlignedSplitPoints[i + 1] = rightNeighbor; + pos++; + } + } + } + + return nodeAlignedSplitPoints; + } + + + private Token[] computeUniformSplitPoints(int splitPointCount) + { + // Want the shard count here to get the right ratio. + var rangeStep = 1.0 / (splitPointCount + 1); + var tokens = new Token[splitPointCount]; + for (int i = 0; i < splitPointCount; i++) + { + // Multiply the step by the index + 1 to get the ratio to the left of the minimum token. + var ratioToLeft = rangeStep * (i + 1); + tokens[i] = partitioner.split(partitioner.getMinimumToken(), partitioner.getMaximumToken(), ratioToLeft); + } + return tokens; + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/ShardManagerTrivial.java b/src/java/org/apache/cassandra/db/compaction/ShardManagerTrivial.java index 407bff4f0d67..0aa37499c2d3 100644 --- a/src/java/org/apache/cassandra/db/compaction/ShardManagerTrivial.java +++ b/src/java/org/apache/cassandra/db/compaction/ShardManagerTrivial.java @@ -18,13 +18,15 @@ package org.apache.cassandra.db.compaction; +import java.util.Collection; +import java.util.List; import java.util.Set; +import java.util.function.BiFunction; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.io.sstable.format.SSTableReader; public class ShardManagerTrivial implements ShardManager { @@ -35,31 +37,30 @@ public ShardManagerTrivial(IPartitioner partitioner) this.partitioner = partitioner; } - public boolean isOutOfDate(long ringVersion) + @Override + public double rangeSpanned(Range tableRange) { - // We don't do any routing, always up to date - return false; + return 1; } @Override - public double rangeSpanned(Range tableRange) + public double rangeSpanned(CompactionSSTable rdr) { return 1; } @Override - public double rangeSpanned(SSTableReader rdr) + public double density(long onDiskLength, PartitionPosition min, PartitionPosition max, long approximatePartitionCount) { - return 1; + return onDiskLength; } @Override - public double calculateCombinedDensity(Set sstables) + public List splitSSTablesInShards(Collection sstables, + int numShardsForDensity, + BiFunction, Range, T> maker) { - double totalSize = 0; - for (SSTableReader sstable : sstables) - totalSize += sstable.onDiskLength(); - return totalSize; + return List.of(maker.apply(sstables, new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken()))); } @Override @@ -74,6 +75,11 @@ public double shardSetCoverage() return 1; } + public double minimumPerPartitionSpan() + { + throw new AssertionError(); // rangeSpanned is overridden and does not call this method + } + ShardTracker iterator = new ShardTracker() { @Override @@ -85,7 +91,7 @@ public Token shardStart() @Override public Token shardEnd() { - return partitioner.getMinimumToken(); + return null; } @Override @@ -131,10 +137,10 @@ public int shardIndex() } @Override - public long shardAdjustedKeyCount(Set sstables) + public long shardAdjustedKeyCount(Set sstables) { long shardAdjustedKeyCount = 0; - for (SSTableReader sstable : sstables) + for (CompactionSSTable sstable : sstables) shardAdjustedKeyCount += sstable.estimatedKeys(); return shardAdjustedKeyCount; } diff --git a/src/java/org/apache/cassandra/db/compaction/ShardTracker.java b/src/java/org/apache/cassandra/db/compaction/ShardTracker.java index 46b20638dbd4..0010bf8a7705 100644 --- a/src/java/org/apache/cassandra/db/compaction/ShardTracker.java +++ b/src/java/org/apache/cassandra/db/compaction/ShardTracker.java @@ -24,7 +24,6 @@ import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableWriter; public interface ShardTracker @@ -38,12 +37,12 @@ public interface ShardTracker double shardSpanSize(); - /** - * Advance to the given token (e.g. before writing a key). Returns true if this resulted in advancing to a new - * shard, and false otherwise. - */ + /// Advance to the given token (e.g. before writing a key). Returns true if this resulted in advancing to a new + /// shard, and false otherwise. boolean advanceTo(Token nextToken); + /// Returns the number of shards tracked by this tracker. This is not necessarily the number of shards requested + /// when [ShardManager#boundaries] was called, because this requests the per-disk number. int count(); /** @@ -54,13 +53,14 @@ public interface ShardTracker double rangeSpanned(PartitionPosition first, PartitionPosition last); + /// The index of the shard this tracker is currently on, between `0` and `count() - 1`. int shardIndex(); - default long shardAdjustedKeyCount(Set sstables) + default long shardAdjustedKeyCount(Set sstables) { // Note: computationally non-trivial; can be optimized if we save start/stop shards and size per table. long shardAdjustedKeyCount = 0; - for (SSTableReader sstable : sstables) + for (CompactionSSTable sstable : sstables) shardAdjustedKeyCount += sstable.estimatedKeys() * fractionInShard(ShardManager.coveringRange(sstable)); return shardAdjustedKeyCount; } diff --git a/src/java/org/apache/cassandra/db/compaction/SharedCompactionObserver.java b/src/java/org/apache/cassandra/db/compaction/SharedCompactionObserver.java new file mode 100644 index 000000000000..71123add4eef --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/SharedCompactionObserver.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.TimeUUID; + +/// Utility class to share a compaction observer among multiple compaction tasks and only report start and completion +/// once when the first task starts and completion when all tasks complete (successfully or not, where the passed +/// `isSuccess` state is a logical and of the subtasks'). +/// +/// Because subtasks may start in any order, we need to know the number of tasks in advance. This is done by calling +/// [#registerExpectedSubtask] once per subtask before starting any of them. +/// +/// This class assumes that all subtasks use the same progress object and the same transaction id, and will verify that +/// if assertions are enabled. +public class SharedCompactionObserver implements CompactionObserver +{ + private static final Logger logger = LoggerFactory.getLogger(SharedCompactionObserver.class); + private final AtomicInteger toReportOnComplete = new AtomicInteger(0); + private final AtomicReference onCompleteException = new AtomicReference<>(null); + private final AtomicReference inProgressReported = new AtomicReference<>(null); + + private final List compObservers; + private final TimeUUID parentId; + + public SharedCompactionObserver(TimeUUID parentId, CompactionObserver observer) + { + this(parentId, observer, null); + } + + public SharedCompactionObserver(TimeUUID parentId, CompactionObserver primary, @Nullable CompactionObserver secondary) + { + if (primary == null) + throw new IllegalArgumentException("Primary observer cannot be null"); + + this.parentId = parentId; + this.compObservers = secondary != null ? ImmutableList.of(primary, secondary) : ImmutableList.of(primary); + } + + public void registerExpectedSubtask() + { + toReportOnComplete.incrementAndGet(); + assert inProgressReported.get() == null + : "Task started before all subtasks registered for operation " + inProgressReported.get().operationId(); + } + + /// Called to disable sending unwanted messages when the attached subtasks are not going to be used. + public void disableReportingOnComplete() + { + toReportOnComplete.set(Integer.MAX_VALUE); + } + + @Override + public void onInProgress(CompactionProgress progress) + { + if (inProgressReported.compareAndSet(null, progress)) + { + Throwable err = null; + for (CompactionObserver compObserver : compObservers) + err = Throwables.perform(err, () -> compObserver.onInProgress(progress)); + + Throwables.maybeFail(err); + } + else + { + assert inProgressReported.get() == progress; // progress object must also be shared + assert progress.operationId().equals(parentId) : "progress.operationId() must match parentId"; + } + } + + @Override + public void onCompleted(TimeUUID id, Throwable err) + { + if (err != null) + onCompleteException.compareAndSet(null, err); + + final int remainingToComplete = toReportOnComplete.decrementAndGet(); + assert remainingToComplete >= 0 : "onCompleted called without corresponding registerExpectedSubtask"; + + if (remainingToComplete == 0) + { + Throwable error = null; + Throwable finalErr = onCompleteException.get(); + for (CompactionObserver compObserver : compObservers) + error = Throwables.perform(error, () -> compObserver.onCompleted(parentId, finalErr)); + + Throwables.maybeFail(error); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/SharedCompactionProgress.java b/src/java/org/apache/cassandra/db/compaction/SharedCompactionProgress.java new file mode 100644 index 000000000000..9316b29e0e43 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/SharedCompactionProgress.java @@ -0,0 +1,312 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import javax.annotation.Nullable; + +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.TimeUUID; + +/// Shared compaction progress tracker. This combines the progress tracking of multiple compaction tasks into a single +/// progress tracker, and of reporting completion of all tasks when all of them complete. +/// +/// Subtasks may start and add themselves in any order. There may also be periods of time when all started tasks have +/// completed but there are new ones to still initiate. Because of this all parameters returned by this progress may +/// increase over time, including the total sizes and sstable lists. +/// +/// To know how many subtasks to expect, this class's [#registerExpectedSubtask] method must be called once per subtask +/// before any of them start. +public class SharedCompactionProgress implements CompactionProgress +{ + private final List sources = new CopyOnWriteArrayList<>(); + private final AtomicInteger toComplete = new AtomicInteger(0); + private final AtomicLong totalSize = new AtomicLong(0); + private final AtomicLong totalCompressedSize = new AtomicLong(0); + private final AtomicLong totalUncompressedSize = new AtomicLong(0); + private final TimeUUID operationId; + private final TableOperation.Unit unit; + + public SharedCompactionProgress(TimeUUID operationId, OperationType operationType, TableOperation.Unit unit) + { + this.operationId = operationId; + // ignore operationType; TODO: remove the argument + this.unit = unit; + } + + /// Register a subtask to be expected to run. This must be called once per subtask before any of them start. + /// + /// @param taskSize The size of the task that its [CompactionProgress#total] will report. + public void registerExpectedSubtask(long taskSize, long taskCompressedSize, long taskUncompressedSize) + { + toComplete.incrementAndGet(); + totalSize.addAndGet(taskSize); + totalCompressedSize.addAndGet(taskCompressedSize); + totalUncompressedSize.addAndGet(taskUncompressedSize); + } + + public void addSubtask(CompactionProgress progress) + { + sources.add(progress); + assert sources.isEmpty() || progress.operationType() == sources.get(0).operationType(); + assert progress.unit() == unit; + } + + /// Mark a subtask as complete. Returns true if the caller is the last subtask to complete. + /// This must be called once per subtask. + /// Note that completion is determined by the number of tasks expected to run, not by the set that is currently + /// registered/running. + /// @param progress The progress of the subtask that is complete (currently unused) + public boolean completeSubtask(CompactionProgress progress) + { + return toComplete.decrementAndGet() == 0; + } + + @Nullable + @Override + public CompactionStrategy strategy() + { + if (sources.isEmpty()) + return null; + return sources.get(0).strategy(); + } + + + @Override + public Optional keyspace() + { + if (sources.isEmpty()) + return Optional.empty(); + return sources.get(0).keyspace(); + } + + @Override + public Optional table() + { + if (sources.isEmpty()) + return Optional.empty(); + return sources.get(0).table(); + } + + @Nullable + @Override + public TableMetadata metadata() + { + if (sources.isEmpty()) + return null; + return sources.get(0).metadata(); + } + + @Override + public OperationType operationType() + { + return sources.isEmpty() ? OperationType.COMPACTION : sources.get(0).operationType(); + } + + @Override + public TimeUUID operationId() + { + return operationId; + } + + @Override + public TableOperation.Unit unit() + { + return unit; + } + + @Override + public Set inSSTables() + { + Set set = new HashSet<>(); + for (CompactionProgress source : sources) + set.addAll(source.inSSTables()); + + return set; + } + + @Override + public Set outSSTables() + { + Set set = new HashSet<>(); + for (CompactionProgress source : sources) + set.addAll(source.outSSTables()); + + return set; + } + + @Override + public Set sstables() + { + Set set = new HashSet<>(); + for (CompactionProgress p : sources) + set.addAll(p.sstables()); + + return set; + } + + @Override + public long inputDiskSize() + { + return totalCompressedSize.get(); + } + + @Override + public long inputUncompressedSize() + { + return totalUncompressedSize.get(); + } + + @Override + public long adjustedInputDiskSize() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.adjustedInputDiskSize(); + + return sum; + } + + @Override + public long outputDiskSize() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.outputDiskSize(); + + return sum; + } + + @Override + public long uncompressedBytesRead() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.uncompressedBytesRead(); + + return sum; + } + + @Override + public long uncompressedBytesRead(int level) + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.uncompressedBytesRead(level); + + return sum; + } + + @Override + public long uncompressedBytesWritten() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.uncompressedBytesWritten(); + + return sum; + } + + @Override + public long partitionsRead() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.partitionsRead(); + + return sum; + } + + @Override + public long rowsRead() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.rowsRead(); + + return sum; + } + + @Override + public long completed() + { + long sum = 0L; + for (CompactionProgress source : sources) + sum += source.completed(); + + return sum; + } + + @Override + public long total() + { + return totalSize.get(); + } + + @Override + public long startTimeMillis() + { + long min = Long.MAX_VALUE; + for (CompactionProgress source : sources) + min = Math.min(min, source.startTimeMillis()); + + return min; + } + + @Override + public long[] partitionsHistogram() + { + return mergeHistograms(CompactionProgress::partitionsHistogram); + } + + @Override + public long[] rowsHistogram() + { + return mergeHistograms(CompactionProgress::rowsHistogram); + } + + private long[] mergeHistograms(Function retriever) + { + long[] merged = new long[0]; + for (CompactionProgress source : sources) + { + long[] histogram = retriever.apply(source); + if (histogram.length > merged.length) + merged = Arrays.copyOf(merged, histogram.length); + for (int i = 0; i < histogram.length; i++) + merged[i] += histogram[i]; + } + return merged; + } + + @Override + public String toString() + { + return progressToString(); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/SharedTableOperation.java b/src/java/org/apache/cassandra/db/compaction/SharedTableOperation.java new file mode 100644 index 000000000000..a8336b3403ea --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/SharedTableOperation.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.cassandra.utils.NonThrowingCloseable; + +/// A [TableOperation] tracking the progress and offering stop control of a composite operation. +/// This class is used for [UnifiedCompactionStrategy]'s parallelized compactions together with +/// [SharedCompactionProgress] and [SharedCompactionObserver]. It uses a shared progress to present an integrated view +/// of the composite operation for a [TableOperationObserver] (e.g [org.apache.cassandra.db.compaction.ActiveOperations]). +public class SharedTableOperation extends AbstractTableOperation implements TableOperation, TableOperationObserver +{ + private final Progress sharedProgress; + private NonThrowingCloseable obsCloseable; + private final List components = new CopyOnWriteArrayList<>(); + private final AtomicBoolean started = new AtomicBoolean(false); + private final AtomicInteger toClose = new AtomicInteger(0); + private final AtomicReference observer = new AtomicReference<>(null); + private volatile boolean isGlobal; + + public SharedTableOperation(Progress sharedProgress) + { + this.sharedProgress = sharedProgress; + } + + public void registerExpectedSubtask() + { + toClose.incrementAndGet(); + } + + @Override + public Progress getProgress() + { + return sharedProgress; + } + + @Override + public void stop(StopTrigger trigger) + { + super.stop(trigger); + // Stop all ongoing subtasks + for (TableOperation component : components) + component.stop(trigger); + // We will also issue a stop immediately after the start of any operation that is still to initiate in + // [onOperationStart]. + } + + @Override + public boolean isGlobal() + { + return isGlobal; + } + + public TableOperationObserver wrapObserver(TableOperationObserver observer) + { + if (!this.observer.compareAndSet(null, observer)) + assert this.observer.get() == observer : "All components must use the same observer"; + // We will register with the observer when one of the components starts. + + // Note: if the observer is Noop, we still want to wrap to complete the shared operation when all subtasks complete. + return this; + } + + @Override + public NonThrowingCloseable onOperationStart(TableOperation operation) + { + if (started.compareAndSet(false, true)) + { + obsCloseable = observer.get().onOperationStart(this); + isGlobal = operation.isGlobal(); + } + // Save the component reference to be able to stop it if needed. + components.add(operation); + + if (isStopRequested()) + operation.stop(trigger()); + return this::closeOne; + } + + private void closeOne() + { + final int stillToClose = toClose.decrementAndGet(); + if (stillToClose == 0 && obsCloseable != null) + obsCloseable.close(); + assert stillToClose >= 0 : "Closed more than expected"; + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/SimpleShardTracker.java b/src/java/org/apache/cassandra/db/compaction/SimpleShardTracker.java new file mode 100644 index 000000000000..4833d98b9ac7 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/SimpleShardTracker.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.Set; +import javax.annotation.Nullable; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.format.SSTableWriter; + +/** + * A shard tracker that uses the provided tokens as a complete list of split points. The first token is typically + * the minimum token. + */ +class SimpleShardTracker implements ShardTracker +{ + private final Token[] sortedTokens; + private int index; + private Token currentEnd; + + SimpleShardTracker(Token[] sortedTokens) + { + assert sortedTokens.length > 0; + assert sortedTokens[0].isMinimum(); + this.sortedTokens = sortedTokens; + this.index = 0; + this.currentEnd = calculateCurrentEnd(); + } + + @Override + public Token shardStart() + { + return sortedTokens[index]; + } + + @Nullable + @Override + public Token shardEnd() + { + return currentEnd; + } + + @Override + public Range shardSpan() + { + return new Range<>(shardStart(), end()); + } + + @Override + public double shardSpanSize() + { + // No weight applied because weighting is a local range property. + return shardStart().size(end()); + } + + /** + * Non-nullable implementation of {@link ShardTracker#shardEnd()}. Returns the first token if the current shard + * is the last shard. + * @return the end token of the current shard + */ + private Token end() + { + Token end = shardEnd(); + return end != null ? end : sortedTokens[0]; + } + + private Token calculateCurrentEnd() + { + return index + 1 < sortedTokens.length ? sortedTokens[index + 1] : null; + } + + @Override + public boolean advanceTo(Token nextToken) + { + if (currentEnd == null || nextToken.compareTo(currentEnd) <= 0) + return false; + do + { + index++; + currentEnd = calculateCurrentEnd(); + if (currentEnd == null) + break; + } + while (nextToken.compareTo(currentEnd) > 0); + return true; + } + + @Override + public int count() + { + return sortedTokens.length; + } + + @Override + public double fractionInShard(Range targetSpan) + { + Range shardSpan = shardSpan(); + Range covered = targetSpan.intersectionNonWrapping(shardSpan); + if (covered == null) + return 0; + if (covered == targetSpan) + return 1; + double inShardSize = covered.left.size(covered.right); + double totalSize = targetSpan.left.size(targetSpan.right); + return inShardSize / totalSize; + } + + @Override + public double rangeSpanned(PartitionPosition first, PartitionPosition last) + { + // Ignore local range owndership for initial implementation. + return first.getToken().size(last.getToken()); + } + + @Override + public int shardIndex() + { + return index; + } + + @Override + public long shardAdjustedKeyCount(Set sstables) + { + // Not sure if this needs a custom implementation yet + return ShardTracker.super.shardAdjustedKeyCount(sstables); + } + + @Override + public void applyTokenSpaceCoverage(SSTableWriter writer) + { + // Not sure if this needs a custom implementation yet + ShardTracker.super.applyTokenSpaceCoverage(writer); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/SingleSSTableLCSTask.java b/src/java/org/apache/cassandra/db/compaction/SingleSSTableLCSTask.java index 1f73c4cd30de..34cd41488397 100644 --- a/src/java/org/apache/cassandra/db/compaction/SingleSSTableLCSTask.java +++ b/src/java/org/apache/cassandra/db/compaction/SingleSSTableLCSTask.java @@ -18,15 +18,10 @@ package org.apache.cassandra.db.compaction; -import java.util.Set; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Directories; -import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -42,25 +37,20 @@ public class SingleSSTableLCSTask extends AbstractCompactionTask private static final Logger logger = LoggerFactory.getLogger(SingleSSTableLCSTask.class); private final int level; + private final LeveledCompactionStrategy strategy; - public SingleSSTableLCSTask(ColumnFamilyStore cfs, LifecycleTransaction txn, int level) + public SingleSSTableLCSTask(LeveledCompactionStrategy strategy, ILifecycleTransaction txn, int level) { - super(cfs, txn); + super(strategy.realm, txn); + this.strategy = strategy; assert txn.originals().size() == 1; this.level = level; + addObserver(strategy); } - @Override - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables) - { - throw new UnsupportedOperationException("This method should never be called on SingleSSTableLCSTask"); - } - - @Override - protected int executeInternal(ActiveCompactionsTracker activeCompactions) + int getLevel() { - run(); - return 1; + return level; } @Override @@ -84,7 +74,7 @@ protected void runMayThrow() transaction.abort(); throw new CorruptSSTableException(t, sstable.descriptor.fileFor(Components.DATA)); } - cfs.getTracker().notifySSTableMetadataChanged(sstable, metadataBefore); + strategy.metadataChanged(metadataBefore, sstable); } finishTransaction(sstable); } @@ -97,4 +87,10 @@ private void finishTransaction(SSTableReader sstable) transaction.prepareToCommit(); transaction.commit(); } + + @Override + public long getSpaceOverhead() + { + return 0; // This is just metadata modification, no overhead. + } } diff --git a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStatistics.java b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStatistics.java new file mode 100644 index 000000000000..e5592ed48c9b --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStatistics.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The statistics for size tiered compaction. + *

    + * Implements serializable to allow structured info to be returned via JMX. + */ +public class SizeTieredCompactionStatistics extends TieredCompactionStatistics +{ + /** The average sstable size in this tier */ + private final long avgSSTableSize; + + SizeTieredCompactionStatistics(CompactionAggregateStatistics base, long avgSSTableSize) + { + super(base); + this.avgSSTableSize = avgSSTableSize; + } + + /** The average sstable size in this tier */ + public long avgSSTableSize() + { + return avgSSTableSize; + } + + @Override + @JsonProperty("Bucket") + protected String tierValue() + { + return toString(avgSSTableSize); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java index 74a96ca211ab..1d7dba6b6ac6 100644 --- a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java @@ -17,16 +17,27 @@ */ package org.apache.cassandra.db.compaction; -import java.util.*; -import java.util.Map.Entry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; import org.apache.cassandra.db.compaction.writers.SplittingSizeTieredCompactionWriter; @@ -36,272 +47,301 @@ import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.utils.Pair; -import static com.google.common.collect.Iterables.filter; - -public class SizeTieredCompactionStrategy extends AbstractCompactionStrategy +public class SizeTieredCompactionStrategy extends LegacyAbstractCompactionStrategy.WithAggregates { private static final Logger logger = LoggerFactory.getLogger(SizeTieredCompactionStrategy.class); - private static final Comparator,Double>> bucketsByHotnessComparator = new Comparator, Double>>() - { - public int compare(Pair, Double> o1, Pair, Double> o2) - { - int comparison = Double.compare(o1.right, o2.right); - if (comparison != 0) - return comparison; - - // break ties by compacting the smallest sstables first (this will probably only happen for - // system tables and new/unread sstables) - return Long.compare(avgSize(o1.left), avgSize(o2.left)); - } - - private long avgSize(List sstables) - { - long n = 0; - for (SSTableReader sstable : sstables) - n += sstable.bytesOnDisk(); - return n / sstables.size(); - } - }; + /** + * Compare {@link CompactionPick} instances by hotness first and in case of a tie by sstable size by + * selecting the largest first (a tie would happen for system tables and new/unread sstables). + *

    + * Note that in previous version there is a comment saying "break ties by compacting the smallest sstables first" + * but the code was doing the opposite. I preserved the behavior and fixed the comment. + */ + private static final Comparator comparePicksByHotness = Comparator.comparing(CompactionPick::hotness) + .thenComparing(CompactionPick::avgSizeInBytes); protected SizeTieredCompactionStrategyOptions sizeTieredOptions; - protected volatile int estimatedRemainingTasks; @VisibleForTesting - protected final Set sstables = new HashSet<>(); + protected final Set sstables = new HashSet<>(); - public SizeTieredCompactionStrategy(ColumnFamilyStore cfs, Map options) + public SizeTieredCompactionStrategy(CompactionStrategyFactory factory, Map options) { - super(cfs, options); - this.estimatedRemainingTasks = 0; + super(factory, options); this.sizeTieredOptions = new SizeTieredCompactionStrategyOptions(options); } - private synchronized List getNextBackgroundSSTables(final long gcBefore) + @Override + protected synchronized CompactionAggregate getNextBackgroundAggregate(final long gcBefore) { // make local copies so they can't be changed out from under us mid-method - int minThreshold = cfs.getMinimumCompactionThreshold(); - int maxThreshold = cfs.getMaximumCompactionThreshold(); + int minThreshold = realm.getMinimumCompactionThreshold(); + int maxThreshold = realm.getMaximumCompactionThreshold(); + + List candidates = new ArrayList<>(); + synchronized (sstables) + { + Iterables.addAll(candidates, nonSuspectAndNotIn(sstables, realm.getCompactingSSTables())); + } - Iterable candidates = filterSuspectSSTables(filter(cfs.getUncompactingSSTables(), sstables::contains)); + SizeTieredBuckets sizeTieredBuckets = new SizeTieredBuckets(candidates, sizeTieredOptions, minThreshold, maxThreshold); + sizeTieredBuckets.aggregate(); - List> buckets = getBuckets(createSSTableAndLengthPairs(candidates), sizeTieredOptions.bucketHigh, sizeTieredOptions.bucketLow, sizeTieredOptions.minSSTableSize); - logger.trace("Compaction buckets are {}", buckets); - estimatedRemainingTasks = getEstimatedCompactionsByTasks(cfs, buckets); - cfs.getCompactionStrategyManager().compactionLogger.pending(this, estimatedRemainingTasks); - List mostInteresting = mostInterestingBucket(buckets, minThreshold, maxThreshold); - if (!mostInteresting.isEmpty()) - return mostInteresting; + backgroundCompactions.setPending(this, sizeTieredBuckets.getAggregates()); + + CompactionAggregate ret = sizeTieredBuckets.getAggregates().isEmpty() ? null : sizeTieredBuckets.getAggregates().get(0); // if there is no sstable to compact in standard way, try compacting single sstable whose droppable tombstone // ratio is greater than threshold. - List sstablesWithTombstones = new ArrayList<>(); - for (SSTableReader sstable : candidates) - { - if (worthDroppingTombstones(sstable, gcBefore)) - sstablesWithTombstones.add(sstable); - } - if (sstablesWithTombstones.isEmpty()) - return Collections.emptyList(); + if (ret == null || ret.isEmpty()) + ret = makeTombstoneCompaction(gcBefore, candidates, list -> Collections.max(list, CompactionSSTable.sizeComparator)); - return Collections.singletonList(Collections.max(sstablesWithTombstones, SSTableReader.sizeComparator)); + return ret; } - /** - * @param buckets list of buckets from which to return the most interesting, where "interesting" is the total hotness for reads - * @param minThreshold minimum number of sstables in a bucket to qualify as interesting - * @param maxThreshold maximum number of sstables to compact at once (the returned bucket will be trimmed down to this) - * @return a bucket (list) of sstables to compact + * This class contains the logic for {@link SizeTieredCompactionStrategy}: + * + * - sorts the sstables by length on disk + * - it sorts the candidates into buckets + * - takes a snapshot of the sstable hotness + * - it organizes the buckets into a list of {@link CompactionAggregate}, an aggregate per bucket. + * An aggregate will have a list of compaction picks, each pick is a list of sstables below the max threshold, + * sorted by hotness. + * - the aggregates are sorted by comparing the total hotness of the first pick of each aggregate + * - the aggregate with the hottest first pick will have its first pick submitted for compaction. */ - public static List mostInterestingBucket(List> buckets, int minThreshold, int maxThreshold) + @NotThreadSafe + final static class SizeTieredBuckets { - // skip buckets containing less than minThreshold sstables, and limit other buckets to maxThreshold sstables - final List, Double>> prunedBucketsAndHotness = new ArrayList<>(buckets.size()); - for (List bucket : buckets) + private final SizeTieredCompactionStrategyOptions options; + private final List tablesBySize; + private final Map> buckets; + private final Map hotnessSnapshot; + private final int minThreshold; + private final int maxThreshold; + + /** + * This is the list of compactions order by most interesting first + */ + private List aggregates; + + /** + * @param candidates list sstables that are not yet compacting + * @param options the options for size tiered compaction strategy + * @param minThreshold minimum number of sstables in a bucket to qualify as interesting + * @param maxThreshold maximum number of sstables to compact at once (the returned bucket will be trimmed down to this) + */ + SizeTieredBuckets(Iterable candidates, + SizeTieredCompactionStrategyOptions options, + int minThreshold, + int maxThreshold) { - Pair, Double> bucketAndHotness = trimToThresholdWithHotness(bucket, maxThreshold); - if (bucketAndHotness != null && bucketAndHotness.left.size() >= minThreshold) - prunedBucketsAndHotness.add(bucketAndHotness); + this.options = options; + this.tablesBySize = new ArrayList<>(); + Iterables.addAll(this.tablesBySize, candidates); + this.tablesBySize.sort(CompactionSSTable.sizeComparator); + this.buckets = getBuckets(tablesBySize, options); + this.hotnessSnapshot = getHotnessSnapshot(buckets.values()); + this.minThreshold = minThreshold; + this.maxThreshold = maxThreshold; + + this.aggregates = new ArrayList<>(buckets.size()); + + if (logger.isTraceEnabled()) + logger.trace("Compaction buckets are {}", buckets); } - if (prunedBucketsAndHotness.isEmpty()) - return Collections.emptyList(); - Pair, Double> hottest = Collections.max(prunedBucketsAndHotness, bucketsByHotnessComparator); - return hottest.left; - } - - /** - * Returns a (bucket, hotness) pair or null if there were not enough sstables in the bucket to meet minThreshold. - * If there are more than maxThreshold sstables, the coldest sstables will be trimmed to meet the threshold. - **/ - @VisibleForTesting - static Pair, Double> trimToThresholdWithHotness(List bucket, int maxThreshold) - { - // Sort by sstable hotness (descending). We first build a map because the hotness may change during the sort. - final Map hotnessSnapshot = getHotnessMap(bucket); - Collections.sort(bucket, new Comparator() + /** + * Group sstables of similar on disk size into buckets. + * The given set must be sorted using CompactionSSTable.sizeComparator + */ + private static Map> getBuckets(List sstables, SizeTieredCompactionStrategyOptions options) { - public int compare(SSTableReader o1, SSTableReader o2) + if (sstables.isEmpty()) + return Collections.EMPTY_MAP; + + Map> buckets = new HashMap<>(); + + long currentAverageSize = 0; + List currentBucket = new ArrayList<>(); + + for (CompactionSSTable sstable: sstables) { - return -1 * Double.compare(hotnessSnapshot.get(o1), hotnessSnapshot.get(o2)); + long size = sstable.onDiskLength(); + assert size >= currentAverageSize; + + if (size >= currentAverageSize * options.bucketHigh + && size >= options.minSSTableSize + && currentAverageSize > 0) // false for first table only + { + // Switch to new bucket + buckets.put(currentAverageSize, currentBucket); + currentBucket = new ArrayList<>(); + } + // TODO: Is it okay that the bucket max can grow unboundedly? + + currentAverageSize = (currentAverageSize * currentBucket.size() + size) / (currentBucket.size() + 1); + currentBucket.add(sstable); } - }); - // and then trim the coldest sstables off the end to meet the maxThreshold - List prunedBucket = bucket.subList(0, Math.min(bucket.size(), maxThreshold)); + buckets.put(currentAverageSize, currentBucket); + return buckets; + } - // bucket hotness is the sum of the hotness of all sstable members - double bucketHotness = 0.0; - for (SSTableReader sstr : prunedBucket) - bucketHotness += hotness(sstr); + /** + * For each bucket with at least minThreshold sstables: + *

    + * - sort the sstables by hotness + * - divide the bucket into max threshold sstables and add it to a temporary list of candidates along with the total hotness of the bucket section + *

    + * Then select the candidate with the max hotness and the most interesting bucket and put the remaining candidates in the pending list. + * + * @return the parent object {@link SizeTieredBuckets} + */ + SizeTieredBuckets aggregate() + { + if (!aggregates.isEmpty()) + return this; // already called - return Pair.create(prunedBucket, bucketHotness); - } + List aggregatesWithoutCompactions = new ArrayList<>(buckets.size()); + List aggregatesWithCompactions = new ArrayList<>(buckets.size()); - private static Map getHotnessMap(Collection sstables) - { - Map hotness = new HashMap<>(sstables.size()); - for (SSTableReader sstable : sstables) - hotness.put(sstable, hotness(sstable)); - return hotness; - } + for (Map.Entry> entry : buckets.entrySet()) + { + long avgSizeBytes = entry.getKey(); + long minSizeBytes = (long) (avgSizeBytes * options.bucketLow); + long maxSizeBytes = (long) (avgSizeBytes * options.bucketHigh); - /** - * Returns the reads per second per key for this sstable, or 0.0 if the sstable has no read meter - */ - private static double hotness(SSTableReader sstr) - { - // system tables don't have read meters, just use 0.0 for the hotness - return sstr.getReadMeter() == null ? 0.0 : sstr.getReadMeter().twoHourRate() / sstr.estimatedKeys(); - } + List bucket = entry.getValue(); + double hotness = totHotness(bucket, hotnessSnapshot); + + if (bucket.size() < minThreshold) + { + if (logger.isTraceEnabled()) + logger.trace("Aggregate with {} avg bytes for {} files not considered for compaction: {}", avgSizeBytes, bucket.size(), bucket); + + aggregatesWithoutCompactions.add(CompactionAggregate.createSizeTiered(bucket, + CompactionPick.EMPTY, + ImmutableList.of(), + hotness, + avgSizeBytes, + minSizeBytes, + maxSizeBytes)); + + continue; + } + + // sort the bucket by hotness + Collections.sort(bucket, (o1, o2) -> -1 * Double.compare(hotnessSnapshot.get(o1), hotnessSnapshot.get(o2))); + + // now divide the candidates into a list of picks, each pick with at most max threshold sstables + int i = 0; + CompactionPick selected = null; + List pending = new ArrayList<>(); - public AbstractCompactionTask getNextBackgroundTask(long gcBefore) - { - List previousCandidate = null; - while (true) - { - List hottestBucket = getNextBackgroundSSTables(gcBefore); - if (hottestBucket.isEmpty()) - return null; + while ((bucket.size() - i) >= minThreshold) + { + List sstables = bucket.subList(i, i + Math.min(bucket.size() - i, maxThreshold)); + if (selected == null) + selected = CompactionPick.create(avgSizeBytes, sstables, totHotness(sstables, hotnessSnapshot)); + else + pending.add(CompactionPick.create(avgSizeBytes, sstables, totHotness(sstables, hotnessSnapshot))); + + i += sstables.size(); + } + + if (logger.isTraceEnabled()) + logger.trace("Aggregate with {} avg bytes for {} files considered for compaction: {}", avgSizeBytes, bucket.size(), bucket); + + // Finally create the new aggregate with the new pending compactions and those already compacting and not yet completed + aggregatesWithCompactions.add(CompactionAggregate.createSizeTiered(bucket, selected, pending, hotness, avgSizeBytes, minSizeBytes, maxSizeBytes)); + } - // Already tried acquiring references without success. It means there is a race with - // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager - if (hottestBucket.equals(previousCandidate)) + // This sorts the aggregates based on the hotness of their selected pick so that the aggregate with the hottest selected pick + // be first in the list and get submitted + if (!aggregatesWithCompactions.isEmpty()) { - logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," + - "unless it happens frequently, in which case it must be reported. Will retry later.", - hottestBucket); - return null; + Collections.sort(aggregatesWithCompactions, (a1, a2) -> comparePicksByHotness.compare(a2.getSelected(), a1.getSelected())); + + if (logger.isTraceEnabled()) + logger.trace("Found compaction for aggregate {}", aggregatesWithCompactions.get(0)); + } + else + { + if (logger.isTraceEnabled()) + logger.trace("No compactions found"); } - LifecycleTransaction transaction = cfs.getTracker().tryModify(hottestBucket, OperationType.COMPACTION); - if (transaction != null) - return new CompactionTask(cfs, transaction, gcBefore); - previousCandidate = hottestBucket; + // publish the results + this.aggregates.addAll(aggregatesWithCompactions); // those with compactions first, because the first one will be the one submitted + this.aggregates.addAll(aggregatesWithoutCompactions); // then add those empty + return this; } - } - public synchronized Collection getMaximalTask(final long gcBefore, boolean splitOutput) - { - Iterable filteredSSTables = filterSuspectSSTables(sstables); - if (Iterables.isEmpty(filteredSSTables)) - return null; - LifecycleTransaction txn = cfs.getTracker().tryModify(filteredSSTables, OperationType.COMPACTION); - if (txn == null) - return null; - if (splitOutput) - return Arrays.asList(new SplittingCompactionTask(cfs, txn, gcBefore)); - return Arrays.asList(new CompactionTask(cfs, txn, gcBefore)); - } + /** + * For diagnostics only. Returns the sorted tables paired with their on-disk length. + */ + public Collection> pairs() + { + return Collections2.transform(tablesBySize, (CompactionSSTable table) -> Pair.create(table, table.onDiskLength())); + } - public AbstractCompactionTask getUserDefinedTask(Collection sstables, final long gcBefore) - { - assert !sstables.isEmpty(); // checked for by CM.submitUserDefined + public List> buckets() + { + return new ArrayList<>(buckets.values()); + } - LifecycleTransaction transaction = cfs.getTracker().tryModify(sstables, OperationType.COMPACTION); - if (transaction == null) + public List getAggregates() { - logger.trace("Unable to mark {} for compaction; probably a background compaction got to it first. You can disable background compactions temporarily if this is a problem", sstables); - return null; + return aggregates; } - return new CompactionTask(cfs, transaction, gcBefore).setUserDefined(true); + public List getCompactions() + { + return aggregates.stream().flatMap(aggr -> aggr.getActive().stream()).collect(Collectors.toList()); + } } - public int getEstimatedRemainingTasks() + /** + * @return a snapshot mapping sstables to their current read hotness. + */ + @VisibleForTesting + static Map getHotnessSnapshot(Collection> buckets) { - return estimatedRemainingTasks; - } + Map ret = new HashMap<>(); - public static List> createSSTableAndLengthPairs(Iterable sstables) - { - List> sstableLengthPairs = new ArrayList<>(Iterables.size(sstables)); - for(SSTableReader sstable : sstables) - sstableLengthPairs.add(Pair.create(sstable, sstable.onDiskLength())); - return sstableLengthPairs; + for (List sstables: buckets) + { + for (CompactionSSTable sstable : sstables) + ret.put(sstable, sstable.hotness()); + } + + return ret; } - /* - * Group files of similar size into buckets. + /** + * @return the sum of the hotness of all the sstables */ - public static List> getBuckets(Collection> files, double bucketHigh, double bucketLow, long minSSTableSize) + private static double totHotness(Iterable sstables, @Nullable final Map hotnessSnapshot) { - // Sort the list in order to get deterministic results during the grouping below - List> sortedFiles = new ArrayList>(files); - Collections.sort(sortedFiles, new Comparator>() - { - public int compare(Pair p1, Pair p2) - { - return p1.right.compareTo(p2.right); - } - }); - - Map> buckets = new HashMap>(); - - outer: - for (Pair pair: sortedFiles) + double hotness = 0.0; + for (CompactionSSTable sstable : sstables) { - long size = pair.right; - - // look for a bucket containing similar-sized files: - // group in the same bucket if it's w/in 50% of the average for this bucket, - // or this file and the bucket are all considered "small" (less than `minSSTableSize`) - for (Entry> entry : buckets.entrySet()) - { - List bucket = entry.getValue(); - long oldAverageSize = entry.getKey(); - if ((size > (oldAverageSize * bucketLow) && size < (oldAverageSize * bucketHigh)) - || (size < minSSTableSize && oldAverageSize < minSSTableSize)) - { - // remove and re-add under new new average size - buckets.remove(oldAverageSize); - long totalSize = bucket.size() * oldAverageSize; - long newAverageSize = (totalSize + size) / (bucket.size() + 1); - bucket.add(pair.left); - buckets.put(newAverageSize, bucket); - continue outer; - } - } - - // no similar bucket found; put it in a new one - ArrayList bucket = new ArrayList(); - bucket.add(pair.left); - buckets.put(size, bucket); + double h = hotnessSnapshot == null ? 0.0 : hotnessSnapshot.getOrDefault(sstable, 0.0); + hotness += h == 0.0 ? sstable.hotness() : h; } - return new ArrayList>(buckets.values()); + return hotness; } - public static int getEstimatedCompactionsByTasks(ColumnFamilyStore cfs, List> tasks) + @Override + protected AbstractCompactionTask createCompactionTask(final long gcBefore, LifecycleTransaction txn, boolean isMaximal, boolean splitOutput) { - int n = 0; - for (List bucket : tasks) - { - if (bucket.size() >= cfs.getMinimumCompactionThreshold()) - n += Math.ceil((double)bucket.size() / cfs.getMaximumCompactionThreshold()); - } - return n; + return isMaximal && splitOutput + ? new SplittingCompactionTask(realm, txn, gcBefore, this) + : new CompactionTask(realm, txn, gcBefore, false, this); } public long getMaxSSTableBytes() @@ -311,7 +351,7 @@ public long getMaxSSTableBytes() public static Map validateOptions(Map options) throws ConfigurationException { - Map uncheckedOptions = AbstractCompactionStrategy.validateOptions(options); + Map uncheckedOptions = CompactionStrategyOptions.validateOptions(options); uncheckedOptions = SizeTieredCompactionStrategyOptions.validateOptions(options, uncheckedOptions); uncheckedOptions.remove(CompactionParams.Option.MIN_THRESHOLD.toString()); @@ -321,44 +361,69 @@ public static Map validateOptions(Map options) t } @Override - public synchronized void addSSTable(SSTableReader added) + public void replaceSSTables(Collection removed, Collection added) + { + synchronized (sstables) + { + for (CompactionSSTable remove : removed) + sstables.remove(remove); + sstables.addAll(added); + } + } + + @Override + public void addSSTable(CompactionSSTable added) { - sstables.add(added); + synchronized (sstables) + { + sstables.add(added); + } } @Override - public synchronized void removeSSTable(SSTableReader sstable) + void removeDeadSSTables() { - sstables.remove(sstable); + removeDeadSSTables(sstables); } @Override - protected synchronized Set getSSTables() + public void removeSSTable(CompactionSSTable sstable) { - return ImmutableSet.copyOf(sstables); + synchronized (sstables) + { + sstables.remove(sstable); + } + } + + @Override + public Set getSSTables() + { + synchronized (sstables) + { + return ImmutableSet.copyOf(sstables); + } } public String toString() { return String.format("SizeTieredCompactionStrategy[%s/%s]", - cfs.getMinimumCompactionThreshold(), - cfs.getMaximumCompactionThreshold()); + realm.getMinimumCompactionThreshold(), + realm.getMaximumCompactionThreshold()); } private static class SplittingCompactionTask extends CompactionTask { - public SplittingCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, long gcBefore) + public SplittingCompactionTask(CompactionRealm realm, LifecycleTransaction txn, long gcBefore, CompactionStrategy strategy) { - super(cfs, txn, gcBefore); + super(realm, txn, gcBefore, false, strategy); } @Override - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, + public CompactionAwareWriter getCompactionAwareWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, Set nonExpiredSSTables) { - return new SplittingSizeTieredCompactionWriter(cfs, directories, txn, nonExpiredSSTables); + return new SplittingSizeTieredCompactionWriter(realm, directories, transaction, nonExpiredSSTables); } } } diff --git a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java index eb1d8f97afe2..84179d4570d5 100644 --- a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java +++ b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java @@ -23,12 +23,12 @@ public final class SizeTieredCompactionStrategyOptions { - protected static final long DEFAULT_MIN_SSTABLE_SIZE = 50L * 1024L * 1024L; - protected static final double DEFAULT_BUCKET_LOW = 0.5; - protected static final double DEFAULT_BUCKET_HIGH = 1.5; - protected static final String MIN_SSTABLE_SIZE_KEY = "min_sstable_size"; - protected static final String BUCKET_LOW_KEY = "bucket_low"; - protected static final String BUCKET_HIGH_KEY = "bucket_high"; + static final long DEFAULT_MIN_SSTABLE_SIZE = 50L * 1024L * 1024L; + static final double DEFAULT_BUCKET_LOW = 0.5; + static final double DEFAULT_BUCKET_HIGH = 1.5; + static final String MIN_SSTABLE_SIZE_KEY = "min_sstable_size"; + static final String BUCKET_LOW_KEY = "bucket_low"; + static final String BUCKET_HIGH_KEY = "bucket_high"; protected long minSSTableSize; protected double bucketLow; @@ -46,9 +46,14 @@ public SizeTieredCompactionStrategyOptions(Map options) public SizeTieredCompactionStrategyOptions() { - minSSTableSize = DEFAULT_MIN_SSTABLE_SIZE; - bucketLow = DEFAULT_BUCKET_LOW; - bucketHigh = DEFAULT_BUCKET_HIGH; + this(DEFAULT_MIN_SSTABLE_SIZE, DEFAULT_BUCKET_LOW, DEFAULT_BUCKET_HIGH); + } + + SizeTieredCompactionStrategyOptions(long minSSTableSize, double bucketLow, double bucketHigh) + { + this.minSSTableSize = minSSTableSize; + this.bucketLow = bucketLow; + this.bucketHigh = bucketHigh; } private static double parseDouble(Map options, String key, double defaultValue) throws ConfigurationException diff --git a/src/java/org/apache/cassandra/db/compaction/TableOperation.java b/src/java/org/apache/cassandra/db/compaction/TableOperation.java new file mode 100644 index 000000000000..3b7f03bd853c --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/TableOperation.java @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import javax.annotation.Nullable; + +import com.google.common.base.Joiner; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.Shared; +import org.apache.cassandra.utils.TimeUUID; + +/** + * This is a table operation that must be able to report the operation progress and to + * interrupt the operation when requested. + *

    + * Any operation defined by {@link OperationType} is normally implementing this interface, + * for example index building, view building, cache saving, anti-compaction, compaction, + * scrubbing, verifying, tombstone collection and others. + *

    + * These operations have in common that they run on the compaction executor and used to be + * known as "compaction". + * */ +public interface TableOperation +{ + /** + * @return the progress of the operation, see {@link Progress}. + */ + TableOperation.Progress getProgress(); + + /** + * Interrupt the current operation if possible. + * + * @param trigger cause of compaction interruption + */ + void stop(StopTrigger trigger); + + /** + * @return true if the operation has been requested to be interrupted. + */ + boolean isStopRequested(); + + default void throwIfStopRequested() + { + if (isStopRequested()) + throw new CompactionInterruptedException(getProgress(), trigger()); + } + + /** + * Return true if the predicate for the given sstables holds, or if the operation + * does not consider any sstables, in which case it will always return true (the + * default behaviour). + *

    + * + * @param predicate the predicate to be applied to the operation sstables + * + * @return true by default, see overrides for different behaviors + */ + boolean shouldStop(Predicate predicate); + + /** + * @return cause of compaction interruption. + */ + StopTrigger trigger(); + + /** + * if this compaction involves several/all tables we can safely check globalCompactionsPaused + * in isStopRequested() below + */ + boolean isGlobal(); + + /** + * The unit for the {@link Progress} report. + */ + enum Unit + { + BYTES("bytes"), RANGES("token range parts"), KEYS("keys"); + + private final String name; + + Unit(String name) + { + this.name = name; + } + + @Override + public String toString() + { + return this.name; + } + + public static boolean isFileSize(String unit) + { + return BYTES.toString().equals(unit); + } + } + + @Shared + enum StopTrigger + { + NONE("Unknown reason", false), + TRUNCATE("Truncated table", true), + DROP_TABLE("Dropped table", true), + INVALIDATE_INDEX("Index invalidation", true), + SHUTDOWN("Shutdown", true), + USER_STOP("User request", true), + COMPACTION("Compaction", true), + CLEANUP("Cleanup", true), + ANTICOMPACTION("Anticompaction after repair", true), + INDEX_BUILD("Secondary index build", true), + SCRUB("Scrub", true), + VERIFY("Verify", true), + RELOCATE("Relocation", true), + GARBAGE_COLLECT("Garbage collection", true), + UPGRADE_SSTABLES("SStable upgrade", true), + UNIT_TESTS("Unit tests", true); + + private final String name; + private final boolean isFinal; + + StopTrigger(String name, boolean isFinal) + { + this.name = name; + this.isFinal = isFinal; + } + + // A stop trigger marked as final should not be overwritten. So a table operation that is + // marked with a final stop trigger cannot have its stop trigger changed to another value. + public boolean isFinal() + { + return isFinal; + } + + @Override + public String toString() + { + return name; + } + } + + /** + * The progress of a table operation. + */ + interface Progress + { + String ID = "id"; + String KEYSPACE = "keyspace"; + String COLUMNFAMILY = "columnfamily"; + String COMPLETED = "completed"; + String TOTAL = "total"; + String OPERATION_TYPE = "operationType"; + String UNIT = "unit"; + String OPERATION_ID = "operationId"; + String SSTABLES = "sstables"; + String TARGET_DIRECTORY = "targetDirectory"; + + /** + * @return the keyspace name, if the metadata is not null. + */ + Optional keyspace(); + + /** + * @return the table name, if the metadata is not null. + */ + Optional table(); + + /** + * @return the table metadata, this may be null if the operation has no metadata. + */ + @Nullable TableMetadata metadata(); + + /** + * @return the number of units completed, see {@link this#unit()}. + */ + long completed(); + + /** + * @return the total number of units that must be processed by the operation, see {@link this#unit()}. + */ + long total(); + + /** + * @return the type of operation, see {@link OperationType}. + */ + OperationType operationType(); + + /** + * @return a unique identifier for this operation. + */ + TimeUUID operationId(); + + /** + * @return the unit to be used for {@link this#completed()} and {@link this#total()}, see {@link Unit}. + */ + Unit unit(); + + /** + * @return a set of SSTables participating in this operation + */ + Set sstables(); + + default String targetDirectory() + { + return ""; + } + + /** + * Note that this estimate is based on the amount of data we have left to read - it assumes input + * size == output size for a compaction, which is not really true, but should most often provide a worst case + * remaining write size. + */ + default long estimatedRemainingWriteBytes() + { + if (unit() == Unit.BYTES && operationType().writesData) + return total() - completed(); + return 0; + } + + /** + * Get the directories this compaction could possibly write to. + * + * @return the directories that we might write to, or empty list if we don't know the metadata + * (like for index summary redistribution), or null if we don't have any disk boundaries + */ + default List getTargetDirectories() + { + if (metadata() != null && !metadata().isIndex()) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(metadata().id); + if (cfs != null) + return cfs.getDirectoriesForFiles(sstables()); + } + return Collections.emptyList(); + } + + default String progressToString() + { + StringBuilder buff = new StringBuilder(); + buff.append(String.format("%s(%s, %s / %s %s)", operationType(), operationId(), completed(), total(), unit())); + TableMetadata metadata = metadata(); + if (metadata != null) + { + buff.append(String.format("@%s(%s, %s)", metadata.id, metadata.keyspace, metadata.name)); + } + return buff.toString(); + } + + default Map asMap() + { + Map ret = new HashMap<>(8); + TableMetadata metadata = metadata(); + ret.put(ID, metadata != null ? metadata.id.toString() : ""); + ret.put(KEYSPACE, keyspace().orElse(null)); + ret.put(COLUMNFAMILY, table().orElse(null)); + ret.put(COMPLETED, Long.toString(completed())); + ret.put(TOTAL, Long.toString(total())); + ret.put(OPERATION_TYPE, operationType().toString()); + ret.put(UNIT, unit().toString()); + ret.put(OPERATION_ID, operationId() == null ? "" : operationId().toString()); + ret.put(SSTABLES, Joiner.on(',').join(sstables())); + ret.put(TARGET_DIRECTORY, targetDirectory()); + return ret; + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/TableOperationObserver.java b/src/java/org/apache/cassandra/db/compaction/TableOperationObserver.java new file mode 100644 index 000000000000..93dc643a5685 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/TableOperationObserver.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import org.apache.cassandra.utils.NonThrowingCloseable; + +/** + * An observer of {@link AbstractTableOperation}. + *

    + * The observer is notified when an operation is started. It returns a closeable that will be closed + * when the operation is finished. The operation can be queried at any time to get the progress information. + */ +public interface TableOperationObserver +{ + TableOperationObserver NOOP = operation -> () -> {}; + + /** + * Signal to the observer that an operation is starting. + * + * @param operation the operation starting + * + * @return a closeable that the caller should close when the operation completes + */ + NonThrowingCloseable onOperationStart(TableOperation operation); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/TieredCompactionStatistics.java b/src/java/org/apache/cassandra/db/compaction/TieredCompactionStatistics.java new file mode 100644 index 000000000000..f785a180efdf --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/TieredCompactionStatistics.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +abstract class TieredCompactionStatistics extends CompactionAggregateStatistics +{ + private static final Collection HEADER = ImmutableList.copyOf(Iterables.concat(ImmutableList.of("Bucket", "Hotness"), + CompactionAggregateStatistics.HEADER)); + + private static final long serialVersionUID = 3695927592357987916L; + + public TieredCompactionStatistics(CompactionAggregateStatistics base) + { + super(base); + } + + /** The total read hotness of the sstables */ + @JsonProperty + public double hotness() + { + return hotness; + } + + @Override + protected Collection header() + { + return HEADER; + } + + @Override + protected Collection data() + { + List data = new ArrayList<>(HEADER.size()); + data.add(tierValue()); + data.add(String.format("%.4f", hotness)); + + data.addAll(super.data()); + + return data; + } + + protected abstract String tierValue(); +} diff --git a/src/java/org/apache/cassandra/db/compaction/TimeTieredCompactionStatistics.java b/src/java/org/apache/cassandra/db/compaction/TimeTieredCompactionStatistics.java new file mode 100644 index 000000000000..ce935e058932 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/TimeTieredCompactionStatistics.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction; + +import java.text.DateFormat; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The statistics for time tiered compaction. + *

    + * Implements serializable to allow structured info to be returned via JMX. + */ +public class TimeTieredCompactionStatistics extends TieredCompactionStatistics +{ + protected static final DateFormat bucketFormatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + + /** The timestamp in this tier */ + private final long timestamp; + + TimeTieredCompactionStatistics(CompactionAggregateStatistics base, long timestamp) + { + super(base); + + this.timestamp = timestamp; + } + + /** The timestamp in this tier */ + public long timestamp() + { + return timestamp; + } + + @Override + @JsonProperty("Bucket") + protected String tierValue() + { + return bucketFormatter.format(new Date(timestamp)); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java index e896a6c8e825..9e88baf3e666 100644 --- a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java +++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java @@ -24,7 +24,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.io.sstable.format.SSTableReader; public class TimeWindowCompactionController extends CompactionController @@ -33,9 +32,9 @@ public class TimeWindowCompactionController extends CompactionController private final boolean ignoreOverlaps; - public TimeWindowCompactionController(ColumnFamilyStore cfs, Set compacting, long gcBefore, boolean ignoreOverlaps) + public TimeWindowCompactionController(CompactionRealm realm, Set compacting, long gcBefore, boolean ignoreOverlaps) { - super(cfs, compacting, gcBefore); + super(realm, compacting, gcBefore); this.ignoreOverlaps = ignoreOverlaps; if (ignoreOverlaps) logger.warn("You are running with sstables overlapping checks disabled, it can result in loss of data"); diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java index 2709d43ae56d..0a3936d0550c 100644 --- a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java @@ -22,89 +22,68 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Iterator; -import java.util.Objects; -import java.util.TreeSet; -import java.util.concurrent.TimeUnit; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.*; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.CompactionParams; -import org.apache.cassandra.utils.Pair; import static com.google.common.collect.Iterables.filter; +import static org.apache.cassandra.db.compaction.CompactionStrategyOptions.TOMBSTONE_COMPACTION_INTERVAL_OPTION; +import static org.apache.cassandra.db.compaction.CompactionStrategyOptions.TOMBSTONE_THRESHOLD_OPTION; +import static org.apache.cassandra.db.compaction.CompactionStrategyOptions.UNCHECKED_TOMBSTONE_COMPACTION_OPTION; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; -public class TimeWindowCompactionStrategy extends AbstractCompactionStrategy +public class TimeWindowCompactionStrategy extends LegacyAbstractCompactionStrategy.WithAggregates { private static final Logger logger = LoggerFactory.getLogger(TimeWindowCompactionStrategy.class); - private final TimeWindowCompactionStrategyOptions options; - protected volatile int estimatedRemainingTasks; - private final Set sstables = new HashSet<>(); + private final TimeWindowCompactionStrategyOptions twcsOptions; + private final Set sstables = new HashSet<>(); private long lastExpiredCheck; private long highestWindowSeen; // This is accessed in both the threading context of compaction / repair and also JMX private volatile Map sstableCountByBuckets = Collections.emptyMap(); - public TimeWindowCompactionStrategy(ColumnFamilyStore cfs, Map options) + public TimeWindowCompactionStrategy(CompactionStrategyFactory factory, Map options) { - super(cfs, options); - this.estimatedRemainingTasks = 0; - this.options = new TimeWindowCompactionStrategyOptions(options); + super(factory, options); + this.twcsOptions = new TimeWindowCompactionStrategyOptions(options); String[] tsOpts = { UNCHECKED_TOMBSTONE_COMPACTION_OPTION, TOMBSTONE_COMPACTION_INTERVAL_OPTION, TOMBSTONE_THRESHOLD_OPTION }; - if (Arrays.stream(tsOpts).map(o -> options.get(o)).filter(Objects::nonNull).anyMatch(v -> !v.equals("false"))) + if (Arrays.stream(tsOpts).map(options::get).filter(Objects::nonNull).anyMatch(v -> !v.equals("false"))) { logger.debug("Enabling tombstone compactions for TWCS"); } else { logger.debug("Disabling tombstone compactions for TWCS"); - disableTombstoneCompactions = true; + super.options.setDisableTombstoneCompactions(true); } } @Override - public AbstractCompactionTask getNextBackgroundTask(long gcBefore) + public AbstractCompactionTask createCompactionTask(final long gcBefore, + LifecycleTransaction txn, + boolean isMaximal, + boolean splitOutput) { - List previousCandidate = null; - while (true) - { - List latestBucket = getNextBackgroundSSTables(gcBefore); - - if (latestBucket.isEmpty()) - return null; - - // Already tried acquiring references without success. It means there is a race with - // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager - if (latestBucket.equals(previousCandidate)) - { - logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," + - "unless it happens frequently, in which case it must be reported. Will retry later.", - latestBucket); - return null; - } - - LifecycleTransaction modifier = cfs.getTracker().tryModify(latestBucket, OperationType.COMPACTION); - if (modifier != null) - return new TimeWindowCompactionTask(cfs, modifier, gcBefore, options.ignoreOverlaps); - previousCandidate = latestBucket; - } + return new TimeWindowCompactionTask(realm, txn, gcBefore, ignoreOverlaps(), this); } /** @@ -112,21 +91,30 @@ public AbstractCompactionTask getNextBackgroundTask(long gcBefore) * @param gcBefore * @return */ - private synchronized List getNextBackgroundSSTables(final long gcBefore) + @Override + protected synchronized CompactionAggregate getNextBackgroundAggregate(final long gcBefore) { - if (Iterables.isEmpty(cfs.getSSTables(SSTableSet.LIVE))) - return Collections.emptyList(); + if (realm.getLiveSSTables().isEmpty()) + return null; - Set uncompacting = ImmutableSet.copyOf(filter(cfs.getUncompactingSSTables(), sstables::contains)); + Set compacting = realm.getCompactingSSTables(); + Set noncompacting; + synchronized (sstables) + { + noncompacting = ImmutableSet.copyOf(filter(sstables, sstable -> !compacting.contains(sstable))); + } // Find fully expired SSTables. Those will be included no matter what. - Set expired = Collections.emptySet(); + Set expired = Collections.emptySet(); - if (currentTimeMillis() - lastExpiredCheck > options.expiredSSTableCheckFrequency) + if (currentTimeMillis() - lastExpiredCheck > twcsOptions.expiredSSTableCheckFrequency) { logger.debug("TWCS expired check sufficiently far in the past, checking for fully expired SSTables"); - expired = CompactionController.getFullyExpiredSSTables(cfs, uncompacting, options.ignoreOverlaps ? Collections.emptySet() : cfs.getOverlappingLiveSSTables(uncompacting), - gcBefore, options.ignoreOverlaps); + expired = CompactionController.getFullyExpiredSSTables(realm, + noncompacting, + realm::getOverlappingLiveSSTables, + gcBefore, + twcsOptions.ignoreOverlaps); lastExpiredCheck = currentTimeMillis(); } else @@ -134,294 +122,286 @@ private synchronized List getNextBackgroundSSTables(final long gc logger.debug("TWCS skipping check for fully expired SSTables"); } - Set candidates = Sets.newHashSet(filterSuspectSSTables(uncompacting)); + Set candidates = Sets.newHashSet(Iterables.filter(noncompacting, sstable -> !sstable.isMarkedSuspect())); - List compactionCandidates = new ArrayList<>(getNextNonExpiredSSTables(Sets.difference(candidates, expired), gcBefore)); - if (!expired.isEmpty()) + CompactionAggregate compactionCandidate = getNextNonExpiredSSTables(Sets.difference(candidates, expired), gcBefore); + if (expired.isEmpty()) + return compactionCandidate; + + logger.debug("Including expired sstables: {}", expired); + if (compactionCandidate == null) { - logger.debug("Including expired sstables: {}", expired); - compactionCandidates.addAll(expired); + long timestamp = getWindowBoundsInMillis(twcsOptions.sstableWindowUnit, twcsOptions.sstableWindowSize, + Collections.max(expired, Comparator.comparing(CompactionSSTable::getMaxTimestamp)).getMaxTimestamp()); + return CompactionAggregate.createTimeTiered(expired, timestamp); } - return compactionCandidates; + return compactionCandidate.withExpired(expired); } - private List getNextNonExpiredSSTables(Iterable nonExpiringSSTables, final long gcBefore) + private CompactionAggregate getNextNonExpiredSSTables(Iterable nonExpiringSSTables, final long gcBefore) { - List mostInteresting = getCompactionCandidates(nonExpiringSSTables); + List candidates = getCompactionCandidates(nonExpiringSSTables); + backgroundCompactions.setPending(this, candidates); - if (mostInteresting != null) - { - return mostInteresting; - } + CompactionAggregate ret = candidates.isEmpty() ? null : candidates.get(0); // if there is no sstable to compact in standard way, try compacting single sstable whose droppable tombstone // ratio is greater than threshold. - List sstablesWithTombstones = new ArrayList<>(); - for (SSTableReader sstable : nonExpiringSSTables) - { - if (worthDroppingTombstones(sstable, gcBefore)) - sstablesWithTombstones.add(sstable); - } - if (sstablesWithTombstones.isEmpty()) - return Collections.emptyList(); + if (ret == null || ret.isEmpty()) + ret = makeTombstoneCompaction(gcBefore, nonExpiringSSTables, list -> Collections.min(list, CompactionSSTable.sizeComparator)); - return Collections.singletonList(Collections.min(sstablesWithTombstones, SSTableReader.sizeComparator)); + return ret; } - private List getCompactionCandidates(Iterable candidateSSTables) + private List getCompactionCandidates(Iterable candidateSSTables) { - Pair, Long> buckets = getBuckets(candidateSSTables, options.sstableWindowUnit, options.sstableWindowSize, options.timestampResolution); + NavigableMap> buckets = getBuckets(candidateSSTables, twcsOptions.sstableWindowUnit, twcsOptions.sstableWindowSize, twcsOptions.timestampResolution); // Update the highest window seen, if necessary - if(buckets.right > this.highestWindowSeen) - this.highestWindowSeen = buckets.right; - - NewestBucket mostInteresting = newestBucket(buckets.left, - cfs.getMinimumCompactionThreshold(), - cfs.getMaximumCompactionThreshold(), - options.stcsOptions, - this.highestWindowSeen); - - this.estimatedRemainingTasks = mostInteresting.estimatedRemainingTasks; - this.sstableCountByBuckets = buckets.left.keySet().stream().collect(Collectors.toMap(Function.identity(), k -> buckets.left.get(k).size())); - if (!mostInteresting.sstables.isEmpty()) - return mostInteresting.sstables; - return null; + if (!buckets.isEmpty()) + { + long maxKey = buckets.lastKey(); + if (maxKey > this.highestWindowSeen) + this.highestWindowSeen = maxKey; + } + + return getBucketAggregates(buckets, + realm.getMinimumCompactionThreshold(), + realm.getMaximumCompactionThreshold(), + twcsOptions.stcsOptions, + this.highestWindowSeen); } + @Override - public synchronized void addSSTable(SSTableReader sstable) + public void replaceSSTables(Collection removed, Collection added) { - sstables.add(sstable); + synchronized (sstables) + { + for (CompactionSSTable remove : removed) + sstables.remove(remove); + sstables.addAll(added); + } } @Override - public synchronized void removeSSTable(SSTableReader sstable) + public void addSSTable(CompactionSSTable sstable) { - sstables.remove(sstable); + synchronized (sstables) + { + sstables.add(sstable); + } } @Override - protected synchronized Set getSSTables() + void removeDeadSSTables() { - return ImmutableSet.copyOf(sstables); + removeDeadSSTables(sstables); } - /** - * Find the lowest and highest timestamps in a given timestamp/unit pair - * Returns milliseconds, caller should adjust accordingly - */ - public static Pair getWindowBoundsInMillis(TimeUnit windowTimeUnit, int windowTimeSize, long timestampInMillis) + @Override + public void removeSSTable(CompactionSSTable sstable) { - long lowerTimestamp; - long upperTimestamp; - long timestampInSeconds = TimeUnit.SECONDS.convert(timestampInMillis, TimeUnit.MILLISECONDS); + synchronized (sstables) + { + sstables.remove(sstable); + } + } - switch(windowTimeUnit) + @Override + public Set getSSTables() + { + synchronized (sstables) { - case MINUTES: - lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (60L * windowTimeSize)); - upperTimestamp = (lowerTimestamp + (60L * (windowTimeSize - 1L))) + 59L; - break; - case HOURS: - lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (3600L * windowTimeSize)); - upperTimestamp = (lowerTimestamp + (3600L * (windowTimeSize - 1L))) + 3599L; - break; - case DAYS: - default: - lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (86400L * windowTimeSize)); - upperTimestamp = (lowerTimestamp + (86400L * (windowTimeSize - 1L))) + 86399L; - break; + return ImmutableSet.copyOf(sstables); } + } - return Pair.create(TimeUnit.MILLISECONDS.convert(lowerTimestamp, TimeUnit.SECONDS), - TimeUnit.MILLISECONDS.convert(upperTimestamp, TimeUnit.SECONDS)); + /** + * Find the lowest timestamp in a given window/unit pair and + * return it expressed as milliseconds, the caller should adjust accordingly + */ + static long getWindowBoundsInMillis(TimeUnit windowTimeUnit, int windowTimeSize, long timestampInMillis) + { + long sizeInMillis = TimeUnit.MILLISECONDS.convert(windowTimeSize, windowTimeUnit); + return (timestampInMillis / sizeInMillis) * sizeInMillis; } /** * Group files with similar max timestamp into buckets. + *

    + * The max timestamp of each sstable is converted into the timestamp resolution and then the window bounds are + * calculated by calling {@link #getWindowBoundsInMillis(TimeUnit, int, long)}. The sstable is added to the bucket + * with the same lower timestamp bound. If the lower timestamp bound is higher than any other seen, then it is recorded + * as the max timestamp seen that will be returned. * - * @param files pairs consisting of a file and its min timestamp - * @param sstableWindowUnit - * @param sstableWindowSize - * @param timestampResolution - * @return A pair, where the left element is the bucket representation (map of timestamp to sstablereader), and the right is the highest timestamp seen + * @param files the candidate sstables + * @param sstableWindowUnit the time unit for {@code sstableWindowSize} + * @param sstableWindowSize the size of the time window by which sstables are grouped + * @param timestampResolution the time unit for converting the sstable timestamp + * @return A pair, where the left element is the bucket representation (multi-map of lower bound timestamp to sstables), + * and the right is the highest lower bound timestamp seen */ @VisibleForTesting - static Pair, Long> getBuckets(Iterable files, TimeUnit sstableWindowUnit, int sstableWindowSize, TimeUnit timestampResolution) + static NavigableMap> getBuckets(Iterable files, TimeUnit sstableWindowUnit, int sstableWindowSize, TimeUnit timestampResolution) { - HashMultimap buckets = HashMultimap.create(); + NavigableMap> buckets = new TreeMap<>(Long::compare); - long maxTimestamp = 0; - // Create hash map to represent buckets // For each sstable, add sstable to the time bucket // Where the bucket is the file's max timestamp rounded to the nearest window bucket - for (SSTableReader f : files) + for (CompactionSSTable f : files) { assert TimeWindowCompactionStrategyOptions.validTimestampTimeUnits.contains(timestampResolution); long tStamp = TimeUnit.MILLISECONDS.convert(f.getMaxTimestamp(), timestampResolution); - Pair bounds = getWindowBoundsInMillis(sstableWindowUnit, sstableWindowSize, tStamp); - buckets.put(bounds.left, f); - if (bounds.left > maxTimestamp) - maxTimestamp = bounds.left; + addToBuckets(buckets, f, tStamp, sstableWindowUnit, sstableWindowSize); } - logger.trace("buckets {}, max timestamp {}", buckets, maxTimestamp); - return Pair.create(buckets, maxTimestamp); + logger.trace("buckets {}, max timestamp {}", buckets, buckets.isEmpty() ? "none" : buckets.lastKey().toString()); + return buckets; } - static final class NewestBucket + @VisibleForTesting + static void addToBuckets(NavigableMap> buckets, CompactionSSTable f, long tStamp, TimeUnit sstableWindowUnit, int sstableWindowSize) { - /** The sstables that should be compacted next */ - final List sstables; - - /** The number of tasks estimated */ - final int estimatedRemainingTasks; - - NewestBucket(List sstables, int estimatedRemainingTasks) - { - this.sstables = sstables; - this.estimatedRemainingTasks = estimatedRemainingTasks; - } - - @Override - public String toString() - { - return String.format("sstables: %s, estimated remaining tasks: %d", sstables, estimatedRemainingTasks); - } + long bound = getWindowBoundsInMillis(sstableWindowUnit, sstableWindowSize, tStamp); + buckets.computeIfAbsent(bound, + key -> new ArrayList<>()) + .add(f); } - /** - * @param buckets list of buckets, sorted from newest to oldest, from which to return the newest bucket within thresholds. + * If the current bucket has at least minThreshold SSTables, choose that one. For any other bucket, at least 2 SSTables is enough. + * In any case, limit to maxThreshold SSTables. + * + * @param buckets A map from a bucket id to a set of tables, sorted by id and then by table size * @param minThreshold minimum number of sstables in a bucket to qualify. * @param maxThreshold maximum number of sstables to compact at once (the returned bucket will be trimmed down to this). - * @return a bucket (list) of sstables to compact. + * @param stcsOptions the options for {@link SizeTieredCompactionStrategy} to be used in the newest bucket + * @param now the latest timestamp in milliseconds + * + * @return a list of compaction aggregates, one per time bucket */ @VisibleForTesting - static NewestBucket newestBucket(HashMultimap buckets, int minThreshold, int maxThreshold, SizeTieredCompactionStrategyOptions stcsOptions, long now) + static List getBucketAggregates(NavigableMap> buckets, + int minThreshold, + int maxThreshold, + SizeTieredCompactionStrategyOptions stcsOptions, + long now) { - // If the current bucket has at least minThreshold SSTables, choose that one. - // For any other bucket, at least 2 SSTables is enough. - // In any case, limit to maxThreshold SSTables. - - List sstables = Collections.emptyList(); - int estimatedRemainingTasks = 0; - - TreeSet allKeys = new TreeSet<>(buckets.keySet()); + List ret = new ArrayList<>(buckets.size()); + boolean nextCompactionFound = false; // set to true once the first bucket with a compaction is found - Iterator it = allKeys.descendingIterator(); - while(it.hasNext()) + for (Map.Entry> entry : buckets.descendingMap().entrySet()) { - Long key = it.next(); - Set bucket = buckets.get(key); + Long key = entry.getKey(); + List bucket = entry.getValue(); logger.trace("Key {}, now {}", key, now); + + CompactionPick selected = CompactionPick.EMPTY; + List pending = new ArrayList<>(1); + if (bucket.size() >= minThreshold && key >= now) { // If we're in the newest bucket, we'll use STCS to prioritize sstables - List> pairs = SizeTieredCompactionStrategy.createSSTableAndLengthPairs(bucket); - List> stcsBuckets = SizeTieredCompactionStrategy.getBuckets(pairs, stcsOptions.bucketHigh, stcsOptions.bucketLow, stcsOptions.minSSTableSize); - List stcsInterestingBucket = SizeTieredCompactionStrategy.mostInterestingBucket(stcsBuckets, minThreshold, maxThreshold); + SizeTieredCompactionStrategy.SizeTieredBuckets stcsBuckets = new SizeTieredCompactionStrategy.SizeTieredBuckets(bucket, + stcsOptions, + minThreshold, + maxThreshold); + stcsBuckets.aggregate(); - // If the tables in the current bucket aren't eligible in the STCS strategy, we'll skip it and look for other buckets - if (!stcsInterestingBucket.isEmpty()) + for (CompactionAggregate stcsAggregate : stcsBuckets.getAggregates()) { - double remaining = bucket.size() - maxThreshold; - estimatedRemainingTasks += 1 + (remaining > minThreshold ? Math.ceil(remaining / maxThreshold) : 0); - if (sstables.isEmpty()) + if (selected.isEmpty()) { - logger.debug("Using STCS compaction for first window of bucket: data files {} , options {}", pairs, stcsOptions); - sstables = stcsInterestingBucket; + selected = stcsAggregate.getSelected().withParent(key); + for (CompactionPick comp : stcsAggregate.getActive()) + { + if (comp != stcsAggregate.getSelected()) + pending.add(comp); + } } else { - logger.trace("First window of bucket is eligible but not selected: data files {} , options {}", pairs, stcsOptions); + pending.addAll(stcsAggregate.getActive()); } } + + if (!selected.isEmpty()) + logger.debug("Newest window has STCS compaction candidates, {}, data files {} , options {}", + nextCompactionFound ? "eligible but not selected due to prior candidate" : "will be selected for compaction", + stcsBuckets.pairs(), + stcsOptions); + else + logger.debug("No STCS compactions found for first window, data files {}, options {}", stcsBuckets.pairs(), stcsOptions); + + if (!nextCompactionFound && !selected.isEmpty()) + { + nextCompactionFound = true; + ret.add(0, CompactionAggregate.createTimeTiered(bucket, selected, pending, key)); // the first one will be submitted for compaction + } + else + { + ret.add(CompactionAggregate.createTimeTiered(bucket, selected, pending, key)); + } } else if (bucket.size() >= 2 && key < now) { - double remaining = bucket.size() - maxThreshold; - estimatedRemainingTasks += 1 + (remaining > minThreshold ? Math.ceil(remaining / maxThreshold) : 0); - if (sstables.isEmpty()) + List sstables = bucket; + + // Sort the largest sstables off the end before splitting by maxThreshold + Collections.sort(sstables, CompactionSSTable.sizeComparator); + + int i = 0; + while ((bucket.size() - i) >= 2) + { + List pick = sstables.subList(i, i + Math.min(bucket.size() - i, maxThreshold)); + if (selected.isEmpty()) + selected = CompactionPick.create(key, pick); + else + pending.add(CompactionPick.create(key, pick)); + + i += pick.size(); + } + + if (!nextCompactionFound) { logger.debug("bucket size {} >= 2 and not in current bucket, compacting what's here: {}", bucket.size(), bucket); - sstables = trimToThreshold(bucket, maxThreshold); + nextCompactionFound = true; + ret.add(0, CompactionAggregate.createTimeTiered(bucket, selected, pending, key)); // the first one will be submitted for compaction } else { logger.trace("bucket size {} >= 2 and not in current bucket, eligible but not selected: {}", bucket.size(), bucket); + ret.add(CompactionAggregate.createTimeTiered(bucket, selected, pending, key)); } } else { logger.trace("No compaction necessary for bucket size {} , key {}, now {}", bucket.size(), key, now); + ret.add(CompactionAggregate.createTimeTiered(bucket, selected, pending, key)); // add an empty aggregate anyway so we get a full view } } - return new NewestBucket(sstables, estimatedRemainingTasks); - } - - /** - * @param bucket set of sstables - * @param maxThreshold maximum number of sstables in a single compaction task. - * @return A bucket trimmed to the maxThreshold newest sstables. - */ - @VisibleForTesting - static List trimToThreshold(Set bucket, int maxThreshold) - { - List ssTableReaders = new ArrayList<>(bucket); - - // Trim the largest sstables off the end to meet the maxThreshold - Collections.sort(ssTableReaders, SSTableReader.sizeComparator); - - return ImmutableList.copyOf(Iterables.limit(ssTableReaders, maxThreshold)); - } - - @Override - public synchronized Collection getMaximalTask(long gcBefore, boolean splitOutput) - { - Iterable filteredSSTables = filterSuspectSSTables(sstables); - if (Iterables.isEmpty(filteredSSTables)) - return null; - LifecycleTransaction txn = cfs.getTracker().tryModify(filteredSSTables, OperationType.COMPACTION); - if (txn == null) - return null; - return Collections.singleton(new TimeWindowCompactionTask(cfs, txn, gcBefore, options.ignoreOverlaps)); + return ret; } /** * TWCS should not group sstables for anticompaction - this can mix new and old data */ @Override - public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) + public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) { - Collection> groups = new ArrayList<>(sstablesToGroup.size()); - for (SSTableReader sstable : sstablesToGroup) + Collection> groups = new ArrayList<>(sstablesToGroup.size()); + for (CompactionSSTable sstable : sstablesToGroup) { groups.add(Collections.singleton(sstable)); } return groups; } - @Override - public synchronized AbstractCompactionTask getUserDefinedTask(Collection sstables, long gcBefore) - { - assert !sstables.isEmpty(); // checked for by CM.submitUserDefined - - LifecycleTransaction modifier = cfs.getTracker().tryModify(sstables, OperationType.COMPACTION); - if (modifier == null) - { - logger.debug("Unable to mark {} for compaction; probably a background compaction got to it first. You can disable background compactions temporarily if this is a problem", sstables); - return null; - } - - return new TimeWindowCompactionTask(cfs, modifier, gcBefore, options.ignoreOverlaps).setUserDefined(true); - } - - public int getEstimatedRemainingTasks() + boolean ignoreOverlaps() { - return this.estimatedRemainingTasks; + return twcsOptions.ignoreOverlaps; } public long getMaxSSTableBytes() @@ -436,7 +416,7 @@ public Map getSSTableCountByBuckets() public static Map validateOptions(Map options) throws ConfigurationException { - Map uncheckedOptions = AbstractCompactionStrategy.validateOptions(options); + Map uncheckedOptions = CompactionStrategyOptions.validateOptions(options); uncheckedOptions = TimeWindowCompactionStrategyOptions.validateOptions(options, uncheckedOptions); uncheckedOptions.remove(CompactionParams.Option.MIN_THRESHOLD.toString()); @@ -448,7 +428,7 @@ public static Map validateOptions(Map options) t public String toString() { return String.format("TimeWindowCompactionStrategy[%s/%s]", - cfs.getMinimumCompactionThreshold(), - cfs.getMaximumCompactionThreshold()); + realm.getMinimumCompactionThreshold(), + realm.getMaximumCompactionThreshold()); } } diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java index 33604e52e636..e9ea0331ef7c 100644 --- a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java +++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java @@ -20,7 +20,6 @@ import java.util.Set; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -28,15 +27,15 @@ public class TimeWindowCompactionTask extends CompactionTask { private final boolean ignoreOverlaps; - public TimeWindowCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, long gcBefore, boolean ignoreOverlaps) + public TimeWindowCompactionTask(CompactionRealm realm, LifecycleTransaction txn, long gcBefore, boolean ignoreOverlaps, TimeWindowCompactionStrategy strategy) { - super(cfs, txn, gcBefore); + super(realm, txn, gcBefore, false, strategy); this.ignoreOverlaps = ignoreOverlaps; } @Override public CompactionController getCompactionController(Set toCompact) { - return new TimeWindowCompactionController(cfs, toCompact, gcBefore, ignoreOverlaps); + return new TimeWindowCompactionController(realm, toCompact, gcBefore, ignoreOverlaps); } } diff --git a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionContainer.java b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionContainer.java new file mode 100644 index 000000000000..fd389bbf8468 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionContainer.java @@ -0,0 +1,421 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.commitlog.IntervalSet; +import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.SSTableMultiWriter; +import org.apache.cassandra.io.sstable.ScannerList; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.notifications.INotification; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.utils.TimeUUID; + +public class UnifiedCompactionContainer implements CompactionStrategyContainer +{ + private final CompactionStrategyFactory factory; + private final CompactionParams params; + private final CompactionParams metadataParams; + private final UnifiedCompactionStrategy strategy; + private final boolean enableAutoCompaction; + private final boolean hasVector; + + AtomicBoolean enabled; + + UnifiedCompactionContainer(CompactionStrategyFactory factory, + BackgroundCompactions backgroundCompactions, + CompactionParams params, + CompactionParams metadataParams, + boolean enabled, + boolean enableAutoCompaction) + { + this.factory = factory; + this.params = params; + this.metadataParams = metadataParams; + this.strategy = new UnifiedCompactionStrategy(factory, backgroundCompactions, params.options()); + this.enabled = new AtomicBoolean(enabled); + this.enableAutoCompaction = enableAutoCompaction; + this.hasVector = strategy.getController().hasVectorType(); + + factory.getCompactionLogger().strategyCreated(this.strategy); + + if (this.strategy.getOptions().isLogEnabled()) + factory.getCompactionLogger().enable(); + else + factory.getCompactionLogger().disable(); + + startup(); + } + + @Override + public void enable() + { + this.enabled.set(true); + } + + @Override + public void disable() + { + this.enabled.set(false); + } + + @Override + public boolean isEnabled() + { + return enableAutoCompaction && enabled.get() && strategy.isActive; + } + + @Override + public boolean isActive() + { + return strategy.isActive; + } + + public static CompactionStrategyContainer create(@Nullable CompactionStrategyContainer previous, + CompactionStrategyFactory strategyFactory, + CompactionParams compactionParams, + CompactionStrategyContainer.ReloadReason reason, + boolean enableAutoCompaction) + { + boolean enabled = CompactionStrategyFactory.enableCompactionOnReload(previous, compactionParams, reason); + BackgroundCompactions backgroundCompactions; + // inherit compactions history from previous UCS container + if (previous instanceof UnifiedCompactionContainer) + backgroundCompactions = ((UnifiedCompactionContainer) previous).getBackgroundCompactions(); + + // for other cases start from scratch + // We don't inherit from legacy compactions right now because there are multiple strategies and we'd need + // to merge their BackgroundCompactions to support that. Merging per se is not tricky, but the bigger problem + // is aggregate cleanup. We'd need to unsubscribe from compaction tasks by legacy strategies and subscribe + // by the new UCS to remove inherited ongoing compactions when they complete. + // We might want to revisit this issue later to improve UX. + else + backgroundCompactions = new BackgroundCompactions(strategyFactory.getRealm()); + CompactionParams metadataParams = createMetadataParams(previous, compactionParams, reason); + + if (previous != null) + previous.shutdown(); + + return new UnifiedCompactionContainer(strategyFactory, + backgroundCompactions, + compactionParams, + metadataParams, + enabled, + enableAutoCompaction); + } + + @Override + public CompactionStrategyContainer reload(@Nonnull CompactionStrategyContainer previous, + CompactionParams compactionParams, + ReloadReason reason) + { + return create(previous, factory, compactionParams, reason, enableAutoCompaction); + } + + @Override + public boolean shouldReload(CompactionParams params, ReloadReason reason) + { + return reason != CompactionStrategyContainer.ReloadReason.METADATA_CHANGE + || !params.equals(getMetadataCompactionParams()) + || hasVector != factory.getRealm().metadata().hasVectorType(); + } + + private static CompactionParams createMetadataParams(@Nullable CompactionStrategyContainer previous, + CompactionParams compactionParams, + ReloadReason reason) + { + CompactionParams metadataParams; + if (reason == CompactionStrategyContainer.ReloadReason.METADATA_CHANGE) + // metadataParams are aligned with compactionParams. We do not access TableParams.compaction to avoid racing with + // concurrent ALTER TABLE metadata change. + metadataParams = compactionParams; + else if (previous != null) + metadataParams = previous.getMetadataCompactionParams(); + else + metadataParams = null; + + return metadataParams; + } + + @Override + public CompactionParams getCompactionParams() + { + return params; + } + + @Override + public CompactionParams getMetadataCompactionParams() + { + return metadataParams; + } + + @Override + public List getStrategies() + { + return ImmutableList.of(strategy); + } + + @Override + public List getStrategies(boolean isRepaired, @Nullable TimeUUID pendingRepair) + { + return getStrategies(); + } + + @Override + public void repairSessionCompleted(TimeUUID sessionID) + { + // We are not tracking SSTables, so nothing to do here. + } + + /** + * UCC does not need to use this method with {@link CompactionRealm#mutateRepairedWithLock} + * @return null + */ + @Override + public ReentrantReadWriteLock.WriteLock getWriteLock() + { + return null; + } + + @Override + public CompactionLogger getCompactionLogger() + { + return strategy.compactionLogger; + } + + @Override + public void pause() + { + strategy.pause(); + } + + @Override + public void resume() + { + strategy.resume(); + } + + @Override + public void startup() + { + strategy.startup(); + } + + @Override + public void shutdown() + { + strategy.shutdown(); + } + + @Override + public Collection getNextBackgroundTasks(long gcBefore) + { + return strategy.getNextBackgroundTasks(gcBefore); + } + + @Override + public CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) + { + return strategy.getMaximalTasks(gcBefore, splitOutput, permittedParallelism); + } + + @Override + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism, OperationType operationType) + { + return strategy.getMaximalTasks(gcBefore, splitOutput, permittedParallelism, operationType); + } + + @Override + public CompactionTasks getUserDefinedTasks(Collection sstables, long gcBefore) + { + return strategy.getUserDefinedTasks(sstables, gcBefore); + } + + @Override + public int getEstimatedRemainingTasks() + { + return strategy.getEstimatedRemainingTasks(); + } + + @Override + public int getEstimatedRemainingTasks(int additionalSSTables, long additionalBytes, boolean isIncremental) + { + return strategy.getEstimatedRemainingTasks(additionalSSTables, additionalBytes, isIncremental); + } + + @Override + public AbstractCompactionTask createCompactionTask(LifecycleTransaction txn, long gcBefore, long maxSSTableBytes) + { + return strategy.createCompactionTask(txn, gcBefore, maxSSTableBytes); + } + + @Override + public int getTotalCompactions() + { + return strategy.getTotalCompactions(); + } + + @Override + public List getStatistics() + { + return strategy.getStatistics(); + } + + @Override + public long getMaxSSTableBytes() + { + return strategy.getMaxSSTableBytes(); + } + + @Override + public int[] getSSTableCountPerLevel() + { + return strategy.getSSTableCountPerLevel(); + } + + @Override + public long[] getPerLevelSizeBytes() + { + return strategy.getPerLevelSizeBytes(); + } + + @Override + public boolean isLeveledCompaction() + { + return strategy.isLeveledCompaction(); + } + + @Override + public int[] getSSTableCountPerTWCSBucket() + { + return strategy.getSSTableCountPerTWCSBucket(); + } + + @Override + public int getLevelFanoutSize() + { + return strategy.getLevelFanoutSize(); + } + + @Override + public ScannerList getScanners(Collection sstables, Collection> ranges) + { + return strategy.getScanners(sstables, ranges); + } + + @Override + public String getName() + { + return strategy.getName(); + } + + @Override + public Set getSSTables() + { + return strategy.getSSTables(); + } + + @Override + public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) + { + return strategy.groupSSTablesForAntiCompaction(sstablesToGroup); + } + + @Override + public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, + long keyCount, + long repairedAt, + TimeUUID pendingRepair, + boolean isTransient, + IntervalSet commitLogPositions, + int sstableLevel, + SerializationHeader header, + Collection indexGroups, + LifecycleNewTracker lifecycleNewTracker) + { + return strategy.createSSTableMultiWriter(descriptor, + keyCount, + repairedAt, + pendingRepair, + isTransient, + commitLogPositions, + sstableLevel, + header, + indexGroups, + lifecycleNewTracker); + } + + @Override + public boolean supportsEarlyOpen() + { + return strategy.supportsEarlyOpen(); + } + + @Override + public void periodicReport() + { + strategy.periodicReport(); + } + + @Override + public Map getMaxOverlapsMap() + { + return strategy.getMaxOverlapsMap(); + } + + BackgroundCompactions getBackgroundCompactions() + { + return strategy.backgroundCompactions; + } + + @Override + public void onInProgress(CompactionProgress progress) + { + strategy.onInProgress(progress); + } + + @Override + public void onCompleted(TimeUUID id, Throwable err) + { + strategy.onCompleted(id, err); + } + + @Override + public void handleNotification(INotification notification, Object sender) + { + // TODO - this is a no-op because the strategy is stateless but we could detect here + // sstables that are added either because of streaming or because of nodetool refresh + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStatistics.java b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStatistics.java new file mode 100644 index 000000000000..3d6453349bf9 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStatistics.java @@ -0,0 +1,153 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.cassandra.utils.FBUtilities; + +/** + * The statistics for size tiered compaction. + *

    + * Implements serializable to allow structured info to be returned via JMX. + */ +public class UnifiedCompactionStatistics extends CompactionAggregateStatistics +{ + private static final Collection HEADER = ImmutableList.copyOf(Iterables.concat(ImmutableList.of("Level", "W", "Min Density", "Max Density", "Overlap"), + CompactionAggregateStatistics.HEADER)); + + private static final long serialVersionUID = 3695927592357345266L; + + /** The bucket number */ + private final int bucket; + + /** The survival factor o */ + private final double survivalFactor; + + /** The scaling parameter W */ + private final int scalingParameter; + + /** The minimum density for an SSTable that belongs to this bucket */ + private final double minDensityBytes; + + /** The maximum density for an SSTable run that belongs to this bucket */ + private final double maxDensityBytes; + + /** The maximum number of overlapping sstables in the shard */ + private final int maxOverlap; + + /** The name of the shard */ + private final String shard; + + UnifiedCompactionStatistics(CompactionAggregateStatistics base, + int bucketIndex, + double survivalFactor, + int scalingParameter, + double minDensityBytes, + double maxDensityBytes, + int maxOverlap, + String shard) + { + super(base); + + this.bucket = bucketIndex; + this.survivalFactor = survivalFactor; + this.scalingParameter = scalingParameter; + this.minDensityBytes = minDensityBytes; + this.maxDensityBytes = maxDensityBytes; + this.maxOverlap = maxOverlap; + this.shard = shard; + } + + /** The bucket number */ + @JsonProperty + public int bucket() + { + return bucket; + } + + /** The survival factor o, currently always one */ + @JsonProperty + public double survivalFactor() + { + return survivalFactor; + } + + /** The scaling parameter W */ + @JsonProperty + public int scalingParameter() + { + return scalingParameter; + } + + /** The minimum size for an SSTable that belongs to this bucket */ + @JsonProperty + public double minDensityBytes() + { + return minDensityBytes; + } + + /** The maximum size for an SSTable that belongs to this bucket */ + @JsonProperty + public double maxDensityBytes() + { + return maxDensityBytes; + } + + /** The maximum number of overlapping sstables in this bucket */ + @JsonProperty + public int maxOverlap() + { + return maxOverlap; + } + + /** The name of the shard, empty if the compaction is not sharded (the default). */ + @JsonProperty + @Override + public String shard() + { + return shard; + } + + @Override + protected Collection header() + { + return HEADER; + } + + @Override + protected Collection data() + { + List data = new ArrayList<>(HEADER.size()); + data.add(Integer.toString(bucket())); + data.add(UnifiedCompactionStrategy.printScalingParameter(scalingParameter)); + data.add(FBUtilities.prettyPrintBinary(minDensityBytes, "B", " ")); + data.add(FBUtilities.prettyPrintBinary(maxDensityBytes, "B", " ")); + + data.add(Integer.toString(maxOverlap)); + + data.addAll(super.data()); + + return data; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java index 64df6ee8f6b0..0038b1f63bd8 100644 --- a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java @@ -1,13 +1,11 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Copyright DataStax, Inc. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -19,55 +17,74 @@ package org.apache.cassandra.db.compaction; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; +import java.util.function.BiPredicate; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nullable; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.DiskBoundaries; +import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; import org.apache.cassandra.db.compaction.unified.Controller; +import org.apache.cassandra.db.compaction.unified.Reservations; import org.apache.cassandra.db.compaction.unified.ShardedMultiWriter; import org.apache.cassandra.db.compaction.unified.UnifiedCompactionTask; +import org.apache.cassandra.db.lifecycle.CompositeLifecycleTransaction; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.PartialLifecycleTransaction; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.index.Index; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableMultiWriter; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Overlaps; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.Throwables; -/** - * The design of the unified compaction strategy is described in the accompanying UnifiedCompactionStrategy.md. - * - * See CEP-26: https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-26%3A+Unified+Compaction+Strategy - */ +import static org.apache.cassandra.utils.Throwables.perform; + +/// The design of the unified compaction strategy is described in [UnifiedCompactionStrategy.md](./UnifiedCompactionStrategy.md). +/// +/// See also [CEP-26](https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-26%3A+Unified+Compaction+Strategy). public class UnifiedCompactionStrategy extends AbstractCompactionStrategy { + @SuppressWarnings("unused") // accessed via reflection + public static final Class CONTAINER_CLASS = UnifiedCompactionContainer.class; + private static final Logger logger = LoggerFactory.getLogger(UnifiedCompactionStrategy.class); - static final int MAX_LEVELS = 32; // This is enough for a few petabytes of data (with the worst case fan factor + public static final int MAX_LEVELS = 32; // This is enough for a few petabytes of data (with the worst case fan factor // at W=0 this leaves room for 2^32 sstables, presumably of at least 1MB each). private static final Pattern SCALING_PARAMETER_PATTERN = Pattern.compile("(N)|L(\\d+)|T(\\d+)|([+-]?\\d+)"); @@ -75,32 +92,46 @@ public class UnifiedCompactionStrategy extends AbstractCompactionStrategy .replaceAll("[()]", "") .replace("\\d", "[0-9]"); + /// Special level definition for major compactions. + static final Level LEVEL_MAXIMAL = new Level(-1, 0, 0, 0, 1, 0, Double.POSITIVE_INFINITY); + private final Controller controller; - private volatile ShardManager shardManager; + private volatile ArenaSelector currentArenaSelector; + private volatile ShardManager currentShardManager; private long lastExpiredCheck; - protected volatile int estimatedRemainingTasks; - @VisibleForTesting - protected final Set sstables = new HashSet<>(); + public UnifiedCompactionStrategy(CompactionStrategyFactory factory, BackgroundCompactions backgroundCompactions, Map options) + { + this(factory, backgroundCompactions, options, Controller.fromOptions(factory.getRealm(), options)); + } - public UnifiedCompactionStrategy(ColumnFamilyStore cfs, Map options) + public UnifiedCompactionStrategy(CompactionStrategyFactory factory, BackgroundCompactions backgroundCompactions, Controller controller) { - this(cfs, options, Controller.fromOptions(cfs, options)); + this(factory, backgroundCompactions, new HashMap<>(), controller); } - public UnifiedCompactionStrategy(ColumnFamilyStore cfs, Map options, Controller controller) + public UnifiedCompactionStrategy(CompactionStrategyFactory factory, BackgroundCompactions backgroundCompactions, Map options, Controller controller) { - super(cfs, options); + super(factory, backgroundCompactions, options); this.controller = controller; - estimatedRemainingTasks = 0; - lastExpiredCheck = Clock.Global.currentTimeMillis(); + } + + @VisibleForTesting + public UnifiedCompactionStrategy(CompactionStrategyFactory factory, Controller controller) + { + this(factory, new BackgroundCompactions(factory.getRealm()), new HashMap<>(), controller); } public static Map validateOptions(Map options) throws ConfigurationException { - return Controller.validateOptions(AbstractCompactionStrategy.validateOptions(options)); + return Controller.validateOptions(CompactionStrategyOptions.validateOptions(options)); + } + + public void storeControllerConfig() + { + getController().storeControllerConfig(); } public static int fanoutFromScalingParameter(int w) @@ -139,123 +170,493 @@ private static int atLeast2(int value, String str) public static String printScalingParameter(int w) { if (w < 0) - return "L" + Integer.toString(2 - w); + return 'L' + Integer.toString(2 - w); else if (w > 0) - return "T" + Integer.toString(w + 2); + return 'T' + Integer.toString(w + 2); else return "N"; } + /// Make a time-based UUID for unified compaction tasks with sequence 0. The reason to do this is to accommodate + /// parallelized compactions: + /// - Sequence 0 (visible as `-8000-` in the UUID string) denotes single-task (i.e. non-parallelized) compactions. + /// - Sequence >0 (`-800n-`) denotes the individual task's index of a parallelized compaction. + /// - Parallelized compactions use sequence 0 as the transaction id, and sequences from 1 to the number of tasks + /// for the ids of individual tasks. + public static TimeUUID nextTimeUUID() + { + return TimeUUID.Generator.withSequence(TimeUUID.Generator.nextTimeUUID(), 0); + } + @Override - public synchronized Collection getMaximalTask(long gcBefore, boolean splitOutput) + public Collection> groupSSTablesForAntiCompaction(Collection sstablesToGroup) { - maybeUpdateShardManager(); - // The tasks are split by repair status and disk, as well as in non-overlapping sections to enable some - // parallelism (to the amount that L0 sstables are split, i.e. at least base_shard_count). The result will be - // split across shards according to its density. Depending on the parallelism, the operation may require up to - // 100% extra space to complete. - List tasks = new ArrayList<>(); - List> nonOverlapping = splitInNonOverlappingSets(filterSuspectSSTables(getSSTables())); - for (Set set : nonOverlapping) + Collection> groups = new ArrayList<>(); + for (Arena arena : getCompactionArenas(sstablesToGroup, (i1, i2) -> true)) // take all sstables { - LifecycleTransaction txn = cfs.getTracker().tryModify(set, OperationType.COMPACTION); - if (txn != null) - tasks.add(createCompactionTask(txn, gcBefore)); + groups.addAll(super.groupSSTablesForAntiCompaction(arena.sstables)); } - return tasks; + + return groups; + } + + @Override + public synchronized CompactionTasks getUserDefinedTasks(Collection sstables, long gcBefore) + { + // The tasks need to be split by repair status and disk, but otherwise we must assume the user knows what they + // are doing. + List tasks = new ArrayList<>(); + for (Arena arena : getCompactionArenas(sstables, UnifiedCompactionStrategy::isSuitableForCompaction)) + tasks.addAll(super.getUserDefinedTasks(arena.sstables, gcBefore)); + return CompactionTasks.create(tasks); } - private static List> splitInNonOverlappingSets(Collection sstables) + /// Get a list of maximal aggregates that can be compacted independently in parallel to achieve a major compaction. + /// + /// These aggregates split the sstables in each arena into non-overlapping groups where the boundaries between these + /// groups are also boundaries of the current sharding configuration. Compacting the groups independently has the + /// same effect as compacting all of the sstables in the arena together in one operation. + public synchronized List getMaximalAggregates() { - List> overlapSets = Overlaps.constructOverlapSets(new ArrayList<>(sstables), - UnifiedCompactionStrategy::startsAfter, - SSTableReader.firstKeyComparator, - SSTableReader.lastKeyComparator); - if (overlapSets.isEmpty()) - return overlapSets; + return getMaximalAggregates(Sets.newHashSet(realm.getSSTables(SSTableSet.NONCOMPACTING))); + } + + public synchronized List getMaximalAggregates(Collection sstables) + { + maybeUpdateSelector(); // must be called before computing compaction arenas + return getMaximalAggregatesWithArenas(getCompactionArenas(sstables, UnifiedCompactionStrategy::isSuitableForCompaction)); + } - Set group = overlapSets.get(0); - List> groups = new ArrayList<>(); - for (int i = 1; i < overlapSets.size(); ++i) + private synchronized List getMaximalAggregatesWithArenas(Collection compactionArenas) + { + // The aggregates are split into arenas by repair status and disk, as well as in non-overlapping sections to + // enable some parallelism and efficient use of extra space. The result will be split across shards according to + // its density. + // Depending on the parallelism, the operation may require up to 100% extra space to complete. + List aggregates = new ArrayList<>(); + + for (Arena arena : compactionArenas) { - Set current = overlapSets.get(i); - if (Sets.intersection(current, group).isEmpty()) - { - groups.add(group); - group = current; - } - else + // If possible, we want to issue separate compactions for non-overlapping sets of sstables, to allow + // for smaller extra space requirements. However, if the sharding configuration has changed, a major + // compaction should combine non-overlapping sets if they are split on a boundary that is no longer + // in effect. + List> groups = + getShardManager().splitSSTablesInShards(arena.sstables, + makeShardingStats(arena.sstables).shardCountForDensity, + (sstableShard, shardRange) -> Sets.newHashSet(sstableShard)); + + // Now combine all of these groups that share an sstable so that we have valid independent transactions. + groups = Overlaps.combineSetsWithCommonElement(groups); + + for (Set group : groups) { - group.addAll(current); + aggregates.add(CompactionAggregate.createUnified(group, + Overlaps.maxOverlap(group, + CompactionSSTable.startsAfter, + CompactionSSTable.firstKeyComparator, + CompactionSSTable.lastKeyComparator), + createPick(nextTimeUUID(), LEVEL_MAXIMAL.index, group), + Collections.emptyList(), + arena, + LEVEL_MAXIMAL)); } } - groups.add(group); - return groups; + return aggregates; } @Override - public AbstractCompactionTask getUserDefinedTask(Collection sstables, final long gcBefore) + public synchronized CompactionTasks getMaximalTasks(long gcBefore, boolean splitOutput, int permittedParallelism) { - assert !sstables.isEmpty(); // checked for by CM.submitUserDefined + if (permittedParallelism <= 0) + permittedParallelism = Integer.MAX_VALUE; - LifecycleTransaction transaction = cfs.getTracker().tryModify(sstables, OperationType.COMPACTION); - if (transaction == null) + List tasks = new ArrayList<>(); + LifecycleTransaction txn = null; + try { - logger.trace("Unable to mark {} for compaction; probably a background compaction got to it first. You can disable background compactions temporarily if this is a problem", sstables); - return null; + // Split the space into independently compactable groups. + for (var aggregate : getMaximalAggregates()) + { + txn = realm.tryModify(aggregate.getSelected().sstables(), + OperationType.COMPACTION, + aggregate.getSelected().id()); + + // Create (potentially parallelized) tasks for each group. + if (txn != null) + createAndAddTasks(gcBefore, txn, getShardingStats(aggregate), permittedParallelism, tasks); + // we ignore splitOutput (always split according to the strategy's sharding) and do not need isMaximal + + // Note: major compactions should not end up in the background compactions tracker to avoid wreaking + // havok in the thread assignment logic. + } + + // If we have more arenas/non-overlapping sets than the permitted parallelism, we will try to run all the + // individual tasks in parallel (including as parallelized compactions) so that they finish quickest and release + // any space they hold, and then reuse the compaction thread to run the next set of tasks. + return CompactionTasks.create(CompositeCompactionTask.applyParallelismLimit(tasks, permittedParallelism)); } + catch (Throwable t) + { + if (txn != null) + txn.close(); + throw rejectTasks(tasks, t); + } + } - return createCompactionTask(transaction, gcBefore).setUserDefined(true); + @Override + public void startup() + { + perform(super::startup, + () -> controller.startup(this, ScheduledExecutors.scheduledTasks)); } - /** - * Returns a compaction task to run next. - * - * This method is synchronized because task creation is significantly more expensive in UCS; the strategy is - * stateless, therefore it has to compute the shard/bucket structure on each call. - * - * @param gcBefore throw away tombstones older than this - */ @Override - public synchronized UnifiedCompactionTask getNextBackgroundTask(long gcBefore) + public void shutdown() + { + perform(super::shutdown, + controller::shutdown); + } + + /// Returns a collections of compaction tasks. + /// + /// This method is synchornized because task creation is significantly more expensive in UCS; the strategy is + /// stateless, therefore it has to compute the shard/bucket structure on each call. + /// + /// @param gcBefore throw away tombstones older than this + /// @return collection of AbstractCompactionTask, which could be either a CompactionTask or an UnifiedCompactionTask + @Override + public synchronized Collection getNextBackgroundTasks(long gcBefore) + { + // TODO - we should perhaps consider executing this code less frequently than legacy strategies + // since it's more expensive, and we should therefore prevent a second concurrent thread from executing at all + + // Repairs can leave behind sstables in pending repair state if they race with a compaction on those sstables. + // Both the repair and the compact process can't modify the same sstables set at the same time. So compaction + // is left to eventually move those sstables from FINALIZED repair sessions away from repair states. + Collection repairFinalizationTasks = ActiveRepairService + .instance() + .consistent + .local + .getZombieRepairFinalizationTasks(realm, realm.getLiveSSTables()); + if (!repairFinalizationTasks.isEmpty()) + return repairFinalizationTasks; + + // Expirations have to run before compaction (if run in parallel they may cause overlap tracker to leave + // unnecessary tombstones in place), so return only them if found. + Collection expirationTasks = getExpirationTasks(gcBefore); + if (expirationTasks != null) + return expirationTasks; + + return getNextBackgroundTasks(getNextCompactionAggregates(), gcBefore, null); + } + + /// Check for fully expired sstables and return a collection of expiration tasks if found. + public Collection getExpirationTasks(long gcBefore) + { + long ts = Clock.Global.currentTimeMillis(); + boolean expiredCheck = ts - lastExpiredCheck > controller.getExpiredSSTableCheckFrequency(); + if (!expiredCheck) + return null; + lastExpiredCheck = ts; + + var expired = getFullyExpiredSSTables(gcBefore); + if (expired.isEmpty()) + return null; + + if (logger.isDebugEnabled()) + logger.debug("Expiration check found {} fully expired SSTables", expired.size()); + + return createExpirationTasks(expired); + } + + /// Create expiration tasks for the given set of expired sstables. + /// Used by CNDB + public List createExpirationTasks(Set expired) { - while (true) + // if we found sstables to expire, split them to arenas to correctly isolate their repair status. + var tasks = new ArrayList(); + try { - CompactionPick pick = getNextCompactionPick(gcBefore); - if (pick == null) - return null; - UnifiedCompactionTask task = createCompactionTask(pick, gcBefore); - if (task != null) - return task; + for (var arena : getCompactionArenas(expired, (i1, i2) -> true)) + { + LifecycleTransaction txn = realm.tryModify(arena.sstables, OperationType.COMPACTION); + if (txn != null) + tasks.add(createExpirationTask(txn)); + else + logger.warn("Failed to submit expiration task because a transaction could not be created. If this happens frequently, it should be reported"); + } + return tasks; + } + catch (Throwable t) + { + throw rejectTasks(tasks, t); } } - private UnifiedCompactionTask createCompactionTask(CompactionPick pick, long gcBefore) + /// Get all expired sstables, regardless of expiration status. + /// This is simpler and faster than per-arena collection, and will find nothing in most calls. + /// Used by CNDB + public Set getFullyExpiredSSTables(long gcBefore) { - Preconditions.checkNotNull(pick); - Preconditions.checkArgument(!pick.isEmpty()); + return CompactionController.getFullyExpiredSSTables(realm, + getSuitableSSTables(), + realm::getOverlappingLiveSSTables, + gcBefore, + controller.getIgnoreOverlapsInExpirationCheck()); + } + + /// Used by CNDB where compaction aggregates come from etcd rather than the strategy. + /// @return collection of `AbstractCompactionTask`, which could be either a `CompactionTask` or a `UnifiedCompactionTask` + public synchronized Collection getNextBackgroundTasks(Collection aggregates, long gcBefore, + @Nullable CompactionObserver additionalObserver) + { + controller.onStrategyBackgroundTaskRequest(); + return createCompactionTasks(aggregates, gcBefore, additionalObserver); + } + + private Collection createCompactionTasks(Collection aggregates, long gcBefore, + @Nullable CompactionObserver additionalObserver) + { + Collection tasks = new ArrayList<>(aggregates.size()); + try + { + for (CompactionAggregate aggregate : aggregates) + createAndAddTasks(gcBefore, (CompactionAggregate.UnifiedAggregate) aggregate, tasks, additionalObserver); + + return tasks; + } + catch (Throwable t) + { + throw rejectTasks(tasks, t); + } + } - LifecycleTransaction transaction = cfs.getTracker().tryModify(pick, - OperationType.COMPACTION); + /// Create compaction tasks for the given aggregate and add them to the given tasks list. + public void createAndAddTasks(long gcBefore, CompactionAggregate.UnifiedAggregate aggregate, + Collection tasks, @Nullable CompactionObserver additionalObserver) + { + CompactionPick selected = aggregate.getSelected(); + int parallelism = aggregate.getPermittedParallelism(); + Preconditions.checkNotNull(selected); + Preconditions.checkArgument(!selected.isEmpty()); + + LifecycleTransaction transaction = realm.tryModify(selected.sstables(), + OperationType.COMPACTION, + selected.id()); if (transaction != null) { - return createCompactionTask(transaction, gcBefore); + try + { + // This will ignore the range of the operation, which is fine. + backgroundCompactions.setSubmitted(this, transaction.opId(), aggregate); + createAndAddTasks(gcBefore, transaction, aggregate.operationRange(), aggregate.keepOriginals(), getShardingStats(aggregate), parallelism, tasks, additionalObserver); + } + catch (Throwable e) + { + transaction.close(); + throw e; + } } else { - // This can happen e.g. due to a race with upgrade tasks. - logger.warn("Failed to submit compaction {} because a transaction could not be created. If this happens frequently, it should be reported", pick); - // This may be an indication of an SSTableReader reference leak. See CASSANDRA-18342. - return null; + // This can happen e.g. due to a race with upgrade tasks + logger.error("Failed to submit compaction {} because a transaction could not be created. If this happens frequently, it should be reported", aggregate); } } - /** - * Create the sstable writer used for flushing. - * - * @return an sstable writer that will split sstables into a number of shards as calculated by the controller for - * the expected flush density. - */ + /// Return the num of in-progress compactions tracked by UCS + public int getCompactionInProgress() + { + return backgroundCompactions.getCompactionsInProgress().size(); + } + + private static RuntimeException rejectTasks(Iterable tasks, Throwable error) + { + for (var task : tasks) + error = task.rejected(error); + throw Throwables.throwAsUncheckedException(error); + } + + public static class ShardingStats + { + public final PartitionPosition min; + public final PartitionPosition max; + public final long totalOnDiskSize; + public final double overheadToDataRatio; + public final double uniqueKeyRatio; + public final double density; + public final int shardCountForDensity; + public final int coveredShardCount; + + public ShardingStats(Collection sstables, ShardManager shardManager, Controller controller) + { + this(sstables, shardManager, getOverheadToDataRatio(sstables, controller), controller); + } + + /// Construct sharding statistics for the given collection of sstables that are to be compacted in full. + public ShardingStats(Collection sstables, ShardManager shardManager, double overheadToDataRatio, Controller controller) + { + assert !sstables.isEmpty(); + // the partition count aggregation is costly, so we only perform this once when the aggregate is selected for execution. + long onDiskLength = 0; + long partitionCountSum = 0; + PartitionPosition min = null; + PartitionPosition max = null; + boolean hasOnlySSTableReaders = true; + for (CompactionSSTable sstable : sstables) + { + onDiskLength += sstable.onDiskLength(); + partitionCountSum += sstable.estimatedKeys(); + min = min == null || min.compareTo(sstable.getFirst()) > 0 ? sstable.getFirst() : min; + max = max == null || max.compareTo(sstable.getLast()) < 0 ? sstable.getLast() : max; + if (!(sstable instanceof SSTableReader) + || ((SSTableReader) sstable).descriptor == null) // for tests + hasOnlySSTableReaders = false; + } + long estimatedPartitionCount; + if (hasOnlySSTableReaders) + estimatedPartitionCount = SSTableReader.getApproximateKeyCount(Iterables.filter(sstables, SSTableReader.class)); + else + estimatedPartitionCount = partitionCountSum; + + this.totalOnDiskSize = onDiskLength; + this.overheadToDataRatio = overheadToDataRatio; + this.uniqueKeyRatio = 1.0 * estimatedPartitionCount / partitionCountSum; + this.min = min; + this.max = max; + this.density = shardManager.density(onDiskLength, min, max, estimatedPartitionCount); + this.shardCountForDensity = controller.getNumShards(this.density * shardManager.shardSetCoverage()); + this.coveredShardCount = shardManager.coveredShardCount(min, max, shardCountForDensity); + } + + /// Construct sharding statistics for the given collection of sstables that are to be partially compacted + /// in the given operation range. Done by adjusting numbers by the fraction of the sstable that is in range. + public ShardingStats(Collection sstables, Range operationRange, ShardManager shardManager, double overheadToDataRatio, Controller controller) + { + assert !sstables.isEmpty(); + assert operationRange != null; + long onDiskLengthInRange = 0; + long partitionCountSum = 0; + long partitionCountSumInRange = 0; + PartitionPosition min = null; + PartitionPosition max = null; + boolean hasOnlySSTableReaders = true; + for (CompactionSSTable sstable : sstables) + { + PartitionPosition left = sstable.getFirst(); + PartitionPosition right = sstable.getLast(); + boolean extendsBefore = left.getToken().compareTo(operationRange.left) <= 0; + boolean extendsAfter = !operationRange.right.isMinimum() && right.getToken().compareTo(operationRange.right) > 0; + if (extendsBefore) + left = operationRange.left.nextValidToken().minKeyBound(); + if (extendsAfter) + right = operationRange.right.maxKeyBound(); + double fractionInRange = extendsBefore || extendsAfter + ? shardManager.rangeSpanned(left, right) / shardManager.rangeSpanned(sstable.getFirst(), sstable.getLast()) + : 1; + + onDiskLengthInRange += (long) (sstable.onDiskLength() * fractionInRange); + partitionCountSumInRange += (long) (sstable.estimatedKeys() * fractionInRange); + partitionCountSum += sstable.estimatedKeys(); + min = min == null || min.compareTo(left) > 0 ? left : min; + max = max == null || max.compareTo(right) < 0 ? right : max; + if (!(sstable instanceof SSTableReader) + || ((SSTableReader) sstable).descriptor == null) // for tests + hasOnlySSTableReaders = false; + } + long estimatedPartitionCount; + if (hasOnlySSTableReaders) + estimatedPartitionCount = SSTableReader.getApproximateKeyCount(Iterables.filter(sstables, SSTableReader.class)); + else + estimatedPartitionCount = partitionCountSum; + + this.min = min; + this.max = max; + this.totalOnDiskSize = onDiskLengthInRange; + this.overheadToDataRatio = overheadToDataRatio; + this.uniqueKeyRatio = 1.0 * estimatedPartitionCount / partitionCountSum; + this.density = shardManager.density(onDiskLengthInRange, min, max, (long) (partitionCountSumInRange * uniqueKeyRatio)); + this.shardCountForDensity = controller.getNumShards(this.density * shardManager.shardSetCoverage()); + this.coveredShardCount = shardManager.coveredShardCount(min, max, shardCountForDensity); + } + + /// Testing only, use specified values. + @VisibleForTesting + ShardingStats(PartitionPosition min, PartitionPosition max, long totalOnDiskSize, double overheadToDataRatio, double uniqueKeyRatio, double density, int shardCountForDensity, int coveredShardCount) + { + + this.min = min; + this.max = max; + this.totalOnDiskSize = totalOnDiskSize; + this.overheadToDataRatio = overheadToDataRatio; + this.uniqueKeyRatio = uniqueKeyRatio; + this.density = density; + this.shardCountForDensity = shardCountForDensity; + this.coveredShardCount = coveredShardCount; + } + } + + /// Get and store the sharding stats for a given aggregate + public ShardingStats getShardingStats(CompactionAggregate.UnifiedAggregate aggregate) + { + var shardingStats = aggregate.getShardingStats(); + if (shardingStats == null) + { + final Range operationRange = aggregate.operationRange(); + shardingStats = operationRange != null + ? new ShardingStats(aggregate.getSelected().sstables(), operationRange, getShardManager(), aggregate.getSelected().overheadToDataRatio(), controller) + : new ShardingStats(aggregate.getSelected().sstables(), getShardManager(), aggregate.getSelected().overheadToDataRatio(), controller); + aggregate.setShardingStats(shardingStats); + } + return shardingStats; + } + + ShardingStats makeShardingStats(ILifecycleTransaction txn) + { + return makeShardingStats(txn.originals()); + } + + ShardingStats makeShardingStats(Collection sstables) + { + return new ShardingStats(sstables, getShardManager(), controller); + } + + static double getOverheadToDataRatio(Collection sstables, Controller controller) + { + final long totSizeBytes = CompactionAggregate.getTotSizeBytes(sstables); + return controller.getOverheadSizeInBytes(sstables, totSizeBytes) / Math.max(1.0, totSizeBytes); + } + + void createAndAddTasks(long gcBefore, + LifecycleTransaction transaction, + ShardingStats shardingStats, + int parallelism, + Collection tasks) + { + createAndAddTasks(gcBefore, transaction, null, false, shardingStats, parallelism, tasks, null); + } + + @VisibleForTesting + void createAndAddTasks(long gcBefore, + LifecycleTransaction transaction, + Range operationRange, + boolean keepOriginals, + ShardingStats shardingStats, + int parallelism, + Collection tasks, + @Nullable CompactionObserver additionalObserver) + { + if (controller.parallelizeOutputShards() && parallelism > 1) + tasks.addAll(createParallelCompactionTasks(transaction, operationRange, keepOriginals, shardingStats, gcBefore, parallelism, additionalObserver)); + else + tasks.add(createCompactionTask(transaction, operationRange, keepOriginals, shardingStats, gcBefore, additionalObserver)); + } + + /// Create the sstable writer used for flushing. + /// + /// @return an sstable writer that will split sstables into a number of shards as calculated by the controller for + /// the expected flush density. @Override public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, @@ -269,11 +670,9 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, LifecycleNewTracker lifecycleNewTracker) { ShardManager shardManager = getShardManager(); - double flushDensity = cfs.metric.flushSizeOnDisk.get() * shardManager.shardSetCoverage() / shardManager.localSpaceCoverage(); - boolean supportsSharding = sstableLevel > 0 || indexGroups.stream().allMatch(Index.Group::supportsL0Shards); - int numShards = supportsSharding ? controller.getNumShards(flushDensity) : 1; - ShardTracker boundaries = shardManager.boundaries(numShards); - return new ShardedMultiWriter(cfs, + double flushDensity = realm.metrics().flushSizeOnDisk().get() * shardManager.shardSetCoverage() / shardManager.localSpaceCoverage(); + ShardTracker boundaries = shardManager.boundaries(controller.getFlushShards(flushDensity)); + return new ShardedMultiWriter(realm, descriptor, keyCount, repairedAt, @@ -286,113 +685,469 @@ public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, boundaries); } - /** - * Create the task that in turns creates the sstable writer used for compaction. - * - * @return a sharded compaction task that in turn will create a sharded compaction writer. - */ - private UnifiedCompactionTask createCompactionTask(LifecycleTransaction transaction, long gcBefore) + /// Create the task that in turns creates the sstable writer used for compaction. + /// + /// @return a sharded compaction task that in turn will create a sharded compaction writer. + private UnifiedCompactionTask createCompactionTask(LifecycleTransaction transaction, ShardingStats shardingStats, long gcBefore) { - return new UnifiedCompactionTask(cfs, this, transaction, gcBefore, getShardManager()); + return new UnifiedCompactionTask(realm, this, transaction, gcBefore, getShardManager(), shardingStats); } - private void maybeUpdateShardManager() + /// Create the task that in turns creates the sstable writer used for compaction. This version is for a ranged task, + /// where we produce outputs but cannot delete the input sstables until all components of the operation are complete. + /// + /// @return a sharded compaction task that in turn will create a sharded compaction writer. + private UnifiedCompactionTask createCompactionTask(LifecycleTransaction transaction, Range operationRange, boolean keepOriginals, ShardingStats shardingStats, long gcBefore, + @Nullable CompactionObserver additionalObserver) { - if (shardManager != null && !shardManager.isOutOfDate(StorageService.instance.getTokenMetadata().getRingVersion())) + UnifiedCompactionTask task = new UnifiedCompactionTask(realm, this, transaction, gcBefore, keepOriginals, getShardManager(), shardingStats, operationRange, transaction.originals(), null, null, null); + if (additionalObserver != null) + task.addObserver(additionalObserver); + return task; + } + + @Override + protected UnifiedCompactionTask createCompactionTask(final long gcBefore, LifecycleTransaction txn, boolean isMaximal, boolean splitOutput) + { + return createCompactionTask(txn, makeShardingStats(txn), gcBefore); + } + + @Override + public UnifiedCompactionTask createCompactionTask(LifecycleTransaction txn, final long gcBefore, long maxSSTableBytes) + { + return createCompactionTask(txn, makeShardingStats(txn), gcBefore); + } + + /// Create a collection of parallelized compaction tasks that perform the compaction in parallel. + private Collection createParallelCompactionTasks(LifecycleTransaction transaction, + Range operationRange, + boolean keepOriginals, + ShardingStats shardingStats, + long gcBefore, + int parallelism, + @Nullable CompactionObserver additionalObserver) + { + final int coveredShardCount = shardingStats.coveredShardCount; + assert parallelism > 1; + + Collection sstables = transaction.originals(); + ShardManager shardManager = getShardManager(); + CompositeLifecycleTransaction compositeTransaction = new CompositeLifecycleTransaction(transaction); + SharedCompactionProgress sharedProgress = new SharedCompactionProgress(transaction.opId(), transaction.opType(), TableOperation.Unit.BYTES); + SharedCompactionObserver sharedObserver = new SharedCompactionObserver(transaction.opId(), this, additionalObserver); + SharedTableOperation sharedOperation = new SharedTableOperation(sharedProgress); + List tasks = shardManager.splitSSTablesInShardsLimited( + sstables, + operationRange, + shardingStats.shardCountForDensity, + shardingStats.coveredShardCount, + parallelism, + (rangeSSTables, range) -> new UnifiedCompactionTask(realm, + this, + new PartialLifecycleTransaction(compositeTransaction), + gcBefore, + keepOriginals, + shardManager, + shardingStats, + range, + rangeSSTables, + sharedProgress, + sharedObserver, + sharedOperation) + ); + assert tasks.size() <= parallelism : "Task size: " + tasks.size() + " vs parallelism of: " + parallelism; + assert tasks.size() <= coveredShardCount : "Task size: " + tasks.size() + " vs covered shard count: " + coveredShardCount; + + if (tasks.isEmpty()) + transaction.close(); // this should not be reachable normally, close the transaction for safety + + if (tasks.size() == 1) // if there's just one range, make it a non-ranged task (to apply early open etc.) + { + // Reject the already constructed task, so that it is not tracked as an operation still expecting to be run. + compositeTransaction.cancelInitialization(); + sharedObserver.disableReportingOnComplete(); + UnifiedCompactionTask oneTask = tasks.get(0); + Throwables.maybeFail(oneTask.rejected(null)); + assert oneTask.inputSSTables().equals(sstables); + return Collections.singletonList(createCompactionTask(transaction, operationRange, keepOriginals, shardingStats, gcBefore, additionalObserver)); + } + else + { + compositeTransaction.completeInitialization(); + return tasks; + } + } + + private ExpirationTask createExpirationTask(LifecycleTransaction transaction) + { + return new ExpirationTask(realm, transaction); + } + + private void maybeUpdateSelector() + { + if (currentArenaSelector != null && !currentArenaSelector.diskBoundaries.isOutOfDate()) return; // the disk boundaries (and thus the local ranges too) have not changed since the last time we calculated synchronized (this) { - // Recheck after entering critical section, another thread may have beaten us to it. - while (shardManager == null || shardManager.isOutOfDate(StorageService.instance.getTokenMetadata().getRingVersion())) - shardManager = ShardManager.create(cfs); + if (currentArenaSelector != null && !currentArenaSelector.diskBoundaries.isOutOfDate()) + return; // another thread beat us to the update + + DiskBoundaries currentBoundaries = realm.getDiskBoundaries(); + var maybeShardManager = realm.buildShardManager(); + currentShardManager = maybeShardManager != null + ? maybeShardManager + : ShardManager.create(currentBoundaries, realm.getKeyspaceReplicationStrategy(), controller.isReplicaAware()); + currentArenaSelector = new ArenaSelector(controller, currentBoundaries); // Note: this can just as well be done without the synchronization (races would be benign, just doing some // redundant work). For the current usages of this blocking is fine and expected to perform no worse. } } - @VisibleForTesting - ShardManager getShardManager() + /// Get the current shard manager. Used internally, in tests and by CNDB. + public ShardManager getShardManager() { - maybeUpdateShardManager(); - return shardManager; + maybeUpdateSelector(); + return currentShardManager; } - /** - * Selects a compaction to run next. - */ - @VisibleForTesting - CompactionPick getNextCompactionPick(long gcBefore) + ArenaSelector getArenaSelector() { - SelectionContext context = new SelectionContext(controller); - List suitable = getCompactableSSTables(getSSTables(), UnifiedCompactionStrategy::isSuitableForCompaction); - Set expired = maybeGetExpiredSSTables(gcBefore, suitable); - suitable.removeAll(expired); + maybeUpdateSelector(); + return currentArenaSelector; + } - CompactionPick selected = chooseCompactionPick(suitable, context); - estimatedRemainingTasks = context.estimatedRemainingTasks; - if (selected == null) + private CompactionLimits getCurrentLimits(int maxConcurrentCompactions) + { + // Calculate the running compaction limits, i.e. the overall number of compactions permitted, which is either + // the compaction thread count, or the compaction throughput divided by the compaction rate (to prevent slowing + // down individual compaction progress). + String rateLimitLog = ""; + + // identify space limit + long spaceOverheadLimit = controller.maxCompactionSpaceBytes(); + + // identify throughput limit + double throughputLimit = controller.maxThroughput(); + int maxCompactions; + if (throughputLimit < Double.MAX_VALUE) { - if (expired.isEmpty()) - return null; + int maxCompactionsForThroughput; + + double compactionRate = backgroundCompactions.compactionRate.get(); + if (compactionRate > 0) + { + // Start as many as can saturate the limit, making sure to also account for compactions that have + // already been started but don't have progress yet. + + // Note: the throughput limit is adjusted here because the limiter won't let compaction proceed at more + // than the given rate, and small hiccups or rounding errors could cause this to go above the current + // running count when we are already at capacity. + // Allow up to 5% variability, or if we are permitted more than 20 concurrent compactions, one/maxcount + // so that we don't issue less tasks than we should. + double adjustment = Math.min(0.05, 1.0 / maxConcurrentCompactions); + maxCompactionsForThroughput = (int) Math.ceil(throughputLimit * (1 - adjustment) / compactionRate); + } else - return new CompactionPick(-1, -1, expired); + { + // If we don't have running compactions we don't know the effective rate. + // Allow only one compaction; this will be called again soon enough to recheck. + maxCompactionsForThroughput = 1; + } + + rateLimitLog = String.format(" rate-based limit %d (rate %s/%s)", + maxCompactionsForThroughput, + FBUtilities.prettyPrintMemoryPerSecond((long) compactionRate), + FBUtilities.prettyPrintMemoryPerSecond((long) throughputLimit)); + maxCompactions = Math.min(maxConcurrentCompactions, maxCompactionsForThroughput); + } + else + maxCompactions = maxConcurrentCompactions; + + // Now that we have a count, make sure it is spread close to equally among levels. In other words, reserve + // floor(permitted / levels) compactions for each level and don't permit more than ceil(permitted / levels) on + // any, to make sure that no level hogs all threads and thus lowest-level ops (which need to run more often but + // complete quickest) have a chance to run frequently. Also, running compactions can't go above the specified + // space overhead limit. + // To do this we count the number and size of already running compactions on each level and make sure any new + // ones we select satisfy these constraints. + int[] perLevel = new int[MAX_LEVELS]; + int levelCount = 1; // Start at 1 to avoid division by zero if the aggregates list is empty. + int runningCompactions = 0; + long spaceAvailable = spaceOverheadLimit; + int remainingAdaptiveCompactions = controller.getMaxRecentAdaptiveCompactions(); //limit for number of compactions triggered by new W value + if (remainingAdaptiveCompactions == -1) + remainingAdaptiveCompactions = Integer.MAX_VALUE; + for (CompactionPick compaction : backgroundCompactions.getCompactionsInProgress()) + { + final int level = levelOf(compaction); + if (level < 0) // expire-only compactions are allowed to run outside of the limits + continue; + ++perLevel[level]; + ++runningCompactions; + levelCount = Math.max(levelCount, level + 1); + spaceAvailable -= compaction.totalOverheadInBytes(); + if (controller.isRecentAdaptive(compaction)) + --remainingAdaptiveCompactions; } - selected.addAll(expired); - return selected; + CompactionLimits limits = new CompactionLimits(runningCompactions, + maxCompactions, + maxConcurrentCompactions, + perLevel, + levelCount, + spaceAvailable, + remainingAdaptiveCompactions); + logger.trace("Selecting up to {} new compactions of up to {}, concurrency limit {}{}", + Math.max(0, limits.maxCompactions - limits.runningCompactions), + FBUtilities.prettyPrintMemory(limits.spaceAvailable), + limits.maxConcurrentCompactions, + rateLimitLog); + return limits; } - private Set maybeGetExpiredSSTables(long gcBefore, List suitable) + private Collection updateLevelCountWithParentAndGetSelection(final CompactionLimits limits, + List pending) { - Set expired; - long ts = Clock.Global.currentTimeMillis(); - if (ts - lastExpiredCheck > controller.getExpiredSSTableCheckFrequency()) + long totalCompactionLimit = controller.maxCompactionSpaceBytes(); + int levelCount = limits.levelCount; + for (CompactionAggregate.UnifiedAggregate aggregate : pending) { - lastExpiredCheck = ts; - expired = CompactionController.getFullyExpiredSSTables(cfs, - suitable, - cfs.getOverlappingLiveSSTables(suitable), - gcBefore, - controller.getIgnoreOverlapsInExpirationCheck()); - if (logger.isTraceEnabled() && !expired.isEmpty()) - logger.trace("Expiration check for {}.{} found {} fully expired SSTables", - cfs.getKeyspaceName(), - cfs.getTableName(), - expired.size()); + warnIfSizeAbove(aggregate, totalCompactionLimit); + + // Make sure the level count includes all levels for which we have sstables (to be ready to compact + // as soon as the threshold is crossed)... + levelCount = Math.max(levelCount, aggregate.bucketIndex() + 1); + CompactionPick selected = aggregate.getSelected(); + if (selected != null) + { + // ... and also the levels that a layout-preserving selection would create. + levelCount = Math.max(levelCount, levelOf(selected) + 1); + } } - else - expired = Collections.emptySet(); - return expired; + int[] perLevel = limits.perLevel; + if (levelCount != perLevel.length) + perLevel = Arrays.copyOf(perLevel, levelCount); + + return getSelection(pending, + limits.maxCompactions, + perLevel, + limits.spaceAvailable, + limits.remainingAdaptiveCompactions); + } + + /// Selects compactions to run next. + /// + /// @return a subset of compaction aggregates to run next + private Collection getNextCompactionAggregates() + { + final CompactionLimits limits = getCurrentLimits(controller.maxConcurrentCompactions()); + + List pending = getPendingCompactionAggregates(limits.spaceAvailable); + setPendingCompactionAggregates(pending); + + return updateLevelCountWithParentAndGetSelection(limits, pending); } - private CompactionPick chooseCompactionPick(List suitable, SelectionContext context) + /// Selects compactions to run next from the passed aggregates. + /// + /// The intention here is to use this method directly from outside processes, to run compactions from a set + /// of pre-existing aggregates, that have been generated out of process. + /// + /// @param aggregates a collection of aggregates from which to select the next compactions + /// @param maxConcurrentCompactions the maximum number of concurrent compactions + /// @return a subset of compaction aggregates to run next + public Collection getNextCompactionAggregates(Collection aggregates, + int maxConcurrentCompactions) { - // Select the level with the highest overlap; when multiple levels have the same overlap, prefer the lower one - // (i.e. reduction of RA for bigger token coverage). - int maxOverlap = -1; - CompactionPick selected = null; - for (Level level : formLevels(suitable)) + final CompactionLimits limits = getCurrentLimits(maxConcurrentCompactions); + maybeUpdateSelector(); + return updateLevelCountWithParentAndGetSelection(limits, new ArrayList<>(aggregates)); + } + + /// Returns all pending compaction aggregates. + /// + /// This method is used by CNDB to find all pending compactions and put them to etcd. + /// + /// @return all pending compaction aggregates + public Collection getPendingCompactionAggregates() + { + return getPendingCompactionAggregates(controller.maxCompactionSpaceBytes()); + } + + /// Set the compaction aggregates passed in as pending in [BackgroundCompactions]. This ensures + /// that the compaction statistics will be accurate. + /// + /// This is called by [#getNextCompactionAggregates()] + /// and externally after calling [#getPendingCompactionAggregates()] + /// or before submitting tasks. + /// + /// Also, note that skipping the call to [#setPending(CompactionStrategy,Collection)] + /// would result in memory leaks: the aggregates added in [#setSubmitted(CompactionStrategy,TimeUUID,CompactionAggregate)] + /// would never be removed, and the aggregates hold references to the compaction tasks, so they retain a significant + /// size of heap memory. + /// + /// @param pending the aggregates that should be set as pending compactions + public void setPendingCompactionAggregates(Collection pending) + { + backgroundCompactions.setPending(this, pending); + } + + private List getPendingCompactionAggregates(long spaceAvailable) + { + maybeUpdateSelector(); + + List pending = new ArrayList<>(); + + for (Map.Entry> entry : getLevels().entrySet()) + { + Arena arena = entry.getKey(); + + for (Level level : entry.getValue()) + { + Collection aggregates = level.getCompactionAggregates(arena, controller, getShardManager(), spaceAvailable); + // Note: We allow empty aggregates into the list of pending compactions. The pending compactions list + // is for progress tracking only, and it is helpful to see empty levels there. + pending.addAll(aggregates); + } + } + + return pending; + } + + /// This method logs a warning related to the fact that the space overhead limit also applies when a + /// single compaction is above that limit. This should prevent running out of space at the expense of ending up + /// with several extra sstables at the highest-level (compared to the number of sstables that we should have + /// as per config of the strategy), i.e. slightly higher read amplification. This is a sensible tradeoff but + /// the operators must be warned if this happens, and that's the purpose of this warning. + private void warnIfSizeAbove(CompactionAggregate.UnifiedAggregate aggregate, long spaceOverheadLimit) + { + if (aggregate.getSelected().totalOverheadInBytes() > spaceOverheadLimit) + logger.warn("Compaction needs to perform an operation that is bigger than the current space overhead " + + "limit - size {} (compacting {} sstables in arena {}/bucket {}); limit {} = {}% of dataset size {}. " + + "To honor the limit, this operation will not be performed, which may result in degraded performance.\n" + + "Please verify the compaction parameters, specifically {} and {}.", + FBUtilities.prettyPrintMemory(aggregate.getSelected().totalOverheadInBytes()), + aggregate.getSelected().sstables().size(), + aggregate.getArena().name(), + aggregate.bucketIndex(), + FBUtilities.prettyPrintMemory(spaceOverheadLimit), + controller.getMaxSpaceOverhead() * 100, + FBUtilities.prettyPrintMemory(controller.getDataSetSizeBytes()), + Controller.DATASET_SIZE_OPTION, + Controller.MAX_SPACE_OVERHEAD_OPTION); + } + + /// Returns a selection of the compactions to be submitted. The selection will be chosen so that the total + /// number of compactions is at most totalCount, where each level gets a share that is the whole part of the ratio + /// between the total permitted number of compactions, and the remainder gets distributed among the levels + /// according to the preferences of the [#prioritize] method. Usually this means preferring + /// compaction picks with a higher max overlap, with a random selection when multiple picks have the same maximum. + /// Note that if a level does not have tasks to fill its share, its quota will remain unused in this + /// allocation. + /// + /// The selection also limits the size of the newly scheduled compactions to be below spaceAvailable by not + /// scheduling compactions if they would push the combined size above that limit. + /// + /// @param pending list of all current aggregates with possible selection for each bucket + /// @param totalCount maximum number of compactions permitted to run + /// @param perLevel int array with the number of in-progress compactions per level + /// @param spaceAvailable amount of space in bytes available for the new compactions + /// @param remainingAdaptiveCompactions number of adaptive compactions (i.e. ones triggered by scaling parameter + /// change by the adaptive controller) that can still be scheduled + List getSelection(List pending, + int totalCount, + int[] perLevel, + long spaceAvailable, + int remainingAdaptiveCompactions) + { + Controller controller = getController(); + Reservations reservations = Reservations.create(totalCount, + perLevel, + controller.getReservedThreads(), + controller.getReservationsType()); + // If the inclusion method is not transitive, we may have multiple buckets/selections for the same sstable. + boolean shouldCheckSSTableSelected = controller.overlapInclusionMethod() != Overlaps.InclusionMethod.TRANSITIVE; + // If so, make sure we only select one such compaction. + Set selectedSSTables = shouldCheckSSTableSelected ? new HashSet<>() : null; + + int remaining = totalCount; + for (int countInLevel : perLevel) + remaining -= countInLevel; + + // Note: if we are in the middle of changes in the parameters or level count, remainder might become negative. + // This is okay, some buckets will temporarily not get their rightful share until these tasks complete. + + // Let the controller prioritize the compactions. + pending = controller.prioritize(pending); + int proposed = 0; + + // Select the first ones, permitting only the specified number per level. + List selected = new ArrayList<>(pending.size()); + for (CompactionAggregate.UnifiedAggregate aggregate : pending) { - CompactionPick pick = level.getCompactionPick(context); - int levelOverlap = level.maxOverlap; - if (levelOverlap > maxOverlap) + if (remaining == 0) + break; // no threads to allocate from + + final CompactionPick pick = aggregate.getSelected(); + if (pick.isEmpty()) + continue; + + ++proposed; + long overheadSizeInBytes = pick.totalOverheadInBytes(); + if (overheadSizeInBytes > spaceAvailable) { - maxOverlap = levelOverlap; - selected = pick; + getBackgroundCompactions().incrementSkippedAggregatesDueToDiskSpace(); + continue; // compaction is too large for current cycle } + + int currentLevel = levelOf(pick); + boolean isAdaptive = controller.isRecentAdaptive(pick); + // avoid computing sharding stats if are not going to schedule the compaction at all + if (!reservations.hasRoom(currentLevel)) + continue; // honor the reserved thread counts + if (isAdaptive && remainingAdaptiveCompactions <= 0) + continue; // do not allow more than remainingAdaptiveCompactions to limit latency spikes upon changing W + if (shouldCheckSSTableSelected && !Collections.disjoint(selectedSSTables, pick.sstables())) + continue; // do not allow multiple selections of the same sstable + + int parallelism = controller.parallelizeOutputShards() ? getShardingStats(aggregate).coveredShardCount : 1; + if (parallelism > remaining) + parallelism = remaining; + assert currentLevel >= 0 : "Invalid level in " + pick; + + if (isAdaptive) + { + if (parallelism > remainingAdaptiveCompactions) + { + parallelism = remainingAdaptiveCompactions; + assert parallelism > 0; // we checked the remainingAdaptiveCompactions in advance + } + } + + parallelism = reservations.accept(currentLevel, parallelism); + assert parallelism > 0; // we checked hasRoom in advance, there must always be at least one thread to use + + // Note: the reservations tracker assumes it is the last check and a pick is accepted if it returns true. + + if (isAdaptive) + remainingAdaptiveCompactions -= parallelism; + remaining -= parallelism; + spaceAvailable -= overheadSizeInBytes; + aggregate.setPermittedParallelism(parallelism); + selected.add(aggregate); + if (shouldCheckSSTableSelected) + selectedSSTables.addAll(pick.sstables()); } - if (logger.isDebugEnabled() && selected != null) - logger.debug("Selected compaction on level {} overlap {} sstables {}", - selected.level, selected.overlap, selected.size()); + reservations.debugOutput(selected.size(), proposed, remaining); return selected; } @Override public int getEstimatedRemainingTasks() { - return estimatedRemainingTasks; + return backgroundCompactions.getEstimatedRemainingTasks(); } @Override @@ -401,133 +1156,306 @@ public long getMaxSSTableBytes() return Long.MAX_VALUE; } + @Override + public Set getSSTables() + { + return realm.getLiveSSTables(); + } + @VisibleForTesting + public int getW(int index) + { + return controller.getScalingParameter(index); + } + public Controller getController() { return controller; } - public static boolean isSuitableForCompaction(SSTableReader rdr) + /// Group candidate sstables into compaction arenas. + /// Each compaction arena is obtained by comparing using a compound comparator for the equivalence classes + /// configured in the arena selector of this strategy. + /// + /// @param sstables a collection of the sstables to be assigned to arenas + /// @param compactionFilter a bifilter (sstable, isCompacting) to include CompactionSSTables suitable for compaction + /// @return a list of arenas, where each arena contains sstables that belong to that arena + public Collection getCompactionArenas(Collection sstables, + BiPredicate compactionFilter) { - return !rdr.isMarkedSuspect() && rdr.openReason != SSTableReader.OpenReason.EARLY; + return getCompactionArenas(sstables, compactionFilter, getArenaSelector()); } - @Override - public synchronized void addSSTable(SSTableReader added) + Collection getCompactionArenas(Collection sstables, + BiPredicate compactionFilter, + ArenaSelector arenaSelector) { - sstables.add(added); + Map arenasBySSTables = new TreeMap<>(arenaSelector); + Set compacting = realm.getCompactingSSTables(); + for (CompactionSSTable sstable : sstables) + if (compactionFilter.test(sstable, compacting.contains(sstable))) + arenasBySSTables.computeIfAbsent(sstable, t -> new Arena(arenaSelector)) + .add(sstable); + + return arenasBySSTables.values(); } - @Override - public synchronized void removeSSTable(SSTableReader sstable) + @SuppressWarnings("unused") // used by CNDB to deserialize aggregates + public Arena getCompactionArena(Collection sstables) { - sstables.remove(sstable); + Arena arena = new Arena(getArenaSelector()); + for (CompactionSSTable table : sstables) + arena.add(table); + return arena; } - @Override - protected synchronized Set getSSTables() + @SuppressWarnings("unused") // used by CNDB to deserialize aggregates + public Level getLevel(int index, double min, double max) { - // Filter the set of sstables through the live set. This is to ensure no zombie sstables are picked for - // compaction (see CASSANDRA-18342). - return ImmutableSet.copyOf(Iterables.filter(cfs.getLiveSSTables(), sstables::contains)); + return new Level(controller, index, min, max); } - /** - * @return a list of the levels in the compaction hierarchy - */ + /// @return a LinkedHashMap of arenas with buckets where order of arenas are preserved @VisibleForTesting - List getLevels() + Map> getLevels() { - return getLevels(getSSTables(), UnifiedCompactionStrategy::isSuitableForCompaction); + return getLevels(Sets.newHashSet(realm.getSSTables(SSTableSet.NONCOMPACTING)), UnifiedCompactionStrategy::isSuitableForCompaction); } - /** - * Groups the sstables passed in into levels. This is used by the strategy to determine - * new compactions, and by external tools to analyze the strategy decisions. - * - * @param sstables a collection of the sstables to be assigned to levels - * @param compactionFilter a filter to exclude CompactionSSTables, - * e.g., {@link #isSuitableForCompaction} - * - * @return a list of the levels in the compaction hierarchy - */ - public List getLevels(Collection sstables, - Predicate compactionFilter) + private static boolean isSuitableForCompaction(CompactionSSTable sstable, boolean isCompacting) { - List suitable = getCompactableSSTables(sstables, compactionFilter); - return formLevels(suitable); + return sstable.isSuitableForCompaction() && !isCompacting; } - private List formLevels(List suitable) + Iterable getSuitableSSTables() { - maybeUpdateShardManager(); - List levels = new ArrayList<>(MAX_LEVELS); - suitable.sort(shardManager::compareByDensity); + return getFilteredSSTables(UnifiedCompactionStrategy::isSuitableForCompaction); + } - double maxDensity = controller.getMaxLevelDensity(0, controller.getBaseSstableSize(controller.getFanout(0)) / shardManager.localSpaceCoverage()); - int index = 0; - Level level = new Level(controller, index, 0, maxDensity); - for (SSTableReader candidate : suitable) - { - final double density = shardManager.density(candidate); - if (density < level.max) - { - level.add(candidate); - continue; - } + Iterable getFilteredSSTables(BiPredicate predicate) + { + return Iterables.filter(realm.getSSTables(SSTableSet.NONCOMPACTING), s -> predicate.test(s, false)); + } - level.complete(); - levels.add(level); // add even if empty + /// Groups the sstables passed in into arenas and buckets. This is used by the strategy to determine + /// new compactions, and by external tools in CNDB to analyze the strategy decisions. + /// + /// @param sstables a collection of the sstables to be assigned to arenas + /// @param compactionFilter a bifilter(sstable, isCompacting) to include CompactionSSTables, + /// e.g., [#isSuitableForCompaction()] + /// + /// @return a map of arenas to their buckets + public Map> getLevels(Collection sstables, + BiPredicate compactionFilter) + { + // Copy to avoid race condition + var currentShardManager = getShardManager(); + Collection arenas = getCompactionArenas(sstables, compactionFilter); + Map> ret = new LinkedHashMap<>(); // should preserve the order of arenas - while (true) + for (Arena arena : arenas) + { + List levels = new ArrayList<>(MAX_LEVELS); + + // Precompute the density, then sort. + List ssTableWithDensityList = new ArrayList<>(arena.sstables.size()); + for (CompactionSSTable sstable : arena.sstables) + ssTableWithDensityList.add(new SSTableWithDensity(sstable, currentShardManager.density(sstable))); + Collections.sort(ssTableWithDensityList); + + double maxSize = controller.getMaxLevelDensity(0, controller.getBaseSstableSize(controller.getFanout(0)) / currentShardManager.localSpaceCoverage()); + int index = 0; + Level level = new Level(controller, index, 0, maxSize); + for (SSTableWithDensity candidateWithDensity : ssTableWithDensityList) { - ++index; - double minDensity = maxDensity; - maxDensity = controller.getMaxLevelDensity(index, minDensity); - level = new Level(controller, index, minDensity, maxDensity); - if (density < level.max) + final CompactionSSTable candidate = candidateWithDensity.sstable; + final double size = candidateWithDensity.density; + if (size < level.max) { level.add(candidate); - break; + continue; } - else + + level.complete(); + levels.add(level); // add even if empty + + while (true) { - levels.add(level); // add the empty level + ++index; + double minSize = maxSize; + maxSize = controller.getMaxLevelDensity(index, minSize); + level = new Level(controller, index, minSize, maxSize); + if (size < level.max) + { + level.add(candidate); + break; + } + else + { + levels.add(level); // add the empty level + } } } - } - if (!level.sstables.isEmpty()) - { - level.complete(); - levels.add(level); + if (!level.sstables.isEmpty()) + { + level.complete(); + levels.add(level); + } + + if (!levels.isEmpty()) + ret.put(arena, levels); + + if (logger.isTraceEnabled()) + logger.trace("Arena {} has {} levels", arena, levels.size()); } - return levels; + logger.trace("Found {} arenas with buckets for {}.{}", ret.size(), realm.getKeyspaceName(), realm.getTableName()); + return ret; } - private List getCompactableSSTables(Collection sstables, - Predicate compactionFilter) + /** + * Creates a map of maximum overlap, organized as a map from arena:level to the maximum number of sstables that + * overlap in that level, as well as a list showing the per-shard maximum overlap. + * + * The number of shards to list is calculated based on the maximum density of the sstables in the realm. + */ + @Override + public Map getMaxOverlapsMap() { - Set compacting = cfs.getTracker().getCompacting(); - List suitable = new ArrayList<>(sstables.size()); - for (SSTableReader rdr : sstables) + final Set liveSSTables = Sets.newHashSet(realm.getSSTables(SSTableSet.NONCOMPACTING)); + Map> arenas = + getLevels(liveSSTables, (i1, i2) -> true); // take all sstables + + ShardManager shardManager = getShardManager(); + Map map = new LinkedHashMap<>(); + + // max general overlap (max # of sstables per query) + map.put("all", getMaxOverlapsPerShardString(liveSSTables, shardManager)); + + for (var arena : arenas.entrySet()) { - if (compactionFilter.test(rdr) && !compacting.contains(rdr)) - suitable.add(rdr); + final String arenaName = arena.getKey().name(); + for (var level : arena.getValue()) + map.put(arenaName + "-L" + level.getIndex(), getMaxOverlapsPerShardString(level.getSSTables(), shardManager)); } - return suitable; + return map; + } + + private String getMaxOverlapsPerShardString(Collection sstables, ShardManager shardManager) + { + // Find the sstable with the biggest density to define the shard count. + // This is better than using a level's max bound as that will show more shards than there actually are. + double maxDensity = 0; + for (CompactionSSTable liveSSTable : sstables) + maxDensity = Math.max(maxDensity, shardManager.density(liveSSTable)); + int shardCount = controller.getNumShards(maxDensity); + + int[] overlapsMap = getMaxOverlapsPerShard(sstables, shardManager, shardCount); + int max = 0; + for (int i : overlapsMap) + max = Math.max(max, i); + return max + " (per shard: " + Arrays.toString(overlapsMap) + ")"; + } + + public static int[] getMaxOverlapsPerShard(Collection sstables, ShardManager shardManager, int shardCount) + { + int[] overlapsMap = new int[shardCount]; + shardManager.assignSSTablesToShardIndexes(sstables, null, shardCount, + (shardSSTables, shard) -> + // Note: the shard index we are given is the global index, which includes + // other arenas. The modulo below converts it to an index for the arena. + // If an sstable extends outside a disk's region (because e.g. local + // ownership changed and disk boundaries moved), it will be incorrectly + // counted. This is not trivial to recognize here and is not corrected. + overlapsMap[shard % shardCount] = Overlaps.maxOverlap(shardSSTables, + CompactionSSTable.startsAfter, + CompactionSSTable.firstKeyComparator, + CompactionSSTable.lastKeyComparator)); + // Indexes that do not have sstables are left with 0 overlaps. + return overlapsMap; + } + + @Override + public int getLevel(ILifecycleTransaction txn) + { + CompactionPick pick = backgroundCompactions.getCompaction(txn.opId()); + if (pick != null) + return (int) pick.parent(); + + return -1; + } + + private static int levelOf(CompactionPick pick) + { + return (int) pick.parent(); } public TableMetadata getMetadata() { - return cfs.metadata(); + return realm.metadata(); } - private static boolean startsAfter(SSTableReader a, SSTableReader b) + CompactionPick createPick(TimeUUID id, long parent, Collection sstables) { - // Strict comparison because the span is end-inclusive. - return a.getFirst().compareTo(b.getLast()) > 0; + return createPick(controller, id, parent, sstables); + } + + static CompactionPick createPick(Controller controller, TimeUUID id, long parent, Collection sstables) + { + long totalDataSize = CompactionAggregate.getTotSizeBytes(sstables); + long totalSpaceOverhead = controller.getOverheadSizeInBytes(sstables, totalDataSize); + return CompactionPick.create(id, + parent, + sstables, + Collections.emptyList(), + 0, + totalDataSize / Math.max(sstables.size(), 1), + totalDataSize, + totalSpaceOverhead); + } + + /// A compaction arena contains the list of sstables that belong to this arena as well as the arena + /// selector used for comparison. + public static class Arena implements Comparable + { + final List sstables; + final ArenaSelector selector; + + Arena(ArenaSelector selector) + { + this.sstables = new ArrayList<>(); + this.selector = selector; + } + + void add(CompactionSSTable ssTableReader) + { + sstables.add(ssTableReader); + } + + public String name() + { + CompactionSSTable t = sstables.get(0); + return selector.name(t); + } + + @Override + public int compareTo(Arena o) + { + return selector.compare(this.sstables.get(0), o.sstables.get(0)); + } + + @Override + public String toString() + { + return String.format("%s, %d sstables", name(), sstables.size()); + } + + @VisibleForTesting + public List getSSTables() + { + return sstables; + } } @Override @@ -536,12 +1464,10 @@ public String toString() return String.format("Unified strategy %s", getMetadata()); } - /** - * A level: index, sstables and some properties. - */ + /// A level: index, sstables and some properties. public static class Level { - final List sstables; + final List sstables; final int index; final double survivalFactor; final int scalingParameter; // scaling parameter used to calculate fanout and threshold @@ -549,22 +1475,33 @@ public static class Level final int threshold; // number of SSTables that trigger a compaction final double min; // min density of sstables for this level final double max; // max density of sstables for this level - int maxOverlap = -1; // maximum number of overlapping sstables, i.e. maximum number of sstables that need - // to be queried on this level for any given key + double avg = 0; // avg size of sstables in this level + int maxOverlap = -1; // maximum number of overlapping sstables - Level(Controller controller, int index, double minSize, double maxSize) + Level(int index, int scalingParameter, int fanout, int threshold, double survivalFactor, double min, double max) { this.index = index; - this.survivalFactor = controller.getSurvivalFactor(index); - this.scalingParameter = controller.getScalingParameter(index); - this.fanout = controller.getFanout(index); - this.threshold = controller.getThreshold(index); + this.scalingParameter = scalingParameter; + this.fanout = fanout; + this.threshold = threshold; + this.survivalFactor = survivalFactor; + this.min = min; + this.max = max; this.sstables = new ArrayList<>(threshold); - this.min = minSize; - this.max = maxSize; } - public Collection getSSTables() + Level(Controller controller, int index, double min, double max) + { + this(index, + controller.getScalingParameter(index), + controller.getFanout(index), + controller.getThreshold(index), + controller.getSurvivalFactor(index), + min, + max); + } + + public Collection getSSTables() { return sstables; } @@ -574,9 +1511,41 @@ public int getIndex() return index; } - void add(SSTableReader sstable) + // The stats below are set up by getLevels and useful for diagnostics. + public int getFanout() + { + return fanout; + } + + public int getThreshold() + { + return threshold; + } + + public double getMinDensity() + { + return min; + } + + public double getMaxDensity() + { + return max; + } + + public double getAverageSSTableSize() + { + return avg; + } + + // We don't expose max overlap as it's not valid until getCompactionAggregates gets called. + + void add(CompactionSSTable sstable) { this.sstables.add(sstable); + // consider size of all components to reduce chance of out-of-disk + long size = CassandraRelevantProperties.UCS_COMPACTION_INCLUDE_NON_DATA_FILES_SIZE.getBoolean() + ? sstable.onDiskComponentsSize() : sstable.onDiskLength(); + this.avg += (size - avg) / sstables.size(); } void complete() @@ -585,100 +1554,143 @@ void complete() logger.trace("Level: {}", this); } - /** - * Return the compaction pick for this level. - *

    - * This is done by splitting the level into buckets that we can treat as independent regions for compaction. - * We then use the maxOverlap value (i.e. the maximum number of sstables that can contain data for any covered - * key) of each bucket to determine if compactions are needed, and to prioritize the buckets that contribute - * most to the complexity of queries: if maxOverlap is below the level's threshold, no compaction is needed; - * otherwise, we choose one from the buckets that have the highest maxOverlap. - */ - CompactionPick getCompactionPick(SelectionContext context) + private List getOversizeShardsAggregates(Arena arena, + Controller controller, + ShardManager shardManager) { - List buckets = getBuckets(context); - if (buckets == null) - { - if (logger.isDebugEnabled()) - logger.debug("Level {} sstables {} max overlap {} buckets with compactions {} tasks {}", - index, sstables.size(), maxOverlap, 0, 0); - return null; // nothing crosses the threshold in this level, nothing to do - } - - int estimatedRemainingTasks = 0; - int overlapMatchingCount = 0; - Bucket selectedBucket = null; - Controller controller = context.controller; - for (Bucket bucket : buckets) + List aggregates = new ArrayList<>(); + double shardThreshold = fanout * controller.getMaxSstablesPerShardFactor(); + if (sstables.size() > shardThreshold) { - // We can have just one pick in each level. Pick one bucket randomly out of the ones with - // the highest overlap. - // The random() part below implements reservoir sampling with size 1, giving us a uniformly random selection. - if (bucket.maxOverlap == maxOverlap && controller.random().nextInt(++overlapMatchingCount) == 0) - selectedBucket = bucket; - // The estimated remaining tasks is a measure of the remaining amount of work, thus we prefer to - // calculate the number of tasks we would do in normal operation, even though we may compact in bigger - // chunks when we are late. - estimatedRemainingTasks += bucket.maxOverlap / threshold; - } - context.estimatedRemainingTasks += estimatedRemainingTasks; - assert selectedBucket != null; - - if (logger.isDebugEnabled()) - logger.debug("Level {} sstables {} max overlap {} buckets with compactions {} tasks {}", - index, sstables.size(), maxOverlap, buckets.size(), estimatedRemainingTasks); + List> groups = shardManager.splitSSTablesInShards(sstables, + controller.getNumShards(max), + (sstableShard, shardRange) -> Sets.newHashSet(sstableShard)); - CompactionPick selected = selectedBucket.constructPick(controller); + Set sstablesInOversizeGroup = new HashSet<>(); + for (Set ssTables : groups) + { + if (ssTables.size() > shardThreshold) + { + sstablesInOversizeGroup.addAll(ssTables); + } + } - if (logger.isTraceEnabled()) - logger.trace("Returning compaction pick with selected compaction {}", - selected); - return selected; + if (!sstablesInOversizeGroup.isEmpty()) + { + // Now combine the groups that share an sstable so that we have valid independent transactions. + // Only keep the groups that were combined with an oversize group. + groups = Overlaps.combineSetsWithCommonElement(groups); + List unbucketed = new ArrayList<>(); + + for (Set group : groups) + { + boolean inOverSizeGroup = false; + for (CompactionSSTable sstable : group) + { + if (sstablesInOversizeGroup.contains(sstable)) + { + inOverSizeGroup = true; + break; + } + } + if (inOverSizeGroup) + { + aggregates.add( + CompactionAggregate.createUnified(group, + Overlaps.maxOverlap(group, + CompactionSSTable.startsAfter, + CompactionSSTable.firstKeyComparator, + CompactionSSTable.lastKeyComparator), + createPick(controller, nextTimeUUID(), index, group), + Collections.emptyList(), + arena, + this) + ); + } + else + { + unbucketed.addAll(group); + } + } + // Add all unbucketed sstables separately. Note that this will list the level (with its set of sstables) + // even if it does not need compaction. + if (!unbucketed.isEmpty()) + aggregates.add(CompactionAggregate.createUnified(unbucketed, + maxOverlap, + CompactionPick.EMPTY, + Collections.emptySet(), + arena, + this)); + return aggregates; + } + } + return aggregates; } - /** - * Group the sstables in this level into buckets. - *

    - * The buckets are formed by grouping sstables that overlap at some key together, and then expanded to cover - * any overlapping sstable according to the overlap inclusion method. With the usual TRANSITIVE method this - * results into non-overlapping buckets that can't affect one another and can be compacted in parallel without - * any loss of efficiency. - *

    - * Other overlap inclusion methods are provided to cover situations where we may be okay with compacting - * sstables partially and doing more than the strictly necessary amount of compaction to solve a problem: e.g. - * after an upgrade from LCS where transitive overlap may cause a complete level to be compacted together - * (creating an operation that will take a very long time to complete) and we want to make some progress as - * quickly as possible at the cost of redoing some work. - *

    - * The number of sstables that overlap at some key defines the "overlap" of a set of sstables. The maximum such - * value in the bucket is its "maxOverlap", i.e. the highest number of sstables we need to read to find the - * data associated with a given key. - */ - @VisibleForTesting - List getBuckets(SelectionContext context) + /// Return the compaction aggregate + Collection getCompactionAggregates(Arena arena, + Controller controller, + ShardManager shardManager, + long spaceAvailable) { - List liveSet = sstables; - if (logger.isTraceEnabled()) - logger.trace("Creating compaction pick with live set {}", liveSet); + logger.trace("Creating compaction aggregate with sstable set {}", sstables); - List> overlaps = Overlaps.constructOverlapSets(liveSet, - UnifiedCompactionStrategy::startsAfter, - SSTableReader.firstKeyComparator, - SSTableReader.lastKeyComparator); - for (Set overlap : overlaps) + List aggregates = new ArrayList<>(); + + if (sstables.isEmpty()) + { + if (logger.isTraceEnabled()) + logger.trace("No sstables in level {} of arena {}, skipping compaction", this, arena); + return aggregates; + } + + // Note that adjacent overlap sets may include deduplicated sstable + List> overlaps = Overlaps.constructOverlapSets(sstables, + CompactionSSTable.startsAfter, + CompactionSSTable.firstKeyComparator, + CompactionSSTable.lastKeyComparator); + for (Set overlap : overlaps) maxOverlap = Math.max(maxOverlap, overlap.size()); - if (maxOverlap < threshold) - return null; + List unbucketed = new ArrayList<>(); List buckets = Overlaps.assignOverlapsIntoBuckets(threshold, - context.controller.overlapInclusionMethod(), + controller.overlapInclusionMethod(), overlaps, - this::makeBucket); - return buckets; + this::makeBucket, + unbucketed::addAll); + + if (!buckets.isEmpty()) + { + for (Bucket bucket : buckets) + aggregates.add(bucket.constructAggregate(controller, spaceAvailable, arena)); + } + else + { + // CNDB-14577: If there are no overlaps, we look if some shards have too many SSTables. + // If that's the case, we perform a major compaction on those shards. + List oversizeShardsAggregates = getOversizeShardsAggregates(arena, controller, shardManager); + if (!oversizeShardsAggregates.isEmpty()) + return oversizeShardsAggregates; + } + + // Add all unbucketed sstables separately. Note that this will list the level (with its set of sstables) + // even if it does not need compaction. + if (!unbucketed.isEmpty()) + aggregates.add(CompactionAggregate.createUnified(unbucketed, + maxOverlap, + CompactionPick.EMPTY, + Collections.emptySet(), + arena, + this)); + + if (logger.isTraceEnabled()) + logger.trace("Returning compaction aggregates {} for level {} of arena {}", + aggregates, this, arena); + return aggregates; } - private Bucket makeBucket(List> overlaps, int startIndex, int endIndex) + private Bucket makeBucket(List> overlaps, int startIndex, int endIndex) { return endIndex == startIndex + 1 ? new SimpleBucket(this, overlaps.get(startIndex)) @@ -705,187 +1717,317 @@ private String densityAsString(double density) } } - - /** - * A compaction bucket, i.e. a selection of overlapping sstables from which a compaction should be selected. - */ + /// A compaction bucket, i.e. a selection of overlapping sstables from which a compaction should be selected. static abstract class Bucket { final Level level; - final List allSSTablesSorted; + final List allSSTablesSorted; final int maxOverlap; - Bucket(Level level, Collection allSSTablesSorted, int maxOverlap) + Bucket(Level level, Collection allSSTablesSorted, int maxOverlap) { // single section this.level = level; this.allSSTablesSorted = new ArrayList<>(allSSTablesSorted); - this.allSSTablesSorted.sort(SSTableReader.maxTimestampDescending); // we remove entries from the back + this.allSSTablesSorted.sort(CompactionSSTable.maxTimestampDescending); // we remove entries from the back this.maxOverlap = maxOverlap; } - Bucket(Level level, List> overlapSections) + Bucket(Level level, List> overlapSections) { // multiple sections this.level = level; int maxOverlap = 0; - Set all = new HashSet<>(); - for (Set section : overlapSections) + Set all = new HashSet<>(); + for (Set section : overlapSections) { maxOverlap = Math.max(maxOverlap, section.size()); all.addAll(section); } this.allSSTablesSorted = new ArrayList<>(all); - this.allSSTablesSorted.sort(SSTableReader.maxTimestampDescending); // we remove entries from the back + this.allSSTablesSorted.sort(CompactionSSTable.maxTimestampDescending); // we remove entries from the back this.maxOverlap = maxOverlap; } - /** - * Select compactions from this bucket. Normally this would form a compaction out of all sstables in the - * bucket, but if compaction is very late we may prefer to act more carefully: - * - we should not use more inputs than the permitted maximum - * - we should select SSTables in a way that preserves the structure of the compaction hierarchy - * These impose a limit on the size of a compaction; to make sure we always reduce the read amplification by - * this much, we treat this number as a limit on overlapping sstables, i.e. if A and B don't overlap with each - * other but both overlap with C and D, all four will be selected to form a limit-three compaction. A limit-two - * one may choose CD, ABC or ABD. - * Also, the subset is selected by max timestamp order, oldest first, to avoid violating sstable time order. In - * the example above, if B is oldest and C is older than D, the limit-two choice would be ABC (if A is older - * than D) or BC (if A is younger, avoiding combining C with A skipping D). - * - * @param controller The compaction controller. - * @return A compaction pick to execute next. - */ - CompactionPick constructPick(Controller controller) + /// Select compactions from this bucket. Normally this would form a compaction out of all sstables in the + /// bucket, but if compaction is very late we may prefer to act more carefully: + /// - we should not use more inputs than the permitted maximum + /// - we should not select a compaction whose execution will use more temporary space than is available + /// - we should select SSTables in a way that preserves the structure of the compaction hierarchy + /// These impose a limit on the size of a compaction; to make sure we always reduce the read amplification by + /// this much, we treat this number as a limit on overlapping sstables, i.e. if A and B don't overlap with each + /// other but both overlap with C and D, all four will be selected to form a limit-three compaction. A limit-two + /// one may choose CD, ABC or ABD. + /// Also, the subset is selected by max timestamp order, oldest first, to avoid violating sstable time order. In + /// the example above, if B is oldest and C is older than D, the limit-two choice would be ABC (if A is older + /// than D) or BC (if A is younger, avoiding combining C with A skipping D). + /// + /// @param controller The compaction controller. + /// @param spaceAvailable The amount of space available for compaction, limits the maximum number of sstables + /// that can be selected. This only applies after the first fanout-many overlapping + /// sstables have been selected, to ensure that the compaction strategy can honor its + /// write amplification expectations. + /// @return A compaction pick to execute next. + CompactionAggregate.UnifiedAggregate constructAggregate(Controller controller, long spaceAvailable, Arena arena) { int count = maxOverlap; int threshold = level.threshold; int fanout = level.fanout; int index = level.index; - int maxSSTablesToCompact = Math.max(fanout, controller.maxSSTablesToCompact()); + int maxSSTablesToCompact = Math.max(fanout, (int) Math.min(spaceAvailable / level.avg, controller.maxSSTablesToCompact())); assert count >= threshold; if (count <= fanout) { - /** - * Happy path. We are not late or (for levelled) we are only so late that a compaction now will - * have the same effect as doing levelled compactions one by one. Compact all. We do not cap - * this pick at maxSSTablesToCompact due to an assumption that maxSSTablesToCompact is much - * greater than F. See {@link Controller#MAX_SSTABLES_TO_COMPACT_OPTION} for more details. - */ - return new CompactionPick(index, count, allSSTablesSorted); + // Happy path. We are not late or (for levelled) we are only so late that a compaction now will + // have the same effect as doing levelled compactions one by one. Compact all. We do not cap + // this pick at maxSSTablesToCompact, or reduce the size of the compaction to the available disk + // space because that would violate the strategy's write amplification promises. + // If a compaction is too big to fit the available space, protections in [getSelection] will + // prevent if from being selected; space may be available on a later compaction round. + return CompactionAggregate.createUnified(allSSTablesSorted, + maxOverlap, + createPick(controller, nextTimeUUID(), index, allSSTablesSorted), + Collections.emptySet(), + arena, + level); } + // The choices below assume that pulling the oldest sstables will reduce maxOverlap by the selected + // number of sstables. This is not always true (we may, e.g. select alternately from different overlap + // sections if the structure is complex enough), but is good enough heuristic that results in usable + // compaction sets. else if (count <= fanout * controller.getFanout(index + 1) || maxSSTablesToCompact == fanout) { // Compaction is a bit late, but not enough to jump levels via layout compactions. We need a special // case to cap compaction pick at maxSSTablesToCompact. if (count <= maxSSTablesToCompact) - return new CompactionPick(index, count, allSSTablesSorted); + return CompactionAggregate.createUnified(allSSTablesSorted, + maxOverlap, + createPick(controller, nextTimeUUID(), index, allSSTablesSorted), + Collections.emptySet(), + arena, + level); + + CompactionPick pick = createPick(controller, nextTimeUUID(), index, pullOldestSSTables(maxSSTablesToCompact)); + count -= maxSSTablesToCompact; + List pending = new ArrayList<>(); + while (count >= threshold) + { + pending.add(createPick(controller, nextTimeUUID(), index, pullOldestSSTables(maxSSTablesToCompact))); + count -= maxSSTablesToCompact; + } - return new CompactionPick(index, maxSSTablesToCompact, pullOldestSSTables(maxSSTablesToCompact)); + return CompactionAggregate.createUnified(allSSTablesSorted, maxOverlap, pick, pending, arena, level); } + // We may, however, have accumulated a lot more than T if compaction is very late, or a set of small + // tables was dumped on us (e.g. when converting from legacy LCS or for tests). else { - // We may, however, have accumulated a lot more than T if compaction is very late. - // In this case we pick a compaction in such a way that the result of doing it spreads the data in + // We need to pick the compactions in such a way that the result of doing them all spreads the data in // a similar way to how compaction would lay them if it was able to keep up. This means: // - for tiered compaction (w >= 0), compact in sets of as many as required to get to a level. - // for example, for w=2 and 55 sstables, pick a compaction of 16 sstables (on the next calls, given no - // new files, 2 more of 16, 1 of 4, and leaving the other 3 sstables alone). + // for example, for w=2 and 55 sstables, do 3 compactions of 16 sstables, 1 of 4, and leave the other 3 alone // - for levelled compaction (w < 0), compact all that would reach a level. - // for w=-2 and 55, this means pick a compaction of 48 (on the next calls, given no new files, one of - // 4, and one of 3 sstables). - int pickSize = selectPickSize(controller, maxSSTablesToCompact); - return new CompactionPick(index, pickSize, pullOldestSSTables(pickSize)); + // for w=-2 and 55, this means one compaction of 48, one of 4, and one of 3 sstables. + List picks = layoutCompactions(controller, maxSSTablesToCompact); + // Out of the set of necessary compactions, choose the one to run randomly. This gives a better + // distribution among levels and should result in more compactions running in parallel in a big data + // dump. + assert !picks.isEmpty(); // we only enter this if count > F: layoutCompactions must have selected something to run + CompactionPick selected = picks.remove(controller.random().nextInt(picks.size())); + return CompactionAggregate.createUnified(allSSTablesSorted, maxOverlap, selected, picks, arena, level); } } - private int selectPickSize(Controller controller, int maxSSTablesToCompact) + private List layoutCompactions(Controller controller, int maxSSTablesToCompact) { - int pickSize; - int fanout = level.fanout; - int nextStep = fanout; - int index = level.index; - int limit = Math.min(maxSSTablesToCompact, maxOverlap); - do + List pending = new ArrayList<>(); + int pos = layoutCompactions(controller, level.index + 1, level.fanout, maxSSTablesToCompact, pending); + int size = maxOverlap; + if (size - pos >= level.threshold) // can only happen in the levelled case. { - pickSize = nextStep; - fanout = controller.getFanout(++index); - nextStep *= fanout; + assert size - pos < maxSSTablesToCompact; // otherwise it should have already been picked + pending.add(createPick(controller, nextTimeUUID(), level.index, allSSTablesSorted)); } - while (nextStep <= limit); + return pending; + } - if (level.scalingParameter < 0) + /// Collects in {@param list} compactions of {@param sstables} such that they land in {@param level} and higher. + /// + /// Recursively combines SSTables into [CompactionPick]s in way that up to {@param maxSSTablesToCompact} + /// SSTables are combined to reach the highest possible level, then the rest is combined for the level before, + /// etc up to {@param level}. + /// + /// To agree with what compaction normally does, the first sstables from the list are placed in the picks that + /// combine to reach the highest levels. + /// + /// @param level minimum target level for compactions to land + /// @param step - number of source SSTables required to reach level + /// @param maxSSTablesToCompact limit on the number of sstables per compaction + /// @param list - result list of layout-preserving compaction picks + /// @return index of the last used SSTable from {@param sstables}; the number of remaining sstables will be lower + /// than step + private int layoutCompactions(Controller controller, + int level, + int step, + int maxSSTablesToCompact, + List list) + { + if (step > maxOverlap || step > maxSSTablesToCompact) + return 0; + + int w = controller.getScalingParameter(level); + int f = controller.getFanout(level); + int pos = layoutCompactions(controller, + level + 1, + step * f, + maxSSTablesToCompact, + list); + + int total = maxOverlap; + // step defines the number of source sstables that are needed to reach this level (ignoring overwrites + // and deletions). + // For tiered compaction we will select batches of this many. + int pickSize = step; + if (w < 0) { // For levelled compaction all the sstables that would reach this level need to be compacted to one, - // so select the highest multiple of step that fits. - pickSize *= limit / pickSize; - assert pickSize > 0; + // so select the highest multiple of step that is available, but make sure we don't do a compaction + // bigger than the limit. + pickSize *= Math.min(total - pos, maxSSTablesToCompact) / pickSize; + + if (pickSize == 0) // Not enough sstables to reach this level, we can skip the processing below. + return pos; // Note: this cannot happen on the top level, but can on lower ones. } - return pickSize; + + while (pos + pickSize <= total) + { + // Note that we assign these compactions to the level that would normally produce them, which means that + // they won't be taking up threads dedicated to the busy level. + // Normally sstables end up on a level when a compaction on the previous brings their size to the + // threshold (which corresponds to pickSize == step, always the case for tiered); in the case of + // levelled compaction, when we compact more than 1 but less than F sstables on a level (which + // corresponds to pickSize > step), it is an operation that is triggered on the same level. + list.add(createPick(controller, + nextTimeUUID(), + pickSize > step ? level : level - 1, + pullOldestSSTables(pickSize))); + pos += pickSize; + } + + // In the levelled case, if we had to adjust pickSize due to maxSSTablesToCompact, there may + // still be enough sstables to reach this level (e.g. if max was enough for 2*step, but we had 3*step). + if (pos + step <= total) + { + pickSize = ((total - pos) / step) * step; + list.add(createPick(controller, + nextTimeUUID(), + pickSize > step ? level : level - 1, + pullOldestSSTables(pickSize))); + pos += pickSize; + } + return pos; + } + + static List pullLast(List source, int limit) + { + List result = new ArrayList<>(limit); + while (--limit >= 0) + result.add(source.remove(source.size() - 1)); + return result; } /** * Pull the oldest sstables to get at most limit-many overlapping sstables to compact in each overlap section. */ - abstract Collection pullOldestSSTables(int overlapLimit); + abstract Collection pullOldestSSTables(int overlapLimit); } public static class SimpleBucket extends Bucket { - public SimpleBucket(Level level, Collection sstables) + public SimpleBucket(Level level, Collection sstables) { super(level, sstables, sstables.size()); } - Collection pullOldestSSTables(int overlapLimit) + Collection pullOldestSSTables(int overlapLimit) { if (allSSTablesSorted.size() <= overlapLimit) return allSSTablesSorted; - return Overlaps.pullLast(allSSTablesSorted, overlapLimit); + return pullLast(allSSTablesSorted, overlapLimit); } } public static class MultiSetBucket extends Bucket { - final List> overlapSets; + final List> overlapSets; - public MultiSetBucket(Level level, List> overlapSets) + public MultiSetBucket(Level level, List> overlapSets) { super(level, overlapSets); this.overlapSets = overlapSets; } - Collection pullOldestSSTables(int overlapLimit) + Collection pullOldestSSTables(int overlapLimit) { return Overlaps.pullLastWithOverlapLimit(allSSTablesSorted, overlapSets, overlapLimit); } } - /** - * Utility class holding a collection of sstables for compaction. - */ - static class CompactionPick extends ArrayList + static class CompactionLimits { - final int level; - final int overlap; + final int runningCompactions; + final int maxConcurrentCompactions; + final int maxCompactions; + final int[] perLevel; + int levelCount; + final long spaceAvailable; + final int remainingAdaptiveCompactions; + + public CompactionLimits(int runningCompactions, + int maxCompactions, + int maxConcurrentCompactions, + int[] perLevel, + int levelCount, + long spaceAvailable, + int remainingAdaptiveCompactions) + { + this.runningCompactions = runningCompactions; + this.maxCompactions = maxCompactions; + this.maxConcurrentCompactions = maxConcurrentCompactions; + this.perLevel = perLevel; + this.levelCount = levelCount; + this.spaceAvailable = spaceAvailable; + this.remainingAdaptiveCompactions = remainingAdaptiveCompactions; + } - CompactionPick(int level, int overlap, Collection sstables) + @Override + public String toString() { - super(sstables); - this.level = level; - this.overlap = overlap; + return String.format("Current limits: running=%d, max=%d, maxConcurrent=%d, perLevel=%s, levelCount=%d, spaceAvailable=%s, remainingAdaptiveCompactions=%d", + runningCompactions, maxCompactions, maxConcurrentCompactions, Arrays.toString(perLevel), levelCount, + FBUtilities.prettyPrintMemory(spaceAvailable), remainingAdaptiveCompactions); } } - static class SelectionContext + /** + * Utility wrapper to efficiently store the density of an SSTable with the SSTable itself. + */ + private static class SSTableWithDensity implements Comparable { - final Controller controller; - int estimatedRemainingTasks = 0; + final CompactionSSTable sstable; + final double density; + + SSTableWithDensity(CompactionSSTable sstable, double density) + { + this.sstable = sstable; + this.density = density; + } - SelectionContext(Controller controller) + @Override + public int compareTo(SSTableWithDensity o) { - this.controller = controller; + return Double.compare(density, o.density); } } } diff --git a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.md b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.md index 5f8d548af97c..2e6561c1712e 100644 --- a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.md +++ b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.md @@ -1,19 +1,19 @@ # Unified compaction strategy (UCS) @@ -218,12 +218,12 @@ This sharding mechanism is independent of the compaction specification. This sharding scheme easily admits extensions. In particular, when the size of the data set is expected to grow very large, to avoid having to pre-specify a high enough target size to avoid problems with per-sstable overhead, we can -apply an "SSTtable growth" parameter, which determines what part of the density growth should be assigned to increased +apply an "sstable growth" parameter, which determines what part of the density growth should be assigned to increased SSTable size, reducing the growth of the number of shards (and hence non-overlapping sstables). Additionally, to allow for a mode of operation with a fixed number of shards, and splitting conditional on reaching -a minimum size, we provide for a "minimum SSTable size" that reduces the base shard count whenever that would result -in SSTables smaller than the provided minimum. +a minimum size, we provide for a "minimum sstable size" that reduces the base shard count whenever that would result +in sstables smaller than the provided minimum. Generally, the user can specify four sharding parameters: @@ -239,8 +239,8 @@ S = \begin{cases} 1 & \text{if } d < m \\ -min(2^{\left\lfloor \log_2 \frac d m \right\rfloor}, x) - & \text{if } d < mb \text{, where } x \text{ is the largest power of 2 divisor of } b \\ +2^{\left\lfloor \log_2 \frac d m \right\rfloor} + & \text{if } d < mb \\ b & \text{if } d < tb \\ 2^{\left\lfloor (1-\lambda) \cdot \log_2 \left( {\frac d t \cdot \frac 1 b}\right)\right\rceil} \cdot b @@ -262,14 +262,14 @@ Some useful combinations of these parameters: ![Graph with lambda 0.5](unified/shards_graph_lambda_0_5.svg) - Similarly, $\lambda = 1/3$ makes the sstable growth the cubic root of the density growth, i.e. the sstable size - grows with the square root of the growth of the shard count. The graph below uses $b=1$ and $t = 1\mathrm{GB}$ + grows with the square root of the growth of the shard count. The graph below uses $b=1$ and $t = 1\mathrm{GB}$ (note: when $b=1$ the minimal size has no effect): ![Graph with lambda 0.33](unified/shards_graph_lambda_0_33.svg) - A growth component of 1 constructs a hierarchy with exactly $b$ shards at every level. Combined with a minumum - sstable size, this defines a mode of operation where we use a pre-specified - number of shards, but split only after reaching a minimum size. Illustrated below for $b=10$ and $m=100\mathrm{MB}$ + sstable size, this defines a mode of operation similar to UCS V1 (as used in DSE 6.8), where we use a pre-specified + number of shards, but split only after reaching a minimum size. Illustrated below for $b=10$ and $m=100\mathrm{MB}$ (note: the target sstable size is irrelevant when $\lambda=1$): ![Graph with lambda 1](unified/shards_graph_lambda_1.svg) @@ -306,8 +306,8 @@ than this set alone. It is possible for our sharding scheme to end up constructing sstables spanning differently-sized shards for the same level. One clear example is the case of levelled compaction, where, for example, sstables enter at some density, and -after the first compaction the result — being 2x bigger than that density — is split in the middle because -it has double the density. As another sstable enters the same level, we will have separate overlap sets for the first +after the first compaction the result — being 2x bigger than that density — is split in the middle because +it has double the density. As another sstable enters the same level, we will have separate overlap sets for the first and second half of that older sstable; to be efficient, the compaction that is triggered next needs to select both. To deal with this and any other cases of partial overlap, the compaction strategy will transitively extend @@ -319,27 +319,110 @@ on the number of overlapping sources we compact; in that case we use the collect select at most limit-many in any included overlap set, making sure that if an sstable is included in this compaction, all older ones are also included to maintain time order. -## Selecting the compaction to run +## Non-overlapping sstables trigger -Compaction strategies aim to minimize the read amplification of queries, which is defined by the number of sstables -that overlap on any given key. In order to do this most efficiently in situations where compaction is late, we select -a compaction bucket whose overlap is the highest among the possible choices. If there are multiple such choices, we -choose one uniformly randomly within each level, and between the levels we prefer the lowest level (as this is expected -to cover a larger fraction of the token space for the same amount of work). +In some scenarios it is possible for (small) non-overlapping sstables to accumulate in numbers that can cause problems +due to the sheer number of sstables present. For example, in tables with regularly scheduled snapshots, which also use +time-based partitioning, that regular snapshot will often flush single-partition sstables. When the partition time +window passes, the normal overlap processing will no longer find newly flushed data that overlaps with older sstables +and will thus leave those sstables alone. Eventually we can end up with thousands of sstables on a lower level that are +never compacted. -Under sustained load, this mechanism prevents the accumulation of sstables on some level that could sometimes happen -with legacy strategies (e.g. all resources consumed by L0 and sstables accumulating on L1) and can lead to a -steady state where compactions always use more sstables than the assigned threshold and fan factor and maintain a tiered -hierarchy based on the lowest overlap they are able to maintain for the load. +This can be a problem, especially in combination with SAI indexing, which prefers a lower number of sstables overall. +To address it, the strategy offers a threshold for the number of sstables that can be present on any of the shards that +the sharding strategy assigns for a given level. The threshold is specified as a multiple of a level's fan factor: if +there are no normal compactions to perform on a level, and we can find a shard that has more sstables than the +threshold, we perform the equivalent of a major compaction for the smallest set of shards that contains it. The result +is split on each output shard boundary and results in a single sstable for each of the output shards. This should +create sstables that span many partitions and that will thus progress nicely through the normal processing in the +next levels of the hierarchy. -## Major compaction +## Prioritization of compactions -Under the working principles of UCS, a major compaction is an operation which compacts together all sstables that have -(transitive) overlap, and where the output is split on shard boundaries appropriate for the expected result density. +Compaction strategies aim to minimize the read amplification of queries, which is defined by the number of sstables +that overlap on any given key. In order to do this most efficiently in situations where compaction is late, we +prioritize compaction buckets whose overlap is higher. If there are multiple such choices, we choose one uniformly +randomly within each level, and between the levels we prefer the lowest level (as this is expected to cover a larger +fraction of the token space for the same amount of work). + +Under sustained load, this mechanism in combination with the above prevents the accumulation of sstables on some level +that could sometimes happen with legacy strategies (e.g. all resources consumed by L0 and sstables accumulating on L1) +and can lead to a steady state where compactions always use more sstables than the assigned threshold and fan factor and +maintain a tiered hierarchy based on the lowest overlap they are able to maintain for the load. + +## Compaction thread allocation + +Because of sharding, UCS can do more compactions in parallel. This is especially true for higher levels of the +hierarchy, where we would often end up with 10s or 100s of pending compactions in a very short time as new data pushes +all shards over the threshold at almost the same time. + +The above can cause all compaction threads to start work on such levels, starving other levels of computing resources +to run. The starvation is especially apparent for level 0, where new sstables quickly accummulate and increase overlap. +This is a real but manageable problem when sstables are small, where the short compaction time combined with the +prioritization mechanism above can improve the situation relatively quickly. + +However, with higher lambdas (especially in fixed shards mode), higher-level compactions can take a very long time and +will often hog all available threads, causing very large accummulations of sstables on the lowest levels that can remain +present for a long time and cause significant problems. To prevent this, UCS will by default limit the number of threads +that can perform higher-level compactions to only a fair share of the total number of threads. This is fully +configurable through parameters for a number of thread reservations, as well as a reservation mode (`per_level` or +`level_or_below`). + +When the number of reservations is 0, the mode does not matter and all compaction threads are assigned according to the +prioritization explained in the previous paragraph. This provides the best utilization of compaction threads in the +system and works well with small sstable sizes. Even in these cases, it will result in small spikes of sstable overlap +on lower levels of the hierarchy when compactions on top levels are initiated. + +In `per-level` mode, when the number of reservations is set to an integer, UCS will reserve that many threads for each +level of the hierarchy and assign work to the rest of the threads according to the prioritization above. Some threads +will be idle if no work is needed on the associated level. UCS will also reserve the given number of threads for the top +level before it needs any compaction, to be able to respond to a new need quickly. + +When the number of reservations is set to `max`, or exceeds the number of available threads divided by the number of +levels, UCS will reserve the integer part of that ratio for each level, and will assign the remainder according +to the prioritization, but only up to one additional compaction per level. This setting provides better smoothness, +reducing or fully eliminating the overlap spikes, and is imperative when sstables can grow large (i.e. with higher +lambda). The downside of this setting is that this means fewer compaction threads will be actively used. This is thus +best combined with higher compaction thread counts. + +Using the `level_and_below` mode splits the threads as above, but makes threads for higher levels available for +lower-level work. In other words, it only limits the resources that higher levels may use: up to the given number plus +any remainder for the top level, up to two times that number plus the remainder for the top two levels and so on. This +still solves the original problem (higher-level compactions starving low levels of resources) while making better use of +the compaction threads. This is the mode (with `max` reservations) used by default. + +## Output shard parallelization + +Because the sharding of the output of a compaction operation is known in advance, we can parallelize the compaction +process by starting a separate task for each shard. This can dramatically speed the throughput of compaction and is +especially helpful for the lower levels of the compaction heirarchy, where the number of input shards is very low +(often just one). To make sure that we correctly change the state of input and output sstables, such operations will +share a transaction and will complete only when all individual tasks complete (and, conversely, abort if any of the +individual tasks abort). Early opening of sstables is not supported in this mode, because we currently do not support +arbitraty filtering of the requests to an sstable; it is expected that the smaller size and quicker completion time of +compactions should make up for this. + +This is controlled by the `parallelize_output_shards` parameter, which is `true` by default. + +## Major compaction -In other words, it is expected that a major compaction will result in $b$ concurrent compactions, each containing all -sstables covered in each of the base shards, and that the result will be split on shard boundaries whose number -depends on the total size of data contained in the shard. +Major compaction in UCS always splits the output into a shard number suitable for the expected result density. +If the input sstables can be split into non-overlapping sets that correspond to current shard boundaries, the compaction +will construct independent operations that work over these sets, to improve the space overhead of the operation as well +as the time needed to persistently complete individual steps. Because all levels will usually be split in $b$ shards, +it will very often be the case that major compactions split into $b$ individual jobs, reducing the space overhead by a +factor close to $b$. Note that this does not always apply; for example, if a topology change causes the sharding +boundaries to move, the mismatch between old and new sharding boundaries will cause the compaction to produce a single +operation and require 100% space overhead. + +Output shard parallelization also applies to major compactions: if the `parallelize_output_shards` option is enabled, +shards of individual compactions will be compacted concurrently, which can significantly reduce the time needed to +perform the compaction; if the option is not enabled, major compaction will only be parallelized up to the number of +individual non-overlapping sets the sstables can be split into. In either case, the number of parallel operations is +limited to a number specified as a parameter of the operation (e.g. `nodetool compact -j n`), which is set to half the +compaction thread count by default. Using a jobs of 0 will let the compaction use all available threads and run +as quickly as possible, but this will prevent other compaction operations from running until it completes and thus +should be used with caution, only while the database is known to not receive any writes. ## Differences with STCS and LCS @@ -398,7 +481,7 @@ the span of the lower-density ones. UCS accepts these compaction strategy parameters: -* **scaling_parameters**. A list of per-level scaling parameters, specified as L*f*, T*f*, N, or an integer value +* `scaling_parameters` A list of per-level scaling parameters, specified as L*f*, T*f*, N, or an integer value specifying $w$ directly. If more levels are present than the length of this list, the last value is used for all higher levels. Often this will be a single parameter, specifying the behaviour for all levels of the hierarchy. @@ -410,24 +493,26 @@ UCS accepts these compaction strategy parameters: expense of making reads more difficult. N is the middle ground that has the features of levelled (one sstable run per level) as well as tiered (one compaction to be promoted to the next level) and a fan factor of 2. This can also be specified as T2 or L2. - The default value is T4, matching the default STCS behaviour with threshold 4. To select an equivalent of LCS - with its default fan factor 10, use L10. -* **target_sstable_size**. The target sstable size $t$, specified as a human-friendly size in bytes (e.g. 100 MiB = + The default value is T4, matching the default STCS behaviour with threshold 4. The default value in vector mode (see + paragraph below) is L10, equivalent to LCS with its default fan factor 10. +* `target_sstable_size` The target sstable size $t$, specified as a human-friendly size in bytes (e.g. 100 MiB = $100\cdot 2^{20}$ B or (10 MB = 10,000,000 B)). The strategy will split data in shards that aim to produce sstables of size between $t / \sqrt 2$ and $t \cdot \sqrt 2$. Smaller sstables improve streaming and repair, and make compactions shorter. On the other hand, each sstable on disk has a non-trivial in-memory footprint that also affects garbage collection times. - Increase this if the memory pressure from the number of sstables in the system becomes too high. - The default value is 1 GiB. -* **base_shard_count**. The minimum number of shards $b$, used for levels with the smallest density. This gives the + Increase this if the memory pressure from the number of sstables in the system becomes too high. Also see + `sstable_growth` below. + The default value is 1 GiB. The default value in vector mode is 5GiB. +* `base_shard_count` The minimum number of shards $b$, used for levels with the smallest density. This gives the minimum compaction concurrency for the lowest levels. A low number would result in larger L0 sstables but may limit - the overall maximum write throughput (as every piece of data has to go through L0). The base shard count only applies after `min_sstable_size` is reached. - The default value is 4 for all tables -* **sstable_growth** The sstable growth component $\lambda$, applied as a factor in the shard exponent calculation. + the overall maximum write throughput (as every piece of data has to go through L0). The base shard count only applies + after `min_sstable_size` is reached. + The default value is 4. The default value in vector mode is 1. +* `sstable_growth` The sstable growth component $\lambda$, applied as a factor in the shard exponent calculation. This is a number between 0 and 1 that controls what part of the density growth should apply to individual sstable size and what part should increase the number of shards. Using a value of 1 has the effect of fixing the shard count to the base value. Using 0.5 makes the shard count and sstable size grow with the square root of the density - growth. + growth. This is useful to decrease the sheer number of sstables that will be created for very large data sets. For example, without growth correction a data set of 10TiB with 1GiB target size would result in over 10k sstables, which may present as too much overhead both as on-heap memory used by per-sstable structures as well as time to look @@ -435,19 +520,62 @@ UCS accepts these compaction strategy parameters: in this scenario (with base count 4) will reduce the potential number of sstables to ~160 of ~64GiB, which is still manageable both as memory overhead and individual compaction duration and space overhead. The balance between the two can be further tweaked by increasing $\lambda$ to get fewer but bigger sstables on the top level, and decreasing - it to favour a higher count of smaller sstables. The default value is 0.333 meaning the sstable size - grows with the square root of the growth of the shard count. -* **min_sstable_size** The minimum sstable size $m$, applicable when the base shard count will result is sstables + it to favour a higher count of smaller sstables. + The default value is 0.333 meaning the sstable size grows with the square root of the growth of the shard count. + The default value in vector mode is 1 which means the shard count will be fixed to the base value. +* `min_sstable_size` The minimum sstable size $m$, applicable when the base shard count will result is sstables that are considered too small. If set, the strategy will split the space into fewer than the base count shards, to - make the estimated sstables size at least as large as this value. A value of 0 disables this feature. - The default value is 100MiB. -* **expired_sstable_check_frequency_seconds**. Determines how often to check for expired SSTables. + make the estimated sstables size at least as large as this value. A value of 0 disables this feature. + A value of `auto` sets the minimum sstable size to the size of sstables resulting from flushes. + The default value is 100MiB. The default value in vector mode is 1GiB. +* `reserved_threads` Specifies the number of threads to reserve per level. Any remaining threads will take + work according to the prioritization mechanism (i.e. higher overlap first). Higher reservations mean better + responsiveness of the compaction strategy to new work, or smoother performance, at the expense of reducing the + overall utilization of compaction threads. Higher values work best with high `concurrent_compactors` values. + The default value is `max`, which spreads all threads as close to evenly between levels as possible. It is recommended + to keep this option and the next at their defaults, which should offer a good balance between responsiveness and + thread utilization. +* `reservations_type` Specifies whether reservations can be used by lower levels. If set to `per_level`, the + reservations are only used by the specific level. If set to `level_or_below`, the reservations can be used by this + level as well as any one below it. + The default value is `level_or_below`. +* `parallelize_output_shards` Enables or disables parallelization of compaction tasks for the output shards of a + compaction. This can dramatically improve compaction throughput especially on the lowest levels of the hierarchy, + but disables early open and thus may be less efficient when compaction is configured to produce very large + sstables. + The default value is `true`. +* `expired_sstable_check_frequency_seconds` Determines how often to check for expired SSTables. The default value is 10 minutes. - -In **cassandra.yaml**: - -* **concurrent_compactors**. The number of compaction threads available. Higher values increase compaction performance - but may increase read and write latencies. +* `num_shards` Specifying this switches the strategy to UCS V1 mode, where the number of shards is fixed, but a + minimum sstable size applies for the lowest levels. Provided for compatibility with DSE 6.8's UCS implementation. + Sets $b$ to the specified value, $\lambda$ to 1, and the default minimum sstable size to 'auto'. + Disabled by default and cannot be used in combination with `base_shard_count`, `target_sstable_size` or + `sstable_growth`. +* `max_sstables_per_shard_factor` Limits the number of SSTables per shard. If the number of sstables in a shard + exceeds this factor times the shard compaction threshold, a major compaction of the shard will be triggered. + Some conditions like slow writes can lead to SSTables being very small, and never overlap with enough other SSTables + to be compacted. + So this setting is useful to prevent the number of SSTables in a shard from growing too large, which can cause + problems due to the per-sstable overhead. Also these small SSTables may still have overlaps even if under the + compaction threshold (eg. due to write replicas) and never compacting them wastes storage space. + The default value is 10. + +All UCS options can also be supplied as system properties, using the prefix `unified_compaction.`, e.g. +`-Dunified_compaction.sstable_growth=0.5` sets the default `sstable_growth` to 0.5. + +In addition to this, the strategy permits different defaults to be applied to tables that have a vector column when the +system property `unified_compaction.override_ucs_config_for_vector_tables` is set to `true`. If this is enabled and the +table has a column of type `vector`, the "vector mode" defaults in the list above apply. These vector defaults can be +altered using the prefix `unified_compaction.vector_`, e.g. +`-Dunified_compaction.vector_sstable_growth=1` in combination with +`-Dunified_compaction.override_ucs_config_for_vector_tables=true` sets the growth to 1 only for tables with a vector +column. + +In `cassandra.yaml`: + +* `concurrent_compactors` The number of compaction threads available. Higher values increase compaction performance + but may increase read and write latencies. Combine a high compactor count with thread reservations for more consistent + performance with sustained loads. [^1]: Note: in addition to TRANSITIVE, "overlap inclusion methods" of NONE and SINGLE are also implemented for experimentation, but they are not recommended for the UCS sharding scheme. diff --git a/src/java/org/apache/cassandra/db/compaction/Upgrader.java b/src/java/org/apache/cassandra/db/compaction/Upgrader.java index 9e4c4dd7502b..4d16de759c7a 100644 --- a/src/java/org/apache/cassandra/db/compaction/Upgrader.java +++ b/src/java/org/apache/cassandra/db/compaction/Upgrader.java @@ -17,18 +17,17 @@ */ package org.apache.cassandra.db.compaction; -import java.util.Collections; import java.util.function.LongPredicate; import com.google.common.base.Throwables; import com.google.common.collect.Sets; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableRewriter; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableWriter; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; @@ -41,59 +40,50 @@ public class Upgrader { - private final ColumnFamilyStore cfs; + private final CompactionRealm realm; private final SSTableReader sstable; private final LifecycleTransaction transaction; private final File directory; private final CompactionController controller; - private final CompactionStrategyManager strategyManager; - private final long estimatedRows; private final OutputHandler outputHandler; - public Upgrader(ColumnFamilyStore cfs, LifecycleTransaction txn, OutputHandler outputHandler) + public Upgrader(CompactionRealm realm, LifecycleTransaction txn, OutputHandler outputHandler) { - this.cfs = cfs; + this.realm = realm; this.transaction = txn; this.sstable = txn.onlyOne(); this.outputHandler = outputHandler; - this.directory = new File(sstable.getFilename()).parent(); - - this.controller = new UpgradeController(cfs); - - this.strategyManager = cfs.getCompactionStrategyManager(); - long estimatedTotalKeys = Math.max(cfs.metadata().params.minIndexInterval, SSTableReader.getApproximateKeyCount(Collections.singletonList(this.sstable))); - long estimatedSSTables = Math.max(1, SSTableReader.getTotalBytes(Collections.singletonList(this.sstable)) / strategyManager.getMaxSSTableBytes()); - this.estimatedRows = (long) Math.ceil((double) estimatedTotalKeys / estimatedSSTables); + this.controller = new UpgradeController(realm); } private SSTableWriter createCompactionWriter(StatsMetadata metadata) { - MetadataCollector sstableMetadataCollector = new MetadataCollector(cfs.getComparator()); + MetadataCollector sstableMetadataCollector = new MetadataCollector(realm.metadata().comparator); sstableMetadataCollector.sstableLevel(sstable.getSSTableLevel()); - Descriptor descriptor = cfs.newSSTableDescriptor(directory); + Descriptor descriptor = realm.newSSTableDescriptor(directory); return descriptor.getFormat().getWriterFactory().builder(descriptor) - .setKeyCount(estimatedRows) + .setKeyCount(metadata.totalRows) // TODO is it correct? I don't know why did we estimate that value instead of just copying it from metadata .setRepairedAt(metadata.repairedAt) .setPendingRepair(metadata.pendingRepair) .setTransientSSTable(metadata.isTransient) - .setTableMetadataRef(cfs.metadata) + .setTableMetadataRef(realm.metadataRef()) .setMetadataCollector(sstableMetadataCollector) - .setSerializationHeader(SerializationHeader.make(cfs.metadata(), Sets.newHashSet(sstable))) - .addDefaultComponents(cfs.indexManager.listIndexGroups()) - .setSecondaryIndexGroups(cfs.indexManager.listIndexGroups()) - .build(transaction, cfs); + .setSerializationHeader(SerializationHeader.make(realm.metadata(), Sets.newHashSet(sstable))) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(transaction, realm); } public void upgrade(boolean keepOriginals) { outputHandler.output("Upgrading " + sstable); long nowInSec = FBUtilities.nowInSeconds(); - try (SSTableRewriter writer = SSTableRewriter.construct(cfs, transaction, keepOriginals, CompactionTask.getMaxDataAge(transaction.originals())); - AbstractCompactionStrategy.ScannerList scanners = strategyManager.getScanners(transaction.originals()); + try (SSTableRewriter writer = SSTableRewriter.construct(realm, transaction, keepOriginals, CompactionTask.getMaxDataAge(transaction.originals())); + ScannerList scanners = ScannerList.of(transaction.originals(), null); CompactionIterator iter = new CompactionIterator(transaction.opType(), scanners.scanners, controller, nowInSec, nextTimeUUID())) { writer.switchWriter(createCompactionWriter(sstable.getSSTableMetadata())); @@ -117,7 +107,7 @@ public void upgrade(boolean keepOriginals) private static class UpgradeController extends CompactionController { - public UpgradeController(ColumnFamilyStore cfs) + public UpgradeController(CompactionRealm cfs) { super(cfs, Integer.MAX_VALUE); } diff --git a/src/java/org/apache/cassandra/db/compaction/unified/AdaptiveController.java b/src/java/org/apache/cassandra/db/compaction/unified/AdaptiveController.java new file mode 100644 index 000000000000..ba5a16b32a21 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/unified/AdaptiveController.java @@ -0,0 +1,613 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction.unified; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.compaction.CompactionPick; +import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.FSError; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileReader; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.MonotonicClock; +import org.apache.cassandra.utils.Overlaps; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_ADAPTIVE_INTERVAL_SEC; +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_ADAPTIVE_MAX_SCALING_PARAMETER; +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_ADAPTIVE_MIN_COST; +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_ADAPTIVE_MIN_SCALING_PARAMETER; +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_ADAPTIVE_THRESHOLD; +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_MAX_ADAPTIVE_COMPACTIONS; + +/** + * The adaptive compaction controller dynamically calculates the optimal scaling parameter W. + *

    + * Generally it tries to find a local minimum for the total IO cost that is projected + * by the strategy. The projected IO cost is composed by two parts: the read amplification, + * which is weighted by the number of partitions read by the user, and the write amplification, which + * is weighted by the number of bytes inserted into memtables. Other parameters are also considered, such + * as the cache miss rate and the time it takes to read and write from disk. See also the comments in + * {@link CostsCalculator}. + * + * Design doc: TODO: link to design doc or SEP + */ +public class AdaptiveController extends Controller +{ + private static final Logger logger = LoggerFactory.getLogger(AdaptiveController.class); + + /** The starting value for the scaling parameter */ + private static final int DEFAULT_STARTING_SCALING_PARAMETER = 0; + + /** The minimum valid value for the scaling parameter */ + static final String MIN_SCALING_PARAMETER = "adaptive_min_scaling_parameter"; + static private final int DEFAULT_MIN_SCALING_PARAMETER = UCS_ADAPTIVE_MIN_SCALING_PARAMETER.getIntWithLegacyFalback(); + + /** The maximum valid value for the scaling parameter */ + static final String MAX_SCALING_PARAMETER = "adaptive_max_scaling_parameter"; + static private final int DEFAULT_MAX_SCALING_PARAMETER = UCS_ADAPTIVE_MAX_SCALING_PARAMETER.getIntWithLegacyFalback(); + + /** The interval for periodically checking the optimal value for the scaling parameter */ + static final String INTERVAL_SEC = "adaptive_interval_sec"; + static private final int DEFAULT_INTERVAL_SEC = UCS_ADAPTIVE_INTERVAL_SEC.getIntWithLegacyFalback(); + + /** The gain is a number between 0 and 1 used to determine if a new choice of the scaling parameter is better than the current one */ + static final String THRESHOLD = "adaptive_threshold"; + private static final double DEFAULT_THRESHOLD = UCS_ADAPTIVE_THRESHOLD.getDoubleWithLegacyFallback(); + + /** Below the minimum cost we don't try to optimize the scaling parameter, we consider the current scaling parameter good enough. This is necessary because the cost + * can vanish to zero when there are neither reads nor writes and right now we don't know how to handle this case. */ + static final String MIN_COST = "adaptive_min_cost"; + static private final int DEFAULT_MIN_COST = UCS_ADAPTIVE_MIN_COST.getIntWithLegacyFalback(); + + /** The maximum number of concurrent Adaptive Compactions */ + static final String MAX_ADAPTIVE_COMPACTIONS = "max_adaptive_compactions"; + private static final int DEFAULT_MAX_ADAPTIVE_COMPACTIONS = UCS_MAX_ADAPTIVE_COMPACTIONS.getIntWithLegacyFalback(); + private final int intervalSec; + private final int minScalingParameter; + private final int maxScalingParameter; + private final double threshold; + private final int minCost; + /** Protected by the synchronized block in UnifiedCompactionStrategy#getNextBackgroundTasks */ + private int[] scalingParameters; + private int[] previousScalingParameters; + private volatile long lastChecked; + private final int maxAdaptiveCompactions; + + @VisibleForTesting + public AdaptiveController(MonotonicClock clock, + Environment env, + int[] scalingParameters, + int[] previousScalingParameters, + double[] survivalFactors, + long dataSetSize, + long minSSTableSize, + long flushSizeOverride, + long currentFlushSize, + double maxSpaceOverhead, + int maxSSTablesToCompact, + long expiredSSTableCheckFrequency, + boolean ignoreOverlapsInExpirationCheck, + int baseShardCount, + boolean isReplicaAware, + long targetSStableSize, + double sstableGrowthModifier, + int reservedThreadsPerLevel, + Reservations.Type reservationsType, + Overlaps.InclusionMethod overlapInclusionMethod, + boolean parallelizeOutputShards, + boolean hasVectorType, + double maxSstablesPerShardFactor, + int intervalSec, + int minScalingParameter, + int maxScalingParameter, + double threshold, + int minCost, + int maxAdaptiveCompactions, + TableMetadata metadata) + { + super(clock, + env, + survivalFactors, + dataSetSize, + minSSTableSize, + flushSizeOverride, + currentFlushSize, + maxSpaceOverhead, + maxSSTablesToCompact, + expiredSSTableCheckFrequency, + ignoreOverlapsInExpirationCheck, + baseShardCount, + isReplicaAware, + targetSStableSize, + sstableGrowthModifier, + reservedThreadsPerLevel, + reservationsType, + overlapInclusionMethod, + parallelizeOutputShards, + hasVectorType, + maxSstablesPerShardFactor, + metadata); + + this.scalingParameters = scalingParameters; + this.previousScalingParameters = previousScalingParameters; + this.intervalSec = intervalSec; + this.minScalingParameter = minScalingParameter; + this.maxScalingParameter = maxScalingParameter; + this.threshold = threshold; + this.minCost = minCost; + this.maxAdaptiveCompactions = maxAdaptiveCompactions; + } + + static Controller fromOptions(Environment env, + double[] survivalFactors, + long dataSetSize, + long minSSTableSize, + long flushSizeOverride, + double maxSpaceOverhead, + int maxSSTablesToCompact, + long expiredSSTableCheckFrequency, + boolean ignoreOverlapsInExpirationCheck, + int baseShardCount, + boolean isReplicaAware, + long targetSSTableSize, + double sstableGrowthModifier, + int reservedThreadsPerLevel, + Reservations.Type reservationsType, + Overlaps.InclusionMethod overlapInclusionMethod, + boolean parallelizeOutputShards, + boolean hasVectorType, + double maxSstablesPerShardFactor, + TableMetadata metadata, + Map options) + { + int[] scalingParameters = null; + long currentFlushSize = flushSizeOverride; + + File f = getControllerConfigPath(metadata); + try + { + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObject = (JSONObject) jsonParser.parse(new FileReader(f)); + scalingParameters = readStoredScalingParameters((JSONArray) jsonObject.get("scaling_parameters")); + if (jsonObject.get("current_flush_size") != null && flushSizeOverride == 0) + { + currentFlushSize = (long) jsonObject.get("current_flush_size"); + logger.debug("Successfully read stored current_flush_size from disk"); + } + } + catch (IOException e) + { + logger.debug("No controller config file found. Using starting value instead."); + } + catch (ParseException e) + { + logger.warn("Unable to parse saved options. Using starting value instead:", e); + } + catch (FSError e) + { + logger.warn("Unable to read controller config file. Using starting value instead:", e); + } + catch (Throwable e) + { + logger.warn("Unable to read controller config file. Using starting value instead:", e); + JVMStabilityInspector.inspectThrowable(e); + } + + if (scalingParameters == null) + { + logger.info("Unable to read scaling_parameters. Using starting value instead."); + scalingParameters = new int[UnifiedCompactionStrategy.MAX_LEVELS]; + String staticScalingParameters = options.remove(SCALING_PARAMETERS_OPTION); + String staticScalingFactors = options.remove(STATIC_SCALING_FACTORS_OPTION); + + if (staticScalingParameters != null) + { + int[] parameters = parseScalingParameters(staticScalingParameters); + for (int i = 0; i < scalingParameters.length; i++) + { + if (i < parameters.length) + scalingParameters[i] = parameters[i]; + else + scalingParameters[i] = scalingParameters[i-1]; + } + } + else if (staticScalingFactors != null) + { + int[] factors = parseScalingParameters(staticScalingFactors); + for (int i = 0; i < scalingParameters.length; i++) + { + if (i < factors.length) + scalingParameters[i] = factors[i]; + else + scalingParameters[i] = scalingParameters[i-1]; + } + logger.info("Option: '{}' used to initialize scaling parameters for Adaptive Controller", STATIC_SCALING_FACTORS_OPTION); + } + else + Arrays.fill(scalingParameters, DEFAULT_STARTING_SCALING_PARAMETER); + } + else + { + logger.debug("Successfully read stored scaling parameters from disk."); + if (options.containsKey(SCALING_PARAMETERS_OPTION)) + logger.warn("Option: '{}' is defined but not used. Stored configuration was used instead", SCALING_PARAMETERS_OPTION); + if (options.containsKey(STATIC_SCALING_FACTORS_OPTION)) + logger.warn("Option: '{}' is defined but not used. Stored configuration was used instead", STATIC_SCALING_FACTORS_OPTION); + } + int[] previousScalingParameters = scalingParameters.clone(); + + int minScalingParameter = options.containsKey(MIN_SCALING_PARAMETER) ? Integer.parseInt(options.get(MIN_SCALING_PARAMETER)) : DEFAULT_MIN_SCALING_PARAMETER; + int maxScalingParameter = options.containsKey(MAX_SCALING_PARAMETER) ? Integer.parseInt(options.get(MAX_SCALING_PARAMETER)) : DEFAULT_MAX_SCALING_PARAMETER; + int intervalSec = options.containsKey(INTERVAL_SEC) ? Integer.parseInt(options.get(INTERVAL_SEC)) : DEFAULT_INTERVAL_SEC; + double threshold = options.containsKey(THRESHOLD) ? Double.parseDouble(options.get(THRESHOLD)) : DEFAULT_THRESHOLD; + int minCost = options.containsKey(MIN_COST) ? Integer.parseInt(options.get(MIN_COST)) : DEFAULT_MIN_COST; + int maxAdaptiveCompactions = options.containsKey(MAX_ADAPTIVE_COMPACTIONS) ? Integer.parseInt(options.get(MAX_ADAPTIVE_COMPACTIONS)) : DEFAULT_MAX_ADAPTIVE_COMPACTIONS; + + return new AdaptiveController(MonotonicClock.Global.preciseTime, + env, + scalingParameters, + previousScalingParameters, + survivalFactors, + dataSetSize, + minSSTableSize, + flushSizeOverride, + currentFlushSize, + maxSpaceOverhead, + maxSSTablesToCompact, + expiredSSTableCheckFrequency, + ignoreOverlapsInExpirationCheck, + baseShardCount, + isReplicaAware, + targetSSTableSize, + sstableGrowthModifier, + reservedThreadsPerLevel, + reservationsType, + overlapInclusionMethod, + parallelizeOutputShards, + hasVectorType, + maxSstablesPerShardFactor, + intervalSec, + minScalingParameter, + maxScalingParameter, + threshold, + minCost, + maxAdaptiveCompactions, + metadata); + } + + private static int[] readStoredScalingParameters(JSONArray storedScalingParameters) + { + if (storedScalingParameters.size() > 0) + { + int[] scalingParameters = new int[UnifiedCompactionStrategy.MAX_LEVELS]; + for (int i = 0; i < scalingParameters.length; i++) + { + //if the file does not have enough entries, use the last entry for the rest of the levels + if (i < storedScalingParameters.size()) + scalingParameters[i] = ((Long) storedScalingParameters.get(i)).intValue(); + else + scalingParameters[i] = scalingParameters[i-1]; + } + //successfuly read scaling_parameters + return scalingParameters; + } + else + { + return null; + } + } + + public static Map validateOptions(Map options) throws ConfigurationException + { + int scalingParameter = DEFAULT_STARTING_SCALING_PARAMETER; + int minScalingParameter = DEFAULT_MIN_SCALING_PARAMETER; + int maxScalingParameter = DEFAULT_MAX_SCALING_PARAMETER; + + String s; + String staticScalingFactors = options.remove(STATIC_SCALING_FACTORS_OPTION); + String staticScalingParameters = options.remove(SCALING_PARAMETERS_OPTION); + if (staticScalingFactors != null && staticScalingParameters != null) + throw new ConfigurationException(String.format("Either '%s' or '%s' should be used, not both", SCALING_PARAMETERS_OPTION, STATIC_SCALING_FACTORS_OPTION)); + else if (staticScalingFactors != null) + parseScalingParameters(staticScalingFactors); + else if (staticScalingParameters != null) + parseScalingParameters(staticScalingParameters); + s = options.remove(MIN_SCALING_PARAMETER); + if (s != null) + minScalingParameter = Integer.parseInt(s); + s = options.remove(MAX_SCALING_PARAMETER); + if (s != null) + maxScalingParameter = Integer.parseInt(s); + + if (minScalingParameter >= maxScalingParameter || scalingParameter < minScalingParameter || scalingParameter > maxScalingParameter) + throw new ConfigurationException(String.format("Invalid configuration for the scaling parameter: %d, min: %d, max: %d", scalingParameter, minScalingParameter, maxScalingParameter)); + + s = options.remove(INTERVAL_SEC); + if (s != null) + { + int intervalSec = Integer.parseInt(s); + if (intervalSec <= 0) + throw new ConfigurationException(String.format("Invalid configuration for interval, it should be positive: %d", intervalSec)); + } + s = options.remove(THRESHOLD); + if (s != null) + { + double threshold = Double.parseDouble(s); + if (threshold <= 0 || threshold > 1) + { + throw new ConfigurationException(String.format("Invalid configuration for threshold, it should be within (0,1]: %f", threshold)); + } + } + s = options.remove(MIN_COST); + if (s != null) + { + int minCost = Integer.parseInt(s); + if (minCost <= 0) + throw new ConfigurationException(String.format("Invalid configuration for minCost, it should be positive: %d", minCost)); + } + s = options.remove(MAX_ADAPTIVE_COMPACTIONS); + if (s != null) + { + int maxAdaptiveCompactions = Integer.parseInt(s); + if (maxAdaptiveCompactions < -1) + throw new ConfigurationException(String.format("Invalid configuration for maxAdaptiveCompactions, it should be >= -1 (-1 for no limit): %d", maxAdaptiveCompactions)); + } + return options; + } + + @Override + void startup(UnifiedCompactionStrategy strategy, CostsCalculator calculator) + { + super.startup(strategy, calculator); + this.lastChecked = clock.now(); + } + + @Override + public int getScalingParameter(int index) + { + if (index < 0) + throw new IllegalArgumentException("Index should be >= 0: " + index); + + return index < scalingParameters.length ? scalingParameters[index] : scalingParameters[scalingParameters.length - 1]; + } + + @Override + public int getPreviousScalingParameter(int index) + { + if (index < 0) + throw new IllegalArgumentException("Index should be >= 0: " + index); + + return index < previousScalingParameters.length ? previousScalingParameters[index] : previousScalingParameters[previousScalingParameters.length - 1]; + } + + @Override + @Nullable + public CostsCalculator getCalculator() + { + return calculator; + } + + public int getInterval() + { + return intervalSec; + } + + public int getMinScalingParameter() + { + return minScalingParameter; + } + + public int getMaxScalingParameter() + { + return maxScalingParameter; + } + + public double getThreshold() + { + return threshold; + } + + public int getMinCost() + { + return minCost; + } + + /** + * Checks to see if the chosen compaction is a result of recent adaptive parameter change. + * An adaptive compaction is a compaction triggered by changing the scaling parameter W + */ + @Override + public boolean isRecentAdaptive(CompactionPick pick) + { + int numTables = pick.sstables().size(); + int level = (int) pick.parent(); + return (numTables >= getThreshold(level) && numTables < getPreviousThreshold(level)); + } + + @Override + public int getMaxRecentAdaptiveCompactions() + { + return maxAdaptiveCompactions; + } + + /** Protected by the synchronized block in UnifiedCompactionStrategy#getNextBackgroundTasks */ + @Override + public void onStrategyBackgroundTaskRequest() + { + if (!isRunning()) + return; + + long now = clock.now(); + if (now - lastChecked < TimeUnit.SECONDS.toNanos(intervalSec)) + return; + + try + { + maybeUpdate(now); + } + finally + { + lastChecked = now; + } + } + + /** + * Maybe updates the scaling parameter according to the data size, read, and write costs. + * + * The scaling parameter calculation is based on current read and write query costs for the entire data size. + * We use the entire data size instead of shard size here because query cost calculations do not take + * sharding into account. Also, the same scaling parameter is going to be used across all shards. + * + * Protected by the synchronized block in UnifiedCompactionStrategy#getNextBackgroundTasks + * + * @param now current timestamp only used for debug logging + */ + private void maybeUpdate(long now) + { + final long targetSize = Math.max(getDataSetSizeBytes(), (long) Math.ceil(calculator.spaceUsed())); + + final int RA = readAmplification(targetSize, scalingParameters[0]); + final int WA = writeAmplification(targetSize, scalingParameters[0]); + + final double readCost = calculator.getReadCostForQueries(RA); + final double writeCost = calculator.getWriteCostForQueries(WA); + final double cost = readCost + writeCost; + + if (cost <= minCost) + { + logger.debug("Adaptive compaction controller not updated, cost for current scaling parameter {} is below minimum cost {}: read cost: {}, write cost: {}\nAverages: {}", scalingParameters[0], minCost, readCost, writeCost, calculator); + return; + } + + final double[] totCosts = new double[maxScalingParameter - minScalingParameter + 1]; + final double[] readCosts = new double[maxScalingParameter - minScalingParameter + 1]; + final double[] writeCosts = new double[maxScalingParameter - minScalingParameter + 1]; + int candScalingParameter = scalingParameters[0]; + double candCost = cost; + + for (int i = minScalingParameter; i <= maxScalingParameter; i++) + { + final int idx = i - minScalingParameter; + if (i == scalingParameters[0]) + { + readCosts[idx] = readCost; + writeCosts[idx] = writeCost; + } + else + { + final int ra = readAmplification(targetSize, i); + final int wa = writeAmplification(targetSize, i); + + readCosts[idx] = calculator.getReadCostForQueries(ra); + writeCosts[idx] = calculator.getWriteCostForQueries(wa); + } + totCosts[idx] = readCosts[idx] + writeCosts[idx]; + // in case of a tie, for neg.ve scalingParameters we prefer higher scalingParameters (smaller WA), but not for pos.ve scalingParameters we prefer lower scalingParameters (more parallelism) + if (totCosts[idx] < candCost || (i < 0 && totCosts[idx] == candCost)) + { + candScalingParameter = i; + candCost = totCosts[idx]; + } + } + + logger.debug("Min cost: {}, min scaling parameter: {}, target sstable size: {}\nread costs: {}\nwrite costs: {}\ntot costs: {}\nAverages: {}", + candCost, + candScalingParameter, + FBUtilities.prettyPrintMemory(getTargetSSTableSize()), + Arrays.toString(readCosts), + Arrays.toString(writeCosts), + Arrays.toString(totCosts), + calculator); + + StringBuilder str = new StringBuilder(100); + str.append("Adaptive compaction controller "); + + if (scalingParameters[0] != candScalingParameter && (cost - candCost) >= threshold * cost) + { + //scaling parameter is updated + str.append("updated ").append(scalingParameters[0]).append(" -> ").append(candScalingParameter); + this.previousScalingParameters[0] = scalingParameters[0]; //need to keep track of the previous scaling parameter for isAdaptive check + this.scalingParameters[0] = candScalingParameter; + + //store updated scaling parameters in case a node fails and needs to restart + storeControllerConfig(); + } + else if (scalingParameters[0] == candScalingParameter) + { + // only update the lowest level that is not equal to candScalingParameter + // example: candScalingParameter = 4, scalingParameters = {4, 4, 12, 16} --> scalingParameters = {4, 4, 4, 16} + // as a result, higher levels will be less prone to changes + for (int i = 1; i < scalingParameters.length; i++) + { + if (scalingParameters[i] != candScalingParameter) + { + str.append("updated for level ").append(i).append(": ").append(scalingParameters[i]).append(" -> ").append(candScalingParameter); + this.previousScalingParameters[i] = scalingParameters[i]; + this.scalingParameters[i] = candScalingParameter; + + //store updated scaling parameters in case a node fails and needs to restart + storeControllerConfig(); + break; + } + else if (i == scalingParameters.length-1) + { + str.append("unchanged because all levels have the same scaling parameter"); + } + } + } + else + { + //scaling parameter is not updated + str.append("unchanged"); + } + + str.append(", data size: ").append(FBUtilities.prettyPrintMemory(targetSize)); + str.append(", query cost: ").append(cost); + str.append(", new query cost: ").append(candCost); + str.append(", took ").append(TimeUnit.NANOSECONDS.toMicros(clock.now() - now)).append(" us"); + + logger.debug(str.toString()); + } + + @Override + public void storeControllerConfig() + { + storeOptions(metadata, scalingParameters, getFlushSizeBytes()); + } + + @Override + public String toString() + { + return String.format("t: %s, o: %s, scalingParameters: %s - %s", FBUtilities.prettyPrintMemory(targetSSTableSize), Arrays.toString(survivalFactors), Arrays.toString(scalingParameters), calculator); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/unified/Controller.java b/src/java/org/apache/cassandra/db/compaction/unified/Controller.java index 2d0869369e6d..9b741fcb11ae 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/Controller.java +++ b/src/java/org/apache/cassandra/db/compaction/unified/Controller.java @@ -1,13 +1,11 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Copyright DataStax, Inc. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,76 +16,174 @@ package org.apache.cassandra.db.compaction.unified; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.config.CassandraRelevantProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.codahale.metrics.Gauge; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.compaction.CompactionAggregate; +import org.apache.cassandra.db.compaction.CompactionPick; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.db.compaction.CompactionStrategy; import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.utils.Overlaps; +import org.apache.cassandra.io.FSError; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileWriter; +import org.apache.cassandra.metrics.DefaultNameFactory; +import org.apache.cassandra.metrics.MetricNameFactory; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MonotonicClock; +import org.apache.cassandra.utils.Overlaps; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import static org.apache.cassandra.config.CassandraRelevantProperties.*; /** * The controller provides compaction parameters to the unified compaction strategy */ -public class Controller +// TODO there is a lot to be done with configuration - conversions, parsing, defaulting of configuration options should +// moved to some configuration utilities or use the existing ones. Also, maybe we should consider moving the config +// part out from this class into a dedicated compaction configuration class. +public abstract class Controller { protected static final Logger logger = LoggerFactory.getLogger(Controller.class); + private static final ConcurrentMap allMetrics = new ConcurrentHashMap<>(); /** - * The scaling parameters W, one per bucket index and separated by a comma. - * Higher indexes will use the value of the last index with a W specified. + * The data size in GB, it will be assumed that the node will have on disk roughly this size of data when it + * reaches equilibrium. The default is calculated by looking at the free space on all data directories, adjusting + * for ones belonging to the same drive. + */ + public static final String DATASET_SIZE_OPTION = "dataset_size"; + /** @deprecated See STAR-1878 */ + @Deprecated(since = "CC 4.0") + public static final String DATASET_SIZE_OPTION_GB = "dataset_size_in_gb"; + static final long DEFAULT_DATASET_SIZE = UCS_DATASET_SIZE.getSizeInBytesWithLegacyFallback(DatabaseDescriptor.getDataFileDirectoriesMinTotalSpaceInGB() << 30); + + /** + * The number of shards. This is the main configuration option for UCS V1 (i.e. before the density/overlap + * improvements). If the value is set, the strategy will switch to V1 mode which entails: + *

      + *
    • base_shard_count = num_shards + *
    • sstable_growth = 1 (i.e. always use the same number of shards) + *
    • min_sstable_size = auto (i.e. set from the size of first flush) + *
    • reserved_threads_per_level = max + *
    + * The option is undefined by default to engage the density version of UCS. */ - final static String SCALING_PARAMETERS_OPTION = "scaling_parameters"; - private final static String DEFAULT_SCALING_PARAMETERS = - CassandraRelevantProperties.UCS_SCALING_PARAMETER.getString(); + /** @deprecated See STAR-1878 */ + @Deprecated(since = "CC 4.0") + static final String NUM_SHARDS_OPTION = "num_shards"; + + /** + * The default number of shards defined via system property, see {@link #NUM_SHARDS_OPTION}. + * The property exists for backward compatibility, and is deprecated. It allows for configuring compactors, writers + * and replayers in CNDB without having to change the schema for each tenant. + */ + /** @deprecated See STAR-1898 */ + @Deprecated(since = "CC 4.0") + static final Optional DEFAULT_NUM_SHARDS = Optional.ofNullable(UCS_NUM_SHARDS.getStringWithLegacyFallback()).map(Integer::valueOf); /** * The minimum sstable size. Sharded writers split sstables over shard only if they are at least as large as the * minimum size. + *

    + * This is mainly present to support UCS V1 mode, which relies heavily on minimal SSTable + * size, and defaults to 0 which provides minimal parallelism on all levels of the hierarchy. + * In UCS V1 mode (engaged by using "num_shards" above) the default 'auto'. */ static final String MIN_SSTABLE_SIZE_OPTION = "min_sstable_size"; + /** @deprecated See STAR-1878 */ + @Deprecated(since = "CC 4.0") + static final String MIN_SSTABLE_SIZE_OPTION_MB = "min_sstable_size_in_mb"; + static final String MIN_SSTABLE_SIZE_OPTION_AUTO = "auto"; - private static final String DEFAULT_MIN_SSTABLE_SIZE = CassandraRelevantProperties.UCS_MIN_SSTABLE_SIZE.getString(); + static final long DEFAULT_MIN_SSTABLE_SIZE = UCS_MIN_SSTABLE_SIZE.getSizeInBytesWithLegacyFallback(); + static final long DEFAULT_VECTOR_MIN_SSTABLE_SIZE = UCS_VECTOR_MIN_SSTABLE_SIZE.getSizeInBytesWithLegacyFallback(); + /** + * Value to use to set the min sstable size from the flush size. + */ + static final long MIN_SSTABLE_SIZE_AUTO = -1; /** * Override for the flush size in MB. The database should be able to calculate this from executing flushes, this * should only be necessary in rare cases. */ static final String FLUSH_SIZE_OVERRIDE_OPTION = "flush_size_override"; + /** @deprecated See STAR-1878 */ + @Deprecated(since = "CC 4.0") + static final String FLUSH_SIZE_OVERRIDE_OPTION_MB = "flush_size_override_mb"; + + /** + * The maximum tolerable compaction-induced space amplification, as fraction of the dataset size. The idea behind + * this property is to be able to tune how much to limit concurrent "oversized" compactions in different shards. + * On one hand allowing such compactions concurrently running in all shards allows for STCS-like space + * amplification, where at some point you might need free space double the size of your working set to do a (top + * tier) compaction, while on the other hand limiting such compactions too much might lead to compaction lagging + * behind, higher read amplification, and other problems of that nature. + */ + public static final String MAX_SPACE_OVERHEAD_OPTION = "max_space_overhead"; + static final double DEFAULT_MAX_SPACE_OVERHEAD = UCS_MAX_SPACE_OVERHEAD.getDoubleWithLegacyFallback(); + static final double MAX_SPACE_OVERHEAD_LOWER_BOUND = 0.01; + static final double MAX_SPACE_OVERHEAD_UPPER_BOUND = 1.0; static final String BASE_SHARD_COUNT_OPTION = "base_shard_count"; /** - * Default base shard count, used when a base count is not explicitly supplied. This value applies as long as the - * table is not a system one, and directories are not defined. + * Default base shard count, used when a base count is not explicitly supplied. This value applies to all tables as + * long as they are larger than the minimum sstable size. * * For others a base count of 1 is used as system tables are usually small and do not need as much compaction * parallelism, while having directories defined provides for parallelism in a different way. */ - public static final int DEFAULT_BASE_SHARD_COUNT = - CassandraRelevantProperties.UCS_BASE_SHARD_COUNT.getInt(); + public static final int DEFAULT_BASE_SHARD_COUNT = UCS_BASE_SHARD_COUNT.getIntWithLegacyFalback(); + public static final int DEFAULT_VECTOR_BASE_SHARD_COUNT = UCS_VECTOR_BASE_SHARD_COUNT.getIntWithLegacyFalback(); + /** + * The target SSTable size. This is the size of the SSTables that the controller will try to create. + */ static final String TARGET_SSTABLE_SIZE_OPTION = "target_sstable_size"; - public static final long DEFAULT_TARGET_SSTABLE_SIZE = - CassandraRelevantProperties.UCS_TARGET_SSTABLE_SIZE.getSizeInBytes(); + public static final long DEFAULT_TARGET_SSTABLE_SIZE = UCS_TARGET_SSTABLE_SIZE.getSizeInBytesWithLegacyFallback(); + public static final long DEFAULT_VECTOR_TARGET_SSTABLE_SIZE = UCS_VECTOR_TARGET_SSTABLE_SIZE.getSizeInBytesWithLegacyFallback(); static final long MIN_TARGET_SSTABLE_SIZE = 1L << 20; + static final String IS_REPLICA_AWARE_OPTION = "is_replica_aware"; + public static final boolean DEFAULT_IS_REPLICA_AWARE = UCS_IS_REPLICA_AWARE.getBoolean(); + /** - * Provision for growth of the constructed SSTables as the size of the data grows. By default, the target SSTable - * size is fixed for all levels. In some scenarios it may be better to reduce the overall number of SSTables when + * Provision for growth of the constructed SSTables as the size of the data grows. By default the target SSTable + * size is fixed for all levels. In some scenarios is may be better to reduce the overall number of SSTables when * the data size becomes larger to avoid using too much memory and processing for the corresponding structures. * The setting enables such control and determines how much we reduce the growth of the number of split points as - * the data size grows. The number specifies the SSTable growth part, and the difference from 1 is the shard count + * the data size grows. The number specifies the sstable growth part, and the difference from 1 is the shard count * growth component, which is a multiplier applied to the logarithm of the data size, before it is rounded and * applied as an exponent in the number of split points. In other words, the given value applies as a negative * exponent in the calculation of the number of split points. @@ -96,7 +192,7 @@ public class Controller * target size. Setting this number to 1 will make UCS never split beyong the base shard count. Using 0.5 will * make the number of split points a square root of the required number for the target SSTable size, making * the number of split points and the size of SSTables grow in lockstep as the density grows. Using - * 0.333 (the default) makes the sstable growth the cubic root of the density growth, i.e. the SSTable size + * 0.333 (the default) makes the sstable growth the cubic root of the density growth, i.e. the sstable size * grows with the square root of the growth of the shard count. *

    * For example, given a data size of 1TiB on the top density level and 1GiB target size with base shard count of 1, @@ -108,38 +204,104 @@ public class Controller * a growth value of 0.333, and 64 (~16GiB each) for a growth value of 0.5. */ static final String SSTABLE_GROWTH_OPTION = "sstable_growth"; - private static final double DEFAULT_SSTABLE_GROWTH = CassandraRelevantProperties.UCS_SSTABLE_GROWTH.getDouble(); + static final double DEFAULT_SSTABLE_GROWTH = UCS_SSTABLE_GROWTH.getPercentageWithLegacyFallback(); + static final double DEFAULT_VECTOR_SSTABLE_GROWTH = UCS_VECTOR_SSTABLE_GROWTH.getPercentageWithLegacyFallback(); + + /** + * Number of reserved threads to keep for each compaction level. This is used to ensure that there are always + * threads ready to start processing a level when new data arrives. This is most valuable to prevent large + * compactions from keeping all threads busy for a long time; with smaller target sizes the overlap-driven + * preference mechanism should achieve better results. + *

    + * If the number is greater than the number of compaction threads divided by the number of levels rounded down, the + * latter will apply. Specifying "max" reserves as many threads as possible for each level. + *

    + * The default value is max, all compaction threads are distributed among the levels. + */ + static final String RESERVED_THREADS_OPTION = "reserved_threads"; + public static final int DEFAULT_RESERVED_THREADS = FBUtilities.parseIntAllowingMax(UCS_RESERVED_THREADS.getStringWithLegacyFallback("max")); + public static final int DEFAULT_VECTOR_RESERVED_THREADS = FBUtilities.parseIntAllowingMax(UCS_VECTOR_RESERVED_THREADS.getStringWithLegacyFallback("max")); + + /** + * Reservation type, defining whether reservations can be used by lower levels. If set to `per_level`, the + * reservations are only used by the specific level. If set to `level_or_below`, the reservations can be used by + * the specific level as well as any one below it. + *

    + * The default value is `level_or_below`. + */ + static final String RESERVATIONS_TYPE_OPTION = "reservations_type"; + public static final Reservations.Type DEFAULT_RESERVED_THREADS_TYPE = UCS_RESERVATIONS_TYPE_OPTION.getEnumWithLegacyFallback(true, Reservations.Type.class); /** * This parameter is intended to modify the shape of the LSM by taking into account the survival ratio of data, for now it is fixed to one. */ - static final double DEFAULT_SURVIVAL_FACTOR = - CassandraRelevantProperties.UCS_SURVIVAL_FACTOR.getDouble(); - static final double[] DEFAULT_SURVIVAL_FACTORS = new double[] { DEFAULT_SURVIVAL_FACTOR }; + static final double DEFAULT_SURVIVAL_FACTOR = UCS_SURVIVAL_FACTOR.getDoubleWithLegacyFallback(); + final static double[] DEFAULT_SURVIVAL_FACTORS = new double[] { DEFAULT_SURVIVAL_FACTOR }; + + /** + * Either true or false. This parameter determines which controller will be used. + */ + static final String ADAPTIVE_OPTION = "adaptive"; + static final boolean DEFAULT_ADAPTIVE = UCS_ADAPTIVE_ENABLED.getBooleanWithLegacyFallback(); /** * The maximum number of sstables to compact in one operation. * - * The default is 32, which aims to keep the length of operations under control and prevent accummulation of - * sstables while compactions are taking place. + * This is expected to be large and never be reached, but compaction going very very late may cause the accumulation + * of thousands and even tens of thousands of sstables which may cause problems if compacted in one long operation. + * The default is chosen to be half of the maximum permitted space overhead when the source sstables are of the + * minimum sstable size. * * If the fanout factor is larger than the maximum number of sstables, the strategy will ignore the latter. */ static final String MAX_SSTABLES_TO_COMPACT_OPTION = "max_sstables_to_compact"; static final String ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_OPTION = "unsafe_aggressive_sstable_expiration"; - static final boolean ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION = - CassandraRelevantProperties.ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION.getBoolean(); + static final String ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY = Config.PROPERTY_PREFIX + "allow_unsafe_aggressive_sstable_expiration"; + static final boolean ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION = CassandraRelevantProperties.ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION.getBoolean(); static final boolean DEFAULT_ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION = false; + /** + * System property to control writing scaling parameters to JSON configuration file. + * When set to true (default), the controller will persist scaling parameters and flush size to disk. + * When set to false, persistence is disabled. + */ + public static final String SCALING_PARAMETER_PERSISTENCE_PROPERTY = UCS_SCALING_PARAMETER_PERSISTENCE.getKey(); + public static final boolean SCALING_PARAMETER_PERSISTENCE = UCS_SCALING_PARAMETER_PERSISTENCE.getBooleanWithLegacyFallback(); + + /** + * This property allows seperate defaults for vector and non-vector tables. If this property is set to true + * and the table has a {@link VectorType}, the "vector" defaults are used over the regular defaults. For instance, + * "-Dunified_compaction.vector_sstable_growth" will be used over "-Dunified_compaction.sstable_growth". + */ + static final boolean OVERRIDE_UCS_CONFIG_FOR_VECTOR_TABLES = UCS_OVERRIDE_UCS_CONFIG_FOR_VECTOR_TABLES.getBoolean(false); + static final int DEFAULT_EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS = 60 * 10; static final String EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION = "expired_sstable_check_frequency_seconds"; + /** + * Either true or false. This parameter determines whether L0 will use + * shards or not. If L0 does not use shards then: + * - all flushed sstables use an ordinary writer, not a sharded writer + * - the arena selector disregards the first token of L0 sstables, placing + * them all in a unique shard. + */ + static final String L0_SHARDS_ENABLED_OPTION = "l0_shards_enabled"; + final static boolean DEFAULT_L0_SHARDS_ENABLED = UCS_L0_SHARDS_ENABLED.getBoolean(); + + /** + * True if L0 data may be coming from different replicas. + */ + public static final String SHARED_STORAGE = "shared_storage"; + /** The maximum exponent for shard splitting. The maximum number of shards is this number the base count shifted this many times left. */ static final int MAX_SHARD_SHIFT = 20; /** The maximum splitting factor for shards. The maximum number of shards is this number multiplied by the base count. */ static final double MAX_SHARD_SPLIT = Math.scalb(1, MAX_SHARD_SHIFT); + private static final double INVERSE_LOG_2 = 1.0 / Math.log(2); + private static final double INVERSE_SQRT_2 = Math.sqrt(0.5); + /** * Overlap inclusion method. NONE for participating sstables only (not recommended), SINGLE to only include sstables * that overlap with participating (LCS-like, higher concurrency during upgrades but some double compaction), @@ -147,115 +309,227 @@ public class Controller */ static final String OVERLAP_INCLUSION_METHOD_OPTION = "overlap_inclusion_method"; static final Overlaps.InclusionMethod DEFAULT_OVERLAP_INCLUSION_METHOD = - CassandraRelevantProperties.UCS_OVERLAP_INCLUSION_METHOD.getEnum(Overlaps.InclusionMethod.TRANSITIVE); + Overlaps.InclusionMethod.valueOf(UCS_OVERLAP_INCLUSION_METHOD.getStringWithLegacyFallback(Overlaps.InclusionMethod.TRANSITIVE.toString()).toUpperCase()); + + /** + * Whether to create subtask for the output shards of individual compactions and execute them in parallel. + * Defaults to true for improved parallelization and efficiency. + */ + static final String PARALLELIZE_OUTPUT_SHARDS_OPTION = "parallelize_output_shards"; + static final boolean DEFAULT_PARALLELIZE_OUTPUT_SHARDS = UCS_PARALLELIZE_OUTPUT_SHARDS.getBooleanWithLegacyFallback(); + + /** + * The scaling parameters W, one per bucket index and separated by a comma. + * Higher indexes will use the value of the last index with a W specified. + */ + static final String SCALING_PARAMETERS_OPTION = "scaling_parameters"; + /** @deprecated See STAR-1898 */ + @Deprecated(since = "CC 4.0") + static final String STATIC_SCALING_FACTORS_OPTION = "static_scaling_factors"; + + static final String MAX_SSTABLES_PER_SHARD_FACTOR_OPTION = "max_sstables_per_shard_factor"; + static final double DEFAULT_MAX_SSTABLES_PER_SHARD_FACTOR = UCS_MAX_SSTABLES_PER_SHARD_FACTOR.getDoubleWithLegacyFallback(); + + static final boolean USE_FACTORIZATION_SHARD_COUNT_GROWTH = CassandraRelevantProperties.USE_FACTORIZATION_SHARD_COUNT_GROWTH.getBoolean(); - protected final ColumnFamilyStore cfs; protected final MonotonicClock clock; - private final int[] scalingParameters; + protected final Environment env; protected final double[] survivalFactors; + protected final long dataSetSize; protected volatile long minSSTableSize; + protected final double maxSpaceOverhead; protected final long flushSizeOverride; protected volatile long currentFlushSize; protected final int maxSSTablesToCompact; protected final long expiredSSTableCheckFrequency; protected final boolean ignoreOverlapsInExpirationCheck; + protected final boolean parallelizeOutputShards; + protected final TableMetadata metadata; protected final int baseShardCount; + private final Optional factorizedShardSequence; - protected final double targetSSTableSize; + private final boolean isReplicaAware; + protected final long targetSSTableSize; protected final double sstableGrowthModifier; - static final double INVERSE_SQRT_2 = Math.sqrt(0.5); + protected final int reservedThreads; + protected final Reservations.Type reservationsType; - private static final double INVERSE_LOG_2 = 1.0 / Math.log(2); + @Nullable protected volatile CostsCalculator calculator; + @Nullable private volatile Metrics metrics; protected final Overlaps.InclusionMethod overlapInclusionMethod; - Controller(ColumnFamilyStore cfs, - MonotonicClock clock, - int[] scalingParameters, + final boolean l0ShardsEnabled; + final boolean hasVectorType; + + final double maxSstablesPerShardFactor; + + Controller(MonotonicClock clock, + Environment env, double[] survivalFactors, + long dataSetSize, long minSSTableSize, long flushSizeOverride, + long currentFlushSize, + double maxSpaceOverhead, int maxSSTablesToCompact, long expiredSSTableCheckFrequency, boolean ignoreOverlapsInExpirationCheck, int baseShardCount, - double targetSStableSize, + boolean isReplicaAware, + long targetSStableSize, double sstableGrowthModifier, - Overlaps.InclusionMethod overlapInclusionMethod) + int reservedThreads, + Reservations.Type reservationsType, + Overlaps.InclusionMethod overlapInclusionMethod, + boolean parallelizeOutputShards, + boolean hasVectorType, + double maxSstablesPerShardFactor, + TableMetadata metadata) { - this.cfs = cfs; this.clock = clock; - this.scalingParameters = scalingParameters; + this.env = env; this.survivalFactors = survivalFactors; + this.dataSetSize = dataSetSize; this.minSSTableSize = minSSTableSize; this.flushSizeOverride = flushSizeOverride; - this.currentFlushSize = flushSizeOverride; + this.currentFlushSize = currentFlushSize; this.expiredSSTableCheckFrequency = TimeUnit.MILLISECONDS.convert(expiredSSTableCheckFrequency, TimeUnit.SECONDS); this.baseShardCount = baseShardCount; + this.isReplicaAware = isReplicaAware; this.targetSSTableSize = targetSStableSize; this.overlapInclusionMethod = overlapInclusionMethod; this.sstableGrowthModifier = sstableGrowthModifier; - - if (maxSSTablesToCompact <= 0) - maxSSTablesToCompact = Integer.MAX_VALUE; + this.reservedThreads = reservedThreads; + this.reservationsType = reservationsType; + this.maxSpaceOverhead = maxSpaceOverhead; + this.l0ShardsEnabled = UCS_L0_SHARDS_ENABLED.getBooleanWithLegacyFallback(false); // FIXME VECTOR-23 + this.parallelizeOutputShards = parallelizeOutputShards; + this.hasVectorType = hasVectorType; + this.maxSstablesPerShardFactor = maxSstablesPerShardFactor; + this.metadata = metadata; + + if (maxSSTablesToCompact <= 0) // use half the maximum permitted compaction size as upper bound by default + maxSSTablesToCompact = (int) (dataSetSize * this.maxSpaceOverhead * 0.5 / getMinSstableSizeBytes()); this.maxSSTablesToCompact = maxSSTablesToCompact; if (ignoreOverlapsInExpirationCheck && !ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION) { - logger.warn("Not enabling aggressive SSTable expiration, as the system property '" + - CassandraRelevantProperties.ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION.name() + - "' is set to 'false'. " + - "Set it to 'true' to enable aggressive SSTable expiration."); + logger.warn("Not enabling aggressive SSTable expiration, as the system property '" + ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY + "' is set to 'false'. " + + "Set it to 'true' to enable aggressive SSTable expiration."); } this.ignoreOverlapsInExpirationCheck = ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION && ignoreOverlapsInExpirationCheck; + + this.factorizedShardSequence = useFactorizationShardCountGrowth() ? Optional.of(factorizedSmoothShardSequence(baseShardCount)) : Optional.empty(); } - /** - * @return the scaling parameter W - * @param index - */ - public int getScalingParameter(int index) + public static File getControllerConfigPath(TableMetadata metadata) { - if (index < 0) - throw new IllegalArgumentException("Index should be >= 0: " + index); + String suffix = "-controller-config.JSON"; + String fileName = metadata.keyspace + '.' + metadata.name + suffix; + if (fileName.length() > 255) + { + int spaceLeft = 255 - suffix.length() - 36 - 2; // 36 is the length of a UUID, 2 - for two separators + String keyspaceAbbrev = metadata.keyspace.substring(0, Math.min(metadata.keyspace.length(), spaceLeft / 2)); + spaceLeft -= keyspaceAbbrev.length(); + String tableAbbrev = metadata.name.substring(0, Math.min(metadata.name.length(), spaceLeft)); + fileName = String.format("%s.%s.%s%s", keyspaceAbbrev, tableAbbrev, metadata.id.toHexString(), suffix); + } + return new File(DatabaseDescriptor.getMetadataDirectory(), fileName); + } + + public static void storeOptions(TableMetadata metadata, int[] scalingParameters, long flushSizeBytes) + { + if (!SCALING_PARAMETER_PERSISTENCE) + { + logger.debug("Scaling parameter persistence is disabled via {}. Skipping write to disk.", SCALING_PARAMETER_PERSISTENCE_PROPERTY); + return; + } - return index < scalingParameters.length ? scalingParameters[index] : scalingParameters[scalingParameters.length - 1]; + if (SchemaConstants.isSystemKeyspace(metadata.keyspace)) + return; + File f = getControllerConfigPath(metadata); + try(FileWriter fileWriter = new FileWriter(f, File.WriteMode.OVERWRITE);) + { + JSONArray jsonArray = new JSONArray(); + JSONObject jsonObject = new JSONObject(); + for (int i = 0; i < scalingParameters.length; i++) + { + jsonArray.add(scalingParameters[i]); + } + jsonObject.put("scaling_parameters", jsonArray); + jsonObject.put("current_flush_size", flushSizeBytes); + fileWriter.write(jsonObject.toString()); + fileWriter.flush(); + + logger.debug(String.format("Writing current scaling parameters and flush size to file %s: %s", f.toPath().toString(), jsonObject)); + } + catch (IOException | FSError e) + { + logger.warn("Unable to save current scaling parameters and flush size. Current controller configuration will be lost if a node restarts: ", e); + } + catch (Throwable e) + { + logger.warn("Unable to save current scaling parameters and flush size. Current controller configuration will be lost if a node restarts: ", e); + JVMStabilityInspector.inspectThrowable(e); + } } - @Override - public String toString() + public abstract void storeControllerConfig(); + + @VisibleForTesting + public Environment getEnv() { - return String.format("Controller, m: %s, o: %s, Ws: %s", - FBUtilities.prettyPrintBinary(targetSSTableSize, "B", ""), - Arrays.toString(survivalFactors), - printScalingParameters(scalingParameters)); + return env; } + /** + * @return the scaling parameter W + * @param index + */ + public abstract int getScalingParameter(int index); + + public abstract int getPreviousScalingParameter(int index); + + public abstract int getMaxRecentAdaptiveCompactions(); + public abstract boolean isRecentAdaptive(CompactionPick pick); + public int getFanout(int index) { - int W = getScalingParameter(index); - return UnifiedCompactionStrategy.fanoutFromScalingParameter(W); + return UnifiedCompactionStrategy.fanoutFromScalingParameter(getScalingParameter(index)); } public int getThreshold(int index) { - int W = getScalingParameter(index); - return UnifiedCompactionStrategy.thresholdFromScalingParameter(W); + return UnifiedCompactionStrategy.thresholdFromScalingParameter(getScalingParameter(index)); + } + + public int getPreviousFanout(int index) { + return UnifiedCompactionStrategy.fanoutFromScalingParameter(getPreviousScalingParameter(index)); + } + + public int getPreviousThreshold(int index) { + return UnifiedCompactionStrategy.thresholdFromScalingParameter(getPreviousScalingParameter(index)); + } + + public int getFlushShards(double density) + { + return areL0ShardsEnabled() ? getNumShards(density) : 1; } /** - * Calculate the number of shards to split the local token space in for the given SSTable density. - * This is calculated as a power-of-two multiple of baseShardCount, so that the expected size of resulting SSTables + * Calculate the number of shards to split the local token space in for the given sstable density. + * This is calculated as a power-of-two multiple of baseShardCount, so that the expected size of resulting sstables * is between sqrt(0.5) and sqrt(2) times the target size, which is calculated from targetSSTableSize to grow * at the given sstableGrowthModifier of the exponential growth of the density. *

    - * Additionally, if a minimum SSTable size is set, we can go below the baseShardCount when that would result in - * SSTables smaller than that minimum. Note that in the case of a non-power-of-two base count, we will only - * split to divisors of baseShardCount. + * Additionally, if a minimum sstable size is set, we can go below the baseShardCount when that would result in + * sstables smaller than that minimum. Note that in the case of a non-power-of-two base count this will cause + * smaller sstables to not be aligned with the ones whose size is enough for the base count. *

    - * Note that to get the SSTables resulting from this splitting within the bounds, the density argument must be + * Note that to get the sstables resulting from this splitting within the bounds, the density argument must be * normalized to the span that is being split. In other words, if no disks are defined, the density should be * scaled by the token coverage of the locally-owned ranges. If multiple data directories are defined, the density * should be scaled by the token coverage of the respective data directory. That is, localDensity = size / span, @@ -265,24 +539,33 @@ public int getNumShards(double localDensity) { int shards; // Check the minimum size first. - if (minSSTableSize > 0) + long minSize = getMinSstableSizeBytes(); + if (minSize > 0) { - double count = localDensity / minSSTableSize; + double count = localDensity / minSize; // Minimum size only applies if the base count would result in smaller sstables. // We also want to use the min size if we don't yet know the flush size (density is NaN). // Note: the minimum size cannot be larger than the target size's minimum. if (!(count >= baseShardCount)) // also true for count == NaN { - // Make it a power of two, rounding down so that sstables are greater in size than the min. - // Setting the bottom bit to 1 ensures the result is at least 1. - // If baseShardCount is not a power of 2, split only to powers of two that are divisors of baseShardCount so boundaries match higher levels - shards = Math.min(Integer.highestOneBit((int) count | 1), baseShardCount & -baseShardCount); + // Use factorization-based growth for smoother progression if it's not power of 2 + if (factorizedShardSequence.isPresent()) + { + shards = getLargestFactorizedShardCount(count); + } + else + { + // Make it a power of two, rounding down so that sstables are greater in size than the min. + // Setting the bottom bit to 1 ensures the result is at least 1. + // If baseShardCount is not a power of 2, split only to powers of two that are divisors of baseShardCount so boundaries match higher levels + shards = Math.min(Integer.highestOneBit((int) count | 1), baseShardCount & -baseShardCount); + } if (logger.isDebugEnabled()) logger.debug("Shard count {} for density {}, {} times min size {}", shards, FBUtilities.prettyPrintBinary(localDensity, "B", " "), - localDensity / minSSTableSize, - FBUtilities.prettyPrintBinary(minSSTableSize, "B", " ")); + localDensity / minSize, + FBUtilities.prettyPrintBinary(minSize, "B", " ")); return shards; } @@ -291,9 +574,10 @@ public int getNumShards(double localDensity) if (sstableGrowthModifier == 1) { shards = baseShardCount; - logger.debug("Shard count {} for density {} in fixed shards mode", + logger.debug("Shard count {} for density {} in fixed shards mode. SStableGrowthModifier {}", shards, - FBUtilities.prettyPrintBinary(localDensity, "B", " ")); + FBUtilities.prettyPrintBinary(localDensity, "B", " "), + sstableGrowthModifier); return shards; } else if (sstableGrowthModifier == 0) @@ -313,11 +597,12 @@ else if (sstableGrowthModifier == 0) shards = baseShardCount * Integer.highestOneBit((int) count | 1); if (logger.isDebugEnabled()) - logger.debug("Shard count {} for density {}, {} times target {}", + logger.debug("Shard count {} for density {}, {} times target {}. SStableGrowthModifier {}", shards, FBUtilities.prettyPrintBinary(localDensity, "B", " "), localDensity / targetSSTableSize, - FBUtilities.prettyPrintBinary(targetSSTableSize, "B", " ")); + FBUtilities.prettyPrintBinary(targetSSTableSize, "B", " "), + sstableGrowthModifier); return shards; } else @@ -330,7 +615,7 @@ else if (sstableGrowthModifier == 0) // targetSSTableSize * sqrt(2). Finally, make sure the exponent is at least 0 and not greater than the // fixed maximum. // Note: This code also works correctly for the special cases of sstableGrowthModifier == 0 and 1, - // but the above code avoids the imprecise floating point arithmetic for these common cases. + // but the above code avoids the floating point arithmetic for these common cases. // Note: We use log instead of getExponent because we also need the non-integer part of the logarithm // in order to apply the growth modifier correctly. final double countLog = Math.log(count); @@ -345,16 +630,35 @@ else if (pow >= 0) if (logger.isDebugEnabled()) { long targetSize = (long) (targetSSTableSize * Math.exp(countLog * sstableGrowthModifier)); - logger.debug("Shard count {} for density {}, {} times target {}", + logger.debug("Shard count {} for density {}, {} times target {}. SStableGrowthModifier {}", shards, FBUtilities.prettyPrintBinary(localDensity, "B", " "), localDensity / targetSize, - FBUtilities.prettyPrintBinary(targetSize, "B", " ")); + FBUtilities.prettyPrintBinary(targetSize, "B", " "), + sstableGrowthModifier); } return shards; } } + public boolean parallelizeOutputShards() + { + return parallelizeOutputShards; + } + + public boolean isReplicaAware() + { + return isReplicaAware; + } + + /** + * @return whether L0 should use shards + */ + public boolean areL0ShardsEnabled() + { + return l0ShardsEnabled; + } + /** * @return the survival factor o * @param index @@ -367,6 +671,57 @@ public double getSurvivalFactor(int index) return index < survivalFactors.length ? survivalFactors[index] : survivalFactors[survivalFactors.length - 1]; } + /** + * The user specified dataset size. + * + * @return the target size of the entire data set, in bytes. + */ + public long getDataSetSizeBytes() + { + return dataSetSize; + } + + public long getTargetSSTableSize() + { + return targetSSTableSize; + } + + /** + * Return the sstable size in bytes. + * + * This is either set by the user in the options or calculated by rounding up the first flush size to 50 MB. + * + * @return the minimum sstable size in bytes. + */ + public long getMinSstableSizeBytes() + { + if (minSSTableSize >= 0) + return minSSTableSize; + + synchronized (this) + { + if (minSSTableSize >= 0) + return minSSTableSize; + + // round the avg flush size to the nearest byte + long envFlushSize = Math.round(env.flushSize()); + long fiftyMB = 50 << 20; + + // round up to 50 MB + long flushSize = ((Math.max(1, envFlushSize) + fiftyMB - 1) / fiftyMB) * fiftyMB; + + // If the env flush size is positive, then we've flushed at least once and we use this value permanently + if (envFlushSize > 0) + { + // When a target size is specified, the minimum cannot be higher than the lower bound for that target size. + flushSize = Math.min(flushSize, (long) (targetSSTableSize * INVERSE_SQRT_2)); + minSSTableSize = flushSize; + } + + return flushSize; + } + } + /** * Return the flush sstable size in bytes. * @@ -381,7 +736,7 @@ public long getFlushSizeBytes() if (flushSizeOverride > 0) return flushSizeOverride; - double envFlushSize = cfs.metric.flushSizeOnDisk.get(); + double envFlushSize = env.flushSize(); if (currentFlushSize == 0 || Math.abs(1 - (currentFlushSize / envFlushSize)) > 0.5) { // The current size is not initialized, or it differs by over 50% from the observed. @@ -391,6 +746,36 @@ public long getFlushSizeBytes() return currentFlushSize; } + /** + * Returns the maximum tolerable compaction-induced space amplification, as a fraction of the dataset size. + * Currently this is not a strict limit for which compaction gives an ironclad guarantee never to exceed it, but + * the main input in a simple heuristic that is designed to limit UCS' space amplification in exchange of some + * delay in top bucket compactions. + * + * @return a {@code double} value between 0.01 and 1.0, representing the fraction of the expected uncompacted + * dataset size that should be additionally available for compaction's space amplification overhead. + */ + public double getMaxSpaceOverhead() + { + return maxSpaceOverhead; + } + + /** + * Returns the number of reserved threads per level. If the size of SSTables is small, this can be 0 as operations + * finish quickly and the prioritization will do a good job of assigning threads to the levels. If the size of + * SSTables can grow large, threads must be reserved to ensure that compactions, esp. on level 0, do not have to + * wait for long operations to complete. + */ + public int getReservedThreads() + { + return reservedThreads; + } + + public Reservations.Type getReservationsType() + { + return reservationsType; + } + /** * @return whether is allowed to drop expired SSTables without checking if partition keys appear in other SSTables. * Same behavior as in TWCS. @@ -405,13 +790,200 @@ public long getExpiredSSTableCheckFrequency() return expiredSSTableCheckFrequency; } - public static Controller fromOptions(ColumnFamilyStore cfs, Map options) + /** + * Perform any initialization that requires the strategy. + */ + public void startup(UnifiedCompactionStrategy strategy, ScheduledExecutorService executorService) + { + if (calculator != null) + throw new IllegalStateException("Already started"); + + startup(strategy, new CostsCalculator(env, strategy, executorService)); + } + + @VisibleForTesting + void startup(UnifiedCompactionStrategy strategy, CostsCalculator calculator) { - int[] Ws = parseScalingParameters(options.getOrDefault(SCALING_PARAMETERS_OPTION, DEFAULT_SCALING_PARAMETERS)); + this.calculator = calculator; + metrics = allMetrics.computeIfAbsent(strategy.getMetadata(), Controller.Metrics::new); + metrics.setController(this); + logger.debug("Started compaction {}", this); + } + + /** + * Signals that the strategy is about to be deleted or stopped. + */ + public void shutdown() + { + if (calculator == null) + return; + + calculator.close(); + calculator = null; - long flushSizeOverride = FBUtilities.parseHumanReadableBytes(options.getOrDefault(FLUSH_SIZE_OVERRIDE_OPTION, - "0MiB")); - int maxSSTablesToCompact = Integer.parseInt(options.getOrDefault(MAX_SSTABLES_TO_COMPACT_OPTION, "0")); + if (metrics != null) + { + metrics.release(); + metrics.removeController(); + metrics = null; + } + + logger.debug("Stopped compaction controller {}", this); + } + + public boolean hasVectorType() + { + return hasVectorType; + } + + public double getMaxSstablesPerShardFactor() + { + return maxSstablesPerShardFactor; + } + + /** + * @return true if the controller is running + */ + public boolean isRunning() + { + return calculator != null; + } + + /** + * @return the cost calculator, will be null until {@link this#startup(UnifiedCompactionStrategy, ScheduledExecutorService)} is called. + */ + @Nullable + @VisibleForTesting + public CostsCalculator getCalculator() + { + return calculator; + } + + /** + * The strategy will call this method each time {@link CompactionStrategy#getNextBackgroundTasks(long)} is called. + */ + public void onStrategyBackgroundTaskRequest() + { + } + + /** + * Calculate the read amplification assuming a single scaling parameter W and a given total + * length of data on disk. + * + * @param length the total length on disk + * @param scalingParameter the scaling parameter to use for the calculation + * + * @return the read amplification of all the buckets needed to cover the total length + */ + public int readAmplification(long length, int scalingParameter) + { + double o = getSurvivalFactor(0); + long m = getFlushSizeBytes(); + + int F = UnifiedCompactionStrategy.fanoutFromScalingParameter(scalingParameter); + int T = UnifiedCompactionStrategy.thresholdFromScalingParameter(scalingParameter); + int maxIndex = maxBucketIndex(length, F); + + int ret = 0; + for (int i = 0; i < maxIndex; i++) + ret += T - 1; + + if (scalingParameter >= 0) + ret += Math.max(0, Math.ceil(length / (m * Math.pow(o * F, maxIndex))) - 1); + else + ret += 1; + + return ret; + } + + /** + * Calculate the write amplification assuming a single scaling parameter W and a given total + * length of data on disk. + * + * @param length the total length on disk + * @param scalingParameter the scaling parameter to use for the calculation + * + * @return the write amplification of all the buckets needed to cover the total length + */ + public int writeAmplification(long length, int scalingParameter) + { + double o = getSurvivalFactor(0); + long m = getFlushSizeBytes(); + + int F = UnifiedCompactionStrategy.fanoutFromScalingParameter(scalingParameter); + int maxIndex = maxBucketIndex(length, F); + + int ret = 0; + + if (scalingParameter >= 0) + { // for tiered, at each level the WA is 1. We start at level 0 and end up at level maxIndex so that's a WA of maxIndex. + ret += maxIndex + 1; + } + else + { // for leveled, at each level the WA is F - 1 except for the last one, where it's (size / size of previous level) - 1 + // or (size / (m*(o*F)^maxIndex)) - 1 + for (int i = 0; i < maxIndex; i++) + ret += F - 1; + + ret += Math.max(0, Math.ceil(length / (m * Math.pow(o * F, maxIndex)))); + } + + return ret; + } + + /** + * Returns a maximum bucket index for the given data size and fanout. + */ + private int maxBucketIndex(long totalLength, int fanout) + { + double o = getSurvivalFactor(0); + long m = getFlushSizeBytes(); + return Math.max(0, (int) Math.floor((Math.log(totalLength) - Math.log(m)) / (Math.log(fanout) - Math.log(o)))); + } + + private double getReadIOCost() + { + if (calculator == null) + return 0; + + int scalingParameter = getScalingParameter(0); + long length = (long) Math.ceil(calculator.spaceUsed()); + return calculator.getReadCostForQueries(readAmplification(length, scalingParameter)); + } + + private double getWriteIOCost() + { + if (calculator == null) + return 0; + + int scalingParameter = getScalingParameter(0); + long length = (long) Math.ceil(calculator.spaceUsed()); + return calculator.getWriteCostForQueries(writeAmplification(length, scalingParameter)); + } + + public static Controller fromOptions(CompactionRealm realm, Map options) + { + // Note: These options have been validated, but the defaults are configured with -D options that may be + // different. We thus may end up with configurations combinations that do not make sense. + // We will attempt to correct such combinations and issue warnings where possible. + + boolean hasVectorType = realm.metadata().hasVectorType(); + boolean vectorOverride = OVERRIDE_UCS_CONFIG_FOR_VECTOR_TABLES; + boolean useVectorOptions = hasVectorType && vectorOverride; + if (logger.isTraceEnabled()) + { + if (useVectorOptions) + logger.trace("Using UCS configuration optimized for vector for {}.{}", realm.getKeyspaceName(), realm.getTableName()); + else + logger.trace("Using non-vector UCS configuration for {}.{}", realm.getKeyspaceName(), realm.getTableName()); + } + boolean adaptive = options.containsKey(ADAPTIVE_OPTION) ? Boolean.parseBoolean(options.get(ADAPTIVE_OPTION)) : DEFAULT_ADAPTIVE; + long dataSetSize = getSizeWithAlt(options, DATASET_SIZE_OPTION, DATASET_SIZE_OPTION_GB, 30, DEFAULT_DATASET_SIZE); + long flushSizeOverride = getSizeWithAlt(options, FLUSH_SIZE_OVERRIDE_OPTION, FLUSH_SIZE_OVERRIDE_OPTION_MB, 20, 0); + double maxSpaceOverhead = options.containsKey(MAX_SPACE_OVERHEAD_OPTION) + ? FBUtilities.parsePercent(options.get(MAX_SPACE_OVERHEAD_OPTION)) + : DEFAULT_MAX_SPACE_OVERHEAD; + int maxSSTablesToCompact = Integer.parseInt(options.getOrDefault(MAX_SSTABLES_TO_COMPACT_OPTION, "32")); long expiredSSTableCheckFrequency = options.containsKey(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION) ? Long.parseLong(options.get(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION)) : DEFAULT_EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS; @@ -426,162 +998,355 @@ public static Controller fromOptions(ColumnFamilyStore cfs, Map } else { - baseShardCount = DEFAULT_BASE_SHARD_COUNT; + baseShardCount = useVectorOptions ? DEFAULT_VECTOR_BASE_SHARD_COUNT : DEFAULT_BASE_SHARD_COUNT; } + boolean isReplicaAware = options.containsKey(IS_REPLICA_AWARE_OPTION) + ? Boolean.parseBoolean(options.get(IS_REPLICA_AWARE_OPTION)) + : DEFAULT_IS_REPLICA_AWARE; + long targetSStableSize = options.containsKey(TARGET_SSTABLE_SIZE_OPTION) - ? FBUtilities.parseHumanReadableBytes(options.get(TARGET_SSTABLE_SIZE_OPTION)) - : DEFAULT_TARGET_SSTABLE_SIZE; + ? FBUtilities.parseHumanReadableBytes(options.get(TARGET_SSTABLE_SIZE_OPTION)) + : useVectorOptions ? DEFAULT_VECTOR_TARGET_SSTABLE_SIZE : DEFAULT_TARGET_SSTABLE_SIZE; - long minSSTableSize = options.containsKey(MIN_SSTABLE_SIZE_OPTION) - ? FBUtilities.parseHumanReadableBytes(options.get(MIN_SSTABLE_SIZE_OPTION)) - : FBUtilities.parseHumanReadableBytes(DEFAULT_MIN_SSTABLE_SIZE); + long minSSTableSize; + if (MIN_SSTABLE_SIZE_OPTION_AUTO.equalsIgnoreCase(options.get(MIN_SSTABLE_SIZE_OPTION))) + minSSTableSize = MIN_SSTABLE_SIZE_AUTO; + else + minSSTableSize = getSizeWithAlt(options, + MIN_SSTABLE_SIZE_OPTION, + MIN_SSTABLE_SIZE_OPTION_MB, + 20, + useVectorOptions ? DEFAULT_VECTOR_MIN_SSTABLE_SIZE : DEFAULT_MIN_SSTABLE_SIZE); - double sstableGrowthModifier = DEFAULT_SSTABLE_GROWTH; + double sstableGrowthModifier = useVectorOptions ? DEFAULT_VECTOR_SSTABLE_GROWTH : DEFAULT_SSTABLE_GROWTH; if (options.containsKey(SSTABLE_GROWTH_OPTION)) sstableGrowthModifier = FBUtilities.parsePercent(options.get(SSTABLE_GROWTH_OPTION)); - Overlaps.InclusionMethod inclusionMethod = options.containsKey(OVERLAP_INCLUSION_METHOD_OPTION) - ? Overlaps.InclusionMethod.valueOf(options.get(OVERLAP_INCLUSION_METHOD_OPTION).toUpperCase()) - : DEFAULT_OVERLAP_INCLUSION_METHOD; - - return new Controller(cfs, - MonotonicClock.Global.preciseTime, - Ws, - DEFAULT_SURVIVAL_FACTORS, - minSSTableSize, - flushSizeOverride, - maxSSTablesToCompact, - expiredSSTableCheckFrequency, - ignoreOverlapsInExpirationCheck, - baseShardCount, - targetSStableSize, - sstableGrowthModifier, - inclusionMethod); + int reservedThreadsPerLevel = options.containsKey(RESERVED_THREADS_OPTION) + ? FBUtilities.parseIntAllowingMax(options.get(RESERVED_THREADS_OPTION)) + : useVectorOptions ? DEFAULT_VECTOR_RESERVED_THREADS : DEFAULT_RESERVED_THREADS; + Reservations.Type reservationsType = options.containsKey(RESERVATIONS_TYPE_OPTION) + ? Reservations.Type.valueOf(options.get(RESERVATIONS_TYPE_OPTION).toUpperCase()) + : DEFAULT_RESERVED_THREADS_TYPE; + + if (options.containsKey(NUM_SHARDS_OPTION) || DEFAULT_NUM_SHARDS.isPresent()) + { + // Legacy V1 mode is enabled when the number of shards is defined and has a positive value. + // Table property takes precendence over system property. + int numShards = options.containsKey(NUM_SHARDS_OPTION) + ? Integer.parseInt(options.get(NUM_SHARDS_OPTION)) + : DEFAULT_NUM_SHARDS.get(); + + if (numShards > 0) + { + if (!options.containsKey(MIN_SSTABLE_SIZE_OPTION)) + minSSTableSize = MIN_SSTABLE_SIZE_AUTO; + baseShardCount = numShards; + sstableGrowthModifier = 1.0; + targetSStableSize = Long.MAX_VALUE; // this no longer plays a part, the result of getNumShards before + // accounting for minimum size is always baseShardCount + + double maxSpaceOverheadLowerBound = 1.0d / numShards; + if (maxSpaceOverhead < maxSpaceOverheadLowerBound) + { + logger.warn("{} shards are not enough to maintain the required maximum space overhead of {}!\n" + + "Falling back to {}={} instead. If this limit needs to be satisfied, please increase the number" + + " of shards.", + numShards, + maxSpaceOverhead, + MAX_SPACE_OVERHEAD_OPTION, + String.format("%.3f", maxSpaceOverheadLowerBound)); + maxSpaceOverhead = maxSpaceOverheadLowerBound; + } + } + } + + if (baseShardCount > 1 && sstableGrowthModifier != 1.0 && minSSTableSize != MIN_SSTABLE_SIZE_AUTO && minSSTableSize > targetSStableSize * INVERSE_SQRT_2) + { + // Note: not checked for baseShardCount == 1 as min size is irrelevant when the base count is 1. + // Note: not checked for sstableGrowthModifier = 1.0 as target size is irrelevant when the growth is 1. + long newTargetSize = (long) (minSSTableSize / INVERSE_SQRT_2); + logger.warn("Minimum sstable size {} is larger than target sstable size's minimum bound {}. Adjusting target size to {}.", + FBUtilities.prettyPrintMemory(minSSTableSize), + FBUtilities.prettyPrintMemory((long) (targetSStableSize * INVERSE_SQRT_2)), + FBUtilities.prettyPrintMemory(newTargetSize)); + targetSStableSize = newTargetSize; + } + + Environment env = realm.makeUCSEnvironment(); + + // For remote storage, the sstables on L0 are created by the different replicas, and therefore it is likely + // that there are RF identical copies, so here we adjust the survival factor for L0 + double[] survivalFactors = !UCS_SHARED_STORAGE.getBooleanWithLegacyFallback() + ? DEFAULT_SURVIVAL_FACTORS + : new double[] { DEFAULT_SURVIVAL_FACTOR / realm.getKeyspaceReplicationStrategy().getReplicationFactor().allReplicas, DEFAULT_SURVIVAL_FACTOR }; + + Overlaps.InclusionMethod overlapInclusionMethod = options.containsKey(OVERLAP_INCLUSION_METHOD_OPTION) + ? Overlaps.InclusionMethod.valueOf(options.get(OVERLAP_INCLUSION_METHOD_OPTION).toUpperCase()) + : DEFAULT_OVERLAP_INCLUSION_METHOD; + + boolean parallelizeOutputShards = options.containsKey(PARALLELIZE_OUTPUT_SHARDS_OPTION) + ? Boolean.parseBoolean(options.get(PARALLELIZE_OUTPUT_SHARDS_OPTION)) + : DEFAULT_PARALLELIZE_OUTPUT_SHARDS; + + double maxSstablesPerShardFactor = options.containsKey(MAX_SSTABLES_PER_SHARD_FACTOR_OPTION) + ? Double.parseDouble(options.get(MAX_SSTABLES_PER_SHARD_FACTOR_OPTION)) + : DEFAULT_MAX_SSTABLES_PER_SHARD_FACTOR; + + return adaptive + ? AdaptiveController.fromOptions(env, + survivalFactors, + dataSetSize, + minSSTableSize, + flushSizeOverride, + maxSpaceOverhead, + maxSSTablesToCompact, + expiredSSTableCheckFrequency, + ignoreOverlapsInExpirationCheck, + baseShardCount, + isReplicaAware, + targetSStableSize, + sstableGrowthModifier, + reservedThreadsPerLevel, + reservationsType, + overlapInclusionMethod, + parallelizeOutputShards, + hasVectorType, + maxSstablesPerShardFactor, + realm.metadata(), + options) + : StaticController.fromOptions(env, + survivalFactors, + dataSetSize, + minSSTableSize, + flushSizeOverride, + maxSpaceOverhead, + maxSSTablesToCompact, + expiredSSTableCheckFrequency, + ignoreOverlapsInExpirationCheck, + baseShardCount, + isReplicaAware, + targetSStableSize, + sstableGrowthModifier, + reservedThreadsPerLevel, + reservationsType, + overlapInclusionMethod, + parallelizeOutputShards, + hasVectorType, + maxSstablesPerShardFactor, + realm.metadata(), + options, + useVectorOptions); } public static Map validateOptions(Map options) throws ConfigurationException { + // Note: Validation must ignore the defaults set with -D options, because this node may be getting a configuration + // applied via a different coordinator which had different -D settings. If we abort because of such differences, + // we may cause schema mismatches between nodes which can quickly become a serious problem. + + String nonPositiveErr = "Invalid configuration, %s should be positive: %d"; + String intParseErr = "%s is not a parsable int (base10) for %s"; + String longParseErr = "%s is not a parsable long (base10) for %s"; + String floatParseErr = "%s is not a parsable float for %s"; options = new HashMap<>(options); String s; + long minSSTableSize = -1; + long targetSSTableSize = -1; - s = options.remove(SCALING_PARAMETERS_OPTION); - if (s != null) - parseScalingParameters(s); - - s = options.remove(BASE_SHARD_COUNT_OPTION); + s = options.remove(NUM_SHARDS_OPTION); if (s != null) { try { int numShards = Integer.parseInt(s); - if (numShards <= 0) - throw new ConfigurationException(String.format("Invalid configuration, %s should be positive: %d", - BASE_SHARD_COUNT_OPTION, + if (numShards <= 0 && numShards != -1) + throw new ConfigurationException(String.format("Invalid configuration, %s=%d should be positive, or -1 " + + "to explicitly disable static sharding for this table.", + NUM_SHARDS_OPTION, numShards)); + if (numShards != -1) + { + List incompatibleOptions = List.of(TARGET_SSTABLE_SIZE_OPTION, SSTABLE_GROWTH_OPTION, BASE_SHARD_COUNT_OPTION); + if (incompatibleOptions.stream().anyMatch(options::containsKey)) + { + throw new ConfigurationException(String.format("Option %s cannot be used in combination with %s", + NUM_SHARDS_OPTION, + incompatibleOptions.stream().filter(options::containsKey).collect(Collectors.joining(", ")))); + } + } + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format(intParseErr, s, NUM_SHARDS_OPTION), e); + } + } + + boolean adaptive = validateBoolean(options, ADAPTIVE_OPTION, DEFAULT_ADAPTIVE); + validateBoolean(options, IS_REPLICA_AWARE_OPTION, DEFAULT_IS_REPLICA_AWARE); + validateBoolean(options, PARALLELIZE_OUTPUT_SHARDS_OPTION, DEFAULT_PARALLELIZE_OUTPUT_SHARDS); + + validateSizeWithAlt(options, FLUSH_SIZE_OVERRIDE_OPTION, FLUSH_SIZE_OVERRIDE_OPTION_MB, 20); + validateSizeWithAlt(options, DATASET_SIZE_OPTION, DATASET_SIZE_OPTION_GB, 30); + + s = options.remove(MAX_SSTABLES_TO_COMPACT_OPTION); + if (s != null) + { + try + { + Integer.parseInt(s); // values less than or equal to 0 enable the default + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format(intParseErr, + s, + MAX_SSTABLES_TO_COMPACT_OPTION), + e); + } + } + s = options.remove(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION); + if (s != null) + { + try + { + long expiredSSTableCheckFrequency = Long.parseLong(s); + if (expiredSSTableCheckFrequency <= 0) + throw new ConfigurationException(String.format(nonPositiveErr, + EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION, + expiredSSTableCheckFrequency)); } catch (NumberFormatException e) { - throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", + throw new ConfigurationException(String.format(longParseErr, s, - BASE_SHARD_COUNT_OPTION), e); + EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION), + e); } } - // preserve the configuration for later use during min_sstable_size. - long targetSSTableSize = DEFAULT_TARGET_SSTABLE_SIZE; - s = options.remove(TARGET_SSTABLE_SIZE_OPTION); + s = options.remove(MAX_SPACE_OVERHEAD_OPTION); if (s != null) { try { - double targetSize = FBUtilities.parseHumanReadable(s, null, "B"); - if (targetSize >= Long.MAX_VALUE) { - throw new ConfigurationException(String.format("%s %s is out of range of Long.", - TARGET_SSTABLE_SIZE_OPTION, - s)); - } - if (targetSize < MIN_TARGET_SSTABLE_SIZE) - { - throw new ConfigurationException(String.format("%s %s is not acceptable, size must be at least %s", - TARGET_SSTABLE_SIZE_OPTION, - s, - FBUtilities.prettyPrintMemory(MIN_TARGET_SSTABLE_SIZE))); - } - targetSSTableSize = (long) Math.ceil(targetSize); + double maxSpaceOverhead = FBUtilities.parsePercent(s); + if (maxSpaceOverhead < MAX_SPACE_OVERHEAD_LOWER_BOUND || maxSpaceOverhead > MAX_SPACE_OVERHEAD_UPPER_BOUND) + throw new ConfigurationException(String.format("Invalid configuration, %s must be between %f and %f: %s", + MAX_SPACE_OVERHEAD_OPTION, + MAX_SPACE_OVERHEAD_LOWER_BOUND, + MAX_SPACE_OVERHEAD_UPPER_BOUND, + s)); } catch (NumberFormatException e) { - throw new ConfigurationException(String.format("%s %s is not a valid size in bytes: %s", - TARGET_SSTABLE_SIZE_OPTION, + throw new ConfigurationException(String.format(floatParseErr, s, + MAX_SPACE_OVERHEAD_OPTION), + e); + } + } + + validateBoolean(options, ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_OPTION, false); + + s = options.remove(SSTABLE_GROWTH_OPTION); + if (s != null) + { + try + { + double ssTableGrowthModifier = FBUtilities.parsePercent(s); + if (ssTableGrowthModifier < 0 || ssTableGrowthModifier > 1) + throw new ConfigurationException(String.format("%s %s must be between 0 and 1", + SSTABLE_GROWTH_OPTION, + s)); + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format("%s is not a valid number between 0 and 1: %s", + SSTABLE_GROWTH_OPTION, e.getMessage()), e); } } - s = options.remove(FLUSH_SIZE_OVERRIDE_OPTION); + s = options.remove(BASE_SHARD_COUNT_OPTION); if (s != null) { try { - long flushSize = FBUtilities.parseHumanReadableBytes(s); - if (flushSize < MIN_TARGET_SSTABLE_SIZE) + int baseShardCount = Integer.parseInt(s); + if (baseShardCount <= 0) + throw new ConfigurationException(String.format(nonPositiveErr, + BASE_SHARD_COUNT_OPTION, + baseShardCount)); + } + catch (NumberFormatException e) + { + throw new ConfigurationException(String.format(intParseErr, s, BASE_SHARD_COUNT_OPTION), e); + } + } + + s = options.remove(TARGET_SSTABLE_SIZE_OPTION); + if (s != null) + { + try + { + targetSSTableSize = FBUtilities.parseHumanReadableBytes(s); + if (targetSSTableSize < MIN_TARGET_SSTABLE_SIZE) throw new ConfigurationException(String.format("%s %s is not acceptable, size must be at least %s", - FLUSH_SIZE_OVERRIDE_OPTION, + TARGET_SSTABLE_SIZE_OPTION, s, FBUtilities.prettyPrintMemory(MIN_TARGET_SSTABLE_SIZE))); } catch (NumberFormatException e) { - throw new ConfigurationException(String.format("%s %s is not a valid size in bytes: %s", - FLUSH_SIZE_OVERRIDE_OPTION, - s, + throw new ConfigurationException(String.format("%s is not a valid size in bytes: %s", + TARGET_SSTABLE_SIZE_OPTION, e.getMessage()), e); } } - s = options.remove(MAX_SSTABLES_TO_COMPACT_OPTION); - if (s != null) - { - try - { - Integer.parseInt(s); // values less than or equal to 0 enable the default - } - catch (NumberFormatException e) - { - throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", - s, - MAX_SSTABLES_TO_COMPACT_OPTION), - e); - } - } - s = options.remove(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION); + minSSTableSize = validateSizeWithAlt(options, MIN_SSTABLE_SIZE_OPTION, MIN_SSTABLE_SIZE_OPTION_MB, 20, MIN_SSTABLE_SIZE_OPTION_AUTO, -1, -1); + // If both target and min sstable size are defined, check that they are compatible. + if (minSSTableSize > 0 && targetSSTableSize > 0 && minSSTableSize > targetSSTableSize * INVERSE_SQRT_2) + throw new ConfigurationException(String.format("The minimum sstable size %s cannot be larger than the target size's lower bound %s.", + FBUtilities.prettyPrintMemory(minSSTableSize), + FBUtilities.prettyPrintMemory((long) (targetSSTableSize * INVERSE_SQRT_2)))); + + s = options.remove(RESERVED_THREADS_OPTION); if (s != null) { try { - long expiredSSTableCheckFrequency = Long.parseLong(s); - if (expiredSSTableCheckFrequency <= 0) - throw new ConfigurationException(String.format("Invalid configuration, %s should be positive: %d", - EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION, - expiredSSTableCheckFrequency)); + int reservedThreads = FBUtilities.parseIntAllowingMax(s); + if (reservedThreads < 0) + throw new ConfigurationException(String.format("%s %s must be an integer >= 0 or \"max\"", + RESERVED_THREADS_OPTION, + s)); } catch (NumberFormatException e) { - throw new ConfigurationException(String.format("%s is not a parsable long (base10) for %s", - s, - EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_OPTION), + throw new ConfigurationException(String.format("%s is not a valid integer >= 0 or \"max\": %s", + RESERVED_THREADS_OPTION, + e.getMessage()), e); } } - s = options.remove(ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_OPTION); - if (s != null && !s.equalsIgnoreCase("true") && !s.equalsIgnoreCase("false")) + s = options.remove(RESERVATIONS_TYPE_OPTION); + if (s != null) { - throw new ConfigurationException(String.format("%s should either be 'true' or 'false', not %s", - ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_OPTION, s)); + try + { + Reservations.Type.valueOf(s.toUpperCase()); + } + catch (IllegalArgumentException e) + { + throw new ConfigurationException(String.format("Invalid reserved threads type %s. The valid options are %s.", + s, + Arrays.toString(Reservations.Type.values()))); + } } s = options.remove(OVERLAP_INCLUSION_METHOD_OPTION); @@ -599,55 +1364,112 @@ public static Map validateOptions(Map options) t } } - s = options.remove(MIN_SSTABLE_SIZE_OPTION); + s = options.remove(MAX_SSTABLES_PER_SHARD_FACTOR_OPTION); if (s != null) { try { - long sizeInBytes = FBUtilities.parseHumanReadableBytes(s); - // zero is a valid option to disable feature - if (sizeInBytes < 0) - throw new ConfigurationException(String.format("Invalid configuration, %s should be greater than or equal to 0 (zero)", - MIN_SSTABLE_SIZE_OPTION)); - long limit = (long) Math.ceil(targetSSTableSize * INVERSE_SQRT_2); - if (sizeInBytes >= limit) - throw new ConfigurationException(String.format("Invalid configuration, %s (%s) should be less than 70%% of the targetSSTableSize (%s)", - MIN_SSTABLE_SIZE_OPTION, - FBUtilities.prettyPrintMemory(sizeInBytes), - FBUtilities.prettyPrintMemory(targetSSTableSize))); + double maxSstablesPerShardFactor = Double.parseDouble(s); + if (maxSstablesPerShardFactor < 1) + throw new ConfigurationException(String.format("%s %s must be a float >= 1", + MAX_SSTABLES_PER_SHARD_FACTOR_OPTION, + s)); } catch (NumberFormatException e) { - throw new ConfigurationException(String.format("%s is not a valid size in bytes for %s", + throw new ConfigurationException(String.format(floatParseErr, s, - MIN_SSTABLE_SIZE_OPTION), + MAX_SSTABLES_PER_SHARD_FACTOR_OPTION), e); } } - s = options.remove(SSTABLE_GROWTH_OPTION); + return adaptive ? AdaptiveController.validateOptions(options) : StaticController.validateOptions(options); + } + + private static long getSizeWithAlt(Map options, String optionHumanReadable, String optionAlt, int altShift, long defaultValue) + { + if (options.containsKey(optionHumanReadable)) + return FBUtilities.parseHumanReadableBytes(options.get(optionHumanReadable)); + else if (options.containsKey(optionAlt)) + return Long.parseLong(options.get(optionAlt)) << altShift; + else + return defaultValue; + } + + private static boolean validateBoolean(Map options, String option, boolean defaultValue) throws ConfigurationException + { + var s = options.remove(option); if (s != null) { - try + if (!s.equalsIgnoreCase("true") && !s.equalsIgnoreCase("false")) + throw new ConfigurationException(String.format("%s should either be 'true' or 'false', not %s", option, s)); + return Boolean.parseBoolean(s); + } + return defaultValue; + } + + private static void validateSizeWithAlt(Map options, String optionHumanReadable, String optionAlt, int altShift) + { + validateSizeWithAlt(options, optionHumanReadable, optionAlt, altShift, null, 0, 0); + } + + private static long validateSizeWithAlt(Map options, String optionHumanReadable, String optionAlt, int altShift, String specialText, long specialValue, long defaultValue) + { + validateOneOf(options, optionHumanReadable, optionAlt); + long sizeInBytes; + String s = null; + String opt = optionHumanReadable; + try + { + s = options.remove(opt); + if (s != null) { - double targetSSTableGrowth = FBUtilities.parsePercent(s); - if (targetSSTableGrowth < 0 || targetSSTableGrowth > 1) - { - throw new ConfigurationException(String.format("%s %s must be between 0 and 1", - SSTABLE_GROWTH_OPTION, - s)); - } + if (s.equalsIgnoreCase(specialText)) + return specialValue; // all good + sizeInBytes = FBUtilities.parseHumanReadableBytes(s); } - catch (NumberFormatException e) + else { - throw new ConfigurationException(String.format("%s is not a valid number between 0 and 1: %s", - SSTABLE_GROWTH_OPTION, - e.getMessage()), - e); + opt = optionAlt; + s = options.remove(opt); + if (s != null) + sizeInBytes = Long.parseLong(s) << altShift; + else + return defaultValue; } + + } + catch (NumberFormatException e) + { + if (specialText != null) + throw new ConfigurationException(String.format("%s must be a valid size in bytes or %s for %s", + s, + specialText, + opt), + e); + else + throw new ConfigurationException(String.format("%s is not a valid size in bytes for %s", + s, + opt), + e); } - return options; + if (sizeInBytes < 0) + throw new ConfigurationException(String.format("Invalid configuration, %s should be positive: %s", + opt, + s)); + return sizeInBytes; + } + + private static void validateOneOf(Map options, String option1, String option2) + { + if (options.containsKey(option1) && options.containsKey(option2)) + { + throw new ConfigurationException(String.format("Cannot specify both %s and %s", + option1, + option2)); + } } // The methods below are implemented here (rather than directly in UCS) to aid testability. @@ -672,15 +1494,25 @@ public double getMaxLevelDensity(int index, double minSize) public double maxThroughput() { - double compactionThroughputMbPerSec = DatabaseDescriptor.getCompactionThroughputMebibytesPerSec(); - if (compactionThroughputMbPerSec <= 0) - return Double.MAX_VALUE; - return Math.scalb(compactionThroughputMbPerSec, 20); + return env.maxThroughput(); + } + + public long getOverheadSizeInBytes(Iterable sstables, long totalDataSize) + { + return env.getOverheadSizeInBytes(sstables, totalDataSize); } public int maxConcurrentCompactions() { - return DatabaseDescriptor.getConcurrentCompactors(); + return env.maxConcurrentCompactions(); + } + + public long maxCompactionSpaceBytes() + { + // Note: Compaction will not proceed with operations larger than this size (i.e. it will compact on the lower + // levels but will accumulate sstables on the top until the space on the drive fills up). This sounds risky but + // is less of a problem than running out of space during compaction. + return (long) (getDataSetSizeBytes() * getMaxSpaceOverhead()); } public int maxSSTablesToCompact() @@ -743,4 +1575,202 @@ public static String printScalingParameters(int[] parameters) builder.append(UnifiedCompactionStrategy.printScalingParameter(parameters[i])); return builder.toString(); } -} + + /** + * Prioritize the given aggregates. Because overlap is the primary measure we aim to control, reducing the max + * overlap of the aggregates is the primary goal. We do this by sorting the aggregates by max overlap, so that + * the ones with the highest overlap are chosen first. + * Among choices with matching overlap, we order randomly to give each level and bucket a good chance to run. + */ + public List prioritize(List aggregates) + { + // Randomize the list. + Collections.shuffle(aggregates, random()); + // Sort the array so that aggregates with the highest overlap come first. On ties, prefer lower levels. + // Because this is a stable sort, entries with the same overlap and level will remain randomly ordered. + aggregates.sort((a1, a2) -> { + int cmp = Long.compare(a2.maxOverlap(), a1.maxOverlap()); + if (cmp != 0) + return cmp; + else + return Integer.compare(a1.bucketIndex(), a2.bucketIndex()); + }); + return aggregates; + } + + /** + * Check if factorization-based growth is enabled and base shard count is not power of 2. + */ + @VisibleForTesting + boolean useFactorizationShardCountGrowth() + { + return USE_FACTORIZATION_SHARD_COUNT_GROWTH && baseShardCount > 0 && Integer.bitCount(baseShardCount) != 1; + } + + /** + * Compute a smooth shard count sequence based on prime factorization and find the largest shard count that is not larger + * than current shard count based on density or first shard if current count is NaN or negative + * + * For num_shards=1000 (5^3 * 2^3), returns the appropriate shard count + * based on current density to achieve smooth growth: 1, 5, 25, 125, 250, 500, 1000 + * + * @param currentCountBasedOnDensity the current count based on density + * @return the largest shard count for the current density + */ + @VisibleForTesting + int getLargestFactorizedShardCount(double currentCountBasedOnDensity) + { + int[] sequence = factorizedShardSequence.get(); + if (Double.isNaN(currentCountBasedOnDensity)) + return sequence[0]; + + int searchKey = (int) Math.floor(currentCountBasedOnDensity); + int idx = Arrays.binarySearch(sequence, searchKey); + // exact match + if (idx >= 0) + return sequence[idx]; + + // insertion point + int insertionPoint = -idx - 1; + // we need the value before insertion point or 0 if insertion point is 0 + int candidateIndex = Math.max(0, insertionPoint - 1); + return sequence[candidateIndex]; + } + + /** + * Generate a factorized shard sequence for smooth shard count growth. + * Uses prime factorization with largest factors first to create a cumulative sequence. + * + * For example: 1000 (5³×2³) → [1, 5, 25, 125, 250, 500, 1000] + * This provides much smoother growth than power-of-2 jumps. + */ + @VisibleForTesting + static int[] factorizedSmoothShardSequence(int target) + { + if (target <= 0) throw new IllegalArgumentException("target must be positive"); + if (target == 1) return new int[]{ 1 }; + + // 1) Factorize targeShards into list of prime factors in ascending order + List primesAscending = primeFactors(target); + + // 2) Cumulative product to form the chain by using largest prime first. + int[] divisors = new int[primesAscending.size() + 1]; + int cur = 1; + divisors[0] = cur; + for (int i = 0; i < primesAscending.size(); i++) + { + cur *= primesAscending.get(primesAscending.size() - 1 - i); + divisors[i + 1] = cur; + } + return divisors; + } + + /** + * Prime factorization for shard count (usually small) to produce a list of prime factors in ascending order + * For example: 1000 -> [2, 2, 2, 5, 5, 5] + */ + @VisibleForTesting + static List primeFactors(int num) + { + if (num <= 1) + throw new IllegalArgumentException("num must be greater than 1, got: " + num); + + List result = new ArrayList<>(); + + // Factor out 2 using more readable modulo check + while (num % 2 == 0) + { + result.add(2); + num /= 2; + } + + for (int factor = 3; (long) factor * factor <= num; factor += 2) + { + while (num % factor == 0) + { + result.add(factor); + num /= factor; + } + } + + // If num is still > 1, then it's a prime factor + if (num > 1) + result.add(num); + + return result; + } + + static final class Metrics + { + private final MetricNameFactory factory; + private final AtomicReference controllerRef; + private final Gauge totWAGauge; + private final Gauge readIOCostGauge; + private final Gauge writeIOCostGauge; + private final Gauge totIOCostGauge; + + Metrics(TableMetadata metadata) + { + this.factory = new DefaultNameFactory("CompactionCosts", + String.format("%s.%s", metadata.keyspace, metadata.name)); + this.controllerRef = new AtomicReference<>(); + this.totWAGauge = org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.register(factory.createMetricName("WA"), this::getMeasuredWA); + this.readIOCostGauge = org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.register(factory.createMetricName("ReadIOCost"), this::getReadIOCost); + this.writeIOCostGauge = org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.register(factory.createMetricName("WriteIOCost"), this::getWriteIOCost); + this.totIOCostGauge = org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.register(factory.createMetricName("TotIOCost"), this::getTotalIOCost); + } + + void setController(Controller controller) + { + this.controllerRef.set(controller); + } + + void removeController() + { + this.controllerRef.set(null); + } + + void release() + { + org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.remove(factory.createMetricName("WA")); + org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.remove(factory.createMetricName("ReadIOCost")); + org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.remove(factory.createMetricName("WriteIOCost")); + org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics.remove(factory.createMetricName("TotIOCost")); + } + + double getMeasuredWA() + { + double ret = 0; + Controller controller = controllerRef.get(); + if (controller != null) + ret = controller.env.WA(); + + return ret; + } + + double getReadIOCost() + { + double ret = 0; + Controller controller = controllerRef.get(); + if (controller != null) + ret = controller.getReadIOCost(); + + return ret; + } + + double getWriteIOCost() + { + double ret = 0; + Controller controller = controllerRef.get(); + if (controller != null) + ret = controller.getWriteIOCost(); + + return ret; + } + + double getTotalIOCost() + { + return getReadIOCost() + getWriteIOCost(); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/unified/CostsCalculator.java b/src/java/org/apache/cassandra/db/compaction/unified/CostsCalculator.java new file mode 100644 index 000000000000..f21b11c25058 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/unified/CostsCalculator.java @@ -0,0 +1,256 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction.unified; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.metrics.CompactionMetrics; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.MovingAverage; + +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_ADAPTIVE_SAMPLE_TIME_MS; + +/** + * This class periodically retrieves delta values from the environment and stores them into exponentially weighted averages. + * It then uses these values to calculate IO costs that are exported to {@link CompactionMetrics} and used by {@link AdaptiveController} + * to choose the optimal configuration for compaction. + */ +public class CostsCalculator +{ + private final static Logger logger = LoggerFactory.getLogger(CostsCalculator.class); + + /** How often values are sampled. Sampling for periods that are too short (<= 1 second) may not give good results since + * we many not collect sufficient data. */ + final static int samplingPeriodMs = UCS_ADAPTIVE_SAMPLE_TIME_MS.getIntWithLegacyFalback(); + + private final Environment env; + private final MovingAverageOfDelta partitionsReadPerPeriod; + private final MovingAverageOfDelta bytesInsertedPerPeriod; + private final MovingAverage numSSTables; + private final MovingAverage spaceUsed; + private final UnifiedCompactionStrategy strategy; + + private final ReentrantReadWriteLock lock; + private final ReentrantReadWriteLock.ReadLock readLock; + private final ReentrantReadWriteLock.WriteLock writeLock; + private final ScheduledFuture future; + + CostsCalculator(Environment env, + UnifiedCompactionStrategy strategy, + ScheduledExecutorService executorService) + { + this.env = env; + this.partitionsReadPerPeriod = new MovingAverageOfDelta(env.makeExpMovAverage()); + this.bytesInsertedPerPeriod = new MovingAverageOfDelta(env.makeExpMovAverage()); + this.numSSTables = env.makeExpMovAverage(); + this.spaceUsed = env.makeExpMovAverage(); + this.strategy = strategy; + this.lock = new ReentrantReadWriteLock(); + this.readLock = lock.readLock(); + this.writeLock = lock.writeLock(); + this.future = executorService.scheduleAtFixedRate(this::sampleValues, samplingPeriodMs, samplingPeriodMs, TimeUnit.MILLISECONDS); + } + + public void close() + { + writeLock.lock(); + + try + { + logger.debug("Stopping cost calculations for {}", strategy.getMetadata()); + future.cancel(false); + logger.debug("Stopped cost calculations for {}", strategy.getMetadata()); + } + finally + { + writeLock.unlock(); + } + } + + @VisibleForTesting + void sampleValues() + { + writeLock.lock(); + + try + { + partitionsReadPerPeriod.update(env.partitionsRead()); + bytesInsertedPerPeriod.update(env.bytesInserted()); + + numSSTables.update(strategy.getSSTables().size()); + spaceUsed.update(strategy.getSSTables().stream().map(CompactionSSTable::onDiskLength).reduce(0L, Long::sum)); + } + catch (Throwable err) + { + JVMStabilityInspector.inspectThrowable(err); + logger.error("Failed to update values: {}/{}", err.getClass().getName(), err.getMessage(), err); + } + finally + { + writeLock.unlock(); + } + } + + /** + * @return the estimated read cost for the given number of partitions, in milliseconds + */ + private double getReadCost(double partitionsRead) + { + return (env.sstablePartitionReadLatencyNanos() * partitionsRead) / TimeUnit.MILLISECONDS.toNanos(1); + } + + /** + * Calculate the projected read cost for user queries. + * + * The projected read cost is given by the number of partitions read, times the mean partition latency and is calculated + * by {@link this#getReadCost(double)}. This value is then multiplied by the number of sstables we're likely to hit + * per partition read and the read multiplier. + *

    + * The number of sstables is calculated as Math.min(1 + env.bloomFilterFpRatio() * RA / survivalFactor, RA). Here we + * assume there is going to be at least one sstable accessed, possibly more in case of : + * + * - bloom filter's false positives; + * - partitions not surviving a compaction (1/survivalFactor is the limit of the sum of (1-survivalFactor)^n), that + * is partitions that would not exist if compaction was done; Note that the survival factor is currently fixed to 1. + * + * The RA is then a cap since we cannot read more than RA sstables, which are the sstables that exist because + * compactions allows them to exist. + *

    + * The read multiplier is a factor that operators can use to tweak the algorithm. + *

    + * @param RA the expected read amplification due to the current choice of compaction strategy + * + * @return the projected read cost for user queries + */ + public double getReadCostForQueries(int RA) + { + readLock.lock(); + + try + { + return getReadCost(partitionsReadPerPeriod.avg.get()) * RA * strategy.getOptions().getReadMultiplier(); + } + finally + { + readLock.unlock(); + } + } + + private double getFlushCost(double bytesWritten) + { + return ((bytesWritten / (1 << 10)) * env.flushTimePerKbInNanos()) / (double) TimeUnit.MILLISECONDS.toNanos(1); + } + + private double getCompactionCost(double bytesWritten) + { + // So, the compaction latency will depend on the size of the sstables, so in the correct solution each level + // should pass its output size and we should measure latency in MB or something like that + return ((bytesWritten / (1 << 10)) * env.compactionTimePerKbInNanos()) / (double) TimeUnit.MILLISECONDS.toNanos(1); + } + + /** + * Calculate the projected write cost for user insertions. + * + * The projected write cost is given by the number of bytes that were inserted times the flush cost + * plus the same number of bytes times the compaction cost and the compaction WA. We also multiply by + * a write multiplier to let users change the weights if needed. + * + * @param WA the expected write amplification due to compaction + * + * @return the projected flush and write cost. + */ + public double getWriteCostForQueries(int WA) + { + readLock.lock(); + + try + { + double bytesInserted = this.bytesInsertedPerPeriod.avg.get(); + // using bytesInserted for the compaction cost doesn't take into account overwrites but for now it's good enough + return (getFlushCost(bytesInserted) + getCompactionCost(bytesInserted) * WA) * strategy.getOptions().getWriteMultiplier(); + } + finally + { + readLock.unlock(); + } + } + + public double partitionsRead() + { + return partitionsReadPerPeriod.avg.get(); + } + + public double numSSTables() + { + return numSSTables.get(); + } + + public double spaceUsed() + { + return spaceUsed.get(); + } + + public Environment getEnv() + { + return env; + } + + @Override + public String toString() + { + return String.format("num partitions read %s, bytes inserted: %s, num sstables %s; Environment: %s", + partitionsReadPerPeriod, bytesInsertedPerPeriod, numSSTables, env); + } + + @NotThreadSafe + private static final class MovingAverageOfDelta + { + private final MovingAverage avg; + private volatile double prev; + + MovingAverageOfDelta(MovingAverage avg) + { + this.avg = avg; + this.prev = Double.MIN_VALUE; + } + + void update(double val) + { + if (prev != Double.MIN_VALUE) + avg.update(val - prev); + + prev = val; + } + + @Override + public String toString() + { + return String.format("%s/%d sec", FBUtilities.prettyPrintMemory((long) (avg != null ? avg.get() : 0)), TimeUnit.MILLISECONDS.toSeconds(samplingPeriodMs)); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/unified/Environment.java b/src/java/org/apache/cassandra/db/compaction/unified/Environment.java new file mode 100644 index 000000000000..8306ce5c4549 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/unified/Environment.java @@ -0,0 +1,115 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction.unified; + +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.utils.MovingAverage; + +/** + * This class supplies to the cost calculator the required parameters for the calculations. + * There are two implementations, one used in real life and one for the simulation. + */ +public interface Environment +{ + /** + * @return an exponential moving average. New values have greater representation in the average, and older samples' + * effect exponentially decays with new data. + */ + MovingAverage makeExpMovAverage(); + + /** + * @return the cache miss ratio in the last 5 minutes + */ + double cacheMissRatio(); + + /** + * @return the bloom filter false positive ratio for all sstables + */ + double bloomFilterFpRatio(); + + /** + * @return the size of the chunk that read from disk. + */ + int chunkSize(); + + /** + * @return the total bytes inserted into the memtables so far + */ + long bytesInserted(); + + /** + * @return the total number of partitions read so far + */ + long partitionsRead(); + + /** + * @return the mean read latency in nano seconds to read a partition from an sstable + */ + double sstablePartitionReadLatencyNanos(); + + /** + * @return the mean compaction time per 1 Kb of input, in nano seconds + */ + double compactionTimePerKbInNanos(); + + /** + * @return the mean flush latency per 1 Kb of input, in nano seconds + */ + double flushTimePerKbInNanos(); + + /** + * @return the write amplification (bytes flushed + bytes compacted / bytes flushed). + */ + double WA(); + + /** + * @return the average size of sstables when they are flushed, averaged over the last 5 minutes. + */ + double flushSize(); + + /** + * @return the maximum number of concurrent compactions that can be running at any one time + */ + int maxConcurrentCompactions(); + + /** + * @return the maximum compaction throughput + */ + double maxThroughput(); + + /** + * This method returns the expected temporary space overhead of performing + * a compaction. This overhead is due to the fact that whilst compactions + * are in progress, both input and output sstables need to be present, since + * the input sstables can only be deleted after compaction has completed. + *

    + * The default implementation looks at the size of the input data files of the + * compaction, assuming that the output compaction will be just as large. + * This does not take into account indexes, and thus may underestimate the + * total required space. This method is used to evaluate the actual space + * that may be required. + * + * @param sstables set of sstables to be compacted + * @param totalDataSize precalculated data size, to use when total space + * adjustment is not required + * @return the expecte overhead size in bytes for compacting the given sstables + */ + default long getOverheadSizeInBytes(Iterable sstables, long totalDataSize) + { + return totalDataSize; + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/unified/RealEnvironment.java b/src/java/org/apache/cassandra/db/compaction/unified/RealEnvironment.java new file mode 100644 index 000000000000..614786bdaea1 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/unified/RealEnvironment.java @@ -0,0 +1,190 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction.unified; + +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.cache.ChunkCache; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.io.util.PageAware; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.metrics.TableMetrics; +import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.utils.ExpMovingAverage; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.MovingAverage; + +/** + * An implementation of {@link Environment} that returns + * real values. + */ +public class RealEnvironment implements Environment +{ + private final CompactionRealm realm; + + public RealEnvironment(CompactionRealm realm) + { + assert realm != null; + this.realm = realm; + } + + private TableMetrics metrics() + { + return realm.metrics(); + } + + @Override + public MovingAverage makeExpMovAverage() + { + return ExpMovingAverage.decayBy100(); + } + + @Override + public double cacheMissRatio() + { + double hitRate = ChunkCache.instance != null ? ChunkCache.instance.metrics.hitRate() : Double.NaN; + if (Double.isNaN(hitRate)) + return 1; // if the cache is not yet initialized then assume all requests are a cache miss + + return 1 - Math.min(1, hitRate); // hit rate should never be > 1 but just in case put a check + } + + @Override + public double bloomFilterFpRatio() + { + return metrics() == null ? 0.0 : metrics().bloomFilterFalseRatio.getValue(); + } + + @Override + public int chunkSize() + { + CompressionParams compressionParams = realm.metadata().params.compression; + if (compressionParams.isEnabled()) + return compressionParams.chunkLength(); + + return PageAware.PAGE_SIZE; + } + + @Override + public long partitionsRead() + { + return metrics() == null ? 0 : metrics().readRequests.getCount(); + } + + @Override + public double sstablePartitionReadLatencyNanos() + { + return metrics() == null ? 0.0 : metrics().sstablePartitionReadLatency.get(); + } + + @Override + public double compactionTimePerKbInNanos() + { + return metrics() == null ? 0.0 : metrics().compactionTimePerKb.get(); + } + + @Override + public double flushTimePerKbInNanos() + { + return metrics() == null ? 0.0 : metrics().flushTimePerKb.get(); + } + + @Override + public long bytesInserted() + { + return metrics() == null ? 0 : metrics().bytesInserted.getCount(); + } + + @Override + public double WA() + { + return realm.getWA(); + } + + @Override + public double flushSize() + { + return metrics() == null ? 0.0 : metrics().flushSizeOnDisk().get(); + } + + @Override + public int maxConcurrentCompactions() + { + return CompactionManager.instance.getMaximumCompactorThreads(); + } + + @Override + public double maxThroughput() + { + final int compactionThroughputMbPerSec = DatabaseDescriptor.getCompactionThroughputMebibytesPerSecAsInt(); + if (compactionThroughputMbPerSec <= 0) + return Double.MAX_VALUE; + return compactionThroughputMbPerSec * 1024.0 * 1024.0; + } + + /** + * @return the compaction overhead size in bytes of the given sstables, i.e. the value used to determine how many + * compactions we can run without exceeding the available space. + * This is configurable via {@link CassandraRelevantProperties#UCS_COMPACTION_INCLUDE_NON_DATA_FILES_SIZE} to + * either report only the data file size, or the total size of all sstable components on disk. + */ + public static long getCompactionOverheadSizeInBytes(Iterable sstables) + { + if (CassandraRelevantProperties.UCS_COMPACTION_INCLUDE_NON_DATA_FILES_SIZE.getBoolean()) + return CompactionSSTable.getTotalOnDiskComponentsBytes(sstables); + else + return CompactionSSTable.getTotalDataBytes(sstables); // only includes data file size + } + + /** + * @return the compaction overhead size in bytes of the given sstables, i.e. the value used to determine how many + * compactions we can run without exceeding the available space. + * This is configurable via {@link CassandraRelevantProperties#UCS_COMPACTION_INCLUDE_NON_DATA_FILES_SIZE} to + * either report only the data file size, or the total size of all sstable components on disk. + * This variation of the method uses a pre-calculated total data size. + */ + public static long getCompactionOverheadSizeInBytes(Iterable sstables, long totalDataSize) + { + if (CassandraRelevantProperties.UCS_COMPACTION_INCLUDE_NON_DATA_FILES_SIZE.getBoolean()) + return CompactionSSTable.getTotalOnDiskComponentsBytes(sstables); + else + return totalDataSize; // only includes data file size + } + + @Override + public long getOverheadSizeInBytes(Iterable sstables, long totalDataSize) + { + return getCompactionOverheadSizeInBytes(sstables, totalDataSize); + } + + @Override + public String toString() + { + return String.format("Default Environment for %s - Read latency: %d us / partition, flush latency: %d us / KiB, " + + "compaction latency: %d us / KiB, bfpr: %f, measured WA: %.2f, flush size %s", + realm.metadata(), + TimeUnit.NANOSECONDS.toMicros((long) sstablePartitionReadLatencyNanos()), + TimeUnit.NANOSECONDS.toMicros((long) flushTimePerKbInNanos()), + TimeUnit.NANOSECONDS.toMicros((long) compactionTimePerKbInNanos()), + bloomFilterFpRatio(), + WA(), + FBUtilities.prettyPrintMemory((long)flushSize())); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/unified/Reservations.java b/src/java/org/apache/cassandra/db/compaction/unified/Reservations.java new file mode 100644 index 000000000000..6354f7d15dd5 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/unified/Reservations.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction.unified; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/// Reservations management for compaction. Defines the two types of reservations, and implements the code for accepting +/// or rejecting compactions to satisfy the reservation requirements. +public abstract class Reservations +{ + public enum Type + { + /// The given number of reservations can be used only for the level. + PER_LEVEL, + /// The reservations can be used for the level, or any one below it. + LEVEL_OR_BELOW + } + + private static final Logger logger = LoggerFactory.getLogger(Reservations.class); + + /// Number of compactions to reserve for each level. + final int perLevelCount; + /// Remainder of compactions to be distributed among the levels. + final int remainder; + /// Whether only one compaction over the reservation count is allowed per level. + final boolean oneRemainderPerLevel; + /// Number of compactions already running or selected in each level. + final int[] perLevel; + + private Reservations(int totalCount, int[] perLevel, int reservedThreadsTarget) + { + this.perLevel = perLevel; + + int levelCount = perLevel.length; + // Each level has this number of tasks reserved for it. + perLevelCount = Math.min(totalCount / levelCount, reservedThreadsTarget); + // The remainder is distributed according to the prioritization. + remainder = totalCount - perLevelCount * levelCount; + // If the user requested more than we can give, do not allow more than one extra per level. + oneRemainderPerLevel = perLevelCount < reservedThreadsTarget; + } + + /// Accept a compaction in the given level if possible. + /// @param parallelismRequested The number of threads requested for the compaction. + /// @returns The number of threads given to the compaction, or 0 if the compaction cannot be accepted. + public abstract int accept(int inLevel, int parallelismRequested); + + public abstract boolean hasRoom(int inLevel); + + public abstract void debugOutput(int selectedCount, int proposedCount, int remaining); + + public static Reservations create(int totalCount, int[] perLevel, int reservedThreadsTarget, Type reservationsType) + { + if (reservedThreadsTarget == 0) + return new Trivial(totalCount, perLevel); + return reservationsType == Type.PER_LEVEL + ? new PerLevel(totalCount, perLevel, reservedThreadsTarget) + : new LevelOrBelow(totalCount, perLevel, reservedThreadsTarget); + } + + /// Trivial tracker used when there are no reservations. All compactions are accepted. + private static class Trivial extends Reservations + { + private Trivial(int totalCount, int[] perLevel) + { + super(totalCount, perLevel, 0); + } + + @Override + public int accept(int inLevel, int requestedParallelism) + { + perLevel[inLevel] += requestedParallelism; + return requestedParallelism; + } + + @Override + public boolean hasRoom(int inLevel) + { + return true; + } + + @Override + public void debugOutput(int selectedCount, int proposedCount, int remaining) + { + if (proposedCount > 0) + logger.debug("Selected {} compactions (out of {} pending). Compactions per level {} (no reservations) remaining {}.", + selectedCount, proposedCount, perLevel, remaining); + else + logger.trace("Selected {} compactions (out of {} pending). Compactions per level {} (no reservations) remaining {}.", + selectedCount, proposedCount, perLevel, remaining); + } + } + + /// Per-level tracker. + /// + /// Reservations are applied by tracking how much of the remainder threads are being used, and only allowing + /// compactions in a level if their number is below the per-level count, or if there is a remainder slot to be given. + private static class PerLevel extends Reservations + { + int remainderDistributed; + + PerLevel(int totalCount, int[] perLevel, int reservedThreadsTarget) + { + super(totalCount, perLevel, reservedThreadsTarget); + + remainderDistributed = 0; + for (int countInLevel : perLevel) + if (countInLevel > perLevelCount) + remainderDistributed += countInLevel - perLevelCount; + } + + @Override + public int accept(int inLevel, int requestedParallelism) + { + int assigned = perLevelCount - perLevel[inLevel]; + assigned = Math.min(assigned, requestedParallelism); + assigned = Math.max(assigned, 0); + + if (assigned < requestedParallelism && remainderDistributed < remainder) + { + // we have a remainder to distribute + if (oneRemainderPerLevel) + { + if (perLevel[inLevel] <= perLevelCount) // we can only give one above, and only if that one is not yet used + { + ++assigned; + ++remainderDistributed; + } + } + else + { + int requestedFromRemainder = requestedParallelism - assigned; + int assignedFromRemainder = Math.min(requestedFromRemainder, remainder - remainderDistributed); + assigned += assignedFromRemainder; + remainderDistributed += assignedFromRemainder; + } + } + + perLevel[inLevel] += assigned; + return assigned; + } + + @Override + public boolean hasRoom(int inLevel) + { + // If we have room in the level, we can accommodate. + return (perLevel[inLevel] < perLevelCount) || + // Otherwise, we need to have remainder to distribute, and not used the one extra if we are in oneRemainderPerLevel mode. + (remainderDistributed < remainder) && (!oneRemainderPerLevel || perLevel[inLevel] == perLevelCount); + } + + @Override + public void debugOutput(int selectedCount, int proposedCount, int remaining) + { + int remainingNonReserved = remainder - remainderDistributed; + logger.debug("Selected {} compactions (out of {} pending). Compactions per level {} (reservations {}{}) remaining reserved {} non-reserved {}.", + selectedCount, proposedCount, perLevel, perLevelCount, oneRemainderPerLevel ? "+1" : "", remaining - remainingNonReserved, remainingNonReserved); + } + } + + /// Tracker for the level or below case. + /// + /// For any given level, the reservations are satisfied if the total sum of compactions for the level and all levels + /// above it is at most the product of the number of levels and the per-level count, plus any remainder (up to the + /// number of levels when oneRemainderPerLevel is true). + /// + /// To permit a compaction, we gather this sum for all levels above, and make sure this property will not be violated + /// by adding the new compaction for the current, as well as all levels below it. The latter is necessary because + /// a lower level may have already used up all allocations for this one. + private static class LevelOrBelow extends Reservations + { + LevelOrBelow(int totalCount, int[] perLevel, int reservedThreadsTarget) + { + super(totalCount, perLevel, reservedThreadsTarget); + } + + @Override + public int accept(int inLevel, int requestedParallelism) + { + return checkRoom(inLevel, requestedParallelism, true); + } + + @Override + public boolean hasRoom(int inLevel) + { + return checkRoom(inLevel, 1, false) > 0; + } + + public int checkRoom(int inLevel, int requestedParallelism, boolean markUse) + { + // Limit the sum of the number of threads of any level and all higher to their number + // times perLevelCount, plus any remainder (up to the number when oneRemainderPerLevel is true). + int sum = 0; + int permittedQuota = 0; + int permittedRemainder = oneRemainderPerLevel ? 0 : remainder; + int level = perLevel.length - 1; + int tentativelyAssigned = requestedParallelism; + // For all higher levels, calculate the total number of threads used and permitted. + for (; level > inLevel; --level) + { + sum += perLevel[level]; + permittedQuota += perLevelCount; + if (oneRemainderPerLevel && permittedRemainder < remainder) + ++permittedRemainder; + } + + // Also adjust for the limit as it applies for this level and all below. + for (; level >= 0; --level) + { + sum += perLevel[level]; + permittedQuota += perLevelCount; + if (oneRemainderPerLevel && permittedRemainder < remainder) + ++permittedRemainder; + if (tentativelyAssigned > permittedQuota + permittedRemainder - sum) + { + tentativelyAssigned = permittedQuota + permittedRemainder - sum; + if (tentativelyAssigned <= 0) + return 0; // some lower level used up our share + } + } + if (markUse) + perLevel[inLevel] += tentativelyAssigned; + return tentativelyAssigned; + } + + @Override + public void debugOutput(int selectedCount, int proposedCount, int remaining) + { + if (proposedCount > 0) + logger.debug("Selected {} compactions (out of {} pending). Compactions per level {} (reservations level or below {}{}) remaining {}.", + selectedCount, proposedCount, perLevel, perLevelCount, oneRemainderPerLevel ? "+1" : "", remaining); + else + logger.trace("Selected {} compactions (out of {} pending). Compactions per level {} (reservations level or below {}{}) remaining {}.", + selectedCount, proposedCount, perLevel, perLevelCount, oneRemainderPerLevel ? "+1" : "", remaining); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriter.java index ca5e99749cca..d9e23bcff3f9 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriter.java @@ -1,13 +1,11 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Copyright DataStax, Inc. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -23,14 +21,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.ShardTracker; import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.utils.FBUtilities; /** @@ -45,20 +47,20 @@ public class ShardedCompactionWriter extends CompactionAwareWriter private final ShardTracker boundaries; - public ShardedCompactionWriter(ColumnFamilyStore cfs, + /// @param uniqueKeyRatio the expected ratio between the expected number of unique keys in the output sstable and + /// the number of keys in the individual inputs. + public ShardedCompactionWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, + ILifecycleTransaction txn, Set nonExpiredSSTables, + double uniqueKeyRatio, boolean keepOriginals, + boolean earlyOpenAllowed, ShardTracker boundaries) { - super(cfs, directories, txn, nonExpiredSSTables, keepOriginals); - + super(realm, directories, txn, nonExpiredSSTables, keepOriginals, earlyOpenAllowed); this.boundaries = boundaries; - long totalKeyCount = nonExpiredSSTables.stream() - .mapToLong(SSTableReader::estimatedKeys) - .sum(); - this.uniqueKeyRatio = 1.0 * SSTableReader.getApproximateKeyCount(nonExpiredSSTables) / totalKeyCount; + this.uniqueKeyRatio = uniqueKeyRatio; } @Override @@ -74,7 +76,7 @@ protected boolean shouldSwitchWriterInCurrentLocation(DecoratedKey key) key.getToken(), boundaries.shardStart(), boundaries.shardIndex(), FBUtilities.prettyPrintMemory(uncompressedBytesWritten), - cfs.getKeyspaceName(), cfs.getTableName()); + realm.getKeyspaceName(), realm.getTableName()); return true; } @@ -82,16 +84,23 @@ protected boolean shouldSwitchWriterInCurrentLocation(DecoratedKey key) } @Override - protected SSTableWriter sstableWriter(Directories.DataDirectory directory, DecoratedKey nextKey) + protected SSTableWriter sstableWriter(Directories.DataDirectory directory, Token nextKey) { if (nextKey != null) - boundaries.advanceTo(nextKey.getToken()); - return super.sstableWriter(directory, nextKey); - } + boundaries.advanceTo(nextKey); - protected long sstableKeyCount() - { - return shardAdjustedKeyCount(boundaries, nonExpiredSSTables, uniqueKeyRatio); + Descriptor descriptor = realm.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); + return descriptor.getFormat().getWriterFactory().builder(descriptor) + .setKeyCount(shardAdjustedKeyCount(boundaries, nonExpiredSSTables, uniqueKeyRatio)) + .setRepairedAt(minRepairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(txn.originals(), realm.metadata().comparator)) + .setSerializationHeader(SerializationHeader.make(realm.metadata(), nonExpiredSSTables)) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(txn, realm); } private static long shardAdjustedKeyCount(ShardTracker boundaries, diff --git a/src/java/org/apache/cassandra/db/compaction/unified/ShardedMultiWriter.java b/src/java/org/apache/cassandra/db/compaction/unified/ShardedMultiWriter.java index a5b5df9e4967..0629be79f93b 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/ShardedMultiWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/unified/ShardedMultiWriter.java @@ -1,13 +1,11 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Copyright DataStax, Inc. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -22,20 +20,23 @@ import java.util.Collection; import java.util.List; +import javax.annotation.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.ShardTracker; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.index.Index; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableMultiWriter; +import org.apache.cassandra.io.sstable.StorageHandler; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableWriter; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; @@ -45,17 +46,19 @@ /** * A {@link SSTableMultiWriter} that splits the output sstable at the partition boundaries of the compaction - * shards used by {@link org.apache.cassandra.db.compaction.UnifiedCompactionStrategy}. + * shards used by {@link org.apache.cassandra.db.compaction.UnifiedCompactionStrategy} as long as the size of + * the sstable so far is sufficiently large. *

    - * This is class is similar to {@link ShardedCompactionWriter} but for flushing. Unfortunately + * This is class is similar to {@link ShardedMultiWriter} but for flushing. Unfortunately * we currently have 2 separate writers hierarchy that are not compatible and so we must - * duplicate the functionality. + * duplicate the functionality of splitting sstables over compaction shards if they have + * reached a minimum size. */ public class ShardedMultiWriter implements SSTableMultiWriter { protected final static Logger logger = LoggerFactory.getLogger(ShardedMultiWriter.class); - private final ColumnFamilyStore cfs; + private final CompactionRealm realm; private final Descriptor descriptor; private final long keyCount; private final long repairedAt; @@ -69,19 +72,19 @@ public class ShardedMultiWriter implements SSTableMultiWriter private final SSTableWriter[] writers; private int currentWriter; - public ShardedMultiWriter(ColumnFamilyStore cfs, - Descriptor descriptor, - long keyCount, - long repairedAt, - TimeUUID pendingRepair, - boolean isTransient, - IntervalSet commitLogPositions, - SerializationHeader header, - Collection indexGroups, - LifecycleNewTracker lifecycleNewTracker, - ShardTracker boundaries) - { - this.cfs = cfs; + public ShardedMultiWriter(CompactionRealm realm, + Descriptor descriptor, + long keyCount, + long repairedAt, + TimeUUID pendingRepair, + boolean isTransient, + IntervalSet commitLogPositions, + SerializationHeader header, + Collection indexGroups, + LifecycleNewTracker lifecycleNewTracker, + ShardTracker boundaries) + { + this.realm = realm; this.descriptor = descriptor; this.keyCount = keyCount; this.repairedAt = repairedAt; @@ -100,25 +103,24 @@ public ShardedMultiWriter(ColumnFamilyStore cfs, private SSTableWriter createWriter() { - Descriptor newDesc = cfs.newSSTableDescriptor(descriptor.directory); + Descriptor newDesc = realm.newSSTableDescriptor(descriptor.directory); return createWriter(newDesc); } - private SSTableWriter createWriter(Descriptor descriptor) + private SSTableWriter createWriter(Descriptor desc) { - MetadataCollector metadataCollector = new MetadataCollector(cfs.metadata().comparator) - .commitLogIntervals(commitLogPositions != null ? commitLogPositions : IntervalSet.empty()); - return descriptor.getFormat().getWriterFactory().builder(descriptor) - .setKeyCount(forSplittingKeysBy(boundaries.count())) - .setRepairedAt(repairedAt) - .setPendingRepair(pendingRepair) - .setTransientSSTable(isTransient) - .setTableMetadataRef(cfs.metadata) - .setMetadataCollector(metadataCollector) - .setSerializationHeader(header) - .addDefaultComponents(indexGroups) - .setSecondaryIndexGroups(indexGroups) - .build(lifecycleNewTracker, cfs); + SSTableWriter.Builder builder = desc.getFormat().getWriterFactory().builder(desc); + return builder + .setKeyCount(forSplittingKeysBy(boundaries.count())) + .setRepairedAt(repairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(realm.metadata().comparator).commitLogIntervals(commitLogPositions)) + .setSerializationHeader(header) + .addDefaultComponents(indexGroups) + .setSecondaryIndexGroups(indexGroups) + .build(lifecycleNewTracker, realm); } private long forSplittingKeysBy(long splits) { @@ -137,7 +139,7 @@ public void append(UnfilteredRowIterator partition) logger.debug("Switching writer at boundary {}/{} index {}, with uncompressed size {} for {}.{}", key.getToken(), boundaries.shardStart(), currentWriter, FBUtilities.prettyPrintMemory(currentUncompressedSize), - cfs.getKeyspaceName(), cfs.getTableName()); + realm.getKeyspaceName(), realm.getTableName()); writers[++currentWriter] = createWriter(); } @@ -146,14 +148,14 @@ public void append(UnfilteredRowIterator partition) } @Override - public Collection finish(boolean openResult) + public Collection finish(boolean openResult, @Nullable StorageHandler storageHandler) { List sstables = new ArrayList<>(writers.length); for (SSTableWriter writer : writers) if (writer != null) { boundaries.applyTokenSpaceCoverage(writer); - sstables.add(writer.finish(openResult)); + sstables.add(writer.finish(openResult, storageHandler)); } return sstables; } @@ -169,12 +171,11 @@ public Collection finished() } @Override - public SSTableMultiWriter setOpenResult(boolean openResult) + public void openResult(@Nullable StorageHandler storageHandler) { for (SSTableWriter writer : writers) if (writer != null) - writer.setOpenResult(openResult); - return this; + writer.openResult(storageHandler); } @Override @@ -190,8 +191,11 @@ public String getFilename() public long getBytesWritten() { long bytesWritten = 0; - for (int i = 0; i <= currentWriter; ++i) - bytesWritten += writers[i].getFilePointer(); + for (int i = 0; i <= currentWriter; ++i) + { + if (writers[i] != null) + bytesWritten += writers[i].getFilePointer(); + } return bytesWritten; } @@ -200,14 +204,22 @@ public long getOnDiskBytesWritten() { long bytesWritten = 0; for (int i = 0; i <= currentWriter; ++i) - bytesWritten += writers[i].getEstimatedOnDiskBytesWritten(); + { + if (writers[i] != null) + bytesWritten += writers[i].getEstimatedOnDiskBytesWritten(); + } return bytesWritten; } + public int getSegmentCount() + { + return currentWriter + 1; + } + @Override public TableId getTableId() { - return cfs.metadata().id; + return realm.metadata().id; } @Override @@ -241,7 +253,7 @@ public void prepareToCommit() { boundaries.applyTokenSpaceCoverage(writer); writer.prepareToCommit(); - } + } } @Override diff --git a/src/java/org/apache/cassandra/db/compaction/unified/StaticController.java b/src/java/org/apache/cassandra/db/compaction/unified/StaticController.java new file mode 100644 index 000000000000..8fc978ff59e1 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/unified/StaticController.java @@ -0,0 +1,247 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compaction.unified; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.db.compaction.CompactionPick; +import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.FSError; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileReader; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.MonotonicClock; +import org.apache.cassandra.utils.Overlaps; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_STATIC_SCALING_PARAMETERS; +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_VECTOR_SCALING_PARAMETERS; + +/** + * The static compaction controller periodically checks the IO costs + * that result from the current configuration of the {@link UnifiedCompactionStrategy}. + */ +public class StaticController extends Controller +{ + /** + * The scaling parameters W, one per bucket index and separated by a comma. + * Higher indexes will use the value of the last index with a W specified. + */ + static final String STATIC_SCALING_FACTORS_OPTION = "static_scaling_factors"; + private final static String DEFAULT_STATIC_SCALING_PARAMETERS = UCS_STATIC_SCALING_PARAMETERS.getStringWithLegacyFallback(); + final static String DEFAULT_VECTOR_STATIC_SCALING_PARAMETERS = UCS_VECTOR_SCALING_PARAMETERS.getStringWithLegacyFallback(); + + private final int[] scalingParameters; + + @VisibleForTesting // comp. simulation + public StaticController(Environment env, + int[] scalingParameters, + double[] survivalFactors, + long dataSetSize, + long minSSTableSize, + long flushSizeOverride, + long currentFlushSize, + double maxSpaceOverhead, + int maxSSTablesToCompact, + long expiredSSTableCheckFrequency, + boolean ignoreOverlapsInExpirationCheck, + int baseShardCount, + boolean isReplicaAware, + long targetSStableSize, + double sstableGrowthModifier, + int reservedThreadsPerLevel, + Reservations.Type reservationsType, + Overlaps.InclusionMethod overlapInclusionMethod, + boolean parallelizeOutputShards, + boolean hasVectorType, + double maxSstablesPerShardFactor, + TableMetadata metadata) + { + super(MonotonicClock.Global.preciseTime, + env, + survivalFactors, + dataSetSize, + minSSTableSize, + flushSizeOverride, + currentFlushSize, + maxSpaceOverhead, + maxSSTablesToCompact, + expiredSSTableCheckFrequency, + ignoreOverlapsInExpirationCheck, + baseShardCount, + isReplicaAware, + targetSStableSize, + sstableGrowthModifier, + reservedThreadsPerLevel, + reservationsType, + overlapInclusionMethod, + parallelizeOutputShards, + hasVectorType, + maxSstablesPerShardFactor, + metadata); + this.scalingParameters = scalingParameters; + } + + static Controller fromOptions(Environment env, + double[] survivalFactors, + long dataSetSize, + long minSSTableSize, + long flushSizeOverride, + double maxSpaceOverhead, + int maxSSTablesToCompact, + long expiredSSTableCheckFrequency, + boolean ignoreOverlapsInExpirationCheck, + int baseShardCount, + boolean isReplicaAware, + long targetSStableSize, + double sstableGrowthModifier, + int reservedThreadsPerLevel, + Reservations.Type reservationsType, + Overlaps.InclusionMethod overlapInclusionMethod, + boolean parallelizeOutputShards, + boolean hasVectorType, + double maxSstablesPerShardFactor, + TableMetadata metadata, + Map options, + boolean useVectorOptions) + { + int[] scalingParameters; + if (options.containsKey(STATIC_SCALING_FACTORS_OPTION)) + scalingParameters = parseScalingParameters(options.get(STATIC_SCALING_FACTORS_OPTION)); + else + scalingParameters = parseScalingParameters(options.getOrDefault(SCALING_PARAMETERS_OPTION, + useVectorOptions ? DEFAULT_VECTOR_STATIC_SCALING_PARAMETERS + : DEFAULT_STATIC_SCALING_PARAMETERS)); + + long currentFlushSize = flushSizeOverride; + + File f = getControllerConfigPath(metadata); + try + { + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObject = (JSONObject) jsonParser.parse(new FileReader(f)); + if (jsonObject.get("current_flush_size") != null && flushSizeOverride == 0) + { + currentFlushSize = (long) jsonObject.get("current_flush_size"); + logger.debug("Successfully read stored current_flush_size from disk"); + } + } + catch (IOException e) + { + logger.debug("No controller config file found. Using starting value instead."); + } + catch (ParseException e) + { + logger.warn("Unable to parse saved flush size. Using starting value instead:", e); + } + catch (FSError e) + { + logger.warn("Unable to read controller config file. Using starting value instead:", e); + } + catch (Throwable e) + { + logger.warn("Unable to read controller config file. Using starting value instead:", e); + JVMStabilityInspector.inspectThrowable(e); + } + return new StaticController(env, + scalingParameters, + survivalFactors, + dataSetSize, + minSSTableSize, + flushSizeOverride, + currentFlushSize, + maxSpaceOverhead, + maxSSTablesToCompact, + expiredSSTableCheckFrequency, + ignoreOverlapsInExpirationCheck, + baseShardCount, + isReplicaAware, + targetSStableSize, + sstableGrowthModifier, + reservedThreadsPerLevel, + reservationsType, + overlapInclusionMethod, + parallelizeOutputShards, + hasVectorType, + maxSstablesPerShardFactor, + metadata); + } + + public static Map validateOptions(Map options) throws ConfigurationException + { + String parameters = options.remove(SCALING_PARAMETERS_OPTION); + if (parameters != null) + parseScalingParameters(parameters); + String factors = options.remove(STATIC_SCALING_FACTORS_OPTION); + if (factors != null) + parseScalingParameters(factors); + if (parameters != null && factors != null) + throw new ConfigurationException(String.format("Either '%s' or '%s' should be used, not both", SCALING_PARAMETERS_OPTION, STATIC_SCALING_FACTORS_OPTION)); + return options; + } + + @Override + public int getScalingParameter(int index) + { + if (index < 0) + throw new IllegalArgumentException("Index should be >= 0: " + index); + + return index < scalingParameters.length ? scalingParameters[index] : scalingParameters[scalingParameters.length - 1]; + } + + @Override + public int getPreviousScalingParameter(int index) + { + //scalingParameters is not updated in StaticController so previous scalingParameters = scalingParameters + return getScalingParameter(index); + } + + @Override + public boolean isRecentAdaptive(CompactionPick pick) + { + return false; + } + + @Override + public int getMaxRecentAdaptiveCompactions() + { + return Integer.MAX_VALUE; + } + + @Override + public void storeControllerConfig() + { + storeOptions(metadata, scalingParameters, getFlushSizeBytes()); + } + + @Override + public String toString() + { + return String.format("Static controller, m: %d, o: %s, scalingParameters: %s, cost: %s", minSSTableSize, + Arrays.toString(survivalFactors), + printScalingParameters(scalingParameters), + calculator); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/unified/UnifiedCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/unified/UnifiedCompactionTask.java index 5a729f643299..430cb39e2b9f 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/UnifiedCompactionTask.java +++ b/src/java/org/apache/cassandra/db/compaction/unified/UnifiedCompactionTask.java @@ -1,13 +1,11 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Copyright DataStax, Inc. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,44 +16,159 @@ package org.apache.cassandra.db.compaction.unified; +import java.util.Collection; import java.util.Set; -import org.apache.cassandra.db.ColumnFamilyStore; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.compaction.AbstractCompactionTask; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.CompactionTask; import org.apache.cassandra.db.compaction.ShardManager; +import org.apache.cassandra.db.compaction.SharedCompactionObserver; +import org.apache.cassandra.db.compaction.SharedCompactionProgress; +import org.apache.cassandra.db.compaction.SharedTableOperation; +import org.apache.cassandra.db.compaction.TableOperationObserver; import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; -/** - * The sole purpose of this class is to currently create a {@link ShardedCompactionWriter}. - */ public class UnifiedCompactionTask extends CompactionTask { private final ShardManager shardManager; - private final Controller controller; + private final Range operationRange; + private final Set actuallyCompact; + private final SharedCompactionProgress sharedProgress; + private final SharedTableOperation sharedOperation; + private final UnifiedCompactionStrategy.ShardingStats shardingStats; + + public UnifiedCompactionTask(CompactionRealm cfs, + UnifiedCompactionStrategy strategy, + ILifecycleTransaction txn, + long gcBefore, + ShardManager shardManager, + UnifiedCompactionStrategy.ShardingStats shardingStats) + { + this(cfs, strategy, txn, gcBefore, false, shardManager, shardingStats, null, null, null, null, null); + } + - public UnifiedCompactionTask(ColumnFamilyStore cfs, + public UnifiedCompactionTask(CompactionRealm cfs, UnifiedCompactionStrategy strategy, - LifecycleTransaction txn, + ILifecycleTransaction txn, long gcBefore, - ShardManager shardManager) + boolean keepOriginals, + ShardManager shardManager, + UnifiedCompactionStrategy.ShardingStats shardingStats, + Range operationRange, + Collection actuallyCompact, + SharedCompactionProgress sharedProgress, + SharedCompactionObserver sharedObserver, + SharedTableOperation sharedOperation) { - super(cfs, txn, gcBefore); - this.controller = strategy.getController(); + super(cfs, + txn, + // Set the total operation sizes early to use in shared progress tracking. This assumes that: + // - there are no expired sstables in the compaction (UCS processes them separately) + // - sstable exclusion for lack of space does not apply (shared progress is only use when an operation + // range applies, which disables this) + sharedProgress != null ? getOperationTotals(actuallyCompact, operationRange) : null, + gcBefore, + keepOriginals, + strategy, + sharedObserver != null ? sharedObserver : strategy); this.shardManager = shardManager; + this.shardingStats = shardingStats; + + if (operationRange != null) + assert actuallyCompact != null : "Ranged tasks should use a set of sstables to compact"; + + this.operationRange = operationRange; + this.sharedProgress = sharedProgress; + this.sharedOperation = sharedOperation; + if (sharedProgress != null) + sharedProgress.registerExpectedSubtask(totals.inputUncompressedSize, totals.inputDiskSize, totals.inputUncompressedSize); + if (sharedObserver != null) + sharedObserver.registerExpectedSubtask(); + if (sharedOperation != null) + sharedOperation.registerExpectedSubtask(); + // To make sure actuallyCompact tracks any removals from txn.originals(), we intersect the given set with it. + // This should not be entirely necessary (as shouldReduceScopeForSpace() is false for ranged tasks), but it + // is cleaner to enforce inputSSTables()'s requirements. + this.actuallyCompact = actuallyCompact != null ? Sets.intersection(ImmutableSet.copyOf(actuallyCompact), + txn.originals()) + : txn.originals(); } @Override - public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, + public CompactionAwareWriter getCompactionAwareWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, Set nonExpiredSSTables) { - double density = shardManager.calculateCombinedDensity(nonExpiredSSTables); - int numShards = controller.getNumShards(density * shardManager.shardSetCoverage()); - return new ShardedCompactionWriter(cfs, directories, txn, nonExpiredSSTables, keepOriginals, shardManager.boundaries(numShards)); + // In multi-task operations we need to expire many ranges in a source sstable for early open. Not doable yet. + final boolean earlyOpenAllowed = operationRange == null; + return new ShardedCompactionWriter(realm, + directories, + transaction, + nonExpiredSSTables, + shardingStats.uniqueKeyRatio, + keepOriginals, + earlyOpenAllowed, + shardManager.boundaries(shardingStats.shardCountForDensity)); + } + + @Override + protected Range tokenRange() + { + return operationRange; + } + + @Override + protected SharedCompactionProgress sharedProgress() + { + return sharedProgress; + } + + @Override + protected boolean shouldReduceScopeForSpace() + { + // Because parallelized tasks share input sstables, we can't reduce the scope of individual tasks + // (as doing that will leave some part of an sstable out of the compaction but still drop the whole sstable + // when the task set completes). + return tokenRange() == null; + } + + @Override + public Set inputSSTables() + { + return actuallyCompact; + } + + @Override + public AbstractCompactionTask setOpObserver(TableOperationObserver opObserver) + { + if (sharedOperation != null) + opObserver = sharedOperation.wrapObserver(opObserver); + return super.setOpObserver(opObserver); + } + + @Override + public long getSpaceOverhead() + { + if (operationRange != null) + { + // totals must be precalculated for ranged tasks + return (long) (totals.inputDiskSize * shardingStats.overheadToDataRatio); + } + else + { + // if we don't have a range, the sharding stats have precise total disk space + return (long) (shardingStats.totalOnDiskSize * shardingStats.overheadToDataRatio); + } } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0.svg b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0.svg index e3d36653492f..afb50514676e 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0.svg +++ b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0.svg @@ -1,20 +1,4 @@ - diff --git a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_33.svg b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_33.svg index 25b101e6abb0..43fd38677453 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_33.svg +++ b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_33.svg @@ -1,20 +1,4 @@ - diff --git a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_5.svg b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_5.svg index f0e583f0ad2c..55ea68ac46b3 100644 --- a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_5.svg +++ b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_0_5.svg @@ -1,20 +1,4 @@ - diff --git a/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_1.svg b/src/java/org/apache/cassandra/db/compaction/unified/shards_graph_lambda_1.svg old mode 100755 new mode 100644 diff --git a/src/java/org/apache/cassandra/db/compaction/validation/CompactionValidationMetrics.java b/src/java/org/apache/cassandra/db/compaction/validation/CompactionValidationMetrics.java new file mode 100644 index 000000000000..338974b0f995 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/validation/CompactionValidationMetrics.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.compaction.validation; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import org.apache.cassandra.metrics.MicrometerMetrics; + +/// Metrics for tracking compaction validation operations and results. +public class CompactionValidationMetrics extends MicrometerMetrics +{ + public static final CompactionValidationMetrics INSTANCE = new CompactionValidationMetrics(); + + public Counter validationCount; + public Counter validationWithoutAbsentKeys; + public Counter absentKeys; + public Counter potentialDataLosses; + + public CompactionValidationMetrics() + { + initializeMetrics(); + } + + @Override + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + super.register(newRegistry, newTags); + initializeMetrics(); + } + + private void initializeMetrics() + { + this.validationCount = registryWithTags().left.counter("compaction_validation_total", registryWithTags().right); + this.validationWithoutAbsentKeys = registryWithTags().left.counter("compaction_validation_without_absent_keys_total", registryWithTags().right); + this.absentKeys = registryWithTags().left.counter("compaction_validation_absent_keys_count_from_output_total", registryWithTags().right); + this.potentialDataLosses = registryWithTags().left.counter("compaction_validation_potential_data_loss_total", registryWithTags().right); + } + + public void incrementValidation() + { + validationCount.increment(); + } + + public void incrementPotentialDataLosses() + { + potentialDataLosses.increment(); + } + + public void incrementValidationWithoutAbsentKeys() + { + validationWithoutAbsentKeys.increment(); + } + + public void incrementAbsentKeys(int keys) + { + absentKeys.increment(keys); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/validation/CompactionValidationTask.java b/src/java/org/apache/cassandra/db/compaction/validation/CompactionValidationTask.java new file mode 100644 index 000000000000..5388e9e3466b --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/validation/CompactionValidationTask.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.compaction.validation; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.TimeUUID; + +/// Validates compaction tasks to detect potential data loss during compaction operations caused by skipping some +/// subranges of source sstables, see HCD-130. +/// The validation ensures all boundary keys from input SSTables are either present in output SSTables +/// or properly obsoleted by tombstones. +public class CompactionValidationTask +{ + private static final Logger logger = LoggerFactory.getLogger(CompactionValidationTask.class); + + public enum Mode + { + NONE, + WARN, + ABORT; + + public boolean shouldValidate() + { + return this != NONE; + } + + public boolean shouldAbortOnDataLoss() + { + return this == ABORT; + } + + public static Mode parseConfig() + { + String rawConfig = null; + try + { + rawConfig = CassandraRelevantProperties.COMPACTION_VALIDATION_MODE.getString(); + return Mode.valueOf(rawConfig); + } + catch (IllegalArgumentException e) + { + logger.error("Unable to pase compaction validation config '{}', fall back to NONE", rawConfig, e); + return NONE; + } + } + } + + private final TimeUUID id; + private final Set inputSSTables; + private final Set outputSSTables; + private final CompactionValidationMetrics metrics; + + private final long nowInSec; + private final Mode mode; + + public CompactionValidationTask(TimeUUID id, Set inputSSTables, Set outputSSTables, CompactionValidationMetrics metrics) + { + this.id = id; + this.inputSSTables = inputSSTables; + this.outputSSTables = outputSSTables; + this.nowInSec = FBUtilities.nowInSeconds(); + this.metrics = metrics; + this.mode = Mode.parseConfig(); + } + + public void validate() + { + if (!mode.shouldValidate()) + return; + + try + { + doValidate(); + } + catch (DataLossException e) + { + // abort compaction task + throw e; + } + catch (Throwable t) + { + logger.error("Caught unexpected error on validation task for {}: {}", id, t.getMessage(), t); + } + } + + private void doValidate() + { + logger.info("Starting compaction validation for task {}", id); + long startedNanos = Clock.Global.nanoTime(); + metrics.incrementValidation(); + + Set absentKeys = new HashSet<>(); + for (SSTableReader inputSSTable : inputSSTables) + { + DecoratedKey firstKey = inputSSTable.first; + DecoratedKey lastKey = inputSSTable.last; + + if (isKeyAbsentInOutputSSTables(firstKey)) + { + if (logger.isTraceEnabled()) + logger.trace("[Task {}] First key {} from input sstable {} not found in update sstables", + id, firstKey, inputSSTable.descriptor); + + absentKeys.add(firstKey); + } + + if (isKeyAbsentInOutputSSTables(lastKey)) + { + if (logger.isTraceEnabled()) + logger.trace("[Task {}] Last key {} from input sstable {} not found in update sstables", + id, lastKey, inputSSTable.descriptor); + + absentKeys.add(lastKey); + } + } + + if (absentKeys.isEmpty()) + { + metrics.incrementValidationWithoutAbsentKeys(); + logger.info("[Task {}] Compaction validation passed: all first/last keys found in update sstables, took {}ms", + id, TimeUnit.NANOSECONDS.toMillis(Clock.Global.nanoTime() - startedNanos)); + return; + } + + metrics.incrementAbsentKeys(absentKeys.size()); + if (validateAbsentKeysAgainstTombstones(absentKeys)) + logger.info("[Task {}] Compaction validation passed: all absent keys are properly obsoleted due to tombstones, took {} ms", + id, TimeUnit.NANOSECONDS.toMillis(Clock.Global.nanoTime() - startedNanos)); + } + + private boolean isKeyAbsentInOutputSSTables(DecoratedKey key) + { + for (SSTableReader outputSSTable : outputSSTables) + { + if (outputSSTable.first.compareTo(key) <= 0 && outputSSTable.last.compareTo(key) >= 0) + { + if (outputSSTable.getPosition(key, SSTableReader.Operator.EQ) >= 0) + { + return false; + } + } + } + return true; + } + + private boolean validateAbsentKeysAgainstTombstones(Set absentKeys) + { + logger.info("[Task {}] Validating {} absent keys against tombstones from input sstables", id, absentKeys.size()); + + for (DecoratedKey absentKey : absentKeys) + { + if (!isFullyExpired(absentKey)) + { + metrics.incrementPotentialDataLosses(); + String errorMsg = String.format( + "POTENTIAL DATA LOSS on compaction task %s: Key %s from input sstables not found in update sstables " + + "and the partition is not fully expired.", id, absentKey); + logger.error(errorMsg); + if (mode.shouldAbortOnDataLoss()) + throw new DataLossException(errorMsg); + + return false; + } + } + return true; + } + + private boolean isFullyExpired(DecoratedKey key) + { + List iterators = new ArrayList<>(); + for (SSTableReader sstable : inputSSTables) + { + if (sstable.mayContainAssumingKeyIsInRange(key)) + iterators.add(readPartition(key, sstable)); + } + + // merge all input iterators + try (UnfilteredRowIterator merged = UnfilteredRowIterators.merge(iterators)) + { + // apply purging function to get rid of all tombstones + RowIterator purged = UnfilteredRowIterators.filter(merged, nowInSec); + // if there are non-purgeable content, e.g. live rows or unexpired tombstones, they should appear in output sstables + if (purged.staticRow() != null && !purged.staticRow().isEmpty()) + return false; + if (purged.hasNext()) + return false; + } + + return true; + } + + private UnfilteredRowIterator readPartition(DecoratedKey partitionKey, SSTableReader sstable) + { + return sstable.rowIterator(partitionKey, Slices.ALL, ColumnFilter.all(sstable.metadata()), false, SSTableReadsListener.NOOP_LISTENER); + } + + public static class DataLossException extends RuntimeException + { + public DataLossException(String errorMsg) + { + super(errorMsg); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java index 145163a39cf6..d95db079ecb3 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java @@ -23,18 +23,22 @@ import java.util.List; import java.util.Set; +import org.apache.cassandra.utils.Throwables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.DiskBoundaries; -import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.CompactionTask; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableRewriter; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -45,16 +49,15 @@ import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Transactional; - /** * Class that abstracts away the actual writing of files to make it possible to use CompactionTask for more * use cases. */ -public abstract class CompactionAwareWriter extends Transactional.AbstractTransactional implements Transactional +public abstract class CompactionAwareWriter extends Transactional.AbstractTransactional implements Transactional, SSTableDataSink { protected static final Logger logger = LoggerFactory.getLogger(CompactionAwareWriter.class); - protected final ColumnFamilyStore cfs; + protected final CompactionRealm realm; protected final Directories directories; protected final Set nonExpiredSSTables; protected final long estimatedTotalKeys; @@ -64,31 +67,41 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa protected final boolean isTransient; protected final SSTableRewriter sstableWriter; - protected final LifecycleTransaction txn; + protected final ILifecycleTransaction txn; private final List locations; - private final List diskBoundaries; + private final List diskBoundaries; private int locationIndex; protected Directories.DataDirectory currentDirectory; - public CompactionAwareWriter(ColumnFamilyStore cfs, - Directories directories, - LifecycleTransaction txn, - Set nonExpiredSSTables, - boolean keepOriginals) + protected CompactionAwareWriter(CompactionRealm realm, + Directories directories, + ILifecycleTransaction txn, + Set nonExpiredSSTables, + boolean keepOriginals) + { + this(realm, directories, txn, nonExpiredSSTables, keepOriginals, true); + } + + protected CompactionAwareWriter(CompactionRealm realm, + Directories directories, + ILifecycleTransaction txn, + Set nonExpiredSSTables, + boolean keepOriginals, + boolean earlyOpenAllowed) { - this.cfs = cfs; + this.realm = realm; this.directories = directories; this.nonExpiredSSTables = nonExpiredSSTables; this.txn = txn; estimatedTotalKeys = SSTableReader.getApproximateKeyCount(nonExpiredSSTables); maxAge = CompactionTask.getMaxDataAge(nonExpiredSSTables); - sstableWriter = SSTableRewriter.construct(cfs, txn, keepOriginals, maxAge); + sstableWriter = SSTableRewriter.construct(realm, txn, keepOriginals, maxAge, earlyOpenAllowed); minRepairedAt = CompactionTask.getMinRepairedAt(nonExpiredSSTables); pendingRepair = CompactionTask.getPendingRepair(nonExpiredSSTables); isTransient = CompactionTask.getIsTransient(nonExpiredSSTables); - DiskBoundaries db = cfs.getDiskBoundaries(); - diskBoundaries = db.positions; + DiskBoundaries db = realm.getDiskBoundaries(); + diskBoundaries = db.getPositions(); locations = db.directories; locationIndex = -1; } @@ -132,13 +145,42 @@ public long estimatedKeys() /** * Writes a partition in an implementation specific way + * * @param partition the partition to append * @return true if the partition was written, false otherwise */ - public final boolean append(UnfilteredRowIterator partition) + public AbstractRowIndexEntry append(UnfilteredRowIterator partition) { maybeSwitchWriter(partition.partitionKey()); - return realAppend(partition); + return appendWithoutSwitchingWriters(partition); + } + + @Override + public boolean startPartition(DecoratedKey partitionKey, DeletionTime deletionTime) throws IOException + { + maybeSwitchWriter(partitionKey); + return sstableWriter.startPartition(partitionKey, deletionTime); + } + + @Override + public AbstractRowIndexEntry endPartition() throws IOException + { + return sstableWriter.endPartition(); + } + + @Override + public void addUnfiltered(Unfiltered unfiltered) throws IOException + { + sstableWriter.addUnfiltered(unfiltered); + } + + /** + * Write a partition without considering location change. + * Exposed for TieredCompactionStrategy which needs to control the location itself. + */ + AbstractRowIndexEntry appendWithoutSwitchingWriters(UnfilteredRowIterator partition) + { + return sstableWriter.append(partition); } public final File getSStableDirectory() throws IOException @@ -149,15 +191,10 @@ public final File getSStableDirectory() throws IOException @Override protected Throwable doPostCleanup(Throwable accumulate) { - sstableWriter.close(); + accumulate = Throwables.close(accumulate, sstableWriter); return super.doPostCleanup(accumulate); } - protected boolean realAppend(UnfilteredRowIterator partition) - { - return sstableWriter.append(partition) != null; - } - /** * Switches the writer if necessary, i.e. if the new key should be placed in a different data directory, or if the * specific strategy has decided a new sstable is needed. @@ -178,7 +215,7 @@ protected void maybeSwitchWriter(DecoratedKey key) */ protected boolean maybeSwitchLocation(DecoratedKey key) { - if (diskBoundaries == null) + if (key == null || diskBoundaries == null) { if (locationIndex < 0) { @@ -190,11 +227,11 @@ protected boolean maybeSwitchLocation(DecoratedKey key) return false; } - if (locationIndex > -1 && key.compareTo(diskBoundaries.get(locationIndex)) < 0) + if (locationIndex > -1 && key.getToken().compareTo(diskBoundaries.get(locationIndex)) < 0) return false; int prevIdx = locationIndex; - while (locationIndex == -1 || key.compareTo(diskBoundaries.get(locationIndex)) > 0) + while (locationIndex == -1 || key.getToken().compareTo(diskBoundaries.get(locationIndex)) > 0) locationIndex++; Directories.DataDirectory newLocation = locations.get(locationIndex); if (prevIdx >= 0) @@ -220,35 +257,25 @@ protected boolean maybeSwitchLocation(DecoratedKey key) protected void switchCompactionWriter(Directories.DataDirectory directory, DecoratedKey nextKey) { currentDirectory = directory; - sstableWriter.switchWriter(sstableWriter(directory, nextKey)); + sstableWriter.switchWriter(sstableWriter(directory, nextKey != null ? nextKey.getToken() : null)); } - protected SSTableWriter sstableWriter(Directories.DataDirectory directory, DecoratedKey nextKey) + protected SSTableWriter sstableWriter(Directories.DataDirectory directory, Token diskBoundary) { - Descriptor descriptor = cfs.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); - MetadataCollector collector = new MetadataCollector(txn.originals(), cfs.metadata().comparator) - .sstableLevel(sstableLevel()); - SerializationHeader header = SerializationHeader.make(cfs.metadata(), nonExpiredSSTables); - - return newWriterBuilder(descriptor).setMetadataCollector(collector) - .setSerializationHeader(header) - .setKeyCount(sstableKeyCount()) - .build(txn, cfs); - } - - /** - * Returns the level that should be used when creating sstables. - */ - protected int sstableLevel() - { - return 0; + Descriptor descriptor = realm.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); + return descriptor.getFormat().getWriterFactory().builder(descriptor) + .setKeyCount(estimatedTotalKeys) + .setRepairedAt(minRepairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(txn.originals(), realm.metadata().comparator)) + .setSerializationHeader(SerializationHeader.make(realm.metadata(), nonExpiredSSTables)) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(txn, realm); } - /** - * Returns the key count with which created sstables should be set up. - */ - abstract protected long sstableKeyCount(); - /** * The directories we can write to */ @@ -302,7 +329,7 @@ public CompactionAwareWriter setRepairedAt(long repairedAt) protected long getExpectedWriteSize() { - return cfs.getExpectedCompactedFileSize(nonExpiredSSTables, txn.opType()); + return realm.getExpectedCompactedFileSize(nonExpiredSSTables, txn.opType()); } /** @@ -314,11 +341,21 @@ protected long getExpectedWriteSize() protected SSTableWriter.Builder newWriterBuilder(Descriptor descriptor) { return descriptor.getFormat().getWriterFactory().builder(descriptor) - .setTableMetadataRef(cfs.metadata) + .setTableMetadataRef(realm.metadataRef()) .setTransientSSTable(isTransient) .setRepairedAt(minRepairedAt) .setPendingRepair(pendingRepair) - .setSecondaryIndexGroups(cfs.indexManager.listIndexGroups()) - .addDefaultComponents(cfs.indexManager.listIndexGroups()); + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()); + } + + public long bytesWritten() + { + return sstableWriter.bytesWritten(); + } + + public String getCurrentFileName() + { + return sstableWriter.currentWriter().getFilename(); } } diff --git a/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java index fbb0e27a99b0..d0722edaffef 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java @@ -23,11 +23,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Directories; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; /** * The default compaction writer - creates one output file in L0 @@ -37,14 +42,15 @@ public class DefaultCompactionWriter extends CompactionAwareWriter protected static final Logger logger = LoggerFactory.getLogger(DefaultCompactionWriter.class); private final int sstableLevel; - public DefaultCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables) + public DefaultCompactionWriter(CompactionRealm realm, Directories directories, ILifecycleTransaction txn, Set nonExpiredSSTables) { - this(cfs, directories, txn, nonExpiredSSTables, false, 0); + this(realm, directories, txn, nonExpiredSSTables, false, 0); } - public DefaultCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables, boolean keepOriginals, int sstableLevel) + @SuppressWarnings("resource") + public DefaultCompactionWriter(CompactionRealm realm, Directories directories, ILifecycleTransaction txn, Set nonExpiredSSTables, boolean keepOriginals, int sstableLevel) { - super(cfs, directories, txn, nonExpiredSSTables, keepOriginals); + super(realm, directories, txn, nonExpiredSSTables, keepOriginals); this.sstableLevel = sstableLevel; } @@ -54,9 +60,22 @@ protected boolean shouldSwitchWriterInCurrentLocation(DecoratedKey key) return false; } - protected int sstableLevel() + @SuppressWarnings("resource") + @Override + protected SSTableWriter sstableWriter(Directories.DataDirectory directory, Token diskBoundary) { - return sstableLevel; + Descriptor descriptor = realm.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); + return descriptor.getFormat().getWriterFactory().builder(descriptor) + .setKeyCount(estimatedTotalKeys) + .setRepairedAt(minRepairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(txn.originals(), realm.metadata().comparator, sstableLevel)) + .setSerializationHeader(SerializationHeader.make(realm.metadata(), nonExpiredSSTables)) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(txn, realm); } protected long sstableKeyCount() diff --git a/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java index 09263df8530b..77952fecf797 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java @@ -19,13 +19,20 @@ import java.util.Set; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Directories; -import org.apache.cassandra.db.compaction.LeveledManifest; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; -import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.compaction.LeveledManifest; +import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; public class MajorLeveledCompactionWriter extends CompactionAwareWriter { @@ -38,34 +45,34 @@ public class MajorLeveledCompactionWriter extends CompactionAwareWriter private final long keysPerSSTable; private final int levelFanoutSize; - public MajorLeveledCompactionWriter(ColumnFamilyStore cfs, + public MajorLeveledCompactionWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, + ILifecycleTransaction txn, Set nonExpiredSSTables, long maxSSTableSize) { - this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, false); + this(realm, directories, txn, nonExpiredSSTables, maxSSTableSize, false); } - public MajorLeveledCompactionWriter(ColumnFamilyStore cfs, + public MajorLeveledCompactionWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, + ILifecycleTransaction txn, Set nonExpiredSSTables, long maxSSTableSize, boolean keepOriginals) { - super(cfs, directories, txn, nonExpiredSSTables, keepOriginals); + super(realm, directories, txn, nonExpiredSSTables, keepOriginals); this.maxSSTableSize = maxSSTableSize; - this.levelFanoutSize = cfs.getLevelFanoutSize(); - long estimatedSSTables = Math.max(1, SSTableReader.getTotalBytes(nonExpiredSSTables) / maxSSTableSize); + this.levelFanoutSize = realm.getLevelFanoutSize(); + long estimatedSSTables = Math.max(1, CompactionSSTable.getTotalDataBytes(nonExpiredSSTables) / maxSSTableSize); keysPerSSTable = estimatedTotalKeys / estimatedSSTables; } @Override - public boolean realAppend(UnfilteredRowIterator partition) + public AbstractRowIndexEntry append(UnfilteredRowIterator partition) { partitionsWritten++; - return super.realAppend(partition); + return super.append(partition); } @Override @@ -95,14 +102,22 @@ public void switchCompactionWriter(Directories.DataDirectory location, Decorated super.switchCompactionWriter(location, nextKey); } - protected int sstableLevel() - { - return currentLevel; - } - - protected long sstableKeyCount() + @Override + @SuppressWarnings("resource") + protected SSTableWriter sstableWriter(Directories.DataDirectory directory, Token diskBoundary) { - return keysPerSSTable; + Descriptor descriptor = realm.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); + return descriptor.getFormat().getWriterFactory().builder(descriptor) + .setKeyCount(keysPerSSTable) + .setRepairedAt(minRepairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(txn.originals(), realm.metadata().comparator, currentLevel)) + .setSerializationHeader(SerializationHeader.make(realm.metadata(), txn.originals())) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(txn, realm); } @Override diff --git a/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java index 1ded2128e77d..49a1f06da2a7 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java @@ -19,12 +19,17 @@ import java.util.Set; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.OperationType; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; public class MaxSSTableSizeWriter extends CompactionAwareWriter { @@ -32,36 +37,36 @@ public class MaxSSTableSizeWriter extends CompactionAwareWriter private final int level; private final long estimatedSSTables; - public MaxSSTableSizeWriter(ColumnFamilyStore cfs, + public MaxSSTableSizeWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, + ILifecycleTransaction txn, Set nonExpiredSSTables, long maxSSTableSize, int level) { - this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, level, false); + this(realm, directories, txn, nonExpiredSSTables, maxSSTableSize, level, false); } - public MaxSSTableSizeWriter(ColumnFamilyStore cfs, + public MaxSSTableSizeWriter(CompactionRealm realm, Directories directories, - LifecycleTransaction txn, + ILifecycleTransaction txn, Set nonExpiredSSTables, long maxSSTableSize, int level, boolean keepOriginals) { - super(cfs, directories, txn, nonExpiredSSTables, keepOriginals); + super(realm, directories, txn, nonExpiredSSTables, keepOriginals); this.level = level; this.maxSSTableSize = maxSSTableSize; - long totalSize = getTotalWriteSize(nonExpiredSSTables, estimatedTotalKeys, cfs, txn.opType()); + long totalSize = getTotalWriteSize(nonExpiredSSTables, estimatedTotalKeys, realm, txn.opType()); estimatedSSTables = Math.max(1, totalSize / maxSSTableSize); } /** * Gets the estimated total amount of data to write during compaction */ - private static long getTotalWriteSize(Iterable nonExpiredSSTables, long estimatedTotalKeys, ColumnFamilyStore cfs, OperationType compactionType) + private static long getTotalWriteSize(Iterable nonExpiredSSTables, long estimatedTotalKeys, CompactionRealm realm, OperationType compactionType) { long estimatedKeysBeforeCompaction = 0; for (SSTableReader sstable : nonExpiredSSTables) @@ -69,7 +74,7 @@ private static long getTotalWriteSize(Iterable nonExpiredSSTables estimatedKeysBeforeCompaction = Math.max(1, estimatedKeysBeforeCompaction); double estimatedCompactionRatio = (double) estimatedTotalKeys / estimatedKeysBeforeCompaction; - return Math.round(estimatedCompactionRatio * cfs.getExpectedCompactedFileSize(nonExpiredSSTables, compactionType)); + return Math.round(estimatedCompactionRatio * realm.getExpectedCompactedFileSize(nonExpiredSSTables, compactionType)); } @Override @@ -78,14 +83,21 @@ protected boolean shouldSwitchWriterInCurrentLocation(DecoratedKey key) return sstableWriter.currentWriter().getEstimatedOnDiskBytesWritten() > maxSSTableSize; } - protected int sstableLevel() - { - return level; - } - - protected long sstableKeyCount() + @Override + protected SSTableWriter sstableWriter(Directories.DataDirectory directory, Token diskBoundary) { - return estimatedTotalKeys / estimatedSSTables; + Descriptor descriptor = realm.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); + return descriptor.getFormat().getWriterFactory().builder(descriptor) + .setKeyCount(estimatedTotalKeys / estimatedSSTables) + .setRepairedAt(minRepairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(txn.originals(), realm.metadata().comparator, level)) + .setSerializationHeader(SerializationHeader.make(realm.metadata(), nonExpiredSSTables)) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(txn, realm); } @Override diff --git a/src/java/org/apache/cassandra/db/compaction/writers/SSTableDataSink.java b/src/java/org/apache/cassandra/db/compaction/writers/SSTableDataSink.java new file mode 100644 index 000000000000..982bd54e001c --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/writers/SSTableDataSink.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compaction.writers; + +import java.io.IOException; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; + +/** + * Abstraction of compaction result writer, implemented by CompactionAwareWriter and tests. + */ +public interface SSTableDataSink +{ + /** + * Append the given partition. + * This is equivalent to a sequence of startPartition, addUnfiltered for each item in the partition, and endPartition. + */ + AbstractRowIndexEntry append(UnfilteredRowIterator partition); + + /** + * Start a partition with the given key and deletion time. + * Returns false if the partition could not be added (e.g. if the key is too long). + */ + boolean startPartition(DecoratedKey partitionKey, DeletionTime deletionTime) throws IOException; + + /** + * Complete a partition. Must be called once for every startPartition. + * + * @return + */ + AbstractRowIndexEntry endPartition() throws IOException; + + /** + * Add a new row or marker in the current partition. Must be preceded by startPartition. + */ + void addUnfiltered(Unfiltered unfiltered) throws IOException; +} diff --git a/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java index 4cd0858e18a4..ea0ac680d40e 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java @@ -23,11 +23,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Directories; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; /** * CompactionAwareWriter that splits input in differently sized sstables @@ -46,16 +51,16 @@ public class SplittingSizeTieredCompactionWriter extends CompactionAwareWriter private long currentBytesToWrite; private int currentRatioIndex = 0; - public SplittingSizeTieredCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables) + public SplittingSizeTieredCompactionWriter(CompactionRealm realm, Directories directories, ILifecycleTransaction txn, Set nonExpiredSSTables) { - this(cfs, directories, txn, nonExpiredSSTables, DEFAULT_SMALLEST_SSTABLE_BYTES); + this(realm, directories, txn, nonExpiredSSTables, DEFAULT_SMALLEST_SSTABLE_BYTES); } - public SplittingSizeTieredCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set nonExpiredSSTables, long smallestSSTable) + public SplittingSizeTieredCompactionWriter(CompactionRealm realm, Directories directories, ILifecycleTransaction txn, Set nonExpiredSSTables, long smallestSSTable) { - super(cfs, directories, txn, nonExpiredSSTables, false); + super(realm, directories, txn, nonExpiredSSTables, false); this.allSSTables = txn.originals(); - totalSize = cfs.getExpectedCompactedFileSize(nonExpiredSSTables, txn.opType()); + totalSize = realm.getExpectedCompactedFileSize(nonExpiredSSTables, txn.opType()); double[] potentialRatios = new double[20]; double currentRatio = 1; for (int i = 0; i < potentialRatios.length; i++) @@ -91,16 +96,24 @@ protected boolean shouldSwitchWriterInCurrentLocation(DecoratedKey key) return false; } - protected int sstableLevel() - { - return 0; - } - - protected long sstableKeyCount() + @Override + protected SSTableWriter sstableWriter(Directories.DataDirectory directory, Token diskBoundary) { long currentPartitionsToWrite = Math.round(ratios[currentRatioIndex] * estimatedTotalKeys); logger.trace("Switching writer, currentPartitionsToWrite = {}", currentPartitionsToWrite); - return currentPartitionsToWrite; + + Descriptor descriptor = realm.newSSTableDescriptor(getDirectories().getLocationForDisk(directory)); + return descriptor.getFormat().getWriterFactory().builder(descriptor) + .setKeyCount(currentPartitionsToWrite) + .setRepairedAt(minRepairedAt) + .setPendingRepair(pendingRepair) + .setTransientSSTable(isTransient) + .setTableMetadataRef(realm.metadataRef()) + .setMetadataCollector(new MetadataCollector(allSSTables, realm.metadata().comparator)) + .setSerializationHeader(SerializationHeader.make(realm.metadata(), nonExpiredSSTables)) + .addDefaultComponents(realm.getIndexManager().listIndexGroups()) + .setSecondaryIndexGroups(realm.getIndexManager().listIndexGroups()) + .build(txn, realm); } @Override diff --git a/src/java/org/apache/cassandra/db/counters/CachedCounterLockManager.java b/src/java/org/apache/cassandra/db/counters/CachedCounterLockManager.java new file mode 100644 index 000000000000..63fa7adb36ae --- /dev/null +++ b/src/java/org/apache/cassandra/db/counters/CachedCounterLockManager.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.counters; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.cassandra.config.DatabaseDescriptor; + +/** + * Implemetation of {@link CounterLockManager} that uses a cache of locks. + * Note: this implemetation tries to reduce the chance of having two counters lock each other, but as the counters + * are identified by the hash of the primary key of the row, it is still possible to + * have some cross-counter contention for counters with different primary keys but the same hash. + *

    + * This code is copied from + * LocalLockManager from the HerdDB project (Apache 2 licensed). + */ +public class CachedCounterLockManager implements CounterLockManager +{ + private final static int EXPECTED_CONCURRENCY = DatabaseDescriptor.getConcurrentCounterWriters() * 16; + /** + * The mapping function in {@link #makeLockForKey(Integer)} relies on the ConcurrentHashMap guarantee that the remapping function is run only once per compute, and that it is run atomically + */ + private final ConcurrentHashMap locks = new ConcurrentHashMap<>(EXPECTED_CONCURRENCY); + + @Override + public List grabLocks(Iterable keys) + { + // we must return the locks in order to avoid deadlocks + // please note that the list may contain duplicates + return StreamSupport.stream(keys.spliterator(), false) + .sorted() + .map(this::makeLockForKey) + .collect(Collectors.toList()); + } + + private ReentrantLock makeLock() + { + return new ReentrantLock(); + } + + private LockHandleImpl makeLockForKey(Integer key) + { + RefCountedLock instance = locks.compute(key, (k, existing) -> { + if (existing != null) + { + existing.count++; + return existing; + } + else + { + return new RefCountedLock(makeLock(), 1); + } + }); + return new LockHandleImpl(key, instance); + } + + private void releaseLockForKey(RefCountedLock instance, Integer key) throws IllegalStateException + { + locks.compute(key, (Integer t, RefCountedLock u) -> { + if (instance != u) + { + throw new IllegalStateException("trying to release un-owned lock"); + } + if (--u.count == 0) + { + return null; + } + else + { + return u; + } + }); + } + + @Override + public boolean hasNumKeys() + { + return true; + } + + @Override + public int getNumKeys() + { + return locks.size(); + } + + /** + * This class is not thread safe, it is expected to be used by a single thread. + */ + private class LockHandleImpl implements LockHandle + { + private boolean acquired; + private final Integer key; + private final RefCountedLock handle; + + private LockHandleImpl(Integer key, RefCountedLock handle) + { + this.key = key; + this.handle = handle; + } + + @Override + public void release() + { + if (acquired) + handle.lock.unlock(); + releaseLockForKey(handle, key); + } + + @Override + public boolean tryLock(long timeout, TimeUnit timeUnit) throws InterruptedException + { + return acquired = handle.lock.tryLock(timeout, timeUnit); + } + + @Override + public String toString() + { + return "{key=" + key + '}'; + } + } + + private static class RefCountedLock + { + + private final ReentrantLock lock; + private int count; + + private RefCountedLock(ReentrantLock lock, int count) + { + this.lock = lock; + this.count = count; + } + + @Override + public String toString() + { + return "RefCountedStampedLock{" + "lock=" + lock + ", count=" + count + '}'; + } + } +} diff --git a/src/java/org/apache/cassandra/db/counters/CounterLockManager.java b/src/java/org/apache/cassandra/db/counters/CounterLockManager.java new file mode 100644 index 000000000000..f0a9efb21302 --- /dev/null +++ b/src/java/org/apache/cassandra/db/counters/CounterLockManager.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.counters; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.config.CassandraRelevantProperties; + +/** + * Interface for managing locks for CounterMutation. + * CounterMutation needs to ensure that each local counter is accessed by only one thread at a time. + * Please note that the id of the counter is an integer hash of the primary key of the row. + */ +public interface CounterLockManager +{ + boolean USE_STRIPED_COUNTER_LOCK_MANAGER = CassandraRelevantProperties.USE_STRIPED_COUNTER_LOCK_MANAGER.getBoolean(); + CounterLockManager instance = USE_STRIPED_COUNTER_LOCK_MANAGER ? new StripedCounterLockManager() : new CachedCounterLockManager(); + + /** + * Handle to a lock for a particular key. + * Some expectations: + * - instances of this class are not thread-safe + * - it is not required that the underlying lock is reentrant + */ + interface LockHandle + { + /** + * Try to get the lock. This method can be called at most once. + * + * @param timeout timeout. + * @param timeUnit time unit. + * @return false in case the lock could not be acquired within the timeout. + * @throws InterruptedException in case the thread is interrupted while waiting for the lock. + */ + boolean tryLock(long timeout, TimeUnit timeUnit) throws InterruptedException; + + /** + * Unlock the lock if it was acquired and release the handle. This method is to be called even if the acquire method failed or even if tryLock has never been called. + * This method is to be called only once. + */ + void release(); + } + + /** + * Grab locks for the given keys. The returned handles must be released by calling {@link LockHandle#release()}. + * The returned list is re-ordered in order to prevent deadlocks. + * It is expected that the caller will release the locks in the inverse order they were acquired. + * The initial set may contain duplicates, it is expected that this method will return a list with the same number of elements. + * + * @param keys list of keys, the Iterable is scanned only once in order to prevent side effects. + * @return a list of lock handles. The List can be iterated multiple times without side effects. + */ + List grabLocks(Iterable keys); + + /** + * Check if the implementation can return the number of keys that are handled by the lock manager. + * This method is useful only for testing. + * + * @return true if the implementation can return the number of keys. + */ + boolean hasNumKeys(); + + /** + * Get the number of keys that are handled by the lock manager. + * This method is useful only for testing. + * + * @return the number of keys. + */ + default int getNumKeys() + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/java/org/apache/cassandra/db/counters/StripedCounterLockManager.java b/src/java/org/apache/cassandra/db/counters/StripedCounterLockManager.java new file mode 100644 index 000000000000..88e2a1d97550 --- /dev/null +++ b/src/java/org/apache/cassandra/db/counters/StripedCounterLockManager.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.counters; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import com.google.common.base.Supplier; +import com.google.common.util.concurrent.Striped; + +import org.apache.cassandra.config.DatabaseDescriptor; + +import static org.apache.cassandra.config.CassandraRelevantProperties.COUNTER_LOCK_FAIR_LOCK; +import static org.apache.cassandra.config.CassandraRelevantProperties.COUNTER_LOCK_NUM_STRIPES_PER_THREAD; +/** + * Legacy implementation of {@link CounterLockManager} that uses a fixed set of locks. + * On a workload with many different counters it is likely to see two counters sharing the same lock. + */ +public class StripedCounterLockManager implements CounterLockManager +{ + private final Striped locks; + + StripedCounterLockManager() + { + int numStripes = COUNTER_LOCK_NUM_STRIPES_PER_THREAD.getInt() * DatabaseDescriptor.getConcurrentCounterWriters(); + if (COUNTER_LOCK_FAIR_LOCK.getBoolean()) + { + try + { + Class stripedClass = Striped.class; + + // Get the custom method Striped.custom + Method customMethod = stripedClass.getDeclaredMethod("custom", int.class, Supplier.class); + customMethod.setAccessible(true); + + Supplier lockSupplier = () -> new ReentrantLock(true); + locks = (Striped) customMethod.invoke(null, numStripes, lockSupplier); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + else + { + locks = Striped.lock(numStripes); + } + } + + @Override + public List grabLocks(Iterable keys) + { + List result = new ArrayList<>(); + Iterable locks = this.locks.bulkGet(keys); + locks.forEach(l -> result.add(new LockImpl(l))); + return result; + } + + @Override + public boolean hasNumKeys() + { + return false; + } + + private static class LockImpl implements LockHandle + { + private final java.util.concurrent.locks.Lock lock; + private boolean acquired; + + public LockImpl(java.util.concurrent.locks.Lock lock) + { + this.lock = lock; + } + + @Override + public void release() + { + if (acquired) + lock.unlock(); + } + + @Override + public boolean tryLock(long timeout, TimeUnit timeUnit) throws InterruptedException + { + acquired = lock.tryLock(timeout, timeUnit); + return acquired; + } + } +} diff --git a/src/java/org/apache/cassandra/db/filter/ANNOptions.java b/src/java/org/apache/cassandra/db/filter/ANNOptions.java new file mode 100644 index 000000000000..2d160ead8189 --- /dev/null +++ b/src/java/org/apache/cassandra/db/filter/ANNOptions.java @@ -0,0 +1,305 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.filter; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.commons.lang3.StringUtils; + +/** + * {@code SELECT} query options for ANN search. + */ +public class ANNOptions +{ + public static final String RERANK_K_OPTION_NAME = "rerank_k"; + public static final String USE_PRUNING_OPTION_NAME = "use_pruning"; + + public static final ANNOptions NONE = new ANNOptions(null, null) + { + @Override + public String toCQLString() + { + return StringUtils.EMPTY; + } + + @Override + public void validate(ClientState state, String keyspace, int limit) + { + // no validation needed for NONE + } + }; + + public static final Serializer serializer = new Serializer(); + + /** + * The amplified limit for the ANN query to get more accurate results. + * A value lesser or equals to zero means no reranking. + * A {@code null} value means the option is not present. + */ + @Nullable + public final Integer rerankK; + + /** + * Whether to use pruning to speed up the ANN search. If {@code null}, the default value is used. + */ + @Nullable + public final Boolean usePruning; + + private ANNOptions(@Nullable Integer rerankK, @Nullable Boolean usePruning) + { + this.rerankK = rerankK; + this.usePruning = usePruning; + } + + public static ANNOptions create(@Nullable Integer rerankK, @Nullable Boolean usePruning) + { + // if all the options are null, return the NONE instance + return rerankK == null && usePruning == null ? NONE : new ANNOptions(rerankK, usePruning); + } + + /** + * Validates the ANN options by checking that they are within the guardrails and that peers support the options. + */ + public void validate(ClientState state, String keyspace, int limit) + { + if (rerankK != null) + { + if (rerankK > 0 && rerankK < limit) + throw new InvalidRequestException(String.format("Invalid rerank_k value %d greater than 0 and less than limit %d", rerankK, limit)); + + Guardrails.annRerankKMaxValue.guard(rerankK, "ANN options", false, state); + } + + // Ensure that all nodes in the cluster are in a version that supports ANN options, including this one + assert keyspace != null; + Set badNodes = MessagingService.instance().endpointsWithConnectionsOnVersionBelow(keyspace, MessagingService.VERSION_DS_11); + if (MessagingService.current_version < MessagingService.VERSION_DS_11) + badNodes.add(FBUtilities.getBroadcastAddressAndPort()); + if (!badNodes.isEmpty()) + throw new InvalidRequestException("ANN options are not supported in clusters below DS 11."); + } + + /** + * Returns the ANN options stored the given map of options. + * + * @param map the map of options in the {@code WITH ANN_OPTION} of a {@code SELECT} query + * @return the ANN options in the specified {@code SELECT} options, or {@link #NONE} if no options are present + */ + public static ANNOptions fromMap(Map map) + { + Integer rerankK = null; + Boolean usePruning = null; + + for (Map.Entry entry : map.entrySet()) + { + String name = entry.getKey(); + String value = entry.getValue(); + + if (name.equals(RERANK_K_OPTION_NAME)) + { + rerankK = parseRerankK(value); + } + else if (name.equals(USE_PRUNING_OPTION_NAME)) + { + usePruning = parseUsePruning(value); + } + else + { + throw new InvalidRequestException("Unknown ANN option: " + name); + } + } + + return ANNOptions.create(rerankK, usePruning); + } + + private static int parseRerankK(String value) + { + int rerankK; + + try + { + rerankK = Integer.parseInt(value); + } + catch (NumberFormatException e) + { + throw new InvalidRequestException(String.format("Invalid '%s' ANN option. Expected a positive int but found: %s", + RERANK_K_OPTION_NAME, value)); + } + + return rerankK; + } + + private static boolean parseUsePruning(String value) + { + value = value.toLowerCase(); + if (!value.equals("true") && !value.equals("false")) + throw new InvalidRequestException(String.format("Invalid '%s' ANN option. Expected a boolean but found: %s", + USE_PRUNING_OPTION_NAME, value)); + return Boolean.parseBoolean(value); + } + + public String toCQLString() + { + StringBuilder sb = new StringBuilder("{"); + if (rerankK != null) + sb.append(String.format("'%s': %d", RERANK_K_OPTION_NAME, rerankK)); + if (usePruning != null) + { + if (rerankK != null) + sb.append(", "); + sb.append(String.format("'%s': %b", USE_PRUNING_OPTION_NAME, usePruning)); + } + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ANNOptions that = (ANNOptions) o; + return Objects.equals(rerankK, that.rerankK) && + Objects.equals(usePruning, that.usePruning); + } + + @Override + public int hashCode() + { + return Objects.hash(rerankK, usePruning); + } + + /** + * Serializer for {@link ANNOptions}. + *

    + * This serializer writes an int containing bit flags that indicate which options are present, allowing the future + * addition of new options without increasing the messaging version. We should be able to create compatible messages + * in the future if we add new options and those are not explicitly set in the user query. If we receive a message + * with unknown newer options from a newer node, we will reject it. + *

    + * This approach should be more space-efficient than simply using a map, as we do with the index creation options. + * Space is more important in this case because the {@link ANNOptions} are sent with every {@code SELECT} query. The + * downside is that we only allow for up to 32 options, which seems reasonable. If we ever need more options, we can + * use the last bit flag to indicate that we need to read more flags from the input. + */ + public static class Serializer + { + /** Bit flags mask to check if the rerank K option is present. */ + private static final int RERANK_K_MASK = 1; + private static final int USE_PRUNING_MASK = 2; + /** Bit flags mask to check if there are any unknown options. It's the negation of all the known flags. */ + private static final int UNKNOWN_OPTIONS_MASK = ~(RERANK_K_MASK | USE_PRUNING_MASK); + + /* + * If you add a new option, then update ANNOptionsTest.FutureANNOptions and possibly add a new test verifying + * that the serialization of the updated and original versions of the options are compatible. + */ + + public void serialize(ANNOptions options, DataOutputPlus out, int version) throws IOException + { + // ANN options are only supported in DS 11 and above, so don't serialize anything if the messaging version is lower + if (version < MessagingService.VERSION_DS_11) + { + if (options != NONE) + throw new IllegalStateException("Unable to serialize ANN options with messaging version: " + version); + return; + } + + int flags = flags(options); + out.writeInt(flags); + + if (options.rerankK != null) + out.writeUnsignedVInt32(options.rerankK); + if (options.usePruning != null) + out.writeBoolean(options.usePruning); + } + + public ANNOptions deserialize(DataInputPlus in, int version) throws IOException + { + // ANN options are only supported in DS 11 and above, so don't read anything if the messaging version is lower + if (version < MessagingService.VERSION_DS_11) + return ANNOptions.NONE; + + int flags = in.readInt(); + + // Reject any flags for unknown options that may have been written by a node running newer code. + if ((flags & UNKNOWN_OPTIONS_MASK) != 0) + throw new IOException("Found unsupported ANN options, likely due to the ANN options containing " + + "new options that are not supported by this node."); + + Integer rerankK = hasRerankK(flags) ? (int) in.readUnsignedVInt() : null; + Boolean usePruning = hasUsePruning(flags) ? in.readBoolean() : null; + + return ANNOptions.create(rerankK, usePruning); + } + + public long serializedSize(ANNOptions options, int version) + { + // ANN options are only supported in DS 11 and above, so no size if the messaging version is lower + if (version < MessagingService.VERSION_DS_11) + return 0; + + int flags = flags(options); + long size = TypeSizes.sizeof(flags); + + if (options.rerankK != null) + size += TypeSizes.sizeofUnsignedVInt(options.rerankK); + if (options.usePruning != null) + size += TypeSizes.sizeof(options.usePruning); + + return size; + } + + private static int flags(ANNOptions options) + { + int flags = 0; + + if (options == NONE) + return flags; + + if (options.rerankK != null) + flags |= RERANK_K_MASK; + if (options.usePruning != null) + flags |= USE_PRUNING_MASK; + + return flags; + } + + private static boolean hasRerankK(int flags) + { + return (flags & RERANK_K_MASK) == RERANK_K_MASK; + } + + private static boolean hasUsePruning(int flags) + { + return (flags & USE_PRUNING_MASK) == USE_PRUNING_MASK; + } + } +} diff --git a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java index ddcaaed812ad..8a274d72c5f6 100644 --- a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java +++ b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java @@ -19,6 +19,7 @@ import java.io.IOException; +import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.*; @@ -47,14 +48,13 @@ public boolean isEmpty(ClusteringComparator comparator) return false; } - protected abstract void serializeInternal(DataOutputPlus out, int version) throws IOException; - protected abstract long serializedSizeInternal(int version); - - protected void appendOrderByToCQLString(TableMetadata metadata, StringBuilder sb) + protected void appendOrderByToCQLString(TableMetadata metadata, CqlBuilder sb) { if (reversed) { - sb.append(" ORDER BY "); + if (sb.length() > 0) + sb.append(' '); + sb.append("ORDER BY "); int i = 0; for (ColumnMetadata column : metadata.clusteringColumns()) { @@ -65,6 +65,9 @@ protected void appendOrderByToCQLString(TableMetadata metadata, StringBuilder sb } } + protected abstract void serializeInternal(DataOutputPlus out, int version) throws IOException; + protected abstract long serializedSizeInternal(int version); + private static class FilterSerializer implements Serializer { public void serialize(ClusteringIndexFilter pfilter, DataOutputPlus out, int version) throws IOException diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java index 564ca1a22314..5042fad33086 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java @@ -20,6 +20,7 @@ import java.io.IOException; import org.apache.cassandra.db.*; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.partitions.CachedPartition; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.rows.*; @@ -153,7 +154,16 @@ static interface InternalDeserializer public Kind kind(); public String toString(TableMetadata metadata); - public String toCQLString(TableMetadata metadata, RowFilter rowFilter); + + /** + * Returns a CQL string representing this clustering index filter and the specified {@link RowFilter}. + * + * @param metadata the table metadata + * @param rowFilter a row filter + * @param redaction whether to redact the clustering column value + * @return a CQL string representing this clustering index filter and the specified {@link RowFilter} + */ + String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction); public interface Serializer { diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java index a98e3bde99ba..bce15b87ae5c 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java @@ -20,8 +20,9 @@ import java.io.IOException; import java.util.*; -import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.db.*; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.partitions.*; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.db.transform.Transformation; @@ -163,38 +164,44 @@ public String toString(TableMetadata metadata) } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction) { if (metadata.clusteringColumns().isEmpty() || clusterings.isEmpty()) - return rowFilter.toCQLString(); + return rowFilter.toCQLString(redaction); boolean isSingleColumn = metadata.clusteringColumns().size() == 1; boolean isSingleClustering = clusterings.size() == 1; - StringBuilder sb = new StringBuilder(); - sb.append(isSingleColumn ? "" : '(') - .append(ColumnMetadata.toCQLString(metadata.clusteringColumns())) - .append(isSingleColumn ? "" : ')'); + CqlBuilder builder = new CqlBuilder(); + builder.append(isSingleColumn ? "" : '(') + .append(ColumnMetadata.toCQLString(metadata.clusteringColumns())) + .append(isSingleColumn ? "" : ')'); - sb.append(isSingleClustering ? " = " : " IN ("); + builder.append(isSingleClustering ? " = " : " IN ("); int i = 0; + int maxClusteringSize = 0; for (Clustering clustering : clusterings) { - sb.append(i++ == 0 ? "" : ", ") - .append(isSingleColumn ? "" : '(') - .append(clustering.toCQLString(metadata)) - .append(isSingleColumn ? "" : ')'); + builder.append(i++ == 0 ? "" : ", ") + .append(isSingleColumn ? "" : '(') + .append(clustering.toCQLString(metadata, redaction)) + .append(isSingleColumn ? "" : ')'); - for (int j = 0; j < clustering.size(); j++) - rowFilter = rowFilter.without(metadata.clusteringColumns().get(j), Operator.EQ, clustering.bufferAt(j)); + maxClusteringSize = Math.max(maxClusteringSize, clustering.size()); } - sb.append(isSingleClustering ? "" : ")"); - - if (!rowFilter.isEmpty()) - sb.append(" AND ").append(rowFilter.toCQLString()); - - appendOrderByToCQLString(metadata, sb); - return sb.toString(); + builder.append(isSingleClustering ? "" : ")"); + + // Remove index restrictions for the clustering columns of this clustering filter from the row filter, + // so we don't print them twice. The row filter can contain expressions copying the clustering filter + // restrictions, because indexed clustering key restrictions are added to the row filter at the CQL layer for + // easier consumption downstream. However, due to CQL validation the row filter won't contain additional + // expressions for columns that are included in the clustering filter, besided the aformentioned copies. + for (i = 0; i < clusterings.first().size(); i++) + rowFilter = rowFilter.withoutFirstLevelExpression(metadata.clusteringColumns().get(i)); + + builder.append(rowFilter, true, redaction); + appendOrderByToCQLString(metadata, builder); + return builder.toString(); } public boolean equals(Object o) @@ -248,4 +255,4 @@ public ClusteringIndexFilter deserialize(DataInputPlus in, int version, TableMet } } } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java index 67aeeb7c56eb..e20d2ed72e93 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java @@ -19,6 +19,9 @@ import java.io.IOException; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.*; import org.apache.cassandra.db.partitions.CachedPartition; import org.apache.cassandra.db.partitions.Partition; @@ -26,7 +29,6 @@ import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.schema.TableMetadata; /** * A filter over a single partition. @@ -134,14 +136,12 @@ public String toString(TableMetadata metadata) } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, Redaction redaction) { - StringBuilder sb = new StringBuilder(); - - sb.append(slices.toCQLString(metadata, rowFilter)); - appendOrderByToCQLString(metadata, sb); - - return sb.toString(); + CqlBuilder builder = new CqlBuilder(); + builder.append(slices.toCQLString(metadata, rowFilter, redaction)); + appendOrderByToCQLString(metadata, builder); + return builder.toString(); } public Kind kind() diff --git a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java index 90fc9f3a1126..aba270473ad6 100644 --- a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java @@ -28,6 +28,7 @@ import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.db.*; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; @@ -69,6 +70,9 @@ public abstract class ColumnFilter public static final Serializer serializer = new Serializer(); + // TODO remove this with Ordering.Ann.USE_SYNTHETIC_SCORE + public abstract boolean fetchesExplicitly(ColumnMetadata column); + /** * The fetching strategy for the different queries. */ @@ -93,7 +97,8 @@ boolean fetchesAllColumns(boolean isStatic) @Override RegularAndStaticColumns getFetchedColumns(TableMetadata metadata, RegularAndStaticColumns queried) { - return metadata.regularAndStaticColumns(); + var merged = queried.regulars.mergeTo(metadata.regularColumns()); + return new RegularAndStaticColumns(metadata.staticColumns(), merged); } }, @@ -114,7 +119,8 @@ boolean fetchesAllColumns(boolean isStatic) @Override RegularAndStaticColumns getFetchedColumns(TableMetadata metadata, RegularAndStaticColumns queried) { - return new RegularAndStaticColumns(queried.statics, metadata.regularColumns()); + var merged = queried.regulars.mergeTo(metadata.regularColumns()); + return new RegularAndStaticColumns(queried.statics, merged); } }, @@ -208,14 +214,16 @@ public static ColumnFilter selection(TableMetadata metadata, } /** - * The columns that needs to be fetched internally for this filter. + * The columns that needs to be fetched internally. See FetchingStrategy for why this is + * always a superset of the queried columns. * * @return the columns to fetch for this filter. */ public abstract RegularAndStaticColumns fetchedColumns(); /** - * The columns actually queried by the user. + * The columns needed to process the query, including selected columns, ordering columns, + * restriction (predicate) columns, and synthetic columns. *

    * Note that this is in general not all the columns that are fetched internally (see {@link #fetchedColumns}). */ @@ -291,9 +299,10 @@ public boolean isWildcard() /** * Returns the CQL string corresponding to this {@code ColumnFilter}. * + * @param redaction whether to redact the queried column names, in case they contain sensitive data. * @return the CQL string corresponding to this {@code ColumnFilter}. */ - public abstract String toCQLString(); + public abstract String toCQLString(Redaction redaction); /** * Returns the sub-selections or {@code null} if there are none. @@ -510,9 +519,7 @@ private SortedSetMultimap buildSubSelectio */ public static class WildCardColumnFilter extends ColumnFilter { - /** - * The queried and fetched columns. - */ + // for wildcards, there is no distinction between fetched and queried because queried is already "everything" private final RegularAndStaticColumns fetchedAndQueried; /** @@ -558,6 +565,12 @@ public boolean fetches(ColumnMetadata column) return true; } + @Override + public boolean fetchesExplicitly(ColumnMetadata column) + { + return false; + } + @Override public boolean fetchedColumnIsQueried(ColumnMetadata column) { @@ -602,7 +615,8 @@ public String toString() return "*/*"; } - public String toCQLString() + @Override + public String toCQLString(Redaction redaction) { return "*"; } @@ -630,14 +644,9 @@ public static class SelectionColumnFilter extends ColumnFilter { public final FetchingStrategy fetchingStrategy; - /** - * The selected columns - */ + // Materializes the columns required to implement queriedColumns() and fetchedColumns(), + // see the comments to superclass's methods private final RegularAndStaticColumns queried; - - /** - * The columns that need to be fetched to be able - */ private final RegularAndStaticColumns fetched; private final SortedSetMultimap subSelections; // can be null @@ -711,6 +720,12 @@ public boolean fetches(ColumnMetadata column) return fetchingStrategy.fetchesAllColumns(column.isStatic()) || fetched.contains(column); } + @Override + public boolean fetchesExplicitly(ColumnMetadata column) + { + return fetched.contains(column); + } + /** * Whether the provided complex cell (identified by its column and path), which is assumed to be _fetched_ by * this filter, is also _queried_ by the user. @@ -803,20 +818,22 @@ public String toString() { prefix = queried.statics.isEmpty() ? "/" - : String.format("+%s/", toString(queried.statics.selectOrderIterator(), false)); + : String.format("+%s/", toString(queried.statics.selectOrderIterator(), false, Redaction.NONE)); } - return prefix + toString(queried.selectOrderIterator(), false); + return prefix + toString(queried.selectOrderIterator(), false, Redaction.NONE); } @Override - public String toCQLString() + public String toCQLString(Redaction redaction) { - return queried.isEmpty() ? "*" : toString(queried.selectOrderIterator(), true); + return queried.isEmpty() ? "*" : toString(queried.selectOrderIterator(), true, redaction); } - private String toString(Iterator columns, boolean cql) + private String toString(Iterator columns, boolean cql, Redaction redaction) { + assert cql || redaction == Redaction.NONE : "Cannot redact non-CQL representation"; + StringJoiner joiner = cql ? new StringJoiner(", ") : new StringJoiner(", ", "[", "]"); while (columns.hasNext()) @@ -831,7 +848,7 @@ private String toString(Iterator columns, boolean cql) if (s.isEmpty()) joiner.add(columnName); else - s.forEach(subSel -> joiner.add(String.format("%s%s", columnName, subSel.toString(cql)))); + s.forEach(subSel -> joiner.add(String.format("%s%s", columnName, subSel.toString(cql, redaction)))); } return joiner.toString(); } diff --git a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java index 459b636a5a0e..feb9364e771d 100644 --- a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java +++ b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java @@ -24,6 +24,7 @@ import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.exceptions.UnknownColumnException; @@ -91,10 +92,17 @@ public int compareTo(ColumnSubselection other) @Override public String toString() { - return toString(false); + return toString(false, Redaction.NONE); } - protected abstract String toString(boolean cql); + /** + * Returns a string representation of this subselection. + * + * @param cql if true, the string representation will be in CQL format + * @param redaction if true, the string representation will redact sensitive data + * @return a string representation of this subselection + */ + protected abstract String toString(boolean cql, Redaction redaction); private static class Slice extends ColumnSubselection { @@ -130,13 +138,13 @@ else if (cmp.compare(to, path) < 0) } @Override - protected String toString(boolean cql) + protected String toString(boolean cql, Redaction redaction) { - // This assert we're dealing with a collection since that's the only thing it's used for so far. + // This asserts we're dealing with a collection since that's the only thing it's used for so far. AbstractType type = ((CollectionType)column().type).nameComparator(); return String.format("[%s:%s]", - from == CellPath.BOTTOM ? "" : (cql ? type.toCQLString(from.get(0)) : type.getString(from.get(0))), - to == CellPath.TOP ? "" : (cql ? type.toCQLString(to.get(0)) : type.getString(to.get(0)))); + from == CellPath.BOTTOM ? "" : (cql ? type.toCQLString(from.get(0), redaction) : type.getString(from.get(0))), + to == CellPath.TOP ? "" : (cql ? type.toCQLString(to.get(0), redaction) : type.getString(to.get(0)))); } } @@ -166,11 +174,11 @@ public int compareInclusionOf(CellPath path) } @Override - protected String toString(boolean cql) + protected String toString(boolean cql, Redaction redaction) { - // This assert we're dealing with a collection since that's the only thing it's used for so far. + // This asserts we're dealing with a collection since that's the only thing it's used for so far. AbstractType type = ((CollectionType)column().type).nameComparator(); - return String.format("[%s]", cql ? type.toCQLString(element.get(0)) : type.getString(element.get(0))); + return String.format("[%s]", cql ? type.toCQLString(element.get(0), redaction) : type.getString(element.get(0))); } } diff --git a/src/java/org/apache/cassandra/db/filter/DataLimits.java b/src/java/org/apache/cassandra/db/filter/DataLimits.java index 60203c3047ff..44c4979f56af 100644 --- a/src/java/org/apache/cassandra/db/filter/DataLimits.java +++ b/src/java/org/apache/cassandra/db/filter/DataLimits.java @@ -19,35 +19,60 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.db.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.db.aggregation.AggregationSpecification; import org.apache.cassandra.db.aggregation.GroupMaker; import org.apache.cassandra.db.aggregation.GroupingState; -import org.apache.cassandra.db.aggregation.AggregationSpecification; -import org.apache.cassandra.db.rows.*; -import org.apache.cassandra.db.partitions.*; +import org.apache.cassandra.db.partitions.CachedPartition; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.BaseRowIterator; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.transform.BasePartitions; import org.apache.cassandra.db.transform.BaseRows; import org.apache.cassandra.db.transform.StoppingTransformation; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ByteBufferUtil; /** - * Object in charge of tracking if we have fetch enough data for a given query. - * - * This is more complicated than a single count because we support PER PARTITION - * limits, but also due to GROUP BY and paging. + * Object in charge of tracking if we have fetched enough data for a given query. + *

    + * This is more complicated than a single count because we support {@code PER PARTITION} + * limits, but also due to {@code GROUP BY} and paging. + *

    + *

    + * Tracking happens by row count ({@see count()}) and bytes ({@see bytes()}), with the first exhausted limit + * taking precedence. + *

    + *

    + * When paging is used (see {@code forPaging} methods), the minimum number between the page size and the rows/bytes + * limit is enforced, meaning that we'll never return more rows than requested. + *

    */ public abstract class DataLimits { + private static final Logger logger = LoggerFactory.getLogger(DataLimits.class); public static final Serializer serializer = new Serializer(); public static final int NO_LIMIT = Integer.MAX_VALUE; - public static final DataLimits NONE = new CQLLimits(NO_LIMIT) + public static final DataLimits NONE = new CQLLimits(NO_LIMIT, NO_LIMIT, NO_LIMIT, false) { @Override public boolean hasEnoughLiveData(CachedPartition cached, long nowInSec, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness) @@ -80,45 +105,92 @@ public PartitionIterator filter(PartitionIterator iter, long nowInSec, boolean c // We currently deal with distinct queries by querying full partitions but limiting the result at 1 row per // partition (see SelectStatement.makeFilter). So an "unbounded" distinct is still actually doing some filtering. - public static final DataLimits DISTINCT_NONE = new CQLLimits(NO_LIMIT, 1, true); + public static final DataLimits DISTINCT_NONE = new CQLLimits(NO_LIMIT, NO_LIMIT, 1, true); public enum Kind { - CQL_LIMIT, - CQL_PAGING_LIMIT, + CQL_LIMIT(0), + CQL_PAGING_LIMIT(1), /** @deprecated See CASSANDRA-16582 */ - @Deprecated(since = "4.0") THRIFT_LIMIT, //Deprecated and unused in 4.0, stop publishing in 5.0, reclaim in 6.0 + @Deprecated(since = "4.0") THRIFT_LIMIT(), //Deprecated and unused in 4.0, stop publishing in 5.0, reclaim in 6.0 /** @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, + @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(2), + CQL_GROUP_BY_PAGING_LIMIT(3); + + /** + * DSE compatibility ordinal for Kind values unknown to DSE. + */ + private static final int UNDEFINED = -1; + /** + * DSE compatibility values for Kind values. Some of the ordinals may be undefined, in which case the value is null. + */ + private static final Kind[] DSE_COMPATIBILITY_VALUES; + + static + { + Kind[] values = values(); + DSE_COMPATIBILITY_VALUES = new Kind[values.length]; + for (Kind kind : values) + { + if (kind.dseCompatibilityOrdinal != UNDEFINED) + { + assert DSE_COMPATIBILITY_VALUES[kind.dseCompatibilityOrdinal] == null : "Duplicate DSE compatibility ordinal " + kind.dseCompatibilityOrdinal; + DSE_COMPATIBILITY_VALUES[kind.dseCompatibilityOrdinal] = kind; + } + } + } + + /** + * Used with DSE compatibility protocol {@link org.apache.cassandra.net.MessagingService.Version#VERSION_30}. + * DSE doesn't know {@link #THRIFT_LIMIT} and {@link #SUPER_COLUMN_COUNTING_LIMIT}, so the compatibility + * ordinals are shifted by 2. + */ + private final int dseCompatibilityOrdinal; + + Kind(int dseCompatibilityOrdinal) + { + this.dseCompatibilityOrdinal = dseCompatibilityOrdinal; + } + + Kind() + { + this(UNDEFINED); + } + + public int dseCompatibilityOrdinal() + { + assert dseCompatibilityOrdinal != UNDEFINED : "DSE compatibility ordinal not defined for kind " + this; + return dseCompatibilityOrdinal; + } } public static DataLimits cqlLimits(int cqlRowLimit) { - return cqlRowLimit == NO_LIMIT ? NONE : new CQLLimits(cqlRowLimit); + return cqlRowLimit == NO_LIMIT ? NONE : new CQLLimits(NO_LIMIT, cqlRowLimit, NO_LIMIT, false); } public static DataLimits cqlLimits(int cqlRowLimit, int perPartitionLimit) { return cqlRowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT ? NONE - : new CQLLimits(cqlRowLimit, perPartitionLimit); + : new CQLLimits(NO_LIMIT, cqlRowLimit, perPartitionLimit, false); } - private static DataLimits cqlLimits(int cqlRowLimit, int perPartitionLimit, boolean isDistinct) + private static DataLimits cqlLimits(int bytesLimit, int cqlRowLimit, int perPartitionLimit, boolean isDistinct) { - return cqlRowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT && !isDistinct + return bytesLimit == NO_LIMIT && cqlRowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT && !isDistinct ? NONE - : new CQLLimits(cqlRowLimit, perPartitionLimit, isDistinct); + : new CQLLimits(bytesLimit, cqlRowLimit, perPartitionLimit, isDistinct); } public static DataLimits groupByLimits(int groupLimit, int groupPerPartitionLimit, + int bytesLimit, int rowLimit, AggregationSpecification groupBySpec) { - return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec); + return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec); } public static DataLimits distinctLimits(int cqlRowLimit) @@ -136,13 +208,20 @@ public boolean isGroupByLimit() return false; } - public boolean isExhausted(Counter counter) + /** + * Returns true if the count limit is not reached. + * + * Note: currently this method's only usage is for paging, where it is checked after processing a page as a quick + * signal that the data for the query is complete - if the count limit is not reached at the end of the page, this + * must be because there is no more data to return. + */ + public boolean isCounterBelowLimits(Counter counter) { - return counter.counted() < count(); + return counter.counted() < count() && counter.bytesCounted() < bytes(); } - public abstract DataLimits forPaging(int pageSize); - public abstract DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining); + public abstract DataLimits forPaging(PageSize pageSize); + public abstract DataLimits forPaging(PageSize pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining); public abstract DataLimits forShortReadRetry(int toFetch); @@ -157,6 +236,17 @@ public DataLimits forGroupByInternalPaging(GroupingState state) throw new UnsupportedOperationException(); } + /** + * Whether this is for the continuation of a paged query, that is, whether it comes from + * {@link #forPaging(PageSize, ByteBuffer, int)} or {@link #forGroupByInternalPaging}. + * + * @return whether this is for the continuation of a paged query + */ + public boolean isPagingContinuation() + { + return false; + } + public abstract boolean hasEnoughLiveData(CachedPartition cached, long nowInSec, boolean countPartitionsWithOnlyStaticData, @@ -180,6 +270,23 @@ public abstract Counter newCounter(long nowInSec, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness); + /** + * The max number of bytes this limits enforces. + *

    + * Note that if this value is set, less rows might be returned if the size of the current rows exceeds the bytes limit. + * + * @return the maximum number of bytes this limits enforces. + */ + public abstract int bytes(); + + /** + * The max number of rows this limits enforces. Note that this means traversed rows, regardless we use grouping or not. + *

    + * @return the maximum number of rows this limits enforces. + */ + @VisibleForTesting + public abstract int rows(); + /** * The max number of results this limits enforces. *

    @@ -198,6 +305,17 @@ public abstract Counter newCounter(long nowInSec, */ public abstract DataLimits withoutState(); + /** + * Returns a copy of this DataLimits with updated counted limit whatever it is (either the rows limit + * or groups limit depending on the actual implementation) + */ + public abstract DataLimits withCountedLimit(int newCountedLimit); + + /** + * Returns a copy of this DataLimits with updated bytes limit. + */ + public abstract DataLimits withBytesLimit(int bytesLimit); + public UnfilteredPartitionIterator filter(UnfilteredPartitionIterator iter, long nowInSec, boolean countPartitionsWithOnlyStaticData) @@ -230,6 +348,8 @@ public PartitionIterator filter(PartitionIterator iter, long nowInSec, boolean c */ public abstract float estimateTotalResults(ColumnFamilyStore cfs); + public abstract String toCQLString(); + public static abstract class Counter extends StoppingTransformation> { protected final long nowInSec; @@ -283,6 +403,12 @@ public RowIterator applyTo(RowIterator partition) public abstract int countedInCurrentPartition(); + /** + * The number of bytes for the counted rows. + * + * @return the number of bytes counted. + */ + public abstract int bytesCounted(); /** * The number of rows counted. * @@ -342,36 +468,32 @@ public void onClose() } /** - * Limits used by CQL; this counts rows. + * Limits used by CQL; this counts rows or bytes read. Please note: + *

      + *
    • When paging on rows, the minimum number of rows between the current limit and the page size is used as actual limit.
    • + *
    • When paging on bytes, the number of bytes takes precedence over the rows limit.
    • + *
    */ private static class CQLLimits extends DataLimits { + protected final int bytesLimit; protected final int rowLimit; protected final int perPartitionLimit; // Whether the query is a distinct query or not. protected final boolean isDistinct; - private CQLLimits(int rowLimit) - { - this(rowLimit, NO_LIMIT); - } - - private CQLLimits(int rowLimit, int perPartitionLimit) + private CQLLimits(int bytesLimit, int rowsLimit, int perPartitionLimit, boolean isDistinct) { - this(rowLimit, perPartitionLimit, false); - } - - private CQLLimits(int rowLimit, int perPartitionLimit, boolean isDistinct) - { - this.rowLimit = rowLimit; + this.bytesLimit = bytesLimit; + this.rowLimit = rowsLimit; this.perPartitionLimit = perPartitionLimit; this.isDistinct = isDistinct; } private static CQLLimits distinct(int rowLimit) { - return new CQLLimits(rowLimit, 1, true); + return new CQLLimits(NO_LIMIT, rowLimit, 1, true); } public Kind kind() @@ -381,7 +503,7 @@ public Kind kind() public boolean isUnlimited() { - return rowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT; + return bytesLimit == NO_LIMIT && rowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT; } public boolean isDistinct() @@ -389,19 +511,27 @@ public boolean isDistinct() return isDistinct; } - public DataLimits forPaging(int pageSize) + public DataLimits forPaging(PageSize pageSize) { - return new CQLLimits(pageSize, perPartitionLimit, isDistinct); + return new CQLLimits(pageSize.minBytesCount(bytesLimit), + pageSize.minRowsCount(rowLimit), + perPartitionLimit, + isDistinct); } - public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) + public DataLimits forPaging(PageSize pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) { - return new CQLPagingLimits(pageSize, perPartitionLimit, isDistinct, lastReturnedKey, lastReturnedKeyRemaining); + return new CQLPagingLimits(pageSize.minBytesCount(bytesLimit), + pageSize.minRowsCount(rowLimit), + perPartitionLimit, + isDistinct, + lastReturnedKey, + lastReturnedKeyRemaining); } public DataLimits forShortReadRetry(int toFetch) { - return new CQLLimits(toFetch, perPartitionLimit, isDistinct); + return new CQLLimits(bytesLimit, toFetch, perPartitionLimit, isDistinct); } public boolean hasEnoughLiveData(CachedPartition cached, long nowInSec, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness) @@ -438,6 +568,16 @@ public Counter newCounter(long nowInSec, return new CQLCounter(nowInSec, assumeLiveData, countPartitionsWithOnlyStaticData, enforceStrictLiveness); } + public int bytes() + { + return bytesLimit; + } + + public int rows() + { + return rowLimit; + } + public int count() { return rowLimit; @@ -453,6 +593,18 @@ public DataLimits withoutState() return this; } + @Override + public DataLimits withCountedLimit(int newCountedLimit) + { + return new CQLLimits(bytesLimit, newCountedLimit, perPartitionLimit, isDistinct); + } + + @Override + public DataLimits withBytesLimit(int bytesLimit) + { + return new CQLLimits(bytesLimit, rowLimit, perPartitionLimit, isDistinct); + } + public float estimateTotalResults(ColumnFamilyStore cfs) { // TODO: we should start storing stats on the number of rows (instead of the number of cells, which @@ -463,10 +615,16 @@ public float estimateTotalResults(ColumnFamilyStore cfs) protected class CQLCounter extends Counter { + /** + * Bytes and rows counted by this counter. + */ + protected int bytesCounted; protected int rowsCounted; protected int rowsInCurrentPartition; protected final boolean countPartitionsWithOnlyStaticData; + protected int staticRowBytes; + protected boolean hasLiveStaticRow; public CQLCounter(long nowInSec, @@ -483,13 +641,14 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) { rowsInCurrentPartition = 0; hasLiveStaticRow = !staticRow.isEmpty() && isLive(staticRow); + staticRowBytes = hasLiveStaticRow && bytesLimit != NO_LIMIT ? staticRow.liveDataSize(nowInSec) : 0; } @Override public Row applyToRow(Row row) { if (isLive(row)) - incrementRowCount(); + incrementRowCount(bytesLimit != NO_LIMIT ? row.liveDataSize(nowInSec) : 0); return row; } @@ -500,15 +659,18 @@ public void onPartitionClose() // rows in the partition. However, if we only have the static row, it will be returned as one row // so count it. if (countPartitionsWithOnlyStaticData && hasLiveStaticRow && rowsInCurrentPartition == 0) - incrementRowCount(); + incrementRowCount(staticRowBytes); super.onPartitionClose(); } - protected void incrementRowCount() + protected void incrementRowCount(int liveRowSize) { - if (++rowsCounted >= rowLimit) + bytesCounted += liveRowSize; + rowsCounted++; + rowsInCurrentPartition++; + if (bytesCounted >= bytesLimit || rowsCounted >= rowLimit) stop(); - if (++rowsInCurrentPartition >= perPartitionLimit) + if (rowsInCurrentPartition >= perPartitionLimit) stopInPartition(); } @@ -522,6 +684,11 @@ public int countedInCurrentPartition() return rowsInCurrentPartition; } + public int bytesCounted() + { + return bytesCounted; + } + public int rowsCounted() { return rowsCounted; @@ -534,31 +701,41 @@ public int rowsCountedInCurrentPartition() public boolean isDone() { - return rowsCounted >= rowLimit; + return rowsCounted >= rowLimit || bytesCounted >= bytesLimit || counted() >= count(); } public boolean isDoneForPartition() { return isDone() || rowsInCurrentPartition >= perPartitionLimit; } + + @Override + public String toString() + { + return String.format("%s(bytes=%s/%s, rows=%s/%s, partition-rows=%s/%s)", this.getClass().getName(), + bytesCounted(), bytesLimit, rowsCounted(), rowLimit, rowsCountedInCurrentPartition(), perPartitionLimit); + } } @Override public String toString() { - StringBuilder sb = new StringBuilder(); + return toCQLString(); + } - if (rowLimit != NO_LIMIT) - { - sb.append("LIMIT ").append(rowLimit); - if (perPartitionLimit != NO_LIMIT) - sb.append(' '); - } + @Override + public String toCQLString() + { + List limits = new ArrayList<>(3); + if (bytesLimit != NO_LIMIT) + limits.add("BYTES LIMIT " + bytesLimit); + if (rowLimit != NO_LIMIT) + limits.add("LIMIT " + rowLimit); if (perPartitionLimit != NO_LIMIT) - sb.append("PER PARTITION LIMIT ").append(perPartitionLimit); + limits.add("PER PARTITION LIMIT " + perPartitionLimit); - return sb.toString(); + return String.join(" ", limits); } } @@ -567,9 +744,9 @@ private static class CQLPagingLimits extends CQLLimits private final ByteBuffer lastReturnedKey; private final int lastReturnedKeyRemaining; - public CQLPagingLimits(int rowLimit, int perPartitionLimit, boolean isDistinct, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) + public CQLPagingLimits(int bytesLimit, int rowLimit, int perPartitionLimit, boolean isDistinct, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) { - super(rowLimit, perPartitionLimit, isDistinct); + super(bytesLimit, rowLimit, perPartitionLimit, isDistinct); this.lastReturnedKey = lastReturnedKey; this.lastReturnedKeyRemaining = lastReturnedKeyRemaining; } @@ -581,21 +758,39 @@ public Kind kind() } @Override - public DataLimits forPaging(int pageSize) + public DataLimits forPaging(PageSize pageSize) { throw new UnsupportedOperationException(); } @Override - public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) + public DataLimits forPaging(PageSize pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) { throw new UnsupportedOperationException(); } + @Override + public boolean isPagingContinuation() + { + return true; + } + @Override public DataLimits withoutState() { - return new CQLLimits(rowLimit, perPartitionLimit, isDistinct); + return new CQLLimits(bytesLimit, rowLimit, perPartitionLimit, isDistinct); + } + + @Override + public DataLimits withCountedLimit(int newCountedLimit) + { + return new CQLPagingLimits(bytesLimit, newCountedLimit, perPartitionLimit, isDistinct, lastReturnedKey, lastReturnedKeyRemaining); + } + + @Override + public DataLimits withBytesLimit(int bytesLimit) + { + return new CQLPagingLimits(bytesLimit, rowLimit, perPartitionLimit, isDistinct, lastReturnedKey, lastReturnedKeyRemaining); } @Override @@ -625,6 +820,7 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) // if any already, so force hasLiveStaticRow to false so we make sure to not count it // once more. hasLiveStaticRow = false; + staticRowBytes = 0; } else { @@ -632,6 +828,16 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) } } } + + @Override + public String toString() + { + return new StringJoiner(", ", CQLPagingLimits.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("lastReturnedKey=" + (lastReturnedKey != null ? ByteBufferUtil.bytesToHex(lastReturnedKey) : null)) + .add("lastReturnedKeyRemaining=" + lastReturnedKeyRemaining) + .toString(); + } } /** @@ -664,19 +870,21 @@ private static class CQLGroupByLimits extends CQLLimits public CQLGroupByLimits(int groupLimit, int groupPerPartitionLimit, + int bytesLimit, int rowLimit, AggregationSpecification groupBySpec) { - this(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec, GroupingState.EMPTY_STATE); + this(groupLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec, GroupingState.EMPTY_STATE); } private CQLGroupByLimits(int groupLimit, int groupPerPartitionLimit, + int bytesLimit, int rowLimit, AggregationSpecification groupBySpec, GroupingState state) { - super(rowLimit, NO_LIMIT, false); + super(bytesLimit, rowLimit, NO_LIMIT, false); this.groupLimit = groupLimit; this.groupPerPartitionLimit = groupPerPartitionLimit; this.groupBySpec = groupBySpec; @@ -697,39 +905,53 @@ public boolean isGroupByLimit() public boolean isUnlimited() { - return groupLimit == NO_LIMIT && groupPerPartitionLimit == NO_LIMIT && rowLimit == NO_LIMIT; + return groupLimit == NO_LIMIT && groupPerPartitionLimit == NO_LIMIT && super.isUnlimited(); } public DataLimits forShortReadRetry(int toFetch) { - return new CQLLimits(toFetch); + return new CQLLimits(NO_LIMIT, toFetch, NO_LIMIT, false); } @Override public float estimateTotalResults(ColumnFamilyStore cfs) { - // For the moment, we return the estimated number of rows as we have no good way of estimating + // For the moment, we return the estimated number of rows as we have no good way of estimating // the number of groups that will be returned. Hopefully, we should be able to fix // that problem at some point. return super.estimateTotalResults(cfs); } @Override - public DataLimits forPaging(int pageSize) + public DataLimits forPaging(PageSize pageSize) { - return new CQLGroupByLimits(pageSize, + if (logger.isTraceEnabled()) + logger.trace("{} forPaging({})", hashCode(), pageSize); + + return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, - rowLimit, + pageSize.minBytesCount(bytesLimit), + pageSize.minRowsCount(rowLimit), groupBySpec, state); } @Override - public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) - { - return new CQLGroupByPagingLimits(pageSize, + public DataLimits forPaging(PageSize pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) + { + if (logger.isTraceEnabled()) + logger.trace("{} forPaging({}, {}, {}) vs state {}/{}", + hashCode(), + pageSize, + lastReturnedKey == null ? "null" : ByteBufferUtil.bytesToHex(lastReturnedKey), + lastReturnedKeyRemaining, + state.partitionKey() == null ? "null" : ByteBufferUtil.bytesToHex(state.partitionKey()), + state.clustering() == null ? "null" : state.clustering().toString()); + + return new CQLGroupByPagingLimits(groupLimit, groupPerPartitionLimit, - rowLimit, + pageSize.minBytesCount(bytesLimit), + pageSize.minRowsCount(rowLimit), groupBySpec, state, lastReturnedKey, @@ -739,8 +961,9 @@ public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastRe @Override public DataLimits forGroupByInternalPaging(GroupingState state) { - return new CQLGroupByLimits(rowLimit, + return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, + bytesLimit, rowLimit, groupBySpec, state); @@ -772,41 +995,44 @@ public DataLimits withoutState() { return state == GroupingState.EMPTY_STATE ? this - : new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec); + : new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec); + } + + @Override + public DataLimits withCountedLimit(int newCountedLimit) + { + return new CQLGroupByLimits(newCountedLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec, state); } + @Override + public DataLimits withBytesLimit(int bytesLimit) + { + return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec, state); + } + + + @Override public String toString() { - StringBuilder sb = new StringBuilder(); + List limits = new ArrayList<>(4); if (groupLimit != NO_LIMIT) - { - sb.append("GROUP LIMIT ").append(groupLimit); - if (groupPerPartitionLimit != NO_LIMIT || rowLimit != NO_LIMIT) - sb.append(' '); - } - + limits.add("GROUP LIMIT " + groupLimit); if (groupPerPartitionLimit != NO_LIMIT) - { - sb.append("GROUP PER PARTITION LIMIT ").append(groupPerPartitionLimit); - if (rowLimit != NO_LIMIT) - sb.append(' '); - } - + limits.add("GROUP PER PARTITION LIMIT " + groupPerPartitionLimit); + if (bytesLimit != NO_LIMIT) + limits.add("BYTES LIMIT " + bytesLimit); if (rowLimit != NO_LIMIT) - { - sb.append("LIMIT ").append(rowLimit); - } + limits.add("ROWS LIMIT " + rowLimit); - return sb.toString(); + return String.join(" ", limits); } @Override - public boolean isExhausted(Counter counter) + public boolean isCounterBelowLimits(Counter counter) { - return ((GroupByAwareCounter) counter).rowsCounted < rowLimit - && counter.counted() < groupLimit; + return counter.rowsCounted() < rowLimit && counter.bytesCounted() < bytesLimit && counter.counted() < groupLimit; } protected class GroupByAwareCounter extends Counter @@ -820,6 +1046,11 @@ protected class GroupByAwareCounter extends Counter */ protected DecoratedKey currentPartitionKey; + /** + * The number of bytes counted so far. + */ + protected int bytesCounted; + /** * The number of rows counted so far. */ @@ -845,6 +1076,8 @@ protected class GroupByAwareCounter extends Counter protected boolean hasLiveStaticRow; + protected int staticRowBytes; + protected boolean hasReturnedRowsFromCurrentPartition; private GroupByAwareCounter(long nowInSec, @@ -865,6 +1098,10 @@ private GroupByAwareCounter(long nowInSec, @Override public void applyToPartition(DecoratedKey partitionKey, Row staticRow) { + if (logger.isTraceEnabled()) + logger.trace("{} - GroupByAwareCounter.newPartition {} with state {}", hashCode(), + ByteBufferUtil.bytesToHex(partitionKey.getKey()), state.partitionKey() != null ? ByteBufferUtil.bytesToHex(state.partitionKey()) : "null"); + if (partitionKey.getKey().equals(state.partitionKey())) { // The only case were we could have state.partitionKey() equals to the partition key @@ -874,6 +1111,7 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) // the static row if any already, so force hasLiveStaticRow to false so we make sure to not count it // once more. hasLiveStaticRow = false; + staticRowBytes = 0; hasReturnedRowsFromCurrentPartition = true; hasUnfinishedGroup = true; } @@ -897,6 +1135,7 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) } hasReturnedRowsFromCurrentPartition = false; hasLiveStaticRow = !staticRow.isEmpty() && isLive(staticRow); + staticRowBytes = hasLiveStaticRow ? staticRow.liveDataSize(nowInSec) : 0; } currentPartitionKey = partitionKey; // If we are done we need to preserve the groupInCurrentPartition and rowsCountedInCurrentPartition @@ -911,12 +1150,19 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) @Override protected Row applyToStatic(Row row) { + if (logger.isTraceEnabled()) + logger.trace("{} - GroupByAwareCounter.applyToStatic {}/{}", + hashCode(), + currentPartitionKey != null ? ByteBufferUtil.bytesToHex(currentPartitionKey.getKey()) : "null", + row == null ? "null" : row.clustering().toString()); + // It's possible that we're "done" if the partition we just started bumped the number of groups (in // applyToPartition() above), in which case Transformation will still call this method. In that case, we // want to ignore the static row, it should (and will) be returned with the next page/group if needs be. if (enforceLimits && isDone()) { hasLiveStaticRow = false; // The row has not been returned + staticRowBytes = 0; return Rows.EMPTY_STATIC_ROW; } return row; @@ -925,6 +1171,12 @@ protected Row applyToStatic(Row row) @Override public Row applyToRow(Row row) { + if (logger.isTraceEnabled()) + logger.trace("{} - GroupByAwareCounter.applyToRow {}/{}", + hashCode(), + ByteBufferUtil.bytesToHex(currentPartitionKey.getKey()), + row.clustering().toString()); + // We want to check if the row belongs to a new group even if it has been deleted. The goal being // to minimize the chances of having to go through the same data twice if we detect on the next // non deleted row that we have reached the limit. @@ -949,7 +1201,7 @@ public Row applyToRow(Row row) if (isLive(row)) { hasUnfinishedGroup = true; - incrementRowCount(); + incrementRowCount(bytesLimit != NO_LIMIT ? row.liveDataSize(nowInSec) : 0); hasReturnedRowsFromCurrentPartition = true; } @@ -968,6 +1220,12 @@ public int countedInCurrentPartition() return groupInCurrentPartition; } + @Override + public int bytesCounted() + { + return bytesCounted; + } + @Override public int rowsCounted() { @@ -980,10 +1238,12 @@ public int rowsCountedInCurrentPartition() return rowsCountedInCurrentPartition; } - protected void incrementRowCount() + protected void incrementRowCount(int rowLiveSize) { rowsCountedInCurrentPartition++; - if (++rowsCounted >= rowLimit) + rowsCounted++; + bytesCounted += rowLiveSize; + if (rowsCounted >= rowLimit || bytesCounted >= bytesLimit) stop(); } @@ -1021,7 +1281,7 @@ public void onPartitionClose() // so count it. if (countPartitionsWithOnlyStaticData && hasLiveStaticRow && !hasReturnedRowsFromCurrentPartition) { - incrementRowCount(); + incrementRowCount(staticRowBytes); incrementGroupCount(); incrementGroupInCurrentPartitionCount(); hasUnfinishedGroup = false; @@ -1038,7 +1298,7 @@ public void onClose() // 2) the end of the data is reached // We know that the end of the data is reached if the group limit has not been reached // and the number of rows counted is smaller than the internal page size. - if (hasUnfinishedGroup && groupCounted < groupLimit && rowsCounted < rowLimit) + if (hasUnfinishedGroup && groupCounted < groupLimit && bytesCounted < bytesLimit && rowsCounted < rowLimit) { incrementGroupCount(); incrementGroupInCurrentPartitionCount(); @@ -1046,6 +1306,13 @@ public void onClose() super.onClose(); } + + @Override + public String toString() + { + return String.format("%s(bytes=%s/%s, rows=%s/%s, partition-rows=%s/%s, groups=%s/%s, partition-groups=%s/%s)", this.getClass().getName(), + bytesCounted(), bytesLimit, rowsCounted(), rowLimit, rowsCountedInCurrentPartition(), perPartitionLimit, groupCounted, groupLimit, groupInCurrentPartition, groupPerPartitionLimit); + } } } @@ -1057,6 +1324,7 @@ private static class CQLGroupByPagingLimits extends CQLGroupByLimits public CQLGroupByPagingLimits(int groupLimit, int groupPerPartitionLimit, + int bytesLimit, int rowLimit, AggregationSpecification groupBySpec, GroupingState state, @@ -1065,6 +1333,7 @@ public CQLGroupByPagingLimits(int groupLimit, { super(groupLimit, groupPerPartitionLimit, + bytesLimit, rowLimit, groupBySpec, state); @@ -1080,13 +1349,13 @@ public Kind kind() } @Override - public DataLimits forPaging(int pageSize) + public DataLimits forPaging(PageSize pageSize) { throw new UnsupportedOperationException(); } @Override - public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) + public DataLimits forPaging(PageSize pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining) { throw new UnsupportedOperationException(); } @@ -1097,6 +1366,12 @@ public DataLimits forGroupByInternalPaging(GroupingState state) throw new UnsupportedOperationException(); } + @Override + public boolean isPagingContinuation() + { + return true; + } + @Override public Counter newCounter(long nowInSec, boolean assumeLiveData, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness) { @@ -1107,9 +1382,23 @@ public Counter newCounter(long nowInSec, boolean assumeLiveData, boolean countPa @Override public DataLimits withoutState() { - return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec); + return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec); + } + + @Override + public DataLimits withCountedLimit(int newCountedLimit) + { + return new CQLGroupByPagingLimits(newCountedLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec, state, lastReturnedKey, lastReturnedKeyRemaining); } + @Override + public DataLimits withBytesLimit(int bytesLimit) + { + return new CQLGroupByPagingLimits(groupLimit, groupPerPartitionLimit, bytesLimit, rowLimit, groupBySpec, state, lastReturnedKey, lastReturnedKeyRemaining); + } + + + private class PagingGroupByAwareCounter extends GroupByAwareCounter { private PagingGroupByAwareCounter(long nowInSec, boolean assumeLiveData, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness) @@ -1120,12 +1409,17 @@ private PagingGroupByAwareCounter(long nowInSec, boolean assumeLiveData, boolean @Override public void applyToPartition(DecoratedKey partitionKey, Row staticRow) { + if (logger.isTraceEnabled()) + logger.trace("{} - CQLGroupByPagingLimits.applyToPartition {}", + hashCode(), ByteBufferUtil.bytesToHex(partitionKey.getKey())); + if (partitionKey.getKey().equals(lastReturnedKey)) { currentPartitionKey = partitionKey; groupInCurrentPartition = groupPerPartitionLimit - lastReturnedKeyRemaining; hasReturnedRowsFromCurrentPartition = true; hasLiveStaticRow = false; + staticRowBytes = 0; hasUnfinishedGroup = state.hasClustering(); } else @@ -1134,13 +1428,29 @@ public void applyToPartition(DecoratedKey partitionKey, Row staticRow) } } } + + @Override + public String toString() + { + return new StringJoiner(", ", CQLGroupByPagingLimits.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("lastReturnedKey=" + (lastReturnedKey != null ? ByteBufferUtil.bytesToHex(lastReturnedKey) : null)) + .add("lastReturnedKeyRemaining=" + lastReturnedKeyRemaining) + .toString(); + } } public static class Serializer { public void serialize(DataLimits limits, DataOutputPlus out, int version, ClusteringComparator comparator) throws IOException { - out.writeByte(limits.kind().ordinal()); + // VERSION_30 is used for migration only (DSE <-> CC compatibility is required). + // DSE doesn't know THRIFT_LIMIT(2) and SUPER_COLUMN_COUNTING_LIMIT(3), so DSE ordinals must be used here. + if (version == MessagingService.VERSION_30) + out.writeByte(limits.kind().dseCompatibilityOrdinal()); + else + out.writeByte(limits.kind().ordinal()); + switch (limits.kind()) { case CQL_LIMIT: @@ -1148,6 +1458,8 @@ public void serialize(DataLimits limits, DataOutputPlus out, int version, Cluste CQLLimits cqlLimits = (CQLLimits)limits; out.writeUnsignedVInt32(cqlLimits.rowLimit); out.writeUnsignedVInt32(cqlLimits.perPartitionLimit); + if (version >= MessagingService.VERSION_DS_10) + out.writeUnsignedVInt32(cqlLimits.bytesLimit); out.writeBoolean(cqlLimits.isDistinct); if (limits.kind() == Kind.CQL_PAGING_LIMIT) { @@ -1162,6 +1474,8 @@ public void serialize(DataLimits limits, DataOutputPlus out, int version, Cluste out.writeUnsignedVInt32(groupByLimits.groupLimit); out.writeUnsignedVInt32(groupByLimits.groupPerPartitionLimit); out.writeUnsignedVInt32(groupByLimits.rowLimit); + if (version >= MessagingService.VERSION_DS_10) + out.writeUnsignedVInt32(groupByLimits.bytesLimit); AggregationSpecification groupBySpec = groupByLimits.groupBySpec; AggregationSpecification.serializer.serialize(groupBySpec, out, version); @@ -1180,7 +1494,13 @@ public void serialize(DataLimits limits, DataOutputPlus out, int version, Cluste public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException { - Kind kind = Kind.values()[in.readUnsignedByte()]; + int ordinal = in.readUnsignedByte(); + Kind kind = version == MessagingService.VERSION_30 ? + Kind.DSE_COMPATIBILITY_VALUES[ordinal] : + Kind.values()[ordinal]; + + assert kind != null : "Unknown DataLimits.Kind with ordinal " + ordinal + " and version " + version; + switch (kind) { case CQL_LIMIT: @@ -1188,12 +1508,13 @@ public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metad { int rowLimit = in.readUnsignedVInt32(); int perPartitionLimit = in.readUnsignedVInt32(); + int bytesLimit = version >= MessagingService.VERSION_DS_10 ? (int) in.readUnsignedVInt() : NO_LIMIT; boolean isDistinct = in.readBoolean(); if (kind == Kind.CQL_LIMIT) - return cqlLimits(rowLimit, perPartitionLimit, isDistinct); + return cqlLimits(bytesLimit, rowLimit, perPartitionLimit, isDistinct); ByteBuffer lastKey = ByteBufferUtil.readWithVIntLength(in); int lastRemaining = in.readUnsignedVInt32(); - return new CQLPagingLimits(rowLimit, perPartitionLimit, isDistinct, lastKey, lastRemaining); + return new CQLPagingLimits(bytesLimit, rowLimit, perPartitionLimit, isDistinct, lastKey, lastRemaining); } case CQL_GROUP_BY_LIMIT: case CQL_GROUP_BY_PAGING_LIMIT: @@ -1201,6 +1522,7 @@ public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metad int groupLimit = in.readUnsignedVInt32(); int groupPerPartitionLimit = in.readUnsignedVInt32(); int rowLimit = in.readUnsignedVInt32(); + int bytesLimit = version >= MessagingService.VERSION_DS_10 ? (int) in.readUnsignedVInt() : NO_LIMIT; AggregationSpecification groupBySpec = AggregationSpecification.serializer.deserialize(in, version, metadata); @@ -1209,6 +1531,7 @@ public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metad if (kind == Kind.CQL_GROUP_BY_LIMIT) return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, + bytesLimit, rowLimit, groupBySpec, state); @@ -1217,6 +1540,7 @@ public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metad int lastRemaining = in.readUnsignedVInt32(); return new CQLGroupByPagingLimits(groupLimit, groupPerPartitionLimit, + bytesLimit, rowLimit, groupBySpec, state, @@ -1237,6 +1561,8 @@ public long serializedSize(DataLimits limits, int version, ClusteringComparator CQLLimits cqlLimits = (CQLLimits) limits; size += TypeSizes.sizeofUnsignedVInt(cqlLimits.rowLimit); size += TypeSizes.sizeofUnsignedVInt(cqlLimits.perPartitionLimit); + if (version >= MessagingService.VERSION_DS_10) + size += TypeSizes.sizeofUnsignedVInt(cqlLimits.bytesLimit); size += TypeSizes.sizeof(cqlLimits.isDistinct); if (limits.kind() == Kind.CQL_PAGING_LIMIT) { @@ -1251,6 +1577,8 @@ public long serializedSize(DataLimits limits, int version, ClusteringComparator size += TypeSizes.sizeofUnsignedVInt(groupByLimits.groupLimit); size += TypeSizes.sizeofUnsignedVInt(groupByLimits.groupPerPartitionLimit); size += TypeSizes.sizeofUnsignedVInt(groupByLimits.rowLimit); + if (version >= MessagingService.VERSION_DS_10) + size += TypeSizes.sizeofUnsignedVInt(groupByLimits.bytesLimit); AggregationSpecification groupBySpec = groupByLimits.groupBySpec; size += AggregationSpecification.serializer.serializedSize(groupBySpec, version); diff --git a/src/java/org/apache/cassandra/db/filter/IndexHints.java b/src/java/org/apache/cassandra/db/filter/IndexHints.java new file mode 100644 index 000000000000..738130bc4f09 --- /dev/null +++ b/src/java/org/apache/cassandra/db/filter/IndexHints.java @@ -0,0 +1,632 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.filter; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import javax.annotation.Nullable; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.exceptions.UnknownIndexException; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static java.lang.String.format; + +/** + * User-provided directives about what indexes should be used by a {@code SELECT} query. + * See {@code IndexHints.md} for further details. + */ +public class IndexHints +{ + private static final Logger logger = LoggerFactory.getLogger(IndexHints.class); + + public static final String CONFLICTING_INDEXES_ERROR = "Indexes cannot be both included and excluded: "; + public static final String WRONG_KEYSPACE_ERROR = "Index %s is not in the same keyspace as the queried table."; + public static final String MISSING_INDEX_ERROR = "Table %s doesn't have an index named %s"; + public static final String NON_INCLUDABLE_INDEXES_ERROR = "It's not possible to use all the specified included indexes with this query."; + public static final String TOO_MANY_INDEXES_ERROR = format("Cannot have more than %d included/excluded indexes, found ", Short.MAX_VALUE); + + public static final IndexHints NONE = new IndexHints(Collections.emptySet(), Collections.emptySet()) + { + @Override + public boolean includes(Index index) + { + return false; + } + + @Override + public boolean includes(String indexName) + { + return false; + } + + @Override + public boolean includesAnyOf(Collection indexes) + { + return false; + } + + @Override + public boolean excludes(Index index) + { + return false; + } + + @Override + public boolean excludes(String indexName) + { + return false; + } + + @Override + public void validate(@Nullable Index.QueryPlan queryPlan) + { + // nothing to validate + } + + @Override + public Comparator comparator() + { + // no index hints, so all plans are equal in that respect + return (x, y) -> 0; + } + }; + + public static final Serializer serializer = new Serializer(); + + /** + * The indexes to use when executing a query. + */ + public final Set included; + + /** + * The indexes not to use when executing the query. + */ + public final Set excluded; + + private IndexHints(Set included, Set excluded) + { + this.included = included; + this.excluded = excluded; + } + + /** + * @param index an index + * @return {@code true} if the index is included, {@code false} otherwise + */ + public boolean includes(Index index) + { + return includes(index.getIndexMetadata().name); + } + + /** + * @param indexName the name of an index + * @return {@code true} if the index is included, {@code false} otherwise + */ + public boolean includes(String indexName) + { + for (IndexMetadata i : included) + { + if (i.name.equals(indexName)) + return true; + } + return false; + } + + /** + * @param indexes a collection of indexes + * @return {@code true} if any of the indexes is included, {@code false} otherwise + */ + public boolean includesAnyOf(Collection indexes) + { + for (Index index : indexes) + { + if (includes(index)) + return true; + } + return false; + } + + /** + * @param index an index + * @return {@code true} if the index is excluded, {@code false} otherwise + */ + public boolean excludes(Index index) + { + return excludes(index.getIndexMetadata().name); + } + + /** + * @param indexName the name of an index + * @return {@code true} if the index is excluded, {@code false} otherwise + */ + public boolean excludes(String indexName) + { + for (IndexMetadata i : excluded) + { + if (i.name.equals(indexName)) + return true; + } + return false; + } + + /** + * Returns the best of the specified indexes that satisfies the specified filter and is not excluded. + * The order of preference to determine whether an index is better than another is: + *
      + *
    1. An index included by these hints is better than an index not included by these hints.
    2. + *
    3. If it's a contains restriction, then a non-analyzed index is better. See CNDB-13925 for details.
    4. + *
    5. An index more selective according to {@link Index#getEstimatedResultRows()} is better. This is done + * accordingly to the {@link Index.QueryPlan#getEstimatedResultRows()} method. Please note that some index + * implementations (SAI) will always return -1 for that method to prioritize themselves. Third party + * implementations can also return similar fixed values. See CNDB-14764 for details.
    6. + *
    + * + * @param indexes a collection of indexes + * @param filter a filter to apply to the indexes + * @param isContains whether the operator of the calling expression is {@code [NOT] CONTAINS [KEY]}, in which case + * we prefer not-analyzed indexes (see CNDB-13925). + * @return the best of the specified indexes that satisfies these index hints and the specified filter + */ + public Optional getBestIndexFor(Collection indexes, Predicate filter, boolean isContains) + { + // filter excluded and filtered indexes + Collection candidates = filter(indexes, index -> !excludes(index) && filter.test(index)); + + // prefer included indexes + candidates = prefer(candidates, this::includes); + + // if we are using a contains operator, we prefer indexes without an analyzer (see CNDB-13925) + if (isContains) + candidates = prefer(candidates, index -> !index.isAnalyzed()); + + // return the candidate with the best selectivity + return bestSelectivityIndex(candidates); + } + + /** + * Returns the indexes in the specified collection of indexes that satisfy the specified filter. + * + * @param indexes a collection of indexes + * @param filter a filter to apply to the indexes + * @return the indexes that satisfy the specified filter + */ + private static Collection filter(Collection indexes, Predicate filter) + { + if (indexes.isEmpty()) + return indexes; + + Set candidates = new HashSet<>(indexes.size()); + for (T index : indexes) + { + if (filter.test(index)) + candidates.add(index); + } + return candidates; + } + + /** + * Returns the indexes in the specified collection that satisfy the specified predicate, or the unmodified + * collection if there are no indexes satisfying the predicate. + * + * @param indexes a collection of indexes + * @param predicate a predicate that returns {@code true} for preferred indexes + * @return the preferred indexes, or the unmodified collection if there are no preferred indexes + */ + private static Collection prefer(Collection indexes, Predicate predicate) + { + if (indexes.isEmpty() || indexes.size() == 1) + return indexes; + + Collection preferred = filter(indexes, predicate); + return preferred.isEmpty() ? indexes : preferred; + } + + /** + * Returns the index with the best selectivity from the specified collection of indexes. + *

    + * The selectivity is determined by the {@link Index#getEstimatedResultRows()} method. Please note that SAI indexes + * will always return -1 for that method, to force their selection. They will later use their own internal planning + * when queried. The index selectivity will still be used for legacy indexes, and potentially for 3rd party + * implementations. + * + * @param indexes a collection of indexes + * @return the index with the best selectivity, according to {@link Index#getEstimatedResultRows()} + */ + private static Optional bestSelectivityIndex(Collection indexes) + { + if (indexes.isEmpty()) + return Optional.empty(); + + if (indexes.size() == 1) + return Optional.of(Iterables.getOnlyElement(indexes)); + + T bestIndex = null; + long bestCardinality = Long.MAX_VALUE; + for (T index : indexes) + { + long cardinality = index.getEstimatedResultRows(); + if (bestIndex == null || cardinality < bestCardinality) + { + bestIndex = index; + bestCardinality = cardinality; + } + } + return Optional.of(bestIndex); + } + + /** + * Creates a new instance of {@link IndexHints} with the specified included and excluded indexes. + * + * @param included the indexes to include when executing the query + * @param excluded the indexes to exclude when executing the query + * @return a new instance of {@link IndexHints} + */ + public static IndexHints create(Set included, Set excluded) + { + if ((included == null || included.isEmpty()) && (excluded == null || excluded.isEmpty())) + return NONE; + + if (included == null) + included = Collections.emptySet(); + if (excluded == null) + excluded = Collections.emptySet(); + + return new IndexHints(included, excluded); + } + + /** + * Validates these index hints for the specified index query plan, to verify that all the included indexes can be + * selected. This might happen if the query doesn't have expressions for each of the included indexes, or if it has + * them but the index implementation hasn't been able to use them for whatever reason. + * + * @param queryPlan the index query plan, which should have been built accordingly to these hints + */ + public void validate(@Nullable Index.QueryPlan queryPlan) + { + if (queryPlan == null) + { + if (included.isEmpty()) + return; + else + throw new InvalidRequestException(NON_INCLUDABLE_INDEXES_ERROR); + } + + for (IndexMetadata indexMetadata : included) + { + boolean found = false; + for (Index i : queryPlan.getIndexes()) + { + if (i.getIndexMetadata().equals(indexMetadata)) + { + found = true; + break; + } + } + if (!found) + throw new InvalidRequestException(NON_INCLUDABLE_INDEXES_ERROR); + } + + // excluded indexes should never be included because the query plans are built from a filtered list + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexHints that = (IndexHints) o; + return Objects.equals(included, that.included) && + Objects.equals(excluded, that.excluded); + } + + @Override + public int hashCode() + { + return Objects.hash(included, excluded); + } + + /** + * Returns the index hints represented by the specified sets of CQL names for the specified queried table. + *

    + * There shouldn't be more included or excluded indexes than can fit in a short, otherwise an + * {@link InvalidRequestException} will be thrown. + *

    + * All the mentioned indexes should exist in the index registry of the queried table, + * or an {@link InvalidRequestException} will be thrown. + * + * @param included the names of the indexes to include when executing the query + * @param excluded the names of the indexes to exclude when executing the query + * @param table the queried table + * @param indexRegistry the index registry of the queried table + * @return the index hints represented by the specified sets of CQL names + * @throws InvalidRequestException if any of the specified indexes do not exist in the specified index registry + */ + public static IndexHints fromCQLNames(Set included, + Set excluded, + TableMetadata table, + IndexRegistry indexRegistry) + { + if (included != null && included.size() > Short.MAX_VALUE) + throw new InvalidRequestException(TOO_MANY_INDEXES_ERROR + included.size()); + + if (excluded != null && excluded.size() > Short.MAX_VALUE) + throw new InvalidRequestException(TOO_MANY_INDEXES_ERROR + excluded.size()); + + IndexHints hints = IndexHints.create(fetchIndexes(included, table, indexRegistry), + fetchIndexes(excluded, table, indexRegistry)); + + if (hints == IndexHints.NONE) + return hints; + + // Ensure that no index is both included and excluded + Set conflictingIndexes = Sets.intersection(hints.included, hints.excluded); + if (!conflictingIndexes.isEmpty()) + { + throw new InvalidRequestException(CONFLICTING_INDEXES_ERROR + IndexMetadata.joinNames(conflictingIndexes)); + } + + // Ensure that all nodes in the cluster are in a version that supports index hints, including this one + Set badNodes = MessagingService.instance().endpointsWithConnectionsOnVersionBelow(table.keyspace, MessagingService.VERSION_DS_12); + if (MessagingService.current_version < MessagingService.VERSION_DS_12) + badNodes.add(FBUtilities.getBroadcastAddressAndPort()); + if (!badNodes.isEmpty()) + throw new InvalidRequestException("Index hints are not supported in clusters below DS 12."); + + return hints; + } + + private static Set fetchIndexes(Set indexNames, TableMetadata table, IndexRegistry indexRegistry) + { + if (indexNames == null || indexNames.isEmpty()) + return Collections.emptySet(); + + Set indexes = new HashSet<>(indexNames.size()); + + for (QualifiedName indexName : indexNames) + { + IndexMetadata index = fetchIndex(indexName, table, indexRegistry); + indexes.add(index); + } + + return indexes; + } + + private static IndexMetadata fetchIndex(QualifiedName indexName, TableMetadata table, IndexRegistry indexRegistry) + { + String name = indexName.getName(); + String keyspace = indexName.getKeyspace(); + + if (keyspace != null && !table.keyspace.equals(keyspace)) + throw new InvalidRequestException(format(WRONG_KEYSPACE_ERROR, indexName)); + + Index index = indexRegistry.getIndexByName(name); + if (index == null) + throw new InvalidRequestException(format(MISSING_INDEX_ERROR, table.name, name)); + + return index.getIndexMetadata(); + } + + /** + * Returns a comparator of index query plans based on which one has the most included indexes, so it can be used to + * select the plans that satisfy the index hints first, and the plans that are closest to satisfy them later. + * + * @return a comparator of index query plans based on which one has the most included indexes + */ + public Comparator comparator() + { + return Comparator.comparing(plan -> Sets.intersection(included, metadata(plan.getIndexes())).size()); + } + + @Override + public String toString() + { + return "IndexHints{" + + "included=" + IndexMetadata.joinNames(included) + + ", excluded=" + IndexMetadata.joinNames(excluded) + + '}'; + } + + private static Set metadata(Collection indexes) + { + Set metadata = new HashSet<>(indexes.size()); + for (Index index : indexes) + metadata.add(index.getIndexMetadata()); + return metadata; + } + + /** + * Serializer for {@link IndexHints}. + *

    + * This serializer writes a byte containing bit flags that indicate which types of hints are present, allowing the + * future addition of new types of hints without necessarily increasing the messaging version. We should be able to + * create compatible messages in the future if we add new types of hints, and those are not explicitly set in the + * user query. If we receive a message with unknown newer types of hints from a newer node, we will reject it. + *

    + * Also, the bit flags are used to skip writing empty sets of indexes, which is the common case. + */ + public static class Serializer + { + /** Bit flags mask to check if there are included indexes. */ + private static final short INCLUDED_MASK = 1; + + /** Bit flags mask to check if there are excluded indexes. */ + private static final short EXCLUDED_MASK = 2; + + /** Bit flags mask to check if there are any unknown hints. It's the negation of all the known flags. */ + private static final short UNKNOWN_HINTS_MASK = ~(INCLUDED_MASK | EXCLUDED_MASK); + + private static final IndexSetSerializer indexSetSerializer = new IndexSetSerializer(); + + public void serialize(IndexHints hints, DataOutputPlus out, int version) throws IOException + { + // index hints are only supported in DS 12 and above, so don't serialize anything if the messaging version is lower + if (version < MessagingService.VERSION_DS_12) + { + if (hints != NONE) + throw new IllegalStateException("Unable to serialize index hints with messaging version: " + version); + return; + } + + byte flags = flags(hints); + out.writeByte(flags); + + indexSetSerializer.serialize(hints.included, out, version); + indexSetSerializer.serialize(hints.excluded, out, version); + } + + public IndexHints deserialize(DataInputPlus in, int version, TableMetadata table) throws IOException + { + // index hints are only supported in DS 12 and above, so don't read anything if the messaging version is lower + if (version < MessagingService.VERSION_DS_12) + return IndexHints.NONE; + + // read the flags first to determine which types of hints are present + byte flags = in.readByte(); + + // Reject any flags for unknown hints that may have been written by a node running newer code. + if ((flags & UNKNOWN_HINTS_MASK) != 0) + throw new IOException("Found unsupported index hints, likely due to the index hints containing " + + "new types of hint that are not supported by this node."); + + // read included and excluded indexes + Set included = hasIncluded(flags) ? indexSetSerializer.deserialize(in, version, table) : Collections.emptySet(); + Set excluded = hasExcluded(flags) ? indexSetSerializer.deserialize(in, version, table) : Collections.emptySet(); + + return IndexHints.create(included, excluded); + } + + public long serializedSize(IndexHints hints, int version) + { + // index hints are only supported in DS 12 and above, so no size if the messaging version is lower + if (version < MessagingService.VERSION_DS_12) + return 0; + + // size of flags + long size = TypeSizes.BYTE_SIZE; + + // size of included and excluded indexes + size += indexSetSerializer.serializedSize(hints.included, version); + size += indexSetSerializer.serializedSize(hints.excluded, version); + + return size; + } + + private static byte flags(IndexHints hints) + { + byte flags = 0; + + if (hints == NONE) + return flags; + + if (!hints.included.isEmpty()) + flags |= INCLUDED_MASK; + + if (!hints.excluded.isEmpty()) + flags |= EXCLUDED_MASK; + + return flags; + } + + private static boolean hasIncluded(int flags) + { + return (flags & INCLUDED_MASK) == INCLUDED_MASK; + } + + private static boolean hasExcluded(int flags) + { + return (flags & EXCLUDED_MASK) == EXCLUDED_MASK; + } + } + + /** + * Serializer for a set of indexes. Nothing is written if the set is empty. Otherwise, we write first the number of + * indexes and then the indexes themselves. Each index is represented by the serialization of its metadata. + */ + private static class IndexSetSerializer + { + private void serialize(Set indexes, DataOutputPlus out, int version) throws IOException + { + if (indexes.isEmpty()) + return; + + int n = indexes.size(); + assert n < Short.MAX_VALUE : TOO_MANY_INDEXES_ERROR + n; + + out.writeVInt32(n); + for (IndexMetadata index : indexes) + IndexMetadata.serializer.serialize(index, out, version); + } + + private Set deserialize(DataInputPlus in, int version, TableMetadata table) throws IOException + { + int n = (int) in.readVInt(); + Set indexes = new HashSet<>(n); + for (short i = 0; i < n; i++) + { + try + { + IndexMetadata metadata = IndexMetadata.serializer.deserialize(in, version, table); + indexes.add(metadata); + } + catch (UnknownIndexException e) + { + logger.info("Couldn't find a defined index on {}.{} with the id {}. " + + "If an index was just created, this is likely due to the schema not " + + "being fully propagated. Index hints for this index will be ignored. " + + "Please wait for schema agreement after index creation.", + table.keyspace, table.name, e.indexId); + } + } + return indexes; + } + + private long serializedSize(Set indexes, int version) + { + if (indexes.isEmpty()) + return 0; + + long size = VIntCoding.computeVIntSize(indexes.size()); + for (IndexMetadata index : indexes) + size += IndexMetadata.serializer.serializedSize(index, version); + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/db/filter/IndexHints.md b/src/java/org/apache/cassandra/db/filter/IndexHints.md new file mode 100644 index 000000000000..be57ef59de78 --- /dev/null +++ b/src/java/org/apache/cassandra/db/filter/IndexHints.md @@ -0,0 +1,136 @@ + + +# Index Hints + +Index hints are user-provided directives about what indexes should be used by a `SELECT` query. +They consist of a set of indexes that should be used (included) and a set of indexes that should not be used (excluded). +The CQL syntax is: +``` +SELECT ... FROM ... WHERE ... + WITH included_indexes = { ... } + AND excluded_indexes = { ... }; +``` +So, for example, given the following schema: +``` +CREATE TABLE users ( + username text PRIMARY KEY, + birth_year int, + country text, + phone text +); + +CREATE INDEX birth_year_idx ON users (birth_year); +CREATE INDEX country_idx ON users (country); +CREATE INDEX phone_idx ON users (phone); +``` +The following query will use the index on `birth_year` and will not use the indexes on `country` and `phone`: +``` +SELECT * FROM users + WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING + WITH included_indexes = {birth_year_idx} + AND excluded_indexes = {country_idx, phone_idx}; +``` +Please note that the query requires `ALLOW FILTERING` because there is a restriction on the `country` column, +and we are explicitly excluding the index on that column. +Note also that excluding the index on `phone` is a no-op because there isn’t any restriction on it. + +It’s guaranteed that the queries will utilize all the included indexes, or fail if it’s not possible to do so. +It will never happen that a query succeeds without using all the included indexes. +Queries might fail because the query doesn't have a restriction for those indexes, +because there is a restriction that could use the index but is not compatible with other restrictions, +or because the underlying index implementation isn't able to use the index for some reason. + +Excluded indexes will never make the query fail, unless they reference a non-existent index. +That's because it’s always possible to exclude an index regardless of the query expressions +and index implementation capabilities. +However, excluding indexes might make it necessary to add `ALLOW FILTERING` to the query. + +Indexes that are applicable to the query and that are not mentioned in these two sets of included and excluded indexes +might or might not be used, depending on the index query planner. + +## Disambiguating queries + +Index hints can also be used to disambiguate queries where a restricted column has multiple indexes that return +different results. For example, we can have analyzed and not-analyzed indexes in the same column. An equality query on +that column would throw an exception due to the ambiguity: +``` +CREATE TABLE t(k int PRIMARY KEY, v text); +CREATE CUSTOM INDEX not_analyzed_idx ON t(v) USING 'StorageAttachedIndex'; +CREATE CUSTOM INDEX analyzed_idx ON t(v) USING 'StorageAttachedIndex' WITH OPTIONS = { 'index_analyzer': 'standard' }; +SELECT * FROM t WHERE v = '...'; # rejected query due to ambiguity +``` +But the query will work if we add hints to include or exclude one of the indexes. +The following will use non-analyzed index and restrict according the exact equality semantics: +``` +SELECT * FROM t WHERE v = '...' WITH included_indexes = {not_analyzed_idx}; +SELECT * FROM t WHERE v = '...' WITH excluded_indexes = {analyzed_idx}; +``` +The following will use analyzed index and restrict according the analyzer-based matching semantics +``` +SELECT * FROM t WHERE v = '...' WITH included_indexes = {analyzed_idx}; +SELECT * FROM t WHERE v = '...' WITH excluded_indexes = {not_analyzed_idx}; +``` +A similar disambiguation can be done for `CONTAINS` queries where the column has both analyzed and not-analyzed indexes: +``` +CREATE TABLE t(k int PRIMARY KEY, v set); +CREATE CUSTOM INDEX not_analyzed_idx ON t(v) USING 'StorageAttachedIndex'; +CREATE CUSTOM INDEX analyzed_idx ON t(v) USING 'StorageAttachedIndex' WITH OPTIONS = { 'index_analyzer': 'standard' }; +INSERT INTO t(k, v) VALUES ( 0, {'apple banana'}); +INSERT INTO t(k, v) VALUES ( 1, {'apple'}); +``` +By default, `CONTAINS` queries will use the not-analyzed index: +``` +SELECT * FROM t WHERE v CONTAINS 'apple'; +``` +This will use the not-analyzed index and return one row only. +But we can use hints to force the use of the analyzed index: +``` +SELECT * FROM t WHERE v CONTAINS 'apple' WITH included_indexes = {analyzed_idx}; +``` +This will use the analyzed index and return two rows instead. + +## Unshading queries + +The presence of indexes can shade queries that used to have a different behaviour without indexes. +For example, an analyzed index will shade `ALLOW FILTERING`'s full-value equality: +``` +CREATE TABLE t(k int PRIMARY KEY, v text); +SELECT * FROM t WHERE v = '...' ALLOW FILTERING; # exact equality match +CREATE CUSTOM INDEX idx ON t(v) USING 'StorageAttachedIndex' WITH OPTIONS = { 'index_analyzer': 'standard' }; +SELECT * FROM t WHERE v = '...' ALLOW FILTERING; # uses the analyzed index, shading the previous query +``` +But we can use hints to exclude that index and get access to the not-indexed behaviour: +``` +SELECT * FROM t WHERE v = '...' ALLOW FILTERING WITH excluded_indexes = {idx}; # uses not-analyzed filtering +``` + +## Choosing between index implementations + +Columns can have multiple indexes with different implementations. +For example, we can have a legacy index and a SAI index on the same column: +``` +CREATE TABLE t(k int PRIMARY KEY, v text); +CREATE INDEX legacy_idx ON t(v); +CREATE CUSTOM INDEX sai_idx ON t(v) USING 'StorageAttachedIndex'; +SELECT * FROM t WHERE v = '...'; # uses the SAI index +``` +The index manager will always prefer the SAI index over the legacy index. +However, we can use hints to prefer the legacy index: +``` +SELECT * FROM t WHERE v = '...' WITH included_indexes = {legacy_idx}; # uses the legacy index +SELECT * FROM t WHERE v = '...' WITH excluded_indexes = {sai_idx}; # also uses the legacy index +``` \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java index 2cb0af969dab..0a51cae573c8 100644 --- a/src/java/org/apache/cassandra/db/filter/RowFilter.java +++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java @@ -22,38 +22,47 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; -import com.google.common.base.Objects; +import javax.annotation.Nullable; +import com.google.common.base.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.restrictions.ExternalRestriction; +import org.apache.cassandra.cql3.restrictions.Restrictions; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; +import org.apache.cassandra.cql3.statements.SelectOptions; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionPurger; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.context.CounterContext; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.CollectionType; import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.FloatType; import org.apache.cassandra.db.marshal.ListType; import org.apache.cassandra.db.marshal.LongType; import org.apache.cassandra.db.marshal.MapType; -import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.rows.BaseRowIterator; @@ -65,14 +74,20 @@ import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.index.sai.utils.GeoUtil; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; +import org.apache.lucene.util.SloppyMath; import static org.apache.cassandra.cql3.statements.RequestValidations.checkBindValueSet; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; @@ -86,70 +101,101 @@ * be handled by a 2ndary index, and the rest is simply filtered out from the * result set (the later can only happen if the query was using ALLOW FILTERING). */ -public class RowFilter implements Iterable +public class RowFilter { private static final Logger logger = LoggerFactory.getLogger(RowFilter.class); public static final Serializer serializer = new Serializer(); - private static final RowFilter NONE = new RowFilter(Collections.emptyList(), false); - - protected final List expressions; + public static final RowFilter NONE = new RowFilter(FilterElement.NONE, false, IndexHints.NONE); + public final FilterElement root; + public final IndexHints indexHints; private final boolean needsReconciliation; - protected RowFilter(List expressions, boolean needsReconciliation) + protected RowFilter(FilterElement root, boolean needsReconciliation, IndexHints indexHints) { - this.expressions = expressions; + this.root = root; this.needsReconciliation = needsReconciliation; + this.indexHints = indexHints; } - /** - * - * @param needsReconciliation whether or not this filter belongs to a read that requires coordinator reconciliation - * - * @return a new {@link RowFilter} with an empty {@link Expression} list - */ - public static RowFilter create(boolean needsReconciliation) + public static RowFilter none() { - return new RowFilter(new ArrayList<>(), needsReconciliation); + return NONE; } - public static RowFilter none() + public FilterElement root() { - return NONE; + return root; } - public SimpleExpression add(ColumnMetadata def, Operator op, ByteBuffer value) + /** + * @return all the expressions in this filter expression tree by traversing it in pre-order + */ + public List expressions() { - SimpleExpression expression = new SimpleExpression(def, op, value); - add(expression); - return expression; + return root.traversedExpressions(); } - public void addMapEquality(ColumnMetadata def, ByteBuffer key, Operator op, ByteBuffer value) + /** + * @return {@code true} if this filter contains any expression with an ANN operator, {@code false} otherwise. + */ + public boolean hasANN() { - add(new MapEqualityExpression(def, key, op, value)); + for (Expression expression : root.expressions()) // ANN expressions are always on the first tree level + { + if (expression.operator == Operator.ANN) + return true; + } + return false; } - public void addCustomIndexExpression(TableMetadata metadata, IndexMetadata targetIndex, ByteBuffer value) + /** + * @return {@code true} if this filter contains any expression with an ordering expression, {@code false} otherwise. + */ + public boolean hasOrdering() + { + for (Expression expression : root.expressions()) // ordering expressions are always on the first tree level + { + if (expression.isOrderingExpression()) + return true; + } + return false; + } + + /** + * Returns a copy of this {@link RowFilter} with ordering expressions removed + */ + public RowFilter withoutOrderingExpressions() { - add(new CustomExpression(metadata, targetIndex, value)); + return restrict(e -> !e.isOrderingExpression()); } - private void add(Expression expression) + /** + * @return the {@link ANNOptions} of the ANN expression in this filter, or {@link ANNOptions#NONE} if there is + * no ANN expression. + */ + public ANNOptions annOptions() { - expression.validate(); - expressions.add(expression); + for (Expression expression : root.expressions()) // ANN expressions are always on the first tree level + { + if (expression.operator == Operator.ANN) + return expression.annOptions(); + } + return ANNOptions.NONE; } - public List getExpressions() + /** + * @return {@code true} if this filter contains any disjunction, {@code false} otherwise. + */ + public boolean containsDisjunctions() { - return expressions; + return root.containsDisjunctions(); } /** * @return true if this filter belongs to a read that requires reconciliation at the coordinator - * @see StatementRestrictions#getRowFilter(IndexRegistry, QueryOptions) + * @see StatementRestrictions#getRowFilter(IndexRegistry, QueryOptions, ClientState, SelectOptions) */ public boolean needsReconciliation() { @@ -176,16 +222,17 @@ public boolean isStrict() */ public boolean isMutableIntersection() { + List exprs = expressions(); Set columns = null; - for (Expression e : expressions) + for (Expression e : exprs) { - if (e.column.isStatic() && expressions.size() > 1) + if (e.column.isStatic() && exprs.size() > 1) return true; if (!e.column.isPrimaryKeyColumn()) { if (columns == null) - columns = new HashSet<>(expressions.size()); + columns = new HashSet<>(exprs.size()); columns.add(e.column); if (columns.size() > 1) @@ -201,7 +248,7 @@ public boolean isMutableIntersection() */ public boolean hasExpressionOnClusteringOrRegularColumns() { - for (Expression expression : expressions) + for (Expression expression : expressions()) { ColumnMetadata column = expression.column(); if (column.isClusteringColumn() || column.isRegular()) @@ -210,28 +257,12 @@ public boolean hasExpressionOnClusteringOrRegularColumns() return false; } - /** - * Note that the application of this transformation does not yet take {@link #isStrict()} into account. This means - * that even when strict filtering is not safe, expressions will be applied as intersections rather than unions. - * The filter will always be evaluated strictly in conjunction with replica filtering protection at the - * coordinator, however, even after CASSANDRA-19007 is addressed. - * - * @see CASSANDRA-19007 - */ protected Transformation> filter(TableMetadata metadata, long nowInSec) { - List partitionLevelExpressions = new ArrayList<>(); - List rowLevelExpressions = new ArrayList<>(); - for (Expression e: expressions) - { - if (e.column.isStatic() || e.column.isPartitionKey()) - partitionLevelExpressions.add(e); - else - rowLevelExpressions.add(e); - } + FilterElement partitionLevelOperation = root.partitionLevelTree(); + FilterElement rowLevelOperation = root.rowLevelTree(); - long numberOfRegularColumnExpressions = rowLevelExpressions.size(); - final boolean filterNonStaticColumns = numberOfRegularColumnExpressions > 0; + final boolean filterNonStaticColumns = !rowLevelOperation.isEmpty(); return new Transformation<>() { @@ -243,12 +274,11 @@ protected BaseRowIterator applyToPartition(BaseRowIterator partition) pk = partition.partitionKey(); // Short-circuit all partitions that won't match based on static and partition keys - for (Expression e : partitionLevelExpressions) - if (!e.isSatisfiedBy(metadata, partition.partitionKey(), partition.staticRow(), nowInSec)) - { - partition.close(); - return null; - } + if (!partitionLevelOperation.isSatisfiedBy(metadata, partition.partitionKey(), partition.staticRow(), nowInSec)) + { + partition.close(); + return null; + } BaseRowIterator iterator = partition instanceof UnfilteredRowIterator ? Transformation.apply((UnfilteredRowIterator) partition, this) @@ -273,9 +303,8 @@ public Row applyToRow(Row row) if (purged == null) return null; - for (Expression e : rowLevelExpressions) - if (!e.isSatisfiedBy(metadata, pk, purged, nowInSec)) - return null; + if (!rowLevelOperation.isSatisfiedBy(metadata, pk, purged, nowInSec)) + return null; return row; } @@ -292,7 +321,7 @@ public Row applyToRow(Row row) */ public UnfilteredPartitionIterator filter(UnfilteredPartitionIterator iter, long nowInSec) { - return expressions.isEmpty() ? iter : Transformation.apply(iter, filter(iter.metadata(), nowInSec)); + return root.isEmpty() ? iter : Transformation.apply(iter, filter(iter.metadata(), nowInSec)); } /** @@ -305,7 +334,7 @@ public UnfilteredPartitionIterator filter(UnfilteredPartitionIterator iter, long */ public PartitionIterator filter(PartitionIterator iter, TableMetadata metadata, long nowInSec) { - return expressions.isEmpty() ? iter : Transformation.apply(iter, filter(metadata, nowInSec)); + return root.isEmpty() ? iter : Transformation.apply(iter, filter(metadata, nowInSec)); } /** @@ -322,23 +351,18 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, // We purge all tombstones as the expressions isSatisfiedBy methods expects it Row purged = row.purge(DeletionPurger.PURGE_ALL, nowInSec, metadata.enforceStrictLiveness()); if (purged == null) - return expressions.isEmpty(); + return root.isEmpty(); - for (Expression e : expressions) - { - if (!e.isSatisfiedBy(metadata, partitionKey, purged, nowInSec)) - return false; - } - return true; + return root.isSatisfiedBy(metadata, partitionKey, purged, nowInSec); } /** - * Returns true if all of the expressions within this filter that apply to the partition key are satisfied by + * Returns true if all the expressions within this filter that apply to the partition key are satisfied by * the given key, false otherwise. */ public boolean partitionKeyRestrictionsAreSatisfiedBy(DecoratedKey key, AbstractType keyValidator) { - for (Expression e : expressions) + for (Expression e : expressions()) { if (!e.column.isPartitionKey()) continue; @@ -346,47 +370,43 @@ public boolean partitionKeyRestrictionsAreSatisfiedBy(DecoratedKey key, Abstract ByteBuffer value = keyValidator instanceof CompositeType ? ((CompositeType) keyValidator).split(key.getKey())[e.column.position()] : key.getKey(); - if (!e.operator().isSatisfiedBy(e.column.type, value, e.value)) + + if (!e.isSatisfiedBy(e.column.type, value)) return false; } return true; } /** - * Returns true if all of the expressions within this filter that apply to the clustering key are satisfied by + * Returns true if all the expressions within this filter that apply to the clustering key are satisfied by * the given Clustering, false otherwise. */ public boolean clusteringKeyRestrictionsAreSatisfiedBy(Clustering clustering) { - for (Expression e : expressions) + for (Expression e : expressions()) { if (!e.column.isClusteringColumn()) continue; - if (!e.operator().isSatisfiedBy(e.column.type, clustering.bufferAt(e.column.position()), e.value)) - { + ByteBuffer value = clustering.bufferAt(e.column.position()); + + if (!e.isSatisfiedBy(e.column.type, value)) return false; - } } return true; } /** - * Returns this filter but without the provided expression. This method - * *assumes* that the filter contains the provided expression. + * Returns this filter but without the provided expression. This method *assumes* that the filter contains the + * provided expression, and it looks in all the levels of the filter tree. */ public RowFilter without(Expression expression) { - assert expressions.contains(expression); - if (expressions.size() == 1) + assert root.contains(expression); + if (root.size() == 1) return RowFilter.none(); - List newExpressions = new ArrayList<>(expressions.size() - 1); - for (Expression e : expressions) - if (!e.equals(expression)) - newExpressions.add(e); - - return withNewExpressions(newExpressions); + return new RowFilter(root.filter(e -> !e.equals(expression)), needsReconciliation, indexHints); } /** @@ -398,17 +418,12 @@ public RowFilter without(ColumnMetadata column, Operator op, ByteBuffer value) if (isEmpty()) return this; - List newExpressions = new ArrayList<>(expressions.size() - 1); - for (Expression e : expressions) - if (!e.column().equals(column) || e.operator() != op || !e.value.equals(value)) - newExpressions.add(e); - - return withNewExpressions(newExpressions); + return new RowFilter(root.filter(e -> !e.column().equals(column) || e.operator() != op || !e.value.equals(value)), needsReconciliation, indexHints); } public boolean hasNonKeyExpression() { - for (Expression e : expressions) + for (Expression e : expressions()) if (!e.column().isPrimaryKeyColumn()) return true; @@ -417,72 +432,608 @@ public boolean hasNonKeyExpression() public boolean hasStaticExpression() { - for (Expression e : expressions) + for (Expression e : expressions()) if (e.column().isStatic()) return true; return false; } - public RowFilter withoutExpressions() + /** + * Returns a copy of this filter but without first level expressions with the provided column. + * If this filter doesn't contain the specified expressions this method will just return an identical copy of this filter. + */ + public RowFilter withoutFirstLevelExpression(ColumnMetadata column) + { + return restrictFirstLevel(e -> !e.column.equals(column)); + } + + /** + * @return this filter pruning all its disjunction branches + */ + public RowFilter withoutDisjunctions() + { + return new RowFilter(root.withoutDisjunctions(), needsReconciliation, indexHints); + } + + public RowFilter restrict(Predicate filter) + { + return new RowFilter(root.filter(filter), needsReconciliation, indexHints); + } + + public RowFilter restrictFirstLevel(Predicate filter) + { + return new RowFilter(root.filterFirstLevel(filter), needsReconciliation, indexHints); + } + + public boolean isEmpty() { - return withNewExpressions(Collections.emptyList()); + return root.isEmpty(); } - protected RowFilter withNewExpressions(List expressions) + @Override + public String toString() { - return new RowFilter(expressions, needsReconciliation); + return toString(false); } - public boolean isEmpty() - { - return expressions.isEmpty(); - } + /** + * Returns a CQL representation of this row filter. + * + * @return a CQL representation of this row filter + */ + public String toCQLString(Redaction redaction) + { + return root.toCQLString(redaction); + } + + public String toString(boolean cql) + { + return root.toString(cql); + } + + public static Builder builder(boolean needsReconciliation) + { + return new Builder(needsReconciliation, null, IndexHints.NONE); + } + + public static Builder builder(boolean needsReconciliation, IndexRegistry indexRegistry) + { + return new Builder(needsReconciliation, indexRegistry, IndexHints.NONE); + } + + public static Builder builder(IndexRegistry indexRegistry, IndexHints indexHints) + { + return new Builder(false, indexRegistry, indexHints); + } + + public static class Builder + { + private FilterElement.Builder current = new FilterElement.Builder(false); + boolean needsReconciliation = false; + private final IndexRegistry indexRegistry; + private final IndexHints indexHints; + + public Builder(boolean needsReconciliation, IndexRegistry indexRegistry, IndexHints indexHints) + { + this.needsReconciliation = needsReconciliation; + this.indexRegistry = indexRegistry; + this.indexHints = indexHints; + } + + public RowFilter build() + { + return new RowFilter(current.build(), needsReconciliation, indexHints); + } + + public RowFilter buildFromRestrictions(StatementRestrictions restrictions, + TableMetadata table, + QueryOptions options, + ClientState state, + ANNOptions annOptions) + { + FilterElement root = doBuild(restrictions, table, options, annOptions); + + if (Guardrails.queryFilters.enabled(state)) + Guardrails.queryFilters.guard(root.numFilteredValues(), "Select query", false, state); + + return new RowFilter(root, needsReconciliation, indexHints); + } + + private FilterElement doBuild(StatementRestrictions restrictions, + TableMetadata table, + QueryOptions options, + ANNOptions annOptions) + { + FilterElement.Builder element = new FilterElement.Builder(restrictions.isDisjunction()); + this.current = element; + + for (Restrictions restrictionSet : restrictions.filterRestrictions().getRestrictions()) + restrictionSet.addToRowFilter(this, indexRegistry, options, annOptions, indexHints); + + for (ExternalRestriction expression : restrictions.filterRestrictions().getExternalExpressions()) + addAllAsConjunction(b -> expression.addToRowFilter(b, table, options)); + + for (StatementRestrictions child : restrictions.children()) + element.children.add(doBuild(child, table, options, annOptions)); + + // Optimize out any conjunctions / disjunctions with TRUE. + // This is not needed for correctness. + if (restrictions.isDisjunction()) + { + // `OR TRUE` swallows all other restrictions in disjunctions. + // Therefore, replace this node with an always true element. + if (element.children.stream().anyMatch(FilterElement::isAlwaysTrue)) + element = new FilterElement.Builder(false); + } + else + { + // `AND TRUE` does nothing in conjunctions, so remove it. + element.children.removeIf(FilterElement::isAlwaysTrue); + } + + return element.build(); + } + + /** + * Adds multiple filter expressions to this {@link RowFilter.Builder} and joins them with AND (conjunction), + * regardless of the current mode (conjunction / disjunction) of the {@link RowFilter.Builder}. + *

    + * + * This wrapper method makes sure we pass a {@code RowFilter.Builder} that is always in conjunction mode to the + * respective {@code addToRowFilterDelegate} method. If multiple expressions are added to the row filter, this + * method makes sure they are joined with AND in their own {@link FilterElement}. + * + * @param addToRowFilterDelegate a function that adds expressions / child filter elements + * to a provided {@link RowFilter.Builder}, and expects all + * added expressions to be joined with AND operator + */ + public void addAllAsConjunction(Consumer addToRowFilterDelegate) + { + if (current.isDisjunction) + { + // If we're in disjunction mode, we must not pass the current builder to addToRowFilter. + // We create a new conjunction sub-builder instead and add all expressions there. + var builder = new Builder(needsReconciliation, indexRegistry, indexHints); + addToRowFilterDelegate.accept(builder); + + if (builder.current.expressions.size() == 1 && builder.current.children.isEmpty()) + { + // Optimization: + // if there is one expression, we can just add it directly to the current FilterElement + // making the result tree flatter + current.expressions.add(builder.current.expressions.get(0)); + } + else if (builder.current.children.size() == 1 && builder.current.expressions.isEmpty()) + { + // Optimization: + // if there is one child, we can just add it directly to the current FilterElement, + // making the result tree flatter + current.children.add(builder.current.children.get(0)); + } + else + { + // More expressions means we have to create a new child node (AND) for them. + // Also note that we use this for adding zero expressions/children as well. + // A conjunction with no restrictions means selecting everything, so if we didn't add an empty + // AND node in such case, we could end up with a filter that misses to match some rows. + current.children.add(builder.current.build()); + } + } + else + { + // Just an optimisation. If we're already in the conjunction mode, we don't need to create + // a sub-builder; we can just use this one to collect the expressions. + addToRowFilterDelegate.accept(this); + } + } + + /** + * Adds the specified simple filter expression to this builder. + * + * @param def the filtered column + * @param op the filtering operator, shouldn't be {@link Operator#ANN}. + * @param value the filtered value + * @return the added expression + */ + public SimpleExpression add(ColumnMetadata def, Operator op, ByteBuffer value) + { + assert op != Operator.ANN : "ANN expressions should be added with the addANNExpression method"; + SimpleExpression expression = new SimpleExpression(def, op, value, analyzer(def, op, value), null); + add(expression); + return expression; + } + + /** + * Adds the specified ANN expression to this builder. + * + * @param def the column for ANN ordering + * @param value the value for ANN ordering + * @param annOptions the ANN options + */ + public void addANNExpression(ColumnMetadata def, ByteBuffer value, ANNOptions annOptions) + { + add(new SimpleExpression(def, Operator.ANN, value, null, annOptions)); + } + + public void addMapComparison(ColumnMetadata def, ByteBuffer key, Operator op, ByteBuffer value) + { + add(new MapComparisonExpression(def, key, op, value)); + } + + @Nullable + private Index.Analyzer analyzer(ColumnMetadata def, Operator op, ByteBuffer value) + { + return indexRegistry == null ? null : indexRegistry.getAnalyzerFor(def, op, value, indexHints).orElse(null); + } + + public void addGeoDistanceExpression(ColumnMetadata def, ByteBuffer point, Operator op, ByteBuffer distance) + { + var primaryGeoDistanceExpression = new GeoDistanceExpression(def, point, op, distance); + // The following logic optionally adds a second search expression in the event that the query area + // crosses then antimeridian. + if (primaryGeoDistanceExpression.crossesAntimeridian()) + { + // The primry GeoDistanceExpression includes points on/over the antimeridian. Since we search + // using the lat/lon coordinates, we must create a shifted expression that will collect + // results on the other side of the antimeridian. + var shiftedGeoDistanceExpression = primaryGeoDistanceExpression.buildShiftedExpression(); + if (current.isDisjunction) + { + // We can add both expressions to this level of the tree because it is a disjunction. + add(primaryGeoDistanceExpression); + add(shiftedGeoDistanceExpression); + } + else + { + // We need to add a new level to the tree so that we can get all results that match the primary + // or the shifted expressions. + var builder = new FilterElement.Builder(true); + primaryGeoDistanceExpression.validate(); + shiftedGeoDistanceExpression.validate(); + builder.expressions.add(primaryGeoDistanceExpression); + builder.expressions.add(shiftedGeoDistanceExpression); + current.children.add(builder.build()); + } + } + else + { + add(primaryGeoDistanceExpression); + } + } + + public void addCustomIndexExpression(TableMetadata metadata, IndexMetadata targetIndex, ByteBuffer value) + { + add(CustomExpression.build(metadata, targetIndex, value)); + } + + public Builder add(Expression expression) + { + expression.validate(); + current.expressions.add(expression); + return this; + } + + public void addUserExpression(UserExpression e) + { + current.expressions.add(e); + } + } + + public static class FilterElement + { + public static final Serializer serializer = new Serializer(); + + public static final FilterElement NONE = new FilterElement(false, Collections.emptyList(), Collections.emptyList()); + + private final boolean isDisjunction; + + private final List expressions; + + private final List children; + + public FilterElement(boolean isDisjunction, List expressions, List children) + { + this.isDisjunction = isDisjunction; + this.expressions = expressions; + this.children = children; + } + + public boolean isDisjunction() + { + return isDisjunction; + } + + private boolean containsDisjunctions() + { + if (isDisjunction) + return true; + + for (FilterElement child : children) + if (child.containsDisjunctions()) + return true; + + return false; + } + + public List expressions() + { + return expressions; + } + + private List traversedExpressions() + { + List allExpressions = new ArrayList<>(expressions); + for (FilterElement child : children) + allExpressions.addAll(child.traversedExpressions()); + return allExpressions; + } + + private FilterElement withoutDisjunctions() + { + if (isDisjunction) + return NONE; + + FilterElement.Builder builder = new Builder(false); + builder.expressions.addAll(expressions); + + for (FilterElement child : children) + { + if (!child.isDisjunction) + builder.children.add(child); + } + + return builder.build(); + } + + public FilterElement filter(Predicate filter) + { + FilterElement.Builder builder = new Builder(isDisjunction); + + expressions.stream().filter(filter).forEach(builder.expressions::add); + + children.stream().map(c -> c.filter(filter)).forEach(builder.children::add); + + return builder.build(); + } + + public FilterElement filterFirstLevel(Predicate filter) + { + FilterElement.Builder builder = new Builder(isDisjunction); + expressions.stream().filter(filter).forEach(builder.expressions::add); + builder.children.addAll(children); + return builder.build(); + } + + public List children() + { + return children; + } + + public boolean isEmpty() + { + return expressions.isEmpty() && children.isEmpty(); + } + + public boolean isAlwaysTrue() + { + return !isDisjunction && isEmpty(); + } + + public boolean contains(Expression expression) + { + return expressions.contains(expression) || children.stream().anyMatch(c -> contains(expression)); + } + + public FilterElement partitionLevelTree() + { + return new FilterElement(isDisjunction, + expressions.stream() + .filter(e -> e.column.isStatic() || e.column.isPartitionKey()) + .collect(Collectors.toList()), + children.stream() + .map(FilterElement::partitionLevelTree) + .collect(Collectors.toList())); + } + + public FilterElement rowLevelTree() + { + return new FilterElement(isDisjunction, + expressions.stream() + .filter(e -> !e.column.isStatic() && !e.column.isPartitionKey()) + .collect(Collectors.toList()), + children.stream() + .map(FilterElement::rowLevelTree) + .collect(Collectors.toList())); + } + + public int size() + { + return expressions.size() + children.stream().mapToInt(FilterElement::size).sum(); + } + + public boolean isSatisfiedBy(TableMetadata table, DecoratedKey key, Row row, long nowInSec) + { + if (isEmpty()) + return true; + if (isDisjunction) + { + for (Expression e : expressions) + if (e.isSatisfiedBy(table, key, row, nowInSec)) + return true; + for (FilterElement child : children) + if (child.isSatisfiedBy(table, key, row, nowInSec)) + return true; + return false; + } + else + { + for (Expression e : expressions) + if (!e.isSatisfiedBy(table, key, row, nowInSec)) + return false; + for (FilterElement child : children) + if (!child.isSatisfiedBy(table, key, row, nowInSec)) + return false; + return true; + } + } + + /** + * Returns the number of values that this filter will filter out after applying any index analyzers. + */ + private int numFilteredValues() + { + int result = 0; + + for (Expression expression : expressions) + result += expression.numFilteredValues(); + + for (FilterElement child : children) + result += child.numFilteredValues(); + + return result; + } + + public String toString(boolean cql) + { + return toCQLString(Redaction.NONE); + } + + public String toCQLString(Redaction redaction) + { + StringBuilder sb = new StringBuilder(); + for (Expression expression : expressions) + { + if (expression.isOrderingExpression()) + continue; + if (sb.length() > 0) + sb.append(isDisjunction ? " OR " : " AND "); + sb.append(expression.toCQLString(redaction)); + } + for (FilterElement child : children) + { + if (sb.length() > 0) + sb.append(isDisjunction ? " OR " : " AND "); + sb.append('('); + sb.append(child.toCQLString(redaction)); + sb.append(')'); + } + for (Expression expression : expressions) + { + if (!expression.isOrderingExpression()) + continue; + if (sb.length() > 0) + sb.append(' '); + sb.append(expression.toCQLString(redaction)); + } + return sb.toString(); + } + + public static class Builder + { + private final boolean isDisjunction; + private final List expressions = new ArrayList<>(); + private final List children = new ArrayList<>(); + + public Builder(boolean isDisjunction) + { + this.isDisjunction = isDisjunction; + } + + public FilterElement build() + { + return new FilterElement(isDisjunction, expressions, children); + } + } + + public static class Serializer + { + public void serialize(FilterElement operation, DataOutputPlus out, int version) throws IOException + { + assert (!operation.isDisjunction && operation.children().isEmpty()) || version >= MessagingService.VERSION_DS_10 : + "Attempting to serialize a disjunct row filter to a node that doesn't support disjunction"; + + out.writeUnsignedVInt32(operation.expressions.size()); + for (Expression expr : operation.expressions) + Expression.serializer.serialize(expr, out, version); + + if (version < MessagingService.VERSION_DS_10) + return; + + out.writeBoolean(operation.isDisjunction); + out.writeUnsignedVInt32(operation.children.size()); + for (FilterElement child : operation.children) + serialize(child, out, version); + } - public Iterator iterator() - { - return expressions.iterator(); - } + public FilterElement deserialize(DataInputPlus in, int version, TableMetadata metadata, IndexHints indexHints) throws IOException + { + int size = in.readUnsignedVInt32(); + List expressions = new ArrayList<>(size); + for (int i = 0; i < size; i++) + expressions.add(Expression.serializer.deserialize(in, version, metadata, indexHints)); + + if (version < MessagingService.VERSION_DS_10) + return new FilterElement(false, expressions, Collections.emptyList()); + + boolean isDisjunction = in.readBoolean(); + size = in.readUnsignedVInt32(); + List children = new ArrayList<>(size); + for (int i = 0; i < size; i++) + children.add(deserialize(in, version, metadata, indexHints)); + return new FilterElement(isDisjunction, expressions, children); + } - @Override - public String toString() - { - return toString(false); - } + public long serializedSize(FilterElement operation, int version) + { + long size = TypeSizes.sizeofUnsignedVInt(operation.expressions.size()); + for (Expression expr : operation.expressions) + size += Expression.serializer.serializedSize(expr, version); - /** - * Returns a CQL representation of this row filter. - * - * @return a CQL representation of this row filter - */ - public String toCQLString() - { - return toString(true); - } + if (version < MessagingService.VERSION_DS_10) + return size; - private String toString(boolean cql) - { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < expressions.size(); i++) - { - if (i > 0) - sb.append(" AND "); - sb.append(expressions.get(i).toString(cql)); + size++; // isDisjunction boolean + size += TypeSizes.sizeofUnsignedVInt(operation.children.size()); + for (FilterElement child : operation.children) + size += serializedSize(child, version); + return size; + } } - return sb.toString(); } public static abstract class Expression { - private static final Serializer serializer = new Serializer(); + public static final Serializer serializer = new Serializer(); - // Note: the order of this enum matter, it's used for serialization, + // Note: the val of this enum is used for serialization, // 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_EQUALITY, UNUSED1, CUSTOM, USER } + // VECTOR + protected enum Kind + { + SIMPLE(0), MAP_COMPARISON(1), UNUSED1(2), CUSTOM(3), USER(4), VECTOR_RADIUS(100); + private final int val; + Kind(int v) { val = v; } + public int getVal() { return val; } + public static Kind fromVal(int val) + { + switch (val) + { + case 0: return SIMPLE; + case 1: return MAP_COMPARISON; + case 2: return UNUSED1; + case 3: return CUSTOM; + case 4: return USER; + case 100: return VECTOR_RADIUS; + default: throw new IllegalArgumentException("Unknown index expression kind: " + val); + } + } + } protected abstract Kind kind(); + protected final ColumnMetadata column; protected final Operator operator; protected final ByteBuffer value; @@ -514,6 +1065,36 @@ public Operator operator() return operator; } + @Nullable + public Index.Analyzer analyzer() + { + return null; + } + + public boolean isOrderingExpression() + { + return operator == Operator.ANN || operator == Operator.BM25 || operator == Operator.ORDER_BY_ASC || operator == Operator.ORDER_BY_DESC; + } + + protected boolean isSatisfiedBy(AbstractType type, ByteBuffer foundValue) + { + if (foundValue == null) + return false; + + Index.Analyzer analyzer = analyzer(); + + // Note that CQL expression are always of the form 'x < 4', i.e. the tested value is on the left. + return analyzer == null + ? operator.isSatisfiedBy(type, foundValue, value) + : operator.isSatisfiedByAnalyzed(type, analyzer.indexedTokens(foundValue), analyzer.queriedTokens()); + } + + @Nullable + public ANNOptions annOptions() + { + return null; + } + /** * Checks if the operator of this IndexExpression is a CONTAINS operator. * @@ -536,6 +1117,16 @@ public boolean isContainsKey() return Operator.CONTAINS_KEY == operator; } + /** + * Checks the operator of this {@code IndexExpression} is any of the variations of {@code [NOT] CONTAINS [KEY]}. + * + * @return {@code true} if this operator is any kind of contains operator, {@code false} otherwise. + */ + public boolean isAnyContains() + { + return operator.isAnyContains(); + } + /** * If this expression is used to query an index, the value to use as * partition key for that index query. @@ -562,8 +1153,7 @@ public void validateForIndexing() /** * Returns whether the provided row satisfied this expression or not. * - * - * @param metadata + * @param metadata the metadata of the queried table * @param partitionKey the partition key for row to check. * @param row the row to check. It should *not* contain deleted cells * (i.e. it should come from a RowIterator). @@ -571,6 +1161,14 @@ public void validateForIndexing() */ public abstract boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec); + /** + * Returns the number of values that this expression will check after applying any index analyzers. + */ + protected int numFilteredValues() + { + return 1; + } + protected ByteBuffer getValue(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { switch (column.kind) @@ -598,9 +1196,9 @@ public boolean equals(Object o) Expression that = (Expression)o; - return Objects.equal(this.kind(), that.kind()) + return this.kind() == that.kind() + && this.operator == that.operator && Objects.equal(this.column.name, that.column.name) - && Objects.equal(this.operator, that.operator) && Objects.equal(this.value, that.value); } @@ -613,7 +1211,7 @@ public int hashCode() @Override public String toString() { - return toString(false); + return toCQLString(Redaction.NONE); } /** @@ -621,18 +1219,21 @@ public String toString() * * @return a CQL representation of this expression */ - public String toCQLString() + public String toCQLString(Redaction redaction) { - return toString(true); + return ""; } - protected abstract String toString(boolean cql); + public String toCQLString(boolean redact) + { + return toCQLString(redact ? Redaction.REDACT : Redaction.NONE); + } - private static class Serializer + public static class Serializer { public void serialize(Expression expression, DataOutputPlus out, int version) throws IOException { - out.writeByte(expression.kind().ordinal()); + out.writeByte(expression.kind().getVal()); // Custom expressions include neither a column or operator, but all // other expressions do. @@ -656,18 +1257,26 @@ public void serialize(Expression expression, DataOutputPlus out, int version) th { case SIMPLE: ByteBufferUtil.writeWithShortLength(expression.value, out); + if (expression.operator == Operator.ANN) + ANNOptions.serializer.serialize(expression.annOptions(), out, version); break; - case MAP_EQUALITY: - MapEqualityExpression mexpr = (MapEqualityExpression)expression; + case MAP_COMPARISON: + MapComparisonExpression mexpr = (MapComparisonExpression)expression; ByteBufferUtil.writeWithShortLength(mexpr.key, out); ByteBufferUtil.writeWithShortLength(mexpr.value, out); break; + case VECTOR_RADIUS: + GeoDistanceExpression gexpr = (GeoDistanceExpression) expression; + gexpr.distanceOperator.writeTo(out); + ByteBufferUtil.writeWithShortLength(gexpr.distance, out); + ByteBufferUtil.writeWithShortLength(gexpr.value, out); + break; } } - public Expression deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException + public Expression deserialize(DataInputPlus in, int version, TableMetadata metadata, IndexHints indexHints) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromVal(in.readByte()); // custom expressions (3.0+ only) do not contain a column or operator, only a value if (kind == Kind.CUSTOM) @@ -683,6 +1292,7 @@ public Expression deserialize(DataInputPlus in, int version, TableMetadata metad ByteBuffer name = ByteBufferUtil.readWithShortLength(in); Operator operator = Operator.readFrom(in); ColumnMetadata column = metadata.getColumn(name); + IndexRegistry indexRegistry = IndexRegistry.obtain(metadata); // Compact storage tables, when used with thrift, used to allow falling through this withouot throwing an // exception. However, since thrift was removed in 4.0, this behaviour was not restored in CASSANDRA-16217 @@ -692,11 +1302,19 @@ public Expression deserialize(DataInputPlus in, int version, TableMetadata metad switch (kind) { case SIMPLE: - return new SimpleExpression(column, operator, ByteBufferUtil.readWithShortLength(in)); - case MAP_EQUALITY: - ByteBuffer key = ByteBufferUtil.readWithShortLength(in); ByteBuffer value = ByteBufferUtil.readWithShortLength(in); - return new MapEqualityExpression(column, key, operator, value); + ANNOptions annOptions = operator == Operator.ANN ? ANNOptions.serializer.deserialize(in, version) : null; + Index.Analyzer analyzer = indexRegistry.getAnalyzerFor(column, operator, value, indexHints).orElse(null); + return new SimpleExpression(column, operator, value, analyzer, annOptions); + case MAP_COMPARISON: + ByteBuffer key = ByteBufferUtil.readWithShortLength(in); + ByteBuffer val = ByteBufferUtil.readWithShortLength(in); + return new MapComparisonExpression(column, key, operator, val); + case VECTOR_RADIUS: + Operator boundaryOperator = Operator.readFrom(in); + ByteBuffer distance = ByteBufferUtil.readWithShortLength(in); + ByteBuffer searchVector = ByteBufferUtil.readWithShortLength(in); + return new GeoDistanceExpression(column, searchVector, boundaryOperator, distance); } throw new AssertionError(); } @@ -714,10 +1332,12 @@ public long serializedSize(Expression expression, int version) switch (expression.kind()) { case SIMPLE: - size += ByteBufferUtil.serializedSizeWithShortLength(((SimpleExpression)expression).value); + size += ByteBufferUtil.serializedSizeWithShortLength((expression).value); + if (expression.operator == Operator.ANN) + size += ANNOptions.serializer.serializedSize(expression.annOptions(), version); break; - case MAP_EQUALITY: - MapEqualityExpression mexpr = (MapEqualityExpression)expression; + case MAP_COMPARISON: + MapComparisonExpression mexpr = (MapComparisonExpression)expression; size += ByteBufferUtil.serializedSizeWithShortLength(mexpr.key) + ByteBufferUtil.serializedSizeWithShortLength(mexpr.value); break; @@ -728,6 +1348,12 @@ public long serializedSize(Expression expression, int version) case USER: size += UserExpression.serializedSize((UserExpression)expression, version); break; + case VECTOR_RADIUS: + GeoDistanceExpression geoDistanceRelation = (GeoDistanceExpression) expression; + size += ByteBufferUtil.serializedSizeWithShortLength(geoDistanceRelation.distance) + + ByteBufferUtil.serializedSizeWithShortLength(geoDistanceRelation.value) + + geoDistanceRelation.distanceOperator.serializedSize(); + break; } return size; } @@ -739,11 +1365,45 @@ public long serializedSize(Expression expression, int version) */ public static class SimpleExpression extends Expression { - SimpleExpression(ColumnMetadata column, Operator operator, ByteBuffer value) + @Nullable + protected final Index.Analyzer analyzer; + + @Nullable + private final ANNOptions annOptions; + + public SimpleExpression(ColumnMetadata column, + Operator operator, + ByteBuffer value, + @Nullable Index.Analyzer analyzer, + @Nullable ANNOptions annOptions) { super(column, operator, value); + this.analyzer = analyzer; + this.annOptions = annOptions; + } + + @Override + @Nullable + public Index.Analyzer analyzer() + { + return analyzer; + } + + @Override + public int numFilteredValues() + { + return analyzer == null + ? super.numFilteredValues() + : analyzer.queriedTokens().size(); + } + + @Nullable + public ANNOptions annOptions() + { + return annOptions; } + @Override public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { // We support null conditions for LWT (in ColumnCondition) but not for RowFilter. @@ -754,6 +1414,7 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, { case EQ: case IN: + case NOT_IN: case LT: case LTE: case GTE: @@ -761,22 +1422,21 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, { assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for collection types"; + ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); + + if (foundValue == null) + return false; + // In order to support operators on Counter types, their value has to be extracted from internal // representation. See CASSANDRA-11629 if (column.type.isCounter()) { - ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); - if (foundValue == null) - return false; - ByteBuffer counterValue = LongType.instance.decompose(CounterContext.instance().total(foundValue, ByteBufferAccessor.instance)); - return operator.isSatisfiedBy(LongType.instance, counterValue, value); + return isSatisfiedBy(LongType.instance, counterValue); } else { - // Note that CQL expression are always of the form 'x < 4', i.e. the tested value is on the left. - ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); - return foundValue != null && operator.isSatisfiedBy(column.type, foundValue, value); + return isSatisfiedBy(column.type, foundValue); } } case NEQ: @@ -784,97 +1444,148 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, case LIKE_SUFFIX: case LIKE_CONTAINS: case LIKE_MATCHES: + case ANALYZER_MATCHES: case ANN: + case BM25: { assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for collection types"; ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); // Note that CQL expression are always of the form 'x < 4', i.e. the tested value is on the left. - return foundValue != null && operator.isSatisfiedBy(column.type, foundValue, value); + return isSatisfiedBy(column.type, foundValue); } case CONTAINS: - assert column.type.isCollection(); - CollectionType type = (CollectionType)column.type; - if (column.isComplex()) + return contains(metadata, partitionKey, row, nowInSec); + case CONTAINS_KEY: + return containsKey(metadata, partitionKey, row, nowInSec); + case NOT_CONTAINS: + return !contains(metadata, partitionKey, row, nowInSec); + case NOT_CONTAINS_KEY: + return !containsKey(metadata, partitionKey, row, nowInSec); + } + throw new AssertionError("Unsupported operator: " + operator); + } + + private boolean contains(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) + { + assert column.type.isCollection(); + + CollectionType type = (CollectionType) column.type; + + if (column.isComplex()) + { + ComplexColumnData complexData = row.getComplexColumnData(column); + if (complexData != null) + { + AbstractType elementType = type.kind == CollectionType.Kind.SET ? type.nameComparator() : type.valueComparator(); + for (Cell cell : complexData) { - ComplexColumnData complexData = row.getComplexColumnData(column); - if (complexData != null) + ByteBuffer elementValue = type.kind == CollectionType.Kind.SET ? cell.path().get(0) : cell.buffer(); + if (analyzer != null) { - for (Cell cell : complexData) - { - if (type.kind == CollectionType.Kind.SET) - { - if (type.nameComparator().compare(cell.path().get(0), value) == 0) - return true; - } - else - { - if (type.valueComparator().compare(cell.buffer(), value) == 0) - return true; - } - } + List elementTokens = analyzer.indexedTokens(elementValue); + List queriedTokens = analyzer.queriedTokens(); + if (Operator.ANALYZER_MATCHES.isSatisfiedByAnalyzed(elementType, elementTokens, queriedTokens)) + return true; } - return false; - } - else - { - ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); - if (foundValue == null) - return false; - - switch (type.kind) + else { - case LIST: - ListType listType = (ListType)type; - return listType.compose(foundValue).contains(listType.getElementsType().compose(value)); - case SET: - SetType setType = (SetType)type; - return setType.compose(foundValue).contains(setType.getElementsType().compose(value)); - case MAP: - MapType mapType = (MapType)type; - return mapType.compose(foundValue).containsValue(mapType.getValuesType().compose(value)); + if (Operator.EQ.isSatisfiedBy(elementType, elementValue, value)) + return true; } - throw new AssertionError(); - } - case CONTAINS_KEY: - assert column.type.isCollection() && column.type instanceof MapType; - MapType mapType = (MapType)column.type; - if (column.isComplex()) - { - return row.getCell(column, CellPath.create(value)) != null; } - else + } + return false; + } + else + { + assert analyzer == null : "Analyzers are not supported on frozen collections"; + ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); + return foundValue != null && Operator.CONTAINS.isSatisfiedBy(type, foundValue, value); + } + } + + private boolean containsKey(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) + { + assert column.type.isCollection() && column.type instanceof MapType; + MapType mapType = (MapType) column.type; + if (column.isComplex()) + { + if (analyzer != null) + { + for (Cell cell : row.getComplexColumnData(column)) { - ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); - return foundValue != null && mapType.getSerializer().getSerializedValue(foundValue, value, mapType.getKeysType()) != null; + AbstractType elementType = mapType.nameComparator(); + ByteBuffer elementValue = cell.path().get(0); + List elementTokens = analyzer.indexedTokens(elementValue); + List queriedTokens = analyzer.queriedTokens(); + if (Operator.ANALYZER_MATCHES.isSatisfiedByAnalyzed(elementType, elementTokens, queriedTokens)) + return true; } + return false; + } + return row.getCell(column, CellPath.create(value)) != null; + } + else + { + assert analyzer == null : "Analyzers are not supported on frozen collections"; + ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); + return foundValue != null && Operator.CONTAINS_KEY.isSatisfiedBy(mapType, foundValue, value); } - throw new AssertionError(); } @Override - protected String toString(boolean cql) + public String toCQLString(Redaction redaction) { AbstractType type = column.type; switch (operator) { case CONTAINS: + case NOT_CONTAINS: assert type instanceof CollectionType; CollectionType ct = (CollectionType)type; type = ct.kind == CollectionType.Kind.SET ? ct.nameComparator() : ct.valueComparator(); break; case CONTAINS_KEY: + case NOT_CONTAINS_KEY: assert type instanceof MapType; type = ((MapType)type).nameComparator(); break; case IN: - type = ListType.getInstance(type, false); + case NOT_IN: + type = ListType.getInstance(type.freeze(), false); break; + case ORDER_BY_ASC: + case ORDER_BY_DESC: + // These don't have a value, so we return here to prevent an error calling type.getString(value) + return String.format("ORDER BY %s %s", column.name.toCQLString(), operator); + case ANN: + return String.format("ORDER BY %s ANN OF %s", column.name.toCQLString(), truncateValue(type.toCQLString(value, redaction))); + case LIKE_PREFIX: + return likeToCQLString("'%s%%'", type, redaction); + case LIKE_SUFFIX: + return likeToCQLString("'%%%s'", type, redaction); + case LIKE_CONTAINS: + return likeToCQLString("'%%%s%%'", type, redaction); + case LIKE_MATCHES: + return likeToCQLString("'%s'", type, redaction); default: break; } - return cql - ? String.format("%s %s %s", column.name.toCQLString(), operator, type.toCQLString(value) ) - : String.format("%s %s %s", column.name.toString(), operator, type.getString(value)); + return String.format("%s %s %s", column.name.toCQLString(), operator, truncateValue(type.toCQLString(value, redaction))); + } + + private String likeToCQLString(String pattern, AbstractType type, Redaction redaction) + { + if (redaction == Redaction.REDACT) + return String.format("%s LIKE ?", column.name.toCQLString()); + + String stringValue = String.format(pattern, type.getString(value)); + return String.format("%s LIKE %s", column.name.toCQLString(), truncateValue(stringValue)); + } + + private static String truncateValue(String value) + { + return value.length() > 9 ? value.substring(0, 6) + "..." : value; } @Override @@ -885,17 +1596,22 @@ protected Kind kind() } /** - * An expression of the form 'column' ['key'] = 'value' (which is only - * supported when 'column' is a map). + * An expression of the form 'column' ['key'] OPERATOR 'value' (which is only + * supported when 'column' is a map) and where the operator can be {@link Operator#EQ}, {@link Operator#NEQ}, + * {@link Operator#LT}, {@link Operator#LTE}, {@link Operator#GT}, or {@link Operator#GTE}. */ - private static class MapEqualityExpression extends Expression + public static class MapComparisonExpression extends Expression { private final ByteBuffer key; + private ByteBuffer indexValue = null; - public MapEqualityExpression(ColumnMetadata column, ByteBuffer key, Operator operator, ByteBuffer value) + public MapComparisonExpression(ColumnMetadata column, + ByteBuffer key, + Operator operator, + ByteBuffer value) { super(column, operator, value); - assert column.type instanceof MapType && operator == Operator.EQ; + assert column.type instanceof MapType && (operator == Operator.EQ || operator == Operator.NEQ || operator.isSlice()); this.key = key; } @@ -911,11 +1627,30 @@ public void validate() throws InvalidRequestException @Override public ByteBuffer getIndexValue() { - return CompositeType.build(ByteBufferAccessor.instance, key, value); + if (indexValue == null) + indexValue = CompositeType.build(ByteBufferAccessor.instance, key, value); + return indexValue; } + /** + * Returns whether the provided row satisfies this expression. For equality, it validates that the row contains + * the exact key/value pair. For inequalities, it validates that the row contains the key, then that the value + * satisfies the inequality. + * + * @param metadata the metadata of the queried table + * @param partitionKey the partition key for row to check. + * @param row the row to check. It should *not* contain deleted cells + * @param nowInSec the current time in seconds (to know what is live and what isn't). + * (i.e. it should come from a RowIterator). + * @return whether the row is satisfied by this expression. + */ @Override public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) + { + return isSatisfiedByEq(metadata, partitionKey, row, nowInSec) ^ (operator == Operator.NEQ); + } + + private boolean isSatisfiedByEq(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) { assert key != null; // We support null conditions for LWT (in ColumnCondition) but not for RowFilter. @@ -925,11 +1660,14 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, if (row.isStatic() != column.isStatic()) return true; + int comp; MapType mt = (MapType)column.type; if (column.isComplex()) { Cell cell = row.getCell(column, CellPath.create(key)); - return cell != null && mt.valueComparator().compare(cell.buffer(), value) == 0; + if (cell == null) + return false; + comp = mt.valueComparator().compare(cell.buffer(), value); } else { @@ -938,19 +1676,36 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, return false; ByteBuffer foundValue = mt.getSerializer().getSerializedValue(serializedMap, key, mt.getKeysType()); - return foundValue != null && mt.valueComparator().compare(foundValue, value) == 0; + if (foundValue == null) + return false; + comp = mt.valueComparator().compare(foundValue, value); + } + switch (operator) { + case EQ: + case NEQ: // NEQ is inverted in calling method. We do this to simplify handling of null cells. + return comp == 0; + case LT: + return comp < 0; + case LTE: + return comp <= 0; + case GT: + return comp > 0; + case GTE: + return comp >= 0; + default: + throw new AssertionError("Unsupported operator: " + operator); } } @Override - protected String toString(boolean cql) + public String toCQLString(Redaction redaction) { MapType mt = (MapType) column.type; - AbstractType nt = mt.nameComparator(); - AbstractType vt = mt.valueComparator(); - return cql - ? String.format("%s[%s] = %s", column.name.toCQLString(), nt.toCQLString(key), vt.toCQLString(value)) - : String.format("%s[%s] = %s", column.name.toString(), nt.getString(key), vt.getString(value)); + return String.format("%s[%s] %s %s", + column.name.toCQLString(), + mt.nameComparator().toCQLString(key, redaction), + operator, + mt.valueComparator().toCQLString(value, redaction)); } @Override @@ -959,13 +1714,13 @@ public boolean equals(Object o) if (this == o) return true; - if (!(o instanceof MapEqualityExpression)) + if (!(o instanceof MapComparisonExpression)) return false; - MapEqualityExpression that = (MapEqualityExpression)o; + MapComparisonExpression that = (MapComparisonExpression)o; return Objects.equal(this.column.name, that.column.name) - && Objects.equal(this.operator, that.operator) + && this.operator == that.operator && Objects.equal(this.key, that.key) && Objects.equal(this.value, that.value); } @@ -979,7 +1734,180 @@ public int hashCode() @Override protected Kind kind() { - return Kind.MAP_EQUALITY; + return Kind.MAP_COMPARISON; + } + + /** + * Get the lower bound for this expression. When the expression is EQ, GT, or GTE, the lower bound is the + * expression itself. When the expression is LT or LTE, the lower bound is the map's key becuase + * {@link ByteBuffer} comparisons will work correctly. + * @return the lower bound for this expression. + */ + public ByteBuffer getLowerBound() + { + switch (operator) { + case EQ: + case GT: + case GTE: + return this.getIndexValue(); + case LT: + case LTE: + return CompositeType.extractFirstComponentAsTrieSearchPrefix(getIndexValue(), true); + default: + throw new AssertionError("Unsupported operator: " + operator); + } + } + + /** + * Get the upper bound for this expression. When the expression is EQ, LT, or LTE, the upper bound is the + * expression itself. When the expression is GT or GTE, the upper bound is the map's key with the last byte + * set to 1 so that {@link ByteBuffer} comparisons will work correctly. + * @return the upper bound for this express + */ + public ByteBuffer getUpperBound() + { + switch (operator) { + case GT: + case GTE: + return CompositeType.extractFirstComponentAsTrieSearchPrefix(getIndexValue(), false); + case EQ: + case LT: + case LTE: + return this.getIndexValue(); + default: + throw new AssertionError("Unsupported operator: " + operator); + } + } + } + + public static class GeoDistanceExpression extends Expression + { + private final ByteBuffer distance; + private final Operator distanceOperator; + private final float searchRadiusMeters; + private final float searchLat; + private final float searchLon; + // Whether this is a shifted expression, which is used to handle crossing the antimeridian + private final boolean isShifted; + + public GeoDistanceExpression(ColumnMetadata column, ByteBuffer point, Operator operator, ByteBuffer distance) + { + this(column, point, operator, distance, false); + } + + private GeoDistanceExpression(ColumnMetadata column, ByteBuffer point, Operator operator, ByteBuffer distance, boolean isShifted) + { + super(column, Operator.BOUNDED_ANN, point); + assert column.type instanceof VectorType && (operator == Operator.LTE || operator == Operator.LT); + this.isShifted = isShifted; + this.distanceOperator = operator; + this.distance = distance; + searchRadiusMeters = FloatType.instance.compose(distance); + float[] pointVector = TypeUtil.decomposeVector(column.type, point); + // This is validated earlier in the parser because the column requires size 2, so only assert on it + assert pointVector.length == 2 : "GEO_DISTANCE requires search vector to have 2 dimensions."; + searchLat = pointVector[0]; + searchLon = pointVector[1]; + } + + public boolean crossesAntimeridian() + { + return GeoUtil.crossesAntimeridian(searchLat, searchLon, searchRadiusMeters); + } + + /** + * @return a new {@link GeoDistanceExpression} that is shifted by 360 degrees and can correctly search + * on the opposite side of the antimeridian. + */ + public GeoDistanceExpression buildShiftedExpression() + { + float shiftedLon = searchLon > 0 ? searchLon - 360 : searchLon + 360; + var newPoint = VectorType.getInstance(FloatType.instance, 2) + .decompose(List.of(searchLat, shiftedLon)); + return new GeoDistanceExpression(column, newPoint, distanceOperator, distance, true); + } + + public Operator getDistanceOperator() + { + return distanceOperator; + } + + public ByteBuffer getDistance() + { + return distance; + } + + @Override + public void validate() throws InvalidRequestException + { + checkBindValueSet(distance, "Unsupported unset distance for column %s", column.name); + checkBindValueSet(value, "Unsupported unset vector value for column %s", column.name); + + if (searchRadiusMeters <= 0) + throw new InvalidRequestException("GEO_DISTANCE radius must be positive, got " + searchRadiusMeters); + + if (searchLat < -90 || searchLat > 90) + throw new InvalidRequestException("GEO_DISTANCE latitude must be between -90 and 90 degrees, got " + searchLat); + if (!isShifted && (searchLon < -180 || searchLon > 180)) + throw new InvalidRequestException("GEO_DISTANCE longitude must be between -180 and 180 degrees, got " + searchLon); + } + + @Override + public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, Row row, long nowInSec) + { + ByteBuffer foundValue = getValue(metadata, partitionKey, row, nowInSec); + if (foundValue == null) + return false; + float[] foundVector = TypeUtil.decomposeVector(column.type, foundValue); + double haversineDistance = SloppyMath.haversinMeters(foundVector[0], foundVector[1], searchLat, searchLon); + switch (distanceOperator) + { + case LTE: + return haversineDistance <= searchRadiusMeters; + case LT: + return haversineDistance < searchRadiusMeters; + default: + throw new AssertionError("Unsupported operator: " + operator); + } + } + + @Override + public String toCQLString(Redaction redaction) + { + return String.format("GEO_DISTANCE(%s, %s) %s %s", + column.name.toCQLString(), + column.type.toCQLString(value, redaction), + distanceOperator, + FloatType.instance.toCQLString(distance, redaction)); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof GeoDistanceExpression)) + return false; + + GeoDistanceExpression that = (GeoDistanceExpression)o; + + return Objects.equal(this.column.name, that.column.name) + && this.distanceOperator == that.distanceOperator + && Objects.equal(this.distance, that.distance) + && Objects.equal(this.value, that.value); + } + + @Override + public int hashCode() + { + return Objects.hashCode(column.name, distanceOperator, value, distance); + } + + @Override + protected Kind kind() + { + return Kind.VECTOR_RADIUS; } } @@ -987,7 +1915,7 @@ protected Kind kind() * A custom index expression for use with 2i implementations which support custom syntax and which are not * necessarily linked to a single column in the base table. */ - public static final class CustomExpression extends Expression + public static class CustomExpression extends Expression { private final IndexMetadata targetIndex; private final TableMetadata table; @@ -1000,6 +1928,12 @@ public CustomExpression(TableMetadata table, IndexMetadata targetIndex, ByteBuff this.table = table; } + public static CustomExpression build(TableMetadata metadata, IndexMetadata targetIndex, ByteBuffer value) + { + // delegate the expression creation to the target custom index + return Keyspace.openAndGetStore(metadata).indexManager.getIndex(targetIndex).customExpressionFor(metadata, value); + } + private static ColumnMetadata makeDefinition(TableMetadata table, IndexMetadata index) { // Similarly to how we handle non-defined columns in thift, we create a fake column definition to @@ -1018,16 +1952,17 @@ public ByteBuffer getValue() } @Override - protected String toString(boolean cql) + public String toCQLString(Redaction redaction) { return String.format("expr(%s, %s)", - cql ? ColumnIdentifier.maybeQuote(targetIndex.name) : targetIndex.name, + ColumnIdentifier.maybeQuote(targetIndex.name), Keyspace.openAndGetStore(table) .indexManager .getIndex(targetIndex) .customExpressionValueType()); } + @Override protected Kind kind() { return Kind.CUSTOM; @@ -1053,7 +1988,7 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, * is important, new types should registered last and obsoleted types should still be registered ( * or dummy implementations registered in their place) to preserve consistent identifiers across * the cluster). - * + *

    * During serialization, the identifier for the Deserializer implementation is prepended to the * implementation specific payload. To deserialize, the identifier is read first to obtain the * Deserializer, which then provides the concrete expression instance. @@ -1061,6 +1996,12 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, public static abstract class UserExpression extends Expression { private static final DeserializerRegistry deserializers = new DeserializerRegistry(); + + @Override + public String toCQLString(Redaction redaction) + { + return toCQLString(redaction == Redaction.REDACT); + } private static final class DeserializerRegistry { private final AtomicInteger counter = new AtomicInteger(0); @@ -1088,7 +2029,7 @@ public Deserializer getDeserializer(int id) } } - protected static abstract class Deserializer + public static abstract class Deserializer { protected abstract UserExpression deserialize(DataInputPlus in, int version, @@ -1126,6 +2067,7 @@ protected UserExpression(ColumnMetadata column, Operator operator, ByteBuffer va super(column, operator, value); } + @Override protected Kind kind() { return Kind.USER; @@ -1140,30 +2082,24 @@ public static class Serializer public void serialize(RowFilter filter, DataOutputPlus out, int version) throws IOException { out.writeBoolean(false); // Old "is for thrift" boolean - out.writeUnsignedVInt32(filter.expressions.size()); - for (Expression expr : filter.expressions) - Expression.serializer.serialize(expr, out, version); + IndexHints.serializer.serialize(filter.indexHints, out, version); // hints first because the expressions might need them + FilterElement.serializer.serialize(filter.root, out, version); } public RowFilter deserialize(DataInputPlus in, int version, TableMetadata metadata, boolean needsReconciliation) throws IOException { in.readBoolean(); // Unused - int size = in.readUnsignedVInt32(); - List expressions = new ArrayList<>(size); - for (int i = 0; i < size; i++) - expressions.add(Expression.serializer.deserialize(in, version, metadata)); - - return new RowFilter(expressions, needsReconciliation); + IndexHints indexHints = IndexHints.serializer.deserialize(in, version, metadata); + FilterElement operation = FilterElement.serializer.deserialize(in, version, metadata, indexHints); + return new RowFilter(operation, needsReconciliation, indexHints); } public long serializedSize(RowFilter filter, int version) { - long size = 1 // unused boolean - + TypeSizes.sizeofUnsignedVInt(filter.expressions.size()); - for (Expression expr : filter.expressions) - size += Expression.serializer.serializedSize(expr, version); - return size; + return 1 // unused boolean + + IndexHints.serializer.serializedSize(filter.indexHints, version) + + FilterElement.serializer.serializedSize(filter.root, version); } } } diff --git a/src/java/org/apache/cassandra/db/filter/TombstoneOverwhelmingException.java b/src/java/org/apache/cassandra/db/filter/TombstoneOverwhelmingException.java index efca3ac4db44..c8a88f19bafe 100644 --- a/src/java/org/apache/cassandra/db/filter/TombstoneOverwhelmingException.java +++ b/src/java/org/apache/cassandra/db/filter/TombstoneOverwhelmingException.java @@ -20,18 +20,29 @@ import java.nio.ByteBuffer; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.RejectException; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.exceptions.InternalRequestExecutionException; +import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.marshal.*; -public class TombstoneOverwhelmingException extends RejectException +public class TombstoneOverwhelmingException extends RejectException implements InternalRequestExecutionException { - public TombstoneOverwhelmingException(int numTombstones, String query, TableMetadata metadata, DecoratedKey lastPartitionKey, ClusteringPrefix lastClustering) + public TombstoneOverwhelmingException(long numTombstones, String query, TableMetadata metadata, DecoratedKey lastPartitionKey, ClusteringPrefix lastClustering) { super(String.format("Scanned over %d tombstones during query '%s' (last scanned row token was %s and partion key was (%s)); query aborted", numTombstones, query, lastPartitionKey.getToken(), makePKString(metadata, lastPartitionKey.getKey(), lastClustering))); } + @Override + public RequestFailureReason getReason() + { + return RequestFailureReason.READ_TOO_MANY_TOMBSTONES; + } + private static String makePKString(TableMetadata metadata, ByteBuffer partitionKey, ClusteringPrefix clustering) { StringBuilder sb = new StringBuilder(); @@ -49,7 +60,7 @@ private static String makePKString(TableMetadata metadata, ByteBuffer partitionK { if (i > 0) sb.append(", "); - sb.append(ct.types.get(i).getString(values[i])); + sb.append(ct.subTypes.get(i).getString(values[i])); } } else diff --git a/src/java/org/apache/cassandra/db/guardrails/CustomUserKeyspaceFilterProvider.java b/src/java/org/apache/cassandra/db/guardrails/CustomUserKeyspaceFilterProvider.java new file mode 100644 index 000000000000..c76e7e2b774b --- /dev/null +++ b/src/java/org/apache/cassandra/db/guardrails/CustomUserKeyspaceFilterProvider.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.guardrails; + +public class CustomUserKeyspaceFilterProvider +{ + public static UserKeyspaceFilterProvider make(String customImpl) + { + try + { + return (UserKeyspaceFilterProvider) Class.forName(customImpl).getDeclaredConstructor().newInstance(); + } + catch (Throwable ex) + { + throw new IllegalStateException("Unknown user keyspace filter provider: " + customImpl, ex); + } + } +} diff --git a/src/java/org/apache/cassandra/db/guardrails/DefaultUserKeyspaceFilter.java b/src/java/org/apache/cassandra/db/guardrails/DefaultUserKeyspaceFilter.java new file mode 100644 index 000000000000..0a0c453ff826 --- /dev/null +++ b/src/java/org/apache/cassandra/db/guardrails/DefaultUserKeyspaceFilter.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.guardrails; + +import org.apache.cassandra.db.Keyspace; + +/** + * A default implementation of the UserKeyspaceFilter, which just includes all keyspaces automatically. + * This is done so that "max table count" guardrails will still work in C* databases that don't have + * keyspaces prefixed by tenant values. + */ +public class DefaultUserKeyspaceFilter implements UserKeyspaceFilter +{ + public boolean filter(Keyspace keyspace) + { + return true; + } +} diff --git a/src/java/org/apache/cassandra/db/guardrails/DefaultUserKeyspaceFilterProvider.java b/src/java/org/apache/cassandra/db/guardrails/DefaultUserKeyspaceFilterProvider.java new file mode 100644 index 000000000000..9642728a1796 --- /dev/null +++ b/src/java/org/apache/cassandra/db/guardrails/DefaultUserKeyspaceFilterProvider.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.guardrails; + +import org.apache.cassandra.service.ClientState; + +public class DefaultUserKeyspaceFilterProvider implements UserKeyspaceFilterProvider +{ + @Override + public UserKeyspaceFilter get(ClientState clientState) + { + return new DefaultUserKeyspaceFilter(); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrail.java b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java index fbd1a5b8803e..a603c5f554a4 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Guardrail.java +++ b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java @@ -162,19 +162,24 @@ String decorateMessage(String message) * default 0 means always log and trigger listeners. * @return current guardrail */ - Guardrail minNotifyIntervalInMs(long minNotifyIntervalInMs) + public Guardrail minNotifyIntervalInMs(long minNotifyIntervalInMs) { assert minNotifyIntervalInMs >= 0; this.minNotifyIntervalInMs = minNotifyIntervalInMs; return this; } + + public long minNotifyIntervalInMs() + { + return minNotifyIntervalInMs; + } /** * reset last notify time to make sure it will notify downstream when {@link this#warn(String, String)} * or {@link this#fail(String, ClientState)} is called next time. */ @VisibleForTesting - void resetLastNotifyTime() + public void resetLastNotifyTime() { lastFailInMs = 0; lastWarnInMs = 0; diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java index 829dab056237..7413e1a0fff5 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java +++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java @@ -108,6 +108,53 @@ public final class Guardrails implements GuardrailsMBean : format("Tables cannot have more than %s secondary indexes, aborting the creation of secondary index %s", threshold, what)); + public static final MaxThreshold sasiIndexesPerTable = + new MaxThreshold("sasi_indexes_per_table", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getSasiIndexesPerTableWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getSasiIndexesPerTableFailThreshold(), + (isWarning, what, value, threshold) -> + isWarning ? format("Creating SASI index %s, current number of indexes %s exceeds warning threshold of %s.", + what, value, threshold) + : format("Tables cannot have more than %s SASI indexes, aborting the creation of secondary index %s", + threshold, what)); + + public static final MaxThreshold saiIndexesPerTable = + new MaxThreshold("sai_indexes_per_table", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getStorageAttachedIndexesPerTableWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getStorageAttachedIndexesPerTableFailThreshold(), + (isWarning, what, value, threshold) -> + isWarning ? format("Creating StorageAttachedIndex secondary index %s, current number of StorageAttachedIndex secondary indexes %s exceeds warning threshold of %s.", + what, value, threshold) + : format("Tables cannot have more than %s StorageAttachedIndex secondary indexes, aborting the creation of secondary index %s", + threshold, what)); + + public static final MaxThreshold saiIndexesTotal = + new MaxThreshold("sai_indexes_total", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getStorageAttachedIndexesTotalWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getStorageAttachedIndexesTotalFailThreshold(), + (isWarning, what, value, threshold) -> + isWarning ? format("Creating StorageAttachedIndex secondary index %s, current number of StorageAttachedIndex secondary indexes across all keyspaces %s exceeds warning threshold of %s.", + what, value, threshold) + : format("Cannot have more than %s StorageAttachedIndex secondary indexes across all keyspaces, aborting the creation of secondary index %s", + threshold, what)); + + /** + * Guardrail on the number of trusted custom secondary indexes per table, counted per implementation class. + */ + public static final MaxThreshold trustedIndexesPerTable = + new MaxThreshold("trusted_indexes_per_table", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getTrustedIndexesPerTableWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getTrustedIndexesPerTableFailThreshold(), + (isWarning, what, value, threshold) -> + isWarning ? format("Creating trusted custom secondary index %s, current number of indexes of the same class %s exceeds warning threshold of %s.", + what, value, threshold) + : format("Tables cannot have more than %s trusted custom secondary indexes of the same class, aborting the creation of secondary index %s", + threshold, what)); + /** * Guardrail disabling user's ability to create secondary indexes */ @@ -151,6 +198,12 @@ public final class Guardrails implements GuardrailsMBean state -> CONFIG_PROVIDER.getOrCreate(state).getUserTimestampsEnabled(), "User provided timestamps (USING TIMESTAMP)"); + public static final EnableFlag loggedBatchEnabled = + new EnableFlag("logged_batch", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getLoggedBatchEnabled(), + "LOGGED batch"); + public static final EnableFlag groupByEnabled = new EnableFlag("group_by", null, @@ -245,6 +298,20 @@ public final class Guardrails implements GuardrailsMBean : format("Aborting query for table %s, page size %s exceeds fail threshold of %s.", what, value, threshold)); + /** + * Guardrail on the weight (bytes) of elements returned within page. + */ + public static final MaxThreshold pageWeight = + new MaxThreshold("page_weight", + null, + state -> sizeToBytes(CONFIG_PROVIDER.getOrCreate(state).getPageWeightWarnThreshold()), + state -> sizeToBytes(CONFIG_PROVIDER.getOrCreate(state).getPageWeightFailThreshold()), + (isWarning, what, value, threshold) -> + isWarning ? format("Query for table %s with page weight %s bytes exceeds warning threshold of %s bytes.", + what, value, threshold) + : format("Aborting query for table %s, page weight %s bytes exceeds fail threshold of %s bytes.", + what, value, threshold)); + /** * Guardrail on the number of partition keys in the IN clause. */ @@ -288,6 +355,15 @@ public final class Guardrails implements GuardrailsMBean state -> CONFIG_PROVIDER.getOrCreate(state).getSimpleStrategyEnabled(), "SimpleStrategy"); + /** + * Guardrail disabling use of Counters + */ + public static final EnableFlag counterEnabled = + new EnableFlag("counter", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getCounterEnabled(), + "Counter"); + /** * Guardrail on the number of restrictions created by a cartesian product of a CQL's {@code IN} query. */ @@ -425,6 +501,18 @@ public final class Guardrails implements GuardrailsMBean format("%s has a vector of %s dimensions, this exceeds the %s threshold of %s.", what, value, isWarning ? "warning" : "failure", threshold)); + /** + * Guardrail on the maximum value for the rerank_k parameter, an ANN query option. + */ + public static final MaxThreshold annRerankKMaxValue = + new MaxThreshold("sai_ann_rerank_k_max_value", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getSaiAnnRerankKWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getSaiAnnRerankKFailThreshold(), + (isWarning, what, value, threshold) -> + format("%s specifies rerank_k=%s, this exceeds the %s threshold of %s.", + what, value, isWarning ? "warning" : "failure", threshold)); + /** * Guardrail on the data disk usage on the local node, used by a periodic task to calculate and propagate that status. * See {@link org.apache.cassandra.service.disk.usage.DiskUsageMonitor} and {@link DiskUsageBroadcaster}. @@ -461,6 +549,8 @@ public final class Guardrails implements GuardrailsMBean long minNotifyInterval = CassandraRelevantProperties.DISK_USAGE_NOTIFY_INTERVAL_MS.getLong(); localDataDiskUsage.minNotifyIntervalInMs(minNotifyInterval); replicaDiskUsage.minNotifyIntervalInMs(minNotifyInterval); + collectionSize.minNotifyIntervalInMs(minNotifyInterval); + itemsPerCollection.minNotifyIntervalInMs(minNotifyInterval); } /** @@ -555,6 +645,58 @@ public final class Guardrails implements GuardrailsMBean "Executing a query on secondary indexes without partition key restriction might degrade performance", state -> CONFIG_PROVIDER.getOrCreate(state).getNonPartitionRestrictedQueryEnabled(), "Non-partition key restricted query"); + public static final Threshold scannedTombstones = + new MaxThreshold("scanned_tombstones", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getTombstoneWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getTombstoneFailThreshold(), + (isWarning, what, v, t) -> isWarning ? + format("Scanned over %s tombstone rows for query %1.512s - more than the warning threshold %s", v, what, t) : + format("Scanned over %s tombstone rows during query %1.512s - more than the maximum allowed %s; query aborted", v, what, t)); + + + public static final Threshold batchSize = + new MaxThreshold("batch_size", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getBatchSizeWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getBatchSizeFailThreshold(), + (isWarning, what, v, t) -> isWarning + ? format("Batch for %s is of size %s, exceeding specified warning threshold %s", what, v, t) + : format("Batch for %s is of size %s, exceeding specified failure threshold %s", what, v, t)); + + public static final Threshold unloggedBatchAcrossPartitions = + new MaxThreshold("unlogged_batch_across_partitions", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getUnloggedBatchAcrossPartitionsWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getUnloggedBatchAcrossPartitionsFailThreshold(), + (x, what, v, t) -> format("Unlogged batch covering %s partitions detected " + + "against table%s %s. You should use a logged batch for " + + "atomicity, or asynchronous writes for performance.", + v, what.contains(", ") ? "s" : "", what)); + + /** + * Guardrail on the number of rows that a SELECT query with LIMIT/OFFSET can skip. + */ + public static final Threshold offsetRows = + new MaxThreshold("offset_rows", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getOffsetRowsWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getOffsetRowsFailThreshold(), + (isWarning, what, v, t) -> isWarning + ? format("%s requested to skip %s rows, this exceeds the warning threshold of %s.", what, v, t) + : format("%s requested to skip %s rows, this exceeds the failure threshold of %s.", what, v, t)); + + /** + * Guardrail on the number of query filtering operations per SELECT query (after analysis). + */ + public static final Threshold queryFilters = + new MaxThreshold("query_filters", + null, + state -> CONFIG_PROVIDER.getOrCreate(state).getQueryFiltersWarnThreshold(), + state -> CONFIG_PROVIDER.getOrCreate(state).getQueryFiltersFailThreshold(), + (isWarning, what, v, t) -> isWarning + ? format("%s has %s column value filters after analysis, this exceeds the warning threshold of %s.", what, v, t) + : format("%s has %s column value filters after analysis, this exceeds the failure threshold of %s.", what, v, t)); private Guardrails() { @@ -639,6 +781,60 @@ public boolean getSecondaryIndexesEnabled() return DEFAULT_CONFIG.getSecondaryIndexesEnabled(); } + @Override + public int getStorageAttachedIndexesPerTableWarnThreshold() + { + return DEFAULT_CONFIG.getStorageAttachedIndexesPerTableWarnThreshold(); + } + + @Override + public int getStorageAttachedIndexesPerTableFailThreshold() + { + return DEFAULT_CONFIG.getStorageAttachedIndexesPerTableFailThreshold(); + } + + @Override + public void setStorageAttachedIndexesPerTableThreshold(int warn, int fail) + { + DEFAULT_CONFIG.setStorageAttachedIndexesPerTableThreshold(warn, fail); + } + + @Override + public int getStorageAttachedIndexesTotalWarnThreshold() + { + return DEFAULT_CONFIG.getStorageAttachedIndexesPerTableWarnThreshold(); + } + + @Override + public int getStorageAttachedIndexesTotalFailThreshold() + { + return DEFAULT_CONFIG.getStorageAttachedIndexesPerTableFailThreshold(); + } + + @Override + public void setStorageAttachedIndexesTotalThreshold(int warn, int fail) + { + DEFAULT_CONFIG.setStorageAttachedIndexesTotalThreshold(warn, fail); + } + + @Override + public int getTrustedIndexesPerTableWarnThreshold() + { + return DEFAULT_CONFIG.getTrustedIndexesPerTableWarnThreshold(); + } + + @Override + public int getTrustedIndexesPerTableFailThreshold() + { + return DEFAULT_CONFIG.getTrustedIndexesPerTableFailThreshold(); + } + + @Override + public void setTrustedIndexesPerTableThreshold(int warn, int fail) + { + DEFAULT_CONFIG.setTrustedIndexesPerTableThreshold(warn, fail); + } + @Override public void setSecondaryIndexesEnabled(boolean enabled) { @@ -834,6 +1030,18 @@ public void setGroupByEnabled(boolean enabled) DEFAULT_CONFIG.setGroupByEnabled(enabled); } + @Override + public boolean getLoggedBatchEnabled() + { + return DEFAULT_CONFIG.getLoggedBatchEnabled(); + } + + @Override + public void setLoggedBatchEnabled(boolean enabled) + { + DEFAULT_CONFIG.setLoggedBatchEnabled(enabled); + } + @Override public boolean getDropTruncateTableEnabled() { @@ -876,6 +1084,26 @@ public void setPageSizeThreshold(int warn, int fail) DEFAULT_CONFIG.setPageSizeThreshold(warn, fail); } + @Override + @Nullable + public String getPageWeightWarnThreshold() + { + return sizeToString(DEFAULT_CONFIG.getPageWeightWarnThreshold()); + } + + @Override + @Nullable + public String getPageWeightFailThreshold() + { + return sizeToString(DEFAULT_CONFIG.getPageWeightFailThreshold()); + } + + @Override + public void setPageWeightThreshold(@Nullable String warnSize, @Nullable String failSize) + { + DEFAULT_CONFIG.setPageWeightThreshold(intSizeFromString(warnSize), intSizeFromString(failSize)); + } + @Override public boolean getReadBeforeWriteListOperationsEnabled() { @@ -1152,6 +1380,24 @@ public void setVectorDimensionsThreshold(int warn, int fail) DEFAULT_CONFIG.setVectorDimensionsThreshold(warn, fail); } + @Override + public int getSaiAnnRerankKWarnThreshold() + { + return DEFAULT_CONFIG.getSaiAnnRerankKWarnThreshold(); + } + + @Override + public int getSaiAnnRerankKFailThreshold() + { + return DEFAULT_CONFIG.getSaiAnnRerankKFailThreshold(); + } + + @Override + public void setSaiAnnRerankKThreshold(int warn, int fail) + { + DEFAULT_CONFIG.setSaiAnnRerankKThreshold(warn, fail); + } + @Override public void setVectorTypeEnabled(boolean enabled) { @@ -1405,6 +1651,42 @@ public void setIntersectFilteringQueryEnabled(boolean value) DEFAULT_CONFIG.setIntersectFilteringQueryEnabled(value); } + @Override + public int getOffsetRowsWarnThreshold() + { + return DEFAULT_CONFIG.getOffsetRowsWarnThreshold(); + } + + @Override + public int getOffsetRowsFailThreshold() + { + return DEFAULT_CONFIG.getOffsetRowsFailThreshold(); + } + + @Override + public void setOffsetRowsThreshold(int warn, int fail) + { + DEFAULT_CONFIG.setOffsetRowsThreshold(warn, fail); + } + + @Override + public int getQueryFiltersWarnThreshold() + { + return DEFAULT_CONFIG.getQueryFiltersWarnThreshold(); + } + + @Override + public int getQueryFiltersFailThreshold() + { + return DEFAULT_CONFIG.getQueryFiltersFailThreshold(); + } + + @Override + public void setQueryFiltersThreshold(int warn, int fail) + { + DEFAULT_CONFIG.setQueryFiltersThreshold(warn, fail); + } + private static String toCSV(Set values) { return values == null || values.isEmpty() ? "" : String.join(",", values); @@ -1444,6 +1726,11 @@ private static Long sizeToBytes(@Nullable DataStorageSpec.LongBytesBound size) return size == null ? -1 : size.toBytes(); } + private static Integer sizeToBytes(@Nullable DataStorageSpec.IntBytesBound size) + { + return size == null ? -1 : size.toBytes(); + } + private static String sizeToString(@Nullable DataStorageSpec size) { return size == null ? null : size.toString(); @@ -1454,6 +1741,11 @@ private static DataStorageSpec.LongBytesBound sizeFromString(@Nullable String si return StringUtils.isEmpty(size) ? null : new DataStorageSpec.LongBytesBound(size); } + private static DataStorageSpec.IntBytesBound intSizeFromString(@Nullable String size) + { + return StringUtils.isEmpty(size) ? null : new DataStorageSpec.IntBytesBound(size); + } + private static String durationToString(@Nullable DurationSpec duration) { return duration == null ? null : duration.toString(); diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java index ece387461d78..ac56040ae592 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java @@ -23,6 +23,7 @@ import javax.annotation.Nullable; import org.apache.cassandra.config.DataStorageSpec; +import org.apache.cassandra.config.DataStorageSpec.IntBytesBound; import org.apache.cassandra.config.DurationSpec; import org.apache.cassandra.db.ConsistencyLevel; @@ -91,6 +92,48 @@ public interface GuardrailsConfig */ boolean getSecondaryIndexesEnabled(); + void setSecondaryIndexesEnabled(boolean enabled); + + /** + * @return The threshold to warn when creating more SASI indexes per table than threshold. + */ + int getSasiIndexesPerTableWarnThreshold(); + + /** + * @return The threshold to fail when creating more SASI indexes per table than threshold. + */ + int getSasiIndexesPerTableFailThreshold(); + + /** + * @return The threshold to warn when creating more SAI indexes per table than threshold. + */ + int getStorageAttachedIndexesPerTableWarnThreshold(); + + /** + * @return The threshold to fail when creating more SAI indexes per table than threshold. + */ + int getStorageAttachedIndexesPerTableFailThreshold(); + + /** + * @return The threshold to warn when creating more SAI indexes in total than threshold. + */ + int getStorageAttachedIndexesTotalWarnThreshold(); + + /** + * @return The threshold to fail when creating more SAI indexes in total than threshold. + */ + int getStorageAttachedIndexesTotalFailThreshold(); + + /** + * @return The threshold to warn when creating more trusted custom indexes per table than threshold. + */ + int getTrustedIndexesPerTableWarnThreshold(); + + /** + * @return The threshold to fail when creating more trusted custom indexes per table than threshold. + */ + int getTrustedIndexesPerTableFailThreshold(); + /** * @return The threshold to warn when creating more materialized views per table than threshold. */ @@ -161,6 +204,13 @@ public interface GuardrailsConfig */ boolean getGroupByEnabled(); + /** + * Returns whether logged batches are allowed + * + * @return {@code true} if allowed, {@code false} otherwise. + */ + boolean getLoggedBatchEnabled(); + /** * Returns whether TRUNCATE or DROP table are allowed * @@ -176,15 +226,25 @@ public interface GuardrailsConfig boolean getDropKeyspaceEnabled(); /** - * @return The threshold to warn when page size exceeds given size. + * @return The threshold to warn when page size exceeds given size in rows. */ int getPageSizeWarnThreshold(); /** - * @return The threshold to fail when page size exceeds given size. + * @return The threshold to fail when page size exceeds given size in rows. */ int getPageSizeFailThreshold(); + /** + * @return The threshold to warn when page size exceeds given size in bytes. + */ + IntBytesBound getPageWeightWarnThreshold(); + + /** + * @return The threshold to fail when page size exceeds given size in bytes. + */ + IntBytesBound getPageWeightFailThreshold(); + /** * Returns whether list operations that require read before write are allowed. * @@ -206,6 +266,13 @@ public interface GuardrailsConfig */ boolean getSimpleStrategyEnabled(); + /** + * Returns whether use of Counters is enabled + * + * @return {@code true} if Counters are allowed, {@code false} otherwise. + */ + boolean getCounterEnabled(); + /** * @return The threshold to warn when an IN query creates a cartesian product with a size exceeding threshold. * -1 means disabled. @@ -326,6 +393,16 @@ public interface GuardrailsConfig */ int getVectorDimensionsFailThreshold(); + /** + * @return The threshold to warn when creating a vector with more dimensions than threshold. + */ + int getSaiAnnRerankKWarnThreshold(); + + /** + * @return The threshold to fail when creating a vector with more dimensions than threshold. + */ + int getSaiAnnRerankKFailThreshold(); + /** * @return The threshold to warn when local disk usage percentage exceeds that threshold. * Allowed values are in the range {@code [1, 100]}, and -1 means disabled. @@ -545,4 +622,62 @@ void setMinimumTimestampThreshold(@Nullable DurationSpec.LongMicrosecondsBound w * @param enabled {@code true} if a query without partition key is enabled or not */ void setNonPartitionRestrictedQueryEnabled(boolean enabled); + + /* + * @return The threshold to warn when a read scans more tombstones than threshold. + */ + int getTombstoneWarnThreshold(); + + /** + * @return The threshold to fail when a read scans more tombstones than threshold. + */ + int getTombstoneFailThreshold(); + + /** + * Sets warning and failure thresholds for the number of tombstones read by a query + * + * @param warn value to set for warn threshold + * @param fail value to set for fail threshold + */ + void setTombstonesThreshold(int warn, int fail); + + /** + * @return The threshold to warn when the number of batch mutations is more than threshold. + */ + long getBatchSizeWarnThreshold(); + + /** + * @return The threshold to fail when the number of batch mutations is more than threshold. + */ + long getBatchSizeFailThreshold(); + + /** + * @return The threshold to warn when the number of unlogged batch partitions is more than threshold. + */ + long getUnloggedBatchAcrossPartitionsWarnThreshold(); + + /** + * @return The threshold to fail when the numner of unlogged batch partitions is more than threshold. + */ + long getUnloggedBatchAcrossPartitionsFailThreshold(); + + /** + * @return the warning threshold for the offset rows used in SELECT queries + */ + int getOffsetRowsWarnThreshold(); + + /** + * @return the failure threshold for the offset rows used in SELECT queries + */ + int getOffsetRowsFailThreshold(); + + /** + * @return the warning threshold for the number of query filtering operations per SELECT query (after analysis) + */ + int getQueryFiltersWarnThreshold(); + + /** + * @return the failure threshold for the number of query filtering operations per SELECT query (after analysis) + */ + int getQueryFiltersFailThreshold(); } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java index 990a07a1ceda..ebae5d53944c 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java @@ -65,7 +65,7 @@ public interface GuardrailsConfigProvider */ static GuardrailsConfigProvider build(String customImpl) { - return FBUtilities.construct(customImpl, "custom guardrails config provider"); + return FBUtilities.construct(customImpl, "custom guardrails config provider", GuardrailsConfigProvider.class); } /** diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java index d40092314a3e..f5d33150af7a 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java @@ -116,6 +116,54 @@ public interface GuardrailsMBean */ void setSecondaryIndexesEnabled(boolean enabled); + /** + * @return The threshold to warn when creating more storage attached indexes per table than threshold. -1 means disabled. + */ + int getStorageAttachedIndexesPerTableWarnThreshold(); + + /** + * @return The threshold to prevent creating more storage attached indexes per table than threshold. -1 means disabled. + */ + int getStorageAttachedIndexesPerTableFailThreshold(); + + /** + * @param warn The threshold to warn when creating more storage attached indexes per table than threshold. -1 means disabled. + * @param fail The threshold to prevent creating more storage attached indexes per table than threshold. -1 means disabled. + */ + void setStorageAttachedIndexesPerTableThreshold(int warn, int fail); + + /** + * @return The threshold to warn when creating more storage attached indexes total than threshold. -1 means disabled. + */ + int getStorageAttachedIndexesTotalWarnThreshold(); + + /** + * @return The threshold to prevent creating more storage attached indexes total than threshold. -1 means disabled. + */ + int getStorageAttachedIndexesTotalFailThreshold(); + + /** + * @param warn The threshold to warn when creating more storage attached indexes total than threshold. -1 means disabled. + * @param fail The threshold to prevent creating more storage attahced indexes total than threshold. -1 means disabled. + */ + void setStorageAttachedIndexesTotalThreshold(int warn, int fail); + + /** + * @return The threshold to warn when creating more trusted custom indexes per table than threshold. -1 means disabled. + */ + int getTrustedIndexesPerTableWarnThreshold(); + + /** + * @return The threshold to prevent creating more trusted custom indexes per table than threshold. -1 means disabled. + */ + int getTrustedIndexesPerTableFailThreshold(); + + /** + * @param warn The threshold to warn when creating more trusted custom indexes per table than threshold. -1 means disabled. + * @param fail The threshold to prevent creating more trusted custom indexes per table than threshold. -1 means disabled. + */ + void setTrustedIndexesPerTableThreshold(int warn, int fail); + /** * @return The threshold to warn when creating more materialized views per table than threshold. * -1 means disabled. @@ -292,6 +340,18 @@ public interface GuardrailsMBean */ void setGroupByEnabled(boolean enabled); + /** + * Returns whether logged batches are allowed. + * + * @return {@code true} if allowed, {@code false} otherwise. + */ + boolean getLoggedBatchEnabled(); + + /** + * Sets whether logged batches are allowed. + */ + void setLoggedBatchEnabled(boolean enabled); + /** * Returns whether users can TRUNCATE or DROP TABLE * @@ -334,6 +394,31 @@ public interface GuardrailsMBean */ void setPageSizeThreshold(int warn, int fail); + /** + * @return The threshold to warn when requesting page with more data (bytes) than threshold, as a string formatted as in, + * for example, {@code 10GiB}, {@code 20MiB}, {@code 30KiB} or {@code 40B}. A {@code null} value means disabled. + */ + @Nullable + String getPageWeightWarnThreshold(); + + /** + * @return The threshold to fail when requesting page with more data (bytes) than threshold, as a string formatted as in, + * for example, {@code 10GiB}, {@code 20MiB}, {@code 30KiB} or {@code 40B}. A {@code null} value means disabled. + */ + @Nullable + String getPageWeightFailThreshold(); + + /** + * @param warnSize The threshold to warn when encountering page weights larger than threshold, as a string formatted + * as in, for example, {@code 10GiB}, {@code 20MiB}, {@code 30KiB} or {@code 40B}. + * A {@code null} value means disabled. + * @param failSize The threshold to fail when encountering page weights larger than threshold, as a string formatted + * as in, for example, {@code 10GiB}, {@code 20MiB}, {@code 30KiB} or {@code 40B}. + * A {@code null} value means disabled. Triggering a failure emits a log message and a diagnostic + * event, but it desn't throw an exception interrupting the offending sstable write. + */ + void setPageWeightThreshold(@Nullable String warnSize, @Nullable String failSize); + /** * Returns whether list operations that require read before write are allowed. * @@ -613,6 +698,22 @@ public interface GuardrailsMBean */ void setVectorDimensionsThreshold(int warn, int fail); + /** + * @return The threshold to warn for the rerank_k parameter, an ANN query option. + */ + int getSaiAnnRerankKWarnThreshold(); + + /** + * @return The threshold to fail for the rerank_k parameter, an ANN query option. + */ + int getSaiAnnRerankKFailThreshold(); + + /** + * @param warn The threshold to warn for the rerank_k parameter, an ANN query option. + * @param fail The threshold to prevent setting the rerank_k parameter, an ANN query option. + */ + void setSaiAnnRerankKThreshold(int warn, int fail); + /** * @param enabled {@code true} if vector type usage is enabled. */ @@ -888,7 +989,7 @@ public interface GuardrailsMBean void setNonPartitionRestrictedQueryEnabled(boolean enabled); /** - * @return true if a client warning is emitted for a filtering query with an intersection on mutable columns at a + * @return true if a client warning is emitted for a filtering query with an intersection on mutable columns at a * consistency level requiring coordinator reconciliation */ boolean getIntersectFilteringQueryWarned(); @@ -902,4 +1003,40 @@ public interface GuardrailsMBean boolean getIntersectFilteringQueryEnabled(); void setIntersectFilteringQueryEnabled(boolean value); + + /** + * @return the warning threshold for the offset rows used in SELECT queries + * -1 means disabled. + */ + int getOffsetRowsWarnThreshold(); + + /** + * @return the failure threshold for the offset rows used in SELECT queries + * -1 means disabled. + */ + int getOffsetRowsFailThreshold(); + + /** + * @param warn the warning threshold for the offset rows used in SELECT queries. -1 means disabled. + * @param fail the failure threshold for the offset rows used in SELECT queries. -1 means disabled. + */ + void setOffsetRowsThreshold(int warn, int fail); + + /** + * @return the warning threshold for the offset rows used in SELECT queries + * -1 means disabled. + */ + int getQueryFiltersWarnThreshold(); + + /** + * @return the failure threshold for the offset rows used in SELECT queries + * -1 means disabled. + */ + int getQueryFiltersFailThreshold(); + + /** + * @param warn the warning threshold for the offset rows used in SELECT queries. -1 means disabled. + * @param fail the failure threshold for the offset rows used in SELECT queries. -1 means disabled. + */ + void setQueryFiltersThreshold(int warn, int fail); } diff --git a/src/java/org/apache/cassandra/db/guardrails/Threshold.java b/src/java/org/apache/cassandra/db/guardrails/Threshold.java index 257ab013b760..91421ce551c3 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Threshold.java +++ b/src/java/org/apache/cassandra/db/guardrails/Threshold.java @@ -18,6 +18,7 @@ package org.apache.cassandra.db.guardrails; +import java.util.function.Supplier; import java.util.function.ToLongFunction; import javax.annotation.Nullable; @@ -33,6 +34,30 @@ */ public abstract class Threshold extends Guardrail { + /** + * A {@link Threshold} with both failure and warning thresholds disabled, so that cannot ever be triggered. + */ + public static final Threshold NEVER_TRIGGERED = new Threshold("never_triggered", null, state -> -1L, state -> -1L, null) + { + @Override + protected boolean compare(long value, long threshold) + { + return false; + } + + @Override + protected long failValue(ClientState state) + { + return Long.MAX_VALUE; + } + + @Override + protected long warnValue(ClientState state) + { + return Long.MAX_VALUE; + } + }; + protected ToLongFunction warnThreshold; protected ToLongFunction failThreshold; protected final ErrorMessageProvider messageProvider; @@ -170,4 +195,94 @@ interface ErrorMessageProvider */ String createMessage(boolean isWarning, String what, String value, String threshold); } + + /** + * Creates a new {@link GuardedCounter} guarded by this threshold guardrail. + * + * @param whatFct a function called when either a warning or failure is triggered by the created counter to + * describe the value. This is equivalent to the {@code what} argument of {@link #guard} but is a function to + * allow the output string to be compute lazily (only if a failure/warn ends up being triggered). + * @param containsUserData if a warning or failure is triggered by the created counter and the {@code whatFct} + * is called, indicates whether the create string contains user data. This is the exact equivalent to the + * similarly named argument of {@link #guard}. + * @param clientState the client state, used to skip the check if the query is internal or is done by a superuser. + * A {@code null} value means that the check should be done regardless of the query. + * @return the newly created guarded counter. + */ + public GuardedCounter newCounter(Supplier whatFct, boolean containsUserData, @Nullable ClientState clientState) + { + Threshold threshold = enabled(clientState) ? this : NEVER_TRIGGERED; + return threshold.new GuardedCounter(whatFct, containsUserData, clientState); + } + + /** + * A facility for when the value to guard is built incrementally, but we want to trigger failures as soon + * as the failure threshold is reached, but only trigger the warning on the final value (and so only if the + * failure threshold hasn't also been reached). + *

    + * Note that instances are neither thread safe nor reusable. + */ + public class GuardedCounter + { + private final long warnValue; + private final long failValue; + private final Supplier what; + private final boolean containsUserData; + + private long accumulated; + + private GuardedCounter(Supplier what, boolean containsUserData, ClientState clientState) + { + // We capture the warn and fail value at the time of the counter construction to ensure we use + // stable value during the counter lifetime (and reading a final field is possibly at tad faster). + this.warnValue = warnValue(clientState); + this.failValue = failValue(clientState); + this.what = what; + this.containsUserData = containsUserData; + } + + /** + * The currently accumulated value of the counter. + */ + public long get() + { + return accumulated; + } + + /** + * Add the provided increment to the counter, triggering a failure if the counter after this addition + * crosses the failure threshold. + * + * @param increment the increment to add. + */ + public void add(long increment) + { + accumulated += increment; + if (accumulated > failValue) + { + // Pass any ClientState so GuardrailViolatedException will be thrown by Guardrail#fail + ClientState dummyClientState = ClientState.forInternalCalls(); + triggerFail(accumulated, failValue, what.get(), containsUserData, dummyClientState); + } + } + + /** + * Trigger the warn if the currently accumulated counter value crosses warning threshold and the failure + * has not been triggered yet. + *

    + * This is generally meant to be called when the guarded value is complete. + * + * @return {@code true} and trigger a warning if the current counter value is greater than the warning + * threshold and less than or equal to the failure threshold, {@code false} otherwise. + */ + public boolean checkAndTriggerWarning() + { + if (accumulated > warnValue && accumulated <= failValue) + { + triggerWarn(accumulated, warnValue, what.get(), containsUserData); + return true; + } + return false; + } + } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/guardrails/UserKeyspaceFilter.java b/src/java/org/apache/cassandra/db/guardrails/UserKeyspaceFilter.java new file mode 100644 index 000000000000..9f0126cc8e52 --- /dev/null +++ b/src/java/org/apache/cassandra/db/guardrails/UserKeyspaceFilter.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.guardrails; + +import org.apache.cassandra.db.Keyspace; + +public interface UserKeyspaceFilter +{ + /** + * Returns true if the keyspace should be included. + */ + boolean filter(Keyspace keyspaceName); +} diff --git a/src/java/org/apache/cassandra/db/guardrails/UserKeyspaceFilterProvider.java b/src/java/org/apache/cassandra/db/guardrails/UserKeyspaceFilterProvider.java new file mode 100644 index 000000000000..7ab3cffb5b06 --- /dev/null +++ b/src/java/org/apache/cassandra/db/guardrails/UserKeyspaceFilterProvider.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.guardrails; + +import javax.annotation.Nullable; + +import org.apache.cassandra.service.ClientState; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_KEYSPACES_FILTER_PROVIDER; + +public interface UserKeyspaceFilterProvider +{ + UserKeyspaceFilterProvider instance = getCustomProviderClass() == null ? + new DefaultUserKeyspaceFilterProvider() : + CustomUserKeyspaceFilterProvider.make(getCustomProviderClass()); + + UserKeyspaceFilter get(ClientState clientState); + + @Nullable + static String getCustomProviderClass() + { + return CUSTOM_KEYSPACES_FILTER_PROVIDER.getString(); + } +} diff --git a/src/java/org/apache/cassandra/db/guardrails/Values.java b/src/java/org/apache/cassandra/db/guardrails/Values.java index 9504a3d63bbe..d97a3168802c 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Values.java +++ b/src/java/org/apache/cassandra/db/guardrails/Values.java @@ -18,12 +18,14 @@ package org.apache.cassandra.db.guardrails; +import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import org.apache.cassandra.service.ClientState; @@ -117,6 +119,7 @@ public void guard(Set values, Consumer ignoreAction, @Nullable ClientState { warn(format("Ignoring provided values %s as they are not supported for %s (ignored values are: %s)", toIgnore.stream().sorted().collect(Collectors.toList()), what, ignored)); + toIgnore = new HashSet<>(toIgnore); // defensive copy as the action may modify the underlying set toIgnore.forEach(ignoreAction); } @@ -126,4 +129,28 @@ public void guard(Set values, Consumer ignoreAction, @Nullable ClientState warn(format("Provided values %s are not recommended for %s (warned values are: %s)", toWarn.stream().sorted().collect(Collectors.toList()), what, warned)); } + + // Used by CNDB + @VisibleForTesting + public Set disallowedValues(Set values, ClientState state) + { + Set disallowed = disallowedValues.apply(state); + return Sets.intersection(values, disallowed); + } + + // Used by CNDB + @VisibleForTesting + public Set ignoredValues(Set values, ClientState state) + { + Set ignored = ignoredValues.apply(state); + return Sets.intersection(values, ignored); + } + + // Used by CNDB + @VisibleForTesting + public Set warnedValues(Set values, ClientState state) + { + Set warned = warnedValues.apply(state); + return Sets.intersection(values, warned); + } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/AbstractLogTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/AbstractLogTransaction.java new file mode 100644 index 000000000000..1fccf1c75a8d --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/AbstractLogTransaction.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.lifecycle; + +import java.util.List; +import java.util.Set; + +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.validation.CompactionValidationMetrics; +import org.apache.cassandra.db.compaction.validation.CompactionValidationTask; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.concurrent.Transactional; + +import static org.apache.cassandra.db.compaction.OperationType.COMPACTION; + +/** + * A class that tracks sstable files involved in a transaction across sstables: + * if the transaction succeeds the old files should be deleted and the new ones kept; + * vice-versa if it fails. + */ +public abstract class AbstractLogTransaction extends Transactional.AbstractTransactional implements Transactional, LifecycleNewTracker +{ + public abstract OperationType type(); + + public abstract TimeUUID id(); + + public abstract Throwable prepareForObsoletion(Iterable readers, + List obsoletions, + Tracker tracker, + Throwable accumulate); + + /** + * Perform optional validation on current transaction's input sstables and output sstables + * + * @param obsolete sstables to obsolete + * @param update sstables to update to system + */ + public void validate(Set obsolete, Set update) + { + // Only validate compaction tasks. + if (opType() != COMPACTION) + return; + + // Nothing to verify if no obsolete SSTables + if (obsolete.isEmpty()) + return; + + CompactionValidationTask task = new CompactionValidationTask(id(), obsolete, update, CompactionValidationMetrics.INSTANCE); + task.validate(); + } + + public static class Obsoletion + { + final SSTableReader reader; + final ReaderTidier tidier; + + public Obsoletion(SSTableReader reader, ReaderTidier tidier) + { + this.reader = reader; + this.tidier = tidier; + } + } + + /** + * An interface received by sstable readers ({@link SSTableReader}) when the sstable is marked for obsoletion. + * They must call either {@link this#commit()} or {@link this#abort(Throwable)}. If neither method is called then + * the parent transaction won't be able to run its own cleanup. + *

    + * Obsoletion may be aborted due to an exception, in which case {@link this#abort(Throwable)} should be called. + * Otherwise the sstable reader must call {@link this#commit()} when all the references to the reader have been + * released, i.e. when it is OK to delete the sstable files. + */ + public interface ReaderTidier + { + /** + * To be called when all references to the sstable reader have been released and the sstable files can be + * deleted. + */ + void commit(); + + /** + * To be called if the obsoletion is aborted, i.e. if the sstable must be kept after all because the parent + * transaction has been aborted. + */ + Throwable abort(Throwable accumulate); + } +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/CompositeLifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/CompositeLifecycleTransaction.java new file mode 100644 index 000000000000..6cb89b06aa4c --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/CompositeLifecycleTransaction.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.lifecycle; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.utils.TimeUUID; + +/// Composite lifecycle transaction. This is a wrapper around a lifecycle transaction that allows for multiple partial +/// operations that comprise the whole transaction. This is used to parallelize compaction operations over individual +/// output shards where the compaction sources are shared among the operations; in this case we can only release the +/// shared sources once all operations are complete. +/// +/// A composite transaction is initialized with a main transaction that will be used to commit the transaction. Each +/// part of the composite transaction must be registered with the transaction before it is used. The transaction must +/// be initialized by calling [#completeInitialization()] before any of the processing is allowed to proceed. +/// +/// The transaction is considered complete when all parts have been committed or aborted. If any part is aborted, the +/// whole transaction is also aborted ([PartialLifecycleTransaction] will also throw an exception on other parts when +/// they access it if the composite is already aborted). +/// +/// When all parts are committed, the full transaction is applied by performing a checkpoint, obsoletion of the +/// originals if any of the parts requested it, preparation and commit. This may somewhat violate the rules of +/// transactions as a part that has been committed may actually have no effect if another part is aborted later. +/// There are also restrictions on the operations that this model can accept, e.g. replacement of sources and partial +/// checkpointing are not supported (as they are parts of early open which we don't aim to support at this time), +/// and we consider that all parts will have the same opinion about the obsoletion of the originals. +public class CompositeLifecycleTransaction +{ + protected static final Logger logger = LoggerFactory.getLogger(CompositeLifecycleTransaction.class); + + final LifecycleTransaction mainTransaction; + private final AtomicInteger partsToCommitOrAbort; + private volatile boolean obsoleteOriginalsRequested; + private volatile boolean wasAborted; + private volatile boolean initializationComplete; + private volatile int partsCount = 0; + + /// Create a composite transaction wrapper over the given transaction. After construction, the individual parts of + /// the operation must be registered using [#register] and the composite sealed by calling [#completeInitialization]. + /// The composite will then track the state of the parts and commit after all of them have committed (respectively + /// abort if one aborts but only after waiting for all the other tasks to complete, successfully or not). + /// + /// To make it easy to recognize the parts of a composite transaction, the given transaction should have an id with + /// sequence number 0, and partial transactions should use the id that [#register] returns. + public CompositeLifecycleTransaction(LifecycleTransaction mainTransaction) + { + this.mainTransaction = mainTransaction; + this.partsToCommitOrAbort = new AtomicInteger(0); + this.wasAborted = false; + this.obsoleteOriginalsRequested = false; + } + + /// Register one part of the composite transaction. Every part must register itself before the composite transaction + /// is initialized and the parts are allowed to proceed. + /// @param part the part to register + public TimeUUID register(PartialLifecycleTransaction part) + { + int index = partsToCommitOrAbort.incrementAndGet(); + return TimeUUID.Generator.withSequence(mainTransaction.opId(), index); + } + + /// Complete the initialization of the composite transaction. This must be called before any of the parts are + /// executed. + public void completeInitialization() + { + partsCount = partsToCommitOrAbort.get(); + initializationComplete = true; + if (logger.isTraceEnabled()) + logger.trace("Composite transaction {} initialized with {} parts.", mainTransaction.opIdString(), partsCount); + } + + /// Abort the initialization of the composite transaction. This disconnects the attached operations from the + /// transaction, so that they can be properly cancelled. + public void cancelInitialization() + { + wasAborted = true; + initializationComplete = true; + partsCount = partsToCommitOrAbort.getAndSet(0); // so that no commit or abort fires + if (logger.isTraceEnabled()) + logger.trace("Composite transaction {} with {} parts cancelled.", mainTransaction.opIdString(), partsCount); + } + + /// Get the number of parts in the composite transaction. 0 if the transaction is not yet initialized. + public int partsCount() + { + return partsCount; + } + + /// Request that the original sstables are obsoleted when the transaction is committed. Note that this class has + /// an expectation that all parts will have the same opinion about this, and one request will be sufficient to + /// trigger obsoletion. + public void requestObsoleteOriginals() + { + obsoleteOriginalsRequested = true; + } + + /// Commit a part of the composite transaction. This will trigger the final commit of the whole transaction if it is + /// the last part to complete. A part has to commit or abort exactly once. + public void commitPart() + { + partCommittedOrAborted(); + } + + /// Signal an abort of one part of the transaction. If this is the last part to signal, the whole transaction will + /// now abort. Otherwise the composite transaction will wait for the other parts to complete and will abort the + /// composite when they all give their commit or abort signal. A part has to commit or abort exactly once. + /// + /// [PartialLifecycleTransaction] will attempt to abort other parts sooner by throwing an exception when any of its + /// methods are called when the composite transaction is already aborted. + public void abortPart() + { + wasAborted = true; + partCommittedOrAborted(); + } + + boolean wasAborted() + { + return wasAborted; + } + + private void partCommittedOrAborted() + { + if (!initializationComplete) + throw new IllegalStateException("Composite transaction used before initialization is complete."); + if (partsToCommitOrAbort.decrementAndGet() == 0) + { + if (wasAborted) + { + if (logger.isTraceEnabled()) + logger.trace("Composite transaction {} with {} parts aborted.", + mainTransaction.opIdString(), + partsCount); + + mainTransaction.abort(); + } + else + { + if (logger.isTraceEnabled()) + logger.trace("Composite transaction {} with {} parts completed{}.", + mainTransaction.opIdString(), + partsCount, + obsoleteOriginalsRequested ? " with obsoletion" : ""); + + mainTransaction.checkpoint(); + if (obsoleteOriginalsRequested) + mainTransaction.obsoleteOriginals(); + mainTransaction.prepareToCommit(); + mainTransaction.commit(); + } + } + } +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/FailedTransactionDeletionHandler.java b/src/java/org/apache/cassandra/db/lifecycle/FailedTransactionDeletionHandler.java new file mode 100644 index 000000000000..cc021b64be48 --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/FailedTransactionDeletionHandler.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.lifecycle; + +/** + * An interface for retrying failed log transaction deletion + */ +public interface FailedTransactionDeletionHandler +{ + /** + * Rescheduled failed log transaction deletion due to mmap not being finalized or Windows constraint. + */ + void rescheduleFailedDeletions(); +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/Helpers.java b/src/java/org/apache/cassandra/db/lifecycle/Helpers.java index 134beec11643..80f06780c442 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/Helpers.java +++ b/src/java/org/apache/cassandra/db/lifecycle/Helpers.java @@ -29,9 +29,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.utils.Throwables; import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.equalTo; @@ -51,7 +49,7 @@ class Helpers * really present, and that the items to add are not (unless we're also removing them) * @return a new set with the contents of the provided one modified */ - static Set replace(Set original, Set remove, Iterable add) + static Set replace(Set original, Set remove, Iterable add) { return ImmutableSet.copyOf(replace(identityMap(original), remove, add).keySet()); } @@ -65,7 +63,7 @@ static Map replace(Map original, Set remove, Iterab { // ensure the ones being removed are the exact same ones present for (T reader : remove) - assert original.get(reader) == reader; + assert original.get(reader) == reader : String.format("%s not found in original set: %s", reader, original); // ensure we don't already contain any we're adding, that we aren't also removing assert !any(add, and(not(in(remove)), in(original.keySet()))) : String.format("original:%s remove:%s add:%s", original.keySet(), remove, add); @@ -118,12 +116,12 @@ static void checkNotReplaced(Iterable readers) assert !reader.isReplaced(); } - static Throwable markObsolete(List obsoletions, Throwable accumulate) + static Throwable markObsolete(List obsoletions, Throwable accumulate) { if (obsoletions == null || obsoletions.isEmpty()) return accumulate; - for (LogTransaction.Obsoletion obsoletion : obsoletions) + for (AbstractLogTransaction.Obsoletion obsoletion : obsoletions) { try { @@ -137,33 +135,26 @@ static Throwable markObsolete(List obsoletions, Throw return accumulate; } - static Throwable prepareForObsoletion(Iterable readers, LogTransaction txnLogs, List obsoletions, Throwable accumulate) + static Throwable prepareForObsoletion(Iterable readers, + AbstractLogTransaction txnLogs, + List obsoletions, + Tracker tracker, + Throwable accumulate) { - Map logRecords = txnLogs.makeRemoveRecords(readers); - for (SSTableReader reader : readers) - { - try - { - obsoletions.add(new LogTransaction.Obsoletion(reader, txnLogs.obsoleted(reader, logRecords.get(reader)))); - } - catch (Throwable t) - { - accumulate = Throwables.merge(accumulate, t); - } - } - return accumulate; + + return txnLogs.prepareForObsoletion(readers, obsoletions, tracker, accumulate); } - static Throwable abortObsoletion(List obsoletions, Throwable accumulate) + static Throwable abortObsoletion(List obsoletions, Throwable accumulate) { if (obsoletions == null || obsoletions.isEmpty()) return accumulate; - for (LogTransaction.Obsoletion obsoletion : obsoletions) + for (AbstractLogTransaction.Obsoletion obsoletion : obsoletions) { try { - obsoletion.tidier.abort(); + obsoletion.tidier.abort(accumulate); } catch (Throwable t) { diff --git a/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java index c014e3865f93..f3e9c9b7c5f1 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java +++ b/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java @@ -21,7 +21,11 @@ import java.util.Collection; import java.util.Set; +import com.google.common.collect.Iterables; + import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Transactional; public interface ILifecycleTransaction extends Transactional, LifecycleNewTracker @@ -29,10 +33,37 @@ public interface ILifecycleTransaction extends Transactional, LifecycleNewTracke void checkpoint(); void update(SSTableReader reader, boolean original); void update(Collection readers, boolean original); - public SSTableReader current(SSTableReader reader); + SSTableReader current(SSTableReader reader); void obsolete(SSTableReader reader); void obsoleteOriginals(); Set originals(); boolean isObsolete(SSTableReader reader); boolean isOffline(); + TimeUUID opId(); + + /// Op identifier as a string to use in debug prints. Usually just the opId, with added part information for partial + /// transactions. + default String opIdString() + { + return opId().toString(); + } + + void cancel(SSTableReader removedSSTable); + + default void abort() + { + Throwables.maybeFail(abort(null)); + } + + default void commit() + { + Throwables.maybeFail(commit(null)); + } + + default SSTableReader onlyOne() + { + final Set originals = originals(); + assert originals.size() == 1; + return Iterables.getFirst(originals, null); + } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/ILogAwareFileLister.java b/src/java/org/apache/cassandra/db/lifecycle/ILogAwareFileLister.java new file mode 100644 index 000000000000..c12a4f18222c --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/ILogAwareFileLister.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.lifecycle; + +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiPredicate; + +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.io.util.File; + +/** + * An interface for listing files in a folder + */ +public interface ILogAwareFileLister +{ + /** + * Listing files that are not removed by log transactions in a folder. + * + * @param folder The folder to scan + * @param filter The filter determines which files the client wants returned + * @param onTxnErr The behavior when we fail to list files + * @return all files that are not removed by log transactions + */ + List list(Path folder, BiPredicate filter, Directories.OnTxnErr onTxnErr); +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/ILogFileCleaner.java b/src/java/org/apache/cassandra/db/lifecycle/ILogFileCleaner.java new file mode 100644 index 000000000000..414012cced57 --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/ILogFileCleaner.java @@ -0,0 +1,42 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.cassandra.db.lifecycle; + +import org.apache.cassandra.io.util.File; + +/** + * Removes any leftovers from unfinished log transactions as indicated by any transaction log files + */ +public interface ILogFileCleaner +{ + /** + * list all log files under given directory + * + * @param directory directory to scan + */ + void list(File directory); + + /** + * Removes any leftovers from unfinished transactions as indicated by any transaction log files that + * are found via {@link #list(File)} + */ + boolean removeUnfinishedLeftovers(); +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/ILogTransactionsFactory.java b/src/java/org/apache/cassandra/db/lifecycle/ILogTransactionsFactory.java new file mode 100644 index 000000000000..0a0a1b20ee59 --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/ILogTransactionsFactory.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.lifecycle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.TimeUUID; + +import static org.apache.cassandra.config.CassandraRelevantProperties.LOG_TRANSACTIONS_FACTORY; + +/** + * Factory to create instances used during log transaction processing: + * - {@link AbstractLogTransaction}: tracks sstable files invovled in a transastion cross sstable. + * - {@link ILogAwareFileLister}: list files which are not removed by log transactions + * - {@link ILogFileCleaner}: removes any leftovers from unfinished log transactions + * - {@link FailedTransactionDeletionHandler}: retries failed log transaction deletions + */ +public interface ILogTransactionsFactory +{ + Logger logger = LoggerFactory.getLogger(ILogTransactionsFactory.class); + + ILogTransactionsFactory instance = !LOG_TRANSACTIONS_FACTORY.isPresent() + ? new LogTransactionsFactory() + : FBUtilities.construct(LOG_TRANSACTIONS_FACTORY.getString(), "log transactions factory"); + + /** + * Create {@link AbstractLogTransaction} that tracks sstable files involved in a transaction across sstables: + */ + AbstractLogTransaction createLogTransaction(OperationType operationType, + TimeUUID uuid, + TableMetadataRef metadata); + + /** + * Create {@link ILogAwareFileLister} that lists files which are not removed by log transactions in a folder. + */ + ILogAwareFileLister createLogAwareFileLister(); + + /** + * Create {@link ILogFileCleaner} that removes any leftovers from unfinished log transactions as indicated by any transaction log files + */ + ILogFileCleaner createLogFileCleaner(); + + /** + * Create {@link FailedTransactionDeletionHandler} used to retry failed log transaction deletions + */ + FailedTransactionDeletionHandler createFailedTransactionDeletionHandler(); +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/LifecycleNewTracker.java b/src/java/org/apache/cassandra/db/lifecycle/LifecycleNewTracker.java index 9a0785c43f80..88cc59e25cf5 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LifecycleNewTracker.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LifecycleNewTracker.java @@ -28,11 +28,29 @@ public interface LifecycleNewTracker { /** - * Called when a new table is about to be created, so that this table can be tracked by a transaction. + * Called when a new sstable is about to be created, so that this table can be tracked by a transaction. * @param table - the new table to be tracked */ void trackNew(SSTable table); + /** + * Called when a new sstable and its indexes have been fully written. + * Implementation must be thread safe and not alter the state of the transaction. + * + * @param table - the newly written sstable to be tracked + */ + default void trackNewWritten(SSTable table) + { + } + + /** + * Track new index files attached to the given sstable. Used by CNDB to upload new archive file + * + * @param table on which index files should be tracked + */ + default void trackNewAttachedIndexFiles(SSTable table) + { + } /** * Called when a new table is no longer required, so that this table can be untracked by a transaction. diff --git a/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java index 383d9b08f26e..e8514b569ef7 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java @@ -24,7 +24,9 @@ import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; import com.google.common.annotations.VisibleForTesting; @@ -32,9 +34,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.Runnables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.compaction.OperationType; @@ -43,6 +48,8 @@ import org.apache.cassandra.io.sstable.format.SSTableReader.UniqueIdentifier; import org.apache.cassandra.io.util.File; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Transactional; @@ -69,6 +76,7 @@ import static org.apache.cassandra.db.lifecycle.View.updateCompacting; import static org.apache.cassandra.db.lifecycle.View.updateLiveSet; import static org.apache.cassandra.utils.Throwables.maybeFail; +import static org.apache.cassandra.utils.Throwables.merge; import static org.apache.cassandra.utils.concurrent.Refs.release; import static org.apache.cassandra.utils.concurrent.Refs.selfRefs; @@ -125,9 +133,9 @@ public String toString() } } - public final Tracker tracker; + private final Tracker tracker; // The transaction logs keep track of new and old sstable files - private final LogTransaction log; + private final AbstractLogTransaction log; // the original readers this transaction was opened over, and that it guards // (no other transactions may operate over these readers concurrently) private final Set originals = new HashSet<>(); @@ -144,59 +152,67 @@ public String toString() private final State staged = new State(); // the tidier and their readers, to be used for marking readers obsoleted during a commit - private List obsoletions; + private List obsoletions; // commit/rollback hooks private List commitHooks = new ArrayList<>(); private List abortHooks = new ArrayList<>(); + /** + * Creates a new unique id that is suitable for a transaction. + */ + public static TimeUUID newId() + { + return TimeUUID.Generator.nextTimeUUID(); + } + /** * construct a Transaction for use in an offline operation */ public static LifecycleTransaction offline(OperationType operationType, SSTableReader reader) { - return offline(operationType, singleton(reader)); + return offline(operationType, reader.metadataRef(), singleton(reader)); } /** * construct a Transaction for use in an offline operation */ - public static LifecycleTransaction offline(OperationType operationType, Collection readers) + public static LifecycleTransaction offline(OperationType operationType, TableMetadataRef metadata, Collection readers) { // if offline, for simplicity we just use a dummy tracker - Tracker dummy = Tracker.newDummyTracker(); + Tracker dummy = Tracker.newDummyTracker(metadata); dummy.addInitialSSTables(readers); dummy.apply(updateCompacting(emptySet(), readers)); - return new LifecycleTransaction(dummy, operationType, readers); + return new LifecycleTransaction(dummy, operationType, readers, newId()); } /** * construct an empty Transaction with no existing readers */ - public static LifecycleTransaction offline(OperationType operationType) + public static LifecycleTransaction offline(OperationType operationType, TableMetadataRef metadata) { - Tracker dummy = Tracker.newDummyTracker(); - return new LifecycleTransaction(dummy, new LogTransaction(operationType, dummy), Collections.emptyList()); + Tracker dummy = Tracker.newDummyTracker(metadata); + return new LifecycleTransaction(dummy, operationType, Collections.emptyList(), newId()); } - LifecycleTransaction(Tracker tracker, OperationType operationType, Iterable readers) - { - this(tracker, new LogTransaction(operationType, tracker), readers); - } - - LifecycleTransaction(Tracker tracker, LogTransaction log, Iterable readers) + @VisibleForTesting + public LifecycleTransaction(Tracker tracker, + OperationType operationType, + Iterable readers, + TimeUUID uuid) { this.tracker = tracker; - this.log = log; + this.log = ILogTransactionsFactory.instance.createLogTransaction(operationType, uuid, tracker.metadata); for (SSTableReader reader : readers) { originals.add(reader); marked.add(reader); identities.add(reader.instanceId); } + logger.debug("LifecycleTransaction {} created", uuid); } - public LogTransaction log() + public AbstractLogTransaction log() { return log; } @@ -204,7 +220,7 @@ public LogTransaction log() @Override //LifecycleNewTracker public OperationType opType() { - return log.type(); + return log.opType(); } public TimeUUID opId() @@ -222,7 +238,14 @@ public void doPrepare() // prepare for compaction obsolete readers as long as they were part of the original set // since those that are not original are early readers that share the same desc with the finals - maybeFail(prepareForObsoletion(filterIn(logged.obsolete, originals), log, obsoletions = new ArrayList<>(), null)); + maybeFail(prepareForObsoletion(filterIn(logged.obsolete, originals), log, obsoletions = new ArrayList<>(), tracker, null)); + + // Use original sstables instead of logged.obsolete which may change their starting position due to early-open + Set obsolete = Sets.newHashSet(filterIn(originals, logged.obsolete)); + log.validate(obsolete, logged.update); + + // This needs to be called after checkpoint and having prepared the obsoletions because it will upload the deletion + // marks in CNDB log.prepareToCommit(); } @@ -250,7 +273,7 @@ public Throwable doCommit(Throwable accumulate) accumulate = tracker.updateSizeTracking(logged.obsolete, logged.update, accumulate); accumulate = runOnCommitHooks(accumulate); accumulate = release(selfRefs(logged.obsolete), accumulate); - accumulate = tracker.notifySSTablesChanged(originals, logged.update, log.type(), accumulate); + accumulate = tracker.notifySSTablesChanged(originals, logged.update, log.opType(), Optional.of(log.id()), accumulate); return accumulate; } @@ -273,7 +296,7 @@ public Throwable doAbort(Throwable accumulate) Iterable obsolete = filterOut(concatUniq(staged.update, logged.update), originals); logger.trace("Obsoleting {}", obsolete); - accumulate = prepareForObsoletion(obsolete, log, obsoletions = new ArrayList<>(), accumulate); + accumulate = prepareForObsoletion(obsolete, log, obsoletions = new ArrayList<>(), tracker, accumulate); // it's safe to abort even if committed, see maybeFail in doCommit() above, in this case it will just report // a failure to abort, which is useful information to have for debug accumulate = log.abort(accumulate); @@ -283,7 +306,7 @@ public Throwable doAbort(Throwable accumulate) List restored = restoreUpdatedOriginals(); List invalid = Lists.newArrayList(Iterables.concat(logged.update, logged.obsolete)); accumulate = tracker.apply(updateLiveSet(logged.update, restored), accumulate); - accumulate = tracker.notifySSTablesChanged(invalid, restored, OperationType.COMPACTION, accumulate); + accumulate = tracker.notifySSTablesChanged(invalid, restored, OperationType.COMPACTION, Optional.of(log.id()), accumulate); // setReplaced immediately preceding versions that have not been obsoleted accumulate = setReplaced(logged.update, accumulate); accumulate = runOnAbortooks(accumulate); @@ -327,8 +350,10 @@ private static Throwable runHooks(Iterable hooks, Throwable accumulate @Override protected Throwable doPostCleanup(Throwable accumulate) { - log.close(); - return unmarkCompacting(marked, accumulate); + accumulate = Throwables.close(accumulate, log); + accumulate = unmarkCompacting(marked, accumulate); + logger.debug("LifecycleTransaction {} finalized", opId()); + return accumulate; } public boolean isOffline() @@ -336,6 +361,12 @@ public boolean isOffline() return tracker.isDummy(); } + @VisibleForTesting + public void unsafeClose() + { + log.close(); + } + /** * call when a consistent batch of changes is ready to be made atomically visible * these will be exposed in the Tracker atomically, or an exception will be thrown; in this case @@ -359,6 +390,10 @@ private Throwable checkpoint(Throwable accumulate) // check the current versions of the readers we're replacing haven't somehow been replaced by someone else checkNotReplaced(filterIn(toUpdate, staged.update)); + // notify the tracker of the new readers are about to be added and visible + if (!fresh.isEmpty()) + accumulate = merge(accumulate, tracker.notifyAdding(fresh, null, null, opType(), Optional.of(opId()))); + // ensure any new readers are in the compacting set, since we aren't done with them yet // and don't want anyone else messing with them // apply atomically along with updating the live set of readers @@ -552,7 +587,7 @@ public LifecycleTransaction split(Collection readers) originals.remove(reader); marked.remove(reader); } - return new LifecycleTransaction(tracker, log.type(), readers); + return new LifecycleTransaction(tracker, log.opType(), readers, newId()); } /** @@ -572,11 +607,12 @@ private Throwable unmarkCompacting(Set unmark, Throwable accumula // when the CFS is invalidated, it will call unreferenceSSTables(). However, unreferenceSSTables only deals // with sstables that aren't currently being compacted. If there are ongoing compactions that finish or are // interrupted after the CFS is invalidated, those sstables need to be unreferenced as well, so we do that here. - accumulate = tracker.dropSSTablesIfInvalid(accumulate); + accumulate = tracker.dropOrUnloadSSTablesIfInvalid("for transaction " + log.id(), accumulate); return accumulate; } // convenience method for callers that know only one sstable is involved in the transaction + // overridden to avoid defensive copying public SSTableReader onlyOne() { assert originals.size() == 1; @@ -591,6 +627,18 @@ public void trackNew(SSTable table) log.trackNew(table); } + @Override + public void trackNewWritten(SSTable table) + { + log.trackNewWritten(table); + } + + @Override + public void trackNewAttachedIndexFiles(SSTable table) + { + log.trackNewAttachedIndexFiles(table); + } + @Override public void untrackNew(SSTable table) { @@ -599,12 +647,36 @@ public void untrackNew(SSTable table) public static boolean removeUnfinishedLeftovers(ColumnFamilyStore cfs) { - return LogTransaction.removeUnfinishedLeftovers(cfs.getDirectories().getCFDirectories()); + return removeUnfinishedLeftovers(cfs.getDirectories().getCFDirectories()); } + /** + * Removes any leftovers from unifinished transactions as indicated by any transaction log files that + * are found in the table directories. This means that any old sstable files for transactions that were committed, + * or any new sstable files for transactions that were aborted or still in progress, should be removed *if + * it is safe to do so*. Refer to the checks in LogFile.verify for further details on the safety checks + * before removing transaction leftovers and refer to the comments at the beginning of this file or in NEWS.txt + * for further details on transaction logs. + * + * This method is called on startup and by the standalone sstableutil tool when the cleanup option is specified, + * @see org.apache.cassandra.tools.StandaloneSSTableUtil + * + * @return true if the leftovers of all transaction logs found were removed, false otherwise. + * + */ public static boolean removeUnfinishedLeftovers(TableMetadata metadata) { - return LogTransaction.removeUnfinishedLeftovers(metadata); + return removeUnfinishedLeftovers(new Directories(metadata).getCFDirectories()); + } + + public static boolean removeUnfinishedLeftovers(List directories) + { + // List directories + ILogFileCleaner cleaner = ILogTransactionsFactory.instance.createLogFileCleaner(); + for (File dir : directories) + cleaner.list(dir); + + return cleaner.removeUnfinishedLeftovers(); } /** @@ -621,7 +693,7 @@ public static boolean removeUnfinishedLeftovers(TableMetadata metadata) */ public static List getFiles(Path folder, BiPredicate filter, Directories.OnTxnErr onTxnErr) { - return new LogAwareFileLister(folder, filter, onTxnErr).list(); + return ILogTransactionsFactory.instance.createLogAwareFileLister().list(folder, filter, onTxnErr); } /** @@ -630,7 +702,7 @@ public static List getFiles(Path folder, BiPredicate filter; //file, file type - - // The behavior when we fail to list files - private final OnTxnErr onTxnErr; - - // The unfiltered result - NavigableMap files = new TreeMap<>(); - - @VisibleForTesting - LogAwareFileLister(Path folder, BiPredicate filter, OnTxnErr onTxnErr) - { - this.folder = folder; - this.filter = filter; - this.onTxnErr = onTxnErr; - } - - public List list() + @Override + public List list(Path folder, BiPredicate filter, OnTxnErr onTxnErr) { try { - return innerList(); + return innerList(folder, filter, onTxnErr); } catch (Throwable t) { @@ -78,8 +60,11 @@ public List list() } } - List innerList() throws Throwable + protected List innerList(Path folder, BiPredicate filter, OnTxnErr onTxnErr) throws Throwable { + // The unfiltered result + NavigableMap files = new TreeMap<>(); + list(Files.newDirectoryStream(folder)) .stream() .filter((f) -> !LogFile.isLogFile(f)) @@ -92,7 +77,7 @@ List innerList() throws Throwable list(Files.newDirectoryStream(folder, '*' + LogFile.EXT)) .stream() .filter(LogFile::isLogFile) - .forEach(this::classifyFiles); + .forEach(txnFile -> classifyFiles(folder, txnFile, onTxnErr, files)); // Finally we apply the user filter before returning our result return files.entrySet().stream() @@ -120,36 +105,36 @@ static List list(DirectoryStream stream) throws IOException * We read txn log files, if we fail we throw only if the user has specified * OnTxnErr.THROW, else we log an error and apply the txn log anyway */ - void classifyFiles(File txnFile) + void classifyFiles(Path folder, File txnFile, OnTxnErr onTxnErr, NavigableMap files) { try (LogFile txn = LogFile.make(txnFile)) { - readTxnLog(txn); - classifyFiles(txn); + readTxnLog(txn, onTxnErr); + classifyFiles(folder, txn, onTxnErr, files); files.put(txnFile, FileType.TXN_LOG); } } - void readTxnLog(LogFile txn) + void readTxnLog(LogFile txn, OnTxnErr onTxnErr) { if (!txn.verify() && onTxnErr == OnTxnErr.THROW) throw new LogTransaction.CorruptTransactionLogException("Some records failed verification. See earlier in log for details.", txn); } - void classifyFiles(LogFile txnFile) + void classifyFiles(Path folder, LogFile txnFile, OnTxnErr onTxnErr, NavigableMap files) { Map> oldFiles = txnFile.getFilesOfType(folder, files.navigableKeySet(), LogRecord.Type.REMOVE); Map> newFiles = txnFile.getFilesOfType(folder, files.navigableKeySet(), LogRecord.Type.ADD); if (txnFile.completed()) { // last record present, filter regardless of disk status - setTemporary(txnFile, oldFiles.values(), newFiles.values()); + setTemporary(txnFile, oldFiles.values(), newFiles.values(), files); return; } if (allFilesPresent(oldFiles)) { // all old files present, transaction is in progress, this will filter as aborted - setTemporary(txnFile, oldFiles.values(), newFiles.values()); + setTemporary(txnFile, oldFiles.values(), newFiles.values(), files); return; } @@ -161,11 +146,11 @@ void classifyFiles(LogFile txnFile) return; // otherwise read the file again to see if it is completed now - readTxnLog(txnFile); + readTxnLog(txnFile, onTxnErr); if (txnFile.completed()) { // if after re-reading the txn is completed then filter accordingly - setTemporary(txnFile, oldFiles.values(), newFiles.values()); + setTemporary(txnFile, oldFiles.values(), newFiles.values(), files); return; } @@ -194,11 +179,11 @@ private static boolean allFilesPresent(Map> oldFiles) .findFirst().isPresent(); } - private void setTemporary(LogFile txnFile, Collection> oldFiles, Collection> newFiles) + private void setTemporary(LogFile txnFile, Collection> oldFiles, Collection> newFiles, NavigableMap files) { Collection> temporary = txnFile.committed() ? oldFiles : newFiles; temporary.stream() .flatMap(Set::stream) - .forEach((f) -> this.files.put(f, FileType.TEMPORARY)); + .forEach((f) -> files.put(f, FileType.TEMPORARY)); } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogFile.java b/src/java/org/apache/cassandra/db/lifecycle/LogFile.java index 13436b112a98..3cbdd6a614fb 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LogFile.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LogFile.java @@ -65,7 +65,9 @@ * of unfinished leftovers when a transaction is completed, or aborted, or when * we clean up on start-up. * - * @see LogTransaction + * Note: this is used by {@link LogTransaction} + * + * @see AbstractLogTransaction */ @NotThreadSafe final class LogFile implements AutoCloseable @@ -391,8 +393,7 @@ private LogRecord makeRecord(Type type, SSTable table, LogRecord record) private void maybeCreateReplica(SSTable sstable) { File directory = sstable.descriptor.directory; - String fileName = StringUtils.join(directory, File.pathSeparator(), getFileName()); - replicas.maybeCreateReplica(directory, fileName, onDiskRecords); + replicas.maybeCreateReplica(directory, getFileName(), onDiskRecords); } void addRecord(LogRecord record) @@ -532,7 +533,7 @@ List getFiles() } @VisibleForTesting - List getFilePaths() + List getFilePaths() { return replicas.getFilePaths(); } diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogFileCleaner.java b/src/java/org/apache/cassandra/db/lifecycle/LogFileCleaner.java new file mode 100644 index 000000000000..27ad59492810 --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/LogFileCleaner.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.lifecycle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.util.File; + +final class LogFileCleaner implements ILogFileCleaner +{ + private static final Logger logger = LoggerFactory.getLogger(LogFileCleaner.class); + + // This maps a transaction log file name to a list of physical files. Each sstable + // can have multiple directories and a transaction is trakced by identical transaction log + // files, one per directory. So for each transaction file name we can have multiple + // physical files. + Map> files = new HashMap<>(); + + @Override + public void list(File directory) + { + Arrays.stream(directory.tryList(LogFile::isLogFile)).forEach(this::add); + } + + void add(File file) + { + List filesByName = files.get(file.name()); + if (filesByName == null) + { + filesByName = new ArrayList<>(); + files.put(file.name(), filesByName); + } + + filesByName.add(file); + } + + @Override + public boolean removeUnfinishedLeftovers() + { + return files.entrySet() + .stream() + .map(LogFileCleaner::removeUnfinishedLeftovers) + .allMatch(Predicate.isEqual(true)); + } + + static boolean removeUnfinishedLeftovers(Map.Entry> entry) + { + try(LogFile txn = LogFile.make(entry.getKey(), entry.getValue())) + { + logger.info("Verifying logfile transaction {}", txn); + if (txn.verify()) + { + Throwable failure = txn.removeUnfinishedLeftovers(null); + if (failure != null) + { + logger.error("Failed to remove unfinished transaction leftovers for transaction log {}", + txn.toString(true), failure); + return false; + } + + return true; + } + else + { + logger.error("Unexpected disk state: failed to read transaction log {}, " + + "check logs before last shutdown for any errors, and ensure txn log files were not edited manually.", + txn.toString(true)); + return false; + } + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java b/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java index fb11455e9021..6f19bfd07b61 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java @@ -56,6 +56,8 @@ /** * A decoded line in a transaction log file replica. * + * Note: this is used by {@link LogTransaction} + * * @see LogReplica and LogFile. */ final class LogRecord @@ -175,8 +177,8 @@ public static LogRecord makeAbort(long updateTime) public static LogRecord make(Type type, SSTable table) { - String absoluteTablePath = absolutePath(table.descriptor.baseFile()); - return make(type, getExistingFiles(absoluteTablePath), table.getAllFilePaths().size(), absoluteTablePath); + String absoluteTablePath = table.descriptor.baseFileUri() + Component.SEPARATOR; + return make(type, getExistingFiles(absoluteTablePath), table.getComponentSize(), absoluteTablePath); } public static Map make(Type type, Iterable tables) @@ -184,7 +186,7 @@ public static Map make(Type type, Iterable ta // contains a mapping from sstable absolute path (everything up until the 'Data'/'Index'/etc part of the filename) to the sstable Map absolutePaths = new HashMap<>(); for (SSTableReader table : tables) - absolutePaths.put(absolutePath(table.descriptor.baseFile()), table); + absolutePaths.put(table.descriptor.baseFileUri() + Component.SEPARATOR, table); // maps sstable base file name to the actual files on disk Map> existingFiles = getExistingFiles(absolutePaths.keySet()); @@ -194,16 +196,11 @@ public static Map make(Type type, Iterable ta List filesOnDisk = entry.getValue(); String baseFileName = entry.getKey(); SSTable sstable = absolutePaths.get(baseFileName); - records.put(sstable, make(type, filesOnDisk, sstable.getAllFilePaths().size(), baseFileName)); + records.put(sstable, make(type, filesOnDisk, sstable.getComponentSize(), baseFileName)); } return records; } - private static String absolutePath(File baseFile) - { - return baseFile.withSuffix(String.valueOf(Component.separator)).canonicalPath(); - } - public LogRecord withExistingFiles(List existingFiles) { if (!absolutePath.isPresent()) @@ -355,7 +352,7 @@ private String format() public static List getExistingFiles(String absoluteFilePath) { - File file = new File(absoluteFilePath); + File file = new File(PathUtils.getPath(absoluteFilePath)); File[] files = file.parent().tryList((dir, name) -> name.startsWith(file.name())); // files may be null if the directory does not exist yet, e.g. when tracking new files return files == null ? Collections.emptyList() : Arrays.asList(files); @@ -374,10 +371,10 @@ public static Map> getExistingFiles(Set absoluteFileP Map> dirToFileNamePrefix = new HashMap<>(); for (String absolutePath : absoluteFilePaths) { - Path fullPath = new File(absolutePath).toPath(); - Path path = fullPath.getParent(); - if (path != null) - dirToFileNamePrefix.computeIfAbsent(new File(path), (k) -> new TreeSet<>()).add(fullPath.getFileName().toString()); + File file = new File(PathUtils.getPath(absolutePath)); + File parent = file.parent(); + if (parent != null) + dirToFileNamePrefix.computeIfAbsent(parent, (k) -> new TreeSet<>()).add(file.name()); } BiPredicate ff = (dir, name) -> { @@ -389,8 +386,8 @@ public static Map> getExistingFiles(Set absoluteFileP String baseName = dirSet.floor(name); if (baseName != null && name.startsWith(baseName)) { - String absolutePath = new File(dir, baseName).path(); - fileMap.computeIfAbsent(absolutePath, k -> new ArrayList<>()).add(new File(dir, name)); + String absolutePath = dir.resolve(baseName).toUri().toString(); + fileMap.computeIfAbsent(absolutePath, k -> new ArrayList<>()).add(dir.resolve(name)); } return false; }; @@ -415,7 +412,7 @@ String fileName() boolean isInFolder(Path folder) { - return absolutePath.isPresent() && PathUtils.isContained(folder, new File(absolutePath.get()).toPath()); + return absolutePath.isPresent() && PathUtils.isContained(folder, new File(PathUtils.getPath(absolutePath.get())).toPath()); } String absolutePath() diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java b/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java index 073ac7c61c16..f84b98e17c00 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java @@ -31,7 +31,7 @@ import org.apache.cassandra.io.FSError; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORE_MISSING_NATIVE_FILE_HINTS; @@ -44,6 +44,8 @@ * partial records in case we crashed after writing to one replica but * before compliting the write to another replica. * + * Note: this is used by {@link LogTransaction} + * * @see LogFile */ final class LogReplica implements AutoCloseable @@ -57,7 +59,7 @@ final class LogReplica implements AutoCloseable static LogReplica create(File directory, String fileName) { - int folderFD = NativeLibrary.tryOpenDirectory(directory.path()); + int folderFD = INativeLibrary.instance.tryOpenDirectory(directory); if (folderFD == -1 && REQUIRE_FD) { if (DatabaseDescriptor.isClientInitialized()) @@ -70,12 +72,12 @@ static LogReplica create(File directory, String fileName) } } - return new LogReplica(new File(fileName), folderFD); + return new LogReplica(directory.resolve(fileName), folderFD); } static LogReplica open(File file) { - int folderFD = NativeLibrary.tryOpenDirectory(file.parent().path()); + int folderFD = INativeLibrary.instance.tryOpenDirectory(file.parent()); if (folderFD == -1) { if (DatabaseDescriptor.isClientInitialized()) @@ -141,7 +143,7 @@ void syncDirectory() try { if (directoryDescriptor >= 0) - NativeLibrary.trySync(directoryDescriptor); + INativeLibrary.instance.trySync(directoryDescriptor); } catch (FSError e) { @@ -165,7 +167,7 @@ public void close() { if (directoryDescriptor >= 0) { - NativeLibrary.tryCloseFD(directoryDescriptor); + INativeLibrary.instance.tryCloseFD(directoryDescriptor); directoryDescriptor = -1; } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java b/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java index 5076a960d3c4..38db1aecd96b 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java @@ -29,6 +29,7 @@ import javax.annotation.concurrent.NotThreadSafe; import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.io.util.File; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,11 +42,13 @@ * A set of log replicas. This class mostly iterates over replicas when writing or reading, * ensuring consistency among them and hiding replication details from LogFile. * + * Note: this is used by {@link LogTransaction} + * * @see LogReplica * @see LogFile */ @NotThreadSafe -public class LogReplicaSet implements AutoCloseable +final class LogReplicaSet implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(LogReplicaSet.class); @@ -223,13 +226,28 @@ void printContentsWithAnyErrors(StringBuilder str) */ void append(LogRecord record) { - Throwable err = Throwables.perform(null, replicas().stream().map(r -> () -> r.append(record))); + Throwable err = null; + int failed = 0; + for (LogReplica replica : replicas()) + { + try + { + replica.append(record); + } + catch (Throwable t) + { + logger.warn("Failed to add record to a replica: {}", t.getMessage()); + err = Throwables.merge(err, t); + failed++; + } + } + if (err != null) { - if (!record.isFinal() || err.getSuppressed().length == replicas().size() -1) + if (!record.isFinal() || failed == replicas().size()) Throwables.maybeFail(err); - logger.error("Failed to add record '{}' to some replicas '{}'", record, this); + logger.error("Failed to add record '{}' to some replicas '{}'", record, this, err); } } @@ -267,8 +285,8 @@ List getFiles() } @VisibleForTesting - List getFilePaths() + List getFilePaths() { - return replicas().stream().map(LogReplica::file).map(File::path).collect(Collectors.toList()); + return replicas().stream().map(LogReplica::file).collect(Collectors.toList()); } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java index 74d8d6e84751..56b3fe4defc6 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java +++ b/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java @@ -29,13 +29,13 @@ import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; import java.util.function.Predicate; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.Runnables; - import com.codahale.metrics.Counter; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,14 +54,10 @@ import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Ref; import org.apache.cassandra.utils.concurrent.RefCounted; -import org.apache.cassandra.utils.concurrent.Transactional; - -import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; /** * IMPORTANT: When this object is involved in a transactional graph, and is not encapsulated in a LifecycleTransaction, @@ -98,7 +94,7 @@ * * See CASSANDRA-7066 for full details. */ -class LogTransaction extends Transactional.AbstractTransactional implements Transactional +final class LogTransaction extends AbstractLogTransaction { private static final Logger logger = LoggerFactory.getLogger(LogTransaction.class); @@ -117,7 +113,6 @@ public CorruptTransactionLogException(String message, LogFile txnFile) } } - private final Tracker tracker; private final LogFile txnFile; // We need an explicit lock because the transaction tidier cannot store a reference to the transaction private final Object lock; @@ -125,17 +120,14 @@ public CorruptTransactionLogException(String message, LogFile txnFile) // Deleting sstables is tricky because the mmapping might not have been finalized yet. // Additionally, we need to make sure to delete the data file first, so on restart the others // will be recognized as GCable. - private static final Queue failedDeletions = new ConcurrentLinkedQueue<>(); + protected static final Queue failedDeletions = new ConcurrentLinkedQueue<>(); - LogTransaction(OperationType opType) + LogTransaction(OperationType opType, TimeUUID uuid) { - this(opType, null); - } + Preconditions.checkNotNull(opType); + Preconditions.checkNotNull(uuid); - LogTransaction(OperationType opType, Tracker tracker) - { - this.tracker = tracker; - this.txnFile = new LogFile(opType, nextTimeUUID()); + this.txnFile = new LogFile(opType, uuid); this.lock = new Object(); this.selfRef = new Ref<>(this, new TransactionTidier(txnFile, lock)); @@ -146,7 +138,8 @@ public CorruptTransactionLogException(String message, LogFile txnFile) /** * Track a reader as new. **/ - void trackNew(SSTable table) + @Override + public void trackNew(SSTable table) { synchronized (lock) { @@ -160,7 +153,8 @@ void trackNew(SSTable table) /** * Stop tracking a reader as new. */ - void untrackNew(SSTable table) + @Override + public void untrackNew(SSTable table) { synchronized (lock) { @@ -168,19 +162,25 @@ void untrackNew(SSTable table) } } + @Override + public OperationType opType() + { + return txnFile.type(); + } + /** * helper method for tests, creates the remove records per sstable */ @VisibleForTesting - SSTableTidier obsoleted(SSTableReader sstable) + ReaderTidier obsoleted(SSTableReader sstable) { - return obsoleted(sstable, LogRecord.make(Type.REMOVE, sstable)); + return obsoleted(sstable, LogRecord.make(Type.REMOVE, sstable), null); } /** * Schedule a reader for deletion as soon as it is fully unreferenced. */ - SSTableTidier obsoleted(SSTableReader reader, LogRecord logRecord) + ReaderTidier obsoleted(SSTableReader reader, LogRecord logRecord, @Nullable Tracker tracker) { synchronized (lock) { @@ -192,7 +192,7 @@ SSTableTidier obsoleted(SSTableReader reader, LogRecord logRecord) if (txnFile.contains(Type.REMOVE, reader, logRecord)) throw new IllegalArgumentException(); - return new SSTableTidier(reader, true, this); + return new SSTableTidier(reader, true, this, tracker); } txnFile.addRecord(logRecord); @@ -200,7 +200,7 @@ SSTableTidier obsoleted(SSTableReader reader, LogRecord logRecord) if (tracker != null) tracker.notifyDeleting(reader); - return new SSTableTidier(reader, false, this); + return new SSTableTidier(reader, false, this, tracker); } } @@ -212,17 +212,40 @@ Map makeRemoveRecords(Iterable sstables) } } - - OperationType type() + @Override + public OperationType type() { return txnFile.type(); } - TimeUUID id() + @Override + public TimeUUID id() { return txnFile.id(); } + @Override + public Throwable prepareForObsoletion(Iterable readers, + List obsoletions, + Tracker tracker, + Throwable accumulate) + { + + Map logRecords = makeRemoveRecords(readers); + for (SSTableReader reader : readers) + { + try + { + obsoletions.add(new AbstractLogTransaction.Obsoletion(reader, obsoleted(reader, logRecords.get(reader), tracker))); + } + catch (Throwable t) + { + accumulate = Throwables.merge(accumulate, t); + } + } + return accumulate; + } + @VisibleForTesting LogFile txnFile() { @@ -236,7 +259,7 @@ List logFiles() } @VisibleForTesting - List logFilePaths() + List logFilePaths() { return txnFile.getFilePaths(); } @@ -246,7 +269,7 @@ static void delete(File file) try { if (!StorageService.instance.isDaemonSetupCompleted()) - logger.info("Unfinished transaction log, deleting {} ", file); + logger.debug("Unfinished transaction log, deleting {} ", file); else if (logger.isTraceEnabled()) logger.trace("Deleting {}", file); @@ -335,25 +358,13 @@ public void run() } } - static class Obsoletion - { - final SSTableReader reader; - final SSTableTidier tidier; - - Obsoletion(SSTableReader reader, SSTableTidier tidier) - { - this.reader = reader; - this.tidier = tidier; - } - } - /** * The SSTableReader tidier. When a reader is fully released and no longer referenced * by any one, we run this. It keeps a reference to the parent transaction and releases * it when done, so that the final transaction cleanup can run when all obsolete readers * are released. */ - public static class SSTableTidier implements Runnable + private static class SSTableTidier implements ReaderTidier { // must not retain a reference to the SSTableReader, else leak detection cannot kick in private final Descriptor desc; @@ -361,15 +372,17 @@ public static class SSTableTidier implements Runnable private final boolean wasNew; private final Object lock; private final Ref parentRef; + private final boolean onlineTxn; private final Counter totalDiskSpaceUsed; - public SSTableTidier(SSTableReader referent, boolean wasNew, LogTransaction parent) + public SSTableTidier(SSTableReader referent, boolean wasNew, LogTransaction parent, Tracker tracker) { this.desc = referent.descriptor; this.sizeOnDisk = referent.bytesOnDisk(); this.wasNew = wasNew; this.lock = parent.lock; this.parentRef = parent.selfRef.tryRef(); + this.onlineTxn = tracker != null && !tracker.isDummy(); if (this.parentRef == null) throw new IllegalStateException("Transaction already completed"); @@ -377,16 +390,15 @@ public SSTableTidier(SSTableReader referent, boolean wasNew, LogTransaction pare // While the parent cfs may be dropped in the interim of us taking a reference to this and using it, at worst // we'll be updating a metric for a now dropped ColumnFamilyStore. We do not hold a reference to the tracker or // cfs as that would create a strong ref loop and violate our ability to do leak detection. - totalDiskSpaceUsed = parent.tracker != null && parent.tracker.cfstore != null ? - parent.tracker.cfstore.metric.totalDiskSpaceUsed : + totalDiskSpaceUsed = tracker != null && tracker.cfstore != null ? + tracker.cfstore.metric.totalDiskSpaceUsed : null; } - public void run() + @Override + public void commit() { - // While this may be a dummy tracker w/out information in the metrics table, we attempt to delete regardless - // and allow the delete to silently fail if this is an invalid ks + cf combination at time of tidy run. - if (DatabaseDescriptor.isDaemonInitialized()) + if (onlineTxn && DatabaseDescriptor.supportsSSTableReadMeter()) SystemKeyspace.clearSSTableReadMeter(desc.ksname, desc.cfname, desc.id); synchronized (lock) @@ -394,9 +406,8 @@ public void run() try { // If we can't successfully delete the DATA component, set the task to be retried later: see TransactionTidier - if (logger.isTraceEnabled()) - logger.trace("Tidier running for old sstable {}", desc); + logger.trace("Tidier running for old sstable {}", desc.baseFileUri()); if (!desc.fileFor(Components.DATA).exists() && !wasNew) logger.error("SSTableTidier ran with no existing data file for an sstable that was not new"); @@ -406,7 +417,7 @@ public void run() catch (Throwable t) { logger.error("Failed deletion for {}, we'll retry after GC and on server restart", desc); - failedDeletions.add(this); + failedDeletions.add(this::commit); return; } @@ -421,11 +432,12 @@ public void run() } } - public void abort() + @Override + public Throwable abort(Throwable accumulate) { synchronized (lock) { - parentRef.release(); + return Throwables.perform(accumulate, parentRef::release); } } } @@ -438,11 +450,6 @@ static void rescheduleFailedDeletions() ScheduledExecutors.nonPeriodicTasks.submit(task); } - static void waitForDeletions() - { - FBUtilities.waitOnFuture(ScheduledExecutors.nonPeriodicTasks.schedule(Runnables.doNothing(), 0, TimeUnit.MILLISECONDS)); - } - @VisibleForTesting Throwable complete(Throwable accumulate) { @@ -561,7 +568,9 @@ static boolean removeUnfinishedLeftovers(Map.Entry> entry) } else { - logger.error("Unexpected disk state: failed to read transaction log {}", txn.toString(true)); + logger.error("Unexpected disk state: failed to read transaction log {}, " + + "check logs before last shutdown for any errors, and ensure txn log files were not edited manually.", + txn.toString(true)); return false; } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogTransactionsFactory.java b/src/java/org/apache/cassandra/db/lifecycle/LogTransactionsFactory.java new file mode 100644 index 000000000000..2a078c136fc6 --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/LogTransactionsFactory.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.lifecycle; + +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.TimeUUID; + +final class LogTransactionsFactory implements ILogTransactionsFactory +{ + @Override + public AbstractLogTransaction createLogTransaction(OperationType operationType, TimeUUID uuid, TableMetadataRef metadata) + { + logger.debug("Creating a transaction for {} on {}", operationType, metadata); + return new LogTransaction(operationType, uuid); + } + + @Override + public ILogAwareFileLister createLogAwareFileLister() + { + return new LogAwareFileLister(); + } + + @Override + public ILogFileCleaner createLogFileCleaner() + { + return new LogFileCleaner(); + } + + @Override + public FailedTransactionDeletionHandler createFailedTransactionDeletionHandler() + { + return LogTransaction::rescheduleFailedDeletions; + } +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/PartialLifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/PartialLifecycleTransaction.java new file mode 100644 index 000000000000..16c1cd4dfed7 --- /dev/null +++ b/src/java/org/apache/cassandra/db/lifecycle/PartialLifecycleTransaction.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.lifecycle; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.io.sstable.SSTable; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.TimeUUID; + +/// Partial lifecycle transaction. This works together with a CompositeLifecycleTransaction to allow for multiple +/// tasks using a shared transaction to be committed or aborted together. This is used to parallelize compaction +/// operations over the same sources. See [CompositeLifecycleTransaction] for more details. +/// +/// This class takes care of synchronizing various operations on the shared transaction, making sure that an abort +/// or commit signal is given exactly once (provided that this partial transaction is closed), and throwing an exception +/// when progress is made when the transaction was already aborted by another part. +public class PartialLifecycleTransaction implements ILifecycleTransaction +{ + final CompositeLifecycleTransaction composite; + final ILifecycleTransaction mainTransaction; + final AtomicBoolean committedOrAborted = new AtomicBoolean(false); + final TimeUUID id; + + public PartialLifecycleTransaction(CompositeLifecycleTransaction composite) + { + this.composite = composite; + this.mainTransaction = composite.mainTransaction; + this.id = composite.register(this); + } + + public void checkpoint() + { + // don't do anything, composite will checkpoint at end + } + + private RuntimeException earlyOpenUnsupported() + { + throw new UnsupportedOperationException("PartialLifecycleTransaction does not support early opening of SSTables"); + } + + public void update(SSTableReader reader, boolean original) + { + throwIfCompositeAborted(); + if (original) + throw earlyOpenUnsupported(); + + synchronized (mainTransaction) + { + mainTransaction.update(reader, original); + } + } + + public void update(Collection readers, boolean original) + { + throwIfCompositeAborted(); + if (original) + throw earlyOpenUnsupported(); + + synchronized (mainTransaction) + { + mainTransaction.update(readers, original); + } + } + + public SSTableReader current(SSTableReader reader) + { + synchronized (mainTransaction) + { + return mainTransaction.current(reader); + } + } + + public void obsolete(SSTableReader reader) + { + earlyOpenUnsupported(); + } + + public void obsoleteOriginals() + { + composite.requestObsoleteOriginals(); + } + + public Set originals() + { + return mainTransaction.originals(); + } + + public boolean isObsolete(SSTableReader reader) + { + throw earlyOpenUnsupported(); + } + + private boolean markCommittedOrAborted() + { + return committedOrAborted.compareAndSet(false, true); + } + + /// Commit the transaction part. Because this is a part of a composite transaction, the actual commit will be + /// carried out only after all parts have committed. + public Throwable commit(Throwable accumulate) + { + Throwables.maybeFail(accumulate); // we must be called with a null accumulate + if (markCommittedOrAborted()) + composite.commitPart(); + else + throw new IllegalStateException("Partial transaction already committed or aborted."); + return null; + } + + public Throwable abort(Throwable accumulate) + { + Throwables.maybeFail(accumulate); // we must be called with a null accumulate + if (markCommittedOrAborted()) + composite.abortPart(); + else + throw new IllegalStateException("Partial transaction already committed or aborted."); + return null; + } + + private void throwIfCompositeAborted() + { + if (composite.wasAborted()) + throw new AbortedException("Transaction aborted, likely by another partial operation."); + } + + public void prepareToCommit() + { + if (committedOrAborted.get()) + throw new IllegalStateException("Partial transaction already committed or aborted."); + + throwIfCompositeAborted(); + // nothing else to do, the composite transaction will perform the preparation when all parts are done + } + + public void close() + { + if (markCommittedOrAborted()) // close should abort if not committed + composite.abortPart(); + } + + public void trackNew(SSTable table) + { + throwIfCompositeAborted(); + synchronized (mainTransaction) + { + mainTransaction.trackNew(table); + } + } + + @Override + public void trackNewWritten(SSTable table) + { + throwIfCompositeAborted(); + // Beware: not synchronized. Thread safety shall be ensured in the main transaction trackNewWritten. + mainTransaction.trackNewWritten(table); + } + + @Override + public void trackNewAttachedIndexFiles(SSTable table) + { + throwIfCompositeAborted(); + synchronized (mainTransaction) + { + mainTransaction.trackNewAttachedIndexFiles(table); + } + } + + public void untrackNew(SSTable table) + { + synchronized (mainTransaction) + { + mainTransaction.untrackNew(table); + } + } + + public OperationType opType() + { + return mainTransaction.opType(); + } + + public boolean isOffline() + { + return mainTransaction.isOffline(); + } + + @Override + public TimeUUID opId() + { + return id; + } + + @Override + public String opIdString() + { + return String.format("%s (%d/%d)", id, TimeUUID.Generator.sequence(id), composite.partsCount()); + } + + @Override + public void cancel(SSTableReader removedSSTable) + { + synchronized (mainTransaction) + { + mainTransaction.cancel(removedSSTable); + } + } + + @Override + public String toString() + { + return opIdString(); + } + + public static class AbortedException extends RuntimeException + { + public AbortedException(String message) + { + super(message); + } + } +} diff --git a/src/java/org/apache/cassandra/db/lifecycle/SSTableIntervalTree.java b/src/java/org/apache/cassandra/db/lifecycle/SSTableIntervalTree.java index 4d5a87f3991d..dcd7035cab39 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/SSTableIntervalTree.java +++ b/src/java/org/apache/cassandra/db/lifecycle/SSTableIntervalTree.java @@ -27,6 +27,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.compaction.CompactionSSTable; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.utils.Interval; import org.apache.cassandra.utils.IntervalTree; @@ -65,23 +66,23 @@ public static SSTableIntervalTree buildSSTableIntervalTree(Collection> buildIntervals(Collection sstables) + public static List> buildIntervals(Collection sstables) { if (sstables == null || sstables.isEmpty()) return Collections.emptyList(); return Arrays.asList(buildIntervalsArray(sstables)); } - public static Interval[] buildIntervalsArray(Collection sstables) + public static Interval[] buildIntervalsArray(Collection sstables) { if (sstables == null || sstables.isEmpty()) return IntervalTree.EMPTY_ARRAY; - Interval[] intervals = new Interval[sstables.size()]; + Interval[] intervals = new Interval[sstables.size()]; int i = 0; int missingIntervals = 0; - for (SSTableReader sstable : sstables) + for (S sstable : sstables) { - Interval interval = sstable.getInterval(); + Interval interval = sstable.getInterval(); if (interval == null) { missingIntervals++; @@ -95,7 +96,7 @@ public static Interval[] buildIntervalsArray(C if (missingIntervals > 0) { checkState(DatabaseDescriptor.isToolInitialized(), "Can only safely build an interval tree on sstables with missing first and last for offline tools"); - Interval[] replacementIntervals = new Interval[intervals.length - missingIntervals]; + Interval[] replacementIntervals = new Interval[intervals.length - missingIntervals]; System.arraycopy(intervals, 0, replacementIntervals, 0, replacementIntervals.length); return replacementIntervals; } diff --git a/src/java/org/apache/cassandra/db/lifecycle/SSTableSet.java b/src/java/org/apache/cassandra/db/lifecycle/SSTableSet.java index 07a3b2b4999c..c1ccc3446a38 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/SSTableSet.java +++ b/src/java/org/apache/cassandra/db/lifecycle/SSTableSet.java @@ -28,5 +28,6 @@ public enum SSTableSet CANONICAL, // returns the live versions of all sstables, i.e. including partially written sstables LIVE, + // returns the non-compacting sstables, i.e. the difference between live and compacting ones NONCOMPACTING } diff --git a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java index ed674fb4be98..84315242c597 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java +++ b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java @@ -21,9 +21,12 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.ReentrantLock; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; @@ -38,10 +41,10 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.compaction.CompactionSSTable; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.metrics.StorageMetrics; @@ -52,13 +55,15 @@ import org.apache.cassandra.notifications.MemtableRenewedNotification; import org.apache.cassandra.notifications.MemtableSwitchedNotification; import org.apache.cassandra.notifications.SSTableAddedNotification; +import org.apache.cassandra.notifications.SSTableAddingNotification; import org.apache.cassandra.notifications.SSTableDeletingNotification; import org.apache.cassandra.notifications.SSTableListChangedNotification; -import org.apache.cassandra.notifications.SSTableMetadataChanged; import org.apache.cassandra.notifications.SSTableRepairStatusChanged; import org.apache.cassandra.notifications.TruncationNotification; +import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.OpOrder; import static com.google.common.base.Predicates.and; @@ -87,8 +92,10 @@ public class Tracker private static final Logger logger = LoggerFactory.getLogger(Tracker.class); private final List subscribers = new CopyOnWriteArrayList<>(); + private final List lateSubscribers = new CopyOnWriteArrayList<>(); public final ColumnFamilyStore cfstore; + public final TableMetadataRef metadata; // Constructing views update can be quite slow so locking generates less CPU/garbage compared to CAS final ReentrantLock viewUpdateLock = new ReentrantLock(true); @@ -96,43 +103,66 @@ public class Tracker public final boolean loadsstables; /** - * @param columnFamilyStore + * @param columnFamilyStore column family store for the table * @param memtable Initial Memtable. Can be null. * @param loadsstables true to indicate to load SSTables (TODO: remove as this is only accessed from 2i) */ public Tracker(ColumnFamilyStore columnFamilyStore, Memtable memtable, boolean loadsstables) { - this.cfstore = columnFamilyStore; + this.cfstore = Objects.requireNonNull(columnFamilyStore); + this.metadata = columnFamilyStore.metadata; this.loadsstables = loadsstables; this.reset(memtable); } - public static Tracker newDummyTracker() + /** + * @param metadata metadata reference for the table + * @param memtable Initial Memtable. Can be null. + * @param loadsstables true to indicate to load SSTables (TODO: remove as this is only accessed from 2i) + */ + public Tracker(TableMetadataRef metadata, Memtable memtable, boolean loadsstables) + { + this.cfstore = null; + this.metadata = Objects.requireNonNull(metadata); + this.loadsstables = loadsstables; + this.reset(memtable); + } + + public static Tracker newDummyTracker(TableMetadataRef metadata) { - return new Tracker(null, null, false); + return new Tracker(metadata, null, false); } public LifecycleTransaction tryModify(SSTableReader sstable, OperationType operationType) { - return tryModify(singleton(sstable), operationType); + return tryModify(singleton(sstable), operationType, LifecycleTransaction.newId()); + } + + public LifecycleTransaction tryModify(Iterable sstables, + OperationType operationType) + { + return tryModify(sstables, operationType, LifecycleTransaction.newId()); } /** * @return a Transaction over the provided sstables if we are able to mark the given @param sstables as compacted, before anyone else */ - public LifecycleTransaction tryModify(Iterable sstables, OperationType operationType) + public LifecycleTransaction tryModify(Iterable sstables, + OperationType operationType, + TimeUUID uuid) { if (Iterables.isEmpty(sstables)) - return new LifecycleTransaction(this, operationType, sstables); + return new LifecycleTransaction(this, operationType, sstables, uuid); if (null == apply(permitCompacting(sstables), updateCompacting(emptySet(), sstables))) return null; - return new LifecycleTransaction(this, operationType, sstables); + return new LifecycleTransaction(this, operationType, sstables, uuid); } // METHODS FOR ATOMICALLY MODIFYING THE VIEW - Pair apply(Function function) + @VisibleForTesting + public Pair apply(Function function) { return apply(Predicates.alwaysTrue(), function); } @@ -226,7 +256,7 @@ Throwable updateSizeTracking(Iterable oldSSTables, Iterable sstables) { - addSSTablesInternal(sstables, true, false, true); + addSSTablesInternal(sstables, OperationType.INITIAL_LOAD, true, false, true); } public void addInitialSSTablesWithoutUpdatingSize(Collection sstables) { - addSSTablesInternal(sstables, true, false, false); + addSSTablesInternal(sstables, OperationType.INITIAL_LOAD, true, false, false); } public void updateInitialSSTableSize(Iterable sstables) @@ -249,16 +279,18 @@ public void updateInitialSSTableSize(Iterable sstables) maybeFail(updateSizeTracking(emptySet(), sstables, null)); } - public void addSSTables(Collection sstables) + public void addSSTables(Collection sstables, OperationType operationType) { - addSSTablesInternal(sstables, false, true, true); + addSSTablesInternal(sstables, operationType, false, true, true); } private void addSSTablesInternal(Collection sstables, + OperationType operationType, boolean isInitialSSTables, boolean maybeIncrementallyBackup, boolean updateSize) { + notifyAdding(sstables, operationType); if (!isDummy()) setupOnline(sstables); apply(updateLiveSet(emptySet(), sstables)); @@ -266,7 +298,7 @@ private void addSSTablesInternal(Collection sstables, maybeFail(updateSizeTracking(emptySet(), sstables, null)); if (maybeIncrementallyBackup) maybeIncrementallyBackup(sstables); - notifyAdded(sstables, isInitialSSTables); + notifyAdded(sstables, operationType, isInitialSSTables); } /** (Re)initializes the tracker, purging all references. */ @@ -288,10 +320,22 @@ public void reset(Memtable memtable) } } - public Throwable dropSSTablesIfInvalid(Throwable accumulate) + public Throwable dropOrUnloadSSTablesIfInvalid(String message, @Nullable Throwable accumulate) { if (!isDummy() && !cfstore.isValid()) - accumulate = dropSSTables(accumulate); + { + ColumnFamilyStore.STATUS status = cfstore.status(); + if (status.isInvalidAndShouldDropData()) + { + logger.info("Dropping sstables for invalidated table {} with status {} {}", metadata.toString(), status, message); + return dropSSTables(accumulate); + } + else + { + logger.info("Unloading sstables for invalidated table {} with status {} {}", metadata.toString(), status, message); + return unloadSSTables(accumulate); + } + } return accumulate; } @@ -302,7 +346,7 @@ public void dropSSTables() public Throwable dropSSTables(Throwable accumulate) { - return dropSSTables(Predicates.alwaysTrue(), OperationType.UNKNOWN, accumulate); + return dropSSTables(Predicates.alwaysTrue(), OperationType.DROP_TABLE, accumulate); } /** @@ -310,7 +354,12 @@ public Throwable dropSSTables(Throwable accumulate) */ public Throwable dropSSTables(final Predicate remove, OperationType operationType, Throwable accumulate) { - try (LogTransaction txnLogs = new LogTransaction(operationType, this)) + logger.debug("Dropping sstables for {} with operation {}: {}", + metadata.name, operationType, accumulate == null ? "null" : accumulate.getMessage()); + + try (AbstractLogTransaction txnLogs = ILogTransactionsFactory.instance.createLogTransaction(operationType, + LifecycleTransaction.newId(), + metadata)) { Pair result = apply(view -> { Set toremove = copyOf(filter(view.sstables, and(remove, notIn(view.compacting)))); @@ -322,8 +371,8 @@ public Throwable dropSSTables(final Predicate remove, OperationTy // It is important that any method accepting/returning a Throwable never throws an exception, and does its best // to complete the instructions given to it - List obsoletions = new ArrayList<>(); - accumulate = prepareForObsoletion(removed, txnLogs, obsoletions, accumulate); + List obsoletions = new ArrayList<>(); + accumulate = prepareForObsoletion(removed, txnLogs, obsoletions, this, accumulate); try { txnLogs.finish(); @@ -333,12 +382,32 @@ public Throwable dropSSTables(final Predicate remove, OperationTy accumulate = updateSizeTracking(removed, emptySet(), accumulate); accumulate = release(selfRefs(removed), accumulate); // notifySSTablesChanged -> LeveledManifest.promote doesn't like a no-op "promotion" - accumulate = notifySSTablesChanged(removed, Collections.emptySet(), txnLogs.type(), accumulate); + accumulate = notifySSTablesChanged(removed, Collections.emptySet(), txnLogs.opType(), Optional.of(txnLogs.id()), accumulate); } } catch (Throwable t) { - accumulate = abortObsoletion(obsoletions, accumulate); + logger.error("Failed to commit transaction for obsoleting sstables of {}", metadata.name, t); + Throwable err = abortObsoletion(obsoletions, null); + if (err == null && cfstore != null && cfstore.isValid()) + { + // if the obsoletions were cancelled and the table is still valid, i.e. not dropped, restore the sstables since they are valid, and for CNDB they are in etcd as well + err = apply(updateLiveSet(emptySet(), removed), accumulate); + } + else if (cfstore != null && !cfstore.isValid()) + { + // if the table is invalid, i.e. dropped, send in the notifications anyway because otherwise CNDB etcd does not get updated + err = notifySSTablesChanged(removed, Collections.emptySet(), txnLogs.opType(), Optional.of(txnLogs.id()), err); + } + else + { + // cfstore should always be != null and either valid or not, so we get here only in case err != null + logger.error("Failed to abort obsoletions for {}, some sstables will be missing from liveset", metadata.name, err); + } + + if (err != null) + accumulate = Throwables.merge(accumulate, err); + accumulate = Throwables.merge(accumulate, t); } } @@ -347,9 +416,30 @@ public Throwable dropSSTables(final Predicate remove, OperationTy accumulate = Throwables.merge(accumulate, t); } + logger.debug("Sstables for {} dropped with operation {}: {}", + metadata.name, operationType, accumulate == null ? "null" : accumulate.getMessage()); return accumulate; } + /** + * Unload all sstables from current tracker without deleting files + */ + public void unloadSSTables() + { + maybeFail(unloadSSTables(null)); + } + + public Throwable unloadSSTables(@Nullable Throwable accumulate) + { + Pair result = apply(view -> { + Set toUnload = copyOf(filter(view.sstables, notIn(view.compacting))); + return updateLiveSet(toUnload, emptySet()).apply(view); + }); + + // compacting sstables will be cleaned up by their transaction in {@link LifecycleTransaction#unmarkCompacting} + Set toRelease = Sets.difference(result.left.sstables, result.right.sstables); + return release(selfRefs(toRelease), accumulate); + } /** * Removes every SSTable in the directory from the Tracker's view. @@ -357,7 +447,7 @@ public Throwable dropSSTables(final Predicate remove, OperationTy */ public void removeUnreadableSSTables(final File directory) { - maybeFail(dropSSTables(reader -> reader.descriptor.directory.equals(directory), OperationType.UNKNOWN, null)); + maybeFail(dropSSTables(reader -> reader.descriptor.directory.equals(directory), OperationType.REMOVE_UNREADEABLE, null)); } @@ -409,7 +499,7 @@ public void markFlushing(Memtable memtable) apply(View.markFlushing(memtable)); } - public void replaceFlushed(Memtable memtable, Collection sstables) + public void replaceFlushed(Memtable memtable, Collection sstables, Optional operationId) { assert !isDummy(); if (Iterables.isEmpty(sstables)) @@ -424,19 +514,20 @@ public void replaceFlushed(Memtable memtable, Collection sstables // back up before creating a new Snapshot (which makes the new one eligible for compaction) maybeIncrementallyBackup(sstables); + Throwable fail; + fail = notifyAdding(sstables, memtable, null, OperationType.FLUSH, operationId); + apply(View.replaceFlushed(memtable, sstables)); - Throwable fail; - fail = updateSizeTracking(emptySet(), sstables, null); + fail = updateSizeTracking(emptySet(), sstables, fail); // TODO: if we're invalidated, should we notifyadded AND removed, or just skip both? - fail = notifyAdded(sstables, false, memtable, fail); + fail = notifyAdded(sstables, OperationType.FLUSH, operationId, false, memtable, fail); - // make sure index sees flushed index files before dicarding memtable index + // make sure SAI sees newly flushed index files before discarding memtable index notifyDiscarded(memtable); - if (!isDummy() && !cfstore.isValid()) - dropSSTables(); + fail = dropOrUnloadSSTablesIfInvalid("during flush", fail); maybeFail(fail); } @@ -450,14 +541,26 @@ public Set getCompacting() return view.compacting; } - public Iterable getUncompacting() + public Iterable getNoncompacting() { return view.select(SSTableSet.NONCOMPACTING); } - public Iterable getUncompacting(Iterable candidates) + public Iterable getNoncompacting(Iterable candidates) { - return view.getUncompacting(candidates); + return view.getNoncompacting(candidates); + } + + public Set getLiveSSTables() + { + return view.liveSSTables(); + } + + // used by CNDB + @Nullable + public SSTableReader getLiveSSTable(String filename) + { + return view.getLiveSSTable(filename); } public void maybeIncrementallyBackup(final Iterable sstables) @@ -474,79 +577,53 @@ public void maybeIncrementallyBackup(final Iterable sstables) // NOTIFICATION - Throwable notifySSTablesChanged(Collection removed, Collection added, OperationType compactionType, Throwable accumulate) + public Throwable notifySSTablesChanged(Collection removed, Collection added, OperationType operationType, Optional operationId, Throwable accumulate) { - INotification notification = new SSTableListChangedNotification(added, removed, compactionType); - for (INotificationConsumer subscriber : subscribers) - { - try - { - subscriber.handleNotification(notification, this); - } - catch (Throwable t) - { - accumulate = merge(accumulate, t); - } - } - return accumulate; + return notify(new SSTableListChangedNotification(added, removed, operationType, operationId), accumulate); } - Throwable notifyAdded(Iterable added, boolean isInitialSSTables, Memtable memtable, Throwable accumulate) + Throwable notifyAdded(Iterable added, OperationType operationType, Optional operationId, boolean isInitialSSTables, Memtable memtable, Throwable accumulate) { INotification notification; if (!isInitialSSTables) - notification = new SSTableAddedNotification(added, memtable); + notification = new SSTableAddedNotification(added, memtable, operationType, operationId); else notification = new InitialSSTableAddedNotification(added); - for (INotificationConsumer subscriber : subscribers) - { - try - { - subscriber.handleNotification(notification, this); - } - catch (Throwable t) - { - accumulate = merge(accumulate, t); - } - } - return accumulate; + return notify(notification, accumulate); } - void notifyAdded(Iterable added, boolean isInitialSSTables) + Throwable notifyAdding(Iterable added, @Nullable Memtable memtable, Throwable accumulate, OperationType type, Optional operationId) { - maybeFail(notifyAdded(added, isInitialSSTables, null, null)); + return notify(new SSTableAddingNotification(added, memtable, type, operationId), accumulate); } - public void notifySSTableRepairedStatusChanged(Collection repairStatusesChanged) + public void notifyAdding(Iterable added, OperationType operationType) { - if (repairStatusesChanged.isEmpty()) - return; - INotification notification = new SSTableRepairStatusChanged(repairStatusesChanged); - for (INotificationConsumer subscriber : subscribers) - subscriber.handleNotification(notification, this); + maybeFail(notifyAdding(added, null, null, operationType, Optional.empty())); } - public void notifySSTableMetadataChanged(SSTableReader levelChanged, StatsMetadata oldMetadata) + @VisibleForTesting + public void notifyAdded(Iterable added, OperationType operationType, boolean isInitialSSTables) { - INotification notification = new SSTableMetadataChanged(levelChanged, oldMetadata); - for (INotificationConsumer subscriber : subscribers) - subscriber.handleNotification(notification, this); + maybeFail(notifyAdded(added, operationType, Optional.empty(), isInitialSSTables, null, null)); + } + public void notifySSTableRepairedStatusChanged(Collection repairStatusesChanged) + { + if (repairStatusesChanged.isEmpty()) + return; + notify(new SSTableRepairStatusChanged(repairStatusesChanged)); } public void notifyDeleting(SSTableReader deleting) { - INotification notification = new SSTableDeletingNotification(deleting); - for (INotificationConsumer subscriber : subscribers) - subscriber.handleNotification(notification, this); + notify(new SSTableDeletingNotification(deleting)); } - public void notifyTruncated(long truncatedAt) + public void notifyTruncated(CommitLogPosition replayAfter, long truncatedAt) { - INotification notification = new TruncationNotification(truncatedAt); - for (INotificationConsumer subscriber : subscribers) - subscriber.handleNotification(notification, this); + notify(new TruncationNotification(replayAfter, truncatedAt)); } public void notifyRenewed(Memtable renewed) @@ -565,30 +642,72 @@ public void notifyDiscarded(Memtable discarded) } private void notify(INotification notification) + { + maybeFail(notify(notification, null)); + } + + private Throwable notify(INotification notification, @Nullable Throwable accumulate) { for (INotificationConsumer subscriber : subscribers) + accumulate = notifyOne(subscriber, notification, accumulate); + for (INotificationConsumer subscriber : lateSubscribers) + accumulate = notifyOne(subscriber, notification, accumulate); + return accumulate; + } + + private Throwable notifyOne(INotificationConsumer subscriber, INotification notification, @Nullable Throwable accumulate) + { + try + { subscriber.handleNotification(notification, this); + return accumulate; + } + catch (Throwable t) + { + return merge(accumulate, t); + } } public boolean isDummy() { - return cfstore == null || !DatabaseDescriptor.isDaemonInitialized(); + return cfstore == null || !DatabaseDescriptor.enableMemtableAndCommitLog(); } public void subscribe(INotificationConsumer consumer) { subscribers.add(consumer); + if (logger.isTraceEnabled()) + logger.trace("{} subscribed to the data tracker.", consumer); + } + + /** + * Subscribes the provided consumer for data tracker notifications, similarly to {@link #subscribe}, but the + * consumer subscribed by this method are guaranteed to be notificed _after_ all the consumers subscribed with + * {@link #subscribe}. + *

    + * The consumers registered by this method are notified in order of subscription (with not particular guarantee + * in case of concurrent calls), but again, they all execute after those of {@link #subscribe}. + *

    + * This method is mainly targeted for non-Cassandra internal subscribers that want to register for notifications + * but need to make sure they are notified only after all the Cassandra internal subscribers have executed. + */ + public void subscribeLateConsumer(INotificationConsumer consumer) + { + lateSubscribers.add(consumer); + if (logger.isTraceEnabled()) + logger.trace("{} subscribed to the data tracker (as a 'late' consumer).", consumer); } @VisibleForTesting public boolean contains(INotificationConsumer consumer) { - return subscribers.contains(consumer); + return subscribers.contains(consumer) || lateSubscribers.contains(consumer); } public void unsubscribe(INotificationConsumer consumer) { subscribers.remove(consumer); + lateSubscribers.remove(consumer); } private static Set emptySet() @@ -604,6 +723,12 @@ public View getView() @VisibleForTesting public void removeUnsafe(Set toRemove) { - Pair result = apply(view -> updateLiveSet(toRemove, emptySet()).apply(view)); + apply(view -> updateLiveSet(toRemove, emptySet()).apply(view)); + } + + @VisibleForTesting + public void removeCompactingUnsafe(Set toRemove) + { + apply(view -> updateCompacting(toRemove, emptySet()).apply(view)); } } diff --git a/src/java/org/apache/cassandra/db/lifecycle/View.java b/src/java/org/apache/cassandra/db/lifecycle/View.java index ba200d5d0bc1..171d37bc1e13 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/View.java +++ b/src/java/org/apache/cassandra/db/lifecycle/View.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Functions; @@ -31,8 +33,13 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.compaction.CompactionSSTable; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -61,6 +68,8 @@ */ public class View { + private static final Logger logger = LoggerFactory.getLogger(View.class); + /** * ordinarily a list of size 1, but when preparing to flush will contain both the memtable we will flush * and the new replacement memtable, until all outstanding write operations on the old table complete. @@ -74,6 +83,7 @@ public class View public final List flushingMemtables; final Set compacting; final Set sstables; + final Map sstablesByFilename; // we use a Map here so that we can easily perform identity checks as well as equality checks. // When marking compacting, we now indicate if we expect the sstables to be present (by default we do), // and we then check that not only are they all present in the live set, but that the exact instance present is @@ -99,6 +109,9 @@ public class View this.compactingMap = compacting; this.compacting = compactingMap.keySet(); this.intervalTree = intervalTree; + this.sstablesByFilename = Maps.newHashMapWithExpectedSize(sstables.size()); + for (SSTableReader sstable : this.sstables) + this.sstablesByFilename.put(sstable.getDataFile().name(), sstable); } public Memtable getCurrentMemtable() @@ -120,6 +133,15 @@ public Set liveSSTables() return sstables; } + @Nullable + /** + * @return the sstable with the provided file name (not a full path), or null if it is not present in this view + */ + public SSTableReader getLiveSSTable(String filename) + { + return sstablesByFilename.get(filename); + } + public Iterable sstables(SSTableSet sstableSet, Predicate filter) { return filter(select(sstableSet), filter); @@ -176,15 +198,11 @@ public Iterable select(SSTableSet sstableSet) } } - public Iterable getUncompacting(Iterable candidates) + + public + Iterable getNoncompacting(Iterable candidates) { - return filter(candidates, new Predicate() - { - public boolean apply(SSTableReader sstable) - { - return !compacting.contains(sstable); - } - }); + return filter(candidates, sstable -> !compacting.contains(sstable)); } public boolean isEmpty() @@ -264,7 +282,8 @@ public static Function> selectLive(AbstractBounds< // METHODS TO CONSTRUCT FUNCTIONS FOR MODIFYING A VIEW: // return a function to un/mark the provided readers compacting in a view - static Function updateCompacting(final Set unmark, final Iterable mark) + @VisibleForTesting + public static Function updateCompacting(final Set unmark, final Iterable mark) { if (unmark.isEmpty() && Iterables.isEmpty(mark)) return Functions.identity(); @@ -290,7 +309,11 @@ public boolean apply(View view) { for (SSTableReader reader : readers) if (view.compacting.contains(reader) || view.sstablesMap.get(reader) != reader || reader.isMarkedCompacted()) + { + logger.debug("Refusing to compact {}, already compacting={}, suspect={}, compacted={}", reader, + view.compacting.contains(reader), reader.isMarkedSuspect(), reader.isMarkedCompacted()); return false; + } return true; } }; diff --git a/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java index 12c46a9573ca..841ba4e682b9 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java +++ b/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java @@ -24,11 +24,12 @@ import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; public class WrappedLifecycleTransaction implements ILifecycleTransaction { - final ILifecycleTransaction delegate; + protected final ILifecycleTransaction delegate; public WrappedLifecycleTransaction(ILifecycleTransaction delegate) { this.delegate = delegate; @@ -99,6 +100,12 @@ public void trackNew(SSTable table) delegate.trackNew(table); } + @Override + public void trackNewWritten(SSTable table) + { + delegate.trackNewWritten(table); + } + public void untrackNew(SSTable table) { delegate.untrackNew(table); @@ -113,4 +120,16 @@ public boolean isOffline() { return delegate.isOffline(); } + + @Override + public TimeUUID opId() + { + return delegate.opId(); + } + + @Override + public void cancel(SSTableReader removedSSTable) + { + delegate.cancel(removedSSTable); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java index 27a67cdfbb66..1b5778e1f925 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.regex.Pattern; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.Term; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.transport.ProtocolVersion; @@ -36,9 +38,9 @@ */ public abstract class AbstractCompositeType extends AbstractType { - protected AbstractCompositeType() + protected AbstractCompositeType(ImmutableList> subTypes) { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, false, subTypes); } @Override @@ -66,8 +68,8 @@ public int compareCustom(VL left, ValueAccessor accessorL, VR right while (!accessorL.isEmptyFromOffset(left, offsetL) && !accessorR.isEmptyFromOffset(right, offsetR)) { AbstractType comparator = getComparator(i, left, accessorL, right, accessorR, offsetL, offsetR); - offsetL += getComparatorSize(i, left, accessorL, offsetL); - offsetR += getComparatorSize(i, right, accessorR, offsetR); + offsetL += getComparatorSize(left, accessorL, offsetL); + offsetR += getComparatorSize(right, accessorR, offsetR); VL value1 = accessorL.sliceWithShortLength(left, offsetL); offsetL += accessorL.sizeWithShortLength(value1); @@ -110,10 +112,9 @@ public ByteBuffer[] split(ByteBuffer bb) boolean isStatic = readIsStatic(bb, ByteBufferAccessor.instance); int offset = startingOffset(isStatic); - int i = 0; while (!ByteBufferAccessor.instance.isEmptyFromOffset(bb, offset)) { - offset += getComparatorSize(i++, bb, ByteBufferAccessor.instance, offset); + offset += getComparatorSize(bb, ByteBufferAccessor.instance, offset); ByteBuffer value = ByteBufferAccessor.instance.sliceWithShortLength(bb, offset); offset += ByteBufferAccessor.instance.sizeWithShortLength(value); l.add(value); @@ -192,7 +193,7 @@ public String getString(V input, ValueAccessor accessor) sb.append(":"); AbstractType comparator = getAndAppendComparator(i, input, accessor, sb, offset); - offset += getComparatorSize(i, input, accessor, offset); + offset += getComparatorSize(input, accessor, offset); V value = accessor.sliceWithShortLength(input, offset); offset += accessor.sizeWithShortLength(value); @@ -291,7 +292,7 @@ public void validate(V input, ValueAccessor accessor) while (!accessor.isEmptyFromOffset(input, offset)) { AbstractType comparator = validateComparator(i, input, accessor, offset); - offset += getComparatorSize(i, input, accessor, offset); + offset += getComparatorSize(input, accessor, offset); if (accessor.sizeFromOffset(input, offset) < 2) throw new MarshalException("Not enough bytes to read value size of component " + i); @@ -318,7 +319,7 @@ public void validate(V input, ValueAccessor accessor) public abstract ByteBuffer decompose(Object... objects); - abstract protected int getComparatorSize(int i, V value, ValueAccessor accessor, int offset); + abstract protected int getComparatorSize(V value, ValueAccessor accessor, int offset); /** * @return the comparator for the given component. static CompositeType will consult * @param i DynamicCompositeType will read the type information from @param bb diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractGeometricType.java b/src/java/org/apache/cassandra/db/marshal/AbstractGeometricType.java new file mode 100644 index 000000000000..6c46ddaae06e --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/AbstractGeometricType.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.db.marshal.geometry.GeometricType; +import org.apache.cassandra.db.marshal.geometry.OgcGeometry; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TypeSerializer; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.JsonUtils; + +public abstract class AbstractGeometricType extends AbstractType +{ + private final TypeSerializer serializer = new TypeSerializer() + { + @Override + public ByteBuffer serialize(T geometry) + { + return geoSerializer.toWellKnownBinary(geometry); + } + + @Override + public T deserialize(V value, ValueAccessor accessor) + { + // OGCGeometry does not respect the current position of the buffer, so you need to use slice() + try + { + ByteBuffer byteBuffer = accessor.toBuffer(value); + return geoSerializer.fromWellKnownBinary(byteBuffer.slice()); + } + catch (IndexOutOfBoundsException ex) + { + throw new MarshalException("Not enough bytes to deserialize value", ex); + } + } + + @Override + public void validate(V value, ValueAccessor accessor) throws MarshalException + { + try + { + ByteBuffer byteBuffer = accessor.toBuffer(value); + int pos = byteBuffer.position(); + // OGCGeometry does not respect the current position of the buffer, so you need to use slice() + geoSerializer.fromWellKnownBinary(byteBuffer.slice()).validate(); + byteBuffer.position(pos); + } + catch (IndexOutOfBoundsException ex) + { + throw new MarshalException("Not enough bytes to deserialize value", ex); + } + } + + @Override + public String toString(T geometry) + { + return geoSerializer.toWellKnownText(geometry); + } + + @Override + public Class getType() + { + return klass; + } + }; + + private final GeometricType type; + private final Class klass; + private final OgcGeometry.Serializer geoSerializer; + + public AbstractGeometricType(GeometricType type) + { + super(ComparisonType.BYTE_ORDER); + this.type = type; + this.klass = (Class) type.getGeoClass(); + this.geoSerializer = type.getSerializer(); + } + + public GeometricType getGeoType() + { + return type; + } + + @Override + public ByteBuffer fromString(String s) throws MarshalException + { + try + { + T geometry = geoSerializer.fromWellKnownText(s); + geometry.validate(); + return geoSerializer.toWellKnownBinary(geometry); + } + catch (Exception e) + { + String parentMsg = e.getMessage() != null ? " " + e.getMessage() : ""; + String msg = String.format("Unable to make %s from '%s'", getClass().getSimpleName(), s) + parentMsg; + throw new MarshalException(msg, e); + } + } + + @Override + public Term fromJSONObject(Object parsed) throws MarshalException + { + if (!(parsed instanceof String)) + { + try + { + parsed = JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(parsed); + } + catch (IOException e) + { + throw new MarshalException(e.getMessage()); + } + } + + T geometry; + try + { + geometry = geoSerializer.fromGeoJson((String) parsed); + } + catch (MarshalException e) + { + try + { + geometry = geoSerializer.fromWellKnownText((String) parsed); + } + catch (MarshalException ignored) + { + throw new MarshalException(e.getMessage()); + } + } + geometry.validate(); + return new Constants.Value(geoSerializer.toWellKnownBinary(geometry)); + } + + @Override + public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) + { + // OGCGeometry does not respect the current position of the buffer, so you need to use slice() + return geoSerializer.toGeoJson(geoSerializer.fromWellKnownBinary(buffer.slice())); + } + + @Override + public TypeSerializer getSerializer() + { + return serializer; + } + +} diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java b/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java index 35778aff24ba..0cd59279834d 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java @@ -98,7 +98,7 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteC swizzled.putLong(0, TimeUUIDType.reorderTimestampBytes(hiBits)); swizzled.putLong(8, accessor.getLong(data, 8) ^ 0x8080808080808080L); - return ByteSource.fixedLength(swizzled); + return ByteSource.preencoded(swizzled); } @Override @@ -224,8 +224,14 @@ public ByteBuffer now() } @Override - public boolean equals(Object obj) + public final boolean equals(Object obj) { return obj instanceof AbstractTimeUUIDType; } + + @Override + public final int hashCode() + { + return AbstractTimeUUIDType.class.hashCode(); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index fe8b5498372b..4919f0fd33ed 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -21,22 +21,36 @@ import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; +import org.apache.cassandra.cql3.statements.schema.AlterTableStatement; import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.exceptions.InvalidColumnTypeException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; @@ -46,11 +60,12 @@ import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; import org.github.jamm.Unmetered; +import static com.google.common.collect.Iterables.transform; import static org.apache.cassandra.db.marshal.AbstractType.ComparisonType.CUSTOM; /** * Specifies a Comparator for a specific type of ByteBuffer. - * + *

    * Note that empty ByteBuffer are used to represent "start at the beginning" * or "stop at the end" arguments to get_slice, so the Comparator * should always handle those values even if they normally do not @@ -59,9 +74,9 @@ @Unmetered public abstract class AbstractType implements Comparator, AssignmentTestable { - private final static int VARIABLE_LENGTH = -1; + private final static Logger logger = LoggerFactory.getLogger(AbstractType.class); - public final Comparator reverseComparator; + private final static int VARIABLE_LENGTH = -1; public enum ComparisonType { @@ -84,12 +99,41 @@ public enum ComparisonType public final ComparisonType comparisonType; public final boolean isByteOrderComparable; public final ValueComparators comparatorSet; + public final boolean isMultiCell; + public final ImmutableList> subTypes; + + private final int hashCode; protected AbstractType(ComparisonType comparisonType) { + this(comparisonType, false, ImmutableList.of()); + } + + protected AbstractType(ComparisonType comparisonType, boolean isMultiCell, ImmutableList> subTypes) + { + this.isMultiCell = isMultiCell; this.comparisonType = comparisonType; this.isByteOrderComparable = comparisonType == ComparisonType.BYTE_ORDER; - reverseComparator = (o1, o2) -> AbstractType.this.compare(o2, o1); + + // A frozen type can only have frozen subtypes, basically by definition. So make sure we don't mess it up + // when constructing types by forgetting to set some multi-cell flag. + if (!isMultiCell) + { + if (Iterables.any(subTypes, AbstractType::isMultiCell)) + this.subTypes = ImmutableList.copyOf(Iterables.transform(subTypes, AbstractType::freeze)); + else + this.subTypes = subTypes; + } + else + { + this.subTypes = subTypes; + } + if (subTypes != this.subTypes) + logger.warn("Detected corrupted type: creating a frozen {} but with some non-frozen subtypes {}. " + + "This is likely a bug and should be reported.", + getClass(), + subTypes.stream().filter(AbstractType::isMultiCell).map(AbstractType::toString).collect(Collectors.joining(", "))); + try { Method custom = getClass().getMethod("compareCustom", Object.class, ValueAccessor.class, Object.class, ValueAccessor.class); @@ -103,8 +147,23 @@ protected AbstractType(ComparisonType comparisonType) throw new IllegalStateException(); } - comparatorSet = new ValueComparators((l, r) -> compare(l, ByteArrayAccessor.instance, r, ByteArrayAccessor.instance), - (l, r) -> compare(l, ByteBufferAccessor.instance, r, ByteBufferAccessor.instance)); + comparatorSet = new ValueComparators(new Comparator<>() + { + @Override + public int compare(byte[] l, byte[] r) + { + return AbstractType.this.compare(l, ByteArrayAccessor.instance, r, ByteArrayAccessor.instance); + } + }, new Comparator<>() + { + @Override + public int compare(ByteBuffer l, ByteBuffer r) + { + return AbstractType.this.compare(l, ByteBufferAccessor.instance, r, ByteBufferAccessor.instance); + } + }); + + hashCode = Objects.hash(getClass(), this.isMultiCell, this.subTypes); } static > int compareComposed(VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR, AbstractType type) @@ -133,6 +192,8 @@ public T compose(V value, ValueAccessor accessor) return getSerializer().deserialize(value, accessor); } + @SuppressWarnings("unchecked") + @VisibleForTesting public ByteBuffer decomposeUntyped(Object value) { return decompose((T) value); @@ -143,6 +204,23 @@ public ByteBuffer decompose(T value) return getSerializer().serialize(value); } + /** + * Returns a CQL literal representing the specified binary value, or "?" if redaction is requested. + * + * @param bytes the value to convert to a CQL literal + * @param redaction whether to mask the value with '?' (for redaction purposes) + */ + public String toCQLString(ByteBuffer bytes, Redaction redaction) + { + if (redaction == Redaction.REDACT) + return RedactionUtil.redact(bytes, isValueLengthFixed()); + + if (bytes == null) + return "null"; + + return asCQL3Type().toCQLLiteral(bytes); + } + /** get a string representation of the bytes used for various identifier (NOT just for log messages) */ public String getString(V value, ValueAccessor accessor) { @@ -160,9 +238,38 @@ public final String getString(ByteBuffer bytes) return getString(bytes, ByteBufferAccessor.instance); } + public final String getString(ByteBuffer bytes, Redaction redaction) + { + if (redaction == Redaction.REDACT) + return RedactionUtil.redact(bytes, isValueLengthFixed()); + + return getString(bytes, ByteBufferAccessor.instance); + } + + public final String getString(ByteBuffer bytes, boolean truncate) + { + String s = getString(bytes); + return truncate ? truncateString(s) : s; + } + + public String toCQLString(ByteBuffer bytes, boolean redact) + { + return toCQLString(bytes, redact ? Redaction.REDACT : Redaction.NONE); + } + + /** + * Generates a CQL literal representing the specified binary value. + * + * @param bytes the value to convert to a CQL literal. + */ public String toCQLString(ByteBuffer bytes) { - return asCQL3Type().toCQLLiteral(bytes); + return bytes == null ? "null" : asCQL3Type().toCQLLiteral(bytes); + } + + private static String truncateString(String valueString) + { + return valueString.length() <= 9 ? valueString : valueString.substring(0, 6) + "..."; } /** get a byte representation of the given string. */ @@ -267,6 +374,12 @@ public int compareForCQL(ByteBuffer v1, ByteBuffer v2) return compare(v1, v2); } + /** + * Returns the serializer for this type. + * Note that the method must return a different instance of serializer for different types even if the types + * use the same serializer - in this case, the method should return separate instances for which equals() returns + * false. + */ public abstract TypeSerializer getSerializer(); /** @@ -277,27 +390,11 @@ public ArgumentDeserializer getArgumentDeserializer() return new DefaultArgumentDeserializer(this); } - /* convenience method */ - public String getString(Collection names) - { - StringBuilder builder = new StringBuilder(); - for (ByteBuffer name : names) - { - builder.append(getString(name)).append(","); - } - return builder.toString(); - } - public boolean isCounter() { return false; } - public boolean isFrozenCollection() - { - return isCollection() && !isMultiCell(); - } - public boolean isReversed() { return false; @@ -308,6 +405,11 @@ public AbstractType unwrap() return isReversed() ? ((ReversedType) this).baseType.unwrap() : this; } + public boolean isList() + { + return false; + } + public static AbstractType parseDefaultParameters(AbstractType baseType, TypeParser parser) throws SyntaxException { Map parameters = parser.getKeyValueParameters(); @@ -323,14 +425,20 @@ public static AbstractType parseDefaultParameters(AbstractType baseType, T } /** - * Returns true if this comparator is compatible with the provided - * previous comparator, that is if previous can safely be replaced by this. + * Returns true if this comparator is compatible with the provided previous comparator, that is if previous can + * safely be replaced by this. * A comparator cn should be compatible with a previous one cp if forall columns c1 and c2, * if cn.validate(c1) and cn.validate(c2) and cn.compare(c1, c2) == v, * then cp.validate(c1) and cp.validate(c2) and cp.compare(c1, c2) == v. - * - * Note that a type should be compatible with at least itself and when in - * doubt, keep the default behavior of not being compatible with any other comparator! + *

    + * Note that a type should be compatible with at least itself and when in doubt, keep the default behavior + * of not being compatible with any other comparator! + *

    + * Used for user functions and aggregates to validate the returning type when the function is replaced. + * Used for validation of table metadata when replacing metadata in ref (alterting a table) and when scrubbing + * an sstable to validate whether metadata stored in the sstable is compatible with the current metadata. + *

    + * Note that this will never return true when one type is multicell and the other is not. */ public boolean isCompatibleWith(AbstractType previous) { @@ -338,36 +446,58 @@ public boolean isCompatibleWith(AbstractType previous) } /** - * Returns true if values of the other AbstractType can be read and "reasonably" interpreted by the this + * Returns true if values of the other AbstractType can be read and "reasonably" interpreted by this * AbstractType. Note that this is a weaker version of isCompatibleWith, as it does not require that both type * compare values the same way. - * + *

    * The restriction on the other type being "reasonably" interpreted is to prevent, for example, IntegerType from * being compatible with all other types. Even though any byte string is a valid IntegerType value, it doesn't * necessarily make sense to interpret a UUID or a UTF8 string as an integer. - * + *

    * Note that a type should be compatible with at least itself. + *

    + * Also note that to ensure consistent handling of the {@link ReversedType} (which should be ignored as far as this + * method goes since it only impacts sorting), this method is final and subclasses should override the + * {@link #isValueCompatibleWithInternal} method instead. + *

    + * Used for type casting and values assignment. It valid if we can compose L values which were decomposed using R + * serializer. Therefore, it does not care about whether the type is reversed or not. It should not whether the + * type is fixed or variable length as for compose/decompose we always deal with all remaining data in the buffer + * (so for example, a variable length type may be compatible with fixed length type given the interpretation is + * consistent, like between BigInt and Long). */ - public boolean isValueCompatibleWith(AbstractType previous) + public final boolean isValueCompatibleWith(AbstractType previous) { - AbstractType thisType = isReversed() ? ((ReversedType) this).baseType : this; - AbstractType thatType = previous.isReversed() ? ((ReversedType) previous).baseType : previous; - return thisType.isValueCompatibleWithInternal(thatType); + if (previous == null) + return false; + + AbstractType unwrapped = this.unwrap(); + AbstractType previousUnwrapped = previous.unwrap(); + if (unwrapped.equals(previousUnwrapped)) + return true; + + return unwrapped.isValueCompatibleWithInternal(previousUnwrapped); } /** - * Needed to handle ReversedType in value-compatibility checks. Subclasses should implement this instead of - * isValueCompatibleWith(). + * Needed to handle {@link ReversedType} in value-compatibility checks. Subclasses should override this instead of + * {@link #isValueCompatibleWith}. However, if said override has subtypes on which they need to check value + * compatibility recursively, they should call {@link #isValueCompatibleWith} instead of this method + * so that reversed types are ignored even if nested. */ - protected boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return isCompatibleWith(otherType); + return isCompatibleWith(previous); } /** * 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. + *

    + * Used in {@link AlterTableStatement} when adding a column with the same name as the previously dropped column. + * The new column type must be serialization compatible with the old one. We must be able to read cells of the new + * type which were serialized as cells of the old type. */ public boolean isSerializationCompatibleWith(AbstractType previous) { @@ -414,42 +544,65 @@ public boolean isVector() return false; } - public boolean isMultiCell() + public final boolean isMultiCell() { - return false; - } - - public boolean isFreezable() - { - return false; + return isMultiCell; } + /** + * If the type is a multi-cell one ({@link #isMultiCell()} is true), returns a frozen copy of this type (one + * for which {@link #isMultiCell()} returns false). + *

    + * Note that as mentioned on {@link #isMultiCell()}, a frozen type necessarily has all its subtypes frozen, so + * this method also ensures that no subtypes (recursively) are marked as multi-cell. + * + * @return a frozen version of this type. If this type is not multi-cell (whether because it is not a "complex" + * type, or because it is already a frozen one), this should return {@code this}. + */ public AbstractType freeze() { - return this; - } + if (!isMultiCell()) + return this; - public AbstractType unfreeze() - { - return this; + return with(freeze(subTypes()), false); } - public List> subTypes() + /** + * Creates an instance of this type (the concrete type extending this class) with the provided updated multi-cell + * flag and subtypes. + *

    + * Any other information (other than multi-cellness and subtypes) the type may have is expected to be left unchanged + * in the created type. + * + * @param isMultiCell whether the returned type must be a multi-cell one or not. + * @param subTypes the subtypes to use for the returned type as a list. The list will have subtypes in the exact + * same order as returned by {@link #subTypes()}, and exactly as many as the concrete class expects. + * @return the created type, which can be {@code this} if the provided subTypes and multi-cell flag are the same + * as that of this type. + */ + public AbstractType with(ImmutableList> subTypes, boolean isMultiCell) { - return Collections.emptyList(); + // Default implementation for types that can neither be multi-cell, nor have subtypes (and thus where this + // is basically a no-op). Any other type must override this. + + assert this.subTypes.isEmpty() && subTypes.isEmpty() : + String.format("Invalid call to 'with' on %s with subTypes %s (provided subTypes: %s)", + this, this.subTypes, subTypes); + + assert !this.isMultiCell() && !isMultiCell: + String.format("Invalid call to 'with' on %s with isMultiCell %b (provided isMultiCell: %b)", + this, this.isMultiCell(), isMultiCell); + + return this; } /** - * Returns an AbstractType instance that is equivalent to this one, but with all nested UDTs and collections - * explicitly frozen. - * - * This is only necessary for {@code 2.x -> 3.x} schema migrations, and can be removed in Cassandra 4.0. - * - * See CASSANDRA-11609 and CASSANDRA-11613. + * If the type has "complex" values that depend on subtypes, return those (direct) subtypes (in undefined order), + * and an empty list otherwise. */ - public AbstractType freezeNestedMulticellTypes() + public final ImmutableList> subTypes() { - return this; + return subTypes; } /** @@ -465,21 +618,23 @@ public boolean isEmptyValueMeaningless() */ public String toString(boolean ignoreFreezing) { - return this.toString(); + return getClass().getName(); } /** - * Return a list of the "subcomponents" this type has. - * This always return a singleton list with the type itself except for CompositeType. + * To override keyspace name in {@link UserType} */ - public List> getComponents() + public AbstractType overrideKeyspace(Function overrideKeyspace) { - return Collections.>singletonList(this); + if (subTypes.isEmpty()) + return this; + else + return with(subTypes.stream().map(t -> t.overrideKeyspace(overrideKeyspace)).collect(ImmutableList.toImmutableList()), isMultiCell); } /** - * The length of values for this type if all values are of fixed length, -1 otherwise. This has an impact on - * serialization. + * The length of values for this type, in bytes, if all values are of fixed length, -1 otherwise. + * This has an impact on serialization. * *

  • see {@link #writeValue}
  • *
  • see {@link #read}
  • @@ -614,9 +769,22 @@ public final boolean referencesUserType(ByteBuffer name) return referencesUserType(name, ByteBufferAccessor.instance); } + /** + * Returns true if this type is or references a user type with provided name. + */ public boolean referencesUserType(V name, ValueAccessor accessor) { - return false; + // Note that non-complex types have no subtypes, so will return false, and UserType overrides this to return + // true if the provided name matches. + return subTypes().stream().anyMatch(t -> t.referencesUserType(name, accessor)); + } + + /** + * Whether this type is or contains any UDT. + */ + public final boolean referencesUserTypes() + { + return isUDT() || subTypes().stream().anyMatch(AbstractType::referencesUserTypes); } /** @@ -625,23 +793,55 @@ public boolean referencesUserType(V name, ValueAccessor accessor) */ public AbstractType withUpdatedUserType(UserType udt) { - return this; + if (!referencesUserType(udt.name)) + return this; + + ImmutableList.Builder> builder = ImmutableList.builder(); + for (AbstractType subType : subTypes) + builder.add(subType.withUpdatedUserType(udt)); + + return with(builder.build(), isMultiCell()); + } + + /** + * Returns an instance of this type with all references to the provided user types recursively replaced with their new + * definition. + */ + public final AbstractType withUpdatedUserTypes(Iterable udts) + { + if (!referencesUserTypes()) + return this; + + AbstractType type = this; + for (UserType udt : udts) + type = type.withUpdatedUserType(udt); + + return type; } /** * Replace any instances of UserType with equivalent TupleType-s. - * + *

    * We need it for dropped_columns, to allow safely dropping unused user types later without retaining any references * to them in system_schema.dropped_columns. */ public AbstractType expandUserTypes() { - return this; + return referencesUserTypes() + ? with(ImmutableList.copyOf(transform(subTypes, AbstractType::expandUserTypes)), isMultiCell()) + : this; } public boolean referencesDuration() { - return false; + // Note that non-complex types have no subtypes, so will return false, and DurationType overrides this to return + // true. + return subTypes().stream().anyMatch(AbstractType::referencesDuration); + } + + public final boolean referencesCounter() + { + return isCounter() || subTypes().stream().anyMatch(AbstractType::referencesCounter); } /** @@ -652,7 +852,7 @@ public AssignmentTestable.TestResult testAssignment(AbstractType receiverType // testAssignement is for CQL literals and native protocol values, none of which make a meaningful // difference between frozen or not and reversed or not. - if (isFreezable() && !isMultiCell()) + if (!isMultiCell()) receiverType = receiverType.freeze(); if (isReversed() && !receiverType.isReversed()) @@ -667,6 +867,138 @@ public AssignmentTestable.TestResult testAssignment(AbstractType receiverType return AssignmentTestable.TestResult.NOT_ASSIGNABLE; } + /** + * Validates whether this type is valid as a column type for a column of the provided kind. + *

    + * A number of limits must be respected by column types (possibly depending on the type of columns). For + * instance, primary key columns must always be frozen, cannot use counters, etc. And for regular columns, amongst + * other things, we currently only support non-frozen types at top-level, so any type with a non-frozen subtype + * is invalid (note that it's valid to create a type with non-frozen subtypes, with a {@code CREATE TYPE} + * for instance, but they cannot be used as column types without being frozen). + * + * @param columnName the name of the column whose type is checked. + * @param isPrimaryKeyColumn whether {@code columnName} is a primary key column or not. + * @param isCounterTable whether the table the {@code columnName} is part of is a counter table. + * @throws InvalidColumnTypeException if this type is not a valid column type for {@code columnName}. + */ + public void validateForColumn(ByteBuffer columnName, + boolean isPrimaryKeyColumn, + boolean isCounterTable, + boolean isDroppedColumn, + boolean isForOfflineTool) + { + if (isPrimaryKeyColumn) + { + if (isMultiCell()) + throw columnException(columnName, + "non-frozen %s are not supported for PRIMARY KEY columns", category()); + if (referencesCounter()) + throw columnException(columnName, + "counters are not supported within PRIMARY KEY columns"); + + // We don't allow durations in anything sorted (primary key here, or in the "name-comparator" part of + // collections below). This isn't really a technical limitation, but duration sorts in a somewhat random + // way, so CASSANDRA-11873 decided to reject them when sorting was involved. + if (referencesDuration()) + throw columnException(columnName, + "duration types are not supported within PRIMARY KEY columns"); + + if (comparisonType == ComparisonType.NOT_COMPARABLE) + throw columnException(columnName, + "type %s is not comparable and cannot be used for PRIMARY KEY columns", asCQL3Type().toSchemaString()); + } + else + { + if (isMultiCell()) + { + // Plain tuples (TupleType, not UserType) are always implicitly frozen in CQL. A multi-cell tuple in the SSTable + // header indicates data from an old SSTable format where tuples could be non-frozen. This + // triggers the tryFix path in SerializationHeader.validateAndMaybeFixColumnType(), which will + // fix subtypes and then use the schema's dropped column type to determine the correct + // isMultiCell for dropped columns. + if (isTuple() && !isForOfflineTool) + throw columnException(columnName, + "tuple type %s is not frozen, which should not have happened", + asCQL3Type().toSchemaString()); + + for (AbstractType subType : subTypes()) + { + if (subType.isMultiCell()) + { + throw columnException(columnName, + "non-frozen %s are only supported at top-level: subtype %s of %s must be frozen", + subType.category(), subType.asCQL3Type().toSchemaString(), asCQL3Type().toSchemaString()); + } + } + + if (this instanceof MultiCellCapableType) + { + AbstractType nameComparator = ((MultiCellCapableType) this).nameComparator(); + // As mentioned above, CASSANDRA-11873 decided to reject durations when sorting was involved. + if (nameComparator.referencesDuration()) + { + // Trying to profile a more precise error message + String what = this instanceof MapType + ? "map keys" + : (this instanceof SetType ? "sets" : category()); + throw columnException(columnName, "duration types are not supported within non-frozen %s", what); + } + } + } + + // Mixing counter with non counter columns is not supported (#2614) + if (isCounterTable) + { + // Everything within a counter table must be a counter, and we don't allow nesting (collections of + // counters), except for legacy backward-compatibility, in the super-column map used to support old + // super columns. + if (!isCounter() && !TableMetadata.isSuperColumnMapColumnName(columnName)) + { + // We don't allow counter inside collections, but to be fair, at least for map, it's a bit of an + // arbitrary limitation (it works internally, we don't expose it mostly because counters have + // their limitations, and we want to restrict how user can use them to hopefully make user think + // twice about their usage). In any case, a slightly more user-friendly message is probably nice. + if (referencesCounter()) + throw columnException(columnName, "counters are not allowed within %s", category()); + + throw columnException(columnName, "Cannot mix counter and non counter columns in the same table"); + } + } + else + { + if (isCounter()) + throw columnException(columnName, "Cannot mix counter and non counter columns in the same table"); + + // For nested counters, we prefer complaining about the nested-ness rather than this not being a counter + // table, because the table won't be marked as a counter one even if it has only nested counters, and so + // that's overall a more intuitive message. + if (referencesCounter()) + throw columnException(columnName, "counters are not allowed within %s", category()); + } + } + + } + + private InvalidColumnTypeException columnException(ByteBuffer columnName, + String reason, + Object... args) + { + String msg = args.length == 0 ? reason : String.format(reason, args); + return new InvalidColumnTypeException(columnName, this, msg); + } + + private String category() + { + if (isCollection()) + return "collections"; + else if (isTuple()) + return "tuples"; + else if (isUDT()) + return "user types"; + else + return "types"; + } + /** * Produce a byte-comparable representation of the given value, i.e. a sequence of bytes that compares the same way * using lexicographical unsigned byte comparison as the original value using the type's comparator. @@ -738,9 +1070,9 @@ public final ByteBuffer fromComparableBytes(ByteSource.Peekable comparableBytes, * For CQL purposes the short name is fine. */ @Override - public String toString() + public final String toString() { - return getClass().getName(); + return toString(false); } public void checkComparable() @@ -771,6 +1103,36 @@ public ByteBuffer getMaskedValue() throw new UnsupportedOperationException("There isn't a defined masked value for type " + asCQL3Type()); } + protected static > V getInstance(ConcurrentMap instances, K key, Supplier value) + { + V cached = instances.get(key); + if (cached != null) + return cached; + + // We avoid constructor calls in Map#computeIfAbsent to avoid recursive update exceptions because the automatic + // fixing of subtypes done by the top-level constructor might attempt a recursive update to the instances map. + V instance = value.get(); + return instances.computeIfAbsent(key, k -> instance); + } + + /** + * Utility method that freezes a list of types. + * + * @param types the list of types to freeze. + * @return a new (unmodifiable) list containing the result of applying {@link #freeze()} on every type of + * {@code types}. + */ + public static ImmutableList> freeze(Iterable> types) + { + if (Iterables.isEmpty(types)) + return ImmutableList.of(); + + ImmutableList.Builder> builder = ImmutableList.builder(); + for (AbstractType type : types) + builder.add(type.freeze()); + return builder.build(); + } + /** * {@link ArgumentDeserializer} that uses the type deserialization. */ @@ -792,4 +1154,44 @@ public Object deserialize(ProtocolVersion protocolVersion, ByteBuffer buffer) return type.compose(buffer); } } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + if (this.hashCode() != o.hashCode()) + return false; + AbstractType that = (AbstractType) o; + return isMultiCell == that.isMultiCell && Objects.equals(subTypes, that.subTypes); + } + + @Override + public int hashCode() + { + return hashCode; + } + + /** + * Checks whether this type's subtypes are compatible with the provided type's subtypes using a provided predicate. + * Regardless of the predicate, this method returns false if this type has fewer subtypes than the provided type + * because in that case it could not safely replace the provided type in any situation. + * + * @param previous the type against which the verification is done - in other words, the type which was originally + * used to serialize the values + * @param predicate one of the methodsd isXXXCompatibleWith + * @return {@code true} if this type has at least the same number of subtypes as the previous type and the predicate + * is satisfied for the corresponding subtypes + */ + protected boolean isSubTypesCompatibleWith(AbstractType previous, BiPredicate, AbstractType> predicate) + { + if (subTypes.size() < previous.subTypes.size()) + return false; + + return Streams.zip(subTypes.stream().limit(previous.subTypes.size()), previous.subTypes.stream(), predicate::test) + .allMatch(Predicate.isEqual(true)); + } + } diff --git a/src/java/org/apache/cassandra/db/marshal/AsciiType.java b/src/java/org/apache/cassandra/db/marshal/AsciiType.java index 119965abeb95..2ecb6b4f9a9f 100644 --- a/src/java/org/apache/cassandra/db/marshal/AsciiType.java +++ b/src/java/org/apache/cassandra/db/marshal/AsciiType.java @@ -44,7 +44,7 @@ public class AsciiType extends StringType AsciiType() {super(ComparisonType.BYTE_ORDER);} // singleton - private final FastThreadLocal encoder = new FastThreadLocal() + private final FastThreadLocal encoder = new FastThreadLocal<>() { @Override protected CharsetEncoder initialValue() diff --git a/src/java/org/apache/cassandra/db/marshal/ByteArrayObjectFactory.java b/src/java/org/apache/cassandra/db/marshal/ByteArrayObjectFactory.java index 8877acbf5a7e..0a6bd9b500ca 100644 --- a/src/java/org/apache/cassandra/db/marshal/ByteArrayObjectFactory.java +++ b/src/java/org/apache/cassandra/db/marshal/ByteArrayObjectFactory.java @@ -18,7 +18,14 @@ package org.apache.cassandra.db.marshal; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.AbstractArrayClusteringPrefix; +import org.apache.cassandra.db.ArrayClustering; +import org.apache.cassandra.db.ArrayClusteringBound; +import org.apache.cassandra.db.ArrayClusteringBoundary; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringBoundary; +import org.apache.cassandra.db.ClusteringPrefix; import org.apache.cassandra.db.rows.ArrayCell; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; @@ -35,27 +42,6 @@ public String toString(TableMetadata metadata) } }; - public static final Clustering STATIC_CLUSTERING = new ArrayClustering(AbstractArrayClusteringPrefix.EMPTY_VALUES_ARRAY) - { - @Override - public Kind kind() - { - return Kind.STATIC_CLUSTERING; - } - - @Override - public String toString() - { - return "STATIC"; - } - - @Override - public String toString(TableMetadata metadata) - { - return toString(); - } - }; - static final ValueAccessor.ObjectFactory instance = new ByteArrayObjectFactory(); private ByteArrayObjectFactory() {} @@ -89,11 +75,6 @@ public Clustering clustering() return EMPTY_CLUSTERING; } - public Clustering staticClustering() - { - return STATIC_CLUSTERING; - } - public ClusteringBound bound(ClusteringPrefix.Kind kind, byte[]... values) { return new ArrayClusteringBound(kind, values); diff --git a/src/java/org/apache/cassandra/db/marshal/ByteBufferObjectFactory.java b/src/java/org/apache/cassandra/db/marshal/ByteBufferObjectFactory.java index 76e49b72713f..549cc07c64c0 100644 --- a/src/java/org/apache/cassandra/db/marshal/ByteBufferObjectFactory.java +++ b/src/java/org/apache/cassandra/db/marshal/ByteBufferObjectFactory.java @@ -20,7 +20,14 @@ import java.nio.ByteBuffer; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.AbstractBufferClusteringPrefix; +import org.apache.cassandra.db.BufferClustering; +import org.apache.cassandra.db.BufferClusteringBound; +import org.apache.cassandra.db.BufferClusteringBoundary; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringBoundary; +import org.apache.cassandra.db.ClusteringPrefix; import org.apache.cassandra.db.rows.BufferCell; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; @@ -61,11 +68,6 @@ public Clustering clustering() return Clustering.EMPTY; } - public Clustering staticClustering() - { - return Clustering.STATIC_CLUSTERING; - } - public ClusteringBound bound(ClusteringPrefix.Kind kind, ByteBuffer... values) { return new BufferClusteringBound(kind, values); diff --git a/src/java/org/apache/cassandra/db/marshal/BytesType.java b/src/java/org/apache/cassandra/db/marshal/BytesType.java index a273bd569171..a334cf932cc0 100644 --- a/src/java/org/apache/cassandra/db/marshal/BytesType.java +++ b/src/java/org/apache/cassandra/db/marshal/BytesType.java @@ -23,9 +23,9 @@ import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; -import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.serializers.BytesSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Hex; @@ -82,13 +82,14 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) @Override public boolean isCompatibleWith(AbstractType previous) { + // TODO BytesType is actually compatible with all types which use BYTE_ORDER comparison type // Both asciiType and utf8Type really use bytes comparison and // bytesType validate everything, so it is compatible with the former. return this == previous || previous == AsciiType.instance || previous == UTF8Type.instance; } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { // BytesType can read anything return true; diff --git a/src/java/org/apache/cassandra/db/marshal/CollectionType.java b/src/java/org/apache/cassandra/db/marshal/CollectionType.java index 0dcf25b0b75d..8e1209976bd9 100644 --- a/src/java/org/apache/cassandra/db/marshal/CollectionType.java +++ b/src/java/org/apache/cassandra/db/marshal/CollectionType.java @@ -17,15 +17,16 @@ */ package org.apache.cassandra.db.marshal; -import java.nio.ByteBuffer; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.List; import java.util.Iterator; -import java.util.Objects; +import java.util.List; import java.util.function.Consumer; import java.util.Locale; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.Lists; @@ -50,7 +51,7 @@ * Please note that this comparator shouldn't be used "manually" (as a custom * type for instance). */ -public abstract class CollectionType extends AbstractType +public abstract class CollectionType extends MultiCellCapableType { public static CellPath.Serializer cellPathSerializer = new CollectionPathSerializer(); @@ -89,13 +90,12 @@ public String toString() public final Kind kind; - protected CollectionType(ComparisonType comparisonType, Kind kind) + protected CollectionType(ComparisonType comparisonType, Kind kind, boolean isMultiCell, ImmutableList> subTypes) { - super(comparisonType); + super(comparisonType, isMultiCell, subTypes); this.kind = kind; } - public abstract AbstractType nameComparator(); public abstract AbstractType valueComparator(); protected abstract List serializedValues(Iterator> cells); @@ -156,12 +156,6 @@ public boolean isMap() return kind == Kind.MAP; } - @Override - public boolean isFreezable() - { - return true; - } - // Overrided by maps protected int collectionSize(List values) { @@ -177,102 +171,43 @@ public ByteBuffer serializeForNativeProtocol(Iterator> cells) } @Override - public boolean isCompatibleWith(AbstractType previous) + protected boolean isCompatibleWithFrozen(MultiCellCapableType previous) { - if (this == previous) - return true; - - if (!getClass().equals(previous.getClass())) - return false; - - CollectionType tprev = (CollectionType) previous; - if (this.isMultiCell() != tprev.isMultiCell()) + if (getClass() != previous.getClass()) return false; - // subclasses should handle compatibility checks for frozen collections - if (!this.isMultiCell()) - return isCompatibleWithFrozen(tprev); - - if (!this.nameComparator().isCompatibleWith(tprev.nameComparator())) - return false; - - // the value comparator is only used for Cell values, so sorting doesn't matter - return this.valueComparator().isSerializationCompatibleWith(tprev.valueComparator()); + // When frozen, the full collection is a blob, so everything must be sorted-compatible for the whole blob to + // be sorted-compatible. + return isSubTypesCompatibleWith(previous, AbstractType::isCompatibleWith); } @Override - public boolean isValueCompatibleWithInternal(AbstractType previous) + protected boolean isCompatibleWithMultiCell(MultiCellCapableType previous) { - // for multi-cell collections, compatibility and value-compatibility are the same - if (this.isMultiCell()) - return isCompatibleWith(previous); - - if (this == previous) - return true; - - if (!getClass().equals(previous.getClass())) - return false; - - CollectionType tprev = (CollectionType) previous; - if (this.isMultiCell() != tprev.isMultiCell()) + if (getClass() != previous.getClass()) return false; - // subclasses should handle compatibility checks for frozen collections - return isValueCompatibleWithFrozen(tprev); + // When multi-cell, the name comparator is the one used to compare cell-path so must be sorted-compatible + // but the value comparator is never used for sorting so serialization-compatibility is enough. + return this.nameComparator().isCompatibleWith(previous.nameComparator()) && + this.valueComparator().isSerializationCompatibleWith(((CollectionType) previous).valueComparator()); } @Override - public boolean isSerializationCompatibleWith(AbstractType previous) + protected boolean isValueCompatibleWithFrozen(MultiCellCapableType previous) { - if (!isValueCompatibleWith(previous)) + if (getClass() != previous.getClass()) return false; - return valueComparator().isSerializationCompatibleWith(((CollectionType)previous).valueComparator()); + return nameComparator().isCompatibleWith(previous.nameComparator()) && + valueComparator().isValueCompatibleWith(((CollectionType) previous).valueComparator()); } - /** A version of isCompatibleWith() to deal with non-multicell (frozen) collections */ - protected abstract boolean isCompatibleWithFrozen(CollectionType previous); - - /** A version of isValueCompatibleWith() to deal with non-multicell (frozen) collections */ - protected abstract boolean isValueCompatibleWithFrozen(CollectionType previous); - public CQL3Type asCQL3Type() { return new CQL3Type.Collection(this); } - @Override - public boolean equals(Object o) - { - if (this == o) - return true; - - if (!(o instanceof CollectionType)) - return false; - - CollectionType other = (CollectionType) o; - - if (kind != other.kind) - return false; - - if (isMultiCell() != other.isMultiCell()) - return false; - - return nameComparator().equals(other.nameComparator()) && valueComparator().equals(other.valueComparator()); - } - - @Override - public int hashCode() - { - return Objects.hash(kind, isMultiCell(), nameComparator(), valueComparator()); - } - - @Override - public String toString() - { - return this.toString(false); - } - static int compareListOrSet(AbstractType elementsComparator, VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR) { // Note that this is only used if the collection is frozen @@ -358,6 +293,14 @@ public static String setOrListToJsonString(ByteBuffer buffer, AbstractType el return sb.append("]").toString(); } + /** + * Checks if the specified serialized collection contains the specified serialized collection element. + * + * @param element a serialized collection element + * @return {@code true} if the collection contains the value, {@code false} otherwise + */ + public abstract boolean contains(ByteBuffer collection, ByteBuffer element); + private static class CollectionPathSerializer implements CellPath.Serializer { public void serialize(CellPath path, DataOutputPlus out) throws IOException @@ -387,4 +330,20 @@ public int size(ByteBuffer buffer) } public abstract void forEach(ByteBuffer input, Consumer action); + + @Override + public String toString(boolean ignoreFreezing) + { + boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); + + StringBuilder sb = new StringBuilder(); + if (includeFrozenType) + sb.append(FrozenType.class.getName()).append('('); + sb.append(getClass().getName()); + sb.append(TypeParser.stringifyTypeParameters(subTypes, ignoreFreezing || !isMultiCell)); + if (includeFrozenType) + sb.append(')'); + return sb.toString(); + } + } diff --git a/src/java/org/apache/cassandra/db/marshal/CompositeType.java b/src/java/org/apache/cassandra/db/marshal/CompositeType.java index df7ee99070de..901959bd9401 100644 --- a/src/java/org/apache/cassandra/db/marshal/CompositeType.java +++ b/src/java/org/apache/cassandra/db/marshal/CompositeType.java @@ -27,7 +27,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; @@ -39,9 +38,6 @@ import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import static com.google.common.collect.Iterables.any; -import static com.google.common.collect.Iterables.transform; - /* * The encoding of a CompositeType column name should be: * ... @@ -73,9 +69,9 @@ public static class Serializer extends BytesSerializer { // types are held to make sure the serializer is unique for each collection of types, this is to make sure it's // safe to cache in all cases - public final List> types; + public final ImmutableList> types; - public Serializer(List> types) + public Serializer(ImmutableList> types) { this.types = types; } @@ -98,11 +94,10 @@ public int hashCode() private static final int STATIC_MARKER = 0xFFFF; - public final List> types; private final Serializer serializer; // interning instances - private static final ConcurrentMap>, CompositeType> instances = new ConcurrentHashMap<>(); + private static final ConcurrentMap>, CompositeType> instances = new ConcurrentHashMap<>(); public static CompositeType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException { @@ -111,12 +106,12 @@ public static CompositeType getInstance(TypeParser parser) throws ConfigurationE public static CompositeType getInstance(Iterable> types) { - return getInstance(Lists.newArrayList(types)); + return getInstance(ImmutableList.copyOf(types)); } - public static CompositeType getInstance(AbstractType... types) + public static CompositeType getInstance(AbstractType... types) { - return getInstance(Arrays.asList(types)); + return getInstance(ImmutableList.copyOf(types)); } protected static int startingOffsetInternal(boolean isStatic) @@ -159,25 +154,31 @@ private static boolean readStatic(ByteBuffer bb) return true; } - public static CompositeType getInstance(List> types) + public static CompositeType getInstance(ImmutableList> types) { assert types != null && !types.isEmpty(); - CompositeType t = instances.get(types); - return null == t - ? instances.computeIfAbsent(types, CompositeType::new) - : t; + ImmutableList> typesCopy = freeze(types); + return getInstance(instances, typesCopy, () -> new CompositeType(typesCopy)); + } + + protected CompositeType(Iterable> subTypes) + { + this(ImmutableList.copyOf(subTypes)); } - protected CompositeType(List> types) + protected CompositeType(ImmutableList> types) { - this.types = ImmutableList.copyOf(types); - this.serializer = new Serializer(this.types); + super(types); + this.serializer = new Serializer(this.subTypes); } @Override - public List> subTypes() + public CompositeType with(ImmutableList> subTypes, boolean isMultiCell) { - return types; + if (isMultiCell) + throw new IllegalArgumentException("Cannot create a multi-cell CompositeType"); + + return getInstance(subTypes); } @Override @@ -190,7 +191,7 @@ protected AbstractType getComparator(int i, V value, ValueAccessor acc { try { - return types.get(i); + return subTypes.get(i); } catch (IndexOutOfBoundsException e) { @@ -210,16 +211,21 @@ protected AbstractType getComparator(int i, VL left, ValueAccessor AbstractType getAndAppendComparator(int i, V value, ValueAccessor accessor, StringBuilder sb, int offset) { - return types.get(i); + return subTypes.get(i); } @Override public ByteSource asComparableBytes(ValueAccessor accessor, V data, Version version) + { + return asComparableBytes(accessor, data, version, ByteSource.TERMINATOR); + } + + public ByteSource asComparableBytes(ValueAccessor accessor, V data, Version version, int terminator) { if (data == null || accessor.isEmpty(data)) return null; - ByteSource[] srcs = new ByteSource[types.size() * 2 + 1]; + ByteSource[] srcs = new ByteSource[subTypes.size() * 2 + 1]; int length = accessor.size(data); // statics go first @@ -237,7 +243,7 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, Versi int componentLength = accessor.getUnsignedShort(data, offset); offset += 2; - srcs[i * 2 + 1] = types.get(i).asComparableBytes(accessor, accessor.slice(data, offset, componentLength), version); + srcs[i * 2 + 1] = subTypes.get(i).asComparableBytes(accessor, accessor.slice(data, offset, componentLength), version); offset += componentLength; lastEoc = accessor.getByte(data, offset); offset += 1; @@ -272,17 +278,17 @@ public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable int separator = comparableBytes.next(); boolean isStatic = ByteSourceInverse.nextComponentNull(separator); int i = 0; - V[] buffers = accessor.createArray(types.size()); + V[] buffers = accessor.createArray(subTypes.size()); byte lastEoc = 0; - while ((separator = comparableBytes.next()) != ByteSource.TERMINATOR && i < types.size()) + while ((separator = comparableBytes.next()) != ByteSource.TERMINATOR && i < subTypes.size()) { // Only the end-of-component byte of the last component of this composite can be non-zero, so the // component before can't have a non-zero end-of-component byte. assert lastEoc == 0 : lastEoc; // Get the next type and decode its payload. - AbstractType type = types.get(i); + AbstractType type = subTypes.get(i); V decoded = type.fromComparableBytes(accessor, ByteSourceInverse.nextComponentSource(comparableBytes, separator), version); @@ -295,29 +301,29 @@ public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable protected ParsedComparator parseComparator(int i, String part) { - return new StaticParsedComparator(types.get(i), part); + return new StaticParsedComparator(subTypes.get(i), part); } protected AbstractType validateComparator(int i, V value, ValueAccessor accessor, int offset) throws MarshalException { - if (i >= types.size()) + if (i >= subTypes.size()) throw new MarshalException("Too many bytes for comparator"); - return types.get(i); + return subTypes.get(i); } - protected int getComparatorSize(int i, V value, ValueAccessor accessor, int offset) + protected int getComparatorSize(V value, ValueAccessor accessor, int offset) { return 0; } public ByteBuffer decompose(Object... objects) { - assert objects.length == types.size() : String.format("Expected length %d but given %d", types.size(), objects.length); + assert objects.length == subTypes.size() : String.format("Expected length %d but given %d", subTypes.size(), objects.length); ByteBuffer[] serialized = new ByteBuffer[objects.length]; for (int i = 0; i < objects.length; i++) { - ByteBuffer buffer = ((AbstractType) types.get(i)).decompose(objects[i]); + ByteBuffer buffer = ((AbstractType) subTypes.get(i)).decompose(objects[i]); serialized[i] = buffer; } return build(ByteBufferAccessor.instance, serialized); @@ -328,7 +334,7 @@ public ByteBuffer[] split(ByteBuffer name) { // Assume all components, we'll trunk the array afterwards if need be, but // most names will be complete. - ByteBuffer[] l = new ByteBuffer[types.size()]; + ByteBuffer[] l = new ByteBuffer[subTypes.size()]; ByteBuffer bb = name.duplicate(); readStatic(bb); int i = 0; @@ -373,86 +379,55 @@ public static ByteBuffer extractComponent(ByteBuffer bb, int idx) return null; } - public static boolean isStaticName(V value, ValueAccessor accessor) + public static ByteBuffer extractFirstComponentAsTrieSearchPrefix(ByteBuffer bb, boolean isLowerBound) { - return accessor.size(value) >= 2 && (accessor.getUnsignedShort(value, 0) & 0xFFFF) == STATIC_MARKER; + bb = bb.duplicate(); + readStatic(bb); + if (bb.remaining() == 0) + return null; + + // We want to return the first two bytes, the component itself, and the end-of-component byte + int componentLength = bb.getShort(bb.position()) + 3; + int endOfComponentPosition = componentLength - 1; + // If this buffer is the lower bound or if the end-of-component byte is 1, we just need to set the limit + if (isLowerBound || bb.get(bb.position() + endOfComponentPosition) == (byte) 1) + return bb.limit(componentLength); + + // We need to copy the first component and set the end-of-component byte to 1. + // See class's javadoc for explanation. + ByteBuffer dest = ByteBuffer.allocate(componentLength); + ByteBufferUtil.copyBytes(bb, bb.position(), dest, 0, endOfComponentPosition); + dest.put(endOfComponentPosition, (byte) 1); + return dest; } - @Override - public List> getComponents() + public static boolean isStaticName(V value, ValueAccessor accessor) { - return types; + return accessor.size(value) >= 2 && (accessor.getUnsignedShort(value, 0) & 0xFFFF) == STATIC_MARKER; } @Override public boolean isCompatibleWith(AbstractType previous) { - if (this == previous) + if (Objects.equals(this, previous)) return true; if (!(previous instanceof CompositeType)) return false; - // Extending with new components is fine - CompositeType cp = (CompositeType)previous; - if (types.size() < cp.types.size()) - return false; - - for (int i = 0; i < cp.types.size(); i++) - { - AbstractType tprev = cp.types.get(i); - AbstractType tnew = types.get(i); - if (!tnew.isCompatibleWith(tprev)) - return false; - } - return true; + return isSubTypesCompatibleWith(previous, AbstractType::isCompatibleWith); } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - if (this == otherType) + if (Objects.equals(this, previous)) return true; - if (!(otherType instanceof CompositeType)) - return false; - - // Extending with new components is fine - CompositeType cp = (CompositeType) otherType; - if (types.size() < cp.types.size()) + if (!(previous instanceof CompositeType)) return false; - for (int i = 0; i < cp.types.size(); i++) - { - AbstractType tprev = cp.types.get(i); - AbstractType tnew = types.get(i); - if (!tnew.isValueCompatibleWith(tprev)) - return false; - } - return true; - } - - @Override - public boolean referencesUserType(V name, ValueAccessor accessor) - { - return any(types, t -> t.referencesUserType(name, accessor)); - } - - @Override - public CompositeType withUpdatedUserType(UserType udt) - { - if (!referencesUserType(udt.name)) - return this; - - instances.remove(types); - - return getInstance(transform(types, t -> t.withUpdatedUserType(udt))); - } - - @Override - public AbstractType expandUserTypes() - { - return getInstance(transform(types, AbstractType::expandUserTypes)); + return isSubTypesCompatibleWith(previous, AbstractType::isValueCompatibleWith); } private static class StaticParsedComparator implements ParsedComparator @@ -485,24 +460,11 @@ public void serializeComparator(ByteBuffer bb) {} } @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CompositeType that = (CompositeType) o; - return types.equals(that.types); - } - - @Override - public int hashCode() - { - return Objects.hash(types); - } - - @Override - public String toString() + public String toString(boolean ignoreFreezing) { - return getClass().getName() + TypeParser.stringifyTypeParameters(types); + // Subtypes will always be frozen (since CompositeType always is), but we don't include it in the string + // representation (so that we ignore our parameter). + return getClass().getName() + TypeParser.stringifyTypeParameters(subTypes, true); } @SafeVarargs diff --git a/src/java/org/apache/cassandra/db/marshal/DateRangeType.java b/src/java/org/apache/cassandra/db/marshal/DateRangeType.java new file mode 100644 index 000000000000..b3db74f3321e --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/DateRangeType.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + + +import java.nio.ByteBuffer; + +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.db.marshal.datetime.DateRange; +import org.apache.cassandra.db.marshal.datetime.DateRangeUtil; +import org.apache.cassandra.serializers.DateRangeSerializer; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TypeSerializer; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.ByteBufferUtil; + + +/** + * Date range C* type with lower and upper bounds represented as timestamps with a millisecond precision. + */ +public class DateRangeType extends AbstractType +{ + public static final DateRangeType instance = new DateRangeType(); + + private static final ByteBuffer MASKED_VALUE = DateRangeSerializer.instance.serialize(new DateRange(DateRange.DateRangeBound.UNBOUNDED, DateRange.DateRangeBound.UNBOUNDED)); + + private DateRangeType() + { + super(ComparisonType.BYTE_ORDER); + } + + @Override + public ByteBuffer fromString(String source) throws MarshalException + { + if (source.isEmpty()) + { + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + } + try + { + DateRange dateRange = DateRangeUtil.parseDateRange(source); + return decompose(dateRange); + } + catch (Exception e) + { + throw new MarshalException(String.format("Could not parse date range: %s %s", source, e.getMessage()), e); + } + } + + @Override + public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) + { + DateRange dateRange = this.getSerializer().deserialize(buffer); + return '"' + dateRange.formatToSolrString() + '"'; + } + + @Override + public Term fromJSONObject(Object parsed) throws MarshalException + { + if (parsed instanceof String) + { + return new Constants.Value(fromString((String) parsed)); + } + throw new MarshalException(String.format( + "Expected a string representation of a date range value, but got a %s: %s", + parsed.getClass().getSimpleName(), parsed)); + } + + @Override + public boolean isEmptyValueMeaningless() + { + return true; + } + + @Override + public TypeSerializer getSerializer() + { + return DateRangeSerializer.instance; + } + + @Override + public ByteBuffer getMaskedValue() + { + return MASKED_VALUE; + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/DateType.java b/src/java/org/apache/cassandra/db/marshal/DateType.java index 05a6c4cf3667..63c2fa3f56e3 100644 --- a/src/java/org/apache/cassandra/db/marshal/DateType.java +++ b/src/java/org/apache/cassandra/db/marshal/DateType.java @@ -20,16 +20,16 @@ import java.nio.ByteBuffer; import java.util.Date; -import org.apache.cassandra.cql3.Constants; -import org.apache.cassandra.cql3.Term; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; -import org.apache.cassandra.serializers.TypeSerializer; -import org.apache.cassandra.serializers.TimestampSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TimestampSerializer; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.bytecomparable.ByteComparable; @@ -46,6 +46,8 @@ public class DateType extends AbstractType private static final Logger logger = LoggerFactory.getLogger(DateType.class); public static final DateType instance = new DateType(); + + private static final TypeSerializer serializer = new TimestampSerializer(); private static final ArgumentDeserializer ARGUMENT_DESERIALIZER = new DefaultArgumentDeserializer(instance); private static final ByteBuffer MASKED_VALUE = instance.decompose(new Date(0)); @@ -121,9 +123,9 @@ public boolean isCompatibleWith(AbstractType previous) } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return this == otherType || otherType == TimestampType.instance || otherType == LongType.instance; + return this == previous || previous == TimestampType.instance || previous == LongType.instance; } @Override @@ -134,7 +136,7 @@ public CQL3Type asCQL3Type() public TypeSerializer getSerializer() { - return TimestampSerializer.instance; + return serializer; } @Override diff --git a/src/java/org/apache/cassandra/db/marshal/DecimalType.java b/src/java/org/apache/cassandra/db/marshal/DecimalType.java index 00da39ac35d0..eb3492f7d4ee 100644 --- a/src/java/org/apache/cassandra/db/marshal/DecimalType.java +++ b/src/java/org/apache/cassandra/db/marshal/DecimalType.java @@ -234,7 +234,7 @@ public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable // but when decoding we don't need that property on the transient mantissa value. BigInteger mantissa = BigInteger.ZERO; int curr = comparableBytes.next(); - while (curr != DECIMAL_LAST_BYTE) + while (curr > DECIMAL_LAST_BYTE) { // The mantissa value is constructed by a standard positional notation value calculation. // The value of the next digit is the next most-significant mantissa byte as an unsigned integer, @@ -338,7 +338,7 @@ public ArgumentDeserializer getArgumentDeserializer() * @param number the value to convert * @return the converted value */ - protected BigDecimal toBigDecimal(Number number) + public BigDecimal toBigDecimal(Number number) { if (number instanceof BigDecimal) return (BigDecimal) number; diff --git a/src/java/org/apache/cassandra/db/marshal/DurationType.java b/src/java/org/apache/cassandra/db/marshal/DurationType.java index 0c466175543f..d53a83994795 100644 --- a/src/java/org/apache/cassandra/db/marshal/DurationType.java +++ b/src/java/org/apache/cassandra/db/marshal/DurationType.java @@ -55,12 +55,6 @@ public ByteBuffer fromString(String source) throws MarshalException return decompose(Duration.from(source)); } - @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) - { - return this == otherType; - } - public Term fromJSONObject(Object parsed) throws MarshalException { try diff --git a/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java b/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java index 658a147bc785..9dc9f4f5bfa1 100644 --- a/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java +++ b/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java @@ -22,17 +22,21 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.google.common.collect.Streams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,12 +49,11 @@ import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import static com.google.common.collect.Iterables.any; - /* * The encoding of a DynamicCompositeType column name should be: * ... @@ -76,9 +79,9 @@ public static class Serializer extends BytesSerializer { // aliases are held to make sure the serializer is unique for each collection of types, this is to make sure it's // safe to cache in all cases - private final Map> aliases; + private final ImmutableMap> aliases; - public Serializer(Map> aliases) + public Serializer(ImmutableMap> aliases) { this.aliases = aliases; } @@ -104,12 +107,12 @@ public int hashCode() private static final String REVERSED_TYPE = ReversedType.class.getSimpleName(); @VisibleForTesting - public final Map> aliases; - private final Map, Byte> inverseMapping; + public final ImmutableMap> aliases; + private final ImmutableMap, Byte> inverseMapping; private final Serializer serializer; // interning instances - private static final ConcurrentHashMap>, DynamicCompositeType> instances = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap>, DynamicCompositeType> instances = new ConcurrentHashMap<>(); public static DynamicCompositeType getInstance(TypeParser parser) { @@ -118,30 +121,36 @@ public static DynamicCompositeType getInstance(TypeParser parser) public static DynamicCompositeType getInstance(Map> aliases) { - DynamicCompositeType dct = instances.get(aliases); - return null == dct - ? instances.computeIfAbsent(aliases, DynamicCompositeType::new) - : dct; + ImmutableMap> aliasesCopy = ImmutableMap.copyOf(new TreeMap<>(Maps.transformValues(aliases, AbstractType::freeze))); + return getInstance(instances, aliasesCopy, () -> new DynamicCompositeType(aliasesCopy)); } - private DynamicCompositeType(Map> aliases) + private DynamicCompositeType(ImmutableMap> aliases) { - this.aliases = ImmutableMap.copyOf(aliases); + super(ImmutableList.copyOf(aliases.values())); + this.aliases = aliases; this.serializer = new Serializer(this.aliases); - this.inverseMapping = new HashMap<>(); + LinkedHashMap, Byte> inverseMappingBuilder = new LinkedHashMap<>(); for (Map.Entry> en : aliases.entrySet()) - this.inverseMapping.put(en.getValue(), en.getKey()); - } - - public int size() - { - return aliases.size(); + inverseMappingBuilder.put(en.getValue(), en.getKey()); + this.inverseMapping = ImmutableMap.copyOf(inverseMappingBuilder); } @Override - public List> subTypes() + public AbstractType with(ImmutableList> subTypes, boolean isMultiCell) { - return new ArrayList<>(aliases.values()); + Preconditions.checkArgument(!isMultiCell, "Cannot create a multi-cell DynamicCompositeType"); + Preconditions.checkArgument(subTypes.size() == aliases.size(), + "Invalid number of subTypes for DynamicCompositeType (got %s, expected %s)", subTypes.size(), aliases.size()); + + if (subTypes.equals(this.subTypes()) && isMultiCell == isMultiCell()) + return this; + + ImmutableMap.Builder> copiedAliases = ImmutableMap.builderWithExpectedSize(subTypes.size()); + Streams.zip(aliases.keySet().stream(), subTypes.stream(), Pair::create) + .forEachOrdered(p -> copiedAliases.put(p.left, p.right)); + + return new DynamicCompositeType(copiedAliases.build()); } @Override @@ -161,7 +170,7 @@ protected int startingOffset(boolean isStatic) return 0; } - protected int getComparatorSize(int i, V value, ValueAccessor accessor, int offset) + protected int getComparatorSize(V value, ValueAccessor accessor, int offset) { int header = accessor.getShort(value, offset); if ((header & 0x8000) == 0) @@ -181,7 +190,6 @@ private AbstractType getComparator(V value, ValueAccessor accessor, in int header = accessor.getShort(value, offset); if ((header & 0x8000) == 0) { - String name = accessor.toString(accessor.slice(value, offset + 2, header)); return TypeParser.parse(name); } @@ -211,10 +219,10 @@ protected AbstractType getComparator(int i, VL left, ValueAccessor) comp1).baseType; - comp2 = ((ReversedType) comp2).baseType; + comp1 = comp1.unwrap(); + comp2 = comp2.unwrap(); } // Fast test if the comparator uses singleton instances @@ -284,11 +292,11 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, Versi assert lastEoc == 0 : lastEoc; AbstractType comp = getComparator(data, accessor, offset); - offset += getComparatorSize(i, data, accessor, offset); + offset += getComparatorSize(data, accessor, offset); // The comparable bytes for the component need to ensure comparisons consistent with // AbstractCompositeType.compareCustom(ByteBuffer, ByteBuffer) and // DynamicCompositeType.getComparator(int, ByteBuffer, ByteBuffer): - if (version == Version.LEGACY || !(comp instanceof ReversedType)) + if (version == Version.LEGACY || !comp.isReversed()) { // ...most often that means just adding the short name of the type, followed by the full name of the type. srcs.add(ByteSource.of(comp.getClass().getSimpleName(), version)); @@ -296,14 +304,14 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, Versi } else { - // ...however some times the component uses a complex type (currently the only supported complex type + // ...however sometimes the component uses a complex type (currently the only supported complex type // is ReversedType - we can't have elements that are of MapType, CompositeType, TupleType, etc.)... - ReversedType reversedComp = (ReversedType) comp; + AbstractType baseType = comp.unwrap(); // ...in this case, we need to add the short name of ReversedType before the short name of the base // type, to ensure consistency with DynamicCompositeType.getComparator(int, ByteBuffer, ByteBuffer). srcs.add(ByteSource.of(REVERSED_TYPE, version)); - srcs.add(ByteSource.of(reversedComp.baseType.getClass().getSimpleName(), version)); - srcs.add(ByteSource.of(reversedComp.baseType.getClass().getName(), version)); + srcs.add(ByteSource.of(baseType.getClass().getSimpleName(), version)); + srcs.add(ByteSource.of(baseType.getClass().getName(), version)); } // Only then the payload of the component gets encoded. int componentLength = accessor.getUnsignedShort(data, offset); @@ -529,7 +537,7 @@ public ByteBuffer decompose(Object... objects) @Override public boolean isCompatibleWith(AbstractType previous) { - if (this == previous) + if (Objects.equals(this, previous)) return true; if (!(previous instanceof DynamicCompositeType)) @@ -539,41 +547,8 @@ public boolean isCompatibleWith(AbstractType previous) // Note that modifying the type for an alias to a compatible type is // *not* fine since this would deal correctly with mixed aliased/not // aliased component. - DynamicCompositeType cp = (DynamicCompositeType)previous; - if (aliases.size() < cp.aliases.size()) - return false; - - for (Map.Entry> entry : cp.aliases.entrySet()) - { - AbstractType tprev = entry.getValue(); - AbstractType tnew = aliases.get(entry.getKey()); - if (tnew == null || tnew != tprev) - return false; - } - return true; - } - - @Override - public boolean referencesUserType(V name, ValueAccessor accessor) - { - return any(aliases.values(), t -> t.referencesUserType(name, accessor)); - } - - @Override - public DynamicCompositeType withUpdatedUserType(UserType udt) - { - if (!referencesUserType(udt.name)) - return this; - - instances.remove(aliases); - - return getInstance(Maps.transformValues(aliases, v -> v.withUpdatedUserType(udt))); - } - - @Override - public AbstractType expandUserTypes() - { - return getInstance(Maps.transformValues(aliases, v -> v.expandUserTypes())); + DynamicCompositeType tprev = (DynamicCompositeType)previous; + return aliases.entrySet().containsAll(tprev.aliases.entrySet()); } private class DynamicParsedComparator implements ParsedComparator @@ -660,8 +635,11 @@ public void serializeComparator(ByteBuffer bb) @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == this) + return true; + if (!super.equals(o)) + return false; + DynamicCompositeType that = (DynamicCompositeType) o; return aliases.equals(that.aliases); } @@ -673,8 +651,9 @@ public int hashCode() } @Override - public String toString() + public String toString(boolean ignoreFreezing) { + // DCT is always frozen, but implicitly so (FrozenType is never used), so we ignore our parameter return getClass().getName() + TypeParser.stringifyAliasesParameters(aliases); } diff --git a/src/java/org/apache/cassandra/db/marshal/EmptyType.java b/src/java/org/apache/cassandra/db/marshal/EmptyType.java index d9c7c22815ac..d6783a32c58f 100644 --- a/src/java/org/apache/cassandra/db/marshal/EmptyType.java +++ b/src/java/org/apache/cassandra/db/marshal/EmptyType.java @@ -75,7 +75,16 @@ private static NonEmptyWriteBehavior parseNonEmptyWriteBehavior() @Override public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteComparable.Version version) { - return null; + switch (version) + { + case LEGACY: + case OSS41: + return null; + case OSS50: + default: + // EmptyType is being used in tuples where a null ByteSource is not acceptable. Use an empty source. + return ByteSource.EMPTY; + } } @Override diff --git a/src/java/org/apache/cassandra/db/marshal/GeometryCodec.java b/src/java/org/apache/cassandra/db/marshal/GeometryCodec.java new file mode 100644 index 000000000000..62c8804235e9 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/GeometryCodec.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +import java.nio.ByteBuffer; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.exceptions.InvalidTypeException; +import org.apache.cassandra.db.marshal.geometry.LineString; +import org.apache.cassandra.db.marshal.geometry.OgcGeometry; +import org.apache.cassandra.db.marshal.geometry.Point; +import org.apache.cassandra.db.marshal.geometry.Polygon; + +public class GeometryCodec extends TypeCodec +{ + public static final TypeCodec pointCodec = new GeometryCodec<>(PointType.instance); + public static final TypeCodec lineStringCodec = new GeometryCodec<>(LineStringType.instance); + public static final TypeCodec polygonCodec = new GeometryCodec<>(PolygonType.instance); + + private final OgcGeometry.Serializer serializer; + + public GeometryCodec(AbstractGeometricType type) + { + super(DataType.custom(type.getClass().getName()), (Class) type.getGeoType().getGeoClass()); + this.serializer = (OgcGeometry.Serializer) type.getGeoType().getSerializer(); + } + + @Override + public T deserialize(ByteBuffer bb, ProtocolVersion protocolVersion) throws InvalidTypeException + { + return bb == null || bb.remaining() == 0 ? null : serializer.fromWellKnownBinary(bb); + } + + @Override + public ByteBuffer serialize(T geometry, ProtocolVersion protocolVersion) throws InvalidTypeException + { + return geometry == null ? null : geometry.asWellKnownBinary(); + } + + @Override + public T parse(String s) throws InvalidTypeException + { + if (s == null || s.isEmpty() || s.equalsIgnoreCase("NULL")) + return null; + return serializer.fromWellKnownText(s); + } + + @Override + public String format(T geometry) throws InvalidTypeException + { + return geometry == null ? "NULL" : geometry.asWellKnownText(); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/IntegerType.java b/src/java/org/apache/cassandra/db/marshal/IntegerType.java index 2dc0ae223861..aaf036f048fa 100644 --- a/src/java/org/apache/cassandra/db/marshal/IntegerType.java +++ b/src/java/org/apache/cassandra/db/marshal/IntegerType.java @@ -26,9 +26,9 @@ import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; -import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.serializers.IntegerSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.bytecomparable.ByteComparable; @@ -183,7 +183,7 @@ public static int compareIntegers(VL lhs, ValueAccessor accessorL, * 2^56-1 as FEFFFFFFFFFFFFFF * 2^56 as FF000100000000000000 * - * See {@link #asComparableBytesLegacy} for description of the legacy format. + * See {@link #asComparableBytes41} for description of the legacy format. */ @Override public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteComparable.Version version) @@ -204,12 +204,12 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteC } } - if (version != ByteComparable.Version.LEGACY) + if (version == ByteComparable.Version.OSS50) return (limit - p < FULL_FORM_THRESHOLD) ? encodeAsVarInt(accessor, data, limit) - : asComparableBytesCurrent(accessor, data, p, limit, (signbyte >> 7) & 0xFF); + : asComparableBytes50(accessor, data, p, limit, (signbyte >> 7) & 0xFF); else - return asComparableBytesLegacy(accessor, data, p, limit, signbyte); + return asComparableBytes41(accessor, data, p, limit, signbyte); } /** @@ -266,13 +266,14 @@ private ByteSource encodeAsVarInt(ValueAccessor accessor, V data, int lim * The representations are prefix-free, because representations of different length always have length bytes that * differ. */ - private ByteSource asComparableBytesCurrent(ValueAccessor accessor, V data, int startpos, int limit, int signbyte) + private ByteSource asComparableBytes50(ValueAccessor accessor, V data, int startpos, int limit, int signbyte) { + assert startpos >= 0 && startpos + FULL_FORM_THRESHOLD <= limit; // start with sign as a byte, then variable-length-encoded length, then bytes (stripped leading sign) return new ByteSource() { int pos = -2; - ByteSource lengthEncoding = new VariableLengthUnsignedInteger(limit - startpos - FULL_FORM_THRESHOLD); + ByteSource lengthEncoding = new VariableLengthUnsignedInteger(limit - (startpos + FULL_FORM_THRESHOLD)); @Override public int next() @@ -323,7 +324,7 @@ else if (pos == -1) * 2^31 as 8380000000 * 2^32 as 840100000000 */ - private ByteSource asComparableBytesLegacy(ValueAccessor accessor, V data, int startpos, int limit, int signbyte) + private ByteSource asComparableBytes41(ValueAccessor accessor, V data, int startpos, int limit, int signbyte) { return new ByteSource() { @@ -366,6 +367,51 @@ public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable if (comparableBytes == null) return accessor.empty(); + switch (version) + { + case OSS41: + return fromComparableBytes41(accessor, comparableBytes); + case OSS50: + return fromComparableBytes50(accessor, comparableBytes); + case LEGACY: + throw new AssertionError("Legacy byte-comparable format is not revertible."); + default: + throw new AssertionError(); + } + } + + private V fromComparableBytes41(ValueAccessor accessor, ByteSource.Peekable comparableBytes) + { + int valueBytes; + byte signedZero; + // Consume the first byte to determine whether the encoded number is positive and + // start iterating through the length header bytes and collecting the number of value bytes. + int curr = comparableBytes.next(); + if (curr >= POSITIVE_VARINT_HEADER) // positive number + { + valueBytes = curr - POSITIVE_VARINT_HEADER + 1; + while (curr == POSITIVE_VARINT_LENGTH_HEADER) + { + curr = comparableBytes.next(); + valueBytes += curr - POSITIVE_VARINT_HEADER + 1; + } + signedZero = 0; + } + else // negative number + { + valueBytes = POSITIVE_VARINT_HEADER - curr; + while (curr == NEGATIVE_VARINT_LENGTH_HEADER) + { + curr = comparableBytes.next(); + valueBytes += POSITIVE_VARINT_HEADER - curr; + } + signedZero = -1; + } + return extractBytes(accessor, comparableBytes, signedZero, valueBytes); + } + + public V fromComparableBytes50(ValueAccessor accessor, ByteSource.Peekable comparableBytes) + { // Consume the first byte to determine whether the encoded number is positive and // start iterating through the length header bytes and collecting the number of value bytes. int sign = comparableBytes.peek() ^ 0xFF; // FF if negative, 00 if positive @@ -491,9 +537,9 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return this == otherType || Int32Type.instance.isValueCompatibleWith(otherType) || LongType.instance.isValueCompatibleWith(otherType); + return this == previous || Int32Type.instance.isValueCompatibleWith(previous) || LongType.instance.isValueCompatibleWith(previous); } public CQL3Type asCQL3Type() diff --git a/src/java/org/apache/cassandra/db/marshal/LineStringType.java b/src/java/org/apache/cassandra/db/marshal/LineStringType.java new file mode 100644 index 000000000000..21fe7e931593 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/LineStringType.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +import java.nio.ByteBuffer; + +import com.esri.core.geometry.ogc.OGCLineString; +import org.apache.cassandra.db.marshal.geometry.GeometricType; +import org.apache.cassandra.db.marshal.geometry.LineString; + +public class LineStringType extends AbstractGeometricType +{ + public static final LineStringType instance = new LineStringType(); + + private static final ByteBuffer MASKED_VALUE = new LineString((OGCLineString) OGCLineString.fromText("LINESTRING EMPTY")).asWellKnownBinary(); + + public LineStringType() + { + super(GeometricType.LINESTRING); + } + + @Override + public ByteBuffer getMaskedValue() + { + return MASKED_VALUE; + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/ListType.java b/src/java/org/apache/cassandra/db/marshal/ListType.java index 71f400dbb1b4..ed71cc0d6ab8 100644 --- a/src/java/org/apache/cassandra/db/marshal/ListType.java +++ b/src/java/org/apache/cassandra/db/marshal/ListType.java @@ -25,18 +25,23 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.Lists; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.serializers.CollectionSerializer; import org.apache.cassandra.serializers.ListSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.JsonUtils; import org.apache.cassandra.utils.TimeUUID; -import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; public class ListType extends CollectionType> { @@ -46,7 +51,6 @@ public class ListType extends CollectionType> private final AbstractType elements; public final ListSerializer serializer; - private final boolean isMultiCell; public static ListType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException { @@ -57,50 +61,31 @@ public static ListType getInstance(TypeParser parser) throws ConfigurationExc return getInstance(l.get(0).freeze(), true); } + @SuppressWarnings("unchecked") public static ListType getInstance(AbstractType elements, boolean isMultiCell) { - ConcurrentHashMap, ListType> internMap = isMultiCell ? instances : frozenInstances; - ListType t = internMap.get(elements); - return null == t - ? internMap.computeIfAbsent(elements, k -> new ListType<>(k, isMultiCell)) - : t; + return getInstance(isMultiCell ? instances : frozenInstances, + elements, + () -> new ListType<>(elements, isMultiCell)); } private ListType(AbstractType elements, boolean isMultiCell) { - super(ComparisonType.CUSTOM, Kind.LIST); + super(ComparisonType.CUSTOM, Kind.LIST, isMultiCell, ImmutableList.of(elements)); this.elements = elements; this.serializer = ListSerializer.getInstance(elements.getSerializer()); - this.isMultiCell = isMultiCell; } @Override - public boolean referencesUserType(V name, ValueAccessor accessor) + @SuppressWarnings("unchecked") + public ListType with(ImmutableList> subTypes, boolean isMultiCell) { - return elements.referencesUserType(name, accessor); - } + Preconditions.checkArgument(subTypes.size() == 1, "Invalid number of subTypes for ListType (got %s)", subTypes.size()); - @Override - public ListType withUpdatedUserType(UserType udt) - { - if (!referencesUserType(udt.name)) + if (subTypes.equals(this.subTypes()) && isMultiCell == this.isMultiCell()) return this; - (isMultiCell ? instances : frozenInstances).remove(elements); - - return getInstance(elements.withUpdatedUserType(udt), isMultiCell); - } - - @Override - public AbstractType expandUserTypes() - { - return getInstance(elements.expandUserTypes(), isMultiCell); - } - - @Override - public boolean referencesDuration() - { - return getElementsType().referencesDuration(); + return getInstance((AbstractType) subTypes.get(0), isMultiCell); } public AbstractType getElementsType() @@ -108,6 +93,7 @@ public AbstractType getElementsType() return elements; } + @Override public AbstractType nameComparator() { return TimeUUIDType.instance; @@ -123,72 +109,66 @@ public ListSerializer getSerializer() return serializer; } - @Override - public AbstractType freeze() - { - // freeze elements to match org.apache.cassandra.cql3.CQL3Type.Raw.RawCollection.freeze - return isMultiCell ? getInstance(this.elements.freeze(), false) : this; - } - - @Override - public AbstractType unfreeze() - { - return isMultiCell ? this : getInstance(this.elements, true); - } - - @Override - public AbstractType freezeNestedMulticellTypes() - { - if (!isMultiCell()) - return this; - - if (elements.isFreezable() && elements.isMultiCell()) - return getInstance(elements.freeze(), isMultiCell); - - return getInstance(elements.freezeNestedMulticellTypes(), isMultiCell); - } - - @Override - public List> subTypes() + public int compareCustom(VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR) { - return Collections.singletonList(elements); + return compareListOrSet(elements, left, accessorL, right, accessorR); } @Override - public boolean isMultiCell() + public ByteSource asComparableBytes(ValueAccessor accessor, V data, Version version) { - return isMultiCell; + return asComparableBytesListOrSet(getElementsType(), accessor, data, version); } @Override - public boolean isCompatibleWithFrozen(CollectionType previous) + public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable comparableBytes, Version version) { - assert !isMultiCell; - return this.elements.isCompatibleWith(((ListType) previous).elements); + return fromComparableBytesListOrSet(accessor, comparableBytes, version, getElementsType()); } - @Override - public boolean isValueCompatibleWithFrozen(CollectionType previous) + static ByteSource asComparableBytesListOrSet(AbstractType elementsComparator, + ValueAccessor accessor, + V data, + Version version) { - assert !isMultiCell; - return this.elements.isValueCompatibleWithInternal(((ListType) previous).elements); - } + if (accessor.isEmpty(data)) + return null; - public int compareCustom(VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR) - { - return compareListOrSet(elements, left, accessorL, right, accessorR); + int offset = 0; + int size = CollectionSerializer.readCollectionSize(data, accessor); + offset += CollectionSerializer.sizeOfCollectionSize(); + ByteSource[] srcs = new ByteSource[size]; + for (int i = 0; i < size; ++i) + { + V v = CollectionSerializer.readValue(data, accessor, offset); + offset += CollectionSerializer.sizeOfValue(v, accessor); + srcs[i] = elementsComparator.asComparableBytes(accessor, v, version); + } + return ByteSource.withTerminatorMaybeLegacy(version, 0x00, srcs); } - @Override - public ByteSource asComparableBytes(ValueAccessor accessor, V data, Version version) + static V fromComparableBytesListOrSet(ValueAccessor accessor, + ByteSource.Peekable comparableBytes, + Version version, + AbstractType elementType) { - return asComparableBytesListOrSet(getElementsType(), accessor, data, version); - } + if (comparableBytes == null) + return accessor.empty(); - @Override - public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable comparableBytes, Version version) - { - return fromComparableBytesListOrSet(accessor, comparableBytes, version, getElementsType()); + List buffers = new ArrayList<>(); + int terminator = version == Version.LEGACY + ? 0x00 + : ByteSource.TERMINATOR; + int separator = comparableBytes.next(); + while (separator != terminator) + { + if (!ByteSourceInverse.nextComponentNull(separator)) + buffers.add(elementType.fromComparableBytes(accessor, comparableBytes, version)); + else + buffers.add(null); + separator = comparableBytes.next(); + } + return CollectionSerializer.pack(buffers, accessor, buffers.size()); } @Override @@ -198,11 +178,11 @@ public String toString(boolean ignoreFreezing) StringBuilder sb = new StringBuilder(); if (includeFrozenType) - sb.append(FrozenType.class.getName()).append("("); + sb.append(FrozenType.class.getName()).append('('); sb.append(getClass().getName()); - sb.append(TypeParser.stringifyTypeParameters(Collections.>singletonList(elements), ignoreFreezing || !isMultiCell)); + sb.append(TypeParser.stringifyTypeParameters(subTypes, ignoreFreezing || !isMultiCell())); if (includeFrozenType) - sb.append(")"); + sb.append(')'); return sb.toString(); } @@ -237,12 +217,6 @@ public Term fromJSONObject(Object parsed) throws MarshalException return new Lists.DelayedValue(terms); } - public ByteBuffer getSliceFromSerialized(ByteBuffer collection, ByteBuffer from, ByteBuffer to) - { - // We don't support slicing on lists so we don't need that function - throw new UnsupportedOperationException(); - } - @Override public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) { @@ -260,4 +234,15 @@ public ByteBuffer getMaskedValue() { return decompose(Collections.emptyList()); } + + public boolean isList() + { + return true; + } + + @Override + public boolean contains(ByteBuffer list, ByteBuffer element) + { + return CollectionSerializer.contains(getElementsType(), list, element, false, false); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/LongType.java b/src/java/org/apache/cassandra/db/marshal/LongType.java index 97b9f7546879..1dd431e19dc1 100644 --- a/src/java/org/apache/cassandra/db/marshal/LongType.java +++ b/src/java/org/apache/cassandra/db/marshal/LongType.java @@ -26,9 +26,9 @@ import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; -import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.serializers.LongSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.bytecomparable.ByteComparable; @@ -77,7 +77,7 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteC { if (accessor.isEmpty(data)) return null; - if (version == ByteComparable.Version.LEGACY) + if (version != ByteComparable.Version.OSS50) return ByteSource.signedFixedLengthNumber(accessor, data); else return ByteSource.variableLengthInteger(accessor.getLong(data, 0)); @@ -88,7 +88,7 @@ public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable { if (comparableBytes == null) return accessor.empty(); - if (version == ByteComparable.Version.LEGACY) + if (version != ByteComparable.Version.OSS50) return ByteSourceInverse.getSignedFixedLength(accessor, comparableBytes, 8); else return accessor.valueOf(ByteSourceInverse.getVariableLengthInteger(comparableBytes)); @@ -142,9 +142,9 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return this == otherType || otherType == DateType.instance || otherType == TimestampType.instance; + return this == previous || previous == DateType.instance || previous == TimestampType.instance; } public CQL3Type asCQL3Type() diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java index 16d533ec5c9e..ce3e2a74650c 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -19,7 +19,6 @@ import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -28,6 +27,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.Maps; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.db.rows.Cell; @@ -38,11 +40,11 @@ import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.JsonUtils; +import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import org.apache.cassandra.utils.Pair; public class MapType extends CollectionType> { @@ -53,7 +55,6 @@ public class MapType extends CollectionType> private final AbstractType keys; private final AbstractType values; private final MapSerializer serializer; - private final boolean isMultiCell; public static MapType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException { @@ -64,55 +65,32 @@ public class MapType extends CollectionType> return getInstance(l.get(0).freeze(), l.get(1).freeze(), true); } + @SuppressWarnings("unchecked") public static MapType getInstance(AbstractType keys, AbstractType values, boolean isMultiCell) { - ConcurrentHashMap, AbstractType>, MapType> internMap = isMultiCell ? instances : frozenInstances; - Pair, AbstractType> p = Pair.create(keys, values); - MapType t = internMap.get(p); - return null == t - ? internMap.computeIfAbsent(p, k -> new MapType<>(k.left, k.right, isMultiCell)) - : t; + return getInstance(isMultiCell ? instances : frozenInstances, Pair.create(keys, values), () -> new MapType<>(keys, values, isMultiCell)); } private MapType(AbstractType keys, AbstractType values, boolean isMultiCell) { - super(ComparisonType.CUSTOM, Kind.MAP); + super(ComparisonType.CUSTOM, Kind.MAP, isMultiCell, ImmutableList.of(keys, values)); this.keys = keys; this.values = values; this.serializer = MapSerializer.getInstance(keys.getSerializer(), values.getSerializer(), keys.comparatorSet); - this.isMultiCell = isMultiCell; } @Override - public boolean referencesUserType(T name, ValueAccessor accessor) + @SuppressWarnings("unchecked") + public MapType with(ImmutableList> subTypes, boolean isMultiCell) { - return keys.referencesUserType(name, accessor) || values.referencesUserType(name, accessor); - } + Preconditions.checkArgument(subTypes.size() == 2, "Invalid number of subTypes for MapType (got %s)", subTypes.size()); - @Override - public MapType withUpdatedUserType(UserType udt) - { - if (!referencesUserType(udt.name)) + if (subTypes.equals(this.subTypes()) && isMultiCell == this.isMultiCell()) return this; - (isMultiCell ? instances : frozenInstances).remove(Pair.create(keys, values)); - - return getInstance(keys.withUpdatedUserType(udt), values.withUpdatedUserType(udt), isMultiCell); - } - - @Override - public AbstractType expandUserTypes() - { - return getInstance(keys.expandUserTypes(), values.expandUserTypes(), isMultiCell); - } - - @Override - public boolean referencesDuration() - { - // Maps cannot be created with duration as keys - return getValuesType().referencesDuration(); + return getInstance((AbstractType) subTypes.get(0), (AbstractType) subTypes.get(1), isMultiCell); } public AbstractType getKeysType() @@ -125,6 +103,7 @@ public AbstractType getValuesType() return values; } + @Override public AbstractType nameComparator() { return keys; @@ -135,64 +114,6 @@ public AbstractType valueComparator() return values; } - @Override - public boolean isMultiCell() - { - return isMultiCell; - } - - @Override - public List> subTypes() - { - return Arrays.asList(keys, values); - } - - @Override - public AbstractType freeze() - { - // freeze key/value to match org.apache.cassandra.cql3.CQL3Type.Raw.RawCollection.freeze - return isMultiCell ? getInstance(this.keys.freeze(), this.values.freeze(), false) : this; - } - - @Override - public AbstractType unfreeze() - { - return isMultiCell ? this : getInstance(this.keys, this.values, true); - } - - @Override - public AbstractType freezeNestedMulticellTypes() - { - if (!isMultiCell()) - return this; - - AbstractType keyType = (keys.isFreezable() && keys.isMultiCell()) - ? keys.freeze() - : keys.freezeNestedMulticellTypes(); - - AbstractType valueType = (values.isFreezable() && values.isMultiCell()) - ? values.freeze() - : values.freezeNestedMulticellTypes(); - - return getInstance(keyType, valueType, isMultiCell); - } - - @Override - public boolean isCompatibleWithFrozen(CollectionType previous) - { - assert !isMultiCell; - MapType tprev = (MapType) previous; - return keys.isCompatibleWith(tprev.keys) && values.isCompatibleWith(tprev.values); - } - - @Override - public boolean isValueCompatibleWithFrozen(CollectionType previous) - { - assert !isMultiCell; - MapType tprev = (MapType) previous; - return keys.isCompatibleWith(tprev.keys) && values.isValueCompatibleWith(tprev.values); - } - public int compareCustom(RL left, ValueAccessor accessorL, TR right, ValueAccessor

    accessorR) { return compareMaps(keys, values, left, accessorL, right, accessorR); @@ -307,19 +228,6 @@ protected int collectionSize(List values) return values.size() / 2; } - public String toString(boolean ignoreFreezing) - { - boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); - - StringBuilder sb = new StringBuilder(); - if (includeFrozenType) - sb.append(FrozenType.class.getName()).append("("); - sb.append(getClass().getName()).append(TypeParser.stringifyTypeParameters(Arrays.asList(keys, values), ignoreFreezing || !isMultiCell)); - if (includeFrozenType) - sb.append(")"); - return sb.toString(); - } - public List serializedValues(Iterator> cells) { assert isMultiCell; @@ -398,4 +306,29 @@ public ByteBuffer getMaskedValue() { return decompose(Collections.emptyMap()); } + + /** + * Checks if the specified serialized map contains the specified serialized map value. + * + * @param map a serialized map + * @param value a serialized map value + * @return {@code true} if the map contains the value, {@code false} otherwise + */ + @Override + public boolean contains(ByteBuffer map, ByteBuffer value) + { + return CollectionSerializer.contains(getValuesType(), map, value, true, false); + } + + /** + * Checks if the specified serialized map contains the specified serialized map key. + * + * @param map a serialized map + * @param key a serialized map key + * @return {@code true} if the map contains the key, {@code false} otherwise + */ + public boolean containsKey(ByteBuffer map, ByteBuffer key) + { + return CollectionSerializer.contains(getKeysType(), map, key, true, true); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/MultiCellCapableType.java b/src/java/org/apache/cassandra/db/marshal/MultiCellCapableType.java new file mode 100644 index 000000000000..c57311fbcea3 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/MultiCellCapableType.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +import javax.annotation.Nonnull; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; + +/** + * Base class for all types that can be multi-cell (when not frozen). + *

    + * A multi-cell type is one whose value is composed of multiple sub-values that are laid out on multiple {@link Cell} + * instances (one for each sub-value), typically collections. This layout allows partial updates (of only some of + * the sub-values) without requiring a read-before-write operation. + *

    + * All multi-cell capable types can either be used as truly multi-cell types or can be used in a frozen state. + * In the latter case, the values are not laid out in multiple cells; instead, the entire value (with all its sub-values) + * is packed within a single cell value. This implies that partial updates without read-before-write are not possible. + * The {@link AbstractType#isMultiCell()} method indicates whether a given type is a multi-cell variant or a frozen one. + * Both variants are technically different types but represent the same values from a user perspective, with different + * capabilities. + * + * @param the type of the values of this type. + */ +public abstract class MultiCellCapableType extends AbstractType +{ + protected MultiCellCapableType(ComparisonType comparisonType, boolean isMultiCell, ImmutableList> subTypes) + { + super(comparisonType, isMultiCell, subTypes); + } + + /** + * Returns the subtype/comparator to use for the {@link CellPath} part of cells forming values for this type when + * used in its multi-cell variant. + *

    + * Note: In theory, this method should not be accessed on frozen instances (where {@code isMultiCell() == false}). + * However, for convenience, it is expected that this method always returns a proper value "as if" the type was a + * multi-cell variant, even if it is not. + * + * @return the comparator for the {@link CellPath} component of cells of this type, regardless of whether the type + * is frozen or not. + */ + public abstract AbstractType nameComparator(); + + @Override + public final boolean isCompatibleWith(AbstractType previous) + { + if (equals(previous)) + return true; + + if (!(previous instanceof MultiCellCapableType)) + return false; + + if (this.isMultiCell() != previous.isMultiCell()) + return false; + + MultiCellCapableType prevType = (MultiCellCapableType) previous; + return this.isMultiCell() ? isCompatibleWithMultiCell(prevType) + : isCompatibleWithFrozen(prevType); + } + + /** + * Whether {@code this} type is compatible (including for sorting) with {@code previous}, assuming both are + * of the same class (so {@code previous} can be safely cast to whichever class implements this) and both are + * frozen. + */ + protected abstract boolean isCompatibleWithFrozen(@Nonnull MultiCellCapableType previous); + + /** + * Whether {@code this} type is compatible (including for sorting) with {@code previous}, assuming both are + * of the same class (so {@code previous} can be safely cast to whichever class implements this) but neither + * are frozen. + */ + protected abstract boolean isCompatibleWithMultiCell(@Nonnull MultiCellCapableType previous); + + @Override + public final boolean isSerializationCompatibleWith(AbstractType previous) + { + if (equals(previous)) + return true; + + if (!(previous instanceof MultiCellCapableType)) + return false; + + if (this.isMultiCell() != previous.isMultiCell()) + return false; + + MultiCellCapableType prevType = (MultiCellCapableType) previous; + return isMultiCell() ? isSerializationCompatibleWithMultiCell(prevType) + : isSerializationCompatibleWithFrozen(prevType); + } + + /** + * Determines if the current type is serialization compatible with the given previous type. + *

    + * Serialization compatibility is primarily concerned with the ability to read the serialized value from a buffer + * that contains other data after the value. This means the value must either have a fixed length or its length must + * be explicitly stored. In frozen collections or tuples, all serialized values are prefixed with their length, + * regardless of whether the value has a fixed or variable length. Therefore, to ensure serialization compatibility, + * it is sufficient to verify whether the types are value-compatible when frozen, in addition to checking the + * isMultiCell and exact type conditions. + *

    + * + * @param previous the previous type to check compatibility against + * @return {@code true} if the current type is serialization compatible with the previous type, false otherwise + */ + protected boolean isSerializationCompatibleWithFrozen(MultiCellCapableType previous) + { + return isValueCompatibleWithFrozen(previous); + } + + protected boolean isSerializationCompatibleWithMultiCell(MultiCellCapableType previous) + { + return isCompatibleWithMultiCell(previous); + } + + @Override + protected final boolean isValueCompatibleWithInternal(AbstractType previous) + { + if (equals(previous)) + return true; + + if (!(previous instanceof MultiCellCapableType)) + return false; + + if (this.isMultiCell() != previous.isMultiCell()) + return false; + + MultiCellCapableType prevType = (MultiCellCapableType) previous; + return isMultiCell() ? isValueCompatibleWithMultiCell(prevType) + : isValueCompatibleWithFrozen(prevType); + } + + /** + * Whether {@code this} type is value-compatible with {@code previous}, assuming both are of the same class (so + * {@code previous} can be safely cast to whichever class implements this) and both are are frozen. + */ + protected abstract boolean isValueCompatibleWithFrozen(MultiCellCapableType previous); + + protected boolean isValueCompatibleWithMultiCell(MultiCellCapableType previous) + { + return isCompatibleWithMultiCell(previous); + } + +} diff --git a/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java b/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java index 3186e6e08b82..bac9e5699ce1 100644 --- a/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java +++ b/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java @@ -19,6 +19,7 @@ import java.nio.ByteBuffer; import java.util.Objects; +import javax.annotation.Nullable; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.Term; @@ -33,20 +34,17 @@ import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.bytecomparable.ByteSource; -import javax.annotation.Nullable; - /** for sorting columns representing row keys in the row ordering as determined by a partitioner. * Not intended for user-defined CFs, and will in fact error out if used with such. */ public class PartitionerDefinedOrder extends AbstractType { private final IPartitioner partitioner; private final AbstractType partitionKeyType; + private final int hashCode; public PartitionerDefinedOrder(IPartitioner partitioner) { - super(ComparisonType.CUSTOM); - this.partitioner = partitioner; - this.partitionKeyType = null; + this(partitioner, null); } public PartitionerDefinedOrder(IPartitioner partitioner, AbstractType partitionKeyType) @@ -54,6 +52,7 @@ public PartitionerDefinedOrder(IPartitioner partitioner, AbstractType partiti super(ComparisonType.CUSTOM); this.partitioner = partitioner; this.partitionKeyType = partitionKeyType; + this.hashCode = Objects.hash(partitioner, partitionKeyType); } public static AbstractType getInstance(TypeParser parser) @@ -159,11 +158,12 @@ public ArgumentDeserializer getArgumentDeserializer() } @Override - public String toString() + public String toString(boolean ignoreFreezing) { if (partitionKeyType != null && !DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5)) { - return String.format("%s(%s:%s)", getClass().getName(), partitioner.getClass().getName(), partitionKeyType); + // TODO Partition key is always frozen, though - should we pass ignoreFreezing to partitionKeyType.toString()? The default toString assumed ignoreFreezing=false so leaving that way for now + return String.format("%s(%s:%s)", getClass().getName(), partitioner.getClass().getName(), partitionKeyType.toString(false)); } // if Cassandra's major version is before 5, use the old behaviour return String.format("%s(%s)", getClass().getName(), partitioner.getClass().getName()); @@ -176,17 +176,20 @@ public AbstractType getPartitionKeyType() } @Override - public boolean equals(Object obj) + public final boolean equals(Object obj) { if (this == obj) - { return true; - } - if (obj instanceof PartitionerDefinedOrder) - { - PartitionerDefinedOrder other = (PartitionerDefinedOrder) obj; - return partitioner.equals(other.partitioner) && Objects.equals(partitionKeyType, other.partitionKeyType); - } - return false; + if (!super.equals(obj)) + return false; + + PartitionerDefinedOrder other = (PartitionerDefinedOrder) obj; + return partitioner.equals(other.partitioner) && Objects.equals(partitionKeyType, other.partitionKeyType); + } + + @Override + public final int hashCode() + { + return hashCode; } } diff --git a/src/java/org/apache/cassandra/db/marshal/PointType.java b/src/java/org/apache/cassandra/db/marshal/PointType.java new file mode 100644 index 000000000000..efd095730d9f --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/PointType.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +import java.nio.ByteBuffer; + +import org.apache.cassandra.db.marshal.geometry.GeometricType; +import org.apache.cassandra.db.marshal.geometry.Point; + +public class PointType extends AbstractGeometricType +{ + public static final PointType instance = new PointType(); + + private static final ByteBuffer MASKED_VALUE = new Point(0, 0).asWellKnownBinary(); + + public PointType() + { + super(GeometricType.POINT); + } + + @Override + public ByteBuffer getMaskedValue() + { + return MASKED_VALUE; + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/PolygonType.java b/src/java/org/apache/cassandra/db/marshal/PolygonType.java new file mode 100644 index 000000000000..b9a7ca8af7c4 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/PolygonType.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +import java.nio.ByteBuffer; + +import com.esri.core.geometry.ogc.OGCPolygon; +import org.apache.cassandra.db.marshal.geometry.GeometricType; +import org.apache.cassandra.db.marshal.geometry.Polygon; + +public class PolygonType extends AbstractGeometricType +{ + public static final PolygonType instance = new PolygonType(); + + private static final ByteBuffer MASKED_VALUE = new Polygon((OGCPolygon) OGCPolygon.fromText("POLYGON EMPTY")).asWellKnownBinary(); + + public PolygonType() + { + super(GeometricType.POLYGON); + } + + @Override + public ByteBuffer getMaskedValue() + { + return MASKED_VALUE; + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/Redaction.java b/src/java/org/apache/cassandra/db/marshal/Redaction.java new file mode 100644 index 000000000000..da5177c91cda --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/Redaction.java @@ -0,0 +1,29 @@ +/* + * Copyright IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal; + +/** + * Named boolan to express whether sensitive data should be presented in clear or redacted. + * This is generally applied to column values, or things containing column values. + *

    + * Column values should be redacted when printed in logs. + * They shouldn't be redacted when used in user-facing error messages or query tracing. + */ +public enum Redaction +{ + NONE, REDACT +} diff --git a/src/java/org/apache/cassandra/db/marshal/RedactionUtil.java b/src/java/org/apache/cassandra/db/marshal/RedactionUtil.java new file mode 100644 index 000000000000..0352a2d9b659 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/RedactionUtil.java @@ -0,0 +1,139 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.marshal; + +import java.nio.ByteBuffer; +import javax.annotation.Nullable; + +/** + * Utility class for redacting sensitive data values while preserving some orientative size information. + *

    + * This class provides methods to replace actual data values with a redacted placeholder ("?"), occasionally including + * size hints to help with debugging and troubleshooting without exposing the actual data content. No size hints will be + * included for values smaller than 100 bytes, or for values of fixed-size data types (e.g., int, UUID, timestamp). + *

    + * Size hints are provided in logarithmic buckets (e.g., ">100B", ">1KiB", ">10KiB", ">100KiB") to give a rough + * indication of data size while maintaining privacy: + *

      + *
    • Up to 100B: no size hint (just "?")
    • + *
    • (100 B, 1 KiB]: "?[>100B]"
    • + *
    • (1 KiB, 10 KiB]: "?[>1KiB]"
    • + *
    • (10 KiB, 100 KiB]: "?[>10KiB]"
    • + *
    • (100 KiB, 1 MiB]: "?[>100KiB]"
    • + *
    • (1 MiB, 10 MiB]: "?[>1MiB]"
    • + *
    • (10 MiB, 100 MiB]: "?[>10MiB]"
    • + *
    • (100 MiB, 1 GiB]: "?[>100MiB]"
    • + *
    • Over 1 GiB: "?[>1GiB]"
    • + *
    + */ +public final class RedactionUtil +{ + // Pre-computed redacted values for each size bucket + private static final String REDACTED = "?"; + private static final String REDACTED_100B = "?[>100B]"; + private static final String REDACTED_1KIB = "?[>1KiB]"; + private static final String REDACTED_10KIB = "?[>10KiB]"; + private static final String REDACTED_100KIB = "?[>100KiB]"; + private static final String REDACTED_1MIB = "?[>1MiB]"; + private static final String REDACTED_10MIB = "?[>10MiB]"; + private static final String REDACTED_100MIB = "?[>100MiB]"; + private static final String REDACTED_1GIB = "?[>1GiB]"; + + // Pre-computed size thresholds for each size bucket + private static final int B_100 = 100; + private static final int KIB = 1024; + private static final int KIB_10 = 10 * KIB; + private static final int KIB_100 = 100 * KIB; + private static final int MIB = 1024 * KIB; + private static final int MIB_10 = 10 * MIB; + private static final int MIB_100 = 100 * MIB; + private static final int GIB = 1024 * MIB; + + private RedactionUtil() + { + } + + /** + * Redacts a byte buffer value, optionally including size information. + *

    + * If the value is null, it's not greater than 100B, or has a fixed length (where size information would not be + * useful), returns a simple "?" placeholder. Otherwise, returns a placeholder with a size hint indicating the + * approximate size of the data, according to {@link #redact(int)}. + * + * @param bytes the value to redact + * @param isValueLengthFixed whether the value has a fixed length (e.g., int, UUID, timestamp) + * @return a redacted string representation, either "?" or "?[size_hint]" + */ + public static String redact(@Nullable ByteBuffer bytes, boolean isValueLengthFixed) + { + if (bytes == null || isValueLengthFixed) + return REDACTED; + + int remaining = bytes.remaining(); + // Early return for small values to avoid method call overhead + if (remaining <= B_100) + return REDACTED; + + return redact(remaining); + } + + /** + * Generates a redacted string with a size hint based on the provided size. + *

    + * The size hint uses logarithmic buckets to provide a rough indication of size: + *

      + *
    • Up to 100B: no size hint (just "?")
    • + *
    • (100 B, 1 KiB]: "?[>100B]"
    • + *
    • (1 KiB, 10 KiB]: "?[>1KiB]"
    • + *
    • (10 KiB, 100 KiB]: "?[>10KiB]"
    • + *
    • (100 KiB, 1 MiB]: "?[>100KiB]"
    • + *
    • And so on, up to "?[>1GiB]" for very large values
    • + *
    + * + * @param size the size in bytes + * @return a redacted string with an appropriate size hint + */ + public static String redact(int size) + { + assert size >= 0 : "Size must be non-negative"; + + // Byte range, don't include size information for the values in the smallest bucket + if (size <= B_100) + return REDACTED; + if (size <= KIB) + return REDACTED_100B; + + // KiB range + if (size <= KIB_10) + return REDACTED_1KIB; + if (size <= KIB_100) + return REDACTED_10KIB; + if (size <= MIB) + return REDACTED_100KIB; + + // MiB range + if (size <= MIB_10) + return REDACTED_1MIB; + if (size <= MIB_100) + return REDACTED_10MIB; + if (size <= GIB) + return REDACTED_100MIB; + + // above 1 GiB + return REDACTED_1GIB; + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/ReversedType.java b/src/java/org/apache/cassandra/db/marshal/ReversedType.java index 89d1adb0399e..006c33c260cf 100644 --- a/src/java/org/apache/cassandra/db/marshal/ReversedType.java +++ b/src/java/org/apache/cassandra/db/marshal/ReversedType.java @@ -18,10 +18,13 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; @@ -35,7 +38,7 @@ public class ReversedType extends AbstractType { // interning instances - private static final Map, ReversedType> instances = new ConcurrentHashMap<>(); + private static final Map, ReversedType> instances = new ConcurrentHashMap<>(); public final AbstractType baseType; @@ -49,18 +52,41 @@ public static ReversedType getInstance(TypeParser parser) public static ReversedType getInstance(AbstractType baseType) { - ReversedType t = instances.get(baseType); - return null == t - ? instances.computeIfAbsent(baseType, ReversedType::new) - : t; + ReversedType type = instances.get(baseType); + if (type != null) + return (ReversedType) type; + + // Stacking {@code ReversedType} is not only unnecessary but can also break some of the code. + // For instance, {@code AbstractType#isValueCompatibleWith} would end up triggering the exception thrown by + // {@code ReversedType#isValueCompatibleWithInternal}. Therefore, an exception should be thrown if such stacking + // is detected. + Preconditions.checkArgument(!(baseType instanceof ReversedType), + "Detected a type with 2 ReversedType() back-to-back, which is not allowed."); + + // We avoid constructor calls in Map#computeIfAbsent to avoid recursive update exceptions because the automatic + // fixing of subtypes done by the top-level constructor might attempt a recursive update to the instances map. + ReversedType instance = new ReversedType<>(baseType); + return (ReversedType) instances.computeIfAbsent(baseType, k -> instance); } private ReversedType(AbstractType baseType) { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, baseType.isMultiCell(), ImmutableList.of(baseType)); this.baseType = baseType; } + @Override + public AbstractType with(ImmutableList> subTypes, boolean isMultiCell) + { + Preconditions.checkArgument(subTypes.size() == 1, + "Invalid number of subTypes for ReversedType (got %s)", subTypes.size()); + + if (subTypes.equals(subTypes()) && isMultiCell == isMultiCell()) + return this; + + return (AbstractType) getInstance(subTypes.get(0)); + } + public boolean isEmptyValueMeaningless() { return baseType.isEmptyValueMeaningless(); @@ -128,10 +154,16 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) @Override public boolean isCompatibleWith(AbstractType otherType) { - if (!(otherType instanceof ReversedType)) + if (!otherType.isReversed()) return false; - return this.baseType.isCompatibleWith(((ReversedType) otherType).baseType); + return this.baseType.isCompatibleWith(otherType.unwrap()); + } + + @Override + protected boolean isValueCompatibleWithInternal(AbstractType otherType) + { + throw new AssertionError("This should have never been called on the ReversedType"); } @Override @@ -152,29 +184,6 @@ public ArgumentDeserializer getArgumentDeserializer() return baseType.getArgumentDeserializer(); } - @Override - public boolean referencesUserType(V name, ValueAccessor accessor) - { - return baseType.referencesUserType(name, accessor); - } - - @Override - public AbstractType expandUserTypes() - { - return getInstance(baseType.expandUserTypes()); - } - - @Override - public ReversedType withUpdatedUserType(UserType udt) - { - if (!referencesUserType(udt.name)) - return this; - - instances.remove(baseType); - - return getInstance(baseType.withUpdatedUserType(udt)); - } - @Override public int valueLengthIfFixed() { @@ -188,12 +197,12 @@ public boolean isReversed() } @Override - public String toString() + public String toString(boolean ignoreFreezing) { - return getClass().getName() + "(" + baseType + ")"; + return getClass().getName() + '(' + baseType + ')'; } - private static final class ReversedPeekableByteSource extends ByteSource.Peekable + private static final class ReversedPeekableByteSource extends ByteSource.PeekableImpl { private final ByteSource.Peekable original; diff --git a/src/java/org/apache/cassandra/db/marshal/SetType.java b/src/java/org/apache/cassandra/db/marshal/SetType.java index 0a12bfa2fc3b..06fa4476b989 100644 --- a/src/java/org/apache/cassandra/db/marshal/SetType.java +++ b/src/java/org/apache/cassandra/db/marshal/SetType.java @@ -18,15 +18,24 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.cql3.Sets; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.serializers.CollectionSerializer; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.SetSerializer; import org.apache.cassandra.transport.ProtocolVersion; @@ -42,7 +51,6 @@ public class SetType extends CollectionType> private final AbstractType elements; private final SetSerializer serializer; - private final boolean isMultiCell; public static SetType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException { @@ -53,44 +61,32 @@ public static SetType getInstance(TypeParser parser) throws ConfigurationExce return getInstance(l.get(0).freeze(), true); } + @SuppressWarnings("unchecked") public static SetType getInstance(AbstractType elements, boolean isMultiCell) { - ConcurrentHashMap, SetType> internMap = isMultiCell ? instances : frozenInstances; - SetType t = internMap.get(elements); - return null == t - ? internMap.computeIfAbsent(elements, k -> new SetType<>(k, isMultiCell)) - : t; + return getInstance(isMultiCell ? instances : frozenInstances, + elements, + () -> new SetType<>(elements, isMultiCell)); } public SetType(AbstractType elements, boolean isMultiCell) { - super(ComparisonType.CUSTOM, Kind.SET); + super(ComparisonType.CUSTOM, Kind.SET, isMultiCell, ImmutableList.of(elements)); this.elements = elements; this.serializer = SetSerializer.getInstance(elements.getSerializer(), elements.comparatorSet); - this.isMultiCell = isMultiCell; } @Override - public boolean referencesUserType(V name, ValueAccessor accessor) + @SuppressWarnings("unchecked") + public SetType with(ImmutableList> subTypes, boolean isMultiCell) { - return elements.referencesUserType(name, accessor); - } + Preconditions.checkArgument(subTypes.size() == 1, + "Invalid number of subTypes for SetType (got %s)", subTypes.size()); - @Override - public SetType withUpdatedUserType(UserType udt) - { - if (!referencesUserType(udt.name)) + if (subTypes.equals(this.subTypes()) && isMultiCell == this.isMultiCell()) return this; - (isMultiCell ? instances : frozenInstances).remove(elements); - - return getInstance(elements.withUpdatedUserType(udt), isMultiCell); - } - - @Override - public AbstractType expandUserTypes() - { - return getInstance(elements.expandUserTypes(), isMultiCell); + return getInstance((AbstractType) subTypes.get(0), isMultiCell); } public AbstractType getElementsType() @@ -98,6 +94,7 @@ public AbstractType getElementsType() return elements; } + @Override public AbstractType nameComparator() { return elements; @@ -108,57 +105,6 @@ public AbstractType valueComparator() return EmptyType.instance; } - @Override - public boolean isMultiCell() - { - return isMultiCell; - } - - @Override - public AbstractType freeze() - { - // freeze elements to match org.apache.cassandra.cql3.CQL3Type.Raw.RawCollection.freeze - return isMultiCell ? getInstance(this.elements.freeze(), false) : this; - } - - @Override - public AbstractType unfreeze() - { - return isMultiCell ? this : getInstance(this.elements, true); - } - - @Override - public List> subTypes() - { - return Collections.singletonList(elements); - } - - @Override - public AbstractType freezeNestedMulticellTypes() - { - if (!isMultiCell()) - return this; - - if (elements.isFreezable() && elements.isMultiCell()) - return getInstance(elements.freeze(), isMultiCell); - - return getInstance(elements.freezeNestedMulticellTypes(), isMultiCell); - } - - @Override - public boolean isCompatibleWithFrozen(CollectionType previous) - { - assert !isMultiCell; - return this.elements.isCompatibleWith(((SetType) previous).elements); - } - - @Override - public boolean isValueCompatibleWithFrozen(CollectionType previous) - { - // because sets are ordered, any changes to the type must maintain the ordering - return isCompatibleWithFrozen(previous); - } - public int compareCustom(VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR) { return compareListOrSet(elements, left, accessorL, right, accessorR); @@ -181,21 +127,6 @@ public SetSerializer getSerializer() return serializer; } - @Override - public String toString(boolean ignoreFreezing) - { - boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); - - StringBuilder sb = new StringBuilder(); - if (includeFrozenType) - sb.append(FrozenType.class.getName()).append("("); - sb.append(getClass().getName()); - sb.append(TypeParser.stringifyTypeParameters(Collections.>singletonList(elements), ignoreFreezing || !isMultiCell)); - if (includeFrozenType) - sb.append(")"); - return sb.toString(); - } - public List serializedValues(Iterator> cells) { List bbs = new ArrayList<>(); @@ -243,4 +174,10 @@ public ByteBuffer getMaskedValue() { return decompose(Collections.emptySet()); } + + @Override + public boolean contains(ByteBuffer set, ByteBuffer element) + { + return CollectionSerializer.contains(getElementsType(), set, element, false, false); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java b/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java index a474d39a81c3..23cd91b45495 100644 --- a/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java +++ b/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java @@ -75,9 +75,9 @@ public long toTimeInMillis(ByteBuffer buffer) throws MarshalException } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return this == otherType || otherType == Int32Type.instance; + return this == previous || previous == Int32Type.instance; } public Term fromJSONObject(Object parsed) throws MarshalException diff --git a/src/java/org/apache/cassandra/db/marshal/TimeType.java b/src/java/org/apache/cassandra/db/marshal/TimeType.java index 67cf7dbceb39..4f2062e0b2fc 100644 --- a/src/java/org/apache/cassandra/db/marshal/TimeType.java +++ b/src/java/org/apache/cassandra/db/marshal/TimeType.java @@ -21,13 +21,13 @@ import java.time.LocalTime; import java.time.ZoneOffset; +import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; +import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.TimeSerializer; -import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.serializers.TypeSerializer; -import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; @@ -67,9 +67,9 @@ public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return this == otherType || otherType == LongType.instance; + return this == previous || previous == LongType.instance; } public Term fromJSONObject(Object parsed) throws MarshalException diff --git a/src/java/org/apache/cassandra/db/marshal/TimestampType.java b/src/java/org/apache/cassandra/db/marshal/TimestampType.java index 124060de995f..b0466a0f6af5 100644 --- a/src/java/org/apache/cassandra/db/marshal/TimestampType.java +++ b/src/java/org/apache/cassandra/db/marshal/TimestampType.java @@ -20,16 +20,16 @@ import java.nio.ByteBuffer; import java.util.Date; -import org.apache.cassandra.cql3.Constants; -import org.apache.cassandra.cql3.Duration; -import org.apache.cassandra.cql3.Term; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.serializers.TypeSerializer; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Duration; +import org.apache.cassandra.cql3.Term; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.TimestampSerializer; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.bytecomparable.ByteComparable; @@ -151,9 +151,9 @@ public boolean isCompatibleWith(AbstractType previous) } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return this == otherType || otherType == DateType.instance || otherType == LongType.instance; + return this == previous || previous == DateType.instance || previous == LongType.instance; } public CQL3Type asCQL3Type() diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java index 24d948425d37..73c3d492ac14 100644 --- a/src/java/org/apache/cassandra/db/marshal/TupleType.java +++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java @@ -24,29 +24,30 @@ import java.util.List; import java.util.regex.Pattern; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Objects; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.Tuples; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; -import org.apache.cassandra.serializers.*; +import org.apache.cassandra.serializers.CollectionSerializer; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TupleSerializer; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.JsonUtils; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import static com.google.common.collect.Iterables.any; -import static com.google.common.collect.Iterables.transform; - /** * This is essentially like a CompositeType, but it's not primarily meant for comparison, just * to pack multiple values together so has a more friendly encoding. */ -public class TupleType extends AbstractType +public class TupleType extends MultiCellCapableType { private static final String COLON = ":"; private static final Pattern COLON_PAT = Pattern.compile(COLON); @@ -56,26 +57,22 @@ public class TupleType extends AbstractType private static final Pattern AT_PAT = Pattern.compile(AT); private static final String ESCAPED_AT = "\\\\@"; private static final Pattern ESCAPED_AT_PAT = Pattern.compile(ESCAPED_AT); - - protected final List> types; - private final TupleSerializer serializer; - public TupleType(List> types) + public TupleType(Iterable> subTypes) { - this(types, true); + this(freeze(subTypes), false); } - @VisibleForTesting - public TupleType(List> types, boolean freezeInner) + public TupleType(Iterable> subTypes, boolean isMultiCell) { - super(ComparisonType.CUSTOM); + this(ImmutableList.copyOf(subTypes), isMultiCell); + } - if (freezeInner) - this.types = Lists.newArrayList(transform(types, AbstractType::freeze)); - else - this.types = types; - this.serializer = new TupleSerializer(fieldSerializers(types)); + public TupleType(ImmutableList> subTypes, boolean isMultiCell) + { + super(ComparisonType.CUSTOM, isMultiCell, subTypes); + this.serializer = new TupleSerializer(fieldSerializers(subTypes)); } @Override @@ -96,56 +93,29 @@ private static List> fieldSerializers(List> ty public static TupleType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException { List> types = parser.getTypeParameters(); - for (int i = 0; i < types.size(); i++) - types.set(i, types.get(i).freeze()); - return new TupleType(types); - } - - @Override - public boolean referencesUserType(V name, ValueAccessor accessor) - { - return any(types, t -> t.referencesUserType(name, accessor)); - } - - @Override - public TupleType withUpdatedUserType(UserType udt) - { - return referencesUserType(udt.name) - ? new TupleType(Lists.newArrayList(transform(types, t -> t.withUpdatedUserType(udt)))) - : this; + return new TupleType(types, true); } @Override - public AbstractType expandUserTypes() + public TupleType with(ImmutableList> subTypes, boolean isMultiCell) { - return new TupleType(Lists.newArrayList(transform(types, AbstractType::expandUserTypes))); + return new TupleType(subTypes, isMultiCell); } @Override - public boolean referencesDuration() + public ShortType nameComparator() { - return allTypes().stream().anyMatch(f -> f.referencesDuration()); + return ShortType.instance; } public AbstractType type(int i) { - return types.get(i); + return subTypes.get(i); } public int size() { - return types.size(); - } - - @Override - public List> subTypes() - { - return types; - } - - public List> allTypes() - { - return types; + return subTypes.size(); } public boolean isTuple() @@ -161,9 +131,9 @@ public int compareCustom(VL left, ValueAccessor accessorL, VR right int offsetL = 0; int offsetR = 0; - for (int i = 0; !accessorL.isEmptyFromOffset(left, offsetL) && !accessorR.isEmptyFromOffset(right, offsetR) && i < types.size(); i++) + for (int i = 0; !accessorL.isEmptyFromOffset(left, offsetL) && !accessorR.isEmptyFromOffset(right, offsetR) && i < subTypes.size(); i++) { - AbstractType comparator = types.get(i); + AbstractType comparator = subTypes.get(i); int sizeL = accessorL.getInt(left, offsetL); offsetL += TypeSizes.INT_SIZE; @@ -216,23 +186,24 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteC switch (version) { case LEGACY: - return asComparableBytesLegacy(accessor, data); + case OSS41: + return asComparableBytesLegacy(accessor, data, version); case OSS50: - return asComparableBytesNew(accessor, data, version); + return asComparableBytes50(accessor, data, version); default: throw new AssertionError(); } } - private ByteSource asComparableBytesLegacy(ValueAccessor accessor, V data) + private ByteSource asComparableBytesLegacy(ValueAccessor accessor, V data, ByteComparable.Version version) { if (accessor.isEmpty(data)) return null; V[] bufs = split(accessor, data); // this may be shorter than types.size -- other srcs remain null in that case - ByteSource[] srcs = new ByteSource[types.size()]; + ByteSource[] srcs = new ByteSource[subTypes.size()]; for (int i = 0; i < bufs.length; ++i) - srcs[i] = bufs[i] != null ? types.get(i).asComparableBytes(accessor, bufs[i], ByteComparable.Version.LEGACY) : null; + srcs[i] = bufs[i] != null ? subTypes.get(i).asComparableBytes(accessor, bufs[i], version) : null; // We always have a fixed number of sources, with the trailing ones possibly being nulls. // This can only result in a prefix if the last type in the tuple allows prefixes. Since that type is required @@ -240,7 +211,7 @@ private ByteSource asComparableBytesLegacy(ValueAccessor accessor, V data return ByteSource.withTerminatorLegacy(ByteSource.END_OF_STREAM, srcs); } - private ByteSource asComparableBytesNew(ValueAccessor accessor, V data, ByteComparable.Version version) + private ByteSource asComparableBytes50(ValueAccessor accessor, V data, ByteComparable.Version version) { if (accessor.isEmpty(data)) return null; @@ -253,7 +224,7 @@ private ByteSource asComparableBytesNew(ValueAccessor accessor, V data, B ByteSource[] srcs = new ByteSource[lengthWithoutTrailingNulls]; for (int i = 0; i < lengthWithoutTrailingNulls; ++i) - srcs[i] = bufs[i] != null ? types.get(i).asComparableBytes(accessor, bufs[i], version) : null; + srcs[i] = bufs[i] != null ? subTypes.get(i).asComparableBytes(accessor, bufs[i], version) : null; // Because we stop early when there are trailing nulls, there needs to be an explicit terminator to make the // type prefix-free. @@ -263,27 +234,30 @@ private ByteSource asComparableBytesNew(ValueAccessor accessor, V data, B @Override public V fromComparableBytes(ValueAccessor accessor, ByteSource.Peekable comparableBytes, ByteComparable.Version version) { - assert version == ByteComparable.Version.OSS50; // Reverse translation is not supported for the legacy version. + assert version != ByteComparable.Version.LEGACY; // Reverse translation is not supported for the legacy version. if (comparableBytes == null) return accessor.empty(); - V[] componentBuffers = accessor.createArray(types.size()); - for (int i = 0; i < types.size(); ++i) + V[] componentBuffers = accessor.createArray(subTypes.size()); + for (int i = 0; i < subTypes.size(); ++i) { if (comparableBytes.peek() == ByteSource.TERMINATOR) break; // the rest of the fields remain null - AbstractType componentType = types.get(i); + AbstractType componentType = subTypes.get(i); ByteSource.Peekable component = ByteSourceInverse.nextComponentSource(comparableBytes); if (component != null) componentBuffers[i] = componentType.fromComparableBytes(accessor, component, version); else componentBuffers[i] = null; } - // consume terminator - int terminator = comparableBytes.next(); - assert terminator == ByteSource.TERMINATOR : String.format("Expected TERMINATOR (0x%2x) after %d components", - ByteSource.TERMINATOR, - types.size()); + if (version == ByteComparable.Version.OSS50) + { + // consume terminator + int terminator = comparableBytes.next(); + assert terminator == ByteSource.TERMINATOR : String.format("Expected TERMINATOR (0x%2x) after %d components", + ByteSource.TERMINATOR, + subTypes.size()); + } return buildValue(accessor, componentBuffers); } @@ -440,13 +414,13 @@ public Term fromJSONObject(Object parsed) throws MarshalException List list = (List) parsed; - if (list.size() > types.size()) - throw new MarshalException(String.format("Tuple contains extra items (expected %s): %s", types.size(), parsed)); - else if (types.size() > list.size()) - throw new MarshalException(String.format("Tuple is missing items (expected %s): %s", types.size(), parsed)); + if (list.size() > subTypes.size()) + throw new MarshalException(String.format("Tuple contains extra items (expected %s): %s", subTypes.size(), parsed)); + else if (subTypes.size() > list.size()) + throw new MarshalException(String.format("Tuple is missing items (expected %s): %s", subTypes.size(), parsed)); List terms = new ArrayList<>(list.size()); - Iterator> typeIterator = types.iterator(); + Iterator> typeIterator = subTypes.iterator(); for (Object element : list) { if (element == null) @@ -469,7 +443,7 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) ByteBuffer duplicated = buffer.duplicate(); int offset = 0; StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < types.size(); i++) + for (int i = 0; i < subTypes.size(); i++) { if (i > 0) sb.append(", "); @@ -479,7 +453,7 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) if (value == null) sb.append("null"); else - sb.append(types.get(i).toJSONString(value, protocolVersion)); + sb.append(subTypes.get(i).toJSONString(value, protocolVersion)); } return sb.append("]").toString(); } @@ -490,61 +464,30 @@ public TypeSerializer getSerializer() } @Override - public boolean isCompatibleWith(AbstractType previous) + protected boolean isCompatibleWithFrozen(MultiCellCapableType previous) { if (!(previous instanceof TupleType)) return false; - // Extending with new components is fine, removing is not - TupleType tt = (TupleType)previous; - if (size() < tt.size()) - return false; - - for (int i = 0; i < tt.size(); i++) - { - AbstractType tprev = tt.type(i); - AbstractType tnew = type(i); - if (!tnew.isCompatibleWith(tprev)) - return false; - } - return true; + return isSubTypesCompatibleWith(previous, AbstractType::isCompatibleWith); } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isCompatibleWithMultiCell(MultiCellCapableType previous) { - if (!(otherType instanceof TupleType)) - return false; - - // Extending with new components is fine, removing is not - TupleType tt = (TupleType) otherType; - if (size() < tt.size()) + if (!(previous instanceof TupleType)) return false; - for (int i = 0; i < tt.size(); i++) - { - AbstractType tprev = tt.type(i); - AbstractType tnew = type(i); - if (!tnew.isValueCompatibleWith(tprev)) - return false; - } - return true; - } - - @Override - public int hashCode() - { - return Objects.hashCode(types); + return isSubTypesCompatibleWith(previous, AbstractType::isSerializationCompatibleWith); } @Override - public boolean equals(Object o) + protected boolean isValueCompatibleWithFrozen(MultiCellCapableType previous) { - if (o.getClass() != TupleType.class) + if (!(previous instanceof TupleType)) return false; - TupleType that = (TupleType)o; - return types.equals(that.types); + return isSubTypesCompatibleWith(previous, AbstractType::isValueCompatibleWith); } @Override @@ -554,18 +497,35 @@ public CQL3Type asCQL3Type() } @Override - public String toString() + public String toString(boolean ignoreFreezing) + { + boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); + + StringBuilder sb = new StringBuilder(); + if (includeFrozenType) + sb.append(FrozenType.class.getName()).append('('); + sb.append(getClass().getName()); + // FrozenType applies to anything nested (it wouldn't make sense otherwise) and so we only put once at the + // highest level. So we can ignore freezing in the subtypes if either we're already within a frozen type + // (we're a sub-type ourselves and frozenType has been included at the outer level), or we're frozen. + sb.append(stringifyTypeParameters(ignoreFreezing || !isMultiCell())); + if (includeFrozenType) + sb.append(')'); + return sb.toString(); + } + + protected String stringifyTypeParameters(boolean ignoreFreezing) { - return getClass().getName() + TypeParser.stringifyTypeParameters(types, true); + return TypeParser.stringifyTypeParameters(subTypes, ignoreFreezing); } @Override public ByteBuffer getMaskedValue() { - ByteBuffer[] buffers = new ByteBuffer[types.size()]; - for (int i = 0; i < types.size(); i++) + ByteBuffer[] buffers = new ByteBuffer[subTypes.size()]; + for (int i = 0; i < subTypes.size(); i++) { - AbstractType type = types.get(i); + AbstractType type = subTypes.get(i); buffers[i] = type.getMaskedValue(); } diff --git a/src/java/org/apache/cassandra/db/marshal/TypeParser.java b/src/java/org/apache/cassandra/db/marshal/TypeParser.java index 87df2c380506..6e75ec895436 100644 --- a/src/java/org/apache/cassandra/db/marshal/TypeParser.java +++ b/src/java/org/apache/cassandra/db/marshal/TypeParser.java @@ -30,6 +30,7 @@ import com.google.common.base.Verify; import com.google.common.collect.ImmutableMap; + import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.FieldIdentifier; import org.apache.cassandra.dht.IPartitioner; @@ -50,7 +51,7 @@ public class TypeParser // A cache of parsed string, specially useful for DynamicCompositeType private static volatile ImmutableMap> cache = ImmutableMap.of(); - public static final TypeParser EMPTY_PARSER = new TypeParser("", 0); + private static final TypeParser EMPTY_PARSER = new TypeParser("", 0); private TypeParser(String str, int idx) { @@ -64,7 +65,9 @@ public TypeParser(String str) } /** - * Parse a string containing an type definition. + * Creates a new TypeParser and uses it to parse the given type definition string. + * + * @param str the string to parse. */ public static AbstractType parse(String str) throws SyntaxException, ConfigurationException { @@ -118,15 +121,10 @@ public static AbstractType parse(String str) throws SyntaxException, Configur } } - public static AbstractType parse(CharSequence compareWith) throws SyntaxException, ConfigurationException - { - return parse(compareWith == null ? null : compareWith.toString()); - } - /** * Parse an AbstractType from current position of this parser. */ - public AbstractType parse() throws SyntaxException, ConfigurationException + private AbstractType parse() throws SyntaxException, ConfigurationException { skipBlank(); String name = readNextIdentifier(); @@ -229,18 +227,13 @@ public Map getKeyValueParameters() throws SyntaxException } else if (str.charAt(idx) != ',' && str.charAt(idx) != ')') { - throwSyntaxError("unexpected character '" + str.charAt(idx) + "'"); + throwSyntaxError("unexpected character '" + str.charAt(idx) + '\''); } map.put(k, v); } throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); } - public static String stringifyVectorParameters(AbstractType type, boolean ignoreFreezing, int dimension) - { - return "(" + type.toString(ignoreFreezing) + " , " + dimension + ")"; - } - public Vector getVectorParameters() { if (isEOS()) @@ -344,51 +337,6 @@ public Map> getAliasParameters() throws SyntaxException, C throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); } - public Map getCollectionsParameters() throws SyntaxException, ConfigurationException - { - Map map = new HashMap<>(); - - if (isEOS()) - return map; - - if (str.charAt(idx) != '(') - throw new IllegalStateException(); - - ++idx; // skipping '(' - - while (skipBlankAndComma()) - { - if (str.charAt(idx) == ')') - { - ++idx; - return map; - } - - ByteBuffer bb = fromHex(readNextIdentifier()); - - skipBlank(); - if (str.charAt(idx) != ':') - throwSyntaxError("expecting ':' token"); - - ++idx; - skipBlank(); - try - { - AbstractType type = parse(); - if (!(type instanceof CollectionType)) - throw new SyntaxException(type + " is not a collection type"); - map.put(bb, (CollectionType)type); - } - catch (SyntaxException e) - { - SyntaxException ex = new SyntaxException(String.format("Exception while parsing '%s' around char %d", str, idx)); - ex.initCause(e); - throw ex; - } - } - throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); - } - private ByteBuffer fromHex(String hex) throws SyntaxException { try @@ -402,7 +350,7 @@ private ByteBuffer fromHex(String hex) throws SyntaxException } } - public Pair, List>> getUserTypeParameters() throws SyntaxException, ConfigurationException + public Pair, List>>> getUserTypeParameters() throws SyntaxException, ConfigurationException { if (isEOS() || str.charAt(idx) != '(') @@ -414,7 +362,7 @@ public Pair, List>> getU String keyspace = readNextIdentifier(); skipBlankAndComma(); ByteBuffer typeName = fromHex(readNextIdentifier()); - List> defs = new ArrayList<>(); + List>> defs = new ArrayList<>(); while (skipBlankAndComma()) { @@ -432,7 +380,7 @@ public Pair, List>> getU skipBlank(); try { - AbstractType type = parse(); + AbstractType type = parse(); defs.add(Pair.create(name, type)); } catch (SyntaxException e) @@ -445,10 +393,13 @@ public Pair, List>> getU throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); } - private static AbstractType getAbstractType(String compareWith) throws ConfigurationException + /** + * Parse a type string and return the corresponding {@link AbstractType}. It is used for the type definition which + * is not followed by an opening parenthesis, e.g. "org.apache.cassandra.db.marshal.UTF8Type". + */ + private static AbstractType getAbstractType(String typeName) throws ConfigurationException { - String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith; - Class> typeClass = FBUtilities.>classForName(className, "abstract-type"); + Class> typeClass = getAbstractTypeClass(typeName); try { Field field = typeClass.getDeclaredField("instance"); @@ -461,10 +412,28 @@ private static AbstractType getAbstractType(String compareWith) throws Config } } - private static AbstractType getAbstractType(String compareWith, TypeParser parser) throws SyntaxException, ConfigurationException + private static Class> getAbstractTypeClass(String compareWith) throws ConfigurationException { String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith; - Class> typeClass = FBUtilities.>classForName(className, "abstract-type"); + // Defer class initialization until after confirming this is an AbstractType. The static instance field + // access or getInstance(TypeParser) invocation below performs the initialization for valid types. + @SuppressWarnings("unchecked") + Class> typeClass = + (Class>) FBUtilities.classForNameWithoutInitialization(className, + "abstract-type", + AbstractType.class); + return typeClass; + } + + /** + * Parse a type string and return the corresponding {@link AbstractType}. It is used for the type definition + * which is followed by an opening parenthesis, e.g. + * "org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type)". + */ + private static AbstractType getAbstractType(String compareWith, TypeParser parser) throws SyntaxException, ConfigurationException + { + Class> typeClass = getAbstractTypeClass(compareWith); + try { Method method = typeClass.getDeclaredMethod("getInstance", TypeParser.class); @@ -478,12 +447,15 @@ private static AbstractType getAbstractType(String compareWith, TypeParser pa } catch (InvocationTargetException e) { - ConfigurationException ex = new ConfigurationException("Invalid definition for comparator " + typeClass.getName() + "."); - ex.initCause(e.getTargetException()); - throw ex; + throw new ConfigurationException("Invalid definition for comparator " + typeClass.getName(), e.getTargetException()); } } + /** + * Parse a type string and return the corresponding AbstractType. It is used for the type definition which is not + * followed by an opening parenthesis, e.g. "org.apache.cassandra.db.marshal.UTF8Type", but does not have static + * {@code instance} field. + */ private static AbstractType getRawAbstractType(Class> typeClass) throws ConfigurationException { try @@ -510,9 +482,7 @@ private static AbstractType getRawAbstractType(Class> alias if (iter.hasNext()) { Map.Entry> entry = iter.next(); - sb.append((char)(byte)entry.getKey()).append("=>").append(entry.getValue()); + // Aliases are only used by DynamicCompositeType that is always frozen but without requiring a 'Frozen()' + // in its subtypes' representation. + sb.append((char)(byte)entry.getKey()).append("=>").append(entry.getValue().toString(true)); } while (iter.hasNext()) { Map.Entry> entry = iter.next(); - sb.append(',').append((char)(byte)entry.getKey()).append("=>").append(entry.getValue()); + sb.append(',').append((char)(byte)entry.getKey()).append("=>").append(entry.getValue().toString(true)); } sb.append(')'); return sb.toString(); } - /** - * Helper function to ease the writing of AbstractType.toString() methods. - */ - public static String stringifyTypeParameters(List> types) - { - return stringifyTypeParameters(types, false); - } - /** * Helper function to ease the writing of AbstractType.toString() methods. */ @@ -637,40 +601,22 @@ public static String stringifyTypeParameters(List> types, boolea for (int i = 0; i < types.size(); i++) { if (i > 0) - sb.append(","); + sb.append(','); sb.append(types.get(i).toString(ignoreFreezing)); } return sb.append(')').toString(); } - public static String stringifyCollectionsParameters(Map collections) - { - StringBuilder sb = new StringBuilder(); - sb.append('('); - boolean first = true; - for (Map.Entry entry : collections.entrySet()) - { - if (!first) - sb.append(','); - - first = false; - sb.append(ByteBufferUtil.bytesToHex(entry.getKey())).append(":"); - sb.append(entry.getValue()); - } - sb.append(')'); - return sb.toString(); - } - public static String stringifyUserTypeParameters(String keysace, ByteBuffer typeName, List fields, List> columnTypes, boolean ignoreFreezing) { StringBuilder sb = new StringBuilder(); - sb.append('(').append(keysace).append(",").append(ByteBufferUtil.bytesToHex(typeName)); + sb.append('(').append(keysace).append(',').append(ByteBufferUtil.bytesToHex(typeName)); for (int i = 0; i < fields.size(); i++) { sb.append(','); - sb.append(ByteBufferUtil.bytesToHex(fields.get(i).bytes)).append(":"); + sb.append(ByteBufferUtil.bytesToHex(fields.get(i).bytes)).append(':'); sb.append(columnTypes.get(i).toString(ignoreFreezing)); } sb.append(')'); diff --git a/src/java/org/apache/cassandra/db/marshal/UUIDType.java b/src/java/org/apache/cassandra/db/marshal/UUIDType.java index a5abd922342d..5228c21cb555 100644 --- a/src/java/org/apache/cassandra/db/marshal/UUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/UUIDType.java @@ -27,14 +27,14 @@ import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; -import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.serializers.UUIDSerializer; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.UUIDGen; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import org.apache.cassandra.utils.UUIDGen; /** * Compares UUIDs using the following criteria:
    @@ -136,7 +136,7 @@ public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteC swizzled.putLong(8, accessor.getLong(data, 8)); // fixed-length thus prefix-free - return ByteSource.fixedLength(swizzled); + return ByteSource.preencoded(swizzled); } @Override @@ -177,9 +177,9 @@ static V makeUuidBytes(ValueAccessor accessor, long high, long low) } @Override - public boolean isValueCompatibleWithInternal(AbstractType otherType) + protected boolean isValueCompatibleWithInternal(AbstractType previous) { - return otherType instanceof UUIDType || otherType instanceof TimeUUIDType; + return previous instanceof UUIDType || previous instanceof TimeUUIDType; } @Override diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index a5e79aca8409..089183574e2b 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -18,16 +18,28 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; -import java.util.*; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; import com.google.common.base.Objects; -import com.google.common.collect.Lists; - +import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.cql3.FieldIdentifier; +import org.apache.cassandra.cql3.SchemaElement; +import org.apache.cassandra.cql3.Term; +import org.apache.cassandra.cql3.UserTypes; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.schema.Difference; @@ -57,70 +69,80 @@ public class UserType extends TupleType implements SchemaElement public final String keyspace; public final ByteBuffer name; - private final List fieldNames; - private final List stringFieldNames; - private final boolean isMultiCell; + private final ImmutableList fieldNames; + private final ImmutableList stringFieldNames; private final UserTypeSerializer serializer; + private final int hashCode; + + public UserType(String keyspace, ByteBuffer name, Iterable fieldNames, Iterable> fieldTypes, boolean isMultiCell) + { + this(keyspace, name, ImmutableList.copyOf(fieldNames), ImmutableList.copyOf(fieldTypes), isMultiCell); + } - public UserType(String keyspace, ByteBuffer name, List fieldNames, List> fieldTypes, boolean isMultiCell) + public UserType(String keyspace, ByteBuffer name, ImmutableList fieldNames, ImmutableList> fieldTypes, boolean isMultiCell) { - super(fieldTypes, false); + super(isMultiCell ? fieldTypes : freeze(fieldTypes), isMultiCell); assert fieldNames.size() == fieldTypes.size(); + this.hashCode = Objects.hashCode(fieldNames, keyspace, name, super.hashCode()); this.keyspace = keyspace; this.name = name; this.fieldNames = fieldNames; - this.stringFieldNames = new ArrayList<>(fieldNames.size()); - this.isMultiCell = isMultiCell; + ImmutableList.Builder stringFieldNamesBuilder = ImmutableList.builderWithExpectedSize(this.fieldNames.size()); - LinkedHashMap> fieldSerializers = new LinkedHashMap<>(fieldTypes.size()); - for (int i = 0, m = fieldNames.size(); i < m; i++) + LinkedHashMap> fieldSerializers = new LinkedHashMap<>(subTypes().size()); + for (int i = 0; i < this.fieldNames.size(); i++) { - String stringFieldName = fieldNames.get(i).toString(); - stringFieldNames.add(stringFieldName); - TypeSerializer existing = fieldSerializers.put(stringFieldName, fieldTypes.get(i).getSerializer()); + String stringFieldName = this.fieldNames.get(i).toString(); + stringFieldNamesBuilder.add(stringFieldName); + TypeSerializer existing = fieldSerializers.put(stringFieldName, subTypes().get(i).getSerializer()); if (existing != null) CONFLICT_BEHAVIOR.onConflict(keyspace, getNameAsString(), stringFieldName); } + this.stringFieldNames = stringFieldNamesBuilder.build(); this.serializer = new UserTypeSerializer(fieldSerializers); } + @Override + public UserType with(ImmutableList> subTypes, boolean isMultiCell) + { + return new UserType(keyspace, name, fieldNames, subTypes, isMultiCell); + } + public static UserType getInstance(TypeParser parser) { - Pair, List>> params = parser.getUserTypeParameters(); + Pair, List>>> params = parser.getUserTypeParameters(); String keyspace = params.left.left; ByteBuffer name = params.left.right; - List columnNames = new ArrayList<>(params.right.size()); - List> columnTypes = new ArrayList<>(params.right.size()); - for (Pair p : params.right) + ImmutableList.Builder columnNames = ImmutableList.builderWithExpectedSize(params.right.size()); + ImmutableList.Builder> columnTypes = ImmutableList.builderWithExpectedSize(params.right.size()); + for (Pair> p : params.right) { columnNames.add(new FieldIdentifier(p.left)); columnTypes.add(p.right); } - return new UserType(keyspace, name, columnNames, columnTypes, true); + return new UserType(keyspace, name, columnNames.build(), columnTypes.build(), true); } @Override - public boolean isUDT() + public UserType overrideKeyspace(Function overrideKeyspace) { - return true; - } + String newKeyspace = overrideKeyspace.apply(keyspace); + if (newKeyspace.equals(keyspace)) + return this; - public boolean isTuple() - { - return false; + return new UserType(newKeyspace, name, fieldNames, subTypes().stream().map(t -> t.overrideKeyspace(overrideKeyspace)).collect(ImmutableList.toImmutableList()), isMultiCell()); } @Override - public boolean isMultiCell() + public boolean isUDT() { - return isMultiCell; + return true; } - @Override - public boolean isFreezable() + public boolean isTuple() { - return true; + return false; } public AbstractType fieldType(int i) @@ -128,9 +150,9 @@ public AbstractType fieldType(int i) return type(i); } - public List> fieldTypes() + public ImmutableList> fieldTypes() { - return types; + return subTypes; } public FieldIdentifier fieldName(int i) @@ -143,7 +165,7 @@ public String fieldNameAsString(int i) return stringFieldNames.get(i); } - public List fieldNames() + public ImmutableList fieldNames() { return fieldNames; } @@ -164,11 +186,6 @@ public CellPath cellPathForField(FieldIdentifier fieldName) return CellPath.create(ByteBufferUtil.bytes((short)fieldPosition(fieldName))); } - public ShortType nameComparator() - { - return ShortType.instance; - } - public ByteBuffer serializeForNativeProtocol(Iterator> cells, ProtocolVersion protocolVersion) { assert isMultiCell; @@ -223,13 +240,13 @@ public Term fromJSONObject(Object parsed) throws MarshalException JsonUtils.handleCaseSensitivity(map); - List terms = new ArrayList<>(types.size()); + List terms = new ArrayList<>(subTypes.size()); Set keys = map.keySet(); assert keys.isEmpty() || keys.iterator().next() instanceof String; int foundValues = 0; - for (int i = 0; i < types.size(); i++) + for (int i = 0; i < subTypes.size(); i++) { Object value = map.get(stringFieldNames.get(i)); if (value == null) @@ -238,7 +255,7 @@ public Term fromJSONObject(Object parsed) throws MarshalException } else { - terms.add(types.get(i).fromJSONObject(value)); + terms.add(subTypes.get(i).fromJSONObject(value)); foundValues += 1; } } @@ -262,7 +279,7 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) { ByteBuffer[] buffers = split(ByteBufferAccessor.instance, buffer); StringBuilder sb = new StringBuilder("{"); - for (int i = 0; i < types.size(); i++) + for (int i = 0; i < subTypes.size(); i++) { if (i > 0) sb.append(", "); @@ -279,7 +296,7 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) if (valueBuffer == null) sb.append("null"); else - sb.append(types.get(i).toJSONString(valueBuffer, protocolVersion)); + sb.append(subTypes.get(i).toJSONString(valueBuffer, protocolVersion)); } return sb.append("}").toString(); } @@ -287,72 +304,25 @@ public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) @Override public UserType freeze() { - return isMultiCell ? new UserType(keyspace, name, fieldNames, fieldTypes(), false) : this; - } - - @Override - public UserType unfreeze() - { - return isMultiCell ? this : new UserType(keyspace, name, fieldNames, fieldTypes(), true); - } - - @Override - public AbstractType freezeNestedMulticellTypes() - { - if (!isMultiCell()) - return this; - - // the behavior here doesn't exactly match the method name: we want to freeze everything inside of UDTs - List> newTypes = fieldTypes().stream() - .map(subtype -> (subtype.isFreezable() && subtype.isMultiCell() ? subtype.freeze() : subtype)) - .collect(Collectors.toList()); - - return new UserType(keyspace, name, fieldNames, newTypes, isMultiCell); + return (UserType) super.freeze(); } @Override public int hashCode() { - return Objects.hashCode(keyspace, name, fieldNames, types, isMultiCell); - } - - @Override - public boolean isValueCompatibleWith(AbstractType previous) - { - if (this == previous) - return true; - - if (!(previous instanceof UserType)) - return false; - - UserType other = (UserType) previous; - if (isMultiCell != other.isMultiCell()) - return false; - - if (!keyspace.equals(other.keyspace)) - return false; - - Iterator> thisTypeIter = types.iterator(); - Iterator> previousTypeIter = other.types.iterator(); - while (thisTypeIter.hasNext() && previousTypeIter.hasNext()) - { - if (!thisTypeIter.next().isCompatibleWith(previousTypeIter.next())) - return false; - } - - // it's okay for the new type to have additional fields, but not for the old type to have additional fields - return !previousTypeIter.hasNext(); + return hashCode; } @Override public boolean equals(Object o) { - if (o.getClass() != UserType.class) + if (o == this) + return true; + if (!super.equals(o)) return false; - UserType that = (UserType)o; - - return equalsWithoutTypes(that) && types.equals(that.types); + UserType that = (UserType) o; + return equalsWithoutTypes(that); } private boolean equalsWithoutTypes(UserType other) @@ -402,49 +372,27 @@ public boolean referencesUserType(V name, ValueAccessor accessor) @Override public UserType withUpdatedUserType(UserType udt) { - if (!referencesUserType(udt.name)) - return this; + // If we're not the UDT to update, we can rely on the default implementation + if (!name.equals(udt.name)) + return (UserType) super.withUpdatedUserType(udt); - // preserve frozen/non-frozen status of the updated UDT - if (name.equals(udt.name)) - { - return isMultiCell == udt.isMultiCell - ? udt - : new UserType(keyspace, name, udt.fieldNames(), udt.fieldTypes(), isMultiCell); - } + assert udt.isMultiCell(); - return new UserType(keyspace, - name, - fieldNames, - Lists.newArrayList(transform(fieldTypes(), t -> t.withUpdatedUserType(udt))), - isMultiCell()); + // The type we're updating may be frozen, while the updated user type will never be (a UDT is never frozen in + // its definition, only in its use). So if we are frozen, we should freeze the UDT we switch to. + return isMultiCell() ? udt : udt.freeze(); } @Override - public boolean referencesDuration() + public AbstractType expandUserTypes() { - return fieldTypes().stream().anyMatch(f -> f.referencesDuration()); + return new TupleType(ImmutableList.copyOf(transform(subTypes, AbstractType::expandUserTypes)), isMultiCell()); } @Override - public String toString() + protected String stringifyTypeParameters(boolean ignoreFreezing) { - return this.toString(false); - } - - @Override - public String toString(boolean ignoreFreezing) - { - boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); - - StringBuilder sb = new StringBuilder(); - if (includeFrozenType) - sb.append(FrozenType.class.getName()).append("("); - sb.append(getClass().getName()); - sb.append(TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types, ignoreFreezing || !isMultiCell)); - if (includeFrozenType) - sb.append(")"); - return sb.toString(); + return TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, subTypes, ignoreFreezing || !isMultiCell()); } public String getCqlTypeName() @@ -489,7 +437,7 @@ public String toCqlString(boolean withInternals, boolean ifNotExists) builder.appendQuotingIfNeeded(keyspace) .append('.') - .appendQuotingIfNeeded(getNameAsString()) + .appendTypeQuotingIfNeeded(getNameAsString()) .append(" (") .newLine() .increaseIndent(); diff --git a/src/java/org/apache/cassandra/db/marshal/ValueAccessor.java b/src/java/org/apache/cassandra/db/marshal/ValueAccessor.java index b4f775de9522..935714901fb1 100644 --- a/src/java/org/apache/cassandra/db/marshal/ValueAccessor.java +++ b/src/java/org/apache/cassandra/db/marshal/ValueAccessor.java @@ -41,7 +41,12 @@ import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.vint.VIntCoding; -import static org.apache.cassandra.db.ClusteringPrefix.Kind.*; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.EXCL_END_BOUND; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.EXCL_START_BOUND; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.INCL_END_BOUND; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.INCL_END_EXCL_START_BOUNDARY; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.INCL_START_BOUND; /** * ValueAccessor allows serializers and other code dealing with raw bytes to operate on different backing types @@ -69,7 +74,7 @@ public interface ObjectFactory Cell cell(ColumnMetadata column, long timestamp, int ttl, long localDeletionTime, V value, CellPath path); Clustering clustering(V... values); Clustering clustering(); - Clustering staticClustering(); + // Note: the static clustering is always Clustering.STATIC_CLUSTERING (of ByteBuffer accessor). ClusteringBound bound(ClusteringPrefix.Kind kind, V... values); ClusteringBound bound(ClusteringPrefix.Kind kind); ClusteringBoundary boundary(ClusteringPrefix.Kind kind, V... values); diff --git a/src/java/org/apache/cassandra/db/marshal/VectorType.java b/src/java/org/apache/cassandra/db/marshal/VectorType.java index d922d37b4262..db659a713e20 100644 --- a/src/java/org/apache/cassandra/db/marshal/VectorType.java +++ b/src/java/org/apache/cassandra/db/marshal/VectorType.java @@ -19,15 +19,19 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; +import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; - import javax.annotation.Nullable; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.cql3.Vectors; @@ -41,6 +45,7 @@ import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; + public final class VectorType extends AbstractType> { private static class Key @@ -74,18 +79,27 @@ public int hashCode() return Objects.hash(type, dimension); } } + @SuppressWarnings("rawtypes") - private static final ConcurrentHashMap instances = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> instances = new ConcurrentHashMap<>(); public final AbstractType elementType; public final int dimension; private final TypeSerializer elementSerializer; private final int valueLengthIfFixed; private final VectorSerializer serializer; + private final int hashCode; + + private static final boolean isVectorTypeAllowed = CassandraRelevantProperties.VECTOR_TYPE_ALLOWED.getBoolean(); + private static final boolean isVectorTypeFloatOnly = CassandraRelevantProperties.VECTOR_FLOAT_ONLY.getBoolean(); private VectorType(AbstractType elementType, int dimension) { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, false, ImmutableList.of(elementType)); + if (!isVectorTypeAllowed) + throw new InvalidRequestException("vector type is not allowed"); + if (isVectorTypeFloatOnly && !(elementType instanceof FloatType)) + throw new InvalidRequestException(String.format("vectors may only use float. given %s", elementType.asCQL3Type())); if (dimension <= 0) throw new InvalidRequestException(String.format("vectors may only have positive dimensions; given %d", dimension)); this.elementType = elementType; @@ -97,13 +111,14 @@ private VectorType(AbstractType elementType, int dimension) this.serializer = elementType.isValueLengthFixed() ? new FixedLengthSerializer() : new VariableLengthSerializer(); + this.hashCode = Objects.hash(elementType, dimension); } @SuppressWarnings("unchecked") public static VectorType getInstance(AbstractType elements, int dimension) { Key key = new Key(elements, dimension); - return instances.computeIfAbsent(key, Key::create); + return (VectorType) getInstance(instances, key, key::create); } public static VectorType getInstance(TypeParser parser) @@ -112,6 +127,19 @@ public static VectorType getInstance(TypeParser parser) return getInstance(v.type.freeze(), v.dimension); } + @Override + @SuppressWarnings("unchecked") + public VectorType with(ImmutableList> subTypes, boolean isMultiCell) + { + Preconditions.checkArgument(subTypes.size() == 1, "Invalid number of subTypes for VectorType (got %s)", subTypes.size()); + Preconditions.checkArgument(!isMultiCell, "Cannot create a multi-cell VectorType"); + + if (subTypes.equals(this.subTypes())) + return this; + + return getInstance((AbstractType) subTypes.get(0), dimension); + } + @Override public boolean isVector() { @@ -162,6 +190,36 @@ public float[] composeAsFloat(V input, ValueAccessor accessor) return accessor.toFloatArray(input, dimension); } + @Override + @SuppressWarnings("unchecked") + public ByteBuffer decomposeUntyped(Object value) + { + if (value instanceof List && elementType instanceof NumberType) + { + List list = (List) value; + Class expectedClass = elementSerializer.getType(); + if (!list.isEmpty() && !expectedClass.isInstance(list.get(0)) && list.get(0) instanceof Number) + { + List converted = new ArrayList<>(list.size()); + for (Object e : list) + converted.add((T) convertNumber((Number) e, expectedClass)); + return decompose(converted); + } + } + return super.decomposeUntyped(value); + } + + private static Number convertNumber(Number value, Class targetClass) + { + if (targetClass == Float.class) return value.floatValue(); + if (targetClass == Double.class) return value.doubleValue(); + if (targetClass == Integer.class) return value.intValue(); + if (targetClass == Long.class) return value.longValue(); + if (targetClass == Short.class) return value.shortValue(); + if (targetClass == Byte.class) return value.byteValue(); + throw new IllegalArgumentException("Unsupported numeric type: " + targetClass); + } + public ByteBuffer decompose(T... values) { return decompose(Arrays.asList(values)); @@ -262,12 +320,6 @@ public ByteBuffer fromString(String source) throws MarshalException } } - @Override - public List> subTypes() - { - return Collections.singletonList(elementType); - } - @Override public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) { @@ -317,28 +369,24 @@ public Term fromJSONObject(Object parsed) throws MarshalException @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (!super.equals(o)) + return false; VectorType that = (VectorType) o; - return dimension == that.dimension && Objects.equals(elementType, that.elementType); + return dimension == that.dimension; } @Override public int hashCode() { - return Objects.hash(elementType, dimension); - } - - @Override - public String toString() - { - return toString(false); + return hashCode; } @Override public String toString(boolean ignoreFreezing) { - return getClass().getName() + TypeParser.stringifyVectorParameters(elementType, ignoreFreezing, dimension); + return String.format("%s(%s,%d)", getClass().getName(), elementType, dimension); } private void check(List values) @@ -380,6 +428,8 @@ public abstract class VectorSerializer extends TypeSerializer> public abstract List split(V buffer, ValueAccessor accessor); public abstract V serializeRaw(List elements, ValueAccessor accessor); + public abstract float[] deserializeFloatArray(ByteBuffer input); + public abstract ByteBuffer serializeFloatArray(float[] value); @Override public String toString(List value) @@ -426,6 +476,8 @@ public int compareCustom(VL left, ValueAccessor accessorL, { if (elementType.isByteOrderComparable) return ValueAccessor.compare(left, accessorL, right, accessorR); + if (accessorL.isEmpty(left) || accessorR.isEmpty(right)) + return Boolean.compare(accessorR.isEmpty(right), accessorL.isEmpty(left)); int offset = 0; int elementLength = elementType.valueLengthIfFixed(); for (int i = 0; i < dimension; i++) @@ -510,6 +562,34 @@ public List deserialize(V input, ValueAccessor accessor) return result; } + @Override + public float[] deserializeFloatArray(ByteBuffer input) + { + if (input == null || input.remaining() == 0) + return null; + + FloatBuffer floatBuffer = input.asFloatBuffer(); + float[] floatArray = new float[floatBuffer.remaining()]; + floatBuffer.get(floatArray); + + return floatArray; + } + + @Override + public ByteBuffer serializeFloatArray(float[] value) + { + if (elementType != FloatType.instance) + throw new UnsupportedOperationException(); + + if (value.length != dimension) + throw new MarshalException(String.format("Required %d elements, but saw %d", dimension, value.length)); + + var fb = FloatBuffer.wrap(value); + var bb = ByteBuffer.allocate(fb.capacity() * Float.BYTES); + bb.asFloatBuffer().put(fb); + return bb; + } + @Override public void validate(V input, ValueAccessor accessor) throws MarshalException { @@ -543,6 +623,9 @@ private VariableLengthSerializer() public int compareCustom(VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR) { + if (accessorL.isEmpty(left) || accessorR.isEmpty(right)) + return Boolean.compare(accessorR.isEmpty(right), accessorL.isEmpty(left)); + int leftOffset = 0; int rightOffset = 0; for (int i = 0; i < dimension; i++) @@ -648,6 +731,17 @@ public List deserialize(V input, ValueAccessor accessor) return result; } + public float[] deserializeFloatArray(ByteBuffer input) + { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuffer serializeFloatArray(float[] value) + { + throw new UnsupportedOperationException(); + } + @Override public void validate(V input, ValueAccessor accessor) throws MarshalException { diff --git a/src/java/org/apache/cassandra/db/marshal/datetime/DateRange.java b/src/java/org/apache/cassandra/db/marshal/datetime/DateRange.java new file mode 100644 index 000000000000..867528a6470a --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/datetime/DateRange.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.marshal.datetime; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Locale; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.builder.EqualsBuilder; + +import org.apache.cassandra.db.marshal.DateRangeType; + +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MILLI_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; + +/** + * Domain object of type {@link DateRangeType}. Lower and upper bounds are inclusive. Value type. + */ +public class DateRange +{ + private final DateRangeBound lowerBound; + private final DateRangeBound upperBound; + + public DateRange(DateRangeBound lowerBound) + { + Preconditions.checkArgument(lowerBound != null); + this.lowerBound = lowerBound; + this.upperBound = null; + } + + public DateRange(DateRangeBound lowerBound, DateRangeBound upperBound) + { + Preconditions.checkArgument(lowerBound != null); + Preconditions.checkArgument(upperBound != null); + Preconditions.checkArgument(upperBound.isAfter(lowerBound), "Wrong order: " + lowerBound + " TO " + upperBound); + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + private DateRange(DateRangeBuilder builder) + { + this.lowerBound = builder.lowerBound; + this.upperBound = builder.upperBound; + } + + public DateRangeBound getLowerBound() + { + return lowerBound; + } + + public DateRangeBound getUpperBound() + { + return upperBound; + } + + public boolean isUpperBoundDefined() + { + return upperBound != null; + } + + public String formatToSolrString() + { + if (isUpperBoundDefined()) + { + return String.format("[%s TO %s]", lowerBound, upperBound); + } + else + { + return lowerBound.toString(); + } + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("lowerBound", lowerBound) + .add("precision", lowerBound.getPrecision()) + .add("upperBound", upperBound) + .add("precision", upperBound != null ? upperBound.getPrecision() : "null") + .toString(); + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || obj.getClass() != getClass()) + { + return false; + } + if (obj == this) + { + return true; + } + + DateRange rhs = (DateRange) obj; + return new EqualsBuilder() + .append(lowerBound, rhs.lowerBound) + .append(upperBound, rhs.upperBound) + .isEquals(); + } + + @Override + public int hashCode() + { + return Objects.hashCode(lowerBound, upperBound); + } + + public static class DateRangeBound + { + public static final DateRangeBound UNBOUNDED = new DateRangeBound(); + + private final ZonedDateTime timestamp; + private final Precision precision; + + private DateRangeBound(ZonedDateTime timestamp, Precision precision) + { + Preconditions.checkArgument(timestamp != null); + Preconditions.checkArgument(precision != null); + this.timestamp = timestamp; + this.precision = precision; + } + + private DateRangeBound() + { + this.timestamp = null; + this.precision = null; + } + + public static DateRangeBound lowerBound(Instant timestamp, Precision precision) + { + return lowerBound(ZonedDateTime.ofInstant(timestamp, ZoneOffset.UTC), precision); + } + + public static DateRangeBound lowerBound(ZonedDateTime timestamp, Precision precision) + { + ZonedDateTime roundedLowerBound = DateRangeUtil.roundLowerBoundTimestampToPrecision(timestamp, precision); + return new DateRangeBound(roundedLowerBound, precision); + } + + public static DateRangeBound upperBound(Instant timestamp, Precision precision) + { + return upperBound(ZonedDateTime.ofInstant(timestamp, ZoneOffset.UTC), precision); + } + + public static DateRangeBound upperBound(ZonedDateTime timestamp, Precision precision) + { + ZonedDateTime roundedUpperBound = DateRangeUtil.roundUpperBoundTimestampToPrecision(timestamp, precision); + return new DateRangeBound(roundedUpperBound, precision); + } + + public boolean isUnbounded() + { + return timestamp == null; + } + + public boolean isAfter(DateRangeBound other) + { + return isUnbounded() || other.isUnbounded() || timestamp.isAfter(other.timestamp); + } + + public Instant getTimestamp() + { + return timestamp.toInstant(); + } + + public Precision getPrecision() + { + return precision; + } + + @Override + public String toString() + { + if (isUnbounded()) + { + return "*"; + } + + return precision.formatter.format(timestamp); + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || obj.getClass() != getClass()) + { + return false; + } + if (obj == this) + { + return true; + } + + DateRangeBound rhs = (DateRangeBound) obj; + return new EqualsBuilder() + .append(isUnbounded(), rhs.isUnbounded()) + .append(timestamp, rhs.timestamp) + .append(precision, rhs.precision) + .isEquals(); + } + + @Override + public int hashCode() + { + return Objects.hashCode(timestamp, precision); + } + + public enum Precision + { + YEAR(0x00, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu") + .parseDefaulting(MONTH_OF_YEAR, 1) + .parseDefaulting(DAY_OF_MONTH, 1) + .parseDefaulting(HOUR_OF_DAY, 0) + .parseDefaulting(MINUTE_OF_HOUR, 0) + .parseDefaulting(SECOND_OF_MINUTE, 0) + .parseDefaulting(MILLI_OF_SECOND, 0) + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)), + + MONTH(0x01, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu-MM") + .parseDefaulting(DAY_OF_MONTH, 1) + .parseDefaulting(HOUR_OF_DAY, 0) + .parseDefaulting(MINUTE_OF_HOUR, 0) + .parseDefaulting(SECOND_OF_MINUTE, 0) + .parseDefaulting(MILLI_OF_SECOND, 0) + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)), + + DAY(0x02, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu-MM-dd") + .parseDefaulting(HOUR_OF_DAY, 0) + .parseDefaulting(MINUTE_OF_HOUR, 0) + .parseDefaulting(SECOND_OF_MINUTE, 0) + .parseDefaulting(MILLI_OF_SECOND, 0) + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)), + + HOUR(0x03, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu-MM-dd'T'HH") + .parseDefaulting(MINUTE_OF_HOUR, 0) + .parseDefaulting(SECOND_OF_MINUTE, 0) + .parseDefaulting(MILLI_OF_SECOND, 0) + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)), + + MINUTE(0x04, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu-MM-dd'T'HH:mm") + .parseDefaulting(SECOND_OF_MINUTE, 0) + .parseDefaulting(MILLI_OF_SECOND, 0) + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)), + + SECOND(0x05, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu-MM-dd'T'HH:mm:ss") + .parseDefaulting(MILLI_OF_SECOND, 0) + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)), + + MILLISECOND(0x06, + new DateTimeFormatterBuilder() + .parseCaseSensitive() + .parseStrict() + .appendPattern("uuuu-MM-dd'T'HH:mm:ss.SSS") + .optionalStart() + .appendZoneId() + .optionalEnd() + .toFormatter() + .withZone(ZoneOffset.UTC) + .withLocale(Locale.ROOT)); + + private final int encoded; + private final DateTimeFormatter formatter; + + Precision(int encoded, DateTimeFormatter formatter) + { + this.encoded = encoded; + this.formatter = formatter; + } + + public int toEncoded() + { + return encoded; + } + + public static Precision fromEncoded(byte encoded) + { + for (Precision precision : values()) + { + if (precision.encoded == encoded) + { + return precision; + } + } + throw new IllegalArgumentException("Invalid precision encoding: " + encoded); + } + } + } + + public static class DateRangeBuilder + { + private DateRangeBound lowerBound = null; + private DateRangeBound upperBound = null; + + private DateRangeBuilder() {} + + public static DateRangeBuilder dateRange() + { + return new DateRangeBuilder(); + } + + public DateRangeBuilder withLowerBound(String lowerBound, DateRangeBound.Precision precision) + { + return withLowerBound(Instant.parse(lowerBound), precision); + } + + public DateRangeBuilder withUnboundedLowerBound() + { + this.lowerBound = DateRangeBound.UNBOUNDED; + return this; + } + + public DateRangeBuilder withUnboundedUpperBound() + { + this.upperBound = DateRangeBound.UNBOUNDED; + return this; + } + + public DateRangeBuilder withUpperBound(String upperBound, DateRangeBound.Precision precision) + { + return withUpperBound(Instant.parse(upperBound), precision); + } + + public DateRangeBuilder withLowerBound(Instant lowerBound, DateRangeBound.Precision precision) + { + this.lowerBound = DateRangeBound.lowerBound(lowerBound, precision); + return this; + } + + public DateRangeBuilder withUpperBound(Instant upperBound, DateRangeBound.Precision precision) + { + this.upperBound = DateRangeBound.upperBound(upperBound, precision); + return this; + } + + public DateRange build() + { + return new DateRange(this); + } + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/datetime/DateRangeUtil.java b/src/java/org/apache/cassandra/db/marshal/datetime/DateRangeUtil.java new file mode 100644 index 000000000000..8b73cfaa0899 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/datetime/DateRangeUtil.java @@ -0,0 +1,286 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal.datetime; + +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.commons.lang3.StringUtils; + +import org.apache.cassandra.db.marshal.datetime.DateRange.DateRangeBound; + +import static java.time.temporal.TemporalAdjusters.firstDayOfMonth; +import static java.time.temporal.TemporalAdjusters.firstDayOfYear; +import static java.time.temporal.TemporalAdjusters.lastDayOfMonth; +import static java.time.temporal.TemporalAdjusters.lastDayOfYear; + +public class DateRangeUtil +{ + private static final int YEAR_LEVEL = 3; + private static final int[] FIELD_BY_LEVEL = + { + -1/*unused*/, -1, -1, Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, + Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND + }; + private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); + + public static DateRange parseDateRange(String source) throws ParseException + { + if (StringUtils.isBlank(source)) + { + throw new IllegalArgumentException("Date range is null or blank"); + } + if (source.charAt(0) == '[') + { + if (source.charAt(source.length() - 1) != ']') + { + throw new IllegalArgumentException("If date range starts with [ must end with ]; got " + source); + } + int middle = source.indexOf(" TO "); + if (middle < 0) + { + throw new IllegalArgumentException("If date range starts with [ must contain ' TO '; got " + source); + } + String lowerBoundString = source.substring(1, middle); + String upperBoundString = source.substring(middle + " TO ".length(), source.length() - 1); + return new DateRange(parseLowerBound(lowerBoundString), parseUpperBound(upperBoundString)); + } + else + { + return new DateRange(parseLowerBound(source)); + } + } + + public static ZonedDateTime roundUpperBoundTimestampToPrecision(ZonedDateTime timestamp, DateRangeBound.Precision precision) + { + switch (precision) + { + case YEAR: + timestamp = timestamp.with(lastDayOfYear()); + case MONTH: + timestamp = timestamp.with(lastDayOfMonth()); + case DAY: + timestamp = timestamp.with(ChronoField.HOUR_OF_DAY, 23); + case HOUR: + timestamp = timestamp.with(ChronoField.MINUTE_OF_HOUR, 59); + case MINUTE: + timestamp = timestamp.with(ChronoField.SECOND_OF_MINUTE, 59); + case SECOND: + timestamp = timestamp.with(ChronoField.MILLI_OF_SECOND, 999); + case MILLISECOND: + // DateRangeField ignores any precision beyond milliseconds + return timestamp; + default: + throw new IllegalStateException("Unsupported date time precision for the upper bound: " + precision); + } + } + + public static ZonedDateTime roundLowerBoundTimestampToPrecision(ZonedDateTime timestamp, DateRangeBound.Precision precision) + { + switch (precision) + { + case YEAR: + timestamp = timestamp.with(firstDayOfYear()); + case MONTH: + timestamp = timestamp.with(firstDayOfMonth()); + case DAY: + timestamp = timestamp.with(ChronoField.HOUR_OF_DAY, 0); + case HOUR: + timestamp = timestamp.with(ChronoField.MINUTE_OF_HOUR, 0); + case MINUTE: + timestamp = timestamp.with(ChronoField.SECOND_OF_MINUTE, 0); + case SECOND: + timestamp = timestamp.with(ChronoField.MILLI_OF_SECOND, 0); + case MILLISECOND: + // DateRangeField ignores any precision beyond milliseconds + return timestamp; + default: + throw new IllegalStateException("Unsupported date time precision for the upper bound: " + precision); + } + } + + private static DateRangeBound parseLowerBound(String source) throws ParseException + { + Calendar lowerBoundCalendar = parseCalendar(source); + int calPrecisionField = getCalPrecisionField(lowerBoundCalendar); + if (calPrecisionField < 0) + { + return DateRangeBound.UNBOUNDED; + } + return DateRangeBound.lowerBound(toZonedDateTime(lowerBoundCalendar), getCalendarPrecision(calPrecisionField)); + } + + private static DateRangeBound parseUpperBound(String source) throws ParseException + { + Calendar upperBoundCalendar = parseCalendar(source); + int calPrecisionField = getCalPrecisionField(upperBoundCalendar); + if (calPrecisionField < 0) + { + return DateRangeBound.UNBOUNDED; + } + ZonedDateTime upperBoundDateTime = toZonedDateTime(upperBoundCalendar); + DateRangeBound.Precision precision = getCalendarPrecision(calPrecisionField); + return DateRangeBound.upperBound(upperBoundDateTime, precision); + } + + /** + * This method was extracted from org.apache.lucene.spatial.prefix.tree.DateRangePrefixTree + * (Apache Lucene™) for compatibility with DSE. + * The class is distributed under Apache-2.0 License attached to this release. + * + * Calendar utility method: + * Gets the Calendar field code of the last field that is set prior to an unset field. It only + * examines fields relevant to the prefix tree. If no fields are set, it returns -1. */ + private static int getCalPrecisionField(Calendar cal) { + int lastField = -1; + for (int level = YEAR_LEVEL; level < FIELD_BY_LEVEL.length; level++) { + int field = FIELD_BY_LEVEL[level]; + if (!cal.isSet(field)) + break; + lastField = field; + } + return lastField; + } + + /** + * This method was extracted from org.apache.lucene.spatial.prefix.tree.DateRangePrefixTree + * (Apache Lucene™) for compatibility with DSE. + * The class is distributed under Apache-2.0 License attached to this release. + * + * Calendar utility method: + * It will only set the fields found, leaving + * the remainder in an un-set state. A leading '-' or '+' is optional (positive assumed), and a + * trailing 'Z' is also optional. + * @param str not null and not empty + * @return not null + */ + private static Calendar parseCalendar(String str) throws ParseException { + // example: +2014-10-23T21:22:33.159Z + if (str == null || str.isEmpty()) + throw new IllegalArgumentException("str is null or blank"); + Calendar cal = Calendar.getInstance(UTC_TIME_ZONE, Locale.ROOT); + cal.clear(); + if (str.equals("*")) + return cal; + int offset = 0;//a pointer + try { + //year & era: + int lastOffset = str.charAt(str.length()-1) == 'Z' ? str.length() - 1 : str.length(); + int hyphenIdx = str.indexOf('-', 1);//look past possible leading hyphen + if (hyphenIdx < 0) + hyphenIdx = lastOffset; + int year = Integer.parseInt(str.substring(offset, hyphenIdx)); + cal.set(Calendar.ERA, year <= 0 ? 0 : 1); + cal.set(Calendar.YEAR, year <= 0 ? -1*year + 1 : year); + offset = hyphenIdx + 1; + if (lastOffset < offset) + return cal; + + //NOTE: We aren't validating separator chars, and we unintentionally accept leading +/-. + // The str.substring()'s hopefully get optimized to be stack-allocated. + + //month: + cal.set(Calendar.MONTH, Integer.parseInt(str.substring(offset, offset+2)) - 1);//starts at 0 + offset += 3; + if (lastOffset < offset) + return cal; + //day: + cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(str.substring(offset, offset+2))); + offset += 3; + if (lastOffset < offset) + return cal; + //hour: + cal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(str.substring(offset, offset+2))); + offset += 3; + if (lastOffset < offset) + return cal; + //minute: + cal.set(Calendar.MINUTE, Integer.parseInt(str.substring(offset, offset+2))); + offset += 3; + if (lastOffset < offset) + return cal; + //second: + cal.set(Calendar.SECOND, Integer.parseInt(str.substring(offset, offset+2))); + offset += 3; + if (lastOffset < offset) + return cal; + //ms: + cal.set(Calendar.MILLISECOND, Integer.parseInt(str.substring(offset, offset+3))); + offset += 3;//last one, move to next char + if (lastOffset == offset) + return cal; + } catch (Exception e) { + ParseException pe = new ParseException("Improperly formatted date: "+str, offset); + pe.initCause(e); + throw pe; + } + throw new ParseException("Improperly formatted date: "+str, offset); + } + + private static DateRangeBound.Precision getCalendarPrecision(int calendarPrecision) + { + switch (calendarPrecision) + { + case Calendar.YEAR: + return DateRangeBound.Precision.YEAR; + case Calendar.MONTH: + return DateRangeBound.Precision.MONTH; + case Calendar.DAY_OF_MONTH: + return DateRangeBound.Precision.DAY; + case Calendar.HOUR_OF_DAY: + return DateRangeBound.Precision.HOUR; + case Calendar.MINUTE: + return DateRangeBound.Precision.MINUTE; + case Calendar.SECOND: + return DateRangeBound.Precision.SECOND; + case Calendar.MILLISECOND: + return DateRangeBound.Precision.MILLISECOND; + default: + throw new IllegalStateException("Unsupported date time precision: " + calendarPrecision); + } + } + + private static ZonedDateTime toZonedDateTime(Calendar calendar) + { + int year = calendar.get(Calendar.YEAR); + if (calendar.get(Calendar.ERA) == 0) + { + // BC era; 1 BC == 0 AD, 0 BD == -1 AD, etc + year -= 1; + if (year > 0) + { + year = -year; + } + } + LocalDateTime localDateTime = LocalDateTime.of(year, + calendar.get(Calendar.MONTH) + 1, + calendar.get(Calendar.DAY_OF_MONTH), + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + calendar.get(Calendar.SECOND)); + localDateTime = localDateTime.with(ChronoField.MILLI_OF_SECOND, calendar.get(Calendar.MILLISECOND)); + return ZonedDateTime.of(localDateTime, ZoneOffset.UTC); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/geometry/GeometricType.java b/src/java/org/apache/cassandra/db/marshal/geometry/GeometricType.java new file mode 100644 index 000000000000..2516d7d23bda --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/geometry/GeometricType.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal.geometry; + +public enum GeometricType +{ + POINT(Point.class, Point.serializer), + LINESTRING(LineString.class, LineString.serializer), + POLYGON(Polygon.class, Polygon.serializer); + + private final Class geoClass; + private final OgcGeometry.Serializer serializer; + + GeometricType(Class geoClass, OgcGeometry.Serializer serializer) + { + this.geoClass = geoClass; + this.serializer = serializer; + } + + public Class getGeoClass() + { + return geoClass; + } + + public OgcGeometry.Serializer getSerializer() + { + return serializer; + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/geometry/LineString.java b/src/java/org/apache/cassandra/db/marshal/geometry/LineString.java new file mode 100644 index 000000000000..a31854fa9100 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/geometry/LineString.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal.geometry; + +import java.nio.ByteBuffer; + +import com.esri.core.geometry.GeoJsonExportFlags; +import com.esri.core.geometry.Operator; +import com.esri.core.geometry.OperatorExportToGeoJson; +import com.esri.core.geometry.OperatorFactoryLocal; +import com.esri.core.geometry.ogc.OGCGeometry; +import com.esri.core.geometry.ogc.OGCLineString; +import org.apache.cassandra.serializers.MarshalException; + +public class LineString extends OgcGeometry +{ + public static final Serializer serializer = new Serializer() + { + @Override + public String toWellKnownText(LineString geometry) + { + return geometry.lineString.asText(); + } + + @Override + public ByteBuffer toWellKnownBinaryNativeOrder(LineString geometry) + { + return geometry.lineString.asBinary(); + } + + @Override + public String toGeoJson(LineString geometry) + { + OperatorExportToGeoJson op = (OperatorExportToGeoJson) OperatorFactoryLocal.getInstance().getOperator(Operator.Type.ExportToGeoJson); + return op.execute(GeoJsonExportFlags.geoJsonExportSkipCRS, geometry.lineString.esriSR, geometry.lineString.getEsriGeometry()); + } + + @Override + public LineString fromWellKnownText(String source) + { + return new LineString(fromOgcWellKnownText(source, OGCLineString.class)); + } + + @Override + public LineString fromWellKnownBinary(ByteBuffer source) + { + return new LineString(fromOgcWellKnownBinary(source, OGCLineString.class)); + } + + @Override + public LineString fromGeoJson(String source) + { + return new LineString(fromOgcGeoJson(source, OGCLineString.class)); + } + }; + + private final OGCLineString lineString; + + public LineString(OGCLineString lineString) + { + this.lineString = lineString; + validate(); + } + + @Override + public GeometricType getType() + { + return GeometricType.LINESTRING; + } + + @Override + public void validate() throws MarshalException + { + validateOgcGeometry(lineString); + } + + @Override + public Serializer getSerializer() + { + return serializer; + } + + @Override + protected OGCGeometry getOgcGeometry() + { + return lineString; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LineString that = (LineString) o; + + return !(lineString != null ? !lineString.equals(that.lineString) : that.lineString != null); + + } + + @Override + public int hashCode() + { + return lineString != null ? lineString.hashCode() : 0; + } + + @Override + public String toString() + { + return asWellKnownText(); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/geometry/OgcGeometry.java b/src/java/org/apache/cassandra/db/marshal/geometry/OgcGeometry.java new file mode 100644 index 000000000000..6f3be38e7415 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/geometry/OgcGeometry.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal.geometry; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.esri.core.geometry.GeometryException; +import com.esri.core.geometry.JsonGeometryException; +import com.esri.core.geometry.SpatialReference; +import com.esri.core.geometry.ogc.OGCGeometry; +import org.apache.cassandra.serializers.MarshalException; + +public abstract class OgcGeometry +{ + + // default spatial reference for wkt/wkb + public static final SpatialReference SPATIAL_REFERENCE_4326 = SpatialReference.create(4326); + + public interface Serializer + { + String toWellKnownText(T geometry); + + // We need to return a Big Endian ByteBuffer as that's required by org.apache.cassandra.db.NativeDecoratedKey + // when the memtable allocation type is "offheap_objects". See https://datastax.jira.com/browse/DSP-16302 + // Note that the order set here may not match the actual endianess. OGC serialization encodes actual endianess + // and discards BB order set here. + default ByteBuffer toWellKnownBinary(T geometry) + { + return toWellKnownBinaryNativeOrder(geometry).order(ByteOrder.BIG_ENDIAN); + } + + ByteBuffer toWellKnownBinaryNativeOrder(T geometry); + + String toGeoJson(T geometry); + + T fromWellKnownText(String source); + + T fromWellKnownBinary(ByteBuffer source); + + T fromGeoJson(String source); + } + + public abstract GeometricType getType(); + + public abstract void validate() throws MarshalException; + + public abstract Serializer getSerializer(); + + static void validateType(OGCGeometry geometry, Class klass) + { + if (!geometry.getClass().equals(klass)) + { + throw new MarshalException(String.format("%s is not of type %s", + geometry.getClass().getSimpleName(), + klass.getSimpleName())); + } + } + + static ByteBuffer getWkb(OGCGeometry geometry) + { + try + { + return geometry.asBinary(); + } + catch (GeometryException | IllegalArgumentException e) + { + throw new MarshalException("Invalid Geometry", e); + } + } + + static String getWkt(OGCGeometry geometry) + { + try + { + return geometry.asText(); + } + catch (GeometryException | IllegalArgumentException e) + { + throw new MarshalException("Invalid Geometry", e); + } + } + + static void validateNormalization(OGCGeometry geometry, ByteBuffer source) + { + ByteBuffer normalized = getWkb(geometry); + ByteBuffer inputCopy = source.slice(); + + // since the data we get is sometimes part of a longer string of bytes, we set the limit to the normalized + // buffer length. Normalization only ever adds and rearranges points though, so this should be ok + if (inputCopy.remaining() > normalized.remaining()) + { + inputCopy.limit(normalized.remaining()); + } + + if (!normalized.equals(inputCopy)) + { + String klass = geometry.getClass().getSimpleName(); + String msg = String.format("%s is not normalized. %s should be defined/serialized as: %s", klass, klass, getWkt(geometry)); + throw new MarshalException(msg); + } + } + + static T fromOgcWellKnownText(String source, Class klass) + { + OGCGeometry geometry; + try + { + geometry = OGCGeometry.fromText(source); + } + catch (IllegalArgumentException e) + { + throw new MarshalException(e.getMessage()); + } + validateType(geometry, klass); + return (T) geometry; + } + + static T fromOgcWellKnownBinary(ByteBuffer source, Class klass) + { + OGCGeometry geometry; + try + { + geometry = OGCGeometry.fromBinary(source); + } + catch (IllegalArgumentException e) + { + throw new MarshalException(e.getMessage()); + } + validateType(geometry, klass); + validateNormalization(geometry, source); + return (T) geometry; + } + + static T fromOgcGeoJson(String source, Class klass) + { + OGCGeometry geometry; + try + { + geometry = OGCGeometry.fromGeoJson(source); + } + catch (IllegalArgumentException | JsonGeometryException e) + { + throw new MarshalException(e.getMessage()); + } + validateType(geometry, klass); + return (T) geometry; + } + + public boolean contains(OgcGeometry geometry) + { + if (!(geometry instanceof OgcGeometry)) + { + throw new UnsupportedOperationException(String.format("%s is not compatible with %s.contains", + geometry.getClass().getSimpleName(), getClass().getSimpleName())); + } + + OGCGeometry thisGeometry = getOgcGeometry(); + OGCGeometry thatGeometry = ((OgcGeometry) geometry).getOgcGeometry(); + if (thisGeometry != null && thatGeometry != null) + { + return thisGeometry.contains(thatGeometry); + } + else + { + return false; + } + } + + protected abstract OGCGeometry getOgcGeometry(); + + static void validateOgcGeometry(OGCGeometry geometry) + { + try + { + if (geometry.is3D()) + { + throw new MarshalException(String.format("'%s' is not 2D", getWkt(geometry))); + } + + if (!geometry.isSimple()) + { + throw new MarshalException(String.format("'%s' is not simple. Points and edges cannot self-intersect.", getWkt(geometry))); + } + } + catch (GeometryException e) + { + throw new MarshalException("Invalid geometry", e); + } + } + + public String asWellKnownText() + { + return getSerializer().toWellKnownText(this); + } + + public ByteBuffer asWellKnownBinary() + { + return getSerializer().toWellKnownBinary(this); + } + + public String asGeoJson() + { + return getSerializer().toGeoJson(this); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/geometry/Point.java b/src/java/org/apache/cassandra/db/marshal/geometry/Point.java new file mode 100644 index 000000000000..0992bd768725 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/geometry/Point.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal.geometry; + +import java.nio.ByteBuffer; + +import com.esri.core.geometry.GeoJsonExportFlags; +import com.esri.core.geometry.Operator; +import com.esri.core.geometry.OperatorExportToGeoJson; +import com.esri.core.geometry.OperatorFactoryLocal; +import com.esri.core.geometry.ogc.OGCGeometry; +import com.esri.core.geometry.ogc.OGCPoint; +import org.apache.cassandra.serializers.MarshalException; + +public class Point extends OgcGeometry +{ + public static final Serializer serializer = new Serializer() + { + @Override + public String toWellKnownText(Point geometry) + { + return geometry.point.asText(); + } + + @Override + public ByteBuffer toWellKnownBinaryNativeOrder(Point geometry) + { + return geometry.point.asBinary(); + } + + @Override + public String toGeoJson(Point geometry) + { + OperatorExportToGeoJson op = (OperatorExportToGeoJson) OperatorFactoryLocal.getInstance().getOperator(Operator.Type.ExportToGeoJson); + return op.execute(GeoJsonExportFlags.geoJsonExportSkipCRS, geometry.point.esriSR, geometry.point.getEsriGeometry()); + } + + @Override + public Point fromWellKnownText(String source) + { + return new Point(fromOgcWellKnownText(source, OGCPoint.class)); + } + + @Override + public Point fromWellKnownBinary(ByteBuffer source) + { + return new Point(fromOgcWellKnownBinary(source, OGCPoint.class)); + } + + @Override + public Point fromGeoJson(String source) + { + return new Point(fromOgcGeoJson(source, OGCPoint.class)); + } + }; + + final OGCPoint point; + + public Point(double x, double y) + { + this(new OGCPoint(new com.esri.core.geometry.Point(x, y), OgcGeometry.SPATIAL_REFERENCE_4326)); + } + + private Point(OGCPoint point) + { + this.point = point; + validate(); + } + + @Override + public boolean contains(OgcGeometry geometry) + { + return false; + } + + @Override + public GeometricType getType() + { + return GeometricType.POINT; + } + + @Override + public void validate() throws MarshalException + { + validateOgcGeometry(point); + if (point.isEmpty() || point.is3D()) + throw new MarshalException(getClass().getSimpleName() + " requires exactly 2 coordinate values"); + } + + @Override + protected OGCGeometry getOgcGeometry() + { + return point; + } + + @Override + public Serializer getSerializer() + { + return serializer; + } + + public OGCPoint getOgcPoint() + { + return point; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Point point1 = (Point) o; + + return !(point != null ? !point.equals(point1.point) : point1.point != null); + + } + + @Override + public int hashCode() + { + return point != null ? point.hashCode() : 0; + } + + @Override + public String toString() + { + return asWellKnownText(); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/geometry/Polygon.java b/src/java/org/apache/cassandra/db/marshal/geometry/Polygon.java new file mode 100644 index 000000000000..d51181566d04 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/geometry/Polygon.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.marshal.geometry; + +import java.nio.ByteBuffer; + +import com.esri.core.geometry.GeoJsonExportFlags; +import com.esri.core.geometry.Operator; +import com.esri.core.geometry.OperatorExportToGeoJson; +import com.esri.core.geometry.OperatorFactoryLocal; +import com.esri.core.geometry.ogc.OGCGeometry; +import com.esri.core.geometry.ogc.OGCPolygon; +import org.apache.cassandra.serializers.MarshalException; + +public class Polygon extends OgcGeometry +{ + public static final Serializer serializer = new Serializer() + { + @Override + public String toWellKnownText(Polygon geometry) + { + return geometry.polygon.asText(); + } + + @Override + public ByteBuffer toWellKnownBinaryNativeOrder(Polygon geometry) + { + return geometry.polygon.asBinary(); + } + + @Override + public String toGeoJson(Polygon geometry) + { + OperatorExportToGeoJson op = (OperatorExportToGeoJson) OperatorFactoryLocal.getInstance().getOperator(Operator.Type.ExportToGeoJson); + return op.execute(GeoJsonExportFlags.geoJsonExportSkipCRS, geometry.polygon.esriSR, geometry.polygon.getEsriGeometry()); + } + + @Override + public Polygon fromWellKnownText(String source) + { + return new Polygon(fromOgcWellKnownText(source, OGCPolygon.class)); + } + + @Override + public Polygon fromWellKnownBinary(ByteBuffer source) + { + return new Polygon(fromOgcWellKnownBinary(source, OGCPolygon.class)); + } + + @Override + public Polygon fromGeoJson(String source) + { + return new Polygon(fromOgcGeoJson(source, OGCPolygon.class)); + } + }; + + OGCPolygon polygon; + + public Polygon(OGCPolygon polygon) + { + this.polygon = polygon; + validate(); + } + + @Override + protected OGCGeometry getOgcGeometry() + { + return polygon; + } + + @Override + public GeometricType getType() + { + return GeometricType.POLYGON; + } + + @Override + public void validate() throws MarshalException + { + validateOgcGeometry(polygon); + } + + @Override + public Serializer getSerializer() + { + return serializer; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Polygon polygon1 = (Polygon) o; + + return !(polygon != null ? !polygon.equals(polygon1.polygon) : polygon1.polygon != null); + + } + + @Override + public int hashCode() + { + return polygon != null ? polygon.hashCode() : 0; + } + + @Override + public String toString() + { + return asWellKnownText(); + } +} diff --git a/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java b/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java index 8526dace3925..71ee65caa0f0 100644 --- a/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java @@ -31,7 +31,13 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.index.transactions.UpdateTransaction; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; @@ -48,6 +54,8 @@ import org.apache.cassandra.utils.memory.SlabPool; import org.github.jamm.Unmetered; +import static org.apache.cassandra.io.sstable.SSTableReadsListener.NOOP_LISTENER; + /** * A memtable that uses memory tracked and maybe allocated via a MemtableAllocator from a MemtablePool. * Provides methods of memory tracking and triggering flushes when the relevant limits are reached. @@ -74,6 +82,14 @@ public abstract class AbstractAllocatorMemtable extends AbstractMemtableWithComm private final long creationNano = Clock.Global.nanoTime(); + /** + * Keeps an estimate of the average row size in this memtable, computed from a small sample of rows. + * Because computing this estimate is potentially costly, as it requires iterating the rows, + * the estimate is updated only whenever the number of operations on the memtable increases significantly from the + * last update. This estimate is not very accurate but should be ok for planning or diagnostic purposes. + */ + private volatile MemtableAverageRowSize estimatedAverageRowSize; + @VisibleForTesting static MemtablePool createMemtableAllocatorPool() { @@ -111,7 +127,6 @@ public static MemtablePool createMemtableAllocatorPoolInternal(Config.MemtableAl } } - // only to be used by init(), to setup the very first memtable for the cfs public AbstractAllocatorMemtable(AtomicReference commitLogLowerBound, TableMetadataRef metadataRef, Owner owner) { super(metadataRef, commitLogLowerBound); @@ -127,6 +142,58 @@ public MemtableAllocator getAllocator() return allocator; } + public long rowCount(final ColumnFilter columnFilter, final DataRange dataRange) + { + int total = 0; + for (var iter = partitionIterator(columnFilter, dataRange, NOOP_LISTENER); iter.hasNext(); ) + { + for (UnfilteredRowIterator it = iter.next(); it.hasNext(); ) + { + Unfiltered uRow = it.next(); + if (uRow.isRow()) + total++; + } + } + + return total; + } + + @Override + public long getEstimatedAverageRowSize() + { + if (estimatedAverageRowSize == null || currentOperations.get() > estimatedAverageRowSize.operations * 1.5) + estimatedAverageRowSize = new MemtableAverageRowSize(this); + return estimatedAverageRowSize.rowSize; + } + + /** + * CASSANDRA-21019: the memory limit is enforced once here, before a mutation starts + * and before any memtable-internal locks are taken; once started, a mutation runs to + * completion and individual allocations only track usage. Implemented here so every + * allocator-backed memtable (TrieMemtable, TrieMemtableStage1, SkipListMemtable, and + * future implementations) gets the gate; subclasses implement performPut(). + */ + @Override + public final long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + { + allocator.awaitRoomToStart(opGroup); + return performPut(update, indexer, opGroup); + } + + /** + * CASSANDRA-21019: nested writes skip the room gate, as the enclosing mutation was + * gated when it started. (waiting for room here would run under the base table's + * memtable-internal locks where Barrier.markBlocking() cannot release a queued + * pre-barrier writer) + */ + @Override + public final long putNested(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + { + return performPut(update, indexer, opGroup); + } + + protected abstract long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup); + @Override public boolean shouldSwitch(ColumnFamilyStore.FlushReason reason) { @@ -142,6 +209,12 @@ public boolean shouldSwitch(ColumnFamilyStore.FlushReason reason) } } + @Override + public OpOrder readOrdering() + { + return owner.readOrdering(); + } + public void metadataUpdated() { // We decided not to swap out this memtable, but if the flush period has changed we must schedule it for the @@ -182,7 +255,16 @@ public String toString() usage); } - @Override + /** + * For testing only. Give this memtable too big a size to make it always fail flushing. + */ + @VisibleForTesting + public void makeUnflushable() + { + liveDataSize.addAndGet(1024L * 1024 * 1024 * 1024 * 1024); + } + +@Override public void addMemoryUsageTo(MemoryUsage stats) { stats.ownershipRatioOnHeap += getAllocator().onHeap().ownershipRatio(); @@ -193,17 +275,24 @@ public void addMemoryUsageTo(MemoryUsage stats) public void markExtraOnHeapUsed(long additionalSpace, OpOrder.Group opGroup) { - getAllocator().onHeap().allocate(additionalSpace, opGroup); + getAllocator().onHeap().adjust(additionalSpace, opGroup); } public void markExtraOffHeapUsed(long additionalSpace, OpOrder.Group opGroup) { - getAllocator().offHeap().allocate(additionalSpace, opGroup); + getAllocator().offHeap().adjust(additionalSpace, opGroup); } + @Override + public long unusedReservedOnHeapMemory() + { + return allocator.unusedReservedOnHeapMemory(); + } + + void scheduleFlush() { - int period = metadata().params.memtableFlushPeriodInMs; + int period = owner.getMemtableFlushPeriodInMs(); if (period > 0) scheduleFlush(owner, period); } @@ -225,7 +314,7 @@ protected void runMayThrow() private void flushIfPeriodExpired() { - int period = metadata().params.memtableFlushPeriodInMs; + int period = owner.getMemtableFlushPeriodInMs(); if (period > 0 && (Clock.Global.nanoTime() - creationNano >= TimeUnit.MILLISECONDS.toNanos(period))) { if (isClean()) diff --git a/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java b/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java index 2f2c2a25516c..f01fc49a0cd8 100644 --- a/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java @@ -41,6 +41,7 @@ public abstract class AbstractMemtable implements Memtable { private final AtomicReference flushTransaction = new AtomicReference<>(null); + protected final AtomicLong liveDataSize = new AtomicLong(0); protected final AtomicLong currentOperations = new AtomicLong(0); protected final ColumnsCollector columnsCollector; protected final StatsCollector statsCollector = new StatsCollector(); @@ -74,6 +75,11 @@ public TableMetadata metadata() return metadata.get(); } + @Override + public long getLiveDataSize() + { + return liveDataSize.get(); + } @Override public long operationCount() { @@ -81,6 +87,13 @@ public long operationCount() } @Override + /** + * Returns the minTS if one available, otherwise NO_MIN_TIMESTAMP. + * + * EncodingStats uses a synthetic epoch TS at 2015. We don't want to leak that (CASSANDRA-18118) so we return NO_MIN_TIMESTAMP instead. + * + * @return The minTS or NO_MIN_TIMESTAMP if none available + */ public long getMinTimestamp() { return minTimestamp.get() != EncodingStats.NO_STATS.minTimestamp ? minTimestamp.get() : NO_MIN_TIMESTAMP; diff --git a/src/java/org/apache/cassandra/db/memtable/AbstractShardedMemtable.java b/src/java/org/apache/cassandra/db/memtable/AbstractShardedMemtable.java index 081570188b3f..e4a33528daa7 100644 --- a/src/java/org/apache/cassandra/db/memtable/AbstractShardedMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/AbstractShardedMemtable.java @@ -43,7 +43,12 @@ public abstract class AbstractShardedMemtable extends AbstractAllocatorMemtable } // default shard count, used when a specific number of shards is not specified in the options - private static volatile int defaultShardCount = MEMTABLE_SHARD_COUNT.getInt(FBUtilities.getAvailableProcessors()); + private static volatile int defaultShardCount = MEMTABLE_SHARD_COUNT.getInt(autoShardCount()); + + private static int autoShardCount() + { + return 4 * FBUtilities.getAvailableProcessors(); + } // The boundaries for the keyspace as they were calculated when the memtable is created. // The boundaries will be NONE for system keyspaces or if StorageService is not yet initialized. @@ -69,7 +74,7 @@ public void setDefaultShardCount(String shardCount) { if ("auto".equalsIgnoreCase(shardCount)) { - defaultShardCount = FBUtilities.getAvailableProcessors(); + defaultShardCount = autoShardCount(); } else { diff --git a/src/java/org/apache/cassandra/db/memtable/Flushing.java b/src/java/org/apache/cassandra/db/memtable/Flushing.java index 3fc856858294..278543c37dac 100644 --- a/src/java/org/apache/cassandra/db/memtable/Flushing.java +++ b/src/java/org/apache/cassandra/db/memtable/Flushing.java @@ -22,7 +22,9 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import org.slf4j.Logger; @@ -39,13 +41,18 @@ import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.FSDiskFullWriteError; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableMultiWriter; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; +import static org.apache.cassandra.utils.Throwables.maybeFail; + public class Flushing { private static final Logger logger = LoggerFactory.getLogger(Flushing.class); @@ -65,8 +72,18 @@ public static List flushRunnables(ColumnFamilyStore cfs, cfs.name); DiskBoundaries diskBoundaries = cfs.getDiskBoundaries(); - List boundaries = diskBoundaries.positions; + List boundaries = diskBoundaries.getPositions(); List locations = diskBoundaries.directories; + return flushRunnables(cfs, memtable, boundaries, locations, txn); + } + + @VisibleForTesting + static List flushRunnables(ColumnFamilyStore cfs, + Memtable memtable, + List boundaries, + List locations, + LifecycleTransaction txn) + { if (boundaries == null) { FlushRunnable runnable = flushRunnable(cfs, memtable, null, null, txn, null); @@ -79,7 +96,7 @@ public static List flushRunnables(ColumnFamilyStore cfs, { for (int i = 0; i < boundaries.size(); i++) { - PartitionPosition t = boundaries.get(i); + PartitionPosition t = boundaries.get(i).maxKeyBound(); FlushRunnable runnable = flushRunnable(cfs, memtable, rangeStart, t, txn, locations.get(i)); runnables.add(runnable); @@ -89,9 +106,7 @@ public static List flushRunnables(ColumnFamilyStore cfs, } catch (Throwable e) { - Throwable t = abortRunnables(runnables, e); - Throwables.throwIfUnchecked(t); - throw new RuntimeException(t); + throw Throwables.propagate(abortRunnables(runnables, e)); } } @@ -106,9 +121,19 @@ static FlushRunnable flushRunnable(ColumnFamilyStore cfs, SSTableFormat format = DatabaseDescriptor.getSelectedSSTableFormat(); long estimatedSize = format.getWriterFactory().estimateSize(flushSet); - Descriptor descriptor = flushLocation == null - ? cfs.newSSTableDescriptor(cfs.getDirectories().getWriteableLocationAsFile(estimatedSize), format) - : cfs.newSSTableDescriptor(cfs.getDirectories().getLocationForDisk(flushLocation), format); + Descriptor descriptor; + if (flushLocation == null) + { + descriptor = cfs.newSSTableDescriptor(cfs.getDirectories().getWriteableLocationAsFile(estimatedSize), format); + } + else + { + // exclude directory if its total writeSize does not fit to data directory + if (flushLocation.getAvailableSpace() < estimatedSize) + throw new FSDiskFullWriteError(cfs.metadata.keyspace, estimatedSize); + + descriptor = cfs.newSSTableDescriptor(cfs.getDirectories().getLocationForDisk(flushLocation), format); + } SSTableMultiWriter writer = createFlushWriter(cfs, flushSet, @@ -123,10 +148,26 @@ public static Throwable abortRunnables(List runnables, Throwable { if (runnables != null) for (FlushRunnable runnable : runnables) - t = runnable.writer.abort(t); + t = runnable.abort(t); return t; } + /** + * The valid states for {@link FlushRunnable} writers. The thread writing the contents + * will transition from IDLE -> RUNNING and back to IDLE when finished using the writer + * or from ABORTING -> ABORTED if another thread has transitioned from RUNNING -> ABORTING. + * We can also transition directly from IDLE -> ABORTED. Whichever threads transitions + * to ABORTED is responsible to abort the writer. + */ + @VisibleForTesting + enum FlushRunnableWriterState + { + IDLE, // the runnable is idle, either not yet started or completed but with the writer waiting to be committed + RUNNING, // the runnable is executing, therefore the writer cannot be aborted or else a SEGV may ensue + ABORTING, // an abort request has been issued, this only happens if abort() is called whilst RUNNING + ABORTED // the writer has been aborted, no resources will be leaked + } + public static class FlushRunnable implements Callable { private final Memtable.FlushablePartitionSet toFlush; @@ -135,6 +176,7 @@ public static class FlushRunnable implements Callable private final TableMetrics metrics; private final boolean isBatchLogTable; private final boolean logCompletion; + private final AtomicReference state; public FlushRunnable(Memtable.FlushablePartitionSet flushSet, SSTableMultiWriter writer, @@ -146,42 +188,77 @@ public FlushRunnable(Memtable.FlushablePartitionSet flushSet, this.metrics = metrics; this.isBatchLogTable = toFlush.metadata() == SystemKeyspace.Batches; this.logCompletion = logCompletion; + this.state = new AtomicReference<>(FlushRunnableWriterState.IDLE); } private void writeSortedContents() { - logger.info("Writing {}, flushed range = [{}, {})", toFlush.memtable(), toFlush.from(), toFlush.to()); + if (!state.compareAndSet(FlushRunnableWriterState.IDLE, FlushRunnableWriterState.RUNNING)) + { + logger.debug("Failed to write {}, flushed range = ({}, {}], state: {}", + toFlush.memtable().toString(), toFlush.from(), toFlush.to(), state); + return; + } + + long before = Clock.Global.nanoTime(); + logger.debug("Writing {}, flushed range = ({}, {}], state: {}", + toFlush.memtable().toString(), toFlush.from(), toFlush.to(), state); - // (we can't clear out the map as-we-go to free up memory, - // since the memtable is being used for queries in the "pending flush" category) - for (Partition partition : toFlush) + try { - // Each batchlog partition is a separate entry in the log. And for an entry, we only do 2 - // operations: 1) we insert the entry and 2) we delete it. Further, BL data is strictly local, - // we don't need to preserve tombstones for repair. So if both operation are in this - // memtable (which will almost always be the case if there is no ongoing failure), we can - // just skip the entry (CASSANDRA-4667). - if (isBatchLogTable && !partition.partitionLevelDeletion().isLive() && partition.hasRows()) - continue; - - if (!partition.isEmpty()) + // (we can't clear out the map as-we-go to free up memory, + // since the memtable is being used for queries in the "pending flush" category) + for (Partition partition : toFlush) { - try (UnfilteredRowIterator iter = partition.unfilteredIterator()) + if (state.get() == FlushRunnableWriterState.ABORTING) + break; + + // Each batchlog partition is a separate entry in the log. And for an entry, we only do 2 + // operations: 1) we insert the entry and 2) we delete it. Further, BL data is strictly local, + // we don't need to preserve tombstones for repair. So if both operation are in this + // memtable (which will almost always be the case if there is no ongoing failure), we can + // just skip the entry (CASSANDRA-4667). + if (isBatchLogTable && !partition.partitionLevelDeletion().isLive() && partition.hasRows()) + continue; + + if (!partition.isEmpty()) { - writer.append(iter); + try (UnfilteredRowIterator iter = partition.unfilteredIterator()) + { + writer.append(iter); + } } } } - - if (logCompletion) + finally { - long bytesFlushed = writer.getBytesWritten(); - logger.info("Completed flushing {} ({}) for commitlog position {}", - writer.getFilename(), - FBUtilities.prettyPrintMemory(bytesFlushed), - toFlush.memtable().getFinalCommitLogUpperBound()); - // Update the metrics - metrics.bytesFlushed.inc(bytesFlushed); + while (true) + { + if (state.compareAndSet(FlushRunnableWriterState.RUNNING, FlushRunnableWriterState.IDLE)) + { + if (logCompletion) + { + long bytesFlushed = writer.getBytesWritten(); + long segmentCount = writer.getSegmentCount(); + logger.debug("Completed flushing {} ({}/{} files) for commitlog position {}", + writer.getFilename(), + FBUtilities.prettyPrintMemory(bytesFlushed), + segmentCount, + toFlush.memtable().getFinalCommitLogUpperBound()); + // Update the metrics + metrics.incBytesFlushed(toFlush.memtable().getLiveDataSize(), bytesFlushed, Clock.Global.nanoTime() - before); + metrics.flushSegmentCount.update(segmentCount); + } + + break; + } + else if (state.compareAndSet(FlushRunnableWriterState.ABORTING, FlushRunnableWriterState.ABORTED)) + { + logger.debug("Flushing of {} aborted", writer.getFilename()); + maybeFail(writer.abort(null)); + break; + } + } } } @@ -198,6 +275,29 @@ public String toString() { return "Flush " + toFlush.metadata().keyspace + '.' + toFlush.metadata().name; } + + public Throwable abort(Throwable throwable) + { + while (true) + { + if (state.compareAndSet(FlushRunnableWriterState.IDLE, FlushRunnableWriterState.ABORTED)) + { + logger.debug("Flushing of {} aborted", writer.getFilename()); + return writer.abort(throwable); + } + else if (state.compareAndSet(FlushRunnableWriterState.RUNNING, FlushRunnableWriterState.ABORTING)) + { + // thread currently executing writeSortedContents() will take care of aborting and throw any exceptions + return throwable; + } + } + } + + @VisibleForTesting + FlushRunnableWriterState state() + { + return state.get(); + } } public static SSTableMultiWriter createFlushWriter(ColumnFamilyStore cfs, diff --git a/src/java/org/apache/cassandra/db/memtable/Memtable.java b/src/java/org/apache/cassandra/db/memtable/Memtable.java index d4f0dedb995e..e79dfec36acd 100644 --- a/src/java/org/apache/cassandra/db/memtable/Memtable.java +++ b/src/java/org/apache/cassandra/db/memtable/Memtable.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicReference; import javax.annotation.concurrent.NotThreadSafe; +import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.db.CellSourceIdentifier; import org.apache.cassandra.db.ColumnFamilyStore; @@ -28,6 +29,7 @@ import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.partitions.BTreePartitionUpdate; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.EncodingStats; @@ -39,6 +41,8 @@ import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.utils.concurrent.OpOrder; /** @@ -149,6 +153,14 @@ default TableMetrics.ReleasableMetric createMemtableMetrics(TableMetadataRef met { return null; } + + /** + * Override this method to provide a custom partition update factory for more efficient merging of updates. + */ + default PartitionUpdate.Factory partitionUpdateFactory() + { + return BTreePartitionUpdate.FACTORY; + } } /** @@ -178,6 +190,16 @@ interface Owner * {@link #localRangesUpdated()} call. */ ShardBoundaries localRangeSplits(int shardCount); + + /** + * Get the op-order primitive that protects data for the duration of reads. + */ + public OpOrder readOrdering(); + + /** + * Get the memtable flush period in millis based on schema config or system configs + */ + int getMemtableFlushPeriodInMs(); } // Main write and read operations @@ -196,6 +218,37 @@ interface Owner */ long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup); + /** + * Put variant for writes performed inside an already-started mutation on the same + * thread, e.g. legacy 2i applying to its index table's memtable from + * indexer.onInserted(), which runs under the base table's memtable-internal locks + * (CassandraIndex.insert -> CassandraTableWriteHandler.write, updateIndexes=false). + * + * Such writes must not wait for memtable pool room: parking there would hold the + * enclosing locks and deadlock the flush writeBarrier (CASSANDRA-21019). The memory + * limit was already enforced when the enclosing mutation started, we don't need to check again. + * + * Implementations without a room gate may keep this default. + */ + default long putNested(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + { + return put(update, indexer, opGroup); + } + + /** + * Get the partition for the specified key. Returns null if no such partition is present. + */ + Partition getPartition(DecoratedKey key); + + interface MemtableUnfilteredPartitionIterator extends UnfilteredPartitionIterator + { + /** + * Returns the minimum local deletion time for all partitions in the range. + * Required for the efficiency of partition range read commands. + */ + long getMinLocalDeletionTime(); + } + // Read operations are provided by the UnfilteredSource interface. // Statistics @@ -206,12 +259,24 @@ interface Owner /** Size of the data not accounting for any metadata / mapping overheads */ long getLiveDataSize(); + /** Average size of the data of each row */ + long getEstimatedAverageRowSize(); + /** * Number of "operations" (in the sense defined in {@link PartitionUpdate#operationCount()}) the memtable has * executed. */ long operationCount(); + /** Minimum timestamp of all stored data */ + long getMinTimestamp(); + + /** Min partition key inserted so far. */ + DecoratedKey minPartitionKey(); + + /** Max partition key inserted so far. */ + DecoratedKey maxPartitionKey(); + /** * The table's definition metadata. * @@ -220,6 +285,16 @@ interface Owner */ TableMetadata metadata(); + /** + * The {@link OpOrder} that guards reads from this memtable. This is used to ensure that the memtable does not corrupt any + * active reads because of other operations on it. Returns null if the memtable is not protected by an OpOrder + * (overridden by {@link AbstractAllocatorMemtable}). + */ + default OpOrder readOrdering() + { + return null; + } + // Memory usage tracking @@ -251,6 +326,28 @@ static MemoryUsage getMemoryUsage(Memtable memtable) return usage; } + /** + * Estimates the total number of rows stored in the memtable. + * It is optimized for speed, not for accuracy. + */ + static long estimateRowCount(Memtable memtable) + { + long rowSize = memtable.getEstimatedAverageRowSize(); + return rowSize > 0 ? memtable.getLiveDataSize() / rowSize : 0; + } + + /** + * Returns the amount of on-heap memory that has been allocated for this memtable but is not yet used. + * This is not counted in the memory usage to have a better flushing decision behaviour -- we do not want to flush + * immediately after allocating a new buffer but when we have actually used the space provided. + * The method is provided for testing the memory usage tracking of memtables. + */ + @VisibleForTesting + default long unusedReservedOnHeapMemory() + { + return 0; + } + @NotThreadSafe class MemoryUsage { @@ -274,6 +371,14 @@ public String toString() } } + /** + * Signal underlying memtable that flush is required for given reason + * + * @param flushReason reason to flush + * @param skipIfSignaled skip signaling if memtable is already requested to switch + */ + void signalFlushRequired(ColumnFamilyStore.FlushReason flushReason, boolean skipIfSignaled); + /** * Adjust the used on-heap space by the given size (e.g. to reflect memory used by a non-table-based index). * This operation may block until enough memory is available in the memory pool. @@ -331,6 +436,7 @@ default TableMetadata metadata() return memtable().metadata(); } + long partitionCount(); default boolean isEmpty() { return partitionCount() > 0; diff --git a/src/java/org/apache/cassandra/db/memtable/MemtableAverageRowSize.java b/src/java/org/apache/cassandra/db/memtable/MemtableAverageRowSize.java new file mode 100644 index 000000000000..24afc3df6b03 --- /dev/null +++ b/src/java/org/apache/cassandra/db/memtable/MemtableAverageRowSize.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.memtable; + +import org.apache.cassandra.db.DataRange; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.SSTableReadsListener; + +class MemtableAverageRowSize +{ + private final static long MAX_ROWS = 100; + + public final long rowSize; + public final long operations; + + + public MemtableAverageRowSize(Memtable memtable) + { + DataRange range = DataRange.allData(memtable.metadata().partitioner); + ColumnFilter columnFilter = ColumnFilter.allRegularColumnsBuilder(memtable.metadata(), true).build(); + + long rowCount = 0; + long totalSize = 0; + + try (var partitionsIter = memtable.partitionIterator(columnFilter, range, SSTableReadsListener.NOOP_LISTENER)) + { + while (partitionsIter.hasNext() && rowCount < MAX_ROWS) + { + UnfilteredRowIterator rowsIter = partitionsIter.next(); + while (rowsIter.hasNext() && rowCount < MAX_ROWS) + { + Unfiltered uRow = rowsIter.next(); + if (uRow.isRow()) + { + rowCount++; + totalSize += ((Row) uRow).dataSize(); + } + } + } + } + this.operations = memtable.operationCount(); + this.rowSize = (rowCount > 0) + ? totalSize / rowCount + : 0; + } +} diff --git a/src/java/org/apache/cassandra/db/memtable/PersistentMemoryMemtable.java b/src/java/org/apache/cassandra/db/memtable/PersistentMemoryMemtable.java new file mode 100644 index 000000000000..f8e4b0501243 --- /dev/null +++ b/src/java/org/apache/cassandra/db/memtable/PersistentMemoryMemtable.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.memtable; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DataRange; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.partitions.Partition; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.index.transactions.UpdateTransaction; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.concurrent.OpOrder; + +/** + * Skeleton for persistent memory memtable. + */ +public class PersistentMemoryMemtable +//extends AbstractMemtable +extends SkipListMemtable // to test framework +{ + public PersistentMemoryMemtable(TableMetadataRef metadaRef, Owner owner) + { + super(null, metadaRef, owner); + // We should possibly link the persistent data of this memtable + } + + @Override + protected long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + { + // TODO: implement + return super.performPut(update, indexer, opGroup); + } + + public MemtableUnfilteredPartitionIterator partitionIterator(ColumnFilter columnFilter, DataRange dataRange) + { + // TODO: implement + return super.partitionIterator(columnFilter, dataRange, SSTableReadsListener.NOOP_LISTENER); + } + + public Partition getPartition(DecoratedKey key) + { + // TODO: implement + return super.getPartition(key); + } + + public long partitionCount() + { + // TODO: implement + return super.partitionCount(); + } + + public FlushablePartitionSet getFlushSet(PartitionPosition from, PartitionPosition to) + { + // TODO: implement + // FIXME: If the memtable can still be written to, this uses a view of the metadata that may not be up-to-date + // with the content. This may cause streaming to fail e.g. if a new column appears and is added to some row in + // the memtable between the time that this is constructed and the relevant row is written. Such failures should + // be recoverable by redoing the stream. + // If an implementation can produce a view/snapshot of the data at a point before the features were collected, + // this problem will not occur. + return super.getFlushSet(from, to); + } + + public boolean shouldSwitch(ColumnFamilyStore.FlushReason reason) + { + // We want to avoid all flushing. + switch (reason) + { + case STARTUP: // Called after reading and replaying the commit log. + case DRAIN: // Called to flush data before shutdown. + case INTERNALLY_FORCED: // Called to ensure ordering and persistence of system table events. + case MEMTABLE_PERIOD_EXPIRED: // The specified memtable expiration time elapsed. + case INDEX_MEMTABLE_PERIOD_EXPIRED: // The specified memtable expiration time elapsed. + case INDEX_TABLE_FLUSH: // Flush requested on index table because main table is flushing. + case STREAMS_RECEIVED: // Flush to save streamed data that was written to memtable. + return false; // do not do anything + + case INDEX_BUILD_COMPLETED: + case INDEX_REMOVED: + // Both of these are needed as safepoints for index management. Nothing to do. + return false; + + case VIEW_BUILD_STARTED: + case INDEX_BUILD_STARTED: + // TODO: Figure out secondary indexes and views. + return false; + + case SCHEMA_CHANGE: + if (!(metadata().params.memtable.factory() instanceof Factory)) + return true; // User has switched to a different memtable class. Flush and release all held data. + // Otherwise, assuming we can handle the change, don't switch. + // TODO: Handle + return false; + + case STREAMING: // Called to flush data so it can be streamed. TODO: How dow we stream? + case VALIDATION: // Called to flush data for repair. TODO: How do we repair? + // ColumnFamilyStore will create sstables of the affected ranges which will not be consulted on reads and + // will be deleted after streaming. + return false; + + case SNAPSHOT: + // We don't flush for this. Returning false will trigger a performSnapshot call. + return false; + + case DROP: // Called when a table is dropped. This memtable is no longer necessary. + case TRUNCATE: // The data is being deleted, but the table remains. + // Returning true asks the ColumnFamilyStore to replace this memtable object without flushing. + // This will call discard() below to delete all held data. + return true; + + case MEMTABLE_LIMIT: // The memtable size limit is reached, and this table was selected for flushing. + // Also passed if we call owner.signalLimitReached() + case TRIE_LIMIT: // Trie size limt is reached + case INDEX_MEMTABLE_LIMIT: // Index memtable size limit is reached + case COMMITLOG_DIRTY: // Commitlog thinks it needs to keep data from this table. + // Neither of the above should happen as we specify writesAreDurable and don't use an allocator/cleaner. + throw new AssertionError(); + + case USER_FORCED: + case UNIT_TESTS: + return false; + default: + throw new AssertionError(); + } + } + + public void metadataUpdated() + { + // TODO: handle + } + + public void performSnapshot(String snapshotName) + { + // TODO: implement. Figure out how to restore snapshot (with external tools). + } + + public void switchOut(OpOrder.Barrier writeBarrier, AtomicReference commitLogUpperBound) + { + super.switchOut(writeBarrier, commitLogUpperBound); + // This can prepare the memtable data for deletion; it will still be used while the flush is proceeding. + // A discard call will follow. + } + + public void discard() + { + // This will be called to release/delete all held data because the memtable is switched, due to having + // its data flushed, due to a truncate/drop, or due to a schema change to a different memtable class. + + // TODO: Implement. This should delete all memtable data from pmem. + super.discard(); + } + + public CommitLogPosition getApproximateCommitLogLowerBound() + { + // We don't maintain commit log positions + return CommitLogPosition.NONE; + } + + public CommitLogPosition getCommitLogLowerBound() + { + // We don't maintain commit log positions + return CommitLogPosition.NONE; + } + + public LastCommitLogPosition getFinalCommitLogUpperBound() + { + // We don't maintain commit log positions + return new LastCommitLogPosition(CommitLogPosition.NONE); + } + + public boolean isClean() + { + return partitionCount() == 0; + } + + public boolean mayContainDataBefore(CommitLogPosition position) + { + // We don't track commit log positions, so if we are dirty, we may. + return !isClean(); + } + + public void addMemoryUsageTo(MemoryUsage stats) + { + // our memory usage is not counted + } + + public void markExtraOnHeapUsed(long additionalSpace, OpOrder.Group opGroup) + { + // we don't track this + } + + public void markExtraOffHeapUsed(long additionalSpace, OpOrder.Group opGroup) + { + // we don't track this + } + + public static Factory factory(Map furtherOptions) + { + Boolean skipOption = Boolean.parseBoolean(furtherOptions.remove("skipCommitLog")); + return skipOption ? commitLogSkippingFactory : commitLogWritingFactory; + } + + private static final Factory commitLogSkippingFactory = new Factory(true); + private static final Factory commitLogWritingFactory = new Factory(false); + + static class Factory implements Memtable.Factory + { + private final boolean skipCommitLog; + + public Factory(boolean skipCommitLog) + { + this.skipCommitLog = skipCommitLog; + } + + public Memtable create(AtomicReference commitLogLowerBound, + TableMetadataRef metadaRef, + Owner owner) + { + return new PersistentMemoryMemtable(metadaRef, owner); + } + + public boolean writesShouldSkipCommitLog() + { + return skipCommitLog; + } + + public boolean writesAreDurable() + { + return true; + } + + public boolean streamToMemtable() + { + return true; + } + + public boolean streamFromMemtable() + { + return true; + } + } + +} diff --git a/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java b/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java index 864899f6a40b..4dc20bf7869a 100644 --- a/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java +++ b/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java @@ -22,8 +22,10 @@ import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; /** @@ -31,8 +33,8 @@ * In practice, each keyspace has its associated boundaries, see {@link Keyspace}. *

    * Technically, if we use {@code n} shards, this is a list of {@code n-1} tokens and each token {@code tk} gets assigned - * to the shard ID corresponding to the slot of the smallest token in the list that is greater to {@code tk}, or {@code n} - * if {@code tk} is bigger than any token in the list. + * to the shard ID corresponding to the slot of the smallest token in the list that is equal or greater than {@code tk}, + * or {@code n} if {@code tk} is bigger than any token in the list. */ public class ShardBoundaries { @@ -67,7 +69,7 @@ public int getShardForToken(Token tk) { for (int i = 0; i < boundaries.length; i++) { - if (tk.compareTo(boundaries[i]) < 0) + if (tk.compareTo(boundaries[i]) <= 0) // boundaries are end-inclusive return i; } return boundaries.length; @@ -86,6 +88,33 @@ public int getShardForKey(PartitionPosition key) return getShardForToken(key.getToken()); } + public AbstractBounds getBounds(int shard) + { + checkShardIndex(shard); + return AbstractBounds.bounds(getMinBound(shard), false, getMaxBound(shard), true); + } + + private void checkShardIndex(int shard) + { + if (shard < 0 || shard > boundaries.length) + throw new IllegalArgumentException(String.format("Shard %d out of bounds [0, %d]", shard, boundaries.length)); + } + + + private PartitionPosition getMinBound(int shard) + { + return (shard == 0) + ? DatabaseDescriptor.getPartitioner().getMinimumToken().maxKeyBound() + : boundaries[shard - 1].maxKeyBound(); + } + + private PartitionPosition getMaxBound(int shard) + { + return (shard == boundaries.length) + ? DatabaseDescriptor.getPartitioner().getMaximumToken().maxKeyBound() + : boundaries[shard].maxKeyBound(); + } + /** * The number of shards that this boundaries support, that is how many different shard ids {@link #getShardForToken} might * possibly return. diff --git a/src/java/org/apache/cassandra/db/memtable/ShardedSkipListMemtable.java b/src/java/org/apache/cassandra/db/memtable/ShardedSkipListMemtable.java index 92cdbbad9fe0..1f0bffd545b4 100644 --- a/src/java/org/apache/cassandra/db/memtable/ShardedSkipListMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/ShardedSkipListMemtable.java @@ -27,9 +27,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Iterators; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; @@ -40,11 +42,13 @@ import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator; import org.apache.cassandra.db.partitions.AtomicBTreePartition; +import org.apache.cassandra.db.partitions.BTreePartitionUpdate; import org.apache.cassandra.db.partitions.BTreePartitionUpdater; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Bounds; @@ -52,6 +56,7 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.index.transactions.UpdateTransaction; import org.apache.cassandra.io.sstable.SSTableReadsListener; +import static org.apache.cassandra.io.sstable.SSTableReadsListener.NOOP_LISTENER; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.concurrent.OpOrder; @@ -118,7 +123,8 @@ public boolean isClean() * * commitLogSegmentPosition should only be null if this is a secondary index, in which case it is *expected* to be null */ - public long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + @Override + public long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) { DecoratedKey key = update.partitionKey(); MemtableShard shard = shards[boundaries.getShardForKey(key)]; @@ -156,6 +162,21 @@ public long partitionCount() return total; } + public long rowCount(final ColumnFilter columnFilter, final DataRange dataRange) + { + int total = 0; + for (var iter = partitionIterator(columnFilter, dataRange, NOOP_LISTENER); iter.hasNext(); ) + { + for (UnfilteredRowIterator it = iter.next(); it.hasNext(); ) + { + Unfiltered uRow = it.next(); + if (uRow.isRow()) + total++; + } + } + return total; + } + /** * Returns the minTS if one available, otherwise NO_MIN_TIMESTAMP. * @@ -181,6 +202,36 @@ public long getMinLocalDeletionTime() return min; } + @Override + public void signalFlushRequired(ColumnFamilyStore.FlushReason flushReason, boolean skipIfSignaled) + { + owner.signalFlushRequired(this, flushReason); + } + + @Override + public DecoratedKey minPartitionKey() + { + for (int i = 0; i < shards.length; i++) + { + MemtableShard shard = shards[i]; + if (!shard.isClean()) + return shard.minPartitionKey(); + } + return null; + } + + @Override + public DecoratedKey maxPartitionKey() + { + for (int i = shards.length - 1; i >= 0; i--) + { + MemtableShard shard = shards[i]; + if (!shard.isClean()) + return shard.maxPartitionKey(); + } + return null; + } + @Override RegularAndStaticColumns columns() { @@ -237,7 +288,7 @@ private Iterator getPartitionIterator(PartitionPosition le return iterator; } - private Partition getPartition(DecoratedKey key) + public Partition getPartition(DecoratedKey key) { int shardIndex = boundaries.getShardForKey(key); return shards[shardIndex].partitions.get(key); @@ -369,7 +420,7 @@ public long put(DecoratedKey key, PartitionUpdate update, UpdateTransaction inde } } - BTreePartitionUpdater updater = previous.addAll(update, cloner, opGroup, indexer); + BTreePartitionUpdater updater = previous.addAll(BTreePartitionUpdate.asBTreeUpdate(update), cloner, opGroup, indexer); updateMin(minTimestamp, update.stats().minTimestamp); updateMin(minLocalDeletionTime, update.stats().minLocalDeletionTime); liveDataSize.addAndGet(initialSize + updater.dataSize); @@ -434,6 +485,22 @@ public long minLocalDeletionTime() { return minLocalDeletionTime.get(); } + + public DecoratedKey minPartitionKey() + { + Map.Entry entry = partitions.firstEntry(); + return (entry != null) + ? entry.getValue().partitionKey() + : null; + } + + public DecoratedKey maxPartitionKey() + { + Map.Entry entry = partitions.lastEntry(); + return (entry != null) + ? entry.getValue().partitionKey() + : null; + } } public static class MemtableUnfilteredPartitionIterator extends AbstractUnfilteredPartitionIterator implements UnfilteredPartitionIterator @@ -484,7 +551,8 @@ static class Locking extends ShardedSkipListMemtable * * commitLogSegmentPosition should only be null if this is a secondary index, in which case it is *expected* to be null */ - public long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + @Override + public long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) { DecoratedKey key = update.partitionKey(); MemtableShard shard = shards[boundaries.getShardForKey(key)]; diff --git a/src/java/org/apache/cassandra/db/memtable/SkipListMemtable.java b/src/java/org/apache/cassandra/db/memtable/SkipListMemtable.java index 379b1fe0a595..6a6c92f02a44 100644 --- a/src/java/org/apache/cassandra/db/memtable/SkipListMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/SkipListMemtable.java @@ -18,6 +18,7 @@ package org.apache.cassandra.db.memtable; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentNavigableMap; @@ -31,6 +32,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.BufferDecoratedKey; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; @@ -41,10 +43,10 @@ import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator; import org.apache.cassandra.db.partitions.AtomicBTreePartition; import org.apache.cassandra.db.partitions.BTreePartitionData; +import org.apache.cassandra.db.partitions.BTreePartitionUpdate; import org.apache.cassandra.db.partitions.BTreePartitionUpdater; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Bounds; @@ -55,6 +57,7 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.memory.Cloner; import org.apache.cassandra.utils.memory.MemtableAllocator; @@ -86,12 +89,77 @@ public class SkipListMemtable extends AbstractAllocatorMemtable private final AtomicLong liveDataSize = new AtomicLong(0); - protected SkipListMemtable(AtomicReference commitLogLowerBound, TableMetadataRef metadataRef, Owner owner) + /** + * Keeps an estimate of the average row size in this memtable, computed from a small sample of rows. + * Because computing this estimate is potentially costly, as it requires iterating the rows, + * the estimate is updated only whenever the number of operations on the memtable increases significantly from the + * last update. This estimate is not very accurate but should be ok for planning or diagnostic purposes. + */ + private volatile MemtableAverageRowSize estimatedAverageRowSize; + + SkipListMemtable(AtomicReference commitLogLowerBound, TableMetadataRef metadataRef, Owner owner) { super(commitLogLowerBound, metadataRef, owner); } + // Only for testing + @VisibleForTesting + public SkipListMemtable(TableMetadataRef metadataRef) + { + this(null, metadataRef, new Owner() + { + @Override + public Future signalFlushRequired(Memtable memtable, ColumnFamilyStore.FlushReason reason) + { + return null; + } + + @Override + public Memtable getCurrentMemtable() + { + return null; + } + + @Override + public Iterable getIndexMemtables() + { + return Collections.emptyList(); + } + + public ShardBoundaries localRangeSplits(int shardCount) + { + return null; // not implemented + } + + public OpOrder readOrdering() + { + return null; + } + + @Override + public int getMemtableFlushPeriodInMs() + { + return -1; + } + }); + } + + protected Factory factory() + { + return FACTORY; + } @Override + public void addMemoryUsageTo(MemoryUsage stats) + { + super.addMemoryUsageTo(stats); + } + + @Override + public void signalFlushRequired(ColumnFamilyStore.FlushReason flushReason, boolean skipIfSignaled) + { + owner.signalFlushRequired(this, flushReason); + } + public boolean isClean() { return partitions.isEmpty(); @@ -104,7 +172,7 @@ public boolean isClean() * commitLogSegmentPosition should only be null if this is a secondary index, in which case it is *expected* to be null */ @Override - public long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + protected long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) { Cloner cloner = allocator.cloner(opGroup); AtomicBTreePartition previous = partitions.get(update.partitionKey()); @@ -127,7 +195,7 @@ public long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group } } - BTreePartitionUpdater updater = previous.addAll(update, cloner, opGroup, indexer); + BTreePartitionUpdater updater = previous.addAll(BTreePartitionUpdate.asBTreeUpdate(update), cloner, opGroup, indexer); updateMin(minTimestamp, update.stats().minTimestamp); updateMin(minLocalDeletionTime, update.stats().minLocalDeletionTime); liveDataSize.addAndGet(initialSize + updater.dataSize); @@ -191,7 +259,7 @@ private Map getPartitionsSubMap(Partiti } } - Partition getPartition(DecoratedKey key) + public Partition getPartition(DecoratedKey key) { return partitions.get(key); } @@ -213,6 +281,24 @@ public UnfilteredRowIterator rowIterator(DecoratedKey key) return p != null ? p.unfilteredIterator() : null; } + @Override + public DecoratedKey minPartitionKey() + { + Map.Entry entry = partitions.firstEntry(); + return (entry != null) + ? entry.getValue().partitionKey() + : null; + } + + @Override + public DecoratedKey maxPartitionKey() + { + Map.Entry entry = partitions.lastEntry(); + return (entry != null) + ? entry.getValue().partitionKey() + : null; + } + private static int estimateRowOverhead(final int count) { // calculate row overhead @@ -319,21 +405,33 @@ public long partitionKeysSize() } - private static class MemtableUnfilteredPartitionIterator extends AbstractUnfilteredPartitionIterator implements UnfilteredPartitionIterator + public static class MemtableUnfilteredPartitionIterator extends AbstractUnfilteredPartitionIterator implements Memtable.MemtableUnfilteredPartitionIterator { private final TableMetadata metadata; private final Iterator> iter; + private final Map source; private final ColumnFilter columnFilter; private final DataRange dataRange; - MemtableUnfilteredPartitionIterator(TableMetadata metadata, Map map, ColumnFilter columnFilter, DataRange dataRange) + public MemtableUnfilteredPartitionIterator(TableMetadata metadata, Map map, ColumnFilter columnFilter, DataRange dataRange) { this.metadata = metadata; + this.source = map; this.iter = map.entrySet().iterator(); this.columnFilter = columnFilter; this.dataRange = dataRange; } + @Override + public long getMinLocalDeletionTime() + { + long minLocalDeletionTime = Long.MAX_VALUE; + for (AtomicBTreePartition partition : source.values()) + minLocalDeletionTime = Math.min(minLocalDeletionTime, partition.stats().minLocalDeletionTime); + + return minLocalDeletionTime; + } + @Override public TableMetadata metadata() { diff --git a/src/java/org/apache/cassandra/db/memtable/TrieMemtable.java b/src/java/org/apache/cassandra/db/memtable/TrieMemtable.java index 83b02db06a0c..9e012724b5c8 100644 --- a/src/java/org/apache/cassandra/db/memtable/TrieMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/TrieMemtable.java @@ -21,24 +21,23 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NavigableSet; -import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Iterators; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.BufferDecoratedKey; -import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.MutableDeletionInfo; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.Slices; @@ -46,17 +45,19 @@ import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator; -import org.apache.cassandra.db.partitions.BTreePartitionData; -import org.apache.cassandra.db.partitions.BTreePartitionUpdater; -import org.apache.cassandra.db.partitions.ImmutableBTreePartition; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.TrieBackedPartition; +import org.apache.cassandra.db.partitions.TriePartitionUpdate; +import org.apache.cassandra.db.partitions.TriePartitionUpdater; import org.apache.cassandra.db.rows.EncodingStats; -import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.tries.Direction; import org.apache.cassandra.db.tries.InMemoryTrie; import org.apache.cassandra.db.tries.Trie; +import org.apache.cassandra.db.tries.TrieEntriesWalker; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; +import org.apache.cassandra.db.tries.TrieTailsIterator; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Bounds; import org.apache.cassandra.dht.IncludingExcludingBounds; @@ -64,15 +65,18 @@ import org.apache.cassandra.index.transactions.UpdateTransaction; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.sstable.SSTableReadsListener; -import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.TrieMemtableMetricsView; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.memory.EnsureOnHeap; +import org.apache.cassandra.utils.memory.HeapCloner; import org.apache.cassandra.utils.memory.MemtableAllocator; import org.github.jamm.Unmetered; @@ -93,12 +97,23 @@ public class TrieMemtable extends AbstractShardedMemtable /** Buffer type to use for memtable tries (on- vs off-heap) */ public static final BufferType BUFFER_TYPE = DatabaseDescriptor.getMemtableAllocationType().toBufferType(); - /** If keys is below this length, we will use a recursive procedure for inserting data in the memtable trie. */ - @VisibleForTesting - public static final int MAX_RECURSIVE_KEY_LENGTH = 128; + /** + * Force copy checker (see InMemoryTrie.ApplyState) ensuring all modifications apply atomically and consistently to + * the whole partition. + */ + public static final Predicate> FORCE_COPY_PARTITION_BOUNDARY = features -> isPartitionBoundary(features.content()); + + public static final Predicate IS_PARTITION_BOUNDARY = TrieMemtable::isPartitionBoundary; - /** The byte-ordering conversion version to use for memtables. */ - public static final ByteComparable.Version BYTE_COMPARABLE_VERSION = ByteComparable.Version.OSS50; + public static volatile int SHARD_COUNT = CassandraRelevantProperties.TRIE_MEMTABLE_SHARD_COUNT.getInt(autoShardCount()); + public static volatile boolean SHARD_LOCK_FAIRNESS = CassandraRelevantProperties.TRIE_MEMTABLE_SHARD_LOCK_FAIRNESS.getBoolean(); + + public static final String TRIE_MEMTABLE_CONFIG_OBJECT_NAME = "org.apache.cassandra.db:type=TrieMemtableConfig"; + + static + { + MBeanWrapper.instance.registerMBean(new TrieMemtableConfig(), TRIE_MEMTABLE_CONFIG_OBJECT_NAME, MBeanWrapper.OnException.LOG); + } // Set to true when the memtable requests a switch (e.g. for trie size limit being reached) to ensure only one // thread calls cfs.switchMemtableIfCurrent. @@ -115,34 +130,51 @@ public class TrieMemtable extends AbstractShardedMemtable * A merged view of the memtable map. Used for partition range queries and flush. * For efficiency we serve single partition requests off the shard which offers more direct InMemoryTrie methods. */ - private final Trie mergedTrie; + private final Trie mergedTrie; @Unmetered private final TrieMemtableMetricsView metrics; + /** + * Keeps an estimate of the average row size in this memtable, computed from a small sample of rows. + * Because computing this estimate is potentially costly, as it requires iterating the rows, + * the estimate is updated only whenever the number of operations on the memtable increases significantly from the + * last update. This estimate is not very accurate but should be ok for planning or diagnostic purposes. + */ + private volatile MemtableAverageRowSize estimatedAverageRowSize; + TrieMemtable(AtomicReference commitLogLowerBound, TableMetadataRef metadataRef, Owner owner, Integer shardCountOption) { super(commitLogLowerBound, metadataRef, owner, shardCountOption); - this.metrics = new TrieMemtableMetricsView(metadataRef.keyspace, metadataRef.name); - this.shards = generatePartitionShards(boundaries.shardCount(), allocator, metadataRef, metrics); + this.metrics = TrieMemtableMetricsView.getOrCreate(metadataRef.keyspace, metadataRef.name); + this.shards = generatePartitionShards(boundaries.shardCount(), metadataRef, metrics, owner.readOrdering()); this.mergedTrie = makeMergedTrie(shards); + logger.trace("Created memtable with {} shards", this.shards.length); + } + + private static int autoShardCount() + { + return 4 * FBUtilities.getAvailableProcessors(); } private static MemtableShard[] generatePartitionShards(int splits, - MemtableAllocator allocator, TableMetadataRef metadata, - TrieMemtableMetricsView metrics) + TrieMemtableMetricsView metrics, + OpOrder opOrder) { + if (splits == 1) + return new MemtableShard[] { new MemtableShard(metadata, metrics, opOrder) }; + MemtableShard[] partitionMapContainer = new MemtableShard[splits]; for (int i = 0; i < splits; i++) - partitionMapContainer[i] = new MemtableShard(metadata, allocator, metrics); + partitionMapContainer[i] = new MemtableShard(metadata, metrics, opOrder); return partitionMapContainer; } - private static Trie makeMergedTrie(MemtableShard[] shards) + private static Trie makeMergedTrie(MemtableShard[] shards) { - List> tries = new ArrayList<>(shards.length); + List> tries = new ArrayList<>(shards.length); for (MemtableShard shard : shards) tries.add(shard.data); return Trie.mergeDistinct(tries); @@ -157,6 +189,16 @@ public boolean isClean() return true; } + @VisibleForTesting + @Override + public void switchOut(OpOrder.Barrier writeBarrier, AtomicReference commitLogUpperBound) + { + super.switchOut(writeBarrier, commitLogUpperBound); + + for (MemtableShard shard : shards) + shard.allocator.setDiscarding(); + } + @Override public void discard() { @@ -170,6 +212,7 @@ public void discard() // the buffer release is a longer-running process, do it in a separate loop to not make the metrics update wait for (MemtableShard shard : shards) { + shard.allocator.setDiscarded(); shard.data.discardBuffers(); } } @@ -181,34 +224,41 @@ public void discard() * commitLogSegmentPosition should only be null if this is a secondary index, in which case it is *expected* to be null */ @Override - public long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + protected long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) { - try - { - DecoratedKey key = update.partitionKey(); - MemtableShard shard = shards[boundaries.getShardForKey(key)]; - long colUpdateTimeDelta = shard.put(key, update, indexer, opGroup); + DecoratedKey key = update.partitionKey(); + MemtableShard shard = shards[boundaries.getShardForKey(key)]; + long colUpdateTimeDelta = shard.put(update, indexer, opGroup); - if (shard.data.reachedAllocatedSizeThreshold() && !switchRequested.getAndSet(true)) - { - logger.info("Scheduling flush due to trie size limit reached."); - owner.signalFlushRequired(this, ColumnFamilyStore.FlushReason.MEMTABLE_LIMIT); - } + if (shard.data.reachedAllocatedSizeThreshold()) + signalFlushRequired(ColumnFamilyStore.FlushReason.TRIE_LIMIT, true); + + return colUpdateTimeDelta; + } - return colUpdateTimeDelta; + @Override + public void signalFlushRequired(ColumnFamilyStore.FlushReason flushReason, boolean skipIfSignaled) + { + if (!switchRequested.getAndSet(true) || !skipIfSignaled) + { + logger.info("Scheduling flush for table {} due to {}", this.metadata.get(), flushReason); + owner.signalFlushRequired(this, flushReason); } - catch (InMemoryTrie.SpaceExhaustedException e) + } + + @Override + public void addMemoryUsageTo(MemoryUsage stats) + { + super.addMemoryUsageTo(stats); + for (MemtableShard shard : shards) { - // This should never happen as {@link InMemoryTrie#reachedAllocatedSizeThreshold} should become - // true and trigger a memtable switch long before this limit is reached. - throw new IllegalStateException(e); + stats.ownsOnHeap += shard.allocator.onHeap().owns(); + stats.ownsOffHeap += shard.allocator.offHeap().owns(); + stats.ownershipRatioOnHeap += shard.allocator.onHeap().ownershipRatio(); + stats.ownershipRatioOffHeap += shard.allocator.offHeap().ownershipRatio(); } } - /** - * Technically we should scatter gather on all the core threads because the size in following calls are not - * using volatile variables, but for metrics purpose this should be good enough. - */ @Override public long getLiveDataSize() { @@ -232,10 +282,15 @@ public long partitionCount() { int total = 0; for (MemtableShard shard : shards) - total += shard.size(); + total += shard.partitionCount(); return total; } + public int getShardCount() + { + return shards.length; + } + /** * Returns the minTS if one available, otherwise NO_MIN_TIMESTAMP. * @@ -248,7 +303,7 @@ public long getMinTimestamp() { long min = Long.MAX_VALUE; for (MemtableShard shard : shards) - min = Long.min(min, shard.minTimestamp()); + min = EncodingStats.mergeMinTimestamp(min, shard.stats); return min != EncodingStats.NO_STATS.minTimestamp ? min : NO_MIN_TIMESTAMP; } @@ -257,15 +312,39 @@ public long getMinLocalDeletionTime() { long min = Long.MAX_VALUE; for (MemtableShard shard : shards) - min = Long.min(min, shard.minLocalDeletionTime()); + min = EncodingStats.mergeMinLocalDeletionTime(min, shard.stats); return min; } + @Override + public DecoratedKey minPartitionKey() + { + for (int i = 0; i < shards.length; i++) + { + MemtableShard shard = shards[i]; + if (!shard.isClean()) + return shard.minPartitionKey(); + } + return null; + } + + @Override + public DecoratedKey maxPartitionKey() + { + for (int i = shards.length - 1; i >= 0; i--) + { + MemtableShard shard = shards[i]; + if (!shard.isClean()) + return shard.maxPartitionKey(); + } + return null; + } + @Override RegularAndStaticColumns columns() { for (MemtableShard shard : shards) - columnsCollector.update(shard.columnsCollector); + columnsCollector.update(shard.columns); return columnsCollector.get(); } @@ -273,10 +352,17 @@ RegularAndStaticColumns columns() EncodingStats encodingStats() { for (MemtableShard shard : shards) - statsCollector.update(shard.statsCollector.get()); + statsCollector.update(shard.stats); return statsCollector.get(); } + static boolean isPartitionBoundary(Object content) + { + // In the trie we use PartitionData for the root of a partition, but PartitionUpdates come with DeletionInfo. + // Both are descendants of DeletionInfo. + return content instanceof DeletionInfo; + } + @Override public MemtableUnfilteredPartitionIterator partitionIterator(final ColumnFilter columnFilter, final DataRange dataRange, @@ -284,35 +370,53 @@ public MemtableUnfilteredPartitionIterator partitionIterator(final ColumnFilter { AbstractBounds keyRange = dataRange.keyRange(); - PartitionPosition left = keyRange.left; - PartitionPosition right = keyRange.right; - if (left.isMinimum()) - left = null; - if (right.isMinimum()) - right = null; - boolean isBound = keyRange instanceof Bounds; boolean includeStart = isBound || keyRange instanceof IncludingExcludingBounds; boolean includeStop = isBound || keyRange instanceof Range; - Trie subMap = mergedTrie.subtrie(left, includeStart, right, includeStop); + Trie subMap = mergedTrie.subtrie(toComparableBound(keyRange.left, includeStart), + toComparableBound(keyRange.right, !includeStop)); return new MemtableUnfilteredPartitionIterator(metadata(), allocator.ensureOnHeap(), subMap, columnFilter, - dataRange); - // readsListener is ignored as it only accepts sstable signals + dataRange, + getMinLocalDeletionTime()); + // Note: the minLocalDeletionTime reported by the iterator is the memtable's minLocalDeletionTime. This is okay + // because we only need to report a lower bound that will eventually advance, and calculating a more precise + // bound would be an unnecessary expense. } - private Partition getPartition(DecoratedKey key) + private static ByteComparable toComparableBound(PartitionPosition position, boolean before) + { + return position.isMinimum() ? null : position.asComparableBound(before); + } + + public Partition getPartition(DecoratedKey key) { int shardIndex = boundaries.getShardForKey(key); - BTreePartitionData data = shards[shardIndex].data.get(key); - if (data != null) - return createPartition(metadata(), allocator.ensureOnHeap(), key, data); - else + Trie trie = shards[shardIndex].data.tailTrie(key); + return createPartition(metadata(), allocator.ensureOnHeap(), key, trie); + } + + private static TrieBackedPartition createPartition(TableMetadata metadata, EnsureOnHeap ensureOnHeap, DecoratedKey key, Trie trie) + { + if (trie == null) return null; + PartitionData holder = (PartitionData) trie.get(ByteComparable.EMPTY); + // If we found a matching path in the trie, it must be the root of this partition (because partition keys are + // prefix-free, it can't be a prefix for a different path, or have another partition key as prefix) and contain + // PartitionData (because the attachment of a new or modified partition to the trie is atomic). + assert holder != null : "Entry for " + key + " without associated PartitionData"; + + return TrieBackedPartition.create(key, + holder.columns(), + holder.stats(), + holder.rowCountIncludingStatic(), + trie, + metadata, + ensureOnHeap); } @Override @@ -332,40 +436,113 @@ public UnfilteredRowIterator rowIterator(DecoratedKey key) return p != null ? p.unfilteredIterator() : null; } - private static MemtablePartition createPartition(TableMetadata metadata, EnsureOnHeap ensureOnHeap, DecoratedKey key, BTreePartitionData data) + private static DecoratedKey getPartitionKeyFromPath(TableMetadata metadata, ByteComparable path) { - return new MemtablePartition(metadata, ensureOnHeap, key, data); + return BufferDecoratedKey.fromByteComparable(path, + TrieBackedPartition.BYTE_COMPARABLE_VERSION, + metadata.partitioner); } - private static MemtablePartition getPartitionFromTrieEntry(TableMetadata metadata, EnsureOnHeap ensureOnHeap, Map.Entry en) + /** + * Metadata object signifying the root node of a partition. Holds the deletion information as well as a link + * to the owning subrange, which is used for compiling statistics and column sets. + * + * Descends from MutableDeletionInfo to permit tail tries to be passed directly to TrieBackedPartition. + */ + public static class PartitionData extends MutableDeletionInfo { - DecoratedKey key = BufferDecoratedKey.fromByteComparable(en.getKey(), - BYTE_COMPARABLE_VERSION, - metadata.partitioner); - return createPartition(metadata, ensureOnHeap, key, en.getValue()); + @Unmetered + public final MemtableShard owner; + + private int rowCountIncludingStatic; + + public static final long HEAP_SIZE = ObjectSizes.measure(new PartitionData(DeletionInfo.LIVE, null)); + + public PartitionData(DeletionInfo deletion, + MemtableShard owner) + { + super(deletion.getPartitionDeletion(), deletion.copyRanges(HeapCloner.instance)); + this.owner = owner; + this.rowCountIncludingStatic = 0; + } + + public PartitionData(PartitionData existing, + DeletionInfo update) + { + // Start with the update content, to properly copy it + this(update, existing.owner); + rowCountIncludingStatic = existing.rowCountIncludingStatic; + add(existing); + } + + public RegularAndStaticColumns columns() + { + return owner.columns; + } + + public EncodingStats stats() + { + return owner.stats; + } + + public int rowCountIncludingStatic() + { + return rowCountIncludingStatic; + } + + public void markInsertedRows(int howMany) + { + rowCountIncludingStatic += howMany; + } + + @Override + public String toString() + { + return "partition " + super.toString(); + } + + @Override + public long unsharedHeapSize() + { + return super.unsharedHeapSize() + HEAP_SIZE - MutableDeletionInfo.EMPTY_SIZE; + } } - @Override - public FlushablePartitionSet getFlushSet(PartitionPosition from, PartitionPosition to) + class KeySizeAndCountCollector extends TrieEntriesWalker { - Trie toFlush = mergedTrie.subtrie(from, true, to, false); long keySize = 0; int keyCount = 0; - for (Iterator> it = toFlush.entryIterator(); it.hasNext(); ) + @Override + public Void complete() + { + return null; + } + + @Override + protected void content(Object content, byte[] bytes, int byteLength) { - Map.Entry en = it.next(); - byte[] keyBytes = DecoratedKey.keyFromByteSource(ByteSource.peekable(en.getKey().asComparableBytes(BYTE_COMPARABLE_VERSION)), - BYTE_COMPARABLE_VERSION, + // This is used with processSkippingBranches which should ensure that we only see the partition roots. + assert content instanceof PartitionData; + ++keyCount; + byte[] keyBytes = DecoratedKey.keyFromByteSource(ByteSource.preencoded(bytes, 0, byteLength), + TrieBackedPartition.BYTE_COMPARABLE_VERSION, metadata().partitioner); keySize += keyBytes.length; - keyCount++; } - long partitionKeySize = keySize; - int partitionCount = keyCount; + } + + public FlushablePartitionSet getFlushSet(PartitionPosition from, PartitionPosition to) + { + Trie toFlush = mergedTrie.subtrie(from, true, to, false); + + var counter = new KeySizeAndCountCollector(); // need to jump over tails keys + toFlush.processSkippingBranches(counter, Direction.FORWARD); + int partitionCount = counter.keyCount; + long partitionKeySize = counter.keySize; - return new AbstractFlushablePartitionSet() + return new AbstractFlushablePartitionSet() { public Memtable memtable() { @@ -387,12 +564,9 @@ public long partitionCount() return partitionCount; } - public Iterator iterator() + public Iterator iterator() { - return Iterators.transform(toFlush.entryIterator(), - // During flushing we are certain the memtable will remain at least until - // the flush completes. No copying to heap is necessary. - entry -> getPartitionFromTrieEntry(metadata(), EnsureOnHeap.NOOP, entry)); + return new PartitionIterator(toFlush, metadata(), EnsureOnHeap.NOOP); } public long partitionKeysSize() @@ -402,7 +576,7 @@ public long partitionKeysSize() }; } - static class MemtableShard + public static class MemtableShard { // The following fields are volatile as we have to make sure that when we // collect results from all sub-ranges, the thread accessing the value @@ -417,8 +591,10 @@ static class MemtableShard private volatile long currentOperations = 0; + private volatile int partitionCount = 0; + @Unmetered - private final ReentrantLock writeLock = new ReentrantLock(); + private final ReentrantLock writeLock = new ReentrantLock(SHARD_LOCK_FAIRNESS); // Content map for the given shard. This is implemented as a memtable trie which uses the prefix-free // byte-comparable ByteSource representations of the keys to address the partitions. @@ -433,11 +609,11 @@ static class MemtableShard // unsafely, meaning that the memtable will not be discarded as long as the data is used, or whether the data // should be copied on heap for off-heap allocators. @VisibleForTesting - final InMemoryTrie data; + final InMemoryTrie data; - private final ColumnsCollector columnsCollector; + RegularAndStaticColumns columns; - private final StatsCollector statsCollector; + EncodingStats stats; @Unmetered // total pool size should not be included in memtable's deep size private final MemtableAllocator allocator; @@ -445,19 +621,27 @@ static class MemtableShard @Unmetered private final TrieMemtableMetricsView metrics; + private final TableMetadataRef metadata; + + MemtableShard(TableMetadataRef metadata, TrieMemtableMetricsView metrics, OpOrder opOrder) + { + this(metadata, AbstractAllocatorMemtable.MEMORY_POOL.newAllocator(metadata.toString()), metrics, opOrder); + } + @VisibleForTesting - MemtableShard(TableMetadataRef metadata, MemtableAllocator allocator, TrieMemtableMetricsView metrics) + MemtableShard(TableMetadataRef metadata, MemtableAllocator allocator, TrieMemtableMetricsView metrics, OpOrder opOrder) { - this.data = new InMemoryTrie<>(BUFFER_TYPE); - this.columnsCollector = new AbstractMemtable.ColumnsCollector(metadata.get().regularAndStaticColumns()); - this.statsCollector = new AbstractMemtable.StatsCollector(); + this.metadata = metadata; + this.data = InMemoryTrie.longLived(TrieBackedPartition.BYTE_COMPARABLE_VERSION, BUFFER_TYPE, opOrder); + this.columns = RegularAndStaticColumns.NONE; + this.stats = EncodingStats.NO_STATS; this.allocator = allocator; this.metrics = metrics; } - public long put(DecoratedKey key, PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) throws InMemoryTrie.SpaceExhaustedException + public long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) { - BTreePartitionUpdater updater = new BTreePartitionUpdater(allocator, allocator.cloner(opGroup), opGroup, indexer); + TriePartitionUpdater updater = new TriePartitionUpdater(allocator.cloner(opGroup), indexer, metadata.get(), this); boolean locked = writeLock.tryLock(); if (locked) { @@ -474,25 +658,37 @@ public long put(DecoratedKey key, PartitionUpdate update, UpdateTransaction inde { try { - long onHeap = data.sizeOnHeap(); - long offHeap = data.sizeOffHeap(); + indexer.start(); + // Add the initial trie size on the first operation. This technically isn't correct (other shards + // do take their memory share even if they are empty) but doing it during construction may cause + // the allocator to block while we are trying to flush a memtable and become a deadlock. + long onHeap = data.isEmpty() ? 0 : data.usedSizeOnHeap(); + long offHeap = data.isEmpty() ? 0 : data.usedSizeOffHeap(); // Use the fast recursive put if we know the key is small enough to not cause a stack overflow. - data.putSingleton(key, - update, - updater::mergePartitions, - key.getKeyLength() < MAX_RECURSIVE_KEY_LENGTH); - allocator.offHeap().adjust(data.sizeOffHeap() - offHeap, opGroup); - allocator.onHeap().adjust(data.sizeOnHeap() - onHeap, opGroup); + try + { + data.apply(TriePartitionUpdate.asMergableTrie(update), + updater, + FORCE_COPY_PARTITION_BOUNDARY); + } + catch (TrieSpaceExhaustedException e) + { + // This should never really happen as a flush would be triggered long before this limit is reached. + throw new AssertionError(e); + } + allocator.offHeap().adjust(data.usedSizeOffHeap() - offHeap, opGroup); + allocator.onHeap().adjust((data.usedSizeOnHeap() - onHeap) + updater.heapSize, opGroup); + partitionCount += updater.partitionsAdded; } finally { - minTimestamp = Math.min(minTimestamp, update.stats().minTimestamp); - minLocalDeletionTime = Math.min(minLocalDeletionTime, update.stats().minLocalDeletionTime); - liveDataSize += updater.dataSize; - currentOperations += update.operationCount(); + indexer.commit(); + updateMinTimestamp(update.stats().minTimestamp); + updateLiveDataSize(updater.dataSize); + updateCurrentOperations(update.operationCount()); - columnsCollector.update(update.columns()); - statsCollector.update(update.stats()); + columns = columns.mergeTo(update.columns()); + stats = stats.mergeWith(update.stats()); } } finally @@ -507,151 +703,130 @@ public boolean isClean() return data.isEmpty(); } - public int size() + private void updateMinTimestamp(long timestamp) { - return data.valuesCount(); + if (timestamp < minTimestamp) + minTimestamp = timestamp; } - long minTimestamp() + void updateLiveDataSize(long size) { - return minTimestamp; + liveDataSize = liveDataSize + size; } - long liveDataSize() + private void updateCurrentOperations(long op) { - return liveDataSize; + currentOperations = currentOperations + op; } - long currentOperations() + public int partitionCount() { - return currentOperations; + return partitionCount; } - long minLocalDeletionTime() + long liveDataSize() { - return minLocalDeletionTime; + return liveDataSize; } - } - static class MemtableUnfilteredPartitionIterator extends AbstractUnfilteredPartitionIterator implements UnfilteredPartitionIterator - { - private final TableMetadata metadata; - private final EnsureOnHeap ensureOnHeap; - private final Iterator> iter; - private final ColumnFilter columnFilter; - private final DataRange dataRange; - - public MemtableUnfilteredPartitionIterator(TableMetadata metadata, - EnsureOnHeap ensureOnHeap, - Trie source, - ColumnFilter columnFilter, - DataRange dataRange) + long currentOperations() { - this.metadata = metadata; - this.ensureOnHeap = ensureOnHeap; - this.iter = source.entryIterator(); - this.columnFilter = columnFilter; - this.dataRange = dataRange; + return currentOperations; } - public TableMetadata metadata() + private DecoratedKey firstPartitionKey(Direction direction) { - return metadata; + Iterator> iter = data.filteredEntryIterator(direction, PartitionData.class); + if (!iter.hasNext()) + return null; + + Map.Entry entry = iter.next(); + return getPartitionKeyFromPath(metadata.get(), entry.getKey()); } - public boolean hasNext() + public DecoratedKey minPartitionKey() { - return iter.hasNext(); + return firstPartitionKey(Direction.FORWARD); } - public UnfilteredRowIterator next() + public DecoratedKey maxPartitionKey() { - Partition partition = getPartitionFromTrieEntry(metadata(), ensureOnHeap, iter.next()); - DecoratedKey key = partition.partitionKey(); - ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(key); - - return filter.getUnfilteredRowIterator(columnFilter, partition); + return firstPartitionKey(Direction.REVERSE); } } - static class MemtablePartition extends ImmutableBTreePartition + static class PartitionIterator extends TrieTailsIterator { - - private final EnsureOnHeap ensureOnHeap; - - private MemtablePartition(TableMetadata table, EnsureOnHeap ensureOnHeap, DecoratedKey key, BTreePartitionData data) + final TableMetadata metadata; + final EnsureOnHeap ensureOnHeap; + PartitionIterator(Trie source, TableMetadata metadata, EnsureOnHeap ensureOnHeap) { - super(table, key, data); + super(source, Direction.FORWARD, PartitionData.class::isInstance); + this.metadata = metadata; this.ensureOnHeap = ensureOnHeap; } @Override - protected boolean canHaveShadowedData() - { - // The BtreePartitionData we store in the memtable are build iteratively by BTreePartitionData.add(), which - // doesn't make sure there isn't shadowed data, so we'll need to eliminate any. - return true; - } - - - @Override - public DeletionInfo deletionInfo() - { - return ensureOnHeap.applyToDeletionInfo(super.deletionInfo()); - } - - @Override - public Row staticRow() - { - return ensureOnHeap.applyToStatic(super.staticRow()); - } - - @Override - public DecoratedKey partitionKey() - { - return ensureOnHeap.applyToPartitionKey(super.partitionKey()); + protected TrieBackedPartition mapContent(Object content, Trie tailTrie, byte[] bytes, int byteLength) + { + PartitionData pd = (PartitionData) content; + DecoratedKey key = getPartitionKeyFromPath(metadata, + ByteComparable.preencoded(TrieBackedPartition.BYTE_COMPARABLE_VERSION, + bytes, 0, byteLength)); + return TrieBackedPartition.create(key, + pd.columns(), + pd.stats(), + pd.rowCountIncludingStatic(), + tailTrie, + metadata, + ensureOnHeap); } + } - @Override - public Row getRow(Clustering clustering) - { - return ensureOnHeap.applyToRow(super.getRow(clustering)); - } + static class MemtableUnfilteredPartitionIterator extends AbstractUnfilteredPartitionIterator implements Memtable.MemtableUnfilteredPartitionIterator + { + private final TableMetadata metadata; + private final Iterator iter; + private final ColumnFilter columnFilter; + private final DataRange dataRange; + private final long minLocalDeletionTime; - @Override - public Row lastRow() + public MemtableUnfilteredPartitionIterator(TableMetadata metadata, + EnsureOnHeap ensureOnHeap, + Trie source, + ColumnFilter columnFilter, + DataRange dataRange, + long minLocalDeletionTime) { - return ensureOnHeap.applyToRow(super.lastRow()); + this.iter = new PartitionIterator(source, metadata, ensureOnHeap); + this.metadata = metadata; + this.columnFilter = columnFilter; + this.dataRange = dataRange; + this.minLocalDeletionTime = minLocalDeletionTime; } - @Override - public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, Slices slices, boolean reversed) + public long getMinLocalDeletionTime() { - return unfilteredIterator(holder(), selection, slices, reversed); + return minLocalDeletionTime; } - @Override - public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, NavigableSet> clusteringsInQueryOrder, boolean reversed) + public TableMetadata metadata() { - return ensureOnHeap.applyToPartition(super.unfilteredIterator(selection, clusteringsInQueryOrder, reversed)); + return metadata; } - @Override - public UnfilteredRowIterator unfilteredIterator() + public boolean hasNext() { - return unfilteredIterator(ColumnFilter.selection(super.columns()), Slices.ALL, false); + return iter.hasNext(); } - @Override - public UnfilteredRowIterator unfilteredIterator(BTreePartitionData current, ColumnFilter selection, Slices slices, boolean reversed) + public UnfilteredRowIterator next() { - return ensureOnHeap.applyToPartition(super.unfilteredIterator(current, selection, slices, reversed)); - } + Partition partition = iter.next(); + DecoratedKey key = partition.partitionKey(); + ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(key); - @Override - public Iterator iterator() - { - return ensureOnHeap.applyToPartition(super.iterator()); + return filter.getUnfilteredRowIterator(columnFilter, partition); } } @@ -659,54 +834,78 @@ public static Factory factory(Map optionsCopy) { String shardsString = optionsCopy.remove(SHARDS_OPTION); Integer shardCount = shardsString != null ? Integer.parseInt(shardsString) : null; - return new Factory(shardCount); + return new TrieMemtableFactory(shardCount); } - static class Factory implements Memtable.Factory + @Override + public long unusedReservedOnHeapMemory() { - final Integer shardCount; - - Factory(Integer shardCount) + long size = 0; + for (MemtableShard shard : shards) { - this.shardCount = shardCount; + size += shard.data.unusedReservedOnHeapMemory(); + size += shard.allocator.unusedReservedOnHeapMemory(); } + size += this.allocator.unusedReservedOnHeapMemory(); + return size; + } - public Memtable create(AtomicReference commitLogLowerBound, - TableMetadataRef metadaRef, - Owner owner) + /** + * Release all recycled content references, including the ones waiting in still incomplete recycling lists. + * This is a test method and can cause null pointer exceptions if used on a live trie. + */ + @VisibleForTesting + void releaseReferencesUnsafe() + { + for (MemtableShard shard : shards) + shard.data.releaseReferencesUnsafe(); + } + + public static class TrieMemtableConfig implements TrieMemtableConfigMXBean + { + @Override + public void setShardCount(String shardCount) { - return new TrieMemtable(commitLogLowerBound, metadaRef, owner, shardCount); + if ("auto".equalsIgnoreCase(shardCount)) + { + SHARD_COUNT = autoShardCount(); + CassandraRelevantProperties.TRIE_MEMTABLE_SHARD_COUNT.setInt(SHARD_COUNT); + } + else + { + try + { + SHARD_COUNT = Integer.valueOf(shardCount); + CassandraRelevantProperties.TRIE_MEMTABLE_SHARD_COUNT.setInt(SHARD_COUNT); + } + catch (NumberFormatException ex) + { + logger.warn("Unable to parse {} as valid value for shard count; leaving it as {}", + shardCount, SHARD_COUNT); + return; + } + } + logger.info("Requested setting shard count to {}; set to: {}", shardCount, SHARD_COUNT); } @Override - public TableMetrics.ReleasableMetric createMemtableMetrics(TableMetadataRef metadataRef) + public String getShardCount() { - TrieMemtableMetricsView metrics = new TrieMemtableMetricsView(metadataRef.keyspace, metadataRef.name); - return metrics::release; + return "" + SHARD_COUNT; } - public boolean equals(Object o) + @Override + public void setLockFairness(String fairness) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Factory factory = (Factory) o; - return Objects.equals(shardCount, factory.shardCount); + SHARD_LOCK_FAIRNESS = Boolean.parseBoolean(fairness); + CassandraRelevantProperties.TRIE_MEMTABLE_SHARD_LOCK_FAIRNESS.setBoolean(SHARD_LOCK_FAIRNESS); + logger.info("Requested setting shard lock fairness to {}; set to: {}", fairness, SHARD_LOCK_FAIRNESS); } - public int hashCode() + @Override + public String getLockFairness() { - return Objects.hash(shardCount); + return "" + SHARD_LOCK_FAIRNESS; } } - - @VisibleForTesting - public long unusedReservedMemory() - { - long size = 0; - for (MemtableShard shard : shards) - size += shard.data.unusedReservedMemory(); - return size; - } } diff --git a/src/java/org/apache/cassandra/db/memtable/TrieMemtableConfigMXBean.java b/src/java/org/apache/cassandra/db/memtable/TrieMemtableConfigMXBean.java new file mode 100644 index 000000000000..4b4039f85eea --- /dev/null +++ b/src/java/org/apache/cassandra/db/memtable/TrieMemtableConfigMXBean.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.memtable; + +public interface TrieMemtableConfigMXBean +{ + public void setShardCount(String numShards); + + public String getShardCount(); + + public void setLockFairness(String fairness); + + public String getLockFairness(); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/memtable/TrieMemtableFactory.java b/src/java/org/apache/cassandra/db/memtable/TrieMemtableFactory.java new file mode 100644 index 000000000000..7af6c71d29d0 --- /dev/null +++ b/src/java/org/apache/cassandra/db/memtable/TrieMemtableFactory.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.memtable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.collect.ImmutableMap; + +import org.apache.cassandra.config.InheritingClass; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.partitions.TriePartitionUpdate; +import org.apache.cassandra.metrics.TableMetrics; +import org.apache.cassandra.metrics.TrieMemtableMetricsView; +import org.apache.cassandra.schema.TableMetadataRef; + +import static org.apache.cassandra.db.partitions.PartitionUpdate.*; + +/** + * This class makes better sense as an inner class to TrieMemtable (which could be as simple as + * FACTORY = TrieMemtable::new), but having it there causes the TrieMemtable class to be initialized the first + * time it is referenced (e.g. during default memtable factory construction). + * + * Some tests want to setup table parameters before initializing DatabaseDescriptor -- this allows them to do so, and + * also makes sure the memtable memory pools are not created for offline tools. + */ +public class TrieMemtableFactory implements Memtable.Factory +{ + final Integer shardCount; + + TrieMemtableFactory(Integer shardCount) + { + this.shardCount = shardCount; + } + + @Override + public Memtable create(AtomicReference commitLogLowerBound, TableMetadataRef metadaRef, Memtable.Owner owner) + { + return new TrieMemtable(commitLogLowerBound, metadaRef, owner, shardCount); + } + + public static final TrieMemtableFactory INSTANCE = new TrieMemtableFactory(null); + public static InheritingClass CONFIGURATION = new InheritingClass(null, TrieMemtable.class.getName(), ImmutableMap.of()); + + @Override + public Factory partitionUpdateFactory() + { + return TriePartitionUpdate.FACTORY; + } + + @Override + public TableMetrics.ReleasableMetric createMemtableMetrics(TableMetadataRef metadataRef) + { + TrieMemtableMetricsView metrics = TrieMemtableMetricsView.getOrCreate(metadataRef.keyspace, metadataRef.name); + return metrics::release; + } + + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TrieMemtableFactory factory = (TrieMemtableFactory) o; + return Objects.equals(shardCount, factory.shardCount); + } + + public int hashCode() + { + return Objects.hash(shardCount); + } +} diff --git a/src/java/org/apache/cassandra/db/memtable/TrieMemtableStage1.java b/src/java/org/apache/cassandra/db/memtable/TrieMemtableStage1.java new file mode 100644 index 000000000000..b3af01707d99 --- /dev/null +++ b/src/java/org/apache/cassandra/db/memtable/TrieMemtableStage1.java @@ -0,0 +1,841 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.memtable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.collect.Iterators; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.BufferDecoratedKey; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DataRange; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.BTreePartitionData; +import org.apache.cassandra.db.partitions.BTreePartitionUpdate; +import org.apache.cassandra.db.partitions.BTreePartitionUpdater; +import org.apache.cassandra.db.partitions.ImmutableBTreePartition; +import org.apache.cassandra.db.partitions.Partition; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.tries.Direction; +import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.db.tries.Trie; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.dht.IncludingExcludingBounds; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.index.transactions.UpdateTransaction; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.metrics.TableMetrics; +import org.apache.cassandra.metrics.TrieMemtableMetricsView; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.concurrent.OpOrder; +import org.apache.cassandra.utils.memory.Cloner; +import org.apache.cassandra.utils.memory.EnsureOnHeap; +import org.apache.cassandra.utils.memory.MemtableAllocator; +import org.github.jamm.Unmetered; + +import static org.apache.cassandra.io.sstable.SSTableReadsListener.NOOP_LISTENER; + +/** + * Previous TrieMemtable implementation, provided for two reasons: + *
      + *
    • to easily compare current and earlier implementations of the trie memtable + *
    • to have an option to change a database back to the older implementation if we find a bug or a performance problem + * with the new code. + *
    + *

    + * To switch a table to this version, use + *

    + *   ALTER TABLE ... WITH memtable = {'class': 'TrieMemtableStage1'}
    + * 
    + * or add + *
    + *   memtable:
    + *     class: TrieMemtableStage1
    + * 
    + * in cassandra.yaml to switch a node to it as default. + * + */ +public class TrieMemtableStage1 extends AbstractAllocatorMemtable +{ + private static final Logger logger = LoggerFactory.getLogger(TrieMemtableStage1.class); + + public static final Factory FACTORY = new TrieMemtableStage1.Factory(); + + static final ByteComparable.Version BYTE_COMPARABLE_VERSION = ByteComparable.Version.OSS41; + + /** If keys is below this length, we will use a recursive procedure for inserting data in the memtable trie. */ + @VisibleForTesting + public static final int MAX_RECURSIVE_KEY_LENGTH = 128; + + // Set to true when the memtable requests a switch (e.g. for trie size limit being reached) to ensure only one + // thread calls cfs.switchMemtableIfCurrent. + private AtomicBoolean switchRequested = new AtomicBoolean(false); + + + // The boundaries for the keyspace as they were calculated when the memtable is created. + // The boundaries will be NONE for system keyspaces or if StorageService is not yet initialized. + // The fact this is fixed for the duration of the memtable lifetime, guarantees we'll always pick the same core + // for the a given key, even if we race with the StorageService initialization or with topology changes. + @Unmetered + private final ShardBoundaries boundaries; + + /** + * Core-specific memtable regions. All writes must go through the specific core. The data structures used + * are concurrent-read safe, thus reads can be carried out from any thread. + */ + private final MemtableShard[] shards; + + /** + * A merged view of the memtable map. Used for partition range queries and flush. + * For efficiency we serve single partition requests off the shard which offers more direct InMemoryTrie methods. + */ + private final Trie mergedTrie; + + @Unmetered + private final TrieMemtableMetricsView metrics; + + /** + * Keeps an estimate of the average row size in this memtable, computed from a small sample of rows. + * Because computing this estimate is potentially costly, as it requires iterating the rows, + * the estimate is updated only whenever the number of operations on the memtable increases significantly from the + * last update. This estimate is not very accurate but should be ok for planning or diagnostic purposes. + */ + private volatile MemtableAverageRowSize estimatedAverageRowSize; + + // only to be used by init(), to setup the very first memtable for the cfs + TrieMemtableStage1(AtomicReference commitLogLowerBound, TableMetadataRef metadataRef, Owner owner) + { + super(commitLogLowerBound, metadataRef, owner); + this.boundaries = owner.localRangeSplits(AbstractShardedMemtable.getDefaultShardCount()); + this.metrics = TrieMemtableMetricsView.getOrCreate(metadataRef.keyspace, metadataRef.name); + this.shards = generatePartitionShards(boundaries.shardCount(), metadataRef, metrics); + this.mergedTrie = makeMergedTrie(shards); + logger.trace("Created memtable with {} shards", this.shards.length); + } + + private static MemtableShard[] generatePartitionShards(int splits, + TableMetadataRef metadata, + TrieMemtableMetricsView metrics) + { + if (splits == 1) + return new MemtableShard[] { new MemtableShard(metadata, metrics) }; + + MemtableShard[] partitionMapContainer = new MemtableShard[splits]; + for (int i = 0; i < splits; i++) + partitionMapContainer[i] = new MemtableShard(metadata, metrics); + + return partitionMapContainer; + } + + private static Trie makeMergedTrie(MemtableShard[] shards) + { + List> tries = new ArrayList<>(shards.length); + for (MemtableShard shard : shards) + tries.add(shard.data); + return Trie.mergeDistinct(tries); + } + + public boolean isClean() + { + for (MemtableShard shard : shards) + if (!shard.isEmpty()) + return false; + return true; + } + + @VisibleForTesting + @Override + public void switchOut(OpOrder.Barrier writeBarrier, AtomicReference commitLogUpperBound) + { + super.switchOut(writeBarrier, commitLogUpperBound); + + for (MemtableShard shard : shards) + shard.allocator.setDiscarding(); + } + + @Override + public void discard() + { + super.discard(); + // metrics here are not thread safe, but I think we can live with that + metrics.lastFlushShardDataSizes.reset(); + for (MemtableShard shard : shards) + { + metrics.lastFlushShardDataSizes.update(shard.liveDataSize()); + } + for (MemtableShard shard : shards) + { + shard.allocator.setDiscarded(); + shard.data.discardBuffers(); + } + } + + /** + * Should only be called by ColumnFamilyStore.apply via Keyspace.apply, which supplies the appropriate + * OpOrdering. + * + * commitLogSegmentPosition should only be null if this is a secondary index, in which case it is *expected* to be null + */ + @Override + protected long performPut(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + { + DecoratedKey key = update.partitionKey(); + MemtableShard shard = shards[boundaries.getShardForKey(key)]; + long colUpdateTimeDelta = shard.put(key, update, indexer, opGroup); + + if (shard.data.reachedAllocatedSizeThreshold()) + signalFlushRequired(ColumnFamilyStore.FlushReason.TRIE_LIMIT, true); + + return colUpdateTimeDelta; + } + + @Override + public void signalFlushRequired(ColumnFamilyStore.FlushReason flushReason, boolean skipIfSignaled) + { + if (!switchRequested.getAndSet(true) || !skipIfSignaled) + { + logger.info("Scheduling flush for table {} due to {}", this.metadata.get(), flushReason); + owner.signalFlushRequired(this, flushReason); + } + } + + @Override + public void addMemoryUsageTo(MemoryUsage stats) + { + super.addMemoryUsageTo(stats); + for (MemtableShard shard : shards) + { + stats.ownsOnHeap += shard.allocator.onHeap().owns(); + stats.ownsOffHeap += shard.allocator.offHeap().owns(); + stats.ownershipRatioOnHeap += shard.allocator.onHeap().ownershipRatio(); + stats.ownershipRatioOffHeap += shard.allocator.offHeap().ownershipRatio(); + } + } + + /** + * Technically we should scatter gather on all the core threads because the size in following calls are not + * using volatile variables, but for metrics purpose this should be good enough. + */ + @Override + public long getLiveDataSize() + { + long total = 0L; + for (MemtableShard shard : shards) + total += shard.liveDataSize(); + return total; + } + + @Override + public long operationCount() + { + long total = 0L; + for (MemtableShard shard : shards) + total += shard.currentOperations(); + return total; + } + + @Override + public long partitionCount() + { + int total = 0; + for (MemtableShard shard : shards) + total += shard.partitionCount(); + return total; + } + + public int getShardCount() + { + return shards.length; + } + + public long rowCount(final ColumnFilter columnFilter, final DataRange dataRange) + { + int total = 0; + for (MemtableUnfilteredPartitionIterator iter = partitionIterator(columnFilter, dataRange, NOOP_LISTENER); iter.hasNext(); ) + { + for (UnfilteredRowIterator it = iter.next(); it.hasNext(); ) + { + Unfiltered uRow = it.next(); + if (uRow.isRow()) + total++; + } + } + + return total; + } + + @Override + public long getEstimatedAverageRowSize() + { + if (estimatedAverageRowSize == null || currentOperations.get() > estimatedAverageRowSize.operations * 1.5) + estimatedAverageRowSize = new MemtableAverageRowSize(this); + return estimatedAverageRowSize.rowSize; + } + + @Override + public UnfilteredRowIterator rowIterator(DecoratedKey key, Slices slices, ColumnFilter columnFilter, boolean reversed, SSTableReadsListener listener) + { + Partition p = getPartition(key); + if (p == null) + return null; + else + return p.unfilteredIterator(columnFilter, slices, reversed); + } + + @Override + public UnfilteredRowIterator rowIterator(DecoratedKey key) + { + Partition p = getPartition(key); + return p != null ? p.unfilteredIterator() : null; + } + + /** + * Returns the minTS if one available, otherwise NO_MIN_TIMESTAMP. + * + * EncodingStats uses a synthetic epoch TS at 2015. We don't want to leak that (CASSANDRA-18118) so we return NO_MIN_TIMESTAMP instead. + * + * @return The minTS or NO_MIN_TIMESTAMP if none available + */ + @Override + public long getMinTimestamp() + { + long min = Long.MAX_VALUE; + for (MemtableShard shard : shards) + min = EncodingStats.mergeMinTimestamp(min, shard.stats); + return min != EncodingStats.NO_STATS.minTimestamp ? min : NO_MIN_TIMESTAMP; + } + + @Override + public DecoratedKey minPartitionKey() + { + for (int i = 0; i < shards.length; i++) + { + MemtableShard shard = shards[i]; + if (!shard.isEmpty()) + return shard.minPartitionKey(); + } + return null; + } + + @Override + public DecoratedKey maxPartitionKey() + { + for (int i = shards.length - 1; i >= 0; i--) + { + MemtableShard shard = shards[i]; + if (!shard.isEmpty()) + return shard.maxPartitionKey(); + } + return null; + } + + @Override + RegularAndStaticColumns columns() + { + for (MemtableShard shard : shards) + columnsCollector.update(shard.columns); + return columnsCollector.get(); + } + + @Override + EncodingStats encodingStats() + { + for (MemtableShard shard : shards) + statsCollector.update(shard.stats); + return statsCollector.get(); + } + + @Override + public MemtableUnfilteredPartitionIterator partitionIterator(final ColumnFilter columnFilter, + final DataRange dataRange, + SSTableReadsListener readsListener) + { + AbstractBounds keyRange = dataRange.keyRange(); + + PartitionPosition left = keyRange.left; + PartitionPosition right = keyRange.right; + if (left.isMinimum()) + left = null; + if (right.isMinimum()) + right = null; + + boolean isBound = keyRange instanceof Bounds; + boolean includeStart = isBound || keyRange instanceof IncludingExcludingBounds; + boolean includeStop = isBound || keyRange instanceof Range; + + Trie subMap = mergedTrie.subtrie(left, includeStart, right, includeStop); + + return new MemtableUnfilteredPartitionIterator(metadata(), + allocator.ensureOnHeap(), + subMap, + columnFilter, + dataRange); + } + + public Partition getPartition(DecoratedKey key) + { + int shardIndex = boundaries.getShardForKey(key); + BTreePartitionData data = shards[shardIndex].data.get(key); + if (data != null) + return createPartition(metadata(), allocator.ensureOnHeap(), key, data); + else + return null; + } + + private static MemtablePartition createPartition(TableMetadata metadata, EnsureOnHeap ensureOnHeap, DecoratedKey key, BTreePartitionData data) + { + return new MemtablePartition(metadata, ensureOnHeap, key, data); + } + + private static MemtablePartition getPartitionFromTrieEntry(TableMetadata metadata, EnsureOnHeap ensureOnHeap, Map.Entry en) + { + DecoratedKey key = BufferDecoratedKey.fromByteComparable(en.getKey(), + BYTE_COMPARABLE_VERSION, + metadata.partitioner); + return createPartition(metadata, ensureOnHeap, key, en.getValue()); + } + + private static DecoratedKey getPartitionKeyFromPath(TableMetadata metadata, ByteComparable path) + { + return BufferDecoratedKey.fromByteComparable(path, BYTE_COMPARABLE_VERSION, metadata.partitioner); + } + + public FlushablePartitionSet getFlushSet(PartitionPosition from, PartitionPosition to) + { + Trie toFlush = mergedTrie.subtrie(from, true, to, false); + long keySize = 0; + int keyCount = 0; + + for (Iterator> it = toFlush.entryIterator(); it.hasNext(); ) + { + Map.Entry en = it.next(); + byte[] keyBytes = DecoratedKey.keyFromByteComparable(en.getKey(), BYTE_COMPARABLE_VERSION, metadata().partitioner); + keySize += keyBytes.length; + keyCount++; + } + long partitionKeySize = keySize; + int partitionCount = keyCount; + + return new AbstractFlushablePartitionSet() + { + public Memtable memtable() + { + return TrieMemtableStage1.this; + } + + public PartitionPosition from() + { + return from; + } + + public PartitionPosition to() + { + return to; + } + + public long partitionCount() + { + return partitionCount; + } + + public Iterator iterator() + { + return Iterators.transform(toFlush.entryIterator(), + // During flushing we are certain the memtable will remain at least until + // the flush completes. No copying to heap is necessary. + entry -> getPartitionFromTrieEntry(metadata(), EnsureOnHeap.NOOP, entry)); + } + + public long partitionKeysSize() + { + return partitionKeySize; + } + }; + } + + static class MemtableShard + { + // The following fields are volatile as we have to make sure that when we + // collect results from all sub-ranges, the thread accessing the value + // is guaranteed to see the changes to the values. + + // The smallest timestamp for all partitions stored in this shard + private volatile long minTimestamp = Long.MAX_VALUE; + + private volatile long liveDataSize = 0; + + private volatile long currentOperations = 0; + + private volatile int partitionCount = 0; + + @Unmetered + private ReentrantLock writeLock = new ReentrantLock(); + + // Content map for the given shard. This is implemented as a memtable trie which uses the prefix-free + // byte-comparable ByteSource representations of the keys to address the partitions. + // + // This map is used in a single-producer, multi-consumer fashion: only one thread will insert items but + // several threads may read from it and iterate over it. Iterators are created when a the first item of + // a flow is requested for example, and then used asynchronously when sub-sequent items are requested. + // + // Therefore, iterators should not throw ConcurrentModificationExceptions if the underlying map is modified + // during iteration, they should provide a weakly consistent view of the map instead. + // + // Also, this data is backed by memtable memory, when accessing it callers must specify if it can be accessed + // unsafely, meaning that the memtable will not be discarded as long as the data is used, or whether the data + // should be copied on heap for off-heap allocators. + @VisibleForTesting + final InMemoryTrie data; + + RegularAndStaticColumns columns; + + EncodingStats stats; + + private final MemtableAllocator allocator; + + @Unmetered + private final TrieMemtableMetricsView metrics; + + private TableMetadataRef metadata; + + MemtableShard(TableMetadataRef metadata, TrieMemtableMetricsView metrics) + { + this(metadata, AbstractAllocatorMemtable.MEMORY_POOL.newAllocator(metadata.toString()), metrics); + } + + @VisibleForTesting + MemtableShard(TableMetadataRef metadata, MemtableAllocator allocator, TrieMemtableMetricsView metrics) + { + this.data = InMemoryTrie.shortLived(BYTE_COMPARABLE_VERSION, TrieMemtable.BUFFER_TYPE); + this.columns = RegularAndStaticColumns.NONE; + this.stats = EncodingStats.NO_STATS; + this.allocator = allocator; + this.metrics = metrics; + this.metadata = metadata; + } + + public long put(DecoratedKey key, PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup) + { + Cloner cloner = allocator.cloner(opGroup); + BTreePartitionUpdater updater = new BTreePartitionUpdater(allocator, cloner, opGroup, indexer); + boolean locked = writeLock.tryLock(); + if (locked) + { + metrics.uncontendedPuts.inc(); + } + else + { + metrics.contendedPuts.inc(); + long lockStartTime = Clock.Global.nanoTime(); + writeLock.lock(); + metrics.contentionTime.addNano(Clock.Global.nanoTime() - lockStartTime); + } + try + { + try + { + // Add the initial trie size on the first operation. This technically isn't correct (other shards + // do take their memory share even if they are empty) but doing it during construction may cause + // the allocator to block while we are trying to flush a memtable and become a deadlock. + long onHeap = data.isEmpty() ? 0 : data.usedSizeOnHeap(); + long offHeap = data.isEmpty() ? 0 : data.usedSizeOffHeap(); + // Use the fast recursive put if we know the key is small enough to not cause a stack overflow. + try + { + data.putSingleton(key, + BTreePartitionUpdate.asBTreeUpdate(update), + updater::mergePartitions, + key.getKeyLength() < MAX_RECURSIVE_KEY_LENGTH); + } + catch (TrieSpaceExhaustedException e) + { + // This should never really happen as a flush would be triggered long before this limit is reached. + throw Throwables.propagate(e); + } + allocator.offHeap().adjust(data.usedSizeOffHeap() - offHeap, opGroup); + allocator.onHeap().adjust(data.usedSizeOnHeap() - onHeap, opGroup); + partitionCount += updater.partitionsAdded; + } + finally + { + updateMinTimestamp(update.stats().minTimestamp); + updateLiveDataSize(updater.dataSize); + updateCurrentOperations(update.operationCount()); + + columns = columns.mergeTo(update.columns()); + stats = stats.mergeWith(update.stats()); + } + } + finally + { + writeLock.unlock(); + } + return updater.colUpdateTimeDelta; + } + + public boolean isEmpty() + { + return data.isEmpty(); + } + + private void updateMinTimestamp(long timestamp) + { + if (timestamp < minTimestamp) + minTimestamp = timestamp; + } + + void updateLiveDataSize(long size) + { + liveDataSize = liveDataSize + size; + } + + private void updateCurrentOperations(long op) + { + currentOperations = currentOperations + op; + } + + public int partitionCount() + { + return partitionCount; + } + + long liveDataSize() + { + return liveDataSize; + } + + long currentOperations() + { + return currentOperations; + } + + private DecoratedKey firstPartitionKey(Direction direction) + { + Iterator> iter = data.entryIterator(direction); + if (!iter.hasNext()) + return null; + + Map.Entry entry = iter.next(); + return getPartitionKeyFromPath(metadata.get(), entry.getKey()); + } + + public DecoratedKey minPartitionKey() + { + return firstPartitionKey(Direction.FORWARD); + } + + public DecoratedKey maxPartitionKey() + { + return firstPartitionKey(Direction.REVERSE); + } + } + + static class MemtableUnfilteredPartitionIterator extends AbstractUnfilteredPartitionIterator implements Memtable.MemtableUnfilteredPartitionIterator + { + private final TableMetadata metadata; + private final EnsureOnHeap ensureOnHeap; + private final Trie source; + private final Iterator> iter; + private final ColumnFilter columnFilter; + private final DataRange dataRange; + + public MemtableUnfilteredPartitionIterator(TableMetadata metadata, + EnsureOnHeap ensureOnHeap, + Trie source, + ColumnFilter columnFilter, + DataRange dataRange) + { + this.metadata = metadata; + this.ensureOnHeap = ensureOnHeap; + this.iter = source.entryIterator(); + this.source = source; + this.columnFilter = columnFilter; + this.dataRange = dataRange; + } + + public long getMinLocalDeletionTime() + { + long minLocalDeletionTime = Long.MAX_VALUE; + for (BTreePartitionData partition : source.values()) + minLocalDeletionTime = EncodingStats.mergeMinLocalDeletionTime(minLocalDeletionTime, partition.stats); + + return minLocalDeletionTime; + } + + public TableMetadata metadata() + { + return metadata; + } + + public boolean hasNext() + { + return iter.hasNext(); + } + + public UnfilteredRowIterator next() + { + Partition partition = getPartitionFromTrieEntry(metadata(), ensureOnHeap, iter.next()); + DecoratedKey key = partition.partitionKey(); + ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(key); + + return filter.getUnfilteredRowIterator(columnFilter, partition); + } + } + + static class MemtablePartition extends ImmutableBTreePartition + { + + private final EnsureOnHeap ensureOnHeap; + + private MemtablePartition(TableMetadata table, EnsureOnHeap ensureOnHeap, DecoratedKey key, BTreePartitionData data) + { + super(table, key, data); + this.ensureOnHeap = ensureOnHeap; + } + + @Override + protected boolean canHaveShadowedData() + { + // The BtreePartitionData we store in the memtable are build iteratively by BTreePartitionData.add(), which + // doesn't make sure there isn't shadowed data, so we'll need to eliminate any. + return true; + } + + + @Override + public DeletionInfo deletionInfo() + { + return ensureOnHeap.applyToDeletionInfo(super.deletionInfo()); + } + + @Override + public Row staticRow() + { + return ensureOnHeap.applyToStatic(super.staticRow()); + } + + @Override + public DecoratedKey partitionKey() + { + return ensureOnHeap.applyToPartitionKey(super.partitionKey()); + } + + @Override + public Row getRow(Clustering clustering) + { + return ensureOnHeap.applyToRow(super.getRow(clustering)); + } + + @Override + public Row lastRow() + { + return ensureOnHeap.applyToRow(super.lastRow()); + } + + @Override + public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, Slices slices, boolean reversed) + { + return unfilteredIterator(holder(), selection, slices, reversed); + } + + @Override + public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, NavigableSet> clusteringsInQueryOrder, boolean reversed) + { + return ensureOnHeap + .applyToPartition(super.unfilteredIterator(selection, clusteringsInQueryOrder, reversed)); + } + + @Override + public UnfilteredRowIterator unfilteredIterator() + { + return unfilteredIterator(ColumnFilter.selection(super.columns()), Slices.ALL, false); + } + + @Override + public UnfilteredRowIterator unfilteredIterator(BTreePartitionData current, ColumnFilter selection, Slices slices, boolean reversed) + { + return ensureOnHeap + .applyToPartition(super.unfilteredIterator(current, selection, slices, reversed)); + } + + @Override + public Iterator rowIterator() + { + return ensureOnHeap.applyToPartition(super.rowIterator()); + } + } + + static class Factory implements Memtable.Factory + { + public Memtable create(AtomicReference commitLogLowerBound, + TableMetadataRef metadaRef, + Owner owner) + { + return new TrieMemtableStage1(commitLogLowerBound, metadaRef, owner); + } + + @Override + public TableMetrics.ReleasableMetric createMemtableMetrics(TableMetadataRef metadataRef) + { + TrieMemtableMetricsView metrics = TrieMemtableMetricsView.getOrCreate(metadataRef.keyspace, metadataRef.name); + return metrics::release; + } + } + + @Override + @VisibleForTesting + public long unusedReservedOnHeapMemory() + { + long size = 0; + for (MemtableShard shard : shards) + { + size += shard.data.unusedReservedOnHeapMemory(); + size += shard.allocator.unusedReservedOnHeapMemory(); + } + return size; + } +} diff --git a/src/java/org/apache/cassandra/db/monitoring/Monitorable.java b/src/java/org/apache/cassandra/db/monitoring/Monitorable.java index 10bd10438aa5..2f14b82d21c7 100644 --- a/src/java/org/apache/cassandra/db/monitoring/Monitorable.java +++ b/src/java/org/apache/cassandra/db/monitoring/Monitorable.java @@ -18,6 +18,8 @@ package org.apache.cassandra.db.monitoring; +import java.util.function.Supplier; + public interface Monitorable { String name(); @@ -33,4 +35,44 @@ public interface Monitorable boolean abort(); boolean complete(); + + /** + * Returns the specific {@link ExecutionInfo} for this monitorable operation. + * + * @return the execution info for this operation + */ + default ExecutionInfo executionInfo() + { + return ExecutionInfo.EMPTY; + } + + /** + * Specific execution details for a monitorable operation. + *

    + * {@link Monitorable} implementations should use this interface to hold and provide additional information about + * the execution of the operation. This information will be logged when the operation is reported as slow. + */ + interface ExecutionInfo + { + String INDENT = " "; + String DOUBLE_INDENT = INDENT + INDENT; + + /** + * An empty no-op implementation. + */ + ExecutionInfo EMPTY = unique -> ""; + + /** + * A supplier for the empty implementation. + */ + Supplier EMPTY_SUPPLIER = () -> EMPTY; + + /** + * Returns a string representation of this execution info, suitable for logging. + * + * @param unique whether the execution info is for a single operation or an aggregation of operations + * @return a log-suitable string representation of this execution info + */ + String toLogString(boolean unique); + } } diff --git a/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java b/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java index 31b54043c834..28438acd7548 100644 --- a/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java +++ b/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java @@ -18,6 +18,8 @@ package org.apache.cassandra.db.monitoring; +import org.apache.cassandra.index.sai.QueryContext; + import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; public abstract class MonitorableImpl implements Monitorable @@ -123,6 +125,9 @@ public boolean complete() private void check() { + if (QueryContext.DISABLE_TIMEOUT) + return; + if (approxCreationTimeNanos < 0 || state != MonitoringState.IN_PROGRESS) return; diff --git a/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java b/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java index 243569910b8a..7d3e6b341d81 100644 --- a/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java +++ b/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java @@ -47,7 +47,8 @@ * We also log timed out operations, see CASSANDRA-7392. * Since CASSANDRA-12403 we also log queries that were slow. */ -class MonitoringTask +@VisibleForTesting +public class MonitoringTask { private static final String LINE_SEPARATOR = CassandraRelevantProperties.LINE_SEPARATOR.getString(); private static final Logger logger = LoggerFactory.getLogger(MonitoringTask.class); @@ -65,14 +66,13 @@ class MonitoringTask private static final int MAX_OPERATIONS = MONITORING_MAX_OPERATIONS.getInt(); @VisibleForTesting - static MonitoringTask instance = make(REPORT_INTERVAL_MS, MAX_OPERATIONS); + public static MonitoringTask instance = make(REPORT_INTERVAL_MS, MAX_OPERATIONS); private final ScheduledFuture reportingTask; private final OperationsQueue failedOperationsQueue; private final OperationsQueue slowOperationsQueue; private long approxLastLogTimeNanos; - @VisibleForTesting static MonitoringTask make(int reportIntervalMillis, int maxTimedoutOperations) { @@ -133,7 +133,7 @@ private List getLogMessages(AggregatedOperations operations) } @VisibleForTesting - private void logOperations(long approxCurrentTimeNanos) + public void logOperations(long approxCurrentTimeNanos) { logSlowOperations(approxCurrentTimeNanos); logFailedOperations(approxCurrentTimeNanos); @@ -328,13 +328,17 @@ protected abstract static class Operation * this is set lazily as it takes time to build the query CQL */ private String name; - Operation(Monitorable operation, long failedAtNanos) + /** Any specific execution info of the slowest operation among the aggregated operations. */ + protected Monitorable.ExecutionInfo slowestOperationExecutionInfo; + + Operation(Monitorable operation, long nowNanos) { this.operation = operation; numTimesReported = 1; - totalTimeNanos = failedAtNanos - operation.creationTimeNanos(); + totalTimeNanos = nowNanos - operation.creationTimeNanos(); minTime = totalTimeNanos; maxTime = totalTimeNanos; + slowestOperationExecutionInfo = operation.executionInfo(); } public String name() @@ -344,15 +348,24 @@ public String name() return name; } - void add(Operation operation) + private void add(Operation operation) { numTimesReported++; totalTimeNanos += operation.totalTimeNanos; + + if (operation.maxTime > maxTime) + slowestOperationExecutionInfo = operation.executionInfo(); + maxTime = Math.max(maxTime, operation.maxTime); minTime = Math.min(minTime, operation.minTime); } public abstract String getLogMessage(); + + protected Monitorable.ExecutionInfo executionInfo() + { + return slowestOperationExecutionInfo; + } } /** @@ -368,50 +381,56 @@ private final static class FailedOperation extends Operation public String getLogMessage() { if (numTimesReported == 1) - return String.format("<%s>, total time %d msec, timeout %d %s", + return String.format("<%s>, total time %d msec, timeout %d %s%s", name(), NANOSECONDS.toMillis(totalTimeNanos), NANOSECONDS.toMillis(operation.timeoutNanos()), - operation.isCrossNode() ? "msec/cross-node" : "msec"); + operation.isCrossNode() ? "msec/cross-node" : "msec", + slowestOperationExecutionInfo.toLogString(true)); else - return String.format("<%s> timed out %d times, avg/min/max %d/%d/%d msec, timeout %d %s", + return String.format("<%s> timed out %d times, avg/min/max %d/%d/%d msec, timeout %d %s%s", name(), numTimesReported, NANOSECONDS.toMillis(totalTimeNanos / numTimesReported), NANOSECONDS.toMillis(minTime), NANOSECONDS.toMillis(maxTime), NANOSECONDS.toMillis(operation.timeoutNanos()), - operation.isCrossNode() ? "msec/cross-node" : "msec"); + operation.isCrossNode() ? "msec/cross-node" : "msec", + slowestOperationExecutionInfo.toLogString(false)); } } /** * An operation (query) that was reported as slow. */ - private final static class SlowOperation extends Operation + @VisibleForTesting + public final static class SlowOperation extends Operation { - SlowOperation(Monitorable operation, long failedAt) + @VisibleForTesting + public SlowOperation(Monitorable operation, long slowAtNanos) { - super(operation, failedAt); + super(operation, slowAtNanos); } public String getLogMessage() { if (numTimesReported == 1) - return String.format("<%s>, time %d msec - slow timeout %d %s", + return String.format("<%s>, time %d msec - slow timeout %d %s%s", name(), NANOSECONDS.toMillis(totalTimeNanos), NANOSECONDS.toMillis(operation.slowTimeoutNanos()), - operation.isCrossNode() ? "msec/cross-node" : "msec"); + operation.isCrossNode() ? "msec/cross-node" : "msec", + slowestOperationExecutionInfo.toLogString(true)); else - return String.format("<%s>, was slow %d times: avg/min/max %d/%d/%d msec - slow timeout %d %s", + return String.format("<%s>, was slow %d times: avg/min/max %d/%d/%d msec - slow timeout %d %s%s", name(), numTimesReported, NANOSECONDS.toMillis(totalTimeNanos/ numTimesReported), NANOSECONDS.toMillis(minTime), NANOSECONDS.toMillis(maxTime), NANOSECONDS.toMillis(operation.slowTimeoutNanos()), - operation.isCrossNode() ? "msec/cross-node" : "msec"); + operation.isCrossNode() ? "msec/cross-node" : "msec", + slowestOperationExecutionInfo.toLogString(false)); } } } diff --git a/src/java/org/apache/cassandra/db/monitoring/SlowQueryLogger.md b/src/java/org/apache/cassandra/db/monitoring/SlowQueryLogger.md new file mode 100644 index 000000000000..6dd38ca4c5b1 --- /dev/null +++ b/src/java/org/apache/cassandra/db/monitoring/SlowQueryLogger.md @@ -0,0 +1,273 @@ + + +# Slow Query Logger + +Slow query logger is a feature that tracks the slowest `SELECT` queries and periodically emits them in logs. +It emits separate log reports for slow but successful queries and queries aborted due to slowness. +It operates on a per-node basis, when the read commands are executed on the replicas. + +The slow queries are logged in their CQL representation, with all filtering column values redacted. +Queries of the same form will be aggregated within the report window, +showing only the number of times the query has been slow and the metrics for the slowest execution. + +The feature is meant to help operators identify abnormally slow queries that might need some kind of improvement. +The logged reports include some metrics about the queries to help operators identify what's wrong with the query. + +## Configuration + +Queries are considered slow if their running time exceeds the `slow_query_log_timeout_in_ms` config property, +defined in `cassandra.yaml`. It defaults to 500 milliseconds. It can not be changed dynamically. + +Also, there are the following JVM properties: +* `-Dcassandra.monitoring_report_interval_ms`: The interval for reporting any operations that have timed out. + That is, the frequency at which the log reports are emited. It defaults to 5000 milliseconds. + It can not be changed dynamically. +* `-Dcassandra.monitoring_max_operations`: + The maximum number of slowest unique queries that will be tracked and reported in each log report. + There are separate counters for slow but successful and aborted queries. + A query hit multiple times during the interval between reports will only produce an entry in the log report. + Due to value redaction, queries of the same form but with different values will produce a single entry. + This single entry will show the number of times the query has been slow and the metrics for the slowest execution. + A negative value means no limit. It defaults to 50 queries per each of the slow and aborted queries queues. + It can not be changed dynamically. +* `-Dcassandra.monitoring_execution_info_enabled`: + Whether to log detailed execution info when logging slow or aborted non-SAI queries. + If this is `false`, only a CQL approximate representation of the query, + the number of hits and metrics about the running time will be printed, taking less space in logs. + If the property is `true`, the reports will also include metrics about the number of fetched and returned partitions, + rows and tombstones, taking more space in logs. Defaults to true. + It can be changed dynamically. +* `-Dcassandra.sai.monitoring_execution_info_enabled`: + Whether to log detailed execution info when logging slow or aborted SAI queries. + If this is `false`, only a CQL approximate representation of the query, + the number of hits and metrics about the running time will be printed, taking less space in logs. + If the property is `true`, the reports will also include internal index metrics, taking more space in logs. + Those metrics include the number of fetched and returned index keys partitions, rows and tombstones, + index segments hits, ANN latency, the index query plan, planner metrics, etc. + It can be changed dynamically. + +## Representing internal commands as CQL queries + +The slow query logger monitors the replica-side internal commands in which a user-provided CQL query is translated to. +Once identified, it prints a CQL representation of those internal commands. +Unfortunately, it's not always possible to produce a CQL translation of the commands +that is identical to the user-provided CQL query that produced those commands. +There are some caveats with the reverse translation that should be taken into account when trying to figure out +what user CQL query produced a slow query logger entry when allow filtering or paging are used. + +### Allow filtering + +The replicas don't know if the original query used `ALLOW FILTERING`. +The slow queries are arbitrarily printed with `ALLOW FILTERING`, +but it doesn't mean that the original query used it. +Thus, the presence of `ALLOW FILTERING` in the slow query log reports should be ignored. + +However, the detailed execution info for those queries can be used to achieve a similar purpose +by comparing the number of rows fetched to the number of rows returned. +If there is a difference, it's likely that `ALLOW FILTERING` was used. + +### Paging + +User queries can use paging, which splits the original CQL into multiple commands with a smaller `LIMIT`. +Paging also adds restrictions about the last seen primary key. +Thus, seeing a `LIMIT` on a slow query report doesn't mean that the original query had that same `LIMIT`. +Restrictions regarding the primary key on the `WHERE` clause can indicate paging too. + +A good hint to identify paging is that the `LIMIT` shown in the log reports will match the paging fetch size, +which in the case of the Java driver is 5000 rows by default. +Also, if the slow query matches more that these rows and has to request multiple pages, +we would see versions of the same query with and without primary key restrictions. + +## Execution info for non-SAI queries + +The log reports for non-SAI `SELECT` queries include execution info details +when `-Dcassandra.monitoring_execution_info_enabled` is `true`. +The execution details can be used to help diagnose why the query is slow. + +Enabling slow query execution details will produce larger log entries because of the addition information provided. +The log messages for slow queries can take up around 60% extra (uncompressed) disk space. +In the worst of the worst cases, with all queries being permanently slower than 500ms, +and at least 50 different queries, reporting every 5 seconds (the default), +the slow query logger can produce around 100MB of text per day and writer, depending on the queries. W +ith the detailed logging, it would be 160MiB per day. +That is for tiniest queries, for queries with more predicates, longer column names, +and long keyspace names the difference would be smaller. + +These slow query execution details consist on the following metrics: + +* Number of fetched partitions, before applying filtering. +* Number of returned partitions, after applying filtering. + If it is lower than the number of fetched partitions it means that filtering is used. + Much filtering has a high performance cost and maybe better modeling or indexing can help. +* Number of found partition tombstones (partition deletions). + Many tombstones can have a significant performance cost. +* Number of fetched rows, before applying filtering. + If there are many rows per partitions it means that there are large partitions. + Partitions over a few tens of thousands rows are generally problematic for performance, + and better modelling should be considered. +* Number of returned rows, after applying filtering. + If it is lower than the number of fetched rows it means that filtering is used. + Much filtering has a high performance cost and maybe better modeling or indexing can help. +* Number of found row tombstones (row deletions). + It counts both individual row tombstones and range tombstones as a single tombstone, + even if a range tombstone can mean the deletion of many rows. + Many tombstones can have a significant performance cost. + +Here is an example of how the execution details look like: +``` +1 operations were slow in the last 5001 msecs: +, time 507 msec - slow timeout 500 msec/cross-node + Fetched/returned/tombstones: + partitions: 1/1/0 + rows: 123456/10/0 +``` +It shows the same CQL single-partition query as before. +This time there are no tombstones. +However, it has fetched 123456 rows but returned only 10, indicating that it actively used `ALLOW FILTERING`, +which might be the cause of it being slow. + +If multiple queries of the same form are slow during the reporting window defined by +`-Dcassandra.monitoring_report_interval_ms`, they will be aggregated +showing only the number of times the query has been slow and the metrics for the slowest execution. +Here is an example: +``` + 1 operations were slow in the last 5001 msecs: += ? AND v1 = ? AND v2 = ? LIMIT 5000 ALLOW FILTERING>, time 1109 msec - slow timeout 500 msec/cross-node + SAI slow query metrics: + sstablesHit: 8 + segmentsHit: 8 + keysFetched: 1 + partitionsFetched: 1 + partitionsReturned: 1 + partitionTombstonesFetched: 0 + rowsFetched: 222053 + rowsReturned: 2 + rowTombstonesFetched: 0 + trieSegmentsHit: 2 + triePostingsSkips: 0 + triePostingsDecodes: 2 + bkdPostingListsHit: 6 + bkdSegmentsHit: 6 + bkdPostingsSkips: 0 + bkdPostingsDecodes: 1342 + annGraphSearchLatencyNanos: 0 + SAI slow query plan: + Limit 5000 (rows: 0.0, cost/row: 15743601.7, cost: 112585.6..112799.5) + └─ Filter c >= ? AND c < ? AND v1 = ? AND v2 = ? (sel: 0.000006793) (rows: 0.0, cost/row: 15743601.7, cost: 112585.6..112799.5) + └─ Fetch (rows: 2.0, cost/row: 106.9, cost: 112585.6..112799.5) + └─ LiteralIndexScan of v2_index (sel: 0.000000004, step: 1.0) (keys: 2.0, cost/key: 0.1, cost: 112585.6..112585.8) + predicate: Expression{name: v2, op: EQ, lower: (?, true), upper: (?, true), exclusions: []} +``` +This example is showing a single-partition query. We know this because the table schema contains `PRIMARY KEY(k, c)`. +The number of fetched and returned partitions is 1, as one would expect from a single-partition query. +There are no tombstones, so that's not likely a problem. + +The number of fetched rows, 222053, is much higher than the number of fetched partitions, 1. +This means that the query has found a probably too large partitions, which contributes to performance issues. +If queries are unacceptably slow, we should probably try to remodel to use smaller partitions. + +The number of fetched rows, 222053, is also much higher than the number of returned rows, 2. +This means that a lot of filtering is being done in that partition, and that's likely a cause of the query slowness. +If we look at the query plan, we see that only an index on the restriction on `v2` non-primary key column is used. +There is however another query restriction on `v1` non-primary key column. +That could be what is causing the query slowness. We should consider adding an index on that column. + +More importantly, the numbers of index row keys fetched is 1, whereas the number of fetched rows is 222053. +This means that the index is using the `aa` index format, which only indexes partition keys. +The index uses that partition key to retrieve the large partition and filters almost everything from it. +This combined with the large partition is very inefficient, and it is the most likely cause of the performance issue. +We should either consider using a post-`aa` row-aware index format, +or try to remodel to reduce the size of the partitions. + +On a final note, the query indicates `LIMIT 5000`. Since 5000 is the default fetch size of the Java driver client, +it is possible that the query didn't have a limit and that limit has been added by paging. That's indistinguishable +from the original user query really using a limit equals or greater than 5000. The `ALLOW FILTERING` cause is +meaningless because queries are always printed with it. diff --git a/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java b/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java index 33272375733f..4b390bd9c158 100644 --- a/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java +++ b/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java @@ -33,7 +33,7 @@ import static org.apache.cassandra.utils.btree.BTree.Dir.desc; -public abstract class AbstractBTreePartition implements Partition, Iterable +public abstract class AbstractBTreePartition implements Partition { protected final DecoratedKey partitionKey; @@ -352,50 +352,22 @@ protected static BTreePartitionData build(RowIterator rows, DeletionInfo deletio @Override public String toString() { - return toString(true); - } - - public String toString(boolean includeFullDetails) - { - StringBuilder sb = new StringBuilder(); - if (includeFullDetails) - { - sb.append(String.format("[%s.%s] key=%s partition_deletion=%s columns=%s", - metadata().keyspace, - metadata().name, - metadata().partitionKeyType.getString(partitionKey().getKey()), - partitionLevelDeletion(), - columns())); - } - else - { - sb.append("key=").append(metadata().partitionKeyType.getString(partitionKey().getKey())); - } - - if (staticRow() != Rows.EMPTY_STATIC_ROW) - sb.append("\n ").append(staticRow().toString(metadata(), includeFullDetails)); - - try (UnfilteredRowIterator iter = unfilteredIterator()) - { - while (iter.hasNext()) - sb.append("\n ").append(iter.next().toString(metadata(), includeFullDetails)); - } - return sb.toString(); + return Partition.toString(this); } @Override public boolean equals(Object obj) { - if (!(obj instanceof PartitionUpdate)) + if (!(obj instanceof BTreePartitionUpdate)) return false; - PartitionUpdate that = (PartitionUpdate) obj; + BTreePartitionUpdate that = (BTreePartitionUpdate) obj; BTreePartitionData a = this.holder(), b = that.holder(); return partitionKey.equals(that.partitionKey) && metadata().id.equals(that.metadata().id) && a.deletionInfo.equals(b.deletionInfo) && a.staticRow.equals(b.staticRow) - && Iterators.elementsEqual(iterator(), that.iterator()); + && Iterators.elementsEqual(rowIterator(), that.rowIterator()); } public int rowCount() @@ -403,7 +375,7 @@ public int rowCount() return BTree.size(holder().tree); } - public Iterator iterator() + public Iterator rowIterator() { return BTree.iterator(holder().tree); } diff --git a/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java b/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java index c9035befbde5..5ba04184abc5 100644 --- a/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java +++ b/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java @@ -19,6 +19,7 @@ import java.nio.ByteBuffer; import java.util.Iterator; +import java.util.NavigableSet; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -117,7 +118,10 @@ protected boolean canHaveShadowedData() * @return an array containing first the difference in size seen after merging the updates, and second the minimum * time delta between updates. */ - public BTreePartitionUpdater addAll(final PartitionUpdate update, Cloner cloner, OpOrder.Group writeOp, UpdateTransaction indexer) + public BTreePartitionUpdater addAll(final BTreePartitionUpdate update, + Cloner cloner, + OpOrder.Group writeOp, + UpdateTransaction indexer) { return new Updater(allocator, cloner, writeOp, indexer).addAll(update); } @@ -143,7 +147,7 @@ public Updater(MemtableAllocator allocator, Cloner cloner, OpOrder.Group writeOp super(allocator, cloner, writeOp, indexer); } - Updater addAll(final PartitionUpdate update) + Updater addAll(final BTreePartitionUpdate update) { try { @@ -176,7 +180,7 @@ Updater addAll(final PartitionUpdate update) } } - private boolean tryUpdateData(PartitionUpdate update) + private boolean tryUpdateData(BTreePartitionUpdate update) { current = ref; this.dataSize = 0; @@ -216,6 +220,24 @@ public Row lastRow() return allocator.ensureOnHeap().applyToRow(super.lastRow()); } + @Override + public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, Slices slices, boolean reversed) + { + return allocator.ensureOnHeap().applyToPartition(super.unfilteredIterator(selection, slices, reversed)); + } + + @Override + public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, NavigableSet> clusteringsInQueryOrder, boolean reversed) + { + return allocator.ensureOnHeap().applyToPartition(super.unfilteredIterator(selection, clusteringsInQueryOrder, reversed)); + } + + @Override + public UnfilteredRowIterator unfilteredIterator() + { + return allocator.ensureOnHeap().applyToPartition(super.unfilteredIterator()); + } + @Override public UnfilteredRowIterator unfilteredIterator(BTreePartitionData current, ColumnFilter selection, Slices slices, boolean reversed) { @@ -223,9 +245,9 @@ public UnfilteredRowIterator unfilteredIterator(BTreePartitionData current, Colu } @Override - public Iterator iterator() + public Iterator rowIterator() { - return allocator.ensureOnHeap().applyToPartition(super.iterator()); + return allocator.ensureOnHeap().applyToPartition(super.rowIterator()); } private boolean shouldLock(OpOrder.Group writeOp) diff --git a/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdate.java new file mode 100644 index 000000000000..7dedd2d66544 --- /dev/null +++ b/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdate.java @@ -0,0 +1,630 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.partitions; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.primitives.Ints; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.Columns; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.MutableDeletionInfo; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; +import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.btree.BTree; +import org.apache.cassandra.utils.btree.UpdateFunction; + +/** + * Implementation of PartitionUpdate using a BTree of rows. + */ +public class BTreePartitionUpdate extends AbstractBTreePartition implements PartitionUpdate +{ + protected static final Logger logger = LoggerFactory.getLogger(BTreePartitionUpdate.class); + + public static final BTreeFactory FACTORY = new BTreeFactory(); + + private final BTreePartitionData holder; + private final DeletionInfo deletionInfo; + private final TableMetadata metadata; + + private final boolean canHaveShadowedData; + + private BTreePartitionUpdate(TableMetadata metadata, + DecoratedKey key, + BTreePartitionData holder, + MutableDeletionInfo deletionInfo, + boolean canHaveShadowedData) + { + super(key); + this.metadata = metadata; + this.holder = holder; + this.deletionInfo = deletionInfo; + this.canHaveShadowedData = canHaveShadowedData; + } + + /** + * Creates a empty immutable partition update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the created update. + * + * @return the newly created empty (and immutable) update. + */ + public static BTreePartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey key) + { + MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); + BTreePartitionData holder = new BTreePartitionData(RegularAndStaticColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS); + return new BTreePartitionUpdate(metadata, key, holder, deletionInfo, false); + } + + /** + * Creates an immutable partition update that entirely deletes a given partition. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition that the created update should delete. + * @param timestamp the timestamp for the deletion. + * @param nowInSec the current time in seconds to use as local deletion time for the partition deletion. + * + * @return the newly created partition deletion update. + */ + public static BTreePartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec) + { + MutableDeletionInfo deletionInfo = new MutableDeletionInfo(timestamp, nowInSec); + BTreePartitionData holder = new BTreePartitionData(RegularAndStaticColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS); + return new BTreePartitionUpdate(metadata, key, holder, deletionInfo, false); + } + + /** + * Creates an immutable partition update that contains a single row update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition to update. + * @param row the row for the update (may be null). + * @param row the static row for the update (may be null). + * + * @return the newly created partition update containing only {@code row}. + */ + public static BTreePartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey key, Row row, Row staticRow) + { + MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); + BTreePartitionData holder = new BTreePartitionData( + new RegularAndStaticColumns( + staticRow == null ? Columns.NONE : Columns.from(staticRow), + row == null ? Columns.NONE : Columns.from(row) + ), + row == null ? BTree.empty() : BTree.singleton(row), + deletionInfo, + staticRow == null ? Rows.EMPTY_STATIC_ROW : staticRow, + EncodingStats.NO_STATS + ); + return new BTreePartitionUpdate(metadata, key, holder, deletionInfo, false); + } + + /** + * Creates an immutable partition update that contains a single row update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition to update. + * @param row the row for the update (may be static). + * + * @return the newly created partition update containing only {@code row}. + */ + public static BTreePartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey key, Row row) + { + return singleRowUpdate(metadata, key, row.isStatic() ? null : row, row.isStatic() ? row : null); + } + + /** + * Creates an immutable partition update that contains a single row update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition to update. + * @param row the row for the update. + * + * @return the newly created partition update containing only {@code row}. + */ + public static BTreePartitionUpdate singleRowUpdate(TableMetadata metadata, ByteBuffer key, Row row) + { + return singleRowUpdate(metadata, metadata.partitioner.decorateKey(key), row); + } + + @Override + public PartitionUpdate withOnlyPresentColumns() + { + Set columnSet = new HashSet<>(); + + for (Row row : rows()) + for (ColumnData column : row) + columnSet.add(column.column()); + + RegularAndStaticColumns columns = RegularAndStaticColumns.builder().addAll(columnSet).build(); + return new BTreePartitionUpdate(metadata, partitionKey, holder.withColumns(columns), deletionInfo.mutableCopy(), false); + } + + /** + * Turns the given iterator into an update. + * + * @param iterator the iterator to turn into updates. + * + * Warning: this method does not close the provided iterator, it is up to + * the caller to close it. + */ + @SuppressWarnings("resource") + public static BTreePartitionUpdate fromIterator(UnfilteredRowIterator iterator) + { + BTreePartitionData holder = build(iterator, 16); + MutableDeletionInfo deletionInfo = (MutableDeletionInfo) holder.deletionInfo; + return new BTreePartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false); + } + + /** + * Turns the given iterator into an update. + * + * @param iterator the iterator to turn into updates. + * @param filter the column filter used when querying {@code iterator}. This is used to make + * sure we don't include data for which the value has been skipped while reading (as we would + * then be writing something incorrect). + * + * Warning: this method does not close the provided iterator, it is up to + * the caller to close it. + */ + @SuppressWarnings("resource") + public static BTreePartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter) + { + return fromIterator(UnfilteredRowIterators.withOnlyQueriedData(iterator, filter)); + } + + protected boolean canHaveShadowedData() + { + return canHaveShadowedData; + } + + /** + * Creates a partition update that entirely deletes a given partition. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition that the created update should delete. + * @param timestamp the timestamp for the deletion. + * @param nowInSec the current time in seconds to use as local deletion time for the partition deletion. + * + * @return the newly created partition deletion update. + */ + public static BTreePartitionUpdate fullPartitionDelete(TableMetadata metadata, ByteBuffer key, long timestamp, int nowInSec) + { + return fullPartitionDelete(metadata, metadata.partitioner.decorateKey(key), timestamp, nowInSec); + } + + public static BTreePartitionUpdate asBTreeUpdate(PartitionUpdate update) + { + if (update instanceof BTreePartitionUpdate) + return (BTreePartitionUpdate) update; + + try (UnfilteredRowIterator iterator = update.unfilteredIterator()) + { + return fromIterator(iterator); + } + } + + // We override this, because the version in the super-class calls holder(), which build the update preventing + // further updates, but that's not necessary here and being able to check at least the partition deletion without + // "locking" the update is nice (and used in DataResolver.RepairMergeListener.MergeListener). + @Override + public DeletionInfo deletionInfo() + { + return deletionInfo; + } + + /** + * The number of "operations" contained in the update. + *

    + * This is used by {@code Memtable} to approximate how much work this update does. In practice, this + * count how many rows are updated and how many ranges are deleted by the partition update. + * + * @return the number of "operations" performed by the update. + */ + @Override + public int operationCount() + { + return rowCount() + + (staticRow().isEmpty() ? 0 : 1) + + deletionInfo.rangeCount() + + (deletionInfo.getPartitionDeletion().isLive() ? 0 : 1); + } + + /** + * The size of the data contained in this update. + * + * @return the size of the data contained in this update. + */ + @Override + public int dataSize() + { + return Ints.saturatedCast(BTree.accumulate(holder.tree, (row, value) -> row.dataSize() + value, 0L) + + holder.staticRow.dataSize()); + } + + /** + * The size of the data contained in this update. + * + * @return the size of the data contained in this update. + */ + @Override + public long unsharedHeapSize() + { + return BTree.accumulate(holder.tree, (row, value) -> row.unsharedHeapSize() + value, 0L) + + holder.staticRow.unsharedHeapSize(); + } + + @Override + public TableMetadata metadata() + { + return metadata; + } + + @Override + public RegularAndStaticColumns columns() + { + // The superclass implementation calls holder(), but that triggers a build of the PartitionUpdate. But since + // the columns are passed to the ctor, we know the holder always has the proper columns even if it doesn't have + // the built rows yet, so just bypass the holder() method. + return holder.columns; + } + + protected BTreePartitionData holder() + { + return holder; + } + + @Override + public EncodingStats stats() + { + return holder().stats; + } + + /** + * The maximum timestamp used in this update. + * + * @return the maximum timestamp used in this update. + */ + @Override + public long maxTimestamp() + { + long maxTimestamp = deletionInfo.maxTimestamp(); + for (Row row : rows()) + maxTimestamp = Math.max(maxTimestamp, Rows.collectMaxTimestamp(row)); + + if (this.holder.staticRow != null) + maxTimestamp = Math.max(maxTimestamp, Rows.collectMaxTimestamp(this.holder.staticRow)); + + return maxTimestamp; + } + + /** + * For an update on a counter table, returns a list containing a {@code CounterMark} for + * every counter contained in the update. + * + * @return a list with counter marks for every counter in this update. + */ + @Override + public List collectCounterMarks() + { + assert metadata().isCounter(); + // We will take aliases on the rows of this update, and update them in-place. So we should be sure the + // update is now immutable for all intent and purposes. + List marks = new ArrayList<>(); + addMarksForRow(staticRow(), marks); + for (Row row : rows()) + addMarksForRow(row, marks); + return marks; + } + + private static void addMarksForRow(Row row, List marks) + { + for (Cell cell : row.cells()) + { + if (cell.isCounterCell()) + marks.add(new CounterMark(row, cell.column(), cell.path())); + } + } + + @Override + public void validateIndexedColumns(ClientState state) + { + IndexRegistry.obtain(metadata()).validate(this, state); + } + + @VisibleForTesting + public static BTreePartitionUpdate unsafeConstruct(TableMetadata metadata, + DecoratedKey key, + BTreePartitionData holder, + MutableDeletionInfo deletionInfo, + boolean canHaveShadowedData) + { + return new BTreePartitionUpdate(metadata, key, holder, deletionInfo, canHaveShadowedData); + } + + @Override + public BTreePartitionUpdate withUpdatedTimestamps(long timestamp) + { + return new Builder(this, rowCount()).updateAllTimestamp(timestamp).build(); + } + + + /** + * Builder for PartitionUpdates + * + * This class is not thread safe, but the PartitionUpdate it produces is (since it is immutable). + */ + public static class Builder implements PartitionUpdate.Builder + { + private final TableMetadata metadata; + private final DecoratedKey key; + private final MutableDeletionInfo deletionInfo; + private final boolean canHaveShadowedData; + private Object[] tree = BTree.empty(); + private final BTree.Builder rowBuilder; + private Row staticRow = Rows.EMPTY_STATIC_ROW; + private final RegularAndStaticColumns columns; + private boolean isBuilt = false; + + public Builder(TableMetadata metadata, + DecoratedKey key, + RegularAndStaticColumns columns, + int initialRowCapacity, + boolean canHaveShadowedData) + { + this(metadata, key, columns, initialRowCapacity, canHaveShadowedData, Rows.EMPTY_STATIC_ROW, MutableDeletionInfo.live(), BTree.empty()); + } + + private Builder(TableMetadata metadata, + DecoratedKey key, + RegularAndStaticColumns columns, + int initialRowCapacity, + boolean canHaveShadowedData, + BTreePartitionData holder) + { + this(metadata, key, columns, initialRowCapacity, canHaveShadowedData, holder.staticRow, holder.deletionInfo, holder.tree); + } + + private Builder(TableMetadata metadata, + DecoratedKey key, + RegularAndStaticColumns columns, + int initialRowCapacity, + boolean canHaveShadowedData, + Row staticRow, + DeletionInfo deletionInfo, + Object[] tree) + { + this.metadata = metadata; + this.key = key; + this.columns = columns; + this.rowBuilder = rowBuilder(initialRowCapacity); + this.canHaveShadowedData = canHaveShadowedData; + this.deletionInfo = deletionInfo.mutableCopy(); + this.staticRow = staticRow; + this.tree = tree; + } + + public Builder(TableMetadata metadata, DecoratedKey key, RegularAndStaticColumns columnDefinitions, int size) + { + this(metadata, key, columnDefinitions, size, true); + } + + public Builder(BTreePartitionUpdate base, int initialRowCapacity) + { + this(base.metadata, base.partitionKey, base.columns(), initialRowCapacity, base.canHaveShadowedData, base.holder); + } + + public Builder(TableMetadata metadata, + ByteBuffer key, + RegularAndStaticColumns columns, + int initialRowCapacity) + { + this(metadata, metadata.partitioner.decorateKey(key), columns, initialRowCapacity, true); + } + + /** + * Adds a row to this update. + * + * There is no particular assumption made on the order of row added to a partition update. It is further + * allowed to add the same row (more precisely, multiple row objects for the same clustering). + * + * Note however that the columns contained in the added row must be a subset of the columns used when + * creating this update. + * + * @param row the row to add. + */ + public void add(Row row) + { + if (row.isEmpty()) + return; + + if (row.isStatic()) + { + // this assert is expensive, and possibly of limited value; we should consider removing it + // or introducing a new class of assertions for test purposes + assert columns().statics.containsAll(row.columns()) : columns().statics + " is not superset of " + row.columns(); + staticRow = staticRow.isEmpty() + ? row + : Rows.merge(staticRow, row); + } + else + { + // this assert is expensive, and possibly of limited value; we should consider removing it + // or introducing a new class of assertions for test purposes + assert columns().regulars.containsAll(row.columns()) : columns().regulars + " is not superset of " + row.columns(); + rowBuilder.add(row); + } + } + + public void addPartitionDeletion(DeletionTime deletionTime) + { + deletionInfo.add(deletionTime); + } + + public void add(RangeTombstone range) + { + deletionInfo.add(range, metadata.comparator); + } + + public DecoratedKey partitionKey() + { + return key; + } + + public TableMetadata metadata() + { + return metadata; + } + + public BTreePartitionUpdate build() + { + // assert that we are not calling build() several times + assert !isBuilt : "A PartitionUpdate.Builder should only get built once"; + Object[] add = rowBuilder.build(); + Object[] merged = BTree.update(tree, add, metadata.comparator, + UpdateFunction.Simple.of(Rows::merge)); + + EncodingStats newStats = EncodingStats.Collector.collect(staticRow, BTree.iterator(merged), deletionInfo); + + isBuilt = true; + return new BTreePartitionUpdate(metadata, + partitionKey(), + new BTreePartitionData(columns, + merged, + deletionInfo, + staticRow, + newStats), + deletionInfo, + canHaveShadowedData); + } + + public RegularAndStaticColumns columns() + { + return columns; + } + + public DeletionTime partitionLevelDeletion() + { + return deletionInfo.getPartitionDeletion(); + } + + private BTree.Builder rowBuilder(int initialCapacity) + { + return BTree.builder(metadata.comparator, initialCapacity) + .setQuickResolver(Rows::merge); + } + /** + * Modify this update to set every timestamp for live data to {@code newTimestamp} and + * every deletion timestamp to {@code newTimestamp - 1}. + * + * There is no reason to use that expect on the Paxos code path, where we need ensure that + * anything inserted use the ballot timestamp (to respect the order of update decided by + * the Paxos algorithm). We use {@code newTimestamp - 1} for deletions because tombstones + * always win on timestamp equality and we don't want to delete our own insertions + * (typically, when we overwrite a collection, we first set a complex deletion to delete the + * previous collection before adding new elements. If we were to set that complex deletion + * to the same timestamp that the new elements, it would delete those elements). And since + * tombstones always wins on timestamp equality, using -1 guarantees our deletion will still + * delete anything from a previous update. + */ + public Builder updateAllTimestamp(long newTimestamp) + { + deletionInfo.updateAllTimestamp(newTimestamp - 1); + tree = BTree.transformAndFilter(tree, (x) -> x.updateAllTimestamp(newTimestamp)); + staticRow = this.staticRow.updateAllTimestamp(newTimestamp); + return this; + } + + @Override + public String toString() + { + return "Builder{" + + "metadata=" + metadata + + ", key=" + key + + ", deletionInfo=" + deletionInfo + + ", canHaveShadowedData=" + canHaveShadowedData + + ", staticRow=" + staticRow + + ", columns=" + columns + + ", isBuilt=" + isBuilt + + '}'; + } + + } + + public static class BTreeFactory implements PartitionUpdate.Factory + { + + @Override + public PartitionUpdate.Builder builder(TableMetadata metadata, DecoratedKey partitionKey, RegularAndStaticColumns columns, int initialRowCapacity) + { + return new Builder(metadata, partitionKey, columns, initialRowCapacity); + } + + @Override + public PartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey partitionKey) + { + return BTreePartitionUpdate.emptyUpdate(metadata, partitionKey); + } + + @Override + public PartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey valueKey, Row row) + { + return BTreePartitionUpdate.singleRowUpdate(metadata, valueKey, row); + } + + @Override + public PartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec) + { + return BTreePartitionUpdate.fullPartitionDelete(metadata, key, timestamp, nowInSec); + } + + @Override + public PartitionUpdate fromIterator(UnfilteredRowIterator iterator) + { + return BTreePartitionUpdate.fromIterator(iterator); + } + + @Override + public PartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter) + { + return BTreePartitionUpdate.fromIterator(iterator, filter); + } + } +} diff --git a/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdater.java b/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdater.java index 023019242d2c..aa708ab23819 100644 --- a/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdater.java +++ b/src/java/org/apache/cassandra/db/partitions/BTreePartitionUpdater.java @@ -20,8 +20,6 @@ import org.apache.cassandra.db.DeletionInfo; import org.apache.cassandra.db.RegularAndStaticColumns; -import org.apache.cassandra.db.rows.Cell; -import org.apache.cassandra.db.rows.ColumnData; import org.apache.cassandra.db.rows.EncodingStats; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Rows; @@ -36,32 +34,48 @@ /** * the function we provide to the trie and btree utilities to perform any row and column replacements */ -public class BTreePartitionUpdater implements UpdateFunction, ColumnData.PostReconciliationFunction +public class BTreePartitionUpdater extends BasePartitionUpdater implements UpdateFunction { final MemtableAllocator allocator; final OpOrder.Group writeOp; - final Cloner cloner; final UpdateTransaction indexer; - public long dataSize; - long heapSize; - public long colUpdateTimeDelta = Long.MAX_VALUE; + public int partitionsAdded = 0; public BTreePartitionUpdater(MemtableAllocator allocator, Cloner cloner, OpOrder.Group writeOp, UpdateTransaction indexer) { + super(cloner); this.allocator = allocator; - this.cloner = cloner; this.writeOp = writeOp; this.indexer = indexer; - this.heapSize = 0; - this.dataSize = 0; } - public BTreePartitionData mergePartitions(BTreePartitionData current, final PartitionUpdate update) + @Override + public Row insert(Row insert) + { + Row data = insert.clone(cloner); + indexer.onInserted(insert); + + this.dataSize += data.dataSize(); + this.heapSize += data.unsharedHeapSizeExcludingData(); + return data; + } + + @Override + public Row merge(Row existing, Row update) + { + Row reconciled = Rows.merge(existing, update, this); + indexer.onUpdated(existing, reconciled); + + return reconciled; + } + + public BTreePartitionData mergePartitions(BTreePartitionData current, final BTreePartitionUpdate update) { if (current == null) { current = BTreePartitionData.EMPTY; - onAllocatedOnHeap(BTreePartitionData.UNSHARED_HEAP_SIZE); + this.onAllocatedOnHeap(BTreePartitionData.UNSHARED_HEAP_SIZE); + ++partitionsAdded; } try @@ -77,7 +91,7 @@ public BTreePartitionData mergePartitions(BTreePartitionData current, final Part } } - protected BTreePartitionData makeMergedPartition(BTreePartitionData current, PartitionUpdate update) + protected BTreePartitionData makeMergedPartition(BTreePartitionData current, BTreePartitionUpdate update) { DeletionInfo newDeletionInfo = merge(current.deletionInfo, update.deletionInfo()); @@ -122,60 +136,6 @@ private DeletionInfo merge(DeletionInfo existing, DeletionInfo update) return newInfo; } - @Override - public Row insert(Row insert) - { - Row data = insert.clone(cloner); - indexer.onInserted(insert); - - dataSize += data.dataSize(); - heapSize += data.unsharedHeapSizeExcludingData(); - return data; - } - - public Row merge(Row existing, Row update) - { - Row reconciled = Rows.merge(existing, update, this); - indexer.onUpdated(existing, reconciled); - - return reconciled; - } - - public Cell merge(Cell previous, Cell insert) - { - if (insert == previous) - return insert; - - long timeDelta = Math.abs(insert.timestamp() - previous.timestamp()); - if (timeDelta < colUpdateTimeDelta) - colUpdateTimeDelta = timeDelta; - if (cloner != null) - insert = cloner.clone(insert); - dataSize += insert.dataSize() - previous.dataSize(); - heapSize += insert.unsharedHeapSizeExcludingData() - previous.unsharedHeapSizeExcludingData(); - return insert; - } - - public ColumnData insert(ColumnData insert) - { - if (cloner != null) - insert = insert.clone(cloner); - dataSize += insert.dataSize(); - heapSize += insert.unsharedHeapSizeExcludingData(); - return insert; - } - - @Override - public void delete(ColumnData existing) - { - dataSize -= existing.dataSize(); - heapSize -= existing.unsharedHeapSizeExcludingData(); - } - - public void onAllocatedOnHeap(long heapSize) - { - this.heapSize += heapSize; - } public void reportAllocatedMemory() { diff --git a/src/java/org/apache/cassandra/db/partitions/BasePartitionUpdater.java b/src/java/org/apache/cassandra/db/partitions/BasePartitionUpdater.java new file mode 100644 index 000000000000..42090bf428d6 --- /dev/null +++ b/src/java/org/apache/cassandra/db/partitions/BasePartitionUpdater.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.partitions; + +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.utils.memory.Cloner; + +public class BasePartitionUpdater implements ColumnData.PostReconciliationFunction +{ + final Cloner cloner; + public long dataSize = 0; + public long heapSize = 0; + public long colUpdateTimeDelta = Long.MAX_VALUE; + + public BasePartitionUpdater(Cloner cloner) + { + this.cloner = cloner; + } + + public Cell merge(Cell previous, Cell insert) + { + if (insert == previous) + return insert; + long timeDelta = Math.abs(insert.timestamp() - previous.timestamp()); + if (timeDelta < colUpdateTimeDelta) + colUpdateTimeDelta = timeDelta; + if (cloner != null) + insert = cloner.clone(insert); + dataSize += insert.dataSize() - previous.dataSize(); + heapSize += insert.unsharedHeapSizeExcludingData() - previous.unsharedHeapSizeExcludingData(); + return insert; + } + + public ColumnData insert(ColumnData insert) + { + if (cloner != null) + insert = insert.clone(cloner); + dataSize += insert.dataSize(); + heapSize += insert.unsharedHeapSizeExcludingData(); + return insert; + } + + public void delete(ColumnData existing) + { + dataSize -= existing.dataSize(); + heapSize -= existing.unsharedHeapSizeExcludingData(); + } + + public void onAllocatedOnHeap(long heapSize) + { + this.heapSize += heapSize; + } +} diff --git a/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java b/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java index d7a0171d9a20..f44c982c7488 100644 --- a/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java +++ b/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java @@ -43,9 +43,10 @@ public static FilteredPartition create(RowIterator iterator) return new FilteredPartition(iterator); } + @Override public RowIterator rowIterator() { - final Iterator iter = iterator(); + final Iterator iter = super.rowIterator(); return new RowIterator() { public TableMetadata metadata() diff --git a/src/java/org/apache/cassandra/db/partitions/Partition.java b/src/java/org/apache/cassandra/db/partitions/Partition.java index 8888104d95fe..9b6dace1d00e 100644 --- a/src/java/org/apache/cassandra/db/partitions/Partition.java +++ b/src/java/org/apache/cassandra/db/partitions/Partition.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.db.partitions; +import java.util.Iterator; import java.util.NavigableSet; import javax.annotation.Nullable; @@ -50,10 +51,36 @@ public interface Partition public boolean isEmpty(); /** - * Whether the partition object has rows. This may be false but partition still be non-empty if it has a deletion. + * Whether the partition object has any rows, excluding the static row. + * This may be false but partition still be non-empty if it has a deletion or a non-empty static row. */ boolean hasRows(); + /** + * Returns the number of rows in this partition, excluding the static row. + */ + int rowCount(); + + /** + * Returns an iterator over the rows of this partition excluding the static row. + */ + Iterator rowIterator(); + + /** + * Returns the collection of rows of this partition excluding the static row as an iterable. + */ + default Iterable rows() + { + return this::rowIterator; + } + + Row staticRow(); + + /** + * Returns the last non-static row in the partition. + */ + Row lastRow(); + /** * Returns the row corresponding to the provided clustering, or null if there is not such row. * @@ -78,4 +105,37 @@ public interface Partition * selected by the provided clusterings. */ public UnfilteredRowIterator unfilteredIterator(ColumnFilter columns, NavigableSet> clusteringsInQueryOrder, boolean reversed); + + static String toString(Partition p) + { + return toString(p, true); + } + + static String toString(Partition p, boolean includeFullDetails) + { + StringBuilder sb = new StringBuilder(); + if (includeFullDetails) + { + sb.append(String.format("[%s.%s] key=%s partition_deletion=%s columns=%s", + p.metadata().keyspace, + p.metadata().name, + p.metadata().partitionKeyType.getString(p.partitionKey().getKey()), + p.partitionLevelDeletion(), + p.columns())); + } + else + { + sb.append("key=").append(p.metadata().partitionKeyType.getString(p.partitionKey().getKey())); + } + + if (p.staticRow() != Rows.EMPTY_STATIC_ROW) + sb.append("\n ").append(p.staticRow().toString(p.metadata(), includeFullDetails)); + + try (UnfilteredRowIterator iter = p.unfilteredIterator()) + { + while (iter.hasNext()) + sb.append("\n ").append(iter.next().toString(p.metadata(), includeFullDetails)); + } + return sb.toString(); + } } diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java index 5375b2cf0f16..0a0c4a9efb6a 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java @@ -18,28 +18,48 @@ package org.apache.cassandra.db.partitions; import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.transform.MorePartitions; import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.db.SinglePartitionReadQuery; import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.utils.NoSpamLogger; public abstract class PartitionIterators { + private static final Logger logger = LoggerFactory.getLogger(PartitionIterators.class); + private PartitionIterators() {} public static RowIterator getOnlyElement(final PartitionIterator iter, SinglePartitionReadQuery query) { - // If the query has no results, we'll get an empty iterator, but we still - // want a RowIterator out of this method, so we return an empty one. - RowIterator toReturn = iter.hasNext() - ? iter.next() - : EmptyIterators.row(query.metadata(), - query.partitionKey(), - query.clusteringIndexFilter().isReversed()); + RowIterator toReturn; + try + { + // If the query has no results, we'll get an empty iterator, but we still + // want a RowIterator out of this method, so we return an empty one. + toReturn = iter.hasNext() + ? iter.next() + : EmptyIterators.row(query.metadata(), + query.partitionKey(), + query.clusteringIndexFilter().isReversed()); + } + catch (RuntimeException e) + { + iter.close(); + throw e; + } // Note that in general, we should wrap the result so that it's close method actually // close the whole PartitionIterator. @@ -143,6 +163,57 @@ public RowIterator next() }; } + /** + * Wraps the provided iterator to run a specified actions whenever a new partition or row is iterated over. + * The resulting iterator is tolerant to the provided actions throwing exceptions. + * The actions are allowed to fail and won't stop iteration, but that fact will be logged on ERROR level. + * + * The wrapper iterators do not delegate Object class methods to the wrapped ones (PartitionIterator and RowIterator) + * + * @param delegate the iterator to wrap + * @param onPartition the action to run when a new partition is iterated over + * @param onStaticRow the action to run when the partition has a static row + * @param onRow the action to run when a new row is iterated over + */ + public static PartitionIterator filteredRowTrackingIterator(PartitionIterator delegate, + Consumer onPartition, + Consumer onStaticRow, + Consumer onRow) + { + return new PartitionIterator() + { + public void close() + { + delegate.close(); + } + + public boolean hasNext() + { + return delegate.hasNext(); + } + + public RowIterator next() + { + RowIterator next = delegate.next(); + try + { + onPartition.accept(next.partitionKey()); + if (!next.staticRow().isEmpty()) + { + onStaticRow.accept(next.staticRow()); + } + } + catch (Throwable t) + { + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 60, TimeUnit.SECONDS, + "Tracking callback for read rows failed on new partition {}", next.partitionKey(), t); + } + return new RowTrackingIterator(next, onRow); + } + }; + } + + private static class SingletonPartitionIterator extends AbstractIterator implements PartitionIterator { private final RowIterator iterator; @@ -167,4 +238,94 @@ public void close() iterator.close(); } } + + private static class RowTrackingIterator implements RowIterator + { + private final RowIterator delegate; + private final Consumer onRow; + + RowTrackingIterator(RowIterator delegate, Consumer onRow) + { + this.delegate = delegate; + this.onRow = onRow; + } + + @Override + public TableMetadata metadata() + { + return delegate.metadata(); + } + + @Override + public boolean isReverseOrder() + { + return delegate.isReverseOrder(); + } + + @Override + public RegularAndStaticColumns columns() + { + return delegate.columns(); + } + + @Override + public DecoratedKey partitionKey() + { + return delegate.partitionKey(); + } + + @Override + public Row staticRow() + { + return delegate.staticRow(); + } + + @Override + public boolean hasNext() + { + return delegate.hasNext(); + } + + @Override + public Row next() + { + Row next = delegate.next(); + try + { + onRow.accept(next); + } + catch (Throwable t) + { + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 60, TimeUnit.SECONDS, + "Tracking callback for read rows failed on row {}", next, t); + + } + return next; + } + + @Override + public void remove() + { + delegate.remove(); + } + + @Override + public void forEachRemaining(Consumer action) + { + delegate.forEachRemaining(action); + } + + @Override + public void close() + { + delegate.close(); + } + + @Override + public boolean isEmpty() + { + return delegate.isEmpty(); + } + } + } diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionStatisticsCollector.java b/src/java/org/apache/cassandra/db/partitions/PartitionStatisticsCollector.java index 7c3ba150aefd..e98642968b0a 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionStatisticsCollector.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionStatisticsCollector.java @@ -28,4 +28,4 @@ public interface PartitionStatisticsCollector void update(Cell cell); void updateColumnSetPerRow(long columnSetInRow); void updateHasLegacyCounterShards(boolean hasLegacyCounterShards); -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java index 035cb0edd6d1..2b663077de7e 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java @@ -20,33 +20,45 @@ import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.common.primitives.Ints; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.*; +import net.openhft.chronicle.core.util.ThrowingFunction; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.CounterMutation; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.SimpleBuilders; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; +import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.utils.btree.BTree; -import org.apache.cassandra.utils.btree.UpdateFunction; import org.apache.cassandra.utils.vint.VIntCoding; import static org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer.IS_EMPTY; @@ -65,172 +77,134 @@ * is also a few static helper constructor methods for special cases ({@code emptyUpdate()}, * {@code fullPartitionDelete} and {@code singleRowUpdate}). */ -public class PartitionUpdate extends AbstractBTreePartition +public interface PartitionUpdate extends Partition { - protected static final Logger logger = LoggerFactory.getLogger(PartitionUpdate.class); + @SuppressWarnings("Convert2MethodRef") + public static final PartitionUpdateSerializer serializer = new PartitionUpdateSerializer(tableId -> Schema.instance.getExistingTableMetadata(tableId)); - public static final PartitionUpdateSerializer serializer = new PartitionUpdateSerializer(); + DeletionInfo deletionInfo(); - private final BTreePartitionData holder; - private final DeletionInfo deletionInfo; - private final TableMetadata metadata; + /** + * The number of "operations" contained in the update. + *

    + * This is used by {@code Memtable} to approximate how much work this update does. In practice, this + * count how many rows are updated and how many ranges are deleted by the partition update. + * + * @return the number of "operations" performed by the update. + */ + int operationCount(); - private final boolean canHaveShadowedData; + /** + * The size of the data contained in this update. + * + * @return the size of the data contained in this update. + */ + int dataSize(); - private PartitionUpdate(TableMetadata metadata, - DecoratedKey key, - BTreePartitionData holder, - MutableDeletionInfo deletionInfo, - boolean canHaveShadowedData) - { - super(key); - this.metadata = metadata; - this.holder = holder; - this.deletionInfo = deletionInfo; - this.canHaveShadowedData = canHaveShadowedData; - } + long unsharedHeapSize(); + + @Override + RegularAndStaticColumns columns(); + + @Override + EncodingStats stats(); + + Row staticRow(); + + int rowCount(); /** - * Creates a empty immutable partition update. - * - * @param metadata the metadata for the created update. - * @param key the partition key for the created update. + * Validates the data contained in this update. * - * @return the newly created empty (and immutable) update. + * @throws org.apache.cassandra.serializers.MarshalException if some of the data contained in this update is corrupted. */ - public static PartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey key) + default void validate() { - MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); - BTreePartitionData holder = new BTreePartitionData(RegularAndStaticColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS); - return new PartitionUpdate(metadata, key, holder, deletionInfo, false); + for (Row row : rows()) + { + metadata().comparator.validate(row.clustering()); + for (ColumnData cd : row) + cd.validate(); + } } /** - * Creates an immutable partition update that entirely deletes a given partition. - * - * @param metadata the metadata for the created update. - * @param key the partition key for the partition that the created update should delete. - * @param timestamp the timestamp for the deletion. - * @param nowInSec the current time in seconds to use as local deletion time for the partition deletion. + * The maximum timestamp used in this update. * - * @return the newly created partition deletion update. + * @return the maximum timestamp used in this update. */ - public static PartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec) - { - MutableDeletionInfo deletionInfo = new MutableDeletionInfo(timestamp, nowInSec); - BTreePartitionData holder = new BTreePartitionData(RegularAndStaticColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS); - return new PartitionUpdate(metadata, key, holder, deletionInfo, false); - } + long maxTimestamp(); /** - * Creates an immutable partition update that contains a single row update. - * - * @param metadata the metadata for the created update. - * @param key the partition key for the partition to update. - * @param row the row for the update (may be null). - * @param row the static row for the update (may be null). + * For an update on a counter table, returns a list containing a {@code CounterMark} for + * every counter contained in the update. * - * @return the newly created partition update containing only {@code row}. + * @return a list with counter marks for every counter in this update. */ - public static PartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey key, Row row, Row staticRow) + List collectCounterMarks(); + + default void validateIndexedColumns(ClientState state) { - MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); - BTreePartitionData holder = new BTreePartitionData( - new RegularAndStaticColumns( - staticRow == null ? Columns.NONE : Columns.from(staticRow), - row == null ? Columns.NONE : Columns.from(row) - ), - row == null ? BTree.empty() : BTree.singleton(row), - deletionInfo, - staticRow == null ? Rows.EMPTY_STATIC_ROW : staticRow, - EncodingStats.NO_STATS - ); - return new PartitionUpdate(metadata, key, holder, deletionInfo, false); + IndexRegistry.obtain(metadata()).validate(this, state); } - /** - * Creates an immutable partition update that contains a single row update. - * - * @param metadata the metadata for the created update. - * @param key the partition key for the partition to update. - * @param row the row for the update (may be static). - * - * @return the newly created partition update containing only {@code row}. - */ - public static PartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey key, Row row) + PartitionUpdate withUpdatedTimestamps(long timestamp); + + static Builder builder(TableMetadata metadata, DecoratedKey partitionKey, RegularAndStaticColumns columns, int initialRowCapacity) { - return singleRowUpdate(metadata, key, row.isStatic() ? null : row, row.isStatic() ? row : null); + return metadata.partitionUpdateFactory().builder(metadata, partitionKey, columns, initialRowCapacity); } - /** - * Creates an immutable partition update that contains a single row update. - * - * @param metadata the metadata for the created update. - * @param key the partition key for the partition to update. - * @param row the row for the update. - * - * @return the newly created partition update containing only {@code row}. - */ - public static PartitionUpdate singleRowUpdate(TableMetadata metadata, ByteBuffer key, Row row) + static Builder builder(TableMetadata metadata, ByteBuffer partitionKey, RegularAndStaticColumns columns, int initialRowCapacity) { - return singleRowUpdate(metadata, metadata.partitioner.decorateKey(key), row); + return builder(metadata, metadata.partitioner.decorateKey(partitionKey), columns, initialRowCapacity); } - /** - * Turns the given iterator into an update. - * - * @param iterator the iterator to turn into updates. - * @param filter the column filter used when querying {@code iterator}. This is used to make - * sure we don't include data for which the value has been skipped while reading (as we would - * then be writing something incorrect). - * - * Warning: this method does not close the provided iterator, it is up to - * the caller to close it. - */ - public static PartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter) + static PartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey partitionKey) { - iterator = UnfilteredRowIterators.withOnlyQueriedData(iterator, filter); - BTreePartitionData holder = build(iterator, 16); - MutableDeletionInfo deletionInfo = (MutableDeletionInfo) holder.deletionInfo; - return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false); + return metadata.partitionUpdateFactory().emptyUpdate(metadata, partitionKey); } - /** - * Turns the given iterator into an update. - * - * @param iterator the iterator to turn into updates. - * @param filter the column filter used when querying {@code iterator}. This is used to make - * sure we don't include data for which the value has been skipped while reading (as we would - * then be writing something incorrect). - * - * Warning: this method does not close the provided iterator, it is up to - * the caller to close it. - */ - public static PartitionUpdate fromIterator(RowIterator iterator, ColumnFilter filter) + static PartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey valueKey, Row row) { - iterator = RowIterators.withOnlyQueriedData(iterator, filter); - MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); - BTreePartitionData holder = build(iterator, deletionInfo, true); - return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false); + return metadata.partitionUpdateFactory().singleRowUpdate(metadata, valueKey, row); } + static PartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec) + { + return metadata.partitionUpdateFactory().fullPartitionDelete(metadata, key, timestamp, nowInSec); + } - public PartitionUpdate withOnlyPresentColumns() + static PartitionUpdate fullPartitionDelete(TableMetadata metadata, ByteBuffer key, long timestamp, long nowInSec) { - Set columnSet = new HashSet<>(); + return fullPartitionDelete(metadata, metadata.partitioner.decorateKey(key), timestamp, nowInSec); + } - for (Row row : this) - for (ColumnData column : row) - columnSet.add(column.column()); + static PartitionUpdate fromIterator(UnfilteredRowIterator partition, ColumnFilter filter) + { + return partition.metadata().partitionUpdateFactory().fromIterator(partition, filter); + } - RegularAndStaticColumns columns = RegularAndStaticColumns.builder().addAll(columnSet).build(); - return new PartitionUpdate(this.metadata, this.partitionKey, this.holder.withColumns(columns), this.deletionInfo.mutableCopy(), false); + static PartitionUpdate merge(List updates) + { + assert !updates.isEmpty(); + return updates.get(0).metadata().partitionUpdateFactory().merge(updates); } + PartitionUpdate withOnlyPresentColumns(); - protected boolean canHaveShadowedData() + /** + * Creates a new simple partition update builder. + * + * @param metadata the metadata for the table this is a partition of. + * @param partitionKeyValues the values for partition key columns identifying this partition. The values for each + * partition key column can be passed either directly as {@code ByteBuffer} or using a "native" value (int for + * Int32Type, string for UTF8Type, ...). It is also allowed to pass a single {@code DecoratedKey} value directly. + * @return a newly created builder. + */ + static SimpleBuilder simpleBuilder(TableMetadata metadata, Object... partitionKeyValues) { - return canHaveShadowedData; + return new SimpleBuilders.PartitionUpdateBuilder(metadata, partitionKeyValues); } /** @@ -241,7 +215,8 @@ protected boolean canHaveShadowedData() * * @return the deserialized update or {@code null} if {@code bytes == null}. */ - public static PartitionUpdate fromBytes(ByteBuffer bytes, int version) + @SuppressWarnings("resource") + static PartitionUpdate fromBytes(ByteBuffer bytes, int version) { if (bytes == null) return null; @@ -266,7 +241,7 @@ public static PartitionUpdate fromBytes(ByteBuffer bytes, int version) * * @return a newly allocated byte buffer containing the serialized update. */ - public static ByteBuffer toBytes(PartitionUpdate update, int version) + static ByteBuffer toBytes(PartitionUpdate update, int version) { try (DataOutputBuffer out = new DataOutputBuffer()) { @@ -280,195 +255,10 @@ public static ByteBuffer toBytes(PartitionUpdate update, int version) } /** - * Creates a partition update that entirely deletes a given partition. - * - * @param metadata the metadata for the created update. - * @param key the partition key for the partition that the created update should delete. - * @param timestamp the timestamp for the deletion. - * @param nowInSec the current time in seconds to use as local deletion time for the partition deletion. - * - * @return the newly created partition deletion update. - */ - public static PartitionUpdate fullPartitionDelete(TableMetadata metadata, ByteBuffer key, long timestamp, long nowInSec) - { - return fullPartitionDelete(metadata, metadata.partitioner.decorateKey(key), timestamp, nowInSec); - } - - /** - * Merges the provided updates, yielding a new update that incorporates all those updates. - * - * @param updates the collection of updates to merge. This shouldn't be empty. - * - * @return a partition update that include (merge) all the updates from {@code updates}. - */ - public static PartitionUpdate merge(List updates) - { - assert !updates.isEmpty(); - final int size = updates.size(); - - if (size == 1) - return Iterables.getOnlyElement(updates); - - List asIterators = Lists.transform(updates, AbstractBTreePartition::unfilteredIterator); - return fromIterator(UnfilteredRowIterators.merge(asIterators), ColumnFilter.all(updates.get(0).metadata())); - } - - // We override this, because the version in the super-class calls holder(), which build the update preventing - // further updates, but that's not necessary here and being able to check at least the partition deletion without - // "locking" the update is nice (and used in DataResolver.RepairMergeListener.MergeListener). - @Override - public DeletionInfo deletionInfo() - { - return deletionInfo; - } - - /** - * The number of "operations" contained in the update. - *

    - * This is used by {@code Memtable} to approximate how much work this update does. In practice, this - * count how many rows are updated and how many ranges are deleted by the partition update. - * - * @return the number of "operations" performed by the update. - */ - public int operationCount() - { - return rowCount() - + (staticRow().isEmpty() ? 0 : 1) - + deletionInfo.rangeCount() - + (deletionInfo.getPartitionDeletion().isLive() ? 0 : 1); - } - - /** - * The size of the data contained in this update. - * - * @return the size of the data contained in this update. - */ - public int dataSize() - { - return Ints.saturatedCast(BTree.accumulate(holder.tree, (row, value) -> row.dataSize() + value, 0L) - + holder.staticRow.dataSize() + holder.deletionInfo.dataSize()); - } - - /** - * The size of the data contained in this update. - * - * @return the size of the data contained in this update. - */ - public long unsharedHeapSize() - { - return BTree.accumulate(holder.tree, (row, value) -> row.unsharedHeapSize() + value, 0L) - + holder.staticRow.unsharedHeapSize() + holder.deletionInfo.unsharedHeapSize(); - } - - public TableMetadata metadata() - { - return metadata; - } - - @Override - public RegularAndStaticColumns columns() - { - // The superclass implementation calls holder(), but that triggers a build of the PartitionUpdate. But since - // the columns are passed to the ctor, we know the holder always has the proper columns even if it doesn't have - // the built rows yet, so just bypass the holder() method. - return holder.columns; - } - - protected BTreePartitionData holder() - { - return holder; - } - - public EncodingStats stats() - { - return holder().stats; - } - - /** - * Validates the data contained in this update. - * - * @throws org.apache.cassandra.serializers.MarshalException if some of the data contained in this update is corrupted. - */ - public void validate() - { - for (Row row : this) - { - metadata().comparator.validate(row.clustering()); - for (ColumnData cd : row) - cd.validate(); - } - } - - /** - * The maximum timestamp used in this update. * - * @return the maximum timestamp used in this update. + * @return the estimated number of rows affected by this mutation */ - public long maxTimestamp() - { - long maxTimestamp = deletionInfo.maxTimestamp(); - for (Row row : this) - { - maxTimestamp = Math.max(maxTimestamp, row.primaryKeyLivenessInfo().timestamp()); - for (ColumnData cd : row) - { - if (cd.column().isSimple()) - { - maxTimestamp = Math.max(maxTimestamp, ((Cell)cd).timestamp()); - } - else - { - ComplexColumnData complexData = (ComplexColumnData)cd; - maxTimestamp = Math.max(maxTimestamp, complexData.complexDeletion().markedForDeleteAt()); - for (Cell cell : complexData) - maxTimestamp = Math.max(maxTimestamp, cell.timestamp()); - } - } - } - - if (this.holder.staticRow != null) - { - for (ColumnData cd : this.holder.staticRow.columnData()) - { - if (cd.column().isSimple()) - { - maxTimestamp = Math.max(maxTimestamp, ((Cell) cd).timestamp()); - } - else - { - ComplexColumnData complexData = (ComplexColumnData) cd; - maxTimestamp = Math.max(maxTimestamp, complexData.complexDeletion().markedForDeleteAt()); - for (Cell cell : complexData) - maxTimestamp = Math.max(maxTimestamp, cell.timestamp()); - } - } - } - return maxTimestamp; - } - - /** - * For an update on a counter table, returns a list containing a {@code CounterMark} for - * every counter contained in the update. - * - * @return a list with counter marks for every counter in this update. - */ - public List collectCounterMarks() - { - assert metadata().isCounter(); - // We will take aliases on the rows of this update, and update them in-place. So we should be sure the - // update is now immutable for all intent and purposes. - List marks = new ArrayList<>(); - addMarksForRow(staticRow(), marks); - for (Row row : this) - addMarksForRow(row, marks); - return marks; - } - - /** - * - * @return the estimated number of rows affected by this mutation - */ - public int affectedRowCount() + default int affectedRowCount() { // If there is a partition-level deletion, we intend to delete at least one row. if (!partitionLevelDeletion().isLive()) @@ -492,7 +282,7 @@ public int affectedRowCount() * * @return the estimated total number of columns that either have live data or are covered by a delete */ - public int affectedColumnCount() + default int affectedColumnCount() { // If there is a partition-level deletion, we intend to delete at least the columns of one row. if (!partitionLevelDeletion().isLive()) @@ -504,10 +294,10 @@ public int affectedColumnCount() if (deletionInfo().hasRanges()) count += deletionInfo().rangeCount() * metadata().regularColumns().size(); - for (Row row : this) + for (Row row : rows()) { if (row.deletion().isLive()) - // If the row is live, this will include simple tombstones as well as cells w/ actual data. + // If the row is live, this will include simple tombstones as well as cells w/ actual data. count += row.columnCount(); else // We have a row deletion, so account for the columns that might be deleted. @@ -520,44 +310,6 @@ public int affectedColumnCount() return count; } - private static void addMarksForRow(Row row, List marks) - { - for (Cell cell : row.cells()) - { - if (cell.isCounterCell()) - marks.add(new CounterMark(row, cell.column(), cell.path())); - } - } - - /** - * Creates a new simple partition update builder. - * - * @param metadata the metadata for the table this is a partition of. - * @param partitionKeyValues the values for partition key columns identifying this partition. The values for each - * partition key column can be passed either directly as {@code ByteBuffer} or using a "native" value (int for - * Int32Type, string for UTF8Type, ...). It is also allowed to pass a single {@code DecoratedKey} value directly. - * @return a newly created builder. - */ - public static SimpleBuilder simpleBuilder(TableMetadata metadata, Object... partitionKeyValues) - { - return new SimpleBuilders.PartitionUpdateBuilder(metadata, partitionKeyValues); - } - - public void validateIndexedColumns(ClientState state) - { - IndexRegistry.obtain(metadata()).validate(this, state); - } - - @VisibleForTesting - public static PartitionUpdate unsafeConstruct(TableMetadata metadata, - DecoratedKey key, - BTreePartitionData holder, - MutableDeletionInfo deletionInfo, - boolean canHaveShadowedData) - { - return new PartitionUpdate(metadata, key, holder, deletionInfo, canHaveShadowedData); - } - /** * Interface for building partition updates geared towards human. *

    @@ -705,51 +457,47 @@ public interface RangeTombstoneBuilder } } - public static class PartitionUpdateSerializer + class PartitionUpdateSerializer { + private final ThrowingFunction tableMetadataResolver; + + public PartitionUpdateSerializer(ThrowingFunction tableMetadataResolver) + { + this.tableMetadataResolver = tableMetadataResolver; + } + public void serialize(PartitionUpdate update, DataOutputPlus out, int version) throws IOException { + Preconditions.checkArgument(version != MessagingService.VERSION_DSE_68, + "Can't serialize to version " + version); try (UnfilteredRowIterator iter = update.unfilteredIterator()) { assert !iter.isReverseOrder(); - update.metadata.id.serialize(out); + update.metadata().id.serialize(out); UnfilteredRowIteratorSerializer.serializer.serialize(iter, null, out, version, update.rowCount()); } } public PartitionUpdate deserialize(DataInputPlus in, int version, DeserializationHelper.Flag flag) throws IOException { - TableMetadata metadata = Schema.instance.getExistingTableMetadata(TableId.deserialize(in)); + TableMetadata metadata = tableMetadataResolver.apply(TableId.deserialize(in)); + if (version == MessagingService.VERSION_DSE_68) + { + // ignore maxTimestamp + in.readLong(); + } + Factory factory = metadata.partitionUpdateFactory(); UnfilteredRowIteratorSerializer.Header header = UnfilteredRowIteratorSerializer.serializer.deserializeHeader(metadata, null, in, version, flag); if (header.isEmpty) - return emptyUpdate(metadata, header.key); + return factory.emptyUpdate(metadata, header.key); assert !header.isReversed; assert header.rowEstimate >= 0; - - MutableDeletionInfo.Builder deletionBuilder = MutableDeletionInfo.builder(header.partitionDeletion, metadata.comparator, false); - Object[] rows; - try (BTree.FastBuilder builder = BTree.fastBuilder(); - UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, flag, header)) + try (UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, flag, header)) { - while (partition.hasNext()) - { - Unfiltered unfiltered = partition.next(); - if (unfiltered.kind() == Unfiltered.Kind.ROW) - builder.add((Row)unfiltered); - else - deletionBuilder.add((RangeTombstoneMarker)unfiltered); - } - rows = builder.build(); + return factory.fromIterator(partition); } - - MutableDeletionInfo deletionInfo = deletionBuilder.build(); - return new PartitionUpdate(metadata, - header.key, - new BTreePartitionData(header.sHeader.columns(), rows, deletionInfo, header.staticRow, header.sHeader.stats()), - deletionInfo, - false); } public static boolean isEmpty(ByteBuffer in, DeserializationHelper.Flag flag, DecoratedKey key) throws IOException @@ -771,8 +519,9 @@ public long serializedSize(PartitionUpdate update, int version) { try (UnfilteredRowIterator iter = update.unfilteredIterator()) { - return update.metadata.id.serializedSize() - + UnfilteredRowIteratorSerializer.serializer.serializedSize(iter, null, version, update.rowCount()); + return update.metadata().id.serializedSize() + + (version == MessagingService.VERSION_DSE_68 ? TypeSizes.LONG_SIZE : 0) + + UnfilteredRowIteratorSerializer.serializer.serializedSize(iter, null, version, update.rowCount()); } } } @@ -782,13 +531,13 @@ public long serializedSize(PartitionUpdate update, int version) * us to update the counter value based on the pre-existing value read during the read-before-write that counters * do. See {@link CounterMutation} to understand how this is used. */ - public static class CounterMark + class CounterMark { private final Row row; private final ColumnMetadata column; private final CellPath path; - private CounterMark(Row row, ColumnMetadata column, CellPath path) + protected CounterMark(Row row, ColumnMetadata column, CellPath path) { this.row = row; this.column = column; @@ -831,74 +580,8 @@ public void setValue(ByteBuffer value) * * This class is not thread safe, but the PartitionUpdate it produces is (since it is immutable). */ - public static class Builder + interface Builder { - private final TableMetadata metadata; - private final DecoratedKey key; - private final MutableDeletionInfo deletionInfo; - private final boolean canHaveShadowedData; - private Object[] tree = BTree.empty(); - private final BTree.Builder rowBuilder; - private Row staticRow = Rows.EMPTY_STATIC_ROW; - private final RegularAndStaticColumns columns; - private boolean isBuilt = false; - - public Builder(TableMetadata metadata, - DecoratedKey key, - RegularAndStaticColumns columns, - int initialRowCapacity, - boolean canHaveShadowedData) - { - this(metadata, key, columns, initialRowCapacity, canHaveShadowedData, Rows.EMPTY_STATIC_ROW, MutableDeletionInfo.live(), BTree.empty()); - } - - private Builder(TableMetadata metadata, - DecoratedKey key, - RegularAndStaticColumns columns, - int initialRowCapacity, - boolean canHaveShadowedData, - BTreePartitionData holder) - { - this(metadata, key, columns, initialRowCapacity, canHaveShadowedData, holder.staticRow, holder.deletionInfo, holder.tree); - } - - private Builder(TableMetadata metadata, - DecoratedKey key, - RegularAndStaticColumns columns, - int initialRowCapacity, - boolean canHaveShadowedData, - Row staticRow, - DeletionInfo deletionInfo, - Object[] tree) - { - this.metadata = metadata; - this.key = key; - this.columns = columns; - this.rowBuilder = rowBuilder(initialRowCapacity); - this.canHaveShadowedData = canHaveShadowedData; - this.deletionInfo = deletionInfo.mutableCopy(); - this.staticRow = staticRow; - this.tree = tree; - } - - public Builder(TableMetadata metadata, DecoratedKey key, RegularAndStaticColumns columnDefinitions, int size) - { - this(metadata, key, columnDefinitions, size, true); - } - - public Builder(PartitionUpdate base, int initialRowCapacity) - { - this(base.metadata, base.partitionKey, base.columns(), initialRowCapacity, base.canHaveShadowedData, base.holder); - } - - public Builder(TableMetadata metadata, - ByteBuffer key, - RegularAndStaticColumns columns, - int initialRowCapacity) - { - this(metadata, metadata.partitioner.decorateKey(key), columns, initialRowCapacity, true); - } - /** * Adds a row to this update. * @@ -910,121 +593,49 @@ public Builder(TableMetadata metadata, * * @param row the row to add. */ - public void add(Row row) - { - if (row.isEmpty()) - return; + void add(Row row); - if (row.isStatic()) - { - // this assert is expensive, and possibly of limited value; we should consider removing it - // or introducing a new class of assertions for test purposes - assert columns().statics.containsAll(row.columns()) : columns().statics + " is not superset of " + row.columns(); - staticRow = staticRow.isEmpty() - ? row - : Rows.merge(staticRow, row); - } - else - { - // this assert is expensive, and possibly of limited value; we should consider removing it - // or introducing a new class of assertions for test purposes - assert columns().regulars.containsAll(row.columns()) : columns().regulars + " is not superset of " + row.columns(); - rowBuilder.add(row); - } - } + void addPartitionDeletion(DeletionTime deletionTime); - public void addPartitionDeletion(DeletionTime deletionTime) - { - deletionInfo.add(deletionTime); - } + void add(RangeTombstone range); - public void add(RangeTombstone range) - { - deletionInfo.add(range, metadata.comparator); - } + DecoratedKey partitionKey(); - public DecoratedKey partitionKey() - { - return key; - } + TableMetadata metadata(); - public TableMetadata metadata() - { - return metadata; - } + PartitionUpdate build(); - public PartitionUpdate build() - { - // assert that we are not calling build() several times - assert !isBuilt : "A PartitionUpdate.Builder should only get built once"; - Object[] add = rowBuilder.build(); - Object[] merged = BTree.update(tree, add, metadata.comparator, - UpdateFunction.Simple.of(Rows::merge)); - - EncodingStats newStats = EncodingStats.Collector.collect(staticRow, BTree.iterator(merged), deletionInfo); - - isBuilt = true; - return new PartitionUpdate(metadata, - partitionKey(), - new BTreePartitionData(columns, - merged, - deletionInfo, - staticRow, - newStats), - deletionInfo, - canHaveShadowedData); - } + RegularAndStaticColumns columns(); - public RegularAndStaticColumns columns() - { - return columns; - } + DeletionTime partitionLevelDeletion(); + } - public DeletionTime partitionLevelDeletion() - { - return deletionInfo.getPartitionDeletion(); - } + interface Factory + { + Builder builder(TableMetadata metadata, DecoratedKey partitionKey, RegularAndStaticColumns columns, int initialRowCapacity); + PartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey partitionKey); + PartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey valueKey, Row row); + PartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec); + PartitionUpdate fromIterator(UnfilteredRowIterator iterator); + PartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter); - private BTree.Builder rowBuilder(int initialCapacity) - { - return BTree.builder(metadata.comparator, initialCapacity) - .setQuickResolver(Rows::merge); - } /** - * Modify this update to set every timestamp for live data to {@code newTimestamp} and - * every deletion timestamp to {@code newTimestamp - 1}. - * - * There is no reason to use that expect on the Paxos code path, where we need ensure that - * anything inserted use the ballot timestamp (to respect the order of update decided by - * the Paxos algorithm). We use {@code newTimestamp - 1} for deletions because tombstones - * always win on timestamp equality and we don't want to delete our own insertions - * (typically, when we overwrite a collection, we first set a complex deletion to delete the - * previous collection before adding new elements. If we were to set that complex deletion - * to the same timestamp that the new elements, it would delete those elements). And since - * tombstones always wins on timestamp equality, using -1 guarantees our deletion will still - * delete anything from a previous update. + * Merge the provided updates into a single update. The method must also work (possibly inefficiently) when the + * given updates do not match the type of this factory. */ - public Builder updateAllTimestamp(long newTimestamp) + default PartitionUpdate merge(List updates) { - deletionInfo.updateAllTimestamp(newTimestamp - 1); - tree = BTree.transformAndFilter(tree, (x) -> x.updateAllTimestamp(newTimestamp)); - staticRow = this.staticRow.updateAllTimestamp(newTimestamp); - return this; - } + assert !updates.isEmpty(); + final int size = updates.size(); - @Override - public String toString() - { - return "Builder{" + - "metadata=" + metadata + - ", key=" + key + - ", deletionInfo=" + deletionInfo + - ", canHaveShadowedData=" + canHaveShadowedData + - ", staticRow=" + staticRow + - ", columns=" + columns + - ", isBuilt=" + isBuilt + - '}'; - } + if (size == 1) + return Iterables.getOnlyElement(updates); + List asIterators = Lists.transform(updates, Partition::unfilteredIterator); + try (UnfilteredRowIterator merge = UnfilteredRowIterators.merge(asIterators)) + { + return fromIterator(merge); + } + } } } diff --git a/src/java/org/apache/cassandra/db/partitions/TrieBackedPartition.java b/src/java/org/apache/cassandra/db/partitions/TrieBackedPartition.java new file mode 100644 index 000000000000..ad0c75b8662d --- /dev/null +++ b/src/java/org/apache/cassandra/db/partitions/TrieBackedPartition.java @@ -0,0 +1,709 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.partitions; + +import java.util.Iterator; +import java.util.NavigableSet; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.primitives.Ints; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.MutableDeletionInfo; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.db.rows.AbstractUnfilteredRowIterator; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowAndDeletionMergeIterator; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; +import org.apache.cassandra.db.tries.Direction; +import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.db.tries.Trie; +import org.apache.cassandra.db.tries.TrieEntriesIterator; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.btree.BTree; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.memory.Cloner; +import org.apache.cassandra.utils.memory.EnsureOnHeap; + +/** + * In-memory partition backed by a trie. The rows of the partition are values in the leaves of the trie, where the key + * to the row is only stored as the path to reach that leaf; static rows are also treated as a row with STATIC_CLUSTERING + * path; the deletion information is placed as a metadata object at the root of the trie -- this matches how Memtable + * stores partitions within the larger map, so that TrieBackedPartition objects can be created directly from Memtable + * tail tries. + * + * This object also holds the partition key, as well as some metadata (columns and statistics). + * + * Currently all descendants and instances of this class are immutable (even tail tries from mutable memtables are + * guaranteed to not change as we use forced copying below the partition level), though this may change in the future. + */ +public class TrieBackedPartition implements Partition +{ + /** + * If keys are below this length, we will use a recursive procedure for inserting data when building the backing + * trie. + */ + @VisibleForTesting + public static final int MAX_RECURSIVE_KEY_LENGTH = 128; + + public static final ByteComparable.Version BYTE_COMPARABLE_VERSION = ByteComparable.Version.OSS50; + + /** Pre-made path for STATIC_CLUSTERING, to avoid creating path object when querying static path. */ + public static final ByteComparable STATIC_CLUSTERING_PATH = v -> ByteSource.oneByte(ClusteringPrefix.Kind.STATIC_CLUSTERING.asByteComparableValue(v)); + /** Pre-made path for BOTTOM, to avoid creating path object when iterating rows. */ + public static final ByteComparable BOTTOM_PATH = v -> ByteSource.oneByte(ClusteringPrefix.Kind.INCL_START_BOUND.asByteComparableValue(v)); + + /** + * The representation of a row stored at the leaf of a trie. Does not contain the row key. + * + * The methods toRow and copyToOnHeapRow combine this with a clustering for the represented Row. + */ + public static class RowData + { + final Object[] columnsBTree; + final LivenessInfo livenessInfo; + final DeletionTime deletion; + final long minLocalDeletionTime; + + RowData(Object[] columnsBTree, LivenessInfo livenessInfo, DeletionTime deletion) + { + this(columnsBTree, livenessInfo, deletion, BTreeRow.minDeletionTime(columnsBTree, livenessInfo, deletion)); + } + + RowData(Object[] columnsBTree, LivenessInfo livenessInfo, DeletionTime deletion, long minLocalDeletionTime) + { + this.columnsBTree = columnsBTree; + this.livenessInfo = livenessInfo; + this.deletion = deletion; + this.minLocalDeletionTime = minLocalDeletionTime; + } + + Row toRow(Clustering clustering) + { + return BTreeRow.create(clustering, + livenessInfo, + Row.Deletion.regular(deletion), + columnsBTree, + minLocalDeletionTime); + } + + public int dataSize() + { + int dataSize = livenessInfo.dataSize() + deletion.dataSize(); + + return Ints.checkedCast(BTree.accumulate(columnsBTree, (ColumnData cd, long v) -> v + cd.dataSize(), dataSize)); + } + + public long unsharedHeapSizeExcludingData() + { + long heapSize = EMPTY_ROWDATA_SIZE + + BTree.sizeOfStructureOnHeap(columnsBTree) + + livenessInfo.unsharedHeapSize() + + deletion.unsharedHeapSize(); + + return BTree.accumulate(columnsBTree, (ColumnData cd, long v) -> v + cd.unsharedHeapSizeExcludingData(), heapSize); + } + + public String toString() + { + return "row " + livenessInfo + " size " + dataSize(); + } + + public RowData clone(Cloner cloner) + { + Object[] tree = BTree.transform(columnsBTree, c -> c.clone(cloner)); + return new RowData(tree, livenessInfo, deletion, minLocalDeletionTime); + } + } + + private static final long EMPTY_ROWDATA_SIZE = ObjectSizes.measure(new RowData(null, null, null, 0)); + + protected final Trie trie; + protected final DecoratedKey partitionKey; + protected final TableMetadata metadata; + protected final RegularAndStaticColumns columns; + protected final EncodingStats stats; + protected final int rowCountIncludingStatic; + protected final boolean canHaveShadowedData; + + public TrieBackedPartition(DecoratedKey partitionKey, + RegularAndStaticColumns columns, + EncodingStats stats, + int rowCountIncludingStatic, + Trie trie, + TableMetadata metadata, + boolean canHaveShadowedData) + { + this.partitionKey = partitionKey; + this.trie = trie; + this.metadata = metadata; + this.columns = columns; + this.stats = stats; + this.rowCountIncludingStatic = rowCountIncludingStatic; + this.canHaveShadowedData = canHaveShadowedData; + // There must always be deletion info metadata. + // Note: we can't use deletionInfo() because WithEnsureOnHeap's override is not yet set up. + assert trie.get(ByteComparable.EMPTY) != null; + assert stats != null; + } + + public static TrieBackedPartition fromIterator(UnfilteredRowIterator iterator) + { + ContentBuilder builder = build(iterator, false); + return new TrieBackedPartition(iterator.partitionKey(), + iterator.columns(), + iterator.stats(), + builder.rowCountIncludingStatic(), + builder.trie(), + iterator.metadata(), + false); + } + + protected static ContentBuilder build(UnfilteredRowIterator iterator, boolean collectDataSize) + { + try + { + ContentBuilder builder = new ContentBuilder(iterator.metadata(), iterator.partitionLevelDeletion(), iterator.isReverseOrder(), collectDataSize); + + builder.addStatic(iterator.staticRow()); + + while (iterator.hasNext()) + builder.addUnfiltered(iterator.next()); + + return builder.complete(); + } + catch (TrieSpaceExhaustedException e) + { + throw new AssertionError(e); + } + } + + /** + * Create a row with the given properties and content, making sure to copy all off-heap data to keep it alive when + * the given access mode requires it. + */ + public static TrieBackedPartition create(DecoratedKey partitionKey, + RegularAndStaticColumns columnMetadata, + EncodingStats encodingStats, + int rowCountIncludingStatic, + Trie trie, + TableMetadata metadata, + EnsureOnHeap ensureOnHeap) + { + return ensureOnHeap == EnsureOnHeap.NOOP + ? new TrieBackedPartition(partitionKey, columnMetadata, encodingStats, rowCountIncludingStatic, trie, metadata, true) + : new WithEnsureOnHeap(partitionKey, columnMetadata, encodingStats, rowCountIncludingStatic, trie, metadata, true, ensureOnHeap); + } + + class RowIterator extends TrieEntriesIterator + { + public RowIterator(Trie trie, Direction direction) + { + super(trie, direction, RowData.class::isInstance); + } + + @Override + protected Row mapContent(Object content, byte[] bytes, int byteLength) + { + var rd = (RowData) content; + return toRow(rd, + metadata.comparator.clusteringFromByteComparable( + ByteBufferAccessor.instance, + ByteComparable.preencoded(BYTE_COMPARABLE_VERSION, bytes, 0, byteLength))); + } + } + + private Iterator rowIterator(Trie trie, Direction direction) + { + return new RowIterator(trie, direction); + } + + static RowData rowToData(Row row) + { + BTreeRow brow = (BTreeRow) row; + return new RowData(brow.getBTree(), row.primaryKeyLivenessInfo(), row.deletion().time(), brow.getMinLocalDeletionTime()); + } + + /** + * Conversion from RowData to Row. TrieBackedPartitionOnHeap overrides this to do the necessary copying + * (hence the non-static method). + */ + Row toRow(RowData data, Clustering clustering) + { + return data.toRow(clustering); + } + + /** + * Put the given unfiltered in the trie. + * @param comparator for converting key to byte-comparable + * @param useRecursive whether the key length is guaranteed short and recursive put can be used + * @param trie destination + * @param row content to put + */ + protected static void putInTrie(ClusteringComparator comparator, boolean useRecursive, InMemoryTrie trie, Row row) throws TrieSpaceExhaustedException + { + trie.putSingleton(comparator.asByteComparable(row.clustering()), rowToData(row), NO_CONFLICT_RESOLVER, useRecursive); + } + + /** + * Check if we can use recursive operations when putting a value in tries. + * True if all types in the clustering keys are fixed length, and total size is small enough. + */ + protected static boolean useRecursive(ClusteringComparator comparator) + { + int length = 1; // terminator + for (AbstractType type : comparator.subtypes()) + if (!type.isValueLengthFixed()) + return false; + else + length += 1 + type.valueLengthIfFixed(); // separator + value + + return length <= MAX_RECURSIVE_KEY_LENGTH; + } + + public TableMetadata metadata() + { + return metadata; + } + + public DecoratedKey partitionKey() + { + return partitionKey; + } + + public DeletionTime partitionLevelDeletion() + { + return deletionInfo().getPartitionDeletion(); + } + + public RegularAndStaticColumns columns() + { + return columns; + } + + public EncodingStats stats() + { + return stats; + } + + public int rowCount() + { + return rowCountIncludingStatic - (hasStaticRow() ? 1 : 0); + } + + public DeletionInfo deletionInfo() + { + return (DeletionInfo) trie.get(ByteComparable.EMPTY); + } + + public ByteComparable path(ClusteringPrefix clustering) + { + return metadata.comparator.asByteComparable(clustering); + } + + public Row staticRow() + { + RowData staticRow = (RowData) trie.get(STATIC_CLUSTERING_PATH); + + if (staticRow != null) + return toRow(staticRow, Clustering.STATIC_CLUSTERING); + else + return Rows.EMPTY_STATIC_ROW; + } + + public boolean isEmpty() + { + return rowCountIncludingStatic == 0 && deletionInfo().isLive(); + } + + private boolean hasStaticRow() + { + return trie.get(STATIC_CLUSTERING_PATH) != null; + } + + public boolean hasRows() + { + return rowCountIncludingStatic > 1 || rowCountIncludingStatic > 0 && !hasStaticRow(); + } + + /** + * Provides read access to the trie for users that can take advantage of it directly (e.g. Memtable). + */ + public Trie trie() + { + return trie; + } + + private Trie nonStaticSubtrie() + { + // skip static row if present - the static clustering sorts before BOTTOM so that it's never included in + // any slices (we achieve this by using the byte ByteSource.EXCLUDED for its representation, which is lower + // than BOTTOM's ByteSource.LT_NEXT_COMPONENT). + return trie.subtrie(BOTTOM_PATH, null); + } + + public Iterator rowIterator() + { + return rowIterator(nonStaticSubtrie(), Direction.FORWARD); + } + + public Iterator rowsIncludingStatic() + { + return rowIterator(trie, Direction.FORWARD); + } + + @Override + public Row lastRow() + { + Iterator reverseIterator = rowIterator(nonStaticSubtrie(), Direction.REVERSE); + return reverseIterator.hasNext() ? reverseIterator.next() : null; + } + + public Row getRow(Clustering clustering) + { + RowData data = (RowData) trie.get(path(clustering)); + + DeletionInfo deletionInfo = deletionInfo(); + RangeTombstone rt = deletionInfo.rangeCovering(clustering); + + // The trie only contains rows, so it doesn't allow to directly account for deletion that should apply to row + // (the partition deletion or the deletion of a range tombstone that covers it). So if needs be, reuse the row + // deletion to carry the proper deletion on the row. + DeletionTime partitionDeletion = deletionInfo.getPartitionDeletion(); + DeletionTime activeDeletion = partitionDeletion; + if (rt != null && rt.deletionTime().supersedes(activeDeletion)) + activeDeletion = rt.deletionTime(); + + if (data == null) + { + // this means our partition level deletion supersedes all other deletions and we don't have to keep the row deletions + if (activeDeletion == partitionDeletion) + return null; + // no need to check activeDeletion.isLive here - if anything superseedes the partitionDeletion + // it must be non-live + return BTreeRow.emptyDeletedRow(clustering, Row.Deletion.regular(activeDeletion)); + } + + Row row = toRow(data, clustering); + if (!activeDeletion.isLive()) + row = row.filter(ColumnFilter.selection(columns()), activeDeletion, true, metadata()); + return row; + } + + public UnfilteredRowIterator unfilteredIterator() + { + return unfilteredIterator(ColumnFilter.selection(columns()), Slices.ALL, false); + } + + public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, Slices slices, boolean reversed) + { + Row staticRow = staticRow(selection, false); + if (slices.size() == 0) + { + DeletionTime partitionDeletion = deletionInfo().getPartitionDeletion(); + return UnfilteredRowIterators.noRowsIterator(metadata(), partitionKey(), staticRow, partitionDeletion, reversed); + } + + return slices.size() == 1 + ? sliceIterator(selection, slices.get(0), reversed, staticRow) + : new SlicesIterator(selection, slices, reversed, staticRow); + } + + public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, NavigableSet> clusteringsInQueryOrder, boolean reversed) + { + Row staticRow = staticRow(selection, false); + if (clusteringsInQueryOrder.isEmpty()) + { + DeletionTime partitionDeletion = deletionInfo().getPartitionDeletion(); + return UnfilteredRowIterators.noRowsIterator(metadata(), partitionKey(), staticRow, partitionDeletion, reversed); + } + + Iterator rowIter = new AbstractIterator() { + + Iterator> clusterings = clusteringsInQueryOrder.iterator(); + + @Override + protected Row computeNext() + { + while (clusterings.hasNext()) + { + Clustering clustering = clusterings.next(); + Object rowData = trie.get(path(clustering)); + if (rowData instanceof RowData) + return toRow((RowData) rowData, clustering); + } + return endOfData(); + } + }; + + // not using DeletionInfo.rangeCovering(Clustering), because it returns the original range tombstone, + // but we need DeletionInfo.rangeIterator(Set) that generates tombstones based on given clustering bound. + Iterator deleteIter = deletionInfo().rangeIterator(clusteringsInQueryOrder, reversed); + + return merge(rowIter, deleteIter, selection, reversed, staticRow); + } + + private UnfilteredRowIterator sliceIterator(ColumnFilter selection, Slice slice, boolean reversed, Row staticRow) + { + ClusteringBound start = slice.start(); + ClusteringBound end = slice.end() == ClusteringBound.TOP ? null : slice.end(); + Iterator rowIter = slice(start, end, reversed); + Iterator deleteIter = deletionInfo().rangeIterator(slice, reversed); + return merge(rowIter, deleteIter, selection, reversed, staticRow); + } + + private Iterator slice(ClusteringBound start, ClusteringBound end, boolean reversed) + { + ByteComparable endPath = end != null ? path(end) : null; + // use BOTTOM as bound to skip over static rows + ByteComparable startPath = start != null ? path(start) : BOTTOM_PATH; + return rowIterator(trie.subtrie(startPath, endPath), Direction.fromBoolean(reversed)); + } + + private Row staticRow(ColumnFilter columns, boolean setActiveDeletionToRow) + { + DeletionTime partitionDeletion = deletionInfo().getPartitionDeletion(); + Row staticRow = staticRow(); + if (columns.fetchedColumns().statics.isEmpty() || (staticRow.isEmpty() && partitionDeletion.isLive())) + return Rows.EMPTY_STATIC_ROW; + + Row row = staticRow.filter(columns, partitionDeletion, setActiveDeletionToRow, metadata()); + return row == null ? Rows.EMPTY_STATIC_ROW : row; + } + + private RowAndDeletionMergeIterator merge(Iterator rowIter, Iterator deleteIter, + ColumnFilter selection, boolean reversed, Row staticRow) + { + return new RowAndDeletionMergeIterator(metadata(), partitionKey(), deletionInfo().getPartitionDeletion(), + selection, staticRow, reversed, stats(), + rowIter, deleteIter, canHaveShadowedData); + } + + + @Override + public String toString() + { + return Partition.toString(this); + } + + class SlicesIterator extends AbstractUnfilteredRowIterator + { + private final Slices slices; + + private int idx; + private Iterator currentSlice; + private final ColumnFilter selection; + + private SlicesIterator(ColumnFilter selection, + Slices slices, + boolean isReversed, + Row staticRow) + { + super(TrieBackedPartition.this.metadata(), TrieBackedPartition.this.partitionKey(), + TrieBackedPartition.this.partitionLevelDeletion(), + selection.fetchedColumns(), staticRow, isReversed, TrieBackedPartition.this.stats()); + this.selection = selection; + this.slices = slices; + } + + protected Unfiltered computeNext() + { + while (true) + { + if (currentSlice == null) + { + if (idx >= slices.size()) + return endOfData(); + + int sliceIdx = isReverseOrder ? slices.size() - idx - 1 : idx; + currentSlice = sliceIterator(selection, slices.get(sliceIdx), isReverseOrder, Rows.EMPTY_STATIC_ROW); + idx++; + } + + if (currentSlice.hasNext()) + return currentSlice.next(); + + currentSlice = null; + } + } + } + + + /** + * An snapshot of the current TrieBackedPartition data, copied on heap when retrieved. + */ + private static final class WithEnsureOnHeap extends TrieBackedPartition + { + final DeletionInfo onHeapDeletion; + EnsureOnHeap ensureOnHeap; + + public WithEnsureOnHeap(DecoratedKey partitionKey, + RegularAndStaticColumns columns, + EncodingStats stats, + int rowCountIncludingStatic, + Trie trie, + TableMetadata metadata, + boolean canHaveShadowedData, + EnsureOnHeap ensureOnHeap) + { + super(partitionKey, columns, stats, rowCountIncludingStatic, trie, metadata, canHaveShadowedData); + this.ensureOnHeap = ensureOnHeap; + this.onHeapDeletion = ensureOnHeap.applyToDeletionInfo(super.deletionInfo()); + } + + @Override + public Row toRow(RowData data, Clustering clustering) + { + return ensureOnHeap.applyToRow(super.toRow(data, clustering)); + } + + @Override + public DeletionInfo deletionInfo() + { + return onHeapDeletion; + } + } + + /** + * Resolver for operations with trie-backed partitions. We don't permit any overwrites/merges. + */ + public static final InMemoryTrie.UpsertTransformer NO_CONFLICT_RESOLVER = + (existing, update) -> + { + if (existing != null) + throw new AssertionError("Unique rows expected."); + return update; + }; + + /** + * Helper class for constructing tries and deletion info from an iterator or flowable partition. + * + * Note: This is not collecting any stats or columns! + */ + public static class ContentBuilder + { + final TableMetadata metadata; + final ClusteringComparator comparator; + + private final MutableDeletionInfo.Builder deletionBuilder; + private final InMemoryTrie trie; + + private final boolean useRecursive; + private final boolean collectDataSize; + + private int rowCountIncludingStatic; + private long dataSize; + + public ContentBuilder(TableMetadata metadata, DeletionTime partitionLevelDeletion, boolean isReverseOrder, boolean collectDataSize) + { + this.metadata = metadata; + this.comparator = metadata.comparator; + + this.deletionBuilder = MutableDeletionInfo.builder(partitionLevelDeletion, + comparator, + isReverseOrder); + this.trie = InMemoryTrie.shortLived(BYTE_COMPARABLE_VERSION); + + this.useRecursive = useRecursive(comparator); + this.collectDataSize = collectDataSize; + + rowCountIncludingStatic = 0; + dataSize = 0; + } + + public ContentBuilder addStatic(Row staticRow) throws TrieSpaceExhaustedException + { + if (!staticRow.isEmpty()) + return addRow(staticRow); + else + return this; + } + + public ContentBuilder addRow(Row row) throws TrieSpaceExhaustedException + { + putInTrie(comparator, useRecursive, trie, row); + ++rowCountIncludingStatic; + if (collectDataSize) + dataSize += row.dataSize(); + return this; + } + + public ContentBuilder addRangeTombstoneMarker(RangeTombstoneMarker unfiltered) + { + deletionBuilder.add(unfiltered); + return this; + } + + public ContentBuilder addUnfiltered(Unfiltered unfiltered) throws TrieSpaceExhaustedException + { + if (unfiltered.kind() == Unfiltered.Kind.ROW) + return addRow((Row) unfiltered); + else + return addRangeTombstoneMarker((RangeTombstoneMarker) unfiltered); + } + + public ContentBuilder complete() throws TrieSpaceExhaustedException + { + MutableDeletionInfo deletionInfo = deletionBuilder.build(); + trie.putRecursive(ByteComparable.EMPTY, deletionInfo, NO_CONFLICT_RESOLVER); // will throw if called more than once + // dataSize does not include the deletion info bytes + return this; + } + + public Trie trie() + { + return trie; + } + + public int rowCountIncludingStatic() + { + return rowCountIncludingStatic; + } + + public int dataSize() + { + assert collectDataSize; + return Ints.saturatedCast(dataSize); + } + } +} diff --git a/src/java/org/apache/cassandra/db/partitions/TriePartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/TriePartitionUpdate.java new file mode 100644 index 000000000000..bd5e120b9dd0 --- /dev/null +++ b/src/java/org/apache/cassandra/db/partitions/TriePartitionUpdate.java @@ -0,0 +1,639 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.partitions; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterators; +import com.google.common.primitives.Ints; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.Columns; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.MutableDeletionInfo; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; +import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.db.tries.Trie; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.btree.BTree; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * A trie-backed PartitionUpdate. Immutable. + *

    + * Provides factories for simple variations (e.g. singleRowUpdate) and a mutable builder for constructing one. + * The builder holds a mutable trie to which content may be added in any order, also taking care of + * merging any duplicate rows, and keeping track of statistics and column coverage. + */ +public class TriePartitionUpdate extends TrieBackedPartition implements PartitionUpdate +{ + protected static final Logger logger = LoggerFactory.getLogger(TriePartitionUpdate.class); + + public static final Factory FACTORY = new TrieFactory(); + + final int dataSize; + + private TriePartitionUpdate(TableMetadata metadata, + DecoratedKey key, + RegularAndStaticColumns columns, + EncodingStats stats, + int rowCountIncludingStatic, + int dataSize, + Trie trie, + boolean canHaveShadowedData) + { + super(key, columns, stats, rowCountIncludingStatic, trie, metadata, canHaveShadowedData); + this.dataSize = dataSize; + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof TriePartitionUpdate)) + return false; + + TriePartitionUpdate that = (TriePartitionUpdate) obj; + return partitionKey.equals(that.partitionKey) + && metadata().id.equals(that.metadata().id) + && deletionInfo().equals(that.deletionInfo()) + && staticRow().equals(that.staticRow()) + && Iterators.elementsEqual(rowIterator(), that.rowIterator()); + } + + + private static InMemoryTrie newTrie(DeletionInfo deletion) + { + InMemoryTrie trie = InMemoryTrie.shortLived(BYTE_COMPARABLE_VERSION); + try + { + trie.putRecursive(ByteComparable.EMPTY, deletion, NO_CONFLICT_RESOLVER); + } + catch (TrieSpaceExhaustedException e) + { + throw new AssertionError(e); + } + return trie; + } + + /** + * Creates a empty immutable partition update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the created update. + * + * @return the newly created empty (and immutable) update. + */ + public static TriePartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey key) + { + return new TriePartitionUpdate(metadata, + key, + RegularAndStaticColumns.NONE, + EncodingStats.NO_STATS, + 0, + 0, + newTrie(MutableDeletionInfo.live()), + false); + } + + /** + * Creates an immutable partition update that entirely deletes a given partition. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition that the created update should delete. + * @param timestamp the timestamp for the deletion. + * @param nowInSec the current time in seconds to use as local deletion time for the partition deletion. + * + * @return the newly created partition deletion update. + */ + public static TriePartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec) + { + MutableDeletionInfo deletion = new MutableDeletionInfo(timestamp, nowInSec); + return new TriePartitionUpdate(metadata, + key, + RegularAndStaticColumns.NONE, + new EncodingStats(timestamp, nowInSec, LivenessInfo.NO_TTL), + 0, + 0, + newTrie(deletion), + false); + } + + /** + * Creates an immutable partition update that contains a single row update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition to update. + * @param row the row for the update, may be a regular or static row and cannot be null. + * + * @return the newly created partition update containing only {@code row}. + */ + public static TriePartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey key, Row row) + { + EncodingStats stats = EncodingStats.Collector.forRow(row); + InMemoryTrie trie = newTrie(DeletionInfo.LIVE); + + RegularAndStaticColumns columns; + if (row.isStatic()) + columns = new RegularAndStaticColumns(Columns.from(row.columns()), Columns.NONE); + else + columns = new RegularAndStaticColumns(Columns.NONE, Columns.from(row.columns())); + + try + { + putInTrie(metadata.comparator, useRecursive(metadata.comparator), trie, row); + } + catch (TrieSpaceExhaustedException e) + { + throw new AssertionError(e); + } + + return new TriePartitionUpdate(metadata, key, columns, stats, 1, row.dataSize(), trie, false); + } + + /** + * Creates an immutable partition update that contains a single row update. + * + * @param metadata the metadata for the created update. + * @param key the partition key for the partition to update. + * @param row the row for the update. + * + * @return the newly created partition update containing only {@code row}. + */ + public static TriePartitionUpdate singleRowUpdate(TableMetadata metadata, ByteBuffer key, Row row) + { + return singleRowUpdate(metadata, metadata.partitioner.decorateKey(key), row); + } + + /** + * Turns the given iterator into an update. + * + * @param iterator the iterator to turn into updates. + * + * Warning: this method does not close the provided iterator, it is up to + * the caller to close it. + */ + @SuppressWarnings("resource") + public static TriePartitionUpdate fromIterator(UnfilteredRowIterator iterator) + { + ContentBuilder builder = build(iterator, true); + + return new TriePartitionUpdate(iterator.metadata(), + iterator.partitionKey(), + iterator.columns(), + iterator.stats(), + builder.rowCountIncludingStatic(), + builder.dataSize(), + builder.trie(), + false); + } + + public static TriePartitionUpdate asTrieUpdate(PartitionUpdate update) + { + if (update instanceof TriePartitionUpdate) + return (TriePartitionUpdate) update; + + try (UnfilteredRowIterator iterator = update.unfilteredIterator()) + { + return fromIterator(iterator); + } + } + + public static Trie asMergableTrie(PartitionUpdate update) + { + return asTrieUpdate(update).trie.prefixedBy(update.partitionKey()); + } + + /** + * Modify this update to set every timestamp for live data to {@code newTimestamp} and + * every deletion timestamp to {@code newTimestamp - 1}. + * + * There is no reason to use that except on the Paxos code path, where we need to ensure that + * anything inserted uses the ballot timestamp (to respect the order of updates decided by + * the Paxos algorithm). We use {@code newTimestamp - 1} for deletions because tombstones + * always win on timestamp equality and we don't want to delete our own insertions + * (typically, when we overwrite a collection, we first set a complex deletion to delete the + * previous collection before adding new elements. If we were to set that complex deletion + * to the same timestamp that the new elements, it would delete those elements). And since + * tombstones always wins on timestamp equality, using -1 guarantees our deletion will still + * delete anything from a previous update. + */ + @Override + public TriePartitionUpdate withUpdatedTimestamps(long newTimestamp) + { + + InMemoryTrie t = InMemoryTrie.shortLived(BYTE_COMPARABLE_VERSION); + try + { + t.apply(trie, new InMemoryTrie.UpsertTransformer() + { + public Object apply(Object shouldBeNull, Object o) + { + assert shouldBeNull == null; + if (o instanceof RowData) + return applyRowData((RowData) o); + else + return applyDeletion((DeletionInfo) o); + } + + public RowData applyRowData(RowData update) + { + LivenessInfo newInfo = update.livenessInfo.isEmpty() + ? update.livenessInfo + : update.livenessInfo.withUpdatedTimestamp(newTimestamp); + DeletionTime newDeletion = update.deletion.isLive() + ? DeletionTime.LIVE + : DeletionTime.build(newTimestamp - 1, update.deletion.localDeletionTime()); + + return new RowData(BTree.transformAndFilter(update.columnsBTree, + (ColumnData cd) -> cd.updateAllTimestamp(newTimestamp)), + newInfo, newDeletion); + } + + public DeletionInfo applyDeletion(DeletionInfo update) + { + if (update.isLive()) + return update; + + MutableDeletionInfo mdi = update.mutableCopy(); + mdi.updateAllTimestamp(newTimestamp - 1); + return mdi; + } + }, x -> false); + } + catch (TrieSpaceExhaustedException e) + { + throw new AssertionError(e); + } + return new TriePartitionUpdate(metadata, partitionKey, columns, stats, rowCountIncludingStatic, dataSize, t, canHaveShadowedData); + } + + /** + * The number of "operations" contained in the update. + *

    + * This is used by {@code Memtable} to approximate how much work this update does. In practice, this + * count how many rows are updated and how many ranges are deleted by the partition update. + * + * @return the number of "operations" performed by the update. + */ + @Override + public int operationCount() + { + return rowCountIncludingStatic + + deletionInfo().rangeCount() + + (deletionInfo().getPartitionDeletion().isLive() ? 0 : 1); + } + + /** + * The size of the data contained in this update. + * + * @return the size of the data contained in this update. + */ + @Override + public int dataSize() + { + return dataSize; + } + + /** + * The size of the data contained in this update. + * + * @return the size of the data contained in this update. + */ + @Override + public long unsharedHeapSize() + { + assert trie instanceof InMemoryTrie; + InMemoryTrie inMemoryTrie = (InMemoryTrie) trie; + long heapSize = inMemoryTrie.usedSizeOnHeap(); + for (Object o : inMemoryTrie.values()) + { + if (o instanceof RowData) + heapSize += ((RowData) o).unsharedHeapSizeExcludingData(); + else + heapSize += ((DeletionInfo) o).unsharedHeapSize(); + } + return heapSize; + } + + /** + * Validates the data contained in this update. + * + * @throws org.apache.cassandra.serializers.MarshalException if some of the data contained in this update is corrupted. + */ + @Override + public void validate() + { + for (Iterator it = rowsIncludingStatic(); it.hasNext();) + { + Row row = it.next(); + metadata().comparator.validate(row.clustering()); + for (ColumnData cd : row) + cd.validate(); + } + } + + /** + * The maximum timestamp used in this update. + * + * @return the maximum timestamp used in this update. + */ + @Override + public long maxTimestamp() + { + long maxTimestamp = deletionInfo().maxTimestamp(); + for (Iterator it = rowsIncludingStatic(); it.hasNext();) + maxTimestamp = Math.max(maxTimestamp, Rows.collectMaxTimestamp(it.next())); + + return maxTimestamp; + } + + /** + * For an update on a counter table, returns a list containing a {@code CounterMark} for + * every counter contained in the update. + * + * @return a list with counter marks for every counter in this update. + */ + @Override + public List collectCounterMarks() + { + assert metadata().isCounter(); + // We will take aliases on the rows of this update, and update them in-place. So we should be sure the + // update is now immutable for all intent and purposes. + List marks = new ArrayList<>(); + for (Iterator it = rowsIncludingStatic(); it.hasNext();) + { + Row row = it.next(); + addMarksForRow(row, marks); + } + return marks; + } + + private static void addMarksForRow(Row row, List marks) + { + for (Cell cell : row.cells()) + { + if (cell.isCounterCell()) + marks.add(new CounterMark(row, cell.column(), cell.path())); + } + } + + @Override + public PartitionUpdate withOnlyPresentColumns() + { + Set columnSet = new HashSet<>(); + + for (Row row : rows()) + for (ColumnData column : row) + columnSet.add(column.column()); + + RegularAndStaticColumns columns = RegularAndStaticColumns.builder().addAll(columnSet).build(); + return new TriePartitionUpdate(metadata, partitionKey, columns, stats, rowCountIncludingStatic, dataSize, trie, false); + } + + /** + * Builder for PartitionUpdates + * + * This class is not thread safe, but the PartitionUpdate it produces is (since it is immutable). + */ + public static class Builder implements PartitionUpdate.Builder + { + private final TableMetadata metadata; + private final DecoratedKey key; + private final MutableDeletionInfo deletionInfo; + private final boolean canHaveShadowedData; + private final RegularAndStaticColumns columns; + private final InMemoryTrie trie = InMemoryTrie.shortLived(BYTE_COMPARABLE_VERSION); + private final EncodingStats.Collector statsCollector = new EncodingStats.Collector(); + private final boolean useRecursive; + private int rowCountIncludingStatic; + private long dataSize; + + public Builder(TableMetadata metadata, + DecoratedKey key, + RegularAndStaticColumns columns) + { + this(metadata, key, columns, true, Rows.EMPTY_STATIC_ROW, DeletionInfo.LIVE); + } + + private Builder(TableMetadata metadata, + DecoratedKey key, + RegularAndStaticColumns columns, + boolean canHaveShadowedData, + Row staticRow, + DeletionInfo deletionInfo) + { + this.metadata = metadata; + this.key = key; + this.columns = columns; + this.canHaveShadowedData = canHaveShadowedData; + this.deletionInfo = deletionInfo.mutableCopy(); + useRecursive = useRecursive(metadata.comparator); + rowCountIncludingStatic = 0; + dataSize = 0; + add(staticRow); + } + + // This is wasteful, only to be used for testing. + @VisibleForTesting + public Builder(TriePartitionUpdate base) + { + this(base.metadata, base.partitionKey, base.columns(), base.canHaveShadowedData, Rows.EMPTY_STATIC_ROW, base.deletionInfo()); + for (Iterator it = base.rowsIncludingStatic(); it.hasNext();) + add(it.next()); + } + + /** + * Adds a row to this update. + *

    + * There is no particular assumption made on the order of row added to a partition update. It is further + * allowed to add the same row (more precisely, multiple row objects for the same clustering). + *

    + * Note however that the columns contained in the added row must be a subset of the columns used when + * creating this update. + * + * @param row the row to add. + */ + public void add(Row row) + { + if (row.isEmpty()) + return; + + // this assert is expensive, and possibly of limited value; we should consider removing it + // or introducing a new class of assertions for test purposes + assert (row.isStatic() ? columns().statics : columns().regulars).containsAll(row.columns()) + : (row.isStatic() ? columns().statics : columns().regulars) + " is not superset of " + row.columns(); + + try + { + trie.putSingleton(metadata.comparator.asByteComparable(row.clustering()), + row, + this::merge, + useRecursive); + } + catch (TrieSpaceExhaustedException e) + { + throw new AssertionError(e); + } + Rows.collectStats(row, statsCollector); + } + + public void addPartitionDeletion(DeletionTime deletionTime) + { + deletionInfo.add(deletionTime); + } + + public void add(RangeTombstone range) + { + deletionInfo.add(range, metadata.comparator); + } + + public DecoratedKey partitionKey() + { + return key; + } + + public TableMetadata metadata() + { + return metadata; + } + + public TriePartitionUpdate build() + { + try + { + trie.putRecursive(ByteComparable.EMPTY, deletionInfo, NO_CONFLICT_RESOLVER); + } + catch (TrieSpaceExhaustedException e) + { + throw new AssertionError(e); + } + deletionInfo.collectStats(statsCollector); + TriePartitionUpdate pu = new TriePartitionUpdate(metadata, + partitionKey(), + columns, + statsCollector.get(), + rowCountIncludingStatic, + Ints.saturatedCast(dataSize), + trie, + canHaveShadowedData); + + return pu; + } + + RowData merge(Object existing, Row update) + { + if (existing != null) + { + // this is not expected to happen much, so going through toRow and the existing size is okay + RowData rowData = (RowData) existing; + update = Rows.merge(rowData.toRow(update.clustering()), update); + dataSize += update.dataSize() - rowData.dataSize(); + } + else + { + ++rowCountIncludingStatic; + dataSize += update.dataSize(); + } + + return rowToData(update); + } + + public RegularAndStaticColumns columns() + { + return columns; + } + + public DeletionTime partitionLevelDeletion() + { + return deletionInfo.getPartitionDeletion(); + } + + @Override + public String toString() + { + return "Builder{" + + "metadata=" + metadata + + ", key=" + key + + ", deletionInfo=" + deletionInfo + + ", canHaveShadowedData=" + canHaveShadowedData + + ", columns=" + columns + + '}'; + } + } + + public static class TrieFactory implements PartitionUpdate.Factory + { + + @Override + public PartitionUpdate.Builder builder(TableMetadata metadata, DecoratedKey partitionKey, RegularAndStaticColumns columns, int initialRowCapacity) + { + return new TriePartitionUpdate.Builder(metadata, partitionKey, columns); + } + + @Override + public PartitionUpdate emptyUpdate(TableMetadata metadata, DecoratedKey partitionKey) + { + return TriePartitionUpdate.emptyUpdate(metadata, partitionKey); + } + + @Override + public PartitionUpdate singleRowUpdate(TableMetadata metadata, DecoratedKey valueKey, Row row) + { + return TriePartitionUpdate.singleRowUpdate(metadata, valueKey, row); + } + + @Override + public PartitionUpdate fullPartitionDelete(TableMetadata metadata, DecoratedKey key, long timestamp, long nowInSec) + { + return TriePartitionUpdate.fullPartitionDelete(metadata, key, timestamp, nowInSec); + } + + @Override + public PartitionUpdate fromIterator(UnfilteredRowIterator iterator) + { + return TriePartitionUpdate.fromIterator(iterator); + } + + @Override + public PartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter) + { + return TriePartitionUpdate.fromIterator(UnfilteredRowIterators.withOnlyQueriedData(iterator, filter)); + } + } +} diff --git a/src/java/org/apache/cassandra/db/partitions/TriePartitionUpdater.java b/src/java/org/apache/cassandra/db/partitions/TriePartitionUpdater.java new file mode 100644 index 000000000000..832c38dbf3de --- /dev/null +++ b/src/java/org/apache/cassandra/db/partitions/TriePartitionUpdater.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.partitions; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.marshal.ByteArrayAccessor; +import org.apache.cassandra.db.memtable.TrieMemtable; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.index.transactions.UpdateTransaction; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.memory.Cloner; + +import static org.apache.cassandra.db.partitions.TrieBackedPartition.RowData; + +/** + * The function we provide to the trie utilities to perform any partition and row inserts and updates + */ +public final class TriePartitionUpdater +extends BasePartitionUpdater +implements InMemoryTrie.UpsertTransformerWithKeyProducer +{ + private final UpdateTransaction indexer; + private final TableMetadata metadata; + private TrieMemtable.PartitionData currentPartition; + private final TrieMemtable.MemtableShard owner; + public int partitionsAdded = 0; + + public TriePartitionUpdater(Cloner cloner, + UpdateTransaction indexer, + TableMetadata metadata, + TrieMemtable.MemtableShard owner) + { + super(cloner); + this.indexer = indexer; + this.metadata = metadata; + this.owner = owner; + } + + @Override + public Object apply(Object existing, Object update, InMemoryTrie.KeyProducer keyState) + { + if (update instanceof RowData) + return applyRow((RowData) existing, (RowData) update, keyState); + else if (update instanceof DeletionInfo) + return applyDeletion((TrieMemtable.PartitionData) existing, (DeletionInfo) update); + else + throw new AssertionError("Unexpected update type: " + update.getClass()); + } + + /** + * Called when a row needs to be copied to the Memtable trie. + * + * @param existing Existing RowData for this clustering, or null if there isn't any. + * @param insert RowData to be inserted. + * @param keyState Used to obtain the path through which this node was reached. + * @return the insert row, or the merged row, copied using our allocator + */ + private RowData applyRow(RowData existing, RowData insert, InMemoryTrie.KeyProducer keyState) + { + if (existing == null) + { + RowData data = insert.clone(cloner); + + if (indexer != UpdateTransaction.NO_OP) + indexer.onInserted(data.toRow(clusteringFor(keyState))); + + this.dataSize += data.dataSize(); + this.heapSize += data.unsharedHeapSizeExcludingData(); + currentPartition.markInsertedRows(1); // null pointer here means a problem in applyDeletion + return data; + } + else + { + // data and heap size are updated during merge through the PostReconciliationFunction interface + RowData reconciled = merge(existing, insert); + + if (indexer != UpdateTransaction.NO_OP) + { + Clustering clustering = clusteringFor(keyState); + indexer.onUpdated(existing.toRow(clustering), reconciled.toRow(clustering)); + } + + return reconciled; + } + } + + private RowData merge(RowData existing, RowData update) + { + + LivenessInfo livenessInfo = LivenessInfo.merge(update.livenessInfo, existing.livenessInfo); + DeletionTime deletion = DeletionTime.merge(update.deletion, existing.deletion); + if (deletion.deletes(livenessInfo)) + livenessInfo = LivenessInfo.EMPTY; + + Object[] tree = BTreeRow.mergeRowBTrees(this, + existing.columnsBTree, update.columnsBTree, + deletion, existing.deletion); + return new RowData(tree, livenessInfo, deletion); + } + + private Clustering clusteringFor(InMemoryTrie.KeyProducer keyState) + { + return metadata.comparator.clusteringFromByteComparable( + ByteArrayAccessor.instance, + ByteComparable.preencoded(TrieBackedPartition.BYTE_COMPARABLE_VERSION, + keyState.getBytes(TrieMemtable.IS_PARTITION_BOUNDARY))); + } + + /** + * Called at the partition boundary to merge the existing and new metadata associated with the partition. This needs + * to update the deletion time with any new deletion introduced by the update, but also make sure that the + * statistics we track for the partition (dataSize) are updated for the changes caused by merging the update's rows + * (note that this is called _after_ the rows of the partition have been merged, on the return path of the + * recursion). + * + * @param existing Any partition data already associated with the partition. + * @param update The update, always non-null. + * @return the combined partition data, copying any updated deletion information to heap. + */ + private TrieMemtable.PartitionData applyDeletion(TrieMemtable.PartitionData existing, DeletionInfo update) + { + if (indexer != UpdateTransaction.NO_OP) + { + if (!update.getPartitionDeletion().isLive()) + indexer.onPartitionDeletion(update.getPartitionDeletion()); + if (update.hasRanges()) + update.rangeIterator(false).forEachRemaining(indexer::onRangeTombstone); + } + + if (existing == null) + { + // Note: Always on-heap, regardless of cloner + TrieMemtable.PartitionData newRef = new TrieMemtable.PartitionData(update, owner); + this.heapSize += newRef.unsharedHeapSize(); + ++this.partitionsAdded; + return currentPartition = newRef; + } + + assert owner == existing.owner; + if (update.isLive() || !update.mayModify(existing)) + return currentPartition = existing; + + // Note: Always on-heap, regardless of cloner + TrieMemtable.PartitionData merged = new TrieMemtable.PartitionData(existing, update); + this.heapSize += merged.unsharedHeapSize() - existing.unsharedHeapSize(); + return currentPartition = merged; + } +} diff --git a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java index e68603c9f3dd..906c7a42b841 100644 --- a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java +++ b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java @@ -19,18 +19,30 @@ import java.io.IOError; import java.io.IOException; -import java.util.*; - -import org.apache.cassandra.db.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Digest; +import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.LazilyInitializedUnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.db.transform.FilteredPartitions; import org.apache.cassandra.db.transform.MorePartitions; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.CloseableIterator; import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.Reducer; /** * Static methods to work with partition iterators. @@ -128,7 +140,7 @@ public static UnfilteredPartitionIterator merge(final List merged = MergeIterator.get(iterators, partitionComparator, new MergeIterator.Reducer() + final CloseableIterator merged = MergeIterator.getCloseable(iterators, partitionComparator, new Reducer() { private final List toMerge = new ArrayList<>(iterators.size()); @@ -152,7 +164,7 @@ public void reduce(int idx, UnfilteredRowIterator current) } } - protected UnfilteredRowIterator getReduced() + public UnfilteredRowIterator getReduced() { UnfilteredRowIterators.MergeListener rowListener = listener == null ? null @@ -178,7 +190,7 @@ protected UnfilteredRowIterator getReduced() return UnfilteredRowIterators.merge(toMerge, rowListener); } - protected void onKeyChange() + public void onKeyChange() { toMerge.clear(); if (preserveOrder) @@ -226,7 +238,7 @@ public static UnfilteredPartitionIterator mergeLazily(final List merged = MergeIterator.get(iterators, partitionComparator, new MergeIterator.Reducer() + final CloseableIterator merged = MergeIterator.getCloseable(iterators, partitionComparator, new Reducer() { private final List toMerge = new ArrayList<>(iterators.size()); @@ -235,7 +247,7 @@ public void reduce(int idx, UnfilteredRowIterator current) toMerge.add(current); } - protected UnfilteredRowIterator getReduced() + public UnfilteredRowIterator getReduced() { return new LazilyInitializedUnfilteredRowIterator(toMerge.get(0).partitionKey()) { @@ -246,7 +258,7 @@ protected UnfilteredRowIterator initializeIterator() }; } - protected void onKeyChange() + public void onKeyChange() { toMerge.clear(); } diff --git a/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java b/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java index 4e54d6ee7782..1793c46d5a34 100644 --- a/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java +++ b/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java @@ -20,13 +20,20 @@ import java.io.IOException; import java.util.Collection; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.RepairFinishedCompactionTask; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.dht.Bounds; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; @@ -37,10 +44,12 @@ import org.apache.cassandra.repair.ValidationPartitionIterator; import org.apache.cassandra.repair.NoSuchRepairSessionException; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.repair.consistent.LocalSessions; import org.apache.cassandra.service.ActiveRepairService; public class CassandraTableRepairManager implements TableRepairManager { + private static final Logger logger = LoggerFactory.getLogger(CassandraTableRepairManager.class); private final ColumnFamilyStore cfs; private final SharedContext ctx; @@ -68,9 +77,38 @@ public Future submitValidation(Callable validation) } @Override - public void incrementalSessionCompleted(TimeUUID sessionID) + public synchronized void incrementalSessionCompleted(TimeUUID sessionID) { - CompactionManager.instance.submitBackground(cfs); + LocalSessions sessions = ActiveRepairService.instance().consistent.local; + if (sessions.isSessionInProgress(sessionID)) + return; + + Set pendingRepairSSTables = cfs.getPendingRepairSSTables(sessionID); + if (pendingRepairSSTables.isEmpty()) + return; + + logger.debug("Number of sstables in pending repair: {} for session {}", pendingRepairSSTables.size(), sessionID); + LifecycleTransaction txn = cfs.getTracker().tryModify(pendingRepairSSTables, OperationType.COMPACTION); + if (txn == null) + return; + + boolean isTransient = false; + for (SSTableReader sstable : pendingRepairSSTables) + { + if (sstable.isTransient()) + { + isTransient = true; + break; + } + } + + long repairedAt = sessions.getFinalSessionRepairedAt(sessionID); + RepairFinishedCompactionTask task = new RepairFinishedCompactionTask(cfs, + txn, + sessionID, + repairedAt, + isTransient); + task.run(); } @Override @@ -95,6 +133,8 @@ public boolean apply(SSTableReader sstable) } catch (Exception ex) { + if (ex instanceof InterruptedException) + Thread.currentThread().interrupt(); throw new RuntimeException(String.format("Unable to take a snapshot %s on %s.%s", name, cfs.metadata.keyspace, cfs.metadata.name), ex); } diff --git a/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java b/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java index a31a7038b069..8b5b39d5cbc7 100644 --- a/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java +++ b/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java @@ -31,17 +31,14 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Collections2; import com.google.common.collect.Maps; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; -import org.apache.cassandra.db.compaction.ActiveCompactionsTracker; import org.apache.cassandra.db.compaction.CompactionController; import org.apache.cassandra.db.compaction.CompactionIterator; -import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.View; @@ -50,13 +47,14 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.ScannerList; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.metrics.TopPartitionTracker; +import org.apache.cassandra.repair.NoSuchRepairSessionException; import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.repair.ValidationPartitionIterator; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; -import org.apache.cassandra.repair.NoSuchRepairSessionException; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Refs; @@ -106,9 +104,9 @@ public static long getDefaultGcBefore(ColumnFamilyStore cfs, long nowInSec) private static class ValidationCompactionIterator extends CompactionIterator { - public ValidationCompactionIterator(List scanners, ValidationCompactionController controller, long nowInSec, ActiveCompactionsTracker activeCompactions, TopPartitionTracker.Collector topPartitionCollector) + public ValidationCompactionIterator(List scanners, ValidationCompactionController controller, long nowInSec, TopPartitionTracker.Collector topPartitionCollector) { - super(OperationType.VALIDATION, scanners, controller, nowInSec, nextTimeUUID(), activeCompactions, topPartitionCollector); + super(OperationType.VALIDATION, scanners, controller, nowInSec, nextTimeUUID(), topPartitionCollector, null); } } @@ -165,7 +163,7 @@ else if (isIncremental) private final boolean isGlobalSnapshotValidation; private final boolean isSnapshotValidation; - private final AbstractCompactionStrategy.ScannerList scanners; + private final ScannerList scanners; private final ValidationCompactionController controller; private final CompactionIterator ci; @@ -195,7 +193,7 @@ public CassandraValidationIterator(ColumnFamilyStore cfs, SharedContext ctx, Col } else { - if (!isIncremental) + if (!isIncremental && DatabaseDescriptor.enableMemtableAndCommitLog()) { // flush first so everyone is validating data that is as similar as possible cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.VALIDATION); @@ -220,8 +218,8 @@ public CassandraValidationIterator(ColumnFamilyStore cfs, SharedContext ctx, Col cfs.getTableName()); controller = new ValidationCompactionController(cfs, getDefaultGcBefore(cfs, nowInSec)); - scanners = cfs.getCompactionStrategyManager().getScanners(sstables, ranges); - ci = new ValidationCompactionIterator(scanners.scanners, controller, nowInSec, CompactionManager.instance.active, topPartitionCollector); + scanners = cfs.getCompactionStrategyContainer().getScanners(sstables, ranges); + ci = new ValidationCompactionIterator(scanners.scanners, controller, nowInSec, topPartitionCollector); long allPartitions = 0; rangePartitionCounts = Maps.newHashMapWithExpectedSize(ranges.size()); @@ -247,7 +245,7 @@ public CassandraValidationIterator(ColumnFamilyStore cfs, SharedContext ctx, Col @Override public long getBytesRead() { - return ci.getBytesRead(); + return ci.bytesRead(); } @Override @@ -282,6 +280,12 @@ public TableMetadata metadata() return cfs.metadata.get(); } + @Override + public CompactionIterator getCompactionIterator() + { + return ci; + } + @Override public boolean hasNext() { diff --git a/src/java/org/apache/cassandra/db/repair/PendingAntiCompaction.java b/src/java/org/apache/cassandra/db/repair/PendingAntiCompaction.java index c39a667be90d..94d4e76664cd 100644 --- a/src/java/org/apache/cassandra/db/repair/PendingAntiCompaction.java +++ b/src/java/org/apache/cassandra/db/repair/PendingAntiCompaction.java @@ -44,9 +44,9 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.compaction.CompactionInfo; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; @@ -147,8 +147,8 @@ public boolean apply(SSTableReader sstable) } return false; } - Collection cis = CompactionManager.instance.active.getCompactionsForSSTable(sstable, OperationType.ANTICOMPACTION); - if (cis != null && !cis.isEmpty()) + Collection ops = CompactionManager.instance.active.getOperationsForSSTable(sstable, OperationType.ANTICOMPACTION); + if (ops != null && !ops.isEmpty()) { // todo: start tracking the parent repair session id that created the anticompaction to be able to give a better error messsage here: StringBuilder sb = new StringBuilder(); @@ -157,8 +157,10 @@ public boolean apply(SSTableReader sstable) sb.append(" has failed because it encountered intersecting sstables belonging to another incremental repair session. "); sb.append("This is caused by starting multiple conflicting incremental repairs at the same time. "); sb.append("Conflicting anticompactions: "); - for (CompactionInfo ci : cis) - sb.append(ci.getTaskId() == null ? "no compaction id" : ci.getTaskId()).append(':').append(ci.getSSTables()).append(','); + for (TableOperation.Progress op : ops) + { + sb.append(op.operationId() == null ? "no compaction id" : op.operationId()).append(':').append(op.sstables()).append(','); + } throw new SSTableAcquisitionException(sb.toString()); } return true; @@ -216,7 +218,7 @@ private AcquireResult acquireTuple() protected AcquireResult acquireSSTables() { - return cfs.runWithCompactionsDisabled(this::acquireTuple, predicate, OperationType.ANTICOMPACTION, false, false, false); + return cfs.runWithCompactionsDisabled(this::acquireTuple, predicate, OperationType.ANTICOMPACTION, false, false, false, TableOperation.StopTrigger.ANTICOMPACTION); } public AcquireResult call() diff --git a/src/java/org/apache/cassandra/db/rows/AbstractCell.java b/src/java/org/apache/cassandra/db/rows/AbstractCell.java index 69ca0b1c315d..e4c306965d1e 100644 --- a/src/java/org/apache/cassandra/db/rows/AbstractCell.java +++ b/src/java/org/apache/cassandra/db/rows/AbstractCell.java @@ -127,6 +127,12 @@ public int dataSize() + (path == null ? 0 : path.dataSize()); } + @Override + public int liveDataSize(long nowInSec) + { + return isLive(nowInSec) ? dataSize() : 0; + } + public void digest(Digest digest) { if (isCounterCell()) @@ -171,6 +177,11 @@ public long maxTimestamp() return timestamp(); } + public long minTimestamp() + { + return timestamp(); + } + public static boolean equals(Cell left, Cell right) { return left.column().equals(right.column()) diff --git a/src/java/org/apache/cassandra/db/rows/AbstractRow.java b/src/java/org/apache/cassandra/db/rows/AbstractRow.java index 416a781fa934..99326613f0e1 100644 --- a/src/java/org/apache/cassandra/db/rows/AbstractRow.java +++ b/src/java/org/apache/cassandra/db/rows/AbstractRow.java @@ -23,6 +23,7 @@ import com.google.common.collect.Iterables; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.marshal.ValueAccessor; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Digest; @@ -140,10 +141,10 @@ public String toString(TableMetadata metadata, boolean includeClusterKeys, boole sb.append(" ]"); } sb.append(": "); - if(includeClusterKeys) + if (includeClusterKeys) sb.append(clustering().toString(metadata)); else - sb.append(clustering().toCQLString(metadata)); + sb.append(clustering().toCQLString(metadata, Redaction.NONE)); sb.append(" | "); boolean isFirst = true; for (ColumnData cd : this) diff --git a/src/java/org/apache/cassandra/db/rows/ArrayCell.java b/src/java/org/apache/cassandra/db/rows/ArrayCell.java index 07823d2be515..90c20e978c9f 100644 --- a/src/java/org/apache/cassandra/db/rows/ArrayCell.java +++ b/src/java/org/apache/cassandra/db/rows/ArrayCell.java @@ -127,7 +127,7 @@ public long unsharedHeapSizeExcludingData() } @Override - protected int localDeletionTimeAsUnsignedInt() + public int localDeletionTimeAsUnsignedInt() { return localDeletionTimeUnsignedInteger; } diff --git a/src/java/org/apache/cassandra/db/rows/ArtificialBoundMarker.java b/src/java/org/apache/cassandra/db/rows/ArtificialBoundMarker.java index ed6e39a5a299..40402fece5fd 100644 --- a/src/java/org/apache/cassandra/db/rows/ArtificialBoundMarker.java +++ b/src/java/org/apache/cassandra/db/rows/ArtificialBoundMarker.java @@ -56,4 +56,4 @@ public String toString(TableMetadata metadata) { return String.format("LowerBoundMarker %s", bound.toString(metadata)); } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java index 52f0639e8e8a..7c5a9a2629b3 100644 --- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java +++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java @@ -90,7 +90,7 @@ public class BTreeRow extends AbstractRow // no expiring cells, this will be Cell.MAX_DELETION_TIME; private final long minLocalDeletionTime; - private BTreeRow(Clustering clustering, + private BTreeRow(Clustering clustering, LivenessInfo primaryKeyLivenessInfo, Deletion deletion, Object[] btree, @@ -206,6 +206,34 @@ public void apply(BiConsumer function, A arg) BTree.apply(btree, function, arg); } + /** + * Computes the maximum timestamp for any data (deletion info, PK liveness or cell) in this row. + */ + public long maxTimestamp() + { + long maxTs = Math.max(primaryKeyLivenessInfo().timestamp(), deletion().time().markedForDeleteAt()); + return reduce(maxTs, (ts, cd) -> Math.max(ts, cd.maxTimestamp())); + } + + /** + * Computes the minimum timestamp for any data (deletion info, PK liveness or cell) in this row. + */ + public long minTimestamp() + { + long minTs = Long.MAX_VALUE; + if (!primaryKeyLivenessInfo().isEmpty()) + minTs = Math.min(minTs, primaryKeyLivenessInfo().timestamp()); + if (!deletion().isLive()) + minTs = Math.min(minTs, deletion().time().markedForDeleteAt()); + + return reduce(minTs, (ts, cd) -> Math.min(ts, cd.minTimestamp())); + } + + public R reduce(R seed, BTree.ReduceFunction reducer) + { + return BTree.reduce(btree, seed, reducer); + } + public long accumulate(LongAccumulator accumulator, long initialValue) { return BTree.accumulate(btree, accumulator, initialValue); @@ -226,7 +254,7 @@ public long accumulate(BiLongAccumulator accumulator, A arg, return BTree.accumulate(btree, accumulator, arg, comparator, from, initialValue); } - private static long minDeletionTime(Object[] btree, LivenessInfo info, DeletionTime rowDeletion) + public static long minDeletionTime(Object[] btree, LivenessInfo info, DeletionTime rowDeletion) { long min = Math.min(minDeletionTime(info), minDeletionTime(rowDeletion)); return BTree.accumulate(btree, (cd, l) -> Math.min(l, minDeletionTime(cd)), min); @@ -429,8 +457,8 @@ public boolean hasInvalidDeletions() /** * Returns a copy of the row where all timestamps for live data have replaced by {@code newTimestamp} and * all deletion timestamp by {@code newTimestamp - 1}. - * - * This exists for the Paxos path, see {@link PartitionUpdate#updateAllTimestamp} for additional details. + *

    + * This exists for the Paxos path, see {@link PartitionUpdate#withUpdatedTimestamps(long)} for additional details. */ public Row updateAllTimestamp(long newTimestamp) { @@ -468,7 +496,8 @@ public Row purge(DeletionPurger purger, long nowInSec, boolean enforceStrictLive if (enforceStrictLiveness && newDeletion.isLive() && newInfo.isEmpty()) return null; - return transformAndFilter(newInfo, newDeletion, (cd) -> cd.purge(purger, nowInSec)); + Function columnDataPurger = (cd) -> cd.purge(purger, nowInSec); + return update(newInfo, newDeletion, BTree.transformAndFilter(btree, columnDataPurger)); } public Row purgeDataOlderThan(long timestamp, boolean enforceStrictLiveness) @@ -498,6 +527,7 @@ private Row update(LivenessInfo info, Deletion deletion, Object[] newTree) return null; long minDeletionTime = minDeletionTime(newTree, info, deletion.time()); + return BTreeRow.create(clustering, info, deletion, newTree, minDeletionTime); } @@ -528,6 +558,16 @@ public int dataSize() return Ints.checkedCast(accumulate((cd, v) -> v + cd.dataSize(), dataSize)); } + @Override + public int liveDataSize(long nowInSec) + { + int dataSize = clustering.dataSize() + + primaryKeyLivenessInfo.dataSize() + + deletion.dataSize(); + + return Ints.checkedCast(accumulate((cd, v) -> v + cd.liveDataSize(nowInSec), dataSize)); + } + @Override public long unsharedHeapSize() { @@ -540,7 +580,6 @@ public long unsharedHeapSize() return accumulate((cd, v) -> v + cd.unsharedHeapSize(), heapSize); } - @Override public long unsharedHeapSizeExcludingData() { long heapSize = EMPTY_SIZE @@ -587,9 +626,7 @@ public static Row merge(BTreeRow existing, Object[] existingBtree = existing.btree; Object[] updateBtree = update.btree; - LivenessInfo existingInfo = existing.primaryKeyLivenessInfo(); - LivenessInfo updateInfo = update.primaryKeyLivenessInfo(); - LivenessInfo livenessInfo = existingInfo.supersedes(updateInfo) ? existingInfo : updateInfo; + LivenessInfo livenessInfo = LivenessInfo.merge(update.primaryKeyLivenessInfo(), existing.primaryKeyLivenessInfo()); Row.Deletion rowDeletion = existing.deletion().supersedes(update.deletion()) ? existing.deletion() : update.deletion(); @@ -599,24 +636,50 @@ else if (rowDeletion.isShadowedBy(livenessInfo)) rowDeletion = Row.Deletion.LIVE; DeletionTime deletion = rowDeletion.time(); + Object[] tree = mergeRowBTrees(reconcileF, existingBtree, updateBtree, deletion, existing.deletion().time()); + return new BTreeRow(existing.clustering, livenessInfo, rowDeletion, tree, minDeletionTime(tree, livenessInfo, deletion)); + } + + public static Object[] mergeRowBTrees(ColumnData.PostReconciliationFunction reconcileF, + Object[] existingBtree, Object[] updateBtree, + DeletionTime deletion, DeletionTime existingDeletion) + { try (ColumnData.Reconciler reconciler = ColumnData.reconciler(reconcileF, deletion)) { - if (!rowDeletion.isLive()) + if (!deletion.isLive()) { - if (rowDeletion == existing.deletion()) + if (deletion == existingDeletion) { - updateBtree = BTree.transformAndFilter(updateBtree, reconciler::retain); + // The existing row's deletion shadows part of the update. Filter those cells out of + // the UPDATE (incoming) side, but do NOT record their removal: that data was never + // owned by the memtable, so accounting its removal would drive the allocator's + // ownership negative and crash the next flush (CASSANDRA-21469). + updateBtree = BTree.transformAndFilter(updateBtree, reconciler::removeShadowed); } else { + // The update's deletion shadows part of the existing row. Those cells ARE owned by + // the memtable, so record their removal via retain(). existingBtree = BTree.transformAndFilter(existingBtree, reconciler::retain); } } - Object[] tree = BTree.update(existingBtree, updateBtree, ColumnData.comparator, reconciler); - return new BTreeRow(existing.clustering, livenessInfo, rowDeletion, tree, minDeletionTime(tree, livenessInfo, deletion)); + return BTree.update(existingBtree, updateBtree, ColumnData.comparator, reconciler); } } + /** + * Exposed for TrieBackedPartition. + */ + public Object[] getBTree() + { + return btree; + } + + public long getMinLocalDeletionTime() + { + return minLocalDeletionTime; + } + private class CellIterator extends AbstractIterator> { private Iterator columnData = iterator(); diff --git a/src/java/org/apache/cassandra/db/rows/BufferCell.java b/src/java/org/apache/cassandra/db/rows/BufferCell.java index d6918533e868..85a2e3aeb458 100644 --- a/src/java/org/apache/cassandra/db/rows/BufferCell.java +++ b/src/java/org/apache/cassandra/db/rows/BufferCell.java @@ -159,7 +159,7 @@ public long unsharedHeapSizeExcludingData() } @Override - protected int localDeletionTimeAsUnsignedInt() + public int localDeletionTimeAsUnsignedInt() { return localDeletionTimeUnsignedInteger; } diff --git a/src/java/org/apache/cassandra/db/rows/Cell.java b/src/java/org/apache/cassandra/db/rows/Cell.java index d60fdda5a012..3fe7733e9af0 100644 --- a/src/java/org/apache/cassandra/db/rows/Cell.java +++ b/src/java/org/apache/cassandra/db/rows/Cell.java @@ -90,11 +90,14 @@ public static long deletionTimeUnsignedIntegerToLong(int deletionTimeUnsignedInt public static long getVersionedMaxDeletiontionTime() { + if (DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5)) + return Cell.MAX_DELETION_TIME_2038_LEGACY_CAP; + if (DatabaseDescriptor.getStorageCompatibilityMode().disabled()) // The whole cluster is 2016, we're out of the 2038/2106 mixed cluster scenario. Shortcut to avoid the 'minClusterVersion' volatile read return Cell.MAX_DELETION_TIME; else - return MessagingService.instance().versions.minClusterVersion >= MessagingService.VERSION_50 + return MessagingService.Version.supportsExtendedDeletionTime(MessagingService.instance().versions.minClusterVersion) ? Cell.MAX_DELETION_TIME : Cell.MAX_DELETION_TIME_2038_LEGACY_CAP; } @@ -212,8 +215,8 @@ public final Cell clone(Cloner cloner) @Override // Overrides super type to provide a more precise return type. public abstract Cell purgeDataOlderThan(long timestamp); - - protected abstract int localDeletionTimeAsUnsignedInt(); + + public abstract int localDeletionTimeAsUnsignedInt(); /** * Handle unsigned encoding and potentially invalid localDeletionTime. @@ -226,9 +229,8 @@ public static long decodeLocalDeletionTime(long localDeletionTime, int ttl, Dese if (localDeletionTime < 0) { // Overflown signed int, decode to long. The result is guaranteed > ttl (and any signed int) - return helper.version < MessagingService.VERSION_50 - ? INVALID_DELETION_TIME - : deletionTimeUnsignedIntegerToLong((int) localDeletionTime); + return MessagingService.Version.supportsExtendedDeletionTime(helper.version) + ? deletionTimeUnsignedIntegerToLong((int) localDeletionTime) : INVALID_DELETION_TIME; } if (ttl == LivenessInfo.EXPIRED_LIVENESS_TTL) @@ -257,7 +259,7 @@ public static long decodeLocalDeletionTime(long localDeletionTime, int ttl, Dese * - [ value ]: the cell value, unless it has the HAS_EMPTY_VALUE_MASK. * - [ path ]: the cell path if the column this is a cell of is complex. */ - static class Serializer + public static class Serializer { private final static int IS_DELETED_MASK = 0x01; // Whether the cell is a tombstone or not. private final static int IS_EXPIRING_MASK = 0x02; // Whether the cell is expiring. diff --git a/src/java/org/apache/cassandra/db/rows/ColumnData.java b/src/java/org/apache/cassandra/db/rows/ColumnData.java index b9f19dc07fce..3bdcae30f4c7 100644 --- a/src/java/org/apache/cassandra/db/rows/ColumnData.java +++ b/src/java/org/apache/cassandra/db/rows/ColumnData.java @@ -179,7 +179,15 @@ public ColumnData retain(ColumnData existing) return removeShadowed(existing, postReconcile); } - private ColumnData removeShadowed(ColumnData existing) + /** + * Like {@link #retain} but does NOT notify the {@link PostReconciliationFunction} of removed + * data. Use this e.g. when filtering shadowed cells out of the UPDATE (incoming) side of a merge: + * that data was never allocated to / owned by the memtable, so recording its removal would + * make the memtable allocator under-count what it owns and eventually report a negative + * release at flush (CASSANDRA-21469). Recording removals (via {@link #retain}) is only correct + * for the EXISTING side, whose data the memtable already owns. + */ + public ColumnData removeShadowed(ColumnData existing) { return removeShadowed(existing, ColumnData.noOp); } @@ -245,6 +253,14 @@ protected ColumnData(ColumnMetadata column) */ public abstract int dataSize(); + /** + * The size of the data hold by this {@code ColumnData} that is live at {@code nowInSec}. + * + * @param nowInSec the query timestamp in seconds + * @return the size used by the live data of this {@code ColumnData}. + */ + public abstract int liveDataSize(long nowInSec); + public abstract long unsharedHeapSizeExcludingData(); public abstract long unsharedHeapSize(); @@ -291,4 +307,6 @@ public static void digest(Digest digest, ColumnData cd) public abstract ColumnData purgeDataOlderThan(long timestamp); public abstract long maxTimestamp(); + + public abstract long minTimestamp(); } diff --git a/src/java/org/apache/cassandra/db/rows/ColumnMetadataVersionComparator.java b/src/java/org/apache/cassandra/db/rows/ColumnMetadataVersionComparator.java index 6b2d97c8370c..4b5403246b62 100644 --- a/src/java/org/apache/cassandra/db/rows/ColumnMetadataVersionComparator.java +++ b/src/java/org/apache/cassandra/db/rows/ColumnMetadataVersionComparator.java @@ -35,7 +35,7 @@ * cannot guarantee when that's fully done). * */ -final class ColumnMetadataVersionComparator implements Comparator +public final class ColumnMetadataVersionComparator implements Comparator { public static final Comparator INSTANCE = new ColumnMetadataVersionComparator(); diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java index dea77413c09d..5cf23e8bdc1a 100644 --- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java +++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java @@ -70,6 +70,11 @@ public class ComplexColumnData extends ColumnData implements Iterable> this.complexDeletion = complexDeletion; } + // Used by CNDB + public boolean hasCells() { + return !BTree.isEmpty(this.cells); + } + public int cellsCount() { return BTree.size(cells); @@ -80,6 +85,11 @@ public Cell getCell(CellPath path) return (Cell) BTree.find(cells, column.asymmetricCellPathComparator(), path); } + public R reduce(R seed, BTree.ReduceFunction reducer) + { + return BTree.reduce(cells, seed, reducer); + } + public Cell getCellByIndex(int idx) { return BTree.findByIndex(cells, idx); @@ -139,13 +149,19 @@ public int dataSize() return size; } + @Override + public int liveDataSize(long nowInSec) + { + return complexDeletion.isLive() ? dataSize() : 0; + } + + @Override public long unsharedHeapSize() { long heapSize = EMPTY_SIZE + BTree.sizeOnHeapOf(cells) + complexDeletion.unsharedHeapSize(); return BTree.accumulate(cells, (cell, value) -> value + cell.unsharedHeapSize(), heapSize); } - @Override public long unsharedHeapSizeExcludingData() { long heapSize = EMPTY_SIZE + BTree.sizeOnHeapOf(cells); @@ -272,6 +288,16 @@ public long maxTimestamp() return timestamp; } + public long minTimestamp() + { + long timestamp = complexDeletion.isLive() + ? Long.MAX_VALUE + : complexDeletion.markedForDeleteAt(); + for (Cell cell : this) + timestamp = Math.min(timestamp, cell.timestamp()); + return timestamp; + } + // This is the partner in crime of ArrayBackedRow.setValue. The exact warning apply. The short // version is: "don't use that method". void setValue(CellPath path, ByteBuffer value) diff --git a/src/java/org/apache/cassandra/db/rows/EncodingStats.java b/src/java/org/apache/cassandra/db/rows/EncodingStats.java index d0f788ae5ae5..dd3f104cade1 100644 --- a/src/java/org/apache/cassandra/db/rows/EncodingStats.java +++ b/src/java/org/apache/cassandra/db/rows/EncodingStats.java @@ -97,19 +97,37 @@ public EncodingStats(long minTimestamp, */ public EncodingStats mergeWith(EncodingStats that) { - long minTimestamp = this.minTimestamp == TIMESTAMP_EPOCH - ? that.minTimestamp - : (that.minTimestamp == TIMESTAMP_EPOCH ? this.minTimestamp : Math.min(this.minTimestamp, that.minTimestamp)); - long minDelTime = this.minLocalDeletionTime == DELETION_TIME_EPOCH - ? that.minLocalDeletionTime - : (that.minLocalDeletionTime == DELETION_TIME_EPOCH ? this.minLocalDeletionTime : Math.min(this.minLocalDeletionTime, that.minLocalDeletionTime)); + return new EncodingStats(mergeMinTimestamp(this.minTimestamp, that), + mergeMinLocalDeletionTime(this.minLocalDeletionTime, that), + mergeMinTTL(this.minTTL, that)); + } + + public static long mergeMinTimestamp(long minTimestamp, EncodingStats stats) + { + return minTimestamp == TIMESTAMP_EPOCH + ? stats.minTimestamp + : (stats.minTimestamp == TIMESTAMP_EPOCH + ? minTimestamp + : Math.min(minTimestamp, stats.minTimestamp)); + } - int minTTL = this.minTTL == TTL_EPOCH - ? that.minTTL - : (that.minTTL == TTL_EPOCH ? this.minTTL : Math.min(this.minTTL, that.minTTL)); + public static long mergeMinLocalDeletionTime(long minLocalDeletionTime, EncodingStats stats) + { + return minLocalDeletionTime == DELETION_TIME_EPOCH + ? stats.minLocalDeletionTime + : (stats.minLocalDeletionTime == DELETION_TIME_EPOCH + ? minLocalDeletionTime + : Math.min(minLocalDeletionTime, stats.minLocalDeletionTime)); + } - return new EncodingStats(minTimestamp, minDelTime, minTTL); + public static int mergeMinTTL(int minTTL, EncodingStats stats) + { + return minTTL == TTL_EPOCH + ? stats.minTTL + : (stats.minTTL == TTL_EPOCH + ? minTTL + : Math.min(minTTL, stats.minTTL)); } /** @@ -265,6 +283,13 @@ public static EncodingStats collect(Row staticRow, Iterator rows, DeletionI Rows.collectStats(rows.next(), collector); return collector.get(); } + + public static EncodingStats forRow(Row row) + { + Collector collector = new Collector(); + Rows.collectStats(row, collector); + return collector.get(); + } } public static class Serializer diff --git a/src/java/org/apache/cassandra/db/rows/NativeCell.java b/src/java/org/apache/cassandra/db/rows/NativeCell.java index 65516ff31fb2..59feb9706d99 100644 --- a/src/java/org/apache/cassandra/db/rows/NativeCell.java +++ b/src/java/org/apache/cassandra/db/rows/NativeCell.java @@ -207,7 +207,7 @@ private boolean hasPath() } @Override - protected int localDeletionTimeAsUnsignedInt() + public int localDeletionTimeAsUnsignedInt() { return NativeEndianMemoryUtil.getInt(peer + DELETION); } diff --git a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java index 67f43c910c72..ae039a555e03 100644 --- a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java +++ b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java @@ -166,6 +166,18 @@ public long unsharedHeapSize() return EMPTY_SIZE + deletion.unsharedHeapSize(); } + @Override + public long minTimestamp() + { + return deletion.markedForDeleteAt(); + } + + @Override + public long maxTimestamp() + { + return deletion.markedForDeleteAt(); + } + public String toString(TableMetadata metadata) { return String.format("Marker %s@%d/%d", bound.toString(metadata), deletion.markedForDeleteAt(), deletion.localDeletionTime()); diff --git a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java index c36dcfdd55e6..e78eae6eefd8 100644 --- a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java +++ b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java @@ -199,6 +199,18 @@ public long unsharedHeapSize() return EMPTY_SIZE + startDeletion.unsharedHeapSize() + endDeletion.unsharedHeapSize(); } + @Override + public long minTimestamp() + { + return Math.min(startDeletion.markedForDeleteAt(), endDeletion.markedForDeleteAt()); + } + + @Override + public long maxTimestamp() + { + return Math.max(startDeletion.markedForDeleteAt(), endDeletion.markedForDeleteAt()); + } + public String toString(TableMetadata metadata) { return String.format("Marker %s@%d/%d-%d/%d", diff --git a/src/java/org/apache/cassandra/db/rows/Row.java b/src/java/org/apache/cassandra/db/rows/Row.java index 5e0bbaf6edf7..f4d9f59f64b6 100644 --- a/src/java/org/apache/cassandra/db/rows/Row.java +++ b/src/java/org/apache/cassandra/db/rows/Row.java @@ -17,13 +17,24 @@ */ package org.apache.cassandra.db.rows; -import java.util.*; +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; +import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import org.apache.cassandra.cache.IMeasurableMemory; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DeletionPurger; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Digest; +import org.apache.cassandra.db.LivenessInfo; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; @@ -32,6 +43,7 @@ import org.apache.cassandra.utils.LongAccumulator; import org.apache.cassandra.utils.MergeIterator; import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.Reducer; import org.apache.cassandra.utils.SearchIterator; import org.apache.cassandra.utils.btree.BTree; import org.apache.cassandra.utils.memory.Cloner; @@ -114,7 +126,7 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory /** * Whether the row has some live information (i.e. it's not just deletion informations). - * + * * @param nowInSec the current time to decide what is deleted and what isn't * @param enforceStrictLiveness whether the row should be purged if there is no PK liveness info, * normally retrieved from {@link TableMetadata#enforceStrictLiveness()} @@ -313,6 +325,14 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory public int dataSize(); + /** + * Returns the size of the data hold by this row that is live at {@code nowInSec}. + * + * @param nowInSec the query timestamp in seconds + * @return the size of the data hold by this row that is live at {@code nowInSec}. + */ + int liveDataSize(long nowInSec); + public long unsharedHeapSizeExcludingData(); public String toString(TableMetadata metadata, boolean fullDetails); @@ -358,7 +378,7 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory *

    * Currently, the only use of shadowable row deletions is Materialized Views, see CASSANDRA-10261. */ - public static class Deletion + public static class Deletion implements IMeasurableMemory { public static final Deletion LIVE = new Deletion(DeletionTime.LIVE, false); private static final long EMPTY_SIZE = ObjectSizes.measure(DeletionTime.build(0, 0)); @@ -790,7 +810,7 @@ public Row[] mergedRows() return rows; } - private static class ColumnDataReducer extends MergeIterator.Reducer + private static class ColumnDataReducer extends Reducer { private ColumnMetadata column; private final List versions; @@ -835,7 +855,7 @@ private boolean useColumnMetadata(ColumnMetadata dataColumn) return ColumnMetadataVersionComparator.INSTANCE.compare(column, dataColumn) < 0; } - protected ColumnData getReduced() + public ColumnData getReduced() { if (column.isSimple()) { @@ -883,14 +903,14 @@ protected ColumnData getReduced() } } - protected void onKeyChange() + public void onKeyChange() { column = null; versions.clear(); } } - private static class CellReducer extends MergeIterator.Reducer, Cell> + private static class CellReducer extends Reducer, Cell> { private DeletionTime activeDeletion; private Cell merged; @@ -907,12 +927,12 @@ public void reduce(int idx, Cell cell) merged = merged == null ? cell : Cells.reconcile(merged, cell); } - protected Cell getReduced() + public Cell getReduced() { return merged; } - protected void onKeyChange() + public void onKeyChange() { merged = null; } diff --git a/src/java/org/apache/cassandra/db/rows/Rows.java b/src/java/org/apache/cassandra/db/rows/Rows.java index df9ff5e28125..82cbaae304ad 100644 --- a/src/java/org/apache/cassandra/db/rows/Rows.java +++ b/src/java/org/apache/cassandra/db/rows/Rows.java @@ -17,16 +17,24 @@ */ package org.apache.cassandra.db.rows; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.SimpleBuilders; +import org.apache.cassandra.db.partitions.PartitionStatisticsCollector; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.partitions.PartitionStatisticsCollector; import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.Reducer; /** * Static utilities to work on Row objects. @@ -97,9 +105,8 @@ private static int unpackColumnCount(long v) * * @param row the row for which to collect stats. * @param collector the stats collector. - * @return the total number of cells in {@code row}. */ - public static int collectStats(Row row, PartitionStatisticsCollector collector) + public static void collectStats(Row row, PartitionStatisticsCollector collector) { assert !row.isEmpty(); @@ -109,7 +116,26 @@ public static int collectStats(Row row, PartitionStatisticsCollector collector) long result = row.accumulate(StatsAccumulation::accumulateOnColumnData, collector, 0); collector.updateColumnSetPerRow(StatsAccumulation.unpackColumnCount(result)); - return StatsAccumulation.unpackCellCount(result); + } + + public static long collectMaxTimestamp(Row row) + { + long maxTimestamp = row.primaryKeyLivenessInfo().timestamp(); + for (ColumnData cd : row) + { + if (cd.column().isSimple()) + { + maxTimestamp = Math.max(maxTimestamp, ((Cell)cd).timestamp()); + } + else + { + ComplexColumnData complexData = (ComplexColumnData)cd; + maxTimestamp = Math.max(maxTimestamp, complexData.complexDeletion().markedForDeleteAt()); + for (Cell cell : complexData) + maxTimestamp = Math.max(maxTimestamp, cell.timestamp()); + } + } + return maxTimestamp; } /** @@ -145,7 +171,7 @@ public static void diff(RowDiffListener diffListener, Row merged, Row...inputs) for (Row row : inputs) inputIterators.add(row == null ? Collections.emptyIterator() : row.iterator()); - Iterator iter = MergeIterator.get(inputIterators, ColumnData.comparator, new MergeIterator.Reducer() + Iterator iter = MergeIterator.get(inputIterators, ColumnData.comparator, new Reducer() { ColumnData mergedData; ColumnData[] inputDatas = new ColumnData[inputs.length]; @@ -157,7 +183,7 @@ public void reduce(int idx, ColumnData current) inputDatas[idx - 1] = current; } - protected Object getReduced() + public Object getReduced() { for (int i = 0 ; i != inputDatas.length ; i++) { @@ -219,7 +245,7 @@ else if (cmp < 0) return null; } - protected void onKeyChange() + public void onKeyChange() { mergedData = null; Arrays.fill(inputDatas, null); diff --git a/src/java/org/apache/cassandra/db/rows/Unfiltered.java b/src/java/org/apache/cassandra/db/rows/Unfiltered.java index 4a90ded50d1f..3f344f22c88d 100644 --- a/src/java/org/apache/cassandra/db/rows/Unfiltered.java +++ b/src/java/org/apache/cassandra/db/rows/Unfiltered.java @@ -83,4 +83,16 @@ default boolean isRangeTombstoneMarker() { return kind() == Kind.RANGE_TOMBSTONE_MARKER; } + + /** + * Minimum the timestamps of all data in the row or marker. + * Note: deletion times are timestamps too, e.g. the min and max timestamp of a range marker is its deletion time. + */ + public long minTimestamp(); + + /** + * Maximum the timestamps of all data in the row or marker. + * Note: deletion times are timestamps too, e.g. the min and max timestamp of a range marker is its deletion time. + */ + public long maxTimestamp(); } diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java index 53a3ba37cbf8..93370fd14a7f 100644 --- a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java +++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java @@ -41,7 +41,6 @@ import org.apache.cassandra.io.sstable.keycache.KeyCacheSupport; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.IteratorWithLowerBound; /** * An unfiltered row iterator with a lower bound retrieved from either the global @@ -51,15 +50,23 @@ * the result is that if we don't need to access this sstable, i.e. due to the LIMIT conditon, * then we will not. See CASSANDRA-8180 for examples of why this is useful. */ -public class UnfilteredRowIteratorWithLowerBound extends LazilyInitializedUnfilteredRowIterator implements IteratorWithLowerBound +public class UnfilteredRowIteratorWithLowerBound extends LazilyInitializedUnfilteredRowIterator { + enum State + { + LOWER_BOUND_NOT_REQUESTED, + LOWER_BOUND_REQUESTED, + LOWER_BOUND_PRODUCED, + PRODUCING_ITEMS; + } + private final SSTableReader sstable; private final Slices slices; private final boolean isReverseOrder; private final ColumnFilter selectedColumns; private final SSTableReadsListener listener; private Optional lowerBoundMarker; - private boolean firstItemRetrieved; + private State state; public UnfilteredRowIteratorWithLowerBound(DecoratedKey partitionKey, SSTableReader sstable, @@ -84,9 +91,20 @@ public UnfilteredRowIteratorWithLowerBound(DecoratedKey partitionKey, this.isReverseOrder = isReverseOrder; this.selectedColumns = selectedColumns; this.listener = listener; - this.firstItemRetrieved = false; + this.state = State.LOWER_BOUND_NOT_REQUESTED; } + /** + * Request that the iterator produce an artificial lower bound (i.e. an ineffective range tombstone that is used to + * delay opening the sstable until the iteration reaches the clustering range that the sstable covers). + */ + public void requestLowerBound() + { + assert state == State.LOWER_BOUND_NOT_REQUESTED || state == State.LOWER_BOUND_REQUESTED; + state = State.LOWER_BOUND_REQUESTED; + } + + @VisibleForTesting public Unfiltered lowerBound() { if (lowerBoundMarker != null) @@ -126,21 +144,35 @@ protected UnfilteredRowIterator initializeIterator() @Override protected Unfiltered computeNext() { - Unfiltered ret = super.computeNext(); - if (firstItemRetrieved) - return ret; - - // Check that the lower bound is not bigger than the first item retrieved - firstItemRetrieved = true; Unfiltered lowerBound = lowerBound(); - if (lowerBound != null && ret != null) - assert comparator().compare(lowerBound.clustering(), ret.clustering()) <= 0 - : String.format("Lower bound [%s ]is bigger than first returned value [%s] for sstable %s", - lowerBound.clustering().toString(metadata()), - ret.toString(metadata()), - sstable.getFilename()); - - return ret; + switch (state) + { + case LOWER_BOUND_REQUESTED: + if (lowerBound != null) + { + state = State.LOWER_BOUND_PRODUCED; + return lowerBound; + } + break; + case LOWER_BOUND_PRODUCED: + state = State.PRODUCING_ITEMS; + Unfiltered ret = super.computeNext(); + + // Check that the lower bound is not bigger than the first item retrieved + if (lowerBound != null && ret != null) + assert comparator().compare(lowerBound.clustering(), ret.clustering()) <= 0 + : String.format("Lower bound [%s ]is bigger than first returned value [%s] for sstable %s", + lowerBound.clustering().toString(metadata()), + ret.toString(metadata()), + sstable.getFilename()); + + return ret; + } + + // if the bound was not requested, was null, or we have already produced it and the first item, pass on all + // items from the source + state = State.PRODUCING_ITEMS; + return super.computeNext(); } private Comparator comparator() diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java index 7ccc6ff97077..c6e41a99ca13 100644 --- a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java +++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java @@ -33,12 +33,14 @@ import org.apache.cassandra.db.transform.MoreRows; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.CloseableIterator; import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.IMergeIterator; import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.Reducer; /** * Static methods to work with atom iterators. @@ -303,12 +305,12 @@ public Unfiltered next() * This is mainly used by scrubber to detect problems in sstables. * * @param iterator the partition to check. - * @param filename the name of the file the data is comming from. + * @param file the data is comming from. * @return an iterator that returns the same data than {@code iterator} but that * checks said data and throws a {@code CorruptedSSTableException} if it detects * invalid data. */ - public static UnfilteredRowIterator withValidation(UnfilteredRowIterator iterator, final String filename) + public static UnfilteredRowIterator withValidation(UnfilteredRowIterator iterator, final File file) { class Validator extends Transformation { @@ -341,7 +343,7 @@ private void validate(Unfiltered unfiltered) } catch (MarshalException me) { - throw new CorruptSSTableException(me, filename); + throw new CorruptSSTableException(me, file); } } } @@ -397,7 +399,7 @@ public RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker) */ private static class UnfilteredRowMergeIterator extends AbstractUnfilteredRowIterator { - private final IMergeIterator mergeIterator; + private final CloseableIterator mergeIterator; private final MergeListener listener; private UnfilteredRowMergeIterator(TableMetadata metadata, @@ -415,9 +417,21 @@ private UnfilteredRowMergeIterator(TableMetadata metadata, reversed, EncodingStats.merge(iterators, UnfilteredRowIterator::stats)); - this.mergeIterator = MergeIterator.get(iterators, - reversed ? metadata.comparator.reversed() : metadata.comparator, - new MergeReducer(iterators.size(), reversed, listener)); + // If merging more than 1 source, ask iterators to provide artificial lower bounds which will help to delay + // opening sstables until they are needed. The tomsbtone processing will throw these ineffective bounds away + // (they are in the form of range tombstone markers with DeletionTime.LIVE). + if (iterators.size() > 1) + { + for (UnfilteredRowIterator iter : iterators) + { + if (iter instanceof UnfilteredRowIteratorWithLowerBound) + ((UnfilteredRowIteratorWithLowerBound) iter).requestLowerBound(); + } + } + + this.mergeIterator = MergeIterator.getCloseable(iterators, + reversed ? metadata.comparator.reversed() : metadata.comparator, + new MergeReducer(iterators.size(), reversed, listener)); this.listener = listener; } @@ -540,7 +554,7 @@ public void close() listener.close(); } - private class MergeReducer extends MergeIterator.Reducer + private class MergeReducer extends Reducer { private final MergeListener listener; @@ -557,7 +571,7 @@ private MergeReducer(int size, boolean reversed, MergeListener listener) } @Override - public boolean trivialReduceIsTrivial() + public boolean singleSourceReduceIsTrivial() { // If we have a listener, we must signal it even when we have a single version return listener == null; @@ -572,7 +586,7 @@ public void reduce(int idx, Unfiltered current) markerMerger.add(idx, (RangeTombstoneMarker)current); } - protected Unfiltered getReduced() + public Unfiltered getReduced() { if (nextKind == Unfiltered.Kind.ROW) { @@ -590,7 +604,7 @@ protected Unfiltered getReduced() } } - protected void onKeyChange() + public void onKeyChange() { if (nextKind == Unfiltered.Kind.ROW) rowMerger.clear(); diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java index 2fcba1bce8ea..7cba7dcc906b 100644 --- a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java +++ b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java @@ -20,16 +20,24 @@ import java.io.IOException; import net.nicoulaj.compilecommand.annotations.Inline; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringBoundOrBoundary; +import org.apache.cassandra.db.ClusteringBoundary; +import org.apache.cassandra.db.Columns; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.ByteArrayAccessor; +import org.apache.cassandra.io.util.TrackedDataInputPlus; +import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.db.rows.Row.Deletion; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.io.util.FileDataInput; -import org.apache.cassandra.io.util.TrackedDataInputPlus; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.utils.SearchIterator; import org.apache.cassandra.utils.WrappedException; @@ -101,11 +109,11 @@ public class UnfilteredSerializer */ private final static int END_OF_PARTITION = 0x01; // Signal the end of the partition. Nothing follows a field with that flag. private final static int IS_MARKER = 0x02; // Whether the encoded unfiltered is a marker or a row. All following markers applies only to rows. - private final static int HAS_TIMESTAMP = 0x04; // Whether the encoded row has a timestamp (i.e. if row.partitionKeyLivenessInfo().hasTimestamp() == true). - private final static int HAS_TTL = 0x08; // Whether the encoded row has some expiration info (i.e. if row.partitionKeyLivenessInfo().hasTTL() == true). - private final static int HAS_DELETION = 0x10; // Whether the encoded row has some deletion info. - private final static int HAS_ALL_COLUMNS = 0x20; // Whether the encoded row has all of the columns from the header present. - private final static int HAS_COMPLEX_DELETION = 0x40; // Whether the encoded row has some complex deletion for at least one of its columns. + public final static int HAS_TIMESTAMP = 0x04; // Whether the encoded row has a timestamp (i.e. if row.partitionKeyLivenessInfo().hasTimestamp() == true). + public final static int HAS_TTL = 0x08; // Whether the encoded row has some expiration info (i.e. if row.partitionKeyLivenessInfo().hasTTL() == true). + public final static int HAS_DELETION = 0x10; // Whether the encoded row has some deletion info. + public final static int HAS_ALL_COLUMNS = 0x20; // Whether the encoded row has all of the columns from the header present. + public final static int HAS_COMPLEX_DELETION = 0x40; // Whether the encoded row has some complex deletion for at least one of its columns. private final static int EXTENSION_FLAG = 0x80; // If present, another byte is read containing the "extended flags" above. /* @@ -582,9 +590,9 @@ public Row deserializeRowBody(DataInputPlus in, if (header.isForSSTable()) { - long rowSize = in.readUnsignedVInt(); - in.readUnsignedVInt(); // previous unfiltered size + int rowSize = Math.toIntExact(in.readUnsignedVInt()); in = new TrackedDataInputPlus(in, rowSize); + in.readUnsignedVInt(); // previous unfiltered size } LivenessInfo rowLiveness = LivenessInfo.EMPTY; @@ -670,10 +678,10 @@ private void readComplexColumn(ColumnMetadata column, DataInputPlus in, Serializ DeletionTime complexDeletion = header.readDeletionTime(in); if (complexDeletion.localDeletionTime() < 0) { - if (helper.version < MessagingService.VERSION_50) - complexDeletion = DeletionTime.build(complexDeletion.markedForDeleteAt(), Cell.INVALID_DELETION_TIME); - else + if (MessagingService.Version.supportsExtendedDeletionTime(helper.version)) complexDeletion = DeletionTime.build(complexDeletion.markedForDeleteAt(), Cell.deletionTimeUnsignedIntegerToLong((int) complexDeletion.localDeletionTime())); + else + complexDeletion = DeletionTime.build(complexDeletion.markedForDeleteAt(), Cell.INVALID_DELETION_TIME); } if (!helper.isDroppedComplexDeletion(complexDeletion)) builder.addComplexDeletion(column, complexDeletion); diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamWriter.java b/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamWriter.java index 806a74a35c30..0300595b19ea 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamWriter.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamWriter.java @@ -47,12 +47,14 @@ public class CassandraCompressedStreamWriter extends CassandraStreamWriter private final CompressionInfo compressionInfo; private final long totalSize; + private final long onDiskOffset; public CassandraCompressedStreamWriter(SSTableReader sstable, CassandraStreamHeader header, StreamSession session) { super(sstable, header, session); this.compressionInfo = header.compressionInfo; this.totalSize = header.size(); + this.onDiskOffset = sstable.getCompressionMetadata().chunkFor(sstable.getDataFileSliceDescriptor().sliceStart).offset; } @Override @@ -84,7 +86,10 @@ public void write(StreamingDataOutputPlus out) throws IOException while (bytesTransferred < length) { int toTransfer = (int) Math.min(CHUNK_SIZE, length - bytesTransferred); - long position = section.start + bytesTransferred; + // since we access the file directly (not through the rebufferer) we need to adjust the position + // manually when dealing with a slice (see ZeroCopyMetadata); therefore we subtract the onDiskOffset + // by which all the section positions are translated + long position = section.start + bytesTransferred - onDiskOffset; out.writeToChannel(bufferSupplier -> { ByteBuffer outBuffer = bufferSupplier.get(toTransfer); diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java b/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java index 97c3b2d4f9e3..9ef105332c2a 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java @@ -22,6 +22,8 @@ import java.util.Collection; import java.util.function.UnaryOperator; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,6 +57,8 @@ public class CassandraEntireSSTableStreamReader implements IStreamReader { private static final Logger logger = LoggerFactory.getLogger(CassandraEntireSSTableStreamReader.class); + private static final boolean SKIP_MUTATING_STATS_AFTER_ZCS = CassandraRelevantProperties.SKIP_MUTATING_STATS_AFTER_ZCS.getBoolean(); + private final TableId tableId; private final StreamSession session; private final StreamMessageHeader messageHeader; @@ -133,11 +137,22 @@ public SSTableMultiWriter read(DataInputPlus in) throws IOException prettyPrintMemory(totalSize)); } - UnaryOperator transform = stats -> stats.mutateLevel(header.sstableLevel) - .mutateRepairedMetadata(messageHeader.repairedAt, messageHeader.pendingRepair, false); - String description = String.format("level %s and repairedAt time %s and pendingRepair %s", - header.sstableLevel, messageHeader.repairedAt, messageHeader.pendingRepair); - writer.descriptor.getMetadataSerializer().mutate(writer.descriptor, description, transform); + if (!SKIP_MUTATING_STATS_AFTER_ZCS) + { + UnaryOperator transform = stats -> stats.mutateLevel(header.sstableLevel) + .mutateRepairedMetadata(messageHeader.repairedAt, messageHeader.pendingRepair, false); + String description = String.format("level %s and repairedAt time %s and pendingRepair %s", + header.sstableLevel, messageHeader.repairedAt, messageHeader.pendingRepair); + writer.descriptor.getMetadataSerializer().mutate(writer.descriptor, description, transform); + } + else + { + logger.debug("[Stream #{}] Skipped mutating {} component from {} for sstable {} by config -Dcassandra.skip_mutating_stats_after_zcs", + session.planId(), + SSTableFormat.Components.STATS, + session.peer, + writer.descriptor); + } return writer; } catch (Throwable e) diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraIncomingFile.java b/src/java/org/apache/cassandra/db/streaming/CassandraIncomingFile.java index e8a6fbcc7ce0..958bb1d2dd0f 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraIncomingFile.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraIncomingFile.java @@ -81,7 +81,7 @@ public synchronized void read(DataInputPlus in, int version) throws Throwable else if (streamHeader.isCompressed()) reader = new CassandraCompressedStreamReader(header, streamHeader, session); else - reader = new CassandraStreamReader(header, streamHeader, session); + reader = new CassandraStreamReader(header, streamHeader, session, version); size = streamHeader.size(); sstable = reader.read(in); diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraOutgoingFile.java b/src/java/org/apache/cassandra/db/streaming/CassandraOutgoingFile.java index 9fc04e7e0dd2..7de67d74d4b9 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraOutgoingFile.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraOutgoingFile.java @@ -42,7 +42,7 @@ */ public class CassandraOutgoingFile implements OutgoingStream { - private final Ref ref; + private final Ref ref; private final long estimatedKeys; private final List sections; private final String filename; @@ -50,7 +50,7 @@ public class CassandraOutgoingFile implements OutgoingStream private final StreamOperation operation; private final CassandraStreamHeader header; - public CassandraOutgoingFile(StreamOperation operation, Ref ref, + public CassandraOutgoingFile(StreamOperation operation, Ref ref, List sections, List> normalizedRanges, long estimatedKeys) { @@ -102,7 +102,7 @@ public static CassandraOutgoingFile fromStream(OutgoingStream stream) } @VisibleForTesting - public Ref getRef() + public Ref getRef() { return ref; } @@ -170,10 +170,16 @@ public void write(StreamSession session, StreamingDataOutputPlus out, int versio CassandraStreamHeader.serializer.serialize(header, out, version); out.flush(); - CassandraStreamWriter writer = header.isCompressed() ? - new CassandraCompressedStreamWriter(sstable, header, session) : - new CassandraStreamWriter(sstable, header, session); - writer.write(out); + if (header.isCompressed()) + { + CassandraCompressedStreamWriter writer = new CassandraCompressedStreamWriter(sstable, header, session); + writer.write(out); + } + else + { + CassandraStreamWriter writer = new CassandraStreamWriter(sstable, header, session); + writer.write(out, version); + } } } diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraStreamManager.java b/src/java/org/apache/cassandra/db/streaming/CassandraStreamManager.java index 6940f11b57fc..5c29f87dd602 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraStreamManager.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraStreamManager.java @@ -147,7 +147,7 @@ else if (pendingRepair == ActiveRepairService.NO_PENDING_REPAIR) List> ranges = sstable.isRepaired() ? normalizedFullRanges : normalizedAllRanges; List sections = sstable.getPositionsForRanges(ranges); - Ref ref = refs.get(sstable); + Ref ref = refs.get(sstable); if (sections.isEmpty()) { ref.release(); diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java index ea911d629d4b..a6b61fb9e54c 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java @@ -36,6 +36,7 @@ import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.commitlog.IntervalSet; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.rows.DeserializationHelper; import org.apache.cassandra.db.rows.EncodingStats; @@ -87,14 +88,20 @@ public class CassandraStreamReader implements IStreamReader protected final int sstableLevel; protected final SerializationHeader.Component header; protected final int fileSeqNum; + protected final int protocolVersion; public CassandraStreamReader(StreamMessageHeader header, CassandraStreamHeader streamHeader, StreamSession session) + { + this(header, streamHeader, session, current_version); + } + + public CassandraStreamReader(StreamMessageHeader header, CassandraStreamHeader streamHeader, StreamSession session, int protocolVersion) { if (session.getPendingRepair() != null) { // we should only ever be streaming pending repair // sstables if the session has a pending repair id - assert session.getPendingRepair().equals(header.pendingRepair); + assert session.getPendingRepair().equals(header.pendingRepair) : session.getPendingRepair() + " != " + header.pendingRepair; } this.session = session; this.tableId = header.tableId; @@ -106,6 +113,7 @@ public CassandraStreamReader(StreamMessageHeader header, CassandraStreamHeader s this.sstableLevel = streamHeader.sstableLevel; this.header = streamHeader.serializationHeader; this.fileSeqNum = header.sequenceNumber; + this.protocolVersion = protocolVersion; } /** @@ -129,7 +137,7 @@ public SSTableMultiWriter read(DataInputPlus inputPlus) throws Throwable StreamDeserializer deserializer = null; SSTableMultiWriter writer = null; - try (StreamCompressionInputStream streamCompressionInputStream = new StreamCompressionInputStream(inputPlus, current_version)) + try (StreamCompressionInputStream streamCompressionInputStream = new StreamCompressionInputStream(inputPlus, protocolVersion)) { TrackedDataInputPlus in = new TrackedDataInputPlus(streamCompressionInputStream); writer = createWriter(cfs, totalSize, repairedAt, pendingRepair, inputVersion.format); @@ -171,7 +179,7 @@ protected StreamDeserializer getDeserializer(TableMetadata metadata, protected SerializationHeader getHeader(TableMetadata metadata) throws UnknownColumnException { - return header != null? header.toHeader(metadata) : null; //pre-3.0 sstable have no SerializationHeader + return header != null? header.toHeader("stream from " + session.peer, metadata, inputVersion, false) : null; //pre-3.0 sstable have no SerializationHeader } protected SSTableMultiWriter createWriter(ColumnFamilyStore cfs, long totalSize, long repairedAt, TimeUUID pendingRepair, SSTableFormat format) throws IOException { @@ -183,7 +191,18 @@ protected SSTableMultiWriter createWriter(ColumnFamilyStore cfs, long totalSize, Preconditions.checkState(streamReceiver instanceof CassandraStreamReceiver); LifecycleNewTracker lifecycleNewTracker = CassandraStreamReceiver.fromReceiver(session.getAggregator(tableId)).createLifecycleNewTracker(); - RangeAwareSSTableWriter writer = new RangeAwareSSTableWriter(cfs, estimatedKeys, repairedAt, pendingRepair, false, format, sstableLevel, totalSize, lifecycleNewTracker, getHeader(cfs.metadata())); + RangeAwareSSTableWriter writer = new RangeAwareSSTableWriter(cfs, + estimatedKeys, + repairedAt, + pendingRepair, + false, + format, + // Commit log intervals for other nodes are not relevant and should not be copied + IntervalSet.empty(), + sstableLevel, + totalSize, + lifecycleNewTracker, + getHeader(cfs.metadata())); return writer; } diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java index 50f87c799ece..08cd48fda2bd 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Set; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import org.slf4j.Logger; @@ -32,6 +33,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.WriteOptions; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; @@ -81,7 +83,7 @@ public CassandraStreamReceiver(ColumnFamilyStore cfs, StreamSession session, int this.session = session; // this is an "offline" transaction, as we currently manually expose the sstables once done; // this should be revisited at a later date, so that LifecycleTransaction manages all sstable state changes - this.txn = LifecycleTransaction.offline(OperationType.STREAM); + this.txn = LifecycleTransaction.offline(OperationType.STREAM, cfs.metadata); this.sstables = new ArrayList<>(totalFiles); this.requiresWritePath = requiresWritePath(cfs); } @@ -107,7 +109,7 @@ public synchronized void received(IncomingStream stream) SSTableMultiWriter sstable = file.getSSTable(); try { - finished = sstable.finish(true); + finished = sstable.finish(true, cfs.getStorageHandler()); } catch (Throwable t) { @@ -141,6 +143,15 @@ public void trackNew(SSTable table) } } + @Override + public void trackNewWritten(SSTable table) + { + synchronized (CassandraStreamReceiver.this) + { + txn.trackNewWritten(table); + } + } + @Override public void untrackNew(SSTable table) { @@ -172,10 +183,10 @@ private boolean hasViews(ColumnFamilyStore cfs) private boolean hasCDC(ColumnFamilyStore cfs) { - return cfs.metadata().params.cdc; + return DatabaseDescriptor.isCDCEnabled() && cfs.metadata().params.cdc; } - // returns true iif it is a cdc table and cdc on repair is enabled. + // returns true if it is a cdc table and cdc on repair is enabled. private boolean cdcRequiresWriteCommitLog(ColumnFamilyStore cfs) { return DatabaseDescriptor.isCDCOnRepairEnabled() && hasCDC(cfs); @@ -190,11 +201,12 @@ private boolean cdcRequiresWriteCommitLog(ColumnFamilyStore cfs) * For CDC-enabled tables and write path for CDC is enabled, we want to ensure that the mutations are * run through the CommitLog, so they can be archived by the CDC process on discard. */ - private boolean requiresWritePath(ColumnFamilyStore cfs) + @VisibleForTesting + boolean requiresWritePath(ColumnFamilyStore cfs) { return cdcRequiresWriteCommitLog(cfs) || cfs.streamToMemtable() - || (session.streamOperation().requiresViewBuild() && hasViews(cfs)); + || (session.streamOperation().requiresViewBuild() && hasViews(cfs) && DatabaseDescriptor.isMaterializedViewsOnRepairEnabled()); } private void sendThroughWritePath(ColumnFamilyStore cfs, Collection readers) @@ -204,6 +216,7 @@ private void sendThroughWritePath(ColumnFamilyStore cfs, Collection hardLinks, ComponentManifest manif public static ComponentContext create(SSTable sstable) { + if (!DatabaseDescriptor.supportsHardlinksForEntireSSTableStreaming()) + return new ComponentContext(Collections.emptyMap(), ComponentManifest.create(sstable)); + Descriptor descriptor = sstable.descriptor; Map hardLinks = new HashMap<>(1); diff --git a/src/java/org/apache/cassandra/db/transform/RTBoundValidator.java b/src/java/org/apache/cassandra/db/transform/RTBoundValidator.java index e197ce20b72a..40fc826f3f1a 100644 --- a/src/java/org/apache/cassandra/db/transform/RTBoundValidator.java +++ b/src/java/org/apache/cassandra/db/transform/RTBoundValidator.java @@ -25,7 +25,8 @@ import org.apache.cassandra.db.rows.UnfilteredRowIterator; /** - * A validating transformation that sanity-checks the sequence of RT bounds and boundaries in every partition. + * A validating transformation that sanity-checks the sequence of Range Tombstone bounds and boundaries in every + * partition. * * What we validate, specifically: * - that open markers are only followed by close markers @@ -109,7 +110,7 @@ public RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker) public void onPartitionClose() { if (enforceIsClosed && null != openMarkerDeletionTime) - throw ise("expected all RTs to be closed, but the last one is open"); + throw ise("expected all Range Tombstones to be closed, but the last one is open"); } private IllegalStateException ise(String why) @@ -119,7 +120,7 @@ private IllegalStateException ise(String why) private String message(String why) { - return String.format("%s UnfilteredRowIterator for %s (key: %s omdt: [%s]) has an illegal RT bounds sequence: %s", + return String.format("%s UnfilteredRowIterator for %s (key: %s omdt: [%s]) has an illegal Range Tombstone bounds sequence: %s", stage, partition.metadata(), partition.metadata().partitionKeyType.getString(partition.partitionKey().getKey()), diff --git a/src/java/org/apache/cassandra/db/tries/CollectionMergeTrie.java b/src/java/org/apache/cassandra/db/tries/CollectionMergeTrie.java index 033691049440..04f732627f4a 100644 --- a/src/java/org/apache/cassandra/db/tries/CollectionMergeTrie.java +++ b/src/java/org/apache/cassandra/db/tries/CollectionMergeTrie.java @@ -23,6 +23,8 @@ import com.google.common.collect.Iterables; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + /** * A merged view of multiple tries. * @@ -48,9 +50,9 @@ class CollectionMergeTrie extends Trie } @Override - protected Cursor cursor() + protected Cursor cursor(Direction direction) { - return new CollectionMergeCursor<>(resolver, inputs); + return new CollectionMergeCursor<>(resolver, direction, inputs); } /** @@ -58,13 +60,13 @@ protected Cursor cursor() * - its depth is greater, or * - its depth is equal, and the incoming transition is smaller. */ - static boolean greaterCursor(Cursor c1, Cursor c2) + static boolean greaterCursor(Direction direction, Cursor c1, Cursor c2) { int c1depth = c1.depth(); int c2depth = c2.depth(); if (c1depth != c2depth) return c1depth < c2depth; - return c1.incomingTransition() > c2.incomingTransition(); + return direction.lt(c2.incomingTransition(), c1.incomingTransition()); } static boolean equalCursor(Cursor c1, Cursor c2) @@ -115,6 +117,7 @@ static boolean equalCursor(Cursor c1, Cursor c2) static class CollectionMergeCursor implements Cursor { private final CollectionMergeResolver resolver; + private final Direction direction; /** * The smallest cursor, tracked separately to improve performance in single-source sections of the trie. @@ -133,9 +136,10 @@ static class CollectionMergeCursor implements Cursor */ private final List contents; - public CollectionMergeCursor(CollectionMergeResolver resolver, Collection> inputs) + public CollectionMergeCursor(CollectionMergeResolver resolver, Direction direction, Collection> inputs) { this.resolver = resolver; + this.direction = direction; int count = inputs.size(); // Get cursors for all inputs. Put one of them in head and the rest in the heap. heap = new Cursor[count - 1]; @@ -143,7 +147,7 @@ public CollectionMergeCursor(CollectionMergeResolver resolver, Collection trie : inputs) { - Cursor cursor = trie.cursor(); + Cursor cursor = trie.cursor(direction); assert cursor.depth() == 0; if (i >= 0) heap[i] = cursor; @@ -155,21 +159,26 @@ public CollectionMergeCursor(CollectionMergeResolver resolver, Collection { void apply(CollectionMergeCursor self, Cursor cursor, int index); + + default boolean shouldContinueWithChild(Cursor child, Cursor head) + { + return equalCursor(child, head); + } } /** * Apply a non-interfering operation, i.e. one that does not change the cursor state, to all inputs in the heap - * that are on equal position to the head. - * For interfering operations like advancing the cursors, use {@link #advanceEqualAndRestoreHeap(AdvancingHeapOp)}. + * that satisfy the {@link HeapOp#shouldContinueWithChild} condition (by default, being equal to the head). + * For interfering operations like advancing the cursors, use {@link #advanceSelectedAndRestoreHeap(AdvancingHeapOp)}. */ - private void applyToEqualOnHeap(HeapOp action) + private void applyToSelectedInHeap(HeapOp action) { - applyToEqualElementsInHeap(action, 0); + applyToSelectedElementsInHeap(action, 0); } /** @@ -195,35 +204,36 @@ default void apply(CollectionMergeCursor self, Cursor cursor, int index) /** - * Advance the state of all inputs in the heap that are on equal position as the head and restore the heap - * invariant. + * Advance the state of all inputs in the heap that satisfy the {@link HeapOp#shouldContinueWithChild} condition + * (by default, being equal to the head) and restore the heap invariant. */ - private void advanceEqualAndRestoreHeap(AdvancingHeapOp action) + private void advanceSelectedAndRestoreHeap(AdvancingHeapOp action) { - applyToEqualElementsInHeap(action, 0); + applyToSelectedElementsInHeap(action, 0); } /** - * Apply an operation to all elements on the heap that are equal to the head. Descends recursively in the heap - * structure to all equal children and applies the operation on the way back. - * + * Apply an operation to all elements on the heap that satisfy, recursively through the heap hierarchy, the + * {@code shouldContinueWithChild} condition (being equal to the head by default). Descends recursively in the + * heap structure to all selected children and applies the operation on the way back. + *

    * This operation can be something that does not change the cursor state (see {@link #content}) or an operation * that advances the cursor to a new state, wrapped in a {@link AdvancingHeapOp} ({@link #advance} or - * {@link #skipChildren}). The latter interface takes care of pushing elements down in the heap after advancing + * {@link #skipTo}). The latter interface takes care of pushing elements down in the heap after advancing * and restores the subheap state on return from each level of the recursion. */ - private void applyToEqualElementsInHeap(HeapOp action, int index) + private void applyToSelectedElementsInHeap(HeapOp action, int index) { if (index >= heap.length) return; Cursor item = heap[index]; - if (!equalCursor(item, head)) + if (!action.shouldContinueWithChild(item, head)) return; // If the children are at the same position, they also need advancing and their subheap // invariant to be restored. - applyToEqualElementsInHeap(action, index * 2 + 1); - applyToEqualElementsInHeap(action, index * 2 + 2); + applyToSelectedElementsInHeap(action, index * 2 + 1); + applyToSelectedElementsInHeap(action, index * 2 + 2); // Apply the action. This is done on the reverse direction to give the action a chance to form proper // subheaps and combine them on processing the parent. @@ -242,10 +252,10 @@ private void heapifyDown(Cursor item, int index) if (next >= heap.length) break; // Select the smaller of the two children to push down to. - if (next + 1 < heap.length && greaterCursor(heap[next], heap[next + 1])) + if (next + 1 < heap.length && greaterCursor(direction, heap[next], heap[next + 1])) ++next; // If the child is greater or equal, the invariant has been restored. - if (!greaterCursor(item, heap[next])) + if (!greaterCursor(direction, item, heap[next])) break; heap[index] = heap[next]; index = next; @@ -263,7 +273,7 @@ private int maybeSwapHead(int headDepth) { int heap0Depth = heap[0].depth(); if (headDepth > heap0Depth || - (headDepth == heap0Depth && head.incomingTransition() <= heap[0].incomingTransition())) + (headDepth == heap0Depth && direction.le(head.incomingTransition(), heap[0].incomingTransition()))) return headDepth; // head is still smallest // otherwise we need to swap heap and heap[0] @@ -273,10 +283,15 @@ private int maybeSwapHead(int headDepth) return heap0Depth; } + boolean branchHasMultipleSources() + { + return equalCursor(heap[0], head); + } + @Override public int advance() { - advanceEqualAndRestoreHeap(Cursor::advance); + advanceSelectedAndRestoreHeap(Cursor::advance); return maybeSwapHead(head.advance()); } @@ -285,7 +300,7 @@ public int advanceMultiple(TransitionsReceiver receiver) { // If the current position is present in just one cursor, we can safely descend multiple levels within // its branch as no one of the other tries has content for it. - if (equalCursor(heap[0], head)) + if (branchHasMultipleSources()) return advance(); // More than one source at current position, do single-step advance. // If there are no children, i.e. the cursor ascends, we have to check if it's become larger than some @@ -294,10 +309,36 @@ public int advanceMultiple(TransitionsReceiver receiver) } @Override - public int skipChildren() + public int skipTo(int skipDepth, int skipTransition) { - advanceEqualAndRestoreHeap(Cursor::skipChildren); - return maybeSwapHead(head.skipChildren()); + // We need to advance all cursors that stand before the requested position. + // If a child cursor does not need to advance as it is greater than the skip position, neither of the ones + // below it in the heap hierarchy do as they can't have an earlier position. + class SkipTo implements AdvancingHeapOp + { + @Override + public boolean shouldContinueWithChild(Cursor child, Cursor head) + { + // When the requested position descends, the inplicit prefix bytes are those of the head cursor, + // and thus we need to check against that if it is a match. + if (equalCursor(child, head)) + return true; + // Otherwise we can compare the child's position against a cursor advanced as requested, and need + // to skip only if it would be before it. + int childDepth = child.depth(); + return childDepth > skipDepth || + childDepth == skipDepth && direction.lt(child.incomingTransition(), skipTransition); + } + + @Override + public void apply(Cursor cursor) + { + cursor.skipTo(skipDepth, skipTransition); + } + } + + applyToSelectedElementsInHeap(new SkipTo(), 0); + return maybeSwapHead(head.skipTo(skipDepth, skipTransition)); } @Override @@ -312,10 +353,25 @@ public int incomingTransition() return head.incomingTransition(); } + @Override + public Direction direction() + { + return direction; + } + + @Override + public ByteComparable.Version byteComparableVersion() + { + return head.byteComparableVersion(); + } + @Override public T content() { - applyToEqualOnHeap(CollectionMergeCursor::collectContent); + if (!branchHasMultipleSources()) + return head.content(); + + applyToSelectedInHeap(CollectionMergeCursor::collectContent); collectContent(head, -1); T toReturn; @@ -341,6 +397,19 @@ private void collectContent(Cursor item, int index) if (itemContent != null) contents.add(itemContent); } + + @Override + public Trie tailTrie() + { + if (!branchHasMultipleSources()) + return head.tailTrie(); + + List> inputs = new ArrayList<>(heap.length); + inputs.add(head.tailTrie()); + applyToSelectedInHeap((self, cursor, index) -> inputs.add(cursor.tailTrie())); + + return new CollectionMergeTrie<>(inputs, resolver); + } } /** diff --git a/src/java/org/apache/cassandra/db/tries/Direction.java b/src/java/org/apache/cassandra/db/tries/Direction.java new file mode 100644 index 000000000000..29f8e2b97b79 --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/Direction.java @@ -0,0 +1,181 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.tries; + +/** + * Class used to specify the direction of iteration. Provides methods used to replace comparisons and values in typical + * loops and allow code to be written without explicit direction checks. + *

    + * For example, iterating between l and r inclusive in forward direction is usually done as
    + * {@code for (int i = l; i <= r; ++i) ...} + *

    + * To loop over them in the specified direction dir, the loop above would change to
    + * {@code for (int i = dir.select(l, r); dir.inLoop(i, l, r); i += dir.increase) ...} + */ +public enum Direction +{ + FORWARD(1) + { + public boolean inLoop(int index, int left, int right) + { + return index <= right; + } + + public boolean lt(int a, int b) + { + return a < b; + } + + public boolean le(int a, int b) + { + return a <= b; + } + + public int min(int a, int b) + { + return Math.min(a, b); + } + + public int max(int a, int b) + { + return Math.max(a, b); + } + + public T select(T forward, T reverse) + { + return forward; + } + + public int select(int forward, int reverse) + { + return forward; + } + + public boolean isForward() + { + return true; + } + + public Direction opposite() + { + return REVERSE; + } + }, + REVERSE(-1) + { + public boolean inLoop(int index, int left, int right) + { + return index >= left; + } + + public boolean lt(int a, int b) + { + return a > b; + } + + public boolean le(int a, int b) + { + return a >= b; + } + + public int min(int a, int b) + { + return Math.max(a, b); + } + + public int max(int a, int b) + { + return Math.min(a, b); + } + + public T select(T forward, T reverse) + { + return reverse; + } + + public int select(int forward, int reverse) + { + return reverse; + } + + public boolean isForward() + { + return false; + } + + public Direction opposite() + { + return FORWARD; + } + }; + + /** Value that needs to be added to advance the iteration, i.e. value corresponding to 1 */ + public final int increase; + + Direction(int increase) + { + this.increase = increase; + } + + /** Returns the result of the operation corresponding to a < b for the forward direction */ + public abstract boolean lt(int a, int b); + /** Returns the result of the operation corresponding to a>b for the forward direction */ + public boolean gt(int a, int b) + { + return lt(b, a); + } + /** Returns the result of the operation corresponding to a<=b for the forward direction */ + public abstract boolean le(int a, int b); + /** Returns the result of the operation corresponding to a>=b for the forward direction */ + public boolean ge(int a, int b) + { + return le(b, a); + } + /** Returns the result of the operation corresponding to min(a, b) for the forward direction */ + public abstract int min(int a, int b); + /** Returns the result of the operation corresponding to max(a, b) for the forward direction */ + public abstract int max(int a, int b); + + /** + * Use the first argument in forward direction and the second in reverse, i.e. isForward() ? forward : reverse. + */ + public abstract T select(T forward, T reverse); + + /** + * Use the first argument in forward direction and the second in reverse, i.e. isForward() ? forward : reverse. + */ + public abstract int select(int forward, int reverse); + + /** + * Helper to perform loops over possible values in the given direction. Returns whether the given index is still + * within bounds when iterating. + *

    + * {@code for} loops implemented as
    + * {@code for (int i = dir.select(l, r); dir.inLoop(i, l, r); i += dir.increase) ...}
    + * will iterate over all values between l and r inclusive in the specified direction. + */ + public abstract boolean inLoop(int index, int left, int right); + + public abstract boolean isForward(); + + public abstract Direction opposite(); + + public static Direction fromBoolean(boolean reversed) + { + return reversed ? REVERSE : FORWARD; + } +} diff --git a/src/java/org/apache/cassandra/db/tries/InMemoryReadTrie.java b/src/java/org/apache/cassandra/db/tries/InMemoryReadTrie.java index 88f5987a3380..ecddfd9544ef 100644 --- a/src/java/org/apache/cassandra/db/tries/InMemoryReadTrie.java +++ b/src/java/org/apache/cassandra/db/tries/InMemoryReadTrie.java @@ -35,24 +35,24 @@ public class InMemoryReadTrie extends Trie /* TRIE FORMAT AND NODE TYPES - The memtable trie uses five different types of nodes: + The in-memory trie uses five different types of nodes: - "leaf" nodes, which have content and no children; - single-transition "chain" nodes, which have exactly one child; while each node is a single transition, they are - called "chain" because multiple such transition are packed in a block. + called "chain" because multiple such transition are packed in a cell. - "sparse" nodes which have between two and six children; - "split" nodes for anything above six children; - "prefix" nodes that augment one of the other types (except leaf) with content. - The data for all nodes except leaf ones is stored in a contiguous 'node buffer' and laid out in blocks of 32 bytes. - A block only contains data for a single type of node, but there is no direct correspondence between block and node + The data for all nodes except leaf ones is stored in a contiguous 'node buffer' and laid out in cells of 32 bytes. + A cell only contains data for a single type of node, but there is no direct correspondence between cell and node in that: - - a single block can contain multiple "chain" nodes. - - a sparse node occupies exactly one block. - - a split node occupies a variable number of blocks. - - a prefix node can be placed in the same block as the node it augments, or in a separate block. + - a single cell can contain multiple "chain" nodes. + - a sparse node occupies exactly one cell. + - a split node occupies a variable number of cells. + - a prefix node can be placed in the same cell as the node it augments, or in a separate cell. Nodes are referenced in that buffer by an integer position/pointer, the 'node pointer'. Note that node pointers are - not pointing at the beginning of blocks, and we call 'pointer offset' the offset of the node pointer to the block it + not pointing at the beginning of cells, and we call 'pointer offset' the offset of the node pointer to the cell it points into. The value of a 'node pointer' is used to decide what kind of node is pointed: - If the pointer is negative, we have a leaf node. Since a leaf has no children, we need no data outside of its @@ -62,12 +62,12 @@ public class InMemoryReadTrie extends Trie - If the 'pointer offset' is smaller than 28, we have a chain node with one transition. The transition character is the byte at the position pointed in the 'node buffer', and the child is pointed by: - - the integer value at offset 28 of the block pointed if the 'pointer offset' is 27 + - the integer value at offset 28 of the cell pointed if the 'pointer offset' is 27 - pointer + 1 (which is guaranteed to have offset smaller than 28, i.e. to be a chain node), otherwise - In other words, a chain block contains a sequence of characters that leads to the child whose address is at - offset 28. It may have between 1 and 28 characters depending on the pointer with which the block is entered. + In other words, a chain cell contains a sequence of characters that leads to the child whose address is at + offset 28. It may have between 1 and 28 characters depending on the pointer with which the cell is entered. - - If the 'pointer offset' is 30, we have a sparse node. The data of a sparse node occupies a full block and is laid + - If the 'pointer offset' is 30, we have a sparse node. The data of a sparse node occupies a full cell and is laid out as: - six pointers to children at offsets 0 to 24 - six transition characters at offsets 24 to 30 @@ -82,27 +82,27 @@ allows iteration over the order word (which divides said word by 6 each step) to - If the 'pointer offset' is 28, the node is a split one. Split nodes are dense, meaning that there is a direct mapping between a transition character and the address of the associated pointer, and new children can easily be added in place. - Split nodes occupy multiple blocks, and a child is located by traversing 3 layers of pointers: - - the first pointer is within the top-level block (the one pointed by the pointer) and points to a "mid" block. - The top-level block has 4 such pointers to "mid" block, located between offset 16 and 32. - - the 2nd pointer is within the "mid" block and points to a "tail" block. A "mid" block has 8 such pointers - occupying the whole block. - - the 3rd pointer is with the "tail" block and is the actual child pointer. Like "mid" block, there are 8 such + Split nodes occupy multiple cells, and a child is located by traversing 3 layers of pointers: + - the first pointer is within the top-level cell (the one pointed by the pointer) and points to a "mid" cell. + The top-level cell has 4 such pointers to "mid" cell, located between offset 16 and 32. + - the 2nd pointer is within the "mid" cell and points to a "tail" cell. A "mid" cell has 8 such pointers + occupying the whole cell. + - the 3rd pointer is with the "tail" cell and is the actual child pointer. Like "mid" cell, there are 8 such pointers (so we finally address 4 * 8 * 8 = 256 children). - To find a child, we thus need to know the index of the pointer to follow within the top-level block, the index - of the one in the "mid" block and the index in the "tail" block. For that, we split the transition byte in a + To find a child, we thus need to know the index of the pointer to follow within the top-level cell, the index + of the one in the "mid" cell and the index in the "tail" cell. For that, we split the transition byte in a sequence of 2-3-3 bits: - - the first 2 bits are the index in the top-level block; - - the next 3 bits, the index in the "mid" block; - - and the last 3 bits the index in the "tail" block. - This layout allows the node to use the smaller fixed-size blocks (instead of 256*4 bytes for the whole character - space) and also leaves some room in the head block (the 16 first bytes) for additional information (which we can + - the first 2 bits are the index in the top-level cell; + - the next 3 bits, the index in the "mid" cell; + - and the last 3 bits the index in the "tail" cell. + This layout allows the node to use the smaller fixed-size cells (instead of 256*4 bytes for the whole character + space) and also leaves some room in the head cell (the 16 first bytes) for additional information (which we can use to store prefix nodes containing things like deletion times). - One split node may need up to 1 + 4 + 4*8 blocks (1184 bytes) to store all its children. + One split node may need up to 1 + 4 + 4*8 cells (1184 bytes) to store all its children. - If the pointer offset is 31, we have a prefix node. These are two types: -- Embedded prefix nodes occupy the free bytes in a chain or split node. The byte at offset 4 has the offset - within the 32-byte block for the augmented node. + within the 32-byte cell for the augmented node. -- Full prefix nodes have 0xFF at offset 4 and a pointer at 28, pointing to the augmented node. Both types contain an index for content at offset 0. The augmented node cannot be a leaf or NONE -- in the former case the leaf itself contains the content index, in the latter we use a leaf instead. @@ -117,40 +117,39 @@ single transitions leading to a chain node, we can expand that node (attaching a (i.e. create a new node and remap the parent) to sparse with two children. When a six-child sparse node needs a new child, we switch to split. - Blocks currently are not reused, because we do not yet have a mechanism to tell when readers are done with blocks - they are referencing. This currently causes a very low overhead (because we change data in place with the only - exception of nodes needing to change type) and is planned to be addressed later. + Cells can be reused once they are no longer used and cannot be in the state of a concurrently running reader. See + MemoryAllocationStrategy for details. For further descriptions and examples of the mechanics of the trie, see InMemoryTrie.md. */ - static final int BLOCK_SIZE = 32; + static final int CELL_SIZE = 32; - // Biggest block offset that can contain a pointer. - static final int LAST_POINTER_OFFSET = BLOCK_SIZE - 4; + // Biggest cell offset that can contain a pointer. + static final int LAST_POINTER_OFFSET = CELL_SIZE - 4; /* - Block offsets used to identify node types (by comparing them to the node 'pointer offset'). + Cell offsets used to identify node types (by comparing them to the node 'pointer offset'). */ - // split node (dense, 2-3-3 transitions), laid out as 4 pointers to "mid" block, with has 8 pointers to "tail" block, + // split node (dense, 2-3-3 transitions), laid out as 4 pointers to "mid" cell, with has 8 pointers to "tail" cell, // which has 8 pointers to children - static final int SPLIT_OFFSET = BLOCK_SIZE - 4; + static final int SPLIT_OFFSET = CELL_SIZE - 4; // sparse node, unordered list of up to 6 transition, laid out as 6 transition pointers followed by 6 transition // bytes. The last two bytes contain an ordering of the transitions (in base-6) which is used for iteration. On // update the pointer is set last, i.e. during reads the node may show that a transition exists and list a character // for it, but pointer may still be null. - static final int SPARSE_OFFSET = BLOCK_SIZE - 2; - // min and max offset for a chain node. A block of chain node is laid out as a pointer at LAST_POINTER_OFFSET, - // preceded by characters that lead to it. Thus a full chain block contains BLOCK_SIZE-4 transitions/chain nodes. + static final int SPARSE_OFFSET = CELL_SIZE - 2; + // min and max offset for a chain node. A cell of chain node is laid out as a pointer at LAST_POINTER_OFFSET, + // preceded by characters that lead to it. Thus a full chain cell contains CELL_SIZE-4 transitions/chain nodes. static final int CHAIN_MIN_OFFSET = 0; - static final int CHAIN_MAX_OFFSET = BLOCK_SIZE - 5; + static final int CHAIN_MAX_OFFSET = CELL_SIZE - 5; // Prefix node, an intermediate node augmenting its child node with content. - static final int PREFIX_OFFSET = BLOCK_SIZE - 1; + static final int PREFIX_OFFSET = CELL_SIZE - 1; /* - Offsets and values for navigating in a block for particular node type. Those offsets are 'from the node pointer' - (not the block start) and can be thus negative since node pointers points towards the end of blocks. + Offsets and values for navigating in a cell for particular node type. Those offsets are 'from the node pointer' + (not the cell start) and can be thus negative since node pointers points towards the end of cells. */ // Limit for the starting cell / sublevel (2 bits -> 4 pointers). @@ -161,14 +160,14 @@ Block offsets used to identify node types (by comparing them to the node 'pointe static final int SPLIT_LEVEL_SHIFT = 3; static final int SPARSE_CHILD_COUNT = 6; - // Offset to the first child pointer of a spare node (laid out from the start of the block) + // Offset to the first child pointer of a spare node (laid out from the start of the cell) static final int SPARSE_CHILDREN_OFFSET = 0 - SPARSE_OFFSET; // Offset to the first transition byte of a sparse node (laid out after the child pointers) static final int SPARSE_BYTES_OFFSET = SPARSE_CHILD_COUNT * 4 - SPARSE_OFFSET; // Offset to the order word of a sparse node (laid out after the children (pointer + transition byte)) static final int SPARSE_ORDER_OFFSET = SPARSE_CHILD_COUNT * 5 - SPARSE_OFFSET; // 0 - // Offset of the flag byte in a prefix node. In shared blocks, this contains the offset of the next node. + // Offset of the flag byte in a prefix node. In shared cells, this contains the offset of the next node. static final int PREFIX_FLAGS_OFFSET = 4 - PREFIX_OFFSET; // Offset of the content id static final int PREFIX_CONTENT_OFFSET = 0 - PREFIX_OFFSET; @@ -178,7 +177,7 @@ Block offsets used to identify node types (by comparing them to the node 'pointe /** * Value used as null for node pointers. * No node can use this address (we enforce this by not allowing chain nodes to grow to position 0). - * Do not change this as the code relies there being a NONE placed in all bytes of the block that are not set. + * Do not change this as the code relies there being a NONE placed in all bytes of the cell that are not set. */ static final int NONE = 0; @@ -200,8 +199,8 @@ Block offsets used to identify node types (by comparing them to the node 'pointe The allocated space starts 256 bytes for the buffer and 16 entries for the content list. - Note that a buffer is not allowed to split 32-byte blocks (code assumes same buffer can be used for all bytes - inside the block). + Note that a buffer is not allowed to split 32-byte cells (code assumes same buffer can be used for all bytes + inside the cell). */ static final int BUF_START_SHIFT = 8; @@ -212,42 +211,44 @@ Block offsets used to identify node types (by comparing them to the node 'pointe static { - assert BUF_START_SIZE % BLOCK_SIZE == 0 : "Initial buffer size must fit a full block."; + assert BUF_START_SIZE % CELL_SIZE == 0 : "Initial buffer size must fit a full cell."; } final UnsafeBuffer[] buffers; final AtomicReferenceArray[] contentArrays; + final ByteComparable.Version byteComparableVersion; - InMemoryReadTrie(UnsafeBuffer[] buffers, AtomicReferenceArray[] contentArrays, int root) + InMemoryReadTrie(ByteComparable.Version byteComparableVersion, UnsafeBuffer[] buffers, AtomicReferenceArray[] contentArrays, int root) { + this.byteComparableVersion = byteComparableVersion; this.buffers = buffers; this.contentArrays = contentArrays; this.root = root; } /* - Buffer, content list and block management + Buffer, content list and cell management */ - int getChunkIdx(int pos, int minChunkShift, int minChunkSize) + int getBufferIdx(int pos, int minBufferShift, int minBufferSize) { - return 31 - minChunkShift - Integer.numberOfLeadingZeros(pos + minChunkSize); + return 31 - minBufferShift - Integer.numberOfLeadingZeros(pos + minBufferSize); } - int inChunkPointer(int pos, int chunkIndex, int minChunkSize) + int inBufferOffset(int pos, int bufferIndex, int minBufferSize) { - return pos + minChunkSize - (minChunkSize << chunkIndex); + return pos + minBufferSize - (minBufferSize << bufferIndex); } - UnsafeBuffer getChunk(int pos) + UnsafeBuffer getBuffer(int pos) { - int leadBit = getChunkIdx(pos, BUF_START_SHIFT, BUF_START_SIZE); + int leadBit = getBufferIdx(pos, BUF_START_SHIFT, BUF_START_SIZE); return buffers[leadBit]; } - int inChunkPointer(int pos) + int inBufferOffset(int pos) { - int leadBit = getChunkIdx(pos, BUF_START_SHIFT, BUF_START_SIZE); - return inChunkPointer(pos, leadBit, BUF_START_SIZE); + int leadBit = getBufferIdx(pos, BUF_START_SHIFT, BUF_START_SIZE); + return inBufferOffset(pos, leadBit, BUF_START_SIZE); } @@ -256,28 +257,39 @@ int inChunkPointer(int pos) */ int offset(int pos) { - return pos & (BLOCK_SIZE - 1); + return pos & (CELL_SIZE - 1); } final int getUnsignedByte(int pos) { - return getChunk(pos).getByte(inChunkPointer(pos)) & 0xFF; + return getBuffer(pos).getByte(inBufferOffset(pos)) & 0xFF; } - final int getUnsignedShort(int pos) + final int getUnsignedShortVolatile(int pos) { - return getChunk(pos).getShort(inChunkPointer(pos)) & 0xFFFF; + return getBuffer(pos).getShortVolatile(inBufferOffset(pos)) & 0xFFFF; } - final int getInt(int pos) + /** + * Following a pointer must be done using a volatile read to enforce happens-before between reading the node we + * advance to and the preparation of that node that finishes in a volatile write of the pointer that makes it + * visible. + */ + final int getIntVolatile(int pos) { - return getChunk(pos).getInt(inChunkPointer(pos)); + return getBuffer(pos).getIntVolatile(inBufferOffset(pos)); } - T getContent(int index) + /** + * Get the content for the given content pointer. + * + * @param id content pointer, encoded as ~index where index is the position in the content array. + * @return the current content value. + */ + T getContent(int id) { - int leadBit = getChunkIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); - int ofs = inChunkPointer(index, leadBit, CONTENTS_START_SIZE); + int leadBit = getBufferIdx(~id, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); + int ofs = inBufferOffset(~id, leadBit, CONTENTS_START_SIZE); AtomicReferenceArray array = contentArrays[leadBit]; return array.get(ofs); } @@ -302,9 +314,9 @@ boolean isNullOrLeaf(int node) } /** - * Returns the number of transitions in a chain block entered with the given pointer. + * Returns the number of transitions in a chain cell entered with the given pointer. */ - private int chainBlockLength(int node) + private int chainCellLength(int node) { return LAST_POINTER_OFFSET - offset(node); } @@ -328,7 +340,7 @@ int getChild(int node, int trans) case CHAIN_MAX_OFFSET: if (trans != getUnsignedByte(node)) return NONE; - return getInt(node + 1); + return getIntVolatile(node + 1); default: if (trans != getUnsignedByte(node)) return NONE; @@ -344,10 +356,10 @@ protected int followContentTransition(int node) if (offset(node) == PREFIX_OFFSET) { int b = getUnsignedByte(node + PREFIX_FLAGS_OFFSET); - if (b < BLOCK_SIZE) + if (b < CELL_SIZE) node = node - PREFIX_OFFSET + b; else - node = getInt(node + PREFIX_POINTER_OFFSET); + node = getIntVolatile(node + PREFIX_POINTER_OFFSET); assert node >= 0 && offset(node) != PREFIX_OFFSET; } @@ -378,14 +390,14 @@ int advance(int node, int first, ByteSource rest) if (getUnsignedByte(node++) != first) return NONE; // Check the rest of the bytes provided by the chain node - for (int length = chainBlockLength(node); length > 0; --length) + for (int length = chainCellLength(node); length > 0; --length) { first = rest.next(); if (getUnsignedByte(node++) != first) return NONE; } // All bytes matched, node is now positioned on the child pointer. Follow it. - return getInt(node); + return getIntVolatile(node); } } @@ -398,7 +410,7 @@ int getSparseChild(int node, int trans) { if (getUnsignedByte(node + SPARSE_BYTES_OFFSET + i) == trans) { - int child = getInt(node + SPARSE_CHILDREN_OFFSET + i * 4); + int child = getIntVolatile(node + SPARSE_CHILDREN_OFFSET + i * 4); // we can't trust the transition character read above, because it may have been fetched before a // concurrent update happened, and the update may have managed to modify the pointer by now. @@ -412,7 +424,7 @@ int getSparseChild(int node, int trans) } /** - * Given a transition, returns the corresponding index (within the node block) of the pointer to the mid block of + * Given a transition, returns the corresponding index (within the node cell) of the pointer to the mid cell of * a split node. */ int splitNodeMidIndex(int trans) @@ -422,7 +434,7 @@ int splitNodeMidIndex(int trans) } /** - * Given a transition, returns the corresponding index (within the mid block) of the pointer to the tail block of + * Given a transition, returns the corresponding index (within the mid cell) of the pointer to the tail cell of * a split node. */ int splitNodeTailIndex(int trans) @@ -432,7 +444,7 @@ int splitNodeTailIndex(int trans) } /** - * Given a transition, returns the corresponding index (within the tail block) of the pointer to the child of + * Given a transition, returns the corresponding index (within the tail cell) of the pointer to the child of * a split node. */ int splitNodeChildIndex(int trans) @@ -446,14 +458,14 @@ int splitNodeChildIndex(int trans) */ int getSplitChild(int node, int trans) { - int mid = getSplitBlockPointer(node, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); + int mid = getSplitCellPointer(node, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); if (isNull(mid)) return NONE; - int tail = getSplitBlockPointer(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int tail = getSplitCellPointer(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); if (isNull(tail)) return NONE; - return getSplitBlockPointer(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + return getSplitCellPointer(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); } /** @@ -462,25 +474,25 @@ int getSplitChild(int node, int trans) T getNodeContent(int node) { if (isLeaf(node)) - return getContent(~node); + return getContent(node); if (offset(node) != PREFIX_OFFSET) return null; - int index = getInt(node + PREFIX_CONTENT_OFFSET); - return (index >= 0) + int index = getIntVolatile(node + PREFIX_CONTENT_OFFSET); + return (isLeaf(index)) ? getContent(index) : null; } - int splitBlockPointerAddress(int node, int childIndex, int subLevelLimit) + int splitCellPointerAddress(int node, int childIndex, int subLevelLimit) { return node - SPLIT_OFFSET + (8 - subLevelLimit + childIndex) * 4; } - int getSplitBlockPointer(int node, int childIndex, int subLevelLimit) + int getSplitCellPointer(int node, int childIndex, int subLevelLimit) { - return getInt(splitBlockPointerAddress(node, childIndex, subLevelLimit)); + return getIntVolatile(splitCellPointerAddress(node, childIndex, subLevelLimit)); } /** @@ -537,15 +549,18 @@ int depth(int backtrackDepth) * (i.e. it is positioned on a leaf node), it goes one level up the backtracking chain, where we are guaranteed to * have a remaining child to advance to. When there's nothing to backtrack to, the trie is exhausted. */ - class MemtableCursor extends CursorBacktrackingState implements Cursor + class InMemoryCursor extends CursorBacktrackingState implements Cursor { private int currentNode; + private int currentFullNode; private int incomingTransition; private T content; - private int depth = -1; + private final Direction direction; + int depth = -1; - MemtableCursor() + InMemoryCursor(Direction direction) { + this.direction = direction; descendInto(root, -1); } @@ -566,26 +581,54 @@ public int advanceMultiple(TransitionsReceiver receiver) return advance(); // Jump directly to the chain's child. - UnsafeBuffer chunk = getChunk(node); - int inChunkNode = inChunkPointer(node); - int bytesJumped = chainBlockLength(node) - 1; // leave the last byte for incomingTransition + UnsafeBuffer buffer = getBuffer(node); + int inBufferNode = inBufferOffset(node); + int bytesJumped = chainCellLength(node) - 1; // leave the last byte for incomingTransition if (receiver != null && bytesJumped > 0) - receiver.addPathBytes(chunk, inChunkNode, bytesJumped); + receiver.addPathBytes(buffer, inBufferNode, bytesJumped); depth += bytesJumped; // descendInto will add one - inChunkNode += bytesJumped; + inBufferNode += bytesJumped; - // inChunkNode is now positioned on the last byte of the chain. + // inBufferNode is now positioned on the last byte of the chain. // Consume it to be the new state's incomingTransition. - int transition = chunk.getByte(inChunkNode++) & 0xFF; - // inChunkNode is now positioned on the child pointer. - int child = chunk.getInt(inChunkNode); + int transition = buffer.getByte(inBufferNode++) & 0xFF; + // inBufferNode is now positioned on the child pointer. + int child = buffer.getIntVolatile(inBufferNode); return descendInto(child, transition); } @Override - public int skipChildren() + public int skipTo(int skipDepth, int skipTransition) { - return backtrack(); + if (skipDepth > depth) + { + // Descent requested. Jump to the given child transition or greater, and backtrack if there's no such. + assert skipDepth == depth + 1; + int advancedDepth = advanceToChildWithTarget(currentNode, skipTransition); + if (advancedDepth < 0) + return backtrack(); + + assert advancedDepth == skipDepth; + return advancedDepth; + } + + // Backtrack until we reach the requested depth. Note that we may have more than one entry for a given + // depth (split sublevels) and we ascend through them individually. + while (--backtrackDepth >= 0) + { + depth = depth(backtrackDepth); + + if (depth < skipDepth - 1) + return advanceToNextChild(node(backtrackDepth), data(backtrackDepth)); + + if (depth == skipDepth - 1) + { + int advancedDepth = advanceToNextChildWithTarget(node(backtrackDepth), data(backtrackDepth), skipTransition); + if (advancedDepth >= 0) + return advancedDepth; + } + } + return exhausted(); } @Override @@ -606,10 +649,39 @@ public int incomingTransition() return incomingTransition; } + @Override + public Direction direction() + { + return direction; + } + + @Override + public ByteComparable.Version byteComparableVersion() + { + return byteComparableVersion; + } + + @Override + public Trie tailTrie() + { + assert depth >= 0 : "tailTrie called on exhausted cursor"; + return new InMemoryReadTrie<>(byteComparableVersion, buffers, contentArrays, currentFullNode); + } + + private int exhausted() + { + depth = -1; + incomingTransition = -1; + currentFullNode = NONE; + currentNode = NONE; + content = null; + return -1; + } + private int backtrack() { if (--backtrackDepth < 0) - return depth = -1; + return exhausted(); depth = depth(backtrackDepth); return advanceToNextChild(node(backtrackDepth), data(backtrackDepth)); @@ -624,12 +696,28 @@ private int advanceToFirstChild(int node) case SPLIT_OFFSET: return descendInSplitSublevel(node, SPLIT_START_LEVEL_LIMIT, 0, SPLIT_LEVEL_SHIFT * 2); case SPARSE_OFFSET: - return nextValidSparseTransition(node, getUnsignedShort(node + SPARSE_ORDER_OFFSET)); + return nextValidSparseTransition(node, prepareOrderWord(node)); default: return getChainTransition(node); } } + private int advanceToChildWithTarget(int node, int skipTransition) + { + if (isNullOrLeaf(node)) + return -1; + + switch (offset(node)) + { + case SPLIT_OFFSET: + return descendInSplitSublevelWithTarget(node, SPLIT_START_LEVEL_LIMIT, 0, SPLIT_LEVEL_SHIFT * 2, skipTransition); + case SPARSE_OFFSET: + return advanceToSparseTransition(node, prepareOrderWord(node), skipTransition); + default: + return advanceToChainTransition(node, skipTransition); + } + } + private int advanceToNextChild(int node, int data) { assert (!isNullOrLeaf(node)); @@ -645,19 +733,34 @@ private int advanceToNextChild(int node, int data) } } + private int advanceToNextChildWithTarget(int node, int data, int transition) + { + assert (!isNullOrLeaf(node)); + + switch (offset(node)) + { + case SPLIT_OFFSET: + return advanceToSplitTransition(node, data, transition); + case SPARSE_OFFSET: + return advanceToSparseTransition(node, data, transition); + default: + throw new AssertionError("Unexpected node type in backtrack state."); + } + } + /** * Descend into the sub-levels of a split node. Advances to the first child and creates backtracking entries * for the following ones. We use the bits of trans (lowest non-zero ones) to identify which sub-level an * entry refers to. * - * @param node The node or block id, must have offset SPLIT_OFFSET. + * @param node The node or cell id, must have offset SPLIT_OFFSET. * @param limit The transition limit for the current sub-level (4 for the start, 8 for the others). * @param collected The transition bits collected from the parent chain (e.g. 0x40 after following 1 on the top * sub-level). * @param shift This level's bit shift (6 for start, 3 for mid and 0 for tail). * @return the depth reached after descending. */ - private int descendInSplitSublevel(int node, int limit, int collected, int shift) + int descendInSplitSublevel(int node, int limit, int collected, int shift) { while (true) { @@ -665,9 +768,11 @@ private int descendInSplitSublevel(int node, int limit, int collected, int shift int childIndex; int child = NONE; // find the first non-null child - for (childIndex = 0; childIndex < limit; ++childIndex) + for (childIndex = direction.select(0, limit - 1); + direction.inLoop(childIndex, 0, limit - 1); + childIndex += direction.increase) { - child = getSplitBlockPointer(node, childIndex, limit); + child = getSplitCellPointer(node, childIndex, limit); if (!isNull(child)) break; } @@ -691,111 +796,297 @@ private int descendInSplitSublevel(int node, int limit, int collected, int shift } /** - * Backtrack to a split sub-level. The level is identified by the lowest non-0 bits in trans. + * As above, but also makes sure that the descend selects a value at least as big as the given + * {@code minTransition}. + */ + private int descendInSplitSublevelWithTarget(int node, int limit, int collected, int shift, int minTransition) + { + minTransition -= collected; + if (minTransition >= limit << shift || minTransition < 0) + return -1; + + while (true) + { + assert offset(node) == SPLIT_OFFSET; + int childIndex; + int child = NONE; + boolean isExact = true; + // find the first non-null child beyond minTransition + for (childIndex = minTransition >> shift; + direction.inLoop(childIndex, 0, limit - 1); + childIndex += direction.increase) + { + child = getSplitCellPointer(node, childIndex, limit); + if (!isNull(child)) + break; + isExact = false; + } + if (!isExact && (childIndex == limit || childIndex == -1)) + return -1; + + // look for any more valid transitions and add backtracking if found + maybeAddSplitBacktrack(node, childIndex, limit, collected, shift); + + // add the bits just found + collected |= childIndex << shift; + // descend to next sub-level or child + if (shift == 0) + return descendInto(child, collected); + + if (isExact) + minTransition -= childIndex << shift; + else + minTransition = direction.select(0, (1 << shift) - 1); + + // continue with next sublevel; same as + // return descendInSplitSublevelWithTarget(child + SPLIT_OFFSET, 8, collected, shift - 3, minTransition) + node = child; + limit = SPLIT_OTHER_LEVEL_LIMIT; + shift -= SPLIT_LEVEL_SHIFT; + } + } + + /** + * Backtrack to a split sub-level. The level is identified by the lowest non-0 bits in data. */ - private int nextValidSplitTransition(int node, int trans) + int nextValidSplitTransition(int node, int data) { - assert trans >= 0 && trans <= 0xFF; - int childIndex = splitNodeChildIndex(trans); - if (childIndex > 0) + // Note: This is equivalent to return advanceToSplitTransition(node, data, data) but quicker. + assert data >= 0 && data <= 0xFF; + int childIndex = splitNodeChildIndex(data); + if (childIndex != direction.select(0, SPLIT_OTHER_LEVEL_LIMIT - 1)) { maybeAddSplitBacktrack(node, childIndex, SPLIT_OTHER_LEVEL_LIMIT, - trans & -(1 << (SPLIT_LEVEL_SHIFT * 1)), + data & -(1 << (SPLIT_LEVEL_SHIFT * 1)), SPLIT_LEVEL_SHIFT * 0); - int child = getSplitBlockPointer(node, childIndex, SPLIT_OTHER_LEVEL_LIMIT); - return descendInto(child, trans); + int child = getSplitCellPointer(node, childIndex, SPLIT_OTHER_LEVEL_LIMIT); + return descendInto(child, data); } - int tailIndex = splitNodeTailIndex(trans); - if (tailIndex > 0) + int tailIndex = splitNodeTailIndex(data); + if (tailIndex != direction.select(0, SPLIT_OTHER_LEVEL_LIMIT - 1)) { maybeAddSplitBacktrack(node, tailIndex, SPLIT_OTHER_LEVEL_LIMIT, - trans & -(1 << (SPLIT_LEVEL_SHIFT * 2)), + data & -(1 << (SPLIT_LEVEL_SHIFT * 2)), SPLIT_LEVEL_SHIFT * 1); - int tail = getSplitBlockPointer(node, tailIndex, SPLIT_OTHER_LEVEL_LIMIT); + int tail = getSplitCellPointer(node, tailIndex, SPLIT_OTHER_LEVEL_LIMIT); return descendInSplitSublevel(tail, SPLIT_OTHER_LEVEL_LIMIT, - trans, + data & -(1 << SPLIT_LEVEL_SHIFT * 1), SPLIT_LEVEL_SHIFT * 0); } - int midIndex = splitNodeMidIndex(trans); - assert midIndex > 0; + int midIndex = splitNodeMidIndex(data); + assert midIndex != direction.select(0, SPLIT_START_LEVEL_LIMIT - 1); maybeAddSplitBacktrack(node, midIndex, SPLIT_START_LEVEL_LIMIT, 0, SPLIT_LEVEL_SHIFT * 2); - int mid = getSplitBlockPointer(node, midIndex, SPLIT_START_LEVEL_LIMIT); + int mid = getSplitCellPointer(node, midIndex, SPLIT_START_LEVEL_LIMIT); return descendInSplitSublevel(mid, SPLIT_OTHER_LEVEL_LIMIT, - trans, + data & -(1 << SPLIT_LEVEL_SHIFT * 2), SPLIT_LEVEL_SHIFT * 1); } + /** + * Backtrack to a split sub-level and advance to given transition if it fits within the sublevel. + * The level is identified by the lowest non-0 bits in data as above. + */ + private int advanceToSplitTransition(int node, int data, int skipTransition) + { + assert data >= 0 && data <= 0xFF; + if (direction.lt(skipTransition, data)) + return nextValidSplitTransition(node, data); // already went over the target in lower sublevel, just advance + + int childIndex = splitNodeChildIndex(data); + if (childIndex != direction.select(0, SPLIT_OTHER_LEVEL_LIMIT - 1)) + { + int sublevelMask = -(1 << (SPLIT_LEVEL_SHIFT * 1)); + int sublevelShift = SPLIT_LEVEL_SHIFT * 0; + int sublevelLimit = SPLIT_OTHER_LEVEL_LIMIT; + return descendInSplitSublevelWithTarget(node, sublevelLimit, data & sublevelMask, sublevelShift, skipTransition); + } + int tailIndex = splitNodeTailIndex(data); + if (tailIndex != direction.select(0, SPLIT_OTHER_LEVEL_LIMIT - 1)) + { + int sublevelMask = -(1 << (SPLIT_LEVEL_SHIFT * 2)); + int sublevelShift = SPLIT_LEVEL_SHIFT * 1; + int sublevelLimit = SPLIT_OTHER_LEVEL_LIMIT; + return descendInSplitSublevelWithTarget(node, sublevelLimit, data & sublevelMask, sublevelShift, skipTransition); + } + int sublevelMask = -(1 << 8); + int sublevelShift = SPLIT_LEVEL_SHIFT * 2; + int sublevelLimit = SPLIT_START_LEVEL_LIMIT; + return descendInSplitSublevelWithTarget(node, sublevelLimit, data & sublevelMask, sublevelShift, skipTransition); + } + /** * Look for any further non-null transitions on this sub-level and, if found, add a backtracking entry. */ private void maybeAddSplitBacktrack(int node, int startAfter, int limit, int collected, int shift) { int nextChildIndex; - for (nextChildIndex = startAfter + 1; nextChildIndex < limit; ++nextChildIndex) + for (nextChildIndex = startAfter + direction.increase; + direction.inLoop(nextChildIndex, 0, limit - 1); + nextChildIndex += direction.increase) { - if (!isNull(getSplitBlockPointer(node, nextChildIndex, limit))) + if (!isNull(getSplitCellPointer(node, nextChildIndex, limit))) break; } - if (nextChildIndex < limit) - addBacktrack(node, collected | (nextChildIndex << shift), depth); + if (direction.inLoop(nextChildIndex, 0, limit - 1)) + { + if (direction.isForward()) + addBacktrack(node, collected | (nextChildIndex << shift), depth); + else + { + // The (((x + 1) << shift) - 1) adjustment will put all 1s in all lower bits + addBacktrack(node, collected | ((((nextChildIndex + 1) << shift)) - 1), depth); + } + } } + private int nextValidSparseTransition(int node, int data) { - UnsafeBuffer chunk = getChunk(node); - int inChunkNode = inChunkPointer(node); - // Peel off the next index. int index = data % SPARSE_CHILD_COUNT; data = data / SPARSE_CHILD_COUNT; + UnsafeBuffer buffer = getBuffer(node); + int inBufferNode = inBufferOffset(node); + + // If there are remaining transitions, add backtracking entry. + if (data != exhaustedOrderWord()) + addBacktrack(node, data, depth); + + // Follow the transition. + int child = buffer.getIntVolatile(inBufferNode + SPARSE_CHILDREN_OFFSET + index * 4); + int transition = buffer.getByte(inBufferNode + SPARSE_BYTES_OFFSET + index) & 0xFF; + return descendInto(child, transition); + } + + /** + * Prepare the sparse node order word for iteration. For forward iteration, this means just reading it. + * For reverse, we also invert the data so that the peeling code above still works. + */ + int prepareOrderWord(int node) + { + int fwdState = getUnsignedShortVolatile(node + SPARSE_ORDER_OFFSET); + if (direction.isForward()) + return fwdState; + else + { + // Produce an inverted state word. + + // One subtlety is that in forward order we know we can terminate the iteration when the state becomes + // 0 because 0 cannot be the largest child (we enforce 10 order for the first two children and then can + // only insert other digits in the word, thus 0 is always preceded by a 1 (not necessarily immediately) + // in the order word) and thus we can't confuse a completed iteration with one that still has the child + // at 0 to present. + // In reverse order 0 can be the last child that needs to be iterated (e.g. for two children the order + // word is always 10, which is 01 inverted; if we treat it exactly as the forward iteration, we will + // only list child 1 because we will interpret the state 0 after peeling the first digit as a completed + // iteration). To know when to stop we must thus use a different marker - since we know 1 is never the + // last child to be iterated in reverse order (because it is preceded by a 0 in the reversed order + // word), we can use another 1 as the termination marker. The generated number may not fit a 16-bit word + // any more, but that does not matter as we don't need to store it. + // For example, the code below translates 120 to 1021, and to iterate we peel the lower order digits + // until the iteration state becomes just 1. + + int revState = 1; // 1 can't be the smallest child + while (fwdState != 0) + { + revState = revState * SPARSE_CHILD_COUNT + fwdState % SPARSE_CHILD_COUNT; + fwdState /= SPARSE_CHILD_COUNT; + } + + return revState; + } + } + + /** + * Returns the state which marks the exhaustion of the order word. + */ + int exhaustedOrderWord() + { + return direction.select(0, 1); + } + + private int advanceToSparseTransition(int node, int data, int skipTransition) + { + UnsafeBuffer buffer = getBuffer(node); + int inBufferNode = inBufferOffset(node); + int index; + int transition; + do + { + // Peel off the next index. + index = data % SPARSE_CHILD_COUNT; + data = data / SPARSE_CHILD_COUNT; + transition = buffer.getByte(inBufferNode + SPARSE_BYTES_OFFSET + index) & 0xFF; + } + while (direction.lt(transition, skipTransition) && data != exhaustedOrderWord()); + if (direction.lt(transition, skipTransition)) + return -1; + // If there are remaining transitions, add backtracking entry. - if (data > 0) + if (data != exhaustedOrderWord()) addBacktrack(node, data, depth); // Follow the transition. - int child = chunk.getInt(inChunkNode + SPARSE_CHILDREN_OFFSET + index * 4); - int transition = chunk.getByte(inChunkNode + SPARSE_BYTES_OFFSET + index) & 0xFF; + int child = buffer.getIntVolatile(inBufferNode + SPARSE_CHILDREN_OFFSET + index * 4); return descendInto(child, transition); } private int getChainTransition(int node) { // No backtracking needed. - UnsafeBuffer chunk = getChunk(node); - int inChunkNode = inChunkPointer(node); - int transition = chunk.getByte(inChunkNode) & 0xFF; + UnsafeBuffer buffer = getBuffer(node); + int inBufferNode = inBufferOffset(node); + int transition = buffer.getByte(inBufferNode) & 0xFF; + int next = node + 1; + if (offset(next) <= CHAIN_MAX_OFFSET) + return descendIntoChain(next, transition); + else + return descendInto(buffer.getIntVolatile(inBufferNode + 1), transition); + } + + private int advanceToChainTransition(int node, int skipTransition) + { + // No backtracking needed. + UnsafeBuffer buffer = getBuffer(node); + int inBufferNode = inBufferOffset(node); + int transition = buffer.getByte(inBufferNode) & 0xFF; + if (direction.gt(skipTransition, transition)) + return -1; + int next = node + 1; if (offset(next) <= CHAIN_MAX_OFFSET) return descendIntoChain(next, transition); else - return descendInto(chunk.getInt(inChunkNode + 1), transition); + return descendInto(buffer.getIntVolatile(inBufferNode + 1), transition); } - private int descendInto(int child, int transition) + int descendInto(int child, int transition) { ++depth; incomingTransition = transition; content = getNodeContent(child); + currentFullNode = child; currentNode = followContentTransition(child); return depth; } - private int descendIntoChain(int child, int transition) + int descendIntoChain(int child, int transition) { ++depth; incomingTransition = transition; content = null; + currentFullNode = child; currentNode = child; return depth; } @@ -806,9 +1097,9 @@ private boolean isChainNode(int node) return !isNullOrLeaf(node) && offset(node) <= CHAIN_MAX_OFFSET; } - public MemtableCursor cursor() + public InMemoryCursor cursor(Direction direction) { - return new MemtableCursor(); + return new InMemoryCursor(direction); } /* @@ -819,10 +1110,11 @@ public MemtableCursor cursor() * Get the content mapped by the specified key. * Fast implementation using integer node addresses. */ + @Override public T get(ByteComparable path) { int n = root; - ByteSource source = path.asComparableBytes(BYTE_COMPARABLE_VERSION); + ByteSource source = path.asComparableBytes(byteComparableVersion); while (!isNull(n)) { int c = source.next(); @@ -840,6 +1132,11 @@ public boolean isEmpty() return isNull(root); } + public ByteComparable.Version byteComparableVersion() + { + return byteComparableVersion; + } + /** * Override of dump to provide more detailed printout that includes the type of each node in the trie. * We do this via a wrapping cursor that returns a content string for the type of node for every node we return. @@ -847,7 +1144,7 @@ public boolean isEmpty() @Override public String dump(Function contentToString) { - MemtableCursor source = cursor(); + InMemoryCursor source = cursor(Direction.FORWARD); class TypedNodesCursor implements Cursor { @Override @@ -864,9 +1161,9 @@ public int advanceMultiple(TransitionsReceiver receiver) } @Override - public int skipChildren() + public int skipTo(int skipDepth, int skipTransition) { - return source.skipChildren(); + return source.skipTo(skipDepth, skipTransition); } @Override @@ -881,6 +1178,24 @@ public int incomingTransition() return source.incomingTransition(); } + @Override + public Direction direction() + { + return source.direction(); + } + + @Override + public ByteComparable.Version byteComparableVersion() + { + return source.byteComparableVersion(); + } + + @Override + public Trie tailTrie() + { + throw new AssertionError(); + } + @Override public String content() { @@ -917,4 +1232,73 @@ public String content() } return process(new TrieDumper<>(Function.identity()), new TypedNodesCursor()); } + + /** + * For use in debugging, dump info about the given node. + */ + @SuppressWarnings("unused") + String dumpNode(int node) + { + if (isNull(node)) + return "NONE"; + else if (isLeaf(node)) + return "~" + (~node); + else + { + StringBuilder builder = new StringBuilder(); + builder.append(node + " "); + switch (offset(node)) + { + case SPARSE_OFFSET: + { + builder.append("Sparse: "); + for (int i = 0; i < SPARSE_CHILD_COUNT; ++i) + { + int child = getIntVolatile(node + SPARSE_CHILDREN_OFFSET + i * 4); + if (child != NONE) + builder.append(String.format("%02x", getUnsignedByte(node + SPARSE_BYTES_OFFSET + i))) + .append(" -> ") + .append(child) + .append('\n'); + } + break; + } + case SPLIT_OFFSET: + { + builder.append("Split: "); + for (int i = 0; i < SPLIT_START_LEVEL_LIMIT; ++i) + { + int child = getIntVolatile(node - (SPLIT_START_LEVEL_LIMIT - 1 - i) * 4); + if (child != NONE) + builder.append(Integer.toBinaryString(i)) + .append(" -> ") + .append(child) + .append('\n'); + } + break; + } + case PREFIX_OFFSET: + { + builder.append("Prefix: "); + int flags = getUnsignedByte(node + PREFIX_FLAGS_OFFSET); + final int content = getIntVolatile(node + PREFIX_CONTENT_OFFSET); + builder.append(content < 0 ? "~" + (~content) : "" + content); + int child = followContentTransition(node); + builder.append(" -> ") + .append(child); + break; + } + default: + { + builder.append("Chain: "); + for (int i = 0; i < chainCellLength(node); ++i) + builder.append(String.format("%02x", getUnsignedByte(node + i))); + builder.append(" -> ") + .append(getIntVolatile(node + chainCellLength(node))); + break; + } + } + return builder.toString(); + } + } } diff --git a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.java b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.java index 9bda82057f9a..7b7ac064418e 100644 --- a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.java +++ b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.java @@ -19,11 +19,15 @@ import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Iterator; -import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicReferenceArray; +import javax.annotation.Nonnull; + import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; + +import java.util.function.Predicate; import org.agrona.concurrent.UnsafeBuffer; import org.apache.cassandra.config.CassandraRelevantProperties; @@ -32,17 +36,42 @@ import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.concurrent.OpOrder; -import org.github.jamm.MemoryMeterStrategy; +import static org.github.jamm.MemoryMeterStrategy.MEMORY_LAYOUT; /** * In-memory trie built for fast modification and reads executing concurrently with writes from a single mutator thread. - * - * This class can currently only provide atomicity (i.e. reads seeing either the content before a write, or the - * content after it; any read seeing the write enforcing any subsequent (i.e. started after it completed) reads to - * also see it) for singleton writes (i.e. calls to {@link #putRecursive}, {@link #putSingleton} or {@link #apply} - * with a singleton trie as argument). - * + *

    + * The main method for performing writes is {@link #apply(Trie, UpsertTransformer, Predicate)} which takes a trie as + * an argument and merges it into the current trie using the methods supplied by the given {@link UpsertTransformer}, + * force copying anything below the points where the third argument returns true. + *

    + * The predicate can be used to implement several forms of atomicity and consistency guarantees: + * + *

  • if the predicate is {@code nf -> false}, neither atomicity nor sequential consistency is guaranteed - readers + * can see any mixture of old and modified content + *
  • if the predicate is {@code nf -> true}, full sequential consistency will be provided, i.e. if a reader sees any + * part of a modification, it will see all of it, and all the results of all previous modifications + *
  • if the predicate is {@code nf -> nf.isBranching()} the write will be atomic, i.e. either none or all of the + * content of the merged trie will be visible by concurrent readers, but not sequentially consistent, i.e. there + * may be writes that are not visible to a reader even when they precede writes that are visible. + *
  • if the predicate is {@code nf -> (nf.content())} the write will be consistent below the identified + * point (used e.g. by Memtable to ensure partition-level consistency) + * + *

    + * Additionally, the class provides several simpler write methods for efficiency and convenience: + * + *

  • {@link #putRecursive(ByteComparable, Object, UpsertTransformer)} inserts a single value using a recursive walk. + * It cannot provide consistency (single-path writes are always atomic). This is more efficient as it stores the + * walk state in the stack rather than on the heap but can cause a {@code StackOverflowException}. + *
  • {@link #putSingleton(ByteComparable, Object, UpsertTransformer)} is a non-recursive version of the above, using + * the {@code apply} machinery. + *
  • {@link #putSingleton(ByteComparable, Object, UpsertTransformer, boolean)} uses the fourth argument to choose + * between the two methods above, where some external property can be used to decide if the keys are short enough + * to permit recursive execution. + * + *

    * Because it uses 32-bit pointers in byte buffers, this trie has a fixed size limit of 2GB. */ public class InMemoryTrie extends InMemoryReadTrie @@ -60,9 +89,9 @@ public class InMemoryTrie extends InMemoryReadTrie { // Default threshold + 10% == 2 GB. This should give the owner enough time to react to the // {@link #reachedAllocatedSizeThreshold()} signal and switch this trie out before it fills up. - int limitInMB = CassandraRelevantProperties.MEMTABLE_OVERHEAD_SIZE.getInt(2048 * 10 / 11); + int limitInMB = CassandraRelevantProperties.MEMTABLE_TRIE_SIZE_LIMIT.getInt(2048 * 10 / 11); if (limitInMB < 1 || limitInMB > 2047) - throw new AssertionError(CassandraRelevantProperties.MEMTABLE_OVERHEAD_SIZE.getKey() + + throw new AssertionError(CassandraRelevantProperties.MEMTABLE_TRIE_SIZE_LIMIT.getKey() + " must be within 1 and 2047"); ALLOCATED_SIZE_THRESHOLD = 1024 * 1024 * limitInMB; } @@ -70,7 +99,10 @@ public class InMemoryTrie extends InMemoryReadTrie private int allocatedPos = 0; private int contentCount = 0; - private final BufferType bufferType; // on or off heap + final BufferType bufferType; // on or off heap + final MemoryAllocationStrategy cellAllocator; + final MemoryAllocationStrategy objectAllocator; + // constants for space calculations private static final long EMPTY_SIZE_ON_HEAP; @@ -79,74 +111,131 @@ public class InMemoryTrie extends InMemoryReadTrie static { - InMemoryTrie empty = new InMemoryTrie<>(BufferType.ON_HEAP); + // Measuring the empty size of long-lived tries, because these are the ones for which we want to track size. + InMemoryTrie empty = new InMemoryTrie<>(ByteComparable.Version.OSS50, BufferType.ON_HEAP, ExpectedLifetime.LONG, null); EMPTY_SIZE_ON_HEAP = ObjectSizes.measureDeep(empty); - empty = new InMemoryTrie<>(BufferType.OFF_HEAP); + empty = new InMemoryTrie<>(ByteComparable.Version.OSS50, BufferType.OFF_HEAP, ExpectedLifetime.LONG, null); EMPTY_SIZE_OFF_HEAP = ObjectSizes.measureDeep(empty); } - public InMemoryTrie(BufferType bufferType) + enum ExpectedLifetime + { + SHORT, LONG + } + + InMemoryTrie(ByteComparable.Version byteComparableVersion, BufferType bufferType, ExpectedLifetime lifetime, OpOrder opOrder) { - super(new UnsafeBuffer[31 - BUF_START_SHIFT], // last one is 1G for a total of ~2G bytes + super(byteComparableVersion, + new UnsafeBuffer[31 - BUF_START_SHIFT], // last one is 1G for a total of ~2G bytes new AtomicReferenceArray[29 - CONTENTS_START_SHIFT], // takes at least 4 bytes to write pointer to one content -> 4 times smaller than buffers NONE); this.bufferType = bufferType; + + switch (lifetime) + { + case SHORT: + cellAllocator = new MemoryAllocationStrategy.NoReuseStrategy(new MemoryAllocationStrategy.Allocator() + { + @Override + public int allocate() throws TrieSpaceExhaustedException + { + return allocateNewCell(); + } + }); + objectAllocator = new MemoryAllocationStrategy.NoReuseStrategy(new MemoryAllocationStrategy.Allocator() + { + @Override + public int allocate() + { + return allocateNewObject(); + } + }); + break; + case LONG: + cellAllocator = new MemoryAllocationStrategy.OpOrderReuseStrategy(new MemoryAllocationStrategy.Allocator() + { + @Override + public int allocate() throws TrieSpaceExhaustedException + { + return allocateNewCell(); + } + }, opOrder); + objectAllocator = new MemoryAllocationStrategy.OpOrderReuseStrategy(new MemoryAllocationStrategy.Allocator() + { + @Override + public int allocate() + { + return allocateNewObject(); + } + }, opOrder); + break; + default: + throw new AssertionError(); + } } - // Buffer, content list and block management + public static InMemoryTrie shortLived(ByteComparable.Version byteComparableVersion) + { + return new InMemoryTrie<>(byteComparableVersion, BufferType.ON_HEAP, ExpectedLifetime.SHORT, null); + } - /** - * Because we use buffers and 32-bit pointers, the trie cannot grow over 2GB of size. This exception is thrown if - * a trie operation needs it to grow over that limit. - * - * To avoid this problem, users should query {@link #reachedAllocatedSizeThreshold} from time to time. If the call - * returns true, they should switch to a new trie (e.g. by flushing a memtable) as soon as possible. The threshold - * is configurable, and is set by default to 10% under the 2GB limit to give ample time for the switch to happen. - */ - public static class SpaceExhaustedException extends Exception + public static InMemoryTrie shortLived(ByteComparable.Version byteComparableVersion, BufferType bufferType) { - public SpaceExhaustedException() - { - super("The hard 2GB limit on trie size has been exceeded"); - } + return new InMemoryTrie<>(byteComparableVersion, bufferType, ExpectedLifetime.SHORT, null); } - final void putInt(int pos, int value) + public static InMemoryTrie longLived(ByteComparable.Version byteComparableVersion, OpOrder opOrder) { - getChunk(pos).putInt(inChunkPointer(pos), value); + return longLived(byteComparableVersion, BufferType.OFF_HEAP, opOrder); } - final void putIntVolatile(int pos, int value) + public static InMemoryTrie longLived(ByteComparable.Version byteComparableVersion, BufferType bufferType, OpOrder opOrder) { - getChunk(pos).putIntVolatile(inChunkPointer(pos), value); + return new InMemoryTrie<>(byteComparableVersion, bufferType, ExpectedLifetime.LONG, opOrder); } - final void putShort(int pos, short value) + + // Buffer, content list and cell management + + private void putInt(int pos, int value) { - getChunk(pos).putShort(inChunkPointer(pos), value); + getBuffer(pos).putInt(inBufferOffset(pos), value); } - final void putShortVolatile(int pos, short value) + private void putIntVolatile(int pos, int value) { - getChunk(pos).putShort(inChunkPointer(pos), value); + getBuffer(pos).putIntVolatile(inBufferOffset(pos), value); } - final void putByte(int pos, byte value) + private void putShort(int pos, short value) { - getChunk(pos).putByte(inChunkPointer(pos), value); + getBuffer(pos).putShort(inBufferOffset(pos), value); } + private void putShortVolatile(int pos, short value) + { + getBuffer(pos).putShort(inBufferOffset(pos), value); + } - private int allocateBlock() throws SpaceExhaustedException + private void putByte(int pos, byte value) + { + getBuffer(pos).putByte(inBufferOffset(pos), value); + } + + /** + * Allocate a new cell in the data buffers. This is called by the memory allocation strategy when it runs out of + * free cells to reuse. + */ + private int allocateNewCell() throws TrieSpaceExhaustedException { // Note: If this method is modified, please run InMemoryTrieTest.testOver1GSize to verify it acts correctly // close to the 2G limit. int v = allocatedPos; - if (inChunkPointer(v) == 0) + if (inBufferOffset(v) == 0) { - int leadBit = getChunkIdx(v, BUF_START_SHIFT, BUF_START_SIZE); + int leadBit = getBufferIdx(v, BUF_START_SHIFT, BUF_START_SIZE); if (leadBit + BUF_START_SHIFT == 31) - throw new SpaceExhaustedException(); + throw new TrieSpaceExhaustedException(); ByteBuffer newBuffer = bufferType.allocate(BUF_START_SIZE << leadBit); buffers[leadBit] = new UnsafeBuffer(newBuffer); @@ -155,34 +244,98 @@ private int allocateBlock() throws SpaceExhaustedException // that attached the new path. } - allocatedPos += BLOCK_SIZE; + allocatedPos += CELL_SIZE; return v; } - private int addContent(T value) + /** + * Allocate a cell to use for storing data. This uses the memory allocation strategy to reuse cells if any are + * available, or to allocate new cells using {@link #allocateNewCell}. Because some node types rely on cells being + * filled with 0 as initial state, any cell we get through the allocator must also be cleaned. + */ + private int allocateCell() throws TrieSpaceExhaustedException + { + int cell = cellAllocator.allocate(); + getBuffer(cell).setMemory(inBufferOffset(cell), CELL_SIZE, (byte) 0); + return cell; + } + + private void recycleCell(int cell) + { + cellAllocator.recycle(cell & -CELL_SIZE); + } + + /** + * Creates a copy of a given cell and marks the original for recycling. Used when a mutation needs to force-copy + * paths to ensure earlier states are still available for concurrent readers. + */ + private int copyCell(int cell) throws TrieSpaceExhaustedException + { + int copy = cellAllocator.allocate(); + getBuffer(copy).putBytes(inBufferOffset(copy), getBuffer(cell), inBufferOffset(cell & -CELL_SIZE), CELL_SIZE); + recycleCell(cell); + return copy | (cell & (CELL_SIZE - 1)); + } + + /** + * Allocate a new position in the object array. Used by the memory allocation strategy to allocate a content spot + * when it runs out of recycled positions. + */ + private int allocateNewObject() { int index = contentCount++; - int leadBit = getChunkIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); - int ofs = inChunkPointer(index, leadBit, CONTENTS_START_SIZE); + int leadBit = getBufferIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); AtomicReferenceArray array = contentArrays[leadBit]; if (array == null) { - assert ofs == 0 : "Error in content arrays configuration."; - contentArrays[leadBit] = array = new AtomicReferenceArray<>(CONTENTS_START_SIZE << leadBit); + assert inBufferOffset(index, leadBit, CONTENTS_START_SIZE) == 0 : "Error in content arrays configuration."; + contentArrays[leadBit] = new AtomicReferenceArray<>(CONTENTS_START_SIZE << leadBit); } - array.lazySet(ofs, value); // no need for a volatile set here; at this point the item is not referenced - // by any node in the trie, and a volatile set will be made to reference it. return index; } - private void setContent(int index, T value) + + /** + * Add a new content value. + * + * @return A content id that can be used to reference the content, encoded as ~index where index is the + * position of the value in the content array. + */ + private int addContent(@Nonnull T value) throws TrieSpaceExhaustedException + { + Preconditions.checkNotNull(value, "Content value cannot be null"); + int index = objectAllocator.allocate(); + int leadBit = getBufferIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); + int ofs = inBufferOffset(index, leadBit, CONTENTS_START_SIZE); + AtomicReferenceArray array = contentArrays[leadBit]; + // no need for a volatile set here; at this point the item is not referenced + // by any node in the trie, and a volatile set will be made to reference it. + array.setPlain(ofs, value); + return ~index; + } + + /** + * Change the content associated with a given content id. + * + * @param id content id, encoded as ~index where index is the position in the content array + * @param value new content value to store + */ + private void setContent(int id, T value) { - int leadBit = getChunkIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); - int ofs = inChunkPointer(index, leadBit, CONTENTS_START_SIZE); + int leadBit = getBufferIdx(~id, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); + int ofs = inBufferOffset(~id, leadBit, CONTENTS_START_SIZE); AtomicReferenceArray array = contentArrays[leadBit]; array.set(ofs, value); } + private void releaseContent(int id) + { + objectAllocator.recycle(~id); + } + + /** + * Called to clean up all buffers when the trie is known to no longer be needed. + */ public void discardBuffers() { if (bufferType == BufferType.ON_HEAP) @@ -195,6 +348,42 @@ public void discardBuffers() } } + private int copyIfOriginal(int node, int originalNode) throws TrieSpaceExhaustedException + { + return (node == originalNode) + ? copyCell(originalNode) + : node; + } + + private int getOrAllocate(int pointerAddress, int offsetWhenAllocating) throws TrieSpaceExhaustedException + { + int child = getIntVolatile(pointerAddress); + if (child != NONE) + return child; + + child = allocateCell() | offsetWhenAllocating; + // volatile writes not needed because this branch is not attached yet + putInt(pointerAddress, child); + return child; + } + + private int getCopyOrAllocate(int pointerAddress, int originalChild, int offsetWhenAllocating) throws TrieSpaceExhaustedException + { + int child = getIntVolatile(pointerAddress); + if (child == originalChild) + { + if (originalChild == NONE) + child = allocateCell() | offsetWhenAllocating; + else + child = copyCell(originalChild); + + // volatile writes not needed because this branch is not attached yet + putInt(pointerAddress, child); + } + + return child; + } + // Write methods // Write visibility model: writes are not volatile, with the exception of the final write before a call returns @@ -209,8 +398,52 @@ public void discardBuffers() * Attach a child to the given non-content node. This may be an update for an existing branch, or a new child for * the node. An update _is_ required (i.e. this is only called when the newChild pointer is not the same as the * existing value). + * This method is called when the original node content must be preserved for concurrent readers (i.e. any cell to + * be modified needs to be copied first.) + * + * @param node pointer to the node to update or copy + * @param originalNode pointer to the node as it was before any updates in the current modification (i.e. apply + * call) were started. In other words, the node that is currently reachable by readers if they + * follow the same key, and which will become unreachable for new readers after this update + * completes. Used to avoid copying again if already done -- if node is already != originalNode + * (which is the case when a second or further child of a node is changed by an update), + * then node is currently not reachable and can be safely modified or completely overwritten. + * @param trans transition to modify/add + * @param newChild new child pointer + * @return pointer to the updated node */ - private int attachChild(int node, int trans, int newChild) throws SpaceExhaustedException + private int attachChildCopying(int node, int originalNode, int trans, int newChild) throws TrieSpaceExhaustedException + { + assert !isLeaf(node) : "attachChild cannot be used on content nodes."; + + switch (offset(node)) + { + case PREFIX_OFFSET: + assert false : "attachChild cannot be used on content nodes."; + case SPARSE_OFFSET: + // If the node is already copied (e.g. this is not the first child being modified), there's no need to copy + // it again. + return attachChildToSparseCopying(node, originalNode, trans, newChild); + case SPLIT_OFFSET: + // This call will copy the split node itself and any intermediate cells as necessary to make sure cells + // reachable from the original node are not modified. + return attachChildToSplitCopying(node, originalNode, trans, newChild); + default: + // chain nodes + return attachChildToChainCopying(node, originalNode, trans, newChild); // always copies + } + } + + /** + * Attach a child to the given node. This may be an update for an existing branch, or a new child for the node. + * An update _is_ required (i.e. this is only called when the newChild pointer is not the same as the existing value). + * + * @param node pointer to the node to update or copy + * @param trans transition to modify/add + * @param newChild new child pointer + * @return pointer to the updated node; same as node if update was in-place + */ + private int attachChild(int node, int trans, int newChild) throws TrieSpaceExhaustedException { assert !isLeaf(node) : "attachChild cannot be used on content nodes."; @@ -221,16 +454,7 @@ private int attachChild(int node, int trans, int newChild) throws SpaceExhausted case SPARSE_OFFSET: return attachChildToSparse(node, trans, newChild); case SPLIT_OFFSET: - attachChildToSplit(node, trans, newChild); - return node; - case LAST_POINTER_OFFSET - 1: - // If this is the last character in a Chain block, we can modify the child in-place - if (trans == getUnsignedByte(node)) - { - putIntVolatile(node + 1, newChild); - return node; - } - // else pass through + return attachChildToSplit(node, trans, newChild); default: return attachChildToChain(node, trans, newChild); } @@ -239,48 +463,95 @@ private int attachChild(int node, int trans, int newChild) throws SpaceExhausted /** * Attach a child to the given split node. This may be an update for an existing branch, or a new child for the node. */ - private void attachChildToSplit(int node, int trans, int newChild) throws SpaceExhaustedException + private int attachChildToSplit(int node, int trans, int newChild) throws TrieSpaceExhaustedException { - int midPos = splitBlockPointerAddress(node, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); - int mid = getInt(midPos); + int midPos = splitCellPointerAddress(node, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); + int mid = getIntVolatile(midPos); if (isNull(mid)) { mid = createEmptySplitNode(); - int tailPos = splitBlockPointerAddress(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int tailPos = splitCellPointerAddress(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); int tail = createEmptySplitNode(); - int childPos = splitBlockPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int childPos = splitCellPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); putInt(childPos, newChild); putInt(tailPos, tail); putIntVolatile(midPos, mid); - return; + return node; } - int tailPos = splitBlockPointerAddress(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); - int tail = getInt(tailPos); + int tailPos = splitCellPointerAddress(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int tail = getIntVolatile(tailPos); if (isNull(tail)) { tail = createEmptySplitNode(); - int childPos = splitBlockPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int childPos = splitCellPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); putInt(childPos, newChild); putIntVolatile(tailPos, tail); - return; + return node; } - int childPos = splitBlockPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int childPos = splitCellPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); putIntVolatile(childPos, newChild); + return node; + } + + /** + * Non-volatile version of attachChildToSplit. Used when the split node is not reachable yet (during the conversion + * from sparse). + */ + private int attachChildToSplitNonVolatile(int node, int trans, int newChild) throws TrieSpaceExhaustedException + { + assert offset(node) == SPLIT_OFFSET : "Invalid split node in trie"; + int midPos = splitCellPointerAddress(node, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); + int mid = getOrAllocate(midPos, SPLIT_OFFSET); + assert offset(mid) == SPLIT_OFFSET : "Invalid split node in trie"; + int tailPos = splitCellPointerAddress(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int tail = getOrAllocate(tailPos, SPLIT_OFFSET); + assert offset(tail) == SPLIT_OFFSET : "Invalid split node in trie"; + int childPos = splitCellPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + putInt(childPos, newChild); + return node; + } + + /** + * Attach a child to the given split node, copying all modified content to enable atomic visibility + * of modification. + * This may be an update for an existing branch, or a new child for the node. + */ + private int attachChildToSplitCopying(int node, int originalNode, int trans, int newChild) throws TrieSpaceExhaustedException + { + if (offset(originalNode) != SPLIT_OFFSET) // includes originalNode == NONE + return attachChildToSplitNonVolatile(node, trans, newChild); + + node = copyIfOriginal(node, originalNode); + assert offset(node) == SPLIT_OFFSET : "Invalid split node in trie"; + + int midPos = splitCellPointerAddress(0, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); + int midOriginal = originalNode != NONE ? getIntVolatile(midPos + originalNode) : NONE; + int mid = getCopyOrAllocate(node + midPos, midOriginal, SPLIT_OFFSET); + assert offset(mid) == SPLIT_OFFSET : "Invalid split node in trie"; + + int tailPos = splitCellPointerAddress(0, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + int tailOriginal = midOriginal != NONE ? getIntVolatile(tailPos + midOriginal) : NONE; + int tail = getCopyOrAllocate(mid + tailPos, tailOriginal, SPLIT_OFFSET); + assert offset(tail) == SPLIT_OFFSET : "Invalid split node in trie"; + + int childPos = splitCellPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); + putInt(childPos, newChild); + return node; } /** * Attach a child to the given sparse node. This may be an update for an existing branch, or a new child for the node. */ - private int attachChildToSparse(int node, int trans, int newChild) throws SpaceExhaustedException + private int attachChildToSparse(int node, int trans, int newChild) throws TrieSpaceExhaustedException { int index; int smallerCount = 0; // first check if this is an update and modify in-place if so for (index = 0; index < SPARSE_CHILD_COUNT; ++index) { - if (isNull(getInt(node + SPARSE_CHILDREN_OFFSET + index * 4))) + if (isNull(getIntVolatile(node + SPARSE_CHILDREN_OFFSET + index * 4))) break; final int existing = getUnsignedByte(node + SPARSE_BYTES_OFFSET + index); if (existing == trans) @@ -296,22 +567,14 @@ else if (existing < trans) if (childCount == SPARSE_CHILD_COUNT) { // Node is full. Switch to split - int split = createEmptySplitNode(); - for (int i = 0; i < SPARSE_CHILD_COUNT; ++i) - { - int t = getUnsignedByte(node + SPARSE_BYTES_OFFSET + i); - int p = getInt(node + SPARSE_CHILDREN_OFFSET + i * 4); - attachChildToSplitNonVolatile(split, t, p); - } - attachChildToSplitNonVolatile(split, trans, newChild); - return split; + return upgradeSparseToSplit(node, trans, newChild); } // Add a new transition. They are not kept in order, so append it at the first free position. putByte(node + SPARSE_BYTES_OFFSET + childCount, (byte) trans); // Update order word. - int order = getUnsignedShort(node + SPARSE_ORDER_OFFSET); + int order = getUnsignedShortVolatile(node + SPARSE_ORDER_OFFSET); int newOrder = insertInOrderWord(order, childCount, smallerCount); // Sparse nodes have two access modes: via the order word, when listing transitions, or directly to characters @@ -331,9 +594,71 @@ else if (existing < trans) return node; } + /** + * Attach a child to the given sparse node. This may be an update for an existing branch, or a new child for the node. + * Resulting node is not reachable, no volatile set needed. + */ + private int attachChildToSparseCopying(int node, int originalNode, int trans, int newChild) throws TrieSpaceExhaustedException + { + int index; + int smallerCount = 0; + // first check if this is an update and modify in-place if so + for (index = 0; index < SPARSE_CHILD_COUNT; ++index) + { + if (isNull(getIntVolatile(node + SPARSE_CHILDREN_OFFSET + index * 4))) + break; + final int existing = getUnsignedByte(node + SPARSE_BYTES_OFFSET + index); + if (existing == trans) + { + node = copyIfOriginal(node, originalNode); + putInt(node + SPARSE_CHILDREN_OFFSET + index * 4, newChild); + return node; + } + else if (existing < trans) + ++smallerCount; + } + int childCount = index; + + if (childCount == SPARSE_CHILD_COUNT) + { + // Node is full. Switch to split. + // Note that even if node != originalNode, we still have to recycle it as it was a temporary one that will + // no longer be attached. + return upgradeSparseToSplit(node, trans, newChild); + } + + node = copyIfOriginal(node, originalNode); + + // Add a new transition. They are not kept in order, so append it at the first free position. + putByte(node + SPARSE_BYTES_OFFSET + childCount, (byte) trans); + + putInt(node + SPARSE_CHILDREN_OFFSET + childCount * 4, newChild); + + // Update order word. + int order = getUnsignedShortVolatile(node + SPARSE_ORDER_OFFSET); + int newOrder = insertInOrderWord(order, childCount, smallerCount); + putShort(node + SPARSE_ORDER_OFFSET, (short) newOrder); + + return node; + } + + private int upgradeSparseToSplit(int node, int trans, int newChild) throws TrieSpaceExhaustedException + { + int split = createEmptySplitNode(); + for (int i = 0; i < SPARSE_CHILD_COUNT; ++i) + { + int t = getUnsignedByte(node + SPARSE_BYTES_OFFSET + i); + int p = getIntVolatile(node + SPARSE_CHILDREN_OFFSET + i * 4); + attachChildToSplitNonVolatile(split, t, p); + } + attachChildToSplitNonVolatile(split, trans, newChild); + recycleCell(node); + return split; + } + /** * Insert the given newIndex in the base-6 encoded order word in the correct position with respect to the ordering. - * + *

    * E.g. * - insertOrderWord(120, 3, 0) must return 1203 (decimal 48*6 + 3) * - insertOrderWord(120, 3, 1, ptr) must return 1230 (decimal 8*36 + 3*6 + 0) @@ -352,61 +677,85 @@ private static int insertInOrderWord(int order, int newIndex, int smallerCount) } /** - * Non-volatile version of attachChildToSplit. Used when the split node is not reachable yet (during the conversion - * from sparse). + * Attach a child to the given chain node. This may be an update for an existing branch with different target + * address, or a second child for the node. + * This method always copies the node -- with the exception of updates that change the child of the last node in a + * chain cell with matching transition byte (which this method is not used for, see attachChild), modifications to + * chain nodes cannot be done in place, either because we introduce a new transition byte and have to convert from + * the single-transition chain type to sparse, or because we have to remap the child from the implicit node + 1 to + * something else. */ - private void attachChildToSplitNonVolatile(int node, int trans, int newChild) throws SpaceExhaustedException + private int attachChildToChain(int node, int transitionByte, int newChild) throws TrieSpaceExhaustedException { - assert offset(node) == SPLIT_OFFSET : "Invalid split node in trie"; - int midPos = splitBlockPointerAddress(node, splitNodeMidIndex(trans), SPLIT_START_LEVEL_LIMIT); - int mid = getInt(midPos); - if (isNull(mid)) - { - mid = createEmptySplitNode(); - putInt(midPos, mid); - } - - assert offset(mid) == SPLIT_OFFSET : "Invalid split node in trie"; - int tailPos = splitBlockPointerAddress(mid, splitNodeTailIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); - int tail = getInt(tailPos); - if (isNull(tail)) + int existingByte = getUnsignedByte(node); + if (transitionByte == existingByte) { - tail = createEmptySplitNode(); - putInt(tailPos, tail); + // This is still a single path. Update child if possible (only if this is the last character in the chain). + if (offset(node) == LAST_POINTER_OFFSET - 1) + { + putIntVolatile(node + 1, newChild); + return node; + } + else + { + // This will only be called if new child is different from old, and the update is not on the final child + // where we can change it in place (see attachChild). We must always create something new. + // Note that since this is not the last character, we either still need this cell or we have already + // released it (a createSparseNode must have been called earlier). + // If the child is a chain, we can expand it (since it's a different value, its branch must be new and + // nothing can already reside in the rest of the cell). + return expandOrCreateChainNode(transitionByte, newChild); + } } - assert offset(tail) == SPLIT_OFFSET : "Invalid split node in trie"; - int childPos = splitBlockPointerAddress(tail, splitNodeChildIndex(trans), SPLIT_OTHER_LEVEL_LIMIT); - putInt(childPos, newChild); + // The new transition is different, so we no longer have only one transition. Change type. + return convertChainToSparse(node, existingByte, newChild, transitionByte); } /** - * Attach a child to the given chain node. This may be an update for an existing branch with different target - * address, or a second child for the node. - * This method always copies the node -- with the exception of updates that change the child of the last node in a - * chain block with matching transition byte (which this method is not used for, see attachChild), modifications to - * chain nodes cannot be done in place, either because we introduce a new transition byte and have to convert from - * the single-transition chain type to sparse, or because we have to remap the child from the implicit node + 1 to - * something else. + * Attach a child to the given chain node, when we are force-copying. */ - private int attachChildToChain(int node, int transitionByte, int newChild) throws SpaceExhaustedException + private int attachChildToChainCopying(int node, int originalNode, int transitionByte, int newChild) + throws TrieSpaceExhaustedException { int existingByte = getUnsignedByte(node); if (transitionByte == existingByte) { - // This will only be called if new child is different from old, and the update is not on the final child - // where we can change it in place (see attachChild). We must always create something new. - // If the child is a chain, we can expand it (since it's a different value, its branch must be new and - // nothing can already reside in the rest of the block). + // This is still a single path. + // Make sure we release the cell if it will no longer be referenced (if we update last reference, the whole + // path has to move as the other nodes in this chain can't be remapped). + if (offset(node) == LAST_POINTER_OFFSET - 1) + { + assert node == originalNode; // if we have already created a node, the character can't match what + // it was created with + + recycleCell(node); + } + return expandOrCreateChainNode(transitionByte, newChild); } + else + { + // The new transition is different, so we no longer have only one transition. Change type. + return convertChainToSparse(node, existingByte, newChild, transitionByte); + } + } - // The new transition is different, so we no longer have only one transition. Change type. + private int convertChainToSparse(int node, int existingByte, int newChild, int transitionByte) + throws TrieSpaceExhaustedException + { int existingChild = node + 1; if (offset(existingChild) == LAST_POINTER_OFFSET) { - existingChild = getInt(existingChild); + existingChild = getIntVolatile(existingChild); + // This was a chain with just one transition which will no longer be referenced. + // The cell may contain other characters/nodes leading to this, which are also guaranteed to be + // unreferenced. + // However, these leading nodes may still be in the parent path and will be needed until the + // mutation completes. + recycleCell(node); } + // Otherwise the sparse node we will now create references this cell, so it can't be recycled. return createSparseNode(existingByte, existingChild, transitionByte, newChild); } @@ -419,7 +768,7 @@ private boolean isExpandableChain(int newChild) /** * Create a sparse node with two children. */ - private int createSparseNode(int byte1, int child1, int byte2, int child2) throws SpaceExhaustedException + private int createSparseNode(int byte1, int child1, int byte2, int child2) throws TrieSpaceExhaustedException { assert byte1 != byte2 : "Attempted to create a sparse node with two of the same transition"; if (byte1 > byte2) @@ -430,7 +779,7 @@ private int createSparseNode(int byte1, int child1, int byte2, int child2) throw t = child1; child1 = child2; child2 = t; } - int node = allocateBlock() + SPARSE_OFFSET; + int node = allocateCell() + SPARSE_OFFSET; putByte(node + SPARSE_BYTES_OFFSET + 0, (byte) byte1); putByte(node + SPARSE_BYTES_OFFSET + 1, (byte) byte2); putInt(node + SPARSE_CHILDREN_OFFSET + 0 * 4, child1); @@ -446,9 +795,9 @@ private int createSparseNode(int byte1, int child1, int byte2, int child2) throw * Note that to avoid creating inefficient tries with under-utilized chain nodes, this should only be called from * {@link #expandOrCreateChainNode} and other call-sites should call {@link #expandOrCreateChainNode}. */ - private int createNewChainNode(int transitionByte, int newChild) throws SpaceExhaustedException + private int createNewChainNode(int transitionByte, int newChild) throws TrieSpaceExhaustedException { - int newNode = allocateBlock() + LAST_POINTER_OFFSET - 1; + int newNode = allocateCell() + LAST_POINTER_OFFSET - 1; putByte(newNode, (byte) transitionByte); putInt(newNode + 1, newChild); // Note: this does not need a volatile write as it is a new node, returning a new pointer, which needs to be @@ -458,7 +807,7 @@ private int createNewChainNode(int transitionByte, int newChild) throws SpaceExh /** Like {@link #createNewChainNode}, but if the new child is already a chain node and has room, expand * it instead of creating a brand new node. */ - private int expandOrCreateChainNode(int transitionByte, int newChild) throws SpaceExhaustedException + private int expandOrCreateChainNode(int transitionByte, int newChild) throws TrieSpaceExhaustedException { if (isExpandableChain(newChild)) { @@ -471,12 +820,12 @@ private int expandOrCreateChainNode(int transitionByte, int newChild) throws Spa return createNewChainNode(transitionByte, newChild); } - private int createEmptySplitNode() throws SpaceExhaustedException + private int createEmptySplitNode() throws TrieSpaceExhaustedException { - return allocateBlock() + SPLIT_OFFSET; + return allocateCell() + SPLIT_OFFSET; } - private int createPrefixNode(int contentIndex, int child, boolean isSafeChain) throws SpaceExhaustedException + private int createPrefixNode(int contentId, int child, boolean isSafeChain) throws TrieSpaceExhaustedException { assert !isNullOrLeaf(child) : "Prefix node cannot reference a childless node."; @@ -488,22 +837,22 @@ private int createPrefixNode(int contentIndex, int child, boolean isSafeChain) t // Note: for chain nodes we have a risk that the node continues beyond the current point, in which case // creating the embedded node may overwrite information that is still needed by concurrent readers or the // mutation process itself. - node = (child & -BLOCK_SIZE) | PREFIX_OFFSET; + node = (child & -CELL_SIZE) | PREFIX_OFFSET; putByte(node + PREFIX_FLAGS_OFFSET, (byte) offset); } else { // Full prefix node - node = allocateBlock() + PREFIX_OFFSET; + node = allocateCell() + PREFIX_OFFSET; putByte(node + PREFIX_FLAGS_OFFSET, (byte) 0xFF); putInt(node + PREFIX_POINTER_OFFSET, child); } - putInt(node + PREFIX_CONTENT_OFFSET, contentIndex); + putInt(node + PREFIX_CONTENT_OFFSET, contentId); return node; } - private int updatePrefixNodeChild(int node, int child) throws SpaceExhaustedException + private int updatePrefixNodeChild(int node, int child, boolean forcedCopy) throws TrieSpaceExhaustedException { assert offset(node) == PREFIX_OFFSET : "updatePrefix called on non-prefix node"; assert !isNullOrLeaf(child) : "Prefix node cannot reference a childless node."; @@ -511,20 +860,30 @@ private int updatePrefixNodeChild(int node, int child) throws SpaceExhaustedExce // We can only update in-place if we have a full prefix node if (!isEmbeddedPrefixNode(node)) { - // This attaches the child branch and makes it reachable -- the write must be volatile. - putIntVolatile(node + PREFIX_POINTER_OFFSET, child); - return node; + if (!forcedCopy) + { + // This attaches the child branch and makes it reachable -- the write must be volatile. + putIntVolatile(node + PREFIX_POINTER_OFFSET, child); + return node; + } + else + { + node = copyCell(node); + putInt(node + PREFIX_POINTER_OFFSET, child); + return node; + } } else { - int contentIndex = getInt(node + PREFIX_CONTENT_OFFSET); - return createPrefixNode(contentIndex, child, true); + // No need to recycle this cell because that is already done by the modification of the child + int contentId = getIntVolatile(node + PREFIX_CONTENT_OFFSET); + return createPrefixNode(contentId, child, true); } } private boolean isEmbeddedPrefixNode(int node) { - return getUnsignedByte(node + PREFIX_FLAGS_OFFSET) < BLOCK_SIZE; + return getUnsignedByte(node + PREFIX_FLAGS_OFFSET) < CELL_SIZE; } /** @@ -539,49 +898,50 @@ private boolean isEmbeddedPrefixNode(int node) * applied; if the modifications were applied in-place, this will be the same as * existingPostContentNode, otherwise a completely different pointer; always a non- * content node + * @param forcedCopy whether or not we need to preserve all pre-existing data for concurrent readers * @return a node which has the children of updatedPostContentNode combined with the content of * existingPreContentNode */ private int preserveContent(int existingPreContentNode, int existingPostContentNode, - int updatedPostContentNode) throws SpaceExhaustedException + int updatedPostContentNode, + boolean forcedCopy) + throws TrieSpaceExhaustedException { if (existingPreContentNode == existingPostContentNode) return updatedPostContentNode; // no content to preserve if (existingPostContentNode == updatedPostContentNode) + { + assert !forcedCopy; return existingPreContentNode; // child didn't change, no update necessary + } // else we have existing prefix node, and we need to reference a new child if (isLeaf(existingPreContentNode)) { - return createPrefixNode(~existingPreContentNode, updatedPostContentNode, true); + return createPrefixNode(existingPreContentNode, updatedPostContentNode, true); } assert offset(existingPreContentNode) == PREFIX_OFFSET : "Unexpected content in non-prefix and non-leaf node."; - return updatePrefixNodeChild(existingPreContentNode, updatedPostContentNode); + return updatePrefixNodeChild(existingPreContentNode, updatedPostContentNode, forcedCopy); } - final ApplyState applyState = new ApplyState(); + private final ApplyState applyState = new ApplyState(); /** * Represents the state for an {@link #apply} operation. Contains a stack of all nodes we descended through * and used to update the nodes with any new data during ascent. - * + *

    * To make this as efficient and GC-friendly as possible, we use an integer array (instead of is an object stack) * and we reuse the same object. The latter is safe because memtable tries cannot be mutated in parallel by multiple * writers. */ - class ApplyState + private class ApplyState implements KeyProducer { int[] data = new int[16 * 5]; int currentDepth = -1; - void reset() - { - currentDepth = -1; - } - /** * Pointer to the existing node before skipping over content nodes, i.e. this is either the same as * existingPostContentNode or a pointer to a prefix or leaf node whose child is existingPostContentNode. @@ -636,96 +996,142 @@ void setTransition(int transition) { data[currentDepth * 5 + 3] = transition; } + int transitionAtDepth(int stackDepth) + { + return data[stackDepth * 5 + 3]; + } /** - * The compiled content index. Needed because we can only access a cursor's content on the way down but we can't + * The compiled content id. Needed because we can only access a cursor's content on the way down but we can't * attach it until we ascend from the node. */ - int contentIndex() + int contentId() { return data[currentDepth * 5 + 4]; } - void setContentIndex(int value) + void setContentId(int value) { data[currentDepth * 5 + 4] = value; } + int contentIdAtDepth(int stackDepth) + { + return data[stackDepth * 5 + 4]; + } + + ApplyState start() + { + int existingFullNode = root; + currentDepth = 0; + + descendInto(existingFullNode); + return this; + } /** - * Descend to a child node. Prepares a new entry in the stack for the node. + * Returns true if the depth signals mutation cursor is exhausted. */ - void descend(int transition, U mutationContent, final UpsertTransformer transformer) + boolean advanceTo(int depth, int transition, int forcedCopyDepth) throws TrieSpaceExhaustedException { - int existingPreContentNode; - if (currentDepth < 0) - existingPreContentNode = root; - else + while (currentDepth > Math.max(0, depth - 1)) { - setTransition(transition); - existingPreContentNode = isNull(existingPostContentNode()) - ? NONE - : getChild(existingPostContentNode(), transition); + // There are no more children. Ascend to the parent state to continue walk. + attachAndMoveToParentState(forcedCopyDepth); } + if (depth == -1) + return true; + // We have a transition, get child to descend into + descend(transition); + return false; + } + + /** + * Descend to a child node. Prepares a new entry in the stack for the node. + */ + void descend(int transition) + { + setTransition(transition); + int existingPreContentNode = getChild(existingPreContentNode(), transition); ++currentDepth; + descendInto(existingPreContentNode); + } + + private void descendInto(int existingPreContentNode) + { if (currentDepth * 5 >= data.length) data = Arrays.copyOf(data, currentDepth * 5 * 2); setExistingPreContentNode(existingPreContentNode); - int existingContentIndex = -1; + int existingContentId = NONE; int existingPostContentNode; if (isLeaf(existingPreContentNode)) { - existingContentIndex = ~existingPreContentNode; + existingContentId = existingPreContentNode; existingPostContentNode = NONE; } else if (offset(existingPreContentNode) == PREFIX_OFFSET) { - existingContentIndex = getInt(existingPreContentNode + PREFIX_CONTENT_OFFSET); + existingContentId = getIntVolatile(existingPreContentNode + PREFIX_CONTENT_OFFSET); existingPostContentNode = followContentTransition(existingPreContentNode); } else existingPostContentNode = existingPreContentNode; setExistingPostContentNode(existingPostContentNode); setUpdatedPostContentNode(existingPostContentNode); + setContentId(existingContentId); + } - int contentIndex = updateContentIndex(mutationContent, existingContentIndex, transformer); - setContentIndex(contentIndex); + T getContent() + { + int contentId = contentId(); + if (contentId == NONE) + return null; + return InMemoryTrie.this.getContent(contentId()); } - /** - * Combine existing and new content. - */ - private int updateContentIndex(U mutationContent, int existingContentIndex, final UpsertTransformer transformer) + void setContent(T content, boolean forcedCopy) throws TrieSpaceExhaustedException { - if (mutationContent != null) + int contentId = contentId(); + if (contentId == NONE) { - if (existingContentIndex != -1) - { - final T existingContent = getContent(existingContentIndex); - T combinedContent = transformer.apply(existingContent, mutationContent); - assert (combinedContent != null) : "Transformer cannot be used to remove content."; - setContent(existingContentIndex, combinedContent); - return existingContentIndex; - } - else - { - T combinedContent = transformer.apply(null, mutationContent); - assert (combinedContent != null) : "Transformer cannot be used to remove content."; - return addContent(combinedContent); - } + if (content != null) + setContentId(InMemoryTrie.this.addContent(content)); + } + else if (content == null) + { + releaseContent(contentId); + setContentId(NONE); + // At this point we are not deleting branches on the way up, just making sure we don't hold on to + // references to content. + } + else if (content == InMemoryTrie.this.getContent(contentId)) + { + // no changes, nothing to do + } + else if (forcedCopy) + { + releaseContent(contentId); + setContentId(InMemoryTrie.this.addContent(content)); } else - return existingContentIndex; + { + InMemoryTrie.this.setContent(contentId, content); + } } /** * Attach a child to the current node. */ - private void attachChild(int transition, int child) throws SpaceExhaustedException + private void attachChild(int transition, int child, boolean forcedCopy) throws TrieSpaceExhaustedException { int updatedPostContentNode = updatedPostContentNode(); if (isNull(updatedPostContentNode)) setUpdatedPostContentNode(expandOrCreateChainNode(transition, child)); + else if (forcedCopy) + setUpdatedPostContentNode(attachChildCopying(updatedPostContentNode, + existingPostContentNode(), + transition, + child)); else setUpdatedPostContentNode(InMemoryTrie.this.attachChild(updatedPostContentNode, transition, @@ -736,70 +1142,215 @@ private void attachChild(int transition, int child) throws SpaceExhaustedExcepti * Apply the collected content to a node. Converts NONE to a leaf node, and adds or updates a prefix for all * others. */ - private int applyContent() throws SpaceExhaustedException + private int applyContent(boolean forcedCopy) throws TrieSpaceExhaustedException { - int contentIndex = contentIndex(); - int updatedPostContentNode = updatedPostContentNode(); - if (contentIndex == -1) - return updatedPostContentNode; - + // Note: the old content id itself is already released by setContent. Here we must release any standalone + // prefix nodes that may reference it. + int contentId = contentId(); + final int updatedPostContentNode = updatedPostContentNode(); + final int existingPreContentNode = existingPreContentNode(); + final int existingPostContentNode = existingPostContentNode(); + + // applyPrefixChange does not understand leaf nodes, handle upgrade from and to one explicitly. if (isNull(updatedPostContentNode)) - return ~contentIndex; + { + if (existingPreContentNode != existingPostContentNode + && !isNullOrLeaf(existingPreContentNode) + && !isEmbeddedPrefixNode(existingPreContentNode)) + recycleCell(existingPreContentNode); + return contentId; // also fine for contentId == NONE + } - int existingPreContentNode = existingPreContentNode(); - int existingPostContentNode = existingPostContentNode(); + if (isLeaf(existingPreContentNode)) + return contentId != NONE + ? createPrefixNode(contentId, updatedPostContentNode, true) + : updatedPostContentNode; + + return applyPrefixChange(updatedPostContentNode, + existingPreContentNode, + existingPostContentNode, + contentId, + forcedCopy); + } + + private int applyPrefixChange(int updatedPostPrefixNode, + int existingPrePrefixNode, + int existingPostPrefixNode, + int prefixData, + boolean forcedCopy) + throws TrieSpaceExhaustedException + { + boolean prefixWasPresent = existingPrePrefixNode != existingPostPrefixNode; + boolean prefixWasEmbedded = prefixWasPresent && isEmbeddedPrefixNode(existingPrePrefixNode); + if (prefixData == NONE) + { + if (prefixWasPresent && !prefixWasEmbedded) + recycleCell(existingPrePrefixNode); + return updatedPostPrefixNode; + } - // We can't update in-place if there was no preexisting prefix, or if the prefix was embedded and the target - // node must change. - if (existingPreContentNode == existingPostContentNode || - isNull(existingPostContentNode) || - isEmbeddedPrefixNode(existingPreContentNode) && updatedPostContentNode != existingPostContentNode) - return createPrefixNode(contentIndex, updatedPostContentNode, isNull(existingPostContentNode)); + boolean childChanged = updatedPostPrefixNode != existingPostPrefixNode; + boolean dataChanged = !prefixWasPresent || prefixData != getIntVolatile(existingPrePrefixNode + PREFIX_CONTENT_OFFSET); + if (!childChanged && !dataChanged) + return existingPrePrefixNode; + + if (forcedCopy) + { + if (!childChanged && prefixWasEmbedded) + { + // If we directly create in this case, we will find embedding is possible and will overwrite the + // previous value. + // We could create a separate metadata node referencing the child, but in that case we'll + // use two nodes while one suffices. Instead, copy the child and embed the new metadata. + updatedPostPrefixNode = copyCell(existingPostPrefixNode); + } + else if (prefixWasPresent && !prefixWasEmbedded) + { + recycleCell(existingPrePrefixNode); + // otherwise cell is already recycled by the recycling of the child + } + return createPrefixNode(prefixData, updatedPostPrefixNode, isNull(existingPostPrefixNode)); + } + + // We can't update in-place if there was no preexisting prefix, or if the + // prefix was embedded and the target node must change. + if (!prefixWasPresent || prefixWasEmbedded && childChanged) + return createPrefixNode(prefixData, updatedPostPrefixNode, isNull(existingPostPrefixNode)); // Otherwise modify in place - if (updatedPostContentNode != existingPostContentNode) // to use volatile write but also ensure we don't corrupt embedded nodes - putIntVolatile(existingPreContentNode + PREFIX_POINTER_OFFSET, updatedPostContentNode); - assert contentIndex == getInt(existingPreContentNode + PREFIX_CONTENT_OFFSET) : "Unexpected change of content index."; - return existingPreContentNode; + if (childChanged) // to use volatile write but also ensure we don't corrupt embedded nodes + putIntVolatile(existingPrePrefixNode + PREFIX_POINTER_OFFSET, updatedPostPrefixNode); + if (dataChanged) + putIntVolatile(existingPrePrefixNode + PREFIX_CONTENT_OFFSET, prefixData); + return existingPrePrefixNode; } /** * After a node's children are processed, this is called to ascend from it. This means applying the collected * content to the compiled updatedPostContentNode and creating a mapping in the parent to it (or updating if * one already exists). - * Returns true if still have work to do, false if the operation is completed. */ - private boolean attachAndMoveToParentState() throws SpaceExhaustedException + void attachAndMoveToParentState(int forcedCopyDepth) throws TrieSpaceExhaustedException { - int updatedPreContentNode = applyContent(); - int existingPreContentNode = existingPreContentNode(); + int updatedFullNode = applyContent(currentDepth >= forcedCopyDepth); + int existingFullNode = existingPreContentNode(); --currentDepth; - if (currentDepth == -1) + + if (updatedFullNode != existingFullNode) + attachChild(transition(), updatedFullNode, currentDepth >= forcedCopyDepth); + } + + /** + * Ascend and update the root at the end of processing. + */ + void attachRoot(int forcedCopyDepth) throws TrieSpaceExhaustedException + { + int updatedPreContentNode = applyContent(0 >= forcedCopyDepth); + int existingPreContentNode = existingPreContentNode(); + assert root == existingPreContentNode : "Unexpected change to root. Concurrent trie modification?"; + if (updatedPreContentNode != existingPreContentNode) { - assert root == existingPreContentNode : "Unexpected change to root. Concurrent trie modification?"; - if (updatedPreContentNode != existingPreContentNode) - { - // Only write to root if they are different (value doesn't change, but - // we don't want to invalidate the value in other cores' caches unnecessarily). - root = updatedPreContentNode; - } - return false; + // Only write to root if they are different (value doesn't change, but + // we don't want to invalidate the value in other cores' caches unnecessarily). + root = updatedPreContentNode; } - if (updatedPreContentNode != existingPreContentNode) - attachChild(transition(), updatedPreContentNode); - return true; } + + public byte[] getBytes() + { + int arrSize = currentDepth; + byte[] data = new byte[arrSize]; + int pos = 0; + for (int i = 0; i < currentDepth; ++i) + { + int trans = transitionAtDepth(i); + data[pos++] = (byte) trans; + } + return data; + } + + public byte[] getBytes(Predicate shouldStop) + { + if (currentDepth == 0) + return new byte[0]; + + int arrSize = 1; + int i; + for (i = currentDepth - 1; i > 0; --i) + { + int content = contentIdAtDepth(i); + if (!isNull(content) && shouldStop.test(InMemoryTrie.this.getContent(content))) + break; + ++arrSize; + } + assert i > 0 || arrSize == currentDepth; // if the loop covers the whole stack, the array must cover the full depth + + byte[] data = new byte[arrSize]; + int pos = 0; + for (; i < currentDepth; ++i) + { + int trans = transitionAtDepth(i); + data[pos++] = (byte) trans; + } + return data; + } + + public ByteComparable.Version byteComparableVersion() + { + return byteComparableVersion; + } + } + + public interface KeyProducer + { + /** + * Get the bytes of the path leading to this node. + */ + byte[] getBytes(); + + /** + * Get the bytes of the path leading to this node from the closest ancestor whose content, after any new inserts + * have been applied, satisfies the given predicate. + * Note that the predicate is not called for the current position, because its content is not yet prepared when + * the method is being called. + */ + byte[] getBytes(Predicate shouldStop); + + ByteComparable.Version byteComparableVersion(); + } + + /** + * Somewhat similar to {@link Trie.MergeResolver}, this encapsulates logic to be applied whenever new content is + * being upserted into a {@link InMemoryTrie}. Unlike {@link Trie.MergeResolver}, {@link UpsertTransformer} will be + * applied no matter if there's pre-existing content for that trie key/path or not. + * + * @param The content type for this {@link InMemoryTrie}. + * @param The type of the new content being applied to this {@link InMemoryTrie}. + */ + public interface UpsertTransformerWithKeyProducer + { + /** + * Called when there's content in the updating trie. + * + * @param existing Existing content for this key, or null if there isn't any. + * @param update The update, always non-null. + * @param keyState An interface that can be used to retrieve the path of the value being updated. + * @return The combined value to use. Cannot be null. + */ + @Nonnull T apply(T existing, @Nonnull U update, @Nonnull KeyProducer keyState); } /** - * Somewhat similar to {@link MergeResolver}, this encapsulates logic to be applied whenever new content is being - * upserted into a {@link InMemoryTrie}. Unlike {@link MergeResolver}, {@link UpsertTransformer} will be applied no - * matter if there's pre-existing content for that trie key/path or not. + * Somewhat similar to {@link Trie.MergeResolver}, this encapsulates logic to be applied whenever new content is + * being upserted into a {@link InMemoryTrie}. Unlike {@link Trie.MergeResolver}, {@link UpsertTransformer} will be + * applied no matter if there's pre-existing content for that trie key/path or not. + *

    + * A version of the above that does not use a {@link KeyProducer}. * * @param The content type for this {@link InMemoryTrie}. * @param The type of the new content being applied to this {@link InMemoryTrie}. */ - public interface UpsertTransformer + public interface UpsertTransformer extends UpsertTransformerWithKeyProducer { /** * Called when there's content in the updating trie. @@ -808,7 +1359,125 @@ public interface UpsertTransformer * @param update The update, always non-null. * @return The combined value to use. Cannot be null. */ - T apply(T existing, U update); + @Nonnull T apply(T existing, @Nonnull U update); + + /** + * Version of the above that also provides the path of a value being updated. + * + * @param existing Existing content for this key, or null if there isn't any. + * @param update The update, always non-null. + * @param keyState An interface that can be used to retrieve the path of the value being updated. + * @return The combined value to use. Cannot be null. + */ + default @Nonnull T apply(T existing, @Nonnull U update, @Nonnull KeyProducer keyState) + { + return apply(existing, update); + } + } + + /** + * Interface providing features of the mutating node during mutation done using {@link #apply}. + * Effectively a subset of the {@link Trie.Cursor} interface which only permits operations that are safe to + * perform before iterating the children of the mutation node to apply the branch mutation. + * + * This is mainly used as an argument to predicates that decide when to copy substructure when modifying tries, + * which enables different kinds of atomicity and consistency guarantees. + * + * See the InMemoryTrie javadoc or InMemoryTrieThreadedTest for demonstration of the typical usages and what they + * achieve. + */ + public interface NodeFeatures + { + /** + * Whether or not the node has more than one descendant. If a checker needs mutations to be atomic, they can + * return true when this becomes true. + */ + boolean isBranching(); + + /** + * The metadata associated with the node. If readers need to see a consistent view (i.e. where older updates + * cannot be missed if a new one is presented) below some specified point (e.g. within a partition), the checker + * should return true when it identifies that point. + */ + T content(); + } + + private static class Mutation implements NodeFeatures + { + final UpsertTransformerWithKeyProducer transformer; + final Predicate> needsForcedCopy; + final Cursor mutationCursor; + final InMemoryTrie.ApplyState state; + int forcedCopyDepth; + + Mutation(UpsertTransformerWithKeyProducer transformer, + Predicate> needsForcedCopy, + Cursor mutationCursor, + InMemoryTrie.ApplyState state) + { + assert mutationCursor.depth() == 0 : "Unexpected non-fresh cursor."; + assert state.currentDepth == 0 : "Unexpected change to applyState. Concurrent trie modification?"; + this.transformer = transformer; + this.needsForcedCopy = needsForcedCopy; + this.mutationCursor = mutationCursor; + this.state = state; + } + + void apply() throws TrieSpaceExhaustedException + { + int depth = state.currentDepth; + while (true) + { + if (depth <= forcedCopyDepth) + forcedCopyDepth = needsForcedCopy.test(this) ? depth : Integer.MAX_VALUE; + + applyContent(); + + depth = mutationCursor.advance(); + if (state.advanceTo(depth, mutationCursor.incomingTransition(), forcedCopyDepth)) + break; + assert state.currentDepth == depth : "Unexpected change to applyState. Concurrent trie modification?"; + } + } + + void applyContent() throws TrieSpaceExhaustedException + { + U content = mutationCursor.content(); + if (content != null) + { + T existingContent = state.getContent(); + T combinedContent = transformer.apply(existingContent, content, state); + if (combinedContent == null) + throw new AssertionError("Transformer " + transformer + " returned null content for " + + existingContent + ", " + content); + state.setContent(combinedContent, + state.currentDepth >= forcedCopyDepth); // this is called at the start of processing + } + } + + + void complete() throws TrieSpaceExhaustedException + { + assert state.currentDepth == 0 : "Unexpected change to applyState. Concurrent trie modification?"; + state.attachRoot(forcedCopyDepth); + } + + @Override + public boolean isBranching() + { + // This is not very efficient, but we only currently use this option in tests. + // If it's needed for production use, isBranching should be implemented in the cursor interface. + Cursor dupe = mutationCursor.tailTrie().cursor(Direction.FORWARD); + int childDepth = dupe.advance(); + return childDepth > 0 && + dupe.skipTo(childDepth, dupe.incomingTransition() + 1) == childDepth; + } + + @Override + public U content() + { + return mutationCursor.content(); + } } /** @@ -818,35 +1487,49 @@ public interface UpsertTransformer * different than the element type for this memtable trie. * @param transformer a function applied to the potentially pre-existing value for the given key, and the new * value. Applied even if there's no pre-existing value in the memtable trie. + * @param needsForcedCopy a predicate which decides when to fully copy a branch to provide atomicity guarantees to + * concurrent readers. See NodeFeatures for details. */ - public void apply(Trie mutation, final UpsertTransformer transformer) throws SpaceExhaustedException + public void apply(Trie mutation, + final UpsertTransformerWithKeyProducer transformer, + final Predicate> needsForcedCopy) + throws TrieSpaceExhaustedException { - Cursor mutationCursor = mutation.cursor(); - assert mutationCursor.depth() == 0 : "Unexpected non-fresh cursor."; - ApplyState state = applyState; - state.reset(); - state.descend(-1, mutationCursor.content(), transformer); - assert state.currentDepth == 0 : "Unexpected change to applyState. Concurrent trie modification?"; - - while (true) + try { - int depth = mutationCursor.advance(); - while (state.currentDepth >= depth) - { - // There are no more children. Ascend to the parent state to continue walk. - if (!state.attachAndMoveToParentState()) - { - assert depth == -1 : "Unexpected change to applyState. Concurrent trie modification?"; - return; - } - } - - // We have a transition, get child to descend into - state.descend(mutationCursor.incomingTransition(), mutationCursor.content(), transformer); - assert state.currentDepth == depth : "Unexpected change to applyState. Concurrent trie modification?"; + Mutation m = new Mutation<>(transformer, + needsForcedCopy, + mutation.cursor(Direction.FORWARD), + applyState.start()); + m.apply(); + m.complete(); + completeMutation(); + } + catch (Throwable t) + { + abortMutation(); + throw t; } } + /** + * Modify this trie to apply the mutation given in the form of a trie. Any content in the mutation will be resolved + * with the given function before being placed in this trie (even if there's no pre-existing content in this trie). + * @param mutation the mutation to be applied, given in the form of a trie. Note that its content can be of type + * different than the element type for this memtable trie. + * @param transformer a function applied to the potentially pre-existing value for the given key, and the new + * value. Applied even if there's no pre-existing value in the memtable trie. + * @param needsForcedCopy a predicate which decides when to fully copy a branch to provide atomicity guarantees to + * concurrent readers. See NodeFeatures for details. + */ + public void apply(Trie mutation, + final UpsertTransformer transformer, + final Predicate> needsForcedCopy) + throws TrieSpaceExhaustedException + { + apply(mutation, (UpsertTransformerWithKeyProducer) transformer, needsForcedCopy); + } + /** * Map-like put method, using the apply machinery above which cannot run into stack overflow. When the correct * position in the trie has been reached, the value will be resolved with the given function before being placed in @@ -861,9 +1544,9 @@ public void apply(Trie mutation, final UpsertTransformer transforme */ public void putSingleton(ByteComparable key, R value, - UpsertTransformer transformer) throws SpaceExhaustedException + UpsertTransformer transformer) throws TrieSpaceExhaustedException { - apply(Trie.singleton(key, value), transformer); + apply(Trie.singleton(key, byteComparableVersion, value), transformer, Predicates.alwaysFalse()); } /** @@ -872,7 +1555,7 @@ public void putSingleton(ByteComparable key, public void putSingleton(ByteComparable key, R value, UpsertTransformer transformer, - boolean useRecursive) throws SpaceExhaustedException + boolean useRecursive) throws TrieSpaceExhaustedException { if (useRecursive) putRecursive(key, value, transformer); @@ -892,14 +1575,23 @@ public void putSingleton(ByteComparable key, * value (of a potentially different type), returning the final value that will stay in the memtable trie. Applied * even if there's no pre-existing value in the memtable trie. */ - public void putRecursive(ByteComparable key, R value, final UpsertTransformer transformer) throws SpaceExhaustedException + public void putRecursive(ByteComparable key, R value, final UpsertTransformer transformer) throws TrieSpaceExhaustedException { - int newRoot = putRecursive(root, key.asComparableBytes(BYTE_COMPARABLE_VERSION), value, transformer); - if (newRoot != root) - root = newRoot; + try + { + int newRoot = putRecursive(root, key.asComparableBytes(byteComparableVersion), value, transformer); + if (newRoot != root) + root = newRoot; + completeMutation(); + } + catch (Throwable t) + { + abortMutation(); + throw t; + } } - private int putRecursive(int node, ByteSource key, R value, final UpsertTransformer transformer) throws SpaceExhaustedException + private int putRecursive(int node, ByteSource key, R value, final UpsertTransformer transformer) throws TrieSpaceExhaustedException { int transition = key.next(); if (transition == ByteSource.END_OF_STREAM) @@ -916,35 +1608,47 @@ private int putRecursive(int node, ByteSource key, R value, final UpsertTran ? attachChild(skippedContent, transition, newChild) // Single path, no copying required : expandOrCreateChainNode(transition, newChild); - return preserveContent(node, skippedContent, attachedChild); + return preserveContent(node, skippedContent, attachedChild, false); } - private int applyContent(int node, R value, UpsertTransformer transformer) throws SpaceExhaustedException + private int applyContent(int node, R value, UpsertTransformer transformer) throws TrieSpaceExhaustedException { if (isNull(node)) - return ~addContent(transformer.apply(null, value)); + return addContent(transformer.apply(null, value)); if (isLeaf(node)) { - int contentIndex = ~node; - setContent(contentIndex, transformer.apply(getContent(contentIndex), value)); + int contentId = node; + setContent(contentId, transformer.apply(getContent(contentId), value)); return node; } if (offset(node) == PREFIX_OFFSET) { - int contentIndex = getInt(node + PREFIX_CONTENT_OFFSET); - setContent(contentIndex, transformer.apply(getContent(contentIndex), value)); + int contentId = getIntVolatile(node + PREFIX_CONTENT_OFFSET); + setContent(contentId, transformer.apply(getContent(contentId), value)); return node; } else return createPrefixNode(addContent(transformer.apply(null, value)), node, false); } + private void completeMutation() + { + cellAllocator.completeMutation(); + objectAllocator.completeMutation(); + } + + private void abortMutation() + { + cellAllocator.abortMutation(); + objectAllocator.abortMutation(); + } + /** * Returns true if the allocation threshold has been reached. To be called by the the writing thread (ideally, just * after the write completes). When this returns true, the user should switch to a new trie as soon as feasible. - * + *

    * The trie expects up to 10% growth above this threshold. Any growth beyond that may be done inefficiently, and * the trie will fail altogether when the size grows beyond 2G - 256 bytes. */ @@ -958,72 +1662,108 @@ public boolean reachedAllocatedSizeThreshold() * full. */ @VisibleForTesting - int advanceAllocatedPos(int wantedPos) throws SpaceExhaustedException + int advanceAllocatedPos(int wantedPos) throws TrieSpaceExhaustedException { while (allocatedPos < wantedPos) - allocateBlock(); + allocateCell(); return allocatedPos; } - /** Returns the off heap size of the memtable trie itself, not counting any space taken by referenced content. */ - public long sizeOffHeap() + /** + * For tests only! Returns the current allocation position. + */ + @VisibleForTesting + int getAllocatedPos() { - return bufferType == BufferType.ON_HEAP ? 0 : allocatedPos; + return allocatedPos; } - /** Returns the on heap size of the memtable trie itself, not counting any space taken by referenced content. */ - public long sizeOnHeap() + /** + * Returns the off heap size of the memtable trie itself, not counting any space taken by referenced content, or + * any space that has been allocated but is not currently in use (e.g. recycled cells or preallocated buffer). + * The latter means we are undercounting the actual usage, but the purpose of this reporting is to decide when + * to flush out e.g. a memtable and if we include the unused space we would almost always end up flushing out + * immediately after allocating a large buffer and not having a chance to use it. Counting only used space makes it + * possible to flush out before making these large allocations. + */ + public long usedSizeOffHeap() { - return contentCount * MemoryMeterStrategy.MEMORY_LAYOUT.getReferenceSize() + - REFERENCE_ARRAY_ON_HEAP_SIZE * getChunkIdx(contentCount, CONTENTS_START_SHIFT, CONTENTS_START_SIZE) + - (bufferType == BufferType.ON_HEAP ? allocatedPos + EMPTY_SIZE_ON_HEAP : EMPTY_SIZE_OFF_HEAP) + - REFERENCE_ARRAY_ON_HEAP_SIZE * getChunkIdx(allocatedPos, BUF_START_SHIFT, BUF_START_SIZE); + return bufferType == BufferType.ON_HEAP ? 0 : usedBufferSpace(); } - @Override - public Iterable valuesUnordered() + /** + * Returns the on heap size of the memtable trie itself, not counting any space taken by referenced content, or + * any space that has been allocated but is not currently in use (e.g. recycled cells or preallocated buffer). + * The latter means we are undercounting the actual usage, but the purpose of this reporting is to decide when + * to flush out e.g. a memtable and if we include the unused space we would almost always end up flushing out + * immediately after allocating a large buffer and not having a chance to use it. Counting only used space makes it + * possible to flush out before making these large allocations. + */ + public long usedSizeOnHeap() { - return () -> new Iterator() - { - int idx = 0; - - public boolean hasNext() - { - return idx < contentCount; - } - - public T next() - { - if (!hasNext()) - throw new NoSuchElementException(); + return usedObjectSpace() + + REFERENCE_ARRAY_ON_HEAP_SIZE * getBufferIdx(contentCount, CONTENTS_START_SHIFT, CONTENTS_START_SIZE) + + (bufferType == BufferType.ON_HEAP ? usedBufferSpace() + EMPTY_SIZE_ON_HEAP : EMPTY_SIZE_OFF_HEAP) + + REFERENCE_ARRAY_ON_HEAP_SIZE * getBufferIdx(allocatedPos, BUF_START_SHIFT, BUF_START_SIZE); + } - return getContent(idx++); - } - }; + private long usedBufferSpace() + { + return allocatedPos - cellAllocator.indexCountInPipeline() * CELL_SIZE; } - public int valuesCount() + private long usedObjectSpace() { - return contentCount; + return (contentCount - objectAllocator.indexCountInPipeline()) * MEMORY_LAYOUT.getReferenceSize(); } - public long unusedReservedMemory() + /** + * Returns the amount of memory that has been allocated for various buffers but isn't currently in use. + * The total on-heap space used by the trie is {@code usedSizeOnHeap() + unusedReservedOnHeapMemory()}. + */ + @VisibleForTesting + public long unusedReservedOnHeapMemory() { int bufferOverhead = 0; if (bufferType == BufferType.ON_HEAP) { int pos = this.allocatedPos; - UnsafeBuffer buffer = getChunk(pos); + UnsafeBuffer buffer = getBuffer(pos); if (buffer != null) - bufferOverhead = buffer.capacity() - inChunkPointer(pos); + bufferOverhead = buffer.capacity() - inBufferOffset(pos); + bufferOverhead += cellAllocator.indexCountInPipeline() * CELL_SIZE; } int index = contentCount; - int leadBit = getChunkIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); - int ofs = inChunkPointer(index, leadBit, CONTENTS_START_SIZE); + int leadBit = getBufferIdx(index, CONTENTS_START_SHIFT, CONTENTS_START_SIZE); + int ofs = inBufferOffset(index, leadBit, CONTENTS_START_SIZE); AtomicReferenceArray contentArray = contentArrays[leadBit]; - int contentOverhead = ((contentArray != null ? contentArray.length() : 0) - ofs) * MemoryMeterStrategy.MEMORY_LAYOUT.getReferenceSize(); + int contentOverhead = ((contentArray != null ? contentArray.length() : 0) - ofs); + contentOverhead += objectAllocator.indexCountInPipeline(); + contentOverhead *= MEMORY_LAYOUT.getReferenceSize(); return bufferOverhead + contentOverhead; } + + /** + * Release all recycled content references, including the ones waiting in still incomplete recycling lists. + * This is a test method and can cause null pointer exceptions if used on a live trie. + *

    + * If similar functionality is required for non-test purposes, a version of this should be developed that only + * releases references on barrier-complete lists. + */ + @VisibleForTesting + public void releaseReferencesUnsafe() + { + for (int idx : objectAllocator.indexesInPipeline()) + setContent(~idx, null); + } + + /** + * Returns the number of values in the trie + */ + public int valuesCount() + { + return contentCount; + } } diff --git a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md index 09c14087319b..1952d864e056 100644 --- a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md +++ b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md @@ -26,6 +26,8 @@ The main features of its implementation are: - using nodes of several different types for efficiency - support for content on any node, including intermediate (prefix) - support for writes from a single mutator thread concurrent with multiple readers +- various consistency and atomicity guarantees for readers +- memory management, off-heap or on-heap - maximum trie size of 2GB @@ -34,8 +36,7 @@ The main features of its implementation are: One of the main design drivers of the memtable trie is the desire to avoid on-heap storage and Java object management. The trie thus implements its own memory management for the structure of the trie (content is, at this time, still given as Java objects in a content array). The structure resides in one `UnsafeBuffer` (which can be on or off heap as -desired) and is broken up in 32-byte "cells" (also called "blocks" in the code), which are the unit of allocation, -update and reuse. +desired) and is broken up in 32-byte "cells", which are the unit of allocation, update and reuse. Like all tries, `InMemoryTrie` is built from nodes and has a root pointer. The nodes reside in cells, but there is no 1:1 correspondence between nodes and cells - some node types pack multiple in one cell, while other types require @@ -282,14 +283,14 @@ offset|content| 18 - 1B|pointer to child for ending 110| 1C - 1F|pointer to child for ending 111| -In any of the cell or pointer positions we can have `NONE`, meaning that such a child (or block of children) does not +In any of the cell or pointer positions we can have `NONE`, meaning that such a child (or cell of children) does not exist. At minimum, a split node occupies 3 cells (one leading, one mid and one end), and at maximum — `1 + 4 + 4*8 = 37` cells i.e. `1184` bytes. If we could allocate contiguous arrays, a full split node would use `1024` bytes, thus this splitting can add ~15% overhead. However, real data often has additional structure that this can make -use of to avoid creating some of the blocks, e.g. if the trie encodes US-ASCII or UTF-encoded strings where some +use of to avoid creating some of the cells, e.g. if the trie encodes US-ASCII or UTF-encoded strings where some character ranges are not allowed at all, and others are prevalent. Another benefit is that to change a transition while -preserving the previous state of the node for concurrent readers we have to only copy three blocks and not the entire -range of children (applications of this will be given later). +preserving the previous state of the node for concurrent readers we have to only copy three cells and not the entire +range of children (applications of this will be given in the [Mutation](#mutation) section). As an example, suppose we need to add a `0x51` `Q` transition to `0x455` to the 6-children sparse node from the previous section. This will generate the following structure: @@ -465,7 +466,7 @@ This substructure is a little more efficient than storing only one entry for the mid-to-tail links do not need to be followed for every new child) and also allows us to easily get the precise next child and remove the backtracking entry when a cell has no further children. -`InMemoryTrie` cursors also implement `advanceMultiple`, which jumps over intermediate nodes in `Chain` blocks: +`InMemoryTrie` cursors also implement `advanceMultiple`, which jumps over intermediate nodes in `Chain` cells: ![graph](InMemoryTrie.md.wc2.svg) @@ -540,7 +541,8 @@ Note that if we perform multiple mutations in sequence, and a reader happens to order), such reader may see only the mutation that is ahead of it _in iteration order_, which is not necessarily the mutation that happened first. For the example above, if we also inserted `trespass`, a reader thread that was paused at `0x018` in a forward traversal and wakes up after both insertions have completed will see `trespass`, but _will not_ -see `traverse` even though it was inserted earlier. +see `traverse` even though it was inserted earlier. This inconsistency is often undesirable; we will describe a +method of avoiding it in one of the next paragraphs. ### In-place modifications @@ -701,6 +703,10 @@ iteration processes a child, we apply the update to the node, which may happen i the original `existingNode`, it was pointing to an unreachable copied node which will remain unreachable as we will only attach the newer version. +For reasons to be described below, copying of existing reachable nodes may be enforced. The test `updatedNode + == existingNode` can be used to tell if the node is indeed reachable; if we have copied it already there is no need to + copy it again to update for a new child (note: we may still need to copy reachable mid or end cells in `Split` nodes). + After all modifications coming as the result of application of child branches have been applied, we have an `updatedNode` that reflects all. As we ascend we apply that new value to the parent's `updatedNode`. @@ -722,6 +728,52 @@ manages to attach `truck`; - a reading thread that iterated to `tree` (while `traverse` was not yet attached) and paused, will see `truck` if the mutating thread applies the update during the pause. +### Atomicity + +Atomicity of writes is usually a desirable property. Atomicity means that readers can see either none of the contents +of a mutation, or all of them, i.e. that they can never see some part of an update and miss another. + +We can achieve this by making sure that the application of a mutation has only one attachment point. This is always the +case for single-path updates (`putRecursive` or `apply` where no mutation node has more than one child). We can achieve +the same for branching updates if we "force-copy" all memtable trie nodes at or below the topmost branching node of +the mutation trie. + +This ensures that any partially applied changes are only done in unreachable copy nodes, while concurrent readers +continue working on the originals. Once the branch is fully prepared, we attach it using one in-place write which makes +the whole of it visible. + +The example above with atomic writes will be done as + +![graph](InMemoryTrie.md.a2.svg) + +### Consistency + +The same idea can also be used to enforce sequential consistency, defined here as the property that all readers that see +an update must also be able to see all updates that happened before it (alternatively, if a reader does not see an +update, it will not see any update that was applied after it in the order of execution of the mutating thread). + +Inconsistencies happen because, while a reader is traversing through it, a branch can change at random places, some of +which may be before or after the reader's position in iteration order. We can avoid this problem if we ensure that the +snapshot the reader is operating on does not change. + +To do this, we must force-copy any node we update, until the modification proceeds to the root pointer, which we then +update to the new value (i.e. we force the attachment point for the mutation to be the root pointer). Any reader who has +already read the root pointer will not see any updates that apply after that point in time. Any reader who reads the new +pointer will see everything that the mutation thread did until it wrote that pointer, i.e. the last mutation and all +mutations that precede it. + +![graph](InMemoryTrie.md.a3.svg) + +At the end of the processing of this example, the root pointer is written volatile to the new value `0x13A`. Although we +maintain a full snapshot of the trie, we did not need to copy all nodes, only the ones that were touched by the update +(i.e. the extra space is proportional to the update size, not to the size of the recipient trie). + +Consistency can also be applied below a point selected by the user (e.g. below user-identifiable metadata). In this case +the snapshot is preserved only for nodes at or below the identified point, i.e. force-copying applies at that level +and below, and the attachment point of any update is above the identified point — readers who see the new link +must also see anything that the mutation thread did below that point, including any mutation that preceded the last and +all modified content in the protected branch. + ### Handling prefix nodes The descriptions above were given without prefix nodes. Handling prefixes is just a little complication over the update @@ -732,7 +784,7 @@ To do this we expand the state tracked to: nodes like a prefix with no child) and is the base for all child updates (i.e. it takes the role of `existingNode` in the descriptions above), - `updatedPostContentNode` which is the node as changed/copied after children modifications are applied, -- `contentIndex` which is the index in the content array for the result of merging existing and newly introduced +- `contentIndex` which is the index in the content array for the result of merging existing and newly introduced content, (Note: The mutation content is only readable when the cursor enters the node, and we can only attach it when we ascend from it.) - `transition` remains as before. @@ -751,3 +803,93 @@ When descending at `tree` we set `existingPreContentNode = ~1`, `existingPostCon Ascending back to add the child `~3`, we add a child to `NONE` and get `updatedPostContentNode = 0x0BB`. To then apply the existing content, we create the embedded prefix node `updatedPreContentNode = 0x0BF` with `contentIndex = 1` and pass that on to the recursion. + +### Memory management and cell reuse + +As mentioned in the beginning, in order to avoid long garbage collection pauses due to large long-lasting content in +memtable tries such as the ones used to store database memtables, `InMemoryTrie` uses its own memory management, and +can be used with on- or off-heap memory. + +The most important uses for the trie are long-lived ones, but there are also cases where we want to compose small +short-lived tries, for example to store the result of a query, or to prepare a partition update before it is merged +with the memtable. The two usecases are served most efficiently by using different allocation and reuse methods, which +is why `InMemoryTrie`s offer two factory methods to create two different kinds of tries: + +- `InMemoryTrie.shortLived()` creates a trie that is expected to remain relatively small, to be used only for a short + period (e.g. the duration of a write or read request), and typically to be accessed by one thread only. These tries + reside on heap, because allocation and release of smaller buffers is done more efficiently using garbage collection, and + do not make any attempt to reclaim cells that become unused due to copying or type change. In tries that are accessed + by one thread only, mutations can safely be made without any atomicity or consistency concerns thus without any forced + copying, which means that the overhead of not reclaiming cells should be inconsequential. + `PartitionUpdate`s are an example of short-lived tries, where mutations are prepared before being sent to the commit log + and merged into a memtable. + +- `InMemoryTrie.longLived(OpOrder)` creates a trie which is expected to grow large, to remain in place for a long time, + and to be read by a multitude of threads concurrently with a single mutator. This kind of trie will usually be off-heap, + and will reclaim any cells that become unreferenced due to copying or type change. Because recycling cells requires the + trie/mutator to know if a reader can still be looking at cells that have become unreachable, long lived tries rely on an + `OpOrder` which readers must take a group from before reading the trie, and release when done. + `MemtableShard` (and in general database memtables) use long-lived tries, with the table's `readOrdering` (which all + reads already use) as the `OpOrder` signal when unreachable cells can be reused. + +The on/off-heap distinction is handled by the `BufferType` used in constructing the trie, while the recycling strategy +is handled by the one of the two `MemtableAllocationStrategies`. `NoReuseStrategy`, used by the short-lived option is +trivial, simply bump-allocating cells and objects from linear byte buffer and object array. `OpOrderReuseStrategy` +handles the long-lived case and will be detailed below. + +The descriptions in this section talk about cells, but we apply exactly the same mechanisms for handling slots in the +java object content array (with separate queues). + +#### Cell recycling in long-lived tries + +During the application of a mutation, the `InMemoryTrie` code knows which cells are being copied to another location and +tells the allocation strategy that the cells are going to be freed (using a `recycleCell` or implied in `copyCell`). +This does not mean that the old cell is already free, because: +- (1) it is probably still reachable (if the process has not backtracked enough to attach the new cell to some + parent) by concurrent readers; +- (2) the procedure may fail before the attachment point and the old cell may remain reachable even for this thread; +- (3) it may still be needed by the mutator (e.g. a chain cell is freed when we recognize that the last node in the + chain needs to be moved, but the other nodes in the cell are still in the parent path for the mutation process); or +- (4) concurrent readers may hold a pointer to the old cell, or a parent or child chain that leads to it. + +Thus, we can only recycle a cell when all four conditions are no longer possible. Once an attachment has been made, (1) +and (2) are no longer possible (since attachment writes are volatile, all threads that visit that point at any time +after the write _must_ see the new paths). (3) becomes impossible when the mutation completes. To make things simple, +we use the completion of a mutation (signalled to the allocation strategy via a `completeMutation` call) as the point +in time when all attachments are in place. To make sure (4) is no longer happening, the allocation strategy relies on +the given `OpOrder` — when a barrier, issued at any point _after_ the `completeMutation` call, expires, no cells +identified by the mutation as recyclable can be referenced in any current readers, because they must have followed the +newly set paths and thus cannot have reached those cells. + +The allocation strategy implements this by maintaining several lists: +- just-released cells, added in response to `recycleCell` calls and awaiting `completeMutation` +- cells awaiting barrier, moved from the top list after `completeMutation`, for which a barrier has been issued, + awaiting the barrier to expire +- reusable cells, moved from the list above after their barrier has expired + +For efficiency the allocation strategy does not work with individual cells, but rather in blocks of ~250. Newly +allocated cells are taken from a `free` block. When a mutation releases cells, they are put in a `justReleased` block, +and if the block is filled, another one is created and linked to form a queue. At mutation completion we do nothing if +no block is yet completed; if one is, we issue a barrier and give it to the block (and any other completed blocks in +the `justReleased` queue), with which we move it/them to the tail of an `awaitingBarrier` queue. The head of this queue +is the oldest block of recycled cells and has the highest probability of having passed its barrier — if any block +in the queue has an expired barrier, all previous ones also will (because of the logic of expiring barriers in +`OpOrder`). Hence, when we need to allocate a new cell and the free block is empty, we check if that head block's barrier +has expired, and if it has, we make that the new `free` block. If the barrier hasn't expired, there is no block of cells +that is ready for recycling, thus we must refill the `free` block with new cells. + +Technically, the "reusable cells" list and "cells awaiting barrier" are in the same linked queue, which is effectively +split in two parts by the property of having an expired barrier. Also, to simplify handling, the `free` block stands at +the head of that queue — it plays the part of a sentinel block for the `awaitingBarrier` queue as we +always have a block at `free`, thus `awaitingBarrierTail` can move to it when the queue becomes empty. + +![diagram](InMemoryTrie.md.recycling.svg) + +If an exception is thrown during a mutation, the `InMemoryTrie` code catches that exception and signals `abortMutation` +to the strategy, which tells it that the cells the current call marked as recyclable will probably remain reachable +and should be discarded; because the strategy works with blocks, it will actually discard everything in the +`justReleased` block and queue. This may result in some cell waste — unreachable cells that cannot be recycled +— if cells were allocated and/or an attachment was made before the exception is thrown. We don't expect this to +happen often, but any users of tries that expect them to live indefinitely (unlike memtables which are flushed +regularly; an example would be the chunk cache map when/if we switch it to `InMemoryTrie`) must ensure that exceptions +cannot happen during mutation, otherwise waste can slowly accumulate to bring the node down. diff --git a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.a2.svg b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.a2.svg new file mode 100644 index 000000000000..553981e795bb --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.a2.svg @@ -0,0 +1,634 @@ + + + + + + + + + G + + + + root + + Multi + 0x9A + + + + start + + start/end + + + + + root->start + + + 0x09A + + + + t + + 0x09B + + + + root->t + + + t + + + + root->t + + + t + + + + start->root + + + + + + tractor + + contentArray[0] + + + + tracto + + 0x01B + + + + tracto->tractor + + + r + + + + tract + + 0x01A + + + + tract->tracto + + + o + + + + trac + + 0x019 + + + + trac->tract + + + t + + + + trav + + Multi + 0x0B8 + + + + + tra + + Multi + 0x018 + + + + tra->trac + + + c + + + + tra->trav + + + v + + + + tra2 + + Sparse + 0x0DE + + + + + tree + + contentArray[1] + + + + trie + + contentArray[2] + + + + + tre + + Multi + 0x03B + + + + tre->tree + + + e + + + + tri + + Multi + 0x05B + + + + + truc + + 0x11B + + + + + tri->trie + + + e + + + + tru + + Multi + 0x11A + + + + + tr + + Sparse + 0x07E + + + + tr->tra + + + a + + + + tr->tra + + + a + + + + tr->tre + + + e + + + + tr->tri + + + i + + + + tr->tru + + + u + + + + tr2 + + Sparse + 0x0FE + + + + + t->root + + + 0x09B + + + + t->tr + + + r + + + + t->tr + + + r + + + + t->tr2 + + + r + + + + + trave + + 0x0B9 + + + + trav->trave + + + e + + + + trav->trave + + + e + + + + trav->tra2 + + + 0x0B8 + + + + trave->trav + + + 0x0B9 + + + + traver + + 0x0BA + + + + trave->traver + + + r + + + + trave->traver + + + r + + + + traver->trave + + + 0x0BA + + + + travers + + 0x0BB + + + + traver->travers + + + s + + + + traver->travers + + + s + + + + travers->traver + + + 0x0BB + + + + traverse + + contentArray[3] + + + + travers->traverse + + + e + + + + travers->traverse + + + e + + + + traverse->travers + + + ~3 + + + + tra2->trac + + + c + + + + + tra2->trav + + + v + + + + tra2->tr2 + + + 0x0DE + + + + tru->truc + + + c + + + + tru->truc + + + c + + + + tru->tr2 + + + 0x11A + + + + truc->tru + + + 0x11B + + + + truck + + contentArray[4] + + + + truc->truck + + + k + + + + truc->truck + + + k + + + + truck->truc + + + ~4 + + + + tr2->tre + + + e + + + + tr2->tri + + + i + + + + tr2->t + + + 0x07E + + + + tr2->tra2 + + + a + + + + tr2->tru + + + u + + + diff --git a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.a3.svg b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.a3.svg new file mode 100644 index 000000000000..7ca3661d9181 --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.a3.svg @@ -0,0 +1,659 @@ + + + + + + + + + G + + + + start + + start/end + + + + root + + Multi + 0x9A + + + + start->root + + + + + + tractor + + contentArray[0] + + + + tracto + + 0x01B + + + + tracto->tractor + + + r + + + + tract + + 0x01A + + + + tract->tracto + + + o + + + + trac + + 0x019 + + + + trac->tract + + + t + + + + trav + + Multi + 0x0B8 + + + + + tra + + Multi + 0x018 + + + + tra->trac + + + c + + + + tra->trav + + + v + + + + tra2 + + Sparse + 0x0DE + + + + + tree + + contentArray[1] + + + + trie + + contentArray[2] + + + + + tre + + Multi + 0x03B + + + + tre->tree + + + e + + + + tri + + Multi + 0x05B + + + + + truc + + 0x11B + + + + + tri->trie + + + e + + + + tru + + Multi + 0x11A + + + + + tr + + Sparse + 0x07E + + + + tr->tra + + + a + + + + tr->tra + + + a + + + + tr->tre + + + e + + + + tr->tri + + + i + + + + tr->tru + + + u + + + + tr2 + + Sparse + 0x0FE + + + + + t + + 0x09B + + + + t->tr + + + r + + + + t->tr + + + r + + + + t2 + + 0x13B + + + + + root->t + + + t + + + + root->t + + + t + + + + root2 + + Multi + 0x13A + + + + + + trave + + 0x0B9 + + + + trav->trave + + + e + + + + trav->trave + + + e + + + + trav->tra2 + + + 0x0B8 + + + + trave->trav + + + 0x0B9 + + + + traver + + 0x0BA + + + + trave->traver + + + r + + + + trave->traver + + + r + + + + traver->trave + + + 0x0BA + + + + travers + + 0x0BB + + + + traver->travers + + + s + + + + traver->travers + + + s + + + + travers->traver + + + 0x0BB + + + + traverse + + contentArray[3] + + + + travers->traverse + + + e + + + + travers->traverse + + + e + + + + traverse->travers + + + ~3 + + + + tra2->trac + + + c + + + + + tra2->trav + + + v + + + + tra2->tr2 + + + 0x0DE + + + + tru->truc + + + c + + + + tru->truc + + + c + + + + tru->tr2 + + + 0x11A + + + + truc->tru + + + 0x11B + + + + truck + + contentArray[4] + + + + truc->truck + + + k + + + + truc->truck + + + k + + + + truck->truc + + + ~4 + + + + root2->start + + + 0x13A + + + + root2->t2 + + + t + + + + t2->root2 + + + 0x13B + + + + t2->tr2 + + + r + + + + tr2->tre + + + e + + + + tr2->tri + + + i + + + + tr2->tra2 + + + a + + + + tr2->tru + + + u + + + + tr2->t2 + + + 0x0FE + + + diff --git a/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.recycling.svg b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.recycling.svg new file mode 100644 index 000000000000..7861ea54b28d --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/InMemoryTrie.md.recycling.svg @@ -0,0 +1,88 @@ + + +block2ready cellspartially usedblock3ready cellsexpired barrier Bblock4cells awaiting barrieractive barrier Cblock5cells awaiting barrieractive barrier Cblock6released cellsfullno barrierblock7released cellspartially filledfreeallocateCelltakes cellsfrom here.freewill move herewhen block2 is exhausted.If block3 is exhaustedbefore the barrier expires,it will be replenishedwith fresh cells.awaitingBarrierTailBlocks 5 and 4 were movedtogether fromjustReleased,they share the same barrier.justReleasedrecycleCellputs cells here.This block is awaitingcompleteMutation. \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/tries/MemoryAllocationStrategy.java b/src/java/org/apache/cassandra/db/tries/MemoryAllocationStrategy.java new file mode 100644 index 000000000000..c34942097437 --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/MemoryAllocationStrategy.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.tries; + +import com.google.common.annotations.VisibleForTesting; + +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.utils.concurrent.OpOrder; + +/** + * Allocation strategy for buffers and arrays for InMemoryTrie's. Controls how space is allocated and reused. + */ +public interface MemoryAllocationStrategy +{ + /** + * Get a free index. This is either a new index, allocated via the passed index producer functions, or one that + * has been previously recycled. + */ + int allocate() throws TrieSpaceExhaustedException; + + /** + * Marks the given index for recycling. + * + * When the index is actually reused depends on the recycling strategy. In any case it cannot be before the current + * mutation is complete (because it may still be walking cells that have been moved), and any concurrent readers + * that have started before this cell has become unreachable must also have completed. + */ + void recycle(int index); + + /** + * To be called when a mutation completes. No new readers must be able to see recycled content at the time of this + * call (the paths for reaching them must have been overwritten via a volatile write; additionally, if the buffer + * has grown, the root variable (which is stored outside the buffer) must have accepted a volatile write). + * No recycled indexes can be made available for reuse before this is called, and before any readers started before + * this call have completed. + */ + void completeMutation(); + + /** + * Called when a mutation is aborted because of an exception. This means that the indexes that were marked for + * recycling are still going to be in use (unless this is called a later separate completeMutation call may release + * and reuse them, causing corruption). + * + * Aborted mutations are not normal, and at this time we are not trying to ensure that a trie will behave at its + * best if an abort has taken place (i.e. it may take more space, be slower etc.), but it should still operate + * correctly. + */ + void abortMutation(); + + /** + * Returns the number of indexes that have been claimed by the allocation strategy but are not currently in use + * (either because they are in various stages of recycling, or have yet to see first use). + */ + long indexCountInPipeline(); + + /** + * Constructs a list of all the indexes that are in the recycling pipeline. + * Used to test available and unreachable indexes are the same thing. + */ + @VisibleForTesting + IntArrayList indexesInPipeline(); + + interface Allocator + { + int allocate() throws TrieSpaceExhaustedException; + + default void allocate(int[] indexList) throws TrieSpaceExhaustedException + { + for (int i = indexList.length - 1; i >= 0; --i) + indexList[i] = allocate(); + } + } + + /** + * Strategy for small short-lived tries, usually on-heap. This strategy does not reuse any indexes. + */ + class NoReuseStrategy implements MemoryAllocationStrategy + { + final Allocator allocator; + + public NoReuseStrategy(Allocator allocator) + { + this.allocator = allocator; + } + + public int allocate() throws TrieSpaceExhaustedException + { + return allocator.allocate(); + } + + public void recycle(int index) + { + // No reuse, do nothing + } + + public void completeMutation() + { + // No reuse, nothing to do + } + + public void abortMutation() + { + // No reuse, nothing to do + } + + @Override + public long indexCountInPipeline() + { + // No indexes recycled + return 0; + } + + @Override + public IntArrayList indexesInPipeline() + { + return new IntArrayList(); + } + } + + /** + * Reuse strategy for large, long-lived tries. Recycles indexes when it knows that the mutation recycling + * them has completed, and all reads started no later than this completion have also completed (signalled by an + * OpOrder which the strategy assumes all readers subscribe to). + * + * The OpOrder recycling strategy holds queues of indexes available for recycling. The queues ar organized in blocks + * of REUSE_BLOCK_SIZE entries. The blocks move through the following stages: + * - Being filled with newly released indexes. In this stage they are at the head of the "justReleased" list. When + * a block becomes full, a new block is created and attached to the head of the list. + * - Full, but the mutation that released one or more of the mutations in them has not yet completed. In this stage + * they are attached to the "justReleased" list as the second or further block. When a mutationComplete is + * received, all such blocks get issued a common OpOrder.Barrier and are attached to "awaitingBarrierTail" (which + * is the tail of the "free" list). + * - Awaiting a barrier. In this stage they are in the "free" list after its head, closer to its + * "awaitingBarrierTail", identified by the fact that their barrier has not yet expired. Note that the blocks are + * put in the order in which their barriers are issued, thus if a block has an active barrier, all blocks that + * follow it in the list also do. + * - Ready for use. In this stage they are still in the "free" list after its head, but their barrier has now + * expired. All the indexes in such blocks can now be reused, and will be when the head of the list is exhausted. + * - Active free block at the head of the "free" list. This block is the one new allocations are served from. When + * it is exhausted, we check if the next block's barrier has expired. If so, the "free" pointer moves to it. + * If not, there's nothing to reuse as any blocks in the list still have an active barrier, thus we grab some new + * memory and refill the block. + * - If a mutation is aborted by an error, we throw away all indexes in the "justReleased" list. This is done so + * that none of the indexes that were marked for release, but whose parent chain may have remained in place, + * making them reachable, are reused and corrupt the trie. This will leak some indexes (from earlier mutations in + * the block and/or ones whose parents have already been moved), but we prefer not to pay the cost of identifying + * the exact indexes that need to remain or be recycled. + * We assume that exceptions while mutating are not normal and should not happen, and thus a temporary leak (e.g. + * until the memtable is switched) is acceptable. Should this change (e.g. if a trie is used for the full lifetime + * of the process or longer and exceptions are expected as part of its function), we can implement a reachability + * walk to identify orphaned indexes and call it with some frequency after one or more exceptions have occured. + */ + static class OpOrderReuseStrategy implements MemoryAllocationStrategy + { + /** + * Cells list holding indexes that are just recycled. When full, new one is allocated and linked. + * + * On mutationComplete, any full (in justReleased.nextList) lists get issued a barrier and are moved to + * awaitingBarrierTail. + */ + IndexBlockList justReleased; + + /** + * Tail of the "free and awaiting barrier" queue. This is reachable by following the links from free. + * + * Full lists are attached to this tail when their barrier is issued. + * Lists are consumed from the head when free becomes empty if the list at the head has an expired barrier. + */ + IndexBlockList awaitingBarrierTail; + + /** + * Current free list, head of the "free and awaiting barrier" queue. Allocations are served from here. + * + * Starts full, and when it is exhausted we check the barrier at the next linked block. + * If expired, update free to point to it (consuming one block from the queue). + * If not, re-fill the block by allocating a new set of REUSE_BLOCK_SIZE indexes. + */ + IndexBlockList free; + + /** + * Called to allocate a new block of indexes to distribute. + */ + final Allocator allocator; + final OpOrder opOrder; + + public OpOrderReuseStrategy(Allocator allocator, OpOrder opOrder) + { + this.allocator = allocator; + this.opOrder = opOrder; + justReleased = new IndexBlockList(null); + awaitingBarrierTail = free = new IndexBlockList(null); + free.count = 0; + } + + @Override + public int allocate() throws TrieSpaceExhaustedException + { + if (free.count == 0) + { + IndexBlockList awaitingBarrierHead = free.nextList; + if (awaitingBarrierHead != null && + (awaitingBarrierHead.barrier == null || awaitingBarrierHead.barrier.allPriorOpsAreFinished())) + { + // A block is ready for reuse. Switch to it. + free = awaitingBarrierHead; + // Index blocks only enter these lists when the justReleased block is filled. Sanity check that + // the block is still full. + assert free.count == free.indexes.length; + // We could recycle/pool the IndexBlockList object that free was pointing to before this. + // As the trie will create and drop many times more objects to end up filling one of these, the + // potential impact does not appear to justify the extra complexity. + } + else + { + // Nothing available for reuse. Grab more memory. + allocator.allocate(free.indexes); + free.count = free.indexes.length; + } + } + + return free.indexes[--free.count]; + } + + @Override + public void recycle(int index) + { + if (justReleased.count == REUSE_BLOCK_SIZE) + { + // Block is full, allocate and attach a new one. + justReleased = new IndexBlockList(justReleased); + } + + justReleased.indexes[justReleased.count++] = index; + } + + @Override + public void completeMutation() + { + IndexBlockList toProcess = justReleased.nextList; + if (toProcess == null) + return; + + // We have some completed blocks now, issue a barrier for them and move them to the + // "free and awaiting barrier" queue. + justReleased.nextList = null; + + OpOrder.Barrier barrier = null; + if (opOrder != null) + { + barrier = opOrder.newBarrier(); + barrier.issue(); + } + + IndexBlockList last = null; + for (IndexBlockList current = toProcess; current != null; current = current.nextList) + { + current.barrier = barrier; + last = current; + } + + assert awaitingBarrierTail.nextList == null; + awaitingBarrierTail.nextList = toProcess; + awaitingBarrierTail = last; + } + + @Override + public void abortMutation() + { + // Some of the releases in the justReleased queue may still be reachable cells. + // We don't have a way of telling which, so we have to remove everything. + justReleased.nextList = null; + justReleased.count = 0; + } + + /** + * Returns the number of indexes that are somewhere in the recycling pipeline. + */ + @Override + public long indexCountInPipeline() + { + long count = 0; + for (IndexBlockList list = justReleased; list != null; list = list.nextList) + count += list.count; + for (IndexBlockList list = free; list != null; list = list.nextList) // includes awaiting barrier + count += list.count; + return count; + } + + @Override + public IntArrayList indexesInPipeline() + { + IntArrayList res = new IntArrayList((int) indexCountInPipeline(), -1); + for (IndexBlockList list = justReleased; list != null; list = list.nextList) + res.addAll(new IntArrayList(list.indexes, list.count, -1)); + for (IndexBlockList list = free; list != null; list = list.nextList) // includes awaiting barrier + res.addAll(new IntArrayList(list.indexes, list.count, -1)); + return res; + } + } + + + static final int REUSE_BLOCK_SIZE = 252; // array fits into 1k bytes + + static class IndexBlockList + { + final int[] indexes; + int count; + OpOrder.Barrier barrier; + IndexBlockList nextList; + + IndexBlockList(IndexBlockList next) + { + indexes = new int[REUSE_BLOCK_SIZE]; + nextList = next; + count = 0; + } + } +} diff --git a/src/java/org/apache/cassandra/db/tries/MergeTrie.java b/src/java/org/apache/cassandra/db/tries/MergeTrie.java index f2807769844f..ffdfee4267e8 100644 --- a/src/java/org/apache/cassandra/db/tries/MergeTrie.java +++ b/src/java/org/apache/cassandra/db/tries/MergeTrie.java @@ -19,6 +19,8 @@ import com.google.common.collect.Iterables; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + /** * A merged view of two tries. * @@ -44,25 +46,27 @@ class MergeTrie extends Trie } @Override - protected Cursor cursor() + protected Cursor cursor(Direction direction) { - return new MergeCursor<>(resolver, t1, t2); + return new MergeCursor<>(resolver, direction, t1, t2); } static class MergeCursor implements Cursor { private final MergeResolver resolver; + private final Direction direction; private final Cursor c1; private final Cursor c2; boolean atC1; boolean atC2; - MergeCursor(MergeResolver resolver, Trie t1, Trie t2) + MergeCursor(MergeResolver resolver, Direction direction, Trie t1, Trie t2) { this.resolver = resolver; - this.c1 = t1.cursor(); - this.c2 = t2.cursor(); + this.direction = direction; + this.c1 = t1.cursor(direction); + this.c2 = t2.cursor(direction); assert c1.depth() == 0; assert c2.depth() == 0; atC1 = atC2 = true; @@ -76,10 +80,17 @@ public int advance() } @Override - public int skipChildren() + public int skipTo(int skipDepth, int skipTransition) { - return checkOrder(atC1 ? c1.skipChildren() : c1.depth(), - atC2 ? c2.skipChildren() : c2.depth()); + int c1depth = c1.depth(); + int c2depth = c2.depth(); + assert skipDepth <= c1depth + 1 || skipDepth <= c2depth + 1; + if (atC1 || skipDepth < c1depth || skipDepth == c1depth && direction.gt(skipTransition, c1.incomingTransition())) + c1depth = c1.skipTo(skipDepth, skipTransition); + if (atC2 || skipDepth < c2depth || skipDepth == c2depth && direction.gt(skipTransition, c2.incomingTransition())) + c2depth = c2.skipTo(skipDepth, skipTransition); + + return checkOrder(c1depth, c2depth); } @Override @@ -116,8 +127,9 @@ private int checkOrder(int c1depth, int c2depth) // c1depth == c2depth int c1trans = c1.incomingTransition(); int c2trans = c2.incomingTransition(); - atC1 = c1trans <= c2trans; - atC2 = c1trans >= c2trans; + atC1 = direction.le(c1trans, c2trans); + atC2 = direction.le(c2trans, c1trans); + assert atC1 | atC2; return c1depth; } @@ -133,6 +145,21 @@ public int incomingTransition() return atC1 ? c1.incomingTransition() : c2.incomingTransition(); } + @Override + public Direction direction() + { + return direction; + } + + @Override + public ByteComparable.Version byteComparableVersion() + { + assert c1.byteComparableVersion() == c2.byteComparableVersion() : + "Merging cursors with different byteComparableVersions: " + + c1.byteComparableVersion() + " vs " + c2.byteComparableVersion(); + return c1.byteComparableVersion(); + } + public T content() { T mc = atC2 ? c2.content() : null; @@ -144,6 +171,19 @@ else if (nc == null) else return resolver.resolve(nc, mc); } + + @Override + public Trie tailTrie() + { + if (atC1 && atC2) + return new MergeTrie<>(resolver, c1.tailTrie(), c2.tailTrie()); + else if (atC1) + return c1.tailTrie(); + else if (atC2) + return c2.tailTrie(); + else + throw new AssertionError(); + } } /** diff --git a/src/java/org/apache/cassandra/db/tries/PrefixedTrie.java b/src/java/org/apache/cassandra/db/tries/PrefixedTrie.java new file mode 100644 index 000000000000..cf5f9dd63513 --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/PrefixedTrie.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.tries; + +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * Prefixed trie. Represents the content of the given trie with the prefix prepended to all keys. + */ +public class PrefixedTrie extends Trie +{ + final ByteComparable prefix; + final Trie trie; + + public PrefixedTrie(ByteComparable prefix, Trie trie) + { + this.prefix = prefix; + this.trie = trie; + } + + @Override + protected Trie.Cursor cursor(Direction direction) + { + Trie.Cursor sourceCursor = trie.cursor(direction); + return new Cursor<>(prefix.asComparableBytes(sourceCursor.byteComparableVersion()), sourceCursor); + } + + private static class Cursor implements Trie.Cursor + { + final Trie.Cursor tail; + ByteSource prefixBytes; + int nextPrefixByte; + int incomingTransition; + int depthOfPrefix; + + Cursor(ByteSource prefix, Trie.Cursor tail) + { + this.tail = tail; + prefixBytes = prefix; + incomingTransition = -1; + nextPrefixByte = prefixBytes.next(); + depthOfPrefix = 0; + } + + int completeAdvanceInTail(int depthInTail) + { + if (depthInTail < 0) + return exhausted(); + + incomingTransition = tail.incomingTransition(); + return depthInTail + depthOfPrefix; + } + + boolean prefixDone() + { + return nextPrefixByte == ByteSource.END_OF_STREAM; + } + + @Override + public int depth() + { + if (prefixDone()) + return tail.depth() + depthOfPrefix; + else + return depthOfPrefix; + } + + @Override + public int incomingTransition() + { + return incomingTransition; + } + + @Override + public int advance() + { + if (prefixDone()) + return completeAdvanceInTail(tail.advance()); + + ++depthOfPrefix; + incomingTransition = nextPrefixByte; + nextPrefixByte = prefixBytes.next(); + return depthOfPrefix; + } + + @Override + public int advanceMultiple(Trie.TransitionsReceiver receiver) + { + if (prefixDone()) + return completeAdvanceInTail(tail.advanceMultiple(receiver)); + + while (!prefixDone()) + { + receiver.addPathByte(incomingTransition); + ++depthOfPrefix; + incomingTransition = nextPrefixByte; + nextPrefixByte = prefixBytes.next(); + } + return depthOfPrefix; + } + + @Override + public int skipTo(int skipDepth, int skipTransition) + { + // regardless if we exhausted prefix, if caller asks for depth <= prefix depth, we're done. + if (skipDepth <= depthOfPrefix) + return exhausted(); + if (prefixDone()) + return completeAdvanceInTail(tail.skipTo(skipDepth - depthOfPrefix, skipTransition)); + assert skipDepth == depthOfPrefix + 1 : "Invalid advance request to depth " + skipDepth + " to cursor at depth " + depthOfPrefix; + if (tail.direction().gt(skipTransition, nextPrefixByte)) + return exhausted(); + return advance(); + } + + private int exhausted() + { + incomingTransition = -1; + depthOfPrefix = -1; + nextPrefixByte = 0; // to make prefixDone() false so incomingTransition/depth/content are -1/-1/null + return depthOfPrefix; + } + + public Direction direction() + { + return tail.direction(); + } + + public ByteComparable.Version byteComparableVersion() + { + return tail.byteComparableVersion(); + } + + @Override + public T content() + { + return prefixDone() ? tail.content() : null; + } + + @Override + public Trie tailTrie() + { + if (prefixDone()) + return tail.tailTrie(); + else + { + assert depthOfPrefix >= 0 : "tailTrie called on exhausted cursor"; + if (!(prefixBytes instanceof ByteSource.Duplicatable)) + prefixBytes = ByteSource.duplicatable(prefixBytes); + ByteSource.Duplicatable duplicatableSource = (ByteSource.Duplicatable) prefixBytes; + + return new PrefixedTrie<>(v -> duplicatableSource.duplicate(), tail.tailTrie()); + } + } + } +} diff --git a/src/java/org/apache/cassandra/db/tries/SingletonTrie.java b/src/java/org/apache/cassandra/db/tries/SingletonTrie.java index 0336a851ffab..e3eb62783ea1 100644 --- a/src/java/org/apache/cassandra/db/tries/SingletonTrie.java +++ b/src/java/org/apache/cassandra/db/tries/SingletonTrie.java @@ -26,26 +26,34 @@ class SingletonTrie extends Trie { private final ByteComparable key; + private final ByteComparable.Version byteComparableVersion; private final T value; - SingletonTrie(ByteComparable key, T value) + SingletonTrie(ByteComparable key, ByteComparable.Version byteComparableVersion, T value) { + this.byteComparableVersion = byteComparableVersion; this.key = key; this.value = value; } - public Cursor cursor() + public Cursor cursor(Direction direction) { - return new Cursor(); + return new Cursor(direction); } class Cursor implements Trie.Cursor { - private final ByteSource src = key.asComparableBytes(BYTE_COMPARABLE_VERSION); + private final Direction direction; + private ByteSource src = key.asComparableBytes(byteComparableVersion); private int currentDepth = 0; private int currentTransition = -1; private int nextTransition = src.next(); + public Cursor(Direction direction) + { + this.direction = direction; + } + @Override public int advance() { @@ -83,9 +91,17 @@ public int advanceMultiple(TransitionsReceiver receiver) } @Override - public int skipChildren() + public int skipTo(int skipDepth, int skipTransition) { - return currentDepth = -1; // no alternatives + if (skipDepth <= currentDepth) + { + assert skipDepth < currentDepth || direction.gt(skipTransition, currentTransition); + return currentDepth = -1; // no alternatives + } + if (direction.gt(skipTransition, nextTransition)) + return currentDepth = -1; // request is skipping over our path + + return advance(); } @Override @@ -105,5 +121,27 @@ public int incomingTransition() { return currentTransition; } + + @Override + public Direction direction() + { + return direction; + } + + @Override + public ByteComparable.Version byteComparableVersion() + { + return byteComparableVersion; + } + + @Override + public Trie tailTrie() + { + if (!(src instanceof ByteSource.Duplicatable)) + src = ByteSource.duplicatable(src); + ByteSource.Duplicatable duplicatableSource = (ByteSource.Duplicatable) src; + + return new SingletonTrie(v -> duplicatableSource.duplicate(), byteComparableVersion, value); + } } } diff --git a/src/java/org/apache/cassandra/db/tries/SlicedTrie.java b/src/java/org/apache/cassandra/db/tries/SlicedTrie.java index 75ae3df27e10..c14f0adde620 100644 --- a/src/java/org/apache/cassandra/db/tries/SlicedTrie.java +++ b/src/java/org/apache/cassandra/db/tries/SlicedTrie.java @@ -61,170 +61,297 @@ public SlicedTrie(Trie source, ByteComparable left, boolean includeLeft, Byte this.includeRight = includeRight; } + static ByteSource openAndMaybeAdd0(ByteComparable key, ByteComparable.Version byteComparableVersion, boolean shouldAdd0) + { + if (key == null) + return null; + ByteSource src = key.asComparableBytes(byteComparableVersion); + if (shouldAdd0) + return ByteSource.append(src, 0); + else + return src; + } + @Override - protected Cursor cursor() + protected Cursor cursor(Direction direction) + { + Cursor sourceCursor = source.cursor(direction); + // The cursor is left-inclusive and right-exclusive by default. If we need to change the inclusiveness, adjust + // the bound to the next possible value by adding a 00 byte at the end. + ByteSource leftSource = openAndMaybeAdd0(left, sourceCursor.byteComparableVersion(), !includeLeft); + ByteSource rightSource = openAndMaybeAdd0(right, sourceCursor.byteComparableVersion(), includeRight); + + // Empty left bound is the same as having no left bound, adjust for that. + int leftNext = -1; + if (leftSource != null) + { + leftNext = leftSource.next(); + if (leftNext == ByteSource.END_OF_STREAM) + leftSource = null; + } + + // Empty right bound means the result can only be empty. Make things easier for the cursor by handling this. + int rightNext = -1; + if (rightSource != null) + { + rightNext = rightSource.next(); + if (rightNext == ByteSource.END_OF_STREAM) + { + assert leftSource == null : "Invalid range " + sliceString(); + return new Trie.EmptyCursor<>(direction, sourceCursor.byteComparableVersion()); + } + } + + return new SlicedCursor<>(sourceCursor, + leftSource, + leftNext, + rightSource, + rightNext, + direction); + } + + String sliceString() { - return new SlicedCursor<>(this); + ByteComparable.Version version = source.cursor(Direction.FORWARD).byteComparableVersion(); + return String.format("%s%s;%s%s", + includeLeft ? "[" : "(", + left.byteComparableAsString(version), + right.byteComparableAsString(version), + includeRight ? "]" : ")"); } private enum State { - /** The cursor is still positioned on some prefix of the left bound. Content should not be produced. */ - BEFORE_LEFT, - /** The cursor is positioned inside the range, i.e. beyond the left bound, possibly on a prefix of the right. */ + /** + * The cursor is at the initial phase while it is walking prefixes of both bounds. + * Content is not to be reported. + */ + COMMON_PREFIX, + /** + * The cursor is positioned on some prefix of the start bound, strictly before any prefix of the end bound in + * iteration order. + * Content should only be reported in the reverse direction (as these prefixes are prefixes of the right bound + * and included in the slice). + */ + START_PREFIX, + /** + * The cursor is positioned inside the range, i.e. strictly between any prefixes of the start and end bounds. + * All content should be reported. + */ INSIDE, - /** The cursor is positioned beyond the right bound. Exhaustion (depth -1) has been reported. */ - AFTER_RIGHT + /** + * The cursor is positioned on some prefix of the end bound, strictly after any prefix of the start bound. + * Content should only be reported in the forward direction. + */ + END_PREFIX, + /** The cursor is positioned beyond the end bound. Exhaustion (depth -1) has been reported. */ + EXHAUSTED; } private static class SlicedCursor implements Cursor { - private final ByteSource left; - private final ByteSource right; - private final boolean includeLeft; - private final boolean excludeRight; + private ByteSource start; + private ByteSource end; private final Cursor source; + private final Direction direction; - private State state; - private int leftNext; - private int leftNextDepth; - private int rightNext; - private int rightNextDepth; + State state; + int startNext; + int startNextDepth; + int endNext; + int endNextDepth; - public SlicedCursor(SlicedTrie slicedTrie) + public SlicedCursor(Cursor source, + ByteSource leftSource, + int leftNext, + ByteSource rightSource, + int rightNext, + Direction direction) { - source = slicedTrie.source.cursor(); - if (slicedTrie.left != null) - { - left = slicedTrie.left.asComparableBytes(BYTE_COMPARABLE_VERSION); - includeLeft = slicedTrie.includeLeft; - leftNext = left.next(); - leftNextDepth = 1; - if (leftNext == ByteSource.END_OF_STREAM && includeLeft) - state = State.INSIDE; - else - state = State.BEFORE_LEFT; - } - else - { - left = null; - includeLeft = true; - state = State.INSIDE; - } - - if (slicedTrie.right != null) - { - right = slicedTrie.right.asComparableBytes(BYTE_COMPARABLE_VERSION); - excludeRight = !slicedTrie.includeRight; - rightNext = right.next(); - rightNextDepth = 1; - if (rightNext == ByteSource.END_OF_STREAM && excludeRight) - state = State.BEFORE_LEFT; // This is a hack, we are after the right bound but we don't want to - // report depth -1 yet. So just make sure root's content is not reported. - } - else - { - right = null; - excludeRight = true; - rightNextDepth = 0; - } + this.source = source; + this.direction = direction; + start = direction.select(leftSource, rightSource); + end = direction.select(rightSource, leftSource); + startNext = direction.select(leftNext, rightNext); + endNext = direction.select(rightNext, leftNext); + startNextDepth = start != null ? 1 : 0; + endNextDepth = end != null ? 1 : 0; + state = start != null + ? end != null + ? State.COMMON_PREFIX + : State.START_PREFIX + : end != null + ? State.END_PREFIX + : State.INSIDE; } @Override public int advance() { - assert (state != State.AFTER_RIGHT); - - int newDepth = source.advance(); - int transition = source.incomingTransition(); + int newDepth; + int transition; - if (state == State.BEFORE_LEFT) + switch (state) { - // Skip any transitions before the left bound - while (newDepth == leftNextDepth && transition < leftNext) - { - newDepth = source.skipChildren(); + case COMMON_PREFIX: + case START_PREFIX: + // Skip any transitions before the start bound + newDepth = source.skipTo(startNextDepth, startNext); transition = source.incomingTransition(); - } - - // Check if we are still following the left bound - if (newDepth == leftNextDepth && transition == leftNext) - { - assert leftNext != ByteSource.END_OF_STREAM; - leftNext = left.next(); - ++leftNextDepth; - if (leftNext == ByteSource.END_OF_STREAM && includeLeft) - state = State.INSIDE; // report the content on the left bound - } - else // otherwise we are beyond it - state = State.INSIDE; + return checkBothBounds(newDepth, transition); + case INSIDE: + case END_PREFIX: + newDepth = source.advance(); + transition = source.incomingTransition(); + return checkEndBound(newDepth, transition); + default: + throw new AssertionError(); } - - return checkRightBound(newDepth, transition); } private int markDone() { - state = State.AFTER_RIGHT; + state = State.EXHAUSTED; return -1; } - private int checkRightBound(int newDepth, int transition) + int checkBothBounds(int newDepth, int transition) + { + // Check if we are still following the start bound + if (newDepth == startNextDepth && transition == startNext) + { + assert startNext != ByteSource.END_OF_STREAM; + startNext = start.next(); + ++startNextDepth; + State currState = state; + // In the forward direction the exact match for the left bound and all descendant states are + // included in the set. + // In the reverse direction we will instead use the -1 as target transition and thus ascend on + // the next advance (skipping the exact right bound and all its descendants). + if (startNext == ByteSource.END_OF_STREAM && direction.isForward()) + state = State.INSIDE; // checkEndBound may adjust this to END_PREFIX + if (currState == State.START_PREFIX) + return newDepth; // there is no need to check the end bound as we descended along a + // strictly earlier path + } + else // otherwise we are beyond the start bound + state = State.INSIDE; // checkEndBound may adjust this to END_PREFIX + + return checkEndBound(newDepth, transition); + } + + private int checkEndBound(int newDepth, int transition) { // Cursor positions compare by depth descending and transition ascending. - if (newDepth > rightNextDepth) - return newDepth; - if (newDepth < rightNextDepth) + if (newDepth > endNextDepth) + return newDepth; // happy and quick path in the interior of the slice + // (state == State.INSIDE can be asserted here (we skip it for efficiency)) + if (newDepth < endNextDepth) return markDone(); - // newDepth == rightDepth - if (transition < rightNext) + // newDepth == endDepth + if (direction.lt(transition, endNext)) + { + adjustStateStrictlyBeforeEnd(); return newDepth; - if (transition > rightNext) + } + if (direction.lt(endNext, transition)) return markDone(); - // Following right bound - rightNext = right.next(); - ++rightNextDepth; - if (rightNext == ByteSource.END_OF_STREAM && excludeRight) - return markDone(); // do not report any content on the right bound + // Following end bound + endNext = end.next(); + ++endNextDepth; + if (endNext == ByteSource.END_OF_STREAM) + { + // At the exact end bound. + if (direction.isForward()) + { + // In forward direction the right bound is not included in the slice. + return markDone(); + } + else + { + // In reverse, the left bound and all its descendants are included, thus we use the -1 as limiting + // transition. We can also see the bound as strictly ahead of our current position as the current + // branch should be fully included. + adjustStateStrictlyBeforeEnd(); + } + } + else + adjustStateAtEndPrefix(); return newDepth; } + private void adjustStateAtEndPrefix() + { + switch (state) + { + case INSIDE: + state = State.END_PREFIX; + break; + } + } + + private void adjustStateStrictlyBeforeEnd() + { + switch (state) + { + case COMMON_PREFIX: + state = State.START_PREFIX; + break; + case END_PREFIX: + state = State.INSIDE; + break; + } + } + @Override public int advanceMultiple(TransitionsReceiver receiver) { switch (state) { - case BEFORE_LEFT: + case COMMON_PREFIX: + case START_PREFIX: + case END_PREFIX: return advance(); // descend only one level to be able to compare cursors correctly case INSIDE: int depth = source.depth(); - if (depth == rightNextDepth - 1) // this is possible because right is already advanced; - return advance(); // we need to check next byte against right boundary in this case int newDepth = source.advanceMultiple(receiver); if (newDepth > depth) - return newDepth; // successfully advanced + return newDepth; // successfully descended // we ascended, check if we are still within boundaries - return checkRightBound(newDepth, source.incomingTransition()); + return checkEndBound(newDepth, source.incomingTransition()); default: throw new AssertionError(); } } @Override - public int skipChildren() + public int skipTo(int skipDepth, int skipTransition) { - assert (state != State.AFTER_RIGHT); + // if skipping beyond end, we are done + if (skipDepth < endNextDepth || skipDepth == endNextDepth && direction.gt(skipTransition, endNext)) + return markDone(); + // if skipping before start, adjust request to skip to start + if (skipDepth == startNextDepth && direction.lt(skipTransition, startNext)) + skipTransition = startNext; - // We are either inside or following the left bound. In the latter case ascend takes us beyond it. - state = State.INSIDE; - return checkRightBound(source.skipChildren(), source.incomingTransition()); + switch (state) + { + case START_PREFIX: + case COMMON_PREFIX: + return checkBothBounds(source.skipTo(skipDepth, skipTransition), source.incomingTransition()); + case INSIDE: + case END_PREFIX: + return checkEndBound(source.skipTo(skipDepth, skipTransition), source.incomingTransition()); + default: + throw new AssertionError("Cursor already exhaused."); + } } @Override public int depth() { - return state == State.AFTER_RIGHT ? -1 : source.depth(); + return state == State.EXHAUSTED ? -1 : source.depth(); } @Override @@ -233,10 +360,99 @@ public int incomingTransition() return source.incomingTransition(); } + @Override + public Direction direction() + { + return direction; + } + + @Override + public ByteComparable.Version byteComparableVersion() + { + return source.byteComparableVersion(); + } + @Override public T content() { - return state == State.INSIDE ? source.content() : null; + switch (state) + { + case INSIDE: + return source.content(); + // Additionally, prefixes of the right bound (which are not prefixes of the left) need to be reported: + case START_PREFIX: + // start prefixes in reverse direction (but making sure we don't report the exact match); + return !direction.isForward() && startNext != ByteSource.END_OF_STREAM ? source.content() : null; + case END_PREFIX: + // end prefixes in forward direction. + return direction.isForward() ? source.content() : null; + default: + return null; + } + } + + @Override + public Trie tailTrie() + { + final Trie sourceTail = source.tailTrie(); + switch (state) + { + case INSIDE: + return sourceTail; + case COMMON_PREFIX: + return makeTrie(sourceTail, duplicatableStart(), startNext, duplicatableEnd(), endNext, direction); + case START_PREFIX: + return makeTrie(sourceTail, duplicatableStart(), startNext, null, -1, direction); + case END_PREFIX: + return makeTrie(sourceTail, null, -1, duplicatableEnd(), endNext, direction); + default: + throw new UnsupportedOperationException("tailTrie on a slice boundary"); + } + } + + private ByteSource.Duplicatable duplicatableStart() + { + if (start == null || start instanceof ByteSource.Duplicatable) + return (ByteSource.Duplicatable) start; + ByteSource.Duplicatable duplicatable = ByteSource.duplicatable(start); + start = duplicatable; + return duplicatable; + } + + private ByteSource.Duplicatable duplicatableEnd() + { + if (end == null || end instanceof ByteSource.Duplicatable) + return (ByteSource.Duplicatable) end; + ByteSource.Duplicatable duplicatable = ByteSource.duplicatable(end); + end = duplicatable; + return duplicatable; + } + + + private static Trie makeTrie(Trie source, + ByteSource.Duplicatable startSource, + int startNext, + ByteSource.Duplicatable endSource, + int endNext, + Direction direction) + { + ByteSource.Duplicatable leftSource = direction.select(startSource, endSource); + ByteSource.Duplicatable rightSource = direction.select(endSource, startSource); + int leftNext = direction.select(startNext, endNext); + int rightNext = direction.select(endNext, startNext); + return new Trie() + { + @Override + protected Cursor cursor(Direction direction) + { + return new SlicedCursor<>(source.cursor(direction), + leftSource != null ? leftSource.duplicate() : null, + leftNext, + rightSource != null ? rightSource.duplicate() : null, + rightNext, + direction); + } + }; } } } diff --git a/src/java/org/apache/cassandra/db/tries/Trie.java b/src/java/org/apache/cassandra/db/tries/Trie.java index a139e08e67df..90006e52525b 100644 --- a/src/java/org/apache/cassandra/db/tries/Trie.java +++ b/src/java/org/apache/cassandra/db/tries/Trie.java @@ -28,31 +28,32 @@ import org.agrona.DirectBuffer; import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; /** * Base class for tries. - * + *

    * Normal users of tries will only use the public methods, which provide various transformations of the trie, conversion * of its content to other formats (e.g. iterable of values), and several forms of processing. - * + *

    * For any unimplemented data extraction operations one can build on the {@link TrieEntriesWalker} (for-each processing) * and {@link TrieEntriesIterator} (to iterator) base classes, which provide the necessary mechanisms to handle walking * the trie. - * + *

    * The internal representation of tries using this interface is defined in the {@link Cursor} interface. - * + *

    * Cursors are a method of presenting the internal structure of a trie without representing nodes as objects, which is * still useful for performing the basic operations on tries (iteration, slicing/intersection and merging). A cursor * will list the nodes of a trie in order, together with information about the path that was taken to reach them. - * - * To begin traversal over a trie, one must retrieve a cursor by calling {@link #cursor()}. Because cursors are + *

    + * To begin traversal over a trie, one must retrieve a cursor by calling {@link #cursor}. Because cursors are * stateful, the traversal must always proceed from one thread. Should concurrent reads be required, separate calls to - * {@link #cursor()} must be made. Any modification that has completed before the construction of a cursor must be + * {@link #cursor} must be made. Any modification that has completed before the construction of a cursor must be * visible, but any later concurrent modifications may be presented fully, partially or not at all; this also means that * if multiple are made, the cursor may see any part of any subset of them. - * + *

    * Note: This model only supports depth-first traversals. We do not currently have a need for breadth-first walks. - * + *

    * See Trie.md for further description of the trie representation model. * * @param The content type of the trie. @@ -61,33 +62,38 @@ public abstract class Trie { /** * A trie cursor. - * + *

    * This is the internal representation of the trie, which enables efficient walks and basic operations (merge, * slice) on tries. - * - * The cursor represents the state of a walk over the nodes of trie. It provides three main features: - * - the current "depth" or descend-depth in the trie; - * - the "incomingTransition", i.e. the byte that was used to reach the current point; - * - the "content" associated with the current node, + *

    + * The cursor represents the state of a walk over the nodes of trie. It provides three main features:

      + *
    • the current {@code depth} or descend-depth in the trie;
    • + *
    • the {@code incomingTransition}, i.e. the byte that was used to reach the current point;
    • + *
    • the {@code content} associated with the current node,
    • + *
    * and provides methods for advancing to the next position. This is enough information to extract all paths, and * also to easily compare cursors over different tries that are advanced together. Advancing is always done in * order; if one imagines the set of nodes in the trie with their associated paths, a cursor may only advance from a - * node with a lexicographically smaller path to one with bigger. The "advance" operation moves to the immediate - * next, it is also possible to skip over some items e.g. all children of the current node ("skipChildren"). - * - * Moving to the immediate next position in the lexicographic order is accomplished by: - * - if the current node has children, moving to its first child; - * - otherwise, ascend the parent chain and return the next child of the closest parent that still has any. + * node with a lexicographically smaller path to one with bigger. The {@code advance} operation moves to the immediate + * next, it is also possible to skip over some items e.g. all children of the current node ({@code skipChildren}). + *

    + * Moving to the immediate next position in the lexicographic order is accomplished by:

      + *
    • if the current node has children, moving to its first child;
    • + *
    • otherwise, ascend the parent chain and return the next child of the closest parent that still has any.
    • + *
    * As long as the trie is not exhausted, advancing always takes one step down, from the current node, or from a node - * on the parent chain. By comparing the new depth (which "advance" also returns) with the one before the advance, - * one can tell if the former was the case (if newDepth == oldDepth + 1) and how many steps up we had to take - * (oldDepth + 1 - newDepth). When following a path down, the cursor will stop on all prefixes. - * - * When it is created the cursor is placed on the root node with depth() = 0, incomingTransition() = -1. Since - * tries can have mappings for empty, content() can possibly be non-null. It is not allowed for a cursor to start - * in exhausted state (i.e. with depth() = -1). - * - * For example, the following trie: + * on the parent chain. By comparing the new depth (which {@code advance} also returns) with the one before the advance, + * one can tell if the former was the case (if {@code newDepth == oldDepth + 1}) and how many steps up we had to take + * ({@code oldDepth + 1 - newDepth}). When following a path down, the cursor will stop on all prefixes. + *

    + * When it is created the cursor is placed on the root node with {@code depth() = 0}, {@code incomingTransition() = -1}. + * Since tries can have mappings for empty, content() can possibly be non-null. The cursor is exhausted when it + * returns a depth of -1 (the operations that advance a cursor return the depth, and {@code depth()} will also + * return -1 if queried afterwards). It is not allowed for a cursor to start in exhausted state; once a cursor is + * exhausted, calling any of the advance methods or {@code tailTrie} is an error. + *

    + * For example, the following trie:
    + *

          *  t
          *   r
          *    e
    @@ -98,17 +104,20 @@ public abstract class Trie
          *  w
          *   i
          *    n  *
    -     * has nodes reachable with the paths
    -     *  "", t, tr, tre, tree*, tri, trie*, trip*, w, wi, win*
    -     * and the cursor will list them with the following (depth, incomingTransition) pairs:
    -     *  (0, -1), (1, t), (2, r), (3, e), (4, e)*, (3, i), (4, e)*, (4, p)*, (1, w), (2, i), (3, n)*
    -     *
    +     * 
    + * has nodes reachable with the paths
    + *   "", t, tr, tre, tree*, tri, trie*, trip*, w, wi, win*
    + * and the cursor will list them with the following {@code (depth, incomingTransition)} pairs:
    + *   (0, -1), (1, t), (2, r), (3, e), (4, e)*, (3, i), (4, e)*, (4, p)*, (1, w), (2, i), (3, n)* + *

    * Because we exhaust transitions on bigger depths before we go the next transition on the smaller ones, when - * cursors are advanced together their positions can be easily compared using only the depth and incomingTransition: - * - one that is higher in depth is before one that is lower; - * - for equal depths, the one with smaller incomingTransition is first. - * - * If we consider walking the trie above in parallel with this: + * cursors are advanced together their positions can be easily compared using only the {@code depth} and + * {@code incomingTransition}:

      + *
    • one that is higher in depth is before one that is lower;
    • + *
    • for equal depths, the one with smaller incomingTransition is first.
    • + *
    + * If we consider walking the trie above in parallel with this:
    + *
          *  t
          *   r
          *    i
    @@ -116,23 +125,31 @@ public abstract class Trie
          *      k *
          *  u
          *   p *
    -     * the combined iteration will proceed as follows:
    -     *  (0, -1)+    (0, -1)+               cursors equal, advance both
    -     *  (1, t)+     (1, t)+        t       cursors equal, advance both
    -     *  (2, r)+     (2, r)+        tr      cursors equal, advance both
    -     *  (3, e)+  <  (3, i)         tre     cursors not equal, advance smaller (3 = 3, e < i)
    -     *  (4, e)+  <  (3, i)         tree*   cursors not equal, advance smaller (4 > 3)
    -     *  (3, i)+     (3, i)+        tri     cursors equal, advance both
    -     *  (4, e)   >  (4, c)+        tric    cursors not equal, advance smaller (4 = 4, e > c)
    -     *  (4, e)   >  (5, k)+        trick*  cursors not equal, advance smaller (4 < 5)
    -     *  (4, e)+  <  (1, u)         trie*   cursors not equal, advance smaller (4 > 1)
    -     *  (4, p)+  <  (1, u)         trip*   cursors not equal, advance smaller (4 > 1)
    -     *  (1, w)   >  (1, u)+        u       cursors not equal, advance smaller (1 = 1, w > u)
    -     *  (1, w)   >  (2, p)+        up*     cursors not equal, advance smaller (1 < 2)
    -     *  (1, w)+  <  (-1, -1)       w       cursors not equal, advance smaller (1 > -1)
    -     *  (2, i)+  <  (-1, -1)       wi      cursors not equal, advance smaller (2 > -1)
    -     *  (3, n)+  <  (-1, -1)       win*    cursors not equal, advance smaller (3 > -1)
    -     *  (-1, -1)    (-1, -1)               both exhasted
    +     * 
    + * the combined iteration will proceed as follows:
    +     *  (0, -1)+  (0, -1)+          cursors equal, advance both
    +     *  (1, t)+   (1, t)+   t       cursors equal, advance both
    +     *  (2, r)+   (2, r)+   tr      cursors equal, advance both
    +     *  (3, e)+ < (3, i)    tre     cursors not equal, advance smaller (3 = 3, e < i)
    +     *  (4, e)+ < (3, i)    tree*   cursors not equal, advance smaller (4 > 3)
    +     *  (3, i)+   (3, i)+   tri     cursors equal, advance both
    +     *  (4, e)  > (4, c)+   tric    cursors not equal, advance smaller (4 = 4, e > c)
    +     *  (4, e)  > (5, k)+   trick*  cursors not equal, advance smaller (4 < 5)
    +     *  (4, e)+ < (1, u)    trie*   cursors not equal, advance smaller (4 > 1)
    +     *  (4, p)+ < (1, u)    trip*   cursors not equal, advance smaller (4 > 1)
    +     *  (1, w)  > (1, u)+   u       cursors not equal, advance smaller (1 = 1, w > u)
    +     *  (1, w)  > (2, p)+   up*     cursors not equal, advance smaller (1 < 2)
    +     *  (1, w)+ < (-1, -1)  w       cursors not equal, advance smaller (1 > -1)
    +     *  (2, i)+ < (-1, -1)  wi      cursors not equal, advance smaller (2 > -1)
    +     *  (3, n)+ < (-1, -1)  win*    cursors not equal, advance smaller (3 > -1)
    +     *  (-1, -1)  (-1, -1)          both exhasted
    +     *  
    + *

    + * Cursors are created with a direction (forward or reverse), which specifies the order in which a node's children + * are iterated (smaller first or larger first). Note that entries returned in reverse direction are in + * lexicographic order for the inverted alphabet, which is not the same as being presented in reverse. For example, + * a cursor for a trie containing "ab", "abc" and "cba", will visit the nodes in order "cba", "ab", "abc", i.e. + * prefixes will still be reported before their descendants. */ protected interface Cursor { @@ -154,13 +171,23 @@ protected interface Cursor */ T content(); + /** + * Returns the direction in which this cursor is progressing. + */ + Direction direction(); + + /** + * Returns the byte-comparable version that this trie uses. + */ + ByteComparable.Version byteComparableVersion(); + /** * Advance one position to the node whose associated path is next lexicographically. - * This can be either: - * - descending one level to the first child of the current node, - * - ascending to the closest parent that has remaining children, and then descending one level to its next + * This can be either:

      + *
    • descending one level to the first child of the current node, + *
    • ascending to the closest parent that has remaining children, and then descending one level to its next * child. - * + *
    * It is an error to call this after the trie has already been exhausted (i.e. when depth() == -1); * for performance reasons we won't always check this. * @@ -173,11 +200,11 @@ protected interface Cursor * (e.g. when positioned on a chain node in a memtable trie). If the current node does not have children this * is exactly the same as advance(), otherwise it may take multiple steps down (but will not necessarily, even * if they exist). - * + *

    * Note that if any positions are skipped, their content must be null. - * + *

    * This is an optional optimization; the default implementation falls back to calling advance. - * + *

    * It is an error to call this after the trie has already been exhausted (i.e. when depth() == -1); * for performance reasons we won't always check this. * @@ -192,7 +219,7 @@ default int advanceMultiple(TransitionsReceiver receiver) /** * Advance all the way to the next node with non-null content. - * + *

    * It is an error to call this after the trie has already been exhausted (i.e. when depth() == -1); * for performance reasons we won't always check this. * @@ -221,18 +248,45 @@ default T advanceToContent(ResettingTransitionsReceiver receiver) } /** - * Ignore the current node's children and advance to the next child of the closest node on the parent chain that - * has any. - * - * It is an error to call this after the trie has already been exhausted (i.e. when depth() == -1); - * for performance reasons we won't always check this. + * Advance to the specified depth and incoming transition or the first valid position that is after the specified + * position. The inputs must be something that could be returned by a single call to {@link #advance} (i.e. + * {@code depth} must be <= current depth + 1, and {@code incomingTransition} must be higher than what the + * current state saw at the requested depth. * * @return the new depth, always <= previous depth; -1 if the trie is exhausted */ - int skipChildren(); + int skipTo(int skipDepth, int skipTransition); + + /** + * Descend into the cursor with the given path. + * + * @return True if the descent is positioned at the end of the given path, false if the trie did not have a path + * for it. In the latter case the cursor is positioned at the first node that follows the given key in iteration + * order. + */ + default boolean descendAlong(ByteSource bytes) + { + int next = bytes.next(); + int depth = depth(); + while (next != ByteSource.END_OF_STREAM) + { + if (skipTo(++depth, next) != depth || incomingTransition() != next) + return false; + next = bytes.next(); + } + return true; + } + + /** + * Returns a tail trie, i.e. a trie whose root is the current position. Walking a tail trie will list all + * descendants of the current position with depth adjusted by the current depth. + *

    + * It is an error to call tailTrie on an exhausted cursor. + */ + Trie tailTrie(); } - protected abstract Cursor cursor(); + protected abstract Cursor cursor(Direction direction); /** * Used by {@link Cursor#advanceMultiple} to feed the transitions taken. @@ -269,9 +323,6 @@ protected interface Walker extends ResettingTransitionsReceiver R complete(); } - // Version of the byte comparable conversion to use for all operations - protected static final ByteComparable.Version BYTE_COMPARABLE_VERSION = ByteComparable.Version.OSS50; - /** * Adapter interface providing the methods a {@link Walker} to a {@link Consumer}, so that the latter can be used * with {@link #process}. @@ -318,15 +369,24 @@ default void addPathBytes(DirectBuffer buffer, int pos, int count) */ public void forEachValue(ValueConsumer consumer) { - process(consumer); + process(consumer, Direction.FORWARD); } /** * Call the given consumer on all (path, content) pairs with non-null content in the trie in order. */ - public void forEachEntry(BiConsumer consumer) + public void forEachEntry(BiConsumer consumer) { - process(new TrieEntriesWalker.WithConsumer(consumer)); + forEachEntry(Direction.FORWARD, consumer); + } + + /** + * Call the given consumer on all (path, content) pairs with non-null content in the trie in order. + */ + public void forEachEntry(Direction direction, BiConsumer consumer) + { + Cursor cursor = cursor(direction); + process(new TrieEntriesWalker.WithConsumer(consumer, cursor.byteComparableVersion()), cursor); // Note: we can't do the ValueConsumer trick here, because the implementation requires state and cannot be // implemented with default methods alone. } @@ -334,9 +394,9 @@ public void forEachEntry(BiConsumer consumer) /** * Process the trie using the given Walker. */ - public R process(Walker walker) + public R process(Walker walker, Direction direction) { - return process(walker, cursor()); + return process(walker, cursor(direction)); } static R process(Walker walker, Cursor cursor) @@ -354,6 +414,72 @@ static R process(Walker walker, Cursor cursor) return walker.complete(); } + + /** + * Process the trie using the given ValueConsumer, skipping all branches below the top content-bearing node. + */ + public Void forEachValueSkippingBranches(Direction direction, ValueConsumer consumer) + { + return processSkippingBranches(consumer, cursor(direction)); + } + + /** + * Call the given consumer on all (path, content) pairs with non-null content in the trie in order, skipping all + * branches below the top content-bearing node. + */ + public void forEachEntrySkippingBranches(Direction direction, BiConsumer consumer) + { + Cursor cursor = cursor(direction); + processSkippingBranches(new TrieEntriesWalker.WithConsumer(consumer, cursor.byteComparableVersion()), cursor); + // Note: we can't do the ValueConsumer trick here, because the implementation requires state and cannot be + // implemented with default methods alone. + } + + /** + * Process the trie using the given Walker, skipping all branches below the top content-bearing node. + */ + public R processSkippingBranches(Walker walker, Direction direction) + { + return processSkippingBranches(walker, cursor(direction)); + } + + static R processSkippingBranches(Walker walker, Cursor cursor) + { + assert cursor.depth() == 0 : "The provided cursor has already been advanced."; + T content = cursor.content(); // handle content on the root node + if (content != null) + { + walker.content(content); + return walker.complete(); + } + content = cursor.advanceToContent(walker); + + while (content != null) + { + walker.content(content); + if (cursor.skipTo(cursor.depth(), cursor.incomingTransition() + cursor.direction().increase) < 0) + break; + walker.resetPathLength(cursor.depth() - 1); + walker.addPathByte(cursor.incomingTransition()); + content = cursor.content(); + if (content == null) + content = cursor.advanceToContent(walker); + } + return walker.complete(); + } + + /** + * Map-like get by key. + */ + public T get(ByteComparable key) + { + Cursor cursor = cursor(Direction.FORWARD); + if (cursor.descendAlong(key.asComparableBytes(cursor.byteComparableVersion()))) + return cursor.content(); + else + return null; + } + /** * Constuct a textual representation of the trie. */ @@ -367,15 +493,15 @@ public String dump() */ public String dump(Function contentToString) { - return process(new TrieDumper<>(contentToString)); + return process(new TrieDumper<>(contentToString), Direction.FORWARD); } /** * Returns a singleton trie mapping the given byte path to content. */ - public static Trie singleton(ByteComparable b, T v) + public static Trie singleton(ByteComparable b, ByteComparable.Version byteComparableVersion, T v) { - return new SingletonTrie<>(b, v); + return new SingletonTrie<>(b, byteComparableVersion, v); } /** @@ -400,19 +526,15 @@ public Trie subtrie(ByteComparable left, boolean includeLeft, ByteComparable } /** - * Returns a view of the subtrie containing everything in this trie whose keys fall between the given boundaries, - * left-inclusive and right-exclusive. + * Returns a view of the subtrie containing everything in this trie whose keys fall between the given boundaries. * The view is live, i.e. any write to the source will be reflected in the subtrie. * - * This method will not check its arguments for correctness. The resulting trie may be empty or throw an exception - * if the right bound is smaller than the left. - * - * Equivalent to calling subtrie(left, true, right, false). - * - * @param left the left bound for the returned subtrie. If {@code null}, the resulting subtrie is not left-bounded. - * @param right the right bound for the returned subtrie. If {@code null}, the resulting subtrie is not right-bounded. - * @return a view of the subtrie containing all the keys of this trie falling between {@code left} (inclusively if - * {@code includeLeft}) and {@code right} (inclusively if {@code includeRight}). + * @param left the left bound for the returned subtrie, inclusive. If {@code null}, the resulting subtrie is not + * left-bounded. + * @param right the right bound for the returned subtrie, exclusive. If {@code null}, the resulting subtrie is not + * right-bounded. + * @return a view of the subtrie containing all the keys of this trie falling between {@code left} inclusively and + * {@code right} exclusively. */ public Trie subtrie(ByteComparable left, ByteComparable right) { @@ -422,17 +544,57 @@ public Trie subtrie(ByteComparable left, ByteComparable right) /** * Returns the ordered entry set of this trie's content as an iterable. */ - public Iterable> entrySet() + public Iterable> entrySet() { return this::entryIterator; } + /** + * Returns the ordered entry set of this trie's content as an iterable. + */ + public Iterable> entrySet(Direction direction) + { + return () -> entryIterator(direction); + } + + /** + * Returns the ordered entry set of this trie's content in an iterator. + */ + public Iterator> entryIterator() + { + return entryIterator(Direction.FORWARD); + } + /** * Returns the ordered entry set of this trie's content in an iterator. */ - public Iterator> entryIterator() + public Iterator> entryIterator(Direction direction) { - return new TrieEntriesIterator.AsEntries<>(this); + return new TrieEntriesIterator.AsEntries<>(cursor(direction)); + } + + /** + * Returns the ordered entry set of this trie's content in an iterable, filtered by the given type. + */ + public Iterable> filteredEntrySet(Class clazz) + { + return filteredEntrySet(Direction.FORWARD, clazz); + } + + /** + * Returns the ordered entry set of this trie's content in an iterable, filtered by the given type. + */ + public Iterable> filteredEntrySet(Direction direction, Class clazz) + { + return () -> filteredEntryIterator(direction, clazz); + } + + /** + * Returns the ordered entry set of this trie's content in an iterator, filtered by the given type. + */ + public Iterator> filteredEntryIterator(Direction direction, Class clazz) + { + return new TrieEntriesIterator.AsEntriesFilteredByType<>(cursor(direction), clazz); } /** @@ -443,12 +605,60 @@ public Iterable values() return this::valueIterator; } + /** + * Returns the ordered set of values of this trie as an iterable. + */ + public Iterable values(Direction direction) + { + return direction.isForward() ? this::valueIterator : this::reverseValueIterator; + } + /** * Returns the ordered set of values of this trie in an iterator. */ public Iterator valueIterator() { - return new TrieValuesIterator<>(this); + return valueIterator(Direction.FORWARD); + } + + /** + * Returns the inversely ordered set of values of this trie in an iterator. + */ + public Iterator reverseValueIterator() + { + return valueIterator(Direction.REVERSE); + } + + /** + * Returns the ordered set of values of this trie in an iterator. + */ + public Iterator valueIterator(Direction direction) + { + return new TrieValuesIterator<>(cursor(direction)); + } + + /** + * Returns the ordered set of values of this trie in an iterable, filtered by the given type. + */ + public Iterable filteredValues(Class clazz) + { + return filteredValues(Direction.FORWARD, clazz); + } + + /** + * Returns the ordered set of values of this trie in an iterable, filtered by the given type. + */ + public Iterable filteredValues(Direction direction, Class clazz) + { + return () -> filteredValuesIterator(direction, clazz); + } + + /** + * Returns the ordered set of values of this trie in an iterator, filtered by the given type. + */ + public Iterator filteredValuesIterator(Direction direction, Class clazz) + { + return new TrieValuesIterator.FilteredByType<>(cursor(direction), clazz); } /** @@ -537,7 +747,7 @@ public static Trie merge(Collection> sources, Collectio switch (sources.size()) { case 0: - return empty(); + throw new AssertionError(); case 1: return sources.iterator().next(); case 2: @@ -563,7 +773,7 @@ public static Trie mergeDistinct(Collection> sources) switch (sources.size()) { case 0: - return empty(); + throw new AssertionError(); case 1: return sources.iterator().next(); case 2: @@ -578,45 +788,107 @@ public static Trie mergeDistinct(Collection> sources) } } - private static final Trie EMPTY = new Trie() + /** + * Returns a Trie that is a view of this one, where the given prefix is prepended before the root. + */ + public Trie prefixedBy(ByteComparable prefix) + { + return new PrefixedTrie(prefix, this); + } + + /** + * Returns an entry set containing all tail tree constructed at the points that contain content of + * the given type. + */ + public Iterable>> tailTries(Direction direction, Class clazz) + { + return () -> new TrieTailsIterator.AsEntries<>(cursor(direction), clazz); + } + + /** + * Returns a trie that corresponds to the branch of this trie rooted at the given prefix. + *

    + * The result will include the same values as {@code subtrie(prefix, nextBranch(prefix))}, but the keys in the + * resulting trie will not include the prefix. In other words, + * {@code tailTrie(prefix).prefixedBy(prefix) = subtrie(prefix, nextBranch(prefix))} + * where nextBranch stands for the key adjusted by adding one at the last position. + */ + public Trie tailTrie(ByteComparable prefix) { - protected Cursor cursor() + Cursor c = cursor(Direction.FORWARD); + if (c.descendAlong(prefix.asComparableBytes(c.byteComparableVersion()))) + return c.tailTrie(); + else + return null; + } + + public static Trie empty(ByteComparable.Version byteComparableVersion) + { + return new Trie() { - return new Cursor() + public Cursor cursor(Direction dir) { - int depth = 0; + return new EmptyCursor<>(dir, byteComparableVersion); + } + }; + } - public int advance() - { - return depth = -1; - } + static class EmptyCursor implements Cursor + { + private final Direction direction; + private final ByteComparable.Version byteComparableVersion; + int depth; - public int skipChildren() - { - return depth = -1; - } + public EmptyCursor(Direction direction, ByteComparable.Version byteComparableVersion) + { + this.direction = direction; + this.byteComparableVersion = byteComparableVersion; + depth = 0; + } - public int depth() - { - return depth; - } + public int advance() + { + return depth = -1; + } - public Object content() - { - return null; - } + public int skipTo(int skipDepth, int skipTransition) + { + return depth = -1; + } - public int incomingTransition() - { - return -1; - } - }; + public ByteComparable.Version byteComparableVersion() + { + if (byteComparableVersion != null) + return byteComparableVersion; + throw new AssertionError(); } - }; - @SuppressWarnings("unchecked") - public static Trie empty() - { - return (Trie) EMPTY; + @Override + public Trie tailTrie() + { + assert depth == 0 : "tailTrie called on exhausted cursor"; + return empty(byteComparableVersion); + } + + public int depth() + { + return depth; + } + + public T content() + { + return null; + } + + @Override + public Direction direction() + { + return direction; + } + + public int incomingTransition() + { + return -1; + } } } diff --git a/src/java/org/apache/cassandra/db/tries/Trie.md b/src/java/org/apache/cassandra/db/tries/Trie.md index 4265871e7b9b..a482d7dc4a80 100644 --- a/src/java/org/apache/cassandra/db/tries/Trie.md +++ b/src/java/org/apache/cassandra/db/tries/Trie.md @@ -248,5 +248,15 @@ as soon as the source becomes larger than the right bound. implicit representation using a pair of `depth` and `incomingTransition` for each bound. In slices we can also use `advanceMultiple` when we are certain to be strictly inside the slice, i.e. beyond the -left bound and before a prefix of the right bound. As above, descending to any depth in this case is safe as the -result will remain smaller than the right bound. \ No newline at end of file +left bound and before the right bound. As above, descending to any depth in this case is safe as the +result will remain smaller than the right bound. + +## Reverse iteration + +Tries and trie cursors support reverse iteration. Reverse trie iteration presents data in lexicographic order +using the inverted alphabet. This is not always the same as the reverse order of the data returned in the forward +direction; the latter is only guaranteed if the entries in the trie can contain no prefixes (i.e. the representation +is prefix-free like the byte-ordered type translations). + +This difference is imposed by the cursor interfaces which necessarily have to present parent nodes before their +children and do not preserve or present any state on ascent. diff --git a/src/java/org/apache/cassandra/db/tries/TrieEntriesIterator.java b/src/java/org/apache/cassandra/db/tries/TrieEntriesIterator.java index 7ab3e7de4628..99e3f764244d 100644 --- a/src/java/org/apache/cassandra/db/tries/TrieEntriesIterator.java +++ b/src/java/org/apache/cassandra/db/tries/TrieEntriesIterator.java @@ -20,6 +20,9 @@ import java.util.AbstractMap; import java.util.Iterator; import java.util.Map; +import java.util.function.Predicate; + +import com.google.common.base.Predicates; import org.apache.cassandra.utils.bytecomparable.ByteComparable; @@ -30,23 +33,33 @@ public abstract class TrieEntriesIterator extends TriePathReconstructor implements Iterator { private final Trie.Cursor cursor; + private final Predicate predicate; T next; boolean gotNext; - protected TrieEntriesIterator(Trie trie) + protected TrieEntriesIterator(Trie trie, Direction direction, Predicate predicate) { - cursor = trie.cursor(); + this(trie.cursor(direction), predicate); + } + + TrieEntriesIterator(Trie.Cursor cursor, Predicate predicate) + { + this.cursor = cursor; + this.predicate = predicate; assert cursor.depth() == 0; next = cursor.content(); - gotNext = next != null; + gotNext = next != null && predicate.test(next); } public boolean hasNext() { - if (!gotNext) + while (!gotNext) { next = cursor.advanceToContent(this); - gotNext = true; + if (next != null) + gotNext = predicate.test(next); + else + gotNext = true; } return next != null; @@ -54,33 +67,59 @@ public boolean hasNext() public V next() { + if (!hasNext()) + throw new IllegalStateException("next without hasNext"); + gotNext = false; T v = next; next = null; return mapContent(v, keyBytes, keyPos); } + ByteComparable.Version byteComparableVersion() + { + return cursor.byteComparableVersion(); + } + protected abstract V mapContent(T content, byte[] bytes, int byteLength); /** * Iterator representing the content of the trie a sequence of (path, content) pairs. */ - static class AsEntries extends TrieEntriesIterator> + static class AsEntries extends TrieEntriesIterator> + { + public AsEntries(Trie.Cursor cursor) + { + super(cursor, Predicates.alwaysTrue()); + } + + @Override + protected Map.Entry mapContent(T content, byte[] bytes, int byteLength) + { + return toEntry(byteComparableVersion(), content, bytes, byteLength); + } + } + + /** + * Iterator representing the content of the trie a sequence of (path, content) pairs. + */ + static class AsEntriesFilteredByType extends TrieEntriesIterator> { - public AsEntries(Trie trie) + public AsEntriesFilteredByType(Trie.Cursor cursor, Class clazz) { - super(trie); + super(cursor, clazz::isInstance); } @Override - protected Map.Entry mapContent(T content, byte[] bytes, int byteLength) + @SuppressWarnings("unchecked") // checked by the predicate + protected Map.Entry mapContent(T content, byte[] bytes, int byteLength) { - return toEntry(content, bytes, byteLength); + return toEntry(byteComparableVersion(), (U) content, bytes, byteLength); } } - static java.util.Map.Entry toEntry(T content, byte[] bytes, int byteLength) + static java.util.Map.Entry toEntry(ByteComparable.Version version, T content, byte[] bytes, int byteLength) { - return new AbstractMap.SimpleImmutableEntry<>(toByteComparable(bytes, byteLength), content); + return new AbstractMap.SimpleImmutableEntry<>(toByteComparable(version, bytes, byteLength), content); } } diff --git a/src/java/org/apache/cassandra/db/tries/TrieEntriesWalker.java b/src/java/org/apache/cassandra/db/tries/TrieEntriesWalker.java index ca06015733e0..362fe8f112b7 100644 --- a/src/java/org/apache/cassandra/db/tries/TrieEntriesWalker.java +++ b/src/java/org/apache/cassandra/db/tries/TrieEntriesWalker.java @@ -40,17 +40,19 @@ public void content(T content) */ static class WithConsumer extends TrieEntriesWalker { - private final BiConsumer consumer; + private final BiConsumer consumer; + private final ByteComparable.Version byteComparableVersion; - public WithConsumer(BiConsumer consumer) + public WithConsumer(BiConsumer consumer, ByteComparable.Version byteComparableVersion) { this.consumer = consumer; + this.byteComparableVersion = byteComparableVersion; } @Override protected void content(T content, byte[] bytes, int byteLength) { - consumer.accept(toByteComparable(bytes, byteLength), content); + consumer.accept(toByteComparable(byteComparableVersion, bytes, byteLength), content); } @Override diff --git a/src/java/org/apache/cassandra/db/tries/TriePathReconstructor.java b/src/java/org/apache/cassandra/db/tries/TriePathReconstructor.java index 4a9883fa006a..c59d126fe272 100644 --- a/src/java/org/apache/cassandra/db/tries/TriePathReconstructor.java +++ b/src/java/org/apache/cassandra/db/tries/TriePathReconstructor.java @@ -49,8 +49,9 @@ public void resetPathLength(int newLength) keyPos = newLength; } - static ByteComparable toByteComparable(byte[] bytes, int byteLength) + static ByteComparable.Preencoded toByteComparable(ByteComparable.Version byteComparableVersion, byte[] bytes, int byteLength) { - return ByteComparable.fixedLength(Arrays.copyOf(bytes, byteLength)); + // Taking a copy here to make sure it does not get modified when the cursor advances. + return ByteComparable.preencoded(byteComparableVersion, Arrays.copyOf(bytes, byteLength)); } } diff --git a/src/java/org/apache/cassandra/db/tries/TrieSpaceExhaustedException.java b/src/java/org/apache/cassandra/db/tries/TrieSpaceExhaustedException.java new file mode 100644 index 000000000000..d355a467ef29 --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/TrieSpaceExhaustedException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.tries; + +/** + * Because we use buffers and 32-bit pointers, the trie cannot grow over 2GB of size. This exception is thrown if + * a trie operation needs it to grow over that limit. + *

    + * To avoid this problem, users should query {@link InMemoryTrie#reachedAllocatedSizeThreshold} from time to time. If + * the call returns true, they should switch to a new trie (e.g. by flushing a memtable) as soon as possible. The + * threshold is configurable, and is set by default to 10% under the 2GB limit to give ample time for the switch to + * happen. + */ +public class TrieSpaceExhaustedException extends Exception +{ + public TrieSpaceExhaustedException() + { + super("The hard 2GB limit on trie size has been exceeded"); + } +} diff --git a/src/java/org/apache/cassandra/db/tries/TrieTailsIterator.java b/src/java/org/apache/cassandra/db/tries/TrieTailsIterator.java new file mode 100644 index 000000000000..e15ce6548206 --- /dev/null +++ b/src/java/org/apache/cassandra/db/tries/TrieTailsIterator.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db.tries; + +import java.util.AbstractMap; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Predicate; + +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * Iterator of trie entries that constructs tail tries for the content-bearing branches that satisfy the given predicate + * and skips over the returned branches. + */ +public abstract class TrieTailsIterator extends TriePathReconstructor implements Iterator +{ + final Trie.Cursor cursor; + private final Predicate predicate; + private T next; + private boolean gotNext; + + protected TrieTailsIterator(Trie trie, Direction direction, Predicate predicate) + { + this.cursor = trie.cursor(direction); + this.predicate = predicate; + assert cursor.depth() == 0; + } + + TrieTailsIterator(Trie.Cursor cursor, Predicate predicate) + { + this.cursor = cursor; + this.predicate = predicate; + assert cursor.depth() == 0; + } + + public boolean hasNext() + { + if (!gotNext) + { + int depth = cursor.depth(); + if (depth > 0) + { + // if we are not just starting, we have returned a branch and must skip over it + depth = cursor.skipTo(depth, cursor.incomingTransition() + cursor.direction().increase); + if (depth < 0) + return false; + resetPathLength(depth - 1); + addPathByte(cursor.incomingTransition()); + } + + next = cursor.content(); + if (next != null) + gotNext = predicate.test(next); + + while (!gotNext) + { + next = cursor.advanceToContent(this); + if (next != null) + gotNext = predicate.test(next); + else + gotNext = true; + } + } + + return next != null; + } + + public V next() + { + gotNext = false; + T v = next; + next = null; + return mapContent(v, cursor.tailTrie(), keyBytes, keyPos); + } + + ByteComparable.Version byteComparableVersion() + { + return cursor.byteComparableVersion(); + } + + protected abstract V mapContent(T value, Trie tailTrie, byte[] bytes, int byteLength); + + /** + * Iterator representing the selected content of the trie a sequence of {@code (path, tail)} pairs, where + * {@code tail} is the branch of the trie rooted at the selected content node (reachable by following + * {@code path}). The tail trie will have the selected content at its root. + */ + static class AsEntries extends TrieTailsIterator>> + { + public AsEntries(Trie.Cursor cursor, Class clazz) + { + super(cursor, clazz::isInstance); + } + + @Override + protected Map.Entry> mapContent(T value, Trie tailTrie, byte[] bytes, int byteLength) + { + ByteComparable.Preencoded key = toByteComparable(byteComparableVersion(), bytes, byteLength); + return new AbstractMap.SimpleImmutableEntry<>(key, tailTrie); + } + } +} diff --git a/src/java/org/apache/cassandra/db/tries/TrieValuesIterator.java b/src/java/org/apache/cassandra/db/tries/TrieValuesIterator.java index 29d3642b2e60..0a99c3ff0b99 100644 --- a/src/java/org/apache/cassandra/db/tries/TrieValuesIterator.java +++ b/src/java/org/apache/cassandra/db/tries/TrieValuesIterator.java @@ -28,9 +28,9 @@ class TrieValuesIterator implements Iterator T next; boolean gotNext; - protected TrieValuesIterator(Trie trie) + protected TrieValuesIterator(Trie.Cursor cursor) { - cursor = trie.cursor(); + this.cursor = cursor; assert cursor.depth() == 0; next = cursor.content(); gotNext = next != null; @@ -49,9 +49,51 @@ public boolean hasNext() public T next() { + if (!hasNext()) + throw new IllegalStateException("next without hasNext"); + gotNext = false; T v = next; next = null; return v; } + + static class FilteredByType implements Iterator + { + private final Trie.Cursor cursor; + T next; + boolean gotNext; + Class clazz; + + FilteredByType(Trie.Cursor cursor, Class clazz) + { + this.cursor = cursor; + this.clazz = clazz; + assert cursor.depth() == 0; + next = cursor.content(); + gotNext = next != null && clazz.isInstance(next); + } + + public boolean hasNext() + { + while (!gotNext) + { + next = cursor.advanceToContent(null); + gotNext = next == null || clazz.isInstance(next); + } + + return next != null; + } + + public U next() + { + if (!hasNext()) + throw new IllegalStateException("next without hasNext"); + + gotNext = false; + T v = next; + next = null; + return (U) v; + } + } } diff --git a/src/java/org/apache/cassandra/db/view/TableViews.java b/src/java/org/apache/cassandra/db/view/TableViews.java index a8ca7b7f6e44..e4c4c217417b 100644 --- a/src/java/org/apache/cassandra/db/view/TableViews.java +++ b/src/java/org/apache/cassandra/db/view/TableViews.java @@ -17,7 +17,14 @@ */ package org.apache.cassandra.db.view; -import java.util.*; +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -27,11 +34,36 @@ import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.ReadQuery; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.WriteOptions; import org.apache.cassandra.db.commitlog.CommitLogPosition; -import org.apache.cassandra.db.filter.*; -import org.apache.cassandra.db.partitions.*; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.Token; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; @@ -123,7 +155,7 @@ public void truncateBlocking(CommitLogPosition replayAfter, long truncatedAt) for (ColumnFamilyStore viewCfs : allViewsCfs()) { viewCfs.discardSSTables(truncatedAt); - SystemKeyspace.saveTruncationRecord(viewCfs, truncatedAt, replayAfter); + SystemKeyspace.saveTruncationRecord(viewCfs.metadata.id, truncatedAt, replayAfter); } } @@ -140,7 +172,7 @@ public void removeByName(String viewName) * @param writeCommitLog whether we should write the commit log for the view updates. * @param baseComplete time from epoch in ms that the local base mutation was (or will be) completed */ - public void pushViewReplicaUpdates(PartitionUpdate update, boolean writeCommitLog, AtomicLong baseComplete) + public void pushViewReplicaUpdates(PartitionUpdate update, WriteOptions writeOptions, AtomicLong baseComplete) { assert update.metadata().id.equals(baseTableMetadata.id); @@ -167,7 +199,7 @@ public void pushViewReplicaUpdates(PartitionUpdate update, boolean writeCommitLo Keyspace.openAndGetStore(update.metadata()).metric.viewReadTime.update(nanoTime() - start, TimeUnit.NANOSECONDS); if (!mutations.isEmpty()) - StorageProxy.mutateMV(update.partitionKey().getKey(), mutations, writeCommitLog, baseComplete, requestTime); + StorageProxy.mutateMV(update.partitionKey().getKey(), mutations, writeOptions, baseComplete, requestTime); } @@ -428,7 +460,7 @@ private SinglePartitionReadCommand readExistingRowsCommand(PartitionUpdate updat NavigableSet> names; try (BTree.FastBuilder> namesBuilder = sliceBuilder == null ? BTree.fastBuilder() : null) { - for (Row row : updates) + for (Row row : updates.rows()) { // Don't read the existing state if we can prove the update won't affect any views if (!affectsAnyViews(key, row, views)) diff --git a/src/java/org/apache/cassandra/db/view/View.java b/src/java/org/apache/cassandra/db/view/View.java index 30bad17b3460..4784b1a110b0 100644 --- a/src/java/org/apache/cassandra/db/view/View.java +++ b/src/java/org/apache/cassandra/db/view/View.java @@ -17,19 +17,27 @@ */ package org.apache.cassandra.db.view; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; - import javax.annotation.Nullable; import com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.Constants; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.selection.RawSelector; import org.apache.cassandra.cql3.selection.Selectable; +import org.apache.cassandra.cql3.statements.SelectOptions; import org.apache.cassandra.cql3.statements.SelectStatement; -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.ReadQuery; +import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; @@ -37,8 +45,6 @@ import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.FBUtilities; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A View copies data from a base table into a view table which can be queried independently from the @@ -150,7 +156,8 @@ public boolean mayBeAffectedBy(DecoratedKey partitionKey, Row update) public boolean matchesViewFilter(DecoratedKey partitionKey, Row baseRow, long nowInSec) { return getReadQuery().selectsClustering(partitionKey, baseRow.clustering()) - && getSelectStatement().rowFilterForInternalCalls().isSatisfiedBy(baseCfs.metadata(), partitionKey, baseRow, nowInSec); + && getSelectStatement().rowFilterForInternalCalls() + .isSatisfiedBy(baseCfs.metadata(), partitionKey, baseRow, nowInSec); } /** @@ -174,11 +181,13 @@ SelectStatement getSelectStatement() selectClause(), definition.whereClause, null, - null); + null, + null, + SelectOptions.EMPTY); rawSelect.setBindVariables(Collections.emptyList()); - select = rawSelect.prepare(ClientState.forInternalCalls(), true); + select = rawSelect.prepare(ClientState.forInternalCalls(), true, Constants.IDENTITY_STRING_MAPPER); } return select; diff --git a/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java b/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java index 9a72c1e270a2..392a3a820dca 100644 --- a/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java +++ b/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java @@ -41,10 +41,11 @@ import org.apache.cassandra.db.ReadQuery; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.compaction.CompactionInfo; -import org.apache.cassandra.db.compaction.CompactionInfo.Unit; +import org.apache.cassandra.db.WriteOptions; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.db.compaction.CompactionInterruptedException; import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; import org.apache.cassandra.db.rows.Rows; @@ -63,7 +64,7 @@ import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; -public class ViewBuilderTask extends CompactionInfo.Holder implements Callable +public class ViewBuilderTask extends AbstractTableOperation implements Callable { private static final Logger logger = LoggerFactory.getLogger(ViewBuilderTask.class); @@ -114,7 +115,7 @@ private void buildKey(DecoratedKey key) .generateViewUpdates(Collections.singleton(view), data, empty, nowInSec, true); AtomicLong noBase = new AtomicLong(Long.MAX_VALUE); - mutations.forEachRemaining(m -> StorageProxy.mutateMV(key.getKey(), m, true, noBase, Dispatcher.RequestTime.forImmediateExecution())); + mutations.forEachRemaining(m -> StorageProxy.mutateMV(key.getKey(), m, WriteOptions.FOR_VIEW_BUILD, noBase, Dispatcher.RequestTime.forImmediateExecution())); } } @@ -190,12 +191,12 @@ private void finish() // If it's stopped due to a compaction interruption we should throw that exception. // Otherwise we assume that the task has been stopped due to a schema update and we can finish successfully. if (isCompactionInterrupted) - throw new StoppedException(ksName, view.name, getCompactionInfo()); + throw new StoppedException(ksName, view.name, getProgress(), trigger()); } } @Override - public CompactionInfo getCompactionInfo() + public OperationProgress getProgress() { // we don't know the sstables at construction of ViewBuilderTask and we could change this to return once we know the // but since we basically only cancel view builds on truncation where we cancel all compactions anyway, this seems reasonable @@ -204,17 +205,17 @@ public CompactionInfo getCompactionInfo() if (range.left.getPartitioner().splitter().isPresent()) { long progress = prevToken == null ? 0 : Math.round(prevToken.getPartitioner().splitter().get().positionInRange(prevToken, range) * 1000); - return CompactionInfo.withoutSSTables(baseCfs.metadata(), OperationType.VIEW_BUILD, progress, 1000, Unit.RANGES, compactionId); + return OperationProgress.withoutSSTables(baseCfs.metadata(), OperationType.VIEW_BUILD, progress, 1000, Unit.RANGES, compactionId); } // When there is no splitter, estimate based on number of total keys but // take the max with keysBuilt + 1 to avoid having more completed than total long keysTotal = Math.max(keysBuilt + 1, baseCfs.estimatedKeysForRange(range)); - return CompactionInfo.withoutSSTables(baseCfs.metadata(), OperationType.VIEW_BUILD, keysBuilt, keysTotal, Unit.KEYS, compactionId); + return OperationProgress.withoutSSTables(baseCfs.metadata(), OperationType.VIEW_BUILD, keysBuilt, keysTotal, Unit.KEYS, compactionId); } @Override - public void stop() + public void stop(StopTrigger trigger) { stop(true); } @@ -247,9 +248,9 @@ static class StoppedException extends CompactionInterruptedException { private final String ksName, viewName; - private StoppedException(String ksName, String viewName, CompactionInfo info) + private StoppedException(String ksName, String viewName, OperationProgress info, TableOperation.StopTrigger trigger) { - super(info); + super(info, trigger); this.ksName = ksName; this.viewName = viewName; } diff --git a/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java b/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java index c49d0ceb0cf5..8b71e6b3dc4b 100644 --- a/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java +++ b/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java @@ -566,16 +566,22 @@ private void submitUpdate() return; DecoratedKey partitionKey = makeCurrentPartitionKey(); - // We can't really know which columns of the view will be updated nor how many row will be updated for this key - // so we rely on hopefully sane defaults. PartitionUpdate.Builder update = updates.computeIfAbsent(partitionKey, - k -> new PartitionUpdate.Builder(viewMetadata, - partitionKey, - viewMetadata.regularAndStaticColumns(), - 4)); + k -> builderFor(viewMetadata, partitionKey)); update.add(row); } + private static PartitionUpdate.Builder builderFor(TableMetadata viewMetadata, + DecoratedKey partitionKey) + { + // We can't really know which columns of the view will be updated nor how many row will be updated for this key + // so we rely on hopefully sane defaults. + return viewMetadata.params.memtable.factory.partitionUpdateFactory().builder(viewMetadata, + partitionKey, + viewMetadata.regularAndStaticColumns(), + 4); + } + private DecoratedKey makeCurrentPartitionKey() { ByteBuffer rawKey = viewMetadata.partitionKeyColumns().size() == 1 diff --git a/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java b/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java index 7044312eac8d..bf421af491ab 100644 --- a/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java +++ b/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java @@ -62,7 +62,7 @@ public final void apply(PartitionUpdate update) ColumnValues partitionKey = ColumnValues.from(metadata(), update.partitionKey()); if (update.deletionInfo().isLive()) - update.forEach(row -> + update.rowIterator().forEachRemaining(row -> { ColumnValues clusteringColumns = ColumnValues.from(metadata(), row.clustering()); diff --git a/src/java/org/apache/cassandra/db/virtual/CachesTable.java b/src/java/org/apache/cassandra/db/virtual/CachesTable.java index 5a265e63304a..b08b9fbdcfd4 100644 --- a/src/java/org/apache/cassandra/db/virtual/CachesTable.java +++ b/src/java/org/apache/cassandra/db/virtual/CachesTable.java @@ -21,6 +21,7 @@ import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.dht.LocalPartitioner; import org.apache.cassandra.metrics.CacheMetrics; +import org.apache.cassandra.metrics.ChunkCacheMetrics; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.CacheService; @@ -57,14 +58,27 @@ final class CachesTable extends AbstractVirtualTable private void addRow(SimpleDataSet result, String name, CacheMetrics metrics) { result.row(name) - .column(CAPACITY_BYTES, metrics.capacity.getValue()) - .column(SIZE_BYTES, metrics.size.getValue()) - .column(ENTRY_COUNT, metrics.entries.getValue()) - .column(REQUEST_COUNT, metrics.requests.getCount()) - .column(HIT_COUNT, metrics.hits.getCount()) - .column(HIT_RATIO, metrics.hitRate.getValue()) - .column(RECENT_REQUEST_RATE_PER_SECOND, (long) metrics.requests.getFifteenMinuteRate()) - .column(RECENT_HIT_RATE_PER_SECOND, (long) metrics.hits.getFifteenMinuteRate()); + .column(CAPACITY_BYTES, metrics.capacity()) + .column(SIZE_BYTES, metrics.size()) + .column(ENTRY_COUNT, metrics.entries()) + .column(REQUEST_COUNT, metrics.requests()) + .column(HIT_COUNT, metrics.hits()) + .column(HIT_RATIO, metrics.hitRate()) + .column(RECENT_REQUEST_RATE_PER_SECOND, (long) metrics.requestsFifteenMinuteRate()) + .column(RECENT_HIT_RATE_PER_SECOND, (long) metrics.hitFifteenMinuteRate()); + } + + private void addRow(SimpleDataSet result, String name, ChunkCacheMetrics metrics) + { + result.row(name) + .column(CAPACITY_BYTES, metrics.capacity()) + .column(SIZE_BYTES, metrics.size()) + .column(ENTRY_COUNT, metrics.entries()) + .column(REQUEST_COUNT, metrics.requests()) + .column(HIT_COUNT, metrics.hits()) + .column(HIT_RATIO, metrics.hitRate()) + .column(RECENT_REQUEST_RATE_PER_SECOND, metrics.requestsFifteenMinuteRate()) + .column(RECENT_HIT_RATE_PER_SECOND, metrics.hitFifteenMinuteRate()); } public DataSet data() diff --git a/src/java/org/apache/cassandra/db/virtual/SSTableTasksTable.java b/src/java/org/apache/cassandra/db/virtual/SSTableTasksTable.java index e2f38f8e9201..59064c56cfe5 100644 --- a/src/java/org/apache/cassandra/db/virtual/SSTableTasksTable.java +++ b/src/java/org/apache/cassandra/db/virtual/SSTableTasksTable.java @@ -17,8 +17,8 @@ */ package org.apache.cassandra.db.virtual; -import org.apache.cassandra.db.compaction.CompactionInfo; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.marshal.DoubleType; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.LongType; @@ -37,6 +37,7 @@ final class SSTableTasksTable extends AbstractVirtualTable private final static String PROGRESS = "progress"; private final static String SSTABLES = "sstables"; private final static String TOTAL = "total"; + private final static String TOTAL_COMPRESSED = "total_compressed"; private final static String UNIT = "unit"; private final static String TARGET_DIRECTORY = "target_directory"; @@ -54,6 +55,7 @@ final class SSTableTasksTable extends AbstractVirtualTable .addRegularColumn(PROGRESS, LongType.instance) .addRegularColumn(SSTABLES, Int32Type.instance) .addRegularColumn(TOTAL, LongType.instance) + .addRegularColumn(TOTAL_COMPRESSED, LongType.instance) .addRegularColumn(UNIT, UTF8Type.instance) .addRegularColumn(TARGET_DIRECTORY, UTF8Type.instance) .build()); @@ -63,22 +65,23 @@ public DataSet data() { SimpleDataSet result = new SimpleDataSet(metadata()); - for (CompactionInfo task : CompactionManager.instance.getSSTableTasks()) + for (TableOperation.Progress task : CompactionManager.instance.getSSTableTasks()) { - long completed = task.getCompleted(); - long total = task.getTotal(); + long completed = task.completed(); + long total = task.total(); double completionRatio = total == 0L ? 1.0 : (((double) completed) / total); - result.row(task.getKeyspace().orElse("*"), - task.getTable().orElse("*"), - task.getTaskId()) + result.row(task.keyspace().orElse("*"), + task.table().orElse("*"), + task.operationId()) .column(COMPLETION_RATIO, completionRatio) - .column(KIND, task.getTaskType().toString().toLowerCase()) + .column(KIND, task.operationType().toString().toLowerCase()) .column(PROGRESS, completed) - .column(SSTABLES, task.getSSTables().size()) + .column(SSTABLES, task.sstables().size()) .column(TOTAL, total) - .column(UNIT, task.getUnit().toString().toLowerCase()) + .column(UNIT, task.unit().toString().toLowerCase()) + .column(TOTAL_COMPRESSED, 0L) .column(TARGET_DIRECTORY, task.targetDirectory()); } diff --git a/src/java/org/apache/cassandra/db/virtual/SettingsTable.java b/src/java/org/apache/cassandra/db/virtual/SettingsTable.java index 526d7b538fe1..5e42db6532bd 100644 --- a/src/java/org/apache/cassandra/db/virtual/SettingsTable.java +++ b/src/java/org/apache/cassandra/db/virtual/SettingsTable.java @@ -111,15 +111,16 @@ else if (value instanceof Collection) else if (value instanceof Map) { Map map = new HashMap<>(); - for (Map.Entry entry : ((Map) value).entrySet()) + for (Map.Entry entry : ((Map) value).entrySet()) { + String key = String.valueOf(entry.getKey()); // this is done on best-effort basis as we do not have names in parameters // inherently under control as this is what a user is responsible for // when dealing with custom implementations - if (entry.getKey().endsWith("_password") || entry.getKey().equals("password")) - map.put(entry.getKey(), Redacted.REDACTED_STRING); + if (key.endsWith("_password") || key.equals("password")) + map.put(key, Redacted.REDACTED_STRING); else - map.put(entry.getKey(), entry.getValue()); + map.put(key, entry.getValue()); } return tryConstructJson(map); diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java index 7d6152bdc207..7622387a01c0 100644 --- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java +++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java @@ -19,8 +19,13 @@ import com.google.common.collect.ImmutableList; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.index.sai.virtual.IndexesSystemView; +import org.apache.cassandra.index.sai.virtual.SSTablesSystemView; import org.apache.cassandra.index.sai.virtual.StorageAttachedIndexTables; +import java.util.Collection; + import static org.apache.cassandra.schema.SchemaConstants.VIRTUAL_VIEWS; public final class SystemViewsKeyspace extends VirtualKeyspace @@ -29,8 +34,14 @@ public final class SystemViewsKeyspace extends VirtualKeyspace private SystemViewsKeyspace() { - super(VIRTUAL_VIEWS, new ImmutableList.Builder() - .add(new CachesTable(VIRTUAL_VIEWS)) + super(VIRTUAL_VIEWS, buildTables()); + } + + private static Collection buildTables() + { + ImmutableList.Builder tables = new ImmutableList.Builder<>(); + if (CassandraRelevantProperties.SYSTEM_VIEWS_INCLUDE_ALL.getBoolean()) + tables.add(new CachesTable(VIRTUAL_VIEWS)) .add(new ClientsTable(VIRTUAL_VIEWS)) .add(new SettingsTable(VIRTUAL_VIEWS)) .add(new SystemPropertiesTable(VIRTUAL_VIEWS)) @@ -39,6 +50,7 @@ private SystemViewsKeyspace() .add(new InternodeOutboundTable(VIRTUAL_VIEWS)) .add(new InternodeInboundTable(VIRTUAL_VIEWS)) .add(new PendingHintsTable(VIRTUAL_VIEWS)) + .add(new SSTablesSystemView(VIRTUAL_VIEWS)) .addAll(TableMetricTables.getAll(VIRTUAL_VIEWS)) .add(new CredentialsCacheKeysTable(VIRTUAL_VIEWS)) .add(new JmxPermissionsCacheKeysTable(VIRTUAL_VIEWS)) @@ -54,7 +66,11 @@ private SystemViewsKeyspace() .add(new SnapshotsTable(VIRTUAL_VIEWS)) .addAll(LocalRepairTables.getAll(VIRTUAL_VIEWS)) .addAll(CIDRFilteringMetricsTable.getAll(VIRTUAL_VIEWS)) - .addAll(StorageAttachedIndexTables.getAll(VIRTUAL_VIEWS)) - .build()); + .addAll(StorageAttachedIndexTables.getAll(VIRTUAL_VIEWS)); + if (CassandraRelevantProperties.SYSTEM_VIEWS_INCLUDE_ALL.getBoolean() + || CassandraRelevantProperties.SYSTEM_VIEWS_INCLUDE_INDEXES.getBoolean()) + tables.add(new IndexesSystemView(VIRTUAL_VIEWS)); + + return tables.build(); } } diff --git a/src/java/org/apache/cassandra/db/virtual/TableMetricTables.java b/src/java/org/apache/cassandra/db/virtual/TableMetricTables.java index 5528c92011cc..82fd34d45223 100644 --- a/src/java/org/apache/cassandra/db/virtual/TableMetricTables.java +++ b/src/java/org/apache/cassandra/db/virtual/TableMetricTables.java @@ -68,14 +68,16 @@ public class TableMetricTables public static Collection getAll(String name) { return ImmutableList.of( - new LatencyTableMetric(name, "local_read_latency", t -> t.readLatency.latency), - new LatencyTableMetric(name, "local_scan_latency", t -> t.rangeLatency.latency), - new LatencyTableMetric(name, "coordinator_read_latency", t -> t.coordinatorReadLatency), - new LatencyTableMetric(name, "coordinator_scan_latency", t -> t.coordinatorScanLatency), - new LatencyTableMetric(name, "local_write_latency", t -> t.writeLatency.latency), - new LatencyTableMetric(name, "coordinator_write_latency", t -> t.coordinatorWriteLatency), - new HistogramTableMetric(name, "tombstones_per_read", t -> t.tombstoneScannedHistogram.cf), - new HistogramTableMetric(name, "rows_per_read", t -> t.liveScannedHistogram.cf), + new LatencyTableMetric(name, "local_read_latency", t -> t.readLatency.tableOrKeyspaceMetric().latency), + new LatencyTableMetric(name, "local_scan_latency", t -> t.rangeLatency.tableOrKeyspaceMetric().latency), + new LatencyTableMetric(name, "coordinator_read_latency", t -> t.coordinatorReadLatency.tableOrKeyspaceTimer()), + new LatencyTableMetric(name, "coordinator_cas_read_latency", t -> t.coordinatorCasReadLatency.tableOrKeyspaceTimer()), + new LatencyTableMetric(name, "coordinator_scan_latency", t -> t.coordinatorScanLatency.tableOrKeyspaceTimer()), + new LatencyTableMetric(name, "local_write_latency", t -> t.writeLatency.tableOrKeyspaceMetric().latency), + new LatencyTableMetric(name, "coordinator_write_latency", t -> t.coordinatorWriteLatency.tableOrKeyspaceTimer()), + new LatencyTableMetric(name, "coordinator_cas_write_latency", t -> t.coordinatorCasWriteLatency.tableOrKeyspaceTimer()), + new HistogramTableMetric(name, "tombstones_per_read", t -> t.tombstoneScannedHistogram.tableOrKeyspaceHistogram()), + new HistogramTableMetric(name, "rows_per_read", t -> t.liveScannedHistogram.tableOrKeyspaceHistogram()), new StorageTableMetric(name, "disk_usage", (TableMetrics t) -> t.totalDiskSpaceUsed), new StorageTableMetric(name, "max_partition_size", (TableMetrics t) -> t.maxPartitionSize), new StorageTableMetric(name, "max_sstable_size", (TableMetrics t) -> t.maxSSTableSize), diff --git a/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java b/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java index 8c3b5b4afda6..ff66335f23a5 100644 --- a/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java +++ b/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java @@ -27,6 +27,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.schema.TableId; @@ -67,6 +68,12 @@ public String getKeyspaceName() return keyspaceName; } + @Override + public Keyspace getKeyspace() + { + return Keyspace.open(keyspaceName); + } + @Override public Collection getTableIds() { diff --git a/src/java/org/apache/cassandra/dht/AbstractBounds.java b/src/java/org/apache/cassandra/dht/AbstractBounds.java index 7a603b0a5dc0..e004e7a4ed42 100644 --- a/src/java/org/apache/cassandra/dht/AbstractBounds.java +++ b/src/java/org/apache/cassandra/dht/AbstractBounds.java @@ -55,6 +55,12 @@ public AbstractBounds(T left, T right) this.right = right; } + public static AbstractBounds unbounded(IPartitioner partitioner) + { + return bounds(partitioner.getMinimumToken().minKeyBound(), true, + partitioner.getMaximumToken().maxKeyBound(), true); + } + /** * Given token T and AbstractBounds ?L,R?, returns Pair(?L,T], (T,R?), * where ? means that the same type of AbstractBounds is returned as the original. @@ -231,9 +237,9 @@ public AbstractBounds deserialize(DataInput in, IPartitioner p, int version) public long serializedSize(AbstractBounds ab, int version) { // !WARNING! See serialize method above for why we still need to have that condition. - int size = version < MessagingService.VERSION_30 - ? TypeSizes.sizeof(kindInt(ab)) - : 1; + long size = version < MessagingService.VERSION_30 + ? TypeSizes.sizeof(kindInt(ab)) + : 1; size += serializer.serializedSize(ab.left, version); size += serializer.serializedSize(ab.right, version); return size; diff --git a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java index 43d4d317287f..93dd9f1c1f7e 100644 --- a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java +++ b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java @@ -141,6 +141,13 @@ public Token nextValidToken() throw new UnsupportedOperationException(String.format("Token type %s does not support token allocation.", getClass().getSimpleName())); } + + @Override + public Token prevValidToken() + { + throw new UnsupportedOperationException(String.format("Token type %s does not support token allocation.", + getClass().getSimpleName())); + } } public BytesToken getToken(ByteBuffer key) diff --git a/src/java/org/apache/cassandra/dht/ComparableObjectToken.java b/src/java/org/apache/cassandra/dht/ComparableObjectToken.java index 98e4017342df..15052bea6d3a 100644 --- a/src/java/org/apache/cassandra/dht/ComparableObjectToken.java +++ b/src/java/org/apache/cassandra/dht/ComparableObjectToken.java @@ -80,4 +80,11 @@ public Token nextValidToken() throw new UnsupportedOperationException(String.format("Token type %s does not support token allocation.", getClass().getSimpleName())); } + + @Override + public Token prevValidToken() + { + throw new UnsupportedOperationException(String.format("Token type %s does not support token allocation.", + getClass().getSimpleName())); + } } diff --git a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java index e2371c09376d..f0464218ea1b 100644 --- a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java +++ b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java @@ -221,7 +221,7 @@ public double size(Token next) @Override public LongToken nextValidToken() { - return new LongToken(token + 1); + return new LongToken(token + 1); // wraparound to MINIMUM if token is MAXIMUM } public LongToken decreaseSlightly() @@ -234,6 +234,12 @@ public static ByteBuffer keyForToken(long token) return keyForToken(new LongToken(token)); } + @Override + public Token prevValidToken() + { + return new LongToken(token - 1); // wraparound to MAXIMUM if token is MINIMUM + } + /** * Reverses murmur3 to find a possible 16 byte key that generates a given token */ @@ -376,6 +382,12 @@ public Token fromByteArray(ByteBuffer bytes) return new LongToken(ByteBufferUtil.toLong(bytes)); } + @Override + public Token fromLongValue(long token) + { + return new LongToken(token); + } + @Override public Token fromByteBuffer(ByteBuffer bytes, int position, int length) { diff --git a/src/java/org/apache/cassandra/dht/RandomPartitioner.java b/src/java/org/apache/cassandra/dht/RandomPartitioner.java index a8fbe764d47d..0b15772c067e 100644 --- a/src/java/org/apache/cassandra/dht/RandomPartitioner.java +++ b/src/java/org/apache/cassandra/dht/RandomPartitioner.java @@ -273,7 +273,33 @@ public long getHeapSize() public Token nextValidToken() { - return new BigIntegerToken(token.add(BigInteger.ONE)); + BigInteger next = token.equals(MAXIMUM) ? ZERO + : token.add(BigInteger.ONE); + return new BigIntegerToken(next); + } + + @Override + public Token prevValidToken() + { + BigInteger prev; + if (token.compareTo(ZERO) == 0) + { + // For ZERO token, return MINIMUM as adjustment + // 1. Range semantics: Most range functions expect minimum as an upper bound, not maximum as a lowerbound + // 2. Wraparound risks: Functions designed for non-wraparound ranges might not handle maximum on the lower side correctly + // Note: this means MAXIMUM.nextValidToken().prevValidToken() != MAXIMUM + prev = MINIMUM.token; + } + else if (this.isMinimum()) + { + // For MINIMUM, wrap around to MAXIMUM. + prev = MAXIMUM; + } + else + { + prev = token.subtract(BigInteger.ONE); + } + return new BigIntegerToken(prev); } public double size(Token next) diff --git a/src/java/org/apache/cassandra/dht/Range.java b/src/java/org/apache/cassandra/dht/Range.java index 18b0be262e7e..a82a106ec440 100644 --- a/src/java/org/apache/cassandra/dht/Range.java +++ b/src/java/org/apache/cassandra/dht/Range.java @@ -18,7 +18,15 @@ package org.apache.cassandra.dht; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; import java.util.function.Predicate; import com.google.common.collect.Iterables; @@ -70,7 +78,7 @@ public static > boolean contains(T left, T right, T po } } - public boolean contains(Range that) + public boolean contains(AbstractBounds that) { if (this.left.equals(this.right)) { @@ -125,9 +133,36 @@ public boolean intersects(AbstractBounds that) return intersects((Range) that); if (that instanceof Bounds) return intersects((Bounds) that); + if (that instanceof ExcludingBounds) + return intersects((ExcludingBounds) that); + if (that instanceof IncludingExcludingBounds) + return intersects((IncludingExcludingBounds) that); + throw new UnsupportedOperationException("Intersection is only supported for Bounds and Range objects; found " + that.getClass()); } + public boolean intersects(IncludingExcludingBounds that) + { + if (!isWrapAround() && !that.right.isMinimum() && (this.left.compareTo(that.right) == 0)) + return false; + else if (isWrapAround() && !that.right.isMinimum() && (this.right.compareTo(that.right) == 0)) + return false; + return contains(that.left) || (!that.left.equals(that.right) && intersects(new Range(that.left, that.right))); + } + + public boolean intersects(ExcludingBounds that) + { + if (!isWrapAround() && + ((!that.right.isMinimum() && (this.left.compareTo(that.right) == 0)) || + (this.right.compareTo(that.left) == 0))) + return false; + else if (isWrapAround() && + ((this.left.compareTo(that.left) == 0) || + (!that.right.isMinimum() && (this.right.compareTo(that.right) == 0)))) + return false; + return contains(that.left) || (!that.left.equals(that.right) && intersects(new Range(that.left, that.right))); + } + /** * @param that range to check for intersection * @return true if the given range intersects with this range. diff --git a/src/java/org/apache/cassandra/dht/RangeStreamer.java b/src/java/org/apache/cassandra/dht/RangeStreamer.java index a95a7c300e3e..1d01b6ae5ac7 100644 --- a/src/java/org/apache/cassandra/dht/RangeStreamer.java +++ b/src/java/org/apache/cassandra/dht/RangeStreamer.java @@ -44,9 +44,8 @@ import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.gms.FailureDetector; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.IFailureDetector; +import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.EndpointsByRange; @@ -86,7 +85,7 @@ public class RangeStreamer (!Gossiper.instance.isEnabled() || (Gossiper.instance.getEndpointStateForEndpoint(replica.endpoint()) == null || Gossiper.instance.getEndpointStateForEndpoint(replica.endpoint()).isAlive())) && - FailureDetector.instance.isAlive(replica.endpoint()); + IFailureDetector.instance.isAlive(replica.endpoint()); /* bootstrap tokens. can be null if replacing the node. */ private final Collection tokens; @@ -284,7 +283,7 @@ public RangeStreamer(TokenMetadata metadata, int connectionsPerHost) { this(metadata, tokens, address, streamOperation, useStrictConsistency, snitch, stateStore, - FailureDetector.instance, connectSequentially, connectionsPerHost); + IFailureDetector.instance, connectSequentially, connectionsPerHost); } RangeStreamer(TokenMetadata metadata, @@ -360,7 +359,7 @@ public void addRanges(String keyspaceName, ReplicaCollection replicas) Multimap workMap; //Only use the optimized strategy if we don't care about strict sources, have a replication factor > 1, and no - //transient replicas or it is intentionally skipped. + //transient replicas or it is intentionally skipped or HCD-84 if (CassandraRelevantProperties.SKIP_OPTIMAL_STREAMING_CANDIDATES_CALCULATION.getBoolean() || useStrictSource || strat == null || @@ -394,7 +393,7 @@ public void addRanges(String keyspaceName, ReplicaCollection replicas) private boolean useStrictSourcesForRanges(AbstractReplicationStrategy strat) { boolean res = useStrictConsistency && tokens != null; - + if (res) { int nodes = 0; @@ -409,10 +408,10 @@ private boolean useStrictSourcesForRanges(AbstractReplicationStrategy strat) } else nodes = metadata.getSizeOfAllEndpoints(); - + res = nodes > strat.getReplicationFactor().allReplicas; } - + return res; } @@ -493,13 +492,15 @@ else if (useStrictConsistency) final EndpointsForRange oldEndpoints = sorted.apply(rangeAddresses.get(range)); //Ultimately we populate this with whatever is going to be fetched from to satisfy toFetch - //It could be multiple endpoints and we must fetch from all of them if they are there + //It could be multiple endpoints, and we must fetch from all of them if they are there //With transient replication and strict consistency this is to get the full data from a full replica and //transient data from the transient replica losing data EndpointsForRange sources; + //Due to CASSANDRA-5953 we can have a higher RF than we have endpoints. //So we need to be careful to only be strict when endpoints == RF boolean isStrictConsistencyApplicable = useStrictConsistency && (oldEndpoints.size() == strat.getReplicationFactor().allReplicas); + if (isStrictConsistencyApplicable) { EndpointsForRange strictEndpoints; diff --git a/src/java/org/apache/cassandra/dht/Splitter.java b/src/java/org/apache/cassandra/dht/Splitter.java index 53b4462221cd..ad890d323bbf 100644 --- a/src/java/org/apache/cassandra/dht/Splitter.java +++ b/src/java/org/apache/cassandra/dht/Splitter.java @@ -23,7 +23,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -118,10 +118,75 @@ public double positionInRange(Token token, Range range) return new BigDecimal(elapsedTokens(token, range)).divide(new BigDecimal(tokensInRange(range)), 3, BigDecimal.ROUND_HALF_EVEN).doubleValue(); } - public List splitOwnedRanges(int parts, List weightedRanges, boolean dontSplitRanges) + /** + * How local ranges should be split + */ + public enum SplitType + { + /** Local ranges should always be split, without attempting to keep them whole */ + ALWAYS_SPLIT, + /** A first pass will try to avoid splitting ranges, but if there aren't enough parts, + * then ranges will be split in a second pass. + */ + PREFER_WHOLE, + /** Ranges Should never be split */ + ONLY_WHOLE + } + + /** + * The result of a split operation, this is just a wrapper of the boundaries and the type + * of split that was done, i.e. if the local ranges were split or not. This is just so that + * we can test the algorithm. + */ + public final static class SplitResult + { + public final List boundaries; + public final boolean rangesWereSplit; + + SplitResult(List boundaries, boolean rangesWereSplit) + { + this.boundaries = boundaries; + this.rangesWereSplit = rangesWereSplit; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof SplitResult)) + return false; + + SplitResult splitResult = (SplitResult) o; + return Objects.equals(boundaries, splitResult.boundaries) + && Objects.equals(rangesWereSplit, splitResult.rangesWereSplit); + } + + @Override + public int hashCode() + { + return Objects.hash(boundaries, rangesWereSplit); + } + } + + /** + * Split the local ranges into the specified number of parts. + * + * Depending on the parameter {@code splitType}, it may attempt to only merge the local ranges first, + * to see if this is sufficient to cover the requested number of parts. If it's not, it will then split + * existing ranges. + * + * @param parts the number of parts + * @param weightedRanges the local ranges owned by this node + * @param splitType how local ranges should be split, see {@link SplitType} + * + * @return the split result, which contains a list of tokens, one per part, and if the ranges were split or not + */ + public SplitResult splitOwnedRanges(int parts, List weightedRanges, SplitType splitType) { - if (weightedRanges.isEmpty() || parts == 1) - return Collections.singletonList(partitioner.getMaximumToken()); + if (weightedRanges.isEmpty() || parts <= 1) + return new SplitResult(Collections.singletonList(partitioner.getMaximumToken()), false); BigInteger totalTokens = BigInteger.ZERO; for (WeightedRange weightedRange : weightedRanges) @@ -132,12 +197,23 @@ public List splitOwnedRanges(int parts, List weightedRange BigInteger perPart = totalTokens.divide(BigInteger.valueOf(parts)); // the range owned is so tiny we can't split it: if (perPart.equals(BigInteger.ZERO)) - return Collections.singletonList(partitioner.getMaximumToken()); - - if (dontSplitRanges) - return splitOwnedRangesNoPartialRanges(weightedRanges, perPart, parts); + return new SplitResult(Collections.singletonList(partitioner.getMaximumToken()), false); List boundaries = new ArrayList<>(); + + if (splitType != SplitType.ALWAYS_SPLIT) + { + // see if we can obtain a sufficient number of parts by only merging local ranges + boundaries = splitOwnedRangesNoPartialRanges(weightedRanges, perPart, parts); + // we were either able to obtain sufficient parts without splitting ranges or we should never split ranges + if (splitType == SplitType.ONLY_WHOLE || boundaries.size() == parts) + return new SplitResult(boundaries, false); + else + boundaries.clear(); + } + + // otherwise continue by splitting ranges + BigInteger sum = BigInteger.ZERO; BigInteger tokensLeft = totalTokens; for (WeightedRange weightedRange : weightedRanges) @@ -155,16 +231,20 @@ public List splitOwnedRanges(int parts, List weightedRange sum = BigInteger.ZERO; int partsLeft = parts - boundaries.size(); if (partsLeft == 0) + { break; + } else if (partsLeft == 1) + { perPart = tokensLeft; + } } sum = sum.add(currentRangeWidth); } boundaries.set(boundaries.size() - 1, partitioner.getMaximumToken()); assert boundaries.size() == parts : boundaries.size() + "!=" + parts + " " + boundaries + ":" + weightedRanges; - return boundaries; + return new SplitResult(boundaries, true); } private List splitOwnedRangesNoPartialRanges(List weightedRanges, BigInteger perPart, int parts) @@ -238,28 +318,26 @@ public Set> split(Collection> ranges, int parts) } /** - * Splits the specified token range in at least {@code minParts} subranges, unless the range has not enough tokens - * in which case the range will be returned without splitting. + * Splits the specified token range in {@code parts} subranges, unless the range has not enough tokens in which case + * the range will be returned without splitting. * * @param range a token range * @param parts the number of subranges - * @return {@code parts} even subranges of {@code range} + * @return {@code parts} even subranges of {@code range}, or {@code range} if it is too small to be splitted */ private Set> split(Range range, int parts) { - // the range might not have enough tokens to split - BigInteger numTokens = tokensInRange(range); - if (BigInteger.valueOf(parts).compareTo(numTokens) > 0) - return Collections.singleton(range); - Token left = range.left; - Set> subranges = new HashSet<>(parts); - for (double i = 1; i <= parts; i++) + Set> subranges = new LinkedHashSet<>(parts); + + for (double i = 1; i < parts; i++) { Token right = partitioner.split(range.left, range.right, i / parts); - subranges.add(new Range<>(left, right)); + if (!left.equals(right)) + subranges.add(new Range<>(left, right)); left = right; } + subranges.add(new Range<>(left, range.right)); return subranges; } diff --git a/src/java/org/apache/cassandra/dht/Token.java b/src/java/org/apache/cassandra/dht/Token.java index fda7f307513d..c6cea228fbca 100644 --- a/src/java/org/apache/cassandra/dht/Token.java +++ b/src/java/org/apache/cassandra/dht/Token.java @@ -40,6 +40,20 @@ public static abstract class TokenFactory public abstract ByteBuffer toByteArray(Token token); public abstract Token fromByteArray(ByteBuffer bytes); + /** + * This method exists so that callers can create tokens from the primitive {@code long} value for this {@link Token}, if + * one exits. It is especially useful to skip ByteBuffer serde operations where performance is critical. + * + * @param token the primitive {@code long} value of this token + * @return the {@link Token} instance corresponding to the given primitive {@code long} value + * + * @throws UnsupportedOperationException if this {@link Token} is not backed by a primitive {@code long} value + */ + public Token fromLongValue(long token) + { + throw new UnsupportedOperationException(); + } + /** * Produce a byte-comparable representation of the token. * See {@link Token#asComparableBytes} @@ -172,6 +186,18 @@ public long getLongValue() */ abstract public Token nextValidToken(); + /** + * Returns the previous possible token in the token space, one that compares + * smaller than this and such that there is no other token that sits + * between this token and it in the token order. + * + * This is not possible for all token types, esp. for comparison-based + * tokens such as the LocalPartioner used for classic secondary indexes. + * + * Used to construct token ranges for sstables. + */ + abstract public Token prevValidToken(); + public Token getToken() { return this; diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/IsolatedTokenAllocator.java b/src/java/org/apache/cassandra/dht/tokenallocator/IsolatedTokenAllocator.java new file mode 100644 index 000000000000..39766e10f8fc --- /dev/null +++ b/src/java/org/apache/cassandra/dht/tokenallocator/IsolatedTokenAllocator.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.dht.tokenallocator; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterators; +import com.google.common.collect.Maps; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.math3.stat.descriptive.SummaryStatistics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.IEndpointSnitch; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaCollection; +import org.apache.cassandra.locator.SimpleSnitch; +import org.apache.cassandra.locator.TokenMetadata; + +/** + * A utility class that allocates additional tokens for a given {@link AbstractReplicationStrategy} by creating mock + * nodes and then allocating tokens for them. The source metadata and replication strategy are not modified. This + * class relies on the detail that the allocation of new tokens for bootstrapping nodes is deterministic. + */ +public class IsolatedTokenAllocator +{ + private static final Logger logger = LoggerFactory.getLogger(IsolatedTokenAllocator.class); + + public static List allocateTokens(int additionalSplits, AbstractReplicationStrategy source) + { + Preconditions.checkArgument(additionalSplits > 0, "additionalSplits must be greater than zero"); + Preconditions.checkNotNull(source); + + List allocatedTokens = new ArrayList<>(); + QuietAllocator allocator = new QuietAllocator(source); + + // Distribute nodes among the racks in round-robin fashion in the order the user is supposed to start them. + var localDc = source.snitch.getLocalDatacenter(); + // Get a list to consistently iterate over the racks as we allocate nodes. Need to clone the map in + // order to retreive the topology. + var localRacks = source.getTokenMetadata().cloneOnlyTokenMap().getTopology().getDatacenterRacks().get(localDc); + assert localRacks != null && !localRacks.isEmpty() : "No racks found for local datacenter " + localDc; + // Because we have RF=racks, we do not need to worry about the order of the racks. If we wnat to make anything + // else work, we probably need to know the order of the racks here to ensure we do it the same way each time. + // Issues could arise where we allocate a token for one rack, but that isn't the rack + // that bootstraps a new node next. + var racks = Iterators.cycle(localRacks.keySet()); + int nodeId = 0; + while (allocatedTokens.size() < additionalSplits) + { + // Allocate tokens for current node, distributing tokens round-robin among the racks. + var newTokens = allocator.allocateTokensForNode(nodeId, racks.next()); + int remainingTokensNeeded = additionalSplits - allocatedTokens.size(); + if (newTokens.size() > remainingTokensNeeded) + { + var iter = newTokens.iterator(); + for (int i = 0; i < remainingTokensNeeded; i++) + allocatedTokens.add(iter.next()); + return allocatedTokens; + } + else + { + allocatedTokens.addAll(newTokens); + } + nodeId++; + } + return allocatedTokens; + } + + /** + * A token allocator that takes a source token metadata and replication strategy, but clones the source token + * metadata with a quiet snitch, so that added nodes are not communicated to the rest of the system. + */ + private static class QuietAllocator + { + private final QuietSnitch quietSnitch; + private final TokenMetadata quietTokenMetadata; + private final TokenAllocation allocation; + private final Map lastCheckPoint = Maps.newHashMap(); + + private QuietAllocator(AbstractReplicationStrategy rs) + { + // Wrap the replication strategy's snitch with a quiet snitch + this.quietSnitch = new QuietSnitch(rs.snitch); + this.quietTokenMetadata = rs.getTokenMetadata().cloneWithNewSnitch(quietSnitch); + var numTokens = DatabaseDescriptor.getNumTokens(); + this.allocation = TokenAllocation.create(quietTokenMetadata, rs, quietSnitch, numTokens); + } + + private Collection allocateTokensForNode(int nodeId, String rackId) + { + // Update snitch and token metadata info to inform token allocation + InetAddressAndPort fakeNodeAddressAndPort = getLoopbackAddressWithPort(nodeId); + quietSnitch.nodeByRack.put(fakeNodeAddressAndPort, rackId); + quietTokenMetadata.updateTopology(fakeNodeAddressAndPort); + + // Allocate tokens + Collection tokens = allocation.allocate(fakeNodeAddressAndPort); + + // Validate ownership stats + validateAllocation(nodeId, rackId); + + return tokens; + } + + private void validateAllocation(int nodeId, String rackId) + { + SummaryStatistics newOwnership = allocation.getAllocationRingOwnership(SimpleSnitch.DATA_CENTER_NAME, rackId); + SummaryStatistics oldOwnership = lastCheckPoint.put(rackId, newOwnership); + if (oldOwnership != null) + logger.debug(String.format("Replicated node load in rack=%s before allocating node %d: %s.", rackId, nodeId, + TokenAllocation.statToString(oldOwnership))); + logger.debug(String.format("Replicated node load in rack=%s after allocating node %d: %s.", rackId, nodeId, + TokenAllocation.statToString(newOwnership))); + if (oldOwnership != null && oldOwnership.getStandardDeviation() != 0.0) + { + double stdDevGrowth = newOwnership.getStandardDeviation() - oldOwnership.getStandardDeviation(); + if (stdDevGrowth > TokenAllocation.WARN_STDEV_GROWTH) + { + logger.warn(String.format("Growth of %.2f%% in token ownership standard deviation after allocating node %d on rack %s above warning threshold of %d%%", + stdDevGrowth * 100, nodeId, rackId, (int)(TokenAllocation.WARN_STDEV_GROWTH * 100))); + } + } + } + } + + /** + * A snitch that doesn't gossip. + */ + private static class QuietSnitch implements IEndpointSnitch + { + private final Map nodeByRack = new HashMap<>(); + private final IEndpointSnitch fallbackSnitch; + + QuietSnitch(IEndpointSnitch fallbackSnitch) + { + this.fallbackSnitch = fallbackSnitch; + } + + @Override + public String getRack(InetAddressAndPort endpoint) + { + String result = nodeByRack.get(endpoint); + return result != null ? result : fallbackSnitch.getRack(endpoint); + } + + @Override + public String getDatacenter(InetAddressAndPort endpoint) + { + // For our mocked endpoints, we return the local datacenter, otherwise we return the real datacenter + return nodeByRack.containsKey(endpoint) + ? fallbackSnitch.getLocalDatacenter() + : fallbackSnitch.getDatacenter(endpoint); + } + + @Override + public > C sortedByProximity(InetAddressAndPort address, C addresses) + { + throw new NotImplementedException("sortedByProximity not implemented in QuietSnitch"); + } + + @Override + public int compareEndpoints(InetAddressAndPort target, Replica r1, Replica r2) + { + throw new NotImplementedException("compareEndpoints not implemented in QuietSnitch"); + } + + @Override + public void gossiperStarting() + { + // This snitch doesn't gossip. + } + + @Override + public boolean isWorthMergingForRangeQuery(ReplicaCollection merged, ReplicaCollection l1, ReplicaCollection l2) + { + throw new NotImplementedException("isWorthMergingForRangeQuery not implemented in QuietSnitch"); + } + } + + private static InetAddressAndPort getLoopbackAddressWithPort(int port) + { + try + { + return InetAddressAndPort.getByAddressOverrideDefaults(InetAddress.getByName("127.0.0.1"), port); + } + catch (UnknownHostException e) + { + throw new IllegalStateException("Unexpected UnknownHostException", e); + } + } +} diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java b/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java index 255a2c95692f..a82720b3d1eb 100644 --- a/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java +++ b/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java @@ -26,6 +26,7 @@ import java.util.NavigableMap; import java.util.PriorityQueue; import java.util.Queue; +import java.util.function.Supplier; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -49,6 +50,14 @@ public NoReplicationTokenAllocator(NavigableMap sortedTokens, super(sortedTokens, strategy, partitioner); } + public NoReplicationTokenAllocator(NavigableMap sortedTokens, + ReplicationStrategy strategy, + IPartitioner partitioner, + Supplier seedTokenSupplier) + { + super(sortedTokens, strategy, partitioner, seedTokenSupplier); + } + /** * Construct the token ring as a CircularList of TokenInfo, * and populate the ownership of the UnitInfo's provided diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationStrategy.java b/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationStrategy.java index 8cb5fe1cebaf..d127a5027b51 100644 --- a/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationStrategy.java +++ b/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationStrategy.java @@ -17,7 +17,7 @@ */ package org.apache.cassandra.dht.tokenallocator; -interface ReplicationStrategy +public interface ReplicationStrategy { int replicas(); diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java index 7e46b87855ce..35c9a681878c 100644 --- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java +++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java @@ -25,6 +25,7 @@ import java.util.NavigableMap; import java.util.TreeMap; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -33,7 +34,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.AbstractReplicationStrategy; @@ -51,13 +54,17 @@ public class TokenAllocation private static final Logger logger = LoggerFactory.getLogger(TokenAllocation.class); final TokenMetadata tokenMetadata; final AbstractReplicationStrategy replicationStrategy; + // In order for the IsolatedTokenAllocator to work correctly, we need to allow for a different snitch than the + // one provided by the replicationStrategy. + final IEndpointSnitch snitch; final int numTokens; final Map> strategyByRackDc = new HashMap<>(); - private TokenAllocation(TokenMetadata tokenMetadata, AbstractReplicationStrategy replicationStrategy, int numTokens) + private TokenAllocation(TokenMetadata tokenMetadata, AbstractReplicationStrategy replicationStrategy, IEndpointSnitch snitch, int numTokens) { this.tokenMetadata = tokenMetadata.cloneOnlyTokenMap(); this.replicationStrategy = replicationStrategy; + this.snitch = snitch; this.numTokens = numTokens; } @@ -77,6 +84,50 @@ public static Collection allocateTokens(final TokenMetadata tokenMetadata return create(DatabaseDescriptor.getEndpointSnitch(), tokenMetadata, replicas, numTokens).allocate(endpoint); } + // Used by CNDB TokenTracker + public static Collection allocateTokens(TokenMetadata tokenMetadata, + IEndpointSnitch snitch, + int localReplicationFactor, + InetAddressAndPort endpoint, + int numTokens, + StrategyAdapter strategy) + { + return create(snitch, tokenMetadata, localReplicationFactor, numTokens).allocate(endpoint, strategy); + } + + // Used by CNDB + // return the ratio of ownership for each endpoint + public static Map evaluateReplicatedOwnership(TokenMetadata tokenMetadata, AbstractReplicationStrategy rs) + { + Map ownership = Maps.newHashMap(); + List sortedTokens = tokenMetadata.sortedTokens(); + if (sortedTokens.isEmpty()) + return ownership; + + Iterator it = sortedTokens.iterator(); + Token current = it.next(); + while (it.hasNext()) + { + Token next = it.next(); + addOwnership(tokenMetadata, rs, current, next, ownership); + current = next; + } + addOwnership(tokenMetadata, rs, current, sortedTokens.get(0), ownership); + + return ownership; + } + + private static void addOwnership(TokenMetadata tokenMetadata, AbstractReplicationStrategy rs, Token current, Token next, Map ownership) + { + double size = current.size(next); + Token representative = current.getPartitioner().midpoint(current, next); + for (InetAddressAndPort n : rs.calculateNaturalReplicas(representative, tokenMetadata).endpoints()) + { + Double v = ownership.get(n); + ownership.put(n, v != null ? v + size : size); + } + } + static TokenAllocation create(IEndpointSnitch snitch, TokenMetadata tokenMetadata, int replicas, int numTokens) { // We create a fake NTS replication strategy with the specified RF in the local DC @@ -84,25 +135,34 @@ static TokenAllocation create(IEndpointSnitch snitch, TokenMetadata tokenMetadat options.put(snitch.getLocalDatacenter(), Integer.toString(replicas)); NetworkTopologyStrategy fakeReplicationStrategy = new NetworkTopologyStrategy(null, tokenMetadata, snitch, options); - TokenAllocation allocator = new TokenAllocation(tokenMetadata, fakeReplicationStrategy, numTokens); + TokenAllocation allocator = new TokenAllocation(tokenMetadata, fakeReplicationStrategy, snitch, numTokens); return allocator; } static TokenAllocation create(TokenMetadata tokenMetadata, AbstractReplicationStrategy rs, int numTokens) { - return new TokenAllocation(tokenMetadata, rs, numTokens); + return new TokenAllocation(tokenMetadata, rs, rs.snitch, numTokens); + } + + static TokenAllocation create(TokenMetadata tokenMetadata, AbstractReplicationStrategy rs, IEndpointSnitch snitch, int numTokens) + { + return new TokenAllocation(tokenMetadata, rs, snitch, numTokens); } Collection allocate(InetAddressAndPort endpoint) { - StrategyAdapter strategy = getOrCreateStrategy(endpoint); + return allocate(endpoint, getOrCreateStrategy(endpoint)); + } + + private Collection allocate(InetAddressAndPort endpoint, StrategyAdapter strategy) + { Collection tokens = strategy.createAllocator().addUnit(endpoint, numTokens); tokens = strategy.adjustForCrossDatacenterClashes(tokens); - SummaryStatistics os = strategy.replicatedOwnershipStats(); + SummaryStatistics os = replicatedOwnershipStats(strategy); tokenMetadata.updateNormalTokens(tokens, endpoint); - SummaryStatistics ns = strategy.replicatedOwnershipStats(); + SummaryStatistics ns = replicatedOwnershipStats(strategy); logger.info("Selected tokens {}", tokens); logger.debug("Replicated node load in datacenter before allocation {}", statToString(os)); logger.debug("Replicated node load in datacenter after allocation {}", statToString(ns)); @@ -124,20 +184,34 @@ static String statToString(SummaryStatistics stat) SummaryStatistics getAllocationRingOwnership(String datacenter, String rack) { - return getOrCreateStrategy(datacenter, rack).replicatedOwnershipStats(); + return replicatedOwnershipStats(getOrCreateStrategy(datacenter, rack)); } + @VisibleForTesting SummaryStatistics getAllocationRingOwnership(InetAddressAndPort endpoint) { - return getOrCreateStrategy(endpoint).replicatedOwnershipStats(); + return replicatedOwnershipStats(getOrCreateStrategy(endpoint)); } - abstract class StrategyAdapter implements ReplicationStrategy + public static abstract class StrategyAdapter implements ReplicationStrategy { + final TokenMetadata tokenMetadata; + + public StrategyAdapter(TokenMetadata tokenMetadata) + { + this.tokenMetadata = tokenMetadata; + } + // return true iff the provided endpoint occurs in the same virtual token-ring we are allocating for // i.e. the set of the nodes that share ownership with the node we are allocating // alternatively: return false if the endpoint's ownership is independent of the node we are allocating tokens for - abstract boolean inAllocationRing(InetAddressAndPort other); + public abstract boolean inAllocationRing(InetAddressAndPort other); + + // Allows sub classes to override and provide custom partitioners + public IPartitioner partitioner() + { + return tokenMetadata.partitioner; + } final TokenAllocator createAllocator() { @@ -147,7 +221,7 @@ final TokenAllocator createAllocator() if (inAllocationRing(en.getValue())) sortedTokens.put(en.getKey(), en.getValue()); } - return TokenAllocatorFactory.createTokenAllocator(sortedTokens, this, tokenMetadata.partitioner); + return TokenAllocatorFactory.createTokenAllocator(sortedTokens, this, partitioner()); } final Collection adjustForCrossDatacenterClashes(Collection tokens) @@ -156,9 +230,9 @@ final Collection adjustForCrossDatacenterClashes(Collection tokens for (Token t : tokens) { - while (tokenMetadata.getEndpoint(t) != null) + InetAddressAndPort other; + while ((other = tokenMetadata.getEndpoint(t)) != null) { - InetAddressAndPort other = tokenMetadata.getEndpoint(t); if (inAllocationRing(other)) throw new ConfigurationException(String.format("Allocated token %s already assigned to node %s. Is another node also allocating tokens?", t, other)); t = t.nextValidToken(); @@ -167,59 +241,42 @@ final Collection adjustForCrossDatacenterClashes(Collection tokens } return filtered; } + } - final SummaryStatistics replicatedOwnershipStats() + private SummaryStatistics replicatedOwnershipStats(StrategyAdapter strategy) + { + SummaryStatistics stat = new SummaryStatistics(); + for (Map.Entry en : TokenAllocation.evaluateReplicatedOwnership(tokenMetadata, replicationStrategy).entrySet()) { - SummaryStatistics stat = new SummaryStatistics(); - for (Map.Entry en : evaluateReplicatedOwnership().entrySet()) - { - // Filter only in the same allocation ring - if (inAllocationRing(en.getKey())) - stat.addValue(en.getValue() / tokenMetadata.getTokens(en.getKey()).size()); - } - return stat; + // Filter only in the same allocation ring + if (strategy.inAllocationRing(en.getKey())) + stat.addValue(en.getValue() / tokenMetadata.getTokens(en.getKey()).size()); } + return stat; + } - // return the ratio of ownership for each endpoint - private Map evaluateReplicatedOwnership() - { - Map ownership = Maps.newHashMap(); - List sortedTokens = tokenMetadata.sortedTokens(); - if (sortedTokens.isEmpty()) - return ownership; - - Iterator it = sortedTokens.iterator(); - Token current = it.next(); - while (it.hasNext()) - { - Token next = it.next(); - addOwnership(current, next, ownership); - current = next; - } - addOwnership(current, sortedTokens.get(0), ownership); + private StrategyAdapter getOrCreateStrategy(InetAddressAndPort endpoint) + { + String dc = snitch.getDatacenter(endpoint); + String rack = snitch.getRack(endpoint); - return ownership; + try + { + return getOrCreateStrategy(dc, rack); } - - private void addOwnership(Token current, Token next, Map ownership) + catch (ConfigurationException e) { - double size = current.size(next); - Token representative = current.getPartitioner().midpoint(current, next); - for (InetAddressAndPort n : replicationStrategy.calculateNaturalReplicas(representative, tokenMetadata).endpoints()) - { - Double v = ownership.get(n); - ownership.put(n, v != null ? v + size : size); - } + if (CassandraRelevantProperties.USE_RANDOM_ALLOCATION_IF_NOT_SUPPORTED.getBoolean()) + return createRandomStrategy(endpoint); + + throw new ConfigurationException( + String.format("Algorithmic token allocation failed: the number of racks in datacenter %s is lower than its replication factor %d.\n" + + "If you are starting a new datacenter, please make sure that the first %d nodes to start are from different racks.\n" + + "If you wish to fall back to random token allocation, please use '" + CassandraRelevantProperties.USE_RANDOM_ALLOCATION_IF_NOT_SUPPORTED + "'.", + dc, replicationStrategy.getReplicationFactor().allReplicas, replicationStrategy.getReplicationFactor().allReplicas)); } } - private StrategyAdapter getOrCreateStrategy(InetAddressAndPort endpoint) - { - String dc = replicationStrategy.snitch.getDatacenter(endpoint); - String rack = replicationStrategy.snitch.getRack(endpoint); - return getOrCreateStrategy(dc, rack); - } - private StrategyAdapter getOrCreateStrategy(String dc, String rack) { return strategyByRackDc.computeIfAbsent(dc, k -> new HashMap<>()).computeIfAbsent(rack, k -> createStrategy(dc, rack)); @@ -236,7 +293,7 @@ private StrategyAdapter createStrategy(String dc, String rack) private StrategyAdapter createStrategy(final SimpleStrategy rs) { - return createStrategy(rs.snitch, null, null, rs.getReplicationFactor().allReplicas, false); + return createStrategy(snitch, null, null, rs.getReplicationFactor().allReplicas, false); } private StrategyAdapter createStrategy(TokenMetadata tokenMetadata, NetworkTopologyStrategy strategy, String dc, String rack) @@ -252,32 +309,56 @@ private StrategyAdapter createStrategy(TokenMetadata tokenMetadata, NetworkTopol if (replicas <= 1) { // each node is treated as separate and replicates once - return createStrategy(strategy.snitch, dc, null, 1, false); + return createStrategy(snitch, dc, null, 1, false); } else if (racks == replicas) { // each node is treated as separate and replicates once, with separate allocation rings for each rack - return createStrategy(strategy.snitch, dc, rack, 1, false); + return createStrategy(snitch, dc, rack, 1, false); } else if (racks > replicas) { // group by rack - return createStrategy(strategy.snitch, dc, null, replicas, true); + return createStrategy(snitch, dc, null, replicas, true); } else if (racks == 1) { - return createStrategy(strategy.snitch, dc, null, replicas, false); + return createStrategy(snitch, dc, null, replicas, false); } throw new ConfigurationException(String.format("Token allocation failed: the number of racks %d in datacenter %s is lower than its replication factor %d.", racks, dc, replicas)); } + private StrategyAdapter createRandomStrategy(InetAddressAndPort endpoint) + { + return new StrategyAdapter(this.tokenMetadata) + { + @Override + public int replicas() + { + return 1; + } + + @Override + public Object getGroup(InetAddressAndPort unit) + { + return unit; + } + + @Override + public boolean inAllocationRing(InetAddressAndPort other) + { + return endpoint.equals(other); // Make the algorithm believe this is the only node in the DC so it assigns tokens randomly. + } + }; + } + // a null dc will always return true for inAllocationRing(..) // a null rack will return true for inAllocationRing(..) for all nodes in the same dc private StrategyAdapter createStrategy(IEndpointSnitch snitch, String dc, String rack, int replicas, boolean groupByRack) { - return new StrategyAdapter() + return new StrategyAdapter(this.tokenMetadata) { @Override public int replicas() diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java index 3d7e6b96560c..fcd80f9a5f46 100644 --- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java +++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.NavigableMap; import java.util.Random; +import java.util.function.Supplier; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -38,14 +39,24 @@ public abstract class TokenAllocatorBase implements TokenAllocator final NavigableMap sortedTokens; final ReplicationStrategy strategy; final IPartitioner partitioner; + final Supplier seedTokenSupplier; protected TokenAllocatorBase(NavigableMap sortedTokens, - ReplicationStrategy strategy, - IPartitioner partitioner) + ReplicationStrategy strategy, + IPartitioner partitioner) + { + this(sortedTokens, strategy, partitioner, partitioner::getRandomToken); + } + + protected TokenAllocatorBase(NavigableMap sortedTokens, + ReplicationStrategy strategy, + IPartitioner partitioner, + Supplier seedTokenSupplier) { this.sortedTokens = sortedTokens; this.strategy = strategy; this.partitioner = partitioner; + this.seedTokenSupplier = seedTokenSupplier; } public abstract int getReplicas(); @@ -107,9 +118,10 @@ Collection generateSplits(Unit newUnit, int numTokens, double minRatio, d if (sortedTokens.isEmpty()) { - // Select a random start token. This has no effect on distribution, only on where the local ring is "centered". + // Select a start token using the configured seedTokenSupplier. By default, the token is random. It can also + // be supplied by the subclass. This has no effect on distribution, only on where the local ring is "centered". // Using a random start decreases the chances of clash with the tokens of other datacenters in the ring. - Token t = partitioner.getRandomToken(); + Token t = seedTokenSupplier.get(); tokens.add(t); sortedTokens.put(t, newUnit); } diff --git a/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java b/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java index 7da335ca0487..82d23746fe46 100644 --- a/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java +++ b/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java @@ -129,6 +129,7 @@ private void onEvent(DiagnosticEvent event) LastEventIdBroadcaster.instance().setLastEventId(event.getClass().getName(), store.getLastEventId()); } + @SuppressWarnings("unchecked") private Class getEventClass(String eventClazz) throws ClassNotFoundException, InvalidClassException { // get class by eventClazz argument name @@ -136,12 +137,13 @@ private Class getEventClass(String eventClazz) throws ClassNotF if (!eventClazz.startsWith("org.apache.cassandra.")) throw new RuntimeException("Not a Cassandra event class: " + eventClazz); - Class clazz = (Class) Class.forName(eventClazz); + // Load without initialization so the type can be verified before the class's static initializer runs. + Class clazz = Class.forName(eventClazz, false, DiagnosticEventPersistence.class.getClassLoader()); if (!(DiagnosticEvent.class.isAssignableFrom(clazz))) throw new InvalidClassException("Event class must be of type DiagnosticEvent"); - return clazz; + return (Class) clazz.asSubclass(DiagnosticEvent.class); } private DiagnosticEventStore getStore(Class cls) diff --git a/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java b/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java index 1829c5cb1f6c..f0b088356dc9 100644 --- a/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java +++ b/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java @@ -22,7 +22,7 @@ public class AlreadyExistsException extends ConfigurationException public final String ksName; public final String cfName; - private AlreadyExistsException(String ksName, String cfName, String msg) + public AlreadyExistsException(String ksName, String cfName, String msg) { super(ExceptionCode.ALREADY_EXISTS, msg); this.ksName = ksName; diff --git a/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java b/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java index 32cc014da160..e6b49b651f92 100644 --- a/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java @@ -27,7 +27,12 @@ public class CasWriteTimeoutException extends WriteTimeoutException public CasWriteTimeoutException(WriteType writeType, ConsistencyLevel consistency, int received, int blockFor, int contentions) { - super(writeType, consistency, received, blockFor, String.format("CAS operation timed out: received %d of %d required responses after %d contention retries", received, blockFor, contentions)); + this(writeType, consistency, received, blockFor, contentions, String.format("CAS operation timed out: received %d of %d required responses after %d contention retries", received, blockFor, contentions)); + } + + public CasWriteTimeoutException(WriteType writeType, ConsistencyLevel consistency, int received, int blockFor, int contentions, String message) + { + super(writeType, consistency, received, blockFor, message); this.contentions = contentions; } } diff --git a/src/java/org/apache/cassandra/exceptions/IncompatibleSchemaException.java b/src/java/org/apache/cassandra/exceptions/IncompatibleSchemaException.java index fe3a167b6f72..d2d3f00e0c31 100644 --- a/src/java/org/apache/cassandra/exceptions/IncompatibleSchemaException.java +++ b/src/java/org/apache/cassandra/exceptions/IncompatibleSchemaException.java @@ -19,10 +19,18 @@ import java.io.IOException; -public class IncompatibleSchemaException extends IOException +public class IncompatibleSchemaException extends IOException implements InternalRequestExecutionException { - public IncompatibleSchemaException(String msg) + private final RequestFailureReason reason; + + public IncompatibleSchemaException(RequestFailureReason reason, String msg) { super(msg); + this.reason = reason; + } + + public RequestFailureReason getReason() + { + return reason; } } diff --git a/src/java/org/apache/cassandra/exceptions/InternalRequestExecutionException.java b/src/java/org/apache/cassandra/exceptions/InternalRequestExecutionException.java new file mode 100644 index 000000000000..e4cf5eeef7c0 --- /dev/null +++ b/src/java/org/apache/cassandra/exceptions/InternalRequestExecutionException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.exceptions; + +/** + * Indicates an "expected" exception during the execution of a request on a + * replica. + *

    + * This groups exceptions that can happen on replicas but aren't unexpected in + * the sense that the circumstance for it happening is understood and a result + * of a user error, which we simply couldn't detect on the coordinator. + *

    + * Such failures include an index query while the index is not built yet, or a + * 'TombstoneOverwhelmingException' for instance. + */ +public interface InternalRequestExecutionException +{ + RequestFailureReason getReason(); +} diff --git a/src/java/org/apache/cassandra/exceptions/InvalidColumnTypeException.java b/src/java/org/apache/cassandra/exceptions/InvalidColumnTypeException.java new file mode 100644 index 000000000000..164a748ff339 --- /dev/null +++ b/src/java/org/apache/cassandra/exceptions/InvalidColumnTypeException.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.exceptions; + +import java.nio.ByteBuffer; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.db.marshal.AbstractType; + +/** + * Exception thrown when a configured column type is invalid. + */ +public class InvalidColumnTypeException extends ConfigurationException +{ + public InvalidColumnTypeException(ByteBuffer name, + AbstractType invalidType, + String reason) + { + super(msg(name, invalidType, reason)); + } + + private static String msg(ByteBuffer name, + AbstractType invalidType, + String reason) + { + return String.format("Invalid type %s for column %s: %s", + invalidType.asCQL3Type().toSchemaString(), + ColumnIdentifier.toCQLString(name), + reason); + } + +} diff --git a/src/java/org/apache/cassandra/exceptions/QueryCancelledException.java b/src/java/org/apache/cassandra/exceptions/QueryCancelledException.java index 45b6334b8dcb..5c08a26a0bbc 100644 --- a/src/java/org/apache/cassandra/exceptions/QueryCancelledException.java +++ b/src/java/org/apache/cassandra/exceptions/QueryCancelledException.java @@ -23,6 +23,6 @@ public class QueryCancelledException extends RuntimeException { public QueryCancelledException(ReadCommand command) { - super("Query cancelled for taking too long: " + command.toCQLString()); + super("Query cancelled for taking too long: " + command.toRedactedCQLString()); } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java b/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java index 05f3510e7b39..b0ccc91fd8b8 100644 --- a/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java @@ -28,4 +28,10 @@ public ReadTimeoutException(ConsistencyLevel consistency, int received, int bloc super(ExceptionCode.READ_TIMEOUT, consistency, received, blockFor); this.dataPresent = dataPresent; } + + public ReadTimeoutException(ConsistencyLevel consistency) + { + super(ExceptionCode.READ_TIMEOUT, consistency); + this.dataPresent = false; + } } diff --git a/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java b/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java index ae5566104572..838d83343a05 100644 --- a/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java +++ b/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java @@ -18,14 +18,19 @@ package org.apache.cassandra.exceptions; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; +import org.apache.cassandra.index.IndexBuildInProgressException; +import org.apache.cassandra.index.IndexNotAvailableException; +import org.apache.cassandra.index.FeatureNeedsIndexRebuildException; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.utils.vint.VIntCoding; -import static java.lang.Math.max; import static org.apache.cassandra.net.MessagingService.VERSION_40; public enum RequestFailureReason @@ -37,7 +42,19 @@ public enum RequestFailureReason READ_SIZE (4), NODE_DOWN (5), INDEX_NOT_AVAILABLE (6), - READ_TOO_MANY_INDEXES (7); + READ_TOO_MANY_INDEXES (7), + // The following codes are not present in Apache Cassandra's RequestFailureReason + // We should add new codes in HCD (which do not exist in Apache Cassandra) only with big numbers, to avoid conflicts + UNKNOWN_COLUMN (500), + UNKNOWN_TABLE (501), + REMOTE_STORAGE_FAILURE (502), + INDEX_BUILD_IN_PROGRESS (503), + /** + * The index uses an old version that doesn't support the requested feature. + * The problematic old index version can be being used by either the entire index or only some sstables. + * Enabling the feature requires setting the right index version and running a sstable upgrade. + */ + FEATURE_NEEDS_INDEX_REBUILD(504); public static final Serializer serializer = new Serializer(); @@ -48,26 +65,37 @@ public enum RequestFailureReason this.code = code; } - private static final RequestFailureReason[] codeToReasonMap; + public int codeForNativeProtocol() + { + // We explicitly indicated in the protocol spec that drivers should not error out on unknown code, and we + // currently support a superset of the OSS codes, so we don't yet worry about the version. + return code; + } + + private static final Map codeToReasonMap = new HashMap<>(); + private static final Map, RequestFailureReason> exceptionToReasonMap = new HashMap<>(); static { RequestFailureReason[] reasons = values(); - int max = -1; - for (RequestFailureReason r : reasons) - max = max(r.code, max); - - RequestFailureReason[] codeMap = new RequestFailureReason[max + 1]; - for (RequestFailureReason reason : reasons) { - if (codeMap[reason.code] != null) + if (codeToReasonMap.put(reason.code, reason) != null) throw new RuntimeException("Two RequestFailureReason-s that map to the same code: " + reason.code); - codeMap[reason.code] = reason; } - codeToReasonMap = codeMap; + exceptionToReasonMap.put(TombstoneOverwhelmingException.class, READ_TOO_MANY_TOMBSTONES); + exceptionToReasonMap.put(IncompatibleSchemaException.class, INCOMPATIBLE_SCHEMA); + exceptionToReasonMap.put(AbortedOperationException.class, TIMEOUT); + exceptionToReasonMap.put(IndexNotAvailableException.class, INDEX_NOT_AVAILABLE); + exceptionToReasonMap.put(UnknownColumnException.class, UNKNOWN_COLUMN); + exceptionToReasonMap.put(UnknownTableException.class, UNKNOWN_TABLE); + exceptionToReasonMap.put(IndexBuildInProgressException.class, INDEX_BUILD_IN_PROGRESS); + exceptionToReasonMap.put(FeatureNeedsIndexRebuildException.class, FEATURE_NEEDS_INDEX_REBUILD); + + if (exceptionToReasonMap.size() != reasons.length-5) + throw new RuntimeException("A new RequestFailureReasons was probably added and you may need to update the exceptionToReasonMap"); } public static RequestFailureReason fromCode(int code) @@ -76,16 +104,18 @@ public static RequestFailureReason fromCode(int code) throw new IllegalArgumentException("RequestFailureReason code must be non-negative (got " + code + ')'); // be forgiving and return UNKNOWN if we aren't aware of the code - for forward compatibility - return code < codeToReasonMap.length ? codeToReasonMap[code] : UNKNOWN; + return codeToReasonMap.getOrDefault(code, UNKNOWN); } public static RequestFailureReason forException(Throwable t) { - if (t instanceof TombstoneOverwhelmingException) - return READ_TOO_MANY_TOMBSTONES; + RequestFailureReason r = exceptionToReasonMap.get(t.getClass()); + if (r != null) + return r; - if (t instanceof IncompatibleSchemaException) - return INCOMPATIBLE_SCHEMA; + for (Map.Entry, RequestFailureReason> entry : exceptionToReasonMap.entrySet()) + if (entry.getKey().isInstance(t)) + return entry.getValue(); return UNKNOWN; } @@ -96,22 +126,25 @@ private Serializer() { } + @Override public void serialize(RequestFailureReason reason, DataOutputPlus out, int version) throws IOException { assert version >= VERSION_40; out.writeUnsignedVInt32(reason.code); } + @Override public RequestFailureReason deserialize(DataInputPlus in, int version) throws IOException { assert version >= VERSION_40; return fromCode(in.readUnsignedVInt32()); } + @Override public long serializedSize(RequestFailureReason reason, int version) { assert version >= VERSION_40; - return VIntCoding.computeVIntSize(reason.code); + return VIntCoding.computeUnsignedVIntSize(reason.code); } } } diff --git a/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java b/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java index 853ba2fd1c46..d8d2a9c6f73b 100644 --- a/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java @@ -40,4 +40,12 @@ protected RequestTimeoutException(ExceptionCode code, ConsistencyLevel consisten this.received = received; this.blockFor = blockFor; } + + public RequestTimeoutException(ExceptionCode exceptionCode, ConsistencyLevel consistency) + { + super(exceptionCode, "Operation timeout out"); + this.consistency = consistency; + this.received = 0; + this.blockFor = 0; + } } diff --git a/src/java/org/apache/cassandra/exceptions/UnknownColumnException.java b/src/java/org/apache/cassandra/exceptions/UnknownColumnException.java index 93a464e77e02..90548af8e8df 100644 --- a/src/java/org/apache/cassandra/exceptions/UnknownColumnException.java +++ b/src/java/org/apache/cassandra/exceptions/UnknownColumnException.java @@ -21,6 +21,6 @@ public final class UnknownColumnException extends IncompatibleSchemaException { public UnknownColumnException(String msg) { - super(msg); + super(RequestFailureReason.UNKNOWN_COLUMN, msg); } } diff --git a/src/java/org/apache/cassandra/exceptions/UnknownKeyspaceException.java b/src/java/org/apache/cassandra/exceptions/UnknownKeyspaceException.java new file mode 100644 index 000000000000..0fc4f988d278 --- /dev/null +++ b/src/java/org/apache/cassandra/exceptions/UnknownKeyspaceException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.exceptions; + +public class UnknownKeyspaceException extends RuntimeException implements InternalRequestExecutionException +{ + public final String keyspaceName; + + public UnknownKeyspaceException(String keyspaceName) + { + super("Could not find a keyspace " + keyspaceName); + this.keyspaceName = keyspaceName; + } + + @Override + public RequestFailureReason getReason() + { + return RequestFailureReason.INCOMPATIBLE_SCHEMA; + } +} diff --git a/src/java/org/apache/cassandra/exceptions/UnknownTableException.java b/src/java/org/apache/cassandra/exceptions/UnknownTableException.java index 3e9c77537061..128238631d75 100644 --- a/src/java/org/apache/cassandra/exceptions/UnknownTableException.java +++ b/src/java/org/apache/cassandra/exceptions/UnknownTableException.java @@ -25,7 +25,7 @@ public class UnknownTableException extends IncompatibleSchemaException public UnknownTableException(String msg, TableId id) { - super(msg); + super(RequestFailureReason.UNKNOWN_TABLE, msg); this.id = id; } } diff --git a/src/java/org/apache/cassandra/gms/EndpointState.java b/src/java/org/apache/cassandra/gms/EndpointState.java index 7955fd664da4..ad2dd3c316c9 100644 --- a/src/java/org/apache/cassandra/gms/EndpointState.java +++ b/src/java/org/apache/cassandra/gms/EndpointState.java @@ -17,11 +17,17 @@ */ package org.apache.cassandra.gms; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; - +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; @@ -31,14 +37,29 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; +import com.google.common.base.Preconditions; + +import net.openhft.chronicle.core.util.ThrowingConsumer; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.nodes.LocalInfo; +import org.apache.cassandra.nodes.NodeInfo; import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.NullableSerializer; import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Throwables; + +import static org.apache.cassandra.gms.ApplicationState.SCHEMA; +import static org.apache.cassandra.gms.ApplicationState.TOKENS; /** * This abstraction represents both the HeartBeatState and the ApplicationState in an EndpointState @@ -70,6 +91,7 @@ private View(HeartBeatState hbState, Map appli /* fields below do not get serialized */ private volatile long updateTimestamp; private volatile boolean isAlive; + private volatile Consumer>> updater; public EndpointState(HeartBeatState initialHbState) { @@ -97,7 +119,54 @@ public HeartBeatState getHeartBeatState() return ref.get().hbState; } - public void updateHeartBeat() + public synchronized void maybeSetUpdater(Consumer>> updater) + { + Preconditions.checkNotNull(updater); + if (this.updater == null) + this.updater = updater; + } + + public synchronized void maybeUpdate() + { + if (this.updater != null) + update(states()); + } + + public synchronized void maybeRemoveUpdater() + { + this.updater = null; + } + + public void update(Set> entries) + { + Consumer>> updater = this.updater; + if (updater == null) + return; + + List, UnknownHostException>> allUpdates = entries.stream() + .map(e -> updateNodeInfo(e.getKey(), e.getValue())) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!allUpdates.isEmpty()) + { + updater.accept(info -> { + allUpdates.forEach(update -> { + try + { + update.accept(info); + } + catch (UnknownHostException e) + { + throw Throwables.cleaned(e); + } + }); + return info; + }); + } + } + + void updateHeartBeat() { updateHeartBeat(HeartBeatState::updateHeartBeat); } @@ -158,7 +227,7 @@ public void addApplicationStates(Map values) addApplicationStates(values.entrySet()); } - public void addApplicationStates(Set> values) + public synchronized void addApplicationStates(Set> values) { addApplicationStates(values, null); } @@ -176,8 +245,14 @@ public void addApplicationStates(Set if (this.ref.compareAndSet(view, new View(hbState == null ? view.hbState : hbState, copy))) { - if (hbState != null) - updateTimestamp(); + + EnumMap diff = new EnumMap<>(copy); + for (Map.Entry entry : copy.entrySet()) + { + if (Objects.equals(entry.getValue(), orig.get(entry.getKey()))) + diff.remove(entry.getKey()); + } + update(diff.entrySet()); return; } } @@ -210,22 +285,23 @@ private boolean hasLegacyFields() private static Map filterMajorVersion3LegacyApplicationStates(Map states) { return states.entrySet().stream().filter(entry -> { - // Filter out pre-4.0 versions of data for more complete 4.0 versions - switch (entry.getKey()) - { - case INTERNAL_IP: - return !states.containsKey(ApplicationState.INTERNAL_ADDRESS_AND_PORT); - case STATUS: - return !states.containsKey(ApplicationState.STATUS_WITH_PORT); - case RPC_ADDRESS: - return !states.containsKey(ApplicationState.NATIVE_ADDRESS_AND_PORT); - default: - return true; - } - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + // Filter out pre-4.0 versions of data for more complete 4.0 versions + switch (entry.getKey()) + { + case INTERNAL_IP: + return !states.containsKey(ApplicationState.INTERNAL_ADDRESS_AND_PORT); + case STATUS: + return !states.containsKey(ApplicationState.STATUS_WITH_PORT); + case RPC_ADDRESS: + return !states.containsKey(ApplicationState.NATIVE_ADDRESS_AND_PORT); + default: + return true; + } + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } /* getters and setters */ + /** * @return System.nanoTime() when state was updated last time. */ @@ -318,19 +394,65 @@ public String getStatus() @Nullable public UUID getSchemaVersion() { - VersionedValue applicationState = getApplicationState(ApplicationState.SCHEMA); + VersionedValue applicationState = getApplicationState(SCHEMA); return applicationState != null ? UUID.fromString(applicationState.value) : null; } @Nullable - public CassandraVersion getReleaseVersion() + public Collection getTokens(IPartitioner partitioner) { - VersionedValue applicationState = getApplicationState(ApplicationState.RELEASE_VERSION); - return applicationState != null - ? new CassandraVersion(applicationState.value) - : null; + VersionedValue value = getApplicationState(TOKENS); + return value != null ? getTokens(partitioner, value) : null; + } + + @Nonnull + private static Collection getTokens(IPartitioner partitioner, @Nonnull VersionedValue value) + { + try + { + return TokenSerializer.deserialize(partitioner, new DataInputStream(new ByteArrayInputStream(value.toBytes()))); + } + catch (IOException e) + { + throw Throwables.unchecked(e); + } + } + + private static ThrowingConsumer, UnknownHostException> updateNodeInfo(ApplicationState state, VersionedValue value) + { + switch (state) + { + case TOKENS: + return info -> info.setTokens(getTokens(DatabaseDescriptor.getPartitioner(), value)); + case HOST_ID: + return info -> info.setHostId(UUID.fromString(value.value)); + case RELEASE_VERSION: + return info -> info.setReleaseVersion(new CassandraVersion(value.value)); + case DC: + return info -> info.setDataCenter(value.value); + case RACK: + return info -> info.setRack(value.value); + case SCHEMA: + return info -> info.setSchemaVersion(UUID.fromString(value.value)); + case INTERNAL_IP: + return info -> { + if (info instanceof LocalInfo) + ((LocalInfo) info).setListenAddressOnly(InetAddress.getByName(value.value), FBUtilities.getLocalAddressAndPort().getPort()); + }; + case INTERNAL_ADDRESS_AND_PORT: + return info -> { + if (info instanceof LocalInfo) + ((LocalInfo) info).setListenAddressAndPort(InetAddressAndPort.getByName(value.value)); + }; + case RPC_ADDRESS: + return info -> info.setNativeTransportAddressOnly(InetAddress.getByName(value.value), DatabaseDescriptor.getNativeTransportPort()); + case NATIVE_ADDRESS_AND_PORT: + return info -> info.setNativeTransportAddressAndPort(InetAddressAndPort.getByName(value.value)); + default: + return null; + } } public String toString() @@ -352,10 +474,18 @@ public boolean isSupersededBy(EndpointState that) return Gossiper.getMaxEndpointStateVersion(that) > Gossiper.getMaxEndpointStateVersion(this); } + + @VisibleForTesting // delegates package-protected static access for tests to EndpointStateSerializer.filterValue + static VersionedValue serializerFilterValue(ApplicationState state, VersionedValue value, int version) + { + return EndpointStateSerializer.filterValue(state, value, version); + } } class EndpointStateSerializer implements IVersionedSerializer { + private static final Logger logger = LoggerFactory.getLogger(EndpointStateSerializer.class); + public void serialize(EndpointState epState, DataOutputPlus out, int version) throws IOException { /* serialize the HeartBeatState */ @@ -363,13 +493,12 @@ public void serialize(EndpointState epState, DataOutputPlus out, int version) th HeartBeatState.serializer.serialize(hbState, out, version); /* serialize the map of ApplicationState objects */ - Set> states = epState.states(); + Set> states = filterOutgoingStates(epState.states(), version); out.writeInt(states.size()); for (Map.Entry state : states) { - VersionedValue value = state.getValue(); out.writeInt(state.getKey().ordinal()); - VersionedValue.serializer.serialize(value, out, version); + VersionedValue.serializer.serialize(state.getValue(), out, version); } } @@ -386,20 +515,129 @@ public EndpointState deserialize(DataInputPlus in, int version) throws IOExcepti states.put(Gossiper.STATES[key], value); } - return new EndpointState(hbState, states); + return new EndpointState(hbState, filterIncomingStates(states, version)); } public long serializedSize(EndpointState epState, int version) { long size = HeartBeatState.serializer.serializedSize(epState.getHeartBeatState(), version); - Set> states = epState.states(); + Set> states = filterOutgoingStates(epState.states(), version); size += TypeSizes.sizeof(states.size()); for (Map.Entry state : states) { - VersionedValue value = state.getValue(); size += TypeSizes.sizeof(state.getKey().ordinal()); - size += VersionedValue.serializer.serializedSize(value, version); + size += VersionedValue.serializer.serializedSize(state.getValue(), version); } return size; } + + @VisibleForTesting + static VersionedValue filterValue(ApplicationState state, VersionedValue value, int version) + { + // CC versions come with a sha suffix that C* 3.x nodes cannot parse + return version < MessagingService.VERSION_40 && ApplicationState.RELEASE_VERSION == state + ? VersionedValue.unsafeMakeVersionedValue(value.value.replaceFirst("-[0-9a-f]{7,40}$", ""), value.version) + : value; + } + + @VisibleForTesting + static Set> filterOutgoingStates(Set> states, int version) + { + if (version < MessagingService.VERSION_40) + { + Set> filteredStates = new HashSet<>(); + for (Map.Entry state : states) + filteredStates.addAll(filterOutgoingState(state, version).entrySet()); + return filteredStates; + } + return states; + } + + private static Map filterOutgoingState(Map.Entry state, int version) + { + assert version < MessagingService.VERSION_40; + VersionedValue vv = state.getValue(); + if (logger.isTraceEnabled()) + logger.trace("Fetching the key from state {}({}) with value of {}", state.getKey(), state.getKey().ordinal(), vv); + String[] values; + switch (state.getKey()) + { + case INTERNAL_ADDRESS_AND_PORT: + values = splitAddressAndPort(vv); + if (values.length > 1) + return Map.of(ApplicationState.values()[7], VersionedValue.unsafeMakeVersionedValue(values[0], vv.version), + ApplicationState.values()[17], VersionedValue.unsafeMakeVersionedValue(values[1], vv.version)); + else + return Map.of(ApplicationState.values()[17], VersionedValue.unsafeMakeVersionedValue(vv.value, vv.version)); + case NATIVE_ADDRESS_AND_PORT: + values = splitAddressAndPort(vv); + if (values.length > 1) + return Map.of(ApplicationState.values()[15], VersionedValue.unsafeMakeVersionedValue(values[1], vv.version)); + else + return Map.of(ApplicationState.values()[15], VersionedValue.unsafeMakeVersionedValue(vv.value, vv.version)); + case STATUS_WITH_PORT: + values = splitStatusAndPort(vv); + if (values.length > 1) + return Map.of(ApplicationState.values()[0], VersionedValue.unsafeMakeVersionedValue(values[0], vv.version)); + else + return Map.of(ApplicationState.values()[0], VersionedValue.unsafeMakeVersionedValue(vv.value, vv.version)); + case DISK_USAGE: + return Map.of(ApplicationState.values()[21], state.getValue()); + case SSTABLE_VERSIONS: + return Map.of(); + case INDEX_STATUS: + return Map.of(); + case RELEASE_VERSION: + return Map.of(ApplicationState.RELEASE_VERSION, filterValue(state.getKey(), vv, version)); + default: + return Map.of(state.getKey(), state.getValue()); + } + } + + private static String[] splitStatusAndPort(VersionedValue vv) + { + return vv.value.split("[:,]"); + } + + private static String[] splitAddressAndPort(VersionedValue vv) + { + return vv.value.split(":"); + } + + @VisibleForTesting + static Map filterIncomingStates(Map states, int version) + { + if (version < MessagingService.VERSION_40) + { + Map filteredStates = new EnumMap<>(ApplicationState.class); + for (Map.Entry state : states.entrySet()) + { + VersionedValue vv = state.getValue(); + if (logger.isTraceEnabled()) + logger.trace("Storing the key to state {}({}) with value of {}", state.getKey(), state.getKey().ordinal(), vv); + switch (state.getKey().ordinal()) + { + case 15: + filteredStates.put(ApplicationState.NATIVE_ADDRESS_AND_PORT, vv); + break; + case 16: + break; + case 17: + filteredStates.put(ApplicationState.INTERNAL_ADDRESS_AND_PORT, vv); + break; + case 18: + case 19: + case 20: + break; + case 21: + filteredStates.put(ApplicationState.DISK_USAGE, vv); + break; + default: + filteredStates.put(state.getKey(), vv); + } + } + return filteredStates; + } + return states; + } } diff --git a/src/java/org/apache/cassandra/gms/FailureDetector.java b/src/java/org/apache/cassandra/gms/FailureDetector.java index c2e148de1f28..67d9bff0796f 100644 --- a/src/java/org/apache/cassandra/gms/FailureDetector.java +++ b/src/java/org/apache/cassandra/gms/FailureDetector.java @@ -32,7 +32,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; import javax.management.openmbean.CompositeType; @@ -50,7 +49,6 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.Replica; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MBeanWrapper; @@ -58,7 +56,6 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.FD_MAX_INTERVAL_MS; import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR; import static org.apache.cassandra.config.CassandraRelevantProperties.MAX_LOCAL_PAUSE_IN_MS; -import static org.apache.cassandra.config.DatabaseDescriptor.newFailureDetector; import static org.apache.cassandra.utils.MonotonicClock.Global.preciseTime; /** @@ -88,10 +85,6 @@ private static long getMaxLocalPause() return pause * 1000000L; } - public static final IFailureDetector instance = newFailureDetector(); - public static final Predicate isEndpointAlive = instance::isAlive; - public static final Predicate isReplicaAlive = r -> isEndpointAlive.test(r.endpoint()); - // this is useless except to provide backwards compatibility in phi_convict_threshold, // because everyone seems pretty accustomed to the default of 8, and users who have // already tuned their phi_convict_threshold for their own environments won't need to @@ -305,8 +298,8 @@ public boolean isAlive(InetAddressAndPort ep) // we could assert not-null, but having isAlive fail screws a node over so badly that // it's worth being defensive here so minor bugs don't cause disproportionate // badness. (See CASSANDRA-1463 for an example). - if (epState == null) - logger.error("Unknown endpoint: " + ep, new IllegalArgumentException("")); + if (epState == null && Gossiper.instance.isEnabled()) + logger.error("Unknown endpoint: " + ep, new IllegalArgumentException("Unknown endpoint: " + ep)); return epState != null && epState.isAlive(); } diff --git a/src/java/org/apache/cassandra/gms/GossipDigestAck.java b/src/java/org/apache/cassandra/gms/GossipDigestAck.java index 26494eaba9d4..93488ae11cc8 100644 --- a/src/java/org/apache/cassandra/gms/GossipDigestAck.java +++ b/src/java/org/apache/cassandra/gms/GossipDigestAck.java @@ -60,6 +60,7 @@ Map getEndpointStateMap() class GossipDigestAckSerializer implements IVersionedSerializer { + @Override public void serialize(GossipDigestAck gDigestAckMessage, DataOutputPlus out, int version) throws IOException { GossipDigestSerializationHelper.serialize(gDigestAckMessage.gDigestList, out, version); @@ -72,6 +73,7 @@ public void serialize(GossipDigestAck gDigestAckMessage, DataOutputPlus out, int } } + @Override public GossipDigestAck deserialize(DataInputPlus in, int version) throws IOException { List gDigestList = GossipDigestSerializationHelper.deserialize(in, version); @@ -87,9 +89,10 @@ public GossipDigestAck deserialize(DataInputPlus in, int version) throws IOExcep return new GossipDigestAck(gDigestList, epStateMap); } + @Override public long serializedSize(GossipDigestAck ack, int version) { - int size = GossipDigestSerializationHelper.serializedSize(ack.gDigestList, version); + long size = GossipDigestSerializationHelper.serializedSize(ack.gDigestList, version); size += TypeSizes.sizeof(ack.epStateMap.size()); for (Map.Entry entry : ack.epStateMap.entrySet()) size += inetAddressAndPortSerializer.serializedSize(entry.getKey(), version) diff --git a/src/java/org/apache/cassandra/gms/GossipDigestSyn.java b/src/java/org/apache/cassandra/gms/GossipDigestSyn.java index 7c2ae945c80a..9e4fd0907ee8 100644 --- a/src/java/org/apache/cassandra/gms/GossipDigestSyn.java +++ b/src/java/org/apache/cassandra/gms/GossipDigestSyn.java @@ -70,9 +70,9 @@ static List deserialize(DataInputPlus in, int version) throws IOEx return gDigests; } - static int serializedSize(List digests, int version) + static long serializedSize(List digests, int version) { - int size = TypeSizes.sizeof(digests.size()); + long size = TypeSizes.sizeof(digests.size()); for (GossipDigest digest : digests) size += GossipDigest.serializer.serializedSize(digest, version); return size; diff --git a/src/java/org/apache/cassandra/gms/Gossiper.java b/src/java/org/apache/cassandra/gms/Gossiper.java index 647441ffbae5..1cc8798782c9 100644 --- a/src/java/org/apache/cassandra/gms/Gossiper.java +++ b/src/java/org/apache/cassandra/gms/Gossiper.java @@ -17,6 +17,9 @@ */ package org.apache.cassandra.gms; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; @@ -41,10 +44,12 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BooleanSupplier; import java.util.function.Supplier; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; @@ -55,6 +60,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Uninterruptibles; +import org.apache.cassandra.utils.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,28 +70,25 @@ import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.CassandraVersion; -import org.apache.cassandra.utils.ExecutorUtils; -import org.apache.cassandra.utils.ExpiringMemoizingSupplier; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.JVMStabilityInspector; -import org.apache.cassandra.utils.MBeanWrapper; -import org.apache.cassandra.utils.NoSpamLogger; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.RecomputingSupplier; import org.apache.cassandra.utils.concurrent.NotScheduledFuture; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_GOSSIP_ENDPOINT_REMOVAL; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CLUSTER_VERSION_PROVIDER_CLASS_NAME; +import static org.apache.cassandra.config.CassandraRelevantProperties.CLUSTER_VERSION_PROVIDER_MIN_STABLE_DURATION; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIPER_QUARANTINE_DELAY; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIPER_SKIP_WAITING_TO_SETTLE; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; @@ -163,7 +166,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, /* live member set */ @VisibleForTesting - final Set liveEndpoints = new ConcurrentSkipListSet<>(); + public final Set liveEndpoints = new ConcurrentSkipListSet<>(); /* Inflight echo requests. */ private final Map inflightEcho = new ConcurrentHashMap<>(); @@ -195,16 +198,9 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, private volatile long lastProcessedMessageAt = currentTimeMillis(); - /** - * This property is initially set to {@code true} which means that we have no information about the other nodes. - * Once all nodes are on at least this node version, it becomes {@code false}, which means that we are not - * upgrading from the previous version (major, minor). - * - * This property and anything that checks it should be removed in 5.0 - */ - private volatile boolean upgradeInProgressPossible = true; private volatile boolean hasNodeWithUnknownVersion = false; + @VisibleForTesting public void clearUnsafe() { unreachableEndpoints.clear(); @@ -212,71 +208,136 @@ public void clearUnsafe() inflightEcho.clear(); justRemovedEndpoints.clear(); expireTimeEndpointMap.clear(); + endpointStateMap.values().forEach(EndpointState::maybeRemoveUpdater); endpointStateMap.clear(); endpointShadowStateMap.clear(); seedsInShadowRound.clear(); + Nodes.peers().get().forEach(peer -> Nodes.peers().remove(peer.getPeerAddressAndPort(), true, true)); } - // returns true when the node does not know the existence of other nodes. - private static boolean isLoneNode(Map epStates) + private class DefaultClusterVersionProvider implements IClusterVersionProvider { - return epStates.isEmpty() || epStates.keySet().equals(Collections.singleton(FBUtilities.getBroadcastAddressAndPort())); - } + // -1L means that the cluster may be in upgrading state; positive value is the timestamp when the cluster + // was detected as fully upgraded + private final AtomicLong notUpgradingSinceMillis = new AtomicLong(-1L); - private static final ExpiringMemoizingSupplier.Memoized NO_UPGRADE_IN_PROGRESS = new ExpiringMemoizingSupplier.Memoized<>(null); - private static final ExpiringMemoizingSupplier.NotMemoized CURRENT_NODE_VERSION = new ExpiringMemoizingSupplier.NotMemoized<>(SystemKeyspace.CURRENT_VERSION); - final Supplier> upgradeFromVersionSupplier = () -> - { - // Once there are no prior version nodes we don't need to keep rechecking - if (!upgradeInProgressPossible) - return NO_UPGRADE_IN_PROGRESS; + // minimum time that needs to pass after the cluster is detected as fully upgraded + // to report that there is no upgrade in progress + private final long MIN_STABLE_DURATION_MS = CLUSTER_VERSION_PROVIDER_MIN_STABLE_DURATION.getLong(); - CassandraVersion minVersion = SystemKeyspace.CURRENT_VERSION; + private final Supplier> upgradeFromVersionSupplier = () -> + { + long notUpgradingSinceMillis = this.notUpgradingSinceMillis.get(); + long stableDuration = notUpgradingSinceMillis < 0 ? -1 : Clock.Global.currentTimeMillis() - notUpgradingSinceMillis; - // Skip the round if the gossiper has not started yet - // Otherwise, upgradeInProgressPossible can be set to false wrongly. - // If we don't know any epstate we don't know anything about the cluster. - // If we only know about ourselves, we can assume that version is CURRENT_VERSION - if (!isEnabled() || isLoneNode(endpointStateMap)) - return CURRENT_NODE_VERSION; + // The cluster is upgraded + if (stableDuration > 0) + return new ExpiringMemoizingSupplier.Memoized<>(SystemKeyspace.CURRENT_VERSION); - // Check the release version of all the peers it heard of. Not necessary the peer that it has/had contacted with. - hasNodeWithUnknownVersion = false; - for (Entry entry : endpointStateMap.entrySet()) - { + if (!isEnabled()) + { + // start the stabilisation period by setting the current timestamp in notUpgradingSinceMillis + // if Gossiper is going to be enabled, it will be enabled quickly + if (DatabaseDescriptor.isDaemonInitialized()) + { + if (CassandraRelevantProperties.CLUSTER_VERSION_PROVIDER_SKIP_WAIT_FOR_GOSSIP.getBoolean()) + this.notUpgradingSinceMillis.compareAndSet(notUpgradingSinceMillis, Clock.Global.currentTimeMillis()); - if (justRemovedEndpoints.containsKey(entry.getKey())) - continue; + return new ExpiringMemoizingSupplier.NotMemoized<>(SystemKeyspace.CURRENT_VERSION); + } + else + { + // it is not going to be enabled because we are not running in server mode + if (this.notUpgradingSinceMillis.compareAndSet(notUpgradingSinceMillis, 0)) // set 0 to make it stable + return new ExpiringMemoizingSupplier.Memoized<>(SystemKeyspace.CURRENT_VERSION); + else + return new ExpiringMemoizingSupplier.NotMemoized<>(SystemKeyspace.CURRENT_VERSION); + } + } + + // Check the release version of all the peers it heard of. Not necessary the peer that it has/had contacted with. + CassandraVersion minVersion = SystemKeyspace.CURRENT_VERSION; + hasNodeWithUnknownVersion = false; + for (Entry entry : endpointStateMap.entrySet()) + { - CassandraVersion version = getReleaseVersion(entry.getKey()); + if (justRemovedEndpoints.containsKey(entry.getKey())) + continue; - // if it is dead state, we skip the version check - if (isDeadState(entry.getValue())) - continue; - //Raced with changes to gossip state, wait until next iteration - if (version == null) - hasNodeWithUnknownVersion = true; - else if (version.compareTo(minVersion) < 0) - minVersion = version; - } + CassandraVersion version = getReleaseVersion(entry.getKey()); + + // if it is dead state, we skip the version check + if (isDeadState(entry.getValue())) + continue; + //Raced with changes to gossip state, wait until next iteration + if (version == null) + hasNodeWithUnknownVersion = true; + else if (version.compareTo(minVersion) < 0) + minVersion = version; + } - if (minVersion.compareTo(SystemKeyspace.CURRENT_VERSION) < 0) - return new ExpiringMemoizingSupplier.Memoized<>(minVersion); + // remember the minumum version for the expiration duration + if (minVersion.compareTo(SystemKeyspace.CURRENT_VERSION) < 0) + return new ExpiringMemoizingSupplier.Memoized<>(minVersion); - if (hasNodeWithUnknownVersion) - return new ExpiringMemoizingSupplier.NotMemoized<>(minVersion); + // don't remember the minimum version and recheck whenever requested + if (hasNodeWithUnknownVersion) + return new ExpiringMemoizingSupplier.NotMemoized<>(minVersion); - upgradeInProgressPossible = false; - return NO_UPGRADE_IN_PROGRESS; - }; + // all hosts have known versions and == CURRENT_VERSION, we can stop checking - the cluster is fully upgraded + // start the stability period by setting the current timestamp in notUpgradingSinceMillis + if (this.notUpgradingSinceMillis.compareAndSet(notUpgradingSinceMillis, Clock.Global.currentTimeMillis())) + return new ExpiringMemoizingSupplier.Memoized<>(minVersion); + else + return new ExpiringMemoizingSupplier.NotMemoized<>(minVersion); + }; - private final Supplier upgradeFromVersionMemoized = ExpiringMemoizingSupplier.memoizeWithExpiration(upgradeFromVersionSupplier, 1, TimeUnit.MINUTES); + private final ExpiringMemoizingSupplier minVersionMemoized = ExpiringMemoizingSupplier.memoizeWithExpiration(upgradeFromVersionSupplier, 60, TimeUnit.SECONDS); + @Override + public void reset() + { + notUpgradingSinceMillis.set(-1L); + minVersionMemoized.expire(); + } + + @Override + public CassandraVersion getMinClusterVersion() + { + return minVersionMemoized.get(); + } + + @Override + public boolean isUpgradeInProgress() + { + long notUpgradingSince = this.notUpgradingSinceMillis.get(); + long stableDuration = notUpgradingSince < 0 ? -1 : Clock.Global.currentTimeMillis() - notUpgradingSince; + return stableDuration < MIN_STABLE_DURATION_MS; + } + } + + // For testing only @VisibleForTesting - public void expireUpgradeFromVersion() + public void setNotUpgradingSinceMillisUnsafe(long notUpgradingSinceMillis) { - upgradeInProgressPossible = true; - ((ExpiringMemoizingSupplier) upgradeFromVersionMemoized).expire(); + ((DefaultClusterVersionProvider) clusterVersionProvider).notUpgradingSinceMillis.set(notUpgradingSinceMillis); + } + + @VisibleForTesting + public final IClusterVersionProvider clusterVersionProvider; + + private static IClusterVersionProvider maybeCustomClusterVersionProvider() + { + IClusterVersionProvider clusterVersionProvider = null; + String className = CLUSTER_VERSION_PROVIDER_CLASS_NAME.getString(); + if (className != null) + { + clusterVersionProvider = FBUtilities.instanceOrConstruct(className,"Custom implementation of " + IClusterVersionProvider.class.getSimpleName()); + if (clusterVersionProvider != null) + logger.info("Using custom implementation of {}: {} - {}", IClusterVersionProvider.class.getSimpleName(), className, clusterVersionProvider); + } + + return clusterVersionProvider; } private static final boolean disableThreadValidation = GOSSIP_DISABLE_THREAD_VALIDATION.getBoolean(); @@ -398,13 +459,19 @@ public void run() } } - private final RecomputingSupplier minVersionSupplier = new RecomputingSupplier<>(this::computeMinVersion, executor); + public Gossiper(boolean registerJmx) + { + this(registerJmx, maybeCustomClusterVersionProvider()); + } @VisibleForTesting - public Gossiper(boolean registerJmx) + public Gossiper(boolean registerJmx, IClusterVersionProvider customClusterVersionProvider) { + this.clusterVersionProvider = Objects.requireNonNullElseGet(customClusterVersionProvider, DefaultClusterVersionProvider::new); + logger.info("Using cluster version provider {}: {}", this.clusterVersionProvider.getClass().getName(), clusterVersionProvider); + /* register with the Failure Detector for receiving Failure detector events */ - FailureDetector.instance.registerFailureDetectionEventListener(this); + IFailureDetector.instance.registerFailureDetectionEventListener(this); // Register this instance with JMX if (registerJmx) @@ -414,26 +481,29 @@ public Gossiper(boolean registerJmx) subscribers.add(new IEndpointStateChangeSubscriber() { + @Override public void onJoin(InetAddressAndPort endpoint, EndpointState state) - { + { maybeRecompute(state); } + @Override public void onAlive(InetAddressAndPort endpoint, EndpointState state) - { + { maybeRecompute(state); } private void maybeRecompute(EndpointState state) - { + { if (state.getApplicationState(ApplicationState.RELEASE_VERSION) != null) - minVersionSupplier.recompute(); + Gossiper.this.clusterVersionProvider.reset(); } + @Override public void onChange(InetAddressAndPort endpoint, ApplicationState state, VersionedValue value) { if (state == ApplicationState.RELEASE_VERSION) - minVersionSupplier.recompute(); + Gossiper.this.clusterVersionProvider.reset(); } }); } @@ -644,7 +714,7 @@ protected void markAsShutdown(InetAddressAndPort endpoint) epState.addApplicationState(ApplicationState.RPC_READY, StorageService.instance.valueFactory.rpcReady(false)); epState.forceHighestPossibleVersionUnsafe(); markDead(endpoint, epState); - FailureDetector.instance.forceConviction(endpoint); + IFailureDetector.instance.forceConviction(endpoint); GossiperDiagnostics.markedAsShutdown(this, endpoint); for (IEndpointStateChangeSubscriber subscriber : subscribers) subscriber.onChange(endpoint, ApplicationState.STATUS_WITH_PORT, shutdown); @@ -700,10 +770,12 @@ static int getMaxEndpointStateVersion(EndpointState epState) private void evictFromMembership(InetAddressAndPort endpoint) { checkProperThreadForStateMutation(); + inflightEcho.remove(endpoint); unreachableEndpoints.remove(endpoint); - endpointStateMap.remove(endpoint); + removeEndpointState(endpoint); + Nodes.peers().remove(endpoint, true, true); expireTimeEndpointMap.remove(endpoint); - FailureDetector.instance.remove(endpoint); + IFailureDetector.instance.remove(endpoint); quarantineEndpoint(endpoint); if (logger.isDebugEnabled()) logger.debug("evicting {} from gossip", endpoint); @@ -733,6 +805,10 @@ public void removeEndpoint(InetAddressAndPort endpoint) if (disableEndpointRemoval) return; + endpointStateMap.computeIfPresent(endpoint, (key, value) -> { + value.maybeRemoveUpdater(); + return value; + }); liveEndpoints.remove(endpoint); unreachableEndpoints.remove(endpoint); MessagingService.instance().versions.reset(endpoint); @@ -870,7 +946,7 @@ public void advertiseRemoving(InetAddressAndPort endpoint, UUID hostId, UUID loc states.put(ApplicationState.STATUS, StorageService.instance.valueFactory.removingNonlocal(hostId)); states.put(ApplicationState.REMOVAL_COORDINATOR, StorageService.instance.valueFactory.removalCoordinator(localHostId)); epState.addApplicationStates(states); - endpointStateMap.put(endpoint, epState); + putEndpointState(endpoint, epState); } /** @@ -890,7 +966,7 @@ public void advertiseTokenRemoved(InetAddressAndPort endpoint, UUID hostId) epState.addApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.removedNonlocal(hostId, expireTime)); logger.info("Completing removal of {}", endpoint); addExpireTimeForEndpoint(endpoint, expireTime); - endpointStateMap.put(endpoint, epState); + putEndpointState(endpoint, epState); // ensure at least one gossip round occurs before returning Uninterruptibles.sleepUninterruptibly(intervalInMillis * 2, TimeUnit.MILLISECONDS); } @@ -962,9 +1038,128 @@ else if (newState.getHeartBeatState().getHeartBeatVersion() != heartbeat) }); } - public boolean isKnownEndpoint(InetAddressAndPort endpoint) + public void reviveEndpoint(String address) throws UnknownHostException + { + InetAddressAndPort endpoint = InetAddressAndPort.getByName(address); + EndpointState epState = endpointStateMap.get(endpoint); + logger.warn("Reviving {} via gossip", endpoint); + + if (epState == null) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": no endpoint-state"); + + int generation = epState.getHeartBeatState().getGeneration(); + int heartbeat = epState.getHeartBeatState().getHeartBeatVersion(); + + logger.info("Have endpoint-state for {}: status={}, generation={}, heartbeat={}", + endpoint, epState.getStatus(), generation, heartbeat); + + if (!isSilentShutdownState(epState)) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": not in a (silent) shutdown state: " + epState.getStatus()); + + if (FailureDetector.instance.isAlive(endpoint)) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": still alive (failure-detector)"); + + logger.info("Sleeping for {}ms to ensure {} does not change", StorageService.RING_DELAY_MILLIS, endpoint); + Uninterruptibles.sleepUninterruptibly(StorageService.RING_DELAY_MILLIS, TimeUnit.MILLISECONDS); + // make sure the endpoint state did not change + EndpointState newState = endpointStateMap.get(endpoint); + if (newState == null) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": endpoint-state disappeared"); + if (newState.getHeartBeatState().getGeneration() != generation) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": still alive, generation changed while trying to reviving it"); + if (newState.getHeartBeatState().getHeartBeatVersion() != heartbeat) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": still alive, heartbeat changed while trying to reviving it"); + + epState.updateTimestamp(); // make sure we don't evict it too soon + epState.forceNewerGenerationUnsafe(); + + // using the tokens from the endpoint-state as that is the real source of truth + Collection tokens = getTokensFromEndpointState(epState, DatabaseDescriptor.getPartitioner()); + if (tokens == null || tokens.isEmpty()) + throw new RuntimeException("Cannot revive endpoint " + endpoint + ": no tokens from TokenMetadata"); + + epState.addApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.normal(tokens)); + epState.addApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.normal(tokens)); + handleMajorStateChange(endpoint, epState); + Uninterruptibles.sleepUninterruptibly(intervalInMillis * 4, TimeUnit.MILLISECONDS); + logger.warn("Finished reviving {}, status={}, generation={}, heartbeat={}", + endpoint, epState.getStatus(), generation, heartbeat); + } + + public void unsafeSetEndpointState(String address, String status) throws UnknownHostException + { + logger.warn("Forcibly changing gossip status of " + address + " to " + status); + + InetAddressAndPort endpoint = InetAddressAndPort.getByName(address); + EndpointState epState = endpointStateMap.get(endpoint); + + if (epState == null) + throw new RuntimeException("No state for endpoint " + endpoint); + + int generation = epState.getHeartBeatState().getGeneration(); + int heartbeat = epState.getHeartBeatState().getHeartBeatVersion(); + + logger.info("Have endpoint-state for {}: status={}, generation={}, heartbeat={}", + endpoint, epState.getStatus(), generation, heartbeat); + + if (FailureDetector.instance.isAlive(endpoint)) + throw new RuntimeException("Cannot update status for endpoint " + endpoint + ": still alive (failure-detector)"); + + Collection tokens = getTokensFromEndpointState(epState, DatabaseDescriptor.getPartitioner()); + + VersionedValue newStatus; + switch (status.toLowerCase()) + { + case "hibernate": + newStatus = StorageService.instance.valueFactory.hibernate(true); + break; + case "normal": + newStatus = StorageService.instance.valueFactory.normal(tokens); + break; + case "left": + newStatus = StorageService.instance.valueFactory.left(tokens, computeExpireTime()); + break; + case "shutdown": + newStatus = StorageService.instance.valueFactory.shutdown(true); + break; + default: + throw new IllegalArgumentException("Unknown status '" + status + '\''); + } + + epState.updateTimestamp(); // make sure we don't evict it too soon + epState.forceNewerGenerationUnsafe(); + + epState.addApplicationState(ApplicationState.STATUS, newStatus); + epState.addApplicationState(ApplicationState.STATUS_WITH_PORT, newStatus); + + handleMajorStateChange(endpoint, epState); + + logger.warn("Forcibly changed gossip status of " + endpoint + " to " + newStatus); + } + + public Collection getTokensFor(InetAddressAndPort endpoint, IPartitioner partitioner) { - return endpointStateMap.containsKey(endpoint); + EndpointState state = getEndpointStateForEndpoint(endpoint); + if (state == null) + return Collections.emptyList(); + + return getTokensFromEndpointState(state, partitioner); + } + + private Collection getTokensFromEndpointState(EndpointState state, IPartitioner partitioner) + { + try + { + VersionedValue versionedValue = state.getApplicationState(ApplicationState.TOKENS); + if (versionedValue == null) + return Collections.emptyList(); + + return TokenSerializer.deserialize(partitioner, new DataInputStream(new ByteArrayInputStream(versionedValue.toBytes()))); + } + catch (IOException e) + { + throw new RuntimeException(e); + } } public int getCurrentGenerationNumber(InetAddressAndPort endpoint) @@ -1129,7 +1324,7 @@ void doStatusCheck() long now = currentTimeMillis(); long nowNano = nanoTime(); - long pending = Stage.GOSSIP.executor().getPendingTaskCount(); + long pending = Stage.GOSSIP.getPendingTaskCount(); if (pending > 0 && lastProcessedMessageAt < now - 1000) { // if some new messages just arrived, give the executor some time to work on them @@ -1149,7 +1344,7 @@ void doStatusCheck() if (endpoint.equals(getBroadcastAddressAndPort())) continue; - FailureDetector.instance.interpret(endpoint); + IFailureDetector.instance.interpret(endpoint); EndpointState epState = endpointStateMap.get(endpoint); if (epState != null) { @@ -1292,11 +1487,6 @@ long getLastProcessedMessageAt() return lastProcessedMessageAt; } - public UUID getHostId(InetAddressAndPort endpoint) - { - return getHostId(endpoint, endpointStateMap); - } - public UUID getHostId(InetAddressAndPort endpoint, Map epStates) { return UUID.fromString(epStates.get(endpoint).getApplicationState(ApplicationState.HOST_ID).value); @@ -1400,7 +1590,7 @@ void notifyFailureDetector(InetAddressAndPort endpoint, EndpointState remoteEndp */ if (localEndpointState != null) { - IFailureDetector fd = FailureDetector.instance; + IFailureDetector fd = IFailureDetector.instance; int localGeneration = localEndpointState.getHeartBeatState().getGeneration(); int remoteGeneration = remoteEndpointState.getHeartBeatState().getGeneration(); if (remoteGeneration > localGeneration) @@ -1442,13 +1632,30 @@ private void markAlive(final InetAddressAndPort addr, final EndpointState localS { Message echoMessage = Message.out(ECHO_REQ, noPayload); logger.trace("Sending ECHO_REQ to {}", addr); - RequestCallback echoHandler = msg -> + RequestCallback echoHandler = new RequestCallback() { - runInGossipStageBlocking(() -> { - EndpointState epState = inflightEcho.remove(addr); - if (epState != null) - realMarkAlive(addr, epState); - }); + @Override + public void onResponse(Message msg) + { + runInGossipStageBlocking(() -> { + EndpointState epState = inflightEcho.remove(addr); + if (epState != null) + realMarkAlive(addr, epState); + }); + } + + @Override + public boolean invokeOnFailure() + { + return true; + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) + { + logger.trace("ECHO_REQ to {} failed ({})", addr, failureReason); + inflightEcho.remove(addr); + } }; MessagingService.instance().sendWithCallback(echoMessage, addr, echoHandler); } @@ -1513,11 +1720,13 @@ private void silentlyMarkDead(InetAddressAndPort addr, EndpointState localState) /** * This method is called whenever there is a "big" change in ep state (a generation change for a known node). + * It is public as the state change simulation is needed in testing, otherwise should not be used directly. * * @param ep endpoint * @param epState EndpointState for the endpoint */ - private void handleMajorStateChange(InetAddressAndPort ep, EndpointState epState) + @VisibleForTesting + public void handleMajorStateChange(InetAddressAndPort ep, EndpointState epState) { checkProperThreadForStateMutation(); EndpointState localEpState = endpointStateMap.get(ep); @@ -1530,7 +1739,8 @@ private void handleMajorStateChange(InetAddressAndPort ep, EndpointState epState } if (logger.isTraceEnabled()) logger.trace("Adding endpoint state for {}", ep); - endpointStateMap.put(ep, epState); + + putEndpointState(ep, epState); if (localEpState != null) { // the node restarted: it is up to the subscriber to take whatever action is necessary @@ -1760,7 +1970,7 @@ else if (logger.isTraceEnabled()) else { // this is a new node, report it to the FD in case it is the first time we are seeing it AND it's not alive - FailureDetector.instance.report(ep); + IFailureDetector.instance.report(ep); handleMajorStateChange(ep, remoteState); } } @@ -1999,7 +2209,7 @@ public void start(int generationNbr, Map prelo maybeInitializeLocalState(generationNbr); EndpointState localState = endpointStateMap.get(getBroadcastAddressAndPort()); localState.addApplicationStates(preloadLocalStates); - minVersionSupplier.recompute(); + clusterVersionProvider.reset(); //notify snitches that Gossiper is about to start DatabaseDescriptor.getEndpointSnitch().gossiperStarting(); @@ -2181,7 +2391,7 @@ public void maybeInitializeLocalState(int generationNbr) HeartBeatState hbState = new HeartBeatState(generationNbr); EndpointState localState = new EndpointState(hbState); localState.markAlive(); - endpointStateMap.putIfAbsent(getBroadcastAddressAndPort(), localState); + putEndpointStateIfAbsent(FBUtilities.getBroadcastAddressAndPort(), localState); } public void forceNewerGeneration() @@ -2217,7 +2427,7 @@ public void addSavedEndpoint(InetAddressAndPort ep) } epState.markDead(); - endpointStateMap.put(ep, epState); + putEndpointState(ep, epState); silentlyMarkDead(ep, epState); if (logger.isTraceEnabled()) logger.trace("Adding saved endpoint {} {}", ep, epState.getHeartBeatState().getGeneration()); @@ -2267,6 +2477,7 @@ public void stop() EndpointState mystate = endpointStateMap.get(getBroadcastAddressAndPort()); if (mystate != null && !isSilentShutdownState(mystate) && StorageService.instance.isJoined()) { + // HCD-73 note: not using announceShutdown() here because we clone the EndpointState for the message payload logger.info("Announcing shutdown"); addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.shutdown(true)); addLocalApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.shutdown(true)); @@ -2283,6 +2494,21 @@ public void stop() scheduledGossipTask.cancel(false); } + /** + * This method sends the node shutdown status to all live endpoints. + * It does not close the gossiper itself. + */ + public void announceShutdown() + { + logger.info("Announcing shutdown"); + addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.shutdown(true)); + addLocalApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.shutdown(true)); + Message message = Message.out(Verb.GOSSIP_SHUTDOWN, noPayload); + for (InetAddressAndPort ep : liveEndpoints) + MessagingService.instance().send(message, ep); + Uninterruptibles.sleepUninterruptibly(SHUTDOWN_ANNOUNCE_DELAY_IN_MS.getInt(), TimeUnit.MILLISECONDS); + } + public boolean isEnabled() { ScheduledFuture scheduledGossipTask = this.scheduledGossipTask; @@ -2353,7 +2579,7 @@ public void initializeUnreachableNodeUnsafe(InetAddressAndPort addr) { EndpointState state = new EndpointState(HeartBeatState.empty()); state.markDead(); - EndpointState oldState = endpointStateMap.putIfAbsent(addr, state); + EndpointState oldState = putEndpointStateIfAbsent(addr, state); if (null != oldState) { throw new RuntimeException("Attempted to initialize endpoint state for unreachable node, " + @@ -2373,7 +2599,7 @@ public void initializeNodeUnsafe(InetAddressAndPort addr, UUID uuid, int netVers HeartBeatState hbState = new HeartBeatState(generationNbr); EndpointState newState = new EndpointState(hbState); newState.markAlive(); - EndpointState oldState = endpointStateMap.putIfAbsent(addr, newState); + EndpointState oldState = putEndpointStateIfAbsent(addr, newState); EndpointState localState = oldState == null ? newState : oldState; // always add the version state @@ -2391,6 +2617,8 @@ public void injectApplicationState(InetAddressAndPort endpoint, ApplicationState { EndpointState localState = endpointStateMap.get(endpoint); localState.addApplicationState(state, value); + localState.maybeSetUpdater(update -> Nodes.updateLocalOrPeer(endpoint, update, false)); + localState.maybeUpdate(); } public long getEndpointDowntime(String address) throws UnknownHostException @@ -2525,7 +2753,7 @@ public boolean waitForSchemaAgreement(long maxWait, TimeUnit unit, BooleanSuppli public boolean hasMajorVersion3OrUnknownNodes() { return isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0) || // this is quite obvious - // however if we discovered only nodes at current version so far (in particular only this node), + // this is not so obvious:// however if we discovered only nodes at current version so far (in particular only this node), // but still there are nodes with unknown version, we also want to report that the cluster may have nodes at 3.x hasNodeWithUnknownVersion; } @@ -2535,11 +2763,28 @@ public boolean hasMajorVersion3OrUnknownNodes() */ public boolean isUpgradingFromVersionLowerThan(CassandraVersion referenceVersion) { - CassandraVersion v = upgradeFromVersionMemoized.get(); - if (CassandraVersion.NULL_VERSION.equals(v) && scheduledGossipTask == null) - return false; + return getMinVersion().compareTo(referenceVersion) < 0; + } + + /** + * This is a safe way to get the version we are upgrading from. It will return the current version if the cluster + * is not in the upgrade state. If the cluster is in upgrade state, it will return NULL_VERSION, if there is no + * information about the other nodes yet. Otherwise, it will just return the minimum cluster version. + */ + public CassandraVersion getMinVersion() + { + CassandraVersion v = clusterVersionProvider.getMinClusterVersion(); + assert v != null : "API contract violation: cluster version provider implementation should never return null"; + + if (!clusterVersionProvider.isUpgradeInProgress()) + return v; - return v != null && v.compareTo(referenceVersion) < 0; + // we are in the upgrade state but since the minimum reported version is current version, we do not know + // anything about the other nodes + if (v.compareTo(SystemKeyspace.CURRENT_VERSION) < 0) + return v; + else + return CassandraVersion.NULL_VERSION; } private boolean nodesAgreeOnSchema(Collection nodes) @@ -2568,72 +2813,6 @@ public void stopShutdownAndWait(long timeout, TimeUnit unit) throws InterruptedE ExecutorUtils.shutdownAndWait(timeout, unit, executor); } - @Nullable - public CassandraVersion getMinVersion(long delay, TimeUnit timeUnit) - { - try - { - return minVersionSupplier.get(delay, timeUnit); - } - catch (TimeoutException e) - { - // Timeouts here are harmless: they won't cause reprepares and may only - // cause the old version of the hash to be kept for longer - return null; - } - catch (Throwable e) - { - logger.error("Caught an exception while waiting for min version", e); - return null; - } - } - - @Nullable - private String getReleaseVersionString(InetAddressAndPort ep) - { - EndpointState state = getEndpointStateForEndpoint(ep); - if (state == null) - return null; - - VersionedValue value = state.getApplicationState(ApplicationState.RELEASE_VERSION); - return value == null ? null : value.value; - } - - private CassandraVersion computeMinVersion() - { - CassandraVersion minVersion = null; - - for (InetAddressAndPort addr : Iterables.concat(Gossiper.instance.getLiveMembers(), - Gossiper.instance.getUnreachableMembers())) - { - String versionString = getReleaseVersionString(addr); - // Raced with changes to gossip state, wait until next iteration - if (versionString == null) - return null; - - CassandraVersion version; - - try - { - version = new CassandraVersion(versionString); - } - catch (Throwable t) - { - JVMStabilityInspector.inspectThrowable(t); - String message = String.format("Can't parse version string %s", versionString); - logger.warn(message); - if (logger.isDebugEnabled()) - logger.debug(message, t); - return null; - } - - if (minVersion == null || version.compareTo(minVersion) < 0) - minVersion = version; - } - - return minVersion; - } - @Override public boolean getLooseEmptyEnabled() { @@ -2743,4 +2922,39 @@ public Map> compareGossipAndTokenMetadata() } return mismatches; } + + private EndpointState putEndpointState(InetAddressAndPort endpoint, @Nonnull EndpointState state) + { + state.maybeSetUpdater(update -> Nodes.updateLocalOrPeer(endpoint, update, false)); + + EndpointState prev = endpointStateMap.put(endpoint, state); + if (prev != null && prev != state) + prev.maybeRemoveUpdater(); + + state.maybeUpdate(); + + return prev; + } + + private EndpointState putEndpointStateIfAbsent(InetAddressAndPort endpoint, @Nonnull EndpointState state) + { + state.maybeSetUpdater(update -> Nodes.updateLocalOrPeer(endpoint, update, false)); + + EndpointState prev = endpointStateMap.putIfAbsent(endpoint, state); + + if (prev != null && prev != state) + state.maybeRemoveUpdater(); + else + state.maybeUpdate(); + + return prev; + } + + private EndpointState removeEndpointState(InetAddressAndPort endpoint) + { + EndpointState removedState = endpointStateMap.remove(endpoint); + if (removedState != null) + removedState.maybeRemoveUpdater(); + return removedState; + } } diff --git a/src/java/org/apache/cassandra/gms/GossiperMBean.java b/src/java/org/apache/cassandra/gms/GossiperMBean.java index 5b2d4ed1503c..d058038b667c 100644 --- a/src/java/org/apache/cassandra/gms/GossiperMBean.java +++ b/src/java/org/apache/cassandra/gms/GossiperMBean.java @@ -29,8 +29,37 @@ public interface GossiperMBean public void unsafeAssassinateEndpoint(String address) throws UnknownHostException; + /** + * Do not call this method unless you know what you are doing. + * It will try extremely hard to obliterate any endpoint from the ring, + * even if it does not know about it. Sets gossip status to {@code left}. + * + * @param address endpoint to assassinate + */ public void assassinateEndpoint(String address) throws UnknownHostException; + /** + * Do not call this method unless you know what you are doing. + * In case a node went into a hibernate state - i.e. replacing a node with the same address + * or bootstrapping a node without letting join the ring - and it's required to bring that node back + * to a normal status (e.g. for a failed replace operation), use this method. + * It can be called on any node, prefer a seed node, to set the status back to {@code normal}. + * + * @param address endpoint to revive + */ + public void reviveEndpoint(String address) throws UnknownHostException; + + /** + * Completely unsafe method to set the Gossip status of an endpoint. + * Primary intention is for testing only. + * The method will refuse the request if (and only if) {@link FailureDetector} - no further + * lifetime checks nor gossip state change safety barrier. + * + * @param address endpoint address + * @param status One of {@code hibernate}, {@code normal}, {@code left}, {@code shutdown} + */ + public void unsafeSetEndpointState(String address, String status) throws UnknownHostException; + public List reloadSeeds(); public List getSeeds(); diff --git a/src/java/org/apache/cassandra/gms/IClusterVersionProvider.java b/src/java/org/apache/cassandra/gms/IClusterVersionProvider.java new file mode 100644 index 000000000000..27d0b373efb4 --- /dev/null +++ b/src/java/org/apache/cassandra/gms/IClusterVersionProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.gms; + +import org.apache.cassandra.utils.CassandraVersion; + +public interface IClusterVersionProvider +{ + /** + * Returns the minimum Cassandra version of the nodes in the cluster. The method skips the nodes that have no or + * invalid version. However, if such nodes are present, {@link #isUpgradeInProgress()}} returns {@code true}. + */ + CassandraVersion getMinClusterVersion(); + + /** + * Resets the provider to its initial state. This is called by the {@link Gossiper} when nodes join or becomes alive. + * It can be also called by the tests to verify the behaviour. + */ + void reset(); + + /** + * Returns {@code true} if the cluster is in the middle of an upgrade. This is the case when the provider has + * detected that some nodes have no or invalid version. May also implement some grace period to avoid flapping. + */ + boolean isUpgradeInProgress(); +} diff --git a/src/java/org/apache/cassandra/gms/IFailureDetector.java b/src/java/org/apache/cassandra/gms/IFailureDetector.java index 62fc97dbbaba..0c694e1647e4 100644 --- a/src/java/org/apache/cassandra/gms/IFailureDetector.java +++ b/src/java/org/apache/cassandra/gms/IFailureDetector.java @@ -17,7 +17,13 @@ */ package org.apache.cassandra.gms; +import java.util.function.Predicate; + import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_FAILURE_DETECTOR_PROPERTY; /** * An interface that provides an application with the ability @@ -28,6 +34,14 @@ public interface IFailureDetector { + IFailureDetector instance = CUSTOM_FAILURE_DETECTOR_PROPERTY.isPresent() + ? FBUtilities.construct(CUSTOM_FAILURE_DETECTOR_PROPERTY.getString(), + "Failure Detector") + : new FailureDetector(); + + public static final Predicate isEndpointAlive = instance::isAlive; + public static final Predicate isReplicaAlive = r -> isEndpointAlive.test(r.endpoint()); + /** * Failure Detector's knowledge of whether a node is up or * down. diff --git a/src/java/org/apache/cassandra/gms/IGossiper.java b/src/java/org/apache/cassandra/gms/IGossiper.java index aa9d95a97d45..aa5a9db762f5 100644 --- a/src/java/org/apache/cassandra/gms/IGossiper.java +++ b/src/java/org/apache/cassandra/gms/IGossiper.java @@ -37,6 +37,9 @@ public interface IGossiper default CassandraVersion getReleaseVersion(InetAddressAndPort ep) { EndpointState state = getEndpointStateForEndpoint(ep); - return state != null ? state.getReleaseVersion() : null; + VersionedValue applicationState = state != null ? state.getApplicationState(ApplicationState.RELEASE_VERSION) : null; + return applicationState != null + ? new CassandraVersion(applicationState.value) + : null; } } diff --git a/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java b/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java index b57f898a7443..7bed57b3c3ae 100644 --- a/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java +++ b/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java @@ -26,7 +26,6 @@ import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.util.*; import org.apache.cassandra.utils.Throwables; -import org.apache.cassandra.utils.NativeLibrary; /** * A {@link RandomAccessReader} wrapper that calculates the CRC in place. @@ -221,7 +220,7 @@ protected void readBuffer() public void tryUncacheRead() { - NativeLibrary.trySkipCache(getChannel().getFileDescriptor(), 0, getSourcePosition(), getPath()); + getChannel().trySkipCache(0, getSourcePosition()); } private void updateCrc() @@ -245,9 +244,9 @@ public void close() channel.close(); } - protected String getPath() + protected File getFile() { - return channel.filePath(); + return channel.getFile(); } public ChannelProxy getChannel() diff --git a/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java b/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java index 8236364077aa..b2419425f890 100644 --- a/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java +++ b/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java @@ -147,7 +147,7 @@ protected void readBuffer() } catch (IOException e) { - throw new FSReadError(e, getPath()); + throw new FSReadError(e, getFile()); } } @@ -163,7 +163,7 @@ public static ChecksummedDataInput upgradeInput(ChecksummedDataInput input, ICom long position = input.getPosition(); input.close(); - ChannelProxy channel = new ChannelProxy(input.getPath()); + ChannelProxy channel = new ChannelProxy(input.getFile()); try { return new CompressedChecksummedDataInput(channel, compressor, position); diff --git a/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java b/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java index ab788b9ad6cd..dbb972491167 100644 --- a/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java +++ b/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java @@ -128,7 +128,7 @@ protected void readBuffer() } catch (IOException ioe) { - throw new FSReadError(ioe, getPath()); + throw new FSReadError(ioe, getFile()); } } @@ -137,7 +137,7 @@ public static ChecksummedDataInput upgradeInput(ChecksummedDataInput input, Ciph long position = input.getPosition(); input.close(); - ChannelProxy channel = new ChannelProxy(input.getPath()); + ChannelProxy channel = new ChannelProxy(input.getFile()); try { return new EncryptedChecksummedDataInput(channel, cipher, compressor, position); diff --git a/src/java/org/apache/cassandra/hints/Hint.java b/src/java/org/apache/cassandra/hints/Hint.java index 886e781ee638..0a9c8db40f3b 100644 --- a/src/java/org/apache/cassandra/hints/Hint.java +++ b/src/java/org/apache/cassandra/hints/Hint.java @@ -20,14 +20,15 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import com.google.common.primitives.Ints; -import javax.annotation.Nullable; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.WriteOptions; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; @@ -95,7 +96,7 @@ public static Hint create(Mutation mutation, long creationTime, int gcgs) /** * Applies the contained mutation unless it's expired, filtering out any updates for truncated tables */ - Future applyFuture() + public Future applyFuture() { if (isLive()) { @@ -106,7 +107,7 @@ Future applyFuture() filtered = filtered.without(id); if (!filtered.isEmpty()) - return filtered.applyFuture(); + return filtered.applyFuture(WriteOptions.FOR_HINT_REPLAY); } return ImmediateFuture.success(null); @@ -125,6 +126,14 @@ void apply() } } + /** + * @return the mutation stored in the hint + */ + public Mutation mutation() + { + return mutation; + } + /** * @return the overall ttl of the hint - the minimum of all mutation's tables' gc gs now and at the time of creation */ diff --git a/src/java/org/apache/cassandra/hints/HintMessage.java b/src/java/org/apache/cassandra/hints/HintMessage.java index 978ab4170191..29cfcc2f8ee4 100644 --- a/src/java/org/apache/cassandra/hints/HintMessage.java +++ b/src/java/org/apache/cassandra/hints/HintMessage.java @@ -74,6 +74,21 @@ public final class HintMessage implements SerializableHintMessage this.unknownTableID = unknownTableID; } + public UUID hostId() + { + return hostId; + } + + public Hint hint() + { + return hint; + } + + public TableId unknownTableID() + { + return unknownTableID; + } + public static class Serializer implements IVersionedAsymmetricSerializer { public long serializedSize(SerializableHintMessage obj, int version) @@ -95,9 +110,11 @@ else if (obj instanceof Encoded) { Encoded message = (Encoded) obj; - if (version != message.version) - throw new IllegalArgumentException("serializedSize() called with non-matching version " + version); - + // UUID serialization is version-independent, VInt encoding is version-independent, + // and hint bytes are written verbatim. The size is the same regardless of the version + // passed, so we don't need to check for version mismatch here. This allows encoded + // hints written at the storage compatibility version to be dispatched correctly + // even when the peer is at a different (newer) messaging version. long size = UUIDSerializer.serializer.serializedSize(message.hostId, version); size += TypeSizes.sizeofUnsignedVInt(message.hint.remaining()); size += message.hint.remaining(); @@ -131,9 +148,11 @@ else if (obj instanceof Encoded) { Encoded message = (Encoded) obj; - if (version != message.version) - throw new IllegalArgumentException("serialize() called with non-matching version " + version); - + // UUID serialization and VInt encoding are version-independent, and hint bytes are + // written verbatim (already encoded at message.version). This allows encoded hints + // written at the storage compatibility version to be dispatched correctly even when + // the peer is at a different (newer) messaging version. The receiver will deserialize + // the hint bytes using the appropriate version based on the hint file descriptor. UUIDSerializer.serializer.serialize(message.hostId, out, version); out.writeUnsignedVInt32(message.hint.remaining()); out.write(message.hint); diff --git a/src/java/org/apache/cassandra/hints/HintVerbHandler.java b/src/java/org/apache/cassandra/hints/HintVerbHandler.java index 1fb04d995019..f11f29f19f9b 100644 --- a/src/java/org/apache/cassandra/hints/HintVerbHandler.java +++ b/src/java/org/apache/cassandra/hints/HintVerbHandler.java @@ -34,6 +34,9 @@ import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_HINTS_HANDLER; /** * Verb handler used both for hint dispatch and streaming. @@ -44,15 +47,19 @@ */ public final class HintVerbHandler implements IVerbHandler { - public static final HintVerbHandler instance = new HintVerbHandler(); + public static final IVerbHandler instance = CUSTOM_HINTS_HANDLER.isPresent() + ? FBUtilities.construct(CUSTOM_HINTS_HANDLER.getString(), + "Custom Hint Verb Handler") + : new HintVerbHandler(); private static final Logger logger = LoggerFactory.getLogger(HintVerbHandler.class); + @Override public void doVerb(Message message) { UUID hostId = message.payload.hostId; Hint hint = message.payload.hint; - InetAddressAndPort address = StorageService.instance.getEndpointForHostId(hostId); + InetAddressAndPort address = HintsEndpointProvider.instance.endpointForHost(hostId); // If we see an unknown table id, it means the table, or one of the tables in the mutation, had been dropped. // In that case there is nothing we can really do, or should do, other than log it go on. diff --git a/src/java/org/apache/cassandra/hints/HintsBuffer.java b/src/java/org/apache/cassandra/hints/HintsBuffer.java index 646dd72febef..bf0f8c3fe0c6 100644 --- a/src/java/org/apache/cassandra/hints/HintsBuffer.java +++ b/src/java/org/apache/cassandra/hints/HintsBuffer.java @@ -33,8 +33,8 @@ import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputBufferFixed; import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.StorageCompatibilityMode; import org.apache.cassandra.utils.concurrent.OpOrder; import static org.apache.cassandra.utils.FBUtilities.updateChecksum; @@ -245,7 +245,7 @@ private void write(Hint hint) updateChecksumInt(crc, hintSize); dop.writeInt((int) crc.getValue()); - Hint.serializer.serialize(hint, dop, MessagingService.current_version); + Hint.serializer.serialize(hint, dop, StorageCompatibilityMode.current().storageMessagingVersion()); updateChecksum(crc, buffer, buffer.position() - hintSize, hintSize); dop.writeInt((int) crc.getValue()); } diff --git a/src/java/org/apache/cassandra/hints/HintsBufferPool.java b/src/java/org/apache/cassandra/hints/HintsBufferPool.java index 275dbc37e624..24a7e2445481 100644 --- a/src/java/org/apache/cassandra/hints/HintsBufferPool.java +++ b/src/java/org/apache/cassandra/hints/HintsBufferPool.java @@ -21,7 +21,7 @@ import java.util.UUID; import java.util.concurrent.BlockingQueue; -import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.utils.StorageCompatibilityMode; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static org.apache.cassandra.config.CassandraRelevantProperties.MAX_HINT_BUFFERS; @@ -58,7 +58,7 @@ interface FlushCallback */ void write(Iterable hostIds, Hint hint) { - int hintSize = (int) Hint.serializer.serializedSize(hint, MessagingService.current_version); + int hintSize = (int) Hint.serializer.serializedSize(hint, StorageCompatibilityMode.current().storageMessagingVersion()); try (HintsBuffer.Allocation allocation = allocate(hintSize)) { allocation.write(hostIds, hint); diff --git a/src/java/org/apache/cassandra/hints/HintsCatalog.java b/src/java/org/apache/cassandra/hints/HintsCatalog.java index 6bc00309247a..acd364a02e68 100644 --- a/src/java/org/apache/cassandra/hints/HintsCatalog.java +++ b/src/java/org/apache/cassandra/hints/HintsCatalog.java @@ -35,7 +35,7 @@ import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.SyncUtil; import static java.util.stream.Collectors.groupingBy; @@ -148,17 +148,17 @@ void exciseStore(UUID hostId) void fsyncDirectory() { - int fd = NativeLibrary.tryOpenDirectory(hintsDirectory.absolutePath()); + int fd = INativeLibrary.instance.tryOpenDirectory(hintsDirectory.toAbsolute()); if (fd != -1) { try { SyncUtil.trySync(fd); - NativeLibrary.tryCloseFD(fd); + INativeLibrary.instance.tryCloseFD(fd); } catch (FSError e) // trySync failed { - logger.error("Unable to sync directory {}", hintsDirectory.absolutePath(), e); + logger.error("Unable to sync directory {}", hintsDirectory.toAbsolute(), e); FileUtils.handleFSErrorAndPropagate(e); } } diff --git a/src/java/org/apache/cassandra/hints/HintsDescriptor.java b/src/java/org/apache/cassandra/hints/HintsDescriptor.java index 4fce3fbbd081..ab65f76058f8 100644 --- a/src/java/org/apache/cassandra/hints/HintsDescriptor.java +++ b/src/java/org/apache/cassandra/hints/HintsDescriptor.java @@ -18,7 +18,12 @@ package org.apache.cassandra.hints; import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; @@ -33,9 +38,6 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; - -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileInputStreamPlus; import com.google.common.io.ByteStreams; import com.google.common.io.CountingOutputStream; import org.slf4j.Logger; @@ -45,14 +47,19 @@ import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.io.FSReadError; +import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.compress.ICompressor; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileInputStreamPlus; +import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.CompressionParams; import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.utils.Hex; import org.apache.cassandra.utils.JsonUtils; +import org.apache.cassandra.utils.StorageCompatibilityMode; import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt; @@ -69,7 +76,11 @@ final class HintsDescriptor static final int VERSION_30 = 1; static final int VERSION_40 = 2; static final int VERSION_50 = 3; - static final int CURRENT_VERSION = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_50; + static final int VERSION_DS_10 = MessagingService.VERSION_DS_10; + static final int VERSION_DS_11 = MessagingService.VERSION_DS_11; + static final int VERSION_DS_12 = MessagingService.VERSION_DS_12; + static final int VERSION_DS_20 = MessagingService.VERSION_DS_20; + static final int CURRENT_VERSION = MessagingService.current_version; static final String COMPRESSION = "compression"; static final String ENCRYPTION = "encryption"; @@ -86,9 +97,15 @@ final class HintsDescriptor final ImmutableMap parameters; final ParameterizedClass compressionConfig; + // It's set when HintsWriter closed for new hint file or + // when descriptor is deserialized from local hint file. + private volatile long dataSize = 0; + private final Cipher cipher; private final ICompressor compressor; + private volatile Statistics statistics = EMPTY_STATS; + HintsDescriptor(UUID hostId, int version, long timestamp, ImmutableMap parameters) { this.hostId = hostId; @@ -118,12 +135,57 @@ final class HintsDescriptor HintsDescriptor(UUID hostId, long timestamp, ImmutableMap parameters) { - this(hostId, CURRENT_VERSION, timestamp, parameters); + this(hostId, currentStorageVersion(), timestamp, parameters); } HintsDescriptor(UUID hostId, long timestamp) { - this(hostId, CURRENT_VERSION, timestamp, ImmutableMap.of()); + this(hostId, currentStorageVersion(), timestamp, ImmutableMap.of()); + } + + /** + * Returns the hints version to use for new hints files, respecting storage compatibility mode. + * When compatibility mode is set (e.g., HCD_1), this ensures hints are written in a format + * that older versions can read. + * + * @return the hints version appropriate for the current storage compatibility mode + */ + static int currentStorageVersion() + { + return hintsVersionFromMessagingVersion(StorageCompatibilityMode.current().storageMessagingVersion()); + } + + /** + * Translates a messaging version to the corresponding hints file version. + * This is necessary because hints versions have their own numbering scheme + * (1, 2, 3 for OSS versions 3.0, 4.0, 5.0) that differs from messaging versions. + * + * @param messagingVersion the messaging protocol version + * @return the hints file version corresponding to the messaging version + */ + @VisibleForTesting + static int hintsVersionFromMessagingVersion(int messagingVersion) + { + switch (messagingVersion) + { + case MessagingService.VERSION_30: + case MessagingService.VERSION_3014: + return VERSION_30; + case MessagingService.VERSION_40: + return VERSION_40; + case MessagingService.VERSION_50: + return VERSION_50; + case MessagingService.VERSION_DS_10: + return VERSION_DS_10; + case MessagingService.VERSION_DS_11: + return VERSION_DS_11; + case MessagingService.VERSION_DS_12: + return VERSION_DS_12; + case MessagingService.VERSION_DS_20: + return VERSION_DS_20; + default: + throw new IllegalStateException("Unknown messaging version " + messagingVersion); + } } @SuppressWarnings("unchecked") @@ -192,6 +254,36 @@ static EncryptionData createEncryption(ImmutableMap params) } } + public void setDataSize(long length) + { + this.dataSize = length; + } + + public long getDataSize() + { + return dataSize; + } + + public void setStatistics(Statistics statistics) + { + this.statistics = statistics; + } + + public Statistics statistics() + { + return statistics; + } + + String statisticsFileName() + { + return statisticsFileName(hostId, timestamp, version); + } + + static String statisticsFileName(UUID hostId, long timestamp, int version) + { + return String.format("%s-%s-%s-Statistics.hints", hostId, timestamp, version); + } + private static final class EncryptionData { final Cipher cipher; @@ -254,9 +346,17 @@ static int messagingVersion(int hintsVersion) case VERSION_30: return MessagingService.VERSION_30; case VERSION_40: - return MessagingService.VERSION_40; + return MessagingService.Version.VERSION_40.value; case VERSION_50: - return MessagingService.VERSION_50; + return MessagingService.Version.VERSION_50.value; + case VERSION_DS_10: + return MessagingService.VERSION_DS_10; + case VERSION_DS_11: + return MessagingService.VERSION_DS_11; + case VERSION_DS_12: + return MessagingService.VERSION_DS_12; + case VERSION_DS_20: + return MessagingService.Version.VERSION_DS_20.value; default: throw new AssertionError(); } @@ -271,7 +371,10 @@ static Optional readFromFileQuietly(Path path) { try (FileInputStreamPlus raf = new FileInputStreamPlus(path)) { - return Optional.of(deserialize(raf)); + HintsDescriptor descriptor = deserialize(raf); + descriptor.setDataSize(FileUtils.size(path)); + descriptor.loadStatsComponent(path.getParent()); + return Optional.of(descriptor); } catch (ChecksumMismatchException e) { @@ -308,18 +411,6 @@ static void handleDescriptorIOE(IOException e, Path path) } } - static HintsDescriptor readFromFile(File path) - { - try (FileInputStreamPlus raf = new FileInputStreamPlus(path)) - { - return deserialize(raf); - } - catch (IOException e) - { - throw new FSReadError(e, path); - } - } - public boolean isCompressed() { return compressionConfig != null; @@ -488,4 +579,73 @@ private static void validateCRC(int expected, int actual) throws IOException if (expected != actual) throw new ChecksumMismatchException("Hints Descriptor CRC Mismatch"); } + + @VisibleForTesting + void loadStatsComponent(Path hintsDirectory) + { + Path file = hintsDirectory.resolve(statisticsFileName()); + try (InputStream inputStream = Files.newInputStream(file); + DataInputStream statsFile = new DataInputStream(inputStream)) + { + this.statistics = Statistics.deserialize(statsFile); + } + catch (FileNotFoundException e) + { + // Statistics are only used for metrics; it's ok to ignore an absent component during upgrades + logger.warn("Cannot find stats component `{}` for hints descriptor, initialising with empty statistics.", file); + this.statistics = EMPTY_STATS; + } + catch (IOException e) + { + // Ignore error in case of corruption + logger.error("Cannot read stats component `{}` for hints descriptor, initialising with empty statistics.", file, e); + this.statistics = EMPTY_STATS; + } + } + + void writeStatsComponent(Path directory) + { + File file = new File(directory, statisticsFileName()); + try (DataOutputStream out = new DataOutputStream(Files.newOutputStream(file.toPath()))) + { + statistics.serialize(out); + } + catch (IOException e) + { + throw new FSWriteError(e, file); + } + } + + public static Statistics EMPTY_STATS = new Statistics(0); + + public static class Statistics + { + private final long totalCount; + + public Statistics(long totalCount) + { + this.totalCount = totalCount; + } + + public long totalCount() + { + return totalCount; + } + + public void serialize(DataOutput out) throws IOException + { + out.writeLong(totalCount); + } + + public static Statistics deserialize(DataInput in) throws IOException + { + long totalCount = in.readLong(); + return new Statistics(totalCount); + } + + public static int serializedSize() + { + return Long.BYTES; + } + } } diff --git a/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java b/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java index 540f5bd85dc7..a059519b9daa 100644 --- a/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java +++ b/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java @@ -18,6 +18,7 @@ package org.apache.cassandra.hints; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -28,21 +29,21 @@ import java.util.function.Supplier; import com.google.common.util.concurrent.RateLimiter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.concurrent.ExecutorPlus; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.StorageCompatibilityMode; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import org.apache.cassandra.utils.concurrent.Future; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_REWRITING_HINTS_ON_HOST_LEFT; + /** * A multi-threaded (by default) executor for dispatching hints. * @@ -55,10 +56,10 @@ final class HintsDispatchExecutor private final File hintsDirectory; private final ExecutorPlus executor; private final AtomicBoolean isPaused; - private final Predicate isAlive; + private final Predicate isAlive; private final Map scheduledDispatches; - HintsDispatchExecutor(File hintsDirectory, int maxThreads, AtomicBoolean isPaused, Predicate isAlive) + HintsDispatchExecutor(File hintsDirectory, int maxThreads, AtomicBoolean isPaused, Predicate isAlive) { this.hintsDirectory = hintsDirectory; this.isPaused = isPaused; @@ -164,7 +165,7 @@ private TransferHintsTask(HintsCatalog catalog, Supplier hostIdSupplier) public void run() { UUID hostId = hostIdSupplier.get(); - InetAddressAndPort address = StorageService.instance.getEndpointForHostId(hostId); + InetAddressAndPort address = HintsEndpointProvider.instance.endpointForHost(hostId); logger.info("Transferring all hints to {}: {}", address, hostId); if (transfer(hostId)) return; @@ -209,16 +210,7 @@ private final class DispatchHintsTask implements Runnable { this.store = store; this.hostId = hostId; - - // Rate limit is in bytes per second. Uses Double.MAX_VALUE if disabled (set to 0 in cassandra.yaml). - // Max rate is scaled by the number of nodes in the cluster (CASSANDRA-5272), unless we are transferring - // hints during decomission rather than dispatching them to their final destination. - // The goal is to bound maximum hints traffic going towards a particular node from the rest of the cluster, - // not total outgoing hints traffic from this node. This is why the rate limiter is not shared between - // all the dispatch tasks (as there will be at most one dispatch task for a particular host id at a time). - int nodesCount = isTransfer ? 1 : Math.max(1, StorageService.instance.getTokenMetadata().getAllEndpoints().size() - 1); - double throttleInBytes = DatabaseDescriptor.getHintedHandoffThrottleInKiB() * 1024.0 / nodesCount; - this.rateLimiter = RateLimiter.create(throttleInBytes == 0 ? Double.MAX_VALUE : throttleInBytes); + this.rateLimiter = HintsRateLimiterFactory.instance.create(hostId); } DispatchHintsTask(HintsStore store, UUID hostId) @@ -269,14 +261,21 @@ private void dispatch() */ private boolean dispatch(HintsDescriptor descriptor) { - logger.trace("Dispatching hints file {}", descriptor.fileName()); + logger.debug("Dispatching hints file {}", descriptor.fileName()); - InetAddressAndPort address = StorageService.instance.getEndpointForHostId(hostId); + InetAddressAndPort address = HintsEndpointProvider.instance.endpointForHost(hostId); if (address != null) return deliver(descriptor, address); // address == null means the target no longer exist; find new home for each hint entry. - convert(descriptor); + if (SKIP_REWRITING_HINTS_ON_HOST_LEFT.getBoolean()) + { + logger.debug("Host {} is no longer a member of cluster, dropping hints", hostId); + store.cleanUp(descriptor); + store.delete(descriptor); + } + else + convert(descriptor); return true; } @@ -285,8 +284,21 @@ private boolean deliver(HintsDescriptor descriptor, InetAddressAndPort address) File file = descriptor.file(hintsDirectory); InputPosition offset = store.getDispatchOffset(descriptor); - BooleanSupplier shouldAbort = () -> !isAlive.test(address) || isPaused.get(); - try (HintsDispatcher dispatcher = HintsDispatcher.create(file, rateLimiter, address, descriptor.hostId, shouldAbort)) + BooleanSupplier shouldAbort = () -> !isAlive.test(descriptor.hostId) || isPaused.get(); + + Optional optVersion = HintsEndpointProvider.instance.versionForEndpoint(address); + if (optVersion.isEmpty()) + { + logger.debug("Cannot deliver handoff to endpoint {}: its version is unknown. This should be temporary.", address); + return false; + } + + // Use the minimum of peer's messaging version and storage compatibility mode's version. + // This ensures hints written with the storage-compatible version can be dispatched using the + // encoded path without deserialization, as long as the peer supports that version. + int dispatchVersion = Math.min(optVersion.get(), StorageCompatibilityMode.current().storageMessagingVersion()); + + try (HintsDispatcher dispatcher = HintsDispatcher.create(file, rateLimiter, address, descriptor.hostId, dispatchVersion, shouldAbort)) { if (offset != null) dispatcher.seek(offset); @@ -348,4 +360,31 @@ public boolean hasScheduledDispatches() { return !scheduledDispatches.isEmpty(); } + + public void updateDispatcherConcurrency(int concurrency) + { + logger.info("updating HintsDispatchExecutor with new concurrency = {} (current value = {})", concurrency, executor.getCorePoolSize()); + if (concurrency > executor.getCorePoolSize()) + { + // we are increasing the value + executor.setMaximumPoolSize(concurrency); + executor.setCorePoolSize(concurrency); + } + else if (concurrency < executor.getCorePoolSize()) + { + // we are reducing the value + executor.setCorePoolSize(concurrency); + executor.setMaximumPoolSize(concurrency); + } + } + + public int getDispatcherCorePoolSize() + { + return executor.getCorePoolSize(); + } + + public int getDispatcherMaxPoolSize() + { + return executor.getMaximumPoolSize(); + } } diff --git a/src/java/org/apache/cassandra/hints/HintsDispatcher.java b/src/java/org/apache/cassandra/hints/HintsDispatcher.java index b6273385435b..2685ad57b5fd 100644 --- a/src/java/org/apache/cassandra/hints/HintsDispatcher.java +++ b/src/java/org/apache/cassandra/hints/HintsDispatcher.java @@ -73,10 +73,14 @@ private HintsDispatcher(HintsReader reader, UUID hostId, InetAddressAndPort addr this.abortRequested = abortRequested; } - static HintsDispatcher create(File file, RateLimiter rateLimiter, InetAddressAndPort address, UUID hostId, BooleanSupplier abortRequested) + static HintsDispatcher create(File file, + RateLimiter rateLimiter, + InetAddressAndPort address, + UUID hostId, + int peerMessagingVersion, + BooleanSupplier abortRequested) { - int messagingVersion = MessagingService.instance().versions.get(address); - HintsDispatcher dispatcher = new HintsDispatcher(HintsReader.open(file, rateLimiter), hostId, address, messagingVersion, abortRequested); + HintsDispatcher dispatcher = new HintsDispatcher(HintsReader.open(file, rateLimiter), hostId, address, peerMessagingVersion, abortRequested); HintDiagnostics.dispatcherCreated(dispatcher); return dispatcher; } diff --git a/src/java/org/apache/cassandra/hints/HintsEndpointProvider.java b/src/java/org/apache/cassandra/hints/HintsEndpointProvider.java new file mode 100644 index 000000000000..d8b58fc58b77 --- /dev/null +++ b/src/java/org/apache/cassandra/hints/HintsEndpointProvider.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.hints; + +import java.util.Optional; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.IFailureDetector; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.EndpointMessagingVersions; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_HINTS_ENDPOINT_PROVIDER; + +/** + * Provide endpoint info and host info for hints. It's used by CNDB to support cross-region hints + */ +public interface HintsEndpointProvider +{ + static final Logger LOGGER = LoggerFactory.getLogger(HintsDispatcher.class); + + HintsEndpointProvider instance = CUSTOM_HINTS_ENDPOINT_PROVIDER.isPresent() + ? FBUtilities.construct(CUSTOM_HINTS_ENDPOINT_PROVIDER.getString(), + "Hinted Handoff Endpoint Provider") + : new DefaultHintsEndpointProvider(); + + boolean isSameSchemaVersion(UUID hostId); + + boolean isAlive(UUID hostId); + + InetAddressAndPort endpointForHost(UUID hostId); + + UUID hostForEndpoint(InetAddressAndPort endpoint); + + Optional versionForEndpoint(InetAddressAndPort endpoint); + + class DefaultHintsEndpointProvider implements HintsEndpointProvider + { + @Override + public InetAddressAndPort endpointForHost(UUID hostId) + { + return StorageService.instance.getEndpointForHostId(hostId); + } + + @Override + public UUID hostForEndpoint(InetAddressAndPort endpoint) + { + return StorageService.instance.getHostIdForEndpoint(endpoint); + } + + @Override + public Optional versionForEndpoint(InetAddressAndPort endpoint) + { + EndpointMessagingVersions versions = MessagingService.instance().versions; + if (versions.knows(endpoint)) + { + try + { + return Optional.of(versions.getRaw(endpoint)); + } + catch (Exception e) + { + LOGGER.debug("Failed to get raw version for endpoint {}", endpoint, e); + } + } + return Optional.empty(); + } + + @Override + public boolean isSameSchemaVersion(UUID hostId) + { + InetAddressAndPort peer = this.endpointForHost(hostId); + return Schema.instance.isSameVersion(Gossiper.instance.getSchemaVersion(peer)); + } + + @Override + public boolean isAlive(UUID hostId) + { + InetAddressAndPort address = this.endpointForHost(hostId); + return address != null && IFailureDetector.instance.isAlive(address); + } + } +} diff --git a/src/java/org/apache/cassandra/hints/HintsRateLimiterFactory.java b/src/java/org/apache/cassandra/hints/HintsRateLimiterFactory.java new file mode 100644 index 000000000000..c0f6c1bcfa8d --- /dev/null +++ b/src/java/org/apache/cassandra/hints/HintsRateLimiterFactory.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.hints; + +import java.util.UUID; + +import com.google.common.util.concurrent.RateLimiter; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_HINTS_RATE_LIMITER_FACTORY; + +/** + * The factory for creating {@link RateLimiter} for every hints dispatch task + */ +public interface HintsRateLimiterFactory +{ + + HintsRateLimiterFactory instance = CUSTOM_HINTS_RATE_LIMITER_FACTORY.isPresent() ? + make(CUSTOM_HINTS_RATE_LIMITER_FACTORY.getString()): + new DefaultHintsRateLimiterFactory(); + + /** + * return {@link RateLimiter} for current dispatch task + */ + RateLimiter create(UUID hostId); + + class DefaultHintsRateLimiterFactory implements HintsRateLimiterFactory + { + DefaultHintsRateLimiterFactory() + { + } + + @Override + public RateLimiter create(UUID hostId) + { + // rate limit is in bytes per second. Uses Double.MAX_VALUE if disabled (set to 0 in cassandra.yaml). + // max rate is scaled by the number of nodes in the cluster (CASSANDRA-5272). + // the goal is to bound maximum hints traffic going towards a particular node from the rest of the cluster, + // not total outgoing hints traffic from this node - this is why the rate limiter is not shared between + // all the dispatch tasks (as there will be at most one dispatch task for a particular host id at a time). + int nodesCount = Math.max(1, StorageService.instance.getTokenMetadata().getSizeOfAllEndpoints() - 1); + int throttleInKB = DatabaseDescriptor.getHintedHandoffThrottleInKiB() / nodesCount; + return RateLimiter.create(throttleInKB == 0 ? Double.MAX_VALUE : throttleInKB * 1024); + } + } + + static HintsRateLimiterFactory make(String customImpl) + { + try + { + return (HintsRateLimiterFactory) Class.forName(customImpl).newInstance(); + } + catch (Throwable ex) + { + throw new IllegalStateException("Unknown Hinted Handoff Rate Limiter Factory: " + customImpl); + } + } +} diff --git a/src/java/org/apache/cassandra/hints/HintsReader.java b/src/java/org/apache/cassandra/hints/HintsReader.java index fc6796b624ba..5cd148e92831 100644 --- a/src/java/org/apache/cassandra/hints/HintsReader.java +++ b/src/java/org/apache/cassandra/hints/HintsReader.java @@ -21,9 +21,9 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.Iterator; - import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.RateLimiter; @@ -33,7 +33,6 @@ import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.io.FSReadError; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.AbstractIterator; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -78,6 +77,8 @@ static HintsReader open(File file, RateLimiter rateLimiter) try { HintsDescriptor descriptor = HintsDescriptor.deserialize(reader); + descriptor.setDataSize(file.length()); + descriptor.loadStatsComponent(file.toPath().getParent()); if (descriptor.isCompressed()) { // since the hints descriptor is always uncompressed, it needs to be read with the normal ChecksummedDataInput. @@ -95,6 +96,7 @@ else if (descriptor.isEncrypted()) } } + @VisibleForTesting static HintsReader open(File file) { return open(file, null); @@ -242,7 +244,7 @@ private Hint readHint(int size) throws IOException catch (UnknownTableException e) { logger.warn("Failed to read a hint for {}: {} - table with id {} is unknown in file {}", - StorageService.instance.getEndpointForHostId(descriptor.hostId), + HintsEndpointProvider.instance.endpointForHost(descriptor.hostId), descriptor.hostId, e.id, descriptor.fileName()); @@ -256,7 +258,7 @@ private Hint readHint(int size) throws IOException // log a warning and skip the corrupted entry logger.warn("Failed to read a hint for {}: {} - digest mismatch for hint at position {} in file {}", - StorageService.instance.getEndpointForHostId(descriptor.hostId), + HintsEndpointProvider.instance.endpointForHost(descriptor.hostId), descriptor.hostId, input.getPosition() - size - 4, descriptor.fileName()); diff --git a/src/java/org/apache/cassandra/hints/HintsService.java b/src/java/org/apache/cassandra/hints/HintsService.java index 19989a6c87c7..bbfd9a790368 100644 --- a/src/java/org/apache/cassandra/hints/HintsService.java +++ b/src/java/org/apache/cassandra/hints/HintsService.java @@ -28,6 +28,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -43,15 +44,12 @@ import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; -import org.apache.cassandra.gms.FailureDetector; -import org.apache.cassandra.gms.IFailureDetector; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.metrics.HintedHandoffMetrics; import org.apache.cassandra.metrics.StorageMetrics; -import org.apache.cassandra.dht.Token; import org.apache.cassandra.service.StorageProxy; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; @@ -95,11 +93,11 @@ public final class HintsService implements HintsServiceMBean private HintsService() { - this(FailureDetector.instance); + this(HintsEndpointProvider.instance::isAlive); } @VisibleForTesting - HintsService(IFailureDetector failureDetector) + HintsService(Predicate isAlive) { File hintsDirectory = DatabaseDescriptor.getHintsDirectory(); int maxDeliveryThreads = DatabaseDescriptor.getMaxHintsDeliveryThreads(); @@ -111,7 +109,7 @@ private HintsService() bufferPool = new HintsBufferPool(bufferSize, writeExecutor::flushBuffer); isDispatchPaused = new AtomicBoolean(true); - dispatchExecutor = new HintsDispatchExecutor(hintsDirectory, maxDeliveryThreads, isDispatchPaused, failureDetector::isAlive); + dispatchExecutor = new HintsDispatchExecutor(hintsDirectory, maxDeliveryThreads, isDispatchPaused, isAlive); // periodically empty the current content of the buffers int flushPeriod = DatabaseDescriptor.getHintsFlushPeriodInMS(); @@ -163,6 +161,9 @@ public void write(Collection hostIds, Hint hint) if (isShutDown) throw new IllegalStateException("HintsService is shut down and can't accept new hints"); + if (hostIds.isEmpty()) + return; + // we have to make sure that the HintsStore instances get properly initialized - otherwise dispatch will not trigger catalog.maybeLoadStores(hostIds); @@ -187,8 +188,8 @@ public void write(UUID hostId, Hint hint) */ void writeForAllReplicas(Hint hint) { - String keyspaceName = hint.mutation.getKeyspaceName(); - Token token = hint.mutation.key().getToken(); + String keyspaceName = hint.mutation().getKeyspaceName(); + Token token = hint.mutation().key().getToken(); EndpointsForToken replicas = ReplicaLayout.forTokenWriteLiveAndDown(Keyspace.open(keyspaceName), token).all(); @@ -196,7 +197,7 @@ void writeForAllReplicas(Hint hint) // than performing filters / translations 2x extra via Iterables.filter/transform List hostIds = replicas.stream() .filter(replica -> StorageProxy.shouldHint(replica, false)) - .map(replica -> StorageService.instance.getHostIdForEndpoint(replica.endpoint())) + .map(replica -> HintsEndpointProvider.instance.hostForEndpoint(replica.endpoint())) .collect(Collectors.toList()); write(hostIds, hint); @@ -347,7 +348,7 @@ public void deleteAllHintsForEndpoint(String address) */ public void deleteAllHintsForEndpoint(InetAddressAndPort target) { - UUID hostId = StorageService.instance.getHostIdForEndpoint(target); + UUID hostId = HintsEndpointProvider.instance.hostForEndpoint(target); if (hostId == null) throw new IllegalArgumentException("Can't delete hints for unknown address " + target); catalog.deleteAllHints(hostId); @@ -454,6 +455,41 @@ public long findOldestHintTimestamp(UUID hostId) { return catalog.get(hostId).findOldestHintTimestamp(); } + + /** + * Get the total size in bytes of all the hints files on disk. + * @return total file size, in bytes + */ + public long getTotalHintsSize() + { + return catalog.stores().mapToLong(HintsStore::getTotalFileSize).sum(); + } + + /** + * @return the number of all hint files on disk, including corrupted files + */ + public int getTotalFilesNum() + { + return catalog.stores().mapToInt(HintsStore::getTotalFilesNum).sum(); + } + + /** + * @return the number of corrupted hint files on disk. + */ + public int getCorruptedFilesNum() + { + return catalog.stores().mapToInt(HintsStore::getCorruptedFilesNum).sum(); + } + + /** + * Checks hints files total size on disk exceeds the total maximum. + * @return true if the max is exceeded + */ + public boolean exceedsMaxHintsSize() + { + long maxTotalHintsSize = DatabaseDescriptor.getMaxHintsSizePerHost(); + return maxTotalHintsSize > 0 && getTotalHintsSize() > maxTotalHintsSize; + } HintsCatalog getCatalog() { @@ -473,4 +509,28 @@ public boolean isDispatchPaused() { return isDispatchPaused.get(); } + + @VisibleForTesting + public HintsDispatchExecutor dispatcherExecutor() + { + return dispatchExecutor; + } + + // used by CNDB + public void updateDispatcherConcurrency(int concurrency) + { + dispatchExecutor.updateDispatcherConcurrency(concurrency); + } + + // used by CNDB + public int getDispatcherCorePoolSize() + { + return dispatchExecutor.getDispatcherCorePoolSize(); + } + + // used by CNDB + public int getDispatcherMaxPoolSize() + { + return dispatchExecutor.getDispatcherMaxPoolSize(); + } } diff --git a/src/java/org/apache/cassandra/hints/HintsStore.java b/src/java/org/apache/cassandra/hints/HintsStore.java index 969f37ae7f06..e1f745a53d21 100644 --- a/src/java/org/apache/cassandra/hints/HintsStore.java +++ b/src/java/org/apache/cassandra/hints/HintsStore.java @@ -18,13 +18,21 @@ package org.apache.cassandra.hints; import java.io.IOException; -import java.util.*; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Predicate; import javax.annotation.Nullable; +import java.util.stream.Stream; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; @@ -33,9 +41,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.HintsServiceMetrics; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.SyncUtil; @@ -79,6 +87,9 @@ private HintsStore(UUID hostId, File hintsDirectory, ImmutableMap d.timestamp).max().orElse(0L); + + long hintsNum = descriptors.stream().mapToLong(d -> d.statistics().totalCount()).sum(); + HintsServiceMetrics.hintsOnDisk.inc(hintsNum); } static HintsStore create(UUID hostId, File hintsDirectory, ImmutableMap writerParams, List descriptors) @@ -145,8 +156,7 @@ public long findOldestHintTimestamp() boolean isLive() { - InetAddressAndPort address = address(); - return address != null && FailureDetector.instance.isAlive(address); + return HintsEndpointProvider.instance.isAlive(hostId); } HintsDescriptor poll() @@ -177,6 +187,7 @@ void deleteAllHints() { cleanUp(descriptor); delete(descriptor); + HintsServiceMetrics.corruptedHintsOnDisk.dec(descriptor.statistics().totalCount()); } } @@ -229,7 +240,10 @@ void delete(HintsDescriptor descriptor) { File hintsFile = descriptor.file(hintsDirectory); if (hintsFile.tryDelete()) + { + HintsServiceMetrics.hintsOnDisk.dec(descriptor.statistics().totalCount()); logger.info("Deleted hint file {}", descriptor.fileName()); + } else if (hintsFile.exists()) logger.error("Failed to delete hint file {}", descriptor.fileName()); else @@ -237,6 +251,8 @@ else if (hintsFile.exists()) //noinspection ResultOfMethodCallIgnored descriptor.checksumFile(hintsDirectory).tryDelete(); + //noinspection ResultOfMethodCallIgnored + new File(hintsDirectory, descriptor.statisticsFileName()).tryDelete(); } boolean hasFiles() @@ -244,6 +260,12 @@ boolean hasFiles() return !dispatchDequeue.isEmpty(); } + @VisibleForTesting + Stream descriptors() + { + return dispatchDequeue.stream(); + } + InputPosition getDispatchOffset(HintsDescriptor descriptor) { return dispatchPositions.get(descriptor); @@ -261,15 +283,25 @@ long getTotalFileSize() { long total = 0; for (HintsDescriptor descriptor : Iterables.concat(dispatchDequeue, corruptedFiles)) - total += descriptor.hintsFileSize(hintsDirectory); + total += descriptor.getDataSize(); HintsWriter currentWriter = getWriter(); if (null != currentWriter) - total += currentWriter.descriptor().hintsFileSize(hintsDirectory); + total += currentWriter.descriptor().getDataSize(); return total; } + public int getTotalFilesNum() + { + return dispatchDequeue.size() + corruptedFiles.size(); + } + + public int getCorruptedFilesNum() + { + return corruptedFiles.size(); + } + void cleanUp(HintsDescriptor descriptor) { dispatchPositions.remove(descriptor); @@ -279,6 +311,7 @@ void cleanUp(HintsDescriptor descriptor) void markCorrupted(HintsDescriptor descriptor) { corruptedFiles.add(descriptor); + HintsServiceMetrics.corruptedHintsOnDisk.inc(descriptor.statistics().totalCount()); } /* diff --git a/src/java/org/apache/cassandra/hints/HintsWriteExecutor.java b/src/java/org/apache/cassandra/hints/HintsWriteExecutor.java index 2b0a434dd3ea..082cd596902a 100644 --- a/src/java/org/apache/cassandra/hints/HintsWriteExecutor.java +++ b/src/java/org/apache/cassandra/hints/HintsWriteExecutor.java @@ -23,15 +23,15 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import org.apache.cassandra.concurrent.ExecutorPlus; -import org.apache.cassandra.utils.concurrent.Future; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.FSError; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; @@ -75,6 +75,10 @@ void shutdownBlocking() { throw new AssertionError(e); } + finally + { + FileUtils.clean(writeBuffer); + } } /** diff --git a/src/java/org/apache/cassandra/hints/HintsWriter.java b/src/java/org/apache/cassandra/hints/HintsWriter.java index ecee314249de..9bba5bf983d1 100644 --- a/src/java/org/apache/cassandra/hints/HintsWriter.java +++ b/src/java/org/apache/cassandra/hints/HintsWriter.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardOpenOption; +import java.util.concurrent.atomic.AtomicLong; import java.util.zip.CRC32; import com.google.common.annotations.VisibleForTesting; @@ -33,7 +34,8 @@ import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputBufferFixed; import org.apache.cassandra.io.util.File; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.metrics.HintsServiceMetrics; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.SyncUtil; import org.apache.cassandra.utils.Throwables; @@ -55,6 +57,9 @@ class HintsWriter implements AutoCloseable private volatile long lastSyncPosition = 0L; + @VisibleForTesting + AtomicLong totalHintsWritten = new AtomicLong(); + protected HintsWriter(File directory, HintsDescriptor descriptor, File file, FileChannel channel, int fd, CRC32 globalCRC) { this.directory = directory; @@ -70,7 +75,7 @@ static HintsWriter create(File directory, HintsDescriptor descriptor) throws IOE File file = descriptor.file(directory); FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); - int fd = NativeLibrary.getfd(channel); + int fd = INativeLibrary.instance.getfd(channel); CRC32 crc = new CRC32(); @@ -120,6 +125,10 @@ public void close() perform(file, Throwables.FileOpType.WRITE, this::doFsync, channel::close); writeChecksum(); + descriptor.setStatistics(new HintsDescriptor.Statistics(totalHintsWritten.get())); + descriptor.writeStatsComponent(file.toPath().getParent()); + + descriptor.setDataSize(file.length()); } public void fsync() @@ -170,6 +179,7 @@ final class Session implements AutoCloseable private final long initialSize; private long bytesWritten; + private long hintsWritten; Session(ByteBuffer buffer, long initialSize) { @@ -200,6 +210,7 @@ long position() void append(ByteBuffer hint) throws IOException { bytesWritten += hint.remaining(); + hintsWritten += 1; // if the hint to write won't fit in the aggregation buffer, flush it if (hint.remaining() > buffer.remaining()) @@ -257,9 +268,14 @@ void append(Hint hint) throws IOException } if (hintBuffer == buffer) + { bytesWritten += totalSize; + hintsWritten += 1; + } else + { append(hintBuffer.flip()); + } } /** @@ -272,6 +288,11 @@ public void close() throws IOException maybeFsync(); maybeSkipCache(); descriptor.hintsFileSize(position()); + long hintsCnt = totalHintsWritten.addAndGet(hintsWritten); + descriptor.setDataSize(channel.position()); + descriptor.setStatistics(new HintsDescriptor.Statistics(hintsCnt)); + descriptor.writeStatsComponent(file.toPath().getParent()); + HintsServiceMetrics.hintsOnDisk.inc(hintsWritten); } private void flushBuffer() throws IOException @@ -299,7 +320,7 @@ private void maybeSkipCache() // don't skip page cache for tiny files, on the assumption that if they are tiny, the target node is probably // alive, and if so, the file will be closed and dispatched shortly (within a minute), and the file will be dropped. if (position >= DatabaseDescriptor.getTrickleFsyncIntervalInKiB() * 1024L) - NativeLibrary.trySkipCache(fd, 0, position - (position % PAGE_SIZE), file.path()); + INativeLibrary.instance.trySkipCache(fd, 0, position - (position % PAGE_SIZE), file.path()); } } } diff --git a/src/java/org/apache/cassandra/index/FeatureNeedsIndexRebuildException.java b/src/java/org/apache/cassandra/index/FeatureNeedsIndexRebuildException.java new file mode 100644 index 000000000000..85e13f59a7fa --- /dev/null +++ b/src/java/org/apache/cassandra/index/FeatureNeedsIndexRebuildException.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index; + +import java.util.Set; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.exceptions.InternalRequestExecutionException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.service.StorageServiceMBean; + +/** + * Thrown if a secondary index or any of its components is still in an old version that doesn't support the requested + * feature. + *

    + * Users hitting this exception should probably set the right index version and upgrade their sstables. That way, the + * new replacement sstables will use that new version. + *

    + * Please note that rebuilding the index with {@code nodetool rebuild_index}, + * {@link StorageServiceMBean#rebuildSecondaryIndex(String, String, String...)}, + * {@link ColumnFamilyStore#rebuildSecondaryIndex(String, String, String...)}, + * {@link SecondaryIndexManager#rebuildIndexesBlocking(Set)}, etc. will not be enough to upgrade the index version, + * because sstables are fixed to the index version active when they were first indexed. See CNDB-8756 for details. + *

    + * If this error is hit during an index build, when the index has been recorded in the schema, and before it's marked + * as queryable, it means that the index also needs to be rebuilt after upgrading the sstables, so it gets marked as + * queryable. CNDB-16824 is meant to simplify that. + */ +public final class FeatureNeedsIndexRebuildException extends RuntimeException implements InternalRequestExecutionException +{ + /** + * Creates a new {@link FeatureNeedsIndexRebuildException} for the specified message. + * + * @param message the explanation of the error + */ + public FeatureNeedsIndexRebuildException(String message) + { + super(message); + } + + @Override + public RequestFailureReason getReason() + { + return RequestFailureReason.FEATURE_NEEDS_INDEX_REBUILD; + } +} diff --git a/src/java/org/apache/cassandra/index/Index.java b/src/java/org/apache/cassandra/index/Index.java index acc468f1a4eb..2f3525c3c08d 100644 --- a/src/java/org/apache/cassandra/index/Index.java +++ b/src/java/org/apache/cassandra/index/Index.java @@ -22,9 +22,10 @@ import java.io.UncheckedIOException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -36,8 +37,6 @@ import javax.annotation.Nullable; import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.cql3.QueryOptions; -import org.apache.cassandra.cql3.restrictions.Restriction; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionTime; @@ -50,12 +49,14 @@ import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.monitoring.Monitorable; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.internal.CollatedViewIndexBuilder; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.transactions.IndexTransaction; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; @@ -82,35 +83,35 @@ * in the base table. * Searcher: performs queries against the index based on a predicate defined in a RowFilter. An instance * is expected to be single use, being involved in the execution of a single ReadCommand. - * + *

    * The main interface includes factory methods for obtaining instances of both of the sub-interfaces; - * + *

    * The methods defined in the top level interface can be grouped into 3 categories: - * + *

    * Management Tasks: - * This group of methods is primarily concerned with maintenance of secondary indexes are are mainly called from + * This group of methods is primarily concerned with maintenance of secondary indexes are mainly called from * SecondaryIndexManager. It includes methods for registering and un-registering an index, performing maintenance * tasks such as (re)building an index from SSTable data, flushing, invalidating and so forth, as well as some to - * retrieve general metadata about the index (index name, any internal tables used for persistence etc). + * retrieve general metadata about the index (index name, any internal tables used for persistence etc.). * Several of these maintenance functions have a return type of {@code Callable}; the expectation for these methods is * that any work required to be performed by the method be done inside the Callable so that the responsibility for * scheduling its execution can rest with SecondaryIndexManager. For instance, a task like reloading index metadata * following potential updates caused by modifications to the base table may be performed in a blocking way. In * contrast, adding a new index may require it to be built from existing SSTable data, a potentially expensive task * which should be performed asynchronously. - * + *

    * Index Selection: * There are two facets to index selection, write time and read time selection. The former is concerned with * identifying whether an index should be informed about a particular write operation. The latter is about providing * means to use the index for search during query execution. - * + *

    * Validation: * Values that may be written to an index are checked as part of input validation, prior to an update or insert * operation being accepted. - * + *

    * * Sub-interfaces: - * + *

    * Update processing: * Indexes are subscribed to the stream of events generated by modifications to the base table. Subscription is * done via first registering the Index with the base table's SecondaryIndexManager. For each partition update, the set @@ -122,7 +123,7 @@ * discarded by the SecondaryIndexManager. Indexer instances are never re-used by SecondaryIndexManager and the * expectation is that each call to indexerFor should return a unique instance, or at least if instances can * be recycled, that a given instance is only used to process a single partition update at a time. - * + *

    * Search: * Each query (i.e. a single ReadCommand) that uses indexes will use a single instance of Index.Searcher. As with * processing of updates, an Index must be registered with the primary table's SecondaryIndexManager to be able to @@ -136,9 +137,9 @@ * {@code java.util.functions.BiFunction}, that is a function which takes as * arguments a PartitionIterator (containing the reconciled result rows) and a RowFilter (from the ReadCommand being * executed) and returns another iterator of partitions, possibly having transformed the initial results in some way. - * The post processing function is obtained from the Index's postProcessorFor method; the built-in indexes which ship + * The post-processing function is obtained from the Index's postProcessorFor method; the built-in indexes which ship * with Cassandra return a no-op function here. - * + *

    * An optional static method may be provided to validate custom index options (two variants are supported): * *
    {@code public static Map validateOptions(Map options);}
    @@ -149,19 +150,18 @@ * * In this version, the base table's metadata is also supplied as an argument. * If both overloaded methods are provided, only the one including the base table's metadata will be invoked. - * + *

    * The validation method should return a map containing any of the supplied options which are not valid for the * implementation. If the returned map is not empty, validation is considered failed and an error is raised. * Alternatively, the implementation may choose to throw an org.apache.cassandra.exceptions.ConfigurationException * if invalid options are encountered. - * */ public interface Index { /** * Supported loads. An index could be badly initialized and support only reads i.e. */ - public enum LoadType + enum LoadType { READ, WRITE, ALL, NOOP; @@ -182,18 +182,23 @@ public boolean supportsReads() /** * Provider of {@code SecondaryIndexBuilder} instances. See {@code getBuildTaskSupport} and - * {@code SecondaryIndexManager.buildIndexesBlocking} for more detail. + * {@code SecondaryIndexManager} for more detail. */ interface IndexBuildingSupport { SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, Set indexes, Collection sstables, boolean isFullRebuild); + + default List getParallelIndexBuildTasks(ColumnFamilyStore cfs, Set indexes, Collection sstables, boolean isFullRebuild) + { + return Collections.singletonList(getIndexBuildTask(cfs, indexes, sstables, isFullRebuild)); + } } /** * Default implementation of {@code IndexBuildingSupport} which uses a {@code ReducingKeyIterator} to obtain a * collated view of the data in the SSTables. */ - public static class CollatedViewIndexBuildingSupport implements IndexBuildingSupport + class CollatedViewIndexBuildingSupport implements IndexBuildingSupport { public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, Set indexes, Collection sstables, boolean isFullRebuild) { @@ -205,7 +210,7 @@ public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, Set * Singleton instance of {@code CollatedViewIndexBuildingSupport}, which may be used by any {@code Index} * implementation. */ - public static final CollatedViewIndexBuildingSupport INDEX_BUILDER_SUPPORT = new CollatedViewIndexBuildingSupport(); + CollatedViewIndexBuildingSupport INDEX_BUILDER_SUPPORT = new CollatedViewIndexBuildingSupport(); /* * Management functions @@ -245,20 +250,38 @@ default LoadType getSupportedLoadTypeOnFailure(boolean isInitialBuild) return isInitialBuild ? LoadType.WRITE : LoadType.ALL; } + /** + * @return Period in millis to trigger a flush for indexes in the group. Non-positive value to disable it. + */ + default int getFlushPeriodInMs() + { + return -1; + } + + /** + * Returns true if index initialization should be skipped, false if it should run + * (via {@link #getInitializationTask()}); defaults to skipping based on {@link IndexBuildDecider#onInitialBuild()} + * decision. + */ + default boolean shouldSkipInitialization() + { + return IndexBuildDecider.instance.onInitialBuild().skipped(); + } + /** * Return a task to perform any initialization work when a new index instance is created. * This may involve costly operations such as (re)building the index, and is performed asynchronously * by SecondaryIndexManager * @return a task to perform any necessary initialization work */ - public Callable getInitializationTask(); + Callable getInitializationTask(); /** * Returns the IndexMetadata which configures and defines the index instance. This should be the same * object passed as the argument to setIndexMetadata. * @return the index's metadata */ - public IndexMetadata getIndexMetadata(); + IndexMetadata getIndexMetadata(); /** * Return a task to reload the internal metadata of an index. @@ -268,7 +291,7 @@ default LoadType getSupportedLoadTypeOnFailure(boolean isInitialBuild) * by SecondaryIndexManager * @return task to be executed by the index manager during a reload */ - public Callable getMetadataReloadTask(IndexMetadata indexMetadata); + Callable getMetadataReloadTask(IndexMetadata indexMetadata); /** * An index must be registered in order to be able to either subscribe to update events on the base @@ -278,12 +301,12 @@ default LoadType getSupportedLoadTypeOnFailure(boolean isInitialBuild) * to the implementation, not the manager. * @param registry the index registry to register the instance with */ - public void register(IndexRegistry registry); + void register(IndexRegistry registry); /** * Unregister current index when it's removed from system * - * @param registry the index registry to register the instance with + * @param registry the index registry to unregister the instance with */ default void unregister(IndexRegistry registry) { @@ -298,7 +321,7 @@ default void unregister(IndexRegistry registry) * * @return an Optional referencing the Index's backing storage table if it has one, or Optional.empty() if not. */ - public Optional getBackingTable(); + Optional getBackingTable(); /** * Return a task which performs a blocking flush of the index's data corresponding to the provided @@ -324,23 +347,35 @@ public default Callable getBlockingFlushTask(Memtable baseCfs) * * @return task to be executed by the index manager to perform the flush. */ - public Callable getBlockingFlushTask(); + Callable getBlockingFlushTask(); /** * Return a task which invalidates the index, indicating it should no longer be considered usable. * This should include an clean up and releasing of resources required when dropping an index. * @return task to be executed by the index manager to invalidate the index. */ - public Callable getInvalidateTask(); + Callable getInvalidateTask(); + + /** + * Return a task which unload the index, indicating it should no longer be considered usable. + * This should include a cleanup and releasing of resources required without removing files. + * + * @return task to be executed by the index manager to invalidate the index. + */ + default Callable getUnloadTask() + { + return () -> null; + } /** * Return a task to truncate the index with the specified truncation timestamp. * Called when the base table is truncated. + * * @param truncatedAt timestamp of the truncation operation. This will be the same timestamp used * in the truncation of the base table. * @return task to be executed by the index manager when the base table is truncated. */ - public Callable getTruncateTask(long truncatedAt); + Callable getTruncateTask(long truncatedAt); /** * Return a task to be executed before the node enters NORMAL state and finally joins the ring. @@ -348,7 +383,7 @@ public default Callable getBlockingFlushTask(Memtable baseCfs) * @param hadBootstrap If the node had bootstrap before joining. * @return task to be executed by the index manager before joining the ring. */ - default public Callable getPreJoinTask(boolean hadBootstrap) + default Callable getPreJoinTask(boolean hadBootstrap) { return null; } @@ -361,6 +396,7 @@ default public Callable getPreJoinTask(boolean hadBootstrap) * This is called by SecondaryIndexManager in buildIndexBlocking, buildAllIndexesBlocking and rebuildIndexesBlocking * where a return value of false causes the index to be exluded from the set of those which will process the * SSTable data. + * * @return if the index should be included in the set which processes SSTable data, false otherwise. */ boolean shouldBuildBlocking(); @@ -385,7 +421,6 @@ default boolean isSSTableAttached() * * @param descriptor The descriptor of the sstable observer is requested for. * @param tracker The {@link LifecycleNewTracker} associated with the SSTable being written - * * @return SSTable flush observer. */ default SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker) @@ -393,6 +428,27 @@ default SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNe return null; } + /** + * @param indexes the indexes to join + * @return a comma-separated list of alphabetically sorted unqualified index names + */ + static String joinNames(Iterable indexes) + { + return IndexMetadata.joinNames(getMetadata(indexes)); + } + + /** + * @param indexes the indexes to get the metadata from + * @return the list of index metadata + */ + static List getMetadata(Iterable indexes) + { + List metadata = new ArrayList<>(); + for (Index index : indexes) + metadata.add(index.getIndexMetadata()); + return metadata; + } + /* * Index selection */ @@ -408,17 +464,31 @@ default SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNe * @return true if the index depends on the supplied column being present; false if the column may be * safely dropped or modified without adversely affecting the index */ - public boolean dependsOn(ColumnMetadata column); + boolean dependsOn(ColumnMetadata column); + + /** + * Called to determine whether this index can provide a searcher to execute a query on the + * supplied expression. This forms part of the query validation done before a CQL select + * statement is executed. + * + * @param expression a search query predicate + * @return true if this index is capable of supporting such expressions, false otherwise + */ + default boolean supportsExpression(RowFilter.Expression expression) + { + return supportsExpression(expression.column(), expression.operator()); + } /** * Called to determine whether this index can provide a searcher to execute a query on the * supplied column using the specified operator. This forms part of the query validation done * before a CQL select statement is executed. + * * @param column the target column of a search query predicate * @param operator the operator of a search query predicate * @return true if this index is capable of supporting such expressions, false otherwise */ - public boolean supportsExpression(ColumnMetadata column, Operator operator); + boolean supportsExpression(ColumnMetadata column, Operator operator); /** * Returns whether this index does any kind of filtering when the query has multiple contains expressions, assuming @@ -439,10 +509,63 @@ default boolean filtersMultipleContains() * method should return {@code}UTF8Type.instance{@code}. * If the index implementation does not support custom expressions, then it should * return null. - * @return an the type of custom index expressions supported by this index, or an + * + * @return the type of custom index expressions supported by this index, or an * null if custom expressions are not supported. */ - public AbstractType customExpressionValueType(); + AbstractType customExpressionValueType(); + + /** + * If the index supports custom search expressions using the + * {@code SELECT * FROM table WHERE expr(index_name, expression)} syntax, this method should return a new + * {@link RowFilter.CustomExpression} for the specified expression value. Index implementations may provide their + * own implementations using method {@link RowFilter.CustomExpression#isSatisfiedBy(TableMetadata, DecoratedKey, Row)} + * to filter reconciled rows in the coordinator. Otherwise, the default implementation will accept all rows. + * See DB-2185 and DSP-16537 for further details. + * + * @param metadata the indexed table metadata + * @param value the custom expression value + * @return a custom index expression for the specified value + */ + default RowFilter.CustomExpression customExpressionFor(TableMetadata metadata, ByteBuffer value) + { + return new RowFilter.CustomExpression(metadata, getIndexMetadata(), value); + } + + /** + * Returns whether this index transforms the indexed column values. + * + * @return {@code true} if this index transforms the indexed column values, {@code false} otherwise + */ + default boolean isAnalyzed() + { + return false; + } + + /** + * Returns the {@link Analyzer} for this index, if any. If the index doesn't transform the column values, + * this method will return an empty optional. + * + * @return the transforming column value analyzer for the index, if any + */ + default Optional getAnalyzer(ByteBuffer queriedValue) + { + return Optional.empty(); + } + + /** + * Class representing a transformation of the indexed values done by the index. + *

    + * This is used by the CQL operators when a filtering expression supported by an index is evaluated outside the + * index. It can be used to perform the same transformation on values that the index does when indexing. That way, + * the CQL operator can replicate the index behaviour when filtering results. + */ + interface Analyzer + { + List indexedTokens(ByteBuffer indexedValue); + + List queriedTokens(); + } /** * Transform an initial RowFilter into the filter that will still need to applied @@ -454,19 +577,7 @@ default boolean filtersMultipleContains() * @return the (hopefully) reduced filter that would still need to be applied after * the index was used to narrow the initial result set */ - public RowFilter getPostIndexQueryFilter(RowFilter filter); - - /** - * Return a comparator that reorders query result before sending to client - * - * @param restriction restriction that requires current index - * @param options query options - * @return a comparator for post-query ordering; or null if not supported - */ - default Comparator getPostQueryOrdering(Restriction restriction, QueryOptions options) - { - return null; - } + RowFilter getPostIndexQueryFilter(RowFilter filter); /** * Return an estimate of the number of results this index is expected to return for any given @@ -476,7 +587,7 @@ default Comparator getPostQueryOrdering(Restriction restriction, Que * * @return the estimated average number of results a Searcher may return for any given query */ - public long getEstimatedResultRows(); + long getEstimatedResultRows(); /** * Check if current index is queryable based on the index status. @@ -542,7 +653,7 @@ public default boolean notifyIndexerAboutRowsInFullyExpiredSSTables() */ /** - * Creates an new {@code Indexer} object for updates to a given partition. + * Creates a new {@code Indexer} object for updates to a given partition. * * @param key key of the partition being modified * @param columns the regular and static columns the created indexer will have to deal with. @@ -569,22 +680,22 @@ public Indexer indexerFor(DecoratedKey key, * Listener for processing events emitted during a single partition update. * Instances of this are responsible for applying modifications to the index in response to a single update * operation on a particular partition of the base table. - * + *

    * That update may be generated by the normal write path, by iterating SSTables during streaming operations or when * building or rebuilding an index from source. Updates also occur during compaction when multiple versions of a * source partition from different SSTables are merged. - * + *

    * Implementations should not make assumptions about resolution or filtering of the partition update being * processed. That is to say that it is possible for an Indexer instance to receive notification of a * PartitionDelete or RangeTombstones which shadow a Row it then receives via insertRow/updateRow. - * + *

    * It is important to note that the only ordering guarantee made for the methods here is that the first call will * be to begin() and the last call to finish(). The other methods may be called to process update events in any * order. This can also include duplicate calls, in cases where a memtable partition is under contention from * several updates. In that scenario, the same set of events may be delivered to the Indexer as memtable update * which failed due to contention is re-applied. */ - public interface Indexer + interface Indexer { /** * Notification of the start of a partition update. @@ -639,7 +750,7 @@ default void updateRow(Row oldRowData, Row newRowData) {} * Notification that a row was removed from the partition. * Note that this is only called as part of either a compaction or a cleanup. * This context is indicated by the TransactionType supplied to the indexerFor method. - * + *

    * As with updateRow, it cannot be guaranteed that all data belonging to the Clustering * of the supplied Row has been removed (although in the case of a cleanup, that is the * ultimate intention). @@ -666,11 +777,8 @@ default void finish() {} /** * Used to validate the various parameters of a supplied {@code}ReadCommand{@code}, - * this is called prior to execution. In theory, any command instance may be checked - * by any {@code}Index{@code} instance, but in practice the index will be the one - * returned by a call to the {@code}getIndex(ColumnFamilyStore cfs){@code} method on - * the supplied command. - * + * this is called prior to execution. + *

    * Custom index implementations should perform any validation of query expressions here and throw a meaningful * InvalidRequestException when any expression or other parameter is invalid. * @@ -682,20 +790,37 @@ default void validate(ReadCommand command) throws InvalidRequestException { } + /** + * Tells whether this index supports replica fitering protection or not. + *

    + * Replica filtering protection might need to run the query row filter in the coordinator to detect stale results. + * An index implementation will be compatible with this protection mechanism if it returns the same results for the + * row filter as CQL will return with {@code ALLOW FILTERING} and without using the index. This means that index + * implementations using custom query syntax or applying transformations to the indexed data won't support it. + * See CASSANDRA-8272 for further details. + * + * @param rowFilter rowFilter of query to decide if it supports replica filtering protection or not + * @return true if this index supports replica filtering protection, false otherwise + */ + default boolean supportsReplicaFilteringProtection(RowFilter rowFilter) + { + return true; + } + /** * Factory method for query time search helper. * * @param command the read command being executed - * @return an Searcher with which to perform the supplied command + * @return a Searcher with which to perform the supplied command */ - public Searcher searcherFor(ReadCommand command); + Searcher searcherFor(ReadCommand command); /** * Performs the actual index lookup during execution of a ReadCommand. * An instance performs its query according to the RowFilter.Expression it was created for (see searcherFor) * An Expression is a predicate of the form [column] [operator] [value]. */ - public interface Searcher + interface Searcher { /** * Returns the {@link ReadCommand} for which this searcher has been created. @@ -708,29 +833,30 @@ public interface Searcher * @param executionController the collection of OpOrder.Groups which the ReadCommand is being performed under. * @return partitions from the base table matching the criteria of the search. */ - public UnfilteredPartitionIterator search(ReadExecutionController executionController); + UnfilteredPartitionIterator search(ReadExecutionController executionController); /** - * Replica filtering protection may fetch data that doesn't match query conditions. + * Returns a supplier for the custom {@link Monitorable.ExecutionInfo} for this query, to be used by + * {@link ReadCommand#executionInfo()} at the end of the query to collect details about the query execution in + * case it is considered too slow. * - * On coordinator, we need to filter the replicas' responses again. - * - * This will not be called if {@link QueryPlan#supportsReplicaFilteringProtection(RowFilter)} returns false. - * - * @return filtered response that satisfied query conditions + * @return a supplier for the execution info for this query, or {@code null} if no custom execution info is available */ - default PartitionIterator filterReplicaFilteringProtection(PartitionIterator fullResponse) + @Nullable + default Supplier monitorableExecutionInfo() { - return command().rowFilter().filter(fullResponse, command().metadata(), command().nowInSec()); + return null; } } /** * Class providing grouped operations for indexes that communicate with each other. - * + *

    * Index implementations should provide a {@code Group} implementation calling to - * {@link SecondaryIndexManager#registerIndex(Index, Index.Group.Key, Supplier)} during index registering - * at {@link #register(IndexRegistry)} method. + * {@link IndexRegistry#registerIndex(Index, Key, Supplier)} + * at {@link #register(IndexRegistry)} method and provide {@code groupKey} calling to + * {@link IndexRegistry#unregisterIndex(Index, Key)} during index unregistering + * at {@link #unregister(IndexRegistry)} method */ interface Group { @@ -767,7 +893,7 @@ public int hashCode() * * @return the indexes that are members of this group */ - Set getIndexes(); + Set getIndexes(); /** * Adds the specified {@link Index} as a member of this group. @@ -843,13 +969,13 @@ Indexer indexerFor(Predicate indexSelector, /** * Get flush observer to observe partition/cell events generated by flushing SSTable (memtable flush or compaction). * - * @param descriptor The descriptor of the sstable observer is requested for. - * @param tracker The {@link LifecycleNewTracker} associated with the SSTable being written + * @param descriptor The descriptor of the sstable observer is requested for. + * @param tracker The {@link LifecycleNewTracker} associated with the SSTable being written * @param tableMetadata The immutable metadata of the table at the moment the SSTable is flushed - * + * @param keyCount The estimated number of keys in the sstable being flushed * @return SSTable flush observer. */ - SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata); + SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata, long keyCount); /** * @param type index transaction type @@ -867,11 +993,49 @@ default boolean handles(IndexTransaction.Type type) default void invalidate() { } /** - * Returns the SSTable-attached {@link Component}s created by this index group. + * Called when the table associated with this group has been unloaded. Implementations + * should dispose of any resources tied to the lifecycle of the {@link Group} without removing index files. + */ + default void unload() { } + + /** + * Returns the set of sstable-attached components that this group will create for a newly flushed sstable. + *

    + * Note that the result of this method is only valid for newly flushed/written sstables as the components + * returned will assume a version of {@link Version#current(String)} and a generation of 0. SSTables for which + * some index have been rebuild may have index components that do not match what this method return in + * particular. + */ + Set componentsForNewSSTable(); + + /** + * Return the set of sstable-attached components belonging to the group that are currently "active" for the + * provided sstable. + *

    + * The "active" components are the components that are currently in use, meaning that if a given component + * of the sstable exists with multiple versions or generation on disk, only the most recent version/generation + * is the active one. * - * @return the SSTable components created by this group + * @param sstable the sstable to get components for. + * @return the set of the sstable-attached components of the provided sstable for this group. + */ + Set activeComponents(SSTableReader sstable); + + /** + * @return true if this index group is capable of supporting multiple contains restrictions, false otherwise + */ + default boolean supportsMultipleContains() + { + return false; + } + + /** + * @return true is this index group supports disjunction queries of "a = 1 OR a = 2" or "a IN (1, 2)" */ - Set getComponents(); + default boolean supportsDisjunction() + { + return false; + } /** * Validates all indexes in the group against the specified SSTables. @@ -914,7 +1078,6 @@ interface QueryPlan extends Comparable { /** * Returns the indexes selected by this query plan, all of them belonging to the same {@link Group}. - * * It should never be empty. * * @return the indexes selected by this query plan, which is never empty @@ -937,14 +1100,18 @@ default Index getFirst() * that it can be used to answer. Used by {@link SecondaryIndexManager#getBestIndexQueryPlanFor(RowFilter)} * to determine the {@link Group} with the most selective plan for a given {@link RowFilter}. * Additionally, this is also used by StorageProxy.estimateResultsPerRange to calculate the initial concurrency - * factor for range requests + * factor for range requests. + *

    + * Please note that some index implementations (SAI) will always return -1 for that method to + * prioritize themselves. Third party implementations can also return similar fixed values. See CNDB-14764 for + * details. * * @return the estimated average number of results a Searcher may return for any given command */ default long getEstimatedResultRows() { // CQL only supports AND expressions, so the estimated number of results for multiple indexes will be the - // the lowest of the estimates for each index + // lowest of the estimates for each index return getIndexes().stream() .mapToLong(Index::getEstimatedResultRows) .min() @@ -998,18 +1165,18 @@ default void validate(ReadCommand command) throws InvalidRequestException Searcher searcherFor(ReadCommand command); /** - * Return a function which performs post processing on the results of a partition range read command. - * In future, this may be used as a generalized mechanism for transforming results on the coordinator prior + * Return a function which performs post-processing on the results of a partition range read command. + * In the future, this may be used as a generalized mechanism for transforming results on the coordinator prior * to returning them to the caller. - * - * This is used on the coordinator during execution of a range command to perform post - * processing of merged results obtained from the necessary replicas. This is the only way in which results are + *

    + * This is used on the coordinator during execution of a range command to perform post-processing of merged + * results obtained from the necessary replicas. This is the only way in which results are * transformed in this way but this may change over time as usage is generalized. * See CASSANDRA-8717 for further discussion. - * + *

    * The function takes a PartitionIterator of the results from the replicas which has already been collated - * and reconciled, along with the command being executed. It returns another PartitionIterator containing the results - * of the transformation (which may be the same as the input if the transformation is a no-op). + * and reconciled, along with the command being executed. It returns another PartitionIterator containing the + * results of the transformation (which may be the same as the input if the transformation is a no-op). * * @param command the read command being executed */ @@ -1020,10 +1187,10 @@ default Function postProcessor(ReadCommand /** * Transform an initial {@link RowFilter} into the filter that will still need to applied to a set of Rows after - * the index has performed it's initial scan. - * - * Used in {@link ReadCommand#executeLocally(ReadExecutionController)} to reduce the amount of filtering performed on the - * results of the index query. + * the index has performed its initial scan. + *

    + * Used in {@link ReadCommand#executeLocally(ReadExecutionController)} to reduce the amount of filtering + * performed on the results of the index query. * * @return the (hopefully) reduced filter that would still need to be applied after * the index was used to narrow the initial result set @@ -1055,6 +1222,30 @@ default boolean isTopK() { return false; } + + /** + * @return true if the indexes in this plan support querying multiple vnode ranges at once. + */ + default boolean supportsMultiRangeReadCommand() + { + return false; + } + + /** + * @return {@code true} if this plan is a BM25 request, {@code false} otherwise + */ + default boolean isBM25() + { + return false; + } + + /** + * @return {@code true} if this plan uses index-based filtering, {@code false} otherwise + */ + default boolean usesIndexFiltering() + { + return true; + } } /* diff --git a/src/java/org/apache/cassandra/index/IndexBuildDecider.java b/src/java/org/apache/cassandra/index/IndexBuildDecider.java new file mode 100644 index 000000000000..56ec158f7305 --- /dev/null +++ b/src/java/org/apache/cassandra/index/IndexBuildDecider.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.notifications.SSTableAddedNotification; +import org.apache.cassandra.notifications.SSTableListChangedNotification; +import org.apache.cassandra.utils.FBUtilities; + +public interface IndexBuildDecider +{ + IndexBuildDecider instance = CassandraRelevantProperties.CUSTOM_INDEX_BUILD_DECIDER.getString() == null ? + new IndexBuildDecider() {} : + FBUtilities.construct(CassandraRelevantProperties.CUSTOM_INDEX_BUILD_DECIDER.getString(), "custom index build decider"); + + enum Decision + { + /** + * index will be built synchronously + */ + SYNC, + /** + * index will be built asynchronously + */ + ASYNC, + /** + * index build will be skipped + */ + NONE; + + public boolean skipped() + { + return this == NONE; + } + } + + /** + * CNDB overrides this method to skip building indexes for sstables. + * + * @return decision for index initial build {@link Index#getInitializationTask()} + */ + default Decision onInitialBuild() + { + return Decision.SYNC; + } + + /** + * CNDB overrides this method to mark index queryable if there is no sstables on writer. + * + * @return true if index should be queryable after {@link Index#getInitializationTask()} + */ + default boolean isIndexQueryableAfterInitialBuild(ColumnFamilyStore cfs) + { + return true; + } + + /** + * CNDB overrides this method to skip building indexes on writer when sstables are reloaded from remote storage + * + * @return decision for index initial build when receiving {@link SSTableListChangedNotification} + */ + default Decision onSSTableListChanged(SSTableListChangedNotification notification) + { + return notification.operationType.equals(OperationType.REMOTE_RELOAD) ? Decision.ASYNC : Decision.NONE; + } + + /** + * CNDB overrides this method to skip building indexes on writer when sstables are reloaded from remote storage + * + * @return decision for index initial build when receiving {@link SSTableAddedNotification} + */ + default Decision onSSTableAdded(SSTableAddedNotification notification) + { + // SSTables associated to a memtable come from a flush, so their contents have already been indexed + if (notification.memtable().isPresent()) + return Decision.NONE; + + return notification.operationType == OperationType.REMOTE_RELOAD ? Decision.ASYNC : Decision.SYNC; + } +} diff --git a/src/java/org/apache/cassandra/index/IndexBuildInProgressException.java b/src/java/org/apache/cassandra/index/IndexBuildInProgressException.java new file mode 100644 index 000000000000..ff4d31b77a24 --- /dev/null +++ b/src/java/org/apache/cassandra/index/IndexBuildInProgressException.java @@ -0,0 +1,41 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index; + +import org.apache.cassandra.exceptions.InternalRequestExecutionException; +import org.apache.cassandra.exceptions.RequestFailureReason; + +/** + * Thrown if a secondary index is not currently available because it is building. + */ +public final class IndexBuildInProgressException extends RuntimeException implements InternalRequestExecutionException +{ + /** + * Creates a new IndexIsBuildingException for the specified index. + * @param index the index + */ + public IndexBuildInProgressException(Index index) + { + super(String.format("The secondary index '%s' is not yet available as it is building", index.getIndexMetadata().name)); + } + + @Override + public RequestFailureReason getReason() + { + return RequestFailureReason.INDEX_BUILD_IN_PROGRESS; + } +} diff --git a/src/java/org/apache/cassandra/index/IndexNotAvailableException.java b/src/java/org/apache/cassandra/index/IndexNotAvailableException.java index 5e5a753803ea..179b0cb3f508 100644 --- a/src/java/org/apache/cassandra/index/IndexNotAvailableException.java +++ b/src/java/org/apache/cassandra/index/IndexNotAvailableException.java @@ -18,10 +18,13 @@ package org.apache.cassandra.index; +import org.apache.cassandra.exceptions.InternalRequestExecutionException; +import org.apache.cassandra.exceptions.RequestFailureReason; + /** * Thrown if a secondary index is not currently available. */ -public final class IndexNotAvailableException extends RuntimeException +public final class IndexNotAvailableException extends RuntimeException implements InternalRequestExecutionException { /** * Creates a new IndexNotAvailableException for the specified index. @@ -31,4 +34,10 @@ public IndexNotAvailableException(Index index) { super(String.format("The secondary index '%s' is not yet available", index.getIndexMetadata().name)); } + + @Override + public RequestFailureReason getReason() + { + return RequestFailureReason.INDEX_NOT_AVAILABLE; + } } diff --git a/src/java/org/apache/cassandra/index/IndexRegistry.java b/src/java/org/apache/cassandra/index/IndexRegistry.java index d29bb11db4b0..ab54e8487bae 100644 --- a/src/java/org/apache/cassandra/index/IndexRegistry.java +++ b/src/java/org/apache/cassandra/index/IndexRegistry.java @@ -20,8 +20,10 @@ */ package org.apache.cassandra.index; +import java.nio.ByteBuffer; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; @@ -33,6 +35,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.db.*; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.marshal.AbstractType; @@ -44,6 +47,7 @@ import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableFlushObserver; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.TableMetadata; @@ -80,6 +84,12 @@ public Collection listIndexes() return Collections.emptyList(); } + @Override + public Collection listNotExcludedIndexes(IndexHints hints) + { + return Collections.emptyList(); + } + @Override public Collection listIndexGroups() { @@ -93,7 +103,19 @@ public Index getIndex(IndexMetadata indexMetadata) } @Override - public Optional getBestIndexFor(RowFilter.Expression expression) + public Index getIndexByName(String indexName) + { + return null; + } + + @Override + public Optional getBestIndexFor(ColumnMetadata column, Operator operator) + { + return Optional.empty(); + } + + @Override + public Optional getBestIndexFor(ColumnMetadata column, Operator operator, IndexHints hints) { return Optional.empty(); } @@ -135,11 +157,7 @@ public Callable getMetadataReloadTask(IndexMetadata indexMetadata) @Override public void register(IndexRegistry registry) { - } - @Override - public void unregister(IndexRegistry registry) - { } @Override @@ -249,13 +267,19 @@ public Index.QueryPlan queryPlanFor(RowFilter rowFilter) @Nullable @Override - public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata) + public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata, long keyCount) { return null; } @Override - public Set getComponents() + public Set componentsForNewSSTable() + { + return null; + } + + @Override + public Set activeComponents(SSTableReader sstable) { return null; } @@ -266,7 +290,6 @@ public void registerIndex(Index index, Index.Group.Key groupKey, Supplier listIndexes() { return Collections.singletonList(index); } + @Override + public Collection listNotExcludedIndexes(IndexHints hints) + { + return hints.excludes(index) + ? Collections.emptyList() + : Collections.singletonList(index); + } + @Override public Collection listIndexGroups() { @@ -290,7 +327,13 @@ public Collection listIndexGroups() } @Override - public Optional getBestIndexFor(RowFilter.Expression expression) + public Optional getBestIndexFor(ColumnMetadata column, Operator operator) + { + return Optional.empty(); + } + + @Override + public Optional getBestIndexFor(ColumnMetadata column, Operator operator, IndexHints hints) { return Optional.empty(); } @@ -303,20 +346,57 @@ public void validate(PartitionUpdate update, ClientState state) default void registerIndex(Index index) { - registerIndex(index, new Index.Group.Key(index), () -> new SingletonIndexGroup(index)); + registerIndex(index, new Index.Group.Key(index), SingletonIndexGroup::new); } - void registerIndex(Index index, Index.Group.Key groupKey, Supplier groupSupplier); - void unregisterIndex(Index index, Index.Group.Key groupKey); - Collection listIndexGroups(); Index getIndex(IndexMetadata indexMetadata); + @Nullable + Index getIndexByName(String indexName); Collection listIndexes(); - Optional getBestIndexFor(RowFilter.Expression expression); + /** + * Lists the indexes in this registry, minus the ones excluded by the specified {@link IndexHints}. + * + * @param hints the index hints with the indexes to exclude. + * @return the indexes in this registry that are not excluded by the hints. + */ + Collection listNotExcludedIndexes(IndexHints hints); + + default Optional getAnalyzerFor(ColumnMetadata column, Operator operator, ByteBuffer value) + { + for (Index index : listIndexes()) + { + if (index.supportsExpression(column, operator)) + { + Optional analyzer = index.getAnalyzer(value); + if (analyzer.isPresent()) + return analyzer; + } + } + return Optional.empty(); + } + + default Optional getAnalyzerFor(ColumnMetadata column, Operator operator, ByteBuffer value, IndexHints hints) + { + return getBestIndexFor(column, operator, hints).flatMap(i -> i.getAnalyzer(value)); + } + + Optional getBestIndexFor(ColumnMetadata column, Operator operator); + + default Optional getBestIndexFor(RowFilter.Expression expression) + { + return getBestIndexFor(expression.column(), expression.operator()); + } + + Optional getBestIndexFor(ColumnMetadata column, Operator operator, IndexHints hints); + default Optional getBestIndexFor(RowFilter.Expression expression, IndexHints hints) + { + return getBestIndexFor(expression.column(), expression.operator(), hints); + } /** * Called at write time to ensure that values present in the update * are valid according to the rules of all registered indexes which @@ -342,4 +422,90 @@ static IndexRegistry obtain(TableMetadata table) return table.isVirtual() ? EMPTY : Keyspace.openAndGetStore(table).indexManager; } + + enum EqBehavior + { + EQ, + MATCH, + AMBIGUOUS + } + + class EqBehaviorIndexes + { + public EqBehavior behavior; + public final Collection eqIndexes; + public final Collection matchIndexes; + + private EqBehaviorIndexes(Collection eqIndexes, Collection matchIndexes, EqBehavior behavior) + { + this.eqIndexes = eqIndexes; + this.matchIndexes = matchIndexes; + this.behavior = behavior; + } + + public static EqBehaviorIndexes eq(Collection eqIndexes) + { + return new EqBehaviorIndexes(eqIndexes, null, EqBehavior.EQ); + } + + public static EqBehaviorIndexes match(Collection eqAndMatchIndexes) + { + return new EqBehaviorIndexes(eqAndMatchIndexes, eqAndMatchIndexes, EqBehavior.MATCH); + } + + public static EqBehaviorIndexes ambiguous(Collection firstEqIndexes, Collection secondEqIndexes) + { + return new EqBehaviorIndexes(firstEqIndexes, secondEqIndexes, EqBehavior.AMBIGUOUS); + } + } + + /** + * @return + * - AMBIGUOUS if an index that is not excluded by the hints supports EQ and a different one, also not excluded by + * the hints, supports both EQ and ANALYZER_MATCHES. If one of the indexes is included by the hints, the behavior + * is not AMBIGUOUS. + * - MATCHES if it's not AMBIGUOUS and an index supports both EQ and ANALYZER_MATCHES + * - otherwise EQ + */ + default EqBehaviorIndexes getEqBehavior(ColumnMetadata cm, IndexHints hints) + { + Set eqOnlyIndexes = new HashSet<>(); + Set eqAndMatchIndexes = new HashSet<>(); + + for (Index index : listNotExcludedIndexes(hints)) + { + boolean supportsEq = index.supportsExpression(cm, Operator.EQ); + boolean supportsMatches = index.supportsExpression(cm, Operator.ANALYZER_MATCHES); + // This is an edge case due to the NON_DAEMON IndexRegistry, which doesn't have index metadata and + // which uses regular equality by convention. + boolean hasIndexMetadata = index.getIndexMetadata() != null; + + // Categorize indexes based on their capabilities + if (supportsEq && supportsMatches && hasIndexMetadata) + eqAndMatchIndexes.add(index); + else if (supportsEq) + eqOnlyIndexes.add(index); + } + + // we should consider the user-provided index hints, which can be used to disambiguate EQ queries + boolean prefersEq = hints.includesAnyOf(eqOnlyIndexes); + boolean prefersMatch = hints.includesAnyOf(eqAndMatchIndexes); + + // If we have indexes supporting only EQ and indexes supporting both, return AMBIGUOUS, + // unless the index hints prefer one index over the other. + if (!eqOnlyIndexes.isEmpty() && !eqAndMatchIndexes.isEmpty()) + { + if (prefersMatch == prefersEq) + return EqBehaviorIndexes.ambiguous(eqOnlyIndexes, eqAndMatchIndexes); + + return prefersMatch ? EqBehaviorIndexes.match(eqAndMatchIndexes) : EqBehaviorIndexes.eq(eqOnlyIndexes); + } + + // If we have indexes supporting both EQ and MATCHES, return MATCHES + if (!eqAndMatchIndexes.isEmpty()) + return EqBehaviorIndexes.match(eqAndMatchIndexes); + + // Otherwise return EQ + return EqBehaviorIndexes.eq(eqOnlyIndexes); + } } diff --git a/src/java/org/apache/cassandra/index/IndexStatusManager.java b/src/java/org/apache/cassandra/index/IndexStatusManager.java index 31036f2ec631..a16bc76f27b4 100644 --- a/src/java/org/apache/cassandra/index/IndexStatusManager.java +++ b/src/java/org/apache/cassandra/index/IndexStatusManager.java @@ -24,7 +24,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -91,6 +90,8 @@ public > E filterForQuery(E liveEndpoints, Keyspace keysp // UNKNOWN states are transient/rare; only a few replicas should have this state at any time. See CASSANDRA-19400 Set queryableNonSucceeded = new HashSet<>(4); + Map indexStatusMap = new HashMap<>(); + E queryableEndpoints = liveEndpoints.filter(replica -> { boolean allBuilt = true; @@ -98,7 +99,10 @@ public > E filterForQuery(E liveEndpoints, Keyspace keysp { Index.Status status = getIndexStatus(replica.endpoint(), keyspace.getName(), index.getIndexMetadata().name); if (!index.isQueryable(status)) + { + indexStatusMap.put(replica.endpoint(), status); return false; + } if (status != Index.Status.BUILD_SUCCEEDED) allBuilt = false; @@ -126,7 +130,14 @@ public > E filterForQuery(E liveEndpoints, Keyspace keysp { Map failureReasons = new HashMap<>(); liveEndpoints.without(queryableEndpoints.endpoints()) - .forEach(replica -> failureReasons.put(replica.endpoint(), RequestFailureReason.INDEX_NOT_AVAILABLE)); +// .forEach(replica -> failureReasons.put(replica.endpoint(), RequestFailureReason.INDEX_NOT_AVAILABLE)); + .forEach(replica -> { + Index.Status status = indexStatusMap.get(replica.endpoint()); + if (status == Index.Status.FULL_REBUILD_STARTED) + failureReasons.put(replica.endpoint(), RequestFailureReason.INDEX_BUILD_IN_PROGRESS); + else + failureReasons.put(replica.endpoint(), RequestFailureReason.INDEX_NOT_AVAILABLE); + }); throw new ReadFailureException(level, filtered, required, false, failureReasons); } @@ -230,7 +241,7 @@ public synchronized void propagateLocalIndexStatus(String keyspace, String index // Versions 5.0.0 through 5.0.2 use a much more bloated format that duplicates keyspace names // and writes full status names instead of their numeric codes. If the minimum cluster version is // unknown or one of those 3 versions, continue to propagate the old format. - CassandraVersion minVersion = Gossiper.instance.getMinVersion(1, TimeUnit.SECONDS); + CassandraVersion minVersion = Gossiper.instance.getMinVersion(); String newSerializedStatusMap = shouldWriteLegacyStatusFormat(minVersion) ? JsonUtils.writeAsJsonString(statusMap) : toSerializedFormat(statusMap); diff --git a/src/java/org/apache/cassandra/index/NoopIndex.java b/src/java/org/apache/cassandra/index/NoopIndex.java new file mode 100644 index 000000000000..0a377f3febe9 --- /dev/null +++ b/src/java/org/apache/cassandra/index/NoopIndex.java @@ -0,0 +1,197 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; + +import com.google.common.annotations.VisibleForTesting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.WriteContext; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.transactions.IndexTransaction; +import org.apache.cassandra.notifications.INotification; +import org.apache.cassandra.notifications.INotificationConsumer; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; + +/** + * Dummy index used when an unknown index is found in system_schema.index and INDEX_UNKNOWN_IGNORE=true + * + * This is just a mock implementation to prevent problems with old schemas containing unknown indexes. + * It ignores writes, declares that it supports no expressions, and throws an exception when asked to search. + */ +public final class NoopIndex implements Index, INotificationConsumer +{ + private static final String UNSUPPORTED_MESSAGE = "Custom index %s.%s of type '%s' on column %s is not supported anymore. " + + CassandraRelevantProperties.INDEX_UNKNOWN_IGNORE.getKey() + + " is enabled so using an noop index that will ignore writes and won't be queryable. " + + "Please drop this index and/or use a Storage Attached Index (SAI) instead."; + @VisibleForTesting + static final Callable NO_OP_TASK = () -> null; + + private static final Logger logger = LoggerFactory.getLogger(NoopIndex.class); + + private final IndexMetadata config; + private final ColumnMetadata column; + + public NoopIndex(ColumnFamilyStore baseCfs, IndexMetadata config) + { + this.config = config; + column = TargetParser.parse(baseCfs.metadata(), config).left; + } + + public String getUnsupportedMessage() + { + return String.format(UNSUPPORTED_MESSAGE, column.ksName, config.name, config.getIndexClassName(), column.name); + } + + @SuppressWarnings("unused") + public static Map validateOptions(Map options, TableMetadata metadata) + { + return Collections.emptyMap(); + } + + @Override + public void register(IndexRegistry registry) + { + registry.registerIndex(this); + logger.error(getUnsupportedMessage()); + } + + @Override + public IndexMetadata getIndexMetadata() + { + return config; + } + + @Override + public Callable getInitializationTask() + { + return null; + } + + @Override + public Callable getMetadataReloadTask(IndexMetadata indexMetadata) + { + return null; + } + + @Override + public Callable getBlockingFlushTask() + { + return NO_OP_TASK; + } + + @Override + public Callable getInvalidateTask() + { + return NO_OP_TASK; + } + + @Override + public Callable getTruncateTask(long truncatedAt) + { + return NO_OP_TASK; + } + + @Override + public boolean shouldBuildBlocking() + { + return true; + } + + @Override + public Optional getBackingTable() + { + return Optional.empty(); + } + + @Override + public boolean dependsOn(ColumnMetadata column) + { + return this.column.name.equals(column.name); + } + + @Override + public boolean supportsExpression(ColumnMetadata column, Operator operator) + { + return false; + } + + @Override + public AbstractType customExpressionValueType() + { + return null; + } + + @Override + public RowFilter getPostIndexQueryFilter(RowFilter filter) + { + return filter; + } + + @Override + public long getEstimatedResultRows() + { + return Long.MAX_VALUE; + } + + @Override + public void validate(PartitionUpdate update, ClientState state) + { + } + + @Override + public Indexer indexerFor(DecoratedKey key, + RegularAndStaticColumns columns, + long nowInSec, + WriteContext ctx, + IndexTransaction.Type transactionType, + Memtable memtable) + { + return null; + } + + @Override + public Searcher searcherFor(ReadCommand command) throws InvalidRequestException + { + throw new UnsupportedOperationException(getUnsupportedMessage()); + } + + @Override + public void handleNotification(INotification notification, Object sender) + { + // Nothing to handle here since we don't index anything + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java b/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java index 73dc3345a250..eeaf0ce81d70 100644 --- a/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java +++ b/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java @@ -17,12 +17,12 @@ */ package org.apache.cassandra.index; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.AbstractTableOperation; /** * Manages building an entire index from column family data. Runs on to compaction manager. */ -public abstract class SecondaryIndexBuilder extends CompactionInfo.Holder +public abstract class SecondaryIndexBuilder extends AbstractTableOperation { public abstract void build(); diff --git a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java index 496c21068f80..697709c37add 100644 --- a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java +++ b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java @@ -19,8 +19,21 @@ import java.io.UncheckedIOException; import java.lang.reflect.Constructor; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.StringJoiner; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; @@ -29,7 +42,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; - +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; @@ -41,6 +54,9 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; //checkstyle: permit this import +import com.google.common.util.concurrent.ListenableFuture; //checkstyle: permit this import +import com.google.common.util.concurrent.SettableFuture; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,11 +65,29 @@ import org.apache.cassandra.concurrent.FutureTask; import org.apache.cassandra.concurrent.ImmediateExecutor; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.*; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.MutableDeletionInfo; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.WriteContext; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.View; @@ -61,7 +95,15 @@ import org.apache.cassandra.db.partitions.ImmutableBTreePartition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowDiffListener; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.Index.IndexBuildingSupport; import org.apache.cassandra.index.internal.CassandraIndex; @@ -70,9 +112,11 @@ import org.apache.cassandra.index.transactions.IndexTransaction; import org.apache.cassandra.index.transactions.UpdateTransaction; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.notifications.INotification; import org.apache.cassandra.notifications.INotificationConsumer; import org.apache.cassandra.notifications.SSTableAddedNotification; +import org.apache.cassandra.notifications.SSTableListChangedNotification; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.Indexes; @@ -82,10 +126,15 @@ import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JVMStabilityInspector; -import org.apache.cassandra.utils.concurrent.*; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; +import org.apache.cassandra.utils.concurrent.Promise; +import org.apache.cassandra.utils.concurrent.Refs; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.config.CassandraRelevantProperties.FORCE_DEFAULT_INDEXING_PAGE_SIZE; +import static org.apache.cassandra.config.CassandraRelevantProperties.INDEX_UNKNOWN_IGNORE; import static org.apache.cassandra.utils.ExecutorUtils.awaitTermination; import static org.apache.cassandra.utils.ExecutorUtils.shutdown; @@ -155,7 +204,7 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum * The indexes that are available for querying. */ private final Set queryableIndexes = Sets.newConcurrentHashSet(); - + /** * The indexes that are available for writing. */ @@ -164,7 +213,7 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum /** * The groups of all the registered indexes */ - private final Map indexGroups = Maps.newConcurrentMap(); + private final ConcurrentMap indexGroups = Maps.newConcurrentMap(); /** * The count of pending index builds for each index. @@ -234,15 +283,18 @@ private synchronized Future createIndex(IndexMetadata indexDef, boolean is @VisibleForTesting public Future buildIndex(final Index index) { - FutureTask initialBuildTask = null; + FutureTask initialBuildTask = new FutureTask<>(() -> null); // if the index didn't register itself, we can probably assume that no initialization needs to happen if (indexes.containsKey(index.getIndexMetadata().name)) { try { - Callable call = DatabaseDescriptor.isDaemonInitialized() ? index.getInitializationTask() : null; - if (call != null) - initialBuildTask = new FutureTask<>(call); + if (!index.shouldSkipInitialization()) + { + Callable call = index.getInitializationTask(); + if (call != null) + initialBuildTask = new FutureTask<>(call); + } } catch (Throwable t) { @@ -251,13 +303,6 @@ public Future buildIndex(final Index index) } } - // if there's no initialization, just mark as built and return: - if (initialBuildTask == null) - { - markIndexBuilt(index, true); - return ImmediateFuture.success(null); - } - // otherwise run the initialization task asynchronously with a callback to mark it built or failed final Promise initialization = new AsyncPromise<>(); // we want to ensure we invoke this task asynchronously, so we want to add our callback before submission @@ -265,7 +310,8 @@ public Future buildIndex(final Index index) // This is because Keyspace.open("system") can transitively attempt to open Keyspace.open("system") initialBuildTask.addCallback( success -> { - markIndexBuilt(index, true); + if (IndexBuildDecider.instance.isIndexQueryableAfterInitialBuild(baseCfs)) + markIndexBuilt(index, true); initialization.trySuccess(null); }, failure -> { @@ -301,7 +347,18 @@ public synchronized Future addIndex(IndexMetadata indexDef, boolean isNewCF) */ public boolean isIndexQueryable(Index index) { - return queryableIndexes.contains(index.getIndexMetadata().name); + return isIndexQueryable(index.getIndexMetadata().name); + } + + /** + * Checks if the specified index is queryable. + * + * @param indexName name of the index + * @return true if the specified index is registered, false otherwise + */ + public boolean isIndexQueryable(String indexName) + { + return queryableIndexes.contains(indexName); } /** @@ -313,10 +370,22 @@ public boolean isIndexQueryable(Index index) */ public void checkQueryability(Index.QueryPlan queryPlan) { + InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort(); + for (Index index : queryPlan.getIndexes()) { + String indexName = index.getIndexMetadata().name; + Index.Status indexStatus = IndexStatusManager.instance.getIndexStatus(endpoint, keyspace.getName(), indexName); + if (!isIndexQueryable(index)) + { + // In Astra index can be queryable during index build, thus we need to check both not queryable and building + // Plus isQueryable is always true for non-SAI index implementations + if (indexStatus == Index.Status.FULL_REBUILD_STARTED) + throw new IndexBuildInProgressException(index); + throw new IndexNotAvailableException(index); + } } } @@ -346,17 +415,19 @@ public synchronized boolean isIndexBuilding(String indexName) public synchronized void removeIndex(String indexName) { - Index removedIndex = indexes.remove(indexName); + Index removed = indexes.remove(indexName); + logger.trace(removed == null ? "Index {} was not registered" : "Removed index {} from registry", indexName); - if (removedIndex != null) + if (null != removed) { - removedIndex.unregister(this); + removed.unregister(this); - markIndexRemoved(indexName); - executeBlocking(removedIndex.getInvalidateTask(), null); + markIndexRemoved(removed); + executeBlocking(removed.getInvalidateTask(), null); } } + public Set getDependentIndexes(ColumnMetadata column) { if (indexes.isEmpty()) @@ -375,7 +446,7 @@ public Set getDependentIndexes(ColumnMetadata column) */ public void markAllIndexesRemoved() { - getBuiltIndexNames().forEach(this::markIndexRemoved); + listIndexes().forEach(this::markIndexRemoved); } /** @@ -500,14 +571,14 @@ public static String getIndexName(String cfName) } /** - * Validates all index groups against the specified SSTables. + * Validates all index groups against the specified SSTables. * * @param sstables SSTables for which indexes in the group should be built * @param throwOnIncomplete whether to throw an error if any index in the group is incomplete * @param validateChecksum whether to validate checksum or not * * @return true if all indexes in all groups are complete and valid - * false if an index in any group is incomplete and {@code throwOnIncomplete} is false + * false if an index in any group is incomplete and {@code throwOnIncomplete} is false * * @throws IllegalStateException if {@code throwOnIncomplete} is true and an index in any group is incomplete * @throws UncheckedIOException if there is a problem validating any on-disk component in any group @@ -557,27 +628,28 @@ public void buildSSTableAttachedIndexesBlocking(Collection sstabl } // Schedule all index building tasks with callbacks to handle success and failure - List> futures = new ArrayList<>(byType.size()); + List> futures = new ArrayList<>(byType.size()); byType.forEach((buildingSupport, groupedIndexes) -> { - SecondaryIndexBuilder builder = buildingSupport.getIndexBuildTask(baseCfs, groupedIndexes, sstables, false); - AsyncPromise build = new AsyncPromise<>(); - CompactionManager.instance.submitIndexBuild(builder).addCallback(new FutureCallback() + List builders = buildingSupport.getParallelIndexBuildTasks(baseCfs, groupedIndexes, sstables, false); + List> builderFutures = builders.stream().map(CompactionManager.instance::submitIndexBuild).collect(Collectors.toList()); + final SettableFuture build = SettableFuture.create(); + Futures.addCallback(Futures.allAsList(builderFutures), new FutureCallback() { @Override public void onFailure(Throwable t) { - logger.warn("Failed to incrementally build indexes {}", getIndexNames(groupedIndexes)); - build.tryFailure(t); + logger.warn("Failed to incrementally build indexes {}", groupedIndexes.stream().map(i -> i.getIndexMetadata().name).collect(Collectors.toList())); + build.setException(t); } @Override public void onSuccess(Object o) { - logger.info("Incremental index build of {} completed", getIndexNames(groupedIndexes)); - build.trySuccess(o); + logger.info("Incremental index build of {} completed", groupedIndexes.stream().map(i -> i.getIndexMetadata().name).collect(Collectors.toList())); + build.set(o); } - }); + }, ImmediateExecutor.INSTANCE); futures.add(build); }); @@ -590,16 +662,28 @@ public void onSuccess(Object o) *

    * If the index doesn't support ALL {@link Index.LoadType} it performs a recovery {@link Index#getRecoveryTaskSupport()} * instead of a build {@link Index#getBuildTaskSupport()} - * + * * @param sstables the SSTables to be (re)indexed * @param indexes the indexes to be (re)built for the specifed SSTables * @param isFullRebuild True if this method is invoked as a full index rebuild, false otherwise */ @SuppressWarnings({"unchecked", "RedundantSuppression"}) - private void buildIndexesBlocking(Collection sstables, Set indexes, boolean isFullRebuild) + public void buildIndexesBlocking(Collection sstables, Set indexes, boolean isFullRebuild) + { + FBUtilities.waitOnFuture(buildIndexesAsync(sstables, indexes, isFullRebuild)); + } + + /** + * Performs an asynchronous (re)indexing of the specified SSTables for the specified indexes. + * + * @param sstables the SSTables to be (re)indexed + * @param indexes the indexes to be (re)built for the specified SSTables + * @param isFullRebuild True if this method is invoked as a full index rebuild, false otherwise + */ + private java.util.concurrent.Future buildIndexesAsync(Collection sstables, Set indexes, boolean isFullRebuild) { if (indexes.isEmpty()) - return; + return ImmediateFuture.success(null); // Mark all indexes as building: this step must happen first, because if any index can't be marked, the whole // process needs to abort @@ -609,116 +693,112 @@ private void buildIndexesBlocking(Collection sstables, Set final Set builtIndexes = Sets.newConcurrentHashSet(); final Set unbuiltIndexes = Sets.newConcurrentHashSet(); - // Any exception thrown during index building that could be suppressed by the finally block - Exception accumulatedFail = null; + logger.info("Submitting index {} of {} for data in {}", + isFullRebuild ? "recovery" : "build", + Index.joinNames(indexes), + sstables.stream().map(SSTableReader::toString).collect(Collectors.joining(","))); - try + // Group all building tasks + Map> byType = new HashMap<>(); + for (Index index : indexes) { - logger.info("Submitting index {} of {} for data in {}", - isFullRebuild ? "recovery" : "build", - commaSeparated(indexes), - sstables.stream().map(SSTableReader::toString).collect(Collectors.joining(","))); - - // Group all building tasks - Map> byType = new HashMap<>(); - for (Index index : indexes) - { - IndexBuildingSupport buildOrRecoveryTask = isFullRebuild - ? index.getBuildTaskSupport() - : index.getRecoveryTaskSupport(); - Set stored = byType.computeIfAbsent(buildOrRecoveryTask, i -> new HashSet<>()); - stored.add(index); - } + IndexBuildingSupport buildOrRecoveryTask = isFullRebuild + ? index.getBuildTaskSupport() + : index.getRecoveryTaskSupport(); + Set stored = byType.computeIfAbsent(buildOrRecoveryTask, i -> new HashSet<>()); + stored.add(index); + } - // Schedule all index building tasks with a callback to mark them as built or failed - List> futures = new ArrayList<>(byType.size()); - byType.forEach((buildingSupport, groupedIndexes) -> + // Schedule all index building tasks with a callback to mark them as built or failed + List> futures = new ArrayList<>(byType.size()); + byType.forEach((buildingSupport, groupedIndexes) -> + { + List builders = buildingSupport.getParallelIndexBuildTasks(baseCfs, groupedIndexes, sstables, isFullRebuild); + List> builderFutures = builders.stream().map(CompactionManager.instance::submitIndexBuild).collect(Collectors.toList()); + final SettableFuture build = SettableFuture.create(); + Futures.addCallback(Futures.allAsList(builderFutures), new FutureCallback() { - SecondaryIndexBuilder builder = buildingSupport.getIndexBuildTask(baseCfs, groupedIndexes, sstables, isFullRebuild); - final AsyncPromise build = new AsyncPromise<>(); - CompactionManager.instance.submitIndexBuild(builder).addCallback(new FutureCallback() + @Override + public void onFailure(Throwable t) { - @Override - public void onFailure(Throwable t) - { - logAndMarkIndexesFailed(groupedIndexes, t, false); - unbuiltIndexes.addAll(groupedIndexes); - build.tryFailure(t); - } - - @Override - public void onSuccess(Object o) - { - groupedIndexes.forEach(i -> markIndexBuilt(i, isFullRebuild)); - logger.info("Index build of {} completed", getIndexNames(groupedIndexes)); - builtIndexes.addAll(groupedIndexes); - build.trySuccess(o); - } - }); - futures.add(build); - }); - - // Finally wait for the index builds to finish and flush the indexes that built successfully - FBUtilities.waitOnFutures(futures); - } - catch (Exception e) - { - accumulatedFail = e; - throw e; - } - finally - { + logAndMarkIndexesFailed(groupedIndexes, t, false); + unbuiltIndexes.addAll(groupedIndexes); + build.setException(t); + } + + @Override + public void onSuccess(Object o) + { + groupedIndexes.forEach(i -> markIndexBuilt(i, isFullRebuild)); + logger.info("Index build of {} completed", Index.joinNames(groupedIndexes)); + builtIndexes.addAll(groupedIndexes); + build.set(o); + } + }, ImmediateExecutor.INSTANCE); + futures.add(build); + }); + + ListenableFuture> allIndexBuilds = Futures.allAsList(futures); + SettableFuture finalResult = SettableFuture.create(); + allIndexBuilds.addListener(() -> { try { - // Fail any indexes that couldn't be marked - Set failedIndexes = Sets.difference(indexes, Sets.union(builtIndexes, unbuiltIndexes)); - if (!failedIndexes.isEmpty()) - { - logAndMarkIndexesFailed(failedIndexes, accumulatedFail, false); - } - - // Flush all built indexes with an aynchronous callback to log the success or failure of the flush - flushIndexesBlocking(builtIndexes, new FutureCallback<>() - { - final String indexNames = StringUtils.join(builtIndexes.stream() - .map(i -> i.getIndexMetadata().name) - .collect(Collectors.toList()), ','); - - @Override - public void onFailure(Throwable ignored) - { - logger.info("Index flush of {} failed", indexNames); - } + finalizeIndexBuild(indexes, builtIndexes, unbuiltIndexes); + finalResult.setFuture(allIndexBuilds); + } + catch (Exception ex) + { + finalResult.setException(ex); + } + }, ImmediateExecutor.INSTANCE); + return finalResult; + } - @Override - public void onSuccess(Object ignored) - { - logger.info("Index flush of {} completed", indexNames); - } - }); + private void finalizeIndexBuild(Set indexes, Set builtIndexes, Set unbuiltIndexes) + { + Exception accumulatedFail = null; + try + { + // Fail any indexes that couldn't be marked + Set failedIndexes = Sets.difference(indexes, Sets.union(builtIndexes, unbuiltIndexes)); + if (!failedIndexes.isEmpty()) + { + logAndMarkIndexesFailed(failedIndexes, accumulatedFail, false); } - catch (Exception e) + + // Flush all built indexes with an aynchronous callback to log the success or failure of the flush + flushIndexesBlocking(builtIndexes, new FutureCallback() { - if (accumulatedFail != null) + String indexNames = StringUtils.join(builtIndexes.stream() + .map(i -> i.getIndexMetadata().name) + .collect(Collectors.toList()), ','); + + @Override + public void onFailure(Throwable ignored) { - accumulatedFail.addSuppressed(e); + logger.info("Index flush of {} failed", indexNames); } - else + + @Override + public void onSuccess(Object ignored) { - throw e; + logger.info("Index flush of {} completed", indexNames); } + }); + } + catch (Exception e) + { + if (accumulatedFail != null) + { + accumulatedFail.addSuppressed(e); + } + else + { + throw e; } } } - private String getIndexNames(Set indexes) - { - List indexNames = indexes.stream() - .map(i -> i.getIndexMetadata().name) - .collect(Collectors.toList()); - return StringUtils.join(indexNames, ','); - } - /** * Marks the specified indexes as (re)building if: * 1) There's no in progress rebuild of any of the given indexes. @@ -787,12 +867,12 @@ public synchronized void markIndexesBuilding(Set indexes, boolean isFullR * @param index the index to be marked as built * @param isFullRebuild {@code true} if this method is invoked as a full index rebuild, {@code false} otherwise */ - private synchronized void markIndexBuilt(Index index, boolean isFullRebuild) + public synchronized void markIndexBuilt(Index index, boolean isFullRebuild) { String indexName = index.getIndexMetadata().name; if (isFullRebuild) makeIndexQueryable(index, Index.Status.BUILD_SUCCEEDED); - + AtomicInteger counter = inProgressBuilds.get(indexName); if (counter != null) { @@ -834,6 +914,8 @@ private synchronized void markIndexFailed(Index index, boolean isInitialBuild) if (!index.getSupportedLoadTypeOnFailure(isInitialBuild).supportsReads() && queryableIndexes.remove(indexName)) logger.info("Index [{}] became not-queryable because of failed build.", indexName); + + makeIndexNonQueryable(index, Index.Status.BUILD_FAILED); } } @@ -841,19 +923,20 @@ private void logAndMarkIndexesFailed(Set indexes, Throwable indexBuildFai { JVMStabilityInspector.inspectThrowable(indexBuildFailure); if (indexBuildFailure != null) - logger.warn("Index build of {} failed. Please run full index rebuild to fix it.", getIndexNames(indexes), indexBuildFailure); + logger.warn("Index build of {} failed. Please run full index rebuild to fix it.", Index.joinNames(indexes), indexBuildFailure); else - logger.warn("Index build of {} failed. Please run full index rebuild to fix it.", getIndexNames(indexes)); + logger.warn("Index build of {} failed. Please run full index rebuild to fix it.", Index.joinNames(indexes)); indexes.forEach(i -> this.markIndexFailed(i, isInitialBuild)); } /** * Marks the specified index as removed. * - * @param indexName the index name + * @param index the index to be removed */ - private synchronized void markIndexRemoved(String indexName) + private synchronized void markIndexRemoved(Index index) { + String indexName = index.getIndexMetadata().name; SystemKeyspace.setIndexRemoved(baseCfs.getKeyspaceName(), indexName); queryableIndexes.remove(indexName); writableIndexes.remove(indexName); @@ -863,24 +946,46 @@ private synchronized void markIndexRemoved(String indexName) IndexStatusManager.instance.propagateLocalIndexStatus(keyspace.getName(), indexName, Index.Status.DROPPED); } + @Override public Index getIndexByName(String indexName) { return indexes.get(indexName); } + @Nullable private Index createInstance(IndexMetadata indexDef) { Index newIndex; if (indexDef.isCustom()) { assert indexDef.options != null; - // Get the fully qualified index class name from the index metadata - String className = indexDef.getIndexClassName(); + // Find any aliases to the fully qualified index class name: + String className = IndexMetadata.expandAliases(indexDef.options.get(IndexTarget.CUSTOM_INDEX_OPTION_NAME)); assert !Strings.isNullOrEmpty(className); try { - Class indexClass = FBUtilities.classForName(className, "Index"); + Class indexClass; + try + { + indexClass = FBUtilities.classForName(className, "Index"); + } + catch (ConfigurationException e) + { + // ConfigurationException can only be thrown from FBUtilities.classForName(…) + // anything from the index constructor will get wrapped in an InvocationTargetException + if (INDEX_UNKNOWN_IGNORE.getBoolean()) + { + logger.error("Cannot find index type {}, but '{}' is true so creating noop index {}", + className, INDEX_UNKNOWN_IGNORE.getKey(), indexDef.name); + + indexClass = NoopIndex.class; + } + else + { + throw e; + } + } Constructor ctor = indexClass.getConstructor(ColumnFamilyStore.class, IndexMetadata.class); newIndex = ctor.newInstance(baseCfs, indexDef); } @@ -917,6 +1022,15 @@ public void dropAllIndexes(boolean dropData) indexGroups.forEach((key, group) -> group.invalidate()); } + /** + * unload all indexes without removing index data + */ + public void unloadAllIndexes() + { + executeAllBlocking(indexes.values().stream(), Index::getUnloadTask, null); + indexGroups.forEach((key, group) -> group.unload()); + } + @VisibleForTesting public void invalidateAllIndexesBlocking() { @@ -944,7 +1058,7 @@ public void flushIndexesBlocking(Set indexes) */ public void executePreJoinTasksBlocking(boolean hadBootstrap) { - logger.info("Executing pre-join{} tasks for: {}", hadBootstrap ? " post-bootstrap" : "", this.baseCfs); + logger.debug("Executing pre-join{} tasks for: {}", hadBootstrap ? " post-bootstrap" : "", this.baseCfs); executeAllBlocking(indexes.values().stream(), (index) -> { return index.getPreJoinTask(hadBootstrap); @@ -1015,7 +1129,7 @@ public boolean hasIndexes() return !indexes.isEmpty(); } - public void indexPartition(DecoratedKey key, Set indexes, int pageSize) + public void indexPartition(DecoratedKey key, Set indexes, PageSize pageSize) { indexPartition(key, indexes, pageSize, baseCfs.metadata().regularAndStaticColumns()); } @@ -1028,7 +1142,7 @@ public void indexPartition(DecoratedKey key, Set indexes, int pageSize) * @param pageSize the number of {@link Unfiltered} objects to process in a single page * @param columns the columns indexed by at least one of the supplied indexes */ - public void indexPartition(DecoratedKey key, Set indexes, int pageSize, RegularAndStaticColumns columns) + public void indexPartition(DecoratedKey key, Set indexes, PageSize pageSize, RegularAndStaticColumns columns) { if (logger.isTraceEnabled()) logger.trace("Indexing partition {}", baseCfs.metadata().partitionKeyType.getString(key.getKey())); @@ -1065,20 +1179,16 @@ public void indexPartition(DecoratedKey key, Set indexes, int pageSize, R try (WriteContext ctx = keyspace.getWriteHandler().createContextForIndexing()) { { - Set indexers = new HashSet<>(indexGroups.size()); - - for (Index.Group g : indexGroups.values()) - { - Index.Indexer indexerFor = g.indexerFor(indexes::contains, - key, - partition.columns(), - nowInSec, - ctx, - IndexTransaction.Type.UPDATE, - null); - if (indexerFor != null) - indexers.add(indexerFor); - } + Set indexers = indexGroups.values().stream() + .map(g -> g.indexerFor(indexes::contains, + key, + partition.columns(), + nowInSec, + ctx, + IndexTransaction.Type.UPDATE, + null)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); // Short-circuit empty partitions if static row is processed or isn't read if (!readStatic && partition.isEmpty() && partition.staticRow().isEmpty()) @@ -1141,39 +1251,12 @@ public void indexPartition(DecoratedKey key, Set indexes, int pageSize, R /** * Return the page size used when indexing an entire partition */ - public int calculateIndexingPageSize() + public PageSize calculateIndexingPageSize() { if (FORCE_DEFAULT_INDEXING_PAGE_SIZE.getBoolean()) - return DEFAULT_PAGE_SIZE; - - double targetPageSizeInBytes = 32 * 1024 * 1024; - double meanPartitionSize = baseCfs.getMeanPartitionSize(); - if (meanPartitionSize <= 0) - return DEFAULT_PAGE_SIZE; - - int meanCellsPerPartition = baseCfs.getMeanEstimatedCellPerPartitionCount(); - if (meanCellsPerPartition <= 0) - return DEFAULT_PAGE_SIZE; - - int columnsPerRow = baseCfs.metadata().regularColumns().size(); - if (columnsPerRow <= 0) - return DEFAULT_PAGE_SIZE; + return PageSize.inRows(DEFAULT_PAGE_SIZE); - int meanRowsPerPartition = meanCellsPerPartition / columnsPerRow; - double meanRowSize = meanPartitionSize / meanRowsPerPartition; - - int pageSize = (int) Math.max(1, Math.min(DEFAULT_PAGE_SIZE, targetPageSizeInBytes / meanRowSize)); - - logger.trace("Calculated page size {} for indexing {}.{} ({}/{}/{}/{})", - pageSize, - baseCfs.metadata.keyspace, - baseCfs.metadata.name, - meanPartitionSize, - meanCellsPerPartition, - meanRowsPerPartition, - meanRowSize); - - return pageSize; + return PageSize.inBytes(32 * 1024 * 1024); } /** @@ -1215,7 +1298,7 @@ public void deletePartition(UnfilteredRowIterator partition, long nowInSec) /** * Called at query time to choose which (if any) of the registered index implementations to use for a given query. *

    - * This is a two step processes, firstly compiling the set of searchable indexes then choosing the one which reduces + * This is a two-step processes, firstly compiling the set of searchable indexes then choosing the one which reduces * the search space the most. *

    * In the first phase, if the command's RowFilter contains any custom index expressions, the indexes that they @@ -1224,6 +1307,15 @@ public void deletePartition(UnfilteredRowIterator partition, long nowInSec) *

    * The filtered set then sorted by selectivity, as reported by the Index implementations' getEstimatedResultRows * method. + * Once we have the filtered set of indexes, one is selected as the best one according to the following rules: + *

      + *
    1. An index included by the query's index hints is better than an index not included by the hints.
    2. + *
    3. If it's a contains restriction, then a non-analyzed index is better. See CNDB-13925 for details.
    4. + *
    5. An index more selective according to {@link Index#getEstimatedResultRows()} is better. This is done + * accordingly to the {@link Index.QueryPlan#getEstimatedResultRows()} method. Please note that some index + * implementations (SAI) will always return -1 for that method to prioritize themselves. Third party + * implementations can also return similar fixed values. See CNDB-14764 for details.
    6. + *
    *

    * Implementation specific validation of the target expression, either custom or standard, by the selected * index should be performed in the searcherFor method to ensure that we pick the right index regardless of @@ -1242,7 +1334,7 @@ public Index.QueryPlan getBestIndexQueryPlanFor(RowFilter rowFilter) if (indexes.isEmpty() || rowFilter.isEmpty()) return null; - for (RowFilter.Expression expression : rowFilter) + for (RowFilter.Expression expression : rowFilter.expressions()) { if (expression.isCustom()) { @@ -1272,12 +1364,26 @@ public Index.QueryPlan getBestIndexQueryPlanFor(RowFilter rowFilter) return null; } + // Prepare a plan comparator based first on the user-provided hints, which will prefer the plan closest to + // satisfying the index hints, then on rules about peference of non-analyzed indexes over analyzed indexes for + // CONTAINS operators as described by CNDB-13925, and finally on the index-provided selectivity, which will + // prefer the most selective index. + // Selectivity is determined by the Index#getEstimatedResultRows() method. Please note that some index + // implementations (SASI and SAI) will always return -1 for that method to prioritize themselves. Third party + // implementations can also return similar fixed values. See CNDB-14764 for details. + // We let pass plans that don't satisfy the index hints so we can provide a better error message later, + // at the validation at the end of this method. That validation will be done over the plan that is closest to + // satisfying the index hints, so the error message will only complain about the missing parts. + Comparator planComparator = rowFilter.indexHints.comparator() + .thenComparing((Index.QueryPlan plan) -> !hasAnalyzerOnContains(plan, rowFilter)) + .thenComparing(Comparator.naturalOrder().reversed()); + // find the best plan Index.QueryPlan selected = queryPlans.size() == 1 ? Iterables.getOnlyElement(queryPlans) : queryPlans.stream() - .min(Comparator.naturalOrder()) - .orElseThrow(() -> new AssertionError("Could not select most selective index")); + .max(planComparator) + .orElseThrow(() -> new AssertionError("Could not select an index plan.")); // pay for an additional threadlocal get() rather than build the strings unnecessarily if (Tracing.isTracing()) @@ -1294,6 +1400,22 @@ public Index.QueryPlan getBestIndexQueryPlanFor(RowFilter rowFilter) return selected; } + private static boolean hasAnalyzerOnContains(Index.QueryPlan plan, RowFilter rowFilter) + { + for (RowFilter.Expression expression : rowFilter.expressions()) + { + if (expression.isAnyContains()) + { + for (Index index : plan.getIndexes()) + { + if (index.supportsExpression(expression) && index.isAnalyzed()) + return true; + } + } + } + return false; + } + private static String commaSeparated(Collection indexes) { StringJoiner joiner = new StringJoiner(","); @@ -1304,26 +1426,23 @@ private static String commaSeparated(Collection indexes) return joiner.toString(); } - public Optional getBestIndexFor(RowFilter.Expression expression) + public Optional getBestIndexFor(ColumnMetadata column, Operator operator) { - for (Index i : indexes.values()) - { - if (i.supportsExpression(expression.column(), expression.operator())) - { - return Optional.of(i); - } - } - - return Optional.empty(); + return IndexHints.NONE.getBestIndexFor(indexes.values(), i -> i.supportsExpression(column, operator), operator.isAnyContains()); } - public Optional getBestIndexFor(RowFilter.Expression expression, Class indexType) + @Override + public Optional getBestIndexFor(ColumnMetadata column, Operator operator, IndexHints hints) { - for (Index i : indexes.values()) - if (indexType.isInstance(i) && i.supportsExpression(expression.column(), expression.operator())) - return Optional.of(indexType.cast(i)); + return hints.getBestIndexFor(indexes.values(), i -> i.supportsExpression(column, operator), operator.isAnyContains()); + } - return Optional.empty(); + public Optional getBestIndexFor(RowFilter.Expression expression, IndexHints hints, Class indexType) + { + return hints.getBestIndexFor(indexes.values(), + i -> i.supportsExpression(expression) && indexType.isInstance(i), + expression.isAnyContains()) + .map(indexType::cast); } /** @@ -1356,7 +1475,7 @@ public void registerIndex(Index index, Index.Group.Key groupKey, Supplier groupSupplier.get()); - // add the created index to its group if it is not a singleton group + // add the created index to its group group.addIndex(index); } @@ -1369,8 +1488,8 @@ public void unregisterIndex(Index removed, Index.Group.Key groupKey) // Remove the index from non-singleton groups... group.removeIndex(removed); - // if the group is a singleton or there are no more indexes left in the group, remove it - if (group.isSingleton() || group.getIndexes().isEmpty()) + // if no more indexes left in the group, remove it + if (group.getIndexes().isEmpty()) { Index.Group removedGroup = indexGroups.remove(groupKey); if (removedGroup != null) @@ -1379,7 +1498,7 @@ public void unregisterIndex(Index removed, Index.Group.Key groupKey) } } - public Index getIndex(IndexMetadata metadata) + public Index getIndex(@Nonnull IndexMetadata metadata) { return indexes.get(metadata.name); } @@ -1389,6 +1508,24 @@ public Collection listIndexes() return ImmutableSet.copyOf(indexes.values()); } + @Override + public Collection listNotExcludedIndexes(IndexHints hints) + { + if (indexes.isEmpty()) + return Collections.emptySet(); + + if (hints == IndexHints.NONE || hints.excluded.isEmpty()) + return listIndexes(); + + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Index index : indexes.values()) + { + if (!hints.excludes(index)) + builder.add(index); + } + return builder.build(); + } + public Set listIndexGroups() { return ImmutableSet.copyOf(indexGroups.values()); @@ -1408,7 +1545,7 @@ public Index.Group getIndexGroup(Index.Group.Key key) * associated to any group */ @Nullable - public Index.Group getIndexGroup(IndexMetadata metadata) + public Index.Group getIndexGroup(@Nonnull IndexMetadata metadata) { Index index = getIndex(metadata); return index == null ? null : getIndexGroup(index); @@ -1443,23 +1580,18 @@ public UpdateTransaction newUpdateTransaction(PartitionUpdate update, WriteConte if (!hasIndexes()) return UpdateTransaction.NO_OP; - List indexers = new ArrayList<>(indexGroups.size()); + Index.Indexer[] indexers = listIndexGroups().stream() + .map(g -> g.indexerFor(writableIndexSelector(), + update.partitionKey(), + update.columns(), + nowInSec, + ctx, + IndexTransaction.Type.UPDATE, + memtable)) + .filter(Objects::nonNull) + .toArray(Index.Indexer[]::new); - for (Index.Group g : indexGroups.values()) - { - Index.Indexer indexer = g.indexerFor(writableIndexSelector(), - update.partitionKey(), - update.columns(), - nowInSec, - ctx, - IndexTransaction.Type.UPDATE, - memtable); - if (indexer != null) - indexers.add(indexer); - } - - return indexers.isEmpty() ? UpdateTransaction.NO_OP - : new WriteTimeTransaction(indexers.toArray(Index.Indexer[]::new)); + return indexers.length == 0 ? UpdateTransaction.NO_OP : new WriteTimeTransaction(indexers); } private Predicate writableIndexSelector() @@ -1557,19 +1689,19 @@ public void onUpdated(Row existing, Row updated) // diff listener collates the columns to be added & removed from the indexes RowDiffListener diffListener = new RowDiffListener() { - public void onPrimaryKeyLivenessInfo(int i, Clustering clustering, LivenessInfo merged, LivenessInfo original) + public void onPrimaryKeyLivenessInfo(int i, Clustering clustering, LivenessInfo merged, LivenessInfo original) { } - public void onDeletion(int i, Clustering clustering, Row.Deletion merged, Row.Deletion original) + public void onDeletion(int i, Clustering clustering, Row.Deletion merged, Row.Deletion original) { } - public void onComplexDeletion(int i, Clustering clustering, ColumnMetadata column, DeletionTime merged, DeletionTime original) + public void onComplexDeletion(int i, Clustering clustering, ColumnMetadata column, DeletionTime merged, DeletionTime original) { } - public void onCell(int i, Clustering clustering, Cell merged, Cell original) + public void onCell(int i, Clustering clustering, Cell merged, Cell original) { if (merged != null && !merged.equals(original)) toInsert.addCell(merged); @@ -1591,7 +1723,7 @@ public void commit() indexer.finish(); } - private boolean shouldCleanupOldValue(Cell oldCell, Cell newCell) + private boolean shouldCleanupOldValue(Cell oldCell, Cell newCell) { // If either the value or timestamp is different, then we // should delete from the index. If not, then we can infer that @@ -1602,7 +1734,7 @@ private boolean shouldCleanupOldValue(Cell oldCell, Cell newCel // Completely identical cells (including expiring columns with // identical ttl & localExpirationTime) will not get this far due // to the oldCell.equals(newCell) in StandardUpdater.update - return !Cells.valueEqual(oldCell, newCell) || oldCell.timestamp() != newCell.timestamp(); + return !oldCell.value().equals(newCell.value()) || oldCell.timestamp() != newCell.timestamp(); } } @@ -1654,27 +1786,27 @@ public void onRowMerge(Row merged, Row... versions) final Row.Builder[] builders = new Row.Builder[versions.length]; RowDiffListener diffListener = new RowDiffListener() { - public void onPrimaryKeyLivenessInfo(int i, Clustering clustering, LivenessInfo merged, LivenessInfo original) + public void onPrimaryKeyLivenessInfo(int i, Clustering clustering, LivenessInfo merged, LivenessInfo original) { if (original != null && (merged == null || !merged.isLive(nowInSec))) getBuilder(i, clustering).addPrimaryKeyLivenessInfo(original); } - public void onDeletion(int i, Clustering clustering, Row.Deletion merged, Row.Deletion original) + public void onDeletion(int i, Clustering clustering, Row.Deletion merged, Row.Deletion original) { } - public void onComplexDeletion(int i, Clustering clustering, ColumnMetadata column, DeletionTime merged, DeletionTime original) + public void onComplexDeletion(int i, Clustering clustering, ColumnMetadata column, DeletionTime merged, DeletionTime original) { } - public void onCell(int i, Clustering clustering, Cell merged, Cell original) + public void onCell(int i, Clustering clustering, Cell merged, Cell original) { if (original != null && (merged == null || !merged.isLive(nowInSec))) getBuilder(i, clustering).addCell(original); } - private Row.Builder getBuilder(int index, Clustering clustering) + private Row.Builder getBuilder(int index, Clustering clustering) { if (builders[index] == null) { @@ -1821,21 +1953,45 @@ private void executeAllBlocking(Stream indexers, Function !i.isSSTableAttached()) - .collect(Collectors.toSet()), - false); + { + IndexBuildDecider.Decision decision = IndexBuildDecider.instance.onSSTableAdded(notice); + build(decision, notice.added, i -> i.shouldBuildBlocking() && !i.isSSTableAttached()); + } + } + else if (notification instanceof SSTableListChangedNotification) + { + SSTableListChangedNotification notice = (SSTableListChangedNotification) notification; + + IndexBuildDecider.Decision decision = IndexBuildDecider.instance.onSSTableListChanged(notice); + build(decision, notice.added, Index::shouldBuildBlocking); + } + } + + private void build(IndexBuildDecider.Decision decision, Iterable sstables, Predicate indexFilter) + { + if (decision == IndexBuildDecider.Decision.ASYNC) + { + buildIndexesAsync(Lists.newArrayList(sstables), + indexes.values().stream().filter(indexFilter).collect(Collectors.toSet()), + false); + } + else if (decision == IndexBuildDecider.Decision.SYNC) + { + buildIndexesBlocking(Lists.newArrayList(sstables), + indexes.values().stream().filter(indexFilter).collect(Collectors.toSet()), + false); } } @@ -1848,9 +2004,6 @@ public static void shutdownAndWait(long timeout, TimeUnit units) throws Interrup public void makeIndexNonQueryable(Index index, Index.Status status) { - if (status == Index.Status.BUILD_SUCCEEDED) - throw new IllegalStateException("Index cannot be marked non-queryable with status " + status); - String name = index.getIndexMetadata().name; if (indexes.get(name) == index) { @@ -1862,9 +2015,6 @@ public void makeIndexNonQueryable(Index index, Index.Status status) public void makeIndexQueryable(Index index, Index.Status status) { - if (status != Index.Status.BUILD_SUCCEEDED) - throw new IllegalStateException("Index cannot be marked queryable with status " + status); - String name = index.getIndexMetadata().name; if (indexes.get(name) == index) { @@ -1879,4 +2029,5 @@ public void makeIndexQueryable(Index index, Index.Status status) logger.info("Index [{}] became writable after successful build.", name); } } + } diff --git a/src/java/org/apache/cassandra/index/SingletonIndexGroup.java b/src/java/org/apache/cassandra/index/SingletonIndexGroup.java index 162247fd743e..ba75222a623f 100644 --- a/src/java/org/apache/cassandra/index/SingletonIndexGroup.java +++ b/src/java/org/apache/cassandra/index/SingletonIndexGroup.java @@ -25,16 +25,21 @@ import java.util.Set; import java.util.function.Predicate; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; + import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.WriteContext; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.transactions.IndexTransaction; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableFlushObserver; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.TableMetadata; /** @@ -42,13 +47,11 @@ */ public class SingletonIndexGroup implements Index.Group { - private final Index delegate; - private final Set indexes; + private volatile Index delegate; + private final Set indexes = Sets.newConcurrentHashSet(); - protected SingletonIndexGroup(Index delegate) + protected SingletonIndexGroup() { - this.delegate = delegate; - this.indexes = Collections.singleton(delegate); } @Override @@ -62,6 +65,26 @@ public Index getIndex() return delegate; } + @Override + public void addIndex(Index index) + { + Preconditions.checkState(delegate == null); + // This class does not work for SAI because the `componentsForNewBuid` method would be incorrect (so more + // generally, it does not work for indexes that use dedicated sstable components, which is only SAI). See + // comments on `componentsForNewBuid` for more details. + Preconditions.checkState(!(index instanceof StorageAttachedIndex), "This shoudl not be used with SAI"); + delegate = index; + indexes.add(index); + } + + @Override + public void removeIndex(Index index) + { + Preconditions.checkState(containsIndex(index)); + delegate = null; + indexes.clear(); + } + @Override public boolean containsIndex(Index index) { @@ -77,25 +100,56 @@ public Index.Indexer indexerFor(Predicate indexSelector, IndexTransaction.Type transactionType, Memtable memtable) { - return indexSelector.test(delegate) ? delegate.indexerFor(key, columns, nowInSec, ctx, transactionType, memtable) - : null; + Preconditions.checkNotNull(delegate); + return indexSelector.test(delegate) + ? delegate.indexerFor(key, columns, nowInSec, ctx, transactionType, memtable) + : null; } @Override public Index.QueryPlan queryPlanFor(RowFilter rowFilter) { - return SingletonIndexQueryPlan.create(delegate, rowFilter); + Preconditions.checkNotNull(delegate); + + if (rowFilter.indexHints.excludes(delegate)) + return null; + + // Indexes using a singleton group don't support disjunctions, + // so we only consider the top-level AND expressions for index selection. + for (RowFilter.Expression e : rowFilter.withoutDisjunctions().expressions()) + { + if (delegate.supportsExpression(e)) + return new SingletonIndexQueryPlan(delegate, delegate.getPostIndexQueryFilter(rowFilter)); + } + + return null; } @Override - public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata) + public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata, long keyCount) { + Preconditions.checkNotNull(delegate); return delegate.getFlushObserver(descriptor, tracker); } @Override - public Set getComponents() + public Set componentsForNewSSTable() + { + // This class is only used for indexes that don't use per-sstable components, aka not-SAI (note that SASI uses + // some "file" per sstable, but it is not a `Component` in practice). We could add an equivalent + // `componentsForNewSSTable` method in `Index`, so that we can call `delegate.componentsForNewBuild` here, but + // this would kind of weird for SAI because of the per-sstable components: should they be returned by such + // method on `Index` or not? Tldr, for SAI, it's cleaner to deal with components created at the group level, + // which is what `StorageAttachedIndexGroup.componentsForNewBuild` does, and it's simpler to always use + // `StorageAttachedIndexGroup` for SAI, which is the case. So at that point, adding an + // `Index.componentsForNewSSTable` method would just be dead code, so let's avoid it. + return Collections.emptySet(); + } + + @Override + public Set activeComponents(SSTableReader sstable) { - return delegate.getComponents(); + // Same rermarks as for `componentsForNewBuid`. + return Collections.emptySet(); } } diff --git a/src/java/org/apache/cassandra/index/SingletonIndexQueryPlan.java b/src/java/org/apache/cassandra/index/SingletonIndexQueryPlan.java index b475cee145a3..f4d206b0e358 100644 --- a/src/java/org/apache/cassandra/index/SingletonIndexQueryPlan.java +++ b/src/java/org/apache/cassandra/index/SingletonIndexQueryPlan.java @@ -24,7 +24,6 @@ import java.util.Collections; import java.util.Set; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.filter.RowFilter; @@ -42,18 +41,6 @@ protected SingletonIndexQueryPlan(Index index, RowFilter postIndexFilter) this.postIndexFilter = postIndexFilter; } - @Nullable - protected static SingletonIndexQueryPlan create(Index index, RowFilter rowFilter) - { - for (RowFilter.Expression e : rowFilter.getExpressions()) - { - if (index.supportsExpression(e.column(), e.operator())) - return new SingletonIndexQueryPlan(index, index.getPostIndexQueryFilter(rowFilter)); - } - - return null; - } - @Override public Set getIndexes() { diff --git a/src/java/org/apache/cassandra/index/internal/CassandraIndex.java b/src/java/org/apache/cassandra/index/internal/CassandraIndex.java index 20c1a0532864..90e3dd8b18ee 100644 --- a/src/java/org/apache/cassandra/index/internal/CassandraIndex.java +++ b/src/java/org/apache/cassandra/index/internal/CassandraIndex.java @@ -33,6 +33,8 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.compaction.TableOperation; +import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.schema.ColumnMetadata; @@ -45,7 +47,6 @@ import org.apache.cassandra.db.lifecycle.View; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.CollectionType; -import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.dht.LocalPartitioner; @@ -58,6 +59,10 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Refs; @@ -80,10 +85,15 @@ public abstract class CassandraIndex implements Index protected ColumnMetadata indexedColumn; protected CassandraIndexFunctions functions; + private final RequestTracker requestTracker; + private final Context sensorContext; + protected CassandraIndex(ColumnFamilyStore baseCfs, IndexMetadata indexDef) { this.baseCfs = baseCfs; setMetadata(indexDef); + this.requestTracker = RequestTracker.instance; + this.sensorContext = Context.from(baseCfs.metadata()); } /** @@ -107,9 +117,9 @@ protected boolean supportsOperator(ColumnMetadata indexedColumn, Operator operat * @param path from the base data being indexed * @return a clustering prefix to be used to insert into the index table */ - protected abstract CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path); + protected abstract ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path); /** * Used at search time to convert a row in the index table into a simple struct containing the values required @@ -217,7 +227,7 @@ public boolean isQueryable(Status status) @Override public void validate(ReadCommand command) throws InvalidRequestException { - Optional target = getTargetExpression(command.rowFilter().getExpressions()); + Optional target = getTargetExpression(command.rowFilter()); if (target.isPresent()) { @@ -265,11 +275,6 @@ public boolean supportsExpression(ColumnMetadata column, Operator operator) && supportsOperator(indexedColumn, operator); } - private boolean supportsExpression(RowFilter.Expression expression) - { - return supportsExpression(expression.column(), expression.operator()); - } - public AbstractType customExpressionValueType() { return null; @@ -277,23 +282,29 @@ public AbstractType customExpressionValueType() public long getEstimatedResultRows() { - return indexCfs.getMeanRowCount(); + return indexCfs.getMeanRowsPerPartition(); } public RowFilter getPostIndexQueryFilter(RowFilter filter) { - return getTargetExpression(filter.getExpressions()).map(filter::without) - .orElse(filter); + // This index doesn't support disjunctions, so if the query has any, we simply apply the entire filter. + return filter.containsDisjunctions() ? filter : getTargetExpression(filter).map(filter::without).orElse(filter); } - private Optional getTargetExpression(List expressions) + private Optional getTargetExpression(RowFilter rowFilter) { - return expressions.stream().filter(this::supportsExpression).findFirst(); + // This index doesn't support disjunctions, so we only consider the top-level AND expressions. + for (RowFilter.Expression expression : rowFilter.withoutDisjunctions().expressions()) + { + if (supportsExpression(expression)) + return Optional.of(expression); + } + return Optional.empty(); } public Index.Searcher searcherFor(ReadCommand command) { - Optional target = getTargetExpression(command.rowFilter().getExpressions()); + Optional target = getTargetExpression(command.rowFilter()); if (target.isPresent()) { @@ -329,7 +340,7 @@ public void validate(PartitionUpdate update, ClientState state) throws InvalidRe break; case REGULAR: if (update.columns().regulars.contains(indexedColumn)) - validateRows(update); + validateRows(update.rows()); break; case STATIC: if (update.columns().statics.contains(indexedColumn)) @@ -343,7 +354,7 @@ public Indexer indexerFor(final DecoratedKey key, final long nowInSec, final WriteContext ctx, final IndexTransaction.Type transactionType, - Memtable memtable) + final Memtable memtable) { /* * Indexes on regular and static columns (the non primary-key ones) only care about updates with live @@ -448,6 +459,14 @@ private void indexCell(Clustering clustering, Cell cell) cell, LivenessInfo.withExpirationTime(cell.timestamp(), cell.ttl(), cell.localDeletionTime()), ctx); + + RequestSensors sensors = requestTracker.get(); + if (sensors != null) + { + sensors.registerSensor(sensorContext, Type.INDEX_WRITE_BYTES); + // estimate the size of the index entry as the data size of the cell before indexing + sensors.incrementSensor(sensorContext, Type.INDEX_WRITE_BYTES, cell.dataSize()); + } } private void removeCells(Clustering clustering, Iterable> cells) @@ -586,7 +605,7 @@ private void validatePartitionKey(DecoratedKey partitionKey) throws InvalidReque private void validateClusterings(PartitionUpdate update) throws InvalidRequestException { assert indexedColumn.isClusteringColumn(); - for (Row row : update) + for (Row row : update.rows()) validateIndexedValue(getIndexedValue(null, row.clustering(), null)); } @@ -659,7 +678,7 @@ private void invalidate() { // interrupt in-progress compactions Collection cfss = Collections.singleton(indexCfs); - CompactionManager.instance.interruptCompactionForCFs(cfss, (sstable) -> true, true); + CompactionManager.instance.interruptCompactionForCFs(cfss, (sstable) -> true, true, TableOperation.StopTrigger.INVALIDATE_INDEX); CompactionManager.instance.waitForCessation(cfss, (sstable) -> true); Keyspace.writeOrder.awaitNewBarrier(); indexCfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.INDEX_REMOVED); @@ -744,7 +763,7 @@ public static TableMetadata indexCfsMetadata(TableMetadata baseCfsMetadata, Inde TableMetadata.builder(baseCfsMetadata.keyspace, baseCfsMetadata.indexTableName(indexMetadata), baseCfsMetadata.id) .kind(TableMetadata.Kind.INDEX) .partitioner(new LocalPartitioner(indexedValueType)) - .addPartitionKeyColumn(indexedColumn.name, isCompatible ? indexedColumn.type : utils.getIndexedPartitionKeyType(indexedColumn)) + .addPartitionKeyColumn(indexedColumn.name, isCompatible ? indexedValueType : utils.getIndexedValueType(indexedColumn)) .addClusteringColumn("partition_key", isCompatible ? baseCfsMetadata.partitioner.partitionOrdering() : indexedTablePartitionKeyType); // Adding clustering columns, which depends on the index type. diff --git a/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java b/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java index 61d446674ec7..94f1520dd398 100644 --- a/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java +++ b/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java @@ -23,9 +23,6 @@ import java.nio.ByteBuffer; import java.util.SortedSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.apache.cassandra.db.BufferClusteringBound; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.ClusteringBound; @@ -55,7 +52,6 @@ public abstract class CassandraIndexSearcher implements Index.Searcher { - private static final Logger logger = LoggerFactory.getLogger(CassandraIndexSearcher.class); private final RowFilter.Expression expression; protected final CassandraIndex index; diff --git a/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java b/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java index 07bdc420ca07..31a1f55137b6 100644 --- a/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java +++ b/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java @@ -20,15 +20,15 @@ import java.util.Collection; import java.util.Set; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.RegularAndStaticColumns; -import org.apache.cassandra.db.compaction.CompactionInfo; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.SecondaryIndexBuilder; import org.apache.cassandra.io.sstable.ReducingKeyIterator; +import org.apache.cassandra.io.sstable.SSTableWatcher; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.utils.TimeUUID; @@ -55,27 +55,29 @@ public CollatedViewIndexBuilder(ColumnFamilyStore cfs, Set indexers, Redu this.sstables = sstables; } - public CompactionInfo getCompactionInfo() + public OperationProgress getProgress() { - return new CompactionInfo(cfs.metadata(), - OperationType.INDEX_BUILD, - iter.getBytesRead(), - iter.getTotalBytes(), - compactionId, - sstables); + return new OperationProgress(cfs.metadata(), + OperationType.INDEX_BUILD, + iter.getBytesRead(), + iter.getTotalBytes(), + compactionId, + sstables); } public void build() { try { - int pageSize = cfs.indexManager.calculateIndexingPageSize(); + for (SSTableReader sstable : sstables) + SSTableWatcher.instance.onIndexBuild(sstable, indexers); + + PageSize pageSize = cfs.indexManager.calculateIndexingPageSize(); RegularAndStaticColumns targetPartitionColumns = extractIndexedColumns(); - + while (iter.hasNext()) { - if (isStopRequested()) - throw new CompactionInterruptedException(getCompactionInfo()); + throwIfStopRequested(); DecoratedKey key = iter.next(); cfs.indexManager.indexPartition(key, indexers, pageSize, targetPartitionColumns); } @@ -89,11 +91,11 @@ public void build() private RegularAndStaticColumns extractIndexedColumns() { RegularAndStaticColumns.Builder builder = RegularAndStaticColumns.builder(); - + for (Index index : indexers) { boolean isPartitionIndex = true; - + for (ColumnMetadata column : cfs.metadata().regularAndStaticColumns()) { if (index.dependsOn(column)) @@ -108,7 +110,7 @@ private RegularAndStaticColumns extractIndexedColumns() if (isPartitionIndex) return cfs.metadata().regularAndStaticColumns(); } - + return builder.build(); } } diff --git a/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java b/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java index 23cff3f8468c..fc15de8e45cd 100644 --- a/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java +++ b/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java @@ -63,11 +63,11 @@ public ByteBuffer getIndexedValue(ByteBuffer partitionKey, return clustering.bufferAt(indexedColumn.position()); } - public CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path) + public ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path) { - CBuilder builder = CBuilder.create(getIndexComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(getIndexComparator()); builder.add(partitionKey); for (int i = 0; i < Math.min(indexedColumn.position(), prefix.size()); i++) builder.add(prefix.get(i), prefix.accessor()); @@ -82,7 +82,7 @@ public IndexEntry decodeEntry(DecoratedKey indexedValue, int ckCount = baseCfs.metadata().clusteringColumns().size(); Clustering clustering = indexEntry.clustering(); - CBuilder builder = CBuilder.create(baseCfs.getComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(baseCfs.getComparator()); for (int i = 0; i < indexedColumn.position(); i++) builder.add(clustering, i + 1); diff --git a/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java b/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java index f0201e1effec..43196463e7b4 100644 --- a/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java +++ b/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java @@ -48,11 +48,11 @@ public CollectionKeyIndexBase(ColumnFamilyStore baseCfs, IndexMetadata indexDef) super(baseCfs, indexDef); } - public CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path) + public ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path) { - CBuilder builder = CBuilder.create(getIndexComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(getIndexComparator()); builder.add(partitionKey); // When indexing a static column, prefix will be empty but only the @@ -74,7 +74,7 @@ public IndexEntry decodeEntry(DecoratedKey indexedValue, else { int count = 1 + baseCfs.metadata().clusteringColumns().size(); - CBuilder builder = CBuilder.create(baseCfs.getComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(baseCfs.getComparator()); for (int i = 0; i < count - 1; i++) builder.add(clustering, i + 1); indexedEntryClustering = builder.build(); diff --git a/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java b/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java index ed929f22b3c5..6707f664a592 100644 --- a/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java +++ b/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java @@ -54,11 +54,11 @@ public ByteBuffer getIndexedValue(ByteBuffer partitionKey, return cellValue; } - public CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path) + public ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path) { - CBuilder builder = CBuilder.create(getIndexComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(getIndexComparator()); builder.add(partitionKey); for (int i = 0; i < prefix.size(); i++) builder.add(prefix.get(i), prefix.accessor()); @@ -81,7 +81,7 @@ public IndexEntry decodeEntry(DecoratedKey indexedValue, Row indexEntry) indexedEntryClustering = Clustering.STATIC_CLUSTERING; else { - CBuilder builder = CBuilder.create(baseCfs.getComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(baseCfs.getComparator()); for (int i = 0; i < baseCfs.getComparator().size(); i++) builder.add(clustering, i + 1); indexedEntryClustering = builder.build(); diff --git a/src/java/org/apache/cassandra/index/internal/composites/PartitionKeyIndex.java b/src/java/org/apache/cassandra/index/internal/composites/PartitionKeyIndex.java index b8235370b70c..adf8e483cf3b 100644 --- a/src/java/org/apache/cassandra/index/internal/composites/PartitionKeyIndex.java +++ b/src/java/org/apache/cassandra/index/internal/composites/PartitionKeyIndex.java @@ -64,11 +64,11 @@ public ByteBuffer getIndexedValue(ByteBuffer partitionKey, return components[indexedColumn.position()]; } - public CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path) + public ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path) { - CBuilder builder = CBuilder.create(getIndexComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(getIndexComparator()); builder.add(partitionKey); for (int i = 0; i < prefix.size(); i++) builder.add(prefix.get(i), prefix.accessor()); @@ -79,7 +79,7 @@ public IndexEntry decodeEntry(DecoratedKey indexedValue, Row indexEntry) { int ckCount = baseCfs.metadata().clusteringColumns().size(); Clustering clustering = indexEntry.clustering(); - CBuilder builder = CBuilder.create(baseCfs.getComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(baseCfs.getComparator()); for (int i = 0; i < ckCount; i++) builder.add(clustering, i + 1); diff --git a/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java b/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java index 9dcb8dfcc4d9..9fd8596454db 100644 --- a/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java +++ b/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java @@ -61,11 +61,11 @@ public ByteBuffer getIndexedValue(ByteBuffer partitionKey, return cellValue; } - public CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path) + public ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path) { - CBuilder builder = CBuilder.create(getIndexComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(getIndexComparator()); builder.add(partitionKey); for (int i = 0; i < prefix.size(); i++) builder.add(prefix.get(i), prefix.accessor()); @@ -87,7 +87,7 @@ public IndexEntry decodeEntry(DecoratedKey indexedValue, Row indexEntry) else { ClusteringComparator baseComparator = baseCfs.getComparator(); - CBuilder builder = CBuilder.create(baseComparator); + ClusteringBuilder builder = ClusteringBuilder.create(baseComparator); for (int i = 0; i < baseComparator.size(); i++) builder.add(clustering, i + 1); indexedEntryClustering = builder.build(); diff --git a/src/java/org/apache/cassandra/index/internal/keys/KeysIndex.java b/src/java/org/apache/cassandra/index/internal/keys/KeysIndex.java index 695fb67ef94f..bd89070e8789 100644 --- a/src/java/org/apache/cassandra/index/internal/keys/KeysIndex.java +++ b/src/java/org/apache/cassandra/index/internal/keys/KeysIndex.java @@ -49,11 +49,11 @@ public TableMetadata.Builder addIndexClusteringColumns(TableMetadata.Builder bui return builder; } - protected CBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, - ClusteringPrefix prefix, - CellPath path) + protected ClusteringBuilder buildIndexClusteringPrefix(ByteBuffer partitionKey, + ClusteringPrefix prefix, + CellPath path) { - CBuilder builder = CBuilder.create(getIndexComparator()); + ClusteringBuilder builder = ClusteringBuilder.create(getIndexComparator()); builder.add(partitionKey, ByteBufferAccessor.instance); return builder; } diff --git a/src/java/org/apache/cassandra/index/sai/IndexContext.java b/src/java/org/apache/cassandra/index/sai/IndexContext.java new file mode 100644 index 000000000000..e93fdbb2872a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/IndexContext.java @@ -0,0 +1,1032 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.db.marshal.VectorType; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig; +import org.apache.cassandra.index.sai.disk.vector.VectorValidation; +import org.apache.cassandra.index.sai.iterators.KeyRangeAntiJoinIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator; +import org.apache.cassandra.index.sai.memory.MemtableIndex; +import org.apache.cassandra.index.sai.memory.MemtableKeyRangeIterator; +import org.apache.cassandra.index.sai.metrics.AbstractMetrics; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.index.sai.metrics.IndexMetrics; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.index.sai.view.IndexViewManager; +import org.apache.cassandra.index.sai.view.View; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.MonotonicClock; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.concurrent.OpOrder; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_ANALYZED_SIZE; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_FROZEN_TERM_SIZE; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_STRING_TERM_SIZE; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_VECTOR_TERM_SIZE; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_VALIDATE_MAX_TERM_SIZE_AT_COORDINATOR; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_INDEX_READS_DISABLED; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_INDEX_METRICS_ENABLED; +import static org.apache.cassandra.index.sai.plan.QueryController.INDEX_VERSION_DOES_NOT_SUPPORT_BM25; + +/** + * Manage metadata for each column index. + */ +public class IndexContext +{ + private static final Logger logger = LoggerFactory.getLogger(IndexContext.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + + public static final int MAX_STRING_TERM_SIZE = SAI_MAX_STRING_TERM_SIZE.getInt() * 1024; + public static final int MAX_FROZEN_TERM_SIZE = SAI_MAX_FROZEN_TERM_SIZE.getInt() * 1024; + public static final int MAX_VECTOR_TERM_SIZE = SAI_MAX_VECTOR_TERM_SIZE.getInt() * 1024; + public static final int MAX_ANALYZED_SIZE = SAI_MAX_ANALYZED_SIZE.getInt() * 1024; + private static final String TERM_OVERSIZE_LOG_MESSAGE = + "Can't add term of column {} to index for key: {}, term size {} max allowed size {}."; + private static final String TERM_OVERSIZE_ERROR_MESSAGE = + "Term of column %s exceeds the byte limit for index. Term size %s. Max allowed size %s."; + + private static final String ANALYZED_TERM_OVERSIZE_LOG_MESSAGE = + "Term's analyzed size for column {} exceeds the cumulative limit for index. Max allowed size {}."; + private static final String ANALYZED_TERM_OVERSIZE_ERROR_MESSAGE = + "Term's analyzed size for column %s exceeds the cumulative limit for index. Max allowed size %s."; + + private static final Set> EQ_ONLY_TYPES = + ImmutableSet.of(UTF8Type.instance, AsciiType.instance, BooleanType.instance, UUIDType.instance); + + public static final String ENABLE_SEGMENT_COMPACTION_OPTION_NAME = "enable_segment_compaction"; + + private final AbstractType partitionKeyType; + private final ClusteringComparator clusteringComparator; + + private final String keyspace; + private final String table; + private final TableId tableId; + private final ColumnMetadata column; + private final IndexTarget.Type indexType; + private final AbstractType validator; + private final ColumnFamilyStore cfs; + + // Config can be null if the column context is "fake" (i.e. created for a filtering expression). + private final IndexMetadata config; + private final VectorSimilarityFunction vectorSimilarityFunction; + + private final ConcurrentMap liveMemtables = new ConcurrentHashMap<>(); + + private final IndexViewManager viewManager; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private final Optional indexMetrics; + private final ColumnQueryMetrics columnQueryMetrics; + private final IndexWriterConfig indexWriterConfig; + private final boolean isAnalyzed; + private final boolean hasEuclideanSimilarityFunc; + private final AbstractAnalyzer.AnalyzerFactory analyzerFactory; + private final AbstractAnalyzer.AnalyzerFactory queryAnalyzerFactory; + private final PrimaryKey.Factory primaryKeyFactory; + + private final Version version; + private final int maxTermSize; + + private volatile boolean dropped = false; + + public IndexContext(@Nonnull String keyspace, + @Nonnull String table, + @Nonnull TableId tableId, + @Nonnull AbstractType partitionKeyType, + @Nonnull ClusteringComparator clusteringComparator, + @Nonnull ColumnMetadata column, + @Nonnull IndexTarget.Type indexType, + IndexMetadata config, + @Nonnull ColumnFamilyStore cfs) + { + this.keyspace = keyspace; + this.table = table; + this.tableId = tableId; + this.partitionKeyType = partitionKeyType; + this.clusteringComparator = clusteringComparator; + this.column = column; + this.indexType = indexType; + this.config = config; + this.viewManager = new IndexViewManager(this); + this.validator = TypeUtil.cellValueType(column, indexType); + this.cfs = cfs; + this.version = Version.current(keyspace); + this.primaryKeyFactory = version.onDiskFormat().newPrimaryKeyFactory(clusteringComparator); + + String columnName = column.name.toString(); + + if (config != null) + { + String fullIndexName = String.format("%s.%s.%s", this.keyspace, this.table, this.config.name); + this.indexWriterConfig = IndexWriterConfig.fromOptions(fullIndexName, validator, config.options); + this.isAnalyzed = AbstractAnalyzer.isAnalyzed(config.options); + this.analyzerFactory = AbstractAnalyzer.fromOptions(columnName, validator, config.options); + this.queryAnalyzerFactory = AbstractAnalyzer.hasQueryAnalyzer(config.options) + ? AbstractAnalyzer.fromOptionsQueryAnalyzer(validator, config.options) + : this.analyzerFactory; + this.vectorSimilarityFunction = indexWriterConfig.getSimilarityFunction(); + this.hasEuclideanSimilarityFunc = vectorSimilarityFunction == VectorSimilarityFunction.EUCLIDEAN; + + this.indexMetrics = SAI_INDEX_METRICS_ENABLED.getBoolean() ? Optional.of(new IndexMetrics(this)) : Optional.empty(); + this.columnQueryMetrics = isVector() ? new ColumnQueryMetrics.VectorIndexMetrics(keyspace, table, getIndexName()) : + isLiteral() ? new ColumnQueryMetrics.TrieIndexMetrics(keyspace, table, getIndexName()) + : new ColumnQueryMetrics.BKDIndexMetrics(keyspace, table, getIndexName()); + + } + else + { + this.indexWriterConfig = IndexWriterConfig.emptyConfig(); + this.isAnalyzed = AbstractAnalyzer.isAnalyzed(Collections.emptyMap()); + this.analyzerFactory = AbstractAnalyzer.fromOptions(columnName, validator, Collections.EMPTY_MAP); + this.queryAnalyzerFactory = this.analyzerFactory; + this.vectorSimilarityFunction = null; + this.hasEuclideanSimilarityFunc = false; + + // null config indicates a "fake" index context. As such, it won't actually be used for indexing/accessing + // data, leaving these metrics unused. This also eliminates the overhead of creating these metrics on the + // query path. + this.indexMetrics = Optional.empty(); + this.columnQueryMetrics = null; + } + + this.maxTermSize = isVector() ? MAX_VECTOR_TERM_SIZE + : isAnalyzed ? MAX_ANALYZED_SIZE + : isFrozen() ? MAX_FROZEN_TERM_SIZE : MAX_STRING_TERM_SIZE; + + + logger.debug(logMessage("Initialized index context with index writer config: {}"), indexWriterConfig); + } + + public Version version() + { + return version; + } + + public AbstractType keyValidator() + { + return partitionKeyType; + } + + public PrimaryKey.Factory keyFactory() + { + return primaryKeyFactory; + } + + public ClusteringComparator comparator() + { + return clusteringComparator; + } + + public Optional getIndexMetrics() + { + return indexMetrics; + } + + public ColumnQueryMetrics getColumnQueryMetrics() + { + return columnQueryMetrics; + } + + public String getKeyspace() + { + return keyspace; + } + public String getTable() + { + return table; + } + + public TableId getTableId() + { + return tableId; + } + + public ColumnFamilyStore columnFamilyStore() + { + return cfs; + } + + public IPartitioner getPartitioner() + { + return cfs.getPartitioner(); + } + + public MemtableIndex initializeMemtableIndex(Memtable memtable) + { + return liveMemtables.computeIfAbsent(memtable, mt -> MemtableIndex.createIndex(this, mt)); + } + + public void index(DecoratedKey key, Row row, Memtable mt, OpOrder.Group opGroup) + { + MemtableIndex target = initializeMemtableIndex(mt); + + long start = nanoTime(); + + if (isNonFrozenCollection()) + { + Iterator bufferIterator = getValuesOf(row, FBUtilities.nowInSeconds()); + if (bufferIterator != null) + { + while (bufferIterator.hasNext()) + { + ByteBuffer value = bufferIterator.next(); + target.index(key, row.clustering(), value, mt, opGroup); + } + } + } + else + { + ByteBuffer value = getValueOf(key, row, FBUtilities.nowInSeconds()); + target.index(key, row.clustering(), value, mt, opGroup); + } + indexMetrics.flatMap(metrics -> metrics.memtableIndexWriteLatency).ifPresent(timer -> + timer.update(nanoTime() - start, TimeUnit.NANOSECONDS)); + } + + /** + * Validate maximum term size for given row. Throw an exception when invalid. + */ + public void validateMaxTermSizeForRow(DecoratedKey key, Row row) + { + AbstractAnalyzer analyzer = getAnalyzerFactory().create(); + if (isNonFrozenCollection()) + { + Iterator bufferIterator = getValuesOf(row, FBUtilities.nowInSeconds()); + while (bufferIterator != null && bufferIterator.hasNext()) + validateMaxTermSizeForCell(analyzer, key, bufferIterator.next()); + } + else + { + ByteBuffer value = getValueOf(key, row, FBUtilities.nowInSeconds()); + validateMaxTermSizeForCell(analyzer, key, value); + } + } + + private void validateMaxTermSizeForCell(AbstractAnalyzer analyzer, DecoratedKey key, @Nullable ByteBuffer cellBuffer) + { + if (cellBuffer == null || cellBuffer.remaining() == 0) + return; + + analyzer.reset(cellBuffer); + try + { + if (analyzer.transformValue()) + { + if (!validateCumulativeAnalyzedTermLimit(key, analyzer)) + { + String error = String.format(ANALYZED_TERM_OVERSIZE_ERROR_MESSAGE, + column.name, FBUtilities.prettyPrintMemory(maxTermSize)); + throw new InvalidRequestException(error); + } + } + else + { + while (analyzer.hasNext()) + { + int size = analyzer.next().remaining(); + if (!validateMaxTermSize(key, size)) + { + String error = String.format(TERM_OVERSIZE_ERROR_MESSAGE, + column.name, + FBUtilities.prettyPrintMemory(size), + FBUtilities.prettyPrintMemory(maxTermSize)); + throw new InvalidRequestException(error); + } + } + } + } + finally + { + analyzer.end(); + } + } + + + /** + * Validate maximum term size for given term + * @return true if given term is valid; otherwise false. + */ + public boolean validateMaxTermSize(DecoratedKey key, ByteBuffer term) + { + return validateMaxTermSize(key, term.remaining()); + } + + private boolean validateMaxTermSize(DecoratedKey key, int termSize) + { + if (termSize > maxTermSize) + { + noSpamLogger.warn(logMessage(TERM_OVERSIZE_LOG_MESSAGE), + getColumnName(), + keyValidator().getString(key.getKey()), + FBUtilities.prettyPrintMemory(termSize), + FBUtilities.prettyPrintMemory(maxTermSize)); + return false; + } + + return true; + } + + private boolean validateCumulativeAnalyzedTermLimit(DecoratedKey key, AbstractAnalyzer analyzer) + { + int bytesCount = 0; + // VSTODO anayzer.hasNext copies the byteBuffer, but we don't need that here. + while (analyzer.hasNext()) + { + final ByteBuffer token = analyzer.next(); + bytesCount += token.remaining(); + if (bytesCount > maxTermSize) + { + noSpamLogger.warn(logMessage(ANALYZED_TERM_OVERSIZE_LOG_MESSAGE), + getColumnName(), + keyValidator().getString(key.getKey()), + FBUtilities.prettyPrintMemory(maxTermSize)); + return false; + } + } + return true; + } + + public void update(DecoratedKey key, Row oldRow, Row newRow, Memtable memtable, OpOrder.Group opGroup) + { + if (version.equals(Version.AA)) + { + // AA cannot handle updates because it indexes partition keys instead of fully qualified primary keys. + index(key, newRow, memtable, opGroup); + return; + } + + MemtableIndex target = liveMemtables.get(memtable); + if (target == null) + return; + + // Use 0 for nowInSecs to get the value(s) from the oldRow regardless of its liveness status. To get to this point, + // C* has already determined this is the current represntation of the oldRow in the memtable, and that means + // we need to add the newValue to the index and remove the oldValue from it, even if it has already expired via + // TTL. + if (isNonFrozenCollection()) + { + Iterator oldValues = getValuesOf(oldRow, 0); + Iterator newValues = getValuesOf(newRow, FBUtilities.nowInSeconds()); + target.update(key, oldRow.clustering(), oldValues, newValues, memtable, opGroup); + } + else + { + ByteBuffer oldValue = getValueOf(key, oldRow, 0); + ByteBuffer newValue = getValueOf(key, newRow, FBUtilities.nowInSeconds()); + target.update(key, oldRow.clustering(), oldValue, newValue, memtable, opGroup); + } + } + + public void renewMemtable(Memtable renewed) + { + // remove every index but the one that corresponds to the post-truncate Memtable + liveMemtables.keySet().removeIf(m -> m != renewed); + } + + public void discardMemtable(Memtable discarded) + { + liveMemtables.remove(discarded); + } + + public MemtableIndex getPendingMemtableIndex(LifecycleNewTracker tracker) + { + return liveMemtables.keySet().stream() + .filter(m -> tracker.equals(m.getFlushTransaction())) + .findFirst() + .map(liveMemtables::get) + .orElse(null); + } + + // Returns an iterator for NEQ, NOT_CONTAINS_KEY, NOT_CONTAINS_VALUE, which + // 1. Either includes everything if the column type values can be truncated and + // thus the keys cannot be matched precisely, + // 2. or includes everything minus the keys matching the expression + // if the column type values cannot be truncated, i.e., matching the keys is always precise. + // (not matching precisely will lead to false negatives) + // + // keys k such that row(k) not contains v = (all keys) \ (keys k such that row(k) contains v) + // + // Note that rows in other indexes are not matched, so this can return false positives, + // but they are not a problem as post-filtering would get rid of them. + // The keys matched in other indexes cannot be safely subtracted + // as indexes may contain false positives caused by deletes and updates. + private KeyRangeIterator getNonEqIterator(QueryContext context, Collection memtables, Expression expression, AbstractBounds keyRange) + { + KeyRangeIterator allKeys = scanMemtable(keyRange, memtables); + if (TypeUtil.supportsRounding(expression.validator)) + { + return allKeys; + } + else + { + Expression negExpression = expression.negated(); + KeyRangeIterator matchedKeys = searchMemtable(context, memtables, negExpression, keyRange); + return KeyRangeAntiJoinIterator.create(allKeys, matchedKeys); + } + } + + public KeyRangeIterator searchMemtable(QueryContext context, Collection memtables, Expression expression, AbstractBounds keyRange) + { + if (expression.getOp().isNonEquality()) + { + return getNonEqIterator(context, memtables, expression, keyRange); + } + + if (memtables.isEmpty()) + { + return KeyRangeIterator.empty(); + } + + KeyRangeUnionIterator.Builder builder = KeyRangeUnionIterator.builder(); + + try + { + for (MemtableIndex index : memtables) + builder.add(index.search(context, expression, keyRange)); + + return builder.build(); + } + catch (Exception ex) + { + FileUtils.closeQuietly(builder.ranges()); + throw ex; + } + } + + private KeyRangeIterator scanMemtable(AbstractBounds keyRange, Collection memtables) + { + if (memtables.isEmpty()) + { + return KeyRangeIterator.empty(); + } + + KeyRangeIterator.Builder builder = KeyRangeUnionIterator.builder(memtables.size()); + + try + { + for (MemtableIndex memtableIndex : memtables) + { + Memtable memtable = memtableIndex.getMemtable(); + KeyRangeIterator memtableIterator = new MemtableKeyRangeIterator(memtable, primaryKeyFactory, keyRange); + builder.add(memtableIterator); + } + + return builder.build(); + } + catch (Exception ex) + { + FileUtils.closeQuietly(builder.ranges()); + throw ex; + } + } + + + public long liveMemtableWriteCount() + { + return liveMemtables.values().stream().mapToLong(MemtableIndex::writeCount).sum(); + } + + public long estimatedOnHeapMemIndexMemoryUsed() + { + return liveMemtables.values().stream().mapToLong(MemtableIndex::estimatedOnHeapMemoryUsed).sum(); + } + + public long estimatedOffHeapMemIndexMemoryUsed() + { + return liveMemtables.values().stream().mapToLong(MemtableIndex::estimatedOffHeapMemoryUsed).sum(); + } + + /** + * @return A set of SSTables which have attached to them invalid index components. + */ + public Set onSSTableChanged(Collection oldSSTables, + Collection newContexts, + boolean validate) + { + return viewManager.update(oldSSTables, newContexts, validate); + } + + public ColumnMetadata getDefinition() + { + return column; + } + + public AbstractType getValidator() + { + return validator; + } + + public boolean isNonFrozenCollection() + { + return TypeUtil.isNonFrozenCollection(column.type); + } + + public boolean isCollection() + { + return column.type.isCollection(); + } + + public boolean isFrozen() + { + return TypeUtil.isFrozen(column.type); + } + + public String getColumnName() + { + return column.name.toString(); + } + + public String getIndexName() + { + return this.config == null ? null : config.name; + } + + public int getIntOption(String name, int defaultValue) + { + String value = this.config.options.get(name); + if (value == null) + return defaultValue; + + try + { + return Integer.parseInt(value); + } + catch (NumberFormatException e) + { + logger.error("Failed to parse index configuration " + name + " = " + value + " as integer"); + return defaultValue; + } + } + + public AbstractAnalyzer.AnalyzerFactory getAnalyzerFactory() + { + return analyzerFactory; + } + + public AbstractAnalyzer.AnalyzerFactory getQueryAnalyzerFactory() + { + return queryAnalyzerFactory; + } + + public IndexWriterConfig getIndexWriterConfig() + { + return indexWriterConfig; + } + + public View getView() + { + return viewManager.getView(); + } + + public View getReferencedView(long timeoutNanos) + { + long deadline = MonotonicClock.Global.approxTime.now() + timeoutNanos; + do + { + View view = viewManager.getView(); + if (view.reference()) + return view; + } while (!MonotonicClock.Global.approxTime.isAfter(deadline)); + + return null; + } + + /** + * @return total number of per-index open files + */ + public int openPerIndexFiles() + { + return viewManager.getView().size() * version.onDiskFormat().openFilesPerIndex(this); + } + + public void prepareSSTablesForRebuild(Collection sstablesToRebuild) + { + viewManager.prepareSSTablesForRebuild(sstablesToRebuild); + } + + public boolean isIndexed() + { + return config != null && !dropped; + } + + public boolean isDropped() + { + return dropped; + } + + /** + * @return whether the column is analyzed, meaning it uses an analyzer that isn't no-op. + */ + public boolean isAnalyzed() + { + return isAnalyzed; + } + + /** + * Called when index is dropped. Mark all {@link SSTableIndex} as released and per-column index files + * will be removed when in-flight queries completed and {@code obsolete} is true. + * + * @param obsolete true if index files should be deleted after invalidate; false otherwise. + */ + public void invalidate(boolean obsolete) + { + dropped = true; + liveMemtables.clear(); + viewManager.invalidate(obsolete); + indexMetrics.ifPresent(AbstractMetrics::release); + if (columnQueryMetrics != null) + columnQueryMetrics.release(); + + analyzerFactory.close(); + if (queryAnalyzerFactory != analyzerFactory) + { + queryAnalyzerFactory.close(); + } + } + + public ConcurrentMap getLiveMemtables() + { + return liveMemtables; + } + + public SSTableContext getSSTableContext(SSTableReader sstable) + { + // This method is only used in tests, returning null for now + return null; + } + + public boolean supports(Operator op) + { + if (op.isLike() || op == Operator.LIKE) return false; + // Analyzed columns store the indexed result, so we are unable to compute raw equality. + // The only supported operators are ANALYZER_MATCHES and BM25. + if (op == Operator.ANALYZER_MATCHES) return isAnalyzed; + // BM25 frequency calculations only work on non-collection columns because it assumes a 1:1 mapping from PrK + // to frequency, but collections have mulitple documents. + if (op == Operator.BM25) return isAnalyzed && !isCollection(); + + // If the column is analyzed and the operator is EQ, we need to check if the analyzer supports it. + if (op == Operator.EQ && isAnalyzed && !analyzerFactory.supportsEquals()) + return false; + + // ANN is only supported against vectors. + // BOUNDED_ANN is only supported against vectors with a Euclidean similarity function. + // Vector indexes only support ANN and BOUNDED_ANN + if (column.type instanceof VectorType) + return op == Operator.ANN || (op == Operator.BOUNDED_ANN && hasEuclideanSimilarityFunc); + if (op == Operator.ANN || op == Operator.BOUNDED_ANN) + return false; + + // Only regular columns can be sorted by SAI (at least for now) + if (op == Operator.ORDER_BY_ASC || op == Operator.ORDER_BY_DESC) + return !isCollection() + && column.isRegular() + && !isAnalyzed + && !(column.type instanceof InetAddressType // Possible, but need to add decoding logic based on + // SAI's TypeUtil.encode method. + || column.type instanceof DecimalType // Currently truncates to 24 bytes + || column.type instanceof IntegerType); // Currently truncates to 20 bytes + + Expression.Op operator = Expression.Op.valueOf(op); + if (isNonFrozenCollection()) + { + if (indexType == IndexTarget.Type.KEYS) + return operator == Expression.Op.CONTAINS_KEY + || operator == Expression.Op.NOT_CONTAINS_KEY; + if (indexType == IndexTarget.Type.VALUES) + return operator == Expression.Op.CONTAINS_VALUE + || operator == Expression.Op.NOT_CONTAINS_VALUE; + return indexType == IndexTarget.Type.KEYS_AND_VALUES && + (operator == Expression.Op.EQ || operator == Expression.Op.NOT_EQ || operator == Expression.Op.RANGE); + } + if (indexType == IndexTarget.Type.FULL) + return operator == Expression.Op.EQ; + AbstractType validator = getValidator(); + if (operator == Expression.Op.IN) + return true; + if (operator != Expression.Op.EQ && EQ_ONLY_TYPES.contains(validator)) return false; + // RANGE only applicable to non-literal indexes + return (operator != null) && !(TypeUtil.isLiteral(validator) && operator == Expression.Op.RANGE); + } + + public ByteBuffer getValueOf(DecoratedKey key, Row row, long nowInSecs) + { + if (row == null) + return null; + + switch (column.kind) + { + case PARTITION_KEY: + if (key == null) + return null; + return partitionKeyType instanceof CompositeType + ? CompositeType.extractComponent(key.getKey(), column.position()) + : key.getKey(); + case CLUSTERING: + // skip indexing of static clustering when regular column is indexed + return row.isStatic() ? null : row.clustering().bufferAt(column.position()); + + // treat static cell retrieval the same was as regular + // only if row kind is STATIC otherwise return null + case STATIC: + if (!row.isStatic()) + return null; + case REGULAR: + Cell cell = row.getCell(column); + return cell == null || !cell.isLive(nowInSecs) ? null : cell.buffer(); + + default: + return null; + } + } + + public Iterator getValuesOf(Row row, long nowInSecs) + { + if (row == null) + return null; + + switch (column.kind) + { + // treat static cell retrieval the same was as regular + // only if row kind is STATIC otherwise return null + case STATIC: + if (!row.isStatic()) + return null; + case REGULAR: + return TypeUtil.collectionIterator(validator, row.getComplexColumnData(column), column, indexType, nowInSecs); + + default: + return null; + } + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("columnName", getColumnName()) + .add("indexName", getIndexName()) + .toString(); + } + + public boolean isLiteral() + { + return TypeUtil.isLiteral(getValidator()); + } + + public boolean isVector() + { + //VSTODO probably move this down to TypeUtils eventually + return getValidator().isVector(); + } + + public void validate(DecoratedKey key, Row row) + { + // Validate the size of the inserted term. + if (SAI_VALIDATE_MAX_TERM_SIZE_AT_COORDINATOR.getBoolean()) + validateMaxTermSizeForRow(key, row); + + // Verify vector is valid. + if (isVector()) + { + float[] value = TypeUtil.decomposeVector(getValidator(), getValueOf(key, row, FBUtilities.nowInSeconds())); + if (value != null) + VectorValidation.validateIndexable(value, vectorSimilarityFunction); + } + } + + public void validate(RowFilter rowFilter) + { + // Only iterate over the top level expressions because that is where the ANN and BM25 expressions are located. + for (RowFilter.Expression expression : rowFilter.root.expressions()) + { + if (!expression.column().equals(column)) + continue; + + switch (expression.operator()) + { + case ANN: + float[] value = TypeUtil.decomposeVector(getValidator(), expression.getIndexValue()); + VectorValidation.validateIndexable(value, vectorSimilarityFunction); + return; + case BM25: + if (version().onOrAfter(Version.BM25_EARLIEST)) + return; + throw new InvalidRequestException(String.format(INDEX_VERSION_DOES_NOT_SUPPORT_BM25, getIndexName())); + } + } + } + + public boolean equals(Object obj) + { + if (obj == this) + return true; + + if (!(obj instanceof IndexContext)) + return false; + + IndexContext other = (IndexContext) obj; + + return Objects.equals(column, other.column) && + Objects.equals(indexType, other.indexType) && + Objects.equals(config, other.config) && + Objects.equals(partitionKeyType, other.partitionKeyType) && + Objects.equals(clusteringComparator, other.clusteringComparator); + } + + public int hashCode() + { + return Objects.hash(column, indexType, config, partitionKeyType, clusteringComparator); + } + + /** + * A helper method for constructing consistent log messages for specific column indexes. + * + * Example: For the index "idx" in keyspace "ks" on table "tb", calling this method with the raw message + * "Flushing new index segment..." will produce... + * + * "[ks.tb.idx] Flushing new index segment..." + * + * @param message The raw content of a logging message, without information identifying it with an index. + * + * @return A log message with the proper keyspace, table and index name prepended to it. + */ + public String logMessage(String message) + { + // Index names are unique only within a keyspace. + return String.format("[%s.%s.%s] %s", keyspace, table, config == null ? "?" : config.name, message); + } + + /** + * @return the indexes that are built on the given SSTables on the left and corrupted indexes' + * corresponding contexts on the right + */ + public Pair, Set> getBuiltIndexes(Collection sstableContexts, boolean validate) + { + Set valid = ConcurrentHashMap.newKeySet(); + Set invalid = ConcurrentHashMap.newKeySet(); + + sstableContexts.stream().parallel().forEach(context -> { + if (context.sstable.isMarkedCompacted()) + return; + + var perSSTableComponents = context.usedPerSSTableComponents(); + var perIndexComponents = perSSTableComponents.indexDescriptor().perIndexComponents(this); + if (!perSSTableComponents.isComplete() || !perIndexComponents.isComplete()) + { + logger.debug(logMessage("An on-disk index build for SSTable {} has not completed (per-index components={})."), context.descriptor(), perIndexComponents.all()); + return; + } + + try + { + if (validate) + { + if (!perIndexComponents.validateComponents(context.sstable, cfs.getTracker(), false, false)) + { + // Note that a precise warning is already logged by the validation if there is an issue. + invalid.add(context); + return; + } + } + + SSTableIndex index = new SSTableIndex(context, perIndexComponents); + if (SAI_INDEX_READS_DISABLED.getBoolean()) + { + logger.debug(logMessage("Skipped loading index for SSTable {} as it's disabled by {}"), context.descriptor(), SAI_INDEX_READS_DISABLED.getKey()); + } + else + { + long count = context.primaryKeyMapFactory().count(); + logger.debug(logMessage("Successfully loaded index for SSTable {} with {} rows."), context.descriptor(), count); + } + + // Try to add new index to the set, if set already has such index, we'll simply release and move on. + // This covers situation when SSTable collection has the same SSTable multiple + // times because we don't know what kind of collection it actually is. + if (!valid.add(index)) + index.release(); + } + catch (Throwable e) + { + logger.error(logMessage("Failed to update per-column components for SSTable {}"), context.descriptor(), e); + invalid.add(context); + } + }); + + return Pair.create(valid, invalid); + } + + /** + * @return the number of indexed rows in this index (aka. pair of term and rowId) + */ + public long getCellCount() + { + return getView().getIndexes() + .stream() + .mapToLong(SSTableIndex::getRowCount) + .sum(); + } + + /** + * @return the total size (in bytes) of per-column index components + */ + public long diskUsage() + { + return getView().getIndexes() + .stream() + .mapToLong(SSTableIndex::sizeOfPerColumnComponents) + .sum(); + } + + /** + * @return the total memory usage (in bytes) of per-column index on-disk data structure + */ + public long indexFileCacheSize() + { + return getView().getIndexes() + .stream() + .mapToLong(SSTableIndex::indexFileCacheSize) + .sum(); + } + + public IndexFeatureSet indexFeatureSet() + { + IndexFeatureSet.Accumulator accumulator = new IndexFeatureSet.Accumulator(version); + getView().getIndexes().stream().map(SSTableIndex::indexFeatureSet).forEach(set -> accumulator.accumulate(set)); + return accumulator.complete(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/IndexValidation.java b/src/java/org/apache/cassandra/index/sai/IndexValidation.java deleted file mode 100644 index edd9e0fd1c0a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/IndexValidation.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai; - -public enum IndexValidation -{ - /** - * No validation to be performed - */ - NONE, - - /** - * Basic header/footer validation, but no data validation (fast) - */ - HEADER_FOOTER, - - /** - * Full validation with checksumming data (slow) - */ - CHECKSUM - -} diff --git a/src/java/org/apache/cassandra/index/sai/QueryContext.java b/src/java/org/apache/cassandra/index/sai/QueryContext.java index a75886d69a6f..b4a98d61a390 100644 --- a/src/java/org/apache/cassandra/index/sai/QueryContext.java +++ b/src/java/org/apache/cassandra/index/sai/QueryContext.java @@ -18,81 +18,407 @@ package org.apache.cassandra.index.sai; -import java.util.Collection; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; -import org.apache.cassandra.db.ReadCommand; -import org.apache.cassandra.exceptions.QueryCancelledException; -import org.apache.cassandra.index.sai.plan.FilterTree; -import org.apache.cassandra.index.sai.plan.QueryController; -import org.apache.cassandra.utils.Clock; +import com.google.common.annotations.VisibleForTesting; -import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_TEST_DISABLE_TIMEOUT; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.index.sai.plan.Plan; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; +import org.apache.cassandra.utils.MonotonicClock; + +import static java.lang.Math.max; /** * Tracks state relevant to the execution of a single query, including metrics and timeout monitoring. - *

    - * Fields here are non-volatile, as they are accessed from a single thread. */ -@NotThreadSafe +@NotThreadSafe // this should only be manipulated by the single thread running the query it belongs to public class QueryContext { - private static final boolean DISABLE_TIMEOUT = SAI_TEST_DISABLE_TIMEOUT.getBoolean(); + public static final boolean DISABLE_TIMEOUT = CassandraRelevantProperties.TEST_SAI_DISABLE_TIMEOUT.getBoolean(); - private final ReadCommand readCommand; - private final long queryStartTimeNanos; + /** The thread ID that the query is running on, used to verify single-threaded access. */ + private final long owningThreadId = Thread.currentThread().getId(); - public final long executionQuotaNano; + /** The query start time, in nanoseconds. Used to measure the query execution time. */ + private final long queryStartTimeNanos; - public long sstablesHit = 0; - public long segmentsHit = 0; - public long partitionsRead = 0; - public long rowsFiltered = 0; + /** How long the coordinator waits for SAI queries, in nanoseconds */ + private final long executionQuotaNano; - public long trieSegmentsHit = 0; - public long triePostingsSkips = 0; - public long triePostingsDecodes = 0; + /** + * Whether the query has timed out, checked at {@link #checkpoint()}. + */ + private boolean queryTimedOut = false; - public long balancedTreePostingListsHit = 0; - public long balancedTreeSegmentsHit = 0; - public long balancedTreePostingsSkips = 0; - public long balancedTreePostingsDecodes = 0; + /** Number of sstables visited by the query. */ + private long sstablesHit = 0; - public boolean queryTimedOut = false; + /** Number of index segments having results for the query. */ + private long segmentsHit = 0; /** - * {@code true} if the local query for this context has matches from Memtable-attached indexes or indexes on - * unrepaired SSTables, and {@code false} otherwise. When this is {@code false}, {@link FilterTree} can ignore the - * coordinator suggestion to downgrade to non-strict filtering, potentially reducing the number of false positives. - * - * @see QueryController#getIndexQueryResults(Collection) - * */ - public boolean hasUnrepairedMatches = false; + * Number of partition/row keys fetched from the indexes and that will be used to fetch rows from the base table. + * They will be either partition keys in AA, or row keys in the later row-aware disk formats. + */ + private long keysFetched = 0; + + /** Number of live partitions fetched from the storage engine, before post-filtering. */ + private long partitionsFetched = 0; + + /** Number of live partitions returned to the coordinator, after post-filtering. */ + private long partitionsReturned = 0; + + /** Number of deleted partitions that have been fetched. */ + private long partitionTombstonesFetched = 0; + + /** Number of live rows fetched from the storage engine, before post-filtering. */ + private long rowsFetched = 0; + + /** Number of live rows returned to the coordinator, after post-filtering. */ + private long rowsReturned = 0; + + /** Number of deleted individual rows or ranges of rows that have been fetched. */ + private long rowTombstonesFetched = 0; + + /** Number of trie (literal or key) segments visited by the query. */ + private long trieSegmentsHit = 0; + + /** Number of times the query has jumped to the position of a row ID within a trie (literal or key) posting list. */ + private long triePostingsSkips = 0; + + /** Number of times the query has advanced into a trie (literal or key) posting list. */ + private long triePostingsDecodes = 0; + + /** Number of BKD (numeric) segments visited by the query. */ + private long bkdSegmentsHit = 0; + + /** Number of BKD (numeric) merged posting lists visited by the query. */ + private long bkdPostingListsHit = 0; - public QueryContext(ReadCommand readCommand, long executionQuotaMs) + /** Number of times the query has jumped to the position of a row ID within a BKD (numeric) posting list. */ + private long bkdPostingsSkips = 0; + + /** Number of times the query has advanced into a BKD (numeric) posting list. */ + private long bkdPostingsDecodes = 0; + + /** Cumulative time spent searching ANN graph, in nanoseconds. */ + private long annGraphSearchLatency = 0; + + /** The worst approximate score observed in ANN. */ + private float annRerankFloor = 0.0f; // only called from single-threaded setup code + + private long postFilteringReadLatency = 0; + + // Determines the order of using indexes for filtering and sorting. + // Null means the query execution order hasn't been decided yet. + private FilterSortOrder filterSortOrder = null; + + /** Metrics about the query plan. */ + private PlanInfo queryPlanInfo; + + @VisibleForTesting + public QueryContext() { - this.readCommand = readCommand; - executionQuotaNano = TimeUnit.MILLISECONDS.toNanos(executionQuotaMs); - queryStartTimeNanos = Clock.Global.nanoTime(); + this(DatabaseDescriptor.getRangeRpcTimeout(TimeUnit.MILLISECONDS)); + } + + public QueryContext(long executionQuotaMs) + { + this.executionQuotaNano = TimeUnit.MILLISECONDS.toNanos(executionQuotaMs); + this.queryStartTimeNanos = MonotonicClock.Global.approxTime.now(); } public long totalQueryTimeNs() { - return Clock.Global.nanoTime() - queryStartTimeNanos; + checkThreadOwnership(); + return MonotonicClock.Global.approxTime.now() - queryStartTimeNanos; + } + + public void addSstablesHit(long val) + { + checkThreadOwnership(); + sstablesHit += val; + } + + public void addSegmentsHit(long val) + { + checkThreadOwnership(); + segmentsHit += val; + } + + public void addKeysFetched(long val) + { + checkThreadOwnership(); + keysFetched += val; + } + + public void addPartitionsFetched(long val) + { + checkThreadOwnership(); + partitionsFetched += val; + } + + public void addPartitionsReturned(long val) + { + checkThreadOwnership(); + partitionsReturned += val; + } + + public void addPartitionTombstonesFetched(long val) + { + checkThreadOwnership(); + partitionTombstonesFetched += val; + } + + public void addRowsFetched(long val) + { + checkThreadOwnership(); + rowsFetched += val; + } + + public void addRowsReturned(long val) + { + checkThreadOwnership(); + rowsReturned += val; + } + + public void addRowTombstonesFetched(long val) + { + checkThreadOwnership(); + rowTombstonesFetched += val; + } + + public void addTrieSegmentsHit(long val) + { + checkThreadOwnership(); + trieSegmentsHit += val; + } + + public void addTriePostingsSkips(long val) + { + checkThreadOwnership(); + triePostingsSkips += val; + } + + public void addTriePostingsDecodes(long val) + { + checkThreadOwnership(); + triePostingsDecodes += val; } + public void addBkdSegmentsHit(long val) + { + checkThreadOwnership(); + bkdSegmentsHit += val; + } + + public void addBkdPostingListsHit(long val) + { + checkThreadOwnership(); + bkdPostingListsHit += val; + } + + public void addBkdPostingsSkips(long val) + { + checkThreadOwnership(); + bkdPostingsSkips += val; + } + + public void addBkdPostingsDecodes(long val) + { + checkThreadOwnership(); + bkdPostingsDecodes += val; + } + + public void addAnnGraphSearchLatency(long val) + { + checkThreadOwnership(); + annGraphSearchLatency += val; + } + + public void addPostFilteringReadLatency(long val) + { + checkThreadOwnership(); + postFilteringReadLatency += val; + } + + /** + * Checks if the query has exceeded its execution quota and aborts it if it has timed out. + * + * @throws AbortedOperationException if the query has timed out + */ public void checkpoint() { + checkThreadOwnership(); + if (totalQueryTimeNs() >= executionQuotaNano && !DISABLE_TIMEOUT) { queryTimedOut = true; - throw new QueryCancelledException(readCommand); + throw new AbortedOperationException(); + } + } + + public float getAnnRerankFloor() + { + checkThreadOwnership(); + return annRerankFloor; + } + + public void updateAnnRerankFloor(float observedFloor) + { + checkThreadOwnership(); + + if (observedFloor < Float.POSITIVE_INFINITY) + annRerankFloor = max(annRerankFloor, observedFloor); + } + + public long getPostFilteringReadLatency() + { + checkThreadOwnership(); + return postFilteringReadLatency; + } + + /** + * Determines the order of filtering and sorting operations. + * Currently used only by vector search. + */ + public enum FilterSortOrder + { + /** First get the matching keys from the non-vector indexes, then use vector index to return the top K by similarity order */ + SEARCH_THEN_ORDER, + + /** First get the candidates in ANN order from the vector index, then fetch the rows and filter them until we find K matching the predicates */ + SCAN_THEN_FILTER + } + + public void recordQueryPlan(Plan.RowsIteration originalPlan, Plan.RowsIteration optimizedPlan) + { + if (CassandraRelevantProperties.SAI_QUERY_PLAN_METRICS_ENABLED.getBoolean()) + this.queryPlanInfo = new PlanInfo(originalPlan, optimizedPlan); + } + + /** + * @return a {@link Snapshot} representing an immutable version of this query context. + */ + public Snapshot snapshot() + { + checkThreadOwnership(); + return new Snapshot(this); + } + + /** + * Verifies that the current thread is the owning thread of this QueryContext. + * This is used to enforce single-threaded access to the QueryContext. + * + * @throws AssertionError if assertions are enabled and the current thread is not the owning thread + */ + private void checkThreadOwnership() + { + assert Thread.currentThread().getId() == owningThreadId + : String.format("QueryContext accessed from wrong thread. Expected thread ID: %d, Actual thread: %s (ID: %d)", + owningThreadId, Thread.currentThread().getName(), Thread.currentThread().getId()); + } + + /** + * A snapshot of all relevant metrics in a {@link QueryContext} at a specific point in time. + * This class memoizes the values of those metrics so that they can be reused by multiple metrics instances, + * without calculating the same values once and again. + * Also, this class should be more lightweight than the full {@link QueryContext}, in case of needing to retain it + * for long-ish periods of time, as in the case of the slow query logger, which tracks the metrics of the slowest + * queries over a fixed period of time. + */ + public static class Snapshot + { + public final long totalQueryTimeNs; + public final long sstablesHit; + public final long segmentsHit; + public final long keysFetched; + public final long partitionsFetched; + public final long partitionsReturned; + public final long partitionTombstonesFetched; + public final long rowsFetched; + public final long rowsReturned; + public final long rowTombstonesFetched; + public final long trieSegmentsHit; + public final long triePostingsSkips; + public final long triePostingsDecodes; + public final long bkdSegmentsHit; + public final long bkdPostingListsHit; + public final long bkdPostingsSkips; + public final long bkdPostingsDecodes; + public final boolean queryTimedOut; + public final long annGraphSearchLatency; + public final long postFilteringReadLatency; + public final FilterSortOrder filterSortOrder; + + @Nullable + public final PlanInfo queryPlanInfo; + + /** + * Creates a snapshot of all the metrics in the given {@link QueryContext}. + * + * @param context the query context to snapshot + */ + private Snapshot(QueryContext context) + { + totalQueryTimeNs = context.totalQueryTimeNs(); + sstablesHit = context.sstablesHit; + segmentsHit = context.segmentsHit; + keysFetched = context.keysFetched; + partitionsFetched = context.partitionsFetched; + partitionsReturned = context.partitionsReturned; + partitionTombstonesFetched = context.partitionTombstonesFetched; + rowsFetched = context.rowsFetched; + rowsReturned = context.rowsReturned; + rowTombstonesFetched = context.rowTombstonesFetched; + trieSegmentsHit = context.trieSegmentsHit; + triePostingsSkips = context.triePostingsSkips; + triePostingsDecodes = context.triePostingsDecodes; + bkdSegmentsHit = context.bkdSegmentsHit; + bkdPostingListsHit = context.bkdPostingListsHit; + bkdPostingsSkips = context.bkdPostingsSkips; + bkdPostingsDecodes = context.bkdPostingsDecodes; + queryTimedOut = context.queryTimedOut; + annGraphSearchLatency = context.annGraphSearchLatency; + postFilteringReadLatency = context.postFilteringReadLatency; + filterSortOrder = context.filterSortOrder; + queryPlanInfo = context.queryPlanInfo; } } - public int limit() + /** + * Captures relevant information about a query plan, both original and optimized. + */ + public static class PlanInfo { - return readCommand.limits().count(); + public final boolean searchExecutedBeforeOrder; + public final boolean filterExecutedAfterOrderedScan; + + public final long costEstimated; + public final long rowsToReturnEstimated; + public final long rowsToFetchEstimated; + public final long keysToIterateEstimated; + public final int logSelectivityEstimated; + + public final int indexReferencesInQuery; + public final int indexReferencesInPlan; + + public PlanInfo(@Nonnull Plan.RowsIteration originalPlan, @Nonnull Plan.RowsIteration optimizedPlan) + { + this.costEstimated = Math.round(optimizedPlan.fullCost()); + this.rowsToReturnEstimated = Math.round(optimizedPlan.expectedRows()); + this.rowsToFetchEstimated = Math.round(optimizedPlan.estimatedRowsToFetch()); + this.keysToIterateEstimated = Math.round(optimizedPlan.estimatedKeysToIterate()); + this.logSelectivityEstimated = Math.min(20, (int) Math.floor(-Math.log10(optimizedPlan.selectivity()))); + this.indexReferencesInQuery = originalPlan.referencedIndexCount(); + this.indexReferencesInPlan = optimizedPlan.referencedIndexCount(); + this.searchExecutedBeforeOrder = optimizedPlan.isSearchThenOrderHybrid(); + this.filterExecutedAfterOrderedScan = optimizedPlan.isOrderedScanThenFilterHybrid(); + } } } diff --git a/src/java/org/apache/cassandra/index/sai/README.md b/src/java/org/apache/cassandra/index/sai/README.md index 36176a4c616d..a475b0c2222e 100644 --- a/src/java/org/apache/cassandra/index/sai/README.md +++ b/src/java/org/apache/cassandra/index/sai/README.md @@ -19,25 +19,25 @@ # Storage-Attached Indexing ## Overview -Storage-attached indexing is a column based local secondary index implementation for Cassandra. +Storage-attached indexes are a new column-based secondary indexing apparatus for DSE. -The project was inspired by SASI (SSTable-Attached Secondary Indexes) and retains some of its high-level +The project was inspired by OSS SASI (SSTable-Attached Secondary Indexes) and retains some of its high-level architectural character (and even some actual code), but makes significant improvements in a number of areas: - The on-disk/SSTable index formats for both string and numeric data have been completely replaced. Strings are indexed - on disk using a byte-ordered trie data structure, while numeric types are indexed using a block-oriented balanced tree. + on disk using our proprietary on-disk byte-ordered trie data structure, while numeric types are indexed using Lucene's + balanced kd-tree. - While indexes continue to be managed at the column level from the user's perspective, the storage design at the column index level is row-based, with related offset and token information stored only once at the SSTable level. This drastically reduces our on-disk footprint when several columns are indexed on the same table. -- Tracing, metrics, virtual table-based metadata and snapshot-based backup/restore are supported out of the box. -- On-disk index components can be streamed completely when entire SSTable streaming is enabled. -- Incremental index building is supported, and on-disk index components are included in snapshots. +- The query path is synchronous and index searches run on IO threads. +- Tracing, metrics, virtual table-based metadata, RLAC, and snapshot-based backup/restore are supported out of the box. Many similarities with standard secondary indexes remain: - The full set of C* consistency levels is supported for both reads and writes. - Index updates are synchronous with mutations and do not require any kind of read-before-write. -- Global queries are implemented on the back of C* range reads. +- Queries are implemented on the back of C* range reads. - Paging is supported. - Only token ordering of results is supported. - Index builds are visible to operators as compactions and are executed on compaction threads. @@ -48,23 +48,37 @@ Many similarities with standard secondary indexes remain: The following short tutorial will get you up-and-running with storage-attached indexing. -### Build and Start Cassandra +### Build and Start DSE -Follow the instructions to build and start Cassandra in README.asc in root folder of the Cassandra repository +1.) Make sure you've created the following directories and given yourself permissions on them: + +`/var/log/cassandra` + +`/var/lib/cassandra` + +2.) From the bdp root directory, run the following commands: + +`./gradlew jar` + +`bin/dse cassandra` + +3.) When the node stabilizes, open up `cqlsh` from the bdp root directory. + +`bin/cqlsh` ### Create a Simple Data Model 1.) Run the following DDL statements to create a table and two indexes: -`CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'};` +`CREATE KEYSPACE test WITH replication = {'class': 'NetworkTopologyStrategy' , 'Cassandra': '1'};` `USE test;` `CREATE TABLE person (id int, name text, age int, PRIMARY KEY (id));` -`CREATE INDEX ON person (name) USING 'sai' WITH OPTIONS = {'case_sensitive': false};` +`CREATE CUSTOM INDEX ON person (name) USING 'StorageAttachedIndex' WITH OPTIONS = {'case_sensitive': false};` -`CREATE INDEX ON person (age) USING 'sai';` +`CREATE CUSTOM INDEX ON person (age) USING 'StorageAttachedIndex';` 2.) Add some data. @@ -107,10 +121,9 @@ Follow the instructions to build and start Cassandra in README.asc in root folde - Zhao Yang - Jason Rutherglen - Maciej Zasada -- Andres de la Peña +- Andrew de la Peña - Mike Adamson - Zahir Patni - Tomek Lasica - Berenguer Blasi - Rocco Varela -- Piotr Kołaczkowski diff --git a/src/java/org/apache/cassandra/index/sai/SSTableContext.java b/src/java/org/apache/cassandra/index/sai/SSTableContext.java index 96c53a228acb..e18a697dcddd 100644 --- a/src/java/org/apache/cassandra/index/sai/SSTableContext.java +++ b/src/java/org/apache/cassandra/index/sai/SSTableContext.java @@ -17,13 +17,12 @@ */ package org.apache.cassandra.index.sai; -import java.util.Collections; - import com.google.common.base.Objects; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.SSTableIndex; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.utils.Throwables; @@ -32,26 +31,28 @@ import org.apache.cassandra.utils.concurrent.SharedCloseableImpl; /** - * An {@link SSTableContext} is created for an individual sstable and is shared across column indexes to track per-sstable - * index files. - *

    - * The {@link SSTableContext} will be released when receiving a sstable removed notification, but its shared copies in - * individual {@link SSTableIndex}es will be released when in-flight read requests complete. + * SSTableContext is created for individual sstable shared across indexes to track per-sstable index files. + * + * SSTableContext itself will be released when receiving sstable removed notification, but its shared copies in individual + * SSTableIndex will be released when in-flight read requests complete. */ public class SSTableContext extends SharedCloseableImpl { public final SSTableReader sstable; - public final IndexDescriptor indexDescriptor; + private final IndexComponents.ForRead perSSTableComponents; + public final PrimaryKey.Factory primaryKeyFactory; public final PrimaryKeyMap.Factory primaryKeyMapFactory; private SSTableContext(SSTableReader sstable, - IndexDescriptor indexDescriptor, + IndexComponents.ForRead perSSTableComponents, + PrimaryKey.Factory primaryKeyFactory, PrimaryKeyMap.Factory primaryKeyMapFactory, Cleanup cleanup) { super(cleanup); this.sstable = sstable; - this.indexDescriptor = indexDescriptor; + this.perSSTableComponents = perSSTableComponents; + this.primaryKeyFactory = primaryKeyFactory; this.primaryKeyMapFactory = primaryKeyMapFactory; } @@ -59,16 +60,21 @@ private SSTableContext(SSTableContext copy) { super(copy); this.sstable = copy.sstable; - this.indexDescriptor = copy.indexDescriptor; + this.perSSTableComponents = copy.perSSTableComponents; + this.primaryKeyFactory = copy.primaryKeyFactory; this.primaryKeyMapFactory = copy.primaryKeyMapFactory; } - public static SSTableContext create(SSTableReader sstable) + @SuppressWarnings("resource") + public static SSTableContext create(SSTableReader sstable, IndexComponents.ForRead perSSTableComponents) { + var onDiskFormat = perSSTableComponents.onDiskFormat(); + // no disk access thus no need to use EmptyFactory + PrimaryKey.Factory primaryKeyFactory = onDiskFormat.newPrimaryKeyFactory(sstable.metadata().comparator); + Ref sstableRef = null; PrimaryKeyMap.Factory primaryKeyMapFactory = null; - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); try { sstableRef = sstable.tryRef(); @@ -78,11 +84,14 @@ public static SSTableContext create(SSTableReader sstable) throw new IllegalStateException("Couldn't acquire reference to the sstable: " + sstable); } - primaryKeyMapFactory = indexDescriptor.newPrimaryKeyMapFactory(sstable); + // avoid opening SAI metadata if reads are disabled + primaryKeyMapFactory = CassandraRelevantProperties.SAI_INDEX_READS_DISABLED.getBoolean() + ? new PrimaryKeyMap.DummyThrowingFactory() + : onDiskFormat.newPrimaryKeyMapFactory(perSSTableComponents, primaryKeyFactory, sstable); - Cleanup cleanup = new Cleanup(primaryKeyMapFactory, indexDescriptor, sstableRef); + Cleanup cleanup = new Cleanup(primaryKeyMapFactory, sstableRef); - return new SSTableContext(sstable, indexDescriptor, primaryKeyMapFactory, cleanup); + return new SSTableContext(sstable, perSSTableComponents, primaryKeyFactory, primaryKeyMapFactory, cleanup); } catch (Throwable t) { @@ -91,22 +100,22 @@ public static SSTableContext create(SSTableReader sstable) sstableRef.release(); } - throw Throwables.unchecked(Throwables.close(t, Collections.singleton(primaryKeyMapFactory))); + throw Throwables.unchecked(Throwables.close(t, primaryKeyMapFactory)); } } - @Override - public SSTableContext sharedCopy() + /** + * Returns the concrete on-disk perSStable components used by this context instance. + */ + public IndexComponents.ForRead usedPerSSTableComponents() { - return new SSTableContext(this); + return perSSTableComponents; } - /** - * Returns a new {@link SSTableIndex} for a per-column index - */ - public SSTableIndex newSSTableIndex(StorageAttachedIndex index) + @Override + public SSTableContext sharedCopy() { - return indexDescriptor.newSSTableIndex(this, index); + return new SSTableContext(this); } /** @@ -117,12 +126,19 @@ public Descriptor descriptor() return sstable.descriptor; } - /** - * @return disk usage (in bytes) of per-sstable index files - */ - public long diskUsage() + public SSTableReader sstable() + { + return sstable; + } + + public PrimaryKey.Factory primaryKeyFactory() + { + return primaryKeyFactory; + } + + public PrimaryKeyMap.Factory primaryKeyMapFactory() { - return indexDescriptor.sizeOnDiskOfPerSSTableComponents(); + return primaryKeyMapFactory; } /** @@ -130,7 +146,7 @@ public long diskUsage() */ public int openFilesPerSSTable() { - return indexDescriptor.version.onDiskFormat().openFilesPerSSTableIndex(indexDescriptor.hasClustering()); + return perSSTableComponents.onDiskFormat().openFilesPerSSTable(); } @Override @@ -159,15 +175,11 @@ public int hashCode() private static class Cleanup implements RefCounted.Tidy { private final PrimaryKeyMap.Factory primaryKeyMapFactory; - private final IndexDescriptor indexDescriptor; private final Ref sstableRef; - private Cleanup(PrimaryKeyMap.Factory primaryKeyMapFactory, - IndexDescriptor indexDescriptor, - Ref sstableRef) + private Cleanup(PrimaryKeyMap.Factory primaryKeyMapFactory, Ref sstableRef) { this.primaryKeyMapFactory = primaryKeyMapFactory; - this.indexDescriptor = indexDescriptor; this.sstableRef = sstableRef; } @@ -175,7 +187,7 @@ private Cleanup(PrimaryKeyMap.Factory primaryKeyMapFactory, public void tidy() { Throwable t = sstableRef.ensureReleased(null); - t = Throwables.close(t, Collections.singleton(primaryKeyMapFactory)); + t = Throwables.close(t, primaryKeyMapFactory); Throwables.maybeFail(t); } @@ -183,7 +195,7 @@ public void tidy() @Override public String name() { - return indexDescriptor.toString(); + return null; } } } diff --git a/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java b/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java index b2df0f29c248..82732f499b3e 100644 --- a/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java +++ b/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java @@ -17,90 +17,149 @@ */ package org.apache.cassandra.index.sai; +import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.HashSet; -import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.db.lifecycle.Tracker; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.utils.Pair; /** - * Manages per-sstable {@link SSTableContext}s for {@link StorageAttachedIndexGroup} + * Manage per-sstable {@link SSTableContext} for {@link StorageAttachedIndexGroup} */ @ThreadSafe public class SSTableContextManager { - private static final Logger logger = LoggerFactory.getLogger(SSTableContextManager.class); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + // Even though `SSTableContext` happens to point to its corresponding `IndexDescriptor` for convenience, we track + // the latter separately because we need to track descriptors before it is safe to build a context (we create + // a descriptor as soon as we start indexing a sstable to start tracking the added components, but can only create + // its context when the per-sstable components are complete). + private final ConcurrentHashMap sstableDescriptors = new ConcurrentHashMap<>(); private final ConcurrentHashMap sstableContexts = new ConcurrentHashMap<>(); + private final Tracker tracker; + + SSTableContextManager(Tracker tracker) + { + this.tracker = tracker; + } + /** * Initialize {@link SSTableContext}s if they are not already initialized. * * @param removed SSTables being removed * @param added SSTables being added - * @param validation Controls how indexes should be validated + * @param validate if true, header and footer will be validated. * - * @return a set of contexts for SSTables with valid per-SSTable components, and a set of - * SSTables with invalid or missing components + * @return if all the added (and still "live" at the time of this call) sstable with complete index build have + * valid per-sstable components, then an optional with the context for all those sstables. Otherwise, if any sstable + * has invalid/missing components, then an empty optional is returned (and all invalid sstable will have had their + * context removed, after a call to onInvalid). */ - public Pair, Set> update(Collection removed, Iterable added, IndexValidation validation) + @SuppressWarnings("resource") + public Optional> update(Collection removed, Iterable added, boolean validate, Set indices) { release(removed); Set contexts = new HashSet<>(); - Set invalid = new HashSet<>(); + boolean hasInvalid = false; for (SSTableReader sstable : added) { if (sstable.isMarkedCompacted()) { + logger.debug("Skipped tracking sstable {} because it's marked compacted", sstable); continue; } - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); - - if (!indexDescriptor.isPerSSTableIndexBuildComplete()) + IndexDescriptor indexDescriptor = getOrLoadIndexDescriptor(sstable, indices); + var perSSTableComponents = indexDescriptor.perSSTableComponents(); + if (!perSSTableComponents.isComplete()) { - // Don't even try to validate or add the context if the completion marker is missing. + // This usually means no index has been built for that sstable yet (the alternative would be that we + // lost the completion marker when index build failed). Not point in running + // validation (it would fail), and we also don't want to add it to the returned contexts, since it's + // not ready yet. We know a future call of this method will be triggered for that sstable once the + // index finishes building. + logger.debug("Skipped tracking sstable {} because per sstable components are not complete (components={})", sstable, perSSTableComponents.all()); continue; } try { // Only validate on restart or newly refreshed SSTable. Newly built files are unlikely to be corrupted. - if (!sstableContexts.containsKey(sstable) && !indexDescriptor.validatePerSSTableComponents(validation, true, false)) + if (validate && !sstableContexts.containsKey(sstable) && !perSSTableComponents.validateComponents(sstable, tracker, true, false)) { - invalid.add(sstable); - removeInvalidSSTableContext(sstable); + // Note that the validation already log details on the problem if it fails, so no reason to log further + hasInvalid = true; continue; } - // ConcurrentHashMap#computeIfAbsent guarantees atomicity, so {@link SSTableContext#create(SSTableReader)}} - // is called at most once per key. - contexts.add(sstableContexts.computeIfAbsent(sstable, SSTableContext::create)); + // ConcurrentHashMap#compute guarantees atomicity, so {@link SSTableContext#create(SSTableReader)}} is + // called at most once per key and underlying components. + contexts.add(sstableContexts.compute(sstable, (__, prevContext) -> computeUpdatedContext(sstable, prevContext, perSSTableComponents))); } catch (Throwable t) { - logger.warn(indexDescriptor.logMessage("Failed to update per-SSTable components for SSTable {}"), sstable.descriptor, t); - invalid.add(sstable); - removeInvalidSSTableContext(sstable); + logger.warn(indexDescriptor.logMessage("Unexpected error updating per-SSTable components for SSTable {}"), sstable.descriptor, t); + // We haven't been able to correctly set the context, so the index shouldn't be used, and we invalidate + // the components to ensure that's the case. + perSSTableComponents.invalidate(sstable, tracker); + hasInvalid = true; + remove(sstable); } } - return Pair.create(contexts, invalid); + return hasInvalid ? Optional.empty() : Optional.of(contexts); + } + + private static SSTableContext computeUpdatedContext(SSTableReader reader, @Nullable SSTableContext previousContext, IndexComponents.ForRead perSSTableComponents) + { + // We can (and should) keep the previous context if both: + // 1. it exists + // 2. it uses a "complete" set of per-sstable components (not that we always initially create a `SSTableContext` + // from a complete set, so if it is not complete, it means the previous components have been corrupted, and + // we want to use the new one (a rebuild)). + // 3. it uses "up-to-date" per-sstable components. + if (previousContext != null && previousContext.usedPerSSTableComponents().isComplete() && previousContext.usedPerSSTableComponents().buildId().equals(perSSTableComponents.buildId())) + return previousContext; + + // Now, if we create a new one, we should close the previous one if it exists. + // Note that `SSTableIndex` references `SSTableContext` through a `#sharedCopy() so even if there is still + // index referencing this context currently in use, this will not break ongoing queries. + if (previousContext != null) + previousContext.close(); + + return SSTableContext.create(reader, perSSTableComponents); + } + + private void release(Collection toRelease) + { + toRelease.forEach(this::remove); + } + + Collection allContexts() + { + return sstableContexts.values(); } - public void release(Collection toRelease) + @VisibleForTesting + SSTableContext getContext(SSTableReader sstable) { - toRelease.stream().map(sstableContexts::remove).filter(Objects::nonNull).forEach(SSTableContext::close); + return sstableContexts.get(sstable); } /** @@ -112,16 +171,19 @@ int openFiles() } /** - * @return total disk usage (in bytes) of all per-sstable index files + * @return total disk usage of all per-sstable index files */ long diskUsage() { - return sstableContexts.values().stream().mapToLong(SSTableContext::diskUsage).sum(); + return sstableContexts.values().stream() + .mapToLong(ssTableContext -> ssTableContext.usedPerSSTableComponents().liveSizeOnDiskInBytes()) + .sum(); } - Set sstables() + @VisibleForTesting + public boolean contains(SSTableReader sstable) { - return sstableContexts.keySet(); + return sstableContexts.containsKey(sstable); } @VisibleForTesting @@ -133,15 +195,52 @@ public int size() @VisibleForTesting public void clear() { + sstableDescriptors.clear(); sstableContexts.values().forEach(SSTableContext::close); sstableContexts.clear(); } - @SuppressWarnings("EmptyTryBlock") - private void removeInvalidSSTableContext(SSTableReader sstable) + @SuppressWarnings("resource") + private void remove(SSTableReader sstable) { - try (SSTableContext ignored = sstableContexts.remove(sstable)) - { - } + sstableDescriptors.remove(sstable); + SSTableContext context = sstableContexts.remove(sstable); + if (context != null) + context.close(); + } + + /** + * Returns a descriptor for the given sstable and indexes, reusing a cached one when possible. + *

    + * Note that during index initialization, index is added to {@link StorageAttachedIndexGroup} one by one but initialization + * tasks are executed concurrently, each task may have a different view of registered indexes. The cache must handle + * it and load indexes if they are not already loaded by the cached IndexDescriptor. + */ + IndexDescriptor getOrLoadIndexDescriptor(SSTableReader sstable, Set indices) + { + Set requested = contexts(indices); + return sstableDescriptors.compute(sstable, (k, existing) -> { + // load for the 1st time + if (existing == null) + return IndexDescriptor.load(sstable, requested); + + // all requested indexes are loaded + if (existing.includedIndexes().containsAll(requested)) + return existing; + + logger.debug("Reloading existing IndexDescriptor for {} because included indexes {} do not include all indexes from {}", sstable.descriptor.id, existing.includedIndexes(), requested); + // Load indexes that are not included. Not creating a new IndexDescriptor because existing IndexDescriptor + // is already referenced in SSTableContext#perSSTableComponents + existing = existing.loadIfAbsent(sstable, requested); + return existing; + }); + } + + private static Set contexts(Set indices) + { + Set contexts = Sets.newHashSetWithExpectedSize(indices.size()); + for (StorageAttachedIndex index : indices) + contexts.add(index.getIndexContext()); + return contexts; } } diff --git a/src/java/org/apache/cassandra/index/sai/SSTableIndex.java b/src/java/org/apache/cassandra/index/sai/SSTableIndex.java new file mode 100644 index 000000000000..a9ab01f196d1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/SSTableIndex.java @@ -0,0 +1,409 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.virtual.SimpleDataSet; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.disk.EmptyIndex; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMapIterator; +import org.apache.cassandra.index.sai.disk.SearchableIndex; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.Segment; +import org.apache.cassandra.index.sai.iterators.KeyRangeAntiJoinIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.SSTableIdFactory; +import org.apache.cassandra.io.sstable.SSTableWatcher; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.Throwables; + +/** + * SSTableIndex is created for each column index on individual sstable to track per-column indexer. + */ +public class SSTableIndex implements Comparable +{ + private static final Logger logger = LoggerFactory.getLogger(SSTableIndex.class); + + // sort sstable index by first key then last key + public static final Comparator COMPARATOR = Comparator.comparing((SSTableIndex s) -> s.getSSTable().first) + .thenComparing(s -> s.getSSTable().last) + .thenComparing(s -> s.getSSTable().descriptor.id, SSTableIdFactory.COMPARATOR); + + private final SSTableContext sstableContext; + private final IndexContext indexContext; + private final SSTableReader sstable; + private final SearchableIndex searchableIndex; + private final IndexComponents.ForRead perIndexComponents; + + private final AtomicInteger references = new AtomicInteger(1); + private final AtomicBoolean indexWasDropped = new AtomicBoolean(false); + + public SSTableIndex(SSTableContext sstableContext, IndexComponents.ForRead perIndexComponents) + { + assert perIndexComponents.context().getValidator() != null; + this.perIndexComponents = perIndexComponents; + this.searchableIndex = createSearchableIndex(sstableContext, perIndexComponents); + + this.sstableContext = sstableContext.sharedCopy(); // this line must not be before any code that may throw + this.indexContext = perIndexComponents.context(); + this.sstable = sstableContext.sstable; + } + + private static SearchableIndex createSearchableIndex(SSTableContext sstableContext, IndexComponents.ForRead perIndexComponents) + { + if (CassandraRelevantProperties.SAI_INDEX_READS_DISABLED.getBoolean()) + { + logger.info("Creating dummy (empty) index searcher for sstable {} as SAI index reads are disabled", sstableContext.sstable.descriptor); + return new EmptyIndex(); + } + + return perIndexComponents.onDiskFormat().newSearchableIndex(sstableContext, perIndexComponents); + } + + public IndexContext getIndexContext() + { + return indexContext; + } + + /** + * Returns the concrete on-disk perIndex components used by this index instance. + */ + public IndexComponents.ForRead usedPerIndexComponents() + { + return perIndexComponents; + } + + public SSTableContext getSSTableContext() + { + return sstableContext; + } + + public List getSegments() + { + return searchableIndex.getSegments(); + } + + public long indexFileCacheSize() + { + return searchableIndex.indexFileCacheSize(); + } + + /** + * @return number of indexed rows, note that rows may have been updated or removed in sstable. + */ + public long getRowCount() + { + return searchableIndex.getRowCount(); + } + + /** + * Returns the total number of terms in all indexed rows of this index. + * This number is approximate because it does not account for any deletions + * that may have occurred since the index was built. + */ + public long getApproximateTermCount() + { + return searchableIndex.getApproximateTermCount(); + } + + /** + * Estimates the number of rows that would be returned by this index given the predicate using the index + * histogram. + * Note that this is not a guarantee of the number of rows that will actually be returned. + * + * @return an approximate number of the matching rows + */ + public long estimateMatchingRowsCount(Expression predicate) + { + return searchableIndex.estimateMatchingRowsCount(predicate); + } + + /** + * Counts the number of rows that would be returned by this index given the predicate. + * + * @return the row count + */ + public long getMatchingRowsCount(Expression predicate, AbstractBounds keyRange, QueryContext queryContext) + { + queryContext.checkpoint(); + queryContext.addSstablesHit(1); + assert !isReleased(); + + try (KeyRangeIterator keyIterator = search(predicate, keyRange, queryContext, false)) + { + return keyIterator.getMaxKeys(); + } + catch (Throwable e) + { + if (logger.isDebugEnabled() && !(e instanceof AbortedOperationException)) + logger.debug(String.format("Failed search an index %s.", getSSTable()), e); + throw Throwables.cleaned(e); + } + } + + /** + * @return total size of per-column SAI components, in bytes + */ + public long sizeOfPerColumnComponents() + { + return perIndexComponents.liveSizeOnDiskInBytes(); + } + + /** + * @return total size of per-sstable SAI components, in bytes + */ + public long sizeOfPerSSTableComponents() + { + return sstableContext.usedPerSSTableComponents().liveSizeOnDiskInBytes(); + } + + /** + * @return the smallest possible sstable row id in this index. + */ + public long minSSTableRowId() + { + return searchableIndex.minSSTableRowId(); + } + + /** + * @return the largest possible sstable row id in this index. + */ + public long maxSSTableRowId() + { + return searchableIndex.maxSSTableRowId(); + } + + public ByteBuffer minTerm() + { + return searchableIndex.minTerm(); + } + + public ByteBuffer maxTerm() + { + return searchableIndex.maxTerm(); + } + + public DecoratedKey minKey() + { + return searchableIndex.minKey(); + } + + public DecoratedKey maxKey() + { + return searchableIndex.maxKey(); + } + + // Returns an iterator for NEQ, NOT_CONTAINS_KEY, NOT_CONTAINS_VALUE, which + // 1. Either includes everything if the column type values can be truncated and + // thus the keys cannot be matched precisely, + // 2. or includes everything minus the keys matching the expression + // if the column type values cannot be truncated, i.e., matching the keys is always precise. + // (not matching precisely will lead to false negatives) + // + // keys k such that row(k) not contains v = (all keys) \ (keys k such that row(k) contains v) + // + // Note that rows in other indexes are not matched, so this can return false positives, + // but they are not a problem as post-filtering would get rid of them. + // The keys matched in other indexes cannot be safely subtracted + // as indexes may contain false positives caused by deletes and updates. + private KeyRangeIterator getNonEqIterator(Expression expression, + AbstractBounds keyRange, + QueryContext context, + boolean defer) throws IOException + { + KeyRangeIterator allKeys = allSSTableKeys(keyRange); + if (TypeUtil.supportsRounding(expression.validator)) + { + return allKeys; + } + else + { + Expression negExpression = expression.negated(); + KeyRangeIterator matchedKeys = searchableIndex.search(negExpression, keyRange, context, defer); + return KeyRangeAntiJoinIterator.create(allKeys, matchedKeys); + } + } + + public KeyRangeIterator search(Expression expression, + AbstractBounds keyRange, + QueryContext context, + boolean defer) throws IOException + { + if (expression.getOp().isNonEquality()) + { + return getNonEqIterator(expression, keyRange, context, defer); + } + + return searchableIndex.search(expression, keyRange, context, defer); + } + + public List> orderBy(Orderer orderer, + Expression predicate, + AbstractBounds keyRange, + QueryContext context, + int limit, + long totalRows) throws IOException + { + context.checkpoint(); + context.addSstablesHit(1); + assert !isReleased(); + + return searchableIndex.orderBy(orderer, predicate, keyRange, context, limit, totalRows); + } + + public void populateSegmentView(SimpleDataSet dataSet) + { + searchableIndex.populateSystemView(dataSet, sstable); + } + + public Version getVersion() + { + return perIndexComponents.version(); + } + + public IndexFeatureSet indexFeatureSet() + { + return getVersion().onDiskFormat().indexFeatureSet(); + } + + public SSTableReader getSSTable() + { + return sstable; + } + + public boolean reference() + { + while (true) + { + int n = references.get(); + if (n <= 0) + return false; + if (references.compareAndSet(n, n + 1)) + { + return true; + } + } + } + + public boolean isReleased() + { + return references.get() <= 0; + } + + public boolean isEmpty() + { + return searchableIndex instanceof EmptyIndex; + } + + public void release() + { + int n = references.decrementAndGet(); + + if (n == 0) + { + FileUtils.closeQuietly(searchableIndex); + sstableContext.close(); + + /* + * When SSTable is removed, storage-attached index components will be automatically removed by LogTransaction. + * We only remove index components explicitly in case of index corruption or index rebuild if immutable + * components are not in use. + */ + if (indexWasDropped.get()) + SSTableWatcher.instance.onIndexDropped(sstable.metadata(), perIndexComponents.forWrite()); + } + } + + /** + * Indicates that this index has been dropped by the user, and so the underlying files can be safely removed. + */ + public void markIndexDropped() + { + indexWasDropped.getAndSet(true); + release(); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SSTableIndex other = (SSTableIndex)o; + return Objects.equal(sstableContext, other.sstableContext) && Objects.equal(indexContext, other.indexContext); + } + + public int hashCode() + { + return Objects.hashCode(sstableContext, indexContext); + } + + public List> orderResultsBy(QueryContext context, List keys, Orderer orderer, int limit, long totalRows) throws IOException + { + context.checkpoint(); + context.addSstablesHit(1); + assert !isReleased(); + + return searchableIndex.orderResultsBy(context, keys, orderer, limit, totalRows); + } + + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("column", indexContext.getColumnName()) + .add("sstable", sstable.descriptor) + .add("totalRows", sstable.getTotalRows()) + .toString(); + } + + @Override + public int compareTo(SSTableIndex index) + { + // SSTableReader is truly unique for comparison which is relied on in IntervalTree + return getSSTable().compareTo(index.getSSTable()); + } + + protected final KeyRangeIterator allSSTableKeys(AbstractBounds keyRange) throws IOException + { + return PrimaryKeyMapIterator.create(sstableContext, keyRange); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java index 10c2a93cc910..3c853dcc2325 100644 --- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java +++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java @@ -24,51 +24,46 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.NavigableMap; import java.util.Optional; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; -import javax.annotation.Nullable; - import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.collect.ImmutableSet; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.cql3.QueryOptions; -import org.apache.cassandra.cql3.restrictions.Restriction; -import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction; import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.CassandraWriteContext; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.RangeTombstone; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.WriteContext; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.guardrails.GuardrailViolatedException; -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.db.guardrails.MaxThreshold; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; @@ -79,86 +74,151 @@ import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.IndexBuildDecider; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.index.FeatureNeedsIndexRebuildException; +import org.apache.cassandra.index.SecondaryIndexBuilder; +import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.index.TargetParser; import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; +import org.apache.cassandra.index.sai.analyzer.AnalyzerEqOperatorSupport; +import org.apache.cassandra.index.sai.analyzer.LuceneAnalyzer; import org.apache.cassandra.index.sai.analyzer.NonTokenizingOptions; -import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.index.sai.disk.StorageAttachedIndexWriter; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig; -import org.apache.cassandra.index.sai.memory.MemtableIndexManager; -import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; -import org.apache.cassandra.index.sai.metrics.IndexMetrics; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.view.IndexViewManager; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.index.sai.view.View; import org.apache.cassandra.index.transactions.IndexTransaction; -import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableFlushObserver; -import org.apache.cassandra.io.sstable.SSTableIdFactory; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.concurrent.ImmediateFuture; -import org.apache.cassandra.utils.concurrent.OpOrder; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_VALIDATE_TERMS_AT_COORDINATOR; import static org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig.MAX_TOP_K; public class StorageAttachedIndex implements Index { public static final String NAME = "sai"; - - public static final String VECTOR_USAGE_WARNING = "SAI ANN indexes on vector columns are experimental and are not recommended for production use.\n" + - "They don't yet support SELECT queries with:\n" + - " * Consistency level higher than ONE/LOCAL_ONE.\n" + - " * Paging.\n" + - " * No LIMIT clauses.\n" + - " * PER PARTITION LIMIT clauses.\n" + - " * GROUP BY clauses.\n" + - " * Aggregation functions.\n" + - " * Filters on columns without a SAI index."; - - public static final String VECTOR_NON_FLOAT_ERROR = "SAI ANN indexes are only allowed on vector columns with float elements"; - public static final String VECTOR_1_DIMENSION_COSINE_ERROR = "Cosine similarity is not supported for single-dimension vectors"; - public static final String VECTOR_MULTIPLE_DATA_DIRECTORY_ERROR = "SAI ANN indexes are not allowed on multiple data directories"; + public static final String NGRAM_WITHOUT_QUERY_ANALYZER_WARNING = + "Using an ngram analyzer without defining a query_analyzer. " + + "This means that the same ngram analyzer will be applied to both indexed and queried column values. " + + "Applying ngram analysis to the queried values usually produces too many search tokens to be useful. " + + "The large number of tokens can also have a negative impact in performance. " + + "In most cases it's better to use a simpler query_analyzer such as the standard one."; - @VisibleForTesting - public static final String ANALYSIS_ON_KEY_COLUMNS_MESSAGE = "Analysis options are not supported on primary key columns, but found "; + private static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndex.class); - public static final String ANN_LIMIT_ERROR = "Use of ANN OF in an ORDER BY clause requires a LIMIT that is not greater than %s. LIMIT was %s"; + private static final boolean VALIDATE_TERMS_AT_COORDINATOR = SAI_VALIDATE_TERMS_AT_COORDINATOR.getBoolean(); - private static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndex.class); + private static class StorageAttachedIndexBuildingSupport implements IndexBuildingSupport + { + public NavigableMap> prepareSSTablesToBuild(StorageAttachedIndexGroup group, + Set indexes, + Collection sstablesToRebuild, + boolean isFullRebuild) + { + NavigableMap> sstables = new TreeMap<>(SSTableReader.idComparator); + + indexes.stream() + .filter((i) -> i instanceof StorageAttachedIndex) + .forEach((i) -> + { + StorageAttachedIndex sai = (StorageAttachedIndex) i; + IndexContext indexContext = ((StorageAttachedIndex) i).getIndexContext(); + + // If this is not a full manual index rebuild we can skip SSTables that already have an + // attached index. Otherwise, we override any pre-existent index. + Collection ss = sstablesToRebuild; + if (!isFullRebuild) + { + ss = sstablesToRebuild.stream() + .filter(s -> !IndexDescriptor.isIndexBuildCompleteOnDisk(s, indexContext)) + .collect(Collectors.toList()); + } + + group.prepareIndexSSTablesForRebuild(ss, sai); + + ss.forEach((sstable) -> + { + Set toBuild = sstables.get(sstable); + if (toBuild == null) sstables.put(sstable, (toBuild = new HashSet<>())); + toBuild.add(sai); + }); + }); + + return sstables; + } + + @Override + public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, Set indexes, Collection sstablesToRebuild, boolean isFullRebuild) + { + StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs); + NavigableMap> sstables = prepareSSTablesToBuild(group, indexes, sstablesToRebuild, isFullRebuild); + return new StorageAttachedIndexBuilder(group, sstables, isFullRebuild, false); + } + + @Override + public List getParallelIndexBuildTasks(ColumnFamilyStore cfs, Set indexes, Collection sstablesToRebuild, boolean isFullRebuild) + { + StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(cfs); + NavigableMap> sstables = prepareSSTablesToBuild(indexGroup, indexes, sstablesToRebuild, isFullRebuild); + + List> groups = groupBySize(new ArrayList<>(sstables.keySet()), DatabaseDescriptor.getConcurrentCompactors()); + List builders = new ArrayList<>(); - private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + for (List group : groups) + { + SortedMap> current = new TreeMap<>(Comparator.comparing(sstable -> sstable.descriptor.id)); + group.forEach(sstable -> current.put(sstable, sstables.get(sstable))); + + builders.add(new StorageAttachedIndexBuilder(indexGroup, current, isFullRebuild, false)); + } + + logger.info("Creating {} parallel index builds over {} total sstables for {}...", builders.size(), sstables.size(), cfs.metadata()); - public static final String TERM_OVERSIZE_MESSAGE = "Term in column '%s' for key '%s' is too large and cannot be indexed. (term size: %s)"; + return builders; + } + } // Used to build indexes on newly added SSTables: private static final StorageAttachedIndexBuildingSupport INDEX_BUILDER_SUPPORT = new StorageAttachedIndexBuildingSupport(); - private static final Set VALID_OPTIONS = ImmutableSet.of(IndexTarget.TARGET_OPTION_NAME, + private static final Set VALID_OPTIONS = ImmutableSet.of(NonTokenizingOptions.CASE_SENSITIVE, + NonTokenizingOptions.NORMALIZE, + NonTokenizingOptions.ASCII, + // For now, we leave this for backward compatibility even though it's not used + IndexContext.ENABLE_SEGMENT_COMPACTION_OPTION_NAME, + IndexTarget.TARGET_OPTION_NAME, IndexTarget.CUSTOM_INDEX_OPTION_NAME, + IndexWriterConfig.POSTING_LIST_LVL_MIN_LEAVES, + IndexWriterConfig.POSTING_LIST_LVL_SKIP_OPTION, IndexWriterConfig.MAXIMUM_NODE_CONNECTIONS, IndexWriterConfig.CONSTRUCTION_BEAM_WIDTH, + IndexWriterConfig.NEIGHBORHOOD_OVERFLOW, + IndexWriterConfig.ALPHA, + IndexWriterConfig.ENABLE_HIERARCHY, IndexWriterConfig.SIMILARITY_FUNCTION, + IndexWriterConfig.SOURCE_MODEL, IndexWriterConfig.OPTIMIZE_FOR, - NonTokenizingOptions.CASE_SENSITIVE, - NonTokenizingOptions.NORMALIZE, - NonTokenizingOptions.ASCII); + LuceneAnalyzer.INDEX_ANALYZER, + LuceneAnalyzer.QUERY_ANALYZER, + AnalyzerEqOperatorSupport.OPTION); + // this does not include vectors because each Vector declaration is a separate type instance public static final Set SUPPORTED_TYPES = ImmutableSet.of(CQL3Type.Native.ASCII, CQL3Type.Native.BIGINT, CQL3Type.Native.DATE, CQL3Type.Native.DOUBLE, CQL3Type.Native.FLOAT, CQL3Type.Native.INT, CQL3Type.Native.SMALLINT, CQL3Type.Native.TEXT, CQL3Type.Native.TIME, @@ -170,44 +230,34 @@ public class StorageAttachedIndex implements Index ImmutableSet.of(OrderPreservingPartitioner.class, LocalPartitioner.class, ByteOrderedPartitioner.class, RandomPartitioner.class); private final ColumnFamilyStore baseCfs; - private final IndexMetadata indexMetadata; - private final IndexTermType indexTermType; - private final IndexIdentifier indexIdentifier; - private final IndexViewManager viewManager; - private final ColumnQueryMetrics columnQueryMetrics; - private final IndexWriterConfig indexWriterConfig; - @Nullable private final AbstractAnalyzer.AnalyzerFactory analyzerFactory; - private final PrimaryKey.Factory primaryKeyFactory; - private final MemtableIndexManager memtableIndexManager; - private final IndexMetrics indexMetrics; - private final MaxThreshold maxTermSizeGuardrail; - - // Tracks whether we've started the index build on initialization. - private volatile boolean initBuildStarted = false; - - // Tracks whether the index has been invalidated due to removal, a table drop, etc. - private volatile boolean valid = true; - - public StorageAttachedIndex(ColumnFamilyStore baseCfs, IndexMetadata indexMetadata) + private final IndexMetadata config; + private final IndexContext indexContext; + + // Tracks whether or not we've started the index build on initialization. + private volatile boolean canFlushFromMemtableIndex = false; + + // Tracks whether the index has been dropped due to removal, a table drop, etc or index schema is unloaded after schema unassignment + private volatile boolean dropped = false; + private volatile boolean unloaded = false; + + /** + * Called via reflection from SecondaryIndexManager + */ + public StorageAttachedIndex(ColumnFamilyStore baseCfs, IndexMetadata config) { this.baseCfs = baseCfs; - this.indexMetadata = indexMetadata; + this.config = config; TableMetadata tableMetadata = baseCfs.metadata(); - Pair target = TargetParser.parse(tableMetadata, indexMetadata); - indexTermType = IndexTermType.create(target.left, tableMetadata.partitionKeyColumns(), target.right); - indexIdentifier = new IndexIdentifier(baseCfs.getKeyspaceName(), baseCfs.getTableName(), indexMetadata.name); - primaryKeyFactory = new PrimaryKey.Factory(tableMetadata.partitioner, tableMetadata.comparator); - indexWriterConfig = IndexWriterConfig.fromOptions(indexMetadata.name, indexTermType, indexMetadata.options); - viewManager = new IndexViewManager(this); - columnQueryMetrics = indexTermType.isLiteral() ? new ColumnQueryMetrics.TrieIndexMetrics(indexIdentifier) - : new ColumnQueryMetrics.BalancedTreeIndexMetrics(indexIdentifier); - analyzerFactory = AbstractAnalyzer.fromOptions(indexTermType, indexMetadata.options); - memtableIndexManager = new MemtableIndexManager(this); - indexMetrics = new IndexMetrics(this, memtableIndexManager); - maxTermSizeGuardrail = indexTermType.isVector() - ? Guardrails.saiVectorTermSize - : (indexTermType.isFrozen() ? Guardrails.saiFrozenTermSize - : Guardrails.saiStringTermSize); + Pair target = TargetParser.parse(tableMetadata, config); + this.indexContext = new IndexContext(tableMetadata.keyspace, + tableMetadata.name, + tableMetadata.id, + tableMetadata.partitionKeyType, + tableMetadata.comparator, + target.left, + target.right, + config, + baseCfs); } /** @@ -255,54 +305,100 @@ public static Map validateOptions(Map options, T throw new InvalidRequestException("Failed to retrieve target column for: " + targetColumn); } - // In order to support different index targets on non-frozen map, ie. KEYS, VALUE, ENTRIES, we need to put index - // name as part of index file name instead of column name. We only need to check that the target is different - // between indexes. This will only allow indexes in the same column with a different IndexTarget.Type. - // - // Note that: "metadata.indexes" already includes current index - if (metadata.indexes.stream().filter(index -> index.getIndexClassName().equals(StorageAttachedIndex.class.getName())) - .map(index -> TargetParser.parse(metadata, index.options.get(IndexTarget.TARGET_OPTION_NAME))) - .filter(Objects::nonNull).filter(t -> t.equals(target)).count() > 1) + // Check for duplicate indexes considering both target and analyzer configuration + boolean isAnalyzed = AbstractAnalyzer.isAnalyzed(options); + long duplicateCount = metadata.indexes.stream() + .filter(index -> index.getIndexClassName().equals(StorageAttachedIndex.class.getName())) + .filter(index -> { + // Indexes on the same column with different target (KEYS, VALUES, ENTRIES) + // are allowed on non-frozen Maps + var existingTarget = TargetParser.parse(metadata, index.options.get(IndexTarget.TARGET_OPTION_NAME)); + if (existingTarget == null || !existingTarget.equals(target)) + return false; + // Also allow different indexes if one is analyzed and the other isn't + return isAnalyzed == AbstractAnalyzer.isAnalyzed(index.options); + }) + .count(); + // >1 because "metadata.indexes" already includes current index + if (duplicateCount > 1) + throw new InvalidRequestException(String.format("Cannot create duplicate storage-attached index on column: %s", target.left)); + + // Analyzer is not supported against PK columns + if (isAnalyzed) { - throw new InvalidRequestException("Cannot create more than one storage-attached index on the same column: " + target.left); + for (ColumnMetadata column : metadata.primaryKeyColumns()) + { + if (column.name.equals(target.left.name)) + logger.warn("Schema contains an invalid index analyzer on primary key column, allowed for backwards compatibility: " + target.left); + } } - Map analysisOptions = AbstractAnalyzer.getAnalyzerOptions(options); - if (target.left.isPrimaryKeyColumn() && !analysisOptions.isEmpty()) + AbstractType type = TypeUtil.cellValueType(target.left, target.right); + + // Validate analyzers by building them + try (AbstractAnalyzer.AnalyzerFactory analyzerFactory = AbstractAnalyzer.fromOptions(targetColumn, type, options)) { - throw new InvalidRequestException(ANALYSIS_ON_KEY_COLUMNS_MESSAGE + new CqlBuilder().append(analysisOptions)); + if (AbstractAnalyzer.hasQueryAnalyzer(options)) + AbstractAnalyzer.fromOptionsQueryAnalyzer(type, options).close(); + else if (analyzerFactory.isNGram()) + ClientWarn.instance.warn(NGRAM_WITHOUT_QUERY_ANALYZER_WARNING); } - IndexTermType indexTermType = IndexTermType.create(target.left, metadata.partitionKeyColumns(), target.right); - AbstractAnalyzer.fromOptions(indexTermType, analysisOptions); - IndexWriterConfig config = IndexWriterConfig.fromOptions(null, indexTermType, options); + var config = IndexWriterConfig.fromOptions(null, type, options); - // If we are indexing map entries we need to validate the subtypes - if (indexTermType.isComposite()) + // If we are indexing map entries we need to validate the sub-types + if (TypeUtil.isComposite(type)) { - for (IndexTermType subType : indexTermType.subTypes()) + for (AbstractType subType : type.subTypes()) { - if (!SUPPORTED_TYPES.contains(subType.asCQL3Type()) && !subType.isFrozen()) - throw new InvalidRequestException("Unsupported type: " + subType.asCQL3Type()); + if (!SUPPORTED_TYPES.contains(subType.asCQL3Type()) && !TypeUtil.isFrozen(subType)) + throw new InvalidRequestException("Unsupported composite type for SAI: " + subType.asCQL3Type()); } } - else if (!SUPPORTED_TYPES.contains(indexTermType.asCQL3Type()) && !indexTermType.isFrozen()) + else if (type.isVector()) { - throw new InvalidRequestException("Unsupported type: " + indexTermType.asCQL3Type()); - } - // If this is a vector type we need to validate it for the current vector index constraints - else if (indexTermType.isVector()) - { - if (!(indexTermType.vectorElementType() instanceof FloatType)) - throw new InvalidRequestException(VECTOR_NON_FLOAT_ERROR); + if (!Version.current(metadata.keyspace).onOrAfter(Version.JVECTOR_EARLIEST)) + { + throw new InvalidRequestException(vectorUnsupportedByCurrentVersionError(Version.current(metadata.keyspace))); + } - if (indexTermType.vectorDimension() == 1 && config.getSimilarityFunction() == VectorSimilarityFunction.COSINE) - throw new InvalidRequestException(VECTOR_1_DIMENSION_COSINE_ERROR); + // Also, any pre-existing indexes must be in a version compatible with vectors. + ColumnFamilyStore baseCfs = Schema.instance.getColumnFamilyStoreInstance(metadata.id); + if (baseCfs != null) + { + StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs); + if (indexGroup != null) + { + Version version = indexGroup.getMinVersion(); + if (!version.onOrAfter(Version.JVECTOR_EARLIEST)) + throw new InvalidRequestException(vectorUnsupportedByExistingVersionError(version)); + } + } - if (DatabaseDescriptor.getRawConfig().data_file_directories.length > 1) - throw new InvalidRequestException(VECTOR_MULTIPLE_DATA_DIRECTORY_ERROR); + if (type.valueLengthIfFixed() == 4 && config.getSimilarityFunction() == VectorSimilarityFunction.COSINE) + throw new InvalidRequestException("Cosine similarity is not supported for single-dimension vectors"); - ClientWarn.instance.warn(VECTOR_USAGE_WARNING); + // vectors of fixed length types are fixed length too, so we can reject the index creation + // if that fixed length is over the max term size for vectors + if (type.isValueLengthFixed() && IndexContext.MAX_VECTOR_TERM_SIZE < type.valueLengthIfFixed()) + { + AbstractType elementType = ((VectorType) type).elementType; + var error = String.format("Vector index created with %s will produce terms of %s, " + + "exceeding the max vector term size of %s. " + + "That sets an implicit limit of %d dimensions for %s vectors.", + type.asCQL3Type(), + FBUtilities.prettyPrintMemory(type.valueLengthIfFixed()), + FBUtilities.prettyPrintMemory(IndexContext.MAX_VECTOR_TERM_SIZE), + IndexContext.MAX_VECTOR_TERM_SIZE / elementType.valueLengthIfFixed(), + elementType.asCQL3Type()); + // VSTODO until we can safely differentiate client and system requests, we can only log here + // Ticket for this: https://github.com/riptano/VECTOR-SEARCH/issues/85 + logger.warn(error); + } + } + else if (!SUPPORTED_TYPES.contains(type.asCQL3Type()) && !TypeUtil.isFrozen(type)) + { + throw new InvalidRequestException("Unsupported type for SAI: " + type.asCQL3Type()); } return Collections.emptyMap(); @@ -324,38 +420,176 @@ public void unregister(IndexRegistry registry) @Override public IndexMetadata getIndexMetadata() { - return indexMetadata; + return config; + } + + @Override + public boolean shouldSkipInitialization() + { + // SAI performs partial initialization so it must always execute it; the actual index build is then still skipped + // if IndexBuildDecider.instance.onInitialBuild().skipped() is true. + return false; } @Override public Callable getInitializationTask() { + IndexBuildDecider.Decision decision = IndexBuildDecider.instance.onInitialBuild(); // New storage-attached indexes will be available for queries after on disk index data are built. - // Memtable data will be indexed via flushing triggered by schema change. - // We only want to validate the index files if we are starting up. - boolean isStarting = StorageService.instance.isStarting(); - IndexValidation validation = isStarting ? IndexValidation.HEADER_FOOTER : IndexValidation.NONE; + // Memtable data will be indexed via flushing triggered by schema change + // We only want to validate the index files if we are starting up + return () -> startInitialBuild(baseCfs, StorageService.instance.isStarting(), decision.skipped()).get(); + } - // Only attempt to make the index queryable if we are starting up. Otherwise, if we create a new index on top - // of nothing but existing Memtable data (i.e. no SSTables), that data will temporarily be lost until flush. - if (isStarting) + private Future startInitialBuild(ColumnFamilyStore baseCfs, boolean validate, boolean skipIndexBuild) + { + if (skipIndexBuild) { + logger.info("Skipping initialization task for {}.{} after flushing memtable", baseCfs.metadata(), indexContext.getIndexName()); + // Force another flush to make sure on disk index is generated for memtable data before marking it queryable. + // In case of offline scrub, there is no live memtables. + if (!baseCfs.getTracker().getView().liveMemtables.isEmpty()) + baseCfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.INDEX_BUILD_STARTED); + + // even though we're skipping the index build, we still want to add any initial sstables that have indexes into SAI. + // Index will be queryable if all existing sstables have index files; otherwise non-queryable + Set sstables = baseCfs.getLiveSSTables(); StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs); - assert indexGroup != null : "Index group does not exist for table " + baseCfs.keyspace + '.' + baseCfs.name; + indexGroup.onSSTableChanged(Collections.emptyList(), sstables, Collections.singleton(this), validate); - Collection nonIndexed = findNonIndexedSSTables(baseCfs, indexGroup, validation); + // From now on, all memtable will have attached memtable index. It is now safe to flush indexes directly from flushing Memtables. + canFlushFromMemtableIndex = true; + return ImmediateFuture.success(null); + } - if (nonIndexed.isEmpty()) + if (baseCfs.indexManager.isIndexQueryable(this)) + { + logger.debug(indexContext.logMessage("Skipping validation and building in initialization task, as pre-join has already made the storage attached index queryable...")); + canFlushFromMemtableIndex = true; + return ImmediateFuture.success(null); + } + + StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs); + assert indexGroup != null; + + // verify the compatibility of the on-disk format version with the vector index + if (indexContext.isVector()) + { + // The current version must support vectors + Version currentVersion = indexContext.version(); + if (currentVersion.compareTo(Version.JVECTOR_EARLIEST) < 0) { - // If the index is complete, mark it queryable and avoid an initial build: - baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED); - logger.debug(indexIdentifier.logMessage("Skipping initial build, as index is already queryable...")); - initBuildStarted = true; - return () -> ImmediateFuture.success(null); + throw new FeatureNeedsIndexRebuildException(vectorUnsupportedByCurrentVersionError(currentVersion)); + } + + // Also, any pre-existing indexes must be in a version compatible with vectors + Version minVersion = indexGroup.getMinVersion(); + if (minVersion.compareTo(Version.JVECTOR_EARLIEST) < 0) + { + throw new FeatureNeedsIndexRebuildException(vectorUnsupportedByExistingVersionError(minVersion)); + } + } + + // stop in-progress compaction tasks to prevent compacted sstables not being indexed. + logger.debug(indexContext.logMessage("Stopping active compactions to make sure all sstables are indexed after initial build.")); + CompactionManager.instance.interruptCompactionFor(Collections.singleton(baseCfs.metadata()), + OperationType.REWRITES_SSTABLES, + Predicates.alwaysTrue(), + true, + TableOperation.StopTrigger.INDEX_BUILD); + + // Force another flush to make sure on disk index is generated for memtable data before marking it queryable. + // In case of offline scrub, there is no live memtables. + if (!baseCfs.getTracker().getView().liveMemtables.isEmpty()) + { + baseCfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.INDEX_BUILD_STARTED); + } + + // From now on, all memtable will have attached memtable index. It is now safe to flush indexes directly from flushing Memtables. + canFlushFromMemtableIndex = true; + + List nonIndexed = findNonIndexedSSTables(baseCfs, indexGroup, validate); + + if (nonIndexed.isEmpty()) + { + return ImmediateFuture.success(null); + } + + // split sorted sstables into groups with similar size and build each group in separate compaction thread + List> groups = groupBySize(nonIndexed, DatabaseDescriptor.getConcurrentCompactors()); + List> futures = new ArrayList<>(); + + for (List group : groups) + { + SortedMap> current = new TreeMap<>(SSTableReader.idComparator); + group.forEach(sstable -> current.put(sstable, Collections.singleton(this))); + + futures.add(CompactionManager.instance.submitIndexBuild(new StorageAttachedIndexBuilder(indexGroup, current, false, true))); + } + + logger.info(indexContext.logMessage("Submitting {} parallel initial index builds over {} total sstables..."), futures.size(), nonIndexed.size()); + return FutureCombiner.allOf(futures); + } + + @VisibleForTesting + public static String vectorUnsupportedByCurrentVersionError(Version currentVersion) + { + return String.format("The current configured on-disk format version %s does not support vector indexes. " + + "The minimum version that supports vectors is %s. " + + "The on-disk format version can be set via the -D%s system property.", + currentVersion, + Version.JVECTOR_EARLIEST, + CassandraRelevantProperties.SAI_CURRENT_VERSION.name()); + } + + @VisibleForTesting + public static String vectorUnsupportedByExistingVersionError(Version existingVersion) + { + return String.format("The current sstables have other indexes using the on-disk format version %s, " + + "which is not compatible with vector indexes. " + + "The minimum version that supports vectors is %s. " + + "The on-disk format version can be set via the -D%s system property, " + + "and the indexes on the sstables can be upgraded via nodetool upgradesstables." + + "Then it will be needed to rebuild the index after the upgrade.", + existingVersion, + Version.JVECTOR_EARLIEST, + CassandraRelevantProperties.SAI_CURRENT_VERSION.name()); + } + + /** + * Splits SSTables into groups of similar overall size. + * + * @param toRebuild a list of SSTables to split (Note that this list will be sorted in place!) + * @param parallelism an upper bound on the number of groups + * + * @return a {@link List} of SSTable groups, each represented as a {@link List} of {@link SSTableReader} + */ + @VisibleForTesting + public static List> groupBySize(List toRebuild, int parallelism) + { + List> groups = new ArrayList<>(); + + toRebuild.sort(Comparator.comparingLong(SSTableReader::onDiskLength).reversed()); + Iterator sortedSSTables = toRebuild.iterator(); + double dataPerCompactor = toRebuild.stream().mapToLong(SSTableReader::onDiskLength).sum() * 1.0 / parallelism; + + while (sortedSSTables.hasNext()) + { + long sum = 0; + List current = new ArrayList<>(); + + while (sortedSSTables.hasNext() && sum < dataPerCompactor) + { + SSTableReader sstable = sortedSSTables.next(); + sum += sstable.onDiskLength(); + current.add(sstable); } + + assert !current.isEmpty(); + groups.add(current); } - return () -> startInitialBuild(baseCfs, validation).get(); + return groups; } @Override @@ -376,19 +610,29 @@ public Callable getInvalidateTask() return () -> { // mark index as invalid, in-progress SSTableIndexWriters will abort - valid = false; + dropped = true; // in case of dropping table, SSTable indexes should already been removed by SSTableListChangedNotification. - Set toRemove = getComponents(); - for (SSTableIndex sstableIndex : view().getIndexes()) - sstableIndex.getSSTable().unregisterComponents(toRemove, baseCfs.getTracker()); - - viewManager.invalidate(); - if (analyzerFactory != null) - analyzerFactory.close(); - columnQueryMetrics.release(); - memtableIndexManager.invalidate(); - indexMetrics.release(); + for (SSTableIndex sstableIndex : indexContext.getView().getIndexes()) + { + var components = sstableIndex.usedPerIndexComponents(); + sstableIndex.getSSTable().unregisterComponents(components.allAsCustomComponents(), baseCfs.getTracker()); + } + + indexContext.invalidate(true); + return null; + }; + } + + @Override + public Callable getUnloadTask() + { + return () -> + { + // mark index as invalid, in-progress SSTableIndexWriters will abort + unloaded = true; + + indexContext.invalidate(false); return null; }; } @@ -406,6 +650,51 @@ public Callable getPreJoinTask(boolean hadBootstrap) return this::startPreJoinTask; } + @VisibleForTesting + public boolean canFlushFromMemtableIndex() + { + return canFlushFromMemtableIndex; + } + + public BooleanSupplier isDropped() + { + return () -> dropped; + } + + public BooleanSupplier isUnloaded() + { + return () -> unloaded; + } + + @SuppressWarnings("SameReturnValue") + private Future startPreJoinTask() + { + try + { + if (baseCfs.indexManager.isIndexQueryable(this)) + { + logger.debug(indexContext.logMessage("Skipping validation in pre-join task, as the initialization task has already made the index queryable...")); + baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED); + return null; + } + + StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(baseCfs); + Collection nonIndexed = findNonIndexedSSTables(baseCfs, group, true); + + if (nonIndexed.isEmpty()) + { + // If the index is complete, mark it queryable before the node starts accepting requests: + baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED); + } + } + catch (Throwable t) + { + logger.error(indexContext.logMessage("Failed in pre-join task!"), t); + } + + return null; + } + @Override public Callable getTruncateTask(long truncatedAt) { @@ -415,7 +704,7 @@ public Callable getTruncateTask(long truncatedAt) * build of the index it won't get marked queryable by the build. */ return () -> { - logger.info(indexIdentifier.logMessage("Making index queryable during table truncation")); + logger.info(indexContext.logMessage("Making index queryable during table truncation")); baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED); return null; }; @@ -442,13 +731,13 @@ public Optional getBackingTable() @Override public boolean dependsOn(ColumnMetadata column) { - return indexTermType.dependsOn(column); + return indexContext.getDefinition().compareTo(column) == 0; } @Override public boolean supportsExpression(ColumnMetadata column, Operator operator) { - return dependsOn(column) && indexTermType.supports(operator); + return dependsOn(column) && indexContext.supports(operator); } @Override @@ -464,547 +753,262 @@ public AbstractType customExpressionValueType() } @Override - public RowFilter getPostIndexQueryFilter(RowFilter filter) + public boolean isAnalyzed() { - // it should be executed from the SAI query plan, this is only used by the singleton index query plan - throw new UnsupportedOperationException(); + return indexContext.isAnalyzed(); } @Override - public Comparator getPostQueryOrdering(Restriction restriction, QueryOptions options) + public Optional getAnalyzer(ByteBuffer queriedValue) { - // For now, only support ANN - assert restriction instanceof SingleColumnRestriction.AnnRestriction; + if (!indexContext.isAnalyzed()) + return Optional.empty(); + + // memoize the analyzed queried value, so we don't have to re-analyze it for every evaluated column value + List queriedTokens = analyze(indexContext.getQueryAnalyzerFactory(), queriedValue); - Preconditions.checkState(indexTermType.isVector()); + return Optional.of(new Analyzer() { + @Override + public List indexedTokens(ByteBuffer value) + { + return analyze(indexContext.getAnalyzerFactory(), value); + } - SingleColumnRestriction.AnnRestriction annRestriction = (SingleColumnRestriction.AnnRestriction) restriction; - VectorSimilarityFunction function = indexWriterConfig.getSimilarityFunction(); + @Override + public List queriedTokens() + { + return queriedTokens; + } + }); + } - float[] target = indexTermType.decomposeVector(annRestriction.value(options).duplicate()); + private static List analyze(AbstractAnalyzer.AnalyzerFactory factory, ByteBuffer value) + { + if (value == null) + return null; - return (leftBuf, rightBuf) -> { - float[] left = indexTermType.decomposeVector(leftBuf.duplicate()); - double scoreLeft = function.compare(left, target); + List tokens = new ArrayList<>(); + AbstractAnalyzer analyzer = factory.create(); + try + { + analyzer.reset(value.duplicate()); + while (analyzer.hasNext()) + tokens.add(analyzer.next()); + } + finally + { + analyzer.end(); + } + return tokens; + } - float[] right = indexTermType.decomposeVector(rightBuf.duplicate()); - double scoreRight = function.compare(right, target); - return Double.compare(scoreRight, scoreLeft); // descending order - }; + @Override + public RowFilter getPostIndexQueryFilter(RowFilter filter) + { + // it should be executed from the SAI query plan, this is only used by the singleton index query plan + throw new UnsupportedOperationException(); } @Override public void validate(ReadCommand command) throws InvalidRequestException { - if (!indexTermType.isVector()) + var indexQueryPlan = command.indexQueryPlan(); + if (indexQueryPlan == null || !indexQueryPlan.isTopK()) return; - // to avoid overflow of the vector graph internal data structure and avoid OOM when filtering top-k - if (command.limits().count() > MAX_TOP_K) - throw new InvalidRequestException(String.format(ANN_LIMIT_ERROR, MAX_TOP_K, command.limits().count())); + // to avoid overflow HNSW internal data structure and avoid OOM when filtering top-k + if (command.limits().isUnlimited() || command.limits().count() > MAX_TOP_K) + throw new InvalidRequestException(String.format("SAI based ORDER BY clause requires a LIMIT that is not greater than %s. LIMIT was %s", + MAX_TOP_K, command.limits().isUnlimited() ? "NO LIMIT" : command.limits().count())); + + indexContext.validate(command.rowFilter()); } @Override public long getEstimatedResultRows() { - throw new UnsupportedOperationException("Use StorageAttachedIndexQueryPlan#getEstimatedResultRows() instead."); + // This is temporary (until proper QueryPlan is integrated into Cassandra) + // and allows us to prioritize storage-attached indexes if any in the query since they + // are going to be more efficient, to query and intersect, than built-in indexes. + // See CNDB-14764 for details. + // Please remember to update StorageAttachedIndexQueryPlan#getEstimatedResultRows() when changing this. + return Long.MIN_VALUE; } @Override public boolean isQueryable(Status status) { - // consider unknown status as queryable, because gossip may not be up-to-date for newly joining nodes. - return status == Status.BUILD_SUCCEEDED || status == Status.UNKNOWN; + return !CassandraRelevantProperties.SAI_INDEX_READS_DISABLED.getBoolean() + && (status == Status.BUILD_SUCCEEDED || status == Status.UNKNOWN); } @Override public void validate(PartitionUpdate update, ClientState state) throws InvalidRequestException { - DecoratedKey key = update.partitionKey(); + if (!VALIDATE_TERMS_AT_COORDINATOR) + return; - if (indexTermType.columnMetadata().isStatic()) - validateTermSizeForRow(key, update.staticRow(), true, state); - else - for (Row row : update) - validateTermSizeForRow(key, row, true, state); + DecoratedKey key = update.partitionKey(); + for (Row row : update.rows()) + indexContext.validate(key, row); } - @Override - public Searcher searcherFor(ReadCommand command) throws InvalidRequestException - { - // searchers should be created from the query plan, this is only used by the singleton index query plan - throw new UnsupportedOperationException(); - } @Override - public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker) + public int getFlushPeriodInMs() { - // flush observers should be created from the index group, this is only used by the singleton index group - throw new UnsupportedOperationException("Storage-attached index flush observers should never be created directly."); + return indexContext.isVector() + ? CassandraRelevantProperties.SAI_VECTOR_FLUSH_PERIOD_IN_MILLIS.getInt() + : CassandraRelevantProperties.SAI_NON_VECTOR_FLUSH_PERIOD_IN_MILLIS.getInt(); } - @Override - public Set getComponents() + /** + * This method is called by the startup tasks to find SSTables that don't have indexes. The method is + * synchronized so that the view is unchanged between validation and the selection of non-indexed SSTables. + * + * @return a list SSTables without attached indexes + */ + private synchronized List findNonIndexedSSTables(ColumnFamilyStore baseCfs, StorageAttachedIndexGroup group, boolean validate) { - return Version.LATEST.onDiskFormat() - .perColumnIndexComponents(indexTermType) - .stream() - .map(c -> Version.LATEST.makePerIndexComponent(c, indexIdentifier)) - .collect(Collectors.toSet()); - } + Set sstables = baseCfs.getLiveSSTables(); - @Override - public boolean notifyIndexerAboutRowsInFullyExpiredSSTables() - { - return false; - } + // Initialize the SSTable indexes w/ valid existing components... + assert group != null : "Missing index group on " + baseCfs.name; + group.onSSTableChanged(Collections.emptyList(), sstables, Collections.singleton(this), validate); - @Override - public Indexer indexerFor(DecoratedKey key, - RegularAndStaticColumns columns, - long nowInSec, - WriteContext writeContext, - IndexTransaction.Type transactionType, - Memtable memtable) - { - if (transactionType == IndexTransaction.Type.UPDATE) + // ...then identify and rebuild the SSTable indexes that are missing. + List nonIndexed = new ArrayList<>(); + View view = indexContext.getView(); + + for (SSTableReader sstable : sstables) { - return new UpdateIndexer(key, memtable, writeContext); + // An SSTable is considered not indexed if: + // 1. The current view does not contain the SSTable + // 2. The SSTable is not marked compacted + // 3. The column index does not have a completion marker + if (!view.containsSSTableIndex(sstable.descriptor) + && !sstable.isMarkedCompacted() + && !IndexDescriptor.isIndexBuildCompleteOnDisk(sstable, indexContext)) + { + nonIndexed.add(sstable); + } } - // we are only interested in the data from Memtable - // everything else is going to be handled by SSTableWriter observers - return null; - } - - @Override - public IndexBuildingSupport getBuildTaskSupport() - { - return INDEX_BUILDER_SUPPORT; + return nonIndexed; } - /** - * Splits SSTables into groups of similar overall size. - * - * @param toRebuild a list of SSTables to split (Note that this list will be sorted in place!) - * @param parallelism an upper bound on the number of groups - * - * @return a {@link List} of SSTable groups, each represented as a {@link List} of {@link SSTableReader} - */ - @VisibleForTesting - public static List> groupBySize(List toRebuild, int parallelism) + private class UpdateIndexer extends IndexerAdapter { - List> groups = new ArrayList<>(); - - toRebuild.sort(Comparator.comparingLong(SSTableReader::onDiskLength).reversed()); - Iterator sortedSSTables = toRebuild.iterator(); - double dataPerCompactor = toRebuild.stream().mapToLong(SSTableReader::onDiskLength).sum() * 1.0 / parallelism; + private final DecoratedKey key; + private final Memtable mt; + private final WriteContext writeContext; - while (sortedSSTables.hasNext()) + UpdateIndexer(DecoratedKey key, Memtable mt, WriteContext writeContext) { - long sum = 0; - List current = new ArrayList<>(); - - while (sortedSSTables.hasNext() && sum < dataPerCompactor) - { - SSTableReader sstable = sortedSSTables.next(); - sum += sstable.onDiskLength(); - current.add(sstable); - } - - assert !current.isEmpty(); - groups.add(current); + this.key = key; + this.mt = mt; + this.writeContext = writeContext; } - return groups; - } - - /** - * @return A set of SSTables which have attached to them invalid index components. - */ - public Collection onSSTableChanged(Collection oldSSTables, Collection newSSTables, IndexValidation validation) - { - return viewManager.update(oldSSTables, newSSTables, validation); - } - - public void drop(Collection sstablesToRebuild) - { - viewManager.drop(sstablesToRebuild); - } - - public MemtableIndexManager memtableIndexManager() - { - return memtableIndexManager; - } - - public View view() - { - return viewManager.view(); - } - - public IndexTermType termType() - { - return indexTermType; - } - - public IndexIdentifier identifier() - { - return indexIdentifier; - } - - public PrimaryKey.Factory keyFactory() - { - return primaryKeyFactory; - } - - @VisibleForTesting - public ColumnFamilyStore baseCfs() - { - return baseCfs; - } - - public IndexWriterConfig indexWriterConfig() - { - return indexWriterConfig; - } - - public boolean hasAnalyzer() - { - return analyzerFactory != null; - } - - /** - * Returns an {@link AbstractAnalyzer} for use by write and query paths to transform - * literal values. - */ - public AbstractAnalyzer analyzer() - { - assert analyzerFactory != null : "Index does not support string analysis"; - return analyzerFactory.create(); - } - - public IndexMetrics indexMetrics() - { - return indexMetrics; - } - - public ColumnQueryMetrics columnQueryMetrics() - { - return columnQueryMetrics; - } - - public boolean isInitBuildStarted() - { - return initBuildStarted; - } - - public BooleanSupplier isIndexValid() - { - return () -> valid; - } - - /** - * Vector indexes do not supporrt L0 shards due to the cost associated with resharding at flush time. - * @return true iff the index supports sharding at L0. - */ - public boolean supportsL0Shards() - { - return !indexTermType.isVector(); - } - - public boolean hasClustering() - { - return baseCfs.getComparator().size() > 0; - } - - /** - * @return the number of indexed rows in this index (aka. a pair of term and rowId) - */ - public long cellCount() - { - return view().getIndexes() - .stream() - .mapToLong(SSTableIndex::getRowCount) - .sum(); - } - - /** - * @return total number of per-index open files - */ - public int openPerColumnIndexFiles() - { - return viewManager.view().size() * Version.LATEST.onDiskFormat().openFilesPerColumnIndex(); - } - - /** - * @return the total size (in bytes) of per-column index components - */ - public long diskUsage() - { - return view().getIndexes() - .stream() - .mapToLong(SSTableIndex::sizeOfPerColumnComponents) - .sum(); - } - - /** - * @return the total memory usage (in bytes) of per-column index on-disk data structure - */ - public long indexFileCacheSize() - { - return view().getIndexes() - .stream() - .mapToLong(SSTableIndex::indexFileCacheSize) - .sum(); - } - - /** - * Removes this index from the {@code SecondaryIndexManager}'s set of queryable indexes. - */ - public void makeIndexNonQueryable() - { - baseCfs.indexManager.makeIndexNonQueryable(this, Status.BUILD_FAILED); - logger.warn(indexIdentifier.logMessage("Storage-attached index is no longer queryable. Please restart this node to repair it.")); - } - - /** - * Validate maximum term size for given row - */ - public void validateTermSizeForRow(DecoratedKey key, Row row, boolean isClientMutation, ClientState state) - { - AbstractAnalyzer analyzer = hasAnalyzer() ? analyzer() : null; - if (indexTermType.isNonFrozenCollection()) - { - Iterator bufferIterator = indexTermType.valuesOf(row, FBUtilities.nowInSeconds()); - while (bufferIterator != null && bufferIterator.hasNext()) - validateTermSizeForCell(analyzer, key, bufferIterator.next(), isClientMutation, state); - } - else + @Override + public void insertRow(Row row) { - ByteBuffer value = indexTermType.valueOf(key, row, FBUtilities.nowInSeconds()); - validateTermSizeForCell(analyzer, key, value, isClientMutation, state); + indexContext.index(key, row, mt, CassandraWriteContext.fromContext(writeContext).getGroup()); } - } - - private void validateTermSizeForCell(AbstractAnalyzer analyzer, DecoratedKey key, @Nullable ByteBuffer cellBuffer, boolean isClientMutation, ClientState state) - { - if (cellBuffer == null || cellBuffer.remaining() == 0) - return; - - // analyzer should not return terms that are larger than the origin value. - if (!maxTermSizeGuardrail.warnsOn(cellBuffer.remaining(), null)) - return; - if (analyzer != null) + @Override + public void updateRow(Row oldRow, Row newRow) { - analyzer.reset(cellBuffer.duplicate()); - while (analyzer.hasNext()) - validateTermSize(key, analyzer.next(), isClientMutation, state); + indexContext.update(key, oldRow, newRow, mt, CassandraWriteContext.fromContext(writeContext).getGroup()); } - else + + @Override + public void partitionDelete(DeletionTime deletionTime) { - validateTermSize(key, cellBuffer.duplicate(), isClientMutation, state); + // Initialize the memtable index to ensure proper SAI views of the data + indexContext.initializeMemtableIndex(mt); } - } - /** - * @return true if the size of the given term is below the maximum term size, false otherwise - * - * @throws GuardrailViolatedException if a client mutation contains a term that breaches the failure threshold - */ - public boolean validateTermSize(DecoratedKey key, ByteBuffer term, boolean isClientMutation, ClientState state) - { - if (isClientMutation) + @Override + public void rangeTombstone(RangeTombstone tombstone) { - maxTermSizeGuardrail.guard(term.remaining(), indexTermType.columnName(), false, state); - return true; + // Initialize the memtable index to ensure proper SAI views of the data + indexContext.initializeMemtableIndex(mt); } - if (maxTermSizeGuardrail.failsOn(term.remaining(), state)) + @Override + public void removeRow(Row row) { - String message = indexIdentifier.logMessage(String.format(TERM_OVERSIZE_MESSAGE, - indexTermType.columnName(), - key, - FBUtilities.prettyPrintMemory(term.remaining()))); - noSpamLogger.warn(message); - return false; + // Initialize the memtable index to ensure proper SAI views of the data + indexContext.initializeMemtableIndex(mt); } - - return true; } - @Override - public String toString() + protected static abstract class IndexerAdapter implements Indexer { - return indexIdentifier.toString(); + @Override + public void begin() { } + + @Override + public void finish() { } } @Override - public boolean equals(Object obj) + public Searcher searcherFor(ReadCommand command) throws InvalidRequestException { - if (obj == this) - return true; - - if (!(obj instanceof StorageAttachedIndex)) - return false; - - StorageAttachedIndex other = (StorageAttachedIndex) obj; - - return Objects.equals(indexTermType, other.indexTermType) && - Objects.equals(indexMetadata, other.indexMetadata) && - Objects.equals(baseCfs.getComparator(), other.baseCfs.getComparator()); + // searchers should be created from the query plan, this is only used by the singleton index query plan + throw new UnsupportedOperationException(); } @Override - public int hashCode() + public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker) { - return Objects.hash(indexTermType, indexMetadata, baseCfs.getComparator()); + throw new UnsupportedOperationException("Storage-attached index flush observers should never be created directly."); } - private Future startInitialBuild(ColumnFamilyStore baseCfs, IndexValidation validation) + @Override + public Indexer indexerFor(DecoratedKey key, + RegularAndStaticColumns columns, + long nowInSec, + WriteContext writeContext, + IndexTransaction.Type transactionType, + Memtable memtable) { - if (baseCfs.indexManager.isIndexQueryable(this)) - { - logger.debug(indexIdentifier.logMessage("Skipping validation and building in initialization task, as pre-join has already made the storage-attached index queryable...")); - initBuildStarted = true; - return ImmediateFuture.success(null); - } - - // stop in-progress compaction tasks to prevent compacted sstable not being indexed. - logger.debug(indexIdentifier.logMessage("Stopping active compactions to make sure all sstables are indexed after initial build.")); - CompactionManager.instance.interruptCompactionFor(Collections.singleton(baseCfs.metadata()), - ssTableReader -> true, - true); - - // Force another flush to make sure on disk index is generated for memtable data before marking it queryable. - // In the case of offline scrub, there are no live memtables. - if (!baseCfs.getTracker().getView().liveMemtables.isEmpty()) - baseCfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.INDEX_BUILD_STARTED); - - // It is now safe to flush indexes directly from flushing Memtables. - initBuildStarted = true; - - StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs); - assert indexGroup != null : "Index group does not exist for table " + baseCfs.keyspace + '.' + baseCfs.name; - - List nonIndexed = findNonIndexedSSTables(baseCfs, indexGroup, validation); - - if (nonIndexed.isEmpty()) - return ImmediateFuture.success(null); - - // split sorted sstables into groups with similar size and build each group in separate compaction thread - List> groups = groupBySize(nonIndexed, DatabaseDescriptor.getConcurrentIndexBuilders()); - List> futures = new ArrayList<>(); - - for (List group : groups) + if (transactionType == IndexTransaction.Type.UPDATE) { - SortedMap> current = new TreeMap<>(Comparator.comparing(s -> s.descriptor.id, SSTableIdFactory.COMPARATOR)); - group.forEach(sstable -> current.put(sstable, Collections.singleton(this))); - - futures.add(CompactionManager.instance.submitIndexBuild(new StorageAttachedIndexBuilder(indexGroup, current, false, true))); + return new UpdateIndexer(key, memtable, writeContext); } - logger.info(indexIdentifier.logMessage("Submitting {} parallel initial index builds over {} total sstables..."), futures.size(), nonIndexed.size()); - return FutureCombiner.allOf(futures); + // we are only interested in the data from Memtable + // everything else is going to be handled by SSTableWriter observers + return null; } - @SuppressWarnings("SameReturnValue") - private Future startPreJoinTask() + @Override + public IndexBuildingSupport getBuildTaskSupport() { - try - { - if (baseCfs.indexManager.isIndexQueryable(this)) - { - logger.debug(indexIdentifier.logMessage("Skipping validation in pre-join task, as the initialization task has already made the index queryable...")); - baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED); - return null; - } - - StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs); - assert indexGroup != null : "Index group does not exist for table " + baseCfs.keyspace + '.' + baseCfs.name; - - Collection nonIndexed = findNonIndexedSSTables(baseCfs, indexGroup, IndexValidation.HEADER_FOOTER); + return INDEX_BUILDER_SUPPORT; + } - if (nonIndexed.isEmpty()) - { - // If the index is complete, mark it queryable before the node starts accepting requests: - baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED); - } - } - catch (Throwable t) - { - logger.error(indexIdentifier.logMessage("Failed in pre-join task!"), t); - } + public IndexContext getIndexContext() + { + return indexContext; + } - return null; + @Override + public String toString() + { + return String.format("%s.%s.%s", baseCfs.keyspace.getName(), baseCfs.name, config == null ? "?" : config.name); } /** - * This method is called by the startup tasks to find SSTables that don't have indexes. The method is - * synchronized so that the view is unchanged between validation and the selection of non-indexed SSTables. + * Removes this index from the {@link SecondaryIndexManager}'s set of queryable indexes. * - * @return a list SSTables without attached indexes + * This usually happens in response to an index writing failure from {@link StorageAttachedIndexWriter}. */ - private synchronized List findNonIndexedSSTables(ColumnFamilyStore baseCfs, StorageAttachedIndexGroup group, IndexValidation validation) - { - Set sstables = baseCfs.getLiveSSTables(); - - // Initialize the SSTable indexes w/ valid existing components... - assert group != null : "Missing index group on " + baseCfs.name; - group.onSSTableChanged(Collections.emptyList(), sstables, Collections.singleton(this), validation); - - // ...then identify and rebuild the SSTable indexes that are missing. - List nonIndexed = new ArrayList<>(); - View view = viewManager.view(); - - for (SSTableReader sstable : sstables) - { - // An SSTable is considered not indexed if: - // 1. The current view does not contain the SSTable - // 2. The SSTable is not marked compacted - // 3. The column index does not have a completion marker - if (!view.containsSSTable(sstable) && !sstable.isMarkedCompacted() && - !IndexDescriptor.create(sstable).isPerColumnIndexBuildComplete(indexIdentifier)) - { - nonIndexed.add(sstable); - } - } - - return nonIndexed; - } - - private class UpdateIndexer implements Index.Indexer + public void makeIndexNonQueryable() { - private final DecoratedKey key; - private final Memtable memtable; - private final WriteContext writeContext; - - UpdateIndexer(DecoratedKey key, Memtable memtable, WriteContext writeContext) - { - this.key = key; - this.memtable = memtable; - this.writeContext = writeContext; - } - - @Override - public void insertRow(Row row) - { - adjustMemtableSize(memtableIndexManager.index(key, row, memtable), - CassandraWriteContext.fromContext(writeContext).getGroup()); - } - - @Override - public void updateRow(Row oldRow, Row newRow) - { - adjustMemtableSize(memtableIndexManager.update(key, oldRow, newRow, memtable), - CassandraWriteContext.fromContext(writeContext).getGroup()); - } - - void adjustMemtableSize(long additionalSpace, OpOrder.Group opGroup) - { - // The memtable will assert if we try and reduce its memory usage so, for now, just don't tell it. - if (additionalSpace >= 0) - memtable.markExtraOnHeapUsed(additionalSpace, opGroup); - } + baseCfs.indexManager.makeIndexNonQueryable(this, Status.BUILD_FAILED); + logger.warn(indexContext.logMessage("Storage-attached index is no longer queryable. Please restart this node to repair it.")); } } diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java index 55f6381859cd..209d4c707f2a 100644 --- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java +++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java @@ -21,36 +21,43 @@ package org.apache.cassandra.index.sai; +import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.SortedMap; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.google.common.collect.Maps; + +import org.apache.cassandra.io.sstable.KeyIterator; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.TimeUUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.compaction.CompactionInfo; import org.apache.cassandra.db.compaction.CompactionInterruptedException; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.index.SecondaryIndexBuilder; import org.apache.cassandra.index.sai.disk.StorageAttachedIndexWriter; +import org.apache.cassandra.index.sai.disk.format.ComponentsBuildId; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.io.sstable.KeyIterator; import org.apache.cassandra.io.sstable.SSTableIdentityIterator; -import org.apache.cassandra.io.sstable.SSTableFlushObserver; +import org.apache.cassandra.io.sstable.SSTableWatcher; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Throwables; -import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.Ref; @@ -82,10 +89,7 @@ public class StorageAttachedIndexBuilder extends SecondaryIndexBuilder private long bytesProcessed = 0; private final long totalSizeInBytes; - StorageAttachedIndexBuilder(StorageAttachedIndexGroup group, - SortedMap> sstables, - boolean isFullRebuild, - boolean isInitialBuild) + StorageAttachedIndexBuilder(StorageAttachedIndexGroup group, SortedMap> sstables, boolean isFullRebuild, boolean isInitialBuild) { this.group = group; this.metadata = group.metadata(); @@ -131,6 +135,7 @@ private String logMessage(String message) private boolean indexSSTable(SSTableReader sstable, Set indexes) { logger.debug(logMessage("Starting index build on {}"), sstable.descriptor); + long startTimeNanos = Clock.Global.nanoTime(); CountDownLatch perSSTableFileLock = null; StorageAttachedIndexWriter indexWriter = null; @@ -142,19 +147,25 @@ private boolean indexSSTable(SSTableReader sstable, Set in return false; } - try (RandomAccessReader dataFile = sstable.openDataReader(); + SSTableWatcher.instance.onIndexBuild(sstable, indexes); + + IndexDescriptor indexDescriptor = group.descriptorFor(sstable); + + Set replacedComponents = new HashSet<>(); + + try (RandomAccessReader dataFile = sstable.openDataReader(ReadPattern.SEQUENTIAL); LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.INDEX_BUILD, sstable)) { - perSSTableFileLock = shouldWritePerSSTableFiles(sstable); + perSSTableFileLock = shouldWritePerSSTableFiles(sstable, indexDescriptor, replacedComponents); // If we were unable to get the per-SSTable file lock it means that the - // per-SSTable components are already being built, so we only want to + // per-SSTable components are already being built so we only want to // build the per-index components boolean perIndexComponentsOnly = perSSTableFileLock == null; - // remove existing per column index files instead of overwriting - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); - indexes.forEach(index -> indexDescriptor.deleteColumnIndex(index.termType(), index.identifier())); + for (StorageAttachedIndex index : indexes) + prepareForRebuild(indexDescriptor.perIndexComponents(index.getIndexContext()), replacedComponents); - indexWriter = StorageAttachedIndexWriter.createBuilderWriter(indexDescriptor, indexes, txn, perIndexComponentsOnly); + long keyCount = SSTableReader.getApproximateKeyCount(Set.of(sstable)); + indexWriter = new StorageAttachedIndexWriter(indexDescriptor, metadata, indexes, txn, keyCount, perIndexComponentsOnly, group.table().metric); indexWriter.begin(); @@ -166,15 +177,15 @@ private boolean indexSSTable(SSTableReader sstable, Set in { if (isStopRequested()) { - logger.debug(indexDescriptor.logMessage("Index build has been stopped")); - throw new CompactionInterruptedException(getCompactionInfo()); + logger.debug(indexDescriptor.logMessage("Index build has been stopped. Reason: {}"), trigger()); + throw new CompactionInterruptedException(getProgress(), trigger()); } DecoratedKey key = keys.next(); + long position = sstable.getPosition(key, SSTableReader.Operator.EQ); - indexWriter.startPartition(key, -1, -1); + indexWriter.startPartition(key, position, position); - long position = sstable.getPosition(key, SSTableReader.Operator.EQ); dataFile.seek(position); ByteBufferUtil.readWithShortLength(dataFile); // key @@ -192,8 +203,11 @@ private boolean indexSSTable(SSTableReader sstable, Set in previousBytesRead = bytesRead; } - completeSSTable(indexWriter, sstable, indexes, perSSTableFileLock); + completeSSTable(txn, indexWriter, sstable, indexes, perSSTableFileLock, replacedComponents); } + long timeTaken = Clock.Global.nanoTime() - startTimeNanos; + group.table().metric.updateStorageAttachedIndexBuildTime(timeTaken); + logger.trace("Completed indexing sstable {} in {} seconds", sstable.descriptor, TimeUnit.NANOSECONDS.toSeconds(timeTaken)); return false; } @@ -215,12 +229,12 @@ else if (t instanceof CompactionInterruptedException) //TODO Shouldn't do this if the stop was interrupted by a truncate if (isInitialBuild) { - logger.error(logMessage("Stop requested while building initial indexes {} on SSTable {}."), indexes, sstable.descriptor); + logger.error(logMessage("Stop requested while building initial indexes {} on SSTable {}. {}"), indexes, sstable.descriptor, t.getMessage()); throw Throwables.unchecked(t); } else { - logger.info(logMessage("Stop requested while building indexes {} on SSTable {}."), indexes, sstable.descriptor); + logger.info(logMessage("Stop requested while building indexes {} on SSTable {}. {}"), indexes, sstable.descriptor, t.getMessage()); return true; } } @@ -243,46 +257,64 @@ else if (t instanceof CompactionInterruptedException) } @Override - public CompactionInfo getCompactionInfo() + public OperationProgress getProgress() { - return new CompactionInfo(metadata, - OperationType.INDEX_BUILD, - bytesProcessed, - totalSizeInBytes, - compactionId, - sstables.keySet()); + return new OperationProgress(metadata, + OperationType.INDEX_BUILD, + bytesProcessed, + totalSizeInBytes, + compactionId, + sstables.keySet()); } /** - * if the per sstable index files are already created, no need to write them again, unless found corrupted on rebuild + * if the per sstable index files are already created, not need to write it again, unless it's full rebuild. * if not created, try to acquire a lock, so only one builder will generate per sstable index files */ - private CountDownLatch shouldWritePerSSTableFiles(SSTableReader sstable) + private CountDownLatch shouldWritePerSSTableFiles(SSTableReader sstable, IndexDescriptor indexDescriptor, Set replacedComponents) { - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); - - // if per-table files are incomplete, full rebuild is requested, or checksum fails - if (!indexDescriptor.isPerSSTableIndexBuildComplete() - || isFullRebuild - || !indexDescriptor.validatePerSSTableComponents(IndexValidation.CHECKSUM, true, false)) + // if per-table files are incomplete or checksum failed during full rebuild. + if (!indexDescriptor.perSSTableComponents().isComplete() || isFullRebuild) { CountDownLatch latch = CountDownLatch.newCountDownLatch(1); if (inProgress.putIfAbsent(sstable, latch) == null) { - // lock owner should clean up existing per-SSTable files - group.deletePerSSTableFiles(Collections.singleton(sstable)); + prepareForRebuild(indexDescriptor.perSSTableComponents(), replacedComponents); return latch; } } return null; } - private void completeSSTable(SSTableFlushObserver indexWriter, + private static void prepareForRebuild(IndexComponents.ForRead components, Set replacedComponents) + { + // The current components are "replaced" (by "other" components) if the build create different components than + // the existing ones. This will happen in the following cases: + // 1. if we use immutable components, that's the point of immutable components. + // 2. when we do not use immutable components, the rebuild components will always be for the current version and + // for generation 0, so if the current components are not for that specific built, then we won't be rebuilding + // the exact same components, and we're "replacing", not "overwriting" () + // a) the old components are from an older version: a new build will alawys be for `Version.current()` and + // so will create new files in that case (Note that "normally" we should not have non-0 generation in the + // first place if immutable components are not used, but we handle this case to better support "downgrades" + // where immutable components was enabled, but then disabled for some reason. If that happens, we still + // want to ensure a new build removes the old files both from disk (happens below) and from the sstable TOC + // (which is what `replacedComponents` is about)). + if (components.version().useImmutableComponentFiles() || !components.buildId().equals(ComponentsBuildId.forNewSSTable(components.version()))) + replacedComponents.addAll(components.allAsCustomComponents()); + + if (!components.version().useImmutableComponentFiles()) + components.forWrite().forceDeleteAllComponents(); + } + + private void completeSSTable(LifecycleTransaction txn, + StorageAttachedIndexWriter indexWriter, SSTableReader sstable, Set indexes, - CountDownLatch latch) throws InterruptedException + CountDownLatch latch, + Set replacedComponents) throws InterruptedException, IOException { - indexWriter.complete(); + indexWriter.complete(sstable); if (latch != null) { @@ -308,9 +340,19 @@ private void completeSSTable(SSTableFlushObserver indexWriter, } // register custom index components into existing sstables - sstable.registerComponents(StorageAttachedIndexGroup.getLiveComponents(sstable, existing), tracker); - Set incomplete = group.onSSTableChanged(Collections.emptyList(), Collections.singleton(sstable), existing, IndexValidation.NONE); - + sstable.registerComponents(group.activeComponents(sstable), tracker); + if (!replacedComponents.isEmpty()) + sstable.unregisterComponents(replacedComponents, tracker); + + /** + * During memtable flush, it completes the transaction first which opens the flushed sstable, + * and then notify the new sstable to SAI. Here we should do the same. + */ + txn.trackNewAttachedIndexFiles(sstable); + // there is nothing to commit. Close() effectively abort the transaction. + txn.close(); + + Set incomplete = group.onSSTableChanged(Collections.emptyList(), Collections.singleton(sstable), existing, false); if (!incomplete.isEmpty()) { // If this occurs during an initial index build, there is only one index in play, and @@ -319,7 +361,7 @@ private void completeSSTable(SSTableFlushObserver indexWriter, // set of indexes for a new added/streamed SSTables, we terminate pessimistically. In // other words, we abort the SSTable index write across all column indexes and mark // then non-queryable until a restart or other incremental rebuild occurs. - throw new RuntimeException(logMessage("Failed to update views on column indexes " + incomplete + " on indexes " + indexes + '.')); + throw new RuntimeException(logMessage("Failed to update views on column indexes " + incomplete + " on indexes " + indexes + ".")); } } @@ -342,11 +384,11 @@ private Set validateIndexes(Set inde if (!dropped.isEmpty()) { - String droppedIndexes = dropped.stream().map(sai -> sai.identifier().indexName).collect(Collectors.toList()).toString(); + String droppedIndexes = dropped.stream().map(sai -> sai.getIndexContext().getIndexName()).collect(Collectors.toList()).toString(); if (isFullRebuild) throw new RuntimeException(logMessage(String.format("%s are dropped, will stop index build.", droppedIndexes))); else - logger.debug(logMessage("Skip building dropped index {} on sstable {}"), droppedIndexes, descriptor.baseFile()); + logger.debug(logMessage("Skip building dropped index {} on sstable {}"), droppedIndexes, descriptor.baseFileUri()); } return existing; diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuildingSupport.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuildingSupport.java deleted file mode 100644 index 7a13b4b186ab..000000000000 --- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuildingSupport.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai; - -import java.util.Collection; -import java.util.Comparator; -import java.util.HashSet; -import java.util.NavigableMap; -import java.util.Set; -import java.util.TreeMap; -import java.util.stream.Collectors; - -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.index.Index; -import org.apache.cassandra.index.SecondaryIndexBuilder; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.io.sstable.SSTableIdFactory; -import org.apache.cassandra.io.sstable.format.SSTableReader; - -class StorageAttachedIndexBuildingSupport implements Index.IndexBuildingSupport -{ - @Override - public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, - Set indexes, - Collection sstablesToRebuild, - boolean isFullRebuild) - { - NavigableMap> sstables = new TreeMap<>(Comparator.comparing(s -> s.descriptor.id, SSTableIdFactory.COMPARATOR)); - StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs); - - assert group != null : "Index group does not exist for table " + cfs.keyspace + '.' + cfs.name; - - indexes.stream() - .filter((i) -> i instanceof StorageAttachedIndex) - .forEach((i) -> - { - StorageAttachedIndex sai = (StorageAttachedIndex) i; - - // If this is not a full manual index rebuild we can skip SSTables that already have an - // attached index. Otherwise, we override any pre-existent index. - Collection ss = sstablesToRebuild; - if (!isFullRebuild) - { - ss = sstablesToRebuild.stream() - .filter(s -> !IndexDescriptor.create(s).isPerColumnIndexBuildComplete(sai.identifier())) - .collect(Collectors.toList()); - } - - group.dropIndexSSTables(ss, sai); - - ss.forEach(sstable -> sstables.computeIfAbsent(sstable, ignore -> new HashSet<>()).add(sai)); - }); - - return new StorageAttachedIndexBuilder(group, sstables, isFullRebuild, false); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java index f1b7dd3b54b6..439dbb8315c2 100644 --- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java +++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java @@ -17,12 +17,15 @@ */ package org.apache.cassandra.index.sai; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -30,22 +33,28 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; -import com.google.common.primitives.Ints; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.RangeTombstone; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.WriteContext; +import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.index.Index; -import org.apache.cassandra.index.sai.disk.SSTableIndex; import org.apache.cassandra.index.sai.disk.StorageAttachedIndexWriter; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.metrics.IndexGroupMetrics; @@ -56,17 +65,19 @@ import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableFlushObserver; +import org.apache.cassandra.io.sstable.SSTableWatcher; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.notifications.INotification; import org.apache.cassandra.notifications.INotificationConsumer; import org.apache.cassandra.notifications.MemtableDiscardedNotification; import org.apache.cassandra.notifications.MemtableRenewedNotification; -import org.apache.cassandra.notifications.MemtableSwitchedNotification; import org.apache.cassandra.notifications.SSTableAddedNotification; import org.apache.cassandra.notifications.SSTableListChangedNotification; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.Throwables; +import org.apache.lucene.index.CorruptIndexException; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_TABLE_STATE_METRICS_ENABLED; /** * Orchestrates building of storage-attached indices, and manages lifecycle of resources shared between them. @@ -79,20 +90,28 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons public static final Index.Group.Key GROUP_KEY = new Index.Group.Key(StorageAttachedIndexGroup.class); private final TableQueryMetrics queryMetrics; - private final TableStateMetrics stateMetrics; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private final Optional stateMetrics; private final IndexGroupMetrics groupMetrics; - private final Set indexes = ConcurrentHashMap.newKeySet(); + + private final Set indices = ConcurrentHashMap.newKeySet(); private final ColumnFamilyStore baseCfs; private final SSTableContextManager contextManager; + private final Version version; + + StorageAttachedIndexGroup(ColumnFamilyStore baseCfs) { this.baseCfs = baseCfs; this.queryMetrics = new TableQueryMetrics(baseCfs.metadata()); - this.stateMetrics = new TableStateMetrics(baseCfs.metadata(), this); + this.stateMetrics = SAI_TABLE_STATE_METRICS_ENABLED.getBoolean() + ? Optional.of(new TableStateMetrics(baseCfs.metadata(), this)) + : Optional.empty(); this.groupMetrics = new IndexGroupMetrics(baseCfs.metadata(), this); - this.contextManager = new SSTableContextManager(); + this.contextManager = new SSTableContextManager(baseCfs.getTracker()); + this.version = Version.current(baseCfs.keyspace.getName()); Tracker tracker = baseCfs.getTracker(); tracker.subscribe(this); @@ -101,60 +120,89 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons @Nullable public static StorageAttachedIndexGroup getIndexGroup(ColumnFamilyStore cfs) { - return (StorageAttachedIndexGroup) cfs.indexManager.getIndexGroup(StorageAttachedIndexGroup.GROUP_KEY); + return (StorageAttachedIndexGroup) cfs.indexManager.getIndexGroup(GROUP_KEY); } @Override - public Set getIndexes() + public Set getIndexes() { - return ImmutableSet.copyOf(indexes); + return ImmutableSet.copyOf(indices); } @Override public void addIndex(Index index) { assert index instanceof StorageAttachedIndex; - indexes.add((StorageAttachedIndex) index); + indices.add((StorageAttachedIndex) index); } @Override public void removeIndex(Index index) { assert index instanceof StorageAttachedIndex; - boolean removed = indexes.remove(index); + boolean removed = indices.remove(index); assert removed : "Cannot remove non-existing index " + index; /* * per index files are dropped via {@link StorageAttachedIndex#getInvalidateTask()} */ - if (indexes.isEmpty()) + if (indices.isEmpty()) { - for (SSTableReader sstable : contextManager.sstables()) - sstable.unregisterComponents(IndexDescriptor.create(sstable).getLivePerSSTableComponents(), baseCfs.getTracker()); - deletePerSSTableFiles(baseCfs.getLiveSSTables()); + // We unregister the per-sstable components first, then we clear the context, which closes all the contexts + // and unsure there is not more reference to it. When that's done, we can safely remove the component files + // on disk. Note that we copy the contexts list because we're going to clear the manager, and we need to + // make sure this does not clear the `contexts` collection below (since it exists to be used after the clear). + Collection contexts = new ArrayList<>(contextManager.allContexts()); + contexts.forEach(context -> { + var components = context.usedPerSSTableComponents(); + context.sstable.unregisterComponents(components.allAsCustomComponents(), baseCfs.getTracker()); + }); + + contextManager.clear(); + + contexts.forEach(context -> { + SSTableWatcher.instance.onIndexDropped(baseCfs.metadata(), context.usedPerSSTableComponents().forWrite()); + }); } } @Override public void invalidate() { - // in case of removing last index from group, sstable contexts should already been removed by removeIndex + // in case of dropping table, sstable contexts should already been removed by SSTableListChangedNotification. + // in case of removing last index from group, sstable contexts should already been removed by StorageAttachedIndexGroup#removeIndex queryMetrics.release(); groupMetrics.release(); - stateMetrics.release(); + stateMetrics.ifPresent(TableStateMetrics::release); baseCfs.getTracker().unsubscribe(this); } @Override - @SuppressWarnings("SuspiciousMethodCalls") - public boolean containsIndex(Index index) + public void unload() + { + baseCfs.getTracker().unsubscribe(this); + + contextManager.clear(); + queryMetrics.release(); + groupMetrics.release(); + stateMetrics.ifPresent(TableStateMetrics::release); + } + + @Override + public boolean supportsMultipleContains() { - return indexes.contains(index); + return true; } @Override - public boolean isSingleton() + public boolean supportsDisjunction() { - return false; + return true; + } + + @Override + public boolean containsIndex(Index index) + { + return index instanceof StorageAttachedIndex && indices.contains(index); } @Override @@ -167,29 +215,46 @@ public Index.Indexer indexerFor(Predicate indexSelector, Memtable memtable) { final Set indexers = - indexes.stream().filter(indexSelector) - .map(i -> i.indexerFor(key, columns, nowInSec, ctx, transactionType, memtable)) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); + indices.stream().filter(indexSelector) + .map(i -> i.indexerFor(key, columns, nowInSec, ctx, transactionType, memtable)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); - return indexers.isEmpty() ? null : new Index.Indexer() + return indexers.isEmpty() ? null : new StorageAttachedIndex.IndexerAdapter() { @Override public void insertRow(Row row) { - // SAI does not index deletions, as these are resolved during post-filtering. - if (row.hasLiveData(nowInSec, false)) - for (Index.Indexer indexer : indexers) - indexer.insertRow(row); + forEach(indexer -> indexer.insertRow(row)); } @Override public void updateRow(Row oldRow, Row newRow) { - // SAI does not index deletions, as these are resolved during post-filtering. - if (newRow.hasLiveData(nowInSec, false)) - for (Index.Indexer indexer : indexers) - indexer.updateRow(oldRow, newRow); + forEach(indexer -> indexer.updateRow(oldRow, newRow)); + } + + @Override + public void removeRow(Row row) + { + forEach(indexer -> indexer.removeRow(row)); + } + + @Override + public void partitionDelete(DeletionTime deletionTime) + { + forEach(indexer -> indexer.partitionDelete(deletionTime)); + } + + @Override + public void rangeTombstone(RangeTombstone tombstone) + { + forEach(indexer -> indexer.rangeTombstone(tombstone)); + } + + private void forEach(Consumer action) + { + indexers.forEach(action::accept); } }; } @@ -197,23 +262,23 @@ public void updateRow(Row oldRow, Row newRow) @Override public StorageAttachedIndexQueryPlan queryPlanFor(RowFilter rowFilter) { - return StorageAttachedIndexQueryPlan.create(baseCfs, queryMetrics, indexes, rowFilter); + return StorageAttachedIndexQueryPlan.create(baseCfs, queryMetrics, indices, rowFilter); } @Override - public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata) + public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata, long keyCount) { - IndexDescriptor indexDescriptor = IndexDescriptor.create(descriptor, tableMetadata.partitioner, tableMetadata.comparator); + IndexDescriptor indexDescriptor = IndexDescriptor.empty(descriptor); try { - return StorageAttachedIndexWriter.createFlushObserverWriter(indexDescriptor, indexes, tracker); + return new StorageAttachedIndexWriter(indexDescriptor, tableMetadata, indices, tracker, keyCount, baseCfs.metric); } catch (Throwable t) { String message = "Unable to create storage-attached index writer on SSTable flush." + " All indexes from this table are going to be marked as non-queryable and will need to be rebuilt."; logger.error(indexDescriptor.logMessage(message), t); - indexes.forEach(StorageAttachedIndex::makeIndexNonQueryable); + indices.forEach(StorageAttachedIndex::makeIndexNonQueryable); return null; } } @@ -226,30 +291,31 @@ public boolean handles(IndexTransaction.Type type) } @Override - public Set getComponents() + public Set componentsForNewSSTable() { - return getComponents(indexes); + return IndexDescriptor.componentsForNewlyFlushedSSTable(indices, version); } - private Set getComponents(Collection indices) + @Override + public Set activeComponents(SSTableReader sstable) { - Set components = Version.LATEST.onDiskFormat() - .perSSTableIndexComponents(baseCfs.metadata.get().comparator.size() > 0) - .stream() - .map(Version.LATEST::makePerSSTableComponent) - .collect(Collectors.toSet()); - indices.forEach(index -> components.addAll(index.getComponents())); - return components; - } + IndexDescriptor indexDescriptor = descriptorFor(sstable); + Set components = indexDescriptor + .perSSTableComponents() + .all() + .stream() + .map(IndexComponent::asCustomComponent) + .collect(Collectors.toSet()); + + for (StorageAttachedIndex index : indices) + { + indexDescriptor.perIndexComponents(index.getIndexContext()) + .all() + .stream() + .map(IndexComponent::asCustomComponent) + .forEach(components::add); + } - // This differs from getComponents in that it only returns index components that exist on disk. - // It avoids errors being logged by the SSTable.readTOC method when we have an empty index. - @VisibleForTesting - public static Set getLiveComponents(SSTableReader sstable, Collection indices) - { - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); - Set components = indexDescriptor.getLivePerSSTableComponents(); - indices.forEach(index -> components.addAll(indexDescriptor.getLivePerIndexComponents(index.termType(), index.identifier()))); return components; } @@ -261,42 +327,35 @@ public void handleNotification(INotification notification, Object sender) { SSTableAddedNotification notice = (SSTableAddedNotification) notification; - // Avoid validation for index files just written following Memtable flush. Otherwise, the new SSTables have - // come either from import, streaming, or a standalone tool, where they have also already been validated. - onSSTableChanged(Collections.emptySet(), notice.added, indexes, IndexValidation.NONE); + // Avoid validation for index files just written following Memtable flush. ZCS streaming should + // validate index checksum. Also avoid validation for UNKNOWN operations (imports) as they + // are already validated in SSTableImporter. + boolean validate = notice.fromStreaming() || + (!notice.memtable().isPresent() && notice.operationType != OperationType.UNKNOWN); + onSSTableChanged(Collections.emptySet(), Lists.newArrayList(notice.added), indices, validate); } else if (notification instanceof SSTableListChangedNotification) { SSTableListChangedNotification notice = (SSTableListChangedNotification) notification; // Avoid validation for index files just written during compaction. - onSSTableChanged(notice.removed, notice.added, indexes, IndexValidation.NONE); + onSSTableChanged(notice.removed, notice.added, indices, false); } else if (notification instanceof MemtableRenewedNotification) { - indexes.forEach(index -> index.memtableIndexManager().renewMemtable(((MemtableRenewedNotification) notification).renewed)); - } - else if (notification instanceof MemtableSwitchedNotification) - { - indexes.forEach(index -> index.memtableIndexManager().maybeInitializeMemtableIndex(((MemtableSwitchedNotification) notification).next)); + indices.forEach(index -> index.getIndexContext().renewMemtable(((MemtableRenewedNotification) notification).renewed)); } else if (notification instanceof MemtableDiscardedNotification) { - indexes.forEach(index -> index.memtableIndexManager().discardMemtable(((MemtableDiscardedNotification) notification).memtable)); + indices.forEach(index -> index.getIndexContext().discardMemtable(((MemtableDiscardedNotification) notification).memtable)); } } - void deletePerSSTableFiles(Collection sstables) - { - contextManager.release(sstables); - sstables.forEach(sstableReader -> IndexDescriptor.create(sstableReader).deletePerSSTableIndexComponents()); - } - - void dropIndexSSTables(Collection ss, StorageAttachedIndex index) + void prepareIndexSSTablesForRebuild(Collection ss, StorageAttachedIndex index) { try { - index.drop(ss); + index.getIndexContext().prepareSSTablesForRebuild(ss); } catch (Throwable t) { @@ -313,23 +372,14 @@ void dropIndexSSTables(Collection ss, StorageAttachedIndex index) * @return the set of column indexes that were marked as non-queryable as a result of their per-SSTable index * files being corrupt or being unable to successfully update their views */ - synchronized Set onSSTableChanged(Collection removed, Iterable added, - Set indexes, IndexValidation validation) + public synchronized Set onSSTableChanged(Collection removed, Iterable added, + Set indexes, boolean validate) { - Pair, Set> results = contextManager.update(removed, added, validation); - - if (!results.right.isEmpty()) + Optional> optValid = contextManager.update(removed, added, validate, indices); + if (optValid.isEmpty()) { - results.right.forEach(sstable -> { - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); - indexDescriptor.deletePerSSTableIndexComponents(); - // Column indexes are invalid if their SSTable-level components are corrupted so delete - // their associated index files and mark them non-queryable. - indexes.forEach(index -> { - indexDescriptor.deleteColumnIndex(index.termType(), index.identifier()); - index.makeIndexNonQueryable(); - }); - }); + // This means at least one sstable had invalid per-sstable components, so mark all indexes non-queryable. + indices.forEach(StorageAttachedIndex::makeIndexNonQueryable); return indexes; } @@ -337,13 +387,11 @@ synchronized Set onSSTableChanged(Collection invalid = index.onSSTableChanged(removed, results.left, validation); + Set invalid = index.getIndexContext().onSSTableChanged(removed, optValid.get(), validate); if (!invalid.isEmpty()) { - // Delete the index files and mark the index non-queryable, as its view may be compromised, - // and incomplete, for our callers: - invalid.forEach(context -> context.indexDescriptor.deleteColumnIndex(index.termType(), index.identifier())); + // Mark the index non-queryable, as its view may be compromised, and incomplete, for our callers. index.makeIndexNonQueryable(); incomplete.add(index); } @@ -351,6 +399,20 @@ synchronized Set onSSTableChanged(Collection sstables, boolean throwOnIncomplete, boolean validateChecksum) { @@ -358,25 +420,38 @@ public boolean validateSSTableAttachedIndexes(Collection sstables for (SSTableReader sstable : sstables) { - IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable); + logger.debug("Validating {} SAI indices for {}", indices.size(), sstable.getFilename()); + if (indices.isEmpty()) + { + logger.debug("No SAI indices to validate for {}", sstable.getFilename()); + return true; + } + + // For validation, we need to load a fresh descriptor to ensure we see the current state of files + IndexDescriptor indexDescriptor = IndexDescriptor.load(sstable, contexts()); + IndexComponents.ForRead perSSTableComponents = indexDescriptor.perSSTableComponents(); - if (indexDescriptor.isPerSSTableIndexBuildComplete()) + logger.debug("Per-SSTable components complete: {}", indexDescriptor.perSSTableComponents().isComplete()); + if (indexDescriptor.perSSTableComponents().isComplete()) { - indexDescriptor.validatePerSSTableComponents(IndexValidation.CHECKSUM, validateChecksum, true); + perSSTableComponents.validateComponents(sstable, baseCfs.getTracker(), validateChecksum, true); - for (StorageAttachedIndex index : indexes) + for (StorageAttachedIndex index : indices) { - if (indexDescriptor.isPerColumnIndexBuildComplete(index.identifier())) - indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, validateChecksum, true); + IndexComponents.ForRead perIndexComponents = indexDescriptor.perIndexComponents(index.getIndexContext()); + + logger.debug("Per-index components complete for {}: {}", index.getIndexContext().getIndexName(), perIndexComponents.isComplete()); + if (perIndexComponents.isComplete()) + perIndexComponents.validateComponents(sstable, baseCfs.getTracker(), validateChecksum, true); else if (throwOnIncomplete) - throw new IllegalStateException(indexDescriptor.logMessage("Incomplete per-column index build for SSTable " + sstable.descriptor.toString())); + throw new IllegalStateException(indexDescriptor.logMessage("Incomplete per-column index build for SSTable " + sstable.descriptor)); else complete = false; } } else if (throwOnIncomplete) { - throw new IllegalStateException(indexDescriptor.logMessage("Incomplete per-SSTable index build" + sstable.descriptor.toString())); + throw new IllegalStateException(indexDescriptor.logMessage("Incomplete per-SSTable index build" + sstable.descriptor)); } else { @@ -387,17 +462,6 @@ else if (throwOnIncomplete) return complete; } - @Override - public boolean supportsL0Shards() - { - for (StorageAttachedIndex index : indexes) - if (!index.supportsL0Shards()) - return false; - - // All indexes must support L0 sharding for the flush to shard at L0 - return true; - } - /** * open index files by checking number of {@link SSTableContext} and {@link SSTableIndex}, * so transient open files during validation and files that are still open for in-flight requests will not be tracked. @@ -406,11 +470,11 @@ public boolean supportsL0Shards() */ public int openIndexFiles() { - return contextManager.openFiles() + indexes.stream().mapToInt(StorageAttachedIndex::openPerColumnIndexFiles).sum(); + return contextManager.openFiles() + indices.stream().mapToInt(index -> index.getIndexContext().openPerIndexFiles()).sum(); } /** - * @return total disk usage (in bytes) of all per-sstable index files + * @return total disk usage of all per-sstable index files */ public long diskUsage() { @@ -422,7 +486,7 @@ public long diskUsage() */ public int totalIndexBuildsInProgress() { - return (int) indexes.stream().filter(i -> baseCfs.indexManager.isIndexBuilding(i.getIndexMetadata().name)).count(); + return (int) indices.stream().filter(i -> baseCfs.indexManager.isIndexBuilding(i.getIndexMetadata().name)).count(); } /** @@ -430,7 +494,7 @@ public int totalIndexBuildsInProgress() */ public int totalQueryableIndexCount() { - return Ints.checkedCast(indexes.stream().filter(baseCfs.indexManager::isIndexQueryable).count()); + return (int) indices.stream().filter(i -> baseCfs.indexManager.isIndexQueryable(i)).count(); } /** @@ -438,7 +502,7 @@ public int totalQueryableIndexCount() */ public int totalIndexCount() { - return indexes.size(); + return indices.size(); } /** @@ -446,7 +510,13 @@ public int totalIndexCount() */ public long totalDiskUsage() { - return diskUsage() + indexes.stream().flatMap(index -> index.view().getIndexes().stream()) + // Note that this only account the "active" files. That is, if we have old versions/generations or incomplete + // build still on disk, those won't be counted. Counting only "live" data here is consistent with the fact + // that `TableStateMetrics.diskUsagePercentageOfBaseTable` compare the number obtain from this to the base + // table "live" disk space use. But there is certainly a small risk for being misleading, and where base + // tables expose both a "liveDiskSpaceUsed" and "totalDiskSpaceUsed", SAI only exposes "diskUsageBytes", which + // has we just mentioned is the "live" usage. Might be worth improving at some point. + return diskUsage() + indices.stream().flatMap(i -> i.getIndexContext().getView().getIndexes().stream()) .mapToLong(SSTableIndex::sizeOfPerColumnComponents).sum(); } @@ -455,6 +525,18 @@ public TableMetadata metadata() return baseCfs.metadata(); } + // Needed by CNDB + public TableQueryMetrics queryMetrics() + { + return queryMetrics; + } + + // Needed by CNDB + public Optional stateMetrics() + { + return stateMetrics; + } + public ColumnFamilyStore table() { return baseCfs; @@ -466,6 +548,17 @@ public SSTableContextManager sstableContextManager() return contextManager; } + /** + * Returns the {@link IndexDescriptor} for the given {@link SSTableReader} (which must belong to the base table + * of this group). + * Note that this always return a non-null value, since all sstables must be indexed, but that descriptor could + * be "empty" if the sstable has never had an index built yet. + */ + public IndexDescriptor descriptorFor(SSTableReader sstable) + { + return contextManager.getOrLoadIndexDescriptor(sstable, indices); + } + /** * simulate index loading on restart with index file validation */ @@ -473,8 +566,8 @@ public SSTableContextManager sstableContextManager() public void unsafeReload() { contextManager.clear(); - onSSTableChanged(baseCfs.getLiveSSTables(), Collections.emptySet(), indexes, IndexValidation.NONE); - onSSTableChanged(Collections.emptySet(), baseCfs.getLiveSSTables(), indexes, IndexValidation.HEADER_FOOTER); + onSSTableChanged(baseCfs.getLiveSSTables(), Collections.emptySet(), indices, false); + onSSTableChanged(Collections.emptySet(), baseCfs.getLiveSSTables(), indices, true); } /** @@ -484,7 +577,36 @@ public void unsafeReload() public void reset() { contextManager.clear(); - indexes.forEach(StorageAttachedIndex::makeIndexNonQueryable); - onSSTableChanged(baseCfs.getLiveSSTables(), Collections.emptySet(), indexes, IndexValidation.NONE); + indices.forEach(index -> index.makeIndexNonQueryable()); + onSSTableChanged(baseCfs.getLiveSSTables(), Collections.emptySet(), indices, false); + } + + private Set contexts() + { + Set contexts = Sets.newHashSetWithExpectedSize(indices.size()); + for (StorageAttachedIndex index : indices) + contexts.add(index.getIndexContext()); + return contexts; + } + + /** + * @return the minimum index version among all the per-sstable index components of this group, + * or the current version if there are no sstable indexes. + */ + public Version getMinVersion() + { + StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs); + assert indexGroup != null; + Version minVersion = null; + for (SSTableReader sstable : baseCfs.getLiveSSTables()) + { + IndexDescriptor indexDescriptor = indexGroup.descriptorFor(sstable); + assert indexDescriptor != null; + + Version version = indexDescriptor.perSSTableComponents().version(); + if (minVersion == null || version.compareTo(minVersion) < 0) + minVersion = version; + } + return minVersion == null ? Version.current(baseCfs.metadata.keyspace) : minVersion; } } diff --git a/src/java/org/apache/cassandra/index/sai/VECTOR.md b/src/java/org/apache/cassandra/index/sai/VECTOR.md new file mode 100644 index 000000000000..0bc7a85fa06f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/VECTOR.md @@ -0,0 +1,371 @@ + + +# SAI Vector Graph Construction Configuration Options + +## Basic Configuration + +The following options can be specified when creating a SAI vector index: + +| Option | Default | Valid Range | Description | +|--------------------------|----------------------------------------------------------------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| maximum_node_connections | 16 | 1-512 | Controls the maximum number of connections per node in the graph. The actual graph degree will be 2x this value. Higher values increase graph quality but also increase storage and query costs. | +| construction_beam_width | 100 | 1-3200 | Controls how many candidates to evaluate during graph construction. Higher values increase graph quality but also increase build time. | +| neighborhood_overflow | 1.0 in memtable, 1.2 in compaction | > 0 | Controls graph pruning during construction. Higher values result in denser graphs. | +| alpha | dimesion > 3 gets 1.2. Otherwise, 2.0 in memtable, 1.4 in compaction | > 0 | Controls how aggressively to explore the graph during search. Higher values increase recall at the cost of latency. | +| enable_hierarchy | false | true/false | When true, enables hierarchical graph construction. | +| source_model | `OTHER` | enum (see [below](#vector-source-models)) | Preset configurations optimized for specific vector embedding models. | +| similarity_function | (from `source_model`) | `COSINE`, `DOT_PRODUCT`, `EUCLIDEAN` | Defines how vector similarity is computed. | + +### Example Usage + +Basic index creation with defaults: +```cql +CREATE CUSTOM INDEX ON mytable (vec_col) +USING 'StorageAttachedIndex'; +``` + +Setting some options: +```cql +CREATE CUSTOM INDEX ON mytable (vec_col) +USING 'StorageAttachedIndex' +WITH OPTIONS = { + 'maximum_node_connections': '32', + 'construction_beam_width': '200' +}; +``` + +Setting all options: +```cql +CREATE CUSTOM INDEX ON mytable (vec_col) +USING 'StorageAttachedIndex' +WITH OPTIONS = { + 'maximum_node_connections': '32', + 'construction_beam_width': '200', + 'neighborhood_overflow': '1.2', + 'alpha': '1.1', + 'enable_hierarchy': 'true', + 'similarity_function': 'COSINE', + 'source_model': 'ADA002' +}; +``` + +## Vector Source Models + +The source_model option provides preset configurations optimized for specific vector embedding models. Each model affects: +- Default similarity function +- Compression settings +- Overquery behavior during search + +Available models: + +| Model | Similarity | Compression | Overquery Factor | Notes | +|-----------------|-------------|----------------|------------------|------------------------------------------| +| ADA002 | DOT_PRODUCT | PQ (0.125) | 1.25x | Optimized for OpenAI Ada-002 embeddings | +| OPENAI_V3_SMALL | DOT_PRODUCT | PQ (0.0625) | 1.5x | For text-embedding-3-small | +| OPENAI_V3_LARGE | DOT_PRODUCT | PQ (0.0625) | 1.25x | For text-embedding-3-large | +| BERT | COSINE | PQ (0.25) | 1.0x | For BERT-style embeddings | +| GECKO | DOT_PRODUCT | PQ (0.125) | 1.25x | For Cohere Gecko embeddings | +| NV_QA_4 | DOT_PRODUCT | PQ (0.125) | 1.25x | For NVIDIA NeMo embeddings | +| COHERE_V3 | DOT_PRODUCT | PQ (0.0625) | 1.25x | For Cohere V3 embeddings | +| OTHER | COSINE | Auto-scaled PQ | Dynamic | Generic configuration for unknown models | + +The `OTHER` model uses dimension-based heuristics to automatically determine appropriate compression settings and adjusts overquery based on compression ratio: +- High compression (>16x): 1.5x overquery +- Standard compression: 1.0x overquery + +# SAI Vector ANN Query Options + +## Query-Time Configuration + +Vector similarity searches can be fine-tuned at query time using the `WITH ANN_OPTIONS` clause. These options allow you to balance between search accuracy (recall) and performance (latency). + +| Option | Default | Valid Range | Description | +|-------------|-------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| rerank_k | value computed based on `LIMIT`, configured [source_model](#vector-source-models), and number of vector graphs being searched | ≤ 0 or > LIMIT (up to guardrail) | The number of candidates to collect before reranking. Values ≤ 0 disable reranking. Higher values increase recall at the cost of latency. Subject to guardrail `sai_ann_rerank_k_max_value`. | +| use_pruning | true | true/false | When enabled, allows the search to skip parts of the graph that are unlikely to contain good matches. Can improve latency by possibly reducing recall. | + +### Example Usage + +> **IMPORTANT**: ANN_OPTIONS comes after LIMIT in the query text. +The following examples show the correct ordering. + +Basic vector search with default options: +```sql +SELECT * FROM mytable +ORDER BY vec ANN OF [1.0, 2.0, 3.0] +LIMIT 10; +``` + +Setting rerank_k to improve recall: +```sql +SELECT * FROM mytable +ORDER BY vec ANN OF [1.0, 2.0, 3.0] +LIMIT 10 +WITH ANN_OPTIONS = {'rerank_k': 100}; +``` + +Setting rerank_k to disable reranking: +```sql +SELECT * FROM mytable +ORDER BY vec ANN OF [1.0, 2.0, 3.0] +LIMIT 10 +WITH ANN_OPTIONS = {'rerank_k': 0}; +``` + + +Enabling pruning to improve latency: +```sql +SELECT * FROM mytable +ORDER BY vec ANN OF [1.0, 2.0, 3.0] +LIMIT 10 +WITH ANN_OPTIONS = {'use_pruning': true}; +``` + +Setting all options: +```sql +SELECT * FROM mytable +ORDER BY vec ANN OF [1.0, 2.0, 3.0] +LIMIT 10 +WITH ANN_OPTIONS = { + 'rerank_k': 100, + 'use_pruning': true +}; +``` + +## Performance Considerations + +### Reranking (rerank_k) + +The `rerank_k` parameter controls the trade-off between search accuracy and latency: + +- Higher values (e.g., 2-4x LIMIT): + - Increased recall (more accurate results) + - Higher latency due to deeper graph exploration + - Increased memory usage during search + - More I/O operations + +- Lower values (closer to LIMIT): + - Faster searches + - Lower memory usage + - Potentially lower recall + - Fewer I/O operations + +- Rerankless (rerank_k <= 0): + - Fastest searches by only computing similarity scores of quantized vectors + - Lowest recall + - Minimal I/O operations + +The optimal value depends on your use case: +- For high-precision requirements: Use larger rerank_k (3-4x LIMIT) +- For latency-sensitive applications: Use smaller rerank_k (1.5-2x LIMIT) +- For balanced performance: Start with 2x LIMIT and adjust based on requirements + +As always, test your specific use case to find the best balance, and consider tuning the index graph consturction configuration as described above in [Vector Graph Configuration Options](#sai-vector-graph-construction-configuration-options). + +### Pruning (use_pruning) + +Graph pruning trades recall for latency to improve search performance by skipping unlikely paths: + +- Enabled (true): + - Reduced latency + - Lower resource usage + - Slight decrease in recall + - Better performance on large datasets + +- Disabled (false): + - Maximum recall + - Higher latency + - More resource intensive + - More thorough graph exploration + +## System Guardrails + +To prevent resource exhaustion, the following guardrails are in place: + +- Maximum rerank_k value: Controlled by `sai_ann_rerank_k_max_value` guardrail +- Validation: rerank_k must be greater than the query LIMIT + +# SAI Vector ANN Query Execution + +## Overview + +Vector search within SAI has taken two major forms to date. The first utilized PrimaryKey ordered iterators and was +very sensitive to Shadowed Primary Keys as well as overwritten vectors for rows. The second utilized Score ordered +iterators, which was able to handle these cases more gracefully. + +This document describes vector search using Score ordered iterators. + +## Storage-Attached Index Basics + +* We can create indexes on columns to support searching them without requiring `ALLOW FILTERING` and without requiring +that they are part of the primary key +* An indexed column index consists of local indexes for each memtable and each sstable segment within the table +* Query execution scatters across each index to get the collection of Primary Keys that satisfy a predicate +* Each sstable segment's index is immutable +* Memtable indexes are mutable and are updated as the memtable is updated + +## Vector Index Basics + +* A vector index gives us the ability to search for similar vectors +* We take advantage of the fact that each sstable segment is immutable and finite +* If we take the top k vectors from each sstable segment, we can materialize them from storage and get the top k vectors + from the entire table (more on this later) +* The `K` in `topK` is generally the `LIMIT` of the query, but can be larger (more on this later) + +## Query Types + +### Vector Only Query + +When a query is only limited by ANN, the query execution is scatter gather across all relevant vector indexes. The query +results in a lazily evaluated iterator that materializes rows from storage in index score order, which can differ from +the "global" score order in the case of updates. +1. Eagerly query each sstable's and memtable's vector indexes producing local top k ordinals. Return them in best-first score order. +2. Lazily map ordinals to row ids then to Primary Keys keeping them in descending (best-first) score order. +3. Merge the iterators while maintaining relative score order. This merge does not dedupe iterator elements. +4. Materialize one row from storage at a time. +5. Filter out deleted rows. Then, compute the vector similarity score. If the score is at least as good as the score computed by the index, the vector + is in the global top k. If it is worse than the index's score, temporarily ignore that key. Finally, reorder into + Primary Key order. +6. Return the global top k rows to the coordinator. + +```mermaid +--- +title: "SELECT * FROM my.table ORDER BY vec ANN OF [...] LIMIT N" +--- +graph LR + subgraph 1: Get topK + G[SSTable A\nVector Index] + H[SSTable B\nVector Index] + I[Memtable\nVector Index] + end + subgraph "2: Map" + X[Ordinal -> Row ID -> Scored PK] + Y[Ordinal -> Row ID -> Scored PK] + Z[Ordinal -> Scored PK] + end + subgraph 3: Merge + J + end + G -.-> X -.-> J[Merge\nIndex\nIterators] + H -.-> Y -.-> J + I -.-> Z -.-> J + subgraph "4: Materialize" + K[Unfiltered\nPartition\nIterator] + end + subgraph "5: Filter, Score, Reorder" + L[Global top k] + end + J -.-> K + K -.-> L + L ==> M[Coordinator] + + subgraph Legend + direction LR + start1[ ] -.->|Score Order Iterator| stop1[ ] + style start1 height:0px; + style stop1 height:0px; + start2[ ] ==>|PrimaryKey Order Iterator| stop2[ ] + style start2 height:0px; + style stop2 height:0px; + end +``` + +Notes: +* The flow is much lazier than before. Now, we only materialize the top k rows from storage, not every top k row from + every sstable segment and memtable. +* Range queries on the Primary Key that do not require an index are supported and are considered ANN only. +* `ALLOW FILTERING` is not supported. + +### Pre-fitered Boolean Predicates Combined with ANN Query + +When a query relies on non vector SAI indexes and an ANN ordering predicate, the query execution is more complex. The execution +of query `SELECT * FROM my.table WHERE x = 1 AND y = 'foo' ORDER BY vec ANN OF [...] LIMIT 10` follows this path: +1. Query each boolean predicate's index to get the Primary Keys that satisfy the predicate. +2. Merge the results with a `RangeUnionIterator` that deduplicates results for the predicate and maintains PK ordering. +3. Intersect the results with a `RangeIntersectionIterator` to get the Primary Keys that satisfy all boolean predicates. +4. Materialize the Primary Keys that satisfy all boolean predicates. +5. Map resulting Primary Keys back to row ids and search each vector index for the local top k ordinals, then map those to +Primary Keys. Ultimately producing a single score ordered iterator. **This is expensive.** +6. Materialize one row from storage at a time. +7. Filter out deleted rows and validate the row against the logical filter. If the row does not match the WHERE clause, ignore the result. Then, + compute the vector similarity score. If the score is at least as good as the score computed by the index, the vector + is in the global top k. If it is worse than the index's score, temporarily ignore that key. Finally, reorder into + Primary Key order. +8. Return the global top k rows to the coordinator. + +```mermaid +--- +title: "SELECT * FROM my.table WHERE A = 1 AND B = 'foo' ORDER BY vec ANN OF [...] LIMIT 10" +--- +graph LR + subgraph Step 1 and 2: Query Boolean Predicates + subgraph Indexes on Column A + A[SSTable 1\nIndex] --A=1--> B[Range\nUnion\nIterator] + C[SSTable 2\nIndex] --A=1--> B + D[Memtable\nIndex] --A=1--> B + end + subgraph Indexes on Column B + M[SSTable 1\nIndex] --B='foo'--> N[Range\nUnion\nIterator] + P[SSTable 2\nIndex] --B='foo'--> N + O[Memtable\nIndex] --B='foo'--> N + end + end + subgraph Step 3: Find PKs\nMatching Both\nPredicates + N --> E[Range\nIntersection\nIterator] + B --> E + end + E --> F[Materialize\nALL\nPrimary Keys] + subgraph "Steps 4 & 5: Index on Column vec" + F --> G1[PK -> SSTable 1\nRowIds] --> G[SSTable 1\nVector Index] .-> X[Ordinal -> PK] + F --> H1[PK -> SSTable 2\nRowIds] --> H[SSTable 2\nVector Index] .-> Y[Ordinal -> PK] + F --> I[Memtable\nVector Index] + X -.-> J[Merge\nScored PKs\nPriority Queue] + Y -.-> J + I -..-> J + end + subgraph "Step 6: Materialize" + K[Unfiltered\nPartition\nIterator] + end + subgraph "Step 7: Filter, Score, Reorder" + L[Global top k] + end + J -.-> K[Unfiltered\nPartition\nIterator] + K -.-> L[Global top k] + L --> Z[Coordinator] + + subgraph Legend + direction LR + start1[ ] -.->|Score Order Iterator| stop1[ ] + style start1 height:0px; + style stop1 height:0px; + start2[ ] -->|PrimaryKey Order Iterator| stop2[ ] + style start2 height:0px; + style stop2 height:0px; + end +``` + +### Post-fitered Boolean Predicates Combined with ANN Query + +Sometimes, the boolean predicates are expensive to evaluate using the pre-filtered approach described above. An +alternate query execution path is to sort the results using ANN first, then filter the materialized rows using the +boolean predicates. The execution of query `SELECT * FROM my.table WHERE x = 1 AND y = 'foo' ORDER BY vec ANN OF [...] LIMIT 10` +using a post-filtered approach follows the same path as the [Vector Only Query](#vector-only-query) with the exception +that the "filter" in step 7 additionally applies the boolean predicates and filters out any rows that do not match. + +The primary cost of post-filtering is that we might materialize many rows before finding the ones that match the boolean +predicates. As such, we have a cost based optimizer that helps determine which approach is best for a given query. \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java b/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java index f9878c2f3ff4..35c0e9ce8552 100644 --- a/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java @@ -1,3 +1,9 @@ +/* + * All changes to the original code are Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,22 +24,38 @@ package org.apache.cassandra.index.sai.analyzer; +import java.io.Closeable; import java.nio.ByteBuffer; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.stream.Collectors; +import java.util.Set; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; + +import org.slf4j.Logger; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.Pair; +import org.apache.lucene.analysis.Analyzer; public abstract class AbstractAnalyzer implements Iterator { + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(AbstractAnalyzer.class); + + public static final Set> ANALYZABLE_TYPES = ImmutableSet.of(UTF8Type.instance, AsciiType.instance); + protected ByteBuffer next = null; - protected String nextLiteral = null; + String nextLiteral = null; /** - * @return true if index value is transformed, e.g. normalized or lower-cased or tokenized. + * @return true if index value is transformed, eg. normalized or lower-cased or tokenized. */ public abstract boolean transformValue(); @@ -73,44 +95,155 @@ public void reset(ByteBuffer input) resetInternal(input); } - public interface AnalyzerFactory + public static boolean hasQueryAnalyzer(Map options) + { + return options.containsKey(LuceneAnalyzer.QUERY_ANALYZER); + } + + public interface AnalyzerFactory extends Closeable { AbstractAnalyzer create(); + /** + * @return true if the analyzer supports EQ queries (see {@link AnalyzerEqOperatorSupport}) + */ + default boolean supportsEquals() + { + return true; + } + + /** + * @return {@link true} if this analyzer configuration has a n-gram tokenizer or any of its filters is n-gram. + */ + default boolean isNGram() + { + return false; + } + + @Override default void close() { } } - public static AnalyzerFactory fromOptions(IndexTermType indexTermType, Map options) + public static AnalyzerFactory fromOptionsQueryAnalyzer(final AbstractType type, final Map options) + { + final String json = options.get(LuceneAnalyzer.QUERY_ANALYZER); + return toAnalyzerFactory(json, type, options); + } + + public static AnalyzerFactory toAnalyzerFactory(String json, final AbstractType type, final Map options) //throws Exception { - if (hasNonTokenizingOptions(options)) + if (!TypeUtil.isIn(type, ANALYZABLE_TYPES)) { - if (indexTermType.isString()) - { - // validate options - NonTokenizingOptions.fromMap(options); - return () -> new NonTokenizingAnalyzer(indexTermType, options); - } - else - { - throw new InvalidRequestException("CQL type " + indexTermType.asCQL3Type() + " cannot be analyzed."); - } + logger.warn("CQL type {} cannot be analyzed options={}; using NoOpAnalyzer", type.asCQL3Type(), options); + return NoOpAnalyzer::new; } - return null; + try + { + Pair analyzerAndConfig = JSONAnalyzerParser.parse(json); + final Analyzer analyzer = analyzerAndConfig.left; + final boolean isNGram = analyzerAndConfig.right != null && analyzerAndConfig.right.isNGram(); + + return new AnalyzerFactory() + { + @Override + public void close() + { + analyzer.close(); + } + + @Override + public AbstractAnalyzer create() + { + return new LuceneAnalyzer(type, analyzer, options); + } + + @Override + public boolean isNGram() + { + return isNGram; + } + + @Override + public boolean supportsEquals() + { + return AnalyzerEqOperatorSupport.supportsEqualsFromOptions(options); + } + }; + } + catch (InvalidRequestException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new InvalidRequestException("CQL type " + type.asCQL3Type() + " cannot be analyzed options="+options, ex); + } } - private static boolean hasNonTokenizingOptions(Map options) - { - return options.keySet().stream().anyMatch(NonTokenizingOptions::hasOption); + public static boolean isAnalyzed(Map options) { + return options.containsKey(LuceneAnalyzer.INDEX_ANALYZER) || NonTokenizingOptions.hasNonDefaultOptions(options); } - public static Map getAnalyzerOptions(Map options) + public static AnalyzerFactory fromOptions(String target, AbstractType type, Map options) { - return options.entrySet().stream() - .filter(e -> NonTokenizingOptions.hasOption(e.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } + boolean containsIndexAnalyzer = options.containsKey(LuceneAnalyzer.INDEX_ANALYZER); + boolean containsNonTokenizingOptions = NonTokenizingOptions.hasNonDefaultOptions(options); + boolean supportsEquals = AnalyzerEqOperatorSupport.supportsEqualsFromOptions(options); + if (containsIndexAnalyzer && containsNonTokenizingOptions) + { + logger.warn("Invalid combination of options for index_analyzer: {}", options); + List optionsToStrip = List.of(NonTokenizingOptions.CASE_SENSITIVE, NonTokenizingOptions.NORMALIZE, NonTokenizingOptions.ASCII); + options = Maps.filterKeys(options, k -> !optionsToStrip.contains(k)); + logger.warn("Rewrote options to {}", options); + } + boolean containsQueryAnalyzer = options.containsKey(LuceneAnalyzer.QUERY_ANALYZER); + if (containsQueryAnalyzer && !containsIndexAnalyzer && !containsNonTokenizingOptions) + { + throw new InvalidRequestException("Cannot specify query_analyzer without an index_analyzer option or any" + + " combination of case_sensitive, normalize, or ascii options. options=" + options); + } + if ((containsIndexAnalyzer || containsNonTokenizingOptions) && type.isCollection() && !type.isMultiCell()) + throw new InvalidRequestException("Cannot use an analyzer on " + target + " because it's a frozen collection."); + + if (containsIndexAnalyzer) + { + String json = options.get(LuceneAnalyzer.INDEX_ANALYZER); + return toAnalyzerFactory(json, type, options); + } + + if (containsNonTokenizingOptions) + { + if (TypeUtil.isIn(type, ANALYZABLE_TYPES)) + { + // load NonTokenizingAnalyzer so it'll validate options + NonTokenizingAnalyzer a = new NonTokenizingAnalyzer(type, options); + a.end(); + Map finalOptions = options; + + return new AnalyzerFactory() + { + @Override + public AbstractAnalyzer create() + { + return new NonTokenizingAnalyzer(type, finalOptions); + } + + @Override + public boolean supportsEquals() + { + return supportsEquals; + } + }; + } + else + { + throw new InvalidRequestException("CQL type " + type.asCQL3Type() + " cannot be analyzed."); + } + } + return NoOpAnalyzer::new; + } } diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/AnalyzerEqOperatorSupport.java b/src/java/org/apache/cassandra/index/sai/analyzer/AnalyzerEqOperatorSupport.java new file mode 100644 index 000000000000..21ed0ba69154 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/AnalyzerEqOperatorSupport.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.analyzer; + +import java.util.Arrays; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.exceptions.InvalidRequestException; + +/** + * Index config property for defining the behaviour of the equals operator (=) when the index is analyzed. + *

    + * Analyzers transform the indexed value, so EQ queries using an analyzed index can return results different to those of + * an equivalent query without indexes. Having EQ queries returning different results depending on if/how the column is + * indexed can be confusing for users, so probably the safest approach is to reject EQ queries on analyzed indexes, and + * let users use the analyzer matches operator (:) instead. However, for backwards compatibility reasons, we should + * allow users to let equality queries behave same as match queries through this index config property. We use an enum + * value rather than a boolean to allow for future extensions. + */ +public class AnalyzerEqOperatorSupport +{ + public static final String OPTION = "equals_behaviour_when_analyzed"; + public static final Value DEFAULT = Value.MATCH; // default to : behaviour for backwards compatibility + + @VisibleForTesting + static final String NOT_ANALYZED_ERROR = "The behaviour of the equals operator (=) cannot be " + + "defined with the '" + OPTION + "' index option because " + + "the index is not analyzed."; + + @VisibleForTesting + static final String WRONG_OPTION_ERROR = String.format("Invalid value for '%s' option. " + + "Possible values are %s but found ", + OPTION, Arrays.toString(Value.values())); + + public static final String EQ_RESTRICTION_ON_ANALYZED_WARNING = + String.format("Column [%%s] is restricted by '=' and has analyzed indexes [%%s] able to process those restrictions. " + + "Analyzed indexes might process '=' restrictions in a way that is inconsistent with non-indexed queries. " + + "While '=' is still supported on analyzed indexes for backwards compatibility, " + + "it is recommended to use the ':' operator instead to prevent the ambiguity. " + + "Future versions will remove support for '=' on analyzed indexes. " + + "If you want to forbid the use of '=' on analyzed indexes now, " + + "please use '%s':'%s' in the index options.", + OPTION, Value.UNSUPPORTED.toString().toLowerCase()); + + public static final String EQ_AMBIGUOUS_ERROR = + String.format("Column [%%s] equality predicate is ambiguous. It has both analyzed indexes [%%s] configured with '%s':'%s', " + + "and an un-analyzed indexes [%%s]. " + + "To avoid ambiguity, drop the analyzed indexes and recreate them with option '%s':'%s', " + + "or use index hints to disambiguate, as in SELECT ... WITH included_indexes={%%s}.", + OPTION, Value.MATCH.toString().toLowerCase(), OPTION, Value.UNSUPPORTED.toString().toLowerCase()); + + + public static final String LWT_CONDITION_ON_ANALYZED_WARNING = + "Index analyzers not applied to LWT conditions on columns [%s]."; + + public enum Value + { + /** + * The index won't support equality (=) expressions on analyzed indexes. + */ + UNSUPPORTED, + /** + * Allow equality (=) expressions on analyzed indexes. They will behave same as match queries (:). + */ + MATCH + } + + public static boolean supportsEqualsFromOptions(Map options) + { + return fromMap(options) == Value.MATCH; + } + + public static Value fromMap(Map options) + { + if (options == null || !options.containsKey(OPTION)) + return DEFAULT; + + if (!AbstractAnalyzer.isAnalyzed(options)) + throw new InvalidRequestException(NOT_ANALYZED_ERROR); + + String option = options.get(OPTION).toUpperCase(); + try + { + return Value.valueOf(option); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException(WRONG_OPTION_ERROR + option); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/ArgsStringLoader.java b/src/java/org/apache/cassandra/index/sai/analyzer/ArgsStringLoader.java new file mode 100644 index 000000000000..c12518268334 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/ArgsStringLoader.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.lucene.util.ResourceLoader; + +/** + * A resource loader that considers each passed string as the resource. This class allows us to configure stop words + * and synonyms as arguments in the 'args' parameter of the filter's configuration. + * Example: WITH OPTIONS = {'index_analyzer':'{"tokenizer":{"name" : "whitespace"}, "filters":[{"name":"stop", "args": {"words": "the, test"}}]}'} + * The above configuration will create a stop filter with the words "the" and "test". The args key name, e.g. words, + * is specific to the lucene component being configured. The delimiter can vary based on the filter, but appears to + * be comma delimited for most lucene components. Note that commas can be escaped with a backslash. + */ +public class ArgsStringLoader implements ResourceLoader +{ + @Override + public InputStream openResource(String s) throws IOException + { + return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public Class findClass(String cname, Class expectedType) { + try { + return Class.forName(cname).asSubclass(expectedType); + } catch (Exception e) { + throw new RuntimeException("Cannot load class: " + cname, e); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/ByteLimitedMaterializer.java b/src/java/org/apache/cassandra/index/sai/analyzer/ByteLimitedMaterializer.java new file mode 100644 index 000000000000..3a5d2f57e2d3 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/ByteLimitedMaterializer.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; + +/** + * Utility class for conditionally materializing a list of tokens given an analyzer and the term to be analyzed. + * If the cumulative size of the analyzed term exceeds the configured maximum, an empty list is returned to prevent + * excessive memory/disk usage for a single term. + */ +public class ByteLimitedMaterializer +{ + private static final Logger logger = LoggerFactory.getLogger(ByteLimitedMaterializer.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + public static final String ANALYZED_TERM_OVERSIZE_MESSAGE = "Cannot add term's analyzed tokens of column {} to index" + + " for key: {}, analzyed term size {}, but max allowed size {}."; + + /** + * Using the configured analyzer, materialize the tokens for the given term. If the cumulative size of the analyzed + * term exceeds the configured maximum, an empty list is returned to prevent excessive memory/disk usage for a + * single term. If the analyzer does not transform the value, the analyzer's byte limit is ignored because indexes + * already have a limit on the size of a single term. + * @param analyzer the analyzer to use + * @param term the term to analyze + * @param indexContext the index context used for logging + * @param primaryKey the primary key of the row being indexed + * @return all the terms produced by the analyzer, or an empty list if the cumulative size of the analyzed term + * exceeds the configured maximum + */ + public static List materializeTokens(AbstractAnalyzer analyzer, ByteBuffer term, IndexContext indexContext, PrimaryKey primaryKey) + { + try + { + analyzer.reset(term); + if (!analyzer.transformValue()) + return analyzer.hasNext() ? List.of(analyzer.next()) : List.of(); + + List tokens = new ArrayList<>(); + int bytesCount = 0; + while (analyzer.hasNext()) + { + final ByteBuffer token = analyzer.next(); + tokens.add(token); + bytesCount += token.remaining(); + if (bytesCount >= IndexContext.MAX_ANALYZED_SIZE) + { + noSpamLogger.warn(indexContext.logMessage(ANALYZED_TERM_OVERSIZE_MESSAGE), + indexContext.getColumnName(), + indexContext.keyValidator().getString(primaryKey.partitionKey().getKey()), + FBUtilities.prettyPrintMemory(bytesCount), + FBUtilities.prettyPrintMemory(IndexContext.MAX_ANALYZED_SIZE)); + return List.of(); + } + } + return tokens; + } + finally + { + analyzer.end(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/JSONAnalyzerParser.java b/src/java/org/apache/cassandra/index/sai/analyzer/JSONAnalyzerParser.java new file mode 100644 index 000000000000..e3aa3f349faa --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/JSONAnalyzerParser.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer; + +import java.io.IOException; +import java.util.Map; + +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.sai.analyzer.filter.BuiltInAnalyzers; +import org.apache.cassandra.utils.Pair; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.CharFilterFactory; +import org.apache.lucene.analysis.TokenFilterFactory; +import org.apache.lucene.analysis.TokenizerFactory; +import org.apache.lucene.analysis.custom.CustomAnalyzer; +import org.apache.lucene.analysis.ngram.NGramTokenizerFactory; + +import static org.apache.cassandra.utils.JsonUtils.JSON_OBJECT_MAPPER; + +public class JSONAnalyzerParser +{ + public static Pair parse(String json) throws IOException + { + Analyzer analyzer = matchBuiltInAnalzyer(json.toUpperCase()); + if (analyzer != null) + { + return Pair.create(analyzer, null); + } + + LuceneCustomAnalyzerConfig analyzerModel; + try + { + // Don't have built in analyzer, parse JSON + analyzerModel = JSON_OBJECT_MAPPER.readValue(json, LuceneCustomAnalyzerConfig.class); + } + catch (UnrecognizedPropertyException e) + { + throw new InvalidRequestException("Invalid field name '" + e.getPropertyName() + "' in analyzer config. Valid fields are: [tokenizer, filters, charFilters]"); + } + catch (IOException e) + { + throw new InvalidRequestException("Invalid analyzer config: " + e.getMessage()); + } + + CustomAnalyzer.Builder builder = CustomAnalyzer.builder(new ArgsStringLoader()); + // An ommitted tokenizer maps directly to the keyword tokenizer, which is an identity map on input terms + if (analyzerModel.getTokenizer() == null) + { + if (analyzerModel.getFilters().isEmpty() && analyzerModel.getCharFilters().isEmpty()) + { + throw new InvalidRequestException("Analzyer config requires at least a tokenizer, a filter, or a charFilter, but none found. config=" + json); + } + builder.withTokenizer("keyword"); + } + else + { + String name = analyzerModel.getTokenizer().getName(); + try + { + // Validate before attempting to build the tokenizer so we can provide a more helpful error message. + // We use lookupClass because it does an internal lowercase to match the class name, which we cannot + // easily do because the list of available tokenizers is loaded via reflection. + TokenizerFactory.lookupClass(name); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException("Unknown tokenizer '" + name + "'. Valid options: " + TokenizerFactory.availableTokenizers()); + } + + Map args = analyzerModel.getTokenizer().getArgs(); + try + { + builder.withTokenizer(name, applyTokenizerDefaults(name, args)); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException("Error configuring analyzer's tokenizer '" + name + "': " + e.getMessage()); + } + } + for (LuceneClassNameAndArgs filter : analyzerModel.getFilters()) + { + if (filter.getName() == null) + throw new InvalidRequestException("filter 'name' field is required for options=" + json); + + try + { + // Validate before attempting to build the filter so we can provide a more helpful error message. + // We use lookupClass because it does an internal lowercase to match the class name, which we cannot + // easily do because the list of available tokenizers is loaded via reflection. + TokenFilterFactory.lookupClass(filter.getName()); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException("Unknown filter '" + filter.getName() + "'. Valid options: " + TokenFilterFactory.availableTokenFilters()); + } + + try + { + builder.addTokenFilter(filter.getName(), filter.getArgs()); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException("Error configuring analyzer's filter '" + filter.getName() + "': " + e.getMessage()); + } + } + + for (LuceneClassNameAndArgs charFilter : analyzerModel.getCharFilters()) + { + if (charFilter.getName() == null) + throw new InvalidRequestException("charFilter 'name' field is required for options=" + json); + + try + { + // Validate before attempting to build the charFilter so we can provide a more helpful error message. + // We use lookupClass because it does an internal lowercase to match the class name, which we cannot + // easily do because the list of available tokenizers is loaded via reflection. + CharFilterFactory.lookupClass(charFilter.getName()); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException("Unknown charFilter '" + charFilter.getName() + "'. Valid options: " + CharFilterFactory.availableCharFilters()); + } + + try + { + builder.addCharFilter(charFilter.getName(), charFilter.getArgs()); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException("Error configuring analyzer's charFilter '" + charFilter.getName() + "': " + e.getMessage()); + } + } + return Pair.create(builder.build(), analyzerModel); + } + + private static Analyzer matchBuiltInAnalzyer(String maybeAnalyzer) + { + for (BuiltInAnalyzers analyzer : BuiltInAnalyzers.values()) + { + if (analyzer.name().equals(maybeAnalyzer)) + { + return analyzer.getNewAnalyzer(); + } + } + return null; + } + + private static Map applyTokenizerDefaults(String filterName, Map args) + { + if (NGramTokenizerFactory.NAME.equalsIgnoreCase(filterName)) + { + // Lucene's defaults are 1 and 2 respectively, which has a large memory overhead. + args.putIfAbsent("minGramSize", "3"); + args.putIfAbsent("maxGramSize", "7"); + } + return args; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/LuceneAnalyzer.java b/src/java/org/apache/cassandra/index/sai/analyzer/LuceneAnalyzer.java new file mode 100644 index 000000000000..0890e0333ca1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/LuceneAnalyzer.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer; + +import java.io.CharArrayReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.NoSuchElementException; + +import com.google.common.base.MoreObjects; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.disk.io.BytesRefUtil; +import org.apache.cassandra.utils.Throwables; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.TermToBytesRefAttribute; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.CharsRef; +import org.apache.lucene.util.CharsRefBuilder; + +public class LuceneAnalyzer extends AbstractAnalyzer +{ + public static final String INDEX_ANALYZER = "index_analyzer"; + public static final String QUERY_ANALYZER = "query_analyzer"; + private AbstractType type; + private boolean hasNext = false; + + private final Analyzer analyzer; + private TokenStream tokenStream; + private final CharsRefBuilder charsBuilder = new CharsRefBuilder(); + private TermToBytesRefAttribute termAttr; + private final BytesRefBuilder bytesBuilder = new BytesRefBuilder(); + private final Map options; + + public LuceneAnalyzer(AbstractType type, Analyzer analyzer, Map options) + { + this.type = type; + this.analyzer = analyzer; + this.options = options; + } + + @Override + public boolean hasNext() + { + if (tokenStream == null) + { + throw new IllegalStateException("resetInternal(ByteBuffer term) must be called prior to hasNext()"); + } + try + { + hasNext = tokenStream.incrementToken(); + + if (hasNext) + { + final BytesRef br = termAttr.getBytesRef(); + // TODO: should be able to reuse the bytes ref however + // MemoryIndex#setMinMaxTerm requires a copy + // getting the max term from the mem trie is best + next = ByteBuffer.wrap(BytesRef.deepCopyOf(br).bytes); + } + return hasNext; + } + catch (IOException ex) + { + throw Throwables.cleaned(ex); + } + } + + @Override + public ByteBuffer next() + { + if (!hasNext) + { + throw new NoSuchElementException(); + } + return next; + } + + @Override + public void end() + { + if (tokenStream == null) + return; + try + { + try + { + tokenStream.end(); + } + finally + { + tokenStream.close(); + } + } + catch (IOException ex) + { + throw Throwables.cleaned(ex); // highly unlikely exception + } + } + + @Override + public boolean transformValue() + { + return true; + } + + @Override + protected void resetInternal(ByteBuffer input) + { + try + { + // the following uses a byte[] and char[] buffer to reduce object creation + BytesRefUtil.copyBufferToBytesRef(input, bytesBuilder); + + charsBuilder.copyUTF8Bytes(bytesBuilder.get()); + + final CharsRef charsRef = charsBuilder.get(); + + // the field name doesn't matter here, it's an internal lucene thing + tokenStream = analyzer.tokenStream("field", new CharArrayReader(charsRef.chars, charsRef.offset, charsRef.length)); + tokenStream.reset(); + termAttr = tokenStream.getAttribute(TermToBytesRefAttribute.class); + + this.hasNext = true; + } + catch (Exception ex) + { + throw Throwables.cleaned(ex); + } + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("type", type) + .add("hasNext", hasNext) + .add("analyzer", analyzer) + .add("tokenStream", tokenStream) + .add("termAttr", termAttr) + .add("options", options) + .toString(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/LuceneClassNameAndArgs.java b/src/java/org/apache/cassandra/index/sai/analyzer/LuceneClassNameAndArgs.java new file mode 100644 index 000000000000..1b44b0238d35 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/LuceneClassNameAndArgs.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A class representing the name of a Lucene class and a map of arguments to pass as configuration. + */ +public class LuceneClassNameAndArgs +{ + private final String name; + private final Map args; + + public LuceneClassNameAndArgs(@JsonProperty("name") String name, + @JsonProperty("args") Map args) + { + this.name = name; + this.args = args != null ? args : new HashMap<>(); + } + + public String getName() + { + return name; + } + + public Map getArgs() + { + return args; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/LuceneCustomAnalyzerConfig.java b/src/java/org/apache/cassandra/index/sai/analyzer/LuceneCustomAnalyzerConfig.java new file mode 100644 index 000000000000..de8bfbe6091a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/LuceneCustomAnalyzerConfig.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LuceneCustomAnalyzerConfig +{ + private final LuceneClassNameAndArgs tokenizer; + private final List filters; + private final List charFilters; + + public LuceneCustomAnalyzerConfig(@JsonProperty("tokenizer") LuceneClassNameAndArgs tokenizer, + @JsonProperty("filters") List filters, + @JsonProperty("charFilters") List charFilters) + { + this.tokenizer = tokenizer; + this.filters = filters != null ? filters : List.of(); + this.charFilters = charFilters != null ? charFilters : List.of(); + } + + public LuceneClassNameAndArgs getTokenizer() + { + return tokenizer; + } + + public List getFilters() + { + return filters; + } + + public List getCharFilters() + { + return charFilters; + } + + /** + * @return {@link true} if this analyzer configuration has a n-gram tokenizer or any of its filters is n-gram. + */ + public boolean isNGram() + { + if (getTokenizer().getName().equals("ngram")) + return true; + + for (LuceneClassNameAndArgs filter : getFilters()) + { + if (filter.getName().equals("ngram")) + return true; + } + + return false; + } +} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/NoOpAnalyzer.java b/src/java/org/apache/cassandra/index/sai/analyzer/NoOpAnalyzer.java similarity index 77% rename from src/java/org/apache/cassandra/index/sasi/analyzer/NoOpAnalyzer.java rename to src/java/org/apache/cassandra/index/sai/analyzer/NoOpAnalyzer.java index 1a427897918f..d8ae78ae6297 100644 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/NoOpAnalyzer.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/NoOpAnalyzer.java @@ -15,12 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.cassandra.index.sasi.analyzer; + +package org.apache.cassandra.index.sai.analyzer; import java.nio.ByteBuffer; -import java.util.Map; -import org.apache.cassandra.db.marshal.AbstractType; +import com.google.common.base.MoreObjects; /** * Default noOp tokenizer. The iterator will iterate only once @@ -31,9 +31,10 @@ public class NoOpAnalyzer extends AbstractAnalyzer private ByteBuffer input; private boolean hasNext = false; - public void init(Map options, AbstractType validator) - {} + @SuppressWarnings("unused") + NoOpAnalyzer() {} + @Override public boolean hasNext() { if (hasNext) @@ -42,19 +43,26 @@ public boolean hasNext() this.hasNext = false; return true; } + this.next = null; return false; } - public void reset(ByteBuffer input) + @Override + protected void resetInternal(ByteBuffer input) { - this.next = null; this.input = input; this.hasNext = true; } @Override - public boolean isCompatibleWith(AbstractType validator) + public boolean transformValue() + { + return false; + } + + @Override + public String toString() { - return true; + return MoreObjects.toStringHelper(this).toString(); } } diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingAnalyzer.java b/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingAnalyzer.java index 30eedc08f39b..8a46dbbad595 100644 --- a/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingAnalyzer.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingAnalyzer.java @@ -25,36 +25,38 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.index.sai.analyzer.filter.BasicFilters; -import org.apache.cassandra.index.sai.analyzer.filter.FilterPipeline; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.analyzer.filter.BasicResultFilters; +import org.apache.cassandra.index.sai.analyzer.filter.FilterPipelineBuilder; import org.apache.cassandra.index.sai.analyzer.filter.FilterPipelineExecutor; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.analyzer.filter.FilterPipelineTask; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.utils.ByteBufferUtil; /** * Analyzer that does *not* tokenize the input. Optionally will - * apply filters for the input based on {@link NonTokenizingOptions}. + * apply filters for the input output as defined in analyzers options */ public class NonTokenizingAnalyzer extends AbstractAnalyzer { private static final Logger logger = LoggerFactory.getLogger(NonTokenizingAnalyzer.class); - private final IndexTermType indexTermType; - private final NonTokenizingOptions options; - private final FilterPipeline filterPipeline; + private AbstractType type; + private NonTokenizingOptions options; + private FilterPipelineTask filterPipeline; private ByteBuffer input; private boolean hasNext = false; - NonTokenizingAnalyzer(IndexTermType indexTermType, Map options) + NonTokenizingAnalyzer(AbstractType type, Map options) { - this(indexTermType, NonTokenizingOptions.fromMap(options)); + this(type, NonTokenizingOptions.fromMap(options)); } - NonTokenizingAnalyzer(IndexTermType indexTermType, NonTokenizingOptions tokenizerOptions) + NonTokenizingAnalyzer(AbstractType type, NonTokenizingOptions tokenizerOptions) { - this.indexTermType = indexTermType; + this.type = type; this.options = tokenizerOptions; this.filterPipeline = getFilterPipeline(); } @@ -63,19 +65,18 @@ public class NonTokenizingAnalyzer extends AbstractAnalyzer public boolean hasNext() { // check that we know how to handle the input, otherwise bail - if (!indexTermType.isString()) - return false; + if (!TypeUtil.isIn(type, ANALYZABLE_TYPES)) return false; if (hasNext) { try { - String input = indexTermType.asString(this.input); + String input = type.getString(this.input); if (input == null) { throw new MarshalException(String.format("'null' deserialized value for %s with %s", - ByteBufferUtil.bytesToHex(this.input), indexTermType)); + ByteBufferUtil.bytesToHex(this.input), type)); } String result = FilterPipelineExecutor.execute(filterPipeline, input); @@ -88,13 +89,13 @@ public boolean hasNext() } nextLiteral = result; - next = indexTermType.fromString(result); + next = type.fromString(result); return true; } catch (MarshalException e) { - logger.error("Failed to deserialize value with " + indexTermType, e); + logger.error("Failed to deserialize value with " + type, e); return false; } finally @@ -119,20 +120,26 @@ protected void resetInternal(ByteBuffer input) this.hasNext = true; } - private FilterPipeline getFilterPipeline() + private FilterPipelineTask getFilterPipeline() { - FilterPipeline builder = new FilterPipeline(new BasicFilters.NoOperation()); + FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation()); if (!options.isCaseSensitive()) - builder = builder.add("to_lower", new BasicFilters.LowerCase()); + { + builder = builder.add("to_lower", new BasicResultFilters.LowerCase()); + } if (options.isNormalized()) - builder = builder.add("normalize", new BasicFilters.Normalize()); + { + builder = builder.add("normalize", new BasicResultFilters.Normalize()); + } if (options.isAscii()) - builder = builder.add("ascii", new BasicFilters.Ascii()); + { + builder = builder.add("ascii", new BasicResultFilters.Ascii()); + } - return builder; + return builder.build(); } @Override diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingOptions.java b/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingOptions.java index 66c7740a5393..bb823c5997a0 100644 --- a/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingOptions.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/NonTokenizingOptions.java @@ -30,6 +30,9 @@ public class NonTokenizingOptions public static final String NORMALIZE = "normalize"; public static final String CASE_SENSITIVE = "case_sensitive"; public static final String ASCII = "ascii"; + static final boolean NORMALIZE_DEFAULT = false; + static final boolean CASE_SENSITIVE_DEFAULT = true; + static final boolean ASCII_DEFAULT = false; private boolean caseSensitive; private boolean normalized; @@ -65,16 +68,11 @@ void setNormalized(boolean normalized) this.normalized = normalized; } - static boolean hasOption(String option) - { - return option.equals(NORMALIZE) || option.equals(CASE_SENSITIVE) || option.equals(ASCII); - } - public static class OptionsBuilder { - private boolean caseSensitive = true; - private boolean normalized = false; - private boolean ascii = false; + private boolean caseSensitive = CASE_SENSITIVE_DEFAULT; + private boolean normalized = NORMALIZE_DEFAULT; + private boolean ascii = ASCII_DEFAULT; OptionsBuilder() {} @@ -108,7 +106,7 @@ public NonTokenizingOptions build() public static NonTokenizingOptions getDefaultOptions() { - return fromMap(new HashMap<>(1)); + return fromMap(new HashMap(1)); } public static NonTokenizingOptions fromMap(Map options) @@ -148,7 +146,7 @@ private static boolean validateBoolean(String value, String option) { if (Strings.isNullOrEmpty(value)) { - throw new InvalidRequestException("Empty value for boolean option '" + option + '\''); + throw new InvalidRequestException("Empty value for boolean option '" + option + "'"); } if (!value.equalsIgnoreCase(Boolean.TRUE.toString()) && !value.equalsIgnoreCase(Boolean.FALSE.toString())) @@ -158,4 +156,23 @@ private static boolean validateBoolean(String value, String option) return Boolean.parseBoolean(value); } + + /** + * Returns true if any of the options are set to a non-default value. Can be used to determine whether the + * parameterized OPTIONS should be used to construct a {@link NonTokenizingAnalyzer} + * or a {@link NoOpAnalyzer} instance. + * @param options - index options + * @return true if and only if any of the options are set to a non-default value. + */ + static boolean hasNonDefaultOptions(Map options) { + return hasNonDefaultBooleanOption(options.get(CASE_SENSITIVE), CASE_SENSITIVE_DEFAULT) + || hasNonDefaultBooleanOption(options.get(NORMALIZE), NORMALIZE_DEFAULT) + || hasNonDefaultBooleanOption(options.get(ASCII), ASCII_DEFAULT); + } + + private static boolean hasNonDefaultBooleanOption(String value, boolean defaultValue) + { + // Use string equality here to preven the need to parse the input string value. + return value != null && !Boolean.toString(defaultValue).equalsIgnoreCase(value); + } } diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/README.md b/src/java/org/apache/cassandra/index/sai/analyzer/README.md new file mode 100644 index 000000000000..f4c6099abe00 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/README.md @@ -0,0 +1,160 @@ + + +# Configuring an SAI with an Analyzer + +Analyzers are built on the Lucene Java Analyzer API. The SAI uses the Lucene Java Analyzer API to transform text columns into tokens for indexing and querying. The SAI supports the use of built-in analyzers and custom analyzers. + +## Defining an Analyzer + +Analyzers have one `tokenizer`, a list of `filters`, and a list of `charFilters`. The `tokenizer` splits the input text into tokens. The `filters` and `charFilters` transform the tokens into a form that is suitable for indexing and querying. The `filters` and `charFilters` are applied in the order they are defined in the configuration. The `filters` and the `charFilters` are optional. + +## Configuration Formatting + +The `OPTIONS` configuration argument is formatted as a JSON object: + +``` +OPTIONS = { 'index_analyzer' : '' } +``` + +OR + +``` +OPTIONS = { + 'index_analyzer': + { + "tokenizer" : { + "name" : "", + "args" : {} + }, + "filters" : [ + { + "name" : "", + "args": {} + } + ], + "charFilters" : [ + { + "name" : "", + "args": {} + } + ] + } +} +``` + +## Built-in Analyzers + +The following built-in analyzers are available: + +| Analyzer Name | Description from Lucene Java Docs | +|---------------|----------------------------------------------------------------------------------------------------------| +| `standard` | Filters `StandardTokenizer` output with `LowerCaseFilter` | +| `simple` | Filters `LetterTokenizer` output with `LowerCaseFilter` | +| `whitespace` | Analyzer that uses `WhitespaceTokenizer`. | +| `stop` | Filters `LetterTokenizer` output with `LowerCaseFilter` and removes Lucene's default English stop words. | +| `lowercase` | Normalizes input by applying `LowerCaseFilter` (no additional tokenization is performed). | +| `keyword` | Analyzer that uses `KeywordTokenizer`, which is an identity function on input values. | +| `` | Analyzers for specific languages. For example, `english` and `french`. | + +### Standard Analyzer + +Here is the custom analyzer configuration for the standard analyzer: + +``` +OPTIONS = { + 'index_analyzer': + '{ + "tokenizer" : { + "name" : "standard", + "args" : {} + }, + "filters" : [ + { + "name" : "lowercase", + "args": {} + } + ], + "charFilters" : [] + }' +} +``` + +### Simple Analyzer + +Here is the custom analyzer configuration for the simple analyzer: + +``` +OPTIONS = { + 'index_analyzer': + '{ + "tokenizer" : { + "name" : "letter", + "args" : {} + }, + "filters" : [ + { + "name" : "lowercase", + "args": {} + } + ], + "charFilters" : [] + }' +} +``` + +### Whitespace Analyzer + +Here is the custom analyzer configuration for the whitespace analyzer: + +``` +OPTIONS = { + 'index_analyzer': + '{ + "tokenizer" : { + "name" : "whitespace", + "args" : {} + }, + "filters" : [], + "charFilters" : [] + }' +} +``` + +### Lowercase Analyzer + +Here is the custom analyzer configuration for the lowercase analyzer: + +``` +OPTIONS = { + 'index_analyzer': + '{ + "tokenizer" : { + "name" : "keyword", + "args" : {} + }, + "filters" : [ + { + "name" : "lowercase", + "args": {} + } + ], + "charFilters" : [] + }' +} +``` \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/filter/BasicResultFilters.java b/src/java/org/apache/cassandra/index/sai/analyzer/filter/BasicResultFilters.java new file mode 100644 index 000000000000..b5ad225f65fe --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/filter/BasicResultFilters.java @@ -0,0 +1,2007 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.analyzer.filter; + +import java.text.Normalizer; +import java.util.Locale; + +/** + * Basic/General Token Filters + */ +public class BasicResultFilters +{ + private static final Locale DEFAULT_LOCALE = Locale.getDefault(); + + public static class LowerCase extends FilterPipelineTask + { + private final Locale locale; + + public LowerCase() + { + this.locale = DEFAULT_LOCALE; + } + + public String process(String input) + { + return input.toLowerCase(locale); + } + } + + public static class Normalize extends FilterPipelineTask + { + public Normalize() { } + + public String process(String input) + { + if (input == null) return null; + return Normalizer.isNormalized(input, Normalizer.Form.NFC) ? input : Normalizer.normalize(input, Normalizer.Form.NFC); + } + } + + public static class Ascii extends FilterPipelineTask + { + public Ascii() { } + + public String process(String input) + { + if (input == null) return null; + char[] inputChars = input.toCharArray(); + // The output can (potentially) be 4 times the size of the input + char[] outputChars = new char[inputChars.length * 4]; + int outputSize = foldToASCII(inputChars, 0, outputChars, 0, inputChars.length); + return new String(outputChars, 0, outputSize); + } + } + + public static class NoOperation extends FilterPipelineTask + { + public String process(String input) + { + return input; + } + } + + // copied from lucene org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilter + public static final int foldToASCII(char input[], int inputPos, char output[], int outputPos, int length) + { + final int end = inputPos + length; + for (int pos = inputPos; pos < end ; ++pos) { + final char c = input[pos]; + + // Quick test: if it's not in range then just keep current character + if (c < '\u0080') { + output[outputPos++] = c; + } else { + switch (c) { + case '\u00C0': // À [LATIN CAPITAL LETTER A WITH GRAVE] + case '\u00C1': // Á [LATIN CAPITAL LETTER A WITH ACUTE] + case '\u00C2': //  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] + case '\u00C3': // à [LATIN CAPITAL LETTER A WITH TILDE] + case '\u00C4': // Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] + case '\u00C5': // Å [LATIN CAPITAL LETTER A WITH RING ABOVE] + case '\u0100': // Ā [LATIN CAPITAL LETTER A WITH MACRON] + case '\u0102': // Ă [LATIN CAPITAL LETTER A WITH BREVE] + case '\u0104': // Ą [LATIN CAPITAL LETTER A WITH OGONEK] + case '\u018F': // Ə http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] + case '\u01CD': // Ǎ [LATIN CAPITAL LETTER A WITH CARON] + case '\u01DE': // Ǟ [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] + case '\u01E0': // Ǡ [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] + case '\u01FA': // Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] + case '\u0200': // Ȁ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] + case '\u0202': // Ȃ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] + case '\u0226': // Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] + case '\u023A': // Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] + case '\u1D00': // ᴀ [LATIN LETTER SMALL CAPITAL A] + case '\u1E00': // Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] + case '\u1EA0': // Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] + case '\u1EA2': // Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] + case '\u1EA4': // Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] + case '\u1EA6': // Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] + case '\u1EA8': // Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EAA': // Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] + case '\u1EAC': // Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EAE': // Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] + case '\u1EB0': // Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] + case '\u1EB2': // Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] + case '\u1EB4': // Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] + case '\u1EB6': // Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] + case '\u24B6': // Ⓐ [CIRCLED LATIN CAPITAL LETTER A] + case '\uFF21': // A [FULLWIDTH LATIN CAPITAL LETTER A] + output[outputPos++] = 'A'; + break; + case '\u00E0': // à [LATIN SMALL LETTER A WITH GRAVE] + case '\u00E1': // á [LATIN SMALL LETTER A WITH ACUTE] + case '\u00E2': // â [LATIN SMALL LETTER A WITH CIRCUMFLEX] + case '\u00E3': // ã [LATIN SMALL LETTER A WITH TILDE] + case '\u00E4': // ä [LATIN SMALL LETTER A WITH DIAERESIS] + case '\u00E5': // å [LATIN SMALL LETTER A WITH RING ABOVE] + case '\u0101': // ā [LATIN SMALL LETTER A WITH MACRON] + case '\u0103': // ă [LATIN SMALL LETTER A WITH BREVE] + case '\u0105': // ą [LATIN SMALL LETTER A WITH OGONEK] + case '\u01CE': // ǎ [LATIN SMALL LETTER A WITH CARON] + case '\u01DF': // ǟ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] + case '\u01E1': // ǡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] + case '\u01FB': // ǻ [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] + case '\u0201': // ȁ [LATIN SMALL LETTER A WITH DOUBLE GRAVE] + case '\u0203': // ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] + case '\u0227': // ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] + case '\u0250': // ɐ [LATIN SMALL LETTER TURNED A] + case '\u0259': // ə [LATIN SMALL LETTER SCHWA] + case '\u025A': // ɚ [LATIN SMALL LETTER SCHWA WITH HOOK] + case '\u1D8F': // ᶏ [LATIN SMALL LETTER A WITH RETROFLEX HOOK] + case '\u1D95': // ᶕ [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] + case '\u1E01': // ạ [LATIN SMALL LETTER A WITH RING BELOW] + case '\u1E9A': // ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] + case '\u1EA1': // ạ [LATIN SMALL LETTER A WITH DOT BELOW] + case '\u1EA3': // ả [LATIN SMALL LETTER A WITH HOOK ABOVE] + case '\u1EA5': // ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] + case '\u1EA7': // ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] + case '\u1EA9': // ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EAB': // ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] + case '\u1EAD': // ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EAF': // ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] + case '\u1EB1': // ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] + case '\u1EB3': // ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] + case '\u1EB5': // ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] + case '\u1EB7': // ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] + case '\u2090': // ₐ [LATIN SUBSCRIPT SMALL LETTER A] + case '\u2094': // ₔ [LATIN SUBSCRIPT SMALL LETTER SCHWA] + case '\u24D0': // ⓐ [CIRCLED LATIN SMALL LETTER A] + case '\u2C65': // ⱥ [LATIN SMALL LETTER A WITH STROKE] + case '\u2C6F': // Ɐ [LATIN CAPITAL LETTER TURNED A] + case '\uFF41': // a [FULLWIDTH LATIN SMALL LETTER A] + output[outputPos++] = 'a'; + break; + case '\uA732': // Ꜳ [LATIN CAPITAL LETTER AA] + output[outputPos++] = 'A'; + output[outputPos++] = 'A'; + break; + case '\u00C6': // Æ [LATIN CAPITAL LETTER AE] + case '\u01E2': // Ǣ [LATIN CAPITAL LETTER AE WITH MACRON] + case '\u01FC': // Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] + case '\u1D01': // ᴁ [LATIN LETTER SMALL CAPITAL AE] + output[outputPos++] = 'A'; + output[outputPos++] = 'E'; + break; + case '\uA734': // Ꜵ [LATIN CAPITAL LETTER AO] + output[outputPos++] = 'A'; + output[outputPos++] = 'O'; + break; + case '\uA736': // Ꜷ [LATIN CAPITAL LETTER AU] + output[outputPos++] = 'A'; + output[outputPos++] = 'U'; + break; + case '\uA738': // Ꜹ [LATIN CAPITAL LETTER AV] + case '\uA73A': // Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] + output[outputPos++] = 'A'; + output[outputPos++] = 'V'; + break; + case '\uA73C': // Ꜽ [LATIN CAPITAL LETTER AY] + output[outputPos++] = 'A'; + output[outputPos++] = 'Y'; + break; + case '\u249C': // ⒜ [PARENTHESIZED LATIN SMALL LETTER A] + output[outputPos++] = '('; + output[outputPos++] = 'a'; + output[outputPos++] = ')'; + break; + case '\uA733': // ꜳ [LATIN SMALL LETTER AA] + output[outputPos++] = 'a'; + output[outputPos++] = 'a'; + break; + case '\u00E6': // æ [LATIN SMALL LETTER AE] + case '\u01E3': // ǣ [LATIN SMALL LETTER AE WITH MACRON] + case '\u01FD': // ǽ [LATIN SMALL LETTER AE WITH ACUTE] + case '\u1D02': // ᴂ [LATIN SMALL LETTER TURNED AE] + output[outputPos++] = 'a'; + output[outputPos++] = 'e'; + break; + case '\uA735': // ꜵ [LATIN SMALL LETTER AO] + output[outputPos++] = 'a'; + output[outputPos++] = 'o'; + break; + case '\uA737': // ꜷ [LATIN SMALL LETTER AU] + output[outputPos++] = 'a'; + output[outputPos++] = 'u'; + break; + case '\uA739': // ꜹ [LATIN SMALL LETTER AV] + case '\uA73B': // ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] + output[outputPos++] = 'a'; + output[outputPos++] = 'v'; + break; + case '\uA73D': // ꜽ [LATIN SMALL LETTER AY] + output[outputPos++] = 'a'; + output[outputPos++] = 'y'; + break; + case '\u0181': // Ɓ [LATIN CAPITAL LETTER B WITH HOOK] + case '\u0182': // Ƃ [LATIN CAPITAL LETTER B WITH TOPBAR] + case '\u0243': // Ƀ [LATIN CAPITAL LETTER B WITH STROKE] + case '\u0299': // ʙ [LATIN LETTER SMALL CAPITAL B] + case '\u1D03': // ᴃ [LATIN LETTER SMALL CAPITAL BARRED B] + case '\u1E02': // Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] + case '\u1E04': // Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] + case '\u1E06': // Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] + case '\u24B7': // Ⓑ [CIRCLED LATIN CAPITAL LETTER B] + case '\uFF22': // B [FULLWIDTH LATIN CAPITAL LETTER B] + output[outputPos++] = 'B'; + break; + case '\u0180': // ƀ [LATIN SMALL LETTER B WITH STROKE] + case '\u0183': // ƃ [LATIN SMALL LETTER B WITH TOPBAR] + case '\u0253': // ɓ [LATIN SMALL LETTER B WITH HOOK] + case '\u1D6C': // ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] + case '\u1D80': // ᶀ [LATIN SMALL LETTER B WITH PALATAL HOOK] + case '\u1E03': // ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] + case '\u1E05': // ḅ [LATIN SMALL LETTER B WITH DOT BELOW] + case '\u1E07': // ḇ [LATIN SMALL LETTER B WITH LINE BELOW] + case '\u24D1': // ⓑ [CIRCLED LATIN SMALL LETTER B] + case '\uFF42': // b [FULLWIDTH LATIN SMALL LETTER B] + output[outputPos++] = 'b'; + break; + case '\u249D': // ⒝ [PARENTHESIZED LATIN SMALL LETTER B] + output[outputPos++] = '('; + output[outputPos++] = 'b'; + output[outputPos++] = ')'; + break; + case '\u00C7': // Ç [LATIN CAPITAL LETTER C WITH CEDILLA] + case '\u0106': // Ć [LATIN CAPITAL LETTER C WITH ACUTE] + case '\u0108': // Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] + case '\u010A': // Ċ [LATIN CAPITAL LETTER C WITH DOT ABOVE] + case '\u010C': // Č [LATIN CAPITAL LETTER C WITH CARON] + case '\u0187': // Ƈ [LATIN CAPITAL LETTER C WITH HOOK] + case '\u023B': // Ȼ [LATIN CAPITAL LETTER C WITH STROKE] + case '\u0297': // ʗ [LATIN LETTER STRETCHED C] + case '\u1D04': // ᴄ [LATIN LETTER SMALL CAPITAL C] + case '\u1E08': // Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] + case '\u24B8': // Ⓒ [CIRCLED LATIN CAPITAL LETTER C] + case '\uFF23': // C [FULLWIDTH LATIN CAPITAL LETTER C] + output[outputPos++] = 'C'; + break; + case '\u00E7': // ç [LATIN SMALL LETTER C WITH CEDILLA] + case '\u0107': // ć [LATIN SMALL LETTER C WITH ACUTE] + case '\u0109': // ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] + case '\u010B': // ċ [LATIN SMALL LETTER C WITH DOT ABOVE] + case '\u010D': // č [LATIN SMALL LETTER C WITH CARON] + case '\u0188': // ƈ [LATIN SMALL LETTER C WITH HOOK] + case '\u023C': // ȼ [LATIN SMALL LETTER C WITH STROKE] + case '\u0255': // ɕ [LATIN SMALL LETTER C WITH CURL] + case '\u1E09': // ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] + case '\u2184': // ↄ [LATIN SMALL LETTER REVERSED C] + case '\u24D2': // ⓒ [CIRCLED LATIN SMALL LETTER C] + case '\uA73E': // Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] + case '\uA73F': // ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] + case '\uFF43': // c [FULLWIDTH LATIN SMALL LETTER C] + output[outputPos++] = 'c'; + break; + case '\u249E': // ⒞ [PARENTHESIZED LATIN SMALL LETTER C] + output[outputPos++] = '('; + output[outputPos++] = 'c'; + output[outputPos++] = ')'; + break; + case '\u00D0': // Ð [LATIN CAPITAL LETTER ETH] + case '\u010E': // Ď [LATIN CAPITAL LETTER D WITH CARON] + case '\u0110': // Đ [LATIN CAPITAL LETTER D WITH STROKE] + case '\u0189': // Ɖ [LATIN CAPITAL LETTER AFRICAN D] + case '\u018A': // Ɗ [LATIN CAPITAL LETTER D WITH HOOK] + case '\u018B': // Ƌ [LATIN CAPITAL LETTER D WITH TOPBAR] + case '\u1D05': // ᴅ [LATIN LETTER SMALL CAPITAL D] + case '\u1D06': // ᴆ [LATIN LETTER SMALL CAPITAL ETH] + case '\u1E0A': // Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] + case '\u1E0C': // Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] + case '\u1E0E': // Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] + case '\u1E10': // Ḑ [LATIN CAPITAL LETTER D WITH CEDILLA] + case '\u1E12': // Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] + case '\u24B9': // Ⓓ [CIRCLED LATIN CAPITAL LETTER D] + case '\uA779': // Ꝺ [LATIN CAPITAL LETTER INSULAR D] + case '\uFF24': // D [FULLWIDTH LATIN CAPITAL LETTER D] + output[outputPos++] = 'D'; + break; + case '\u00F0': // ð [LATIN SMALL LETTER ETH] + case '\u010F': // ď [LATIN SMALL LETTER D WITH CARON] + case '\u0111': // đ [LATIN SMALL LETTER D WITH STROKE] + case '\u018C': // ƌ [LATIN SMALL LETTER D WITH TOPBAR] + case '\u0221': // ȡ [LATIN SMALL LETTER D WITH CURL] + case '\u0256': // ɖ [LATIN SMALL LETTER D WITH TAIL] + case '\u0257': // ɗ [LATIN SMALL LETTER D WITH HOOK] + case '\u1D6D': // ᵭ [LATIN SMALL LETTER D WITH MIDDLE TILDE] + case '\u1D81': // ᶁ [LATIN SMALL LETTER D WITH PALATAL HOOK] + case '\u1D91': // ᶑ [LATIN SMALL LETTER D WITH HOOK AND TAIL] + case '\u1E0B': // ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] + case '\u1E0D': // ḍ [LATIN SMALL LETTER D WITH DOT BELOW] + case '\u1E0F': // ḏ [LATIN SMALL LETTER D WITH LINE BELOW] + case '\u1E11': // ḑ [LATIN SMALL LETTER D WITH CEDILLA] + case '\u1E13': // ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] + case '\u24D3': // ⓓ [CIRCLED LATIN SMALL LETTER D] + case '\uA77A': // ꝺ [LATIN SMALL LETTER INSULAR D] + case '\uFF44': // d [FULLWIDTH LATIN SMALL LETTER D] + output[outputPos++] = 'd'; + break; + case '\u01C4': // DŽ [LATIN CAPITAL LETTER DZ WITH CARON] + case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] + output[outputPos++] = 'D'; + output[outputPos++] = 'Z'; + break; + case '\u01C5': // Dž [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] + case '\u01F2': // Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] + output[outputPos++] = 'D'; + output[outputPos++] = 'z'; + break; + case '\u249F': // ⒟ [PARENTHESIZED LATIN SMALL LETTER D] + output[outputPos++] = '('; + output[outputPos++] = 'd'; + output[outputPos++] = ')'; + break; + case '\u0238': // ȸ [LATIN SMALL LETTER DB DIGRAPH] + output[outputPos++] = 'd'; + output[outputPos++] = 'b'; + break; + case '\u01C6': // dž [LATIN SMALL LETTER DZ WITH CARON] + case '\u01F3': // dz [LATIN SMALL LETTER DZ] + case '\u02A3': // ʣ [LATIN SMALL LETTER DZ DIGRAPH] + case '\u02A5': // ʥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] + output[outputPos++] = 'd'; + output[outputPos++] = 'z'; + break; + case '\u00C8': // È [LATIN CAPITAL LETTER E WITH GRAVE] + case '\u00C9': // É [LATIN CAPITAL LETTER E WITH ACUTE] + case '\u00CA': // Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] + case '\u00CB': // Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] + case '\u0112': // Ē [LATIN CAPITAL LETTER E WITH MACRON] + case '\u0114': // Ĕ [LATIN CAPITAL LETTER E WITH BREVE] + case '\u0116': // Ė [LATIN CAPITAL LETTER E WITH DOT ABOVE] + case '\u0118': // Ę [LATIN CAPITAL LETTER E WITH OGONEK] + case '\u011A': // Ě [LATIN CAPITAL LETTER E WITH CARON] + case '\u018E': // Ǝ [LATIN CAPITAL LETTER REVERSED E] + case '\u0190': // Ɛ [LATIN CAPITAL LETTER OPEN E] + case '\u0204': // Ȅ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] + case '\u0206': // Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] + case '\u0228': // Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] + case '\u0246': // Ɇ [LATIN CAPITAL LETTER E WITH STROKE] + case '\u1D07': // ᴇ [LATIN LETTER SMALL CAPITAL E] + case '\u1E14': // Ḕ [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] + case '\u1E16': // Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] + case '\u1E18': // Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] + case '\u1E1A': // Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] + case '\u1E1C': // Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] + case '\u1EB8': // Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] + case '\u1EBA': // Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] + case '\u1EBC': // Ẽ [LATIN CAPITAL LETTER E WITH TILDE] + case '\u1EBE': // Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] + case '\u1EC0': // Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] + case '\u1EC2': // Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EC4': // Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] + case '\u1EC6': // Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + case '\u24BA': // Ⓔ [CIRCLED LATIN CAPITAL LETTER E] + case '\u2C7B': // ⱻ [LATIN LETTER SMALL CAPITAL TURNED E] + case '\uFF25': // E [FULLWIDTH LATIN CAPITAL LETTER E] + output[outputPos++] = 'E'; + break; + case '\u00E8': // è [LATIN SMALL LETTER E WITH GRAVE] + case '\u00E9': // é [LATIN SMALL LETTER E WITH ACUTE] + case '\u00EA': // ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] + case '\u00EB': // ë [LATIN SMALL LETTER E WITH DIAERESIS] + case '\u0113': // ē [LATIN SMALL LETTER E WITH MACRON] + case '\u0115': // ĕ [LATIN SMALL LETTER E WITH BREVE] + case '\u0117': // ė [LATIN SMALL LETTER E WITH DOT ABOVE] + case '\u0119': // ę [LATIN SMALL LETTER E WITH OGONEK] + case '\u011B': // ě [LATIN SMALL LETTER E WITH CARON] + case '\u01DD': // ǝ [LATIN SMALL LETTER TURNED E] + case '\u0205': // ȅ [LATIN SMALL LETTER E WITH DOUBLE GRAVE] + case '\u0207': // ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] + case '\u0229': // ȩ [LATIN SMALL LETTER E WITH CEDILLA] + case '\u0247': // ɇ [LATIN SMALL LETTER E WITH STROKE] + case '\u0258': // ɘ [LATIN SMALL LETTER REVERSED E] + case '\u025B': // ɛ [LATIN SMALL LETTER OPEN E] + case '\u025C': // ɜ [LATIN SMALL LETTER REVERSED OPEN E] + case '\u025D': // ɝ [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] + case '\u025E': // ɞ [LATIN SMALL LETTER CLOSED REVERSED OPEN E] + case '\u029A': // ʚ [LATIN SMALL LETTER CLOSED OPEN E] + case '\u1D08': // ᴈ [LATIN SMALL LETTER TURNED OPEN E] + case '\u1D92': // ᶒ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] + case '\u1D93': // ᶓ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] + case '\u1D94': // ᶔ [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] + case '\u1E15': // ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] + case '\u1E17': // ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] + case '\u1E19': // ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] + case '\u1E1B': // ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] + case '\u1E1D': // ḝ [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] + case '\u1EB9': // ẹ [LATIN SMALL LETTER E WITH DOT BELOW] + case '\u1EBB': // ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] + case '\u1EBD': // ẽ [LATIN SMALL LETTER E WITH TILDE] + case '\u1EBF': // ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] + case '\u1EC1': // ề [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] + case '\u1EC3': // ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EC5': // ễ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] + case '\u1EC7': // ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + case '\u2091': // ₑ [LATIN SUBSCRIPT SMALL LETTER E] + case '\u24D4': // ⓔ [CIRCLED LATIN SMALL LETTER E] + case '\u2C78': // ⱸ [LATIN SMALL LETTER E WITH NOTCH] + case '\uFF45': // e [FULLWIDTH LATIN SMALL LETTER E] + output[outputPos++] = 'e'; + break; + case '\u24A0': // ⒠ [PARENTHESIZED LATIN SMALL LETTER E] + output[outputPos++] = '('; + output[outputPos++] = 'e'; + output[outputPos++] = ')'; + break; + case '\u0191': // Ƒ [LATIN CAPITAL LETTER F WITH HOOK] + case '\u1E1E': // Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] + case '\u24BB': // Ⓕ [CIRCLED LATIN CAPITAL LETTER F] + case '\uA730': // ꜰ [LATIN LETTER SMALL CAPITAL F] + case '\uA77B': // Ꝼ [LATIN CAPITAL LETTER INSULAR F] + case '\uA7FB': // ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] + case '\uFF26': // F [FULLWIDTH LATIN CAPITAL LETTER F] + output[outputPos++] = 'F'; + break; + case '\u0192': // ƒ [LATIN SMALL LETTER F WITH HOOK] + case '\u1D6E': // ᵮ [LATIN SMALL LETTER F WITH MIDDLE TILDE] + case '\u1D82': // ᶂ [LATIN SMALL LETTER F WITH PALATAL HOOK] + case '\u1E1F': // ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] + case '\u1E9B': // ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] + case '\u24D5': // ⓕ [CIRCLED LATIN SMALL LETTER F] + case '\uA77C': // ꝼ [LATIN SMALL LETTER INSULAR F] + case '\uFF46': // f [FULLWIDTH LATIN SMALL LETTER F] + output[outputPos++] = 'f'; + break; + case '\u24A1': // ⒡ [PARENTHESIZED LATIN SMALL LETTER F] + output[outputPos++] = '('; + output[outputPos++] = 'f'; + output[outputPos++] = ')'; + break; + case '\uFB00': // ff [LATIN SMALL LIGATURE FF] + output[outputPos++] = 'f'; + output[outputPos++] = 'f'; + break; + case '\uFB03': // ffi [LATIN SMALL LIGATURE FFI] + output[outputPos++] = 'f'; + output[outputPos++] = 'f'; + output[outputPos++] = 'i'; + break; + case '\uFB04': // ffl [LATIN SMALL LIGATURE FFL] + output[outputPos++] = 'f'; + output[outputPos++] = 'f'; + output[outputPos++] = 'l'; + break; + case '\uFB01': // fi [LATIN SMALL LIGATURE FI] + output[outputPos++] = 'f'; + output[outputPos++] = 'i'; + break; + case '\uFB02': // fl [LATIN SMALL LIGATURE FL] + output[outputPos++] = 'f'; + output[outputPos++] = 'l'; + break; + case '\u011C': // Ĝ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] + case '\u011E': // Ğ [LATIN CAPITAL LETTER G WITH BREVE] + case '\u0120': // Ġ [LATIN CAPITAL LETTER G WITH DOT ABOVE] + case '\u0122': // Ģ [LATIN CAPITAL LETTER G WITH CEDILLA] + case '\u0193': // Ɠ [LATIN CAPITAL LETTER G WITH HOOK] + case '\u01E4': // Ǥ [LATIN CAPITAL LETTER G WITH STROKE] + case '\u01E5': // ǥ [LATIN SMALL LETTER G WITH STROKE] + case '\u01E6': // Ǧ [LATIN CAPITAL LETTER G WITH CARON] + case '\u01E7': // ǧ [LATIN SMALL LETTER G WITH CARON] + case '\u01F4': // Ǵ [LATIN CAPITAL LETTER G WITH ACUTE] + case '\u0262': // ɢ [LATIN LETTER SMALL CAPITAL G] + case '\u029B': // ʛ [LATIN LETTER SMALL CAPITAL G WITH HOOK] + case '\u1E20': // Ḡ [LATIN CAPITAL LETTER G WITH MACRON] + case '\u24BC': // Ⓖ [CIRCLED LATIN CAPITAL LETTER G] + case '\uA77D': // Ᵹ [LATIN CAPITAL LETTER INSULAR G] + case '\uA77E': // Ꝿ [LATIN CAPITAL LETTER TURNED INSULAR G] + case '\uFF27': // G [FULLWIDTH LATIN CAPITAL LETTER G] + output[outputPos++] = 'G'; + break; + case '\u011D': // ĝ [LATIN SMALL LETTER G WITH CIRCUMFLEX] + case '\u011F': // ğ [LATIN SMALL LETTER G WITH BREVE] + case '\u0121': // ġ [LATIN SMALL LETTER G WITH DOT ABOVE] + case '\u0123': // ģ [LATIN SMALL LETTER G WITH CEDILLA] + case '\u01F5': // ǵ [LATIN SMALL LETTER G WITH ACUTE] + case '\u0260': // ɠ [LATIN SMALL LETTER G WITH HOOK] + case '\u0261': // ɡ [LATIN SMALL LETTER SCRIPT G] + case '\u1D77': // ᵷ [LATIN SMALL LETTER TURNED G] + case '\u1D79': // ᵹ [LATIN SMALL LETTER INSULAR G] + case '\u1D83': // ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] + case '\u1E21': // ḡ [LATIN SMALL LETTER G WITH MACRON] + case '\u24D6': // ⓖ [CIRCLED LATIN SMALL LETTER G] + case '\uA77F': // ꝿ [LATIN SMALL LETTER TURNED INSULAR G] + case '\uFF47': // g [FULLWIDTH LATIN SMALL LETTER G] + output[outputPos++] = 'g'; + break; + case '\u24A2': // ⒢ [PARENTHESIZED LATIN SMALL LETTER G] + output[outputPos++] = '('; + output[outputPos++] = 'g'; + output[outputPos++] = ')'; + break; + case '\u0124': // Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] + case '\u0126': // Ħ [LATIN CAPITAL LETTER H WITH STROKE] + case '\u021E': // Ȟ [LATIN CAPITAL LETTER H WITH CARON] + case '\u029C': // ʜ [LATIN LETTER SMALL CAPITAL H] + case '\u1E22': // Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] + case '\u1E24': // Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] + case '\u1E26': // Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] + case '\u1E28': // Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] + case '\u1E2A': // Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] + case '\u24BD': // Ⓗ [CIRCLED LATIN CAPITAL LETTER H] + case '\u2C67': // Ⱨ [LATIN CAPITAL LETTER H WITH DESCENDER] + case '\u2C75': // Ⱶ [LATIN CAPITAL LETTER HALF H] + case '\uFF28': // H [FULLWIDTH LATIN CAPITAL LETTER H] + output[outputPos++] = 'H'; + break; + case '\u0125': // ĥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] + case '\u0127': // ħ [LATIN SMALL LETTER H WITH STROKE] + case '\u021F': // ȟ [LATIN SMALL LETTER H WITH CARON] + case '\u0265': // ɥ [LATIN SMALL LETTER TURNED H] + case '\u0266': // ɦ [LATIN SMALL LETTER H WITH HOOK] + case '\u02AE': // ʮ [LATIN SMALL LETTER TURNED H WITH FISHHOOK] + case '\u02AF': // ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] + case '\u1E23': // ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] + case '\u1E25': // ḥ [LATIN SMALL LETTER H WITH DOT BELOW] + case '\u1E27': // ḧ [LATIN SMALL LETTER H WITH DIAERESIS] + case '\u1E29': // ḩ [LATIN SMALL LETTER H WITH CEDILLA] + case '\u1E2B': // ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] + case '\u1E96': // ẖ [LATIN SMALL LETTER H WITH LINE BELOW] + case '\u24D7': // ⓗ [CIRCLED LATIN SMALL LETTER H] + case '\u2C68': // ⱨ [LATIN SMALL LETTER H WITH DESCENDER] + case '\u2C76': // ⱶ [LATIN SMALL LETTER HALF H] + case '\uFF48': // h [FULLWIDTH LATIN SMALL LETTER H] + output[outputPos++] = 'h'; + break; + case '\u01F6': // Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] + output[outputPos++] = 'H'; + output[outputPos++] = 'V'; + break; + case '\u24A3': // ⒣ [PARENTHESIZED LATIN SMALL LETTER H] + output[outputPos++] = '('; + output[outputPos++] = 'h'; + output[outputPos++] = ')'; + break; + case '\u0195': // ƕ [LATIN SMALL LETTER HV] + output[outputPos++] = 'h'; + output[outputPos++] = 'v'; + break; + case '\u00CC': // Ì [LATIN CAPITAL LETTER I WITH GRAVE] + case '\u00CD': // Í [LATIN CAPITAL LETTER I WITH ACUTE] + case '\u00CE': // Î [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] + case '\u00CF': // Ï [LATIN CAPITAL LETTER I WITH DIAERESIS] + case '\u0128': // Ĩ [LATIN CAPITAL LETTER I WITH TILDE] + case '\u012A': // Ī [LATIN CAPITAL LETTER I WITH MACRON] + case '\u012C': // Ĭ [LATIN CAPITAL LETTER I WITH BREVE] + case '\u012E': // Į [LATIN CAPITAL LETTER I WITH OGONEK] + case '\u0130': // İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] + case '\u0196': // Ɩ [LATIN CAPITAL LETTER IOTA] + case '\u0197': // Ɨ [LATIN CAPITAL LETTER I WITH STROKE] + case '\u01CF': // Ǐ [LATIN CAPITAL LETTER I WITH CARON] + case '\u0208': // Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] + case '\u020A': // Ȋ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] + case '\u026A': // ɪ [LATIN LETTER SMALL CAPITAL I] + case '\u1D7B': // ᵻ [LATIN SMALL CAPITAL LETTER I WITH STROKE] + case '\u1E2C': // Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] + case '\u1E2E': // Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] + case '\u1EC8': // Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] + case '\u1ECA': // Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] + case '\u24BE': // Ⓘ [CIRCLED LATIN CAPITAL LETTER I] + case '\uA7FE': // ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] + case '\uFF29': // I [FULLWIDTH LATIN CAPITAL LETTER I] + output[outputPos++] = 'I'; + break; + case '\u00EC': // ì [LATIN SMALL LETTER I WITH GRAVE] + case '\u00ED': // í [LATIN SMALL LETTER I WITH ACUTE] + case '\u00EE': // î [LATIN SMALL LETTER I WITH CIRCUMFLEX] + case '\u00EF': // ï [LATIN SMALL LETTER I WITH DIAERESIS] + case '\u0129': // ĩ [LATIN SMALL LETTER I WITH TILDE] + case '\u012B': // ī [LATIN SMALL LETTER I WITH MACRON] + case '\u012D': // ĭ [LATIN SMALL LETTER I WITH BREVE] + case '\u012F': // į [LATIN SMALL LETTER I WITH OGONEK] + case '\u0131': // ı [LATIN SMALL LETTER DOTLESS I] + case '\u01D0': // ǐ [LATIN SMALL LETTER I WITH CARON] + case '\u0209': // ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] + case '\u020B': // ȋ [LATIN SMALL LETTER I WITH INVERTED BREVE] + case '\u0268': // ɨ [LATIN SMALL LETTER I WITH STROKE] + case '\u1D09': // ᴉ [LATIN SMALL LETTER TURNED I] + case '\u1D62': // ᵢ [LATIN SUBSCRIPT SMALL LETTER I] + case '\u1D7C': // ᵼ [LATIN SMALL LETTER IOTA WITH STROKE] + case '\u1D96': // ᶖ [LATIN SMALL LETTER I WITH RETROFLEX HOOK] + case '\u1E2D': // ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] + case '\u1E2F': // ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] + case '\u1EC9': // ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] + case '\u1ECB': // ị [LATIN SMALL LETTER I WITH DOT BELOW] + case '\u2071': // ⁱ [SUPERSCRIPT LATIN SMALL LETTER I] + case '\u24D8': // ⓘ [CIRCLED LATIN SMALL LETTER I] + case '\uFF49': // i [FULLWIDTH LATIN SMALL LETTER I] + output[outputPos++] = 'i'; + break; + case '\u0132': // IJ [LATIN CAPITAL LIGATURE IJ] + output[outputPos++] = 'I'; + output[outputPos++] = 'J'; + break; + case '\u24A4': // ⒤ [PARENTHESIZED LATIN SMALL LETTER I] + output[outputPos++] = '('; + output[outputPos++] = 'i'; + output[outputPos++] = ')'; + break; + case '\u0133': // ij [LATIN SMALL LIGATURE IJ] + output[outputPos++] = 'i'; + output[outputPos++] = 'j'; + break; + case '\u0134': // Ĵ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] + case '\u0248': // Ɉ [LATIN CAPITAL LETTER J WITH STROKE] + case '\u1D0A': // ᴊ [LATIN LETTER SMALL CAPITAL J] + case '\u24BF': // Ⓙ [CIRCLED LATIN CAPITAL LETTER J] + case '\uFF2A': // J [FULLWIDTH LATIN CAPITAL LETTER J] + output[outputPos++] = 'J'; + break; + case '\u0135': // ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] + case '\u01F0': // ǰ [LATIN SMALL LETTER J WITH CARON] + case '\u0237': // ȷ [LATIN SMALL LETTER DOTLESS J] + case '\u0249': // ɉ [LATIN SMALL LETTER J WITH STROKE] + case '\u025F': // ɟ [LATIN SMALL LETTER DOTLESS J WITH STROKE] + case '\u0284': // ʄ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] + case '\u029D': // ʝ [LATIN SMALL LETTER J WITH CROSSED-TAIL] + case '\u24D9': // ⓙ [CIRCLED LATIN SMALL LETTER J] + case '\u2C7C': // ⱼ [LATIN SUBSCRIPT SMALL LETTER J] + case '\uFF4A': // j [FULLWIDTH LATIN SMALL LETTER J] + output[outputPos++] = 'j'; + break; + case '\u24A5': // ⒥ [PARENTHESIZED LATIN SMALL LETTER J] + output[outputPos++] = '('; + output[outputPos++] = 'j'; + output[outputPos++] = ')'; + break; + case '\u0136': // Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] + case '\u0198': // Ƙ [LATIN CAPITAL LETTER K WITH HOOK] + case '\u01E8': // Ǩ [LATIN CAPITAL LETTER K WITH CARON] + case '\u1D0B': // ᴋ [LATIN LETTER SMALL CAPITAL K] + case '\u1E30': // Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] + case '\u1E32': // Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] + case '\u1E34': // Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] + case '\u24C0': // Ⓚ [CIRCLED LATIN CAPITAL LETTER K] + case '\u2C69': // Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] + case '\uA740': // Ꝁ [LATIN CAPITAL LETTER K WITH STROKE] + case '\uA742': // Ꝃ [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] + case '\uA744': // Ꝅ [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] + case '\uFF2B': // K [FULLWIDTH LATIN CAPITAL LETTER K] + output[outputPos++] = 'K'; + break; + case '\u0137': // ķ [LATIN SMALL LETTER K WITH CEDILLA] + case '\u0199': // ƙ [LATIN SMALL LETTER K WITH HOOK] + case '\u01E9': // ǩ [LATIN SMALL LETTER K WITH CARON] + case '\u029E': // ʞ [LATIN SMALL LETTER TURNED K] + case '\u1D84': // ᶄ [LATIN SMALL LETTER K WITH PALATAL HOOK] + case '\u1E31': // ḱ [LATIN SMALL LETTER K WITH ACUTE] + case '\u1E33': // ḳ [LATIN SMALL LETTER K WITH DOT BELOW] + case '\u1E35': // ḵ [LATIN SMALL LETTER K WITH LINE BELOW] + case '\u24DA': // ⓚ [CIRCLED LATIN SMALL LETTER K] + case '\u2C6A': // ⱪ [LATIN SMALL LETTER K WITH DESCENDER] + case '\uA741': // ꝁ [LATIN SMALL LETTER K WITH STROKE] + case '\uA743': // ꝃ [LATIN SMALL LETTER K WITH DIAGONAL STROKE] + case '\uA745': // ꝅ [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] + case '\uFF4B': // k [FULLWIDTH LATIN SMALL LETTER K] + output[outputPos++] = 'k'; + break; + case '\u24A6': // ⒦ [PARENTHESIZED LATIN SMALL LETTER K] + output[outputPos++] = '('; + output[outputPos++] = 'k'; + output[outputPos++] = ')'; + break; + case '\u0139': // Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] + case '\u013B': // Ļ [LATIN CAPITAL LETTER L WITH CEDILLA] + case '\u013D': // Ľ [LATIN CAPITAL LETTER L WITH CARON] + case '\u013F': // Ŀ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] + case '\u0141': // Ł [LATIN CAPITAL LETTER L WITH STROKE] + case '\u023D': // Ƚ [LATIN CAPITAL LETTER L WITH BAR] + case '\u029F': // ʟ [LATIN LETTER SMALL CAPITAL L] + case '\u1D0C': // ᴌ [LATIN LETTER SMALL CAPITAL L WITH STROKE] + case '\u1E36': // Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] + case '\u1E38': // Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] + case '\u1E3A': // Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] + case '\u1E3C': // Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] + case '\u24C1': // Ⓛ [CIRCLED LATIN CAPITAL LETTER L] + case '\u2C60': // Ⱡ [LATIN CAPITAL LETTER L WITH DOUBLE BAR] + case '\u2C62': // Ɫ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] + case '\uA746': // Ꝇ [LATIN CAPITAL LETTER BROKEN L] + case '\uA748': // Ꝉ [LATIN CAPITAL LETTER L WITH HIGH STROKE] + case '\uA780': // Ꞁ [LATIN CAPITAL LETTER TURNED L] + case '\uFF2C': // L [FULLWIDTH LATIN CAPITAL LETTER L] + output[outputPos++] = 'L'; + break; + case '\u013A': // ĺ [LATIN SMALL LETTER L WITH ACUTE] + case '\u013C': // ļ [LATIN SMALL LETTER L WITH CEDILLA] + case '\u013E': // ľ [LATIN SMALL LETTER L WITH CARON] + case '\u0140': // ŀ [LATIN SMALL LETTER L WITH MIDDLE DOT] + case '\u0142': // ł [LATIN SMALL LETTER L WITH STROKE] + case '\u019A': // ƚ [LATIN SMALL LETTER L WITH BAR] + case '\u0234': // ȴ [LATIN SMALL LETTER L WITH CURL] + case '\u026B': // ɫ [LATIN SMALL LETTER L WITH MIDDLE TILDE] + case '\u026C': // ɬ [LATIN SMALL LETTER L WITH BELT] + case '\u026D': // ɭ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] + case '\u1D85': // ᶅ [LATIN SMALL LETTER L WITH PALATAL HOOK] + case '\u1E37': // ḷ [LATIN SMALL LETTER L WITH DOT BELOW] + case '\u1E39': // ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] + case '\u1E3B': // ḻ [LATIN SMALL LETTER L WITH LINE BELOW] + case '\u1E3D': // ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] + case '\u24DB': // ⓛ [CIRCLED LATIN SMALL LETTER L] + case '\u2C61': // ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] + case '\uA747': // ꝇ [LATIN SMALL LETTER BROKEN L] + case '\uA749': // ꝉ [LATIN SMALL LETTER L WITH HIGH STROKE] + case '\uA781': // ꞁ [LATIN SMALL LETTER TURNED L] + case '\uFF4C': // l [FULLWIDTH LATIN SMALL LETTER L] + output[outputPos++] = 'l'; + break; + case '\u01C7': // LJ [LATIN CAPITAL LETTER LJ] + output[outputPos++] = 'L'; + output[outputPos++] = 'J'; + break; + case '\u1EFA': // Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] + output[outputPos++] = 'L'; + output[outputPos++] = 'L'; + break; + case '\u01C8': // Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] + output[outputPos++] = 'L'; + output[outputPos++] = 'j'; + break; + case '\u24A7': // ⒧ [PARENTHESIZED LATIN SMALL LETTER L] + output[outputPos++] = '('; + output[outputPos++] = 'l'; + output[outputPos++] = ')'; + break; + case '\u01C9': // lj [LATIN SMALL LETTER LJ] + output[outputPos++] = 'l'; + output[outputPos++] = 'j'; + break; + case '\u1EFB': // ỻ [LATIN SMALL LETTER MIDDLE-WELSH LL] + output[outputPos++] = 'l'; + output[outputPos++] = 'l'; + break; + case '\u02AA': // ʪ [LATIN SMALL LETTER LS DIGRAPH] + output[outputPos++] = 'l'; + output[outputPos++] = 's'; + break; + case '\u02AB': // ʫ [LATIN SMALL LETTER LZ DIGRAPH] + output[outputPos++] = 'l'; + output[outputPos++] = 'z'; + break; + case '\u019C': // Ɯ [LATIN CAPITAL LETTER TURNED M] + case '\u1D0D': // ᴍ [LATIN LETTER SMALL CAPITAL M] + case '\u1E3E': // Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] + case '\u1E40': // Ṁ [LATIN CAPITAL LETTER M WITH DOT ABOVE] + case '\u1E42': // Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] + case '\u24C2': // Ⓜ [CIRCLED LATIN CAPITAL LETTER M] + case '\u2C6E': // Ɱ [LATIN CAPITAL LETTER M WITH HOOK] + case '\uA7FD': // ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] + case '\uA7FF': // ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] + case '\uFF2D': // M [FULLWIDTH LATIN CAPITAL LETTER M] + output[outputPos++] = 'M'; + break; + case '\u026F': // ɯ [LATIN SMALL LETTER TURNED M] + case '\u0270': // ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] + case '\u0271': // ɱ [LATIN SMALL LETTER M WITH HOOK] + case '\u1D6F': // ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] + case '\u1D86': // ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] + case '\u1E3F': // ḿ [LATIN SMALL LETTER M WITH ACUTE] + case '\u1E41': // ṁ [LATIN SMALL LETTER M WITH DOT ABOVE] + case '\u1E43': // ṃ [LATIN SMALL LETTER M WITH DOT BELOW] + case '\u24DC': // ⓜ [CIRCLED LATIN SMALL LETTER M] + case '\uFF4D': // m [FULLWIDTH LATIN SMALL LETTER M] + output[outputPos++] = 'm'; + break; + case '\u24A8': // ⒨ [PARENTHESIZED LATIN SMALL LETTER M] + output[outputPos++] = '('; + output[outputPos++] = 'm'; + output[outputPos++] = ')'; + break; + case '\u00D1': // Ñ [LATIN CAPITAL LETTER N WITH TILDE] + case '\u0143': // Ń [LATIN CAPITAL LETTER N WITH ACUTE] + case '\u0145': // Ņ [LATIN CAPITAL LETTER N WITH CEDILLA] + case '\u0147': // Ň [LATIN CAPITAL LETTER N WITH CARON] + case '\u014A': // Ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] + case '\u019D': // Ɲ [LATIN CAPITAL LETTER N WITH LEFT HOOK] + case '\u01F8': // Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] + case '\u0220': // Ƞ [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] + case '\u0274': // ɴ [LATIN LETTER SMALL CAPITAL N] + case '\u1D0E': // ᴎ [LATIN LETTER SMALL CAPITAL REVERSED N] + case '\u1E44': // Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] + case '\u1E46': // Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] + case '\u1E48': // Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] + case '\u1E4A': // Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] + case '\u24C3': // Ⓝ [CIRCLED LATIN CAPITAL LETTER N] + case '\uFF2E': // N [FULLWIDTH LATIN CAPITAL LETTER N] + output[outputPos++] = 'N'; + break; + case '\u00F1': // ñ [LATIN SMALL LETTER N WITH TILDE] + case '\u0144': // ń [LATIN SMALL LETTER N WITH ACUTE] + case '\u0146': // ņ [LATIN SMALL LETTER N WITH CEDILLA] + case '\u0148': // ň [LATIN SMALL LETTER N WITH CARON] + case '\u0149': // ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] + case '\u014B': // ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] + case '\u019E': // ƞ [LATIN SMALL LETTER N WITH LONG RIGHT LEG] + case '\u01F9': // ǹ [LATIN SMALL LETTER N WITH GRAVE] + case '\u0235': // ȵ [LATIN SMALL LETTER N WITH CURL] + case '\u0272': // ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] + case '\u0273': // ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] + case '\u1D70': // ᵰ [LATIN SMALL LETTER N WITH MIDDLE TILDE] + case '\u1D87': // ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] + case '\u1E45': // ṅ [LATIN SMALL LETTER N WITH DOT ABOVE] + case '\u1E47': // ṇ [LATIN SMALL LETTER N WITH DOT BELOW] + case '\u1E49': // ṉ [LATIN SMALL LETTER N WITH LINE BELOW] + case '\u1E4B': // ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] + case '\u207F': // ⁿ [SUPERSCRIPT LATIN SMALL LETTER N] + case '\u24DD': // ⓝ [CIRCLED LATIN SMALL LETTER N] + case '\uFF4E': // n [FULLWIDTH LATIN SMALL LETTER N] + output[outputPos++] = 'n'; + break; + case '\u01CA': // NJ [LATIN CAPITAL LETTER NJ] + output[outputPos++] = 'N'; + output[outputPos++] = 'J'; + break; + case '\u01CB': // Nj [LATIN CAPITAL LETTER N WITH SMALL LETTER J] + output[outputPos++] = 'N'; + output[outputPos++] = 'j'; + break; + case '\u24A9': // ⒩ [PARENTHESIZED LATIN SMALL LETTER N] + output[outputPos++] = '('; + output[outputPos++] = 'n'; + output[outputPos++] = ')'; + break; + case '\u01CC': // nj [LATIN SMALL LETTER NJ] + output[outputPos++] = 'n'; + output[outputPos++] = 'j'; + break; + case '\u00D2': // Ò [LATIN CAPITAL LETTER O WITH GRAVE] + case '\u00D3': // Ó [LATIN CAPITAL LETTER O WITH ACUTE] + case '\u00D4': // Ô [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] + case '\u00D5': // Õ [LATIN CAPITAL LETTER O WITH TILDE] + case '\u00D6': // Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] + case '\u00D8': // Ø [LATIN CAPITAL LETTER O WITH STROKE] + case '\u014C': // Ō [LATIN CAPITAL LETTER O WITH MACRON] + case '\u014E': // Ŏ [LATIN CAPITAL LETTER O WITH BREVE] + case '\u0150': // Ő [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] + case '\u0186': // Ɔ [LATIN CAPITAL LETTER OPEN O] + case '\u019F': // Ɵ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] + case '\u01A0': // Ơ [LATIN CAPITAL LETTER O WITH HORN] + case '\u01D1': // Ǒ [LATIN CAPITAL LETTER O WITH CARON] + case '\u01EA': // Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] + case '\u01EC': // Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] + case '\u01FE': // Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] + case '\u020C': // Ȍ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] + case '\u020E': // Ȏ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] + case '\u022A': // Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] + case '\u022C': // Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] + case '\u022E': // Ȯ [LATIN CAPITAL LETTER O WITH DOT ABOVE] + case '\u0230': // Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] + case '\u1D0F': // ᴏ [LATIN LETTER SMALL CAPITAL O] + case '\u1D10': // ᴐ [LATIN LETTER SMALL CAPITAL OPEN O] + case '\u1E4C': // Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] + case '\u1E4E': // Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] + case '\u1E50': // Ṑ [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] + case '\u1E52': // Ṓ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] + case '\u1ECC': // Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] + case '\u1ECE': // Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] + case '\u1ED0': // Ố [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] + case '\u1ED2': // Ồ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] + case '\u1ED4': // Ổ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1ED6': // Ỗ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] + case '\u1ED8': // Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EDA': // Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] + case '\u1EDC': // Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] + case '\u1EDE': // Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] + case '\u1EE0': // Ỡ [LATIN CAPITAL LETTER O WITH HORN AND TILDE] + case '\u1EE2': // Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] + case '\u24C4': // Ⓞ [CIRCLED LATIN CAPITAL LETTER O] + case '\uA74A': // Ꝋ [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] + case '\uA74C': // Ꝍ [LATIN CAPITAL LETTER O WITH LOOP] + case '\uFF2F': // O [FULLWIDTH LATIN CAPITAL LETTER O] + output[outputPos++] = 'O'; + break; + case '\u00F2': // ò [LATIN SMALL LETTER O WITH GRAVE] + case '\u00F3': // ó [LATIN SMALL LETTER O WITH ACUTE] + case '\u00F4': // ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] + case '\u00F5': // õ [LATIN SMALL LETTER O WITH TILDE] + case '\u00F6': // ö [LATIN SMALL LETTER O WITH DIAERESIS] + case '\u00F8': // ø [LATIN SMALL LETTER O WITH STROKE] + case '\u014D': // ō [LATIN SMALL LETTER O WITH MACRON] + case '\u014F': // ŏ [LATIN SMALL LETTER O WITH BREVE] + case '\u0151': // ő [LATIN SMALL LETTER O WITH DOUBLE ACUTE] + case '\u01A1': // ơ [LATIN SMALL LETTER O WITH HORN] + case '\u01D2': // ǒ [LATIN SMALL LETTER O WITH CARON] + case '\u01EB': // ǫ [LATIN SMALL LETTER O WITH OGONEK] + case '\u01ED': // ǭ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] + case '\u01FF': // ǿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] + case '\u020D': // ȍ [LATIN SMALL LETTER O WITH DOUBLE GRAVE] + case '\u020F': // ȏ [LATIN SMALL LETTER O WITH INVERTED BREVE] + case '\u022B': // ȫ [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] + case '\u022D': // ȭ [LATIN SMALL LETTER O WITH TILDE AND MACRON] + case '\u022F': // ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] + case '\u0231': // ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] + case '\u0254': // ɔ [LATIN SMALL LETTER OPEN O] + case '\u0275': // ɵ [LATIN SMALL LETTER BARRED O] + case '\u1D16': // ᴖ [LATIN SMALL LETTER TOP HALF O] + case '\u1D17': // ᴗ [LATIN SMALL LETTER BOTTOM HALF O] + case '\u1D97': // ᶗ [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] + case '\u1E4D': // ṍ [LATIN SMALL LETTER O WITH TILDE AND ACUTE] + case '\u1E4F': // ṏ [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] + case '\u1E51': // ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] + case '\u1E53': // ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] + case '\u1ECD': // ọ [LATIN SMALL LETTER O WITH DOT BELOW] + case '\u1ECF': // ỏ [LATIN SMALL LETTER O WITH HOOK ABOVE] + case '\u1ED1': // ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] + case '\u1ED3': // ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] + case '\u1ED5': // ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1ED7': // ỗ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] + case '\u1ED9': // ộ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EDB': // ớ [LATIN SMALL LETTER O WITH HORN AND ACUTE] + case '\u1EDD': // ờ [LATIN SMALL LETTER O WITH HORN AND GRAVE] + case '\u1EDF': // ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] + case '\u1EE1': // ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] + case '\u1EE3': // ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] + case '\u2092': // ₒ [LATIN SUBSCRIPT SMALL LETTER O] + case '\u24DE': // ⓞ [CIRCLED LATIN SMALL LETTER O] + case '\u2C7A': // ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] + case '\uA74B': // ꝋ [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] + case '\uA74D': // ꝍ [LATIN SMALL LETTER O WITH LOOP] + case '\uFF4F': // o [FULLWIDTH LATIN SMALL LETTER O] + output[outputPos++] = 'o'; + break; + case '\u0152': // Œ [LATIN CAPITAL LIGATURE OE] + case '\u0276': // ɶ [LATIN LETTER SMALL CAPITAL OE] + output[outputPos++] = 'O'; + output[outputPos++] = 'E'; + break; + case '\uA74E': // Ꝏ [LATIN CAPITAL LETTER OO] + output[outputPos++] = 'O'; + output[outputPos++] = 'O'; + break; + case '\u0222': // Ȣ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] + case '\u1D15': // ᴕ [LATIN LETTER SMALL CAPITAL OU] + output[outputPos++] = 'O'; + output[outputPos++] = 'U'; + break; + case '\u24AA': // ⒪ [PARENTHESIZED LATIN SMALL LETTER O] + output[outputPos++] = '('; + output[outputPos++] = 'o'; + output[outputPos++] = ')'; + break; + case '\u0153': // œ [LATIN SMALL LIGATURE OE] + case '\u1D14': // ᴔ [LATIN SMALL LETTER TURNED OE] + output[outputPos++] = 'o'; + output[outputPos++] = 'e'; + break; + case '\uA74F': // ꝏ [LATIN SMALL LETTER OO] + output[outputPos++] = 'o'; + output[outputPos++] = 'o'; + break; + case '\u0223': // ȣ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] + output[outputPos++] = 'o'; + output[outputPos++] = 'u'; + break; + case '\u01A4': // Ƥ [LATIN CAPITAL LETTER P WITH HOOK] + case '\u1D18': // ᴘ [LATIN LETTER SMALL CAPITAL P] + case '\u1E54': // Ṕ [LATIN CAPITAL LETTER P WITH ACUTE] + case '\u1E56': // Ṗ [LATIN CAPITAL LETTER P WITH DOT ABOVE] + case '\u24C5': // Ⓟ [CIRCLED LATIN CAPITAL LETTER P] + case '\u2C63': // Ᵽ [LATIN CAPITAL LETTER P WITH STROKE] + case '\uA750': // Ꝑ [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] + case '\uA752': // Ꝓ [LATIN CAPITAL LETTER P WITH FLOURISH] + case '\uA754': // Ꝕ [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] + case '\uFF30': // P [FULLWIDTH LATIN CAPITAL LETTER P] + output[outputPos++] = 'P'; + break; + case '\u01A5': // ƥ [LATIN SMALL LETTER P WITH HOOK] + case '\u1D71': // ᵱ [LATIN SMALL LETTER P WITH MIDDLE TILDE] + case '\u1D7D': // ᵽ [LATIN SMALL LETTER P WITH STROKE] + case '\u1D88': // ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] + case '\u1E55': // ṕ [LATIN SMALL LETTER P WITH ACUTE] + case '\u1E57': // ṗ [LATIN SMALL LETTER P WITH DOT ABOVE] + case '\u24DF': // ⓟ [CIRCLED LATIN SMALL LETTER P] + case '\uA751': // ꝑ [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] + case '\uA753': // ꝓ [LATIN SMALL LETTER P WITH FLOURISH] + case '\uA755': // ꝕ [LATIN SMALL LETTER P WITH SQUIRREL TAIL] + case '\uA7FC': // ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] + case '\uFF50': // p [FULLWIDTH LATIN SMALL LETTER P] + output[outputPos++] = 'p'; + break; + case '\u24AB': // ⒫ [PARENTHESIZED LATIN SMALL LETTER P] + output[outputPos++] = '('; + output[outputPos++] = 'p'; + output[outputPos++] = ')'; + break; + case '\u024A': // Ɋ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] + case '\u24C6': // Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] + case '\uA756': // Ꝗ [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] + case '\uA758': // Ꝙ [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] + case '\uFF31': // Q [FULLWIDTH LATIN CAPITAL LETTER Q] + output[outputPos++] = 'Q'; + break; + case '\u0138': // ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] + case '\u024B': // ɋ [LATIN SMALL LETTER Q WITH HOOK TAIL] + case '\u02A0': // ʠ [LATIN SMALL LETTER Q WITH HOOK] + case '\u24E0': // ⓠ [CIRCLED LATIN SMALL LETTER Q] + case '\uA757': // ꝗ [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] + case '\uA759': // ꝙ [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] + case '\uFF51': // q [FULLWIDTH LATIN SMALL LETTER Q] + output[outputPos++] = 'q'; + break; + case '\u24AC': // ⒬ [PARENTHESIZED LATIN SMALL LETTER Q] + output[outputPos++] = '('; + output[outputPos++] = 'q'; + output[outputPos++] = ')'; + break; + case '\u0239': // ȹ [LATIN SMALL LETTER QP DIGRAPH] + output[outputPos++] = 'q'; + output[outputPos++] = 'p'; + break; + case '\u0154': // Ŕ [LATIN CAPITAL LETTER R WITH ACUTE] + case '\u0156': // Ŗ [LATIN CAPITAL LETTER R WITH CEDILLA] + case '\u0158': // Ř [LATIN CAPITAL LETTER R WITH CARON] + case '\u0210': // Ȓ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] + case '\u0212': // Ȓ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] + case '\u024C': // Ɍ [LATIN CAPITAL LETTER R WITH STROKE] + case '\u0280': // ʀ [LATIN LETTER SMALL CAPITAL R] + case '\u0281': // ʁ [LATIN LETTER SMALL CAPITAL INVERTED R] + case '\u1D19': // ᴙ [LATIN LETTER SMALL CAPITAL REVERSED R] + case '\u1D1A': // ᴚ [LATIN LETTER SMALL CAPITAL TURNED R] + case '\u1E58': // Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] + case '\u1E5A': // Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] + case '\u1E5C': // Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] + case '\u1E5E': // Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] + case '\u24C7': // Ⓡ [CIRCLED LATIN CAPITAL LETTER R] + case '\u2C64': // Ɽ [LATIN CAPITAL LETTER R WITH TAIL] + case '\uA75A': // Ꝛ [LATIN CAPITAL LETTER R ROTUNDA] + case '\uA782': // Ꞃ [LATIN CAPITAL LETTER INSULAR R] + case '\uFF32': // R [FULLWIDTH LATIN CAPITAL LETTER R] + output[outputPos++] = 'R'; + break; + case '\u0155': // ŕ [LATIN SMALL LETTER R WITH ACUTE] + case '\u0157': // ŗ [LATIN SMALL LETTER R WITH CEDILLA] + case '\u0159': // ř [LATIN SMALL LETTER R WITH CARON] + case '\u0211': // ȑ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] + case '\u0213': // ȓ [LATIN SMALL LETTER R WITH INVERTED BREVE] + case '\u024D': // ɍ [LATIN SMALL LETTER R WITH STROKE] + case '\u027C': // ɼ [LATIN SMALL LETTER R WITH LONG LEG] + case '\u027D': // ɽ [LATIN SMALL LETTER R WITH TAIL] + case '\u027E': // ɾ [LATIN SMALL LETTER R WITH FISHHOOK] + case '\u027F': // ɿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] + case '\u1D63': // ᵣ [LATIN SUBSCRIPT SMALL LETTER R] + case '\u1D72': // ᵲ [LATIN SMALL LETTER R WITH MIDDLE TILDE] + case '\u1D73': // ᵳ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] + case '\u1D89': // ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] + case '\u1E59': // ṙ [LATIN SMALL LETTER R WITH DOT ABOVE] + case '\u1E5B': // ṛ [LATIN SMALL LETTER R WITH DOT BELOW] + case '\u1E5D': // ṝ [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] + case '\u1E5F': // ṟ [LATIN SMALL LETTER R WITH LINE BELOW] + case '\u24E1': // ⓡ [CIRCLED LATIN SMALL LETTER R] + case '\uA75B': // ꝛ [LATIN SMALL LETTER R ROTUNDA] + case '\uA783': // ꞃ [LATIN SMALL LETTER INSULAR R] + case '\uFF52': // r [FULLWIDTH LATIN SMALL LETTER R] + output[outputPos++] = 'r'; + break; + case '\u24AD': // ⒭ [PARENTHESIZED LATIN SMALL LETTER R] + output[outputPos++] = '('; + output[outputPos++] = 'r'; + output[outputPos++] = ')'; + break; + case '\u015A': // Ś [LATIN CAPITAL LETTER S WITH ACUTE] + case '\u015C': // Ŝ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] + case '\u015E': // Ş [LATIN CAPITAL LETTER S WITH CEDILLA] + case '\u0160': // Š [LATIN CAPITAL LETTER S WITH CARON] + case '\u0218': // Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] + case '\u1E60': // Ṡ [LATIN CAPITAL LETTER S WITH DOT ABOVE] + case '\u1E62': // Ṣ [LATIN CAPITAL LETTER S WITH DOT BELOW] + case '\u1E64': // Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] + case '\u1E66': // Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] + case '\u1E68': // Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] + case '\u24C8': // Ⓢ [CIRCLED LATIN CAPITAL LETTER S] + case '\uA731': // ꜱ [LATIN LETTER SMALL CAPITAL S] + case '\uA785': // ꞅ [LATIN SMALL LETTER INSULAR S] + case '\uFF33': // S [FULLWIDTH LATIN CAPITAL LETTER S] + output[outputPos++] = 'S'; + break; + case '\u015B': // ś [LATIN SMALL LETTER S WITH ACUTE] + case '\u015D': // ŝ [LATIN SMALL LETTER S WITH CIRCUMFLEX] + case '\u015F': // ş [LATIN SMALL LETTER S WITH CEDILLA] + case '\u0161': // š [LATIN SMALL LETTER S WITH CARON] + case '\u017F': // ſ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] + case '\u0219': // ș [LATIN SMALL LETTER S WITH COMMA BELOW] + case '\u023F': // ȿ [LATIN SMALL LETTER S WITH SWASH TAIL] + case '\u0282': // ʂ [LATIN SMALL LETTER S WITH HOOK] + case '\u1D74': // ᵴ [LATIN SMALL LETTER S WITH MIDDLE TILDE] + case '\u1D8A': // ᶊ [LATIN SMALL LETTER S WITH PALATAL HOOK] + case '\u1E61': // ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] + case '\u1E63': // ṣ [LATIN SMALL LETTER S WITH DOT BELOW] + case '\u1E65': // ṥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] + case '\u1E67': // ṧ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] + case '\u1E69': // ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] + case '\u1E9C': // ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] + case '\u1E9D': // ẝ [LATIN SMALL LETTER LONG S WITH HIGH STROKE] + case '\u24E2': // ⓢ [CIRCLED LATIN SMALL LETTER S] + case '\uA784': // Ꞅ [LATIN CAPITAL LETTER INSULAR S] + case '\uFF53': // s [FULLWIDTH LATIN SMALL LETTER S] + output[outputPos++] = 's'; + break; + case '\u1E9E': // ẞ [LATIN CAPITAL LETTER SHARP S] + output[outputPos++] = 'S'; + output[outputPos++] = 'S'; + break; + case '\u24AE': // ⒮ [PARENTHESIZED LATIN SMALL LETTER S] + output[outputPos++] = '('; + output[outputPos++] = 's'; + output[outputPos++] = ')'; + break; + case '\u00DF': // ß [LATIN SMALL LETTER SHARP S] + output[outputPos++] = 's'; + output[outputPos++] = 's'; + break; + case '\uFB06': // st [LATIN SMALL LIGATURE ST] + output[outputPos++] = 's'; + output[outputPos++] = 't'; + break; + case '\u0162': // Ţ [LATIN CAPITAL LETTER T WITH CEDILLA] + case '\u0164': // Ť [LATIN CAPITAL LETTER T WITH CARON] + case '\u0166': // Ŧ [LATIN CAPITAL LETTER T WITH STROKE] + case '\u01AC': // Ƭ [LATIN CAPITAL LETTER T WITH HOOK] + case '\u01AE': // Ʈ [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] + case '\u021A': // Ț [LATIN CAPITAL LETTER T WITH COMMA BELOW] + case '\u023E': // Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] + case '\u1D1B': // ᴛ [LATIN LETTER SMALL CAPITAL T] + case '\u1E6A': // Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] + case '\u1E6C': // Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] + case '\u1E6E': // Ṯ [LATIN CAPITAL LETTER T WITH LINE BELOW] + case '\u1E70': // Ṱ [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] + case '\u24C9': // Ⓣ [CIRCLED LATIN CAPITAL LETTER T] + case '\uA786': // Ꞇ [LATIN CAPITAL LETTER INSULAR T] + case '\uFF34': // T [FULLWIDTH LATIN CAPITAL LETTER T] + output[outputPos++] = 'T'; + break; + case '\u0163': // ţ [LATIN SMALL LETTER T WITH CEDILLA] + case '\u0165': // ť [LATIN SMALL LETTER T WITH CARON] + case '\u0167': // ŧ [LATIN SMALL LETTER T WITH STROKE] + case '\u01AB': // ƫ [LATIN SMALL LETTER T WITH PALATAL HOOK] + case '\u01AD': // ƭ [LATIN SMALL LETTER T WITH HOOK] + case '\u021B': // ț [LATIN SMALL LETTER T WITH COMMA BELOW] + case '\u0236': // ȶ [LATIN SMALL LETTER T WITH CURL] + case '\u0287': // ʇ [LATIN SMALL LETTER TURNED T] + case '\u0288': // ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] + case '\u1D75': // ᵵ [LATIN SMALL LETTER T WITH MIDDLE TILDE] + case '\u1E6B': // ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] + case '\u1E6D': // ṭ [LATIN SMALL LETTER T WITH DOT BELOW] + case '\u1E6F': // ṯ [LATIN SMALL LETTER T WITH LINE BELOW] + case '\u1E71': // ṱ [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] + case '\u1E97': // ẗ [LATIN SMALL LETTER T WITH DIAERESIS] + case '\u24E3': // ⓣ [CIRCLED LATIN SMALL LETTER T] + case '\u2C66': // ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] + case '\uFF54': // t [FULLWIDTH LATIN SMALL LETTER T] + output[outputPos++] = 't'; + break; + case '\u00DE': // Þ [LATIN CAPITAL LETTER THORN] + case '\uA766': // Ꝧ [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] + output[outputPos++] = 'T'; + output[outputPos++] = 'H'; + break; + case '\uA728': // Ꜩ [LATIN CAPITAL LETTER TZ] + output[outputPos++] = 'T'; + output[outputPos++] = 'Z'; + break; + case '\u24AF': // ⒯ [PARENTHESIZED LATIN SMALL LETTER T] + output[outputPos++] = '('; + output[outputPos++] = 't'; + output[outputPos++] = ')'; + break; + case '\u02A8': // ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] + output[outputPos++] = 't'; + output[outputPos++] = 'c'; + break; + case '\u00FE': // þ [LATIN SMALL LETTER THORN] + case '\u1D7A': // ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] + case '\uA767': // ꝧ [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] + output[outputPos++] = 't'; + output[outputPos++] = 'h'; + break; + case '\u02A6': // ʦ [LATIN SMALL LETTER TS DIGRAPH] + output[outputPos++] = 't'; + output[outputPos++] = 's'; + break; + case '\uA729': // ꜩ [LATIN SMALL LETTER TZ] + output[outputPos++] = 't'; + output[outputPos++] = 'z'; + break; + case '\u00D9': // Ù [LATIN CAPITAL LETTER U WITH GRAVE] + case '\u00DA': // Ú [LATIN CAPITAL LETTER U WITH ACUTE] + case '\u00DB': // Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] + case '\u00DC': // Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] + case '\u0168': // Ũ [LATIN CAPITAL LETTER U WITH TILDE] + case '\u016A': // Ū [LATIN CAPITAL LETTER U WITH MACRON] + case '\u016C': // Ŭ [LATIN CAPITAL LETTER U WITH BREVE] + case '\u016E': // Ů [LATIN CAPITAL LETTER U WITH RING ABOVE] + case '\u0170': // Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] + case '\u0172': // Ų [LATIN CAPITAL LETTER U WITH OGONEK] + case '\u01AF': // Ư [LATIN CAPITAL LETTER U WITH HORN] + case '\u01D3': // Ǔ [LATIN CAPITAL LETTER U WITH CARON] + case '\u01D5': // Ǖ [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] + case '\u01D7': // Ǘ [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] + case '\u01D9': // Ǚ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] + case '\u01DB': // Ǜ [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] + case '\u0214': // Ȕ [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] + case '\u0216': // Ȗ [LATIN CAPITAL LETTER U WITH INVERTED BREVE] + case '\u0244': // Ʉ [LATIN CAPITAL LETTER U BAR] + case '\u1D1C': // ᴜ [LATIN LETTER SMALL CAPITAL U] + case '\u1D7E': // ᵾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] + case '\u1E72': // Ṳ [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] + case '\u1E74': // Ṵ [LATIN CAPITAL LETTER U WITH TILDE BELOW] + case '\u1E76': // Ṷ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] + case '\u1E78': // Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] + case '\u1E7A': // Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] + case '\u1EE4': // Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] + case '\u1EE6': // Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] + case '\u1EE8': // Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] + case '\u1EEA': // Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] + case '\u1EEC': // Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] + case '\u1EEE': // Ữ [LATIN CAPITAL LETTER U WITH HORN AND TILDE] + case '\u1EF0': // Ự [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] + case '\u24CA': // Ⓤ [CIRCLED LATIN CAPITAL LETTER U] + case '\uFF35': // U [FULLWIDTH LATIN CAPITAL LETTER U] + output[outputPos++] = 'U'; + break; + case '\u00F9': // ù [LATIN SMALL LETTER U WITH GRAVE] + case '\u00FA': // ú [LATIN SMALL LETTER U WITH ACUTE] + case '\u00FB': // û [LATIN SMALL LETTER U WITH CIRCUMFLEX] + case '\u00FC': // ü [LATIN SMALL LETTER U WITH DIAERESIS] + case '\u0169': // ũ [LATIN SMALL LETTER U WITH TILDE] + case '\u016B': // ū [LATIN SMALL LETTER U WITH MACRON] + case '\u016D': // ŭ [LATIN SMALL LETTER U WITH BREVE] + case '\u016F': // ů [LATIN SMALL LETTER U WITH RING ABOVE] + case '\u0171': // ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] + case '\u0173': // ų [LATIN SMALL LETTER U WITH OGONEK] + case '\u01B0': // ư [LATIN SMALL LETTER U WITH HORN] + case '\u01D4': // ǔ [LATIN SMALL LETTER U WITH CARON] + case '\u01D6': // ǖ [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] + case '\u01D8': // ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] + case '\u01DA': // ǚ [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] + case '\u01DC': // ǜ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] + case '\u0215': // ȕ [LATIN SMALL LETTER U WITH DOUBLE GRAVE] + case '\u0217': // ȗ [LATIN SMALL LETTER U WITH INVERTED BREVE] + case '\u0289': // ʉ [LATIN SMALL LETTER U BAR] + case '\u1D64': // ᵤ [LATIN SUBSCRIPT SMALL LETTER U] + case '\u1D99': // ᶙ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] + case '\u1E73': // ṳ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] + case '\u1E75': // ṵ [LATIN SMALL LETTER U WITH TILDE BELOW] + case '\u1E77': // ṷ [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] + case '\u1E79': // ṹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] + case '\u1E7B': // ṻ [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] + case '\u1EE5': // ụ [LATIN SMALL LETTER U WITH DOT BELOW] + case '\u1EE7': // ủ [LATIN SMALL LETTER U WITH HOOK ABOVE] + case '\u1EE9': // ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] + case '\u1EEB': // ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] + case '\u1EED': // ử [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] + case '\u1EEF': // ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] + case '\u1EF1': // ự [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] + case '\u24E4': // ⓤ [CIRCLED LATIN SMALL LETTER U] + case '\uFF55': // u [FULLWIDTH LATIN SMALL LETTER U] + output[outputPos++] = 'u'; + break; + case '\u24B0': // ⒰ [PARENTHESIZED LATIN SMALL LETTER U] + output[outputPos++] = '('; + output[outputPos++] = 'u'; + output[outputPos++] = ')'; + break; + case '\u1D6B': // ᵫ [LATIN SMALL LETTER UE] + output[outputPos++] = 'u'; + output[outputPos++] = 'e'; + break; + case '\u01B2': // Ʋ [LATIN CAPITAL LETTER V WITH HOOK] + case '\u0245': // Ʌ [LATIN CAPITAL LETTER TURNED V] + case '\u1D20': // ᴠ [LATIN LETTER SMALL CAPITAL V] + case '\u1E7C': // Ṽ [LATIN CAPITAL LETTER V WITH TILDE] + case '\u1E7E': // Ṿ [LATIN CAPITAL LETTER V WITH DOT BELOW] + case '\u1EFC': // Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] + case '\u24CB': // Ⓥ [CIRCLED LATIN CAPITAL LETTER V] + case '\uA75E': // Ꝟ [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] + case '\uA768': // Ꝩ [LATIN CAPITAL LETTER VEND] + case '\uFF36': // V [FULLWIDTH LATIN CAPITAL LETTER V] + output[outputPos++] = 'V'; + break; + case '\u028B': // ʋ [LATIN SMALL LETTER V WITH HOOK] + case '\u028C': // ʌ [LATIN SMALL LETTER TURNED V] + case '\u1D65': // ᵥ [LATIN SUBSCRIPT SMALL LETTER V] + case '\u1D8C': // ᶌ [LATIN SMALL LETTER V WITH PALATAL HOOK] + case '\u1E7D': // ṽ [LATIN SMALL LETTER V WITH TILDE] + case '\u1E7F': // ṿ [LATIN SMALL LETTER V WITH DOT BELOW] + case '\u24E5': // ⓥ [CIRCLED LATIN SMALL LETTER V] + case '\u2C71': // ⱱ [LATIN SMALL LETTER V WITH RIGHT HOOK] + case '\u2C74': // ⱴ [LATIN SMALL LETTER V WITH CURL] + case '\uA75F': // ꝟ [LATIN SMALL LETTER V WITH DIAGONAL STROKE] + case '\uFF56': // v [FULLWIDTH LATIN SMALL LETTER V] + output[outputPos++] = 'v'; + break; + case '\uA760': // Ꝡ [LATIN CAPITAL LETTER VY] + output[outputPos++] = 'V'; + output[outputPos++] = 'Y'; + break; + case '\u24B1': // ⒱ [PARENTHESIZED LATIN SMALL LETTER V] + output[outputPos++] = '('; + output[outputPos++] = 'v'; + output[outputPos++] = ')'; + break; + case '\uA761': // ꝡ [LATIN SMALL LETTER VY] + output[outputPos++] = 'v'; + output[outputPos++] = 'y'; + break; + case '\u0174': // Ŵ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] + case '\u01F7': // Ƿ http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] + case '\u1D21': // ᴡ [LATIN LETTER SMALL CAPITAL W] + case '\u1E80': // Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] + case '\u1E82': // Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] + case '\u1E84': // Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] + case '\u1E86': // Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] + case '\u1E88': // Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] + case '\u24CC': // Ⓦ [CIRCLED LATIN CAPITAL LETTER W] + case '\u2C72': // Ⱳ [LATIN CAPITAL LETTER W WITH HOOK] + case '\uFF37': // W [FULLWIDTH LATIN CAPITAL LETTER W] + output[outputPos++] = 'W'; + break; + case '\u0175': // ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] + case '\u01BF': // ƿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] + case '\u028D': // ʍ [LATIN SMALL LETTER TURNED W] + case '\u1E81': // ẁ [LATIN SMALL LETTER W WITH GRAVE] + case '\u1E83': // ẃ [LATIN SMALL LETTER W WITH ACUTE] + case '\u1E85': // ẅ [LATIN SMALL LETTER W WITH DIAERESIS] + case '\u1E87': // ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] + case '\u1E89': // ẉ [LATIN SMALL LETTER W WITH DOT BELOW] + case '\u1E98': // ẘ [LATIN SMALL LETTER W WITH RING ABOVE] + case '\u24E6': // ⓦ [CIRCLED LATIN SMALL LETTER W] + case '\u2C73': // ⱳ [LATIN SMALL LETTER W WITH HOOK] + case '\uFF57': // w [FULLWIDTH LATIN SMALL LETTER W] + output[outputPos++] = 'w'; + break; + case '\u24B2': // ⒲ [PARENTHESIZED LATIN SMALL LETTER W] + output[outputPos++] = '('; + output[outputPos++] = 'w'; + output[outputPos++] = ')'; + break; + case '\u1E8A': // Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] + case '\u1E8C': // Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] + case '\u24CD': // Ⓧ [CIRCLED LATIN CAPITAL LETTER X] + case '\uFF38': // X [FULLWIDTH LATIN CAPITAL LETTER X] + output[outputPos++] = 'X'; + break; + case '\u1D8D': // ᶍ [LATIN SMALL LETTER X WITH PALATAL HOOK] + case '\u1E8B': // ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] + case '\u1E8D': // ẍ [LATIN SMALL LETTER X WITH DIAERESIS] + case '\u2093': // ₓ [LATIN SUBSCRIPT SMALL LETTER X] + case '\u24E7': // ⓧ [CIRCLED LATIN SMALL LETTER X] + case '\uFF58': // x [FULLWIDTH LATIN SMALL LETTER X] + output[outputPos++] = 'x'; + break; + case '\u24B3': // ⒳ [PARENTHESIZED LATIN SMALL LETTER X] + output[outputPos++] = '('; + output[outputPos++] = 'x'; + output[outputPos++] = ')'; + break; + case '\u00DD': // Ý [LATIN CAPITAL LETTER Y WITH ACUTE] + case '\u0176': // Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] + case '\u0178': // Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] + case '\u01B3': // Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] + case '\u0232': // Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] + case '\u024E': // Ɏ [LATIN CAPITAL LETTER Y WITH STROKE] + case '\u028F': // ʏ [LATIN LETTER SMALL CAPITAL Y] + case '\u1E8E': // Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] + case '\u1EF2': // Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] + case '\u1EF4': // Ỵ [LATIN CAPITAL LETTER Y WITH DOT BELOW] + case '\u1EF6': // Ỷ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] + case '\u1EF8': // Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] + case '\u1EFE': // Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] + case '\u24CE': // Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] + case '\uFF39': // Y [FULLWIDTH LATIN CAPITAL LETTER Y] + output[outputPos++] = 'Y'; + break; + case '\u00FD': // ý [LATIN SMALL LETTER Y WITH ACUTE] + case '\u00FF': // ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] + case '\u0177': // ŷ [LATIN SMALL LETTER Y WITH CIRCUMFLEX] + case '\u01B4': // ƴ [LATIN SMALL LETTER Y WITH HOOK] + case '\u0233': // ȳ [LATIN SMALL LETTER Y WITH MACRON] + case '\u024F': // ɏ [LATIN SMALL LETTER Y WITH STROKE] + case '\u028E': // ʎ [LATIN SMALL LETTER TURNED Y] + case '\u1E8F': // ẏ [LATIN SMALL LETTER Y WITH DOT ABOVE] + case '\u1E99': // ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] + case '\u1EF3': // ỳ [LATIN SMALL LETTER Y WITH GRAVE] + case '\u1EF5': // ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] + case '\u1EF7': // ỷ [LATIN SMALL LETTER Y WITH HOOK ABOVE] + case '\u1EF9': // ỹ [LATIN SMALL LETTER Y WITH TILDE] + case '\u1EFF': // ỿ [LATIN SMALL LETTER Y WITH LOOP] + case '\u24E8': // ⓨ [CIRCLED LATIN SMALL LETTER Y] + case '\uFF59': // y [FULLWIDTH LATIN SMALL LETTER Y] + output[outputPos++] = 'y'; + break; + case '\u24B4': // ⒴ [PARENTHESIZED LATIN SMALL LETTER Y] + output[outputPos++] = '('; + output[outputPos++] = 'y'; + output[outputPos++] = ')'; + break; + case '\u0179': // Ź [LATIN CAPITAL LETTER Z WITH ACUTE] + case '\u017B': // Ż [LATIN CAPITAL LETTER Z WITH DOT ABOVE] + case '\u017D': // Ž [LATIN CAPITAL LETTER Z WITH CARON] + case '\u01B5': // Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] + case '\u021C': // Ȝ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] + case '\u0224': // Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] + case '\u1D22': // ᴢ [LATIN LETTER SMALL CAPITAL Z] + case '\u1E90': // Ẑ [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] + case '\u1E92': // Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] + case '\u1E94': // Ẕ [LATIN CAPITAL LETTER Z WITH LINE BELOW] + case '\u24CF': // Ⓩ [CIRCLED LATIN CAPITAL LETTER Z] + case '\u2C6B': // Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] + case '\uA762': // Ꝣ [LATIN CAPITAL LETTER VISIGOTHIC Z] + case '\uFF3A': // Z [FULLWIDTH LATIN CAPITAL LETTER Z] + output[outputPos++] = 'Z'; + break; + case '\u017A': // ź [LATIN SMALL LETTER Z WITH ACUTE] + case '\u017C': // ż [LATIN SMALL LETTER Z WITH DOT ABOVE] + case '\u017E': // ž [LATIN SMALL LETTER Z WITH CARON] + case '\u01B6': // ƶ [LATIN SMALL LETTER Z WITH STROKE] + case '\u021D': // ȝ http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] + case '\u0225': // ȥ [LATIN SMALL LETTER Z WITH HOOK] + case '\u0240': // ɀ [LATIN SMALL LETTER Z WITH SWASH TAIL] + case '\u0290': // ʐ [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] + case '\u0291': // ʑ [LATIN SMALL LETTER Z WITH CURL] + case '\u1D76': // ᵶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] + case '\u1D8E': // ᶎ [LATIN SMALL LETTER Z WITH PALATAL HOOK] + case '\u1E91': // ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] + case '\u1E93': // ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] + case '\u1E95': // ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] + case '\u24E9': // ⓩ [CIRCLED LATIN SMALL LETTER Z] + case '\u2C6C': // ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] + case '\uA763': // ꝣ [LATIN SMALL LETTER VISIGOTHIC Z] + case '\uFF5A': // z [FULLWIDTH LATIN SMALL LETTER Z] + output[outputPos++] = 'z'; + break; + case '\u24B5': // ⒵ [PARENTHESIZED LATIN SMALL LETTER Z] + output[outputPos++] = '('; + output[outputPos++] = 'z'; + output[outputPos++] = ')'; + break; + case '\u2070': // ⁰ [SUPERSCRIPT ZERO] + case '\u2080': // ₀ [SUBSCRIPT ZERO] + case '\u24EA': // ⓪ [CIRCLED DIGIT ZERO] + case '\u24FF': // ⓿ [NEGATIVE CIRCLED DIGIT ZERO] + case '\uFF10': // 0 [FULLWIDTH DIGIT ZERO] + output[outputPos++] = '0'; + break; + case '\u00B9': // ¹ [SUPERSCRIPT ONE] + case '\u2081': // ₁ [SUBSCRIPT ONE] + case '\u2460': // ① [CIRCLED DIGIT ONE] + case '\u24F5': // ⓵ [DOUBLE CIRCLED DIGIT ONE] + case '\u2776': // ❶ [DINGBAT NEGATIVE CIRCLED DIGIT ONE] + case '\u2780': // ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] + case '\u278A': // ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] + case '\uFF11': // 1 [FULLWIDTH DIGIT ONE] + output[outputPos++] = '1'; + break; + case '\u2488': // ⒈ [DIGIT ONE FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '.'; + break; + case '\u2474': // ⑴ [PARENTHESIZED DIGIT ONE] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = ')'; + break; + case '\u00B2': // ² [SUPERSCRIPT TWO] + case '\u2082': // ₂ [SUBSCRIPT TWO] + case '\u2461': // ② [CIRCLED DIGIT TWO] + case '\u24F6': // ⓶ [DOUBLE CIRCLED DIGIT TWO] + case '\u2777': // ❷ [DINGBAT NEGATIVE CIRCLED DIGIT TWO] + case '\u2781': // ➁ [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] + case '\u278B': // ➋ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] + case '\uFF12': // 2 [FULLWIDTH DIGIT TWO] + output[outputPos++] = '2'; + break; + case '\u2489': // ⒉ [DIGIT TWO FULL STOP] + output[outputPos++] = '2'; + output[outputPos++] = '.'; + break; + case '\u2475': // ⑵ [PARENTHESIZED DIGIT TWO] + output[outputPos++] = '('; + output[outputPos++] = '2'; + output[outputPos++] = ')'; + break; + case '\u00B3': // ³ [SUPERSCRIPT THREE] + case '\u2083': // ₃ [SUBSCRIPT THREE] + case '\u2462': // ③ [CIRCLED DIGIT THREE] + case '\u24F7': // ⓷ [DOUBLE CIRCLED DIGIT THREE] + case '\u2778': // ❸ [DINGBAT NEGATIVE CIRCLED DIGIT THREE] + case '\u2782': // ➂ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] + case '\u278C': // ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] + case '\uFF13': // 3 [FULLWIDTH DIGIT THREE] + output[outputPos++] = '3'; + break; + case '\u248A': // ⒊ [DIGIT THREE FULL STOP] + output[outputPos++] = '3'; + output[outputPos++] = '.'; + break; + case '\u2476': // ⑶ [PARENTHESIZED DIGIT THREE] + output[outputPos++] = '('; + output[outputPos++] = '3'; + output[outputPos++] = ')'; + break; + case '\u2074': // ⁴ [SUPERSCRIPT FOUR] + case '\u2084': // ₄ [SUBSCRIPT FOUR] + case '\u2463': // ④ [CIRCLED DIGIT FOUR] + case '\u24F8': // ⓸ [DOUBLE CIRCLED DIGIT FOUR] + case '\u2779': // ❹ [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] + case '\u2783': // ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] + case '\u278D': // ➍ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] + case '\uFF14': // 4 [FULLWIDTH DIGIT FOUR] + output[outputPos++] = '4'; + break; + case '\u248B': // ⒋ [DIGIT FOUR FULL STOP] + output[outputPos++] = '4'; + output[outputPos++] = '.'; + break; + case '\u2477': // ⑷ [PARENTHESIZED DIGIT FOUR] + output[outputPos++] = '('; + output[outputPos++] = '4'; + output[outputPos++] = ')'; + break; + case '\u2075': // ⁵ [SUPERSCRIPT FIVE] + case '\u2085': // ₅ [SUBSCRIPT FIVE] + case '\u2464': // ⑤ [CIRCLED DIGIT FIVE] + case '\u24F9': // ⓹ [DOUBLE CIRCLED DIGIT FIVE] + case '\u277A': // ❺ [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] + case '\u2784': // ➄ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] + case '\u278E': // ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] + case '\uFF15': // 5 [FULLWIDTH DIGIT FIVE] + output[outputPos++] = '5'; + break; + case '\u248C': // ⒌ [DIGIT FIVE FULL STOP] + output[outputPos++] = '5'; + output[outputPos++] = '.'; + break; + case '\u2478': // ⑸ [PARENTHESIZED DIGIT FIVE] + output[outputPos++] = '('; + output[outputPos++] = '5'; + output[outputPos++] = ')'; + break; + case '\u2076': // ⁶ [SUPERSCRIPT SIX] + case '\u2086': // ₆ [SUBSCRIPT SIX] + case '\u2465': // ⑥ [CIRCLED DIGIT SIX] + case '\u24FA': // ⓺ [DOUBLE CIRCLED DIGIT SIX] + case '\u277B': // ❻ [DINGBAT NEGATIVE CIRCLED DIGIT SIX] + case '\u2785': // ➅ [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] + case '\u278F': // ➏ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] + case '\uFF16': // 6 [FULLWIDTH DIGIT SIX] + output[outputPos++] = '6'; + break; + case '\u248D': // ⒍ [DIGIT SIX FULL STOP] + output[outputPos++] = '6'; + output[outputPos++] = '.'; + break; + case '\u2479': // ⑹ [PARENTHESIZED DIGIT SIX] + output[outputPos++] = '('; + output[outputPos++] = '6'; + output[outputPos++] = ')'; + break; + case '\u2077': // ⁷ [SUPERSCRIPT SEVEN] + case '\u2087': // ₇ [SUBSCRIPT SEVEN] + case '\u2466': // ⑦ [CIRCLED DIGIT SEVEN] + case '\u24FB': // ⓻ [DOUBLE CIRCLED DIGIT SEVEN] + case '\u277C': // ❼ [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] + case '\u2786': // ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] + case '\u2790': // ➐ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] + case '\uFF17': // 7 [FULLWIDTH DIGIT SEVEN] + output[outputPos++] = '7'; + break; + case '\u248E': // ⒎ [DIGIT SEVEN FULL STOP] + output[outputPos++] = '7'; + output[outputPos++] = '.'; + break; + case '\u247A': // ⑺ [PARENTHESIZED DIGIT SEVEN] + output[outputPos++] = '('; + output[outputPos++] = '7'; + output[outputPos++] = ')'; + break; + case '\u2078': // ⁸ [SUPERSCRIPT EIGHT] + case '\u2088': // ₈ [SUBSCRIPT EIGHT] + case '\u2467': // ⑧ [CIRCLED DIGIT EIGHT] + case '\u24FC': // ⓼ [DOUBLE CIRCLED DIGIT EIGHT] + case '\u277D': // ❽ [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] + case '\u2787': // ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] + case '\u2791': // ➑ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] + case '\uFF18': // 8 [FULLWIDTH DIGIT EIGHT] + output[outputPos++] = '8'; + break; + case '\u248F': // ⒏ [DIGIT EIGHT FULL STOP] + output[outputPos++] = '8'; + output[outputPos++] = '.'; + break; + case '\u247B': // ⑻ [PARENTHESIZED DIGIT EIGHT] + output[outputPos++] = '('; + output[outputPos++] = '8'; + output[outputPos++] = ')'; + break; + case '\u2079': // ⁹ [SUPERSCRIPT NINE] + case '\u2089': // ₉ [SUBSCRIPT NINE] + case '\u2468': // ⑨ [CIRCLED DIGIT NINE] + case '\u24FD': // ⓽ [DOUBLE CIRCLED DIGIT NINE] + case '\u277E': // ❾ [DINGBAT NEGATIVE CIRCLED DIGIT NINE] + case '\u2788': // ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] + case '\u2792': // ➒ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] + case '\uFF19': // 9 [FULLWIDTH DIGIT NINE] + output[outputPos++] = '9'; + break; + case '\u2490': // ⒐ [DIGIT NINE FULL STOP] + output[outputPos++] = '9'; + output[outputPos++] = '.'; + break; + case '\u247C': // ⑼ [PARENTHESIZED DIGIT NINE] + output[outputPos++] = '('; + output[outputPos++] = '9'; + output[outputPos++] = ')'; + break; + case '\u2469': // ⑩ [CIRCLED NUMBER TEN] + case '\u24FE': // ⓾ [DOUBLE CIRCLED NUMBER TEN] + case '\u277F': // ❿ [DINGBAT NEGATIVE CIRCLED NUMBER TEN] + case '\u2789': // ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] + case '\u2793': // ➓ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] + output[outputPos++] = '1'; + output[outputPos++] = '0'; + break; + case '\u2491': // ⒑ [NUMBER TEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '0'; + output[outputPos++] = '.'; + break; + case '\u247D': // ⑽ [PARENTHESIZED NUMBER TEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '0'; + output[outputPos++] = ')'; + break; + case '\u246A': // ⑪ [CIRCLED NUMBER ELEVEN] + case '\u24EB': // ⓫ [NEGATIVE CIRCLED NUMBER ELEVEN] + output[outputPos++] = '1'; + output[outputPos++] = '1'; + break; + case '\u2492': // ⒒ [NUMBER ELEVEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '1'; + output[outputPos++] = '.'; + break; + case '\u247E': // ⑾ [PARENTHESIZED NUMBER ELEVEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '1'; + output[outputPos++] = ')'; + break; + case '\u246B': // ⑫ [CIRCLED NUMBER TWELVE] + case '\u24EC': // ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] + output[outputPos++] = '1'; + output[outputPos++] = '2'; + break; + case '\u2493': // ⒓ [NUMBER TWELVE FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '2'; + output[outputPos++] = '.'; + break; + case '\u247F': // ⑿ [PARENTHESIZED NUMBER TWELVE] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '2'; + output[outputPos++] = ')'; + break; + case '\u246C': // ⑬ [CIRCLED NUMBER THIRTEEN] + case '\u24ED': // ⓭ [NEGATIVE CIRCLED NUMBER THIRTEEN] + output[outputPos++] = '1'; + output[outputPos++] = '3'; + break; + case '\u2494': // ⒔ [NUMBER THIRTEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '3'; + output[outputPos++] = '.'; + break; + case '\u2480': // ⒀ [PARENTHESIZED NUMBER THIRTEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '3'; + output[outputPos++] = ')'; + break; + case '\u246D': // ⑭ [CIRCLED NUMBER FOURTEEN] + case '\u24EE': // ⓮ [NEGATIVE CIRCLED NUMBER FOURTEEN] + output[outputPos++] = '1'; + output[outputPos++] = '4'; + break; + case '\u2495': // ⒕ [NUMBER FOURTEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '4'; + output[outputPos++] = '.'; + break; + case '\u2481': // ⒁ [PARENTHESIZED NUMBER FOURTEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '4'; + output[outputPos++] = ')'; + break; + case '\u246E': // ⑮ [CIRCLED NUMBER FIFTEEN] + case '\u24EF': // ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] + output[outputPos++] = '1'; + output[outputPos++] = '5'; + break; + case '\u2496': // ⒖ [NUMBER FIFTEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '5'; + output[outputPos++] = '.'; + break; + case '\u2482': // ⒂ [PARENTHESIZED NUMBER FIFTEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '5'; + output[outputPos++] = ')'; + break; + case '\u246F': // ⑯ [CIRCLED NUMBER SIXTEEN] + case '\u24F0': // ⓰ [NEGATIVE CIRCLED NUMBER SIXTEEN] + output[outputPos++] = '1'; + output[outputPos++] = '6'; + break; + case '\u2497': // ⒗ [NUMBER SIXTEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '6'; + output[outputPos++] = '.'; + break; + case '\u2483': // ⒃ [PARENTHESIZED NUMBER SIXTEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '6'; + output[outputPos++] = ')'; + break; + case '\u2470': // ⑰ [CIRCLED NUMBER SEVENTEEN] + case '\u24F1': // ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] + output[outputPos++] = '1'; + output[outputPos++] = '7'; + break; + case '\u2498': // ⒘ [NUMBER SEVENTEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '7'; + output[outputPos++] = '.'; + break; + case '\u2484': // ⒄ [PARENTHESIZED NUMBER SEVENTEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '7'; + output[outputPos++] = ')'; + break; + case '\u2471': // ⑱ [CIRCLED NUMBER EIGHTEEN] + case '\u24F2': // ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] + output[outputPos++] = '1'; + output[outputPos++] = '8'; + break; + case '\u2499': // ⒙ [NUMBER EIGHTEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '8'; + output[outputPos++] = '.'; + break; + case '\u2485': // ⒅ [PARENTHESIZED NUMBER EIGHTEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '8'; + output[outputPos++] = ')'; + break; + case '\u2472': // ⑲ [CIRCLED NUMBER NINETEEN] + case '\u24F3': // ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] + output[outputPos++] = '1'; + output[outputPos++] = '9'; + break; + case '\u249A': // ⒚ [NUMBER NINETEEN FULL STOP] + output[outputPos++] = '1'; + output[outputPos++] = '9'; + output[outputPos++] = '.'; + break; + case '\u2486': // ⒆ [PARENTHESIZED NUMBER NINETEEN] + output[outputPos++] = '('; + output[outputPos++] = '1'; + output[outputPos++] = '9'; + output[outputPos++] = ')'; + break; + case '\u2473': // ⑳ [CIRCLED NUMBER TWENTY] + case '\u24F4': // ⓴ [NEGATIVE CIRCLED NUMBER TWENTY] + output[outputPos++] = '2'; + output[outputPos++] = '0'; + break; + case '\u249B': // ⒛ [NUMBER TWENTY FULL STOP] + output[outputPos++] = '2'; + output[outputPos++] = '0'; + output[outputPos++] = '.'; + break; + case '\u2487': // ⒇ [PARENTHESIZED NUMBER TWENTY] + output[outputPos++] = '('; + output[outputPos++] = '2'; + output[outputPos++] = '0'; + output[outputPos++] = ')'; + break; + case '\u00AB': // « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] + case '\u00BB': // » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] + case '\u201C': // “ [LEFT DOUBLE QUOTATION MARK] + case '\u201D': // ” [RIGHT DOUBLE QUOTATION MARK] + case '\u201E': // „ [DOUBLE LOW-9 QUOTATION MARK] + case '\u2033': // ″ [DOUBLE PRIME] + case '\u2036': // ‶ [REVERSED DOUBLE PRIME] + case '\u275D': // ❝ [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] + case '\u275E': // ❞ [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] + case '\u276E': // ❮ [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] + case '\u276F': // ❯ [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] + case '\uFF02': // " [FULLWIDTH QUOTATION MARK] + output[outputPos++] = '"'; + break; + case '\u2018': // ‘ [LEFT SINGLE QUOTATION MARK] + case '\u2019': // ’ [RIGHT SINGLE QUOTATION MARK] + case '\u201A': // ‚ [SINGLE LOW-9 QUOTATION MARK] + case '\u201B': // ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] + case '\u2032': // ′ [PRIME] + case '\u2035': // ‵ [REVERSED PRIME] + case '\u2039': // ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] + case '\u203A': // › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] + case '\u275B': // ❛ [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] + case '\u275C': // ❜ [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] + case '\uFF07': // ' [FULLWIDTH APOSTROPHE] + output[outputPos++] = '\''; + break; + case '\u2010': // ‐ [HYPHEN] + case '\u2011': // ‑ [NON-BREAKING HYPHEN] + case '\u2012': // ‒ [FIGURE DASH] + case '\u2013': // – [EN DASH] + case '\u2014': // — [EM DASH] + case '\u207B': // ⁻ [SUPERSCRIPT MINUS] + case '\u208B': // ₋ [SUBSCRIPT MINUS] + case '\uFF0D': // - [FULLWIDTH HYPHEN-MINUS] + output[outputPos++] = '-'; + break; + case '\u2045': // ⁅ [LEFT SQUARE BRACKET WITH QUILL] + case '\u2772': // ❲ [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] + case '\uFF3B': // [ [FULLWIDTH LEFT SQUARE BRACKET] + output[outputPos++] = '['; + break; + case '\u2046': // ⁆ [RIGHT SQUARE BRACKET WITH QUILL] + case '\u2773': // ❳ [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] + case '\uFF3D': // ] [FULLWIDTH RIGHT SQUARE BRACKET] + output[outputPos++] = ']'; + break; + case '\u207D': // ⁽ [SUPERSCRIPT LEFT PARENTHESIS] + case '\u208D': // ₍ [SUBSCRIPT LEFT PARENTHESIS] + case '\u2768': // ❨ [MEDIUM LEFT PARENTHESIS ORNAMENT] + case '\u276A': // ❪ [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] + case '\uFF08': // ( [FULLWIDTH LEFT PARENTHESIS] + output[outputPos++] = '('; + break; + case '\u2E28': // ⸨ [LEFT DOUBLE PARENTHESIS] + output[outputPos++] = '('; + output[outputPos++] = '('; + break; + case '\u207E': // ⁾ [SUPERSCRIPT RIGHT PARENTHESIS] + case '\u208E': // ₎ [SUBSCRIPT RIGHT PARENTHESIS] + case '\u2769': // ❩ [MEDIUM RIGHT PARENTHESIS ORNAMENT] + case '\u276B': // ❫ [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] + case '\uFF09': // ) [FULLWIDTH RIGHT PARENTHESIS] + output[outputPos++] = ')'; + break; + case '\u2E29': // ⸩ [RIGHT DOUBLE PARENTHESIS] + output[outputPos++] = ')'; + output[outputPos++] = ')'; + break; + case '\u276C': // ❬ [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] + case '\u2770': // ❰ [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] + case '\uFF1C': // < [FULLWIDTH LESS-THAN SIGN] + output[outputPos++] = '<'; + break; + case '\u276D': // ❭ [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] + case '\u2771': // ❱ [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] + case '\uFF1E': // > [FULLWIDTH GREATER-THAN SIGN] + output[outputPos++] = '>'; + break; + case '\u2774': // ❴ [MEDIUM LEFT CURLY BRACKET ORNAMENT] + case '\uFF5B': // { [FULLWIDTH LEFT CURLY BRACKET] + output[outputPos++] = '{'; + break; + case '\u2775': // ❵ [MEDIUM RIGHT CURLY BRACKET ORNAMENT] + case '\uFF5D': // } [FULLWIDTH RIGHT CURLY BRACKET] + output[outputPos++] = '}'; + break; + case '\u207A': // ⁺ [SUPERSCRIPT PLUS SIGN] + case '\u208A': // ₊ [SUBSCRIPT PLUS SIGN] + case '\uFF0B': // + [FULLWIDTH PLUS SIGN] + output[outputPos++] = '+'; + break; + case '\u207C': // ⁼ [SUPERSCRIPT EQUALS SIGN] + case '\u208C': // ₌ [SUBSCRIPT EQUALS SIGN] + case '\uFF1D': // = [FULLWIDTH EQUALS SIGN] + output[outputPos++] = '='; + break; + case '\uFF01': // ! [FULLWIDTH EXCLAMATION MARK] + output[outputPos++] = '!'; + break; + case '\u203C': // ‼ [DOUBLE EXCLAMATION MARK] + output[outputPos++] = '!'; + output[outputPos++] = '!'; + break; + case '\u2049': // ⁉ [EXCLAMATION QUESTION MARK] + output[outputPos++] = '!'; + output[outputPos++] = '?'; + break; + case '\uFF03': // # [FULLWIDTH NUMBER SIGN] + output[outputPos++] = '#'; + break; + case '\uFF04': // $ [FULLWIDTH DOLLAR SIGN] + output[outputPos++] = '$'; + break; + case '\u2052': // ⁒ [COMMERCIAL MINUS SIGN] + case '\uFF05': // % [FULLWIDTH PERCENT SIGN] + output[outputPos++] = '%'; + break; + case '\uFF06': // & [FULLWIDTH AMPERSAND] + output[outputPos++] = '&'; + break; + case '\u204E': // ⁎ [LOW ASTERISK] + case '\uFF0A': // * [FULLWIDTH ASTERISK] + output[outputPos++] = '*'; + break; + case '\uFF0C': // , [FULLWIDTH COMMA] + output[outputPos++] = ','; + break; + case '\uFF0E': // . [FULLWIDTH FULL STOP] + output[outputPos++] = '.'; + break; + case '\u2044': // ⁄ [FRACTION SLASH] + case '\uFF0F': // / [FULLWIDTH SOLIDUS] + output[outputPos++] = '/'; + break; + case '\uFF1A': // : [FULLWIDTH COLON] + output[outputPos++] = ':'; + break; + case '\u204F': // ⁏ [REVERSED SEMICOLON] + case '\uFF1B': // ; [FULLWIDTH SEMICOLON] + output[outputPos++] = ';'; + break; + case '\uFF1F': // ? [FULLWIDTH QUESTION MARK] + output[outputPos++] = '?'; + break; + case '\u2047': // ⁇ [DOUBLE QUESTION MARK] + output[outputPos++] = '?'; + output[outputPos++] = '?'; + break; + case '\u2048': // ⁈ [QUESTION EXCLAMATION MARK] + output[outputPos++] = '?'; + output[outputPos++] = '!'; + break; + case '\uFF20': // @ [FULLWIDTH COMMERCIAL AT] + output[outputPos++] = '@'; + break; + case '\uFF3C': // \ [FULLWIDTH REVERSE SOLIDUS] + output[outputPos++] = '\\'; + break; + case '\u2038': // ‸ [CARET] + case '\uFF3E': // ^ [FULLWIDTH CIRCUMFLEX ACCENT] + output[outputPos++] = '^'; + break; + case '\uFF3F': // _ [FULLWIDTH LOW LINE] + output[outputPos++] = '_'; + break; + case '\u2053': // ⁓ [SWUNG DASH] + case '\uFF5E': // ~ [FULLWIDTH TILDE] + output[outputPos++] = '~'; + break; + default: + output[outputPos++] = c; + break; + } + } + } + return outputPos; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/filter/BuiltInAnalyzers.java b/src/java/org/apache/cassandra/index/sai/analyzer/filter/BuiltInAnalyzers.java new file mode 100644 index 000000000000..d318030c5902 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/analyzer/filter/BuiltInAnalyzers.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.analyzer.filter; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.ar.ArabicAnalyzer; +import org.apache.lucene.analysis.bg.BulgarianAnalyzer; +import org.apache.lucene.analysis.bn.BengaliAnalyzer; +import org.apache.lucene.analysis.br.BrazilianAnalyzer; +import org.apache.lucene.analysis.ca.CatalanAnalyzer; +import org.apache.lucene.analysis.cjk.CJKAnalyzer; +import org.apache.lucene.analysis.ckb.SoraniAnalyzer; +import org.apache.lucene.analysis.core.KeywordAnalyzer; +import org.apache.lucene.analysis.core.SimpleAnalyzer; +import org.apache.lucene.analysis.core.StopAnalyzer; +import org.apache.lucene.analysis.core.WhitespaceAnalyzer; +import org.apache.lucene.analysis.custom.CustomAnalyzer; +import org.apache.lucene.analysis.cz.CzechAnalyzer; +import org.apache.lucene.analysis.da.DanishAnalyzer; +import org.apache.lucene.analysis.de.GermanAnalyzer; +import org.apache.lucene.analysis.el.GreekAnalyzer; +import org.apache.lucene.analysis.en.EnglishAnalyzer; +import org.apache.lucene.analysis.es.SpanishAnalyzer; +import org.apache.lucene.analysis.et.EstonianAnalyzer; +import org.apache.lucene.analysis.eu.BasqueAnalyzer; +import org.apache.lucene.analysis.fa.PersianAnalyzer; +import org.apache.lucene.analysis.fi.FinnishAnalyzer; +import org.apache.lucene.analysis.fr.FrenchAnalyzer; +import org.apache.lucene.analysis.ga.IrishAnalyzer; +import org.apache.lucene.analysis.gl.GalicianAnalyzer; +import org.apache.lucene.analysis.hi.HindiAnalyzer; +import org.apache.lucene.analysis.hu.HungarianAnalyzer; +import org.apache.lucene.analysis.hy.ArmenianAnalyzer; +import org.apache.lucene.analysis.id.IndonesianAnalyzer; +import org.apache.lucene.analysis.it.ItalianAnalyzer; +import org.apache.lucene.analysis.lt.LithuanianAnalyzer; +import org.apache.lucene.analysis.lv.LatvianAnalyzer; +import org.apache.lucene.analysis.nl.DutchAnalyzer; +import org.apache.lucene.analysis.no.NorwegianAnalyzer; +import org.apache.lucene.analysis.pt.PortugueseAnalyzer; +import org.apache.lucene.analysis.ro.RomanianAnalyzer; +import org.apache.lucene.analysis.ru.RussianAnalyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.analysis.sv.SwedishAnalyzer; +import org.apache.lucene.analysis.th.ThaiAnalyzer; +import org.apache.lucene.analysis.tr.TurkishAnalyzer; + +/** + * Built-in {@link Analyzer} implementations. These are provided to allow users to easily configure analyzers with + * a single word. + */ +public enum BuiltInAnalyzers +{ + STANDARD + { + public Analyzer getNewAnalyzer() + { + return new StandardAnalyzer(); + } + }, + SIMPLE + { + public Analyzer getNewAnalyzer() + { + return new SimpleAnalyzer(); + } + }, + WHITESPACE + { + public Analyzer getNewAnalyzer() + { + return new WhitespaceAnalyzer(); + } + }, + STOP + { + public Analyzer getNewAnalyzer() + { + return new StopAnalyzer(EnglishAnalyzer.getDefaultStopSet()); + } + }, + LOWERCASE + { + public Analyzer getNewAnalyzer() + { + try + { + CustomAnalyzer.Builder builder = CustomAnalyzer.builder(); + builder.withTokenizer("keyword"); + builder.addTokenFilter("lowercase"); + return builder.build(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + }, + KEYWORD + { + public Analyzer getNewAnalyzer() + { + try + { + return new KeywordAnalyzer(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + }, + ARABIC + { + public Analyzer getNewAnalyzer() + { + return new ArabicAnalyzer(); + } + }, + ARMENIAN + { + public Analyzer getNewAnalyzer() + { + return new ArmenianAnalyzer(); + } + }, + BASQUE + { + public Analyzer getNewAnalyzer() + { + return new BasqueAnalyzer(); + } + }, + BENGALI + { + public Analyzer getNewAnalyzer() + { + return new BengaliAnalyzer(); + } + }, + BRAZILIAN + { + public Analyzer getNewAnalyzer() + { + return new BrazilianAnalyzer(); + } + }, + BULGARIAN + { + public Analyzer getNewAnalyzer() + { + return new BulgarianAnalyzer(); + } + }, + CATALAN + { + public Analyzer getNewAnalyzer() + { + return new CatalanAnalyzer(); + } + }, + CJK + { + public Analyzer getNewAnalyzer() + { + return new CJKAnalyzer(); + } + }, + CZECH + { + public Analyzer getNewAnalyzer() + { + return new CzechAnalyzer(); + } + }, + DANISH + { + public Analyzer getNewAnalyzer() + { + return new DanishAnalyzer(); + } + }, + DUTCH + { + public Analyzer getNewAnalyzer() + { + return new DutchAnalyzer(); + } + }, + ENGLISH + { + public Analyzer getNewAnalyzer() + { + return new EnglishAnalyzer(); + } + }, + ESTONIAN + { + public Analyzer getNewAnalyzer() + { + return new EstonianAnalyzer(); + } + }, + FINNISH + { + public Analyzer getNewAnalyzer() + { + return new FinnishAnalyzer(); + } + }, + FRENCH + { + public Analyzer getNewAnalyzer() + { + return new FrenchAnalyzer(); + } + }, + GALICIAN + { + public Analyzer getNewAnalyzer() + { + return new GalicianAnalyzer(); + } + }, + GERMAN + { + public Analyzer getNewAnalyzer() + { + return new GermanAnalyzer(); + } + }, + GREEK + { + public Analyzer getNewAnalyzer() + { + return new GreekAnalyzer(); + } + }, + HINDI + { + public Analyzer getNewAnalyzer() + { + return new HindiAnalyzer(); + } + }, + HUNGARIAN + { + public Analyzer getNewAnalyzer() + { + return new HungarianAnalyzer(); + } + }, + INDONESIAN + { + public Analyzer getNewAnalyzer() + { + return new IndonesianAnalyzer(); + } + }, + IRISH + { + public Analyzer getNewAnalyzer() + { + return new IrishAnalyzer(); + } + }, + ITALIAN + { + public Analyzer getNewAnalyzer() + { + return new ItalianAnalyzer(); + } + }, + LATVIAN + { + public Analyzer getNewAnalyzer() + { + return new LatvianAnalyzer(); + } + }, + LITHUANIAN + { + public Analyzer getNewAnalyzer() + { + return new LithuanianAnalyzer(); + } + }, + NORWEGIAN + { + public Analyzer getNewAnalyzer() + { + return new NorwegianAnalyzer(); + } + }, + PERSIAN + { + public Analyzer getNewAnalyzer() + { + return new PersianAnalyzer(); + } + }, + PORTUGUESE + { + public Analyzer getNewAnalyzer() + { + return new PortugueseAnalyzer(); + } + }, + ROMANIAN + { + public Analyzer getNewAnalyzer() + { + return new RomanianAnalyzer(); + } + }, + RUSSIAN + { + public Analyzer getNewAnalyzer() + { + return new RussianAnalyzer(); + } + }, + SORANI + { + public Analyzer getNewAnalyzer() + { + return new SoraniAnalyzer(); + } + }, + SPANISH + { + public Analyzer getNewAnalyzer() + { + return new SpanishAnalyzer(); + } + }, + SWEDISH + { + public Analyzer getNewAnalyzer() + { + return new SwedishAnalyzer(); + } + }, + TURKISH + { + public Analyzer getNewAnalyzer() + { + return new TurkishAnalyzer(); + } + }, + THAI + { + public Analyzer getNewAnalyzer() + { + return new ThaiAnalyzer(); + } + }, + ; + + public abstract Analyzer getNewAnalyzer(); +} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineBuilder.java b/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineBuilder.java similarity index 77% rename from src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineBuilder.java rename to src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineBuilder.java index e9d262d96d52..3a6a72603df2 100644 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineBuilder.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineBuilder.java @@ -15,7 +15,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.cassandra.index.sasi.analyzer.filter; + +package org.apache.cassandra.index.sai.analyzer.filter; /** * Creates a Pipeline object for applying n pieces of logic @@ -23,28 +24,28 @@ */ public class FilterPipelineBuilder { - private final FilterPipelineTask parent; - private FilterPipelineTask current; + private final FilterPipelineTask parent; + private FilterPipelineTask current; - public FilterPipelineBuilder(FilterPipelineTask first) + public FilterPipelineBuilder(FilterPipelineTask first) { this(first, first); } - private FilterPipelineBuilder(FilterPipelineTask first, FilterPipelineTask current) + private FilterPipelineBuilder(FilterPipelineTask first, FilterPipelineTask current) { this.parent = first; this.current = current; } - public FilterPipelineBuilder add(String name, FilterPipelineTask nextTask) + public FilterPipelineBuilder add(String name, FilterPipelineTask nextTask) { this.current.setLast(name, nextTask); this.current = nextTask; return this; } - public FilterPipelineTask build() + public FilterPipelineTask build() { return this.parent; } diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineExecutor.java b/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineExecutor.java index c863f1e3cdb6..dcb04c2a98a4 100644 --- a/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineExecutor.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineExecutor.java @@ -19,22 +19,26 @@ package org.apache.cassandra.index.sai.analyzer.filter; /** - * Executes all linked {@link FilterPipeline.Task}s serially on the provided input and returns a result + * Executes all linked Pipeline Tasks serially and returns + * output (if exists) from the executed logic */ public class FilterPipelineExecutor { - public static String execute(FilterPipeline pipeline, String initialInput) + public static String execute(FilterPipelineTask task, String initialInput) { - FilterPipeline.Task currentTask = pipeline.head(); + FilterPipelineTask taskPtr = task; String result = initialInput; while (true) { - result = currentTask.process(result); - currentTask = currentTask.next; + FilterPipelineTask taskGeneric = taskPtr; + result = taskGeneric.process(result); + taskPtr = taskPtr.next; - if (currentTask == null) + if (taskPtr == null) + { return result; + } } } } diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineTask.java b/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineTask.java similarity index 83% rename from src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineTask.java rename to src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineTask.java index 13e2a174a4d2..b80073e8b3c2 100644 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineTask.java +++ b/src/java/org/apache/cassandra/index/sai/analyzer/filter/FilterPipelineTask.java @@ -15,19 +15,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.cassandra.index.sasi.analyzer.filter; + +package org.apache.cassandra.index.sai.analyzer.filter; /** * A single task or set of work to process an input * and return a single output. Maintains a link to the * next task to be executed after itself */ -public abstract class FilterPipelineTask +public abstract class FilterPipelineTask { private String name; - public FilterPipelineTask next; + public FilterPipelineTask next; - protected void setLast(String name, FilterPipelineTask last) + void setLast(String name, FilterPipelineTask last) { if (last == this) throw new IllegalArgumentException("provided last task [" + last.name + "] cannot be set to itself"); @@ -43,7 +44,7 @@ protected void setLast(String name, FilterPipelineTask last) } } - public abstract T process(F input) throws Exception; + public abstract String process(String input); public String getName() { diff --git a/src/java/org/apache/cassandra/index/sai/disk/ByteSliceReader.java b/src/java/org/apache/cassandra/index/sai/disk/ByteSliceReader.java new file mode 100644 index 000000000000..78660ecaa6ce --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/ByteSliceReader.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk; + +import java.io.DataOutput; +import java.io.IOException; + +import org.apache.lucene.util.BitUtil; +import org.apache.lucene.store.DataInput; +import org.apache.lucene.util.ByteBlockPool; + +/* IndexInput that knows how to read the byte slices written + * by Posting and PostingVector. We read the bytes in + * each slice until we hit the end of that slice at which + * point we read the forwarding address of the next slice + * and then jump to it.*/ +final class ByteSliceReader extends DataInput +{ + ByteBlockPool pool; + int bufferUpto; + byte[] buffer; + public int upto; + int limit; + int level; + public int bufferOffset; + + public int endIndex; + + public void init(ByteBlockPool pool, int startIndex, int endIndex) + { + + assert endIndex - startIndex >= 0 : "startIndex=" + startIndex + " endIndex=" + endIndex; + assert startIndex >= 0; + assert endIndex >= 0; + + this.pool = pool; + this.endIndex = endIndex; + + level = 0; + bufferUpto = startIndex / ByteBlockPool.BYTE_BLOCK_SIZE; + bufferOffset = bufferUpto * ByteBlockPool.BYTE_BLOCK_SIZE; + buffer = pool.buffers[bufferUpto]; + upto = startIndex & ByteBlockPool.BYTE_BLOCK_MASK; + + final int firstSize = ByteBlockPool.LEVEL_SIZE_ARRAY[0]; + + if (startIndex + firstSize >= endIndex) + { + // There is only this one slice to read + limit = endIndex & ByteBlockPool.BYTE_BLOCK_MASK; + } + else + limit = upto + firstSize - 4; + } + + public boolean eof() + { + assert upto + bufferOffset <= endIndex; + return upto + bufferOffset == endIndex; + } + + @Override + public byte readByte() + { + assert !eof(); + assert upto <= limit; + if (upto == limit) + nextSlice(); + return buffer[upto++]; + } + + public long writeTo(DataOutput out) throws IOException + { + long size = 0; + while (true) + { + if (limit + bufferOffset == endIndex) + { + assert endIndex - bufferOffset >= upto; + out.write(buffer, upto, limit - upto); + size += limit - upto; + break; + } + else + { + out.write(buffer, upto, limit - upto); + size += limit - upto; + nextSlice(); + } + } + + return size; + } + + public void nextSlice() + { + + // Skip to our next slice + final int nextIndex = (int) BitUtil.VH_LE_INT.get(buffer, limit); + + level = ByteBlockPool.NEXT_LEVEL_ARRAY[level]; + final int newSize = ByteBlockPool.LEVEL_SIZE_ARRAY[level]; + + bufferUpto = nextIndex / ByteBlockPool.BYTE_BLOCK_SIZE; + bufferOffset = bufferUpto * ByteBlockPool.BYTE_BLOCK_SIZE; + + buffer = pool.buffers[bufferUpto]; + upto = nextIndex & ByteBlockPool.BYTE_BLOCK_MASK; + + if (nextIndex + newSize >= endIndex) + { + // We are advancing to the final slice + assert endIndex - nextIndex > 0; + limit = endIndex - bufferOffset; + } + else + { + // This is not the final slice (subtract 4 for the + // forwarding address at the end of this new slice) + limit = upto + newSize - 4; + } + } + + @Override + public void readBytes(byte[] b, int offset, int len) + { + while (len > 0) + { + final int numLeft = limit - upto; + if (numLeft < len) + { + // Read entire slice + System.arraycopy(buffer, upto, b, offset, numLeft); + offset += numLeft; + len -= numLeft; + nextSlice(); + } + else + { + // This slice is the last one + System.arraycopy(buffer, upto, b, offset, len); + upto += len; + break; + } + } + } + + @Override + public void skipBytes(long l) throws IOException + { + while (l > 0) + { + final int numLeft = limit - upto; + if (numLeft < l) + { + // Skip entire slice + l -= numLeft; + nextSlice(); + } + else + { + // This slice is the last one + upto += l; + break; + } + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/EmptyIndex.java b/src/java/org/apache/cassandra/index/sai/disk/EmptyIndex.java index 4f4046b3290a..c1965a2fafd7 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/EmptyIndex.java +++ b/src/java/org/apache/cassandra/index/sai/disk/EmptyIndex.java @@ -22,39 +22,36 @@ import java.nio.ByteBuffer; import java.util.List; +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.virtual.SimpleDataSet; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.SSTableContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; +import org.apache.cassandra.index.sai.disk.v1.Segment; import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.utils.CloseableIterator; -/** - * A placeholder index for when there is no on-disk index. - * - * Currenly only used by vector indexes becasue ANN queries require a complete view of the table's sstables, even if - * the associated sstable does not have any data indexed for the column. - */ -public class EmptyIndex extends SSTableIndex +public class EmptyIndex implements SearchableIndex { - public EmptyIndex(SSTableContext sstableContext, StorageAttachedIndex index) + @Override + public long indexFileCacheSize() { - super(sstableContext, index); + return 0; } @Override - public long indexFileCacheSize() + public long getRowCount() { return 0; } @Override - public long getRowCount() + public long getApproximateTermCount() { return 0; } @@ -84,38 +81,68 @@ public ByteBuffer maxTerm() } @Override - public AbstractBounds bounds() + public DecoratedKey minKey() { return null; } @Override - public List search(Expression expression, AbstractBounds keyRange, QueryContext context) throws IOException + public DecoratedKey maxKey() { - return List.of(); + return null; + } + + @Override + public KeyRangeIterator search(Expression expression, + AbstractBounds keyRange, + QueryContext context, + boolean defer) throws IOException + { + return KeyRangeIterator.empty(); } @Override - public List> orderBy(Expression orderer, AbstractBounds keyRange, QueryContext context) throws IOException + public List> orderBy(Orderer orderer, + Expression slice, + AbstractBounds keyRange, + QueryContext context, + int limit, + long totalRows) throws IOException { return List.of(); } @Override - public List> orderResultsBy(QueryContext context, List results, Expression orderer) throws IOException + public List getSegments() { return List.of(); } @Override - public void populateSegmentView(SimpleDataSet dataSet) + public void populateSystemView(SimpleDataSet dataSet, SSTableReader sstable) { + // Empty indexes are not visible in the system view, + // as they don't really exist on disk (are not built). + // This is to keep backwards compatibility – before introducing + // this class, empty indexes weren't even included in the SAI View, + // so they did not appear in the system view as well. + } + @Override + public long estimateMatchingRowsCount(Expression predicate) + { + return 0; } @Override - protected void internalRelease() + public void close() throws IOException { + // EmptyIndex does not hold any resources + } + @Override + public List> orderResultsBy(QueryContext context, List keys, Orderer orderer, int limit, long totalRows) throws IOException + { + return List.of(); } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/IndexSearchResultIterator.java b/src/java/org/apache/cassandra/index/sai/disk/IndexSearchResultIterator.java deleted file mode 100644 index e59d7c6e3581..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/IndexSearchResultIterator.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.exceptions.QueryCancelledException; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.memory.MemtableIndex; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator; -import org.apache.cassandra.index.sai.plan.QueryViewBuilder; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.Throwables; - -public class IndexSearchResultIterator extends KeyRangeIterator -{ - private static final Logger logger = LoggerFactory.getLogger(IndexSearchResultIterator.class); - - private final KeyRangeIterator union; - - private IndexSearchResultIterator(KeyRangeIterator union, Runnable onClose) - { - super(union.getMinimum(), union.getMaximum(), union.getMaxKeys(), onClose); - this.union = union; - } - - /** - * Builds a new {@link IndexSearchResultIterator} that wraps a {@link KeyRangeUnionIterator} over the - * results of searching the {@link QueryViewBuilder.QueryExpressionView}. - */ - public static IndexSearchResultIterator build(QueryViewBuilder.QueryExpressionView queryView, - AbstractBounds keyRange, - QueryContext queryContext, - boolean includeMemtables, - Runnable onClose) - { - return build(queryView.expression, queryView.memtableIndexes, queryView.sstableIndexes, keyRange, queryContext, includeMemtables, onClose); - } - - /** - * Builds a new {@link IndexSearchResultIterator} that wraps a {@link KeyRangeUnionIterator} over the - * results of searching the {@link org.apache.cassandra.index.sai.memory.MemtableIndex}es and the {@link SSTableIndex}es. - */ - public static IndexSearchResultIterator build(Expression expression, - Collection memtableIndexes, - Collection sstableIndexes, - AbstractBounds keyRange, - QueryContext queryContext, - boolean includeMemtables, - Runnable onClose) - { - int size = sstableIndexes.size() + (includeMemtables ? memtableIndexes.size() : 0); - List subIterators = new ArrayList<>(size); - - if (includeMemtables) - { - for (MemtableIndex memtableIndex : memtableIndexes) - { - KeyRangeIterator memtableIterator = memtableIndex.search(queryContext, expression, keyRange); - subIterators.add(memtableIterator); - } - } - - for (SSTableIndex sstableIndex : sstableIndexes) - { - try - { - queryContext.checkpoint(); - queryContext.sstablesHit++; - - if (sstableIndex.isReleased()) - throw new IllegalStateException(sstableIndex.getIndexIdentifier().logMessage("Index was released from the view during the query")); - - List indexIterators = sstableIndex.search(expression, keyRange, queryContext); - - if (!indexIterators.isEmpty()) - subIterators.addAll(indexIterators); - } - catch (Throwable e) - { - if (!(e instanceof QueryCancelledException)) - logger.debug(sstableIndex.getIndexIdentifier().logMessage(String.format("Failed search an index %s, aborting query.", sstableIndex.getSSTable())), e); - - throw Throwables.cleaned(e); - } - } - - KeyRangeIterator union = KeyRangeUnionIterator.build(subIterators, () -> {}); - return new IndexSearchResultIterator(union, onClose); - } - - public static IndexSearchResultIterator build(List sstableIntersections, - List memtableResults, - Set referencedIndexes, - QueryContext queryContext, - Runnable onClose) - { - queryContext.sstablesHit += referencedIndexes - .stream() - .map(SSTableIndex::getSSTable).collect(Collectors.toSet()).size(); - queryContext.checkpoint(); - KeyRangeIterator union = KeyRangeUnionIterator.builder(sstableIntersections.size() + 1, () -> {}) - .add(sstableIntersections) - .add(memtableResults) - .build(); - return new IndexSearchResultIterator(union, onClose); - } - - protected PrimaryKey computeNext() - { - return union.hasNext() ? union.next() : endOfData(); - } - - protected void performSkipTo(PrimaryKey nextKey) - { - union.skipTo(nextKey); - } - - @Override - public void close() - { - super.close(); - FileUtils.closeQuietly(union); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/IndexSearcherContext.java b/src/java/org/apache/cassandra/index/sai/disk/IndexSearcherContext.java new file mode 100644 index 000000000000..0f26d1fd9ff6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/IndexSearcherContext.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.utils.PrimaryKey; + +public class IndexSearcherContext +{ + final QueryContext context; + final PostingList postingList; + + final PrimaryKey minimumKey; + final PrimaryKey maximumKey; + final long minSSTableRowId; + final long maxSSTableRowId; + final long segmentRowIdOffset; + final long maxPartitionOffset; + + public IndexSearcherContext(PrimaryKey minimumKey, + PrimaryKey maximumKey, + long minSSTableRowId, + long maxSSTableRowId, + long segmentRowIdOffset, + QueryContext context, + PostingList postingList) throws IOException + { + this.context = context; + this.postingList = postingList; + + this.segmentRowIdOffset = segmentRowIdOffset; + + this.minimumKey = minimumKey; + + // use segment's metadata for the range iterator, may not be accurate, but should not matter to performance. + this.maximumKey = maximumKey; + + this.minSSTableRowId = minSSTableRowId; + this.maxSSTableRowId = maxSSTableRowId; + this.maxPartitionOffset = Long.MAX_VALUE; + } + + public long getSegmentRowIdOffset() + { + return segmentRowIdOffset; + } + + int count() + { + return postingList.size(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/MemtableTermsIterator.java b/src/java/org/apache/cassandra/index/sai/disk/MemtableTermsIterator.java new file mode 100644 index 000000000000..75f37519a966 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/MemtableTermsIterator.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.index.sai.memory.RowMapping; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * Iterator over a token range bounded segment of a Memtable index. Used to flush Memtable index segments to disk. + */ +public class MemtableTermsIterator implements TermsIterator +{ + private final ByteBuffer minTerm; + private final ByteBuffer maxTerm; + private final Iterator>> iterator; + + private Pair> current; + + private int maxSSTableRowId = -1; + private int minSSTableRowId = Integer.MAX_VALUE; + + public MemtableTermsIterator(ByteBuffer minTerm, + ByteBuffer maxTerm, + Iterator>> iterator) + { + Preconditions.checkArgument(iterator != null); + this.minTerm = minTerm; + this.maxTerm = maxTerm; + this.iterator = iterator; + } + + @Override + public ByteBuffer getMinTerm() + { + return minTerm; + } + + @Override + public ByteBuffer getMaxTerm() + { + return maxTerm; + } + + @Override + public void close() {} + + @Override + public PostingList postings() + { + var list = current.right; + + assert list.size() > 0; + + final int minSegmentRowID = list.get(0).rowId; + final int maxSegmentRowID = list.get(list.size() - 1).rowId; + + // Because we are working with postings from the memtable, there is only one segment, so segment row ids + // and sstable row ids are the same. + minSSTableRowId = Math.min(minSSTableRowId, minSegmentRowID); + maxSSTableRowId = Math.max(maxSSTableRowId, maxSegmentRowID); + + var it = list.iterator(); + + return new PostingList() + { + int frequency; + + @Override + public int nextPosting() + { + if (!it.hasNext()) + { + return END_OF_STREAM; + } + + var rowIdWithFrequency = it.next(); + frequency = rowIdWithFrequency.frequency; + return rowIdWithFrequency.rowId; + } + + @Override + public int size() + { + return list.size(); + } + + @Override + public int frequency() + { + return frequency; + } + + @Override + public int advance(int targetRowID) + { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public boolean hasNext() + { + return iterator.hasNext(); + } + + @Override + public ByteComparable next() + { + current = iterator.next(); + return current.left; + } + + public long getMaxSSTableRowId() + { + return maxSSTableRowId; + } + + public long getMinSSTableRowId() + { + return minSSTableRowId; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/ModernResettableByteBuffersIndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/ModernResettableByteBuffersIndexOutput.java new file mode 100644 index 000000000000..c000487813d1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/ModernResettableByteBuffersIndexOutput.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.oldlucene.ResettableByteBuffersIndexOutput; +import org.apache.lucene.store.ByteBuffersDataInput; +import org.apache.lucene.store.ByteBuffersDataOutput; +import org.apache.lucene.store.ByteBuffersIndexInput; +import org.apache.lucene.store.ByteBuffersIndexOutput; +import org.apache.lucene.store.DataInput; +import org.apache.lucene.store.IndexInput; + +/*** + * A wrapper around {@link ByteBuffersIndexOutput} that adds several methods that interact + * with the underlying delegate. This is "modern" in the sense that it uses the current Lucene + * dependency for its implementation of I/O. In particular, this means it cannot be used to write + * indexes/data compatible with the readers in older Lucene versions. + */ +public class ModernResettableByteBuffersIndexOutput extends ResettableByteBuffersIndexOutput +{ + private final ByteBuffersIndexOutput bbio; + private final ByteBuffersDataOutput delegate; + + public ModernResettableByteBuffersIndexOutput(int expectedSize, String name, Version version) + { + super("", name, ByteOrder.LITTLE_ENDIAN, version); + delegate = new ByteBuffersDataOutput(expectedSize); + bbio = new ByteBuffersIndexOutput(delegate, "", name + "-bb"); + } + + public ByteBuffersDataInput toDataInput() + { + return delegate.toDataInput(); + } + + public IndexInput toIndexInput() + { + return new ByteBuffersIndexInput(toDataInput(), ""); + } + + public void copyTo(IndexOutput out) throws IOException + { + delegate.copyTo(out); + } + + public int intSize() { + return Math.toIntExact(bbio.getFilePointer()); + } + + public byte[] toArrayCopy() { + return delegate.toArrayCopy(); + } + + public void reset() + { + delegate.reset(); + } + + @Override + public String toString() + { + return "Resettable" + bbio.toString(); + } + + @Override + public void close() throws IOException + { + bbio.close(); + } + + @Override + public long getFilePointer() + { + return bbio.getFilePointer(); + } + + @Override + public long getChecksum() throws IOException + { + return bbio.getChecksum(); + } + + @Override + public void writeByte(byte b) throws IOException + { + bbio.writeByte(b); + } + + @Override + public void writeBytes(byte[] b, int offset, int length) throws IOException + { + bbio.writeBytes(b, offset, length); + } + + @Override + public void writeBytes(byte[] b, int length) throws IOException + { + bbio.writeBytes(b, length); + } + + @Override + public void writeInt(int i) throws IOException + { + bbio.writeInt(i); + } + + @Override + public void writeShort(short i) throws IOException + { + bbio.writeShort(i); + } + + @Override + public void writeLong(long i) throws IOException + { + bbio.writeLong(i); + } + + @Override + public void writeString(String s) throws IOException + { + bbio.writeString(s); + } + + @Override + public void copyBytes(DataInput input, long numBytes) throws IOException + { + bbio.copyBytes(input, numBytes); + } + + @Override + public void writeMapOfStrings(Map map) throws IOException + { + bbio.writeMapOfStrings(map); + } + + @Override + public void writeSetOfStrings(Set set) throws IOException + { + bbio.writeSetOfStrings(set); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/PerIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/PerIndexWriter.java new file mode 100644 index 000000000000..bc4b5712ea78 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/PerIndexWriter.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; + +import com.google.common.base.Stopwatch; + +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.utils.PrimaryKey; + +/** + * Creates an on-disk index for a given index. + */ +public interface PerIndexWriter +{ + /** + * The index components written on disk by this disk. + */ + IndexComponents.ForWrite writtenComponents(); + + /** + * Adds a row to this index. + */ + void addRow(PrimaryKey key, Row row, long sstableRowId) throws IOException; + + /** + * Builds on-disk index data structures from accumulated data, moves them all to the filesystem, and fsync created files. + */ + void complete(Stopwatch stopwatch) throws IOException; + + /** + * Called when current sstable writer is switched during sharded compaction to free any in-memory resources associated + * with the sstable for current index without waiting for full transaction to complete + */ + void onSSTableWriterSwitched(Stopwatch stopwatch) throws IOException; + + /** + * Aborts accumulating data. Allows to clean up resources on error. + * + * Note: Implementations should be idempotent, i.e. safe to call multiple times without producing undesirable side-effects. + */ + void abort(Throwable cause); + + IndexContext indexContext(); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java b/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java new file mode 100644 index 000000000000..af323b91ecac --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; + +import com.google.common.base.Stopwatch; + +import org.apache.cassandra.index.sai.utils.PrimaryKey; + +/** + * Writes all SSTable-attached index token and offset structures. + */ +public interface PerSSTableWriter +{ + public static final PerSSTableWriter NONE = (key) -> {}; + + default void startPartition(long position) throws IOException + {} + + void nextRow(PrimaryKey primaryKey) throws IOException; + + default void complete(Stopwatch stopwatch) throws IOException + {} + + default void abort(Throwable accumulator) + {} +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/PostingList.java b/src/java/org/apache/cassandra/index/sai/disk/PostingList.java new file mode 100644 index 000000000000..4959c6f0be6a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/PostingList.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.io.Closeable; +import java.io.IOException; +import javax.annotation.concurrent.NotThreadSafe; + +/** + * Interface for advancing on and consuming a posting list. + */ +@NotThreadSafe +public interface PostingList extends Closeable +{ + PostingList EMPTY = new EmptyPostingList(); + + int OFFSET_NOT_FOUND = -1; + int END_OF_STREAM = Integer.MAX_VALUE; + + @Override + default void close() throws IOException {} + + /** + * Retrieves the next segment row ID, not including row IDs that have been returned by {@link #advance(int)}. + * + * @return next segment row ID + */ + int nextPosting() throws IOException; + + /** + * @return the number of occurrences of the term in the current row (the one most recently returned by nextPosting). + */ + default int frequency() + { + return 1; + } + + int size(); + + /** + * @return {@code true} if this posting list contains no postings + */ + default boolean isEmpty() + { + return size() == 0; + } + + /** + * Advances to the first row ID beyond the current that is greater than or equal to the + * target, and returns that row ID. Exhausts the iterator and returns {@link #END_OF_STREAM} if + * the target is greater than the highest row ID. + * + * Note: Callers must use the return value of this method before calling {@link #nextPosting()}, as calling + * that method will return the next posting, not the one to which we have just advanced. + * + * @param targetRowID target row ID to advance to + * + * @return first segment row ID which is >= the target row ID or {@link PostingList#END_OF_STREAM} if one does not exist + */ + int advance(int targetRowID) throws IOException; + + class EmptyPostingList implements PostingList + { + @Override + public int nextPosting() throws IOException + { + return END_OF_STREAM; + } + + @Override + public int size() + { + return 0; + } + + @Override + public int advance(int targetRowID) throws IOException + { + return END_OF_STREAM; + } + } + + /** + * Returns a wrapper for this posting list that runs the specified {@link Closeable} when this posting list is closed, + * unless this posting list is empty, in which case the specified {@link Closeable} will be run immediately. + * + * @param onClose what to do on close + * @return a posting list that makes sure that {@code onClose} is run by the time it is closed. + */ + default PostingList onClose(Closeable onClose) throws IOException + { + if (isEmpty()) + { + onClose.close(); + return EMPTY; + } + + return new PostingListWithOnClose(this, onClose); + } + + class PostingListWithOnClose implements PostingList + { + private final PostingList delegate; + private final Closeable onClose; + + public PostingListWithOnClose(PostingList delegate, Closeable onClose) + { + this.delegate = delegate; + this.onClose = onClose; + } + + @Override + public int size() + { + return delegate.size(); + } + + @Override + public int advance(int targetRowID) throws IOException + { + return delegate.advance(targetRowID); + } + + @Override + public int nextPosting() throws IOException + { + return delegate.nextPosting(); + } + + @Override + public void close() throws IOException + { + delegate.close(); + onClose.close(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java new file mode 100644 index 000000000000..ca4dcc7e0c10 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.base.Stopwatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Throwables; + +/** + * A range iterator based on {@link PostingList}. + * + *
      + *
    1. fetch next unique segment row id from posting list or skip to specific segment row id if {@link #skipTo(PrimaryKey)} is called
    2. + *
    3. add segmentRowIdOffset to obtain the sstable row id
    4. + *
    5. produce a {@link PrimaryKey} from {@link PrimaryKeyMap#primaryKeyFromRowId(long)} which is used + * to avoid fetching duplicated keys due to partition-level indexing on wide partition schema. + *
      + * Note: in order to reduce disk access in multi-index query, partition keys will only be fetched for intersected tokens + * in {@link org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher}. + *
    6. + *
    + * + */ + +@NotThreadSafe +public class PostingListKeyRangeIterator extends KeyRangeIterator +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final Stopwatch timeToExhaust = Stopwatch.createStarted(); + private final QueryContext queryContext; + + private final PostingList postingList; + private final IndexContext indexContext; + private final PrimaryKeyMap primaryKeyMap; + private final IndexSearcherContext searcherContext; + + private final AtomicBoolean isClosed = new AtomicBoolean(false); + + private boolean needsSkipping = false; + private PrimaryKey skipToToken = null; + private long lastSegmentRowId = -1; + + /** + * Create a direct PostingListKeyRangeIterator where the underlying PostingList is materialised + * immediately so the posting list size can be used. + */ + public PostingListKeyRangeIterator(IndexContext indexContext, + PrimaryKeyMap primaryKeyMap, + IndexSearcherContext searcherContext) + { + super(searcherContext.minimumKey, searcherContext.maximumKey, searcherContext.count()); + + this.indexContext = indexContext; + this.primaryKeyMap = primaryKeyMap; + this.postingList = searcherContext.postingList; + this.searcherContext = searcherContext; + this.queryContext = this.searcherContext.context; + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + // If this index indexes a static column, we must skip to the correct static row having the partition key + // equal to or greater than the partition key of the given nextKey. Beware that the primaryKeyMap + // for this index can be row-aware and looking up a regular row will end up in a rowId larger than the + // rowId of the static row of the nextKey's partition. Therefore, we need to explicitly round down + // the primary key to the start of the partition. + // Skipping to a regular row primaryKey can happen if an index on a static column is intersected with + // an index on a regular column. + if (indexContext.getDefinition().isStatic()) + nextKey = nextKey.forStaticRow(); + + // If skipToToken is equal to nextKey, we take the nextKey because in practice, it is greater than or equal + // to the skipToToken. This is because token only PKs are considered equal to all PKs with the same token, + // and for a range query, we first skip on the token-only PK. + if (skipToToken != null && skipToToken.compareTo(nextKey) > 0) + return; + + skipToToken = nextKey; + needsSkipping = true; + } + + @Override + protected PrimaryKey computeNext() + { + try + { + queryContext.checkpoint(); + + // just end the iterator if we don't have a postingList or current segment is skipped + if (exhausted()) + return endOfData(); + + long rowId = getNextRowId(); + if (rowId == PostingList.END_OF_STREAM) + return endOfData(); + + return primaryKeyMap.primaryKeyFromRowId(rowId, searcherContext.minimumKey, searcherContext.maximumKey); + } + catch (Throwable t) + { + if (!(t instanceof AbortedOperationException)) + logger.error(indexContext.logMessage("Unable to provide next token!"), t); + + throw Throwables.cleaned(t); + } + } + + @Override + public void close() throws IOException + { + if (isClosed.compareAndSet(false, true)) + { + if (logger.isTraceEnabled()) + { + // timeToExhaust.stop() throws on already stopped stopwatch + final long closedInMills = timeToExhaust.stop().elapsed(TimeUnit.MILLISECONDS); + logger.trace(indexContext.logMessage("PostinListRangeIterator exhausted after {} ms"), closedInMills); + } + + FileUtils.closeQuietly(postingList, primaryKeyMap); + } + else { + logger.warn("PostingListKeyRangeIterator is already closed", + new IllegalStateException("PostingListKeyRangeIterator is already closed")); + } + + } + + private boolean exhausted() + { + return needsSkipping && skipToToken.compareTo(getMaximum()) > 0; + } + + /** + * reads the next sstable row ID from the underlying posting list, potentially skipping to get there. + */ + private long getNextRowId() throws IOException + { + long segmentRowId; + if (needsSkipping) + { + long targetSstableRowId = primaryKeyMap.ceiling(skipToToken); + // skipToToken is larger than max token in token file + if (targetSstableRowId < 0) + { + return PostingList.END_OF_STREAM; + } + int targetSegmentRowId = Math.toIntExact(targetSstableRowId - searcherContext.getSegmentRowIdOffset()); + segmentRowId = postingList.advance(targetSegmentRowId); + needsSkipping = false; + } + else + { + do + { + segmentRowId = postingList.nextPosting(); + // Do not produce a duplicate segment row id. + } while (segmentRowId == lastSegmentRowId && segmentRowId != PostingList.END_OF_STREAM); + } + lastSegmentRowId = segmentRowId; + return segmentRowId != PostingList.END_OF_STREAM + ? segmentRowId + searcherContext.getSegmentRowIdOffset() + : PostingList.END_OF_STREAM; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java index b2ab64fdc25f..4ecdcc29cc98 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java +++ b/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java @@ -21,15 +21,14 @@ import java.io.Closeable; import java.io.IOException; +import javax.annotation.Nonnull; import javax.annotation.concurrent.NotThreadSafe; -import javax.annotation.concurrent.ThreadSafe; -import org.apache.cassandra.dht.Token; import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.sstable.SSTableId; /** - * A bidirectional map of {@link PrimaryKey} to row ID. Implementations of this interface + * A bidirectional map of {@link PrimaryKey} to row Id. Implementations of this interface * are not expected to be threadsafe. */ @NotThreadSafe @@ -39,19 +38,34 @@ public interface PrimaryKeyMap extends Closeable * A factory for creating {@link PrimaryKeyMap} instances. Implementations of this * interface are expected to be threadsafe. */ - @ThreadSafe interface Factory extends Closeable { /** * Creates a new {@link PrimaryKeyMap} instance * * @return a {@link PrimaryKeyMap} - * @throws IOException if the {@link PrimaryKeyMap} couldn't be created */ - PrimaryKeyMap newPerSSTablePrimaryKeyMap() throws IOException; + PrimaryKeyMap newPerSSTablePrimaryKeyMap(); + + /** + * Returns the number of primary keys in the map. This is part of the factory because + * it can be retrieved without opening the map. + * @return the number of primary keys in the map + */ + default long count() + { + try (PrimaryKeyMap map = newPerSSTablePrimaryKeyMap()) + { + return map.count(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } @Override - default void close() + default void close() throws IOException { } } @@ -60,44 +74,96 @@ default void close() * Returns the {@link SSTableId} associated with this {@link PrimaryKeyMap} * @return an {@link SSTableId} */ - SSTableId getSSTableId(); + SSTableId getSSTableId(); /** * Returns a {@link PrimaryKey} for a row ID * - * @param sstableRowId the row ID to lookup - * @return the {@link PrimaryKey} associated with the row ID + * @param sstableRowId the row Id to lookup + * @return the {@link PrimaryKey} associated with the row Id */ PrimaryKey primaryKeyFromRowId(long sstableRowId); /** - * Returns a row ID for a {@link PrimaryKey} + * Returns a {@link PrimaryKey} for a row Id + * + * Note: the lower and upper bounds are used to avoid reading the primary key from disk in the event + * that compared primary keys are in non-overlapping ranges. The ranges can be within the table, and must + * contain the row id. This requirement is not validated, as validation would remove the performance benefit + * of this optimization. + * + * @param sstableRowId the row Id to lookup + * @param lowerBound the inclusive lower bound of the primary key being created + * @param upperBound the inclusive upper bound of the primary key being created + * @return the {@link PrimaryKey} associated with the row Id + */ + default PrimaryKey primaryKeyFromRowId(long sstableRowId, @Nonnull PrimaryKey lowerBound, @Nonnull PrimaryKey upperBound) + { + return primaryKeyFromRowId(sstableRowId); + } + + /** + * Returns a row Id for a {@link PrimaryKey}. If there is no such term, returns the `-(next row id) - 1` where + * `next row id` is the row id of the next greatest {@link PrimaryKey} in the map. * * @param key the {@link PrimaryKey} to lookup - * @return the row ID associated with the {@link PrimaryKey} + * @return the row Id associated with the {@link PrimaryKey} */ - long rowIdFromPrimaryKey(PrimaryKey key); + long exactRowIdOrInvertedCeiling(PrimaryKey key); /** - * Returns the first row ID of the nearest {@link Token} greater than or equal to the given {@link Token}, - * or a negative value if not found + * Returns the sstable row id associated with the least {@link PrimaryKey} greater than or equal to the given + * {@link PrimaryKey}. If the {@link PrimaryKey} is a prefix of multiple {@link PrimaryKey}s in the map, e.g. it is + * just a token or a token and a partition key, the row id associated with the least {@link PrimaryKey} will be + * returned. If there is no {@link PrimaryKey} in the map that meets this definition, returns a negative value. * - * @param token the {@link Token} to lookup - * @return the ceiling row ID associated with the {@link Token} or a negative value + * @param key the {@link PrimaryKey} to lookup + * @return an sstable row id or a negative value if no row is found */ - long ceiling(Token token); + long ceiling(PrimaryKey key); /** - * Returns the last row ID of the nearest {@link Token} less than or equal to the given {@link Token}, - * or a negative value if the {@link Token} is at its minimum value + * Returns the sstable row id associated with the greatest {@link PrimaryKey} less than or equal to the given + * {@link PrimaryKey}. If the {@link PrimaryKey} is a prefix of multiple {@link PrimaryKey}s in the map, e.g. it is + * just a token or a token and a partition key, the row id associated with the greatest {@link PrimaryKey} will be + * returned. If there is no {@link PrimaryKey} in the map that meets this definition, returns a negative value. * - * @param token the {@link Token} to lookup - * @return the floor row ID associated with the {@link Token} + * @param key the {@link PrimaryKey} to lookup + * @return an sstable row id or a negative value if no row is found + */ + long floor(PrimaryKey key); + + /** + * Returns the number of primary keys in the map */ - long floor(Token token); + long count(); @Override - default void close() + default void close() throws IOException { } + + /** + * When SAI_INDEX_READS_DISABLED is true, this is used to avoid loading SAI files to reduce disk access and memory usage + */ + class DummyThrowingFactory implements Factory + { + @Override + public PrimaryKeyMap newPerSSTablePrimaryKeyMap() + { + throw new UnsupportedOperationException("EmptyFactory doesn't support newPerSSTablePrimaryKeyMap()"); + } + + @Override + public long count() + { + throw new UnsupportedOperationException("EmptyFactory doesn't support count()"); + } + + @Override + public void close() throws IOException + { + // no-op + } + } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMapIterator.java b/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMapIterator.java new file mode 100644 index 000000000000..1f10741c0ba6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMapIterator.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.schema.TableMetadata; + +/** + * Iterates keys in the {@link PrimaryKeyMap} of a SSTable. + * Iterating keys in the primary key map is faster than reading them from the SSTable data component + * because we don't deserialize all the other columns except the primary key columns. + * The primary key map components are also likely much smaller than the whole SSTable data component. + *

    + * The keys are returned in token-clustering order. + */ +public final class PrimaryKeyMapIterator extends KeyRangeIterator +{ + // KeyFilter controls which keys we want to return from the iterator. + // This is a hack to make this iterator work correctly on schemas with static columns. + // If the table has static columns, the primary key map component may contain both keys with clustering + // and with no clustering. The keys of regular rows will likely have clustering and the keys associated with + // updates of the static columns will have no clustering. Hence, depending on the type of the queried column, + // we must return only all keys with clustering or only all keys with no clustering, but not mixed, or we may run + // into duplicate row issues. We also shouldn't return keys without clustering for regular rows that expect + // clustering information - as that would negate the row-awareness advantage. + private enum KeyFilter + { + ALL, // return all keys, fast, but safe only if we know there are no mixed keys with and without clustering + KEYS_WITH_CLUSTERING // return keys with clustering + } + + private final PrimaryKeyMap keys; + private final KeyFilter filter; + private long currentRowId; + + + private PrimaryKeyMapIterator(PrimaryKeyMap keys, PrimaryKey min, PrimaryKey max, long startRowId, KeyFilter filter) + { + super(min, max, keys.count()); + this.keys = keys; + this.filter = filter; + this.currentRowId = startRowId; + } + + public static KeyRangeIterator create(SSTableContext ctx, AbstractBounds keyRange) throws IOException + { + KeyFilter filter; + TableMetadata metadata = ctx.sstable().metadata(); + // if not row-aware, we don't have clustering + var perSSTableComponents = ctx.usedPerSSTableComponents(); + if (perSSTableComponents.onDiskFormat().indexFeatureSet().isRowAware() && metadata.hasStaticColumns()) + filter = KeyFilter.KEYS_WITH_CLUSTERING; + else // the table doesn't consist anything we want to filter out, so let's use the cheap option + filter = KeyFilter.ALL; + + if (perSSTableComponents.isEmpty()) + return KeyRangeIterator.empty(); + + PrimaryKeyMap keys = ctx.primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(); + long count = keys.count(); + if (keys.count() == 0) + { + keys.close(); + return KeyRangeIterator.empty(); + } + + PrimaryKey.Factory pkFactory = ctx.primaryKeyFactory(); + Token minToken = keyRange.left.getToken(); + PrimaryKey minKeyBound = pkFactory.createTokenOnly(minToken); + PrimaryKey sstableMinKey = keys.primaryKeyFromRowId(0); + PrimaryKey sstableMaxKey = keys.primaryKeyFromRowId(count - 1); + PrimaryKey minKey = (minKeyBound.compareTo(sstableMinKey) > 0) + ? minKeyBound + : sstableMinKey; + long startRowId = minToken.isMinimum() ? 0 : keys.ceiling(minKey); + return new PrimaryKeyMapIterator(keys, sstableMinKey, sstableMaxKey, startRowId, filter); + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + this.currentRowId = keys.ceiling(nextKey); + } + + @Override + protected PrimaryKey computeNext() + { + while (currentRowId >= 0 && currentRowId < keys.count()) + { + PrimaryKey key = keys.primaryKeyFromRowId(currentRowId++, getMinimum(), getMaximum()); + if (filter == KeyFilter.KEYS_WITH_CLUSTERING && !key.hasClustering()) + continue; + return key; + } + return endOfData(); + } + + @Override + public void close() throws IOException + { + keys.close(); + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/QueryEventListeners.java b/src/java/org/apache/cassandra/index/sai/disk/QueryEventListeners.java new file mode 100644 index 000000000000..ba6a80265c66 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/QueryEventListeners.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.index.sai.metrics.QueryEventListener; + +public class QueryEventListeners +{ + public static final QueryEventListener NO_OP = new BaseQueryEventListener(); + + public static final QueryEventListener.BKDIndexEventListener NO_OP_BKD_LISTENER = NO_OP.bkdIndexEventListener(); + + public static final QueryEventListener.TrieIndexEventListener NO_OP_TRIE_LISTENER = NO_OP.trieIndexEventListener(); + + public static final QueryEventListener.PostingListEventListener NO_OP_POSTINGS_LISTENER = new NoOpPostingListEventListener(); + + private static class BaseQueryEventListener implements QueryEventListener + { + @Override + public BKDIndexEventListener bkdIndexEventListener() + { + return NoOpBKDIndexEventListener.INSTANCE; + } + + @Override + public TrieIndexEventListener trieIndexEventListener() + { + return NoOpTrieIndexEventListener.INSTANCE; + } + + private enum NoOpTrieIndexEventListener implements TrieIndexEventListener + { + INSTANCE; + + @Override + public void onSegmentHit() { } + + @Override + public void onTraversalComplete(long traversalTotalTime, TimeUnit unit) { } + + @Override + public PostingListEventListener postingListEventListener() + { + return NO_OP_POSTINGS_LISTENER; + } + } + + private enum NoOpBKDIndexEventListener implements BKDIndexEventListener + { + INSTANCE; + + @Override + public void onIntersectionComplete(long intersectionTotalTime, TimeUnit unit) { } + + @Override + public void onIntersectionEarlyExit() { } + + @Override + public void postingListsHit(int count) { } + + @Override + public void onSegmentHit() { } + + @Override + public PostingListEventListener postingListEventListener() + { + return NO_OP_POSTINGS_LISTENER; + } + } + } + + public static class NoOpPostingListEventListener implements QueryEventListener.PostingListEventListener + { + @Override + public void onAdvance() { } + + @Override + public void postingDecoded(long postingsDecoded) { } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/RAMPostingSlices.java b/src/java/org/apache/cassandra/index/sai/disk/RAMPostingSlices.java new file mode 100644 index 000000000000..7266c1dfed59 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/RAMPostingSlices.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.io.IOException; + +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.ByteBlockPool; +import org.apache.lucene.util.Counter; +import org.apache.lucene.util.mutable.MutableValueInt; + +/** + * Encodes postings as variable integers into "slices" of byte blocks for efficient memory usage. + */ +class RAMPostingSlices +{ + static final int DEFAULT_TERM_DICT_SIZE = 1024; + + /** Pool of byte blocks storing the actual posting data */ + private final ByteBlockPool postingsPool; + /** true if we're also writing term frequencies for an analyzed index */ + private final boolean includeFrequencies; + + /** The starting positions of postings for each term. Term id = index in array. */ + private int[] postingStarts = new int[DEFAULT_TERM_DICT_SIZE]; + /** The current write positions for each term's postings. Term id = index in array. */ + private int[] postingUptos = new int[DEFAULT_TERM_DICT_SIZE]; + /** The number of postings for each term. Term id = index in array. */ + private int[] sizes = new int[DEFAULT_TERM_DICT_SIZE]; + + RAMPostingSlices(Counter memoryUsage, boolean includeFrequencies) + { + postingsPool = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(memoryUsage)); + this.includeFrequencies = includeFrequencies; + } + + long arrayMemoryUsage() + { + return postingStarts.length * 4L + postingUptos.length * 4L + sizes.length * 4L; + } + + /** + * Creates and returns a PostingList for the given term ID. + */ + PostingList postingList(int termID, final ByteSliceReader reader, long maxSegmentRowID) + { + initReader(reader, termID); + + final MutableValueInt lastSegmentRowId = new MutableValueInt(); + + return new PostingList() + { + int frequency = Integer.MIN_VALUE; + + @Override + public int nextPosting() throws IOException + { + if (reader.eof()) + { + frequency = Integer.MIN_VALUE; + return PostingList.END_OF_STREAM; + } + else + { + lastSegmentRowId.value += reader.readVInt(); + if (includeFrequencies) + frequency = reader.readVInt(); + return lastSegmentRowId.value; + } + } + + @Override + public int frequency() + { + if (!includeFrequencies) + return 1; + if (frequency <= 0) + throw new IllegalStateException("frequency() called before nextPosting()"); + return frequency; + } + + @Override + public int size() + { + return sizes[termID]; + } + + @Override + public int advance(int targetRowID) + { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * Initializes a ByteSliceReader for reading postings for a specific term. + */ + void initReader(ByteSliceReader reader, int termID) + { + final int upto = postingUptos[termID]; + reader.init(postingsPool, postingStarts[termID], upto); + } + + /** + * Creates a new slice for storing postings for a given term ID. + * Grows the internal arrays if necessary and allocates a new block + * if the current block cannot accommodate a new slice. + */ + void createNewSlice(int termID) + { + if (termID >= postingStarts.length - 1) + { + postingStarts = ArrayUtil.grow(postingStarts, termID + 1); + postingUptos = ArrayUtil.grow(postingUptos, termID + 1); + sizes = ArrayUtil.grow(sizes, termID + 1); + } + + // the slice will not fit in the current block, create a new block + if ((ByteBlockPool.BYTE_BLOCK_SIZE - postingsPool.byteUpto) < ByteBlockPool.FIRST_LEVEL_SIZE) + { + postingsPool.nextBuffer(); + } + + final int upto = postingsPool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE); + postingStarts[termID] = upto + postingsPool.byteOffset; + postingUptos[termID] = upto + postingsPool.byteOffset; + } + + void writePosting(int termID, int deltaRowId, int frequency) + { + assert termID >= 0 : termID; + assert deltaRowId >= 0 : deltaRowId; + writeVInt(termID, deltaRowId); + + if (includeFrequencies) + { + assert frequency > 0 : frequency; + writeVInt(termID, frequency); + } + + sizes[termID]++; + } + + /** + * Writes a variable-length integer to the posting list for a given term. + * The integer is encoded using a variable-length encoding scheme where each + * byte uses 7 bits for the value and 1 bit to indicate if more bytes follow. + */ + private void writeVInt(int termID, int i) + { + while ((i & ~0x7F) != 0) + { + writeByte(termID, (byte) ((i & 0x7f) | 0x80)); + i >>>= 7; + } + writeByte(termID, (byte) i); + } + + /** + * Writes a single byte to the posting list for a given term. + * If the current slice is full, it automatically allocates a new slice. + */ + private void writeByte(int termID, byte b) + { + int upto = postingUptos[termID]; + byte[] block = postingsPool.buffers[upto >> ByteBlockPool.BYTE_BLOCK_SHIFT]; + assert block != null; + int offset = upto & ByteBlockPool.BYTE_BLOCK_MASK; + if (block[offset] != 0) + { + // End of slice; allocate a new one + offset = postingsPool.allocSlice(block, offset); + block = postingsPool.buffer; + postingUptos[termID] = offset + postingsPool.byteOffset; + } + block[offset] = b; + postingUptos[termID]++; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/RAMStringIndexer.java b/src/java/org/apache/cassandra/index/sai/disk/RAMStringIndexer.java new file mode 100644 index 000000000000..4aa8cb1519da --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/RAMStringIndexer.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.NoSuchElementException; + +import com.google.common.annotations.VisibleForTesting; + +import org.agrona.collections.Int2IntHashMap; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.ByteBlockPool; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefHash; +import org.apache.lucene.util.Counter; + +/** + * Indexes strings into an on-heap inverted index to be flushed in an SSTable attached index later. + * For flushing use the PostingTerms interface. + */ +public class RAMStringIndexer +{ + @VisibleForTesting + public static int MAX_BLOCK_BYTE_POOL_SIZE = Integer.MAX_VALUE; + + /** + * Int2IntHashMap "docLengths" needs to resize when size reaches 348_966_081 (capacity * loadFactor). At that point, its capacity is 536870912. + * Its new capacity will be quadrupled and exceed Integer.MAX_VALUE. + * + * Pick 300_000_000 for simplicity to trigger segment flush. + */ + private static final int MAX_DOCS_SIZE = 300_000_000; + + private final BytesRefHash termsHash; + private final RAMPostingSlices slices; + // counters need to be separate so that we can trigger flushes if either ByteBlockPool hits maximum size + private final Counter termsBytesUsed; + private final Counter slicesBytesUsed; + + private int[] lastSegmentRowID = new int[RAMPostingSlices.DEFAULT_TERM_DICT_SIZE]; + + private final boolean writeFrequencies; + private final int maxDocSize; + private final Int2IntHashMap docLengths = new Int2IntHashMap(Integer.MIN_VALUE); + + public RAMStringIndexer(boolean writeFrequencies) + { + this(writeFrequencies, MAX_DOCS_SIZE); + } + + @VisibleForTesting + RAMStringIndexer(boolean writeFrequencies, int maxDocSize) + { + this.writeFrequencies = writeFrequencies; + this.maxDocSize = maxDocSize; + termsBytesUsed = Counter.newCounter(); + slicesBytesUsed = Counter.newCounter(); + + ByteBlockPool termsPool = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(termsBytesUsed)); + + termsHash = new BytesRefHash(termsPool); + + slices = new RAMPostingSlices(slicesBytesUsed, writeFrequencies); + } + + public long estimatedBytesUsed() + { + // record the array memory usage from Int2IntHashMap docLengths: + // * array size is capacity * 2 + // * 4 bytes per int + long docLengthsMemoryUsage = docLengths.capacity() * 2 * 4L; + return docLengthsMemoryUsage + termsBytesUsed.get() + slicesBytesUsed.get() + slices.arrayMemoryUsage(); + } + + public boolean requiresFlush() + { + // ByteBlockPool can't handle more than Integer.MAX_VALUE bytes. These are allocated in fixed-size chunks, + // and additions are guaranteed to be smaller than the chunks. This means that the last chunk allocation will + // be triggered by an addition, and the rest of the space in the final chunk will be wasted, as the bytesUsed + // counters track block allocation, not the size of additions. This means that we can't pass this check and then + // fail to add a term. + return termsBytesUsed.get() >= MAX_BLOCK_BYTE_POOL_SIZE || slicesBytesUsed.get() >= MAX_BLOCK_BYTE_POOL_SIZE + // to avoid Int2IntHashMap new capacity overflow + || docLengths.size() >= maxDocSize; + } + + public boolean isEmpty() + { + return docLengths.isEmpty(); + } + + public Int2IntHashMap getDocLengths() + { + return docLengths; + } + + /** + * EXPENSIVE OPERATION due to sorting the terms, only call once. + */ + // TODO: assert or throw and exception if getTermsWithPostings is called > 1 + public TermsIterator getTermsWithPostings(ByteBuffer minTerm, ByteBuffer maxTerm, ByteComparable.Version byteComparableVersion) + { + final int[] sortedTermIDs = termsHash.sort(); + + final int valueCount = termsHash.size(); + final ByteSliceReader sliceReader = new ByteSliceReader(); + + return new TermsIterator() + { + private int ordUpto = 0; + private final BytesRef br = new BytesRef(); + + @Override + public ByteBuffer getMinTerm() + { + return minTerm; + } + + @Override + public ByteBuffer getMaxTerm() + { + return maxTerm; + } + + public void close() {} + + @Override + public PostingList postings() + { + int termID = sortedTermIDs[ordUpto - 1]; + final int maxSegmentRowId = lastSegmentRowID[termID]; + return slices.postingList(termID, sliceReader, maxSegmentRowId); + } + + @Override + public boolean hasNext() { + return ordUpto < valueCount; + } + + @Override + public ByteComparable next() + { + if (!hasNext()) + throw new NoSuchElementException(); + + termsHash.get(sortedTermIDs[ordUpto], br); + ordUpto++; + return asByteComparable(br.bytes, br.offset, br.length); + } + + private ByteComparable asByteComparable(byte[] bytes, int offset, int length) + { + // The bytes were encoded when they were inserted into the termsHash. + return ByteComparable.preencoded(byteComparableVersion, bytes, offset, length); + } + }; + } + + /** + * @return bytes allocated. may be zero if the (term, row) pair is a duplicate + */ + public long addAll(List terms, int segmentRowId) + { + long startBytes = estimatedBytesUsed(); + Int2IntHashMap frequencies = new Int2IntHashMap(Integer.MIN_VALUE); + Int2IntHashMap deltas = new Int2IntHashMap(Integer.MIN_VALUE); + + for (BytesRef term : terms) + { + int termID = termsHash.add(term); + boolean firstOccurrence = termID >= 0; + + if (firstOccurrence) + { + // first time seeing this term in any row, create the term's first slice ! + slices.createNewSlice(termID); + // grow the termID -> last segment array if necessary + if (termID >= lastSegmentRowID.length - 1) + lastSegmentRowID = ArrayUtil.grow(lastSegmentRowID, termID + 1); + if (writeFrequencies) + frequencies.put(termID, 1); + } + else + { + termID = (-termID) - 1; + // compaction should call this method only with increasing segmentRowIds + assert segmentRowId >= lastSegmentRowID[termID]; + // increment frequency + if (writeFrequencies) + frequencies.put(termID, frequencies.getOrDefault(termID, 0) + 1); + // Skip computing a delta if we've already seen this term in this row + if (segmentRowId == lastSegmentRowID[termID]) + continue; + } + + // Compute the delta from the last time this term was seen, to this row + int delta = segmentRowId - lastSegmentRowID[termID]; + // sanity check that we're advancing the row id, i.e. no duplicate entries. + assert firstOccurrence || delta > 0; + deltas.put(termID, delta); + lastSegmentRowID[termID] = segmentRowId; + } + + // add the postings now that we know the frequencies + deltas.forEachInt((termID, delta) -> { + slices.writePosting(termID, delta, frequencies.get(termID)); + }); + + docLengths.put(segmentRowId, terms.size()); + + return estimatedBytesUsed() - startBytes; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/ResettableByteBuffersIndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/ResettableByteBuffersIndexOutput.java deleted file mode 100644 index 19430a50878c..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/ResettableByteBuffersIndexOutput.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk; - -import java.io.IOException; -import java.util.Map; -import java.util.Set; - -import org.apache.lucene.store.ByteBuffersDataOutput; -import org.apache.lucene.store.ByteBuffersIndexOutput; -import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexOutput; - -/*** - * A wrapper around {@link ByteBuffersIndexOutput} that adds several methods that interact - * with the underlying delegate. - */ -public class ResettableByteBuffersIndexOutput extends IndexOutput -{ - private final ByteBuffersIndexOutput bbio; - private final ByteBuffersDataOutput delegate; - - public ResettableByteBuffersIndexOutput(String name) - { - //TODO CASSANDRA-18280 to investigate the initial size allocation - this(128, name); - } - - public ResettableByteBuffersIndexOutput(int expectedSize, String name) - { - super("", name); - delegate = new ByteBuffersDataOutput(expectedSize); - bbio = new ByteBuffersIndexOutput(delegate, "", name + "-bb"); - } - - public void copyTo(IndexOutput out) throws IOException - { - delegate.copyTo(out); - } - - public int intSize() { - return Math.toIntExact(bbio.getFilePointer()); - } - - public byte[] toArrayCopy() { - return delegate.toArrayCopy(); - } - - public void reset() - { - delegate.reset(); - } - - @Override - public String toString() - { - return "Resettable" + bbio.toString(); - } - - @Override - public void close() throws IOException - { - bbio.close(); - } - - @Override - public long getFilePointer() - { - return bbio.getFilePointer(); - } - - @Override - public long getChecksum() throws IOException - { - return bbio.getChecksum(); - } - - @Override - public void writeByte(byte b) throws IOException - { - bbio.writeByte(b); - } - - @Override - public void writeBytes(byte[] b, int offset, int length) throws IOException - { - bbio.writeBytes(b, offset, length); - } - - @Override - public void writeBytes(byte[] b, int length) throws IOException - { - bbio.writeBytes(b, length); - } - - @Override - public void writeInt(int i) throws IOException - { - bbio.writeInt(i); - } - - @Override - public void writeShort(short i) throws IOException - { - bbio.writeShort(i); - } - - @Override - public void writeLong(long i) throws IOException - { - bbio.writeLong(i); - } - - @Override - public void writeString(String s) throws IOException - { - bbio.writeString(s); - } - - @Override - public void copyBytes(DataInput input, long numBytes) throws IOException - { - bbio.copyBytes(input, numBytes); - } - - @Override - public void writeMapOfStrings(Map map) throws IOException - { - bbio.writeMapOfStrings(map); - } - - @Override - public void writeSetOfStrings(Set set) throws IOException - { - bbio.writeSetOfStrings(set); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java b/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java deleted file mode 100644 index 2b91bc304bc6..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk; - -import java.util.Collections; -import java.util.Iterator; - -import javax.annotation.concurrent.NotThreadSafe; - -import com.carrotsearch.hppc.LongArrayList; -import org.apache.cassandra.db.compaction.OperationType; -import org.apache.cassandra.db.rows.RangeTombstoneMarker; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.tries.InMemoryTrie; -import org.apache.cassandra.index.sai.memory.MemtableIndex; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.PrimaryKeys; -import org.apache.cassandra.io.compress.BufferType; -import org.apache.cassandra.utils.AbstractGuavaIterator; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; - -/** - * In memory representation of {@link PrimaryKey} to row ID mappings which only contains - * {@link Row} regardless of whether it's live or deleted. ({@link RangeTombstoneMarker} is not included.) - *

    - * While this inherits the threading behaviour of {@link InMemoryTrie} of single-writer / multiple-reader, - * since it is only used by {@link StorageAttachedIndexWriter}, which is not threadsafe, we can consider - * this class not threadsafe as well. - */ -@NotThreadSafe -public class RowMapping -{ - private static final InMemoryTrie.UpsertTransformer OVERWRITE_TRANSFORMER = (existing, update) -> update; - - public static final RowMapping DUMMY = new RowMapping() - { - @Override - public Iterator> merge(MemtableIndex index) { return Collections.emptyIterator(); } - - @Override - public void complete() {} - - @Override - public boolean isComplete() - { - return true; - } - - @Override - public void add(PrimaryKey key, long sstableRowId) {} - - @Override - public int get(PrimaryKey key) - { - return -1; - } - }; - - private final InMemoryTrie rowMapping = new InMemoryTrie<>(BufferType.OFF_HEAP); - - private boolean complete = false; - - private RowMapping() - {} - - /** - * Create row mapping for FLUSH operation only. - */ - public static RowMapping create(OperationType opType) - { - if (opType == OperationType.FLUSH) - return new RowMapping(); - return DUMMY; - } - - /** - * Link the term -> {@link PrimaryKeys} mappings from a provided {@link MemtableIndex} to - * the {@link PrimaryKey} -> row ID mappings maintained here in {@link #rowMapping} to produce - * mappings of terms to their postings lists. - * - * @param index a Memtable-attached column index - * - * @return an iterator of term -> postings list {@link Pair}s - */ - public Iterator> merge(MemtableIndex index) - { - assert complete : "RowMapping is not built."; - - Iterator> iterator = index.iterator(); - return new AbstractGuavaIterator<>() - { - @Override - protected Pair computeNext() - { - while (iterator.hasNext()) - { - Pair pair = iterator.next(); - - LongArrayList postings = null; - Iterator primaryKeys = pair.right.iterator(); - - while (primaryKeys.hasNext()) - { - Long sstableRowId = rowMapping.get(primaryKeys.next()); - - // The in-memory index does not handle deletions, so it is possible to - // have a primary key in the index that doesn't exist in the row mapping - if (sstableRowId != null) - { - postings = postings == null ? new LongArrayList() : postings; - postings.add(sstableRowId); - } - } - if (postings != null) - return Pair.create(pair.left, postings); - } - return endOfData(); - } - }; - } - - /** - * Complete building in memory RowMapping, mark it as immutable. - */ - public void complete() - { - assert !complete : "RowMapping can only be built once."; - this.complete = true; - } - - public boolean isComplete() - { - return complete; - } - - /** - * Include PrimaryKey to RowId mapping - */ - public void add(PrimaryKey key, long sstableRowId) throws InMemoryTrie.SpaceExhaustedException - { - assert !complete : "Cannot modify and already built RowMapping."; - rowMapping.putSingleton(key, sstableRowId, OVERWRITE_TRANSFORMER); - } - - /** - * Returns the SSTable row ID for a {@link PrimaryKey} - * - * @param key the {@link PrimaryKey} - * @return a valid SSTable row ID for the {@link PrimaryKey} or -1 if the {@link PrimaryKey} doesn't exist - * in the {@link RowMapping} - */ - public int get(PrimaryKey key) - { - Long sstableRowId = rowMapping.get(key); - return sstableRowId == null ? -1 : Math.toIntExact(sstableRowId); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/SSTableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/SSTableIndex.java deleted file mode 100644 index 0938a50ded68..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/SSTableIndex.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.db.virtual.SimpleDataSet; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.SSTableContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.format.Version; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.sstable.SSTableIdFactory; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.utils.CloseableIterator; - -/** - * A reference-counted container of a {@link SSTableReader} for each column index that: - *

      - *
    • Manages references to the SSTable for each query
    • - *
    • Exposes a version agnostic searcher onto the column index
    • - *
    • Exposes the index metadata for the column index
    • - *
    - */ -public abstract class SSTableIndex implements Comparable -{ - private static final Logger logger = LoggerFactory.getLogger(SSTableIndex.class); - - // sort sstable indexes by first key, then last key, then descriptor id - public static final Comparator COMPARATOR = Comparator.comparing((SSTableIndex s) -> s.getSSTable().getFirst()) - .thenComparing(s -> s.getSSTable().getLast()) - .thenComparing(s -> s.getSSTable().descriptor.id, SSTableIdFactory.COMPARATOR); - - protected final SSTableContext sstableContext; - protected final IndexTermType indexTermType; - protected final IndexIdentifier indexIdentifier; - - private final AtomicInteger references = new AtomicInteger(1); - private final AtomicBoolean obsolete = new AtomicBoolean(false); - - public SSTableIndex(SSTableContext sstableContext, StorageAttachedIndex index) - { - this.sstableContext = sstableContext.sharedCopy(); // this line must not be before any code that may throw - this.indexTermType = index.termType(); - this.indexIdentifier = index.identifier(); - } - - /** - * Returns the amount of memory occupied by the index when it is initially loaded. - * This is the amount of data loaded into internal memory buffers by the index and - * does include the class footprint overhead. It used by the index metrics. - */ - public abstract long indexFileCacheSize(); - - /** - * Returns the number of indexed rows in the index. This comes from the index - * metadata created when the index was written and is used by the index metrics. - */ - public abstract long getRowCount(); - - /** - * Returns the minimum indexed rowId for the index. This comes from the index - * metadata created when the index was written and is used by the index metrics. - */ - public abstract long minSSTableRowId(); - - /** - * Returns the maximum indexed rowId for the index. This comes from the index - * metadata created when the index was written and is used by the index metrics. - */ - public abstract long maxSSTableRowId(); - - /** - * Returns the minimum term held in the index based on the natural sort order of - * the index column type comparator. It comes from the index metadata created when - * the index was written and is used by the index metrics and used in queries to - * determine whether a term, or range or terms, exists in the index. - */ - public abstract ByteBuffer minTerm(); - - /** - * Returns the maximum term held in the index based on the natural sort order of - * the index column type comparator. It comes from the index metadata created when - * the index was written and is used by the index metrics and used in queries to - * determine whether a term, or range or terms, exists in the index. - */ - public abstract ByteBuffer maxTerm(); - - /** - * Returns the key bounds of the index. It is created from the minimum and - * maximum keys held in the metadata and is used to determine whether - * sstable indexes overlap or not. - */ - public abstract AbstractBounds bounds(); - - /** - * Perform a search on the index for a single expression and keyRange. - *

    - * The result is a {@link List} of {@link KeyRangeIterator} because there will - * be a {@link KeyRangeIterator} for each segment in the index. The result - * will never be null but may be an empty {@link List}. - * - * @param expression The {@link Expression} to be searched for - * @param keyRange The {@code AbstractBounds} defining the - * token range for the search - * @param context The {@link QueryContext} holding the per-query state - * @return a {@link List} of {@link KeyRangeIterator}s containing the results - * of the search - */ - public abstract List search(Expression expression, - AbstractBounds keyRange, - QueryContext context) throws IOException; - - public abstract List> orderBy(Expression orderer, AbstractBounds keyRange, QueryContext context) throws IOException; - public abstract List> orderResultsBy(QueryContext context, List results, Expression orderer) throws IOException; - - /** - * Populates a virtual table using the index metadata owned by the index - */ - public abstract void populateSegmentView(SimpleDataSet dataSet); - - protected abstract void internalRelease(); - - /** - * @return total size of per-column index components, in bytes - */ - public long sizeOfPerColumnComponents() - { - return sstableContext.indexDescriptor.sizeOnDiskOfPerIndexComponents(indexTermType, indexIdentifier); - } - - public IndexTermType getIndexTermType() - { - return indexTermType; - } - - public IndexIdentifier getIndexIdentifier() - { - return indexIdentifier; - } - - public SSTableContext getSSTableContext() - { - return sstableContext; - } - - public Version getVersion() - { - return sstableContext.indexDescriptor.version; - } - - public SSTableReader getSSTable() - { - return sstableContext.sstable; - } - - public boolean reference() - { - while (true) - { - int n = references.get(); - if (n <= 0) - return false; - if (references.compareAndSet(n, n + 1)) - { - return true; - } - } - } - - public boolean isReleased() - { - return references.get() <= 0; - } - - public void releaseQuietly() - { - try - { - release(); - } - catch (Throwable e) - { - logger.error(indexIdentifier.logMessage("Failed to release index on SSTable {}"), getSSTable().descriptor, e); - } - } - - public void release() - { - int n = references.decrementAndGet(); - - if (n == 0) - { - internalRelease(); - sstableContext.close(); - - /* - * When SSTable is removed, storage-attached index components will be automatically removed by LogTransaction. - * We only remove index components explicitly in case of index corruption or index rebuild. - */ - if (obsolete.get()) - { - sstableContext.indexDescriptor.deleteColumnIndex(indexTermType, indexIdentifier); - } - } - } - - public void markObsolete() - { - obsolete.getAndSet(true); - release(); - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SSTableIndex other = (SSTableIndex)o; - return Objects.equal(sstableContext, other.sstableContext) && - Objects.equal(indexTermType, other.indexTermType) && - Objects.equal(indexIdentifier, other.indexIdentifier); - } - - @Override - public int hashCode() - { - return Objects.hashCode(sstableContext, indexTermType, indexIdentifier); - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this) - .add("column", indexTermType.columnName()) - .add("sstable", sstableContext.sstable.descriptor) - .add("minTerm", indexTermType.asString(minTerm())) - .add("maxTerm", indexTermType.asString(maxTerm())) - .add("totalRows", sstableContext.sstable.getTotalRows()) - .toString(); - } - - @Override - public int compareTo(SSTableIndex index) - { - // SSTableReader is truly unique for comparison which is relied on in IntervalTree - return getSSTable().compareTo(index.getSSTable()); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/SearchableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/SearchableIndex.java new file mode 100644 index 000000000000..71ee9d0353bc --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/SearchableIndex.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.virtual.SimpleDataSet; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.v1.Segment; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.CloseableIterator; + +/** + * This is used to abstract the index search between on-disk versions. + * Callers to this interface should be unaware of the on-disk version for + * the index. + * + * It is responsible for supplying metadata about the on-disk index. This is + * used during query time to help coordinate queries and is also returned + * by the virtual tables. + */ +public interface SearchableIndex extends Closeable +{ + long indexFileCacheSize(); + + long getRowCount(); + + long getApproximateTermCount(); + + long minSSTableRowId(); + + long maxSSTableRowId(); + + ByteBuffer minTerm(); + + ByteBuffer maxTerm(); + + DecoratedKey minKey(); + + DecoratedKey maxKey(); + + KeyRangeIterator search(Expression expression, + AbstractBounds keyRange, + QueryContext context, + boolean defer) throws IOException; + + List> orderBy(Orderer orderer, + Expression slice, + AbstractBounds keyRange, + QueryContext context, + int limit, + long totalRows) throws IOException; + + List> orderResultsBy(QueryContext context, + List keys, + Orderer orderer, + int limit, + long totalRows) throws IOException; + + List getSegments(); + + void populateSystemView(SimpleDataSet dataSet, SSTableReader sstable); + + long estimateMatchingRowsCount(Expression predicate); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java index cf899567b47b..adaf47dd9139 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java @@ -18,105 +18,127 @@ package org.apache.cassandra.index.sai.disk; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import javax.annotation.concurrent.NotThreadSafe; import com.google.common.base.Stopwatch; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Unfiltered; -import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.OnDiskFormat; +import org.apache.cassandra.index.sai.memory.RowMapping; import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.sstable.SSTableFlushObserver; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.io.sstable.SSTable; +import org.apache.cassandra.metrics.TableMetrics; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.Throwables; /** * Writes all on-disk index structures attached to a given SSTable. */ -@NotThreadSafe public class StorageAttachedIndexWriter implements SSTableFlushObserver { - private static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndexWriter.class); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final IndexDescriptor indexDescriptor; - private final Collection perIndexWriters; - private final PerSSTableIndexWriter perSSTableWriter; + private final PrimaryKey.Factory primaryKeyFactory; + private final Collection indices; + private final Collection perIndexWriters; + private final PerSSTableWriter perSSTableWriter; private final Stopwatch stopwatch = Stopwatch.createUnstarted(); private final RowMapping rowMapping; - private final long nowInSeconds = FBUtilities.nowInSeconds(); + private final OperationType opType; + private final TableMetrics tableMetrics; private DecoratedKey currentKey; private boolean tokenOffsetWriterCompleted = false; private boolean aborted = false; private long sstableRowId = 0; - - public static StorageAttachedIndexWriter createFlushObserverWriter(IndexDescriptor indexDescriptor, - Collection indexes, - LifecycleNewTracker lifecycleNewTracker) throws IOException + private long totalTimeSpent = 0; + + public StorageAttachedIndexWriter(IndexDescriptor indexDescriptor, + TableMetadata tableMetadata, + Collection indices, + LifecycleNewTracker lifecycleNewTracker, + long keyCount, + TableMetrics tableMetrics) throws IOException { - return new StorageAttachedIndexWriter(indexDescriptor, indexes, lifecycleNewTracker, false); - + this(indexDescriptor, tableMetadata, indices, lifecycleNewTracker, keyCount, false, tableMetrics); } - public static StorageAttachedIndexWriter createBuilderWriter(IndexDescriptor indexDescriptor, - Collection indexes, - LifecycleNewTracker lifecycleNewTracker, - boolean perIndexComponentsOnly) throws IOException - { - return new StorageAttachedIndexWriter(indexDescriptor, indexes, lifecycleNewTracker, perIndexComponentsOnly); - } - - private StorageAttachedIndexWriter(IndexDescriptor indexDescriptor, - Collection indexes, - LifecycleNewTracker lifecycleNewTracker, - boolean perIndexComponentsOnly) throws IOException + public StorageAttachedIndexWriter(IndexDescriptor indexDescriptor, + TableMetadata tableMetadata, + Collection indices, + LifecycleNewTracker lifecycleNewTracker, + long keyCount, + boolean perIndexComponentsOnly, + TableMetrics tableMetrics) throws IOException { + // We always use the version of the existing per-sstable components, if any, or the configured current version + // if there are no previously existing per-sstable components. + OnDiskFormat onDiskFormat = indexDescriptor.versionForNewComponents().onDiskFormat(); this.indexDescriptor = indexDescriptor; - this.rowMapping = RowMapping.create(lifecycleNewTracker.opType()); - this.perIndexWriters = indexes.stream().map(index -> indexDescriptor.newPerColumnIndexWriter(index, - lifecycleNewTracker, - rowMapping)) + // Note: I think there is a silent assumption here. That is, the PK factory we use here must be for the current + // format version, because that is what `IndexContext.keyFactory` always uses (see ctor) + this.primaryKeyFactory = onDiskFormat.newPrimaryKeyFactory(tableMetadata.comparator); + this.indices = indices; + this.opType = lifecycleNewTracker.opType(); + this.rowMapping = RowMapping.create(opType); + this.perIndexWriters = indices.stream().map(i -> onDiskFormat.newPerIndexWriter(i, + indexDescriptor, + lifecycleNewTracker, + rowMapping, + keyCount)) .filter(Objects::nonNull) // a null here means the column had no data to flush .collect(Collectors.toList()); // If the SSTable components are already being built by another index build then we don't want - // to build them again so use a null writer - this.perSSTableWriter = perIndexComponentsOnly ? PerSSTableIndexWriter.NONE : indexDescriptor.newPerSSTableIndexWriter(); + // to build them again so use a NO-OP writer + this.perSSTableWriter = perIndexComponentsOnly + ? PerSSTableWriter.NONE + : onDiskFormat.newPerSSTableWriter(indexDescriptor); + this.tableMetrics = tableMetrics; } @Override public void begin() { - logger.debug(indexDescriptor.logMessage("Starting partition iteration for storage-attached index flush for SSTable {}..."), indexDescriptor.sstableDescriptor); + logger.trace(indexDescriptor.logMessage("Starting partition iteration for storage attached index flush for SSTable {}..."), indexDescriptor.descriptor); stopwatch.start(); } @Override - public void startPartition(DecoratedKey key, long keyPosition, long keyPositionForSASI) + public void startPartition(DecoratedKey key, long position, long keyPositionForSASI) { if (aborted) return; - + currentKey = key; try { - perSSTableWriter.startPartition(key); + perSSTableWriter.startPartition(position); } catch (Throwable t) { - logger.error(indexDescriptor.logMessage("Failed to record a partition during an index build"), t); + logger.error(indexDescriptor.logMessage("Failed to record a partition start during an index build"), t); abort(t, true); + // fail compaction task or index build task if SAI failed + throw Throwables.unchecked(t); } } @@ -129,19 +151,16 @@ public void nextUnfilteredCluster(Unfiltered unfiltered) if (!unfiltered.isRow()) return; - // Ignore rows with no live data... - Row row = (Row) unfiltered; - if (!row.hasLiveData(nowInSeconds, false)) - return; - try { - addRow(row); + addRow((Row) unfiltered); } catch (Throwable t) { logger.error(indexDescriptor.logMessage("Failed to record a row during an index build"), t); abort(t, true); + // fail compaction task or index build task if SAI failed + throw Throwables.unchecked(t); } } @@ -149,7 +168,7 @@ public void nextUnfilteredCluster(Unfiltered unfiltered) public void staticRow(Row staticRow) { if (aborted) return; - + if (staticRow.isEmpty()) return; @@ -161,6 +180,8 @@ public void staticRow(Row staticRow) { logger.error(indexDescriptor.logMessage("Failed to record a static row during an index build"), t); abort(t, true); + // fail compaction task or index build task if SAI failed + throw Throwables.unchecked(t); } } @@ -171,60 +192,111 @@ public void onSSTableWriterSwitched() try { - for (PerColumnIndexWriter w : perIndexWriters) + long start = Clock.Global.nanoTime(); + for (PerIndexWriter w : perIndexWriters) { w.onSSTableWriterSwitched(stopwatch); } + totalTimeSpent += (Clock.Global.nanoTime() - start); } catch (Throwable t) { logger.error(indexDescriptor.logMessage("Failed to flush segment on sstable writer switched"), t); abort(t, true); + // fail compaction task or index build task if SAI failed + throw Throwables.unchecked(t); } } @Override - public void complete() + public void complete(SSTable sstable) { - if (aborted) return; - - long start = stopwatch.elapsed(TimeUnit.MILLISECONDS); - - logger.debug(indexDescriptor.logMessage("Completed partition iteration for index flush for SSTable {}. Elapsed time: {} ms"), - indexDescriptor.sstableDescriptor, start); - + long startComplete = Clock.Global.nanoTime(); try { - perSSTableWriter.complete(); - tokenOffsetWriterCompleted = true; + if (aborted) return; + + long start = stopwatch.elapsed(TimeUnit.MILLISECONDS); - long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); + logger.trace(indexDescriptor.logMessage("Completed partition iteration for index flush for SSTable {}. Elapsed time: {} ms"), + indexDescriptor.descriptor, + start); - logger.debug(indexDescriptor.logMessage("Completed per-SSTable write for SSTable {}. Duration: {} ms. Total elapsed time: {} ms."), - indexDescriptor.sstableDescriptor, elapsed - start, elapsed); + try + { + perSSTableWriter.complete(stopwatch); + tokenOffsetWriterCompleted = true; + long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); + logger.trace(indexDescriptor.logMessage("Completed per-SSTable write for SSTable {}. Duration: {} ms. Total elapsed time: {} ms."), + indexDescriptor.descriptor, + elapsed - start, + elapsed); - start = elapsed; + start = elapsed; - rowMapping.complete(); + rowMapping.complete(); - for (PerColumnIndexWriter perIndexWriter : perIndexWriters) + for (PerIndexWriter perIndexWriter : perIndexWriters) + { + perIndexWriter.complete(stopwatch); + + // The handling of components when we flush/compact is a tad backward: instead of registering the + // components as we write them, all the components are collected beforehand in `SSTableWriter#create`, + // which means this is a superset of possible components, but if any components are not written for + // those reason, this needs to be fixed afterward. One case for SAI component for instance is empty + // indexes: if a particular sstable has nothing indexed for a particular index, then only the completion + // marker for that index is kept on disk but no other components, so we need to remove the components + // that were "optimistically" added (and more generally, future index implementation may have some + // components that are only optionally present based on specific conditions). + // Note 1: for index build/rebuild on existing sstable, `SSTableWriter#create` is not used, and instead + // we do only register components written (see `StorageAttachedIndexBuilder#completeSSTable`). + // Note 2: as hinted above, an alternative here would be to change the whole handling of components, + // registering components only as they are effectively written. This is a larger refactor, with some + // subtleties involved, so it is left as potential future work. + if (opType == OperationType.FLUSH || opType == OperationType.COMPACTION) + { + var writtenComponents = perIndexWriter.writtenComponents().allAsCustomComponents(); + var registeredComponents = IndexDescriptor.perIndexComponentsForNewlyFlushedSSTable(perIndexWriter.indexContext()); + var toRemove = Sets.difference(registeredComponents, writtenComponents); + if (!toRemove.isEmpty()) + { + if (logger.isTraceEnabled()) + { + logger.trace(indexDescriptor.logMessage("Removing optimistically added but not writen components from TOC of SSTable {} for index {}"), + indexDescriptor.descriptor, + perIndexWriter.indexContext().getIndexName()); + } + + // During flush, this happens as we finalize the sstable and before its size is tracked, so not + // passing a tracker is correct and intended (there is nothing to update in the tracker). + sstable.unregisterComponents(toRemove, null); + } + } + } + elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); + logger.trace(indexDescriptor.logMessage("Completed per-index writes for SSTable {}. Duration: {} ms. Total elapsed time: {} ms."), + indexDescriptor.descriptor, + elapsed - start, + elapsed); + } + catch (Throwable t) { - perIndexWriter.complete(stopwatch); + logger.error(indexDescriptor.logMessage("Failed to complete an index build"), t); + abort(t, true); + // fail compaction task or index build task if SAI failed + throw Throwables.unchecked(t); } - elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); - logger.debug(indexDescriptor.logMessage("Completed per-index writes for SSTable {}. Duration: {} ms. Total elapsed time: {} ms."), - indexDescriptor.sstableDescriptor, elapsed - start, elapsed); } - catch (Throwable t) + finally { - logger.error(indexDescriptor.logMessage("Failed to complete an index build"), t); - abort(t, true); + totalTimeSpent += (Clock.Global.nanoTime() - startComplete); + tableMetrics.updateStorageAttachedIndexWritingTime(totalTimeSpent, opType); } } /** * Aborts all column index writers and, only if they have not yet completed, SSTable-level component writers. - * + * * @param accumulator the initial exception thrown from the failed writer */ @Override @@ -240,12 +312,23 @@ public void abort(Throwable accumulator) */ public void abort(Throwable accumulator, boolean fromIndex) { - if (aborted) return; + if (aborted) + return; - // Mark the write operation aborted, so we can short-circuit any further operations on the component writers. + // Mark the write aborted, so we can short-circuit any further operations on the component writers. aborted = true; - - for (PerColumnIndexWriter perIndexWriter : perIndexWriters) + + // For non-compaction and non-flush, make any indexes involved in this transaction non-queryable, + // as they will likely not match the backing table. + // For compaction and flush: the task should be aborted and new sstables will not be added to tracker. + // We do not want to mark the index as non-queryable on compaction and flush because otherwise + // the index status would be propagated to the other nodes and that would make querying the index impossible + // also on the other nodes. If the problem with compaction or flush repeats, then it is better to fail + // on this node only and let the rest of the cluster operate normally. + if (fromIndex && opType != OperationType.COMPACTION && opType != OperationType.FLUSH) + indices.forEach(StorageAttachedIndex::makeIndexNonQueryable); + + for (PerIndexWriter perIndexWriter : perIndexWriters) { try { @@ -259,30 +342,35 @@ public void abort(Throwable accumulator, boolean fromIndex) } } } - + if (!tokenOffsetWriterCompleted) { - // If the token/offset files have already been written successfully, they can be reused later. - perSSTableWriter.abort(); + // If the token/offset files have already been written successfully, they can be reused later. + perSSTableWriter.abort(accumulator); } - // If the abort was from an index error, propagate the error upstream so index builds, compactions, and + // If the abort was from an index error, propagate the error upstream so index builds, compactions, and // flushes can handle it correctly. if (fromIndex) throw Throwables.unchecked(accumulator); } - private void addRow(Row row) throws IOException, InMemoryTrie.SpaceExhaustedException + private void addRow(Row row) throws IOException, TrieSpaceExhaustedException { - PrimaryKey primaryKey = indexDescriptor.hasClustering() ? indexDescriptor.primaryKeyFactory.create(currentKey, row.clustering()) - : indexDescriptor.primaryKeyFactory.create(currentKey); + // we are using System.nanoTime() instead of ApproximateTime.nanoTime() here because + // it is verify likely that this method takes microsecronds instead of milliseconds + // and ApproximateTime.nanoTime() precision is 2 milliseconds + long now = Clock.Global.nanoTime(); + PrimaryKey primaryKey = primaryKeyFactory.create(currentKey, row.clustering()); perSSTableWriter.nextRow(primaryKey); rowMapping.add(primaryKey, sstableRowId); - for (PerColumnIndexWriter w : perIndexWriters) + for (PerIndexWriter w : perIndexWriters) { w.addRow(primaryKey, row, sstableRowId); } sstableRowId++; + + totalTimeSpent += (Clock.Global.nanoTime() - now); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/TermsIterator.java b/src/java/org/apache/cassandra/index/sai/disk/TermsIterator.java new file mode 100644 index 000000000000..bd84d1228ecd --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/TermsIterator.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Iterator; +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * Iterator to step through terms to obtain {@link PostingList} for the current term. + * + * Term enumerations are always ordered by their {@link ByteSource}. + */ +@NotThreadSafe +public interface TermsIterator extends Iterator, Closeable +{ + /** + * Get {@link PostingList} for the current term. + */ + PostingList postings() throws IOException; + + /** + * Get the minimum term in the iterator. Due to legacy design, this is the term as represented on disk without + * any special encoding. + */ + ByteBuffer getMinTerm(); + + /** + * Get the maximum term in the iterator. Due to legacy design, this is the term as represented on disk without + * any special encoding. + */ + ByteBuffer getMaxTerm(); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/ComponentsBuildId.java b/src/java/org/apache/cassandra/index/sai/disk/format/ComponentsBuildId.java new file mode 100644 index 000000000000..6f0e87d22a13 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/ComponentsBuildId.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.format; + +import java.util.Objects; +import java.util.function.Predicate; + +import javax.annotation.Nullable; + +import org.apache.cassandra.index.sai.IndexContext; + +/** + * Identifies a particular build of a per-sstable or per-index group of SAI index components, aka a pair of the + * {@link Version} built and the generation. + */ +public class ComponentsBuildId implements Comparable +{ + private final Version version; + private final int generation; + + private ComponentsBuildId(Version version, int generation) + { + this.version = version; + this.generation = generation; + } + + public static ComponentsBuildId of(Version version, int generation) + { + return new ComponentsBuildId(version, generation); + } + + public static ComponentsBuildId forNewSSTable(Version version) + { + return ComponentsBuildId.of(version, 0); + } + + public static ComponentsBuildId forNewBuild(Version version, @Nullable ComponentsBuildId previousBuild, Predicate newBuildIsUsablePredicate) + { + // If we're not using immutable components, we always use generation 0, and we're fine if that overrides existing files + if (!version.useImmutableComponentFiles()) + return new ComponentsBuildId(version, 0); + + // Otherwise, if there is no previous build or the new build is for a new version, then we can "tentatively" + // use generation 0, but if not, we need to bump the generation. + int generation = previousBuild != null && previousBuild.version.equals(version) ? previousBuild.generation + 1 : 0; + var candidate = new ComponentsBuildId(version, generation); + + // Usually, the candidate above is fine, but we want to avoid overriding existing file (it's theoretically + // possible that the next generation was created at some other point, but then corrupted, and so we falled back + // on the previous generation but some of those file for the next generation still exists). So we check, + // repeatedly if that candidate is usable, incrementing the generation until we find one which is. + while (!newBuildIsUsablePredicate.test(candidate)) + candidate = new ComponentsBuildId(version, ++generation); + + return candidate; + } + + public Version version() + { + return version; + } + + public int generation() + { + return generation; + } + + public String formatAsComponent(IndexComponentType indexComponentType, IndexContext indexContext) + { + return version.fileNameFormatter().format(indexComponentType, indexContext, generation); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof ComponentsBuildId)) + return false; + ComponentsBuildId that = (ComponentsBuildId) obj; + return this.version.equals(that.version) && this.generation == that.generation; + } + + @Override + public int hashCode() + { + return Objects.hash(version, generation); + } + + @Override + public int compareTo(ComponentsBuildId that) + { + if (this.version.equals(that.version)) + return Integer.compare(generation, that.generation); + + return this.version.onOrAfter(that.version) ? 1 : -1; + } + + @Override + public String toString() + { + return version + "@" + generation; + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/DefaultIndexComponentDiscovery.java b/src/java/org/apache/cassandra/index/sai/disk/format/DefaultIndexComponentDiscovery.java new file mode 100644 index 000000000000..5fb25fab1866 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/DefaultIndexComponentDiscovery.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.format; + +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.TableMetadata; + +public class DefaultIndexComponentDiscovery extends IndexComponentDiscovery +{ + @Override + public SSTableIndexComponentsState discoverComponents(SSTableReader sstable) + { + return discoverComponents(sstable.getDescriptor(), sstable.metadata()); + } + + @Override + public SSTableIndexComponentsState discoverComponents(Descriptor descriptor, TableMetadata metadata) + { + // Older versions might not have all components in the TOC, we should not trust it (fix for CNDB-13582): + if (descriptor.version.version.compareTo("ca") < 0) + return discoverComponentsFromDiskFallback(descriptor); + + SSTableIndexComponentsState groups = tryDiscoverComponentsFromTOC(descriptor); + return groups == null + ? discoverComponentsFromDiskFallback(descriptor) + : groups; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java index 21cd5cf455d7..f4e5beeaa00d 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java @@ -18,115 +18,70 @@ package org.apache.cassandra.index.sai.disk.format; -import java.util.regex.Pattern; +import java.io.IOException; +import java.nio.ByteOrder; -import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter; -import org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryWriter; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.lucene.store.ChecksumIndexInput; -import static org.apache.cassandra.index.sai.disk.format.Version.SAI_DESCRIPTOR; -import static org.apache.cassandra.index.sai.disk.format.Version.SAI_SEPARATOR; +public interface IndexComponent +{ + IndexComponents parent(); + IndexComponentType componentType(); -/** - * This is a definitive list of all the on-disk components for all versions - */ -public enum IndexComponent -{ - /** - * Metadata for per-column index components - */ - META("Meta"), - - /** - * Balanced tree written by {@code BlockBalancedTreeWriter} indexes mappings of term to one or more segment row IDs - * (segment row ID = SSTable row ID - segment row ID offset). - */ - BALANCED_TREE("BalancedTree"), - - /** - * Term dictionary written by {@link TrieTermsDictionaryWriter} stores mappings of term and - * file pointer to posting block on posting file. - */ - TERMS_DATA("TermsData"), - - /** - * Product Quantization store used to store compressed vectors for the vector index - */ - COMPRESSED_VECTORS("CompressedVectors"), - - /** - * Stores postings written by {@link PostingsWriter} - */ - POSTING_LISTS("PostingLists"), - - /** - * If present indicates that the column index build completed successfully - */ - COLUMN_COMPLETION_MARKER("ColumnComplete"), - - - // per-sstable components - /** - * An on-disk block packed index mapping rowIds to token values. - */ - ROW_TO_TOKEN("RowToToken"), - - /** - * An on-disk block packed index mapping rowIds to partitionIds. - */ - ROW_TO_PARTITION("RowToPartition"), - - /** - * An on-disk block packed index mapping partitionIds to the number of rows for the partition. - */ - PARTITION_TO_SIZE("PartitionToSize"), - - /** - * Prefix-compressed blocks of partition keys used for rowId to partition key lookups - */ - PARTITION_KEY_BLOCKS("PartitionKeyBlocks"), - - /** - * Encoded sequence of offsets to partition key blocks - */ - PARTITION_KEY_BLOCK_OFFSETS("PartitionKeyBlockOffsets"), - - /** - * Prefix-compressed blocks of clustering keys used for rowId to clustering key lookups - */ - CLUSTERING_KEY_BLOCKS("ClusteringKeyBlocks"), - - /** - * Encoded sequence of offsets to clustering key blocks - */ - CLUSTERING_KEY_BLOCK_OFFSETS("ClusteringKeyBlockOffsets"), - - /** - * Metadata for per-SSTable on-disk components. - */ - GROUP_META("GroupMeta"), - - /** - * If present indicates that the per-sstable index build completed successfully - */ - GROUP_COMPLETION_MARKER("GroupComplete"); - - public final String name; - public final Component.Type type; - - IndexComponent(String name) + ByteOrder byteOrder(); + + String fileNamePart(); + Component asCustomComponent(); + File file(); + + default boolean isCompletionMarker() + { + return componentType() == parent().completionMarkerComponent(); + } + + interface ForRead extends IndexComponent { - this.name = name; - this.type = componentType(name); + @Override + IndexComponents.ForRead parent(); + + FileHandle createFileHandle(); + + /** + * Opens a file handle for the provided index component similarly to {@link #createFileHandle()}, + * but this method shoud be called instead of the aforemented one if the access is done during index building, that is + * before the full index that this is a part of has been finalized. + *

    + * The use of this method can allow specific storage providers, typically tiered storage ones, to distinguish accesses + * that happen "at index building time" from other accesses, as the related file may be in different tier of storage. + */ + FileHandle createIndexBuildTimeFileHandle(); + + IndexInput openInput(); + + ChecksumIndexInput openCheckSummedInput(); } - private static Component.Type componentType(String name) + interface ForWrite extends IndexComponent { - String componentName = SAI_DESCRIPTOR + SAI_SEPARATOR + name; - String repr = Pattern.quote(SAI_DESCRIPTOR + SAI_SEPARATOR) - + ".*" - + Pattern.quote(SAI_SEPARATOR + name + ".db"); - return Component.Type.create(componentName, repr, true, null); + @Override + IndexComponents.ForWrite parent(); + + default IndexOutputWriter openOutput() throws IOException + { + return openOutput(false); + } + + IndexOutputWriter openOutput(boolean append) throws IOException; + + void createEmpty() throws IOException; + + /** Deletes the underlying component file if it exists. */ + void delete(); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentDiscovery.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentDiscovery.java new file mode 100644 index 000000000000..81db10f96dca --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentDiscovery.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.format; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.NoSuchFileException; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.TOCComponent; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_CUSTOM_COMPONENTS_DISCOVERY_CLASS; +import static org.apache.cassandra.index.sai.disk.format.SSTableIndexComponentsState.State.toMB; + +/** + * Handles "discovering" SAI index components files from disk for a given sstable. + *

    + * This is used by {@link IndexDescriptor} and should rarely, if ever, be used directly, but it is exposed publicly to + * make the logic "pluggable" (typically for tiered-storage that may not store files directly on disk and thus require + * some specific abstraction). + */ +public abstract class IndexComponentDiscovery +{ + private static final Logger logger = LoggerFactory.getLogger(IndexComponentDiscovery.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + + + // This works around potential (if very unlikely in our case) class-loading issues. + private static class LazyDiscoveryInitialization + { + private static final IndexComponentDiscovery instance = !SAI_CUSTOM_COMPONENTS_DISCOVERY_CLASS.isPresent() + ? new DefaultIndexComponentDiscovery() {} + : FBUtilities.construct(SAI_CUSTOM_COMPONENTS_DISCOVERY_CLASS.getString(), "SAI index components discovery"); + } + + public static IndexComponentDiscovery instance() + { + return LazyDiscoveryInitialization.instance; + } + + /** + * Returns the set of groups of SAI components that should be used for the provided sstable. + *

    + * Note that "discovery" in this method only means finding out the "build ID" (version and generation) that should + * be used for each group of components (per-sstable and per-index). + * + * @param sstable the sstable reader for which to discover components. + * @return the discovered {@link ComponentsBuildId} to use for both per-sstable and each per-index components. The + * returned build IDs should usually correspond to existing index components on disk but this is not a strong + * asumption: if some group of components corresponding to the returned build ID has no completion marker or is + * missing files, the group will not be usuable (and the corresponding index/indexes will not be usable), but this + * should be handled "gracefully" by callers. + */ + public abstract SSTableIndexComponentsState discoverComponents(SSTableReader sstable); + + /** + * Returns the set of groups of SAI components that should be used for the provided sstable. + *

    + * Note that "discovery" in this method only means finding out the "build ID" (version and generation) that should + * be used for each group of components (per-sstable and per-index). + *

    + * Please note that the {@link #discoverComponents(SSTableReader)} method should be prefered when a + * {@link SSTableReader} exists as some implementations may be more optimal or use additional checks when provided + * a reader. + * + * @param descriptor the descriptor of the sstable for which to discover components. + * @param metadata the metadata of the table the sstable belongs to. + * @return the discovered {@link ComponentsBuildId} to use for both per-sstable and each per-index component. The + * returned build IDs should usually correspond to existing index components on disk, but this is not a strong + * asumption: if some group of components corresponding to the returned build ID has no completion marker or is + * missing files, the group will not be usuable (and the corresponding index/indexes will not be usable), but this + * should be handled "gracefully" by callers. + */ + public abstract SSTableIndexComponentsState discoverComponents(Descriptor descriptor, TableMetadata metadata); + + protected static IndexComponentType completionMarker(@Nullable String name) + { + return name == null ? IndexComponentType.GROUP_COMPLETION_MARKER : IndexComponentType.COLUMN_COMPLETION_MARKER; + } + + /** + * Tries reading the TOC file of the provided SSTable to discover its current SAI components. + * + * @param descriptor the SSTable to read the TOC file of. + * @return the discovered components, or `null` if the TOC file is missing or if it is corrupted in some way. + */ + protected @Nullable SSTableIndexComponentsState tryDiscoverComponentsFromTOC(Descriptor descriptor) + { + Set componentsFromToc = readSAIComponentFromSSTableTOC(descriptor); + if (componentsFromToc == null) + return null; + + // We collect all the version/generation for which we have files on disk for the per-sstable parts and every + // per-index found. + Map states = new HashMap<>(); + Set invalid = new HashSet<>(); + for (Component component : componentsFromToc) + { + // We try parsing it as an SAI index name, and ignore if it doesn't match. + var opt = Version.tryParseFileName(component.name); + if (opt.isEmpty()) + continue; + + var parsed = opt.get(); + String indexName = parsed.indexName; + + if (invalid.contains(indexName)) + continue; + + var prev = states.computeIfAbsent(indexName, k -> new StateBuilder(parsed.buildId)); + if (!prev.buildId.equals(parsed.buildId)) + { + logger.error("Found multiple versions/generations of SAI components in TOC for SSTable {}: cannot load {}", + descriptor, indexName == null ? "per-SSTable components" : "per-index components of " + indexName); + + states.remove(indexName); + invalid.add(indexName); + } + + prev.totalSizeInBytes += descriptor.fileFor(component).length(); + } + + return StateBuilder.convert(states); + } + + private @Nullable Set readSAIComponentFromSSTableTOC(Descriptor descriptor) + { + try + { + // We skip the check for missing components on purpose: we do the existence check here because we want to + // know when it fails. + Set components = TOCComponent.loadTOC(descriptor, false); + Set SAIComponents = new HashSet<>(); + for (Component component : components) + { + // We only care about SAI components, which are "custom" + if (component.type != SSTableFormat.Components.Types.CUSTOM) + continue; + + // And all start with "SAI" (the rest can depend on the version, but that part is common to all version) + if (!component.name.startsWith(Version.SAI_DESCRIPTOR)) + continue; + + // Lastly, we check that the component file exists. If it doesn't, then we assume something is wrong + // with the TOC and we fall back to scanning the disk. This is admittedly a bit conservative, but + // we do have test data in `test/data/legacy-sai/aa` where the TOC is broken: it lists components that + // simply do not match the accompanying files (the index name differs), and it is unclear if this is + // just a mistake made while gathering the test data or if some old version used to write broken TOC + // for some reason (more precisely, it is hard to be entirely sure this isn't the later). + // Overall, there is no real reason for the TOC to list non-existing files (typically, when we remove + // an index, the TOC is rewritten to omit the removed component _before_ the files are deleted), so + // falling back conservatively feels reasonable. + if (!descriptor.fileFor(component).exists()) + { + noSpamLogger.warn("The TOC file for SSTable {} lists SAI component {} but it doesn't exists. Assuming the TOC is corrupted somehow and falling back on disk scanning (which may be slower)", descriptor, component.name); + return null; + } + + SAIComponents.add(component); + } + return SAIComponents; + } + catch (NoSuchFileException e) + { + // This is totally fine when we're building an `IndexDescriptor` for a new sstable that does not exist. + // But if the sstable exist, then that's less expected as we should have a TOC. But because we want to + // be somewhat resilient to losing the TOC and that historically the TOC hadn't been relyed on too strongly, + // we return `null` which trigger the fall-back path to scan disk. + if (descriptor.fileFor(SSTableFormat.Components.DATA).exists()) + { + noSpamLogger.warn("SSTable {} exists (its data component exists) but it has no TOC file. Will use disk scanning to discover SAI components as fallback (which may be slower).", descriptor); + return null; + } + + return Collections.emptySet(); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + /** + * Scan disk to find all the SAI components for the provided descriptor that exists on disk. Then pick + * the approriate set of those components (the highest version/generation for which there is a completion marker). + * This should usually only be used ask a fallback because this will scan the whole table directory every time and + * can be a bit inefficient, especially when some tiered storage is used underneath where scanning a directory may + * be particularly expensive. And picking the most recent version/generation is usually the right thing to do, but + * may lack flexibility in some cases. + * + * @param descriptor the SSTable for which to discover components for. + * @return the discovered components. This is never {@code null}, but could well be empty if no SAI components are + * found. + */ + protected SSTableIndexComponentsState discoverComponentsFromDiskFallback(Descriptor descriptor) + { + // For each "component group" (of each individual index, plus the per-sstable group), the "active" group is the + // one with the most recent build amongst complete ones. So we scan disk looking for completion markers (since + // that's what tell us a group is complete), and keep for each group the max build we find. + Map> states = new HashMap<>(); + PathUtils.forEach(descriptor.directory.toPath(), path -> { + String filename = path.getFileName().toString(); + // First, we skip any file that do not belong to the sstable this is a descriptor for. + if (!filename.startsWith(descriptor.filenamePart())) + return; + + Version.tryParseFileName(filename) + .ifPresent(parsed -> { + var forGroup = states.computeIfAbsent(parsed.indexName, k -> new HashMap<>()); + var state = forGroup.computeIfAbsent(parsed.buildId, k -> new StateBuilder(parsed.buildId)); + state.totalSizeInBytes += PathUtils.size(path); + if (parsed.component == completionMarker(parsed.indexName)) + state.isComplete = true; + }); + }); + + Map maxStates = new HashMap<>(); + for (var entry : states.entrySet()) + { + entry.getValue() + .values() + .stream() + .filter(s -> s.isComplete) + .max(Comparator.comparing(s -> s.buildId)) + .ifPresent(max -> maxStates.put(entry.getKey(), max)); + } + + return StateBuilder.convert(maxStates); + } + + private static class StateBuilder + { + private final ComponentsBuildId buildId; + private long totalSizeInBytes; + private boolean isComplete; + + StateBuilder(ComponentsBuildId buildId) + { + this.buildId = buildId; + } + + void addTo(SSTableIndexComponentsState.Builder builder, @Nullable String indexName) + { + if (indexName == null) + builder.addPerSSTable(buildId, toMB(totalSizeInBytes)); + else + builder.addPerIndex(indexName, buildId, toMB(totalSizeInBytes)); + } + + static SSTableIndexComponentsState convert(Map states) + { + SSTableIndexComponentsState.Builder builder = SSTableIndexComponentsState.builder(); + states.forEach((indexName, state) -> state.addTo(builder, indexName)); + return builder.build(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java new file mode 100644 index 000000000000..bf38d6b8fbd8 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.format; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * This is a definitive list of all the on-disk components for all versions + */ +public enum IndexComponentType +{ + /** + * Stores per-index metadata. + * + * V1 + */ + META("Meta"), + /** + * KDTree written by {@code BKDWriter} indexes mappings of term to one ore more segment row IDs + * (segment row ID = SSTable row ID - segment row ID offset). + * + * V1 + */ + KD_TREE("KDTree"), + KD_TREE_POSTING_LISTS("KDTreePostingLists"), + + /** + * Vector index components + */ + VECTOR("Vector"), + PQ("PQ"), + + /** + * Term dictionary written by {@code TrieTermsDictionaryWriter} stores mappings of term and + * file pointer to posting block on posting file. + * + * V1 + */ + TERMS_DATA("TermsData"), + /** + * Stores postings written by {@code PostingsWriter} + * + * V1 + */ + POSTING_LISTS("PostingLists"), + /** + * If present indicates that the column index build completed successfully + * + * V1 + */ + COLUMN_COMPLETION_MARKER("ColumnComplete"), + + // per-sstable components + /** + * Partition key token value for rows including row tombstone and static row. (access key is rowId) + * + * V1 V2 + */ + TOKEN_VALUES("TokenValues"), + /** + * Partition key offset in sstable data file for rows including row tombstone and static row. (access key is + * rowId) + * + * V1 + */ + OFFSETS_VALUES("OffsetsValues"), + /** + * An on-disk trie containing the primary keys used for looking up the rowId from a partition key + * + * V2 + */ + PRIMARY_KEY_TRIE("PrimaryKeyTrie"), + /** + * Prefix-compressed blocks of primary keys used for rowId to partition key lookups + * + * V2 + */ + PRIMARY_KEY_BLOCKS("PrimaryKeyBlocks"), + /** + * Encoded sequence of offsets to primary key blocks + * + * V2 + */ + PRIMARY_KEY_BLOCK_OFFSETS("PrimaryKeyBlockOffsets"), + /** + * Stores per-sstable metadata. + * + * V1 + */ + GROUP_META("GroupMeta"), + /** + * If present indicates that the per-sstable index build completed successfully + * + * V1 V2 + */ + GROUP_COMPLETION_MARKER("GroupComplete"), + + /** + * Stores document length information for BM25 scoring + */ + DOC_LENGTHS("DocLengths"); + + public final String representation; + + IndexComponentType(String representation) + { + this.representation = representation; + } + + static final Map byRepresentation = new HashMap<>(); + static + { + for (IndexComponentType component : values()) + byRepresentation.put(component.representation, component); + } + + public static @Nullable IndexComponentType fromRepresentation(String representation) + { + return byRepresentation.get(representation); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java new file mode 100644 index 000000000000..17293ea910e5 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java @@ -0,0 +1,367 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.format; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.apache.lucene.index.CorruptIndexException; +import java.util.Collection; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.lifecycle.Tracker; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.SSTable; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * Represents a related group of concrete SAI components files which are either all the per-sstable components of a + * given sstable, or all the components of a particular column index (for a given sstable). + *

    + * The members of a component group correspond to actual SAI component files on disk, and are components that are + * written together. A group is identified by: + *

      + *
    • the sstable it is the components of (identified by the sstable {@link #descriptor()})
    • + *
    • the index ({@link #context()}) for the components, which is {@code null} for the per-sstable components
    • + *
    • the version the components are using. All the components of a group are on the same version
    • + *
    • the generation, within the version, of the group/components. Generations are used when + * {@link CassandraRelevantProperties#IMMUTABLE_SAI_COMPONENTS} is used, to avoid new builds to override (and thus + * mutate) older builds. When the option of immutable components is not used, then the generation is fixed to 0. + * See below for more details.
    • + *
    + *

    + * + *

    Immutable components

    + * + * As mentioned above, when {@link CassandraRelevantProperties#IMMUTABLE_SAI_COMPONENTS} is enabled, existing components + * are not overwritten by new builds (the underlying intent is to allow rebuilding without stopping reads (to the + * rebuilt indexes). But unless a rebuild uses a different version than the existing components, the new components + * need "something" to distinguish them from the old ones, and it is what the generation provides. + *

    + * The generation is specific to a component group, meaning that all the components of a group share the same + * generation (or to put it another way, if 2 components only differ by their generation, then they belong to different + * build and thus different groups), but that different groups can have different generations. For instance, if a + * specific index is rebuilt but without rebuilding the per-sstable components of each sstable, then after that rebuild + * the per-sstable groups will have generation 0, but the index groups will have generation 1. + *

    + * When a sstable is "loaded", the "active" set of components to use is based on finding, for each kind of groups + * (per-sstable and per-index), the "complete" (see next paragraph) set of components with the highest version, and + * highest generation within that version. + *

    + * A group of components may or may not be "complete" ({@link #isComplete()}): it is complete if the completion marker + * component for that group is present. Groups are temporarily incomplete during writing, but can also be more + * permanently incomplete for 2 main reasons: a build may fail mid-way, leaving one or more group incomplete, or we can + * have some corruption of the component files of some sort (corruption could here mean that a file is mistakenly + * deleted, or that the content of the file is corrupted somehow; the later triggers a removal of the corrupted file and + * of the completion marker, but not of the other component of the group). The bumping of generations takes incomplete + * groups into account, and so incomplete groups are not overridden either. Essentially, the generation used by a new + * build is always one more than the highest generation of any component found on disk (for the group in question, and + * the version we writting, usually {@link Version#current(String)}). + */ +public interface IndexComponents +{ + Logger logger = LoggerFactory.getLogger(IndexComponents.class); + + /** + * SSTable this is the group of. + */ + Descriptor descriptor(); + + /** + * The {@link IndexDescriptor} that created this group. + *

    + * Note that {@link IndexDescriptor} essentially tracks the active and created groups for a sstable, and so a group + * always comes from a particular {@link IndexDescriptor} instance. + */ + IndexDescriptor indexDescriptor(); + + /** + * Context of the group. + * + * @return the context of the index this is a group of, or {@code null} if the group is a per-sstable group. + */ + @Nullable IndexContext context(); + + /** + * The build id of the components of this group. + */ + ComponentsBuildId buildId(); + + /** + * Version used by the components of the groups (part of the {@link #buildId()} but exposed directly because often + * used). + */ + default Version version() + { + return buildId().version(); + } + + /** + * The on-disk format used by the these components. + */ + default OnDiskFormat onDiskFormat() + { + return version().onDiskFormat(); + } + + /** + * Whether that's a per-index group, that is one with components specific to a given index. Otherwise, it is a + * per-sstable group, that is one with components shared by all the SAI indexes (on that sstable). + */ + default boolean isPerIndexGroup() + { + return context() != null; + } + + /** + * The specific component "kind" used for storing the metadata of this group. + */ + default IndexComponentType metadataComponent() + { + return isPerIndexGroup() ? IndexComponentType.META : IndexComponentType.GROUP_META; + } + + /** + * The specific component "kind" used as a completion marker for this group. + */ + default IndexComponentType completionMarkerComponent() + { + return isPerIndexGroup() ? IndexComponentType.COLUMN_COMPLETION_MARKER : IndexComponentType.GROUP_COMPLETION_MARKER; + } + + default String logMessage(String message) + { + return indexDescriptor().logMessage(message); + } + + /** + * Whether the provided component "kind" exists in this group. + */ + boolean has(IndexComponentType component); + + /** + * Whether this group is complete, meaning that is has a completion marker. + */ + default boolean isComplete() + { + return has(completionMarkerComponent()); + } + + /** + * An empty group is one that is complete, but has only a completion marker and no other components. + */ + boolean isEmpty(); + + /** + * The complete set of component types that are expected for this group version. + */ + default Set expectedComponentsForVersion() + { + return isPerIndexGroup() + ? onDiskFormat().perIndexComponentTypes(context()) + : onDiskFormat().perSSTableComponentTypes(); + } + + default ByteComparable.Version byteComparableVersionFor(IndexComponentType component) + { + return version().byteComparableVersionFor(component, descriptor().version); + } + + /** + * Specialisation of {@link IndexComponents} used when working with complete and active groups, and so mostly used + * for reading components. + */ + interface ForRead extends IndexComponents + { + /** + * Get the component of the provided type if present. + * + * @param component the type of the component to retrieve. + * @return the component, or {@code null} if the group has no such component. + */ + IndexComponent.ForRead get(IndexComponentType component); + + /** + * The total size on disk used by the components of this group. + */ + long liveSizeOnDiskInBytes(); + + Collection all(); + + default Set allAsCustomComponents() + { + return all() + .stream() + .map(IndexComponent::asCustomComponent) + .collect(Collectors.toSet()); + } + + /** + * Validates this group and its components. + *

    + * This is a shortcut for {@link #isValid(boolean, Consumer)} when nothing particular is done for + * invalid components. + * + * @param validateChecksum if {@code true}, the checksum of the components will be validated. Otherwise, only + * basic checks on the header and footers will be performed. + * @return whether the group is valid. + */ + default boolean isValid(boolean validateChecksum) + { + return isValid(validateChecksum, c -> {}); + } + + /** + * Validates this group and its components. + *

    + * This method checks that the group has all the components that it should have, and that the content of + * those components are, as far as this method can determine, valid. Specifics about what failed validation + * is also be logged. + * + * @param validateChecksum if {@code true}, the checksum of the components will be validated. Otherwise, only + * basic checks on the header and footers will be performed. + * @param onInvalidComponent called on any component that is present but whose content appears invalid/corrupted + * (what is checked depends on {@code validateChecksum}). + * @return whether the group is valid. + */ + boolean isValid(boolean validateChecksum, Consumer onInvalidComponent); + + /** + * Validate this group and its components, and invalidate the group if it is invalid. + *

    + * This method is a shortcut for a call to {@link #isValid(boolean, Consumer)} that deletes corrupted components + * if this is the configured behavior, followed by a call {@link #invalidate} if the validation fails. + * + * @param sstable the sstable object for which the component are validated (which should correspond to + * {@code this.descriptor()}). This must be provided so if some components are invalid, they + * can be unregistered. + * @param tracker the {@link Tracker} of the table (of the sstable/components). Like the sstable, this is used + * as part of unregistering the components when they are invalid. + * @param validateChecksum if {@code true}, the checksum of the components will be validated. Otherwise, only + * basic checks on the header and footers will be performed. + * @param rethrow whether to throw an {@link UncheckedIOException} if the group is invalid + * @return whether the group is valid. + */ + default boolean validateComponents(SSTable sstable, Tracker tracker, boolean validateChecksum, boolean rethrow) + { + boolean isValid = isValid(validateChecksum, invalid -> { + if (CassandraRelevantProperties.DELETE_CORRUPT_SAI_COMPONENTS.getBoolean()) + { + // We delete the corrupted file. Yes, this may break ongoing reads to that component, but + // if something is wrong with the file, we're rather fail loudly from that point on than + // risking reading and returning corrupted data. + forWrite().getForWrite(invalid.componentType()).delete(); + // Note that invalidation will also delete the completion marker + } + else + { + logger.debug("Leaving believed-corrupt component {} of SSTable {} in place because {} is false", invalid.componentType(), descriptor(), CassandraRelevantProperties.DELETE_CORRUPT_SAI_COMPONENTS.getKey()); + } + }); + if (!isValid) + { + invalidate(sstable, tracker); + if (rethrow) + throw new UncheckedIOException(new CorruptIndexException("Invalid SAI components for " + descriptor(), descriptor().toString())); + } + return isValid; + } + + /** + * Marks the group as invalid/broken. + *

    + * If it is an active group, it will remove it as active in the underlying {@link IndexDescriptor}. It will also + * at least delete the completion marker to ensure the group does not get used on any reload/restart. + *

    + * Please note that this method is already called by {@link #validateComponents} if the group + * is found invalid: it is exposed here for case where we have reason to think the group is invalid but + * validation hasn't detected it for some reason. + * + * @param sstable the sstable object for which the component are invalidated (which should correspond to + * {@code this.descriptor()}). This must be provided so the invalidated components are + * unregistered. + * @param tracker the {@link Tracker} of the table (of the sstable/components). Like the sstable, this is used + * as part of unregistering the components. + */ + void invalidate(SSTable sstable, Tracker tracker); + + /** + * Returns a {@link ForWrite} view of this group, mostly for calling {@link ForWrite#forceDeleteAllComponents()} at + * appropriate times. + */ + ForWrite forWrite(); + } + + /** + * Specialisation of {@link IndexComponents} used when doing a new index build and thus writting a new group of + * components. + *

    + * This extends {@link ForRead} because we sometimes read previously written components to write other ones in the group + */ + interface ForWrite extends ForRead + { + /** + * Adds the provided component "kind" to this writer, or return the previously added one if it had already + * been added. + */ + IndexComponent.ForWrite addOrGet(IndexComponentType component); + + /** + * Get the component of the provided type if present. + *

    + * This is the same as {@link #get}, except that it preserve the type information that is a "for write" object. + * + * @param component the type of the component to retrieve. + * @return the component, or {@code null} if the group has no such component. + */ + IndexComponent.ForWrite getForWrite(IndexComponentType component); + + /** + * Delete the files of all the components in this writer (and remove them so that the group will be empty + * afterward). + */ + void forceDeleteAllComponents(); + + /** + * Writes the completion marker file for the group on disk and, if said write succeeds, adds the components + * of this writer to the {@link IndexDescriptor} that created this writer (and so no additional components + * should be added to this writer after this call). + */ + void markComplete() throws IOException; + + /** + * Create a temporary {@link File} namespaced within the per index components. Repeated calls with the same + * componentName will produce the same file. + * @param componentName - unique name within the per index components + * @return a temprory file for use during index construction + */ + File tmpFileFor(String componentName) throws IOException; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java index 52725ea84335..f69589c34f79 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java @@ -20,473 +20,740 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.nio.ByteOrder; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Objects; -import com.google.common.io.Files; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ClusteringComparator; -import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; -import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.lifecycle.Tracker; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.IndexValidation; -import org.apache.cassandra.index.sai.SSTableContext; -import org.apache.cassandra.index.sai.disk.EmptyIndex; -import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter; -import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.RowMapping; -import org.apache.cassandra.index.sai.disk.SSTableIndex; import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; +import org.apache.cassandra.index.sai.disk.io.IndexInput; import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; -import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.disk.oldlucene.EndiannessReverserChecksumIndexInput; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.SSTable; +import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Throwables; -import org.apache.lucene.store.IndexInput; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.lucene.store.BufferedChecksumIndexInput; +import org.apache.lucene.store.ChecksumIndexInput; import org.apache.lucene.util.IOUtils; /** - * The {@link IndexDescriptor} is an analog of the SSTable {@link Descriptor} and provides version - * specific information about the on-disk state of a {@link StorageAttachedIndex}. + * The `IndexDescriptor` is an analog of the SSTable {@link Descriptor} and provides version + * specific information about the on-disk state of {@link StorageAttachedIndex}es. *

    - * The {@link IndexDescriptor} is primarily responsible for maintaining a view of the on-disk state - * of an index for a specific {@link org.apache.cassandra.io.sstable.SSTable}. + * The `IndexDescriptor` is primarily responsible for maintaining a view of the on-disk state + * of the SAI indexes for a specific {@link org.apache.cassandra.io.sstable.SSTable}. It maintains mappings + * of the current on-disk components and files. It is responsible for opening files for use by + * writers and readers. *

    - * It is responsible for opening files for use by writers and readers. + * Each sstable has per-index components ({@link IndexComponentType}) associated with it, and also components + * that are shared by all indexes (notably, the components that make up the PrimaryKeyMap). *

    - * Its remaining responsibility is to act as a proxy to the {@link OnDiskFormat} associated with the - * index {@link Version}. + * IndexDescriptor's remaining responsibility is to act as a proxy to the {@link OnDiskFormat} + * associated with the index {@link Version}. */ public class IndexDescriptor { - private static final Logger logger = LoggerFactory.getLogger(IndexDescriptor.class); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + + /* + TODO: New indexes can be added at any time to existing data. Since CNDB-8756 we keep all new column indexes in the + same version as the existing per-sstable index components, making sstable upgrade the only way to move to a newer + version. However, prior to that fix the Version of a column index may not match the Version of the base sstable. + OnDiskFormat + IndexFeatureSet + IndexDescriptor was not designed with this in mind, leading to some awkwardness, + notably in IFS where some features are per-sstable (`isRowAware`) and some are per-column (`hasTermsHistogram`). + */ - public final Version version; - public final Descriptor sstableDescriptor; - public final ClusteringComparator clusteringComparator; - public final PrimaryKey.Factory primaryKeyFactory; + public final Descriptor descriptor; + private final ComponentsBuildId emptyGroupMarker; - private IndexDescriptor(Version version, Descriptor sstableDescriptor, IPartitioner partitioner, ClusteringComparator clusteringComparator) + // The per-sstable components for this descriptor. This is never `null` in practice, but 1) it's a bit easier to + // initialize it outsides of the ctor, and 2) it can actually change upon calls to `reload`. + private IndexComponentsImpl perSSTable; + private final Map perIndexes = new ConcurrentHashMap<>(); + + private IndexDescriptor(Descriptor descriptor) { - this.version = version; - this.sstableDescriptor = sstableDescriptor; - this.clusteringComparator = clusteringComparator; - this.primaryKeyFactory = new PrimaryKey.Factory(partitioner, clusteringComparator); + this.descriptor = descriptor; + this.emptyGroupMarker = ComponentsBuildId.of(Version.current(descriptor.ksname), -1); } - public static IndexDescriptor create(Descriptor descriptor, IPartitioner partitioner, ClusteringComparator clusteringComparator) + public static IndexDescriptor empty(Descriptor descriptor) { - return new IndexDescriptor(Version.LATEST, descriptor, partitioner, clusteringComparator); + IndexDescriptor created = new IndexDescriptor(descriptor); + // Some code assumes that you can always at least call `perSSTableComponents()` and not get `null`, so we + // set it to an empty group here. + created.perSSTable = created.createEmptyGroup(null); + return created; } - public static IndexDescriptor create(SSTableReader sstable) + public static IndexDescriptor load(SSTableReader sstable, Set indices) { - for (Version version : Version.ALL) - { - IndexDescriptor indexDescriptor = new IndexDescriptor(version, - sstable.descriptor, - sstable.getPartitioner(), - sstable.metadata().comparator); - - if (version.onDiskFormat().isPerSSTableIndexBuildComplete(indexDescriptor)) - { - return indexDescriptor; - } - } - return new IndexDescriptor(Version.LATEST, - sstable.descriptor, - sstable.getPartitioner(), - sstable.metadata().comparator); + SSTableIndexComponentsState discovered = IndexComponentDiscovery.instance().discoverComponents(sstable); + IndexDescriptor descriptor = new IndexDescriptor(sstable.descriptor); + descriptor.initialize(indices, discovered); + return descriptor; } - public boolean hasClustering() + private void initialize(Set indices, SSTableIndexComponentsState discovered) { - return clusteringComparator.size() > 0; + this.perSSTable = initializeGroup(null, discovered.perSSTableBuild()); + initializeIndexes(indices, discovered); } - public String componentName(IndexComponent indexComponent) + private void initializeIndexes(Set indices, SSTableIndexComponentsState discovered) { - return version.fileNameFormatter().format(indexComponent, null); + for (var context : indices) + perIndexes.put(context, initializeGroup(context, discovered.perIndexBuild(context.getIndexName()))); } - public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(SSTableReader sstable) + private Set expectedComponentsForVersion(Version version, @Nullable IndexContext context) { - return version.onDiskFormat().newPrimaryKeyMapFactory(this, sstable); + return context == null + ? version.onDiskFormat().perSSTableComponentTypes() + : version.onDiskFormat().perIndexComponentTypes(context); } - public SSTableIndex newSSTableIndex(SSTableContext sstableContext, StorageAttachedIndex index) + private IndexComponentsImpl initializeGroup(@Nullable IndexContext context, @Nullable ComponentsBuildId buildId) { - return isIndexEmpty(index.termType(), index.identifier()) - ? new EmptyIndex(sstableContext, index) - : version.onDiskFormat().newSSTableIndex(sstableContext, index); + IndexComponentsImpl components; + if (buildId == null) + { + // Means there isn't a complete build for this context. We add some empty "group" as a marker. + components = createEmptyGroup(context); + } + else + { + components = new IndexComponentsImpl(context, buildId); + var expectedTypes = expectedComponentsForVersion(buildId.version(), context); + // Note that the "expected types" are actually a superset of the components we may have. In particular, + // when a particular index has no data for a particular sstable, the relevant components only have the + // metadata and completion marker components. So we check what exists. + expectedTypes.forEach(components::addIfExists); + components.sealed = true; + + // We'll still track the group if it is incomplete because that's what discovery gave us, and all code know + // how to handle those, but this will mean some index won't be queriable because of this. Also, we'll only + // have incomplete groups if either 1) a build failed mid-way, 2) we detected some corrupted components and + // deleting the completion marker, or 3) we've lost the file, and all of those should be rare, so having + // a warning here feels appropriate. + if (!components.isComplete()) + { + logger.warn("Discovered group of {} for SSTable {} has no completion marker and cannot be used. This will lead to some indexes not being queriable", + context == null ? "per-SSTable SAI components" : "per-index SAI components of " + context.getIndexName(), descriptor); + } + } + return components; } - public PerSSTableIndexWriter newPerSSTableIndexWriter() throws IOException + private IndexComponentsImpl createEmptyGroup(@Nullable IndexContext context) { - return version.onDiskFormat().newPerSSTableIndexWriter(this); + return new IndexComponentsImpl(context, emptyGroupMarker); } - public PerColumnIndexWriter newPerColumnIndexWriter(StorageAttachedIndex index, - LifecycleNewTracker tracker, - RowMapping rowMapping) + /** + * The set of components _expected_ to be written for a newly flushed sstable given the provided set of indices. + * This includes both per-sstable and per-index components. + *

    + * Please note that the final sstable may not contain all of these components, as some may be empty or not written + * due to the specific of the flush, but this should be a superset of the components written. + */ + public static Set componentsForNewlyFlushedSSTable(Collection indices, Version version) { - return version.onDiskFormat().newPerColumnIndexWriter(index, this, tracker, rowMapping); + ComponentsBuildId buildId = ComponentsBuildId.forNewSSTable(version); + Set components = new HashSet<>(); + for (IndexComponentType component : buildId.version().onDiskFormat().perSSTableComponentTypes()) + components.add(customComponentFor(buildId, component, null)); + + for (StorageAttachedIndex index : indices) + addPerIndexComponentsForNewlyFlushedSSTable(components, buildId, index.getIndexContext()); + return components; } - public boolean isPerSSTableIndexBuildComplete() + /** + * The set of per-index components _expected_ to be written for a newly flushed sstable for the provided index. + *

    + * This is a subset of {@link #componentsForNewlyFlushedSSTable(Collection, Version)} and has the same caveats. + */ + public static Set perIndexComponentsForNewlyFlushedSSTable(IndexContext context) { - return version.onDiskFormat().isPerSSTableIndexBuildComplete(this); + return addPerIndexComponentsForNewlyFlushedSSTable(new HashSet<>(), ComponentsBuildId.forNewSSTable(context.version()), context); } - public boolean isPerColumnIndexBuildComplete(IndexIdentifier indexIdentifier) + private static Set addPerIndexComponentsForNewlyFlushedSSTable(Set addTo, ComponentsBuildId buildId, IndexContext context) { - return version.onDiskFormat().isPerColumnIndexBuildComplete(this, indexIdentifier); + for (IndexComponentType component : buildId.version().onDiskFormat().perIndexComponentTypes(context)) + addTo.add(customComponentFor(buildId, component, context)); + return addTo; } - public boolean hasComponent(IndexComponent indexComponent) + private static Component customComponentFor(ComponentsBuildId buildId, IndexComponentType componentType, @Nullable IndexContext context) { - return fileFor(indexComponent).exists(); + return new Component(SSTableFormat.Components.Types.CUSTOM, buildId.formatAsComponent(componentType, context)); } - public boolean hasComponent(IndexComponent indexComponent, IndexIdentifier indexIdentifier) + /** + * Given the indexes for the sstable this is a descriptor for, reload from disk to check if newer components are + * available. + *

    + * This method is generally not safe to call concurrently with the other methods that modify the state + * of {@link IndexDescriptor}, which are {@link #newPerSSTableComponentsForWrite()} and + * {@link #newPerIndexComponentsForWrite(IndexContext)}. This method is in fact meant for tiered storage use-cases + * where (post-flush) index building is done on separate dedicated services, and this method allows to reload the + * result of such external services once it is made available locally. + * + * @param sstable the sstable reader to reload index components + * @param indices The set of indices to should part of the reloaded descriptor. + * @return this descriptor, for chaining purpose. + */ + public IndexDescriptor reload(SSTableReader sstable, Set indices) { - return fileFor(indexComponent, indexIdentifier).exists(); + Preconditions.checkArgument(sstable.getDescriptor().equals(this.descriptor)); + SSTableIndexComponentsState discovered = IndexComponentDiscovery.instance().discoverComponents(sstable); + + // We want to make sure the descriptor only has data for the provided `indices` on reload, so we remove any + // index data that is not in the ones provided. This essentially make sure we don't hold up memory for + // dropped indexes. + for (IndexContext context : new HashSet<>(perIndexes.keySet())) + { + if (!indices.contains(context)) + perIndexes.remove(context); + } + + // Then reload data. + initialize(indices, discovered); + return this; } - public File fileFor(IndexComponent indexComponent) + /** + * Loads per-index components for the provided indexes only when they are currently missing from this descriptor. + *

    + * This method does not modify per-SSTable components and does not replace already tracked per-index groups. + * It is intended for incremental index discovery flows where callers only need to populate indexes that are + * not present yet without disturbing existing in-memory references. + */ + public IndexDescriptor loadIfAbsent(SSTableReader sstable, Set indices) { - return createFile(indexComponent, null); + Preconditions.checkArgument(sstable.getDescriptor().equals(this.descriptor)); + SSTableIndexComponentsState discovered = IndexComponentDiscovery.instance().discoverComponents(sstable); + for (var context : indices) + perIndexes.computeIfAbsent(context, k -> initializeGroup(context, discovered.perIndexBuild(context.getIndexName()))); + return this; } - public File fileFor(IndexComponent indexComponent, IndexIdentifier indexIdentifier) + /** + * Returns the version that should be used for new components for the sstable of this descriptor. + * It is the version of the per-sstable components, which is either the version of the existing per-sstable + * components if any, or the current version if there is no existing components. + */ + public Version versionForNewComponents() { - return createFile(indexComponent, indexIdentifier); + return perSSTableComponents().version(); } - public boolean isIndexEmpty(IndexTermType indexTermType, IndexIdentifier indexIdentifier) + public IndexComponents.ForRead perSSTableComponents() { - // The index is empty if the index build completed successfully in that both - // a GROUP_COMPLETION_MARKER companent and a COLUMN_COMPLETION_MARKER exist for - // the index and the number of per-index components is 1 indicating that only the - // COLUMN_COMPLETION_MARKER exists for the index, as this is the only file that - // will be written if the index is empty - return isPerColumnIndexBuildComplete(indexIdentifier) && numberOfPerIndexComponents(indexTermType, indexIdentifier) == 1; + return perSSTable; } - public void createComponentOnDisk(IndexComponent component) throws IOException + public IndexComponents.ForRead perIndexComponents(IndexContext context) { - Files.touch(fileFor(component).toJavaIOFile()); + var perIndex = perIndexes.get(context); + return perIndex == null ? createEmptyGroup(context) : perIndex; } - public void createComponentOnDisk(IndexComponent component, IndexIdentifier indexIdentifier) throws IOException + public Set includedIndexes() { - Files.touch(fileFor(component, indexIdentifier).toJavaIOFile()); + return Collections.unmodifiableSet(perIndexes.keySet()); } - public IndexInput openPerSSTableInput(IndexComponent indexComponent) + public IndexComponents.ForWrite newPerSSTableComponentsForWrite() { - File file = fileFor(indexComponent); - if (logger.isTraceEnabled()) - logger.trace(logMessage("Opening blocking index input for file {} ({})"), - file, - FBUtilities.prettyPrintMemory(file.length())); - - return IndexFileUtils.instance.openBlockingInput(file); + return newComponentsForWrite(null, perSSTable); } - public IndexInput openPerIndexInput(IndexComponent indexComponent, IndexIdentifier indexIdentifier) + public IndexComponents.ForWrite newPerIndexComponentsForWrite(IndexContext context) { - final File file = fileFor(indexComponent, indexIdentifier); - if (logger.isTraceEnabled()) - logger.trace(logMessage("Opening blocking index input for file {} ({})"), - file, - FBUtilities.prettyPrintMemory(file.length())); - - return IndexFileUtils.instance.openBlockingInput(file); + return newComponentsForWrite(context, perIndexes.get(context)); } - public IndexOutputWriter openPerSSTableOutput(IndexComponent component) throws IOException + private IndexComponents.ForWrite newComponentsForWrite(@Nullable IndexContext context, IndexComponentsImpl currentComponents) { - return openPerSSTableOutput(component, false); + Version version = versionForNewComponents(); + var currentBuildId = currentComponents == null ? null : currentComponents.buildId; + return new IndexComponentsImpl(context, ComponentsBuildId.forNewBuild(version, currentBuildId, candidateId -> { + // This checks that there is no existing files on disk we would overwrite by using `candidateId` for our + // new build. + IndexComponentsImpl candidate = new IndexComponentsImpl(context, candidateId); + boolean isUsable = candidate.expectedComponentsForVersion().stream().noneMatch(candidate::componentExistsOnDisk); + if (!isUsable) + { + noSpamLogger.warn(logMessage("Wanted to use generation {} for new build of {} SAI components of {}, but found some existing components on disk for that generation (maybe leftover from an incomplete/corrupted build?); trying next generation"), + candidateId.generation(), + context == null ? "per-SSTable" : "per-index", + descriptor); + } + return isUsable; + })); } - public IndexOutputWriter openPerSSTableOutput(IndexComponent component, boolean append) throws IOException + /** + * Returns true if the per-column index components of the provided sstable have been built and are valid. + * + * @param sstable The sstable to check + * @param context The {@link IndexContext} for the index + * @return true if the per-column index components have been built and are complete + */ + public static boolean isIndexBuildCompleteOnDisk(SSTableReader sstable, IndexContext context) { - final File file = fileFor(component); + IndexDescriptor descriptor = IndexDescriptor.load(sstable, Set.of(context)); + return descriptor.perSSTableComponents().isComplete() + && descriptor.perIndexComponents(context).isComplete(); + } - if (logger.isTraceEnabled()) - logger.trace(logMessage("Creating SSTable attached index output for component {} on file {}..."), - component, - file); + public boolean isIndexEmpty(IndexContext context) + { + return perSSTableComponents().isComplete() && perIndexComponents(context).isEmpty(); + } - IndexOutputWriter writer = IndexFileUtils.instance.openOutput(file); + @Override + public int hashCode() + { + return Objects.hash(descriptor, perSSTableComponents().version()); + } - if (append) - { - writer.skipBytes(file.length()); - } + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexDescriptor other = (IndexDescriptor)o; + return Objects.equals(descriptor, other.descriptor) && + Objects.equals(perSSTableComponents().version(), other.perSSTableComponents().version()); + } - return writer; + @Override + public String toString() + { + return descriptor.toString() + "-SAI"; } - public IndexOutputWriter openPerIndexOutput(IndexComponent indexComponent, IndexIdentifier indexIdentifier) throws IOException + public String logMessage(String message) { - return openPerIndexOutput(indexComponent, indexIdentifier, false); + // Index names are unique only within a keyspace. + return String.format("[%s.%s.*] %s", + descriptor.ksname, + descriptor.cfname, + message); } - public IndexOutputWriter openPerIndexOutput(IndexComponent component, IndexIdentifier indexIdentifier, boolean append) throws IOException + private class IndexComponentsImpl implements IndexComponents.ForWrite { - final File file = fileFor(component, indexIdentifier); + private final @Nullable IndexContext context; + private final ComponentsBuildId buildId; - if (logger.isTraceEnabled()) - logger.trace(logMessage("Creating sstable attached index output for component {} on file {}..."), component, file); + private final Map components = new EnumMap<>(IndexComponentType.class); - IndexOutputWriter writer = IndexFileUtils.instance.openOutput(file); + // Mark groups that are being read/have been fully written, and thus should not have new components added. + // This is just to catch errors where we'd try to add a component after the completion marker was written. + private volatile boolean sealed; - if (append) + private IndexComponentsImpl(@Nullable IndexContext context, ComponentsBuildId buildId) { - writer.skipBytes(file.length()); + this.context = context; + this.buildId = buildId; } - return writer; - } - - public FileHandle createPerSSTableFileHandle(IndexComponent indexComponent, Throwables.DiscreteAction cleanup) - { - try + private boolean componentExistsOnDisk(IndexComponentType component) { - final File file = fileFor(indexComponent); - - if (logger.isTraceEnabled()) - logger.trace(logMessage("Opening file handle for {} ({})"), file, FBUtilities.prettyPrintMemory(file.length())); + return new IndexComponentImpl(component).file().exists(); + } - return new FileHandle.Builder(file).mmapped(true).complete(); + @Override + public Descriptor descriptor() + { + return descriptor; } - catch (Throwable t) + + @Override + public IndexDescriptor indexDescriptor() { - throw handleFileHandleCleanup(t, cleanup); + return IndexDescriptor.this; } - } - public FileHandle createPerIndexFileHandle(IndexComponent indexComponent, IndexIdentifier indexIdentifier) - { - return createPerIndexFileHandle(indexComponent, indexIdentifier, null); - } + @Nullable + @Override + public IndexContext context() + { + return context; + } - public FileHandle createPerIndexFileHandle(IndexComponent indexComponent, IndexIdentifier indexIdentifier, Throwables.DiscreteAction cleanup) - { - try + @Override + public ComponentsBuildId buildId() { - final File file = fileFor(indexComponent, indexIdentifier); + return buildId; + } - if (logger.isTraceEnabled()) - logger.trace(logMessage("Opening file handle for {} ({})"), file, FBUtilities.prettyPrintMemory(file.length())); + @Override + public boolean has(IndexComponentType component) + { + return components.containsKey(component); + } - return new FileHandle.Builder(file).mmapped(true).complete(); + @Override + public boolean isEmpty() + { + return isComplete() && components.size() == 1; } - catch (Throwable t) + + @Override + public Collection all() { - throw handleFileHandleCleanup(t, cleanup); + return Collections.unmodifiableCollection(components.values()); } - } - private RuntimeException handleFileHandleCleanup(Throwable t, Throwables.DiscreteAction cleanup) - { - if (cleanup != null) + @Override + public boolean isValid(boolean validateChecksum, Consumer onInvalidComponent) { - try - { - cleanup.perform(); - } - catch (Exception e) + if (isEmpty()) + return true; + + boolean isValid = true; + for (IndexComponentType expected : expectedComponentsForVersion()) { - return Throwables.unchecked(Throwables.merge(t, e)); + var component = components.get(expected); + if (component == null) + { + logger.warn(logMessage("Missing index component {} from SSTable {}"), expected, descriptor); + isValid = false; + } + else + { + try + { + onDiskFormat().validateIndexComponent(component, validateChecksum); + } + catch (UncheckedIOException e) + { + logger.warn(logMessage("Invalid/corrupted component {} for SSTable {}"), expected, descriptor); + onInvalidComponent.accept(component); + isValid = false; + } + } } + return isValid; } - return Throwables.unchecked(t); - } - public Set getLivePerSSTableComponents() - { - return version.onDiskFormat() - .perSSTableIndexComponents(hasClustering()) - .stream() - .filter(c -> fileFor(c).exists()) - .map(version::makePerSSTableComponent) - .collect(Collectors.toSet()); - } + private void updateParentLink(IndexComponentsImpl update) + { + if (isPerIndexGroup()) + perIndexes.put(context, update); + else + perSSTable = update; + } - public Set getLivePerIndexComponents(IndexTermType indexTermType, IndexIdentifier indexIdentifier) - { - return version.onDiskFormat() - .perColumnIndexComponents(indexTermType) - .stream() - .filter(c -> fileFor(c, indexIdentifier).exists()) - .map(c -> version.makePerIndexComponent(c, indexIdentifier)) - .collect(Collectors.toSet()); - } + @Override + public void invalidate(SSTable sstable, Tracker tracker) + { + // This rewrite the TOC to stop listing the components, which ensures that if the node is restarted, + // then discovery will use an empty group for that context (like we add at the end of this method). + sstable.unregisterComponents(allAsCustomComponents(), tracker); + + // Also delete the completion marker, to make it clear the group of components shouldn't be used anymore. + // Note it's comparatively safe to do so in that the marker is never accessed during reads, so we cannot + // break ongoing operations here. + var marker = components.remove(completionMarkerComponent()); + if (marker != null) + marker.delete(); + + // Keeping legacy behavior if immutable components is disabled. + if (!buildId.version().useImmutableComponentFiles() && CassandraRelevantProperties.DELETE_CORRUPT_SAI_COMPONENTS.getBoolean()) + forceDeleteAllComponents(); + + // We replace ourselves by an explicitly empty group in the parent. + updateParentLink(createEmptyGroup(context)); + } - public long sizeOnDiskOfPerSSTableComponents() - { - return version.onDiskFormat() - .perSSTableIndexComponents(hasClustering()) - .stream() - .map(this::fileFor) - .filter(File::exists) - .mapToLong(File::length) - .sum(); - } + @Override + public ForWrite forWrite() + { + // The difference between Reader and Writer is just to make code cleaner and make it clear when we read + // components from when we write/modify them. But this concrete implementatation is both in practice. + return this; + } - public long sizeOnDiskOfPerIndexComponents(IndexTermType indexTermType, IndexIdentifier indexIdentifier) - { - return version.onDiskFormat() - .perColumnIndexComponents(indexTermType) - .stream() - .map(c -> fileFor(c, indexIdentifier)) - .filter(File::exists) - .mapToLong(File::length) - .sum(); - } + @Override + public IndexComponent.ForRead get(IndexComponentType component) + { + IndexComponentImpl info = components.get(component); + Preconditions.checkNotNull(info, "SSTable %s has no %s component for build %s (context: %s)", descriptor, component, buildId, context); + return info; + } - @VisibleForTesting - public long sizeOnDiskOfPerIndexComponent(IndexComponent indexComponent, IndexIdentifier indexIdentifier) - { - File componentFile = fileFor(indexComponent, indexIdentifier); - return componentFile.exists() ? componentFile.length() : 0; - } + @Override + public long liveSizeOnDiskInBytes() + { + return components.values().stream().map(IndexComponentImpl::file).mapToLong(File::length).sum(); + } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean validatePerIndexComponents(IndexTermType indexTermType, IndexIdentifier indexIdentifier, IndexValidation validation, boolean validateChecksum, boolean rethrow) - { - if (validation == IndexValidation.NONE) - return true; + public void addIfExists(IndexComponentType component) + { + Preconditions.checkArgument(!sealed, "Should not add components for SSTable %s at this point; the completion marker has already been written", descriptor); + // When a sstable doesn't have any complete group, we use a marker empty one with a generation of -1: + Preconditions.checkArgument(!buildId.equals(emptyGroupMarker), "Should not be adding component to empty components"); + components.computeIfAbsent(component, type -> { + var created = new IndexComponentImpl(type); + return created.file().exists() ? created : null; + }); + } - logger.info(logMessage("Validating per-column index components for {} for SSTable {} using mode {}"), indexIdentifier, sstableDescriptor.toString(), validation); + @Override + public IndexComponent.ForWrite addOrGet(IndexComponentType component) + { + Preconditions.checkArgument(!sealed, "Should not add components for SSTable %s at this point; the completion marker has already been written", descriptor); + // When a sstable doesn't have any complete group, we use a marker empty one with a generation of -1: + Preconditions.checkArgument(!buildId.equals(emptyGroupMarker), "Should not be adding component to empty components"); + return components.computeIfAbsent(component, IndexComponentImpl::new); + } - try + @Override + public IndexComponent.ForWrite getForWrite(IndexComponentType component) { - version.onDiskFormat().validatePerColumnIndexComponents(this, indexTermType, indexIdentifier, validation == IndexValidation.CHECKSUM && validateChecksum); - return true; + IndexComponentImpl info = components.get(component); + Preconditions.checkNotNull(info, "SSTable %s has no %s component for build %s (context: %s)", descriptor, component, buildId, context); + return info; } - catch (UncheckedIOException e) + + @Override + public File tmpFileFor(String componentName) { - if (rethrow) - throw e; - else - return false; + String name = context != null ? String.format("%s_%s_%s", buildId, context.getColumnName(), componentName) + : String.format("%s_%s", buildId, componentName); + return descriptor.tmpFileFor(new Component(SSTableFormat.Components.Types.CUSTOM, name)); } - } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean validatePerSSTableComponents(IndexValidation validation, boolean validateChecksum, boolean rethrow) - { - if (validation == IndexValidation.NONE) - return true; + @Override + public void forceDeleteAllComponents() + { + components.values().forEach(IndexComponentImpl::delete); + components.clear(); + } - logger.info(logMessage("Validating per-sstable index components for SSTable {} using mode {}"), sstableDescriptor.toString(), validation); + @Override + public void markComplete() throws IOException + { + addOrGet(completionMarkerComponent()).createEmpty(); + sealed = true; + // Until this call, the group is not attached to the parent. This create the link. + updateParentLink(this); + } - try + @Override + public int hashCode() { - version.onDiskFormat().validatePerSSTableIndexComponents(this, validation == IndexValidation.CHECKSUM && validateChecksum); - return true; + return Objects.hash(descriptor, context, buildId); } - catch (UncheckedIOException e) + + @Override + public boolean equals(Object o) { - if (rethrow) - throw e; - else - return false; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexComponentsImpl that = (IndexComponentsImpl) o; + return Objects.equals(descriptor, that.descriptor()) + && Objects.equals(context, that.context) + && Objects.equals(buildId, that.buildId); } - } - public void deletePerSSTableIndexComponents() - { - version.onDiskFormat() - .perSSTableIndexComponents(hasClustering()) - .stream() - .map(this::fileFor) - .filter(File::exists) - .forEach(this::deleteComponent); - } + @Override + public String toString() + { + return String.format("%s components for %s (%s): %s", + context == null ? "Per-SSTable" : "Per-Index", + descriptor, + buildId, + components.values()); + } - public void deleteColumnIndex(IndexTermType indexTermType, IndexIdentifier indexIdentifier) - { - version.onDiskFormat() - .perColumnIndexComponents(indexTermType) - .stream() - .map(c -> fileFor(c, indexIdentifier)) - .filter(File::exists) - .forEach(this::deleteComponent); - } + private class IndexComponentImpl implements IndexComponent.ForRead, IndexComponent.ForWrite + { + private final IndexComponentType component; - @Override - public int hashCode() - { - return Objects.hashCode(sstableDescriptor, version); - } + private volatile String filenamePart; + private volatile File file; - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - IndexDescriptor other = (IndexDescriptor)o; - return Objects.equal(sstableDescriptor, other.sstableDescriptor) && - Objects.equal(version, other.version); - } + private IndexComponentImpl(IndexComponentType component) + { + this.component = component; + } - @Override - public String toString() - { - return sstableDescriptor.toString() + "-SAI"; - } + @Override + public IndexComponentsImpl parent() + { + return IndexComponentsImpl.this; + } - public String logMessage(String message) - { - // Index names are unique only within a keyspace. - return String.format("[%s.%s.*] %s", - sstableDescriptor.ksname, - sstableDescriptor.cfname, - message); - } + @Override + public IndexComponentType componentType() + { + return component; + } - private File createFile(IndexComponent component, IndexIdentifier indexIdentifier) - { - Component customComponent = version.makePerIndexComponent(component, indexIdentifier); - return sstableDescriptor.fileFor(customComponent); - } + @Override + public ByteOrder byteOrder() + { + return buildId.version().onDiskFormat().byteOrderFor(component, context); + } - private long numberOfPerIndexComponents(IndexTermType indexTermType, IndexIdentifier indexIdentifier) - { - return version.onDiskFormat() - .perColumnIndexComponents(indexTermType) - .stream() - .map(c -> fileFor(c, indexIdentifier)) - .filter(File::exists) - .count(); - } + @Override + public String fileNamePart() + { + // Not thread-safe, but not really the end of the world if called multiple time + if (filenamePart == null) + filenamePart = buildId.formatAsComponent(component, context); + return filenamePart; + } - private void deleteComponent(File file) - { - logger.debug(logMessage("Deleting storage-attached index component file {}"), file); - try - { - IOUtils.deleteFilesIfExist(file.toPath()); - } - catch (IOException e) - { - logger.warn(logMessage("Unable to delete storage-attached index component file {} due to {}."), file, e.getMessage(), e); + @Override + public Component asCustomComponent() + { + return new Component(SSTableFormat.Components.Types.CUSTOM, fileNamePart()); + } + + @Override + public File file() + { + // Not thread-safe, but not really the end of the world if called multiple time + if (file == null) + file = descriptor.fileFor(asCustomComponent()); + return file; + } + + @Override + public FileHandle createFileHandle() + { + var builder = StorageProvider.instance.fileHandleBuilderFor(this); + var b = builder.order(byteOrder()); + return b.complete(); + } + + @Override + public FileHandle createIndexBuildTimeFileHandle() + { + final FileHandle.Builder builder = StorageProvider.instance.indexBuildTimeFileHandleBuilderFor(this); + return builder.order(byteOrder()).complete(); + } + + @Override + public IndexInput openInput() + { + return IndexFileUtils.instance().openBlockingInput(createFileHandle()); + } + + @Override + public ChecksumIndexInput openCheckSummedInput() + { + var indexInput = openInput(); + return checksumIndexInput(indexInput); + } + + /** + * Returns a ChecksumIndexInput that reads the indexInput in the correct endianness for the context. + * These files were written by the Lucene {@link org.apache.lucene.store.DataOutput}. When written by + * Lucene 7.5, {@link org.apache.lucene.store.DataOutput} wrote the file using big endian formatting. + * After the upgrade to Lucene 9, the {@link org.apache.lucene.store.DataOutput} writes in little endian + * formatting. + * + * @param indexInput The index input to read + * @return A ChecksumIndexInput that reads the indexInput in the correct endianness for the context + */ + private ChecksumIndexInput checksumIndexInput(IndexInput indexInput) + { + if (buildId.version() == Version.AA) + return new EndiannessReverserChecksumIndexInput(indexInput, buildId.version()); + else + return new BufferedChecksumIndexInput(indexInput); + } + + @Override + public IndexOutputWriter openOutput(boolean append) throws IOException + { + File file = file(); + + if (logger.isTraceEnabled()) + logger.trace(this.parent().logMessage("Creating SSTable attached index output for component {} on file {}..."), + component, + file); + + return IndexFileUtils.instance().openOutput(file, byteOrder(), append, buildId.version()); + } + + @Override + public void createEmpty() throws IOException + { + com.google.common.io.Files.touch(file().toJavaIOFile()); + } + + @Override + public void delete() + { + File file = file(); + logger.debug("Deleting storage attached index component file {}", file); + try + { + IOUtils.deleteFilesIfExist(file.toPath()); + } + catch (IOException e) + { + logger.warn("Unable to delete storage attached index component file {} due to {}.", file, e.getMessage(), e); + } + } + + @Override + public int hashCode() + { + return Objects.hash(this.parent(), component); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexComponentImpl that = (IndexComponentImpl) o; + return Objects.equals(this.parent(), that.parent()) + && component == that.component; + } + + @Override + public String toString() + { + return file().toString(); + } } } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexFeatureSet.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexFeatureSet.java new file mode 100644 index 000000000000..e1a6b2e13370 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexFeatureSet.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.format; + +/** + * The {@code IndexFeatureSet} represents the set of features available that are available + * to an {@code OnDiskFormat}. + * + * The baseline features included in the V1 on-disk format are not included in the feature set. + * Thus, V1 on-disk format features should only be added here if support for them is dropped in + * a future version. + */ +public interface IndexFeatureSet +{ + /** + * Returns whether the index supports row-awareness. Row-awareness means that the per-sstable + * index supports mapping rowID -> {@code PrimaryKey} where the {@code PrimaryKey} contains both + * partition key and clustering information. + * + * @return true if the index supports row-awareness + */ + boolean isRowAware(); + + /** + * @return true if index metadata contains term histograms for fast cardinality estimation + */ + boolean hasTermsHistogram(); + + /** + * The {@code Accumulator} is used to accumulate the {@link IndexFeatureSet} responses from + * multiple sources. This will include all the SSTables included in a query and all the indexes + * attached to those SSTables, added using {@link Accumulator#accumulate}. + *

    + * The feature set of the current version denoted by {@link Version#current(String)} + * is implicitly added, so the result feature set will include only the features supported by the + * current version for the keyspace. + *

    + * The {@code Accumulator} creates an {@code IndexFeatureSet} this contains the features from + * all the associated feature sets where {@code false} is the highest priority. This means if any + * on-disk format on any SSTable doesn't support a feature then that feature isn't supported + * by the query. + */ + class Accumulator + { + boolean isRowAware = true; + boolean hasTermsHistogram = true; + boolean complete = false; + + public Accumulator(Version version) + { + accumulate(version.onDiskFormat().indexFeatureSet()); + } + + /** + * Add another {@code IndexFeatureSet} to the accumulation + * + * @param indexFeatureSet the feature set to accumulate + */ + public void accumulate(IndexFeatureSet indexFeatureSet) + { + assert !complete : "Cannot accumulate after complete has been called"; + if (!indexFeatureSet.isRowAware()) + isRowAware = false; + if (!indexFeatureSet.hasTermsHistogram()) + hasTermsHistogram = false; + } + + /** + * Complete the accumulation of feature sets and return the + * result of the accumulation. + * + * @return an {@link IndexFeatureSet} containing the accumulated feature set + */ + public IndexFeatureSet complete() + { + complete = true; + return new IndexFeatureSet() + { + @Override + public boolean isRowAware() + { + return isRowAware; + } + + @Override + public boolean hasTermsHistogram() + { + return hasTermsHistogram; + } + }; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java index 30ba3b6295b0..a39d66651645 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java @@ -20,149 +20,200 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Set; +import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.SSTableContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter; -import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter; +import org.apache.cassandra.index.sai.disk.PerIndexWriter; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.RowMapping; -import org.apache.cassandra.index.sai.disk.SSTableIndex; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.disk.SearchableIndex; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.memory.RowMapping; +import org.apache.cassandra.index.sai.memory.TrieMemtableIndex; +import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** - * An interface to the on-disk format of an index. This provides format agnostic methods + * An interface to the on-disk format of an index. This provides format agnostics methods * to read and write an on-disk format. - *

    + * * The methods on this interface can be logically mapped into the following groups * based on their method parameters: *

      *
    • Methods taking no parameters. These methods return static information about the * format. This can include static information about the per-sstable components
    • - *
    • Methods taking an {@link IndexDescriptor}. These methods interact with the on-disk components, or - * return objects that will interact with the on-disk components, or return information about the on-disk - * components. If they take an {@link IndexTermType} and/or a {@link IndexIdentifier} as well they will be - * interacting with per-column index files; otherwise they will be interacting with per-sstable index files
    • - *
    • Methods taking an {@link IndexComponent}. These methods only interact with a single index component or - * set of index components
    • + *
    • Methods taking just an {@link IndexContext}. These methods return static information + * specific to the index. This can be information relating to the type of index being used
    • + *
    • Methods taking an {@link IndexDescriptor}. These methods interact with the on-disk components or + * return objects that will interact with the on-disk components or return information about the on-disk + * components. If they take an {@link IndexContext} as well they will be interacting with per-index files + * otherwise they will be interacting with per-sstable files
    • + *
    • Methods taking an {@link IndexComponentType}. These methods only interact with a single component or + * set of components
    • * + * To add a new version, + * (1) Create a new class, e.g. VXOnDiskFormat, that extends the previous version and overrides + * methods relating to the new format and functionality + * (2) Wire it up in Version to its version string *
    */ public interface OnDiskFormat { /** - * Returns a {@link PrimaryKeyMap.Factory} for the SSTable + * Returns the {@link IndexFeatureSet} for the on-disk format * - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable - * @param sstable The {@link SSTableReader} associated with the {@link IndexDescriptor} + * @return the index feature set */ - PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexDescriptor indexDescriptor, SSTableReader sstable); + public IndexFeatureSet indexFeatureSet(); /** - * Create a new {@link SSTableIndex} for an on-disk index. + * Returns the {@link PrimaryKey.Factory} for the on-disk format * - * @param sstableContext The {@link SSTableContext} holding the per-SSTable information for the index - * @param index The {@link StorageAttachedIndex} - * @return the new {@link SSTableIndex} for the on-disk index + * @return the primary key factory */ - SSTableIndex newSSTableIndex(SSTableContext sstableContext, StorageAttachedIndex index); + public PrimaryKey.Factory newPrimaryKeyFactory(ClusteringComparator comparator); /** - * Create a new {@link PerSSTableIndexWriter} to write the per-SSTable on-disk components of an index. + * Returns a {@link PrimaryKeyMap.Factory} for the SSTable * - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable - * @throws IOException if the writer couldn't be created + * @param perSSTableComponents The concrete sstable components to use for the factory + * @param primaryKeyFactory The {@link PrimaryKey.Factory} corresponding to the provided {@code perSSTableComponents}. + * @param sstable The {@link SSTableReader} associated with the per-sstable components + * @return a {@link PrimaryKeyMap.Factory} for the SSTable */ - PerSSTableIndexWriter newPerSSTableIndexWriter(IndexDescriptor indexDescriptor) throws IOException; + public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, PrimaryKey.Factory primaryKeyFactory, SSTableReader sstable) throws IOException; /** - * Create a new {@link PerColumnIndexWriter} to write the per-column on-disk components of an index. The {@link LifecycleNewTracker} - * is used to determine the type of index write about to happen this will either be an - * {@code OperationType.FLUSH} indicating that we are about to flush a {@link org.apache.cassandra.index.sai.memory.MemtableIndex} - * or one of the other operation types indicating that we will be writing from an existing SSTable + * Create a new {@link SearchableIndex} for an on-disk index. This is held by the {@SSTableIndex} + * and shared between queries. * - * @param index The {@link StorageAttachedIndex} holding the current index build status - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable - * @param tracker The {@link LifecycleNewTracker} for index build operation. - * @param rowMapping The {@link RowMapping} that is used to map rowID to {@code PrimaryKey} during the write operation + * @param sstableContext The {@link SSTableContext} holding the per-SSTable information for the index + * @param perIndexComponents The group of per-index sstable components to use/read for the returned index (which + * also link to the underlying {@link IndexContext} for the index). + * @return the created {@link SearchableIndex}. */ - PerColumnIndexWriter newPerColumnIndexWriter(StorageAttachedIndex index, - IndexDescriptor indexDescriptor, - LifecycleNewTracker tracker, - RowMapping rowMapping); + public SearchableIndex newSearchableIndex(SSTableContext sstableContext, IndexComponents.ForRead perIndexComponents); - /** - * Returns true if the per-sstable index components have been built and are valid. - * - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index - */ - boolean isPerSSTableIndexBuildComplete(IndexDescriptor indexDescriptor); + IndexSearcher newIndexSearcher(SSTableContext sstableContext, + IndexContext indexContext, + PerIndexFiles indexFiles, + SegmentMetadata segmentMetadata) throws IOException; /** - * Returns true if the per-column index components have been built and are valid. + * Create a new writer for the per-SSTable on-disk components of an index. * - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index - * @param indexIdentifier The {@link IndexIdentifier} for the index + * @param indexDescriptor The {@link IndexDescriptor} for the SSTable + * @return The {@link PerSSTableWriter} to write the per-SSTable on-disk components */ - boolean isPerColumnIndexBuildComplete(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier); + public PerSSTableWriter newPerSSTableWriter(IndexDescriptor indexDescriptor) throws IOException; /** - * Validate all the per-SSTable on-disk components and throw if a component is not valid - * - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index - * @param checksum {@code true} if the checksum should be tested as part of the validation + * Create a new writer for the per-index on-disk components of an index. The {@link LifecycleNewTracker} + * is used to determine the type of index write about to happen this will either be an + * {@code OperationType.FLUSH} indicating that we are about to flush a {@link TrieMemtableIndex} + * or one of the other operation types indicating that we will be writing from an existing SSTable * - * @throws UncheckedIOException if there is a problem validating any on-disk component + * @param index The {@link StorageAttachedIndex} holding the current index build status + * @param indexDescriptor The {@link IndexDescriptor} for the SSTable + * @param tracker The {@link LifecycleNewTracker} for index build operation. + * @param rowMapping The {@link RowMapping} that is used to map rowID to {@code PrimaryKey} during the write + * @param keyCount + * @return The {@link PerIndexWriter} that will write the per-index on-disk components */ - void validatePerSSTableIndexComponents(IndexDescriptor indexDescriptor, boolean checksum); + public PerIndexWriter newPerIndexWriter(StorageAttachedIndex index, + IndexDescriptor indexDescriptor, + LifecycleNewTracker tracker, + RowMapping rowMapping, long keyCount); /** - * Validate all the per-column on-disk components and throw if a component is not valid + * Validate the provided on-disk components (that must be for this version). * - * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index - * @param indexTermType The {@link IndexTermType} of the index - * @param indexIdentifier The {@link IndexIdentifier} for the index + * @param component The component to validate * @param checksum {@code true} if the checksum should be tested as part of the validation * * @throws UncheckedIOException if there is a problem validating any on-disk component */ - void validatePerColumnIndexComponents(IndexDescriptor indexDescriptor, IndexTermType indexTermType, IndexIdentifier indexIdentifier, boolean checksum); + void validateIndexComponent(IndexComponent.ForRead component, boolean checksum); /** - * Returns the set of {@link IndexComponent} for the per-SSTable part of an index. - * This is a complete set of components that could exist on-disk. It does not imply that the + * Returns the set of {@link IndexComponentType} for the per-SSTable part of an index. + * This is a complete set of componentstypes that could exist on-disk. It does not imply that the * components currently exist on-disk. - - * @param hasClustering true if the SSTable forms part of a table using clustering columns + * + * @return The set of {@link IndexComponentType} for the per-SSTable index */ - Set perSSTableIndexComponents(boolean hasClustering); + public Set perSSTableComponentTypes(); /** - * Returns the set of {@link IndexComponent} for the per-column part of an index. - * This is a complete set of components that could exist on-disk. It does not imply that the + * Returns the set of {@link IndexComponentType} for the per-index part of an index. + * This is a complete set of component types that could exist on-disk. It does not imply that the * components currently exist on-disk. * - * @param indexTermType the {@link IndexTermType} of the index + * @param indexContext The {@link IndexContext} for the index + * @return The set of {@link IndexComponentType} for the per-index index */ - Set perColumnIndexComponents(IndexTermType indexTermType); + default public Set perIndexComponentTypes(IndexContext indexContext) + { + return perIndexComponentTypes(indexContext.getValidator()); + } + + public Set perIndexComponentTypes(AbstractType validator); /** * Return the number of open per-SSTable files that can be open during a query. * This is a static indication of the files that can be held open by an index * for queries. It is not a dynamic calculation. * - * @param hasClustering true if the SSTable forms part of a table using clustering columns + * @return The number of open per-SSTable files */ - int openFilesPerSSTableIndex(boolean hasClustering); + public int openFilesPerSSTable(); /** - * Return the number of open per-column index files that can be open during a query. + * Return the number of open per-index files that can be open during a query. * This is a static indication of the files that can be help open by an index * for queries. It is not a dynamic calculation. + * + * @param indexContext The {@link IndexContext} for the index + * @return The number of open per-index files + */ + public int openFilesPerIndex(IndexContext indexContext); + + /** + * Return the {@link ByteOrder} for the given {@link IndexComponentType} and {@link IndexContext}. + * + * @param component - The {@link IndexComponentType} for the index + * @param context - The {@link IndexContext} for the index + * @return The {@link ByteOrder} for the file associated with the {@link IndexComponentType} */ - int openFilesPerColumnIndex(); + public ByteOrder byteOrderFor(IndexComponentType component, IndexContext context); + + /** + * Encode the given {@link ByteBuffer} into a {@link ByteComparable} object based on the provided {@link AbstractType} + * for storage in the trie index. This is used for both in memory and on disk tries. This is valid for encoding + * terms to be inserted, search terms, and search bounds. + * + * @return The encoded {@link ByteComparable} object + */ + ByteComparable encodeForTrie(ByteBuffer input, AbstractType type); + + /** + * Inverse of {@link #encodeForTrie(ByteBuffer, AbstractType)} + */ + ByteBuffer decodeFromTrie(ByteComparable value, AbstractType type); + + /** + * @return the JVector file format version that this on-disk format uses. + */ + int jvectorFileFormatVersion(); + } diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/SSTableIndexComponentsState.java b/src/java/org/apache/cassandra/index/sai/disk/format/SSTableIndexComponentsState.java new file mode 100644 index 000000000000..7f2bc76c4072 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/format/SSTableIndexComponentsState.java @@ -0,0 +1,538 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.format; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +/** + * Represents, for a sstable, the "state" (version and generation) of the index components it is using. + *

    + * This class essentially store, for each "group" of index components (so for the per-sstable group, and for each index), + * a version and generation, identifying a particular build of each group. This is used by {@link IndexComponentDiscovery} + * to return which concrete components should be loaded for the sstable, but as this class is immutable, it can be + * used to figure changes to index files between two different times (by capturing the state before, and comparing to + * the state after); see {@link #indexWasUpdated} as an example. + *

    + * As this state only reference the {@link ComponentsBuildId} of the components, it does not represent whether that + * build is complete/valid, and as such this class does not guarantee _in general_ that the builds it returns are + * usable, and whether it does depend on context. But some methods may explicitly return a state with only complete + * groups (like {@link #of(IndexDescriptor)}). + */ +public class SSTableIndexComponentsState +{ + public static final SSTableIndexComponentsState EMPTY = new SSTableIndexComponentsState(null, Map.of()); + + // The state of the per-sstable group of components, if they exist. + private final @Nullable State perSSTableState; + + // The state for every "group" of per-index components keyed by the index name. + private final Map perIndexStates; + + private SSTableIndexComponentsState(@Nullable State perSSTableState, Map perIndexStates) + { + Preconditions.checkNotNull(perIndexStates); + this.perSSTableState = perSSTableState; + this.perIndexStates = Collections.unmodifiableMap(perIndexStates); + } + + /** + * Extracts the current state of a particular SSTable given its descriptor. + *

    + * Please note that this method only include "complete" component groups in the state, and thus represents the + * "usable" groups. In particular, if the per-sstable group is not complete, the returned state will be empty. + * + * @param descriptor the index descriptor of the sstable for which to get the component state. + * @return the state of the sstable's complete components. + */ + public static SSTableIndexComponentsState of(IndexDescriptor descriptor) + { + var perSSTable = descriptor.perSSTableComponents(); + // If the per-sstable part is not complete, then nothing is complete. + if (!perSSTable.isComplete()) + return EMPTY; + + Map perIndexStates = new HashMap<>(); + for (IndexContext context : descriptor.includedIndexes()) + { + var perIndex = descriptor.perIndexComponents(context); + if (perIndex.isComplete()) + perIndexStates.put(context.getIndexName(), State.of(perIndex)); + } + return new SSTableIndexComponentsState(State.of(perSSTable), perIndexStates); + } + + /** + * Extracts the current index components state of a particular SSTable. + *

    + * This method delegates to {@link #of(IndexDescriptor)}, so see that method for additional details. + * + * @param sstable the sstable for which to get the component state. + * @return the state of the sstable's complete index components. If the sstable belongs to a table that is not + * indexed (or not by SAI), then this will be {@link #EMPTY}. + * + * @throws IllegalStateException if the {@link org.apache.cassandra.db.ColumnFamilyStore} of the sstable cannot + * be found for some reason (it is necessary to retrieve the underlying {@link IndexDescriptor}). This may happen + * if this is called by an "offline" tool. + */ + public static SSTableIndexComponentsState of(SSTableReader sstable) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(sstable.metadata().id); + if (cfs == null) + throw new IllegalStateException("Cannot find the ColumnFamilyStore for the sstable " + sstable); + + StorageAttachedIndexGroup saiGroup = StorageAttachedIndexGroup.getIndexGroup(cfs); + // If the table is not indexed (at least by SAI), fine. + if (saiGroup == null) + return SSTableIndexComponentsState.EMPTY; + + return SSTableIndexComponentsState.of(saiGroup.descriptorFor(sstable)); + } + + public static Builder builder() + { + return new Builder(); + } + + /** + * Returns a newly created builder initialized with the data of this state. + */ + public Builder unbuild() + { + Builder builder = new Builder(); + builder.addPerSSTable(perSSTableState); + perIndexStates.forEach(builder::addPerIndex); + return builder; + } + + public boolean isEmpty() + { + return perSSTableState == null && perIndexStates.isEmpty(); + } + + public @Nullable State perSSTable() + { + return perSSTableState; + } + + public @Nullable State perIndex(String indexName) + { + Preconditions.checkNotNull(indexName); + return perIndexStates.get(indexName); + } + + public @Nullable ComponentsBuildId perSSTableBuild() + { + return perSSTableState == null ? null : perSSTableState.buildId; + } + + public @Nullable ComponentsBuildId perIndexBuild(String indexName) + { + Preconditions.checkNotNull(indexName); + var state = perIndexStates.get(indexName); + return state == null ? null : state.buildId; + } + + /** + * Returns whether the provided index have been updated since the given state. + *

    + * Having been "updated" for this method means that builds of the components used by the index have changed. + * Importantly, this is true if _either_ the per-sstable components have changed, or that of the index itself, + * since every index uses the per-sstable components. + */ + public boolean indexWasUpdated(SSTableIndexComponentsState stateBefore, String indexName) + { + Preconditions.checkNotNull(indexName); + return !Objects.equals(stateBefore.perSSTableBuild(), this.perSSTableBuild()) + || !Objects.equals(stateBefore.perIndexBuild(indexName), this.perIndexBuild(indexName)); + } + + /** + * The set of the names of all the indexes for which the state has a build for. + *

    + * This does not include anything regarding the per-sstable components. + */ + public Set includedIndexes() + { + return perIndexStates.keySet(); + } + + /** + * The total size (in MB) of all the components included in this state. + */ + public long totalSizeInMB() + { + long total = perSSTableState == null ? 0 : perSSTableState.sizeInMB; + for (State state : perIndexStates.values()) + total += state.sizeInMB; + return total; + } + + /** + * Returns a diff between this state and the provided one which is assumed to be an earlier version. + * + * @param before the state to compare this state with. + * @return the diff between the 2 states. + */ + public Diff diff(SSTableIndexComponentsState before) + { + boolean perSSTableModified = !Objects.equals(before.perSSTableBuild(), this.perSSTableBuild()); + Set modifiedIndexes = this.includedIndexes() + .stream() + .filter(index -> !Objects.equals(before.perIndexBuild(index), this.perIndexBuild(index))) + .collect(Collectors.toSet()); + Set removedIndexes = before.includedIndexes() + .stream() + .filter(index -> !this.perIndexStates.containsKey(index)) + .collect(Collectors.toSet()); + return new Diff(before, this, perSSTableModified, modifiedIndexes, removedIndexes); + } + + /** + * Applies the provided diff to this state, if applicable. + *

    + * The assumption of this method is that the state it is applied to is for the same sstable that the 2 states that + * were used to produce the provided diff. + *

    + * The diff will apply successfully if for anything that is modified in the provided diff, the current state is + * equivalent to the "before" state of the diff. If that is not the case, an {@link UnapplicableDiffException} will + * be thrown. But for anything that was not modified by the diff, the current state will be kept as is. Note in + * particular that this means that if {@code this == diff.before}, then the result will be exactly {@code diff.after}, + * but as long as {@code this} has only modifications (compared to {@code diff.before}) that are not in {@code diff}, + * then the diff will still apply correctly. + *

    + * In other word, this method allows to compute the expected result of some index builds represented by the diff + * to the "current" state as long as said "current" state is the "before" state of the diff plus some eventual + * concurrent modifications, as long as those concurrent modifications do not conflict with the ones of the diff. + * + * @param diff the diff to try to apply to this state. + * @return the result of applying the diff to this state, if successful. + * + * @throws UnapplicableDiffException if the diff cannot be applied to this state. + */ + public SSTableIndexComponentsState tryApplyDiff(Diff diff) + { + if (diff.isEmpty()) + return this; + + Builder builder = builder(); + builder.addPerSSTable(diff.perSSTableUpdated + ? diffState(diff.before.perSSTableState, diff.after.perSSTableState, this.perSSTableState, () -> "per-sstable components build") + : this.perSSTableState); + + // Adds anything modified to the "modified" version, but making sure the diff "applies", meaning that the + // "current" state is still the origin of the diff. + for (String modified : diff.perIndexesUpdated) + { + builder.addPerIndex(modified, diffState(diff.before.perIndex(modified), diff.after.perIndex(modified), this.perIndex(modified), () -> "index " + modified + " components build")); + } + // Then mirror all the current index that were not modified, but skipping removed ones. + for (String index : includedIndexes()) + { + // The `perIndexesUpdated` have already been handled above. And a removed index means the index has been + // dropped, so even if some concurrent build on the index happened concurrently, the index is still gone. + if (diff.perIndexesUpdated.contains(index) || diff.perIndexesRemoved.contains(index)) + continue; + + builder.addPerIndex(index, this.perIndex(index)); + } + return builder.build(); + } + + private static State diffState(State diffBefore, State diffAfter, State current, Supplier what) + { + // If current is `null`, but our "before" isn't, that means the index this is a component of has been dropped + // since the state we use to create the diff (and for the per-sstable components, it was the only index that + // was dropped). We want to handle a drop that happens concurrently of some build/rebuild of the same index, + // because it's impossible to completly prevent it anyway, and the result is simply that the index is not there + // anymore. + if (current == null && diffBefore != null) + return null; + + if (!(Objects.equals(diffBefore, current))) + throw new UnapplicableDiffException("Current " + what.get() + " expected to be " + diffBefore + ", but was " + current); + + return diffAfter; + } + + @Override + public int hashCode() + { + return Objects.hash(perSSTableState, perIndexStates); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof SSTableIndexComponentsState)) + return false; + + SSTableIndexComponentsState that = (SSTableIndexComponentsState) obj; + return Objects.equals(this.perSSTableState, that.perSSTableState) + && this.perIndexStates.equals(that.perIndexStates); + } + + @Override + public String toString() + { + Stream perIndex = perIndexStates.entrySet() + .stream() + .map(e -> e.getKey() + ": " + e.getValue()); + Stream all = perSSTableState == null + ? perIndex + : Stream.concat(Stream.of(": " + perSSTableState), perIndex); + + return all.collect(Collectors.joining(", ", "{", "}")); + } + + /** + * Represents the "state" for one "group" of components (so either the per-sstable one, or one of the per-index ones). + */ + public static class State + { + /** The "build" (version and generaton) the components. */ + public final ComponentsBuildId buildId; + + /** The total size (in MB) of the components (we use MB because this is meant to be indicative, is enough + * precision in practice and is more human-readable). */ + public final long sizeInMB; + + private State(ComponentsBuildId buildId, long sizeInMB) + { + Preconditions.checkNotNull(buildId); + this.buildId = buildId; + this.sizeInMB = sizeInMB; + } + + private static State of(IndexComponents.ForRead components) + { + return new State(components.buildId(), toMB(components.liveSizeOnDiskInBytes())); + } + + public static long toMB(long bytes) + { + if (bytes == 0) + return 0; + + // We avoid returning 0 unless the size is truly zero to avoid making it look like the components do not + // exist. Mostly a detail in practice but ... + return Math.max(bytes / 1024 / 1024, 1); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof State)) + return false; + + State that = (State) obj; + return this.buildId.equals(that.buildId) && this.sizeInMB == that.sizeInMB; + } + + @Override + public int hashCode() + { + return Objects.hash(buildId, sizeInMB); + } + + @Override + public String toString() + { + return String.format("%s (%dMB)", buildId, sizeInMB); + } + } + + /** + * Builder for {@link SSTableIndexComponentsState} instances. + *

    + * This should primarily be used by implementations of {@link IndexComponentDiscovery} and tests, as the rest of + * the code should generally not build a state manually (and instead use methods like {@link SSTableIndexComponentsState#of}). + */ + public static class Builder + { + private State perSSTableState; + // We use a linked map to preserve order of insertions. This is not crucial, but the overhead is negligible and + // in the case of tests, the predictability of entry order make things _a lot_ easier/natural. + private final Map perIndexStates = new LinkedHashMap<>(); + // This make extra sure we don't reuse a builder by accident as it is not safe to do so (we pass the map + // directly when we build the state). If one wants to reuse a builder, it should `copy` manually first. + private boolean built; + + public Builder addPerSSTable(Version version, int generation, long sizeInMB) + { + return addPerSSTable(ComponentsBuildId.of(version, generation), sizeInMB); + } + + public Builder addPerSSTable(ComponentsBuildId buildId, long sizeInMB) + { + return addPerSSTable(new State(buildId, sizeInMB)); + } + + public Builder addPerSSTable(State state) + { + Preconditions.checkState(!built, "Builder has already been used"); + this.perSSTableState = state; + return this; + } + + public Builder addPerIndex(String name, Version version, int generation, long sizeInMB) + { + return addPerIndex(name, ComponentsBuildId.of(version, generation), sizeInMB); + } + + public Builder addPerIndex(String name, ComponentsBuildId buildId, long sizeInMB) + { + return addPerIndex(name, new State(buildId, sizeInMB)); + } + + public Builder addPerIndex(String name, State state) + { + Preconditions.checkState(!built, "Builder has already been used"); + Preconditions.checkNotNull(name); + if (state != null) + perIndexStates.put(name, state); + return this; + } + + public Builder removePerSSTable() + { + Preconditions.checkState(!built, "Builder has already been used"); + perSSTableState = null; + return this; + } + + public Builder removePerIndex(String name) + { + Preconditions.checkState(!built, "Builder has already been used"); + Preconditions.checkNotNull(name); + perIndexStates.remove(name); + return this; + } + + public Builder copy() + { + Builder copy = new Builder(); + copy.perSSTableState = perSSTableState; + copy.perIndexStates.putAll(perIndexStates); + return copy; + } + + public SSTableIndexComponentsState build() + { + built = true; + return new SSTableIndexComponentsState(perSSTableState, perIndexStates); + } + } + + /** + * Represents the difference between two {@link SSTableIndexComponentsState} instances that are assumed snapshots + * of the same sstable at 2 different times. + */ + public static class Diff + { + /** Older of the 2 states compared in this diff. */ + public final SSTableIndexComponentsState before; + /** Newer of the 2 states compared in this diff. */ + public final SSTableIndexComponentsState after; + /** Whether the per-sstable components were updated between the 2 states. */ + public final boolean perSSTableUpdated; + /** Which per-index components were updated between the 2 states. */ + public final Set perIndexesUpdated; + /** Which per-index components were removed (where in {@link #before}) but not {@link #after}. */ + public final Set perIndexesRemoved; + + private Diff(SSTableIndexComponentsState before, SSTableIndexComponentsState after, boolean perSSTableUpdated, Set perIndexesUpdated, Set perIndexesRemoved) + { + this.before = before; + this.after = after; + this.perSSTableUpdated = perSSTableUpdated; + this.perIndexesUpdated = Collections.unmodifiableSet(perIndexesUpdated); + this.perIndexesRemoved = Collections.unmodifiableSet(perIndexesRemoved); + } + + /** + * Whether this diff is empty, meaning that no changes were detected between the 2 states. + */ + public boolean isEmpty() + { + return !perSSTableUpdated && perIndexesUpdated.isEmpty() && perIndexesRemoved.isEmpty(); + } + + /** + * Whether the operation that created this diff (meaning, the operation(s) that happened on {@link #before} + * to create {@link #after}) left some "unused" components, meaning that new components (new version or + * generation) were created where previous one existed. + */ + public boolean createsUnusedComponents() + { + // Removing any components left them "unused". + if (!perIndexesRemoved.isEmpty()) + return true; + + return (perSSTableUpdated && before.perSSTableState != null) + || perIndexesUpdated.stream().anyMatch(index -> before.perIndex(index) != null); + } + + @Override + public String toString() + { + if (isEmpty()) + return String.format("%s (no diff)", before); + + List updates = new ArrayList<>(); + if (perSSTableUpdated) + { + if (after.perSSTableState == null) + updates.add("-"); + else + updates.add("+"); + } + for (String updated : perIndexesUpdated) + updates.add('+' + updated); + for (String removed : perIndexesRemoved) + updates.add('-' + removed); + return String.format("%s -> %s (%s)", before, after, String.join(" ", updates)); + } + } + + public static class UnapplicableDiffException extends RuntimeException + { + public UnapplicableDiffException(String message) + { + super(message); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/Version.java b/src/java/org/apache/cassandra/index/sai/disk/format/Version.java index a536e2e91fbe..198ecb24af41 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/Version.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/Version.java @@ -17,20 +17,36 @@ */ package org.apache.cassandra.index.sai.disk.format; -import java.util.Comparator; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.stream.Collectors; - +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; +import com.google.common.collect.Lists; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.disk.v1.V1OnDiskFormat; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.io.sstable.Component; -import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.index.sai.disk.v2.V2OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v3.V3OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v4.V4OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v5.V5OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v6.V6OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v7.V7OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v8.V8OnDiskFormat; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.format.bti.BtiFormat; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +import static com.google.common.base.Preconditions.checkArgument; /** * Format version of indexing component, denoted as [major][minor]. Same forward-compatibility rules apply as to @@ -38,22 +54,47 @@ */ public class Version implements Comparable { - public static final String SAI_DESCRIPTOR = "SAI"; - public static final String SAI_SEPARATOR = "+"; - - // Current version - public static final Version AA = new Version("aa", V1OnDiskFormat.instance, (c, i) -> defaultFileNameFormat(c, i, "aa")); + private static final Logger LOGGER = LoggerFactory.getLogger(Version.class); + // 6.8 formats + public static final Version AA = new Version("aa", V1OnDiskFormat.instance, Version::aaFileNameFormat); + // Stargazer + public static final Version BA = new Version("ba", V2OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "ba")); + // Converged Cassandra with JVector with file format version 2 + // Note: vector index checksums for TERMS files were computed in two different ways for this version. As such, + // we do not validate checksums for this version or any subsequent version until EC. + public static final Version CA = new Version("ca", V3OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "ca")); + // NOTE: use DB to prevent collisions with upstream file formats + // Encode trie entries using their AbstractType to ensure trie entries are sorted for range queries and are prefix free. + public static final Version DB = new Version("db", V4OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "db")); + // revamps vector postings lists to cause fewer reads from disk + public static final Version DC = new Version("dc", V5OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "dc")); + // histograms in index metadata + public static final Version EB = new Version("eb", V6OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "eb")); + // term frequencies index component (support for BM25); bump jvector file format version to 4 + // Start validating vector index component checksums, except for the TERMS_FILE because it's checksum is non-standard + // and isn't easily validated when an sstable index has multiple segments within the TERMS_FILE. + public static final Version EC = new Version("ec", V7OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "ec")); + // total terms count serialization in index metadata, enables ANN_USE_SYNTHETIC_SCORE by default + public static final Version ED = new Version("ed", V7OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "ed")); + // jvector file format version 6 (skipped 5) + public static final Version FA = new Version("fa", V8OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "fa")); - // These should be added in reverse order so that the latest version is used first. Version matching tests + // These are in reverse-chronological order so that the latest version is first. Version matching tests // are more likely to match the latest version, so we want to test that one first. - public static final SortedSet ALL = new TreeSet<>(Comparator.reverseOrder()) {{ - add(AA); - }}; + public static final List ALL = Lists.newArrayList(FA, ED, EC, EB, DC, DB, CA, BA, AA); public static final Version EARLIEST = AA; - // The latest version can be configured to be an earlier version to support partial upgrades that don't - // write newer versions of the on-disk formats. - public static final Version LATEST = CassandraRelevantProperties.SAI_LATEST_VERSION.convert(Version::parse); + public static final Version VECTOR_EARLIEST = BA; + public static final Version JVECTOR_EARLIEST = CA; + public static final Version BM25_EARLIEST = EC; + public static final Version LATEST = ALL.get(0); + + // This is volatile rather than final so that tests may use reflection to change it and safely publish across threads, + // but it should not be changed outside of tests. + @SuppressWarnings("FieldMayBeFinal") + private static volatile Selector SELECTOR = Selector.fromProperty(); + + private static final Pattern GENERATION_PATTERN = Pattern.compile("\\d+"); private final String version; private final OnDiskFormat onDiskFormat; @@ -66,19 +107,78 @@ private Version(String version, OnDiskFormat onDiskFormat, FileNameFormatter fil this.fileNameFormatter = fileNameFormatter; } - public static Version parse(String versionString) + public static Version parse(String input) { - for (Version version : ALL) - if (version.version.equals(versionString)) - return version; - throw new IllegalArgumentException("The version string " + versionString + " does not represent a valid SAI version. " + - "It should be one of " + ALL.stream().map(Version::toString).collect(Collectors.joining(", "))); + checkArgument(input != null); + checkArgument(input.length() == 2); + for (Version v : ALL) { + if (input.equals(v.version)) + return v; + } + throw new IllegalArgumentException("Unrecognized SAI version string " + input); } - @Override - public int compareTo(Version other) + /** + * @param keyspace the keyspace for which to select a version, assumed to belong to an existing keyspace. + * @return the version to use on new SSTables for the provided keyspace. + */ + public static Version current(String keyspace) + { + assert keyspace != null : "Keyspace name must not be null"; + return SELECTOR.select(keyspace); + } + + /** + * Calculates the maximum allowed length for SAI index names to ensure generated filenames + * do not exceed the system's filename length limit (defined in {@link SchemaConstants#FILENAME_LENGTH}). + * This accounts for all additional components in the filename. + * It is only used to validate that {@link SchemaConstants#INDEX_NAME_LENGTH} + * is not bigger than the actual acceptable length. + */ + @VisibleForTesting + public static int calculateIndexNameAllowedLength(String keyspace) + { + int addedLength = getAddedLengthFromDescriptorAndVersion(keyspace); + assert addedLength < SchemaConstants.FILENAME_LENGTH; + return SchemaConstants.FILENAME_LENGTH - addedLength; + } + + /** + * Calculates the length of the added prefixes and suffixes from Descriptor constructor + * and {@link Version#stargazerFileNameFormat}. + * + * @return the length of the added prefixes and suffixes + */ + private static int getAddedLengthFromDescriptorAndVersion(String keyspace) + { + // Prefixes and suffixes constructed by Version.stargazerFileNameFormat + int versionNameLength = current(keyspace).toString().length(); + // room for up to 999 generations + int generationLength = 3 + SAI_SEPARATOR.length(); + int addedLength = SAI_DESCRIPTOR.length() + + versionNameLength + + generationLength + + calculateMaxComponentRepresentationLength() + + SAI_SEPARATOR.length() * 3 + + EXTENSION.length(); + + // Prefixes from Descriptor constructor + int separatorLength = 1; + int indexVersionLength = 2; + int tableIdLength = 28; + addedLength += indexVersionLength + + BtiFormat.NAME.length() + + tableIdLength + + separatorLength * 3; + return addedLength; + } + + private static int calculateMaxComponentRepresentationLength() { - return version.compareTo(other.version); + int maxLength = 0; + for (IndexComponentType component : IndexComponentType.values()) + maxLength = Math.max(maxLength, component.representation.length()); + return maxLength; } @Override @@ -92,7 +192,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Version other = (Version)o; + Version other = (Version) o; return Objects.equal(version, other.version); } @@ -102,6 +202,12 @@ public String toString() return version; } + // Useful for handling features that need a two phase rollout. + public boolean after(Version other) + { + return version.compareTo(other.version) > 0; + } + public boolean onOrAfter(Version other) { return version.compareTo(other.version) >= 0; @@ -112,46 +218,242 @@ public OnDiskFormat onDiskFormat() return onDiskFormat; } - public Component makePerSSTableComponent(IndexComponent indexComponent) + public FileNameFormatter fileNameFormatter() { - return indexComponent.type.createComponent(fileNameFormatter.format(indexComponent, null)); + return fileNameFormatter; } - public Component makePerIndexComponent(IndexComponent indexComponent, IndexIdentifier indexIdentifier) + public boolean useImmutableComponentFiles() { - return indexComponent.type.createComponent(fileNameFormatter.format(indexComponent, indexIdentifier)); + return CassandraRelevantProperties.IMMUTABLE_SAI_COMPONENTS.getBoolean() + && onOrAfter(parse(CassandraRelevantProperties.IMMUTABLE_SAI_COMPONENTS_MIN_VERSION.getString())); } - public FileNameFormatter fileNameFormatter() + @Override + public int compareTo(Version other) { - return fileNameFormatter; + return this.version.compareTo(other.version); } public interface FileNameFormatter { - String format(IndexComponent indexComponent, IndexIdentifier indexIdentifier); + /** + * Format filename for given index component, context and generation. Only the "component" part of the + * filename is returned (so the suffix of the full filename), not a full path. + */ + default String format(IndexComponentType indexComponentType, IndexContext indexContext, int generation) + { + return format(indexComponentType, indexContext == null ? null : indexContext.getIndexName(), generation); + } + + /** + * Format filename for given index component, index and generation. Only the "component" part of the + * filename is returned (so the suffix of the full filename), not a full path. + * + * @param indexComponentType the type of the index component. + * @param indexName the name of the index, or {@code null} for a per-sstable component. + * @param generation the generation of the build of the component. + */ + String format(IndexComponentType indexComponentType, @Nullable String indexName, int generation); } /** - * SAI default filename formatter. This is the current SAI on-disk filename format - *

    - * Format: {@code -SAI+(+)+.db} - * Note: The index name is excluded for per-SSTable index files that are shared - * across all the per-column indexes for the SSTable. + * Try to parse the provided file name as a SAI component file name. + * + * @param filename the file name to try to parse. + * @return the information parsed from the provided file name if it can be successfully parsed, or an empty optional + * if the file name is not recognized as a SAI component file name for a supported version. */ - private static String defaultFileNameFormat(IndexComponent indexComponent, - @Nullable IndexIdentifier indexIdentifier, - String version) + public static Optional tryParseFileName(String filename) + { + if (!filename.endsWith(EXTENSION)) + return Optional.empty(); + + // For flexibility, we handle both "full" filename, of the form "-SAI+....db", or just the component + // part, that is "SAI+....db". In the former, the following `lastIndexOf` will match, and we'll set + // `startOfComponent` at the beginning of "SAI", and in the later it will not match and return -1, which, with + // the +1 will also be set at the beginning of "SAI". + int startOfComponent = filename.lastIndexOf('-') + 1; + + String componentStr = filename.substring(startOfComponent); + if (componentStr.startsWith("SAI_")) + return tryParseAAFileName(componentStr); + else if (componentStr.startsWith("SAI" + SAI_SEPARATOR)) + return tryParseStargazerFileName(componentStr); + else + return Optional.empty(); + } + + public static class ParsedFileName + { + public final ComponentsBuildId buildId; + public final IndexComponentType component; + public final @Nullable String indexName; + + private ParsedFileName(ComponentsBuildId buildId, IndexComponentType component, @Nullable String indexName) + { + this.buildId = buildId; + this.component = component; + this.indexName = indexName; + } + } + + // + // Version.AA filename formatter. This is the old DSE 6.8 SAI on-disk filename format + // + // Format: -SAI(_)_.db + // + private static final String VERSION_AA_PER_SSTABLE_FORMAT = "SAI_%s.db"; + private static final String VERSION_AA_PER_SSTABLE_WITH_GENERATION_FORMAT = "SAI_%s_%d.db"; + private static final String VERSION_AA_PER_INDEX_FORMAT = "SAI_%s_%s.db"; + private static final String VERSION_AA_PER_INDEX_WITH_GENERATION_FORMAT = "SAI_%s_%s_%d.db"; + + private static String aaFileNameFormat(IndexComponentType indexComponentType, @Nullable String indexName, int generation) + { + if (generation > 0) + return (indexName == null ? String.format(VERSION_AA_PER_SSTABLE_WITH_GENERATION_FORMAT, indexComponentType.representation, generation) + : String.format(VERSION_AA_PER_INDEX_WITH_GENERATION_FORMAT, indexName, indexComponentType.representation, generation)); + + return (indexName == null ? String.format(VERSION_AA_PER_SSTABLE_FORMAT, indexComponentType.representation) + : String.format(VERSION_AA_PER_INDEX_FORMAT, indexName, indexComponentType.representation)); + } + + private static Optional tryParseAAFileName(String componentStr) + { + int lastSepIdx = componentStr.lastIndexOf('_'); + if (lastSepIdx == -1) + return Optional.empty(); + + int generation = 0; + // This method is only called by `tryParseFileName` which ensures the `componentStr` ends with ".db", so + // `length() - 3` below is safe. + assert lastSepIdx + 1 <= componentStr.length() - 3; + String maybeGenerationStr = componentStr.substring(lastSepIdx + 1, componentStr.length() - 3); + String indexComponentStr = maybeGenerationStr; + if (GENERATION_PATTERN.matcher(maybeGenerationStr).matches()) + { + generation = Integer.parseInt(maybeGenerationStr); + int prevSepIdx = componentStr.substring(0, lastSepIdx).lastIndexOf('_'); + if (prevSepIdx == -1) + return Optional.empty(); + indexComponentStr = componentStr.substring(prevSepIdx + 1, lastSepIdx); + lastSepIdx = prevSepIdx; + } + + IndexComponentType indexComponentType = IndexComponentType.fromRepresentation(indexComponentStr); + if (indexComponentType == null) + return Optional.empty(); + + String indexName = null; + int firstSepIdx = componentStr.indexOf('_'); + if (firstSepIdx != -1 && firstSepIdx != lastSepIdx) + indexName = componentStr.substring(firstSepIdx + 1, lastSepIdx); + + return Optional.of(new ParsedFileName(ComponentsBuildId.of(AA, generation), indexComponentType, indexName)); + } + + /** + * This is an interface for selecting the appropriate version of the on-disk format to use. + * This is going to be used by CNDB to inject the version of SAI to use for a given tenant + */ + public interface Selector + { + /** + * Default version used by {@link #DEFAULT}. + */ + Version DEFAULT_VERSION = Version.parse(CassandraRelevantProperties.SAI_CURRENT_VERSION.getString()); + + /** + * Default version selector that uses the same version for all keyspaces. + */ + Selector DEFAULT = keyspace -> DEFAULT_VERSION; + + /** + * @param keyspace the keyspace for which to select a version, assumed to belong to an existing keyspace. + * @return the version of the on-disk format to use for the provided keyspace, it should not be null. + */ + @Nonnull + Version select(@Nonnull String keyspace); + + static Selector fromProperty() + { + try + { + String selectorClass = CassandraRelevantProperties.SAI_VERSION_SELECTOR_CLASS.getString(); + if (selectorClass.isEmpty()) + { + return Selector.DEFAULT; + } + else + { + LOGGER.info("Using SAI version selector: {}", selectorClass); + return FBUtilities.construct(selectorClass, "SAI version selector"); + } + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + } + + // + // Stargazer filename formatter. This is the current SAI on-disk filename format + // + // Format: -SAI+(+)(+)+.db + // + public static final String SAI_DESCRIPTOR = "SAI"; + private static final String SAI_SEPARATOR = "+"; + private static final String EXTENSION = ".db"; + + private static String stargazerFileNameFormat(IndexComponentType indexComponentType, @Nullable String indexName, int generation, String version) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(SAI_DESCRIPTOR); stringBuilder.append(SAI_SEPARATOR).append(version); - if (indexIdentifier != null) - stringBuilder.append(SAI_SEPARATOR).append(indexIdentifier.indexName); - stringBuilder.append(SAI_SEPARATOR).append(indexComponent.name); - stringBuilder.append(Descriptor.EXTENSION); + if (generation > 0) + stringBuilder.append(SAI_SEPARATOR).append(generation); + if (indexName != null) + stringBuilder.append(SAI_SEPARATOR).append(indexName); + stringBuilder.append(SAI_SEPARATOR).append(indexComponentType.representation); + stringBuilder.append(EXTENSION); return stringBuilder.toString(); } + + public ByteComparable.Version byteComparableVersionFor(IndexComponentType component, org.apache.cassandra.io.sstable.format.Version sstableFormatVersion) + { + return this == AA && component == IndexComponentType.TERMS_DATA + ? sstableFormatVersion.getByteComparableVersion() + : TypeUtil.BYTE_COMPARABLE_VERSION; + } + + private static Optional tryParseStargazerFileName(String componentStr) + { + // We skip the beginning `SAI+` and ending `.db` parts. + String[] splits = componentStr.substring(4, componentStr.length() - 3).split("\\+"); + if (splits.length < 2 || splits.length > 4) + return Optional.empty(); + + Version version = parse(splits[0]); + IndexComponentType indexComponentType = IndexComponentType.fromRepresentation(splits[splits.length - 1]); + + int generation = 0; + String indexName = null; + if (splits.length > 2) + { + // If we have 4 parts, then we know we have both the generation and index name. If we have 3 + // however, it means we have either one, but we don't know which, so we check if the additional + // part is a number or not to distinguish. + boolean hasGeneration = splits.length == 4 || GENERATION_PATTERN.matcher(splits[1]).matches(); + boolean hasIndexName = splits.length == 4 || !hasGeneration; + if (hasGeneration) + generation = Integer.parseInt(splits[1]); + if (hasIndexName) + indexName = splits[splits.length - 2]; + } + + return Optional.of(new ParsedFileName(ComponentsBuildId.of(version, generation), indexComponentType, indexName)); + } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/BufferedChecksumIndexInput.java b/src/java/org/apache/cassandra/index/sai/disk/io/BufferedChecksumIndexInput.java index 333868466ad1..6cb5fea30d91 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/io/BufferedChecksumIndexInput.java +++ b/src/java/org/apache/cassandra/index/sai/disk/io/BufferedChecksumIndexInput.java @@ -28,7 +28,7 @@ * This implementation of {@link ChecksumIndexInput} is based on {@link org.apache.lucene.store.BufferedChecksumIndexInput} * but uses custom checksum algorithm instead of the hardcoded {@code CRC32} in {@code BufferedChecksumIndexInput}. * - * @see org.apache.cassandra.index.sai.disk.io.IndexFileUtils.ChecksummingWriter + * @see org.apache.cassandra.index.sai.disk.io.IndexFileUtils.IncrementalChecksumSequentialWriter */ class BufferedChecksumIndexInput extends ChecksumIndexInput { diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/BytesRefUtil.java b/src/java/org/apache/cassandra/index/sai/disk/io/BytesRefUtil.java new file mode 100644 index 000000000000..ccad8f85f51a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/io/BytesRefUtil.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.io; + + +import java.nio.ByteBuffer; + +import org.apache.cassandra.utils.FastByteOperations; +import org.apache.lucene.util.BytesRefBuilder; + +public final class BytesRefUtil +{ + private BytesRefUtil() {} + + public static void copyBufferToBytesRef(ByteBuffer buffer, BytesRefBuilder stringBuffer) + { + int length = buffer.remaining(); + stringBuffer.clear(); + stringBuffer.grow(length); + FastByteOperations.copy(buffer, buffer.position(), stringBuffer.bytes(), 0, buffer.remaining()); + stringBuffer.setLength(length); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/CryptoUtils.java b/src/java/org/apache/cassandra/index/sai/disk/io/CryptoUtils.java new file mode 100644 index 000000000000..46467dd999f5 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/io/CryptoUtils.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.io; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.index.sai.disk.oldlucene.ByteArrayIndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.BytesRef; + +public class CryptoUtils +{ + + public static CompressionMetadata getCompressionMeta(SSTableReader ssTableReader) + { + return ssTableReader.compression ? ssTableReader.getCompressionMetadata() : null; + } + + public static CompressionParams getCompressionParams(SSTableReader ssTableReader) + { + return getCompressionParams(getCompressionMeta(ssTableReader)); + } + + public static CompressionParams getCompressionParams(CompressionMetadata meta) + { + return meta != null ? meta.parameters : null; + } + + //TODO Encryption tidyup +// public static ICompressor getEncryptionCompressor(CompressionParams compressionParams) +// { +// ICompressor compressor = compressionParams != null ? compressionParams.getSstableCompressor() : null; +// return compressor != null ? compressor.encryptionOnly() : null; +// } +// +// public static boolean isCryptoEnabled(CompressionParams params) +// { +// ICompressor sstableCompressor = params != null ? params.getSstableCompressor() : null; +// return sstableCompressor != null && sstableCompressor.encryptionOnly() != null ? true : false; +// } + + public static IndexInput uncompress(IndexInput input, ICompressor compressor) throws IOException + { + return uncompress(input, compressor, + new BytesRef(new byte[16]), new BytesRef(new byte[16]) + ); + } + + /** + * Takes an {@link IndexInput} with compressed/encrypted data and returns another {@link IndexInput} with + * that data uncompressed/decrypted. + */ + public static IndexInput uncompress(IndexInput input, ICompressor compressor, BytesRef compBytes, BytesRef uncompBytes) throws IOException + { + final int uncompBytesLen = input.readVInt(); + final int compBytesLength = input.readVInt(); + + assert compBytesLength > 0 : "uncompBytesLen="+uncompBytesLen+" compBytesLength="+compBytesLength; + + compBytes.bytes = ArrayUtil.grow(compBytes.bytes, compBytesLength); + + input.readBytes(compBytes.bytes, 0, compBytesLength); + + if (uncompBytes.bytes == BytesRef.EMPTY_BYTES) + { + // if EMPTY_BYTES use an exact new byte array + uncompBytes.bytes = new byte[uncompBytesLen]; + uncompBytes.length = uncompBytesLen; + } + else + { + uncompBytes.bytes = ArrayUtil.grow(uncompBytes.bytes, uncompBytesLen); + uncompBytes.length = uncompBytesLen; + } + compressor.uncompress(compBytes.bytes, 0, compBytesLength, uncompBytes.bytes, 0); + + return new ByteArrayIndexInput("", uncompBytes.bytes, 0, uncompBytesLen, input.order()); + } + + public static void compress(BytesRef uncompBytes, + IndexOutput out, ICompressor compressor) throws IOException + { + compress(uncompBytes, new BytesRef(new byte[16]), out, compressor); + } + + public static void compress(BytesRef uncompBytes, BytesRef compBytes, + IndexOutput out, ICompressor compressor) throws IOException + { + ByteBuffer input = ByteBuffer.wrap(uncompBytes.bytes, 0, uncompBytes.length); + + final int initCompLen = compressor.initialCompressedBufferLength(uncompBytes.length); + + compBytes.bytes = ArrayUtil.grow(compBytes.bytes, initCompLen); + compBytes.length = initCompLen; + + ByteBuffer output = ByteBuffer.wrap(compBytes.bytes); + + compressor.compress(input, output); + + final int compLen = output.position(); + + compBytes.length = compLen; + + assert uncompBytes.length > 0; + assert compLen > 0; + + out.writeVInt(uncompBytes.length); + out.writeVInt(compLen); + + out.writeBytes(compBytes.bytes, compLen); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/EmptyDirectory.java b/src/java/org/apache/cassandra/index/sai/disk/io/EmptyDirectory.java new file mode 100644 index 000000000000..3176fcd6cf2a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/io/EmptyDirectory.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.io; + +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.store.Lock; + +/** + * Always empty directory. Any operations to create, delete or open index files are unsupported. + */ +public final class EmptyDirectory extends Directory +{ + public static final Directory INSTANCE = new EmptyDirectory(); + + @Override + public String[] listAll() + { + return new String[0]; + } + + @Override + public void close() + { + // no-op + } + + @Override + public void deleteFile(String name) + { + throw new UnsupportedOperationException(); + } + + @Override + public long fileLength(String name) + { + throw new UnsupportedOperationException(); + } + + @Override + public IndexOutput createOutput(String name, IOContext context) + { + throw new UnsupportedOperationException(); + } + + @Override + public IndexOutput createTempOutput(String prefix, String suffix, IOContext context) + { + throw new UnsupportedOperationException(); + } + + @Override + public void sync(Collection names) + { + throw new UnsupportedOperationException(); + } + + @Override + public void syncMetaData() + { + throw new UnsupportedOperationException(); + } + + @Override + public void rename(String source, String dest) + { + throw new UnsupportedOperationException(); + } + + @Override + public IndexInput openInput(String name, IOContext context) + { + throw new UnsupportedOperationException(); + } + + @Override + public Lock obtainLock(String name) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set getPendingDeletions() throws IOException + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/FilterIndexInput.java b/src/java/org/apache/cassandra/index/sai/disk/io/FilterIndexInput.java new file mode 100644 index 000000000000..6f99d3215708 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/io/FilterIndexInput.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.io; + +import java.io.IOException; + + +public abstract class FilterIndexInput extends IndexInputReader +{ + private final IndexInputReader delegate; + + protected FilterIndexInput(IndexInputReader delegate) + { + super(delegate.input, delegate.doOnClose, 0L, delegate.input.length()); + this.delegate = delegate; + } + + public IndexInput getDelegate() + { + return delegate; + } + + @Override + public void close() + { + delegate.close(); + } + + @Override + public long getFilePointer() + { + return delegate.getFilePointer(); + } + + @Override + public void seek(long pos) + { + delegate.seek(pos); + } + + @Override + public long length() + { + return delegate.length(); + } + + @Override + public IndexInput slice(String sliceDescription, long offset, long length) + { + return delegate.slice(sliceDescription, offset, length); + } + + @Override + public byte readByte() throws IOException + { + return delegate.readByte(); + } + + @Override + public void readBytes(byte[] b, int offset, int len) throws IOException + { + delegate.readBytes(b, offset, len); + } + + @Override + public String toString() + { + return delegate.toString(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java index 2c203bdbce0d..ae94e74c7065 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java @@ -20,24 +20,40 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.function.Supplier; +import java.nio.channels.FileChannel; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.CRC32; import java.util.zip.CRC32C; import java.util.zip.Checksum; import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.github.jbellis.jvector.disk.BufferedRandomAccessWriter; +import net.nicoulaj.compilecommand.annotations.DontInline; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.io.compress.BufferType; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.io.util.SequentialWriter; import org.apache.cassandra.io.util.SequentialWriterOption; import org.apache.lucene.store.ChecksumIndexInput; -import org.apache.lucene.store.IndexInput; +import org.apache.lucene.codecs.CodecUtil; public class IndexFileUtils { + protected static final Logger logger = LoggerFactory.getLogger(IndexFileUtils.class); + @VisibleForTesting public static final SequentialWriterOption DEFAULT_WRITER_OPTION = SequentialWriterOption.newBuilder() .trickleFsync(DatabaseDescriptor.getTrickleFsync()) @@ -46,51 +62,84 @@ public class IndexFileUtils .finishOnClose(true) .build(); - public static final IndexFileUtils instance = new IndexFileUtils(DEFAULT_WRITER_OPTION); + private static final IndexFileUtils instance = new IndexFileUtils(DEFAULT_WRITER_OPTION); private static final Supplier CHECKSUM_FACTORY = CRC32C::new; + private static final Supplier LEGACY_CHECKSUM_FACTORY = CRC32::new; + private static IndexFileUtils overrideInstance = null; private final SequentialWriterOption writerOption; + public static synchronized void setOverrideInstance(IndexFileUtils overrideInstance) + { + IndexFileUtils.overrideInstance = overrideInstance; + } + + public static IndexFileUtils instance() + { + if (overrideInstance == null) + return instance; + else + return overrideInstance; + } + + /** + * Remembers checksums of files so we don't have to recompute them from the beginning of the file whenever appending + * to a file. Keeps checksums with respective file lengths and footer checksums so we can detect file changes + * that don't go through this code, and we can evict stale entries. + */ + private static final Cache> checksumCache = Caffeine.newBuilder() + .maximumSize(4096) + .build(); + @VisibleForTesting protected IndexFileUtils(SequentialWriterOption writerOption) { this.writerOption = writerOption; } - public IndexOutputWriter openOutput(File file) + public IndexOutputWriter openOutput(File file, ByteOrder order, boolean append, Version version) throws IOException { assert writerOption.finishOnClose() : "IndexOutputWriter relies on close() to sync with disk."; - - return new IndexOutputWriter(new ChecksummingWriter(file, writerOption)); + var checksumWriter = new IncrementalChecksumSequentialWriter(file, writerOption, version, append); + return new IndexOutputWriter(checksumWriter, order, version); } - public IndexOutputWriter openOutput(File file, boolean append) throws IOException + public BufferedRandomAccessWriter openRandomAccessOutput(File file, boolean append) throws IOException { assert writerOption.finishOnClose() : "IndexOutputWriter relies on close() to sync with disk."; - IndexOutputWriter indexOutputWriter = new IndexOutputWriter(new ChecksummingWriter(file, writerOption)); + var out = new BufferedRandomAccessWriter(file.toPath()); if (append) - indexOutputWriter.skipBytes(file.length()); + out.seek(file.length()); - return indexOutputWriter; + return out; } - public IndexInput openInput(FileHandle handle) + public IndexInputReader openInput(FileHandle handle) { return IndexInputReader.create(handle); } - public IndexInput openBlockingInput(File file) + public IndexInputReader openBlockingInput(FileHandle fileHandle) { - FileHandle fileHandle = new FileHandle.Builder(file).complete(); - RandomAccessReader randomReader = fileHandle.createReader(); - + final RandomAccessReader randomReader = fileHandle.createReader(); return IndexInputReader.create(randomReader, fileHandle::close); } - public static ChecksumIndexInput getBufferedChecksumIndexInput(IndexInput indexInput) + public static ChecksumIndexInput getBufferedChecksumIndexInput(org.apache.lucene.store.IndexInput indexInput, Version version) + { + return new BufferedChecksumIndexInput(indexInput, getChecksumFactory(version).get()); + } + + public static Supplier getChecksumFactory(Version version) + { + // TODO Use the version to determine which checksum algorithm to use + return LEGACY_CHECKSUM_FACTORY; + } + + public interface ChecksumWriter { - return new BufferedChecksumIndexInput(indexInput, CHECKSUM_FACTORY.get()); + long getChecksum(); } /** @@ -99,27 +148,372 @@ public static ChecksumIndexInput getBufferedChecksumIndexInput(IndexInput indexI * with {@link IndexOutputWriter}. This, in turn, is used in conjunction with {@link BufferedChecksumIndexInput} * to verify the checksum of the data read from the file, so they must share the same checksum algorithm. */ - static class ChecksummingWriter extends SequentialWriter + static class IncrementalChecksumSequentialWriter extends SequentialWriter implements ChecksumWriter { - private final Checksum checksum = CHECKSUM_FACTORY.get(); + private final Version version; + /** Protects the checksum so only one Writer can update it */ + private Guard checksumGuard; + /** Current (running) checksum from the beginning of the file till the current position */ + private FileChecksum checksum; + /** Remembers the checksum after closing this writer */ + private long finalChecksum; - ChecksummingWriter(File file, SequentialWriterOption writerOption) + IncrementalChecksumSequentialWriter(File file, SequentialWriterOption writerOption, Version version, boolean append) throws IOException { super(file, writerOption); + this.version = version; + + while (checksum == null) + { + checksumGuard = checksumCache.get(file.path(), s -> new Guard<>(new FileChecksum(version))); + checksum = checksumGuard.tryLock(); + + if (checksum == null) + { + // If we're here this means some other Writer did not unlock the checksum object, + // so we can't use the same checksum safely, as there is a slight probability it + // is in active use. This is not necessarily a bug - e.g. it is also possible + // the other writer was interrupted and the client code simply forgot to close() it. + // Therefore, we'll get a new one, just to be safe. + logger.warn("File {} still in use by another instance of {}", file, this.getClass().getSimpleName()); + checksumCache.invalidate(file.path()); + } + } + + if (append) + { + var fileLength = file.length(); + skipBytes(fileLength); + + // It is possible we didn't get a good checksum. + // We could have gotten a zero checksum because the cache has a limited size, + // or the file could have been changed in the meantime by another process, in which case the + // footer checksum would not match. + // However, we don't recalculate the checksum always, because it is very costly: + var footerChecksum = calculateFooterChecksum(); + if (checksum.fileLength != fileLength || checksum.footerChecksum != footerChecksum) + { + logger.warn("Length and checksum ({}, {}) of file {} does not match the length and checksum ({}, {}) in the checksum cache. " + + "Recomputing the checksum from the beginning.", + fileLength, footerChecksum, file, checksum.fileLength, checksum.footerChecksum); + recalculateFileChecksum(); + } + } + else + { + // We might be overwriting an existing file + checksum.reset(); + } } - public long getChecksum() throws IOException + /** + * Recalculates checksum for the file. + *

    + * Useful when the file is opened for append and checksum will need to account for the existing data. + * e.g. if the file opened for append is a new file, then checksum start at 0 and goes from there with the writes. + * If the file opened for append is an existing file, without recalculating the checksum will start at 0 + * and only account for appended data. Checksum validation will compare it to the checksum of the whole file and fail. + * Hence, for the existing files this method should be called to recalculate the checksum. + * + * @throws IOException if file read failed. + */ + public void recalculateFileChecksum() throws IOException { - flush(); - return checksum.getValue(); + checksum.reset(); + if (!file.exists()) + return; + + try(FileChannel ch = StorageProvider.instance.writeTimeReadFileChannelFor(file)) + { + if (ch.size() == 0) + return; + + final ByteBuffer buf = ByteBuffer.allocateDirect(65536); + int b = ch.read(buf); + while (b > 0) + { + buf.flip(); + checksum.update(buf); + buf.clear(); + b = ch.read(buf); + } + } + + assert checksum.fileLength == position(); + } + + /** + * Returns the checksum of the footer of the index file. + * Those bytes contain the checksum for the whole file, so + * this checksum can be used to verify the integrity of the whole file. + * Note that this is not the same as checksum written in the file footer; + * this is done this way so we don't have to decode the footer here. + */ + public long calculateFooterChecksum() throws IOException + { + Checksum footerChecksum = getChecksumFactory(version).get(); + try (FileChannel ch = StorageProvider.instance.writeTimeReadFileChannelFor(file)) + { + ch.position(Math.max(0, file.length() - CodecUtil.footerLength())); + final ByteBuffer buf = ByteBuffer.allocate(CodecUtil.footerLength()); + int b = ch.read(buf); + while (b > 0) + { + buf.flip(); + footerChecksum.update(buf); + buf.clear(); + b = ch.read(buf); + } + } + return footerChecksum.getValue(); } @Override - protected void flushData() + public void write(ByteBuffer src) throws IOException { - ByteBuffer toAppend = buffer.duplicate().flip(); - super.flushData(); - checksum.update(toAppend); + ByteBuffer shallowCopy = src.slice().order(src.order()); + super.write(src); + checksum.update(shallowCopy); + } + + @Override + public void writeBoolean(boolean v) throws IOException + { + super.writeBoolean(v); + checksum.update(v ? 1 : 0); + } + + @Override + public void writeByte(int b) throws IOException + { + super.writeByte(b); + checksum.update(b); + } + + // Do not override write(byte[] b) to avoid double-counting bytes in the checksum. + // It just calls this method anyway. + @Override + public void write(byte[] b, int off, int len) throws IOException + { + super.write(b, off, len); + checksum.update(b, off, len); + } + + @Override + public void writeChar(int v) throws IOException + { + super.writeChar(v); + addTochecksum(v, 2); + } + + @Override + public void writeInt(int v) throws IOException + { + super.writeInt(v); + addTochecksum(v, 4); + } + + @Override + public void writeLong(long v) throws IOException + { + super.writeLong(v); + addTochecksum(v, 8); + } + + public long getChecksum() + { + return checksum != null ? checksum.getValue() : finalChecksum; + } + + // To avoid double-counting bytes in the checksum. + // Same as super's but calls super.writeByte + @DontInline + @Override + protected void writeSlow(long bytes, int count) throws IOException + { + int origCount = count; + if (ByteOrder.BIG_ENDIAN == buffer.order()) + while (count > 0) super.writeByte((int) (bytes >>> (8 * --count))); + else + while (count > 0) super.writeByte((int) (bytes >>> (8 * (origCount - count--)))); + } + + @Override + public void writeMostSignificantBytes(long register, int bytes) throws IOException + { + super.writeMostSignificantBytes(register, bytes); + addMsbToChecksum(register, bytes); + } + + /** + * Based on {@link DataOutputPlus#writeMostSignificantBytes(long, int)} + */ + private void addMsbToChecksum(long register, int bytes) + { + long msbValue = 0; + switch (bytes) + { + case 0: + break; + case 1: + msbValue = (int) (register >>> 56); + break; + case 2: + msbValue = (int) (register >> 48); + break; + case 3: + msbValue = (int) (register >> 40); + break; + case 4: + msbValue = (int) (register >> 32); + break; + case 5: + msbValue = (int) (register >> 24); + break; + case 6: + msbValue = (int) (register >> 16); + break; + case 7: + msbValue = (int) (register >> 8); + break; + case 8: + msbValue = register; + break; + default: + throw new IllegalArgumentException(); + } + addTochecksum(msbValue, bytes); + } + + private void addTochecksum(long bytes, int count) + { + int origCount = count; + if (ByteOrder.BIG_ENDIAN == buffer.order()) + while (count > 0) checksum.update((int) (bytes >>> (8 * --count))); + else + while (count > 0) checksum.update((int) (bytes >>> (8 * (origCount - count--)))); + } + + @Override + public void truncate(long toSize) + { + if (toSize == 0) + { + checksum.reset(); + super.truncate(toSize); + } + + // this would invalidate the checksum + throw new UnsupportedOperationException("truncate to non zero length not supported"); + } + + @Override + public void close() + { + try + { + super.close(); + } + finally + { + try + { + // Copy the checksum value to a field in order to make the checksum value available past close(). + // Release the FileChecksum object so it can be used by another writer. + finalChecksum = checksum.getValue(); + checksum.footerChecksum = calculateFooterChecksum(); + } + catch (IOException e) + { + // mark the checksum as unusable + // even though it stays in the cache, it won't ever match on the file length + checksum.fileLength = -1; + } + finally + { + checksumGuard.release(); + checksum = null; + } + } + } + } + + /** + * A lightweight helper to guard against concurrent access to an object. + * Used when we know object should be owned by one owner at a time. + */ + static class Guard + { + final T inner; + final AtomicBoolean locked = new AtomicBoolean(false); + + public Guard(T inner) + { + this.inner = inner; + } + + /** + * Locks the object and returns it. + * If it was already locked, return null. + * @return protected object + */ + public T tryLock() + { + return locked.compareAndSet(false, true) + ? inner + : null; + } + + public void release() + { + locked.set(false); } } + + /** + * Computes the checksum from the begining of a file. + * Keeps track of the number of bytes processed. + * We need the number of bytes so we can invalidate the checksum if the file was appended or truncated. + */ + static class FileChecksum + { + long fileLength = 0; + long footerChecksum = 0; + final Checksum fileChecksum; + + public FileChecksum(Version version) + { + fileChecksum = getChecksumFactory(version).get(); + } + + public void reset() + { + fileLength = 0; + fileChecksum.reset(); + } + + public void update(int b) + { + fileLength += 1; + fileChecksum.update(b); + } + + public void update(byte[] b, int off, int len) + { + fileLength += len; + fileChecksum.update(b, off, len); + } + + public void update(ByteBuffer b) + { + fileLength += b.remaining(); + fileChecksum.update(b); + } + + public long getValue() + { + return fileChecksum.getValue(); + } + } + } diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInput.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInput.java new file mode 100644 index 000000000000..414c2dfb9219 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInput.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * A subclass of {@link org.apache.lucene.store.IndexInput} that provides access to the byte order of the underlying data. + */ +public abstract class IndexInput extends org.apache.lucene.store.IndexInput +{ + protected final ByteOrder order; + + protected IndexInput(String resourceDescription, ByteOrder order) + { + super(resourceDescription); + this.order = order; + } + + public ByteOrder order() + { + return order; + } + + @Override + public abstract IndexInput slice(String sliceDescription, long offset, long length) throws IOException; + + + public final ByteBuffer readBytes() throws IOException + { + int len = readVInt(); + byte[] bytes = new byte[len]; + readBytes(bytes, 0, len); + return ByteBuffer.wrap(bytes); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java index b97c727c6abf..cad3d10bff43 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java @@ -20,48 +20,75 @@ import java.io.IOException; +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.io.compress.CorruptBlockException; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexInput; /** * 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 * (via DataInput) with methods that enforce LittleEndian-ness. */ - private final RandomAccessReader input; - private final Runnable doOnClose; + protected final RandomAccessReader input; + protected final Runnable doOnClose; + + /** Absolute offset in the underlying file that this input's position 0 refers to. */ + private final long offset; - private IndexInputReader(RandomAccessReader input, Runnable doOnClose) + /** Bounded length of this input, in bytes. */ + private final long length; + + protected IndexInputReader(RandomAccessReader input, Runnable doOnClose, long offset, long length) { - super(input.getPath()); + super(input.getFile().toString(), input.order()); 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()); } + @SuppressWarnings("resource") public static IndexInputReader create(FileHandle handle) { RandomAccessReader reader = handle.createReader(); - return new IndexInputReader(reader, () -> {}); + return new IndexInputReader(reader, reader::close, 0L, reader.length()); } @Override @@ -73,43 +100,106 @@ public byte readByte() throws IOException @Override public void readBytes(byte[] bytes, int off, int len) throws IOException { - input.readFully(bytes, off, len); + try + { + input.readFully(bytes, off, len); + } + catch (CorruptBlockException ex) + { + throw new CorruptIndexException(input.getFile().toString(), "Corrupted block", ex); + } } + /** + * Using {@link RandomAccessReader#readShort()} directly is faster than {@link DataInput#readShort()} which calls + * {@link DataInput#readByte()} one by one + */ @Override - public void close() + public short readShort() throws IOException + { + try + { + return input.readShort(); + } + catch (CorruptBlockException ex) + { + throw new CorruptIndexException(input.getFile().toString(), "Corrupted block", ex); + } + } + + /** + * Using {@link RandomAccessReader#readInt()} directly is faster than {@link DataInput#readInt()} which + * calls {@link DataInput#readByte()} one by one + */ + @Override + public int readInt() throws IOException + { + try + { + return input.readInt(); + } + catch (CorruptBlockException ex) + { + throw new CorruptIndexException(input.getFile().toString(), "Corrupted block", ex); + } + } + + /** + * Using {@link RandomAccessReader#readLong()} directly is faster than {@link DataInput#readLong()} which + * calls {@link DataInput#readByte()} one by one + */ + @Override + public long readLong() throws IOException { try { - input.close(); + return input.readLong(); } - finally + catch (CorruptBlockException ex) { - doOnClose.run(); + throw new CorruptIndexException(input.getFile().toString(), "Corrupted block", ex); } } + @Override + public void close() + { + doOnClose.run(); + } + @Override public long getFilePointer() { - return input.getFilePointer(); + return input.getFilePointer() - offset; } @Override public void seek(long position) { - input.seek(position); + if (position > length) + throw new IllegalArgumentException("Cannot seek to position " + position + " past length of " + length); + + input.seek(offset + position); } @Override public long length() { - return input.length(); + return length; } @Override public IndexInput slice(String sliceDescription, long offset, long length) { - throw new UnsupportedOperationException("Slice operations are not supported"); + if (offset < 0 || length < 0 || offset + length > this.length) + throw new IllegalArgumentException("Invalid slice: offset=" + offset + ", length=" + length + ", parent length=" + this.length + " for " + sliceDescription); + + // Slices share the underlying reader with their parent; the no-op close keeps the parent's lifecycle intact. + IndexInputReader slice = new IndexInputReader(input, NO_OP_ON_CLOSE, this.offset + offset, length); + + // Seek to the beginning of the slice... + slice.seek(0); + + return slice; } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutput.java new file mode 100644 index 000000000000..ee4d48accabb --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutput.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * A subclass of {@link org.apache.lucene.store.IndexOutput} that provides access to the byte order of the underlying data. + * This is used to select output implementations compatible with varying versions of Lucene. + */ +public abstract class IndexOutput extends org.apache.lucene.store.IndexOutput +{ + protected final ByteOrder order; + protected final Version version; + + protected IndexOutput(String resourceDescription, String name, ByteOrder order, Version version) + { + super(resourceDescription, name); + this.order = order; + this.version = version; + } + + public ByteOrder order() + { + return order; + } + + public Version version() + { + return version; + } + + public final void writeBytes(ByteBuffer buf) throws IOException + { + byte[] bytes = ByteBufferUtil.getArray(buf); + writeVInt(bytes.length); + writeBytes(bytes, 0, bytes.length); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java index 4e801011da2c..5d5bd110de8a 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java @@ -18,36 +18,27 @@ package org.apache.cassandra.index.sai.disk.io; import java.io.IOException; -import javax.annotation.concurrent.NotThreadSafe; +import java.lang.invoke.MethodHandles; +import java.nio.ByteOrder; import com.google.common.base.MoreObjects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.SequentialWriter; -import org.apache.lucene.store.IndexOutput; -/** - * This is a wrapper over a Cassandra {@link SequentialWriter} that provides a Lucene {@link IndexOutput} - * interface for the Lucene index writers. - */ -@NotThreadSafe public class IndexOutputWriter extends IndexOutput { - private static final Logger logger = LoggerFactory.getLogger(IndexOutputWriter.class); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - /** - * the byte order of `out`'s native writeX operations doesn't matter, - * because we only use `write(byte[])` and `writeByte` methods. IndexOutput calls these - * (via DataOutput) with methods that enforce LittleEndian-ness. - */ private final SequentialWriter out; private boolean closed; - public IndexOutputWriter(SequentialWriter out) + public IndexOutputWriter(SequentialWriter out, ByteOrder order, Version version) { - super(out.getPath(), out.getPath()); + super(out.getFile().toString(), out.getFile().name(), order, version); this.out = out; } @@ -58,13 +49,13 @@ public void skipBytes(long length) throws IOException public File getFile() { - return new File(out.getPath()); + return out.getFile(); } @Override - public long getChecksum() throws IOException + public long getChecksum() { - return ((IndexFileUtils.ChecksummingWriter)out).getChecksum(); + return ((IndexFileUtils.ChecksumWriter)out).getChecksum(); } @Override @@ -79,6 +70,56 @@ public void writeBytes(byte[] bytes, int offset, int len) throws IOException out.write(bytes, offset, len); } + @Override + public void writeInt(int v) throws IOException + { + if (order == ByteOrder.BIG_ENDIAN) + { + writeByte((byte) (v >>> 24)); + writeByte((byte) (v >>> 16)); + writeByte((byte) (v >>> 8)); + writeByte((byte) v); + } + else + { + super.writeInt(v); + } + } + + @Override + public void writeShort(short v) throws IOException + { + if (order == ByteOrder.BIG_ENDIAN) + { + writeByte((byte)(v >>> 8)); + writeByte((byte) v); + } + else + { + super.writeShort(v); + } + } + + @Override + public void writeLong(long v) throws IOException + { + if (order == ByteOrder.BIG_ENDIAN) + { + writeByte((byte)(v >>> 56)); + writeByte((byte)(v >>> 48)); + writeByte((byte)(v >>> 40)); + writeByte((byte)(v >>> 32)); + writeByte((byte)(v >>> 24)); + writeByte((byte)(v >>> 16)); + writeByte((byte)(v >>> 8)); + writeByte((byte) v); + } + else + { + super.writeLong(v); + } + } + @Override public void writeByte(byte b) throws IOException { @@ -86,7 +127,7 @@ public void writeByte(byte b) throws IOException } @Override - public void close() + public void close() throws IOException { // IndexOutput#close contract allows any output to be closed multiple times, // and Lucene does it in few places. SequentialWriter can be closed once. @@ -106,21 +147,15 @@ public void close() @Override public String toString() { - String checksum; - try { - checksum = String.valueOf(getChecksum()); - } catch (IOException e) { - checksum = "unknown due to I/O error: " + e; - } return MoreObjects.toStringHelper(this) - .add("path", out.getPath()) + .add("path", out.getFile()) .add("bytesWritten", getFilePointer()) - .add("crc", checksum) + .add("crc", getChecksum()) .toString(); } /** - * Returns {@link SequentialWriter} associated with this writer. Convenient when interacting with Cassandra codebase to + * Returns {@link SequentialWriter} associated with this writer. Convenient when interacting with DSE-DB codebase to * write files to disk. Note that all bytes written to the returned writer will still contribute to the checksum. * * @return {@link SequentialWriter} associated with this writer diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ByteArrayIndexInput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ByteArrayIndexInput.java new file mode 100644 index 000000000000..b24b28da29fd --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ByteArrayIndexInput.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.EOFException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteOrder; +import java.util.Locale; + +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.lucene.store.RandomAccessInput; + +/** + * A {@link IndexInput} backed by a byte array. + * + * ByteBufferIndexInput is nominally the blessed replacement for this, but + * it's a pretty different API. + * + * @lucene.experimental + */ +public final class ByteArrayIndexInput extends IndexInput implements RandomAccessInput +{ + private byte[] bytes; + + private final int offset; + private final int length; + private final boolean isBigEndian; + + private int pos; + + public ByteArrayIndexInput(String description, byte[] bytes, ByteOrder order) { + this(description, bytes, 0, bytes.length, order); + } + + public ByteArrayIndexInput(String description, byte[] bytes, int offs, int length, ByteOrder order) { + super(description, order); + this.offset = offs; + this.bytes = bytes; + this.length = length; + this.pos = offs; + this.isBigEndian = order == ByteOrder.BIG_ENDIAN; + } + + public long getFilePointer() { + return pos - offset; + } + + public void seek(long pos) throws EOFException { + int newPos = Math.toIntExact(pos + offset); + try { + if (pos < 0 || pos > length) { + throw new EOFException(); + } + } finally { + this.pos = newPos; + } + } + + @Override + public long length() { + return length; + } + + @Override + public short readShort() { + var b1 = bytes[pos++] & 0xFF; + var b2 = bytes[pos++] & 0xFF; + return isBigEndian + ? (short) (b1 << 8 | b2) + : (short) (b2 << 8 | b1); + } + + @Override + public int readInt() { + var b1 = bytes[pos++] & 0xFF; + var b2 = bytes[pos++] & 0xFF; + var b3 = bytes[pos++] & 0xFF; + var b4 = bytes[pos++] & 0xFF; + + return isBigEndian + ? b1 << 24 | b2 << 16 | b3 << 8 | b4 + : b4 << 24 | b3 << 16 | b2 << 8 | b1; + } + + @Override + public long readLong() + { + int i1 = readInt(); + int i2 = readInt(); + return isBigEndian + ? (long) i1 << 32 | i2 & 0xFFFFFFFFL + : (long) i2 << 32 | i1 & 0xFFFFFFFFL; + } + + // NOTE: AIOOBE not EOF if you read too much + @Override + public byte readByte() { + return bytes[pos++]; + } + + // NOTE: AIOOBE not EOF if you read too much + @Override + public void readBytes(byte[] b, int offset, int len) { + System.arraycopy(bytes, pos, b, offset, len); + pos += len; + } + + @Override + public void close() { + bytes = null; + } + + @Override + public IndexInput clone() { + ByteArrayIndexInput slice = slice("(cloned)" + toString(), 0, length()); + try { + slice.seek(getFilePointer()); + } catch (EOFException e) { + throw new UncheckedIOException(e); + } + return slice; + } + + public ByteArrayIndexInput slice(String sliceDescription, long offset, long length) { + if (offset < 0 || length < 0 || offset + length > this.length) { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "slice(offset=%s, length=%s) is out of bounds: %s", + offset, length, this)); + } + + return new ByteArrayIndexInput(sliceDescription, + this.bytes, + Math.toIntExact(this.offset + offset), + Math.toIntExact(length), + isBigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + } + + @Override + public byte readByte(long pos) throws IOException { + return bytes[Math.toIntExact(offset + pos)]; + } + + @Override + public short readShort(long pos) throws IOException { + int i = Math.toIntExact(offset + pos); + var b1 = bytes[i] & 0xFF; + var b2 = bytes[i + 1] & 0xFF; + return isBigEndian + ? (short) (b1 << 8 | b2) + : (short) (b2 << 8 | b1); + } + + @Override + public int readInt(long pos) throws IOException { + int i = Math.toIntExact(offset + pos); + var b1 = bytes[i] & 0xFF; + var b2 = bytes[i + 1] & 0xFF; + var b3 = bytes[i + 2] & 0xFF; + var b4 = bytes[i + 3] & 0xFF; + return isBigEndian + ? b1 << 24 | b2 << 16 | b3 << 8 | b4 + : b4 << 24 | b3 << 16 | b2 << 8 | b1; + } + + @Override + public long readLong(long pos) throws IOException { + int i = Math.toIntExact(offset + pos); + int b1 = readInt(i); + int b2 = readInt(i + 4); + return isBigEndian + ? (long) b1 << 32 | b2 & 0xFFFFFFFFL + : (long) b2 << 32 | b1 & 0xFFFFFFFFL; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ByteBuffersDataOutputAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ByteBuffersDataOutputAdapter.java new file mode 100644 index 000000000000..948b60106606 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ByteBuffersDataOutputAdapter.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import org.apache.lucene.store.DataOutput; + +/** + * Minimal wrapper around Lucene's ByteBufferDataOutput/LegacyByteBufferDataOutput, which don't share an interface. + * We need this to call ByteBufferDataOutput-specific methods at callsites that could contain either type. + */ +public abstract class ByteBuffersDataOutputAdapter extends DataOutput +{ + public abstract void reset(); + public abstract long size(); + public abstract byte[] toArrayCopy(); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/DirectWriterAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/DirectWriterAdapter.java new file mode 100644 index 000000000000..df31f3c0ca83 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/DirectWriterAdapter.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; + +/** + * Minimal wrapper around Lucene's DirectWriter/LegacyDirectWriter, which don't share an interface. + * We need this to write out AA version indexes using LegacyDirectWriter. + */ +public interface DirectWriterAdapter +{ + public void add(long l) throws IOException; + public void finish() throws IOException; +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/EndiannessReverserChecksumIndexInput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/EndiannessReverserChecksumIndexInput.java new file mode 100644 index 000000000000..b23135eb3fa2 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/EndiannessReverserChecksumIndexInput.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; +import org.apache.lucene.store.ChecksumIndexInput; +import org.apache.lucene.store.IndexInput; + +/** + * A {@link ChecksumIndexInput} wrapper that changes the endianness of the provided index output. + */ +public final class EndiannessReverserChecksumIndexInput extends ChecksumIndexInput { + + private final ChecksumIndexInput in; + + public EndiannessReverserChecksumIndexInput(IndexInput in, Version version) { + super("Endianness reverser Checksum Index Input wrapper"); + this.in = IndexFileUtils.getBufferedChecksumIndexInput(in, version); + } + + @Override + public long getChecksum() throws IOException { + return in.getChecksum(); + } + + @Override + public byte readByte() throws IOException { + return in.readByte(); + } + + @Override + public void readBytes(byte[] b, int offset, int len) throws IOException { + in.readBytes(b, offset, len); + } + + @Override + public short readShort() throws IOException { + return Short.reverseBytes(in.readShort()); + } + + @Override + public int readInt() throws IOException { + return Integer.reverseBytes(in.readInt()); + } + + @Override + public long readLong() throws IOException { + return Long.reverseBytes(in.readLong()); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public long getFilePointer() { + return in.getFilePointer(); + } + + @Override + public long length() { + return in.length(); + } + + @Override + public IndexInput slice(String sliceDescription, long offset, long length) throws IOException { + throw new UnsupportedOperationException("This operation is not yet supported"); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataInput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataInput.java new file mode 100644 index 000000000000..d0ad53cd623a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataInput.java @@ -0,0 +1,487 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.lucene.store.DataInput; +import org.apache.lucene.store.RandomAccessInput; +import org.apache.lucene.util.Accountable; +import org.apache.lucene.util.RamUsageEstimator; + +/** + * A {@link DataInput} implementing {@link RandomAccessInput} and reading data from a + * list of {@link ByteBuffer}s. This uses the big-endian byte ordering of Lucene 7.5. + * Note that this participates in the type hierarchy of the modern Lucene depencency, + * so DataInput methods that assume little-endianness must be overridden. + * This file was imported from the Apache Lucene project at commit b5bf70b7e32d7ddd9742cc821d471c5fabd4e3df, + * tagged as releases/lucene-solr/7.5.0. The following modifications have been made to the original file: + *

      + *
    • Renamed from ByteBuffersDataInput to LegacyByteBuffersDataInput.
    • + *
    • Return types modified accordingly.
    • + *
    • skipBytes was added.
    • + *
    • readShort/readInt/readLong implemented as big-endian, since superclass implementations are little-endian.
    • + *
    • explicitly override readFloats/readInts/readLongs in case DataInput implementation changes.
    • + *
    + */ +public final class LegacyByteBuffersDataInput extends DataInput implements Accountable, RandomAccessInput +{ + private final ByteBuffer[] blocks; + private final int blockBits; + private final int blockMask; + private final long size; + private final long offset; + + private long pos; + + /** + * Read data from a set of contiguous buffers. All data buffers except for the last one + * must have an identical remaining number of bytes in the buffer (that is a power of two). The last + * buffer can be of an arbitrary remaining length. + */ + public LegacyByteBuffersDataInput(List buffers) + { + ensureAssumptions(buffers); + + this.blocks = buffers.stream().map(buf -> buf.asReadOnlyBuffer()).toArray(ByteBuffer[]::new); + + if (blocks.length == 1) + { + this.blockBits = 32; + this.blockMask = ~0; + } + else + { + final int blockBytes = determineBlockPage(buffers); + this.blockBits = Integer.numberOfTrailingZeros(blockBytes); + this.blockMask = (1 << blockBits) - 1; + } + + this.size = Arrays.stream(blocks).mapToLong(block -> block.remaining()).sum(); + + // The initial "position" of this stream is shifted by the position of the first block. + this.offset = blocks[0].position(); + this.pos = offset; + } + + public long size() + { + return size; + } + + @Override + public long ramBytesUsed() + { + // Return a rough estimation for allocated blocks. Note that we do not make + // any special distinction for what the type of buffer is (direct vs. heap-based). + return RamUsageEstimator.NUM_BYTES_OBJECT_REF * blocks.length + + Arrays.stream(blocks).mapToLong(buf -> buf.capacity()).sum(); + } + + @Override + public byte readByte() throws EOFException + { + try + { + ByteBuffer block = blocks[blockIndex(pos)]; + byte v = block.get(blockOffset(pos)); + pos++; + return v; + } + catch (IndexOutOfBoundsException e) + { + if (pos >= size()) + { + throw new EOFException(); + } + else + { + throw e; // Something is wrong. + } + } + } + + /** + * Reads a specified number of floats into an array at the specified offset. + * + * @param floats the array to read bytes into + * @param offset the offset in the array to start storing floats + * @param len the number of floats to read + */ + @Override + public void readFloats(float[] floats, int offset, int len) throws IOException + { + Objects.checkFromIndexSize(offset, len, floats.length); + for (int i = 0; i < len; i++) + { + floats[offset + i] = Float.intBitsToFloat(readInt()); + } + } + + /** + * Read a specified number of longs. + * + * @lucene.experimental + */ + public void readLongs(long[] dst, int offset, int length) throws IOException + { + Objects.checkFromIndexSize(offset, length, dst.length); + for (int i = 0; i < length; ++i) + { + dst[offset + i] = readLong(); + } + } + + /** + * Reads a specified number of ints into an array at the specified offset. + * + * @param dst the array to read bytes into + * @param offset the offset in the array to start storing ints + * @param length the number of ints to read + */ + public void readInts(int[] dst, int offset, int length) throws IOException + { + Objects.checkFromIndexSize(offset, length, dst.length); + for (int i = 0; i < length; ++i) + { + dst[offset + i] = readInt(); + } + } + + /** + * Reads exactly {@code len} bytes into the given buffer. The buffer must have + * enough remaining limit. + *

    + * If there are fewer than {@code len} bytes in the input, {@link EOFException} + * is thrown. + */ + public void readBytes(ByteBuffer buffer, int len) throws EOFException + { + try + { + while (len > 0) + { + ByteBuffer block = blocks[blockIndex(pos)].duplicate(); + int blockOffset = blockOffset(pos); + block.position(blockOffset); + int chunk = Math.min(len, block.remaining()); + if (chunk == 0) + { + throw new EOFException(); + } + + // Update pos early on for EOF detection on output buffer, then try to get buffer content. + pos += chunk; + block.limit(blockOffset + chunk); + buffer.put(block); + + len -= chunk; + } + } + catch (BufferUnderflowException | ArrayIndexOutOfBoundsException e) + { + if (pos >= size()) + { + throw new EOFException(); + } + else + { + throw e; // Something is wrong. + } + } + } + + @Override + public void readBytes(byte[] arr, int off, int len) throws EOFException + { + try + { + while (len > 0) + { + ByteBuffer block = blocks[blockIndex(pos)].duplicate(); + block.position(blockOffset(pos)); + int chunk = Math.min(len, block.remaining()); + if (chunk == 0) + { + throw new EOFException(); + } + + // Update pos early on for EOF detection, then try to get buffer content. + pos += chunk; + block.get(arr, off, chunk); + + len -= chunk; + off += chunk; + } + } + catch (BufferUnderflowException | ArrayIndexOutOfBoundsException e) + { + if (pos >= size()) + { + throw new EOFException(); + } + else + { + throw e; // Something is wrong. + } + } + } + + @Override + public short readShort() throws IOException + { + return (short) (((readByte() & 0xFF) << 8) | (readByte() & 0xFF)); + } + + @Override + public int readInt() throws IOException + { + return ((readByte() & 0xFF) << 24) | ((readByte() & 0xFF) << 16) + | ((readByte() & 0xFF) << 8) | (readByte() & 0xFF); + } + + @Override + public long readLong() throws IOException + { + return (((long) readInt()) << 32) | (readInt() & 0xFFFFFFFFL); + } + + @Override + public void skipBytes(long l) throws IOException + { + if (l < 0) + { + throw new IllegalArgumentException("l must be >= 0, got " + l); + } + if (l > size() - pos) + { + throw new EOFException(); + } + pos += l; + } + + @Override + public byte readByte(long pos) + { + pos += offset; + return blocks[blockIndex(pos)].get(blockOffset(pos)); + } + + @Override + public short readShort(long pos) + { + long absPos = offset + pos; + int blockOffset = blockOffset(absPos); + if (blockOffset + Short.BYTES <= blockMask) + { + return blocks[blockIndex(absPos)].getShort(blockOffset); + } + else + { + return (short) ((readByte(pos) & 0xFF) << 8 | + (readByte(pos + 1) & 0xFF)); + } + } + + @Override + public int readInt(long pos) + { + long absPos = offset + pos; + int blockOffset = blockOffset(absPos); + if (blockOffset + Integer.BYTES <= blockMask) + { + return blocks[blockIndex(absPos)].getInt(blockOffset); + } + else + { + return ((readByte(pos)) << 24 | + (readByte(pos + 1) & 0xFF) << 16 | + (readByte(pos + 2) & 0xFF) << 8 | + (readByte(pos + 3) & 0xFF)); + } + } + + @Override + public long readLong(long pos) + { + long absPos = offset + pos; + int blockOffset = blockOffset(absPos); + if (blockOffset + Long.BYTES <= blockMask) + { + return blocks[blockIndex(absPos)].getLong(blockOffset); + } + else + { + return (((long) readInt(pos)) << 32) | (readInt(pos + 4) & 0xFFFFFFFFL); + } + } + + public long position() + { + return pos - offset; + } + + public void seek(long position) throws EOFException + { + this.pos = position + offset; + if (position > size()) + { + this.pos = size(); + throw new EOFException(); + } + } + + public LegacyByteBuffersDataInput slice(long offset, long length) + { + if (offset < 0 || length < 0 || offset + length > this.size) + { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "slice(offset=%s, length=%s) is out of bounds: %s", + offset, length, this)); + } + + return new LegacyByteBuffersDataInput(sliceBufferList(Arrays.asList(this.blocks), offset, length)); + } + + @Override + public String toString() + { + return String.format(Locale.ROOT, + "%,d bytes, block size: %,d, blocks: %,d, position: %,d%s", + size(), + blockSize(), + blocks.length, + position(), + offset == 0 ? "" : String.format(Locale.ROOT, " [offset: %,d]", offset)); + } + + private final int blockIndex(long pos) + { + return Math.toIntExact(pos >> blockBits); + } + + private final int blockOffset(long pos) + { + return (int) pos & blockMask; + } + + private int blockSize() + { + return 1 << blockBits; + } + + private static final boolean isPowerOfTwo(int v) + { + return (v & (v - 1)) == 0; + } + + private static void ensureAssumptions(List buffers) + { + if (buffers.isEmpty()) + { + throw new IllegalArgumentException("Buffer list must not be empty."); + } + + if (buffers.size() == 1) + { + // Special case of just a single buffer, conditions don't apply. + } + else + { + final int blockPage = determineBlockPage(buffers); + + // First buffer decides on block page length. + if (!isPowerOfTwo(blockPage)) + { + throw new IllegalArgumentException("The first buffer must have power-of-two position() + remaining(): 0x" + + Integer.toHexString(blockPage)); + } + + // Any block from 2..last-1 should have the same page size. + for (int i = 1, last = buffers.size() - 1; i < last; i++) + { + ByteBuffer buffer = buffers.get(i); + if (buffer.position() != 0) + { + throw new IllegalArgumentException("All buffers except for the first one must have position() == 0: " + buffer); + } + if (i != last && buffer.remaining() != blockPage) + { + throw new IllegalArgumentException("Intermediate buffers must share an identical remaining() power-of-two block size: 0x" + + Integer.toHexString(blockPage)); + } + } + } + } + + static int determineBlockPage(List buffers) + { + ByteBuffer first = buffers.get(0); + final int blockPage = Math.toIntExact((long) first.position() + first.remaining()); + return blockPage; + } + + private static List sliceBufferList(List buffers, long offset, long length) + { + ensureAssumptions(buffers); + + if (buffers.size() == 1) + { + ByteBuffer cloned = buffers.get(0).asReadOnlyBuffer(); + cloned.position(Math.toIntExact(cloned.position() + offset)); + cloned.limit(Math.toIntExact(length + cloned.position())); + return Arrays.asList(cloned); + } + else + { + long absStart = buffers.get(0).position() + offset; + long absEnd = Math.toIntExact(absStart + length); + + int blockBytes = LegacyByteBuffersDataInput.determineBlockPage(buffers); + int blockBits = Integer.numberOfTrailingZeros(blockBytes); + int blockMask = (1 << blockBits) - 1; + + int endOffset = (int) absEnd & blockMask; + + ArrayList cloned = + buffers.subList(Math.toIntExact(absStart / blockBytes), + Math.toIntExact(absEnd / blockBytes + (endOffset == 0 ? 0 : 1))) + .stream() + .map(buf -> buf.asReadOnlyBuffer()) + .collect(Collectors.toCollection(ArrayList::new)); + + if (endOffset == 0) + { + cloned.add(ByteBuffer.allocate(0)); + } + + cloned.get(0).position((int) absStart & blockMask); + cloned.get(cloned.size() - 1).limit(endOffset); + return cloned; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataOutput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataOutput.java new file mode 100644 index 000000000000..eb397a412486 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataOutput.java @@ -0,0 +1,663 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; + +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.util.Accountable; +import org.apache.lucene.util.BitUtil; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.RamUsageEstimator; +import org.apache.lucene.util.UnicodeUtil; + +/** + * A {@link DataOutput} storing data in a list of {@link ByteBuffer}s. The data is written in big-endian byte + * order, as produced by Lucene 7.5. Note that this participates in the type hierarchy of the modern Lucene + * dependency, so it must carefully override DataOutput methods that would use the modern Lucene byte ordering of + * little-endian. + * This file was imported from the Apache Lucene project at commit b5bf70b7e32d7ddd9742cc821d471c5fabd4e3df, + * tagged as releases/lucene-solr/7.5.0. The following modifications have been made to the original file: + *

      + *
    • Renamed from ByteBuffersDataOutput to LegacyByteBuffersDataOutput.
    • + *
    • Return types modified accordingly.
    • + *
    • toDataInput now returns a LegacyByteBuffersDataInput to match encodings.
    • + *
    • writeShort/writeInt/writeLong now use writeCrossBlock* implementations to avoid delegating to superclass.
    • + *
    + */ +public final class LegacyByteBuffersDataOutput extends DataOutput implements Accountable +{ + private final static ByteBuffer EMPTY = ByteBuffer.allocate(0); + private final static byte[] EMPTY_BYTE_ARRAY = {}; + + public final static IntFunction ALLOCATE_BB_ON_HEAP = ByteBuffer::allocate; + + /** + * A singleton instance of "no-reuse" buffer strategy. + */ + public final static Consumer NO_REUSE = (bb) -> { + throw new RuntimeException("reset() is not allowed on this buffer."); + }; + + /** + * An implementation of a {@link ByteBuffer} allocation and recycling policy. + * The blocks are recycled if exactly the same size is requested, otherwise + * they're released to be GCed. + */ + public final static class ByteBufferRecycler + { + private final ArrayDeque reuse = new ArrayDeque<>(); + private final IntFunction delegate; + + public ByteBufferRecycler(IntFunction delegate) + { + this.delegate = Objects.requireNonNull(delegate); + } + + public ByteBuffer allocate(int size) + { + while (!reuse.isEmpty()) + { + ByteBuffer bb = reuse.removeFirst(); + // If we don't have a buffer of exactly the requested size, discard it. + if (bb.remaining() == size) + { + return bb; + } + } + + return delegate.apply(size); + } + + public void reuse(ByteBuffer buffer) + { + buffer.rewind(); + reuse.addLast(buffer); + } + } + + public final static int DEFAULT_MIN_BITS_PER_BLOCK = 10; // 1024 B + public final static int DEFAULT_MAX_BITS_PER_BLOCK = 26; // 64 MB + + /** + * Maximum number of blocks at the current {@link #blockBits} block size + * before we increase the block size (and thus decrease the number of blocks). + */ + final static int MAX_BLOCKS_BEFORE_BLOCK_EXPANSION = 100; + + /** + * Maximum block size: {@code 2^bits}. + */ + private final int maxBitsPerBlock; + + /** + * {@link ByteBuffer} supplier. + */ + private final IntFunction blockAllocate; + + /** + * {@link ByteBuffer} recycler on {@link #reset}. + */ + private final Consumer blockReuse; + + /** + * Current block size: {@code 2^bits}. + */ + private int blockBits; + + /** + * Blocks storing data. + */ + private final ArrayDeque blocks = new ArrayDeque<>(); + + /** + * The current-or-next write block. + */ + private ByteBuffer currentBlock = EMPTY; + + public LegacyByteBuffersDataOutput(long expectedSize) + { + this(computeBlockSizeBitsFor(expectedSize), DEFAULT_MAX_BITS_PER_BLOCK, ALLOCATE_BB_ON_HEAP, NO_REUSE); + } + + public LegacyByteBuffersDataOutput() + { + this(DEFAULT_MIN_BITS_PER_BLOCK, DEFAULT_MAX_BITS_PER_BLOCK, ALLOCATE_BB_ON_HEAP, NO_REUSE); + } + + public LegacyByteBuffersDataOutput(int minBitsPerBlock, + int maxBitsPerBlock, + IntFunction blockAllocate, + Consumer blockReuse) + { + if (minBitsPerBlock < 10 || + minBitsPerBlock > maxBitsPerBlock || + maxBitsPerBlock > 31) + { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "Invalid arguments: %s %s", + minBitsPerBlock, + maxBitsPerBlock)); + } + this.maxBitsPerBlock = maxBitsPerBlock; + this.blockBits = minBitsPerBlock; + this.blockAllocate = Objects.requireNonNull(blockAllocate, "Block allocator must not be null."); + this.blockReuse = Objects.requireNonNull(blockReuse, "Block reuse must not be null."); + } + + @Override + public void writeByte(byte b) + { + if (!currentBlock.hasRemaining()) + { + appendBlock(); + } + currentBlock.put(b); + } + + @Override + public void writeBytes(byte[] src, int offset, int length) + { + assert length >= 0; + while (length > 0) + { + if (!currentBlock.hasRemaining()) + { + appendBlock(); + } + + int chunk = Math.min(currentBlock.remaining(), length); + currentBlock.put(src, offset, chunk); + length -= chunk; + offset += chunk; + } + } + + @Override + public void writeBytes(byte[] b, int length) + { + writeBytes(b, 0, length); + } + + public void writeBytes(byte[] b) + { + writeBytes(b, 0, b.length); + } + + public void writeBytes(ByteBuffer buffer) + { + buffer = buffer.duplicate(); + int length = buffer.remaining(); + while (length > 0) + { + if (!currentBlock.hasRemaining()) + { + appendBlock(); + } + + int chunk = Math.min(currentBlock.remaining(), length); + buffer.limit(buffer.position() + chunk); + currentBlock.put(buffer); + + length -= chunk; + } + } + + /** + * Return a list of read-only view of {@link ByteBuffer} blocks over the + * current content written to the output. + */ + public ArrayList toBufferList() + { + ArrayList result = new ArrayList<>(Math.max(blocks.size(), 1)); + if (blocks.isEmpty()) + { + result.add(EMPTY); + } + else + { + for (ByteBuffer bb : blocks) + { + bb = (ByteBuffer) bb.asReadOnlyBuffer().flip(); // cast for jdk8 (covariant in jdk9+) + result.add(bb); + } + } + return result; + } + + /** + * Returns a list of writeable blocks over the (source) content buffers. + *

    + * This method returns the raw content of source buffers that may change over the lifetime + * of this object (blocks can be recycled or discarded, for example). Most applications + * should favor calling {@link #toBufferList()} which returns a read-only view over + * the content of the source buffers. + *

    + * The difference between {@link #toBufferList()} and {@link #toWriteableBufferList()} is that + * read-only view of source buffers will always return {@code false} from {@link ByteBuffer#hasArray()} + * (which sometimes may be required to avoid double copying). + */ + public ArrayList toWriteableBufferList() + { + ArrayList result = new ArrayList<>(Math.max(blocks.size(), 1)); + if (blocks.isEmpty()) + { + result.add(EMPTY); + } + else + { + for (ByteBuffer bb : blocks) + { + bb = (ByteBuffer) bb.duplicate().flip(); // cast for jdk8 (covariant in jdk9+) + result.add(bb); + } + } + return result; + } + + /** + * Return a {@link LegacyByteBuffersDataInput} for the set of current buffers ({@link #toBufferList()}). + */ + public LegacyByteBuffersDataInput toDataInput() + { + return new LegacyByteBuffersDataInput(toBufferList()); + } + + /** + * Return a contiguous array with the current content written to the output. The returned + * array is always a copy (can be mutated). + */ + public byte[] toArrayCopy() + { + if (blocks.isEmpty()) + { + return EMPTY_BYTE_ARRAY; + } + + // We could try to detect single-block, array-based ByteBuffer here + // and use Arrays.copyOfRange, but I don't think it's worth the extra + // instance checks. + + byte[] arr = new byte[Math.toIntExact(size())]; + int offset = 0; + for (ByteBuffer bb : toBufferList()) + { + int len = bb.remaining(); + bb.get(arr, offset, len); + offset += len; + } + return arr; + } + + /** + * Copy the current content of this object into another {@link DataOutput}. + */ + public void copyTo(DataOutput output) throws IOException + { + for (ByteBuffer bb : toBufferList()) + { + if (bb.hasArray()) + { + output.writeBytes(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining()); + } + else + { + output.copyBytes(new LegacyByteBuffersDataInput(Arrays.asList(bb)), bb.remaining()); + } + } + } + + /** + * @return The number of bytes written to this output so far. + */ + public long size() + { + long size = 0; + int blockCount = blocks.size(); + if (blockCount >= 1) + { + int fullBlockSize = (blockCount - 1) * blockSize(); + int lastBlockSize = blocks.getLast().position(); + size = fullBlockSize + lastBlockSize; + } + return size; + } + + @Override + public String toString() + { + return String.format(Locale.ROOT, + "%,d bytes, block size: %,d, blocks: %,d", + size(), + blockSize(), + blocks.size()); + } + + // Specialized versions of writeXXX methods that break execution into + // fast/ slow path if the result would fall on the current block's + // boundary. + // + // We also remove the IOException from methods because it (theoretically) + // cannot be thrown from byte buffers. + + @Override + public void writeShort(short v) + { + if (currentBlock.remaining() >= Short.BYTES) + { + currentBlock.putShort(v); + } + else + { + writeCrossBlockShort(v); + } + } + + private void writeCrossBlockShort(short v) + { + writeByte((byte) (v >> 8)); + writeByte((byte) v); + } + + @Override + public void writeInt(int v) + { + if (currentBlock.remaining() >= Integer.BYTES) + { + currentBlock.putInt(v); + } + else + { + writeCrossBlockInt(v); + } + } + + private void writeCrossBlockInt(int v) + { + writeByte((byte) (v >>> 24)); + writeByte((byte) (v >>> 16)); + writeByte((byte) (v >>> 8)); + writeByte((byte) v); + } + + @Override + public void writeLong(long v) + { + if (currentBlock.remaining() >= Long.BYTES) + { + currentBlock.putLong(v); + } + else + { + writeCrossBlockLong(v); + } + } + + private void writeCrossBlockLong(long v) + { + writeByte((byte) (v >>> 56)); + writeByte((byte) (v >>> 48)); + writeByte((byte) (v >>> 40)); + writeByte((byte) (v >>> 32)); + writeByte((byte) (v >>> 24)); + writeByte((byte) (v >>> 16)); + writeByte((byte) (v >>> 8)); + writeByte((byte) v); + } + + @Override + public void writeString(String v) + { + try + { + final int MAX_CHARS_PER_WINDOW = 1024; + if (v.length() <= MAX_CHARS_PER_WINDOW) + { + final BytesRef utf8 = new BytesRef(v); + writeVInt(utf8.length); + writeBytes(utf8.bytes, utf8.offset, utf8.length); + } + else + { + writeVInt(UnicodeUtil.calcUTF16toUTF8Length(v, 0, v.length())); + final byte[] buf = new byte[UnicodeUtil.MAX_UTF8_BYTES_PER_CHAR * MAX_CHARS_PER_WINDOW]; + UTF16toUTF8(v, 0, v.length(), buf, (len) -> { + writeBytes(buf, 0, len); + }); + } + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + public void writeMapOfStrings(Map map) + { + try + { + super.writeMapOfStrings(map); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + public void writeSetOfStrings(Set set) + { + try + { + super.writeSetOfStrings(set); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + public long ramBytesUsed() + { + // Return a rough estimation for allocated blocks. Note that we do not make + // any special distinction for direct memory buffers. + return RamUsageEstimator.NUM_BYTES_OBJECT_REF * blocks.size() + + blocks.stream().mapToLong(buf -> buf.capacity()).sum(); + } + + /** + * This method resets this object to a clean (zero-size) state and + * publishes any currently allocated buffers for reuse to the reuse strategy + * provided in the constructor. + *

    + * Sharing byte buffers for reads and writes is dangerous and will very likely + * lead to hard-to-debug issues, use with great care. + */ + public void reset() + { + if (blockReuse != NO_REUSE) + { + blocks.stream().forEach(blockReuse); + } + blocks.clear(); + currentBlock = EMPTY; + } + + /** + * @return Returns a new {@link LegacyByteBuffersDataOutput} with the {@link #reset()} capability. + */ + // TODO: perhaps we can move it out to an utility class (as a supplier of preconfigured instances?) + public static LegacyByteBuffersDataOutput newResettableInstance() + { + LegacyByteBuffersDataOutput.ByteBufferRecycler reuser = new LegacyByteBuffersDataOutput.ByteBufferRecycler( + LegacyByteBuffersDataOutput.ALLOCATE_BB_ON_HEAP); + return new LegacyByteBuffersDataOutput( + LegacyByteBuffersDataOutput.DEFAULT_MIN_BITS_PER_BLOCK, + LegacyByteBuffersDataOutput.DEFAULT_MAX_BITS_PER_BLOCK, + reuser::allocate, + reuser::reuse); + } + + private int blockSize() + { + return 1 << blockBits; + } + + private void appendBlock() + { + if (blocks.size() >= MAX_BLOCKS_BEFORE_BLOCK_EXPANSION && blockBits < maxBitsPerBlock) + { + rewriteToBlockSize(blockBits + 1); + if (blocks.getLast().hasRemaining()) + { + return; + } + } + + final int requiredBlockSize = 1 << blockBits; + currentBlock = blockAllocate.apply(requiredBlockSize); + assert currentBlock.capacity() == requiredBlockSize; + blocks.add(currentBlock); + } + + private void rewriteToBlockSize(int targetBlockBits) + { + assert targetBlockBits <= maxBitsPerBlock; + + // We copy over data blocks to an output with one-larger block bit size. + // We also discard references to blocks as we're copying to allow GC to + // clean up partial results in case of memory pressure. + LegacyByteBuffersDataOutput cloned = new LegacyByteBuffersDataOutput(targetBlockBits, targetBlockBits, blockAllocate, NO_REUSE); + ByteBuffer block; + while ((block = blocks.pollFirst()) != null) + { + block.flip(); + cloned.writeBytes(block); + if (blockReuse != NO_REUSE) + { + blockReuse.accept(block); + } + } + + assert blocks.isEmpty(); + this.blockBits = targetBlockBits; + blocks.addAll(cloned.blocks); + } + + private static int computeBlockSizeBitsFor(long bytes) + { + long powerOfTwo = BitUtil.nextHighestPowerOfTwo(bytes / MAX_BLOCKS_BEFORE_BLOCK_EXPANSION); + if (powerOfTwo == 0) + { + return DEFAULT_MIN_BITS_PER_BLOCK; + } + + int blockBits = Long.numberOfTrailingZeros(powerOfTwo); + blockBits = Math.min(blockBits, DEFAULT_MAX_BITS_PER_BLOCK); + blockBits = Math.max(blockBits, DEFAULT_MIN_BITS_PER_BLOCK); + return blockBits; + } + + // TODO: move this block-based conversion to UnicodeUtil. + + private static final long HALF_SHIFT = 10; + private static final int SURROGATE_OFFSET = + Character.MIN_SUPPLEMENTARY_CODE_POINT - + (UnicodeUtil.UNI_SUR_HIGH_START << HALF_SHIFT) - UnicodeUtil.UNI_SUR_LOW_START; + + /** + * A consumer-based UTF16-UTF8 encoder (writes the input string in smaller buffers.). + */ + private static int UTF16toUTF8(final CharSequence s, + final int offset, + final int length, + byte[] buf, + IntConsumer bufferFlusher) + { + int utf8Len = 0; + int j = 0; + for (int i = offset, end = offset + length; i < end; i++) + { + final int chr = (int) s.charAt(i); + + if (j + 4 >= buf.length) + { + bufferFlusher.accept(j); + utf8Len += j; + j = 0; + } + + if (chr < 0x80) + buf[j++] = (byte) chr; + else if (chr < 0x800) + { + buf[j++] = (byte) (0xC0 | (chr >> 6)); + buf[j++] = (byte) (0x80 | (chr & 0x3F)); + } + else if (chr < 0xD800 || chr > 0xDFFF) + { + buf[j++] = (byte) (0xE0 | (chr >> 12)); + buf[j++] = (byte) (0x80 | ((chr >> 6) & 0x3F)); + buf[j++] = (byte) (0x80 | (chr & 0x3F)); + } + else + { + // A surrogate pair. Confirm valid high surrogate. + if (chr < 0xDC00 && (i < end - 1)) + { + int utf32 = (int) s.charAt(i + 1); + // Confirm valid low surrogate and write pair. + if (utf32 >= 0xDC00 && utf32 <= 0xDFFF) + { + utf32 = (chr << 10) + utf32 + SURROGATE_OFFSET; + i++; + buf[j++] = (byte) (0xF0 | (utf32 >> 18)); + buf[j++] = (byte) (0x80 | ((utf32 >> 12) & 0x3F)); + buf[j++] = (byte) (0x80 | ((utf32 >> 6) & 0x3F)); + buf[j++] = (byte) (0x80 | (utf32 & 0x3F)); + continue; + } + } + // Replace unpaired surrogate or out-of-order low surrogate + // with substitution character. + buf[j++] = (byte) 0xEF; + buf[j++] = (byte) 0xBF; + buf[j++] = (byte) 0xBD; + } + } + + bufferFlusher.accept(j); + utf8Len += j; + + return utf8Len; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataOutputAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataOutputAdapter.java new file mode 100644 index 000000000000..fcd532c351e6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersDataOutputAdapter.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.store.DataInput; + +/** + * Minimal wrapper around LegacyByteBufferDataOutput, to allow for mixed callsites of LegacyByteBufferDataOutput + * and ByteBufferDataOutput. + */ +public class LegacyByteBuffersDataOutputAdapter extends ByteBuffersDataOutputAdapter +{ + private LegacyByteBuffersDataOutput wrapped; + + public LegacyByteBuffersDataOutputAdapter(long expectedSize) + { + wrapped = new LegacyByteBuffersDataOutput(expectedSize); + } + + @Override + public void reset() + { + wrapped.reset(); + } + + @Override + public long size() + { + return wrapped.size(); + } + + @Override + public byte[] toArrayCopy() + { + return wrapped.toArrayCopy(); + } + + @Override + public void writeBytes(byte[] b, int length) throws IOException + { + wrapped.writeBytes(b, length); + } + + @Override + public void writeInt(int i) throws IOException + { + wrapped.writeInt(i); + } + + @Override + public void writeShort(short i) throws IOException + { + wrapped.writeShort(i); + } + + @Override + public void writeLong(long i) throws IOException + { + wrapped.writeLong(i); + } + + @Override + public void writeString(String s) throws IOException + { + wrapped.writeString(s); + } + + @Override + public void copyBytes(DataInput input, long numBytes) throws IOException + { + wrapped.copyBytes(input, numBytes); + } + + @Override + public void writeMapOfStrings(Map map) throws IOException + { + wrapped.writeMapOfStrings(map); + } + + @Override + public void writeSetOfStrings(Set set) throws IOException + { + wrapped.writeSetOfStrings(set); + } + + @Override + public void writeByte(byte b) throws IOException + { + wrapped.writeByte(b); + } + + @Override + public void writeBytes(byte[] src, int offset, int length) throws IOException + { + wrapped.writeBytes(src, offset, length); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersIndexInput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersIndexInput.java new file mode 100644 index 000000000000..2204dba93db6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersIndexInput.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.lucene.store.AlreadyClosedException; +import org.apache.lucene.store.RandomAccessInput; + +/** + * An {@link IndexInput} implementing {@link RandomAccessInput} and backed + * by a {@link LegacyByteBuffersDataInput}. Data is read in big-endian byte order, + * as produced by Lucene 7.5. + * This file was imported from the Apache Lucene project at commit b5bf70b7e32d7ddd9742cc821d471c5fabd4e3df, + * tagged as releases/lucene-solr/7.5.0. The following modifications have been made to the original file: + *

      + *
    • Renamed from ByteBuffersIndexInput to LegacyByteBuffersIndexInput.
    • + *
    • Implements our IndexInput wrapper, which provides endianness.
    • + *
    • Wraps LegacyByteBuffersDataInput instead of ByteBuffersDataInput.
    • + *
    + */ +public final class LegacyByteBuffersIndexInput extends IndexInput implements RandomAccessInput +{ + private LegacyByteBuffersDataInput in; + + public LegacyByteBuffersIndexInput(LegacyByteBuffersDataInput in, String resourceDescription) + { + super(resourceDescription, ByteOrder.BIG_ENDIAN); + this.in = in; + } + + @Override + public void close() throws IOException + { + in = null; + } + + @Override + public long getFilePointer() + { + ensureOpen(); + return in.position(); + } + + @Override + public void seek(long pos) throws IOException + { + ensureOpen(); + in.seek(pos); + } + + @Override + public long length() + { + ensureOpen(); + return in.size(); + } + + @Override + public LegacyByteBuffersIndexInput slice(String sliceDescription, long offset, long length) throws IOException + { + ensureOpen(); + return new LegacyByteBuffersIndexInput(in.slice(offset, length), + "(sliced) offset=" + offset + ", length=" + length + " " + toString() + " [slice=" + sliceDescription + "]"); + } + + @Override + public byte readByte() throws IOException + { + ensureOpen(); + return in.readByte(); + } + + @Override + public void readBytes(byte[] b, int offset, int len) throws IOException + { + ensureOpen(); + in.readBytes(b, offset, len); + } + + @Override + public RandomAccessInput randomAccessSlice(long offset, long length) throws IOException + { + ensureOpen(); + return slice("", offset, length); + } + + @Override + public void readBytes(byte[] b, int offset, int len, boolean useBuffer) throws IOException + { + ensureOpen(); + in.readBytes(b, offset, len, useBuffer); + } + + @Override + public short readShort() throws IOException + { + ensureOpen(); + return in.readShort(); + } + + @Override + public int readInt() throws IOException + { + ensureOpen(); + return in.readInt(); + } + + @Override + public int readVInt() throws IOException + { + ensureOpen(); + return in.readVInt(); + } + + @Override + public int readZInt() throws IOException + { + ensureOpen(); + return in.readZInt(); + } + + @Override + public long readLong() throws IOException + { + ensureOpen(); + return in.readLong(); + } + + @Override + public long readVLong() throws IOException + { + ensureOpen(); + return in.readVLong(); + } + + @Override + public long readZLong() throws IOException + { + ensureOpen(); + return in.readZLong(); + } + + @Override + public String readString() throws IOException + { + ensureOpen(); + return in.readString(); + } + + @Override + public Map readMapOfStrings() throws IOException + { + ensureOpen(); + return in.readMapOfStrings(); + } + + @Override + public Set readSetOfStrings() throws IOException + { + ensureOpen(); + return in.readSetOfStrings(); + } + + @Override + public void skipBytes(long numBytes) throws IOException + { + ensureOpen(); + super.skipBytes(numBytes); + } + + @Override + public byte readByte(long pos) throws IOException + { + ensureOpen(); + return in.readByte(pos); + } + + @Override + public short readShort(long pos) throws IOException + { + ensureOpen(); + return in.readShort(pos); + } + + @Override + public int readInt(long pos) throws IOException + { + ensureOpen(); + return in.readInt(pos); + } + + @Override + public long readLong(long pos) throws IOException + { + ensureOpen(); + return in.readLong(pos); + } + + @Override + public IndexInput clone() + { + ensureOpen(); + LegacyByteBuffersIndexInput cloned = new LegacyByteBuffersIndexInput(in.slice(0, in.size()), "(clone of) " + toString()); + try + { + cloned.seek(getFilePointer()); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + return cloned; + } + + private void ensureOpen() + { + if (in == null) + { + throw new AlreadyClosedException("Already closed."); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersIndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersIndexOutput.java new file mode 100644 index 000000000000..6a685bb53a70 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyByteBuffersIndexOutput.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.lucene.store.AlreadyClosedException; +import org.apache.lucene.store.DataInput; + +/** + * An {@link IndexOutput} writing to a {@link LegacyByteBuffersDataOutput}. + * This uses the big-endian byte ordering of Lucene 7.5 and is used to write indexes/data compatible with the + * readers in older Lucene versions. + * This file was imported from the Apache Lucene project at commit b5bf70b7e32d7ddd9742cc821d471c5fabd4e3df, + * tagged as releases/lucene-solr/7.5.0. The following modifications have been made to the original file: + *
      + *
    • Renamed from ByteBuffersIndexOutput to LegacyByteBuffersIndexOutput.
    • + *
    • Implements our IndexOutput wrapper, which provides endianness.
    • + *
    • Wraps LegacyByteBuffersDataOutput instead of ByteBuffersDataOutput.
    • + *
    + */ +public final class LegacyByteBuffersIndexOutput extends IndexOutput +{ + private final Consumer onClose; + + private final Checksum checksum; + private long lastChecksumPosition; + private long lastChecksum; + + private LegacyByteBuffersDataOutput delegate; + + public LegacyByteBuffersIndexOutput(LegacyByteBuffersDataOutput delegate, String resourceDescription, String name, Version version) + { + this(delegate, resourceDescription, name, new CRC32(), null, version); + } + + public LegacyByteBuffersIndexOutput(LegacyByteBuffersDataOutput delegate, String resourceDescription, String name, + Checksum checksum, + Consumer onClose, Version version) + { + super(resourceDescription, name, ByteOrder.BIG_ENDIAN, version); + this.delegate = delegate; + this.checksum = checksum; + this.onClose = onClose; + } + + @Override + public void close() throws IOException + { + // No special effort to be thread-safe here since IndexOutputs are not required to be thread-safe. + LegacyByteBuffersDataOutput local = delegate; + delegate = null; + if (local != null && onClose != null) + { + onClose.accept(local); + } + } + + @Override + public long getFilePointer() + { + ensureOpen(); + return delegate.size(); + } + + @Override + public long getChecksum() throws IOException + { + ensureOpen(); + + if (checksum == null) + { + throw new IOException("This index output has no checksum computing ability: " + toString()); + } + + // Compute checksum on the current content of the delegate. + // + // This way we can override more methods and pass them directly to the delegate for efficiency of writing, + // while allowing the checksum to be correctly computed on the current content of the output buffer (IndexOutput + // is per-thread, so no concurrent changes). + if (lastChecksumPosition != delegate.size()) + { + lastChecksumPosition = delegate.size(); + checksum.reset(); + byte[] buffer = null; + for (ByteBuffer bb : delegate.toBufferList()) + { + if (bb.hasArray()) + { + checksum.update(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining()); + } + else + { + if (buffer == null) buffer = new byte[1024 * 4]; + + bb = bb.asReadOnlyBuffer(); + int remaining = bb.remaining(); + while (remaining > 0) + { + int len = Math.min(remaining, buffer.length); + bb.get(buffer, 0, len); + checksum.update(buffer, 0, len); + remaining -= len; + } + } + } + lastChecksum = checksum.getValue(); + } + return lastChecksum; + } + + @Override + public void writeByte(byte b) throws IOException + { + ensureOpen(); + delegate.writeByte(b); + } + + @Override + public void writeBytes(byte[] b, int offset, int length) throws IOException + { + ensureOpen(); + delegate.writeBytes(b, offset, length); + } + + @Override + public void writeBytes(byte[] b, int length) throws IOException + { + ensureOpen(); + delegate.writeBytes(b, length); + } + + @Override + public void writeInt(int i) throws IOException + { + ensureOpen(); + delegate.writeInt(i); + } + + @Override + public void writeShort(short i) throws IOException + { + ensureOpen(); + delegate.writeShort(i); + } + + @Override + public void writeLong(long i) throws IOException + { + ensureOpen(); + delegate.writeLong(i); + } + + @Override + public void writeString(String s) throws IOException + { + ensureOpen(); + delegate.writeString(s); + } + + @Override + public void copyBytes(DataInput input, long numBytes) throws IOException + { + ensureOpen(); + delegate.copyBytes(input, numBytes); + } + + @Override + public void writeMapOfStrings(Map map) throws IOException + { + ensureOpen(); + delegate.writeMapOfStrings(map); + } + + @Override + public void writeSetOfStrings(Set set) throws IOException + { + ensureOpen(); + delegate.writeSetOfStrings(set); + } + + private void ensureOpen() + { + if (delegate == null) + { + throw new AlreadyClosedException("Already closed."); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyDirectWriterAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyDirectWriterAdapter.java new file mode 100644 index 000000000000..620f3f35affb --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyDirectWriterAdapter.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; + +import org.apache.lucene.backward_codecs.packed.LegacyDirectWriter; + +/** + * Minimal wrapper around Lucene's LegacyDirectWriter, which doesn't share an interface with DirectWriter. + */ +public class LegacyDirectWriterAdapter implements DirectWriterAdapter +{ + private final LegacyDirectWriter delegate; + + public LegacyDirectWriterAdapter(org.apache.lucene.store.DataOutput output, long numValues, int bitsPerValue) + { + this.delegate = LegacyDirectWriter.getInstance(output, numValues, bitsPerValue); + } + + public void add(long l) throws IOException + { + delegate.add(l); + } + + public void finish() throws IOException + { + delegate.finish(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyResettableByteBuffersIndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyResettableByteBuffersIndexOutput.java new file mode 100644 index 000000000000..642624ebab2f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LegacyResettableByteBuffersIndexOutput.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.lucene.store.ByteBuffersIndexOutput; +import org.apache.lucene.store.DataInput; +import org.apache.lucene.store.IndexInput; + +/*** + * A wrapper around {@link ByteBuffersIndexOutput} that adds several methods that interact + * with the underlying delegate. This uses the big-endian byte ordering of Lucene 7.5 and + * is used to write indexes/data compatible with the readers in older Lucene versions. + */ +public class LegacyResettableByteBuffersIndexOutput extends ResettableByteBuffersIndexOutput +{ + + private final LegacyByteBuffersIndexOutput bbio; + private final LegacyByteBuffersDataOutput delegate; + + public LegacyResettableByteBuffersIndexOutput(int expectedSize, String name, Version version) + { + super("", name, ByteOrder.BIG_ENDIAN, version); + delegate = new LegacyByteBuffersDataOutput(expectedSize); + bbio = new LegacyByteBuffersIndexOutput(delegate, "", name + "-bb", version); + } + + public LegacyByteBuffersDataInput toDataInput() + { + return delegate.toDataInput(); + } + + public IndexInput toIndexInput() + { + return new LegacyByteBuffersIndexInput(toDataInput(), ""); + } + + public void copyTo(IndexOutput out) throws IOException + { + delegate.copyTo(out); + } + + public int intSize() { + return Math.toIntExact(bbio.getFilePointer()); + } + + public byte[] toArrayCopy() { + return delegate.toArrayCopy(); + } + + public void reset() + { + delegate.reset(); + } + + public String toString() + { + return "Resettable" + bbio.toString(); + } + + public void close() throws IOException + { + bbio.close(); + } + + public long getFilePointer() + { + return bbio.getFilePointer(); + } + + public long getChecksum() throws IOException + { + return bbio.getChecksum(); + } + + public void writeByte(byte b) throws IOException + { + bbio.writeByte(b); + } + + public void writeBytes(byte[] b, int offset, int length) throws IOException + { + bbio.writeBytes(b, offset, length); + } + + public void writeBytes(byte[] b, int length) throws IOException + { + bbio.writeBytes(b, length); + } + + public void writeInt(int i) throws IOException + { + bbio.writeInt(i); + } + + public void writeShort(short i) throws IOException + { + bbio.writeShort(i); + } + + public void writeLong(long i) throws IOException + { + bbio.writeLong(i); + } + + public void writeString(String s) throws IOException + { + bbio.writeString(s); + } + + public void copyBytes(DataInput input, long numBytes) throws IOException + { + bbio.copyBytes(input, numBytes); + } + + public void writeMapOfStrings(Map map) throws IOException + { + bbio.writeMapOfStrings(map); + } + + public void writeSetOfStrings(Set set) throws IOException + { + bbio.writeSetOfStrings(set); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LuceneCompat.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LuceneCompat.java new file mode 100644 index 000000000000..c664bdd9edc0 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/LuceneCompat.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.nio.ByteOrder; + +import org.apache.cassandra.index.sai.disk.ModernResettableByteBuffersIndexOutput; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.utils.SeekingRandomAccessInput; +import org.apache.lucene.backward_codecs.packed.LegacyDirectReader; +import org.apache.lucene.backward_codecs.packed.LegacyDirectWriter; +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.util.LongValues; +import org.apache.lucene.util.packed.DirectReader; +import org.apache.lucene.util.packed.DirectWriter; + +/** + * Compatibility layer for Lucene 7.5 and earlier. + */ +public class LuceneCompat +{ + public static LongValues directReaderGetInstance(SeekingRandomAccessInput slice, int bitsPerValue, long offset) + { + // Lucene 7.5 and earlier used big-endian ordering + return slice.order() == ByteOrder.LITTLE_ENDIAN ? DirectReader.getInstance(slice, bitsPerValue, offset) + : LegacyDirectReader.getInstance(slice, bitsPerValue, offset); + } + + public static DirectWriterAdapter directWriterGetInstance(ByteOrder order, DataOutput out, long numValues, int bitsPerValue) + { + // Lucene 7.5 and earlier used big-endian ordering + return order == ByteOrder.LITTLE_ENDIAN ? new ModernDirectWriterAdapter(out, numValues, bitsPerValue) + : new LegacyDirectWriterAdapter(out, numValues, bitsPerValue); + } + + public static int directWriterUnsignedBitsRequired(ByteOrder order, long maxValue) + { + // Lucene 7.5 and earlier used big-endian ordering + return order == ByteOrder.LITTLE_ENDIAN ? DirectWriter.unsignedBitsRequired(maxValue) + : LegacyDirectWriter.unsignedBitsRequired(maxValue); + } + + public static ResettableByteBuffersIndexOutput getResettableByteBuffersIndexOutput(ByteOrder order, int expectedSize, String name, Version version) + { + // Lucene 7.5 and earlier used big-endian ordering + return order == ByteOrder.LITTLE_ENDIAN ? new ModernResettableByteBuffersIndexOutput(expectedSize, name, version) + : new LegacyResettableByteBuffersIndexOutput(expectedSize, name, version); + } + + public static ByteBuffersDataOutputAdapter getByteBuffersDataOutputAdapter(ByteOrder order, long expectedSize) + { + // Lucene 7.5 and earlier used big-endian ordering + return order == ByteOrder.LITTLE_ENDIAN ? new ModernByteBuffersDataOutputAdapter(expectedSize) + : new LegacyByteBuffersDataOutputAdapter(expectedSize); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ModernByteBuffersDataOutputAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ModernByteBuffersDataOutputAdapter.java new file mode 100644 index 000000000000..eeb917c7c788 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ModernByteBuffersDataOutputAdapter.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.store.ByteBuffersDataOutput; +import org.apache.lucene.store.DataInput; + +/** + * Minimal wrapper around ByteBufferDataOutput, to allow for mixed callsites of LegacyByteBufferDataOutput + * and ByteBufferDataOutput. + */ +public class ModernByteBuffersDataOutputAdapter extends ByteBuffersDataOutputAdapter +{ + private ByteBuffersDataOutput wrapped; + + public ModernByteBuffersDataOutputAdapter(long expectedSize) + { + wrapped = new ByteBuffersDataOutput(expectedSize); + } + + @Override + public void reset() + { + wrapped.reset(); + } + + @Override + public long size() + { + return wrapped.size(); + } + + @Override + public byte[] toArrayCopy() + { + return wrapped.toArrayCopy(); + } + + @Override + public void writeBytes(byte[] b, int length) throws IOException + { + wrapped.writeBytes(b, length); + } + + @Override + public void writeInt(int i) throws IOException + { + wrapped.writeInt(i); + } + + @Override + public void writeShort(short i) throws IOException + { + wrapped.writeShort(i); + } + + @Override + public void writeLong(long i) throws IOException + { + wrapped.writeLong(i); + } + + @Override + public void writeString(String s) throws IOException + { + wrapped.writeString(s); + } + + @Override + public void copyBytes(DataInput input, long numBytes) throws IOException + { + wrapped.copyBytes(input, numBytes); + } + + @Override + public void writeMapOfStrings(Map map) throws IOException + { + wrapped.writeMapOfStrings(map); + } + + @Override + public void writeSetOfStrings(Set set) throws IOException + { + wrapped.writeSetOfStrings(set); + } + + @Override + public void writeByte(byte b) throws IOException + { + wrapped.writeByte(b); + } + + @Override + public void writeBytes(byte[] src, int offset, int length) throws IOException + { + wrapped.writeBytes(src, offset, length); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ModernDirectWriterAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ModernDirectWriterAdapter.java new file mode 100644 index 000000000000..ae822cc0aab1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ModernDirectWriterAdapter.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; + +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.util.packed.DirectWriter; + +/** + * Minimal wrapper arount DirectWriter to allow it to be used in a common interface with LegacyDirectWriter. + */ +public class ModernDirectWriterAdapter implements DirectWriterAdapter +{ + private final org.apache.lucene.util.packed.DirectWriter delegate; + + public ModernDirectWriterAdapter(DataOutput output, long numValues, int bitsPerValue) + { + this.delegate = DirectWriter.getInstance(output, numValues, bitsPerValue); + } + + @Override + public void add(long l) throws IOException + { + delegate.add(l); + } + + @Override + public void finish() throws IOException + { + delegate.finish(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/MutablePointValues.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/MutablePointValues.java new file mode 100644 index 000000000000..d233e56b74ae --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/MutablePointValues.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.oldlucene; + +import org.apache.lucene.index.PointValues; +import org.apache.lucene.util.BytesRef; + +/** {@link PointValues} whose order of points can be changed. + * This class is useful for codecs to optimize flush. + * @lucene.internal */ +public abstract class MutablePointValues extends PointValues { + + /** Sole constructor. */ + protected MutablePointValues() {} + + /** Set {@code packedValue} with a reference to the packed bytes of the i-th value. */ + public abstract void getValue(int i, BytesRef packedValue); + + /** Get the k-th byte of the i-th value. */ + public abstract byte getByteAt(int i, int k); + + /** Return the doc ID of the i-th value. */ + public abstract int getDocID(int i); + + /** Swap the i-th and j-th values. */ + public abstract void swap(int i, int j); + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/MutablePointsReaderUtils.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/MutablePointsReaderUtils.java new file mode 100644 index 000000000000..08d9e96c3585 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/MutablePointsReaderUtils.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.util.Arrays; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IntroSelector; +import org.apache.lucene.util.IntroSorter; +import org.apache.lucene.util.MSBRadixSorter; +import org.apache.lucene.util.RadixSelector; +import org.apache.lucene.util.Selector; +import org.apache.lucene.util.packed.PackedInts; + +/** Utility APIs for sorting and partitioning buffered points. + * + * @lucene.internal */ +public final class MutablePointsReaderUtils { + + MutablePointsReaderUtils() {} + + /** Sort the given {@link MutablePointValues} based on its packed value then doc ID. */ + public static void sort(int maxDoc, int packedBytesLength, + MutablePointValues reader, int from, int to) { + final int bitsPerDocId = PackedInts.bitsRequired(maxDoc - 1); + new MSBRadixSorter(packedBytesLength + (bitsPerDocId + 7) / 8) { + + @Override + protected void swap(int i, int j) { + reader.swap(i, j); + } + + @Override + protected int byteAt(int i, int k) { + if (k < packedBytesLength) { + return Byte.toUnsignedInt(reader.getByteAt(i, k)); + } else { + final int shift = bitsPerDocId - ((k - packedBytesLength + 1) << 3); + return (reader.getDocID(i) >>> Math.max(0, shift)) & 0xff; + } + } + + @Override + protected org.apache.lucene.util.Sorter getFallbackSorter(int k) { + return new IntroSorter() { + + final BytesRef pivot = new BytesRef(); + final BytesRef scratch = new BytesRef(); + int pivotDoc; + + @Override + protected void swap(int i, int j) { + reader.swap(i, j); + } + + @Override + protected void setPivot(int i) { + reader.getValue(i, pivot); + pivotDoc = reader.getDocID(i); + } + + @Override + protected int comparePivot(int j) { + if (k < packedBytesLength) { + reader.getValue(j, scratch); + int cmp = Arrays.compareUnsigned(pivot.bytes, pivot.offset + k, pivot.offset + k + packedBytesLength - k, scratch.bytes, scratch.offset + k, scratch.offset + k + packedBytesLength - k); + if (cmp != 0) { + return cmp; + } + } + return pivotDoc - reader.getDocID(j); + } + }; + } + + }.sort(from, to); + } + + /** Sort points on the given dimension. */ + public static void sortByDim(int sortedDim, int bytesPerDim, int[] commonPrefixLengths, + MutablePointValues reader, int from, int to, + BytesRef scratch1, BytesRef scratch2) { + + // No need for a fancy radix sort here, this is called on the leaves only so + // there are not many values to sort + final int offset = sortedDim * bytesPerDim + commonPrefixLengths[sortedDim]; + final int numBytesToCompare = bytesPerDim - commonPrefixLengths[sortedDim]; + new IntroSorter() { + + final BytesRef pivot = scratch1; + int pivotDoc = -1; + + @Override + protected void swap(int i, int j) { + reader.swap(i, j); + } + + @Override + protected void setPivot(int i) { + reader.getValue(i, pivot); + pivotDoc = reader.getDocID(i); + } + + @Override + protected int comparePivot(int j) { + reader.getValue(j, scratch2); + int cmp = Arrays.compareUnsigned(pivot.bytes, pivot.offset + offset, pivot.offset + offset + numBytesToCompare, scratch2.bytes, scratch2.offset + offset, scratch2.offset + offset + numBytesToCompare); + if (cmp == 0) { + cmp = pivotDoc - reader.getDocID(j); + } + return cmp; + } + }.sort(from, to); + } + + /** Partition points around {@code mid}. All values on the left must be less + * than or equal to it and all values on the right must be greater than or + * equal to it. */ + public static void partition(int maxDoc, int splitDim, int bytesPerDim, int commonPrefixLen, + MutablePointValues reader, int from, int to, int mid, + BytesRef scratch1, BytesRef scratch2) { + final int offset = splitDim * bytesPerDim + commonPrefixLen; + final int cmpBytes = bytesPerDim - commonPrefixLen; + final int bitsPerDocId = PackedInts.bitsRequired(maxDoc - 1); + new RadixSelector(cmpBytes + (bitsPerDocId + 7) / 8) { + + @Override + protected Selector getFallbackSelector(int k) { + return new IntroSelector() { + + final BytesRef pivot = scratch1; + int pivotDoc; + + @Override + protected void swap(int i, int j) { + reader.swap(i, j); + } + + @Override + protected void setPivot(int i) { + reader.getValue(i, pivot); + pivotDoc = reader.getDocID(i); + } + + @Override + protected int comparePivot(int j) { + if (k < cmpBytes) { + reader.getValue(j, scratch2); + int cmp = Arrays.compareUnsigned(pivot.bytes, pivot.offset + offset + k, pivot.offset + offset + k + cmpBytes - k, scratch2.bytes, scratch2.offset + offset + k, scratch2.offset + offset + k + cmpBytes - k); + if (cmp != 0) { + return cmp; + } + } + return pivotDoc - reader.getDocID(j); + } + }; + } + + @Override + protected void swap(int i, int j) { + reader.swap(i, j); + } + + @Override + protected int byteAt(int i, int k) { + if (k < cmpBytes) { + return Byte.toUnsignedInt(reader.getByteAt(i, offset + k)); + } else { + final int shift = bitsPerDocId - ((k - cmpBytes + 1) << 3); + return (reader.getDocID(i) >>> Math.max(0, shift)) & 0xff; + } + } + }.select(from, to, mid); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ResettableByteBuffersIndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ResettableByteBuffersIndexOutput.java new file mode 100644 index 000000000000..3fb463217768 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/oldlucene/ResettableByteBuffersIndexOutput.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.oldlucene; + +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; + +/** + * A wrapper around byte-buffer backed IndexOutputs that adds several methods that interact with the underlying + * delegate. + */ + +public abstract class ResettableByteBuffersIndexOutput extends IndexOutput +{ + protected ResettableByteBuffersIndexOutput(String resourceDescription, String name, ByteOrder order, Version version) + { + super(resourceDescription, name, order, version); + } + + public abstract void copyTo(IndexOutput out) throws IOException; + + public abstract byte[] toArrayCopy(); + + public abstract int intSize(); + + public abstract void reset(); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/ColumnCompletionMarkerUtil.java b/src/java/org/apache/cassandra/index/sai/disk/v1/ColumnCompletionMarkerUtil.java deleted file mode 100644 index 6d3a3dd92795..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/ColumnCompletionMarkerUtil.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.io.IOException; - -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.io.IndexOutputWriter; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.lucene.store.IndexInput; -import org.apache.lucene.store.IndexOutput; - -/** - * Utility class for creating and reading the column completion marker, {@link IndexComponent#COLUMN_COMPLETION_MARKER}. - *

    - * The file has a header and a footer, as written by {@link SAICodecUtils#writeHeader(IndexOutput)} and - * {@link SAICodecUtils#writeFooter(IndexOutput)}. The only content of the file is a single byte indicating whether the - * column index is empty or not. If the index is empty the completion marker will be the only per-index component. - */ -public class ColumnCompletionMarkerUtil -{ - private static final byte EMPTY = (byte) 1; - private static final byte NOT_EMPTY = (byte) 0; - - /** - * Creates a column index completion marker for the specified column index, storing in it whether the index is empty. - * - * @param descriptor the index descriptor - * @param indexIdentifier the column index identifier - * @param isEmpty whether the index is empty - */ - public static void create(IndexDescriptor descriptor, IndexIdentifier indexIdentifier, boolean isEmpty) throws IOException - { - try (IndexOutputWriter output = descriptor.openPerIndexOutput(IndexComponent.COLUMN_COMPLETION_MARKER, indexIdentifier)) - { - SAICodecUtils.writeHeader(output); - output.writeByte(isEmpty ? EMPTY : NOT_EMPTY); - SAICodecUtils.writeFooter(output); - } - } - - /** - * Reads the column index completion marker and returns whether if the index is empty. - * - * @param descriptor the index descriptor - * @param indexIdentifier the column index identifier - * @return {@code true} if the index is empty, {@code false} otherwise. - */ - public static boolean isEmptyIndex(IndexDescriptor descriptor, IndexIdentifier indexIdentifier) throws IOException - { - try (IndexInput input = descriptor.openPerIndexInput(IndexComponent.COLUMN_COMPLETION_MARKER, indexIdentifier)) - { - SAICodecUtils.checkHeader(input); // consume header - return input.readByte() == EMPTY; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/DirectReaders.java b/src/java/org/apache/cassandra/index/sai/disk/v1/DirectReaders.java deleted file mode 100644 index 7b2353af6f41..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/DirectReaders.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.util.function.Supplier; - -import org.apache.lucene.index.CorruptIndexException; -import org.apache.lucene.store.IndexInput; - -public class DirectReaders -{ - public static void checkBitsPerValue(int valuesBitsPerValue, IndexInput input, Supplier source) throws CorruptIndexException - { - if (valuesBitsPerValue > 64) - { - String message = String.format("%s is corrupted: Bits per value for block offsets must be no more than 64 and is %d", source.get(), valuesBitsPerValue); - throw new CorruptIndexException(message, input); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/DocLengthsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/DocLengthsReader.java new file mode 100644 index 000000000000..2de1edcd4014 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/DocLengthsReader.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.Closeable; +import java.io.IOException; + +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; +import org.apache.cassandra.index.sai.disk.io.IndexInputReader; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; + +/** + * Reads the component written by {@link org.apache.cassandra.index.sai.disk.v1.trie.DocLengthsWriter}. + */ +@NotThreadSafe +public class DocLengthsReader implements Closeable +{ + private final IndexInputReader input; + private final long offset; + private final long upperBound; + + public DocLengthsReader(FileHandle fileHandle, SegmentMetadata.ComponentMetadata componentMetadata, Version version) + { + this.input = IndexFileUtils.instance().openInput(fileHandle); + // Version EC skipped the header in the doc lengths component metadata. + int headerAdjustment = Version.EC.equals(version) ? 0 : SAICodecUtils.headerSize(); + this.offset = componentMetadata.offset + headerAdjustment; + // The offset + length get you the end of the file for all relevant versions. + this.upperBound = componentMetadata.offset + componentMetadata.length; + } + + public int get(int rowID) throws IOException + { + // Account for header size in offset calculation + long position = offset + (long) rowID * Integer.BYTES; + if (position >= upperBound) + return 0; + input.seek(position); + return input.readInt(); + } + + @Override + public void close() throws IOException + { + FileUtils.close(input); + } +} + diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/IndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/IndexSearcher.java new file mode 100644 index 000000000000..aef2aac9f600 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/IndexSearcher.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import com.google.common.util.concurrent.Runnables; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.IndexSearcherContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.PostingListKeyRangeIterator; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.iterators.RowIdToPrimaryKeyWithSortKeyIterator; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithByteComparable; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RowIdWithMeta; +import org.apache.cassandra.index.sai.utils.SegmentOrdering; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.SortingIterator; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * Abstract reader for individual segments of an on-disk index. + * + * Accepts shared resources (token/offset file readers), and uses them to perform lookups against on-disk data + * structures. + */ +public abstract class IndexSearcher implements Closeable, SegmentOrdering +{ + protected final PrimaryKeyMap.Factory primaryKeyMapFactory; + final PerIndexFiles indexFiles; + protected final SegmentMetadata metadata; + protected final IndexContext indexContext; + + protected final ColumnFilter columnFilter; + + protected IndexSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) + { + this.primaryKeyMapFactory = primaryKeyMapFactory; + this.indexFiles = perIndexFiles; + this.metadata = segmentMetadata; + this.indexContext = indexContext; + columnFilter = ColumnFilter.selection(RegularAndStaticColumns.of(indexContext.getDefinition())); + } + + /** + * @return memory usage of underlying on-disk data structure + */ + public abstract long indexFileCacheSize(); + + /** + * Search on-disk index synchronously. Used for WHERE clause predicates, including BOUNDED_ANN. + * + * @param expression to filter on disk index + * @param keyRange key range specific in read command, used by ANN index + * @param queryContext to track per sstable cache and per query metrics + * @param defer create the iterator in a deferred state + * @return {@link KeyRangeIterator} that matches given expression + */ + public abstract KeyRangeIterator search(Expression expression, AbstractBounds keyRange, QueryContext queryContext, boolean defer) throws IOException; + + /** + * Order the rows by the given Orderer. Used for ORDER BY clause when + * (1) the WHERE predicate is either a partition restriction or a range restriction on the index, + * (2) there is no WHERE predicate, or + * (3) the planner determines it is better to post-filter the ordered results by the predicate. + * + * @param orderer the object containing the ordering logic + * @param slice optional predicate to get a slice of the index + * @param keyRange key range specific in read command, used by ANN index + * @param queryContext to track per sstable cache and per query metrics + * @param limit the initial num of rows to returned, used by ANN index. More rows may be requested if filtering throws away more than expected! + * @return an iterator of {@link PrimaryKeyWithSortKey} in score order + */ + public abstract CloseableIterator orderBy(Orderer orderer, Expression slice, AbstractBounds keyRange, QueryContext queryContext, int limit) throws IOException; + + /** + * Order the rows by the given Orderer. Used for ORDER BY clause when the WHERE predicates + * have been applied first, yielding a list of primary keys. Again, `limit` is a planner hint for ANN to determine + * the initial number of results returned, not a maximum. + */ + @Override + public CloseableIterator orderResultsBy(SSTableReader reader, QueryContext context, List keys, Orderer orderer, int limit) throws IOException + { + return SortingIterator.createCloseable( + orderer.getComparator(), + keys, + key -> + { + var slices = Slices.with(indexContext.comparator(), Slice.make(key.clustering())); + // TODO if we end up needing to read the row still, is it better to store offset and use reader.unfilteredAt? + try (var iter = reader.rowIterator(key.partitionKey(), slices, columnFilter, false, SSTableReadsListener.NOOP_LISTENER)) + { + if (iter.hasNext()) + { + var row = (Row) iter.next(); + assert !iter.hasNext(); + var cell = row.getCell(indexContext.getDefinition()); + if (cell == null) + return null; + // We encode the bytes to make sure they compare correctly. + var byteComparable = encode(cell.buffer()); + return new PrimaryKeyWithByteComparable(indexContext, reader.descriptor.id, key, byteComparable); + } + } + return null; + }, + Runnables.doNothing() + ); + } + + private ByteComparable encode(ByteBuffer input) + { + return indexContext.isLiteral() ? v -> ByteSource.preencoded(input) + : v -> TypeUtil.asComparableBytes(input, indexContext.getValidator(), v); + } + + protected KeyRangeIterator toPrimaryKeyIterator(PostingList postingList, QueryContext queryContext) throws IOException + { + if (postingList == null || postingList.size() == 0) + return KeyRangeIterator.empty(); + + IndexSearcherContext searcherContext = new IndexSearcherContext(metadata.minKey, + metadata.maxKey, + metadata.minSSTableRowId, + metadata.maxSSTableRowId, + metadata.segmentRowIdOffset, + queryContext, + postingList); + return new PostingListKeyRangeIterator(indexContext, primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(), searcherContext); + } + + protected CloseableIterator toMetaSortedIterator(CloseableIterator rowIdIterator, QueryContext queryContext) throws IOException + { + try + { + if (rowIdIterator == null || !rowIdIterator.hasNext()) + { + FileUtils.closeQuietly(rowIdIterator); + return CloseableIterator.emptyIterator(); + } + + IndexSearcherContext searcherContext = new IndexSearcherContext(metadata.minKey, + metadata.maxKey, + metadata.minSSTableRowId, + metadata.maxSSTableRowId, + metadata.segmentRowIdOffset, + queryContext, + null); + var pkm = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(); + return new RowIdToPrimaryKeyWithSortKeyIterator(indexContext, + pkm.getSSTableId(), + rowIdIterator, + pkm, + searcherContext); + } + catch (Throwable t) + { + FileUtils.closeQuietly(rowIdIterator); + throw t; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/IndexWriterConfig.java b/src/java/org/apache/cassandra/index/sai/disk/v1/IndexWriterConfig.java index fe9be1c98df1..2daffb2c3193 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/IndexWriterConfig.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/IndexWriterConfig.java @@ -21,11 +21,13 @@ import java.util.Map; import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; + import io.github.jbellis.jvector.vector.VectorSimilarityFunction; -import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.index.sai.disk.v1.vector.OptimizeFor; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.disk.vector.VectorSourceModel; +import org.apache.cassandra.index.sai.utils.TypeUtil; import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_VECTOR_SEARCH_MAX_TOP_K; @@ -34,52 +36,139 @@ */ public class IndexWriterConfig { - public static final String MAXIMUM_NODE_CONNECTIONS = "maximum_node_connections"; - public static final int MAXIMUM_MAXIMUM_NODE_CONNECTIONS = 512; - public static final int DEFAULT_MAXIMUM_NODE_CONNECTIONS = 16; + public static final String POSTING_LIST_LVL_MIN_LEAVES = "bkd_postings_min_leaves"; + public static final String POSTING_LIST_LVL_SKIP_OPTION = "bkd_postings_skip"; + + private static final int DEFAULT_POSTING_LIST_MIN_LEAVES = 64; + private static final int DEFAULT_POSTING_LIST_LVL_SKIP = 3; + public static final String MAXIMUM_NODE_CONNECTIONS = "maximum_node_connections"; public static final String CONSTRUCTION_BEAM_WIDTH = "construction_beam_width"; + public static final String NEIGHBORHOOD_OVERFLOW = "neighborhood_overflow"; + public static final String ALPHA = "alpha"; + public static final String ENABLE_HIERARCHY = "enable_hierarchy"; + public static final String SIMILARITY_FUNCTION = "similarity_function"; + public static final String SOURCE_MODEL = "source_model"; + public static final String OPTIMIZE_FOR = "optimize_for"; // unused, retained for compatibility w/ old schemas + + public static final int MAXIMUM_MAXIMUM_NODE_CONNECTIONS = 512; public static final int MAXIMUM_CONSTRUCTION_BEAM_WIDTH = 3200; + + public static final int DEFAULT_MAXIMUM_NODE_CONNECTIONS = 16; public static final int DEFAULT_CONSTRUCTION_BEAM_WIDTH = 100; + public static final boolean DEFAULT_ENABLE_HIERARCHY = false; + + public static final int MAX_TOP_K = SAI_VECTOR_SEARCH_MAX_TOP_K.getInt(); - public static final String SIMILARITY_FUNCTION = "similarity_function"; - public static final VectorSimilarityFunction DEFAULT_SIMILARITY_FUNCTION = VectorSimilarityFunction.COSINE; public static final String validSimilarityFunctions = Arrays.stream(VectorSimilarityFunction.values()) .map(Enum::name) .collect(Collectors.joining(", ")); - public static final String OPTIMIZE_FOR = "optimize_for"; - private static final OptimizeFor DEFAULT_OPTIMIZE_FOR = OptimizeFor.LATENCY; - private static final String validOptimizeFor = Arrays.stream(OptimizeFor.values()) - .map(Enum::name) - .collect(Collectors.joining(", ")); + private static final VectorSourceModel DEFAULT_SOURCE_MODEL = VectorSourceModel.OTHER; - public static final int MAX_TOP_K = SAI_VECTOR_SEARCH_MAX_TOP_K.getInt(); + private static final IndexWriterConfig EMPTY_CONFIG = new IndexWriterConfig(null, -1, -1, -1, -1, null, DEFAULT_SOURCE_MODEL); - private static final IndexWriterConfig EMPTY_CONFIG = new IndexWriterConfig(-1, -1, null, null); + /** + * Fully qualified index name, in the format {@literal ".

  • ."}. + */ + private final String indexName; - // The maximum number of outgoing connections a node can have in a graph. - private final int maximumNodeConnections; + /** + * Skip, or the sampling interval, for selecting a bkd tree level that is eligible for an auxiliary posting list. + * Sampling starts from 0, but bkd tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc (no + * level 0, because there is no node at level 0). + */ + private final int bkdPostingsSkip; - // The size of the beam search used when finding nearest neighbours. - private final int constructionBeamWidth; + /** + * Min. number of reachable leaves for a given node to be eligible for an auxiliary posting list. + */ + private final int bkdPostingsMinLeaves; - // Used to determine the search to determine the topK results. The score returned is used to order the topK results. + private final int maximumNodeConnections; + private final int constructionBeamWidth; private final VectorSimilarityFunction similarityFunction; + private final VectorSourceModel sourceModel; + + private final Float neighborhoodOverflow; // default varies for in memory/compaction build + private final Float alpha; // default varies for in memory/compaction build + private final boolean enableHierarchy; // defaults to false - private final OptimizeFor optimizeFor; + public IndexWriterConfig(String indexName, + int bkdPostingsSkip, + int bkdPostingsMinLeaves) + { + this(indexName, + bkdPostingsSkip, + bkdPostingsMinLeaves, + DEFAULT_MAXIMUM_NODE_CONNECTIONS, + DEFAULT_CONSTRUCTION_BEAM_WIDTH, + DEFAULT_SOURCE_MODEL.defaultSimilarityFunction, + DEFAULT_SOURCE_MODEL + ); + } - public IndexWriterConfig(int maximumNodeConnections, + public IndexWriterConfig(String indexName, + int bkdPostingsSkip, + int bkdPostingsMinLeaves, + int maximumNodeConnections, int constructionBeamWidth, VectorSimilarityFunction similarityFunction, - OptimizeFor optimizerFor) + VectorSourceModel sourceModel) { + this(indexName, bkdPostingsSkip, bkdPostingsMinLeaves, maximumNodeConnections, constructionBeamWidth, + similarityFunction, sourceModel, null, null, false); + } + + public IndexWriterConfig(String indexName, + int bkdPostingsSkip, + int bkdPostingsMinLeaves, + int maximumNodeConnections, + int constructionBeamWidth, + VectorSimilarityFunction similarityFunction, + VectorSourceModel sourceModel, + Float neighborhoodOverflow, + Float alpha, + boolean enableHierarchy) + { + this.indexName = indexName; + this.bkdPostingsSkip = bkdPostingsSkip; + this.bkdPostingsMinLeaves = bkdPostingsMinLeaves; this.maximumNodeConnections = maximumNodeConnections; this.constructionBeamWidth = constructionBeamWidth; this.similarityFunction = similarityFunction; - this.optimizeFor = optimizerFor; + this.sourceModel = sourceModel; + this.neighborhoodOverflow = neighborhoodOverflow; + this.alpha = alpha; + this.enableHierarchy = enableHierarchy; + } + + public String getIndexName() + { + return indexName; + } + + public int getBkdPostingsMinLeaves() + { + return bkdPostingsMinLeaves; + } + + public int getBkdPostingsSkip() + { + return bkdPostingsSkip; } + public int getAnnMaxDegree() + { + // For historical reasons (Lucene doubled the maximum node connections for its HNSW), + // maximumNodeConnections represents half of the graph degree, so double it + return 2 * maximumNodeConnections; + } + + /** you should probably use getAnnMaxDegree instead */ + /** @deprecated See https://github.com/datastax/cassandra/pull/1110 */ + @Deprecated(since = "5.0") + @VisibleForTesting public int getMaximumNodeConnections() { return maximumNodeConnections; @@ -95,31 +184,90 @@ public VectorSimilarityFunction getSimilarityFunction() return similarityFunction; } - public OptimizeFor getOptimizeFor() + public VectorSourceModel getSourceModel() + { + return sourceModel; + } + + public float getNeighborhoodOverflow(float defaultValue) + { + return neighborhoodOverflow == null ? defaultValue : neighborhoodOverflow; + } + + public float getAlpha(float defaultValue) + { + return alpha == null ? defaultValue : alpha; + } + + public boolean isHierarchyEnabled() { - return optimizeFor; + return enableHierarchy; } - public static IndexWriterConfig fromOptions(String indexName, IndexTermType indexTermType, Map options) + public static IndexWriterConfig fromOptions(String indexName, AbstractType type, Map options) { + int minLeaves = DEFAULT_POSTING_LIST_MIN_LEAVES; + int skip = DEFAULT_POSTING_LIST_LVL_SKIP; int maximumNodeConnections = DEFAULT_MAXIMUM_NODE_CONNECTIONS; int queueSize = DEFAULT_CONSTRUCTION_BEAM_WIDTH; - VectorSimilarityFunction similarityFunction = DEFAULT_SIMILARITY_FUNCTION; - OptimizeFor optimizeFor = DEFAULT_OPTIMIZE_FOR; + VectorSourceModel sourceModel = DEFAULT_SOURCE_MODEL; + VectorSimilarityFunction similarityFunction = sourceModel.defaultSimilarityFunction; // don't leave null in case no options at all are given + + Float neighborhoodOverflow = null; + Float alpha = null; + boolean enableHierarchy = DEFAULT_ENABLE_HIERARCHY; - if (options.get(MAXIMUM_NODE_CONNECTIONS) != null || - options.get(CONSTRUCTION_BEAM_WIDTH) != null || - options.get(SIMILARITY_FUNCTION) != null || - options.get(OPTIMIZE_FOR) != null) + if (options.get(POSTING_LIST_LVL_MIN_LEAVES) != null || options.get(POSTING_LIST_LVL_SKIP_OPTION) != null) { - if (!indexTermType.isVector()) - throw new InvalidRequestException(String.format("CQL type %s cannot have vector options", indexTermType.asCQL3Type())); + if (TypeUtil.isLiteral(type)) + { + throw new InvalidRequestException(String.format("CQL type %s cannot have auxiliary posting lists on index %s.", type.asCQL3Type(), indexName)); + } - if (options.containsKey(MAXIMUM_NODE_CONNECTIONS)) + for (Map.Entry entry : options.entrySet()) { - if (!CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.getBoolean()) - throw new InvalidRequestException(String.format("Maximum node connections cannot be set without enabling %s", CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.name())); + switch (entry.getKey()) + { + case POSTING_LIST_LVL_MIN_LEAVES: + { + minLeaves = Integer.parseInt(entry.getValue()); + + if (minLeaves < 1) + { + throw new InvalidRequestException(String.format("Posting list min. leaves count can't be less than 1 on index %s.", indexName)); + } + + break; + } + + case POSTING_LIST_LVL_SKIP_OPTION: + { + skip = Integer.parseInt(entry.getValue()); + + if (skip < 1) + { + throw new InvalidRequestException(String.format("Posting list skip can't be less than 1 on index %s.", indexName)); + } + + break; + } + } + } + } + else if (options.get(MAXIMUM_NODE_CONNECTIONS) != null || + options.get(CONSTRUCTION_BEAM_WIDTH) != null || + options.get(OPTIMIZE_FOR) != null || + options.get(SIMILARITY_FUNCTION) != null || + options.get(SOURCE_MODEL) != null || + options.get(NEIGHBORHOOD_OVERFLOW) != null || + options.get(ALPHA) != null || + options.get(ENABLE_HIERARCHY) != null) + { + if (!type.isVector()) + throw new InvalidRequestException(String.format("CQL type %s cannot have vector options", type.asCQL3Type())); + if (options.containsKey(MAXIMUM_NODE_CONNECTIONS)) + { try { maximumNodeConnections = Integer.parseInt(options.get(MAXIMUM_NODE_CONNECTIONS)); @@ -134,9 +282,6 @@ public static IndexWriterConfig fromOptions(String indexName, IndexTermType inde } if (options.containsKey(CONSTRUCTION_BEAM_WIDTH)) { - if (!CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.getBoolean()) - throw new InvalidRequestException(String.format("Construction beam width cannot be set without enabling %s", CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.name())); - try { queueSize = Integer.parseInt(options.get(CONSTRUCTION_BEAM_WIDTH)); @@ -149,6 +294,22 @@ public static IndexWriterConfig fromOptions(String indexName, IndexTermType inde if (queueSize <= 0 || queueSize > MAXIMUM_CONSTRUCTION_BEAM_WIDTH) throw new InvalidRequestException(String.format("Construction beam width for index %s cannot be <= 0 or > %s, was %s", indexName, MAXIMUM_CONSTRUCTION_BEAM_WIDTH, queueSize)); } + if (options.containsKey(SOURCE_MODEL)) + { + String option = options.get(SOURCE_MODEL).toUpperCase().replace("-", "_"); + try + { + sourceModel = VectorSourceModel.valueOf(option); + } + catch (IllegalArgumentException e) + { + var validSourceModels = Arrays.stream(VectorSourceModel.values()) + .map(Enum::name) + .collect(Collectors.joining(", ")); + throw new InvalidRequestException(String.format("source_model '%s' was not recognized for index %s. Valid values are: %s", + option, indexName, validSourceModels)); + } + } if (options.containsKey(SIMILARITY_FUNCTION)) { String option = options.get(SIMILARITY_FUNCTION).toUpperCase(); @@ -162,21 +323,66 @@ public static IndexWriterConfig fromOptions(String indexName, IndexTermType inde option, indexName, validSimilarityFunctions)); } } - if (options.containsKey(OPTIMIZE_FOR)) + else + { + similarityFunction = sourceModel.defaultSimilarityFunction; + } + + if (options.containsKey(NEIGHBORHOOD_OVERFLOW)) { - String option = options.get(OPTIMIZE_FOR).toUpperCase(); try { - optimizeFor = OptimizeFor.valueOf(option); + neighborhoodOverflow = Float.parseFloat(options.get(NEIGHBORHOOD_OVERFLOW)); + if (neighborhoodOverflow < 1.0f) + throw new InvalidRequestException(String.format("Neighborhood overflow for index %s must be >= 1.0, was %s", + indexName, neighborhoodOverflow)); } - catch (IllegalArgumentException e) + catch (NumberFormatException e) { - throw new InvalidRequestException(String.format("optimize_for '%s' was not recognized for index %s. Valid values are: %s", - option, indexName, validOptimizeFor)); + throw new InvalidRequestException(String.format("Neighborhood overflow %s is not a valid float for index %s", + options.get(NEIGHBORHOOD_OVERFLOW), indexName)); } } + + if (options.containsKey(ALPHA)) + { + try + { + alpha = Float.parseFloat(options.get(ALPHA)); + if (alpha <= 0) + throw new InvalidRequestException(String.format("Alpha for index %s must be > 0, was %s", + indexName, alpha)); + } + catch (NumberFormatException e) + { + throw new InvalidRequestException(String.format("Alpha %s is not a valid float for index %s", + options.get(ALPHA), indexName)); + } + } + + if (options.containsKey(ENABLE_HIERARCHY)) + { + String value = options.get(ENABLE_HIERARCHY).toLowerCase(); + if (!value.equals("true") && !value.equals("false")) + throw new InvalidRequestException(String.format("Enable hierarchy must be 'true' or 'false' for index %s, was '%s'", + indexName, value)); + enableHierarchy = Boolean.parseBoolean(value); + } } - return new IndexWriterConfig(maximumNodeConnections, queueSize, similarityFunction, optimizeFor); + + return new IndexWriterConfig(indexName, skip, minLeaves, maximumNodeConnections, queueSize, similarityFunction, sourceModel, neighborhoodOverflow, alpha, enableHierarchy); + } + + public static IndexWriterConfig defaultConfig(String indexName) + { + return new IndexWriterConfig(indexName, + DEFAULT_POSTING_LIST_LVL_SKIP, + DEFAULT_POSTING_LIST_MIN_LEAVES, + DEFAULT_MAXIMUM_NODE_CONNECTIONS, + DEFAULT_CONSTRUCTION_BEAM_WIDTH, + DEFAULT_SOURCE_MODEL.defaultSimilarityFunction, + DEFAULT_SOURCE_MODEL + ); } public static IndexWriterConfig emptyConfig() @@ -187,10 +393,15 @@ public static IndexWriterConfig emptyConfig() @Override public String toString() { - return String.format("IndexWriterConfig{%s=%d, %s=%d, %s=%s, %s=%s}", + return String.format("IndexWriterConfig{%s=%d, %s=%d, %s=%d, %s=%d, %s=%s, %s=%s, %s=%f, %s=%f, %s=%b}", + POSTING_LIST_LVL_SKIP_OPTION, bkdPostingsSkip, + POSTING_LIST_LVL_MIN_LEAVES, bkdPostingsMinLeaves, MAXIMUM_NODE_CONNECTIONS, maximumNodeConnections, CONSTRUCTION_BEAM_WIDTH, constructionBeamWidth, SIMILARITY_FUNCTION, similarityFunction, - OPTIMIZE_FOR, optimizeFor); + SOURCE_MODEL, sourceModel, + NEIGHBORHOOD_OVERFLOW, neighborhoodOverflow, + ALPHA, alpha, + ENABLE_HIERARCHY, enableHierarchy); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/InvertedIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/InvertedIndexSearcher.java new file mode 100644 index 000000000000..07f090c33192 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/InvertedIndexSearcher.java @@ -0,0 +1,384 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.base.MoreObjects; +import org.apache.cassandra.index.sai.plan.QueryController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.FeatureNeedsIndexRebuildException; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.TermsIterator; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.postings.IntersectingPostingList; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners; +import org.apache.cassandra.index.sai.metrics.QueryEventListener; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.BM25Utils; +import org.apache.cassandra.index.sai.utils.BM25Utils.EagerDocTF; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithScore; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RowIdWithByteComparable; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +import static org.apache.cassandra.index.sai.disk.PostingList.END_OF_STREAM; +import static org.apache.cassandra.index.sai.disk.v1.SegmentMetadata.INVALID_TOTAL_TERM_COUNT; + +/** + * Executes {@link Expression}s against the trie-based terms dictionary for an individual index segment. + */ +public class InvertedIndexSearcher extends IndexSearcher +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final TermsReader reader; + private final QueryEventListener.TrieIndexEventListener perColumnEventListener; + private final Version version; + private final boolean filterRangeResults; + private final SSTableReader sstable; + private final SegmentMetadata.ComponentMetadata docLengthsMeta; + private final FileHandle docLengths; + private final long segmentRowIdOffset; + + protected InvertedIndexSearcher(SSTableContext sstableContext, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext, + Version version, + boolean filterRangeResults) throws IOException + { + super(sstableContext.primaryKeyMapFactory(), perIndexFiles, segmentMetadata, indexContext); + this.sstable = sstableContext.sstable; + + long root = metadata.getIndexRoot(IndexComponentType.TERMS_DATA); + assert root >= 0; + + this.version = version; + this.filterRangeResults = filterRangeResults; + perColumnEventListener = (QueryEventListener.TrieIndexEventListener)indexContext.getColumnQueryMetrics(); + this.segmentRowIdOffset = segmentMetadata.segmentRowIdOffset; + this.docLengthsMeta = segmentMetadata.componentMetadatas.getOptional(IndexComponentType.DOC_LENGTHS); + this.docLengths = docLengthsMeta == null ? null : indexFiles.docLengths(); + + Map map = metadata.componentMetadatas.get(IndexComponentType.TERMS_DATA).attributes; + String footerPointerString = map.get(SAICodecUtils.FOOTER_POINTER); + long footerPointer = footerPointerString == null ? -1 : Long.parseLong(footerPointerString); + + var perIndexComponents = perIndexFiles.usedPerIndexComponents(); + reader = new TermsReader(indexContext, + indexFiles.termsData(), + perIndexComponents.byteComparableVersionFor(IndexComponentType.TERMS_DATA), + indexFiles.postingLists(), + root, + footerPointer, + version); + } + + @Override + public long indexFileCacheSize() + { + // trie has no pre-allocated memory. + // TODO: Is this still the case now the trie isn't using the chunk cache? + return 0; + } + + @SuppressWarnings("resource") + public KeyRangeIterator search(Expression exp, AbstractBounds keyRange, QueryContext context, boolean defer) throws IOException + { + PostingList postingList = searchPosting(exp, context); + return toPrimaryKeyIterator(postingList, context); + } + + private PostingList searchPosting(Expression exp, QueryContext context) + { + if (logger.isTraceEnabled()) + logger.trace(indexContext.logMessage("Searching on expression '{}'..."), exp); + + // We use the version to encode the search boundaries for the trie to ensure we use version appropriate bounds. + if (exp.getOp().isEquality() || exp.getOp() == Expression.Op.MATCH) + { + // Value is encoded in non-byte-comparable-version-specific fixed-length format. + final ByteComparable term = version.onDiskFormat().encodeForTrie(exp.lower.value.encoded, indexContext.getValidator()); + QueryEventListener.TrieIndexEventListener listener = MulticastQueryEventListeners.of(context, perColumnEventListener); + return reader.exactMatch(term, listener, context); + } + else if (exp.getOp() == Expression.Op.RANGE) + { + QueryEventListener.TrieIndexEventListener listener = MulticastQueryEventListeners.of(context, perColumnEventListener); + var lower = exp.getEncodedLowerBoundByteComparable(version); + var upper = exp.getEncodedUpperBoundByteComparable(version); + return reader.rangeMatch(filterRangeResults ? exp : null, lower, upper, listener, context); + } + throw new IllegalArgumentException(indexContext.logMessage("Unsupported expression: " + exp)); + } + + private Cell readColumn(SSTableReader sstable, PrimaryKey primaryKey) + { + var dk = primaryKey.partitionKey(); + var slices = Slices.with(indexContext.comparator(), Slice.make(primaryKey.clustering())); + try (var rowIterator = sstable.rowIterator(dk, slices, columnFilter, false, SSTableReadsListener.NOOP_LISTENER)) + { + // primaryKey might not belong to this sstable, thus the iterator will be empty + if (rowIterator.isEmpty()) + return null; + var unfiltered = rowIterator.next(); + assert unfiltered.isRow() : unfiltered; + Row row = (Row) unfiltered; + return row.getCell(indexContext.getDefinition()); + } + } + + @Override + public CloseableIterator orderBy(Orderer orderer, Expression slice, AbstractBounds keyRange, QueryContext queryContext, int limit) throws IOException + { + if (!orderer.isBM25()) + { + var iter = new RowIdWithTermsIterator(reader.allTerms(orderer.isAscending())); + return toMetaSortedIterator(iter, queryContext); + } + if (docLengthsMeta == null) + { + throw new FeatureNeedsIndexRebuildException(String.format(QueryController.INDEX_VERSION_DOES_NOT_SUPPORT_BM25, + indexContext.getIndexName())); + } + + // find documents that match each term + var queryTerms = orderer.getQueryTerms(); + var postingLists = queryTerms.stream() + .collect(Collectors.toMap(Function.identity(), term -> + { + var encodedTerm = version.onDiskFormat().encodeForTrie(term, indexContext.getValidator()); + var listener = MulticastQueryEventListeners.of(queryContext, perColumnEventListener); + var postings = reader.exactMatch(encodedTerm, listener, queryContext); + return postings == null ? PostingList.EMPTY : postings; + })); + + var pkm = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(); + var merged = IntersectingPostingList.intersect(postingLists); + var docLengthsReader = new DocLengthsReader(docLengths, docLengthsMeta, version); + + // Wrap the iterator with resource management + var it = new AbstractIterator() { // Anonymous class extends AbstractIterator + private boolean closed; + + @Override + protected BM25Utils.DocTF computeNext() + { + try + { + int rowId = merged.nextPosting(); + if (rowId == PostingList.END_OF_STREAM) + return endOfData(); + // Reads from disk. + int docLength = docLengthsReader.get(rowId); // segment-local rowid + // We defer creating the primary key because it reads the token from disk, which is only needed + // for the top rows just before they are materialized from disk, so we wait until after scoring + // and sorting to read the token. + return new LazyDocTF(pkm, segmentRowIdOffset + rowId, docLength, merged.frequencies()); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + public void close() + { + if (closed) return; + closed = true; + FileUtils.closeQuietly(pkm, merged, docLengthsReader); + } + }; + return BM25Utils.computeScores(it, + queryTerms, + orderer.bm25stats, + indexContext, + sstable.descriptor.id, + metadata.totalTermCount == INVALID_TOTAL_TERM_COUNT); + } + + @Override + public CloseableIterator orderResultsBy(SSTableReader reader, QueryContext queryContext, List keys, Orderer orderer, int limit) throws IOException + { + if (!orderer.isBM25()) + return super.orderResultsBy(reader, queryContext, keys, orderer, limit); + + if (docLengthsMeta == null) + { + throw new InvalidRequestException(String.format(QueryController.INDEX_VERSION_DOES_NOT_SUPPORT_BM25, + indexContext.getIndexName())); + } + + var queryTerms = orderer.getQueryTerms(); + var analyzer = indexContext.getAnalyzerFactory().create(); + var it = keys.stream() + .map(pk -> EagerDocTF.createFromDocument(pk, readColumn(sstable, pk), analyzer, queryTerms)) + .filter(Objects::nonNull) + .iterator(); + return BM25Utils.computeScores(CloseableIterator.wrap(it), + queryTerms, + orderer.bm25stats, + indexContext, + sstable.descriptor.id, + metadata.totalTermCount == INVALID_TOTAL_TERM_COUNT); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("indexContext", indexContext) + .toString(); + } + + @Override + public void close() + { + FileUtils.closeQuietly(reader, docLengths); + } + + /** + * An iterator that iterates over a source + */ + private static class RowIdWithTermsIterator extends AbstractIterator + { + private final TermsIterator source; + private PostingList currentPostingList = PostingList.EMPTY; + private ByteComparable currentTerm = null; + + RowIdWithTermsIterator(TermsIterator source) + { + this.source = source; + } + + @Override + protected RowIdWithByteComparable computeNext() + { + try + { + while (true) + { + long nextPosting = currentPostingList.nextPosting(); + if (nextPosting != END_OF_STREAM) + return new RowIdWithByteComparable(Math.toIntExact(nextPosting), currentTerm); + + if (!source.hasNext()) + return endOfData(); + + currentTerm = source.next(); + FileUtils.closeQuietly(currentPostingList); + currentPostingList = source.postings(); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public void close() + { + FileUtils.closeQuietly(source, currentPostingList); + } + } + + /** + * A {@link BM25Utils.DocTF} that is lazy in that it does not create the {@link PrimaryKey} until it is required. + */ + private static class LazyDocTF implements BM25Utils.DocTF + { + private final PrimaryKeyMap pkm; + private final long sstableRowId; + private final int docLength; + private final Map frequencies; + + LazyDocTF(PrimaryKeyMap pkm, long sstableRowId, int docLength, Map frequencies) + { + this.pkm = pkm; + this.sstableRowId = sstableRowId; + this.docLength = docLength; + this.frequencies = frequencies; + } + + @Override + public int getTermFrequency(ByteBuffer term) + { + return frequencies.getOrDefault(term, 0); + } + + @Override + public int termCount() + { + return docLength; + } + + @Override + public PrimaryKeyWithSortKey primaryKey(IndexContext context, Memtable source, float score) + { + // Only sstables use this class, so this should never be called + throw new UnsupportedOperationException(); + } + + @Override + public PrimaryKeyWithSortKey primaryKey(IndexContext context, SSTableId source, float score) + { + // We can eagerly get the token now, even though it might not technically be required until we know + // we have the best score. (Perhaps this should be lazy too?) + // BM25 scores are exact. + return new PrimaryKeyWithScore(context, source, pkm.primaryKeyFromRowId(sstableRowId), score, false); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/KDTreeIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/KDTreeIndexSearcher.java new file mode 100644 index 000000000000..5911f2351014 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/KDTreeIndexSearcher.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; + +import com.google.common.base.MoreObjects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.v1.kdtree.BKDReader; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners; +import org.apache.cassandra.index.sai.metrics.QueryEventListener; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RowIdWithByteComparable; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.AbstractGuavaIterator; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +import static org.apache.cassandra.index.sai.disk.v1.kdtree.BKDQueries.bkdQueryFrom; + +/** + * Executes {@link Expression}s against the kd-tree for an individual index segment. + */ +public class KDTreeIndexSearcher extends IndexSearcher +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final BKDReader bkdReader; + private final QueryEventListener.BKDIndexEventListener perColumnEventListener; + + KDTreeIndexSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) throws IOException + { + super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, indexContext); + + final long bkdPosition = metadata.getIndexRoot(IndexComponentType.KD_TREE); + assert bkdPosition >= 0; + final long postingsPosition = metadata.getIndexRoot(IndexComponentType.KD_TREE_POSTING_LISTS); + assert postingsPosition >= 0; + + bkdReader = new BKDReader(indexContext, + indexFiles.kdtree(), + bkdPosition, + indexFiles.kdtreePostingLists(), + postingsPosition); + perColumnEventListener = (QueryEventListener.BKDIndexEventListener)indexContext.getColumnQueryMetrics(); + } + + @Override + public long indexFileCacheSize() + { + return bkdReader.memoryUsage(); + } + + @Override + public KeyRangeIterator search(Expression exp, AbstractBounds keyRange, QueryContext context, boolean defer) throws IOException + { + PostingList postingList = searchPosting(exp, context); + return toPrimaryKeyIterator(postingList, context); + } + + private PostingList searchPosting(Expression exp, QueryContext context) + { + if (logger.isTraceEnabled()) + logger.trace(indexContext.logMessage("Searching on expression '{}'..."), exp); + + if (exp.getOp().isEqualityOrRange()) + { + final BKDReader.IntersectVisitor query = bkdQueryFrom(exp, bkdReader.getNumDimensions(), bkdReader.getBytesPerDimension()); + QueryEventListener.BKDIndexEventListener listener = MulticastQueryEventListeners.of(context, perColumnEventListener); + return bkdReader.intersect(query, listener, context); + } + else + { + throw new IllegalArgumentException(indexContext.logMessage("Unsupported expression during index query: " + exp)); + } + } + + public CloseableIterator orderBy(Orderer orderer, Expression slice, AbstractBounds keyRange, QueryContext queryContext, int limit) throws IOException + { + var query = slice != null && slice.getOp().isEqualityOrRange() + ? bkdQueryFrom(slice, bkdReader.getNumDimensions(), bkdReader.getBytesPerDimension()) + : null; + var direction = orderer.isAscending() ? BKDReader.Direction.FORWARD : BKDReader.Direction.BACKWARD; + var iter = new RowIdIterator(bkdReader.iteratorState(direction, query)); + return toMetaSortedIterator(iter, queryContext); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("indexContext", indexContext) + .add("count", bkdReader.getPointCount()) + .add("numDimensions", bkdReader.getNumDimensions()) + .add("bytesPerDimension", bkdReader.getBytesPerDimension()) + .toString(); + } + + @Override + public void close() + { + bkdReader.close(); + } + + private static class RowIdIterator extends AbstractGuavaIterator implements CloseableIterator + { + private final BKDReader.IteratorState iterator; + RowIdIterator(BKDReader.IteratorState iterator) + { + this.iterator = iterator; + } + + @Override + public RowIdWithByteComparable computeNext() + { + if (!iterator.hasNext()) + return endOfData(); + + var segmentRowId = iterator.next(); + // We have to copy scratch to prevent it from being overwritten by the next call to computeNext() + var indexValue = new byte[iterator.scratch.length]; + System.arraycopy(iterator.scratch, 0, indexValue, 0, iterator.scratch.length); + // We store the indexValue in an already encoded format, so we use the preencoded method here + // to avoid re-encoding it. + return new RowIdWithByteComparable(Math.toIntExact(segmentRowId), + ByteComparable.preencoded(TypeUtil.BYTE_COMPARABLE_VERSION, + indexValue)); + } + + @Override + public void close() + { + FileUtils.closeQuietly(iterator); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java b/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java index 044e41b23ec7..e13c14c8e9bf 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java @@ -37,13 +37,24 @@ public interface LongArray extends Closeable */ long length(); + /** - * Using the given value returns the first index corresponding to the value. + * @param targetValue Value to look up. Must not be smaller than previous value queried + * (the method is stateful) + * @return The index of the first value equal to or greater than the target, + * or negative value if target value is greater than all values + */ + long ceilingIndex(long targetValue); + + /** + * Using the target value returns the first index corresponding to the value. * - * @param value Value to lookup, and it must not be smaller than previous value - * @return The index of the given value or negative value if target value is greater than all values + * @param targetValue Value to lookup, and it must not be smaller than previous value + * @return The index of the target value, + * or negative index for a bigger value closest to the target, + * or Long.MIN_VALUE if target value is greater than all values */ - long indexOf(long value); + long indexOf(long targetValue); @Override default void close() throws IOException { } @@ -75,10 +86,17 @@ public long length() } @Override - public long indexOf(long value) + public long ceilingIndex(long targetValue) + { + open(); + return longArray.ceilingIndex(targetValue); + } + + @Override + public long indexOf(long targetValue) { open(); - return longArray.indexOf(value); + return longArray.indexOf(targetValue); } @Override diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java index 846f65505bfc..453199a7dcfb 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java @@ -18,228 +18,263 @@ package org.apache.cassandra.index.sai.disk.v1; import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.concurrent.TimeUnit; import com.google.common.base.Stopwatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.carrotsearch.hppc.LongArrayList; +import org.agrona.collections.Int2IntHashMap; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter; -import org.apache.cassandra.index.sai.disk.RowMapping; -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.v1.bbtree.NumericIndexWriter; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentWriter; -import org.apache.cassandra.index.sai.disk.v1.trie.LiteralIndexWriter; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.MemtableTermsIterator; +import org.apache.cassandra.index.sai.disk.PerIndexWriter; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.kdtree.ImmutableOneDimPointValues; +import org.apache.cassandra.index.sai.disk.v1.kdtree.NumericIndexWriter; +import org.apache.cassandra.index.sai.disk.v1.trie.InvertedIndexWriter; +import org.apache.cassandra.index.sai.disk.vector.VectorMemtableIndex; import org.apache.cassandra.index.sai.memory.MemtableIndex; -import org.apache.cassandra.index.sai.memory.MemtableTermsIterator; -import org.apache.cassandra.index.sai.metrics.IndexMetrics; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.memory.RowMapping; +import org.apache.cassandra.index.sai.memory.TrieMemoryIndex; +import org.apache.cassandra.index.sai.memory.TrieMemtableIndex; import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** * Column index writer that flushes indexed data directly from the corresponding Memtable index, without buffering index * data in memory. */ -public class MemtableIndexWriter implements PerColumnIndexWriter +public class MemtableIndexWriter implements PerIndexWriter { - private static final Logger logger = LoggerFactory.getLogger(MemtableIndexWriter.class); - private static final int NO_ROWS = -1; - - private final IndexDescriptor indexDescriptor; - private final IndexTermType indexTermType; - private final IndexIdentifier indexIdentifier; - private final IndexMetrics indexMetrics; - private final MemtableIndex memtable; - private final RowMapping rowMapping; + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private PrimaryKey minKey; - private PrimaryKey maxKey; - private long maxSSTableRowId = NO_ROWS; - private int rowCount; + private final IndexComponents.ForWrite perIndexComponents; + private final MemtableIndex memtableIndex; + private final PrimaryKey.Factory pkFactory; + private final RowMapping rowMapping; - public MemtableIndexWriter(MemtableIndex memtable, - IndexDescriptor indexDescriptor, - IndexTermType indexTermType, - IndexIdentifier indexIdentifier, - IndexMetrics indexMetrics, + public MemtableIndexWriter(MemtableIndex memtableIndex, + IndexComponents.ForWrite perIndexComponents, + PrimaryKey.Factory pkFactory, RowMapping rowMapping) { assert rowMapping != null && rowMapping != RowMapping.DUMMY : "Row mapping must exist during FLUSH."; - this.indexDescriptor = indexDescriptor; - this.indexTermType = indexTermType; - this.indexIdentifier = indexIdentifier; - this.indexMetrics = indexMetrics; - this.memtable = memtable; + this.perIndexComponents = perIndexComponents; + this.memtableIndex = memtableIndex; + this.pkFactory = pkFactory; this.rowMapping = rowMapping; } + @Override + public IndexContext indexContext() + { + return perIndexComponents.context(); + } + + @Override + public IndexComponents.ForWrite writtenComponents() + { + return perIndexComponents; + } + @Override public void addRow(PrimaryKey key, Row row, long sstableRowId) { // Memtable indexes are flushed directly to disk with the aid of a mapping between primary // keys and row IDs in the flushing SSTable. This writer, therefore, does nothing in - // response to the flushing of individual rows except for keeping index-specific statistics. - boolean isStatic = indexTermType.columnMetadata().isStatic(); - boolean isPartitionKey = indexTermType.columnMetadata().isPartitionKey(); - - // Indexes on static columns should only track static rows, and indexes on non-static columns - // should only track non-static rows. (Within a partition, the row ID for a static row will always - // come before any non-static row.) The only exception to this is indexes on partition key elements. - if ((key.kind() == PrimaryKey.Kind.STATIC && (isStatic || isPartitionKey)) || key.kind() != PrimaryKey.Kind.STATIC && !isStatic) - { - if (minKey == null) - minKey = key; - - maxKey = key; - rowCount++; - maxSSTableRowId = Math.max(maxSSTableRowId, sstableRowId); - } + // response to the flushing of individual rows. } @Override public void abort(Throwable cause) { - if (cause == null) - // This commonly occurs when a Memtable has no rows to flush, and is harmless: - logger.debug(indexIdentifier.logMessage("Aborting index memtable flush for {}..."), indexDescriptor.sstableDescriptor); - else - logger.warn(indexIdentifier.logMessage("Aborting index memtable flush for {}..."), indexDescriptor.sstableDescriptor, cause); + logger.warn(perIndexComponents.logMessage("Aborting index memtable flush for {}..."), perIndexComponents.descriptor(), cause); + perIndexComponents.forceDeleteAllComponents(); + } - indexDescriptor.deleteColumnIndex(indexTermType, indexIdentifier); + @Override + public void onSSTableWriterSwitched(Stopwatch stopwatch) throws IOException + { + // no-op for memtable index where all terms are already inside memory index, we can't get rid of memory index + // until full flush are completed } @Override public void complete(Stopwatch stopwatch) throws IOException { - assert rowMapping.isComplete() : "Cannot complete the memtable index writer because the row mapping is not complete"; - long start = stopwatch.elapsed(TimeUnit.MILLISECONDS); try { - if (maxSSTableRowId == -1 || memtable == null || memtable.isEmpty()) + if (!rowMapping.hasRows() || (memtableIndex == null) || memtableIndex.isEmpty()) { - logger.debug(indexIdentifier.logMessage("No indexed rows to flush from SSTable {}."), indexDescriptor.sstableDescriptor); - // Write a completion marker even though we haven't written anything to the index, + logger.debug(perIndexComponents.logMessage("No indexed rows to flush from SSTable {}."), perIndexComponents.descriptor()); + // Write a completion marker even though we haven't written anything to the index // so we won't try to build the index again for the SSTable - ColumnCompletionMarkerUtil.create(indexDescriptor, indexIdentifier, true); - + perIndexComponents.markComplete(); return; } - if (indexTermType.isVector()) + final DecoratedKey minKey = rowMapping.minKey.partitionKey(); + final DecoratedKey maxKey = rowMapping.maxKey.partitionKey(); + + if (indexContext().isVector()) { - flushVectorIndex(start, stopwatch); + flushVectorIndex(minKey, maxKey, start, stopwatch); } else { - final Iterator> iterator = rowMapping.merge(memtable); - - long cellCount = 0; - if (iterator.hasNext()) + var iterator = rowMapping.merge(memtableIndex); + try (MemtableTermsIterator terms = new MemtableTermsIterator(memtableIndex.getMinTerm(), memtableIndex.getMaxTerm(), iterator)) { - try (MemtableTermsIterator terms = new MemtableTermsIterator(memtable.getMinTerm(), memtable.getMaxTerm(), iterator)) - { - cellCount = flush(terms); - } + long cellCount = flush(minKey, maxKey, indexContext().getValidator(), terms, rowMapping.maxSegmentRowId); + + completeIndexFlush(cellCount, start, stopwatch); } - completeIndexFlush(cellCount, start, stopwatch); } } catch (Throwable t) { - logger.error(indexIdentifier.logMessage("Error while flushing index {}"), t.getMessage(), t); - indexMetrics.memtableIndexFlushErrors.inc(); + logger.error(perIndexComponents.logMessage("Error while flushing index {}"), t.getMessage(), t); + indexContext().getIndexMetrics().ifPresent(m -> m.memtableIndexFlushErrors.inc()); throw t; } } - @Override - public void onSSTableWriterSwitched(Stopwatch stopwatch) throws IOException + private long flush(DecoratedKey minKey, DecoratedKey maxKey, AbstractType termComparator, MemtableTermsIterator terms, int maxSegmentRowId) throws IOException { - // no-op for memtable index where all terms are already inside memory index, we can't get rid of memory index - // until full flush are completed - } - - private long flush(MemtableTermsIterator terms) throws IOException - { - SegmentWriter writer = indexTermType.isLiteral() ? new LiteralIndexWriter(indexDescriptor, indexIdentifier) - : new NumericIndexWriter(indexDescriptor, - indexIdentifier, - indexTermType.fixedSizeOf()); - - SegmentMetadata.ComponentMetadataMap indexMetas = writer.writeCompleteSegment(terms); - long numRows = writer.getNumberOfRows(); + long numPostings; + long numRows; + long totalTermCount; + SegmentMetadataBuilder metadataBuilder = new SegmentMetadataBuilder(0, perIndexComponents); + SegmentMetadata.ComponentMetadataMap indexMetas; + if (TypeUtil.isLiteral(termComparator)) + { + try (InvertedIndexWriter writer = new InvertedIndexWriter(perIndexComponents, writeFrequencies())) + { + // Convert PrimaryKey->length map to rowId->length using RowMapping + var docLengths = new Int2IntHashMap(Integer.MIN_VALUE); + Arrays.stream(((TrieMemtableIndex) memtableIndex).getRangeIndexes()) + .map(TrieMemoryIndex.class::cast) + .forEach(trieMemoryIndex -> + trieMemoryIndex.getDocLengths().forEach((pk, length) -> { + int rowId = rowMapping.get(pk); + if (rowId >= 0) + docLengths.put(rowId, (int) length); + }) + ); + + indexMetas = writer.writeAll(metadataBuilder.intercept(terms), docLengths); + numPostings = writer.getPostingsCount(); + totalTermCount = docLengths.values().stream().mapToInt(i -> i).sum(); + numRows = docLengths.size(); + } + } + else + { + try (NumericIndexWriter writer = new NumericIndexWriter(perIndexComponents, + TypeUtil.fixedSizeOf(termComparator), + maxSegmentRowId, + // The number of postings is unknown. Also, there are stale entries in IndexMemtable. + Integer.MAX_VALUE, + indexContext().getIndexWriterConfig())) + { + ImmutableOneDimPointValues values = ImmutableOneDimPointValues.fromTermEnum(terms, termComparator); + indexMetas = writer.writeAll(metadataBuilder.intercept(values)); + numPostings = writer.getPointCount(); + numRows = numPostings; + totalTermCount = numPostings; + } + } // If no rows were written we need to delete any created column index components // so that the index is correctly identified as being empty (only having a completion marker) - if (numRows == 0) + if (numPostings == 0) { - indexDescriptor.deleteColumnIndex(indexTermType, indexIdentifier); + perIndexComponents.forceDeleteAllComponents(); return 0; } - // During index memtable flush, the data is sorted based on terms. - SegmentMetadata metadata = new SegmentMetadata(0, - numRows, - terms.getMinSSTableRowId(), terms.getMaxSSTableRowId(), - minKey, maxKey, - terms.getMinTerm(), terms.getMaxTerm(), - indexMetas); + metadataBuilder.setNumRows(numRows); + metadataBuilder.setTotalTermCount(totalTermCount); + metadataBuilder.setKeyRange(pkFactory.createPartitionKeyOnly(minKey), pkFactory.createPartitionKeyOnly(maxKey)); + metadataBuilder.setRowIdRange(terms.getMinSSTableRowId(), terms.getMaxSSTableRowId()); + metadataBuilder.setTermRange(terms.getMinTerm(), terms.getMaxTerm()); + metadataBuilder.setComponentsMetadata(indexMetas); + SegmentMetadata metadata = metadataBuilder.build(); - try (MetadataWriter metadataWriter = new MetadataWriter(indexDescriptor.openPerIndexOutput(IndexComponent.META, indexIdentifier))) + try (MetadataWriter writer = new MetadataWriter(perIndexComponents)) { - SegmentMetadata.write(metadataWriter, Collections.singletonList(metadata)); + SegmentMetadata.write(writer, Collections.singletonList(metadata)); } - return numRows; + return numPostings; } - private void flushVectorIndex(long startTime, Stopwatch stopwatch) throws IOException + private boolean writeFrequencies() { - SegmentMetadata.ComponentMetadataMap metadataMap = memtable.writeDirect(indexDescriptor, indexIdentifier, rowMapping::get); - completeIndexFlush(rowCount, startTime, stopwatch); + return indexContext().isAnalyzed() && indexContext().version().onOrAfter(Version.BM25_EARLIEST); + } - SegmentMetadata metadata = new SegmentMetadata(0, - rowCount, - 0, maxSSTableRowId, - minKey, maxKey, - ByteBufferUtil.bytes(0), ByteBufferUtil.bytes(0), - metadataMap); + private void flushVectorIndex(DecoratedKey minKey, DecoratedKey maxKey, long startTime, Stopwatch stopwatch) throws IOException + { + var vectorIndex = (VectorMemtableIndex) memtableIndex; - try (MetadataWriter writer = new MetadataWriter(indexDescriptor.openPerIndexOutput(IndexComponent.META, indexIdentifier))) + if (!vectorIndex.preFlush(rowMapping::get)) + { + logger.debug(perIndexComponents.logMessage("Whole graph is deleted. Skipping index flush for {}."), perIndexComponents.descriptor()); + perIndexComponents.markComplete(); + return; + } + + SegmentMetadata.ComponentMetadataMap metadataMap = vectorIndex.writeData(perIndexComponents); + + SegmentMetadata metadata = new SegmentMetadata(0, + rowMapping.size(), // TODO this isn't the right size metric. + 0, + rowMapping.maxSegmentRowId, + pkFactory.createPartitionKeyOnly(minKey), + pkFactory.createPartitionKeyOnly(maxKey), + ByteBufferUtil.bytes(0), // VSTODO by pass min max terms for vectors + ByteBufferUtil.bytes(0), // VSTODO by pass min max terms for vectors + null, + metadataMap, + rowMapping.size(), + perIndexComponents.version()); + + try (MetadataWriter writer = new MetadataWriter(perIndexComponents)) { SegmentMetadata.write(writer, Collections.singletonList(metadata)); } + + completeIndexFlush(rowMapping.size(), startTime, stopwatch); } private void completeIndexFlush(long cellCount, long startTime, Stopwatch stopwatch) throws IOException { - // create a completion marker indicating that the index is complete - ColumnCompletionMarkerUtil.create(indexDescriptor, indexIdentifier, cellCount == 0); + perIndexComponents.markComplete(); - indexMetrics.memtableIndexFlushCount.inc(); + indexContext().getIndexMetrics().ifPresent(m -> m.memtableIndexFlushCount.inc()); long elapsedTime = stopwatch.elapsed(TimeUnit.MILLISECONDS); - logger.debug(indexIdentifier.logMessage("Completed flushing {} memtable index cells to SSTable {}. Duration: {} ms. Total elapsed: {} ms"), + logger.debug(perIndexComponents.logMessage("Completed flushing {} memtable index cells to SSTable {}. Duration: {} ms. Total elapsed: {} ms"), cellCount, - indexDescriptor.sstableDescriptor, + perIndexComponents.descriptor(), elapsedTime - startTime, elapsedTime); - indexMetrics.memtableFlushCellsPerSecond.update((long) (cellCount * 1000.0 / Math.max(1, elapsedTime - startTime))); + indexContext().getIndexMetrics() + .ifPresent(m -> m.memtableFlushCellsPerSecond.update((long) (cellCount * 1000.0 / Math.max(1, elapsedTime - startTime)))); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java index e1c5c4f450d1..719da42276c5 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java @@ -18,81 +18,91 @@ package org.apache.cassandra.index.sai.disk.v1; import java.io.IOException; +import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; import javax.annotation.concurrent.NotThreadSafe; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; -import org.apache.lucene.store.ByteArrayDataInput; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; import org.apache.lucene.store.ChecksumIndexInput; -import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexInput; -import org.apache.lucene.util.BytesRef; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.oldlucene.ByteArrayIndexInput; @NotThreadSafe public class MetadataSource { - private final Map components; + private final Version version; + private final Map> components; - private MetadataSource(Map components) + private MetadataSource(Version version, Map> components) { + this.version = version; this.components = components; } - public static MetadataSource loadGroupMetadata(IndexDescriptor indexDescriptor) throws IOException + public static MetadataSource loadMetadata(IndexComponents.ForRead components) throws IOException { - return MetadataSource.load(indexDescriptor.openPerSSTableInput(IndexComponent.GROUP_META)); + IndexComponent.ForRead metadataComponent = components.get(components.metadataComponent()); + try (var input = metadataComponent.openCheckSummedInput()) + { + return MetadataSource.load(input, components.version(), metadataComponent.byteOrder()); + } } - public static MetadataSource loadColumnMetadata(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier) throws IOException + private static MetadataSource load(ChecksumIndexInput input, Version expectedVersion, ByteOrder order) throws IOException { - return MetadataSource.load(indexDescriptor.openPerIndexInput(IndexComponent.META, indexIdentifier)); - } + Map> components = new HashMap<>(); + Version version = SAICodecUtils.checkHeader(input); + if (version != expectedVersion) + throw new IllegalStateException("Unexpected version " + version + " in " + input + ", expected " + expectedVersion); - private static MetadataSource load(IndexInput indexInput) throws IOException - { - Map components = new HashMap<>(); + final int num = input.readInt(); - try (ChecksumIndexInput input = IndexFileUtils.getBufferedChecksumIndexInput(indexInput)) + for (int x = 0; x < num; x++) { - SAICodecUtils.checkHeader(input); - final int num = input.readInt(); - - for (int x = 0; x < num; x++) + if (input.length() == input.getFilePointer()) { - if (input.length() == input.getFilePointer()) - { - // we should never get here, because we always add footer to the file - throw new IllegalStateException("Unexpected EOF in " + input); - } - - final String name = input.readString(); - final int length = input.readInt(); - final byte[] bytes = new byte[length]; - input.readBytes(bytes, 0, length); - - components.put(name, new BytesRef(bytes)); + // we should never get here, because we always add footer to the file + throw new IllegalStateException("Unexpected EOF in " + input); } - SAICodecUtils.checkFooter(input); + final String name = input.readString(); + final int length = input.readInt(); + final byte[] bytes = new byte[length]; + input.readBytes(bytes, 0, length); + + components.put(name, () -> new ByteArrayIndexInput(name, bytes, order)); } - return new MetadataSource(components); + SAICodecUtils.checkFooter(input); + + return new MetadataSource(version, components); } - public DataInput get(String name) + public IndexInput get(IndexComponent component) { - BytesRef bytes = components.get(name); + return get(component.fileNamePart()); + } + + public IndexInput get(String name) + { + var supplier = components.get(name); - if (bytes == null) + if (supplier == null) { throw new IllegalArgumentException(String.format("Could not find component '%s'. Available properties are %s.", name, components.keySet())); } - return new ByteArrayDataInput(bytes.bytes); + return supplier.get(); + } + + public Version getVersion() + { + return version; } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java index 4b4dc7a98e99..d6fe73763cd9 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java @@ -19,41 +19,50 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; import javax.annotation.concurrent.NotThreadSafe; -import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput; -import org.apache.lucene.store.IndexOutput; +import org.apache.cassandra.index.sai.disk.ModernResettableByteBuffersIndexOutput; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.oldlucene.LegacyResettableByteBuffersIndexOutput; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; import org.apache.lucene.util.BytesRef; @NotThreadSafe public class MetadataWriter implements Closeable { + private final Version version; private final IndexOutput output; private final Map map = new HashMap<>(); - public MetadataWriter(IndexOutput output) + public MetadataWriter(IndexComponents.ForWrite components) throws IOException { - this.output = output; + this.version = components.version(); + this.output = components.addOrGet(components.metadataComponent()).openOutput(); } - public Builder builder(String name) + public IndexOutput builder(String name) { - return new Builder(name); - } - - public class Builder extends ResettableByteBuffersIndexOutput implements Closeable - { - private Builder(String name) - { - super(name); - } - - @Override - public void close() - { - map.put(getName(), new BytesRef(toArrayCopy(), 0, intSize())); + if (output.order() == ByteOrder.BIG_ENDIAN) { + return new LegacyResettableByteBuffersIndexOutput(1024, name, version) { + @Override + public void close() + { + map.put(getName(), new BytesRef(toArrayCopy(), 0, intSize())); + } + }; + } else { + return new ModernResettableByteBuffersIndexOutput(1024, name, version) { + @Override + public void close() + { + map.put(getName(), new BytesRef(toArrayCopy(), 0, intSize())); + } + }; } } @@ -82,4 +91,9 @@ public void close() throws IOException output.close(); } } + + public Version version() + { + return version; + } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/PartitionAwarePrimaryKeyFactory.java b/src/java/org/apache/cassandra/index/sai/disk/v1/PartitionAwarePrimaryKeyFactory.java new file mode 100644 index 000000000000..8faf11aeacd7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/PartitionAwarePrimaryKeyFactory.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1; + +import java.util.Objects; +import java.util.function.Supplier; + +import javax.annotation.concurrent.NotThreadSafe; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * A partition-aware {@link PrimaryKey.Factory}. This creates {@link PrimaryKey} instances that are + * sortable by {@link DecoratedKey} only. + */ +public class PartitionAwarePrimaryKeyFactory implements PrimaryKey.Factory +{ + @Override + public PrimaryKey createDeferred(Token token, Supplier primaryKeySupplier) + { + assert token != null; + return new PartitionAwarePrimaryKey(token, null, primaryKeySupplier); + } + + @Override + public PrimaryKey create(DecoratedKey partitionKey, Clustering clustering) + { + assert partitionKey != null; + return new PartitionAwarePrimaryKey(partitionKey.getToken(), partitionKey, null); + } + + @NotThreadSafe + private class PartitionAwarePrimaryKey implements PrimaryKey + { + private final Token token; + private DecoratedKey partitionKey; + private Supplier primaryKeySupplier; + + private PartitionAwarePrimaryKey(Token token, DecoratedKey partitionKey, Supplier primaryKeySupplier) + { + this.token = token; + this.partitionKey = partitionKey; + this.primaryKeySupplier = primaryKeySupplier; + } + + @Override + public PrimaryKey loadDeferred() + { + if (primaryKeySupplier != null) + { + assert partitionKey == null : "While applying existing primaryKeySupplier to load deferred primaryKey the partition key was unexpectedly already set"; + this.partitionKey = primaryKeySupplier.get().partitionKey(); + primaryKeySupplier = null; + assert this.token.equals(this.partitionKey.getToken()) : "Deferred primary key must contain the same token"; + } + return this; + } + + @Override + public PartitionAwarePrimaryKey forStaticRow() + { + return this; + } + + @Override + public Token token() + { + return this.token; + } + + @Override + public DecoratedKey partitionKey() + { + loadDeferred(); + return partitionKey; + } + + @Override + public Clustering clustering() + { + return Clustering.EMPTY; + } + + @Override + public ByteSource asComparableBytes(ByteComparable.Version version) + { + return asComparableBytes(version == ByteComparable.Version.LEGACY ? ByteSource.END_OF_STREAM : ByteSource.TERMINATOR, version, false); + } + + @Override + public ByteSource asComparableBytesMinPrefix(ByteComparable.Version version) + { + return asComparableBytes(ByteSource.LT_NEXT_COMPONENT, version, true); + } + + @Override + public ByteSource asComparableBytesMaxPrefix(ByteComparable.Version version) + { + return asComparableBytes(ByteSource.GT_NEXT_COMPONENT, version, true); + } + + private ByteSource asComparableBytes(int terminator, ByteComparable.Version version, boolean isPrefix) + { + // Note: Unlike row-aware primary keys the asComparable method in for + // partition aware keys is only used on the write side so we do not need + // to enforce deferred loading here. + ByteSource tokenComparable = token.asComparableBytes(version); + ByteSource keyComparable = partitionKey == null ? null + :ByteSource.of(partitionKey.getKey(), version); + + // prefix doesn't include null components + if (isPrefix) + { + if (keyComparable == null) + return ByteSource.withTerminator(terminator, tokenComparable); + else + return ByteSource.withTerminator(terminator, tokenComparable, keyComparable); + } + return ByteSource.withTerminator(terminator, tokenComparable, keyComparable, null); + } + + @Override + public long ramBytesUsed() + { + // Compute shallow size: object header + 4 references (3 declared + 1 implicit outer reference) + long shallowSize = RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + 4L * RamUsageEstimator.NUM_BYTES_OBJECT_REF; + long preHashedDecoratedKeySize = partitionKey == null + ? 0 + : RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + + 2L * RamUsageEstimator.NUM_BYTES_OBJECT_REF // token and key references + + 2L * Long.BYTES; + return shallowSize + token.getHeapSize() + preHashedDecoratedKeySize; + } + + /** + * Compares this primary key with another for ordering purposes. + *

    + * This implementation uses a two-tier comparison strategy: + *

      + *
    • If the given primary key is token only, compares by token only
    • + *
    • If both partition keys are available, performs full partition key comparison
    • + *
    + * Note: This comparison is partition-aware only and does not consider clustering keys. + * + * @param o the primary key to compare with + * @return a negative integer, zero, or a positive integer as this primary key is less than, + * equal to, or greater than the specified primary key + */ + @Override + public int compareTo(PrimaryKey o) + { + if (o.isTokenOnly()) + return token().compareTo(o.token()); + return partitionKey().compareTo(o.partitionKey()); + } + + @Override + public int hashCode() + { + return Objects.hash(token); + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof PrimaryKey) + return compareTo((PrimaryKey)obj) == 0; + return false; + } + + @Override + public String toString() + { + return String.format("TokenAwarePrimaryKey: { token: %s, partition: %s } ", token, partitionKey == null ? null : partitionKey); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/PartitionAwarePrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/PartitionAwarePrimaryKeyMap.java new file mode 100644 index 000000000000..b5fd1ea537ab --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/PartitionAwarePrimaryKeyMap.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.IOException; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.v1.bitpack.BlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.MonotonicBlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.IKeyFetcher; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Throwables; + +/** + * A partition-aware {@link PrimaryKeyMap} + * + * This uses the following on-disk structures: + *
      + *
    • Block-packed structure for rowId to token lookups using {@link BlockPackedReader}. + * Uses component {@link IndexComponentType#TOKEN_VALUES}
    • + *
    • Monotonic-block-packed structure for rowId to partition key offset lookups using {@link MonotonicBlockPackedReader}. + * Uses component {@link IndexComponentType#OFFSETS_VALUES}
    • + *
    + * + * This uses a {@link IKeyFetcher} to read the {@link org.apache.cassandra.db.DecoratedKey} for a {@link PrimaryKey} from the + * sstable using the sstable offset provided by the monotonic-block-packed structure above. + */ +@NotThreadSafe +public class PartitionAwarePrimaryKeyMap implements PrimaryKeyMap +{ + @ThreadSafe + public static class PartitionAwarePrimaryKeyMapFactory implements Factory + { + private final IndexComponents.ForRead perSSTableComponents; + private final LongArray.Factory tokenReaderFactory; + private final LongArray.Factory offsetReaderFactory; + private final SSTableReader sstable; + private final IPartitioner partitioner; + private final PrimaryKey.Factory primaryKeyFactory; + private final SSTableId sstableId; + private final long count; + + private final FileHandle token; + private final FileHandle offset; + + public PartitionAwarePrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, SSTableReader sstable, PrimaryKey.Factory primaryKeyFactory) + { + FileHandle token = null; + FileHandle offset = null; + + IndexComponent.ForRead offsetsComponent = perSSTableComponents.get(IndexComponentType.OFFSETS_VALUES); + IndexComponent.ForRead tokensComponent = perSSTableComponents.get(IndexComponentType.TOKEN_VALUES); + + try + { + MetadataSource metadata = MetadataSource.loadMetadata(perSSTableComponents); + + NumericValuesMeta offsetsMeta = new NumericValuesMeta(metadata.get(offsetsComponent)); + NumericValuesMeta tokensMeta = new NumericValuesMeta(metadata.get(tokensComponent)); + this.count = tokensMeta.valueCount; + + token = tokensComponent.createFileHandle(); + offset = offsetsComponent.createFileHandle(); + + this.tokenReaderFactory = new BlockPackedReader(token, tokensMeta); + this.offsetReaderFactory = new MonotonicBlockPackedReader(offset, offsetsMeta); + } + catch (Throwable t) + { + throw Throwables.unchecked(Throwables.close(t, token, offset)); + } + this.perSSTableComponents = perSSTableComponents; + this.token = token; + this.offset = offset; + this.partitioner = sstable.metadata().partitioner; + this.sstable = sstable; + this.primaryKeyFactory = primaryKeyFactory; + this.sstableId = sstable.getId(); + } + + @Override + public PrimaryKeyMap newPerSSTablePrimaryKeyMap() + { + LongArray rowIdToToken = null; + LongArray rowIdToOffset = null; + IKeyFetcher keyFetcher = null; + try + { + rowIdToToken = new LongArray.DeferredLongArray(() -> tokenReaderFactory.open()); + rowIdToOffset = new LongArray.DeferredLongArray(() -> offsetReaderFactory.open()); + keyFetcher = sstable.openKeyFetcher(false); + + return new PartitionAwarePrimaryKeyMap(rowIdToToken, rowIdToOffset, partitioner, keyFetcher, primaryKeyFactory, sstableId); + } + catch (RuntimeException | Error e) + { + Throwables.closeNonNullAndAddSuppressed(e, rowIdToToken, rowIdToOffset, keyFetcher); + } + return null; + } + + @Override + public long count() + { + return count; + } + + @Override + public void close() throws IOException + { + FileUtils.closeQuietly(offset, token); + } + } + + private final LongArray rowIdToToken; + private final LongArray rowIdToOffset; + private final IPartitioner partitioner; + private final IKeyFetcher keyFetcher; + private final PrimaryKey.Factory primaryKeyFactory; + private final SSTableId sstableId; + + private PartitionAwarePrimaryKeyMap(LongArray rowIdToToken, + LongArray rowIdToOffset, + IPartitioner partitioner, + IKeyFetcher keyFetcher, + PrimaryKey.Factory primaryKeyFactory, + SSTableId sstableId) + { + this.rowIdToToken = rowIdToToken; + this.rowIdToOffset = rowIdToOffset; + this.partitioner = partitioner; + this.keyFetcher = keyFetcher; + this.primaryKeyFactory = primaryKeyFactory; + this.sstableId = sstableId; + } + + @Override + public SSTableId getSSTableId() + { + return sstableId; + } + + @Override + public PrimaryKey primaryKeyFromRowId(long sstableRowId) + { + long token = rowIdToToken.get(sstableRowId); + return primaryKeyFactory.createDeferred(partitioner.getTokenFactory().fromLongValue(token), () -> supplier(sstableRowId)); + } + + @Override + public long exactRowIdOrInvertedCeiling(PrimaryKey key) + { + return rowIdToToken.indexOf(key.token().getLongValue()); + } + + @Override + public long ceiling(PrimaryKey key) + { + return rowIdToToken.ceilingIndex(key.token().getLongValue()); + } + + @Override + public long floor(PrimaryKey key) + { + throw new UnsupportedOperationException(); + } + + @Override + public long count() + { + return rowIdToToken.length(); + } + + @Override + public void close() throws IOException + { + FileUtils.closeQuietly(rowIdToToken, rowIdToOffset, keyFetcher); + } + + private PrimaryKey supplier(long sstableRowId) + { + return primaryKeyFactory.createPartitionKeyOnly(keyFetcher.apply(rowIdToOffset.get(sstableRowId))); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java b/src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java deleted file mode 100644 index 2e03d13b36ba..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.io.Closeable; -import java.util.EnumMap; -import java.util.Map; - -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; - -/** - * Maintains a mapping of {@link IndexComponent}s to associated {@link FileHandle}s for - * read operations on the components. Users of this class are returned copies of the - * {@link FileHandle}s using {@link FileHandle#sharedCopy()} so returned handles still - * need to be closed by the user. - */ -public class PerColumnIndexFiles implements Closeable -{ - private final Map files = new EnumMap<>(IndexComponent.class); - private final IndexDescriptor indexDescriptor; - private final IndexIdentifier indexIdentifier; - - public PerColumnIndexFiles(IndexDescriptor indexDescriptor, IndexTermType indexTermType, IndexIdentifier indexIdentifier) - { - this.indexDescriptor = indexDescriptor; - this.indexIdentifier = indexIdentifier; - for (IndexComponent component : indexDescriptor.version.onDiskFormat().perColumnIndexComponents(indexTermType)) - { - if (component == IndexComponent.META || component == IndexComponent.COLUMN_COMPLETION_MARKER) - continue; - files.put(component, indexDescriptor.createPerIndexFileHandle(component, indexIdentifier, this::close)); - } - } - - public FileHandle termsData() - { - return getFile(IndexComponent.TERMS_DATA); - } - - public FileHandle postingLists() - { - return getFile(IndexComponent.POSTING_LISTS); - } - - public FileHandle balancedTree() - { - return getFile(IndexComponent.BALANCED_TREE); - } - - public FileHandle compressedVectors() - { - return getFile(IndexComponent.COMPRESSED_VECTORS); - } - - private FileHandle getFile(IndexComponent indexComponent) - { - FileHandle file = files.get(indexComponent); - if (file == null) - throw new IllegalArgumentException(String.format(indexIdentifier.logMessage("Component %s not found for SSTable %s"), - indexComponent, indexDescriptor.sstableDescriptor)); - - return file.sharedCopy(); - } - - @Override - public void close() - { - FileUtils.closeQuietly(files.values()); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/PerIndexFiles.java b/src/java/org/apache/cassandra/index/sai/disk/v1/PerIndexFiles.java new file mode 100644 index 000000000000..3df29c987edf --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/PerIndexFiles.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.Closeable; +import java.io.UncheckedIOException; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; + +import org.slf4j.Logger; + +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; + +public class PerIndexFiles implements Closeable +{ + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(PerIndexFiles.class); + + private final Map files = new EnumMap<>(IndexComponentType.class); + private final IndexComponents.ForRead perIndexComponents; + + public PerIndexFiles(IndexComponents.ForRead perIndexComponents) + { + this.perIndexComponents = perIndexComponents; + + var toOpen = new HashSet<>(perIndexComponents.expectedComponentsForVersion()); + toOpen.remove(IndexComponentType.META); + toOpen.remove(IndexComponentType.COLUMN_COMPLETION_MARKER); + + var componentsPresent = new HashSet(); + for (IndexComponentType component : toOpen) + { + try + { + files.put(component, perIndexComponents.get(component).createFileHandle()); + componentsPresent.add(component); + } + catch (UncheckedIOException e) + { + // leave logging until we're done + } + } + + logger.info("Components present for {} are {}", perIndexComponents.indexDescriptor(), componentsPresent); + } + + public IndexComponents.ForRead usedPerIndexComponents() + { + return perIndexComponents; + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle termsData() + { + return getFile(IndexComponentType.TERMS_DATA).sharedCopy(); + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle postingLists() + { + return getFile(IndexComponentType.POSTING_LISTS).sharedCopy(); + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle kdtree() + { + return getFile(IndexComponentType.KD_TREE).sharedCopy(); + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle kdtreePostingLists() + { + return getFile(IndexComponentType.KD_TREE_POSTING_LISTS).sharedCopy(); + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle vectors() + { + return getFile(IndexComponentType.VECTOR).sharedCopy(); + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle pq() + { + return getFile(IndexComponentType.PQ).sharedCopy(); + } + + /** It is the caller's responsibility to close the returned file handle. */ + public FileHandle docLengths() + { + return getFile(IndexComponentType.DOC_LENGTHS).sharedCopy(); + } + + public FileHandle getFile(IndexComponentType indexComponentType) + { + FileHandle file = files.get(indexComponentType); + if (file == null) + throw new IllegalArgumentException(String.format(perIndexComponents.logMessage("Component %s not found for SSTable %s"), + indexComponentType, + perIndexComponents.descriptor())); + + return file; + } + + @Override + public void close() + { + FileUtils.closeQuietly(files.values()); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SAICodecUtils.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SAICodecUtils.java deleted file mode 100644 index 58be96a5314c..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/SAICodecUtils.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.io.IOException; - -import org.apache.cassandra.index.sai.disk.format.Version; -import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; -import org.apache.lucene.index.CorruptIndexException; -import org.apache.lucene.store.ChecksumIndexInput; -import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexInput; -import org.apache.lucene.store.IndexOutput; - -import static org.apache.lucene.codecs.CodecUtil.CODEC_MAGIC; -import static org.apache.lucene.codecs.CodecUtil.FOOTER_MAGIC; -import static org.apache.lucene.codecs.CodecUtil.footerLength; -import static org.apache.lucene.codecs.CodecUtil.readBEInt; -import static org.apache.lucene.codecs.CodecUtil.readBELong; -import static org.apache.lucene.codecs.CodecUtil.writeBEInt; -import static org.apache.lucene.codecs.CodecUtil.writeBELong; - -public class SAICodecUtils -{ - // Lucene switched from big-endian to little-endian file format, but retained - // big-endian values in CodecUtils header and footer for compatibility. - // We follow their lead and use explicitly big-endian values here. - - public static final String FOOTER_POINTER = "footerPointer"; - - public static void writeHeader(IndexOutput out) throws IOException - { - writeBEInt(out, CODEC_MAGIC); - out.writeString(Version.LATEST.toString()); - } - - public static void writeFooter(IndexOutput out) throws IOException - { - writeBEInt(out, FOOTER_MAGIC); - writeBEInt(out, 0); - writeChecksum(out); - } - - public static void checkHeader(DataInput in) throws IOException - { - final int actualMagic = readBEInt(in); - if (actualMagic != CODEC_MAGIC) - { - throw new CorruptIndexException("codec header mismatch: actual header=" + actualMagic + " vs expected header=" + CODEC_MAGIC, in); - } - final Version actualVersion = Version.parse(in.readString()); - if (!actualVersion.onOrAfter(Version.EARLIEST)) - { - throw new IOException("Unsupported version: " + actualVersion); - } - } - - public static void checkFooter(ChecksumIndexInput in) throws IOException - { - validateFooter(in, false); - long actualChecksum = in.getChecksum(); - long expectedChecksum = readChecksum(in); - if (expectedChecksum != actualChecksum) - { - throw new CorruptIndexException("checksum failed (hardware problem?) : expected=" + Long.toHexString(expectedChecksum) + - " actual=" + Long.toHexString(actualChecksum), in); - } - } - - public static void validate(IndexInput input) throws IOException - { - checkHeader(input); - validateFooterAndResetPosition(input); - } - - public static void validate(IndexInput input, long footerPointer) throws IOException - { - checkHeader(input); - - long current = input.getFilePointer(); - input.seek(footerPointer); - validateFooter(input, true); - - input.seek(current); - } - - /** - * See {@link org.apache.lucene.codecs.CodecUtil#checksumEntireFile(org.apache.lucene.store.IndexInput)}. - * @param input IndexInput to validate. - * @throws IOException if a corruption is detected. - */ - public static void validateChecksum(IndexInput input) throws IOException - { - IndexInput clone = input.clone(); - clone.seek(0L); - ChecksumIndexInput in = IndexFileUtils.getBufferedChecksumIndexInput(clone); - - assert in.getFilePointer() == 0L : in.getFilePointer() + " bytes already read from this input!"; - - if (in.length() < (long) footerLength()) - throw new CorruptIndexException("misplaced codec footer (file truncated?): length=" + in.length() + " but footerLength==" + footerLength(), input); - else - { - in.seek(in.length() - (long) footerLength()); - checkFooter(in); - } - } - - // Copied from Lucene PackedInts as they are not public - - public static int checkBlockSize(int blockSize, int minBlockSize, int maxBlockSize) - { - if (blockSize >= minBlockSize && blockSize <= maxBlockSize) - { - if ((blockSize & blockSize - 1) != 0) - { - throw new IllegalArgumentException("blockSize must be a power of two, got " + blockSize); - } - else - { - return Integer.numberOfTrailingZeros(blockSize); - } - } - else - { - throw new IllegalArgumentException("blockSize must be >= " + minBlockSize + " and <= " + maxBlockSize + ", got " + blockSize); - } - } - - public static int numBlocks(long size, int blockSize) - { - if (size < 0) - throw new IllegalArgumentException("size cannot be negative"); - - int numBlocks = (int)(size / (long)blockSize) + (size % (long)blockSize == 0L ? 0 : 1); - if ((long)numBlocks * (long)blockSize < size) - { - throw new IllegalArgumentException("size is too large for this block size"); - } - else - { - return numBlocks; - } - } - - // Copied from Lucene BlockPackedReaderIterator as they are not public - - /** - * Same as DataInput.readVLong but supports negative values - */ - public static long readVLong(DataInput in) throws IOException - { - byte b = in.readByte(); - if (b >= 0) return b; - long i = b & 0x7FL; - b = in.readByte(); - i |= (b & 0x7FL) << 7; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0x7FL) << 14; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0x7FL) << 21; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0x7FL) << 28; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0x7FL) << 35; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0x7FL) << 42; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0x7FL) << 49; - if (b >= 0) return i; - b = in.readByte(); - i |= (b & 0xFFL) << 56; - return i; - } - - public static void validateFooterAndResetPosition(IndexInput in) throws IOException - { - long position = in.getFilePointer(); - long fileLength = in.length(); - long footerLength = footerLength(); - long footerPosition = fileLength - footerLength; - - if (footerPosition < 0) - { - throw new CorruptIndexException("invalid codec footer (file truncated?): file length=" + fileLength + ", footer length=" + footerLength, in); - } - - in.seek(footerPosition); - validateFooter(in, false); - in.seek(position); - } - - /** - * Copied from org.apache.lucene.codecs.CodecUtil.validateFooter(IndexInput). - * - * If the file is segmented then the footer can exist in the middle of the file - * so, we shouldn't check that the footer size is correct, we just check that the - * footer values are correct. - */ - private static void validateFooter(IndexInput in, boolean segmented) throws IOException - { - long remaining = in.length() - in.getFilePointer(); - long expected = footerLength(); - - if (!segmented) - { - if (remaining < expected) - { - throw new CorruptIndexException("misplaced codec footer (file truncated?): remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer(), in); - } - else if (remaining > expected) - { - throw new CorruptIndexException("misplaced codec footer (file extended?): remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer(), in); - } - } - - final int magic = readBEInt(in); - - if (magic != FOOTER_MAGIC) - { - throw new CorruptIndexException("codec footer mismatch (file truncated?): actual footer=" + magic + " vs expected footer=" + FOOTER_MAGIC, in); - } - - final int algorithmID = readBEInt(in); - - if (algorithmID != 0) - { - throw new CorruptIndexException("codec footer mismatch: unknown algorithmID: " + algorithmID, in); - } - } - - // Copied from Lucene CodecUtil as they are not public - - /** - * Writes checksum value as a 64-bit long to the output. - * @throws IllegalStateException if CRC is formatted incorrectly (wrong bits set) - * @throws IOException if an i/o error occurs - */ - private static void writeChecksum(IndexOutput output) throws IOException - { - long value = output.getChecksum(); - if ((value & 0xFFFFFFFF00000000L) != 0) - { - throw new IllegalStateException("Illegal checksum: " + value + " (resource=" + output + ')'); - } - writeBELong(output, value); - } - - /** - * Reads checksum value as a 64-bit long from the input. - * @throws CorruptIndexException if CRC is formatted incorrectly (wrong bits set) - * @throws IOException if an i/o error occurs - */ - private static long readChecksum(IndexInput input) throws IOException - { - long value = readBELong(input); - if ((value & 0xFFFFFFFF00000000L) != 0) - { - throw new CorruptIndexException("Illegal checksum: " + value, input); - } - return value; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java index b6e006584ac0..263ac680cd73 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java @@ -15,115 +15,76 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.cassandra.index.sai.disk.v1; import java.io.IOException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter; -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.io.IndexOutputWriter; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; -import org.apache.cassandra.index.sai.disk.v1.keystore.KeyStoreWriter; import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.lucene.util.IOUtils; -public class SSTableComponentsWriter implements PerSSTableIndexWriter +/** + * Writes all SSTable-attached index token and offset structures. + */ +public class SSTableComponentsWriter implements PerSSTableWriter { protected static final Logger logger = LoggerFactory.getLogger(SSTableComponentsWriter.class); - private final IndexDescriptor indexDescriptor; - private final MetadataWriter metadataWriter; - private final NumericValuesWriter partitionSizeWriter; - private final NumericValuesWriter partitionRowsWriter; + private final IndexComponents.ForWrite perSSTableComponents; private final NumericValuesWriter tokenWriter; - private final KeyStoreWriter partitionKeysWriter; - private final KeyStoreWriter clusteringKeysWriter; + private final NumericValuesWriter offsetWriter; + private final MetadataWriter metadataWriter; - private long partitionId = -1; - // This is used to record the number of rows in each partition - private long partitionRowCount = 0; + private long currentKeyPartitionOffset; - public SSTableComponentsWriter(IndexDescriptor indexDescriptor) throws IOException + public SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException { - this.indexDescriptor = indexDescriptor; - this.metadataWriter = new MetadataWriter(indexDescriptor.openPerSSTableOutput(IndexComponent.GROUP_META)); - this.tokenWriter = new NumericValuesWriter(indexDescriptor, IndexComponent.ROW_TO_TOKEN, metadataWriter, false); - this.partitionRowsWriter = new NumericValuesWriter(indexDescriptor, IndexComponent.ROW_TO_PARTITION, metadataWriter, true); - this.partitionSizeWriter = new NumericValuesWriter(indexDescriptor, IndexComponent.PARTITION_TO_SIZE, metadataWriter, false); - IndexOutputWriter partitionKeyBlocksWriter = indexDescriptor.openPerSSTableOutput(IndexComponent.PARTITION_KEY_BLOCKS); - NumericValuesWriter partitionKeyBlockOffsetWriter = new NumericValuesWriter(indexDescriptor, IndexComponent.PARTITION_KEY_BLOCK_OFFSETS, metadataWriter, true); - this.partitionKeysWriter = new KeyStoreWriter(indexDescriptor.componentName(IndexComponent.PARTITION_KEY_BLOCKS), - metadataWriter, - partitionKeyBlocksWriter, - partitionKeyBlockOffsetWriter, - CassandraRelevantProperties.SAI_SORTED_TERMS_PARTITION_BLOCK_SHIFT.getInt(), - false); - if (indexDescriptor.hasClustering()) - { - IndexOutputWriter clusteringKeyBlocksWriter = indexDescriptor.openPerSSTableOutput(IndexComponent.CLUSTERING_KEY_BLOCKS); - NumericValuesWriter clusteringKeyBlockOffsetWriter = new NumericValuesWriter(indexDescriptor, IndexComponent.CLUSTERING_KEY_BLOCK_OFFSETS, metadataWriter, true); - this.clusteringKeysWriter = new KeyStoreWriter(indexDescriptor.componentName(IndexComponent.CLUSTERING_KEY_BLOCKS), - metadataWriter, - clusteringKeyBlocksWriter, - clusteringKeyBlockOffsetWriter, - CassandraRelevantProperties.SAI_SORTED_TERMS_CLUSTERING_BLOCK_SHIFT.getInt(), - true); - } - else - { - this.clusteringKeysWriter = null; - } + this.perSSTableComponents = perSSTableComponents; + this.metadataWriter = new MetadataWriter(perSSTableComponents); + this.tokenWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.TOKEN_VALUES), + metadataWriter, false); + this.offsetWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.OFFSETS_VALUES), + metadataWriter, true); } @Override - public void startPartition(DecoratedKey partitionKey) throws IOException + public void startPartition(long position) { - if (partitionId >= 0) - partitionSizeWriter.add(partitionRowCount); - - partitionId++; - partitionRowCount = 0; - partitionKeysWriter.add(v -> ByteSource.of(partitionKey.getKey(), v)); - if (indexDescriptor.hasClustering()) - clusteringKeysWriter.startPartition(); + currentKeyPartitionOffset = position; } @Override public void nextRow(PrimaryKey primaryKey) throws IOException { - tokenWriter.add(primaryKey.token().getLongValue()); - partitionRowsWriter.add(partitionId); - partitionRowCount++; - if (indexDescriptor.hasClustering()) - clusteringKeysWriter.add(indexDescriptor.clusteringComparator.asByteComparable(primaryKey.clustering())); + recordCurrentTokenOffset(primaryKey.token().getLongValue(), currentKeyPartitionOffset); } @Override - public void complete() throws IOException + public void complete(Stopwatch stopwatch) throws IOException { - try - { - partitionSizeWriter.add(partitionRowCount); - indexDescriptor.createComponentOnDisk(IndexComponent.GROUP_COMPLETION_MARKER); - } - finally - { - FileUtils.close(tokenWriter, partitionSizeWriter, partitionRowsWriter, partitionKeysWriter, clusteringKeysWriter, metadataWriter); - } + IOUtils.close(tokenWriter, offsetWriter, metadataWriter); + perSSTableComponents.markComplete(); } @Override - public void abort() + public void abort(Throwable accumulator) + { + logger.debug(perSSTableComponents.logMessage("Aborting token/offset writer for {}..."), perSSTableComponents.descriptor()); + perSSTableComponents.forceDeleteAllComponents(); + } + + @VisibleForTesting + public void recordCurrentTokenOffset(long tokenValue, long keyOffset) throws IOException { - logger.debug(indexDescriptor.logMessage("Aborting per-SSTable index component writer for {}..."), indexDescriptor.sstableDescriptor); - indexDescriptor.deletePerSSTableIndexComponents(); + tokenWriter.add(tokenValue); + offsetWriter.add(keyOffset); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java index c3c83992b26b..f464fcd77ad4 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java @@ -26,52 +26,83 @@ import java.util.function.BooleanSupplier; import javax.annotation.concurrent.NotThreadSafe; +import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.github.jbellis.jvector.quantization.ProductQuantization; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter; -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.v1.segment.SegmentBuilder; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.disk.PerIndexWriter; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.v2.V2VectorIndexSearcher; +import org.apache.cassandra.index.sai.disk.v3.V3OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v5.V5VectorIndexSearcher; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.vector.CassandraDiskAnn; +import org.apache.cassandra.index.sai.disk.vector.CassandraOnHeapGraph; +import org.apache.cassandra.index.sai.disk.vector.VectorCompression.CompressionType; +import org.apache.cassandra.index.sai.metrics.IndexMetrics; import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter; import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Throwables; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; /** * Column index writer that accumulates (on-heap) indexed data from a compacted SSTable as it's being flushed to disk. */ @NotThreadSafe -public class SSTableIndexWriter implements PerColumnIndexWriter +public class SSTableIndexWriter implements PerIndexWriter { private static final Logger logger = LoggerFactory.getLogger(SSTableIndexWriter.class); - private final IndexDescriptor indexDescriptor; - private final StorageAttachedIndex index; + private final IndexComponents.ForWrite perIndexComponents; + private final IndexContext indexContext; + private final IndexMetrics indexMetrics; private final long nowInSec = FBUtilities.nowInSeconds(); - private final AbstractAnalyzer analyzer; private final NamedMemoryLimiter limiter; - private final BooleanSupplier isIndexValid; - private final List segments = new ArrayList<>(); + private final BooleanSupplier isIndexDropped; + private final BooleanSupplier isIndexUnloaded; + private final long keyCount; private boolean aborted = false; + + // segment writer private SegmentBuilder currentBuilder; + private final List segments = new ArrayList<>(); - public SSTableIndexWriter(IndexDescriptor indexDescriptor, - StorageAttachedIndex index, - NamedMemoryLimiter limiter, - BooleanSupplier isIndexValid) + public SSTableIndexWriter(IndexComponents.ForWrite perIndexComponents, NamedMemoryLimiter limiter, + BooleanSupplier isIndexDropped, BooleanSupplier isIndexUnloaded, long keyCount) { - this.indexDescriptor = indexDescriptor; - this.index = index; - this.analyzer = index.hasAnalyzer() ? index.analyzer() : null; + this.perIndexComponents = perIndexComponents; + this.indexContext = perIndexComponents.context(); + Preconditions.checkNotNull(indexContext, "Provided components %s are the per-sstable ones, expected per-index ones", perIndexComponents); + this.indexMetrics = indexContext.getIndexMetrics().orElse(null); this.limiter = limiter; - this.isIndexValid = isIndexValid; + this.isIndexDropped = isIndexDropped; + this.isIndexUnloaded = isIndexUnloaded; + this.keyCount = keyCount; + } + + @Override + public IndexContext indexContext() + { + return indexContext; + } + + @Override + public IndexComponents.ForWrite writtenComponents() + { + return perIndexComponents; } @Override @@ -80,24 +111,36 @@ public void addRow(PrimaryKey key, Row row, long sstableRowId) throws IOExceptio if (maybeAbort()) return; - if (index.termType().isNonFrozenCollection()) + // This is to avoid duplicates (and also reduce space taken by indexes on static columns). + // An index on a static column indexes static rows only. + // An index on a non-static column indexes regular rows only. + if (indexContext.getDefinition().isStatic() != row.isStatic()) + return; + + boolean addedRow = false; + if (indexContext.isNonFrozenCollection()) { - Iterator valueIterator = index.termType().valuesOf(row, nowInSec); + Iterator valueIterator = indexContext.getValuesOf(row, nowInSec); if (valueIterator != null) { while (valueIterator.hasNext()) { ByteBuffer value = valueIterator.next(); - addTerm(index.termType().asIndexBytes(value.duplicate()), key, sstableRowId); + addedRow = addTerm(TypeUtil.asIndexBytes(value.duplicate(), indexContext.getValidator()), key, sstableRowId, indexContext.getValidator()); } } } else { - ByteBuffer value = index.termType().valueOf(key.partitionKey(), row, nowInSec); + ByteBuffer value = indexContext.getValueOf(key.partitionKey(), row, nowInSec); if (value != null) - addTerm(index.termType().asIndexBytes(value.duplicate()), key, sstableRowId); + { + addedRow = addTerm(TypeUtil.asIndexBytes(value.duplicate(), indexContext.getValidator()), key, sstableRowId, indexContext.getValidator()); + } } + if (addedRow) + currentBuilder.incRowCount(); + } @Override @@ -107,7 +150,7 @@ public void onSSTableWriterSwitched(Stopwatch stopwatch) throws IOException return; boolean emptySegment = currentBuilder == null || currentBuilder.isEmpty(); - logger.debug(index.identifier().logMessage("Flushing index with {}buffered data on SSTable writer switched..."), emptySegment ? "no " : ""); + logger.debug("Flushing index {} with {}buffered data on sstable writer switched...", indexContext.getIndexName(), emptySegment ? "no " : ""); if (!emptySegment) flushSegment(); } @@ -122,7 +165,7 @@ public void complete(Stopwatch stopwatch) throws IOException long elapsed; boolean emptySegment = currentBuilder == null || currentBuilder.isEmpty(); - logger.debug(index.identifier().logMessage("Completing index flush with {}buffered data..."), emptySegment ? "no " : ""); + logger.debug("Completing index flush with {}buffered data...", emptySegment ? "no " : ""); try { @@ -131,8 +174,8 @@ public void complete(Stopwatch stopwatch) throws IOException { flushSegment(); elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); - logger.debug(index.identifier().logMessage("Completed flush of final segment for SSTable {}. Duration: {} ms. Total elapsed: {} ms"), - indexDescriptor.sstableDescriptor, + logger.debug("Completed flush of final segment for SSTable {}. Duration: {} ms. Total elapsed: {} ms", + perIndexComponents.descriptor(), elapsed - start, elapsed); } @@ -141,35 +184,33 @@ public void complete(Stopwatch stopwatch) throws IOException if (currentBuilder != null) { long bytesAllocated = currentBuilder.totalBytesAllocated(); - long globalBytesUsed = currentBuilder.release(); - logger.debug(index.identifier().logMessage("Flushing final segment for SSTable {} released {}. Global segment memory usage now at {}."), - indexDescriptor.sstableDescriptor, FBUtilities.prettyPrintMemory(bytesAllocated), FBUtilities.prettyPrintMemory(globalBytesUsed)); + long globalBytesUsed = currentBuilder.release(indexContext); + logger.debug("Flushing final segment for SSTable {} released {}. Global segment memory usage now at {}", + perIndexComponents.descriptor(), FBUtilities.prettyPrintMemory(bytesAllocated), FBUtilities.prettyPrintMemory(globalBytesUsed)); } writeSegmentsMetadata(); - - // write column index completion marker, indicating whether the index is empty - ColumnCompletionMarkerUtil.create(indexDescriptor, index.identifier(), segments.isEmpty()); + perIndexComponents.markComplete(); } finally { - index.indexMetrics().segmentsPerCompaction.update(segments.size()); - segments.clear(); - index.indexMetrics().compactionCount.inc(); + indexContext.getIndexMetrics().ifPresent(m -> { + m.segmentsPerCompaction.update(segments.size()); + segments.clear(); + m.compactionCount.inc(); + }); } } @Override public void abort(Throwable cause) { - aborted = true; + if (aborted) + return; - String message = index.identifier().logMessage("Aborting SSTable index flush for {}..."); + aborted = true; - if (cause == null) - logger.debug(message, indexDescriptor.sstableDescriptor); - else - logger.warn(message, indexDescriptor.sstableDescriptor, cause); + logger.warn("Aborting SSTable index flush for {}...", perIndexComponents.descriptor(), cause); // It's possible for the current builder to be unassigned after we flush a final segment. if (currentBuilder != null) @@ -177,12 +218,15 @@ public void abort(Throwable cause) // If an exception is thrown out of any writer operation prior to successful segment // flush, we will end up here, and we need to free up builder memory tracked by the limiter: long allocated = currentBuilder.totalBytesAllocated(); - long globalBytesUsed = currentBuilder.release(); - logger.debug(index.identifier().logMessage("Aborting index writer for SSTable {} released {}. Global segment memory usage now at {}."), - indexDescriptor.sstableDescriptor, FBUtilities.prettyPrintMemory(allocated), FBUtilities.prettyPrintMemory(globalBytesUsed)); + long globalBytesUsed = currentBuilder.release(indexContext); + logger.debug("Aborting index writer for SSTable {} released {}. Global segment memory usage now at {}", + perIndexComponents.descriptor(), FBUtilities.prettyPrintMemory(allocated), FBUtilities.prettyPrintMemory(globalBytesUsed)); } - indexDescriptor.deleteColumnIndex(index.termType(), index.identifier()); + if (CassandraRelevantProperties.DELETE_CORRUPT_SAI_COMPONENTS.getBoolean()) + perIndexComponents.forceDeleteAllComponents(); + else + logger.debug("Skipping delete of index components after failure on index build of {}.{}", perIndexComponents.indexDescriptor(), indexContext); } /** @@ -195,62 +239,57 @@ private boolean maybeAbort() if (aborted) return true; - if (isIndexValid.getAsBoolean()) + boolean dropped = isIndexDropped.getAsBoolean(); + boolean unloaded = isIndexUnloaded.getAsBoolean(); + if (!dropped && !unloaded) return false; - abort(new RuntimeException(String.format("index %s is dropped", index.identifier()))); - return true; + String message = String.format("index %s is %s", indexContext.getIndexName(), dropped ? "dropped" : "unloaded"); + RuntimeException runtimeException = new RuntimeException(message); + + // abort index build for remove on disk index file + abort(runtimeException); + + // if index is dropped, we can continue compaction task or index build without current index + if (dropped) + return true; + + // if index is unloaded after unassigning tenant, fail the compaction task or index build to avoid incomplete index files + throw runtimeException; } - private void addTerm(ByteBuffer term, PrimaryKey key, long sstableRowId) throws IOException + private boolean addTerm(ByteBuffer term, PrimaryKey key, long sstableRowId, AbstractType type) throws IOException { - if (!index.validateTermSize(key.partitionKey(), term, false, null)) - return; + if (!indexContext.validateMaxTermSize(key.partitionKey(), term)) + return false; if (currentBuilder == null) { - currentBuilder = newSegmentBuilder(); + currentBuilder = newSegmentBuilder(sstableRowId); } else if (shouldFlush(sstableRowId)) { flushSegment(); - currentBuilder = newSegmentBuilder(); + currentBuilder = newSegmentBuilder(sstableRowId); } - // Some types support empty byte buffers: - if (term.remaining() == 0 && index.termType().skipsEmptyValue()) return; + if (term.remaining() == 0 && TypeUtil.skipsEmptyValue(indexContext.getValidator())) + return false; - if (analyzer == null || !index.termType().isLiteral()) - { - limiter.increment(currentBuilder.add(term, key, sstableRowId)); - } - else - { - analyzer.reset(term); - try - { - while (analyzer.hasNext()) - { - ByteBuffer tokenTerm = analyzer.next(); - limiter.increment(currentBuilder.add(tokenTerm, key, sstableRowId)); - } - } - finally - { - analyzer.end(); - } - } + long allocated = currentBuilder.analyzeAndAdd(term, type, key, sstableRowId, indexMetrics); + limiter.increment(allocated); + return true; } private boolean shouldFlush(long sstableRowId) { - // If we've hit the minimum flush size and, we've breached the global limit, flush a new segment: + // If we've hit the minimum flush size and we've breached the global limit, flush a new segment: boolean reachMemoryLimit = limiter.usageExceedsLimit() && currentBuilder.hasReachedMinimumFlushSize(); - if (reachMemoryLimit) + if (currentBuilder.requiresFlush() || reachMemoryLimit) { - logger.debug(index.identifier().logMessage("Global limit of {} and minimum flush size of {} exceeded. " + - "Current builder usage is {} for {} cells. Global Usage is {}. Flushing..."), + logger.debug("Global limit of {} and minimum flush size of {} exceeded. " + + "Current builder usage is {} for {} rows. Global Usage is {}. Flushing...", FBUtilities.prettyPrintMemory(limiter.limitBytes()), FBUtilities.prettyPrintMemory(currentBuilder.getMinimumFlushBytes()), FBUtilities.prettyPrintMemory(currentBuilder.totalBytesAllocated()), @@ -258,50 +297,65 @@ private boolean shouldFlush(long sstableRowId) FBUtilities.prettyPrintMemory(limiter.currentBytesUsed())); } - return reachMemoryLimit || currentBuilder.exceedsSegmentLimit(sstableRowId); + return reachMemoryLimit || currentBuilder.exceedsSegmentLimit(sstableRowId) || currentBuilder.requiresFlush(); } private void flushSegment() throws IOException { - long start = Clock.Global.nanoTime(); + currentBuilder.awaitAsyncAdditions(); + if (currentBuilder.supportsAsyncAdd() + && currentBuilder.totalBytesAllocatedConcurrent.sum() > 1.1 * currentBuilder.totalBytesAllocated()) + { + logger.warn("Concurrent memory usage is higher than estimated: {} vs {}", + currentBuilder.totalBytesAllocatedConcurrent.sum(), currentBuilder.totalBytesAllocated()); + } + // throw exceptions that occurred during async addInternal() + var ae = currentBuilder.getAsyncThrowable(); + if (ae != null) + Throwables.throwAsUncheckedException(ae); + + long start = nanoTime(); try { long bytesAllocated = currentBuilder.totalBytesAllocated(); - - SegmentMetadata segmentMetadata = currentBuilder.flush(indexDescriptor); - - long flushMillis = Math.max(1, TimeUnit.NANOSECONDS.toMillis(Clock.Global.nanoTime() - start)); + SegmentMetadata segmentMetadata = currentBuilder.flush(); + long flushMillis = Math.max(1, TimeUnit.NANOSECONDS.toMillis(nanoTime() - start)); if (segmentMetadata != null) { segments.add(segmentMetadata); double rowCount = segmentMetadata.numRows; - index.indexMetrics().compactionSegmentCellsPerSecond.update((long)(rowCount / flushMillis * 1000.0)); - double segmentBytes = segmentMetadata.componentMetadatas.indexSize(); - index.indexMetrics().compactionSegmentBytesPerSecond.update((long)(segmentBytes / flushMillis * 1000.0)); - logger.debug(index.identifier().logMessage("Flushed segment with {} cells for a total of {} in {} ms."), - (long) rowCount, FBUtilities.prettyPrintMemory((long) segmentBytes), flushMillis); + indexContext.getIndexMetrics().ifPresent(m -> { + m.compactionSegmentCellsPerSecond.update((long)(rowCount / flushMillis * 1000.0)); + m.compactionSegmentBytesPerSecond.update((long)(segmentBytes / flushMillis * 1000.0)); + }); + + logger.debug("Flushed segment with {} cells for a total of {} in {} ms for index {} with starting row id {} for sstable {}", + (long) rowCount, FBUtilities.prettyPrintMemory((long) segmentBytes), flushMillis, indexContext.getIndexName(), + segmentMetadata.minSSTableRowId, perIndexComponents.descriptor()); } // Builder memory is released against the limiter at the conclusion of a successful // flush. Note that any failure that occurs before this (even in term addition) will // actuate this column writer's abort logic from the parent SSTable-level writer, and // that abort logic will release the current builder's memory against the limiter. - long globalBytesUsed = currentBuilder.release(); + long globalBytesUsed = currentBuilder.release(indexContext); currentBuilder = null; - logger.debug(index.identifier().logMessage("Flushing index segment for SSTable {} released {}. Global segment memory usage now at {}."), - indexDescriptor.sstableDescriptor, FBUtilities.prettyPrintMemory(bytesAllocated), FBUtilities.prettyPrintMemory(globalBytesUsed)); + logger.debug("Flushing index segment for SSTable {} released {}. Global segment memory usage now at {}", + perIndexComponents.descriptor(), FBUtilities.prettyPrintMemory(bytesAllocated), FBUtilities.prettyPrintMemory(globalBytesUsed)); } catch (Throwable t) { - logger.error(index.identifier().logMessage("Failed to build index for SSTable {}."), indexDescriptor.sstableDescriptor, t); - indexDescriptor.deleteColumnIndex(index.termType(), index.identifier()); - index.indexMetrics().segmentFlushErrors.inc(); + logger.error("Failed to build index for SSTable {}", perIndexComponents.descriptor(), t); + perIndexComponents.forceDeleteAllComponents(); + + indexContext.getIndexMetrics().ifPresent(m -> m.segmentFlushErrors.inc()); + throw t; } } @@ -311,7 +365,7 @@ private void writeSegmentsMetadata() throws IOException if (segments.isEmpty()) return; - try (MetadataWriter writer = new MetadataWriter(indexDescriptor.openPerIndexOutput(IndexComponent.META, index.identifier()))) + try (MetadataWriter writer = new MetadataWriter(perIndexComponents)) { SegmentMetadata.write(writer, segments); } @@ -322,16 +376,97 @@ private void writeSegmentsMetadata() throws IOException } } - private SegmentBuilder newSegmentBuilder() + private SegmentBuilder newSegmentBuilder(long rowIdOffset) throws IOException { - SegmentBuilder builder = index.termType().isVector() ? new SegmentBuilder.VectorSegmentBuilder(index, limiter) - : new SegmentBuilder.TrieSegmentBuilder(index, limiter); + SegmentBuilder builder; + + if (indexContext.isVector()) + { + // if we have a PQ instance available, we can use it to build a CompactionGraph; + // otherwise, build on heap (which will create PQ for next time, if we have enough vectors) + var pqi = CassandraOnHeapGraph.getPqIfPresent(indexContext, vc -> vc.type == CompressionType.PRODUCT_QUANTIZATION); + // If no PQ instance available in indexes of completed sstables, check if we just wrote one in the previous segment + if (pqi == null && !segments.isEmpty()) + pqi = maybeReadPqFromLastSegment(); + + if (pqi != null && V3OnDiskFormat.ENABLE_LTM_CONSTRUCTION) + { + var allRowsHaveVectors = allRowsHaveVectorsInWrittenSegments(indexContext); + builder = new SegmentBuilder.VectorOffHeapSegmentBuilder(perIndexComponents, rowIdOffset, keyCount, pqi.pq, pqi.unitVectors, allRowsHaveVectors, limiter); + } + else + { + // building on heap is the only way to get a PQ from nothing (CompactionGraph only knows how to fine-tune an existing one) + builder = new SegmentBuilder.VectorOnHeapSegmentBuilder(perIndexComponents, rowIdOffset, keyCount, limiter); + } + } + else if (indexContext.isLiteral()) + { + builder = new SegmentBuilder.RAMStringSegmentBuilder(perIndexComponents, rowIdOffset, limiter); + } + else + { + builder = new SegmentBuilder.KDTreeSegmentBuilder(perIndexComponents, rowIdOffset, limiter, indexContext.getIndexWriterConfig()); + } long globalBytesUsed = limiter.increment(builder.totalBytesAllocated()); - logger.debug(index.identifier().logMessage("Created new segment builder while flushing SSTable {}. Global segment memory usage now at {}."), - indexDescriptor.sstableDescriptor, - FBUtilities.prettyPrintMemory(globalBytesUsed)); + logger.debug("Created new segment builder while flushing SSTable {}. Global segment memory usage now at {} with {} active segment builders", + perIndexComponents.descriptor(), + FBUtilities.prettyPrintMemory(globalBytesUsed), + SegmentBuilder.ACTIVE_BUILDER_COUNT.get() - 1); return builder; } + + private static boolean allRowsHaveVectorsInWrittenSegments(IndexContext indexContext) + { + for (SSTableIndex index : indexContext.getView().getIndexes()) + { + for (Segment segment : index.getSegments()) + { + if (segment.getIndexSearcher() instanceof V2VectorIndexSearcher) + return true; // V2 doesn't know, so we err on the side of being optimistic. See comments in CompactionGraph + var searcher = (V5VectorIndexSearcher) segment.getIndexSearcher(); + var structure = searcher.getPostingsStructure(); + if (structure == V5VectorPostingsWriter.Structure.ZERO_OR_ONE_TO_MANY) + return false; + } + } + return true; + } + + private CassandraOnHeapGraph.PqInfo maybeReadPqFromLastSegment() throws IOException + { + var pqComponent = perIndexComponents.get(IndexComponentType.PQ); + assert pqComponent != null; // we always have a PQ component even if it's not actually PQ compression + + var fhBuilder = StorageProvider.instance.indexBuildTimeFileHandleBuilderFor(pqComponent); + try (var fh = fhBuilder.complete(); + var reader = fh.createReader()) + { + var sm = segments.get(segments.size() - 1); + long offset = sm.componentMetadatas.get(IndexComponentType.PQ).offset; + // close parallel to code in CassandraDiskANN constructor, but different enough + // (we only want the PQ codebook) that it's difficult to extract into a common method + reader.seek(offset); + boolean unitVectors; + if (reader.readInt() == CassandraDiskAnn.PQ_MAGIC) + { + reader.readInt(); // skip over version + unitVectors = reader.readBoolean(); + } + else + { + unitVectors = true; + reader.seek(offset); + } + var compressionType = CompressionType.values()[reader.readByte()]; + if (compressionType == CompressionType.PRODUCT_QUANTIZATION) + { + var pq = ProductQuantization.load(reader); + return new CassandraOnHeapGraph.PqInfo(pq, unitVectors, sm.numRows); + } + } + return null; + } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/Segment.java b/src/java/org/apache/cassandra/index/sai/disk/v1/Segment.java new file mode 100644 index 000000000000..d7095130ea0f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/Segment.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; + +import org.slf4j.Logger; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.v2.V2VectorIndexSearcher; +import org.apache.cassandra.index.sai.disk.v3.V3OnDiskFormat; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RangeUtil; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.CloseableIterator; + +/** + * Each segment represents an on-disk index structure (kdtree/terms/postings) flushed by memory limit or token boundaries, + * or max segment rowId limit, because of lucene's limitation on 2B(Integer.MAX_VALUE). It also helps to reduce resource + * consumption for read requests as only segments that intersect with read request data range need to be loaded. + */ +public class Segment implements Closeable +{ + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(Segment.class); + + private final Token.KeyBound minKeyBound; + private final Token.KeyBound maxKeyBound; + + // per sstable + final PrimaryKeyMap.Factory primaryKeyMapFactory; + // per-index + public final PerIndexFiles indexFiles; + // per-segment + public final SegmentMetadata metadata; + public final SSTableContext sstableContext; + + private final IndexSearcher index; + + public Segment(IndexContext indexContext, SSTableContext sstableContext, PerIndexFiles indexFiles, SegmentMetadata metadata) throws IOException + { + this.minKeyBound = metadata.minKey.token().minKeyBound(); + this.maxKeyBound = metadata.maxKey.token().maxKeyBound(); + + this.sstableContext = sstableContext; + this.primaryKeyMapFactory = sstableContext.primaryKeyMapFactory(); + this.indexFiles = indexFiles; + this.metadata = metadata; + + var version = indexFiles.usedPerIndexComponents().version(); + IndexSearcher searcher = version.onDiskFormat().newIndexSearcher(sstableContext, indexContext, indexFiles, metadata); + logger.info("Opened searcher {} for segment {} with row id meta ({},{},{},{}) for index [{}] on column [{}] at version {}", + searcher.getClass().getSimpleName(), + sstableContext.descriptor(), + metadata.segmentRowIdOffset, + metadata.numRows, + metadata.minSSTableRowId, + metadata.maxSSTableRowId, + indexContext.getIndexName(), + indexContext.getColumnName(), + version); + this.index = searcher; + } + + @VisibleForTesting + public Segment(PrimaryKeyMap.Factory primaryKeyMapFactory, + PerIndexFiles indexFiles, + SegmentMetadata metadata, + AbstractType columnType) + { + this.primaryKeyMapFactory = primaryKeyMapFactory; + this.indexFiles = indexFiles; + this.metadata = metadata; + this.minKeyBound = null; + this.maxKeyBound = null; + this.index = null; + this.sstableContext = null; + } + + @VisibleForTesting + public Segment(Token minKey, Token maxKey) + { + this.primaryKeyMapFactory = null; + this.indexFiles = null; + this.metadata = null; + this.minKeyBound = minKey.minKeyBound(); + this.maxKeyBound = maxKey.maxKeyBound(); + this.index = null; + this.sstableContext = null; + } + + /** + * @return true if current segment intersects with query key range + */ + public boolean intersects(AbstractBounds keyRange) + { + return RangeUtil.intersects(minKeyBound, maxKeyBound, keyRange); + } + + public long indexFileCacheSize() + { + return index == null ? 0 : index.indexFileCacheSize(); + } + + /** + * Search on-disk index synchronously + * + * @param expression to filter on disk index + * @param keyRange key range specific in read command, used by ANN index + * @param context to track per sstable cache and per query metrics + * @param defer create the iterator in a deferred state + * @return range iterator of {@link PrimaryKey} that matches given expression + */ + public KeyRangeIterator search(Expression expression, AbstractBounds keyRange, QueryContext context, boolean defer) throws IOException + { + return index.search(expression, keyRange, context, defer); + } + + /** + * Order the on-disk index synchronously and produce an iterator in score order + * + * @param orderer to filter on disk index + * @param keyRange key range specific in read command, used by ANN index + * @param context to track per sstable cache and per query metrics + * @param limit the num of rows to returned, used by ANN index + * @return an iterator of {@link PrimaryKeyWithSortKey} in score order + */ + public CloseableIterator orderBy(Orderer orderer, Expression slice, AbstractBounds keyRange, QueryContext context, int limit) throws IOException + { + return index.orderBy(orderer, slice, keyRange, context, limit); + } + + public IndexSearcher getIndexSearcher() + { + return index; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Segment segment = (Segment) o; + return Objects.equal(metadata, segment.metadata); + } + + @Override + public int hashCode() + { + return Objects.hashCode(metadata); + } + + public CloseableIterator orderResultsBy(QueryContext context, List keys, Orderer orderer, int limit) throws IOException + { + return index.orderResultsBy(sstableContext.sstable, context, keys, orderer, limit); + } + + @Override + public void close() + { + FileUtils.closeQuietly(index); + } + + @Override + public String toString() + { + return String.format("Segment{metadata=%s}", metadata); + } + + /** + * Estimate how many nodes the index will visit to find the top `limit` results + * given the number of candidates that match other predicates and taking into + * account the size of the index itself. (The smaller + * the number of candidates, the more nodes we expect to visit just to find + * results that are in that set.) + */ + public double estimateAnnSearchCost(Orderer orderer, int limit, int candidates) + { + V2VectorIndexSearcher searcher = (V2VectorIndexSearcher) getIndexSearcher(); + int rerankK = orderer.rerankKFor(limit, searcher.getCompression()); + return searcher.estimateAnnSearchCost(rerankK, candidates); + } + + /** + * Returns a modified LIMIT (top k) to use with the ANN index that is proportional + * to the number of rows in this segment, relative to the total rows in the sstable. + */ + public int proportionalAnnLimit(int limit, long totalRows) + { + if (!V3OnDiskFormat.REDUCE_TOPK_ACROSS_SSTABLES) + return limit; + + // Note: it is tempting to think that we should max out results for the first segment + // since that's where we're establishing our rerank floor. This *does* reduce the number + // of calls to resume, but it's 10-15% slower overall, so don't do it. + // if (context.getAnnRerankFloor() == 0 && V3OnDiskFormat.ENABLE_RERANK_FLOOR) + // return limit; + + // We expect the number of top results found in each segment to be proportional to its number of rows. + // (We don't pad this number more because resuming a search if we guess too low is very very inexpensive.) + long segmentRows = 1 + metadata.maxSSTableRowId - metadata.minSSTableRowId; + int proportionalLimit = (int) Math.ceil(limit * ((double) segmentRows / totalRows)); + assert proportionalLimit >= 1 : proportionalLimit; + return proportionalLimit; + } + + public long estimateMatchingRowsCount(Expression predicate) + { + return metadata.estimateNumRowsMatching(predicate); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentBuilder.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentBuilder.java new file mode 100644 index 000000000000..045326e9078f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentBuilder.java @@ -0,0 +1,653 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.quantization.VectorCompressor; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; +import org.apache.cassandra.index.sai.analyzer.ByteLimitedMaterializer; +import org.apache.cassandra.index.sai.analyzer.NoOpAnalyzer; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.RAMStringIndexer; +import org.apache.cassandra.index.sai.disk.TermsIterator; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.kdtree.BKDTreeRamBuffer; +import org.apache.cassandra.index.sai.disk.v1.kdtree.MutableOneDimPointValues; +import org.apache.cassandra.index.sai.disk.v1.kdtree.NumericIndexWriter; +import org.apache.cassandra.index.sai.disk.v1.trie.InvertedIndexWriter; +import org.apache.cassandra.index.sai.disk.vector.CassandraOnHeapGraph; +import org.apache.cassandra.index.sai.disk.vector.CompactionGraph; +import org.apache.cassandra.index.sai.metrics.IndexMetrics; +import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.metrics.QuickSlidingWindowReservoir; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; +import org.apache.lucene.util.BytesRef; + +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_TEST_LAST_VALID_SEGMENTS; +import static org.apache.cassandra.utils.FBUtilities.busyWaitWhile; + +/** + * Creates an on-heap index data structure to be flushed to an SSTable index. + *

    + * Not threadsafe, but does potentially make concurrent calls to addInternal by + * delegating them to an asynchronous executor. This will be done when supportsAsyncAdd is true. + * Callers should check getAsyncThrowable when they are done adding rows to see if there was an error. + */ +@NotThreadSafe +public abstract class SegmentBuilder +{ + private static final Logger logger = LoggerFactory.getLogger(SegmentBuilder.class); + + /** for parallelism within a single compaction */ + public static final ExecutorService compactionExecutor = executorFactory().configurePooled("SegmentBuilder", Runtime.getRuntime().availableProcessors()) + .withQueueLimit(10 * Runtime.getRuntime().availableProcessors()) + .withKeepAlive(1, TimeUnit.MINUTES) + .withThreadPriority(Thread.MIN_PRIORITY) + .build(); + + // Served as safe net in case memory limit is not triggered or when merger merges small segments.. + public static final long LAST_VALID_SEGMENT_ROW_ID = ((long)Integer.MAX_VALUE / 2) - 1L; + private static long testLastValidSegmentRowId = SAI_TEST_LAST_VALID_SEGMENTS.getLong(); + + /** The number of column indexes being built globally. (Starts at one to avoid divide by zero.) */ + public static final AtomicLong ACTIVE_BUILDER_COUNT = new AtomicLong(1); + + /** Minimum flush size, dynamically updated as segment builds are started and completed/aborted. */ + private static volatile long minimumFlushBytes; + + protected final IndexComponents.ForWrite components; + + final AbstractType termComparator; + final AbstractAnalyzer analyzer; + + // track memory usage for this segment so we can flush when it gets too big + private final NamedMemoryLimiter limiter; + long totalBytesAllocated; + // when we're adding terms asynchronously, totalBytesAllocated will be an approximation and this tracks the exact size + final LongAdder totalBytesAllocatedConcurrent = new LongAdder(); + + private final long lastValidSegmentRowID; + + private boolean flushed = false; + private boolean active = true; + + // segment metadata + private long minSSTableRowId = -1; + private long maxSSTableRowId = -1; + private long segmentRowIdOffset = 0; + int rowCount = 0; + long totalTermCount = 0; + int maxSegmentRowId = -1; + // in token order + private PrimaryKey minKey; + private PrimaryKey maxKey; + // in termComparator order + protected ByteBuffer minTerm; + protected ByteBuffer maxTerm; + + protected final AtomicInteger updatesInFlight = new AtomicInteger(0); + protected final QuickSlidingWindowReservoir termSizeReservoir = new QuickSlidingWindowReservoir(100); + protected AtomicReference asyncThrowable = new AtomicReference<>(); + + + public boolean requiresFlush() + { + return false; + } + + public static class KDTreeSegmentBuilder extends SegmentBuilder + { + protected final byte[] buffer; + private final BKDTreeRamBuffer kdTreeRamBuffer; + private final IndexWriterConfig indexWriterConfig; + + KDTreeSegmentBuilder(IndexComponents.ForWrite components, long rowIdOffset, NamedMemoryLimiter limiter, IndexWriterConfig indexWriterConfig) + { + super(components, rowIdOffset, limiter); + + int typeSize = TypeUtil.fixedSizeOf(termComparator); + this.kdTreeRamBuffer = new BKDTreeRamBuffer(1, typeSize); + this.buffer = new byte[typeSize]; + this.indexWriterConfig = indexWriterConfig; + totalBytesAllocated = kdTreeRamBuffer.ramBytesUsed(); + totalBytesAllocatedConcurrent.add(totalBytesAllocated);} + + public boolean isEmpty() + { + return kdTreeRamBuffer.numRows() == 0; + } + + @Override + protected long addInternal(List terms, int segmentRowId) + { + assert terms.size() == 1; + TypeUtil.toComparableBytes(terms.get(0), termComparator, buffer); + return kdTreeRamBuffer.addPackedValue(segmentRowId, new BytesRef(buffer)); + } + + @Override + protected void flushInternal(SegmentMetadataBuilder metadataBuilder) throws IOException + { + try (NumericIndexWriter writer = new NumericIndexWriter(components, + TypeUtil.fixedSizeOf(termComparator), + maxSegmentRowId, + kdTreeRamBuffer.numPoints(), + indexWriterConfig)) + { + + MutableOneDimPointValues values = kdTreeRamBuffer.asPointValues(); + var metadataMap = writer.writeAll(metadataBuilder.intercept(values)); + metadataBuilder.setComponentsMetadata(metadataMap); + } + } + + @Override + public boolean requiresFlush() + { + return kdTreeRamBuffer.requiresFlush(); + } + } + + public static class RAMStringSegmentBuilder extends SegmentBuilder + { + final RAMStringIndexer ramIndexer; + private final ByteComparable.Version byteComparableVersion; + + RAMStringSegmentBuilder(IndexComponents.ForWrite components, long rowIdOffset, NamedMemoryLimiter limiter) + { + super(components, rowIdOffset, limiter); + this.byteComparableVersion = components.byteComparableVersionFor(IndexComponentType.TERMS_DATA); + ramIndexer = new RAMStringIndexer(writeFrequencies()); + totalBytesAllocated = ramIndexer.estimatedBytesUsed(); + totalBytesAllocatedConcurrent.add(totalBytesAllocated); + } + + private boolean writeFrequencies() + { + return !(analyzer instanceof NoOpAnalyzer) && components.version().onOrAfter(Version.BM25_EARLIEST); + } + + public boolean isEmpty() + { + return ramIndexer.isEmpty(); + } + + @Override + protected long addInternal(List terms, int segmentRowId) + { + var bytesRefs = terms.stream() + .map(term -> components.onDiskFormat().encodeForTrie(term, termComparator)) + .map(encodedTerm -> ByteSourceInverse.readBytes(encodedTerm.asComparableBytes(byteComparableVersion))) + .map(BytesRef::new) + .collect(Collectors.toList()); + // ramIndexer is responsible for merging duplicate (term, row) pairs + return ramIndexer.addAll(bytesRefs, segmentRowId); + } + + @Override + protected void flushInternal(SegmentMetadataBuilder metadataBuilder) throws IOException + { + try (InvertedIndexWriter writer = new InvertedIndexWriter(components, writeFrequencies())) + { + TermsIterator termsWithPostings = ramIndexer.getTermsWithPostings(minTerm, maxTerm, byteComparableVersion); + var docLengths = ramIndexer.getDocLengths(); + var metadataMap = writer.writeAll(metadataBuilder.intercept(termsWithPostings), docLengths); + metadataBuilder.setComponentsMetadata(metadataMap); + } + } + + @Override + public boolean requiresFlush() + { + return ramIndexer.requiresFlush(); + } + } + + public static class VectorOffHeapSegmentBuilder extends SegmentBuilder + { + private final CompactionGraph graphIndex; + + public VectorOffHeapSegmentBuilder(IndexComponents.ForWrite components, + long rowIdOffset, + long keyCount, + VectorCompressor compressor, + boolean unitVectors, + boolean allRowsHaveVectors, + NamedMemoryLimiter limiter) + { + super(components, rowIdOffset, limiter); + try + { + graphIndex = new CompactionGraph(components, compressor, unitVectors, keyCount, allRowsHaveVectors); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + totalBytesAllocated = graphIndex.ramBytesUsed(); + totalBytesAllocatedConcurrent.add(totalBytesAllocated); + } + + @Override + public boolean isEmpty() + { + return graphIndex.isEmpty(); + } + + @Override + protected long addInternal(List terms, int segmentRowId) + { + throw new UnsupportedOperationException(); + } + + @Override + protected long addInternalAsync(List terms, int segmentRowId) + { + assert terms.size() == 1; + + // CompactionGraph splits adding a node into two parts: + // (1) maybeAddVector, which must be done serially because it writes to disk incrementally + // (2) addGraphNode, which may be done asynchronously + CompactionGraph.InsertionResult result; + try + { + result = graphIndex.maybeAddVector(terms.get(0), segmentRowId); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + if (result.vector == null) + return result.bytesUsed; + + updatesInFlight.incrementAndGet(); + compactionExecutor.submit(() -> { + try + { + long bytesAdded = result.bytesUsed + graphIndex.addGraphNode(result); + totalBytesAllocatedConcurrent.add(bytesAdded); + termSizeReservoir.update(bytesAdded); + } + catch (Throwable th) + { + asyncThrowable.compareAndExchange(null, th); + } + finally + { + updatesInFlight.decrementAndGet(); + } + }); + // bytes allocated will be approximated immediately as the average of recently added terms, + // rather than waiting until the async update completes to get the exact value. The latter could + // result in a dangerously large discrepancy between the amount of memory actually consumed + // and the amount the limiter knows about if the queue depth grows. + busyWaitWhile(() -> termSizeReservoir.size() == 0 && asyncThrowable.get() == null); + if (asyncThrowable.get() != null) { + throw new RuntimeException("Error adding term asynchronously", asyncThrowable.get()); + } + return (long) termSizeReservoir.getMean(); + } + + @Override + protected void flushInternal(SegmentMetadataBuilder metadataBuilder) throws IOException + { + if (graphIndex.isEmpty()) + return; + var componentsMetadata = graphIndex.flush(); + metadataBuilder.setComponentsMetadata(componentsMetadata); + } + + @Override + public boolean supportsAsyncAdd() + { + return true; + } + + @Override + public boolean requiresFlush() + { + return graphIndex.requiresFlush(); + } + + @Override + long release(IndexContext indexContext) + { + try + { + graphIndex.close(); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + return super.release(indexContext); + } + } + + public static class VectorOnHeapSegmentBuilder extends SegmentBuilder + { + private final CassandraOnHeapGraph graphIndex; + + public VectorOnHeapSegmentBuilder(IndexComponents.ForWrite components, long rowIdOffset, long keyCount, NamedMemoryLimiter limiter) + { + super(components, rowIdOffset, limiter); + graphIndex = new CassandraOnHeapGraph<>(components.context(), false, null); + totalBytesAllocated = graphIndex.ramBytesUsed(); + totalBytesAllocatedConcurrent.add(totalBytesAllocated); + } + + @Override + public boolean isEmpty() + { + return graphIndex.isEmpty(); + } + + @Override + protected long addInternal(List terms, int segmentRowId) + { + assert terms.size() == 1; + return graphIndex.add(terms.get(0), segmentRowId); + } + + @Override + protected long addInternalAsync(List terms, int segmentRowId) + { + updatesInFlight.incrementAndGet(); + compactionExecutor.submit(() -> { + try + { + long bytesAdded = addInternal(terms, segmentRowId); + totalBytesAllocatedConcurrent.add(bytesAdded); + termSizeReservoir.update(bytesAdded); + } + catch (Throwable th) + { + asyncThrowable.compareAndExchange(null, th); + } + finally + { + updatesInFlight.decrementAndGet(); + } + }); + // bytes allocated will be approximated immediately as the average of recently added terms, + // rather than waiting until the async update completes to get the exact value. The latter could + // result in a dangerously large discrepancy between the amount of memory actually consumed + // and the amount the limiter knows about if the queue depth grows. + busyWaitWhile(() -> termSizeReservoir.size() == 0 && asyncThrowable.get() == null); + if (asyncThrowable.get() != null) { + throw new RuntimeException("Error adding term asynchronously", asyncThrowable.get()); + } + return (long) termSizeReservoir.getMean(); + } + + @Override + protected void flushInternal(SegmentMetadataBuilder metadataBuilder) throws IOException + { + var shouldFlush = graphIndex.preFlush(p -> p); + // there are no deletes to worry about when building the index during compaction, + // and SegmentBuilder::flush checks for the empty index case before calling flushInternal + assert shouldFlush; + var componentsMetadata = graphIndex.flush(components); + metadataBuilder.setComponentsMetadata(componentsMetadata); + } + + @Override + public boolean supportsAsyncAdd() + { + return true; + } + } + + private SegmentBuilder(IndexComponents.ForWrite components, long rowIdOffset, NamedMemoryLimiter limiter) + { + IndexContext context = Objects.requireNonNull(components.context(), "IndexContext must be set on segment builder"); + this.components = components; + this.termComparator = context.getValidator(); + this.analyzer = context.getAnalyzerFactory().create(); + this.limiter = limiter; + this.segmentRowIdOffset = rowIdOffset; + this.lastValidSegmentRowID = testLastValidSegmentRowId >= 0 ? testLastValidSegmentRowId : LAST_VALID_SEGMENT_ROW_ID; + + minimumFlushBytes = limiter.limitBytes() / ACTIVE_BUILDER_COUNT.getAndIncrement(); + } + + public SegmentMetadata flush() throws IOException + { + assert !flushed; + flushed = true; + + if (getRowCount() == 0) + { + logger.warn(components.logMessage("No rows to index during flush of SSTable {}."), components.descriptor()); + return null; + } + + SegmentMetadataBuilder metadataBuilder = new SegmentMetadataBuilder(segmentRowIdOffset, components); + metadataBuilder.setKeyRange(minKey, maxKey); + metadataBuilder.setRowIdRange(minSSTableRowId, maxSSTableRowId); + metadataBuilder.setTermRange(minTerm, maxTerm); + metadataBuilder.setNumRows(getRowCount()); + metadataBuilder.setTotalTermCount(totalTermCount); + + flushInternal(metadataBuilder); + return metadataBuilder.build(); + } + + public long analyzeAndAdd(ByteBuffer rawTerm, + AbstractType type, + PrimaryKey key, + long sstableRowId, + @Nullable IndexMetrics indexMetrics) + { + long totalSize = 0; + if (TypeUtil.isLiteral(type)) + { + var terms = ByteLimitedMaterializer.materializeTokens(analyzer, rawTerm, components.context(), key); + totalSize += add(terms, key, sstableRowId); + totalTermCount += terms.size(); + if (indexMetrics != null) + indexMetrics.compactionTermsProcessedCount.inc(terms.size()); + } + else + { + totalSize += add(List.of(rawTerm), key, sstableRowId); + totalTermCount++; + if (indexMetrics != null) + indexMetrics.compactionTermsProcessedCount.inc(); + } + return totalSize; + } + + private long add(List terms, PrimaryKey key, long sstableRowId) + { + if (terms.isEmpty()) + return 0; + + Preconditions.checkState(!flushed, "Cannot add to flushed segment"); + Preconditions.checkArgument(sstableRowId >= maxSSTableRowId, + "rowId must be greater than or equal to the last rowId added: %s < %s", sstableRowId, maxSSTableRowId); + Preconditions.checkArgument(maxKey == null || key.compareTo(maxKey) >= 0, + "Key must be greater than or equal to the last key added: %s < %s", key, maxKey); + + minSSTableRowId = minSSTableRowId < 0 ? sstableRowId : minSSTableRowId; + maxSSTableRowId = sstableRowId; + + minKey = minKey == null ? key : minKey; + maxKey = key; + + // Update term boundaries for all terms in this row + for (ByteBuffer term : terms) + { + assert term != null : "term must not be null"; + minTerm = TypeUtil.min(term, minTerm, termComparator, components.version()); + maxTerm = TypeUtil.max(term, maxTerm, termComparator, components.version()); + } + + assert minTerm != null : "minTerm should not be null at this point"; + assert maxTerm != null : "maxTerm should not be null at this point"; + + // segmentRowIdOffset should encode sstableRowId into Integer + int segmentRowId = Math.toIntExact(sstableRowId - segmentRowIdOffset); + + if (segmentRowId == PostingList.END_OF_STREAM) + throw new IllegalArgumentException("Illegal segment row id: END_OF_STREAM found"); + + maxSegmentRowId = Math.max(maxSegmentRowId, segmentRowId); + + long bytesAllocated; + if (supportsAsyncAdd()) + { + // only vector indexing is done async and there can only be one term + assert terms.size() == 1; + bytesAllocated = addInternalAsync(terms, segmentRowId); + } + else + { + bytesAllocated = addInternal(terms, segmentRowId); + } + + totalBytesAllocated += bytesAllocated; + return bytesAllocated; + } + + protected long addInternalAsync(List terms, int segmentRowId) + { + throw new UnsupportedOperationException(); + } + + public boolean supportsAsyncAdd() { + return false; + } + + public Throwable getAsyncThrowable() + { + return asyncThrowable.get(); + } + + public void awaitAsyncAdditions() + { + // addTerm is only called by the compaction thread, serially, so we don't need to worry about new + // terms being added while we're waiting -- updatesInFlight can only decrease + busyWaitWhile(() -> updatesInFlight.get() > 0); + } + + long totalBytesAllocated() + { + return totalBytesAllocated; + } + + boolean hasReachedMinimumFlushSize() + { + return totalBytesAllocated >= minimumFlushBytes; + } + + long getMinimumFlushBytes() + { + return minimumFlushBytes; + } + + /** + * This method does three things: + * + * 1.) It decrements active builder count and updates the global minimum flush size to reflect that. + * 2.) It releases the builder's memory against its limiter. + * 3.) It defensively marks the builder inactive to make sure nothing bad happens if we try to close it twice. + * + * @param indexContext + * + * @return the number of bytes currently used by the memory limiter + */ + long release(IndexContext indexContext) + { + if (active) + { + minimumFlushBytes = limiter.limitBytes() / ACTIVE_BUILDER_COUNT.decrementAndGet(); + long used = limiter.decrement(totalBytesAllocated); + active = false; + return used; + } + + logger.warn(indexContext.logMessage("Attempted to release storage attached index segment builder memory after builder marked inactive.")); + return limiter.currentBytesUsed(); + } + + public abstract boolean isEmpty(); + + protected abstract long addInternal(List terms, int segmentRowId); + + protected abstract void flushInternal(SegmentMetadataBuilder metadataBuilder) throws IOException; + + int getRowCount() + { + return rowCount; + } + + void incRowCount() + { + rowCount++; + } + + /** + * @return true if next SSTable row ID exceeds max segment row ID + */ + boolean exceedsSegmentLimit(long ssTableRowId) + { + if (getRowCount() == 0) + return false; + + // To handle the case where there are many non-indexable rows. eg. rowId-1 and rowId-3B are indexable, + // the rest are non-indexable. We should flush them as 2 separate segments, because rowId-3B is going + // to cause error in on-disk index structure with 2B limitation. + return ssTableRowId - segmentRowIdOffset > lastValidSegmentRowID; + } + + @VisibleForTesting + public static long updateLastValidSegmentRowId(long lastValidSegmentRowID) + { + long current = testLastValidSegmentRowId; + testLastValidSegmentRowId = lastValidSegmentRowID; + return current; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentMetadata.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentMetadata.java new file mode 100644 index 000000000000..71f80d700365 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentMetadata.java @@ -0,0 +1,570 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v1; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.ModernResettableByteBuffersIndexOutput; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.v6.TermsDistribution; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * Multiple {@link SegmentMetadata} are stored in {@link IndexComponentType#META} file, each corresponds to an on-disk + * index segment. + */ +public class SegmentMetadata implements Comparable +{ + private static final String NAME = "SegmentMetadata"; + + public final Version version; + + /** + * Used to retrieve sstableRowId which equals to offset plus segmentRowId. + */ + public final long segmentRowIdOffset; + + /** + * Min and max sstable rowId in current segment. + * + * For index generated by compaction, minSSTableRowId is the same as segmentRowIdOffset. + * But for flush, segmentRowIdOffset is taken from previous segment's maxSSTableRowId. + */ + public final long minSSTableRowId; + public final long maxSSTableRowId; + + /** + * number of indexed rows (aka. pair of term and segmentRowId) in current segment + */ + public final long numRows; + /** + * Represents the total count of terms in a segment. + * It used to caclulate the average document length for BM25. + */ + public final long totalTermCount; + /** + * A constant representing an invalid total term count when it cannot be read + * from disk, since the SAI format version doesn't support serializing it. + */ + public static final long INVALID_TOTAL_TERM_COUNT = -1; + + /** + * Ordered by their token position in current segment + */ + public final PrimaryKey minKey; + public final PrimaryKey maxKey; + + /** + * Minimum and maximum indexed column value ordered by its {@link org.apache.cassandra.db.marshal.AbstractType}. + */ + public final ByteBuffer minTerm; + public final ByteBuffer maxTerm; + + + /** + * Statistical distribution of term values, useful for estimating selectivity of queries against this segment. + */ + public final TermsDistribution termsDistribution; + + /** + * Root, offset, length for each index structure in the segment. + * + * Note: postings block offsets are stored in terms dictionary, no need to worry about its root. + */ + public final ComponentMetadataMap componentMetadatas; + + SegmentMetadata(long segmentRowIdOffset, + long numRows, + long minSSTableRowId, + long maxSSTableRowId, + PrimaryKey minKey, + PrimaryKey maxKey, + ByteBuffer minTerm, + ByteBuffer maxTerm, + TermsDistribution termsDistribution, + ComponentMetadataMap componentMetadatas, + long totalTermCount, + Version version) + { + // numRows can exceed Integer.MAX_VALUE because it is the count of unique term and segmentRowId pairs. + Objects.requireNonNull(minKey); + Objects.requireNonNull(maxKey); + Objects.requireNonNull(minTerm); + Objects.requireNonNull(maxTerm); + + this.version = version; + this.segmentRowIdOffset = segmentRowIdOffset; + this.minSSTableRowId = minSSTableRowId; + this.maxSSTableRowId = maxSSTableRowId; + this.numRows = numRows; + this.totalTermCount = totalTermCount; + this.minKey = minKey; + this.maxKey = maxKey; + this.minTerm = minTerm; + this.maxTerm = maxTerm; + this.termsDistribution = termsDistribution; + this.componentMetadatas = componentMetadatas; + } + + private static final Logger logger = LoggerFactory.getLogger(SegmentMetadata.class); + + @SuppressWarnings("resource") + private SegmentMetadata(IndexInput input, IndexContext context, Version version, SSTableContext sstableContext, boolean loadFullResolutionBounds) throws IOException + { + if (!loadFullResolutionBounds) + logger.warn("Loading segment metadata without full primary key boundary resolution. Some ORDER BY queries" + + " may not work correctly."); + + AbstractType termsType = context.getValidator(); + + this.version = version; + this.segmentRowIdOffset = input.readLong(); + this.numRows = input.readLong(); + this.minSSTableRowId = input.readLong(); + this.maxSSTableRowId = input.readLong(); + + if (loadFullResolutionBounds) + { + // Skip the min/max partition keys since we want the fully resolved PrimaryKey for better semantics. + // Also, these values are not always correct for flushed sstables, but the min/max row ids are, which + // provides further justification for skipping them. + skipBytes(input); + skipBytes(input); + + // Get the fully qualified PrimaryKey min and max objects to ensure that we skip several edge cases related + // to possibly confusing equality semantics. The main issue is how we handl PrimaryKey objects that + // are not fully qualified when doing a binary search on a collection of PrimaryKeyWithSource objects. + // By materializing the fully qualified PrimaryKey objects, we get the right binary search result. + final PrimaryKey min, max; + try (var pkm = sstableContext.primaryKeyMapFactory().newPerSSTablePrimaryKeyMap()) + { + // We need to load eagerly to allow us to close the partition key map. + min = pkm.primaryKeyFromRowId(minSSTableRowId).loadDeferred(); + max = pkm.primaryKeyFromRowId(maxSSTableRowId).loadDeferred(); + this.minKey = pkm.primaryKeyFromRowId(minSSTableRowId, min, max).loadDeferred(); + this.maxKey = pkm.primaryKeyFromRowId(maxSSTableRowId, min, max).loadDeferred(); + } + } + else + { + assert sstableContext == null; + // Only valid in some very specific tests. + PrimaryKey.Factory primaryKeyFactory = context.keyFactory(); + this.minKey = primaryKeyFactory.createPartitionKeyOnly(DatabaseDescriptor.getPartitioner().decorateKey(readBytes(input))); + this.maxKey = primaryKeyFactory.createPartitionKeyOnly(DatabaseDescriptor.getPartitioner().decorateKey(readBytes(input))); + } + + this.minTerm = readBytes(input); + this.maxTerm = readBytes(input); + TermsDistribution td = null; + if (version.onOrAfter(Version.EB)) + { + int len = input.readInt(); + long fp = input.getFilePointer(); + if (len > 0) + { + td = TermsDistribution.read(input, termsType); + input.seek(fp + len); + } + } + this.termsDistribution = td; + this.componentMetadatas = new SegmentMetadata.ComponentMetadataMap(input); + + if (version.onOrAfter(Version.ED)) + this.totalTermCount = input.readLong(); + else + this.totalTermCount = INVALID_TOTAL_TERM_COUNT; + } + + @SuppressWarnings("resource") + public static List load(MetadataSource source, IndexContext context, SSTableContext sstableContext) throws IOException + { + return load(source, context, sstableContext, true); + } + + /** + * This is only visible for testing because the SegmentFlushTest creates fake boundary scenarios that break + * normal assumptions about the min/max row ids mapping to specific positions in the per-sstable index components. + * Only set loadFullResolutionBounds to false in tests when you are sure that is the only possible solution. + */ + @VisibleForTesting + @SuppressWarnings("resource") + public static List loadForTesting(MetadataSource source, IndexContext context) throws IOException + { + return load(source, context, null, false); + } + + /** + * Only set loadFullResolutionBounds to false in tests when you are sure that is exactly what you want. + */ + private static List load(MetadataSource source, IndexContext context, SSTableContext sstableContext, boolean loadFullResolutionBounds) throws IOException + { + + IndexInput input = source.get(NAME); + + int segmentCount = input.readVInt(); + + List segmentMetadata = new ArrayList<>(segmentCount); + + for (int i = 0; i < segmentCount; i++) + { + segmentMetadata.add(new SegmentMetadata(input, context, source.getVersion(), sstableContext, loadFullResolutionBounds)); + } + + return segmentMetadata; + } + + /** + * Writes disk metadata for the given segment list. + */ + @SuppressWarnings("resource") + public static void write(MetadataWriter writer, List segments) throws IOException + { + try (IndexOutput output = writer.builder(NAME)) + { + output.writeVInt(segments.size()); + + for (SegmentMetadata metadata : segments) + { + output.writeLong(metadata.segmentRowIdOffset); + output.writeLong(metadata.numRows); + output.writeLong(metadata.minSSTableRowId); + output.writeLong(metadata.maxSSTableRowId); + + Stream.of(metadata.minKey.partitionKey().getKey(), + metadata.maxKey.partitionKey().getKey(), + metadata.minTerm, metadata.maxTerm).forEach(bb -> writeBytes(bb, output)); + + if (writer.version().onOrAfter(Version.EB)) + { + if (metadata.termsDistribution != null) + { + var tmp = new ModernResettableByteBuffersIndexOutput(1024, "", output.version()); + metadata.termsDistribution.write(tmp); + output.writeInt(tmp.intSize()); + tmp.copyTo(output); + } + else + { + // some indexes, e.g. vector may have no terms distribution + output.writeInt(0); + } + } + + metadata.componentMetadatas.write(output); + + if (writer.version().onOrAfter(Version.ED)) + { + assert metadata.totalTermCount >= 0 : "totalTermCount cannot be unknown on this or later version"; + output.writeLong(metadata.totalTermCount); + } + } + } + } + + @Override + public int compareTo(SegmentMetadata other) + { + return Long.compare(this.segmentRowIdOffset, other.segmentRowIdOffset); + } + + @Override + public String toString() + { + return "SegmentMetadata{" + + "segmentRowIdOffset=" + segmentRowIdOffset + + ", minSSTableRowId=" + minSSTableRowId + + ", maxSSTableRowId=" + maxSSTableRowId + + ", numRows=" + numRows + + ", componentMetadatas=" + componentMetadatas + + '}'; + } + + public long estimateNumRowsMatching(Expression predicate) + { + if (termsDistribution == null) + throw new IllegalStateException("Terms distribution not available for " + this); + + + switch (predicate.getOp()) + { + case MATCH: + case EQ: + case CONTAINS_KEY: + case CONTAINS_VALUE: + { + var value = asByteComparable(predicate.lower.value.encoded, predicate.validator); + return termsDistribution.estimateNumRowsMatchingExact(value); + } + case NOT_EQ: + case NOT_CONTAINS_KEY: + case NOT_CONTAINS_VALUE: + { + if (TypeUtil.supportsRounding(predicate.validator)) + return numRows; + else + { + var value = asByteComparable(predicate.lower.value.encoded, predicate.validator); + return numRows - termsDistribution.estimateNumRowsMatchingExact(value); + } + } + case RANGE: + { + var lower = predicate.lower != null ? asByteComparable(predicate.lower.value.encoded, predicate.validator) : null; + var upper = predicate.upper != null ? asByteComparable(predicate.upper.value.encoded, predicate.validator) : null; + boolean lowerInclusive = predicate.lower != null && predicate.lower.inclusive; + boolean upperInclusive = predicate.upper != null && predicate.upper.inclusive; + return termsDistribution.estimateNumRowsInRange(lower, lowerInclusive, upper, upperInclusive); + } + default: + throw new IllegalArgumentException("Unsupported expression: " + predicate); + } + } + + private ByteComparable asByteComparable(ByteBuffer value, AbstractType type) + { + if (TypeUtil.isLiteral(type)) + return version.onDiskFormat().encodeForTrie(value, type); + + byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)]; + TypeUtil.toComparableBytes(value, type, buffer); + return ByteComparable.preencoded(termsDistribution.byteComparableVersion, buffer); + } + + private static ByteBuffer readBytes(IndexInput input) throws IOException + { + int len = input.readVInt(); + byte[] bytes = new byte[len]; + input.readBytes(bytes, 0, len); + return ByteBuffer.wrap(bytes); + } + + private static void skipBytes(IndexInput input) throws IOException + { + int len = input.readVInt(); + input.skipBytes(len); + } + + static void writeBytes(ByteBuffer buf, IndexOutput out) + { + try + { + byte[] bytes = ByteBufferUtil.getArray(buf); + out.writeVInt(bytes.length); + out.writeBytes(bytes, 0, bytes.length); + } + catch (IOException ioe) + { + throw new RuntimeException(ioe); + } + } + + long getIndexRoot(IndexComponentType indexComponentType) + { + return componentMetadatas.get(indexComponentType).root; + } + + public int toSegmentRowId(long sstableRowId) + { + int segmentRowId = Math.toIntExact(sstableRowId - segmentRowIdOffset); + + if (segmentRowId == PostingList.END_OF_STREAM) + throw new IllegalArgumentException("Illegal segment row id: END_OF_STREAM found"); + + return segmentRowId; + } + + public static class ComponentMetadataMap + { + private final Map metas = new HashMap<>(); + + ComponentMetadataMap(IndexInput input) throws IOException + { + int size = input.readInt(); + + for (int i = 0; i < size; i++) + { + metas.put(IndexComponentType.valueOf(input.readString()), new ComponentMetadata(input)); + } + } + + public ComponentMetadataMap() + { + } + + public void put(IndexComponentType indexComponentType, long root, long offset, long length) + { + metas.put(indexComponentType, new ComponentMetadata(root, offset, length)); + } + + public void put(IndexComponentType indexComponentType, long root, long offset, long length, Map additionalMap) + { + metas.put(indexComponentType, new ComponentMetadata(root, offset, length, additionalMap)); + } + + private void write(IndexOutput output) throws IOException + { + output.writeInt(metas.size()); + + for (Map.Entry entry : metas.entrySet()) + { + output.writeString(entry.getKey().name()); + entry.getValue().write(output); + } + } + + public ComponentMetadata get(IndexComponentType indexComponentType) + { + if (!metas.containsKey(indexComponentType)) + throw new IllegalArgumentException(indexComponentType + " ComponentMetadata not found"); + + return metas.get(indexComponentType); + } + + public ComponentMetadata getOptional(IndexComponentType indexComponentType) + { + return metas.get(indexComponentType); + } + + public Map> asMap() + { + Map> metaAttributes = new HashMap<>(); + + for (Map.Entry entry : metas.entrySet()) + { + String name = entry.getKey().name(); + ComponentMetadata metadata = entry.getValue(); + + Map componentAttributes = metadata.asMap(); + + assert !metaAttributes.containsKey(name) : "Found duplicate index type: " + name; + metaAttributes.put(name, componentAttributes); + } + + return metaAttributes; + } + + @Override + public String toString() + { + return "ComponentMetadataMap{" + + "metas=" + metas + + '}'; + } + + public double indexSize() + { + return metas.values().stream().mapToLong(meta -> meta.length).sum(); + } + } + + public static class ComponentMetadata + { + public static final String ROOT = "Root"; + public static final String OFFSET = "Offset"; + public static final String LENGTH = "Length"; + + public final long root; + public final long offset; + public final long length; + public final Map attributes; + + public ComponentMetadata(long root, long offset, long length) + { + this.root = root; + this.offset = offset; + this.length = length; + this.attributes = Collections.emptyMap(); + } + + ComponentMetadata(long root, long offset, long length, Map attributes) + { + this.root = root; + this.offset = offset; + this.length = length; + this.attributes = attributes; + } + + ComponentMetadata(IndexInput input) throws IOException + { + this.root = input.readLong(); + this.offset = input.readLong(); + this.length = input.readLong(); + int size = input.readInt(); + + attributes = new HashMap<>(size); + for (int x=0; x < size; x++) + { + String key = input.readString(); + String value = input.readString(); + + attributes.put(key, value); + } + } + + public void write(IndexOutput output) throws IOException + { + output.writeLong(root); + output.writeLong(offset); + output.writeLong(length); + + output.writeInt(attributes.size()); + for (Map.Entry entry : attributes.entrySet()) + { + output.writeString(entry.getKey()); + output.writeString(entry.getValue()); + } + } + + @Override + public String toString() + { + return String.format("ComponentMetadata{root=%d, offset=%d, length=%d, attributes=%s}", root, offset, length, attributes.toString()); + } + + public Map asMap() + { + return ImmutableMap.builder().putAll(attributes).put(OFFSET, Long.toString(offset)).put(LENGTH, Long.toString(length)).put(ROOT, Long.toString(root)).build(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentMetadataBuilder.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentMetadataBuilder.java new file mode 100644 index 000000000000..90e8c925ae96 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SegmentMetadataBuilder.java @@ -0,0 +1,394 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.TermsIterator; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.kdtree.MutableOneDimPointValues; +import org.apache.cassandra.index.sai.disk.v6.TermsDistribution; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.lucene.util.BytesRef; + +/** + * {@link SegmentMetadata} contains a lot of information, so it got its own Builder. + * The builder is not only responsible for setting the fields, but also intercepts + * the index building process and records the {@link TermsDistribution}. + */ +@NotThreadSafe +public class SegmentMetadataBuilder +{ + private static final String HISTOGRAM_SIZE_OPTION = "statistics.histogram_size"; + private static final String MFT_COUNT_OPTION = "statistics.most_frequent_terms_count"; + + private final long segmentRowIdOffset; + + private final List interceptors = new ArrayList<>(); + private final ByteComparable.Version byteComparableVersion; + + private boolean built = false; + + private PrimaryKey minKey; + private PrimaryKey maxKey; + + private long minRowId = -1; + private long maxRowId = -1; + + private ByteBuffer minTerm; + private ByteBuffer maxTerm; + + private long numRows; + private long totalTermCount; + + private final TermsDistribution.Builder termsDistributionBuilder; + private final Version version; + + SegmentMetadata.ComponentMetadataMap metadataMap; + + public SegmentMetadataBuilder(long segmentRowIdOffset, IndexComponents.ForWrite components) + { + IndexContext context = Objects.requireNonNull(components.context()); + this.version = context.version(); + this.segmentRowIdOffset = segmentRowIdOffset; + this.byteComparableVersion = components.byteComparableVersionFor(IndexComponentType.TERMS_DATA); + + int histogramSize = context.getIntOption(HISTOGRAM_SIZE_OPTION, 128); + int mostFrequentTermsCount = context.getIntOption(MFT_COUNT_OPTION, 128); + this.termsDistributionBuilder = new TermsDistribution.Builder(context.getValidator(), byteComparableVersion, histogramSize, mostFrequentTermsCount, version); + } + + public void setNumRows(long numRows) + { + this.numRows = numRows; + } + + public void setTotalTermCount(long totalTermCount) + { + this.totalTermCount = totalTermCount; + } + + public void setKeyRange(@Nonnull PrimaryKey minKey, @Nonnull PrimaryKey maxKey) + { + Preconditions.checkNotNull(minKey, "minKey must not be null"); + Preconditions.checkNotNull(maxKey, "maxKey must not be null"); + Preconditions.checkArgument(minKey.compareTo(maxKey) <= 0, "minKey (" + minKey + ") must not be greater than (" + maxKey + ')'); + this.minKey = minKey; + this.maxKey = maxKey; + } + + public void setRowIdRange(long minRowId, long maxRowId) + { + Preconditions.checkArgument(minRowId <= maxRowId, "minRowId (" + minRowId + ") must not be greater than (" + maxRowId + ')'); + this.minRowId = minRowId; + this.maxRowId = maxRowId; + } + + /** + * Sets the term range of the data indexed by this segment. + * We need this method because we cannot automatically record min and max term. We need exact + * values but the values from the index are trucated to 20 bytes for some types like e.g. BigDecimals. + *

    + * This method requires raw serializations of the term types, not bytecomparable encodings. + */ + public void setTermRange(@Nonnull ByteBuffer minTerm, @Nonnull ByteBuffer maxTerm) + { + Preconditions.checkNotNull(minTerm, "minTerm must not be null"); + Preconditions.checkNotNull(maxTerm, "maxTerm must not be null"); + this.minTerm = minTerm; + this.maxTerm = maxTerm; + } + + public void setComponentsMetadata(SegmentMetadata.ComponentMetadataMap metadataMap) + { + this.metadataMap = metadataMap; + } + + /** + * Should be called whenever a point is added to the index. + * Points must be added in the index term order. + * @param term the term value + * @param rowCount the number of rows with this term value in the segment + */ + void add(ByteComparable term, int rowCount) + { + if (built) + throw new IllegalStateException("Segment metadata already built, no more additions allowed"); + + termsDistributionBuilder.add(term, rowCount); + } + + public @Nonnull SegmentMetadata build() + { + if (minRowId == -1 || maxRowId == -1) + throw new IllegalStateException("Segment row id range not set"); + if (minKey == null || maxKey == null) + throw new IllegalStateException("Segment key range not set"); + if (minTerm == null || maxTerm == null) + throw new IllegalStateException("Term range not set"); + + FileUtils.closeQuietly(interceptors); + built = true; // must be flipped after closing the interceptors, because they may push some data to us when closing + + return new SegmentMetadata(segmentRowIdOffset, + numRows, + minRowId, + maxRowId, + minKey, + maxKey, + minTerm, + maxTerm, + termsDistributionBuilder.build(), + metadataMap, + totalTermCount, + version); + } + + /** + * Wraps a {@link TermsIterator} in such a way that while it is iterated it adds items to this builder. + * Used at index building time to build the {@link TermsDistribution}. + * @return a wrapped iterator which also implements {@link TermsIterator}. + */ + public TermsIterator intercept(TermsIterator iterator) + { + TermsIteratorInterceptor interceptor = new TermsIteratorInterceptor(iterator, this); + interceptors.add(interceptor); + return interceptor; + } + + /** + * Wraps a {@link MutableOneDimPointValues} in such a way that while it is iterated it adds items to this builder. + * Used at index building time to build the {@link TermsDistribution}. + * @return a wrapped iterator which also implements {@link MutableOneDimPointValues}. + */ + public MutableOneDimPointValues intercept(MutableOneDimPointValues values) + { + MutableOneDimPointValuesInterceptor interceptor = new MutableOneDimPointValuesInterceptor(values, this); + interceptors.add(interceptor); + return interceptor; + } + + + private static class TermsIteratorInterceptor implements TermsIterator + { + final TermsIterator iterator; + final SegmentMetadataBuilder builder; + + PostingList postings; + IOException exception; + + public TermsIteratorInterceptor(TermsIterator iterator, SegmentMetadataBuilder builder) + { + this.iterator = iterator; + this.builder = builder; + } + + @Override + public PostingList postings() throws IOException + { + maybeThrow(); + return postings; + } + + @Override + public ByteBuffer getMinTerm() + { + return iterator.getMinTerm(); + } + + @Override + public ByteBuffer getMaxTerm() + { + return iterator.getMaxTerm(); + } + + @Override + public void close() throws IOException + { + iterator.close(); + maybeThrow(); + } + + @Override + public boolean hasNext() + { + return iterator.hasNext(); + } + + @Override + public ByteComparable next() + { + ByteComparable term = iterator.next(); + try + { + postings = iterator.postings(); + } + catch (IOException e) + { + exception = e; + } + builder.add(term, postings.size()); + return term; + } + + private void maybeThrow() throws IOException + { + if (exception != null) + { + IOException e = exception; + exception = null; + throw e; + } + } + } + + private static class MutableOneDimPointValuesInterceptor extends MutableOneDimPointValues implements Closeable + { + final MutableOneDimPointValues values; + final SegmentMetadataBuilder builder; + + byte[] lastTerm; + int count = 0; + + public MutableOneDimPointValuesInterceptor(MutableOneDimPointValues values, SegmentMetadataBuilder builder) + { + this.values = values; + this.builder = builder; + } + + @Override + public int getDocCount() + { + return values.getDocCount(); + } + + @Override + public long size() + { + return values.size(); + } + + @Override + public void getValue(int i, BytesRef packedValue) + { + values.getValue(i, packedValue); + } + + @Override + public byte getByteAt(int i, int k) + { + return values.getByteAt(i, k); + } + + @Override + public int getDocID(int i) + { + return values.getDocID(i); + } + + @Override + public void swap(int i, int j) + { + values.swap(i, j); + } + + @Override + public byte[] getMinPackedValue() + { + return values.getMinPackedValue(); + } + + @Override + public byte[] getMaxPackedValue() + { + return values.getMaxPackedValue(); + } + + @Override + public int getNumDimensions() + { + return values.getNumDimensions(); + } + + @Override + public int getBytesPerDimension() + { + return values.getBytesPerDimension(); + } + + @Override + public int getNumIndexDimensions() throws IOException + { + return values.getNumIndexDimensions(); + } + + @Override + public PointTree getPointTree() throws IOException + { + return values.getPointTree(); + } + + @Override + public void intersect(IntersectVisitor visitor) throws IOException + { + values.intersect((docId, term) -> { + if (!Arrays.equals(term, lastTerm)) + { + if (lastTerm != null) + builder.add(ByteComparable.preencoded(builder.byteComparableVersion, lastTerm), count); + + + count = 0; + lastTerm = Arrays.copyOf(term, term.length); + } + count++; + visitor.visit(docId, term); + }); + } + + @Override + public void close() + { + if (lastTerm != null) + { + builder.add(ByteComparable.preencoded(builder.byteComparableVersion, lastTerm), count); + } + } + + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SkinnyPrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SkinnyPrimaryKeyMap.java deleted file mode 100644 index ffba96d2599a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/SkinnyPrimaryKeyMap.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.io.IOException; -import java.util.Arrays; -import javax.annotation.concurrent.NotThreadSafe; -import javax.annotation.concurrent.ThreadSafe; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -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.v1.bitpack.BlockPackedReader; -import org.apache.cassandra.index.sai.disk.v1.bitpack.MonotonicBlockPackedReader; -import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; -import org.apache.cassandra.index.sai.disk.v1.keystore.KeyLookupMeta; -import org.apache.cassandra.index.sai.disk.v1.keystore.KeyLookup; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.Throwables; - -/** - * A {@link PrimaryKeyMap} for skinny tables (those with no clustering columns). - *

    - * This uses the following on-disk structures: - *

      - *
    • A block-packed structure for rowId to token value lookups using {@link BlockPackedReader}. - * Uses the {@link IndexComponent#ROW_TO_TOKEN} component
    • - *
    • A monotonic block packed structure for rowId to partitionId lookups using {@link MonotonicBlockPackedReader}. - * Uses the {@link IndexComponent#ROW_TO_PARTITION} component
    • - *
    • A key store for rowId to {@link PrimaryKey} and {@link PrimaryKey} to rowId lookups using - * {@link KeyLookup}. Uses the {@link IndexComponent#PARTITION_KEY_BLOCKS} and - * {@link IndexComponent#PARTITION_KEY_BLOCK_OFFSETS} components
    • - *
    - * - * While the {@link Factory} is threadsafe, individual instances of the {@link SkinnyPrimaryKeyMap} - * are not. - */ -@NotThreadSafe -public class SkinnyPrimaryKeyMap implements PrimaryKeyMap -{ - @ThreadSafe - public static class Factory implements PrimaryKeyMap.Factory - { - protected final MetadataSource metadataSource; - protected final LongArray.Factory rowToTokenReaderFactory; - protected final LongArray.Factory rowToPartitionReaderFactory; - protected final KeyLookup partitionKeyReader; - protected final PrimaryKey.Factory primaryKeyFactory; - protected final SSTableId sstableId; - - private final FileHandle rowToTokenFile; - private final FileHandle rowToPartitionFile; - private final FileHandle partitionKeyBlockOffsetsFile; - private final FileHandle partitionKeyBlocksFile; - - public Factory(IndexDescriptor indexDescriptor) - { - this.rowToTokenFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.ROW_TO_TOKEN, this::close); - this.rowToPartitionFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.ROW_TO_PARTITION, this::close); - this.partitionKeyBlockOffsetsFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.PARTITION_KEY_BLOCK_OFFSETS, this::close); - this.partitionKeyBlocksFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.PARTITION_KEY_BLOCKS, this::close); - try - { - this.metadataSource = MetadataSource.loadGroupMetadata(indexDescriptor); - NumericValuesMeta tokensMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.ROW_TO_TOKEN))); - this.rowToTokenReaderFactory = new BlockPackedReader(rowToTokenFile, tokensMeta); - NumericValuesMeta partitionsMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.ROW_TO_PARTITION))); - this.rowToPartitionReaderFactory = new MonotonicBlockPackedReader(rowToPartitionFile, partitionsMeta); - NumericValuesMeta partitionKeyBlockOffsetsMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PARTITION_KEY_BLOCK_OFFSETS))); - KeyLookupMeta partitionKeysMeta = new KeyLookupMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PARTITION_KEY_BLOCKS))); - this.partitionKeyReader = new KeyLookup(partitionKeyBlocksFile, partitionKeyBlockOffsetsFile, partitionKeysMeta, partitionKeyBlockOffsetsMeta); - this.primaryKeyFactory = indexDescriptor.primaryKeyFactory; - this.sstableId = indexDescriptor.sstableDescriptor.id; - } - catch (Throwable t) - { - throw Throwables.unchecked(t); - } - } - - @Override - @SuppressWarnings({"resource", "RedundantSuppression"}) // rowIdToToken, rowIdToPartitionId and cursor are closed by the SkinnyPrimaryKeyMap#close method - public PrimaryKeyMap newPerSSTablePrimaryKeyMap() throws IOException - { - LongArray rowIdToToken = new LongArray.DeferredLongArray(rowToTokenReaderFactory::open); - LongArray rowIdToPartitionId = new LongArray.DeferredLongArray(rowToPartitionReaderFactory::open); - return new SkinnyPrimaryKeyMap(rowIdToToken, - rowIdToPartitionId, - partitionKeyReader.openCursor(), - primaryKeyFactory, - sstableId); - } - - @Override - public void close() - { - FileUtils.closeQuietly(Arrays.asList(rowToTokenFile, rowToPartitionFile, partitionKeyBlocksFile, partitionKeyBlockOffsetsFile)); - } - } - - protected final LongArray rowIdToTokenArray; - protected final LongArray rowIdToPartitionIdArray; - protected final KeyLookup.Cursor partitionKeyCursor; - protected final PrimaryKey.Factory primaryKeyFactory; - protected final SSTableId sstableId; - - protected SkinnyPrimaryKeyMap(LongArray rowIdToTokenArray, - LongArray rowIdToPartitionIdArray, - KeyLookup.Cursor partitionKeyCursor, - PrimaryKey.Factory primaryKeyFactory, - SSTableId sstableId) - { - this.rowIdToTokenArray = rowIdToTokenArray; - this.rowIdToPartitionIdArray = rowIdToPartitionIdArray; - this.partitionKeyCursor = partitionKeyCursor; - this.primaryKeyFactory = primaryKeyFactory; - this.sstableId = sstableId; - } - - @Override - public SSTableId getSSTableId() - { - return sstableId; - } - - @Override - public PrimaryKey primaryKeyFromRowId(long sstableRowId) - { - return primaryKeyFactory.create(readPartitionKey(sstableRowId)); - } - - @Override - public long rowIdFromPrimaryKey(PrimaryKey primaryKey) - { - long rowId = rowIdToTokenArray.indexOf(primaryKey.token().getLongValue()); - // If the key is token only, the token is out of range, we are at the end of our keys, or we have skipped a token - // we can return straight away. - if (primaryKey.kind() == PrimaryKey.Kind.TOKEN || - rowId < 0 || - rowId + 1 == rowIdToTokenArray.length() || rowIdToTokenArray.get(rowId) != primaryKey.token().getLongValue()) - return rowId; - // Otherwise we need to check for token collision. - return tokenCollisionDetection(primaryKey, rowId); - } - - @Override - public long ceiling(Token token) - { - return rowIdToTokenArray.indexOf(token.getLongValue()); - } - - @Override - public long floor(Token token) - { - if (token.isMinimum()) - return Long.MIN_VALUE; - - return rowIdToTokenArray.indexOf(token.getLongValue()); - } - - @Override - public void close() - { - FileUtils.closeQuietly(Arrays.asList(partitionKeyCursor, rowIdToTokenArray, rowIdToPartitionIdArray)); - } - - // Look for token collision by if the ajacent token in the token array matches the - // current token. If we find a collision we need to compare the partition key instead. - protected long tokenCollisionDetection(PrimaryKey primaryKey, long rowId) - { - // Look for collisions while we haven't reached the end of the tokens and the tokens don't collide - while (rowId + 1 < rowIdToTokenArray.length() && primaryKey.token().getLongValue() == rowIdToTokenArray.get(rowId + 1)) - { - // If we had a collision then see if the partition key for this row is >= to the lookup partition key - if (readPartitionKey(rowId).compareTo(primaryKey.partitionKey()) >= 0) - return rowId; - - rowId++; - } - // Note: We would normally expect to get here without going into the while loop - return rowId; - } - - protected DecoratedKey readPartitionKey(long sstableRowId) - { - return primaryKeyFactory.partitionKeyFromComparableBytes(partitionKeyCursor.seekToPointId(rowIdToPartitionIdArray.get(sstableRowId))); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/TermsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/TermsReader.java new file mode 100644 index 000000000000..b7a1687a27fd --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/TermsReader.java @@ -0,0 +1,513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.TermsIterator; +import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList; +import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader; +import org.apache.cassandra.index.sai.disk.v1.postings.ScanningPostingsReader; +import org.apache.cassandra.index.sai.disk.v1.trie.ReverseTrieTermsDictionaryReader; +import org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryReader; +import org.apache.cassandra.index.sai.metrics.QueryEventListener; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.io.util.ReadPattern; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.validate; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +/** + * Synchronous reader of terms dictionary and postings lists to produce a {@link PostingList} with matching row ids. + * + * {@link #exactMatch(ByteComparable, QueryEventListener.TrieIndexEventListener, QueryContext)} does: + *
      + *
    • {@link TermQuery#lookupTermDictionary(ByteComparable)}: does term dictionary lookup to find the posting list file + * position
    • + *
    • {@link TermQuery#getPostingReader(long)}: reads posting list block summary and initializes posting read which + * reads the first block of the posting list into memory
    • + *
    + */ +public class TermsReader implements Closeable +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final IndexContext indexContext; + private final FileHandle termDictionaryFile; + private final FileHandle postingsFile; + private final long termDictionaryRoot; + private final Version version; + private final ByteComparable.Version termDictionaryFileEncodingVersion; + + public TermsReader(IndexContext indexContext, + FileHandle termsData, + ByteComparable.Version termsDataEncodingVersion, + FileHandle postingLists, + long root, + long termsFooterPointer, + Version version) throws IOException + { + this.indexContext = indexContext; + this.version = version; + termDictionaryFile = termsData; + postingsFile = postingLists; + termDictionaryRoot = root; + this.termDictionaryFileEncodingVersion = termsDataEncodingVersion; + + try (final IndexInput indexInput = IndexFileUtils.instance().openInput(termDictionaryFile)) + { + // if the pointer is -1 then this is a previous version of the index + // use the old way to validate the footer + // the footer pointer is used due to encrypted indexes padding extra bytes + if (termsFooterPointer == -1) + { + validate(indexInput); + } + else + { + validate(indexInput, termsFooterPointer); + } + } + + try (final IndexInput indexInput = IndexFileUtils.instance().openInput(postingsFile)) + { + validate(indexInput); + } + } + + @Override + public void close() + { + try + { + termDictionaryFile.close(); + } + finally + { + postingsFile.close(); + } + } + + public TermsIterator allTerms() + { + return allTerms(true); + } + + public TermsIterator allTerms(boolean ascending) + { + // blocking, since we use it only for segment merging for now + return ascending ? new TermsScanner(version, this.indexContext.getValidator()) + : new ReverseTermsScanner(); + } + + public PostingList exactMatch(ByteComparable term, QueryEventListener.TrieIndexEventListener perQueryEventListener, QueryContext context) + { + perQueryEventListener.onSegmentHit(); + return new TermQuery(term, perQueryEventListener, context).execute(); + } + + /** + * Range query that uses the lower and upper bounds to retrieve the search results within the range. When + * the expression is not null, it post-filters results using the expression. + */ + public PostingList rangeMatch(Expression exp, ByteComparable lower, ByteComparable upper, QueryEventListener.TrieIndexEventListener perQueryEventListener, QueryContext context) + { + perQueryEventListener.onSegmentHit(); + return new RangeQuery(exp, lower, upper, perQueryEventListener, context).execute(); + } + + @VisibleForTesting + public class TermQuery + { + private final IndexInput postingsInput; + private final IndexInput postingsSummaryInput; + private final QueryEventListener.TrieIndexEventListener listener; + private final long lookupStartTime; + private final QueryContext context; + + private ByteComparable term; + + TermQuery(ByteComparable term, QueryEventListener.TrieIndexEventListener listener, QueryContext context) + { + this.listener = listener; + postingsInput = IndexFileUtils.instance().openInput(postingsFile); + postingsSummaryInput = IndexFileUtils.instance().openInput(postingsFile); + this.term = term; + lookupStartTime = nanoTime(); + this.context = context; + } + + public PostingList execute() + { + try + { + long postingOffset = lookupTermDictionary(term); + if (postingOffset == PostingList.OFFSET_NOT_FOUND) + { + FileUtils.closeQuietly(postingsInput); + FileUtils.closeQuietly(postingsSummaryInput); + return null; + } + + context.checkpoint(); + + // when posting is found, resources will be closed when posting reader is closed. + return getPostingReader(postingOffset); + } + catch (Throwable e) + { + //TODO Is there an equivalent of AOE in OS? + if (!(e instanceof AbortedOperationException)) + logger.error(indexContext.logMessage("Failed to execute term query"), e); + + closeOnException(); + throw Throwables.cleaned(e); + } + } + + private void closeOnException() + { + FileUtils.closeQuietly(postingsInput); + FileUtils.closeQuietly(postingsSummaryInput); + } + + public long lookupTermDictionary(ByteComparable term) + { + try (TrieTermsDictionaryReader reader = new TrieTermsDictionaryReader(termDictionaryFile.instantiateRebufferer(null, ReadPattern.SEQUENTIAL), termDictionaryRoot, termDictionaryFileEncodingVersion)) + { + final long offset = reader.exactMatch(term); + + listener.onTraversalComplete(nanoTime() - lookupStartTime, TimeUnit.NANOSECONDS); + + if (offset == TrieTermsDictionaryReader.NOT_FOUND) + return PostingList.OFFSET_NOT_FOUND; + + return offset; + } + } + + public PostingsReader getPostingReader(long offset) throws IOException + { + PostingsReader.BlocksSummary header = new PostingsReader.BlocksSummary(postingsSummaryInput, offset); + + return new PostingsReader(postingsInput, header, readFrequencies(), listener.postingListEventListener()); + } + } + + public class RangeQuery + { + private final QueryEventListener.TrieIndexEventListener listener; + private final long lookupStartTime; + private final QueryContext context; + + private final Expression exp; + private final ByteComparable lower; + private final ByteComparable upper; + + // When the exp is not null, we need to post filter the results + RangeQuery(Expression exp, ByteComparable lower, ByteComparable upper, QueryEventListener.TrieIndexEventListener listener, QueryContext context) + { + this.listener = listener; + this.exp = exp; + lookupStartTime = Clock.Global.nanoTime(); + this.context = context; + this.lower = lower; + this.upper = upper; + } + + public PostingList execute() + { + // Note: we always pass true for include start because we use the ByteComparable terminator above + // to selectively determine when we have a match on the first/last term. This is probably part of the API + // that could change, but it's been there for a bit, so we'll leave it for now. + try (TrieTermsDictionaryReader reader = new TrieTermsDictionaryReader(termDictionaryFile.instantiateRebufferer(null, ReadPattern.SEQUENTIAL), + termDictionaryRoot, + lower, + upper, + true, + exp != null, + termDictionaryFileEncodingVersion)) + { + if (!reader.hasNext()) + return PostingList.EMPTY; + + context.checkpoint(); + PostingList postings = exp == null + ? readAndMergePostings(reader) + : readFilterAndMergePosting(reader); + + listener.onTraversalComplete(Clock.Global.nanoTime() - lookupStartTime, TimeUnit.NANOSECONDS); + + return postings; + } + catch (Throwable e) + { + if (!(e instanceof AbortedOperationException)) + logger.error(indexContext.logMessage("Failed to execute term query"), e); + + throw Throwables.cleaned(e); + } + } + + /** + * Reads the posting lists for the matching terms and merges them into a single posting list. + * It assumes that the posting list for each term is sorted. + * + * @return the posting lists for the terms matching the query. + */ + private PostingList readAndMergePostings(TrieTermsDictionaryReader reader) throws IOException + { + assert reader.hasNext(); + ArrayList postingLists = new ArrayList<>(); + + // index inputs will be closed with the onClose method of the returned merged posting list + IndexInput postingsInput = IndexFileUtils.instance().openInput(postingsFile); + IndexInput postingsSummaryInput = IndexFileUtils.instance().openInput(postingsFile); + + do + { + long postingsOffset = reader.nextAsLong(); + var currentReader = currentReader(postingsInput, postingsSummaryInput, postingsOffset); + + if (!currentReader.isEmpty()) + postingLists.add(currentReader); + else + FileUtils.close(currentReader); + } while (reader.hasNext()); + + return MergePostingList.merge(postingLists) + .onClose(() -> FileUtils.close(postingsInput, postingsSummaryInput)); + } + + /** + * Reads the posting lists for the matching terms, apply the expression to filter results, and merge them into + * a single posting list. It assumes that the posting list for each term is sorted. + * + * @return the posting lists for the terms matching the query. + */ + private PostingList readFilterAndMergePosting(TrieTermsDictionaryReader reader) throws IOException + { + assert reader.hasNext(); + ArrayList postingLists = new ArrayList<>(); + + // index inputs will be closed with the onClose method of the returned merged posting list + IndexInput postingsInput = IndexFileUtils.instance().openInput(postingsFile); + IndexInput postingsSummaryInput = IndexFileUtils.instance().openInput(postingsFile); + + do + { + Pair nextTriePair = reader.next(); + ByteSource mapEntry = nextTriePair.left.asComparableBytes(termDictionaryFileEncodingVersion); + long postingsOffset = nextTriePair.right; + byte[] nextBytes = ByteSourceInverse.readBytes(mapEntry); + + if (exp.isSatisfiedBy(ByteBuffer.wrap(nextBytes))) + { + var currentReader = currentReader(postingsInput, postingsSummaryInput, postingsOffset); + + if (!currentReader.isEmpty()) + postingLists.add(currentReader); + else + FileUtils.close(currentReader); + } + } while (reader.hasNext()); + + return MergePostingList.merge(postingLists) + .onClose(() -> FileUtils.close(postingsInput, postingsSummaryInput)); + } + + private PostingsReader currentReader(IndexInput postingsInput, + IndexInput postingsSummaryInput, + long postingsOffset) throws IOException + { + var blocksSummary = new PostingsReader.BlocksSummary(postingsSummaryInput, + postingsOffset, + PostingsReader.InputCloser.NOOP); + return new PostingsReader(postingsInput, + blocksSummary, + readFrequencies(), + listener.postingListEventListener(), + PostingsReader.InputCloser.NOOP); + } + } + + private boolean readFrequencies() + { + return indexContext.isAnalyzed() && version.onOrAfter(Version.BM25_EARLIEST); + } + + private class TermsScanner implements TermsIterator + { + private final TrieTermsDictionaryReader termsDictionaryReader; + private final ByteBuffer minTerm, maxTerm; + private Pair entry; + private final IndexInput postingsInput; + private final IndexInput postingsSummaryInput; + + private TermsScanner(Version version, AbstractType type) + { + this.termsDictionaryReader = new TrieTermsDictionaryReader(termDictionaryFile.instantiateRebufferer(null, ReadPattern.SEQUENTIAL), termDictionaryRoot, termDictionaryFileEncodingVersion); + this.postingsInput = IndexFileUtils.instance().openInput(postingsFile); + this.postingsSummaryInput = IndexFileUtils.instance().openInput(postingsFile); + // We decode based on the logic used to encode the min and max terms in the trie. + if (version.onOrAfter(Version.DB) && TypeUtil.isComposite(type)) + { + this.minTerm = indexContext.getValidator().fromComparableBytes(ByteSource.peekable(termsDictionaryReader.getMinTerm().asComparableBytes(termDictionaryFileEncodingVersion)), termDictionaryFileEncodingVersion); + this.maxTerm = indexContext.getValidator().fromComparableBytes(ByteSource.peekable(termsDictionaryReader.getMaxTerm().asComparableBytes(termDictionaryFileEncodingVersion)), termDictionaryFileEncodingVersion); + } + else + { + this.minTerm = ByteBuffer.wrap(ByteSourceInverse.readBytes(termsDictionaryReader.getMinTerm().asComparableBytes(termDictionaryFileEncodingVersion))); + this.maxTerm = ByteBuffer.wrap(ByteSourceInverse.readBytes(termsDictionaryReader.getMaxTerm().asComparableBytes(termDictionaryFileEncodingVersion))); + } + } + + @Override + @SuppressWarnings("resource") + public PostingList postings() throws IOException + { + assert entry != null; + var blockSummary = new PostingsReader.BlocksSummary(postingsSummaryInput, entry.right, PostingsReader.InputCloser.NOOP); + return new ScanningPostingsReader(postingsInput, blockSummary, readFrequencies()); + } + + @Override + public void close() + { + termsDictionaryReader.close(); + FileUtils.closeQuietly(postingsInput); + FileUtils.closeQuietly(postingsSummaryInput); + } + + @Override + public ByteBuffer getMinTerm() + { + return minTerm; + } + + @Override + public ByteBuffer getMaxTerm() + { + return maxTerm; + } + + @Override + public ByteComparable next() + { + if (termsDictionaryReader.hasNext()) + { + entry = termsDictionaryReader.next(); + return entry.left; + } + return null; + } + + @Override + public boolean hasNext() + { + return termsDictionaryReader.hasNext(); + } + } + + private class ReverseTermsScanner implements TermsIterator + { + private final ReverseTrieTermsDictionaryReader iterator; + private Pair entry; + private final IndexInput postingsInput; + private final IndexInput postingsSummaryInput; + + private ReverseTermsScanner() + { + this.iterator = new ReverseTrieTermsDictionaryReader(termDictionaryFile.instantiateRebufferer(null, ReadPattern.SEQUENTIAL), termDictionaryRoot); + this.postingsInput = IndexFileUtils.instance().openInput(postingsFile); + this.postingsSummaryInput = IndexFileUtils.instance().openInput(postingsFile); + } + + @Override + @SuppressWarnings("resource") + public PostingList postings() throws IOException + { + assert entry != null; + var blockSummary = new PostingsReader.BlocksSummary(postingsSummaryInput, entry.right, PostingsReader.InputCloser.NOOP); + return new ScanningPostingsReader(postingsInput, blockSummary, readFrequencies()); + } + + @Override + public void close() + { + iterator.close(); + FileUtils.closeQuietly(postingsInput); + FileUtils.closeQuietly(postingsSummaryInput); + } + + @Override + public ByteBuffer getMinTerm() + { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuffer getMaxTerm() + { + throw new UnsupportedOperationException(); + } + + @Override + public ByteComparable next() + { + if (iterator.hasNext()) + { + entry = iterator.next(); + return entry.left; + } + return null; + } + + @Override + public boolean hasNext() + { + return iterator.hasNext(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java index 8d8266ac349e..e23d7041396f 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java @@ -20,280 +20,308 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.EnumSet; import java.util.Set; -import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Gauge; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.SSTableContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter; -import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter; +import org.apache.cassandra.index.sai.disk.EmptyIndex; +import org.apache.cassandra.index.sai.disk.PerIndexWriter; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.RowMapping; -import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.index.sai.disk.SearchableIndex; import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; 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.format.Version; +import org.apache.cassandra.index.sai.memory.RowMapping; import org.apache.cassandra.index.sai.metrics.AbstractMetrics; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.utils.IndexTermType; import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.metrics.CassandraMetricsRegistry; import org.apache.cassandra.metrics.DefaultNameFactory; import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; import org.apache.lucene.store.IndexInput; import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory; +/** + * The original SAI OnDiskFormat, found in DSE. Because it has a simple token -> offsets map, queries + * against "wide partitions" are slow in proportion to the partition size, since we have to read + * the whole partition and post-filter the rows + */ public class V1OnDiskFormat implements OnDiskFormat { - private static final Logger logger = LoggerFactory.getLogger(V1OnDiskFormat.class); - - @VisibleForTesting - public static final Set SKINNY_PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponent.GROUP_COMPLETION_MARKER, - IndexComponent.GROUP_META, - IndexComponent.ROW_TO_TOKEN, - IndexComponent.ROW_TO_PARTITION, - IndexComponent.PARTITION_KEY_BLOCKS, - IndexComponent.PARTITION_KEY_BLOCK_OFFSETS); - - @VisibleForTesting - public static final Set WIDE_PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponent.GROUP_COMPLETION_MARKER, - IndexComponent.GROUP_META, - IndexComponent.ROW_TO_TOKEN, - IndexComponent.ROW_TO_PARTITION, - IndexComponent.PARTITION_TO_SIZE, - IndexComponent.PARTITION_KEY_BLOCKS, - IndexComponent.PARTITION_KEY_BLOCK_OFFSETS, - IndexComponent.CLUSTERING_KEY_BLOCKS, - IndexComponent.CLUSTERING_KEY_BLOCK_OFFSETS); - - @VisibleForTesting - public static final Set LITERAL_COMPONENTS = EnumSet.of(IndexComponent.COLUMN_COMPLETION_MARKER, - IndexComponent.META, - IndexComponent.TERMS_DATA, - IndexComponent.POSTING_LISTS); - @VisibleForTesting - public static final Set NUMERIC_COMPONENTS = EnumSet.of(IndexComponent.COLUMN_COMPLETION_MARKER, - IndexComponent.META, - IndexComponent.BALANCED_TREE, - IndexComponent.POSTING_LISTS); - - @VisibleForTesting - public static final Set VECTOR_COMPONENTS = EnumSet.of(IndexComponent.COLUMN_COMPLETION_MARKER, - IndexComponent.META, - IndexComponent.COMPRESSED_VECTORS, - IndexComponent.TERMS_DATA, - IndexComponent.POSTING_LISTS); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final Set PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponentType.GROUP_COMPLETION_MARKER, + IndexComponentType.GROUP_META, + IndexComponentType.TOKEN_VALUES, + IndexComponentType.OFFSETS_VALUES); + + private static final Set LITERAL_COMPONENTS = EnumSet.of(IndexComponentType.COLUMN_COMPLETION_MARKER, + IndexComponentType.META, + IndexComponentType.TERMS_DATA, + IndexComponentType.POSTING_LISTS); + + public static final Set NUMERIC_COMPONENTS = EnumSet.of(IndexComponentType.COLUMN_COMPLETION_MARKER, + IndexComponentType.META, + IndexComponentType.KD_TREE, + IndexComponentType.KD_TREE_POSTING_LISTS); /** * Global limit on heap consumed by all index segment building that occurs outside the context of Memtable flush. - *

    - * Note that to avoid flushing small index segments, a segment is only flushed when + * + * Note that to avoid flushing extremly small index segments, a segment is only flushed when * both the global size of all building segments has breached the limit and the size of the * segment in question reaches (segment_write_buffer_space_mb / # currently building column indexes). - *

    + * * ex. If there is only one column index building, it can buffer up to segment_write_buffer_space_mb. - *

    + * * ex. If there is one column index building per table across 8 compactors, each index will be * eligible to flush once it reaches (segment_write_buffer_space_mb / 8) MBs. */ - public static final long SEGMENT_BUILD_MEMORY_LIMIT = DatabaseDescriptor.getSAISegmentWriteBufferSpace().toBytes(); + public static final long SEGMENT_BUILD_MEMORY_LIMIT = 1024L * 1024L * DatabaseDescriptor.getSAISegmentWriteBufferSpace(); - public static final NamedMemoryLimiter SEGMENT_BUILD_MEMORY_LIMITER = new NamedMemoryLimiter(SEGMENT_BUILD_MEMORY_LIMIT, - "Storage Attached Index Segment Builder"); + public static final NamedMemoryLimiter SEGMENT_BUILD_MEMORY_LIMITER = + new NamedMemoryLimiter(SEGMENT_BUILD_MEMORY_LIMIT, "SSTable-attached Index Segment Builder"); static { + logger.debug("Segment build memory limit set to {} bytes", prettyPrintMemory(SEGMENT_BUILD_MEMORY_LIMIT)); + CassandraMetricsRegistry.MetricName bufferSpaceUsed = DefaultNameFactory.createMetricName(AbstractMetrics.TYPE, "SegmentBufferSpaceUsedBytes", null); CassandraMetricsRegistry.Metrics.register(bufferSpaceUsed, (Gauge) SEGMENT_BUILD_MEMORY_LIMITER::currentBytesUsed); CassandraMetricsRegistry.MetricName bufferSpaceLimit = DefaultNameFactory.createMetricName(AbstractMetrics.TYPE, "SegmentBufferSpaceLimitBytes", null); CassandraMetricsRegistry.Metrics.register(bufferSpaceLimit, (Gauge) () -> SEGMENT_BUILD_MEMORY_LIMIT); + // Note: The active builder count starts at 1 to avoid dividing by zero. CassandraMetricsRegistry.MetricName buildsInProgress = DefaultNameFactory.createMetricName(AbstractMetrics.TYPE, "ColumnIndexBuildsInProgress", null); - CassandraMetricsRegistry.Metrics.register(buildsInProgress, (Gauge) SegmentBuilder::getActiveBuilderCount); + CassandraMetricsRegistry.Metrics.register(buildsInProgress, (Gauge) () -> SegmentBuilder.ACTIVE_BUILDER_COUNT.get() - 1); } public static final V1OnDiskFormat instance = new V1OnDiskFormat(); + private static final IndexFeatureSet v1IndexFeatureSet = new IndexFeatureSet() + { + @Override + public boolean isRowAware() + { + return false; + } + + @Override + public boolean hasTermsHistogram() + { + return false; + } + }; + protected V1OnDiskFormat() {} @Override - public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexDescriptor indexDescriptor, SSTableReader sstable) + public IndexFeatureSet indexFeatureSet() { - return indexDescriptor.hasClustering() ? new WidePrimaryKeyMap.Factory(indexDescriptor, sstable) - : new SkinnyPrimaryKeyMap.Factory(indexDescriptor); + return v1IndexFeatureSet; } @Override - public SSTableIndex newSSTableIndex(SSTableContext sstableContext, StorageAttachedIndex index) + public PrimaryKey.Factory newPrimaryKeyFactory(ClusteringComparator comparator) { - return new V1SSTableIndex(sstableContext, index); + return new PartitionAwarePrimaryKeyFactory(); } @Override - public PerSSTableIndexWriter newPerSSTableIndexWriter(IndexDescriptor indexDescriptor) throws IOException + public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, PrimaryKey.Factory primaryKeyFactory, SSTableReader sstable) throws IOException { - return new SSTableComponentsWriter(indexDescriptor); + return new PartitionAwarePrimaryKeyMap.PartitionAwarePrimaryKeyMapFactory(perSSTableComponents, sstable, primaryKeyFactory); } @Override - public PerColumnIndexWriter newPerColumnIndexWriter(StorageAttachedIndex index, - IndexDescriptor indexDescriptor, - LifecycleNewTracker tracker, - RowMapping rowMapping) + public SearchableIndex newSearchableIndex(SSTableContext sstableContext, IndexComponents.ForRead perIndexComponents) { - // If we're not flushing, or we haven't yet started the initialization build, flush from SSTable contents. - if (tracker.opType() != OperationType.FLUSH || !index.isInitBuildStarted()) - { - NamedMemoryLimiter limiter = SEGMENT_BUILD_MEMORY_LIMITER; - logger.info(index.identifier().logMessage("Starting a compaction index build. Global segment memory usage: {}"), - prettyPrintMemory(limiter.currentBytesUsed())); - - return new SSTableIndexWriter(indexDescriptor, index, limiter, index.isIndexValid()); - } - - return new MemtableIndexWriter(index.memtableIndexManager().getPendingMemtableIndex(tracker), - indexDescriptor, - index.termType(), - index.identifier(), - index.indexMetrics(), - rowMapping); + return perIndexComponents.isEmpty() + ? new EmptyIndex() + : new V1SearchableIndex(sstableContext, perIndexComponents); } @Override - public boolean isPerSSTableIndexBuildComplete(IndexDescriptor indexDescriptor) + public IndexSearcher newIndexSearcher(SSTableContext sstableContext, + IndexContext indexContext, + PerIndexFiles indexFiles, + SegmentMetadata segmentMetadata) throws IOException { - return indexDescriptor.hasComponent(IndexComponent.GROUP_COMPLETION_MARKER); + if (indexContext.isLiteral()) + // We filter because the CA format wrote maps acording to a different order than their abstract type. + return new InvertedIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext, Version.AA, true); + return new KDTreeIndexSearcher(sstableContext.primaryKeyMapFactory(), indexFiles, segmentMetadata, indexContext); } @Override - public boolean isPerColumnIndexBuildComplete(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier) + public PerSSTableWriter newPerSSTableWriter(IndexDescriptor indexDescriptor) throws IOException { - return indexDescriptor.hasComponent(IndexComponent.GROUP_COMPLETION_MARKER) && - indexDescriptor.hasComponent(IndexComponent.COLUMN_COMPLETION_MARKER, indexIdentifier); + return new SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); } @Override - public void validatePerSSTableIndexComponents(IndexDescriptor indexDescriptor, boolean checksum) + public PerIndexWriter newPerIndexWriter(StorageAttachedIndex index, + IndexDescriptor indexDescriptor, + LifecycleNewTracker tracker, + RowMapping rowMapping, + long keyCount) { - for (IndexComponent indexComponent : perSSTableIndexComponents(indexDescriptor.hasClustering())) + IndexContext context = index.getIndexContext(); + IndexComponents.ForWrite perIndexComponents = indexDescriptor.newPerIndexComponentsForWrite(context); + // If we're not flushing or we haven't yet started the initialization build, flush from SSTable contents. + if (tracker.opType() != OperationType.FLUSH || !index.canFlushFromMemtableIndex()) { - if (isNotBuildCompletionMarker(indexComponent)) - { - validateIndexComponent(indexDescriptor, null, indexComponent, checksum); - } + NamedMemoryLimiter limiter = SEGMENT_BUILD_MEMORY_LIMITER; + logger.debug(index.getIndexContext().logMessage("Starting a compaction index build. Global segment memory usage: {}"), + prettyPrintMemory(limiter.currentBytesUsed())); + + return new SSTableIndexWriter(perIndexComponents, limiter, index.isDropped(), index.isUnloaded(), keyCount); } + + return new MemtableIndexWriter(context.getPendingMemtableIndex(tracker), + perIndexComponents, + context.keyFactory(), + rowMapping); } - @Override - public void validatePerColumnIndexComponents(IndexDescriptor indexDescriptor, IndexTermType indexTermType, IndexIdentifier indexIdentifier, boolean checksum) + protected Version getExpectedEarliestVersion(IndexContext context, IndexComponentType indexComponentType) { - // determine if the index is empty, which would be encoded in the column completion marker - boolean isEmptyIndex = false; - if (indexDescriptor.hasComponent(IndexComponent.COLUMN_COMPLETION_MARKER, indexIdentifier)) + Version earliest = Version.EARLIEST; + if (isVectorDataComponent(context, indexComponentType)) { - // first validate the file... - validateIndexComponent(indexDescriptor, indexIdentifier, IndexComponent.COLUMN_COMPLETION_MARKER, checksum); - - // ...then read to check if the index is empty - try - { - isEmptyIndex = ColumnCompletionMarkerUtil.isEmptyIndex(indexDescriptor, indexIdentifier); - } - catch (IOException e) - { - rethrowIOException(e); - } - } - - for (IndexComponent indexComponent : perColumnIndexComponents(indexTermType)) - { - if (!isEmptyIndex && isNotBuildCompletionMarker(indexComponent)) - { - validateIndexComponent(indexDescriptor, indexIdentifier, indexComponent, checksum); - } + if (!context.version().onOrAfter(Version.VECTOR_EARLIEST)) + throw new IllegalStateException("Configured current version " + context.version() + " is not compatible with vector index"); + earliest = Version.VECTOR_EARLIEST; } + return earliest; } - private static void validateIndexComponent(IndexDescriptor indexDescriptor, - IndexIdentifier indexContext, - IndexComponent indexComponent, - boolean checksum) + @Override + public void validateIndexComponent(IndexComponent.ForRead component, boolean checksum) { - try (IndexInput input = indexContext == null - ? indexDescriptor.openPerSSTableInput(indexComponent) - : indexDescriptor.openPerIndexInput(indexComponent, indexContext)) + if (component.isCompletionMarker()) + return; + + // We do not validate vector components until V7, so we skip for earlier versions + IndexContext context = component.parent().context(); + if (isVectorDataComponent(context, component.componentType())) + return; + + Version earliest = getExpectedEarliestVersion(context, component.componentType()); + try (IndexInput input = component.openInput()) { if (checksum) - SAICodecUtils.validateChecksum(input); + SAICodecUtils.validateChecksum(input, earliest); else - SAICodecUtils.validate(input); + SAICodecUtils.validate(input, earliest); } catch (Exception e) { - logger.warn(indexDescriptor.logMessage("{} failed for index component {} on SSTable {}"), - checksum ? "Checksum validation" : "Validation", - indexComponent, - indexDescriptor.sstableDescriptor); - rethrowIOException(e); + logger.warn(component.parent().logMessage("{} failed for index component {} on SSTable {}"), + (checksum ? "Checksum validation" : "Validation"), + component, + component.parent().descriptor(), + e); + + if (e instanceof IOException) + throw new UncheckedIOException((IOException) e); + if (e.getCause() instanceof IOException) + throw new UncheckedIOException((IOException) e.getCause()); + throw Throwables.unchecked(e); } } - private static void rethrowIOException(Exception e) + @Override + public Set perSSTableComponentTypes() { - if (e instanceof IOException) - throw new UncheckedIOException((IOException) e); - if (e.getCause() instanceof IOException) - throw new UncheckedIOException((IOException) e.getCause()); - throw Throwables.unchecked(e); + return PER_SSTABLE_COMPONENTS; } @Override - public Set perSSTableIndexComponents(boolean hasClustering) + public Set perIndexComponentTypes(AbstractType validator) { - return hasClustering ? WIDE_PER_SSTABLE_COMPONENTS : SKINNY_PER_SSTABLE_COMPONENTS; + if (TypeUtil.isLiteral(validator)) + return LITERAL_COMPONENTS; + return NUMERIC_COMPONENTS; } @Override - public Set perColumnIndexComponents(IndexTermType indexTermType) + public int openFilesPerSSTable() { - return indexTermType.isVector() ? VECTOR_COMPONENTS : indexTermType.isLiteral() ? LITERAL_COMPONENTS : NUMERIC_COMPONENTS; + return 2; } @Override - public int openFilesPerSSTableIndex(boolean hasClustering) + public int openFilesPerIndex(IndexContext indexContext) { - // For the V1 format the number of open files depends on whether the table has clustering. For wide tables - // the number of open files will be 6 per SSTable - token values, partition sizes index, partition key blocks, - // partition key block offsets, clustering key blocks & clustering key block offsets and for skinny tables - // the number of files will be 4 per SSTable - token values, partition key sizes, partition key blocks & - // partition key block offsets. - return hasClustering ? 6 : 4; + // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings + return 2; } @Override - public int openFilesPerColumnIndex() + public ByteOrder byteOrderFor(IndexComponentType indexComponentType, IndexContext context) { - // For the V1 format there are always 2 open files per index - index (balanced tree or terms) + auxiliary postings - // for the balanced tree and postings for the literal terms - return 2; + return ByteOrder.BIG_ENDIAN; } - protected boolean isNotBuildCompletionMarker(IndexComponent indexComponent) + @Override + public ByteComparable encodeForTrie(ByteBuffer input, AbstractType type) + { + return TypeUtil.isLiteral(type) ? v -> ByteSource.preencoded(input) + : TypeUtil.asComparableBytes(input, type); + } + + @Override + public ByteBuffer decodeFromTrie(ByteComparable value, AbstractType type) + { + return TypeUtil.isLiteral(type) + ? ByteBuffer.wrap(ByteSourceInverse.readBytes(value.asComparableBytes(ByteComparable.Version.OSS41))) + : TypeUtil.fromComparableBytes(value, type, ByteComparable.Version.OSS41); + } + + /** vector data components (that did not have checksums before v3) */ + private boolean isVectorDataComponent(IndexContext context, IndexComponentType indexComponentType) + { + if (context == null || !context.isVector()) + return false; + + return indexComponentType == IndexComponentType.VECTOR || + indexComponentType == IndexComponentType.PQ || + indexComponentType == IndexComponentType.TERMS_DATA || + indexComponentType == IndexComponentType.POSTING_LISTS; + } + + @Override + public int jvectorFileFormatVersion() { - return indexComponent != IndexComponent.GROUP_COMPLETION_MARKER && - indexComponent != IndexComponent.COLUMN_COMPLETION_MARKER; + throw new UnsupportedOperationException("JVector is not supported in V2OnDiskFormat"); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableIndex.java deleted file mode 100644 index 254d695e4034..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableIndex.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import com.google.common.collect.ImmutableList; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.db.virtual.SimpleDataSet; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.SSTableContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.SSTableIndex; -import org.apache.cassandra.index.sai.disk.v1.segment.Segment; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.CloseableIterator; -import org.apache.cassandra.utils.Throwables; - -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.CELL_COUNT; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.COLUMN_NAME; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.COMPONENT_METADATA; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.END_TOKEN; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MAX_SSTABLE_ROW_ID; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MAX_TERM; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MIN_SSTABLE_ROW_ID; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MIN_TERM; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.START_TOKEN; -import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.TABLE_NAME; - -/** - * A version specific implementation of the {@link SSTableIndex} where the - * index is segmented - */ -public class V1SSTableIndex extends SSTableIndex -{ - private final ImmutableList segments; - private final List metadatas; - private final AbstractBounds bounds; - private final ByteBuffer minTerm; - private final ByteBuffer maxTerm; - private final long minSSTableRowId, maxSSTableRowId; - private final long numRows; - - private PerColumnIndexFiles indexFiles; - - public V1SSTableIndex(SSTableContext sstableContext, StorageAttachedIndex index) - { - super(sstableContext, index); - - try - { - this.indexFiles = new PerColumnIndexFiles(sstableContext.indexDescriptor, indexTermType, indexIdentifier); - - ImmutableList.Builder segmentsBuilder = ImmutableList.builder(); - - final MetadataSource source = MetadataSource.loadColumnMetadata(sstableContext.indexDescriptor, indexIdentifier); - - metadatas = SegmentMetadata.load(source, sstableContext.indexDescriptor.primaryKeyFactory); - - for (SegmentMetadata metadata : metadatas) - { - segmentsBuilder.add(new Segment(index, sstableContext, indexFiles, metadata)); - } - - segments = segmentsBuilder.build(); - assert !segments.isEmpty(); - - DecoratedKey minKey = metadatas.get(0).minKey.partitionKey(); - DecoratedKey maxKey = metadatas.get(metadatas.size() - 1).maxKey.partitionKey(); - - this.bounds = AbstractBounds.bounds(minKey, true, maxKey, true); - - this.minTerm = metadatas.stream().map(m -> m.minTerm).min(indexTermType.comparator()).orElse(null); - this.maxTerm = metadatas.stream().map(m -> m.maxTerm).max(indexTermType.comparator()).orElse(null); - - this.numRows = metadatas.stream().mapToLong(m -> m.numRows).sum(); - - this.minSSTableRowId = metadatas.get(0).minSSTableRowId; - this.maxSSTableRowId = metadatas.get(metadatas.size() - 1).maxSSTableRowId; - } - catch (Throwable t) - { - FileUtils.closeQuietly(indexFiles); - FileUtils.closeQuietly(sstableContext); - throw Throwables.unchecked(t); - } - } - - @Override - public long indexFileCacheSize() - { - return segments.stream().mapToLong(Segment::indexFileCacheSize).sum(); - } - - @Override - public long getRowCount() - { - return numRows; - } - - @Override - public long minSSTableRowId() - { - return minSSTableRowId; - } - - @Override - public long maxSSTableRowId() - { - return maxSSTableRowId; - } - - @Override - public ByteBuffer minTerm() - { - return minTerm; - } - - @Override - public ByteBuffer maxTerm() - { - return maxTerm; - } - - @Override - public AbstractBounds bounds() - { - return bounds; - } - - @Override - public List search(Expression expression, - AbstractBounds keyRange, - QueryContext context) throws IOException - { - List segmentIterators = new ArrayList<>(); - - for (Segment segment : segments) - { - if (segment.intersects(keyRange)) - { - segmentIterators.add(segment.search(expression, keyRange, context)); - } - } - - return segmentIterators; - } - - public List> orderBy(Expression orderer, AbstractBounds keyRange, QueryContext context) throws IOException - { - // Return a list to allow the caller to merge the results from multiple sstables into a single iterator. - List> iterators = new ArrayList<>(segments.size()); - for (Segment segment : segments) - if (segment.intersects(keyRange)) - iterators.add(segment.orderBy(orderer, keyRange, context)); - - return iterators; - } - - public List> orderResultsBy(QueryContext context, List results, Expression orderer) throws IOException - { - // Return a list to allow the caller to merge the results from multiple sstables into a single iterator. - List> iterators = new ArrayList<>(segments.size()); - for (Segment segment : segments) - iterators.add(segment.orderResultsBy(context, results, orderer)); - - return iterators; - } - - @Override - public void populateSegmentView(SimpleDataSet dataset) - { - SSTableReader sstable = getSSTable(); - Token.TokenFactory tokenFactory = sstable.metadata().partitioner.getTokenFactory(); - - for (SegmentMetadata metadata : metadatas) - { - dataset.row(sstable.metadata().keyspace, indexIdentifier.indexName, sstable.getFilename(), metadata.rowIdOffset) - .column(TABLE_NAME, sstable.descriptor.cfname) - .column(COLUMN_NAME, indexTermType.columnName()) - .column(CELL_COUNT, metadata.numRows) - .column(MIN_SSTABLE_ROW_ID, metadata.minSSTableRowId) - .column(MAX_SSTABLE_ROW_ID, metadata.maxSSTableRowId) - .column(START_TOKEN, tokenFactory.toString(metadata.minKey.token())) - .column(END_TOKEN, tokenFactory.toString(metadata.maxKey.token())) - .column(MIN_TERM, indexTermType.indexType().getSerializer().deserialize(metadata.minTerm).toString()) - .column(MAX_TERM, indexTermType.indexType().getSerializer().deserialize(metadata.maxTerm).toString()) - .column(COMPONENT_METADATA, metadata.componentMetadatas.asMap()); - } - } - - @Override - protected void internalRelease() - { - FileUtils.closeQuietly(indexFiles); - FileUtils.closeQuietly(segments); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/V1SearchableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SearchableIndex.java new file mode 100644 index 000000000000..f8053e85545d --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SearchableIndex.java @@ -0,0 +1,310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.virtual.SimpleDataSet; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.SearchableIndex; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.iterators.KeyRangeConcatIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyListUtil; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.Throwables; + +import static org.apache.cassandra.index.sai.disk.v1.SegmentMetadata.INVALID_TOTAL_TERM_COUNT; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.CELL_COUNT; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.COLUMN_NAME; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.COMPONENT_METADATA; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.END_TOKEN; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MAX_SSTABLE_ROW_ID; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MAX_TERM; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MIN_SSTABLE_ROW_ID; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MIN_TERM; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.START_TOKEN; +import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.TABLE_NAME; + +/** + * A version specific implementation of the {@link SearchableIndex} where the + * index is segmented + */ +public class V1SearchableIndex implements SearchableIndex +{ + private final IndexContext indexContext; + private final ImmutableList segments; + private final List metadatas; + private final DecoratedKey minKey; + private final DecoratedKey maxKey; // in token order + private final ByteBuffer minTerm; + private final ByteBuffer maxTerm; + private final long minSSTableRowId, maxSSTableRowId; + private final long numRows; + private final long approximateTermCount; + private PerIndexFiles indexFiles; + + public V1SearchableIndex(SSTableContext sstableContext, IndexComponents.ForRead perIndexComponents) + { + this.indexContext = perIndexComponents.context(); + try + { + this.indexFiles = new PerIndexFiles(perIndexComponents); + + ImmutableList.Builder segmentsBuilder = ImmutableList.builder(); + + final MetadataSource source = MetadataSource.loadMetadata(perIndexComponents); + + metadatas = SegmentMetadata.load(source, indexContext, sstableContext); + + long termCount = 0; + for (SegmentMetadata metadata : metadatas) + { + segmentsBuilder.add(new Segment(indexContext, sstableContext, indexFiles, metadata)); + termCount += metadata.totalTermCount == INVALID_TOTAL_TERM_COUNT ? 0 : metadata.totalTermCount; + } + this.approximateTermCount = termCount; + + segments = segmentsBuilder.build(); + assert !segments.isEmpty(); + + this.minKey = metadatas.get(0).minKey.partitionKey(); + this.maxKey = metadatas.get(metadatas.size() - 1).maxKey.partitionKey(); + + var version = perIndexComponents.version(); + this.minTerm = metadatas.stream().map(m -> m.minTerm).min(TypeUtil.comparator(indexContext.getValidator(), version)).orElse(null); + this.maxTerm = metadatas.stream().map(m -> m.maxTerm).max(TypeUtil.comparator(indexContext.getValidator(), version)).orElse(null); + + this.numRows = metadatas.stream().mapToLong(m -> m.numRows).sum(); + + this.minSSTableRowId = metadatas.get(0).minSSTableRowId; + this.maxSSTableRowId = metadatas.get(metadatas.size() - 1).maxSSTableRowId; + } + catch (Throwable t) + { + FileUtils.closeQuietly(indexFiles); + FileUtils.closeQuietly(sstableContext); + throw Throwables.unchecked(t); + } + } + + @Override + public long indexFileCacheSize() + { + return segments.stream().mapToLong(Segment::indexFileCacheSize).sum(); + } + + @Override + public long getRowCount() + { + return numRows; + } + + @Override + public long getApproximateTermCount() + { + return approximateTermCount; + } + + @Override + public long minSSTableRowId() + { + return minSSTableRowId; + } + + @Override + public long maxSSTableRowId() + { + return maxSSTableRowId; + } + + @Override + public ByteBuffer minTerm() + { + return minTerm; + } + + @Override + public ByteBuffer maxTerm() + { + return maxTerm; + } + + @Override + public DecoratedKey minKey() + { + return minKey; + } + + @Override + public DecoratedKey maxKey() + { + return maxKey; + } + + @Override + public KeyRangeIterator search(Expression expression, + AbstractBounds keyRange, + QueryContext context, + boolean defer) throws IOException + { + KeyRangeConcatIterator.Builder rangeConcatIteratorBuilder = KeyRangeConcatIterator.builder(segments.size()); + + try + { + for (Segment segment : segments) + { + if (segment.intersects(keyRange)) + { + rangeConcatIteratorBuilder.add(segment.search(expression, keyRange, context, defer)); + } + } + + return rangeConcatIteratorBuilder.build(); + } + catch (Throwable t) + { + FileUtils.closeQuietly(rangeConcatIteratorBuilder.ranges()); + throw t; + } + } + + @Override + public List> orderBy(Orderer orderer, Expression slice, + AbstractBounds keyRange, + QueryContext context, + int limit, + long totalRows) throws IOException + { + var iterators = new ArrayList>(segments.size()); + try + { + for (Segment segment : segments) + { + if (segment.intersects(keyRange)) + { + context.addSegmentsHit(1); + // Note that the proportionality is not used when the user supplies a rerank_k value in the + // ANN_OPTIONS map. + var segmentLimit = segment.proportionalAnnLimit(limit, totalRows); + iterators.add(segment.orderBy(orderer, slice, keyRange, context, segmentLimit)); + } + } + + return iterators; + } + catch (Throwable t) + { + FileUtils.closeQuietly(iterators); + throw t; + } + } + + @Override + public List> orderResultsBy(QueryContext context, List keys, Orderer orderer, int limit, long totalRows) throws IOException + { + var results = new ArrayList>(segments.size()); + try + { + for (Segment segment : segments) + { + context.addSegmentsHit(1); + // Only pass the primary keys in a segment's range to the segment index. + var segmentKeys = PrimaryKeyListUtil.getKeysInRange(keys, segment.metadata.minKey, segment.metadata.maxKey); + var segmentLimit = segment.proportionalAnnLimit(limit, totalRows); + results.add(segment.orderResultsBy(context, segmentKeys, orderer, segmentLimit)); + } + + return results; + } + catch (Throwable t) + { + FileUtils.closeQuietly(results); + throw t; + } + } + + @Override + public List getSegments() + { + return segments; + } + + @Override + public void populateSystemView(SimpleDataSet dataset, SSTableReader sstable) + { + Token.TokenFactory tokenFactory = sstable.metadata().partitioner.getTokenFactory(); + + for (SegmentMetadata metadata : metadatas) + { + String minTerm = indexContext.isVector() ? "N/A" : indexContext.getValidator().getSerializer().deserialize(metadata.minTerm).toString(); + String maxTerm = indexContext.isVector() ? "N/A" : indexContext.getValidator().getSerializer().deserialize(metadata.maxTerm).toString(); + + dataset.row(sstable.metadata().keyspace, indexContext.getIndexName(), sstable.getFilename(), metadata.segmentRowIdOffset) + .column(TABLE_NAME, sstable.descriptor.cfname) + .column(COLUMN_NAME, indexContext.getColumnName()) + .column(CELL_COUNT, metadata.numRows) + .column(MIN_SSTABLE_ROW_ID, metadata.minSSTableRowId) + .column(MAX_SSTABLE_ROW_ID, metadata.maxSSTableRowId) + .column(START_TOKEN, tokenFactory.toString(metadata.minKey.partitionKey().getToken())) + .column(END_TOKEN, tokenFactory.toString(metadata.maxKey.partitionKey().getToken())) + .column(MIN_TERM, minTerm) + .column(MAX_TERM, maxTerm) + .column(COMPONENT_METADATA, metadata.componentMetadatas.asMap()); + } + } + + @Override + public long estimateMatchingRowsCount(Expression predicate) + { + long rowCount = 0; + for (Segment segment: segments) + { + long c = segment.estimateMatchingRowsCount(predicate); + assert c >= 0 : "Estimated row count must not be negative: " + c + " (predicate: " + predicate + ')'; + rowCount += c; + } + return rowCount; + } + + @Override + public void close() throws IOException + { + FileUtils.closeQuietly(indexFiles); + FileUtils.closeQuietly(segments); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/WidePrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/WidePrimaryKeyMap.java deleted file mode 100644 index 28033e1547a9..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/WidePrimaryKeyMap.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1; - -import java.io.IOException; -import java.util.Arrays; -import javax.annotation.concurrent.NotThreadSafe; -import javax.annotation.concurrent.ThreadSafe; - -import org.apache.cassandra.db.Clustering; -import org.apache.cassandra.db.ClusteringComparator; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -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.v1.bitpack.BlockPackedReader; -import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; -import org.apache.cassandra.index.sai.disk.v1.keystore.KeyLookupMeta; -import org.apache.cassandra.index.sai.disk.v1.keystore.KeyLookup; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.Throwables; - -/** - * An extension of the {@link SkinnyPrimaryKeyMap} for wide tables (those with clustering columns). - *

    - * This used the following additional on-disk structures to the {@link SkinnyPrimaryKeyMap} - *

      - *
    • A block-packed structure for partitionId to partition size (number of rows in the partition) lookups using - * {@link BlockPackedReader}. Uses the {@link IndexComponent#PARTITION_TO_SIZE} component
    • - *
    • A key store for rowId to {@link Clustering} and {@link Clustering} to rowId lookups using - * {@link KeyLookup}. Uses the {@link IndexComponent#CLUSTERING_KEY_BLOCKS} and - * {@link IndexComponent#CLUSTERING_KEY_BLOCK_OFFSETS} components
    • - *
    - * While the {@link Factory} is threadsafe, individual instances of the {@link WidePrimaryKeyMap} - * are not. - */ -@NotThreadSafe -public class WidePrimaryKeyMap extends SkinnyPrimaryKeyMap -{ - @ThreadSafe - public static class Factory extends SkinnyPrimaryKeyMap.Factory - { - private final ClusteringComparator clusteringComparator; - private final KeyLookup clusteringKeyReader; - private final LongArray.Factory partitionToSizeReaderFactory; - private final FileHandle clusteringKeyBlockOffsetsFile; - private final FileHandle clustingingKeyBlocksFile; - private final FileHandle partitionToSizeFile; - - public Factory(IndexDescriptor indexDescriptor, SSTableReader sstable) - { - super(indexDescriptor); - - this.clusteringKeyBlockOffsetsFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.CLUSTERING_KEY_BLOCK_OFFSETS, this::close); - this.clustingingKeyBlocksFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.CLUSTERING_KEY_BLOCKS, this::close); - this.partitionToSizeFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.PARTITION_TO_SIZE, this::close); - - try - { - this.clusteringComparator = indexDescriptor.clusteringComparator; - NumericValuesMeta partitionSizeMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PARTITION_TO_SIZE))); - this.partitionToSizeReaderFactory = new BlockPackedReader(partitionToSizeFile, partitionSizeMeta); - NumericValuesMeta clusteringKeyBlockOffsetsMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.CLUSTERING_KEY_BLOCK_OFFSETS))); - KeyLookupMeta clusteringKeyMeta = new KeyLookupMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.CLUSTERING_KEY_BLOCKS))); - this.clusteringKeyReader = new KeyLookup(clustingingKeyBlocksFile, clusteringKeyBlockOffsetsFile, clusteringKeyMeta, clusteringKeyBlockOffsetsMeta); - } - catch (Throwable t) - { - throw Throwables.unchecked(t); - } - } - - @Override - @SuppressWarnings({ "resource", "RedundantSuppression" }) // deferred long arrays and cursors are closed in the WidePrimaryKeyMap#close method - public PrimaryKeyMap newPerSSTablePrimaryKeyMap() throws IOException - { - LongArray rowIdToToken = new LongArray.DeferredLongArray(rowToTokenReaderFactory::open); - LongArray partitionIdToToken = new LongArray.DeferredLongArray(rowToPartitionReaderFactory::open); - LongArray partitionIdToSize = new LongArray.DeferredLongArray(partitionToSizeReaderFactory::open); - - return new WidePrimaryKeyMap(rowIdToToken, - partitionIdToToken, - partitionIdToSize, - partitionKeyReader.openCursor(), - clusteringKeyReader.openCursor(), - primaryKeyFactory, - clusteringComparator, - sstableId); - } - - @Override - public void close() - { - super.close(); - FileUtils.closeQuietly(Arrays.asList(clustingingKeyBlocksFile, clusteringKeyBlockOffsetsFile, partitionToSizeFile)); - } - } - - private final LongArray partitionIdToSizeArray; - private final ClusteringComparator clusteringComparator; - private final KeyLookup.Cursor clusteringKeyCursor; - - private WidePrimaryKeyMap(LongArray rowIdToTokenArray, - LongArray rowIdToPartitionIdArray, - LongArray partitionIdToSizeArray, - KeyLookup.Cursor partitionKeyCursor, - KeyLookup.Cursor clusteringKeyCursor, - PrimaryKey.Factory primaryKeyFactory, - ClusteringComparator clusteringComparator, - SSTableId sstableId) - { - super(rowIdToTokenArray, rowIdToPartitionIdArray, partitionKeyCursor, primaryKeyFactory, sstableId); - - this.partitionIdToSizeArray = partitionIdToSizeArray; - this.clusteringComparator = clusteringComparator; - this.clusteringKeyCursor = clusteringKeyCursor; - } - - @Override - public PrimaryKey primaryKeyFromRowId(long sstableRowId) - { - return primaryKeyFactory.create(readPartitionKey(sstableRowId), readClusteringKey(sstableRowId)); - } - - @Override - public long rowIdFromPrimaryKey(PrimaryKey primaryKey) - { - long rowId = rowIdToTokenArray.indexOf(primaryKey.token().getLongValue()); - - // If the key only has a token (initial range skip in the query), the token is out of range, - // or we have skipped a token, return the rowId from the token array. - if (primaryKey.kind() == PrimaryKey.Kind.TOKEN || rowId < 0 || rowIdToTokenArray.get(rowId) != primaryKey.token().getLongValue()) - return rowId; - - rowId = tokenCollisionDetection(primaryKey, rowId); - - // Search the key store for the key in the same partition - return clusteringKeyCursor.clusteredSeekToKey(clusteringComparator.asByteComparable(primaryKey.clustering()), rowId, startOfNextPartition(rowId)); - } - - @Override - public long floor(Token token) - { - if (token.isMinimum()) - return Long.MIN_VALUE; - long rowId = rowIdToTokenArray.indexOf(token.getLongValue()); - return rowId < 0 ? rowId : startOfNextPartition(rowId) - 1; - } - - @Override - public void close() - { - super.close(); - FileUtils.closeQuietly(clusteringKeyCursor); - } - - private Clustering readClusteringKey(long sstableRowId) - { - return primaryKeyFactory.clusteringFromByteComparable(clusteringKeyCursor.seekToPointId(sstableRowId)); - } - - // Returns the rowId of the next partition or the number of rows if supplied rowId is in the last partition - private long startOfNextPartition(long rowId) - { - long partitionSize = partitionIdToSizeArray.get(rowIdToPartitionIdArray.get(rowId)); - return partitionSize == -1 ? rowIdToPartitionIdArray.length() : rowId + partitionSize; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsIndex.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsIndex.java deleted file mode 100644 index 4b87f4673acf..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsIndex.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.IOException; - -import com.carrotsearch.hppc.IntLongHashMap; -import com.carrotsearch.hppc.IntLongMap; -import org.apache.cassandra.index.sai.disk.io.IndexInputReader; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.RandomAccessReader; - -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.validate; - -/** - * Mapping between node ID and an offset to its auxiliary posting list (containing every row id from all leaves - * reachable from that node. See {@link BlockBalancedTreePostingsWriter}). - */ -class BlockBalancedTreePostingsIndex -{ - private final int size; - public final IntLongMap index = new IntLongHashMap(); - - BlockBalancedTreePostingsIndex(FileHandle postingsFileHandle, long filePosition) throws IOException - { - try (RandomAccessReader reader = postingsFileHandle.createReader(); - IndexInputReader input = IndexInputReader.create(reader)) - { - validate(input); - input.seek(filePosition); - - size = input.readVInt(); - - for (int x = 0; x < size; x++) - { - final int node = input.readVInt(); - final long filePointer = input.readVLong(); - - index.put(node, filePointer); - } - } - } - - /** - * Returns true if given node ID has an auxiliary posting list. - */ - boolean exists(int nodeID) - { - return index.containsKey(nodeID); - } - - /** - * Returns an offset within the balanced tree postings file to the begining of the blocks summary of given node's auxiliary - * posting list. - * - * @throws IllegalArgumentException when given nodeID doesn't have an auxiliary posting list. Check first with - * {@link #exists(int)} - */ - long getPostingsFilePointer(int nodeID) - { - return index.get(nodeID); - } - - int size() - { - return size; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java deleted file mode 100644 index 590528e782a4..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.PriorityQueue; -import java.util.TreeMap; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import javax.annotation.concurrent.NotThreadSafe; - -import com.google.common.base.Stopwatch; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Multimap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.agrona.collections.IntArrayList; -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; -import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList; -import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList; -import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter; -import org.apache.cassandra.index.sai.postings.PeekablePostingList; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.util.packed.PackedLongValues; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; - -/** - * Writes leaf postings and auxiliary posting lists for bbtree nodes. If a node has a posting list attached, - * it will contain every row id from all leaves reachable from that node. - *

    - * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible - * nodes and leaf nodes reachable from them. - *

    - * The leaf blocks are written in value order (in the order we pass them to the {@link BlockBalancedTreeWriter}). - * This allows us to skip reading the leaves, instead just order leaf blocks by their offset in the index file, - * and correlate them with buffered posting lists. - */ -@NotThreadSafe -public class BlockBalancedTreePostingsWriter implements BlockBalancedTreeWalker.TraversalCallback -{ - private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class); - - private final TreeMap leafOffsetToNodeID = new TreeMap<>(Long::compareTo); - private final Multimap nodeToChildLeaves = HashMultimap.create(); - - /** - * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list. - */ - private final int minimumPostingsLeaves; - /** - * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list. - * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no - * level 0, because there is no node at level 0). - */ - private final int postingsSkip; - - int numNonLeafPostings = 0; - int numLeafPostings = 0; - - public BlockBalancedTreePostingsWriter() - { - minimumPostingsLeaves = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt(); - postingsSkip = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt(); - } - - /** - * Called when a leaf node is hit as we traverse the packed index. - * - * @param leafNodeID the current leaf node ID in the packed inded - * @param leafBlockFP the file pointer to the on-disk leaf block - * @param pathToRoot the path to the root leaf above this leaf. Contains all the intermediate leaf node IDs. - */ - @Override - public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot) - { - checkArgument(!pathToRoot.containsInt(leafNodeID)); - checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1)); - - leafOffsetToNodeID.put(leafBlockFP, leafNodeID); - for (int i = 0; i < pathToRoot.size(); i++) - { - int level = i + 1; - if (isLevelEligibleForPostingList(level)) - { - int nodeID = pathToRoot.get(i); - nodeToChildLeaves.put(nodeID, leafNodeID); - } - } - } - - /** - * Writes merged posting lists for eligible internal nodes and leaf postings for each leaf in the tree. - * The merged postings list for an internal node contains all postings from the postings lists of leaf nodes - * in the subtree rooted at that node. - *

    - * After writing out the postings, it writes a map of node ID -> postings file pointer for all - * nodes with an attached postings list. It then returns the file pointer to this map. - */ - public long finish(IndexOutputWriter out, List leafPostings, IndexIdentifier indexIdentifier) throws IOException - { - checkState(leafPostings.size() == leafOffsetToNodeID.size(), - "Expected equal number of postings lists (%s) and leaf offsets (%s).", - leafPostings.size(), leafOffsetToNodeID.size()); - - try (PostingsWriter postingsWriter = new PostingsWriter(out)) - { - Iterator postingsIterator = leafPostings.iterator(); - Map leafToPostings = new HashMap<>(); - leafOffsetToNodeID.forEach((fp, nodeID) -> leafToPostings.put(nodeID, postingsIterator.next())); - - long postingsRamBytesUsed = leafPostings.stream() - .mapToLong(PackedLongValues::ramBytesUsed) - .sum(); - - List internalNodeIDs = nodeToChildLeaves.keySet() - .stream() - .filter(i -> nodeToChildLeaves.get(i).size() >= minimumPostingsLeaves) - .collect(Collectors.toList()); - - Collection leafNodeIDs = leafOffsetToNodeID.values(); - - logger.debug(indexIdentifier.logMessage("Writing posting lists for {} internal and {} leaf balanced tree nodes. Leaf postings memory usage: {}."), - internalNodeIDs.size(), leafNodeIDs.size(), FBUtilities.prettyPrintMemory(postingsRamBytesUsed)); - - long startFP = out.getFilePointer(); - Stopwatch flushTime = Stopwatch.createStarted(); - TreeMap nodeIDToPostingsFilePointer = new TreeMap<>(); - PriorityQueue postingLists = new PriorityQueue<>(minimumPostingsLeaves, Comparator.comparingLong(PeekablePostingList::peek)); - for (int nodeID : Iterables.concat(internalNodeIDs, leafNodeIDs)) - { - Collection leaves = nodeToChildLeaves.get(nodeID); - - if (leaves.isEmpty()) - { - leaves = Collections.singletonList(nodeID); - numLeafPostings++; - } - else - { - numNonLeafPostings++; - } - - for (Integer leaf : leaves) - postingLists.add(PeekablePostingList.makePeekable(new PackedLongsPostingList(leafToPostings.get(leaf)))); - - try (PostingList mergedPostingList = MergePostingList.merge(postingLists)) - { - long postingFilePosition = postingsWriter.write(mergedPostingList); - // During compaction, we could end up with an empty postings due to deletions. - // The writer will return a fp of -1 if no postings were written. - if (postingFilePosition >= 0) - nodeIDToPostingsFilePointer.put(nodeID, postingFilePosition); - } - postingLists.clear(); - } - flushTime.stop(); - logger.debug(indexIdentifier.logMessage("Flushed {} of posting lists for balanced tree nodes in {} ms."), - FBUtilities.prettyPrintMemory(out.getFilePointer() - startFP), - flushTime.elapsed(TimeUnit.MILLISECONDS)); - - long indexFilePointer = out.getFilePointer(); - writeMap(nodeIDToPostingsFilePointer, out); - postingsWriter.complete(); - return indexFilePointer; - } - } - - private boolean isLevelEligibleForPostingList(int level) - { - return level > 1 && level % postingsSkip == 0; - } - - private void writeMap(Map map, IndexOutput out) throws IOException - { - out.writeVInt(map.size()); - - for (Map.Entry e : map.entrySet()) - { - out.writeVInt(e.getKey()); - out.writeVLong(e.getValue()); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java deleted file mode 100644 index bb4b477c40d7..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.IndexTermType; -import org.apache.cassandra.utils.ByteArrayUtil; -import org.apache.lucene.index.PointValues.Relation; - -public class BlockBalancedTreeQueries -{ - private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor() - { - @Override - public boolean contains(byte[] packedValue) - { - return true; - } - - @Override - public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) - { - return Relation.CELL_INSIDE_QUERY; - } - }; - - public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue) - { - if (expression.lower() == null && expression.upper() == null) - { - return MATCH_ALL; - } - - Bound lower = null ; - if (expression.lower() != null) - { - final byte[] lowerBound = toComparableBytes(bytesPerValue, expression.lower().value.encoded, expression.getIndexTermType()); - lower = new Bound(lowerBound, !expression.lower().inclusive); - } - - Bound upper = null; - if (expression.upper() != null) - { - final byte[] upperBound = toComparableBytes(bytesPerValue, expression.upper().value.encoded, expression.getIndexTermType()); - upper = new Bound(upperBound, !expression.upper().inclusive); - } - - return new RangeQueryVisitor(lower, upper); - } - - private static byte[] toComparableBytes(int bytesPerDim, ByteBuffer value, IndexTermType indexTermType) - { - byte[] buffer = new byte[indexTermType.fixedSizeOf()]; - assert buffer.length == bytesPerDim; - indexTermType.toComparableBytes(value, buffer); - return buffer; - } - - private static class Bound - { - private final byte[] bound; - private final boolean exclusive; - - Bound(byte[] bound, boolean exclusive) - { - this.bound = bound; - this.exclusive = exclusive; - } - - boolean smallerThan(byte[] packedValue) - { - int cmp = compareTo(packedValue); - return cmp < 0 || (cmp == 0 && exclusive); - } - - boolean greaterThan(byte[] packedValue) - { - int cmp = compareTo(packedValue); - return cmp > 0 || (cmp == 0 && exclusive); - } - - private int compareTo(byte[] packedValue) - { - return ByteArrayUtil.compareUnsigned(bound, 0, packedValue, 0, bound.length); - } - } - - private static class RangeQueryVisitor implements BlockBalancedTreeReader.IntersectVisitor - { - private final Bound lower; - private final Bound upper; - - private RangeQueryVisitor(Bound lower, Bound upper) - { - this.lower = lower; - this.upper = upper; - } - - @Override - public boolean contains(byte[] packedValue) - { - if (lower != null) - { - if (lower.greaterThan(packedValue)) - { - // value is too low, in this dimension - return false; - } - } - - if (upper != null) - { - return !upper.smallerThan(packedValue); - } - - return true; - } - - @Override - public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) - { - boolean crosses = false; - - if (lower != null) - { - if (lower.greaterThan(maxPackedValue)) - return Relation.CELL_OUTSIDE_QUERY; - - crosses = lower.greaterThan(minPackedValue); - } - - if (upper != null) - { - if (upper.smallerThan(minPackedValue)) - return Relation.CELL_OUTSIDE_QUERY; - - crosses |= upper.smallerThan(maxPackedValue); - } - - return crosses ? Relation.CELL_CROSSES_QUERY : Relation.CELL_INSIDE_QUERY; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java deleted file mode 100644 index 0f14db390b4f..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.Closeable; -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.Comparator; -import java.util.PriorityQueue; -import java.util.concurrent.TimeUnit; - -import com.google.common.base.Stopwatch; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.exceptions.QueryCancelledException; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; -import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput; -import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList; -import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList; -import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader; -import org.apache.cassandra.index.sai.metrics.QueryEventListener; -import org.apache.cassandra.index.sai.postings.PeekablePostingList; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.ByteArrayUtil; -import org.apache.cassandra.utils.Throwables; -import org.apache.lucene.index.CorruptIndexException; -import org.apache.lucene.index.PointValues.Relation; -import org.apache.lucene.store.IndexInput; -import org.apache.lucene.util.FixedBitSet; -import org.apache.lucene.util.LongValues; -import org.apache.lucene.util.packed.DirectReader; -import org.apache.lucene.util.packed.DirectWriter; - -/** - * Handles intersection of a point or point range with a block balanced tree previously written with - * {@link BlockBalancedTreeWriter}. - */ -public class BlockBalancedTreeReader extends BlockBalancedTreeWalker implements Closeable -{ - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private static final Comparator COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek); - - private final IndexIdentifier indexIdentifier; - private final FileHandle postingsFile; - private final BlockBalancedTreePostingsIndex postingsIndex; - private final int leafOrderMapBitsRequired; - /** - * Performs a blocking read. - */ - public BlockBalancedTreeReader(IndexIdentifier indexIdentifier, - FileHandle treeIndexFile, - long treeIndexRoot, - FileHandle postingsFile, - long treePostingsRoot) throws IOException - { - super(treeIndexFile, treeIndexRoot); - this.indexIdentifier = indexIdentifier; - this.postingsFile = postingsFile; - this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot); - leafOrderMapBitsRequired = DirectWriter.unsignedBitsRequired(maxValuesInLeafNode - 1); - } - - public int getBytesPerValue() - { - return bytesPerValue; - } - - public long getPointCount() - { - return valueCount; - } - - @Override - public void close() - { - super.close(); - FileUtils.closeQuietly(postingsFile); - } - - public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context) - { - Relation relation = visitor.compare(minPackedValue, maxPackedValue); - - if (relation == Relation.CELL_OUTSIDE_QUERY) - { - listener.onIntersectionEarlyExit(); - return null; - } - - listener.onSegmentHit(); - IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile); - IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile); - IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile); - - Intersection intersection = relation == Relation.CELL_INSIDE_QUERY - ? new Intersection(treeInput, postingsInput, postingsSummaryInput, listener, context) - : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, visitor, listener, context); - - return intersection.execute(); - } - - /** - * Synchronous intersection of a point or point range with a block balanced tree previously written - * with {@link BlockBalancedTreeWriter}. - */ - private class Intersection - { - private final Stopwatch queryExecutionTimer = Stopwatch.createStarted(); - final QueryContext context; - - final TraversalState state; - final IndexInput treeInput; - final IndexInput postingsInput; - final IndexInput postingsSummaryInput; - final QueryEventListener.BalancedTreeEventListener listener; - final PriorityQueue postingLists; - - Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput, - QueryEventListener.BalancedTreeEventListener listener, QueryContext context) - { - this.state = newTraversalState(); - this.treeInput = treeInput; - this.postingsInput = postingsInput; - this.postingsSummaryInput = postingsSummaryInput; - this.listener = listener; - this.context = context; - postingLists = new PriorityQueue<>(numLeaves, COMPARATOR); - } - - public PostingList execute() - { - try - { - executeInternal(); - - FileUtils.closeQuietly(treeInput); - - return mergePostings(); - } - catch (Throwable t) - { - if (!(t instanceof QueryCancelledException)) - logger.error(indexIdentifier.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t); - - closeOnException(); - throw Throwables.cleaned(t); - } - } - - protected void executeInternal() throws IOException - { - collectPostingLists(); - } - - protected void closeOnException() - { - FileUtils.closeQuietly(treeInput); - FileUtils.closeQuietly(postingsInput); - FileUtils.closeQuietly(postingsSummaryInput); - } - - protected PostingList mergePostings() - { - final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS); - - listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS); - listener.postingListsHit(postingLists.size()); - - if (postingLists.isEmpty()) - { - FileUtils.closeQuietly(postingsInput); - FileUtils.closeQuietly(postingsSummaryInput); - return null; - } - else - { - if (logger.isTraceEnabled()) - logger.trace(indexIdentifier.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."), - treeIndexFile.path(), elapsedMicros, postingLists.size()); - return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput)); - } - } - - private void collectPostingLists() throws IOException - { - context.checkpoint(); - - // This will return true if the node is a child leaf that has postings or if there is postings for the - // entire subtree under a leaf - if (postingsIndex.exists(state.nodeID)) - { - postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(state.nodeID))); - return; - } - - if (state.atLeafNode()) - throw new CorruptIndexException(indexIdentifier.logMessage(String.format("Leaf node %s does not have balanced tree postings.", state.nodeID)), ""); - - // Recurse on left subtree: - state.pushLeft(); - collectPostingLists(); - state.pop(); - - // Recurse on right subtree: - state.pushRight(); - collectPostingLists(); - state.pop(); - } - - private PeekablePostingList initPostingReader(long offset) throws IOException - { - final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset); - return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener())); - } - } - - private class FilteringIntersection extends Intersection - { - private final IntersectVisitor visitor; - private final byte[] packedValue; - private final short[] origIndex; - - FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput, - IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context) - { - super(treeInput, postingsInput, postingsSummaryInput, listener, context); - this.visitor = visitor; - this.packedValue = new byte[bytesPerValue]; - this.origIndex = new short[maxValuesInLeafNode]; - } - - @Override - public void executeInternal() throws IOException - { - collectPostingLists(minPackedValue, maxPackedValue); - } - - private void collectPostingLists(byte[] minPackedValue, byte[] maxPackedValue) throws IOException - { - context.checkpoint(); - - final Relation r = visitor.compare(minPackedValue, maxPackedValue); - - // This value range is fully outside the query shape: stop recursing - if (r == Relation.CELL_OUTSIDE_QUERY) - return; - - if (r == Relation.CELL_INSIDE_QUERY) - { - // This value range is fully inside the query shape: recursively add all points from this node without filtering - super.collectPostingLists(); - return; - } - - if (state.atLeafNode()) - { - if (state.nodeExists()) - filterLeaf(); - return; - } - - visitNode(minPackedValue, maxPackedValue); - } - - private void filterLeaf() throws IOException - { - treeInput.seek(state.getLeafBlockFP()); - - int count = treeInput.readVInt(); - int orderMapLength = treeInput.readVInt(); - long orderMapPointer = treeInput.getFilePointer(); - - SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput); - LongValues leafOrderMapReader = DirectReader.getInstance(randomAccessInput, leafOrderMapBitsRequired, orderMapPointer); - for (int index = 0; index < count; index++) - { - origIndex[index] = (short) Math.toIntExact(leafOrderMapReader.get(index)); - } - - // seek beyond the ordermap - treeInput.seek(orderMapPointer + orderMapLength); - - FixedBitSet fixedBitSet = buildPostingsFilter(treeInput, count, visitor, origIndex); - - if (postingsIndex.exists(state.nodeID) && fixedBitSet.cardinality() > 0) - { - long pointer = postingsIndex.getPostingsFilePointer(state.nodeID); - postingLists.add(initFilteringPostingReader(pointer, fixedBitSet)); - } - } - - void visitNode(byte[] minPackedValue, byte[] maxPackedValue) throws IOException - { - assert !state.atLeafNode() : "Cannot recurse down tree because nodeID " + state.nodeID + " is a leaf node"; - - byte[] splitValue = state.getSplitValue(); - - if (BlockBalancedTreeWriter.DEBUG) - { - // make sure cellMin <= splitValue <= cellMax: - assert ByteArrayUtil.compareUnsigned(minPackedValue, 0, splitValue, 0, bytesPerValue) <= 0 :"bytesPerValue=" + bytesPerValue; - assert ByteArrayUtil.compareUnsigned(maxPackedValue, 0, splitValue, 0, bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue; - } - - // Recurse on left subtree: - state.pushLeft(); - collectPostingLists(minPackedValue, splitValue); - state.pop(); - - // Recurse on right subtree: - state.pushRight(); - collectPostingLists(splitValue, maxPackedValue); - state.pop(); - } - - private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException - { - final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset); - PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener()); - return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader)); - } - - private FixedBitSet buildPostingsFilter(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException - { - int commonPrefixLength = readCommonPrefixLength(in); - return commonPrefixLength == bytesPerValue ? buildPostingsFilterForSingleValueLeaf(count, visitor, origIndex) - : buildPostingsFilterForMultiValueLeaf(commonPrefixLength, in, count, visitor, origIndex); - } - - private FixedBitSet buildPostingsFilterForMultiValueLeaf(int commonPrefixLength, - IndexInput in, - int count, - IntersectVisitor visitor, - short[] origIndex) throws IOException - { - // the byte at `compressedByteOffset` is compressed using run-length compression, - // other suffix bytes are stored verbatim - int compressedByteOffset = commonPrefixLength; - commonPrefixLength++; - int i; - - FixedBitSet fixedBitSet = new FixedBitSet(maxValuesInLeafNode); - - for (i = 0; i < count; ) - { - packedValue[compressedByteOffset] = in.readByte(); - final int runLen = Byte.toUnsignedInt(in.readByte()); - for (int j = 0; j < runLen; ++j) - { - in.readBytes(packedValue, commonPrefixLength, bytesPerValue - commonPrefixLength); - final int rowIDIndex = origIndex[i + j]; - if (visitor.contains(packedValue)) - fixedBitSet.set(rowIDIndex); - } - i += runLen; - } - if (i != count) - throw new CorruptIndexException(String.format("Expected %d sub-blocks but read %d.", count, i), in); - - return fixedBitSet; - } - - private FixedBitSet buildPostingsFilterForSingleValueLeaf(int count, IntersectVisitor visitor, final short[] origIndex) - { - FixedBitSet fixedBitSet = new FixedBitSet(maxValuesInLeafNode); - - // All the values in the leaf are the same, so we only - // need to visit once then set the bits for the relevant indexes - if (visitor.contains(packedValue)) - { - for (int i = 0; i < count; ++i) - fixedBitSet.set(origIndex[i]); - } - return fixedBitSet; - } - - private int readCommonPrefixLength(IndexInput in) throws IOException - { - int prefixLength = in.readVInt(); - if (prefixLength > 0) - in.readBytes(packedValue, 0, prefixLength); - return prefixLength; - } - } - - /** - * We recurse the balanced tree, using a provided instance of this to guide the recursion. - */ - public interface IntersectVisitor - { - /** - * Called for all values in a leaf cell that crosses the query. The consumer should scrutinize the packedValue - * to decide whether to accept it. Values are visited in increasing order, and in the case of ties, - * in increasing order by segment row ID. - */ - boolean contains(byte[] packedValue); - - /** - * Called for non-leaf cells to test how the cell relates to the query, to - * determine how to further recurse down the tree. - */ - Relation compare(byte[] minPackedValue, byte[] maxPackedValue); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java deleted file mode 100644 index 5a01b81f0971..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.Closeable; -import java.io.IOException; -import java.util.Arrays; - -import javax.annotation.concurrent.NotThreadSafe; - -import com.google.common.annotations.VisibleForTesting; - -import org.agrona.collections.IntArrayList; -import org.apache.cassandra.index.sai.disk.io.IndexInputReader; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.io.util.RandomAccessReader; -import org.apache.cassandra.utils.ByteArrayUtil; -import org.apache.cassandra.utils.ObjectSizes; -import org.apache.cassandra.utils.Throwables; -import org.apache.lucene.index.CorruptIndexException; -import org.apache.lucene.store.ByteArrayDataInput; -import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexInput; -import org.apache.lucene.util.BytesRef; - -/** - * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}. - *

    - * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}. - */ -public class BlockBalancedTreeWalker implements Closeable -{ - final FileHandle treeIndexFile; - final int bytesPerValue; - final int numLeaves; - final int treeDepth; - final byte[] minPackedValue; - final byte[] maxPackedValue; - final long valueCount; - final int maxValuesInLeafNode; - final byte[] packedIndex; - final long memoryUsage; - - BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot) - { - this.treeIndexFile = treeIndexFile; - - try (RandomAccessReader reader = treeIndexFile.createReader(); - IndexInput indexInput = IndexInputReader.create(reader)) - { - SAICodecUtils.validate(indexInput); - indexInput.seek(treeIndexRoot); - - maxValuesInLeafNode = indexInput.readVInt(); - bytesPerValue = indexInput.readVInt(); - - // Read index: - numLeaves = indexInput.readVInt(); - assert numLeaves > 0; - treeDepth = indexInput.readVInt(); - minPackedValue = new byte[bytesPerValue]; - maxPackedValue = new byte[bytesPerValue]; - - indexInput.readBytes(minPackedValue, 0, bytesPerValue); - indexInput.readBytes(maxPackedValue, 0, bytesPerValue); - - if (ByteArrayUtil.compareUnsigned(minPackedValue, 0, maxPackedValue, 0, bytesPerValue) > 0) - { - String message = String.format("Min packed value %s is > max packed value %s.", - new BytesRef(minPackedValue), new BytesRef(maxPackedValue)); - throw new CorruptIndexException(message, indexInput); - } - - valueCount = indexInput.readVLong(); - - int numBytes = indexInput.readVInt(); - packedIndex = new byte[numBytes]; - indexInput.readBytes(packedIndex, 0, numBytes); - - memoryUsage = ObjectSizes.sizeOfArray(packedIndex) + - ObjectSizes.sizeOfArray(minPackedValue) + - ObjectSizes.sizeOfArray(maxPackedValue); - } - catch (Throwable t) - { - FileUtils.closeQuietly(treeIndexFile); - throw Throwables.unchecked(t); - } - } - - @VisibleForTesting - public BlockBalancedTreeWalker(DataInput indexInput, long treeIndexRoot) throws IOException - { - treeIndexFile = null; - - indexInput.skipBytes(treeIndexRoot); - - maxValuesInLeafNode = indexInput.readVInt(); - bytesPerValue = indexInput.readVInt(); - - // Read index: - numLeaves = indexInput.readVInt(); - assert numLeaves > 0; - treeDepth = indexInput.readVInt(); - minPackedValue = new byte[bytesPerValue]; - maxPackedValue = new byte[bytesPerValue]; - - indexInput.readBytes(minPackedValue, 0, bytesPerValue); - indexInput.readBytes(maxPackedValue, 0, bytesPerValue); - - if (ByteArrayUtil.compareUnsigned(minPackedValue, 0, maxPackedValue, 0, bytesPerValue) > 0) - { - String message = String.format("Min packed value %s is > max packed value %s.", - new BytesRef(minPackedValue), new BytesRef(maxPackedValue)); - throw new CorruptIndexException(message, indexInput); - } - - valueCount = indexInput.readVLong(); - - int numBytes = indexInput.readVInt(); - packedIndex = new byte[numBytes]; - indexInput.readBytes(packedIndex, 0, numBytes); - - memoryUsage = ObjectSizes.sizeOfArray(packedIndex) + - ObjectSizes.sizeOfArray(minPackedValue) + - ObjectSizes.sizeOfArray(maxPackedValue); - } - - public long memoryUsage() - { - return memoryUsage; - } - - public TraversalState newTraversalState() - { - return new TraversalState(); - } - - @Override - public void close() - { - FileUtils.closeQuietly(treeIndexFile); - } - - void traverse(TraversalCallback callback) - { - traverse(newTraversalState(), callback, new IntArrayList()); - } - - private void traverse(TraversalState state, TraversalCallback callback, IntArrayList pathToRoot) - { - if (state.atLeafNode()) - { - // In the unbalanced case it's possible the left most node only has one child: - if (state.nodeExists()) - { - callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot); - } - } - else - { - IntArrayList currentPath = new IntArrayList(); - currentPath.addAll(pathToRoot); - currentPath.add(state.nodeID); - - state.pushLeft(); - traverse(state, callback, currentPath); - state.pop(); - - state.pushRight(); - traverse(state, callback, currentPath); - state.pop(); - } - } - - interface TraversalCallback - { - void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot); - } - - /** - * This maintains the state for a traversal of the packed index. It is loaded once and can be resused - * by calling the reset method. - *

    - * The packed index is a packed representation of a balanced tree and takes the form of a packed array of - * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level - * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth. - *

    - * The tree is traversed by recursively following the left and then right subtrees under the current node. For the - * following tree (split values in square brackets): - *

    -     *        1[16]
    -     *       / \
    -     *      /   \
    -     *     2[8]  3[24]
    -     *    / \   / \
    -     *   4   5 6   7
    -     * 
    - * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes. - *

    - * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following - * values: - *

    -     *         1[0-32]
    -     *        /      \
    -     *    2[0-16]   3[16-32]
    -     * 
    - */ - @NotThreadSafe - final class TraversalState - { - // used to read the packed index byte[] - final ByteArrayDataInput dataInput; - // holds the minimum (left most) leaf block file pointer for each level we've recursed to: - final long[] leafBlockFPStack; - // holds the address, in the packed byte[] index, of the left-node of each level: - final int[] leftNodePositions; - // holds the address, in the packed byte[] index, of the right-node of each level: - final int[] rightNodePositions; - // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses: - final byte[][] splitValuesStack; - - int nodeID; - int level; - @VisibleForTesting - int maxLevel; - - private TraversalState() - { - nodeID = 1; - level = 0; - leafBlockFPStack = new long[treeDepth]; - leftNodePositions = new int[treeDepth]; - rightNodePositions = new int[treeDepth]; - splitValuesStack = new byte[treeDepth][]; - this.dataInput = new ByteArrayDataInput(packedIndex); - readNodeData(false); - } - - public void pushLeft() - { - int nodePosition = leftNodePositions[level]; - nodeID *= 2; - level++; - maxLevel = Math.max(maxLevel, level); - dataInput.setPosition(nodePosition); - readNodeData(true); - } - - public void pushRight() - { - int nodePosition = rightNodePositions[level]; - nodeID = nodeID * 2 + 1; - level++; - maxLevel = Math.max(maxLevel, level); - dataInput.setPosition(nodePosition); - readNodeData(false); - } - - public void pop() - { - nodeID /= 2; - level--; - } - - public boolean atLeafNode() - { - return nodeID >= numLeaves; - } - - public boolean nodeExists() - { - return nodeID - numLeaves < numLeaves; - } - - public long getLeafBlockFP() - { - return leafBlockFPStack[level]; - } - - public byte[] getSplitValue() - { - assert !atLeafNode(); - return splitValuesStack[level]; - } - - private void readNodeData(boolean isLeft) - { - leafBlockFPStack[level] = level == 0 ? 0 : leafBlockFPStack[level - 1]; - - // read leaf block FP delta - if (!isLeft) - leafBlockFPStack[level] += dataInput.readVLong(); - - if (!atLeafNode()) - { - // read prefix, firstDiffByteDelta encoded as int: - int code = dataInput.readVInt(); - int prefix = code % (1 + bytesPerValue); - int suffix = bytesPerValue - prefix; - - pushSplitValueStack(); - if (suffix > 0) - { - int firstDiffByteDelta = code / (1 + bytesPerValue); - // If we are pushing to the left subtree then the delta will be negative - if (isLeft) - firstDiffByteDelta = -firstDiffByteDelta; - int oldByte = splitValuesStack[level][prefix] & 0xFF; - splitValuesStack[level][prefix] = (byte) (oldByte + firstDiffByteDelta); - dataInput.readBytes(splitValuesStack[level], prefix + 1, suffix - 1); - } - - int leftNumBytes = nodeID * 2 < numLeaves ? dataInput.readVInt() : 0; - - leftNodePositions[level] = dataInput.getPosition(); - rightNodePositions[level] = leftNodePositions[level] + leftNumBytes; - } - } - - private void pushSplitValueStack() - { - if (splitValuesStack[level] == null) - splitValuesStack[level] = new byte[bytesPerValue]; - if (level == 0) - Arrays.fill(splitValuesStack[level], (byte) 0); - else - System.arraycopy(splitValuesStack[level - 1], 0, splitValuesStack[level], 0, bytesPerValue); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java deleted file mode 100644 index 0fa4180c0954..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java +++ /dev/null @@ -1,767 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -import javax.annotation.concurrent.NotThreadSafe; - -import com.google.common.base.MoreObjects; - -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.utils.IndexEntry; -import org.apache.cassandra.utils.ByteArrayUtil; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import org.apache.lucene.store.ByteBuffersDataOutput; -import org.apache.lucene.store.DataOutput; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.util.ArrayUtil; -import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.IntroSorter; -import org.apache.lucene.util.Sorter; -import org.apache.lucene.util.bkd.BKDWriter; - -import static org.apache.cassandra.index.sai.postings.PostingList.END_OF_STREAM; - -/** - * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension - * balanced tree. - *

    - * Recursively builds a block balanced tree to assign all incoming points to smaller - * and smaller rectangles (cells) until the number of points in a given - * rectangle is <= maxPointsInLeafNode. The tree is - * fully balanced, which means the leaf nodes will have between 50% and 100% of - * the requested maxPointsInLeafNode. Values that fall exactly - * on a cell boundary may be in either cell. - *

    - * Visual representation of the disk format: - *

    - *
    - * +========+=======================================+==================+========+
    - * | HEADER | LEAF BLOCK LIST                       | BALANCED TREE    | FOOTER |
    - * +========+================+=====+================+==================+========+
    - *          | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF  |
    - *          +----------------+-----+----------------+------------------|
    - *          | ORDER INDEX    |                      | BYTES PER VALUE  |
    - *          +----------------+                      +------------------+
    - *          | PREFIX         |                      | NUMBER OF LEAVES |
    - *          +----------------+                      +------------------+
    - *          | VALUES         |                      | MINIMUM VALUE    |
    - *          +----------------+                      +------------------+
    - *                                                  | MAXIMUM VALUE    |
    - *                                                  +------------------+
    - *                                                  | TOTAL VALUES     |
    - *                                                  +------------------+
    - *                                                  | INDEX TREE       |
    - *                                                  +--------+---------+
    - *                                                  | LENGTH | BYTES   |
    - *                                                  +--------+---------+
    - *  
    - * - *

    - * NOTE: This can write at most Integer.MAX_VALUE * maxPointsInLeafNode total points. - *

    - * @see BKDWriter - */ -@NotThreadSafe -public class BlockBalancedTreeWriter -{ - // Enable to check that values are added to the tree in correct order and within bounds - public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean(); - - // Default maximum number of point in each leaf block - public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024; - - private final int bytesPerValue; - private final int maxPointsInLeafNode; - private final byte[] minPackedValue; - private final byte[] maxPackedValue; - private long valueCount; - - public BlockBalancedTreeWriter(int bytesPerValue, int maxPointsInLeafNode) - { - if (maxPointsInLeafNode <= 0) - throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode); - if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH) - throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " + - ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode); - - this.maxPointsInLeafNode = maxPointsInLeafNode; - this.bytesPerValue = bytesPerValue; - - minPackedValue = new byte[bytesPerValue]; - maxPackedValue = new byte[bytesPerValue]; - } - - public long getValueCount() - { - return valueCount; - } - - public int getBytesPerValue() - { - return bytesPerValue; - } - - public int getMaxPointsInLeafNode() - { - return maxPointsInLeafNode; - } - - /** - * Write the sorted values from an {@link Iterator}. - *

    - * @param treeOutput The {@link IndexOutput} to write the balanced tree to - * @param iterator An {@link Iterator} of {@link IndexEntry}s containing the terms and postings, sorted in term order - * @param callback The {@link Callback} used to record the leaf postings for each leaf - * - * @return The file pointer to the beginning of the balanced tree - */ - public long write(IndexOutput treeOutput, Iterator iterator, final Callback callback) throws IOException - { - SAICodecUtils.writeHeader(treeOutput); - - LeafWriter leafWriter = new LeafWriter(treeOutput, callback); - - while (iterator.hasNext()) - { - IndexEntry indexEntry = iterator.next(); - long segmentRowId; - while ((segmentRowId = indexEntry.postingList.nextPosting()) != END_OF_STREAM) - leafWriter.add(indexEntry.term, segmentRowId); - } - - valueCount = leafWriter.finish(); - - long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer(); - - // There is only any point in writing the balanced tree if any values were added - if (treeFilePointer >= 0) - writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers); - - SAICodecUtils.writeFooter(treeOutput); - - return treeFilePointer; - } - - private void writeBalancedTree(IndexOutput out, int countPerLeaf, List leafBlockStartValues, List leafBlockFilePointer) throws IOException - { - int numInnerNodes = leafBlockStartValues.size(); - byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue]; - int treeDepth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues); - long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray(); - byte[] packedIndex = packIndex(leafBlockFPs, splitValues); - - out.writeVInt(countPerLeaf); - out.writeVInt(bytesPerValue); - - out.writeVInt(leafBlockFPs.length); - out.writeVInt(Math.min(treeDepth, leafBlockFPs.length)); - - out.writeBytes(minPackedValue, 0, bytesPerValue); - out.writeBytes(maxPackedValue, 0, bytesPerValue); - - out.writeVLong(valueCount); - - out.writeVInt(packedIndex.length); - out.writeBytes(packedIndex, 0, packedIndex.length); - } - - /** - * This can, potentially, be removed in the future by CASSANDRA-18597 - */ - private int recurseBalanceTree(int nodeID, int offset, int count, int treeDepth, byte[] splitValues, List leafBlockStartValues) - { - if (count == 1) - { - treeDepth++; - // Leaf index node - System.arraycopy(leafBlockStartValues.get(offset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue); - } - else if (count > 1) - { - treeDepth++; - // Internal index node: binary partition of count - int countAtLevel = 1; - int totalCount = 0; - while (true) - { - int countLeft = count - totalCount; - if (countLeft <= countAtLevel) - { - // This is the last level, possibly partially filled: - int lastLeftCount = Math.min(countAtLevel / 2, countLeft); - assert lastLeftCount >= 0; - int leftHalf = (totalCount - 1) / 2 + lastLeftCount; - - int rootOffset = offset + leftHalf; - - System.arraycopy(leafBlockStartValues.get(rootOffset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue); - - // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree - // under here, to save this while loop on each recursion - - // Recurse left - int leftTreeDepth = recurseBalanceTree(2 * nodeID, offset, leftHalf, treeDepth, splitValues, leafBlockStartValues); - - // Recurse right - int rightTreeDepth = recurseBalanceTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, treeDepth, splitValues, leafBlockStartValues); - return Math.max(leftTreeDepth, rightTreeDepth); - } - totalCount += countAtLevel; - countAtLevel *= 2; - } - } - else - { - assert count == 0; - } - return treeDepth; - } - - // Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. - private byte[] packIndex(long[] leafBlockFPs, byte[] splitValues) throws IOException - { - int numLeaves = leafBlockFPs.length; - - // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens - // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom - // levels of the binary tree: - if (numLeaves > 1) - { - int levelCount = 2; - while (true) - { - if (numLeaves >= levelCount && numLeaves <= 2 * levelCount) - { - int lastLevel = 2 * (numLeaves - levelCount); - assert lastLevel >= 0; - if (lastLevel != 0) - { - // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading - // at read-time, so that we can still delta code them on disk at write: - long[] newLeafBlockFPs = new long[numLeaves]; - System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel); - System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel); - leafBlockFPs = newLeafBlockFPs; - } - break; - } - - levelCount *= 2; - } - } - - // Reused while packing the index - try (ResettableByteBuffersIndexOutput writeBuffer = new ResettableByteBuffersIndexOutput("PackedIndex")) - { - // This is the "file" we append the byte[] to: - List blocks = new ArrayList<>(); - byte[] lastSplitValue = new byte[bytesPerValue]; - int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitValues, 0, blocks, 1, lastSplitValue, false); - // Compact the byte[] blocks into single byte index: - byte[] index = new byte[totalSize]; - int upto = 0; - for (byte[] block : blocks) - { - System.arraycopy(block, 0, index, upto, block.length); - upto += block.length; - } - assert upto == totalSize; - - return index; - } - } - - /** - * lastSplitValue is the split value previously seen; we use this to prefix-code the split byte[] on each - * inner node - */ - private int recursePackIndex(ResettableByteBuffersIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitValues, - long minBlockFP, List blocks, int nodeID, byte[] lastSplitValue, boolean isLeft) throws IOException - { - if (nodeID >= leafBlockFPs.length) - { - int leafID = nodeID - leafBlockFPs.length; - - // In the unbalanced case it's possible the left most node only has one child: - if (leafID < leafBlockFPs.length) - { - long delta = leafBlockFPs[leafID] - minBlockFP; - if (isLeft) - { - assert delta == 0; - return 0; - } - else - { - assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID; - writeBuffer.writeVLong(delta); - return appendBlock(writeBuffer, blocks); - } - } - else - { - throw new IllegalStateException("Unbalanced tree"); - } - } - else - { - long leftBlockFP; - if (!isLeft) - { - leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID); - long delta = leftBlockFP - minBlockFP; - assert nodeID == 1 || delta > 0; - writeBuffer.writeVLong(delta); - } - else - { - // The left tree's left most leaf block FP is always the minimal FP: - leftBlockFP = minBlockFP; - } - - int address = nodeID * bytesPerValue; - - // find common prefix with last split value in this dim: - int prefix = 0; - for (; prefix < bytesPerValue; prefix++) - { - if (splitValues[address + prefix] != lastSplitValue[prefix]) - { - break; - } - } - - int firstDiffByteDelta; - if (prefix < bytesPerValue) - { - firstDiffByteDelta = (splitValues[address + prefix] & 0xFF) - (lastSplitValue[prefix] & 0xFF); - // If this is left then we need to negate the delta - if (isLeft) - firstDiffByteDelta = -firstDiffByteDelta; - assert firstDiffByteDelta > 0; - } - else - { - firstDiffByteDelta = 0; - } - - // pack the prefix and delta first diff byte into a single vInt: - int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix); - - writeBuffer.writeVInt(code); - - // write the split value, prefix coded vs. our parent's split value: - int suffix = bytesPerValue - prefix; - byte[] savSplitValue = new byte[suffix]; - if (suffix > 1) - { - writeBuffer.writeBytes(splitValues, address + prefix + 1, suffix - 1); - } - - byte[] cmp = lastSplitValue.clone(); - - System.arraycopy(lastSplitValue, prefix, savSplitValue, 0, suffix); - - // copy our split value into lastSplitValue for our children to prefix-code against - System.arraycopy(splitValues, address + prefix, lastSplitValue, prefix, suffix); - - int numBytes = appendBlock(writeBuffer, blocks); - - // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into - // the right subtree we can quickly seek to its starting point - int idxSav = blocks.size(); - blocks.add(null); - - int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValue, true); - - if (nodeID * 2 < leafBlockFPs.length) - { - writeBuffer.writeVInt(leftNumBytes); - } - else - { - assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes; - } - int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer()); - byte[] bytes2 = writeBuffer.toArrayCopy(); - writeBuffer.reset(); - // replace our placeholder: - blocks.set(idxSav, bytes2); - - int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValue, false); - - // restore lastSplitValue to what caller originally passed us: - System.arraycopy(savSplitValue, 0, lastSplitValue, prefix, suffix); - - assert Arrays.equals(lastSplitValue, cmp); - - return numBytes + numBytes2 + leftNumBytes + rightNumBytes; - } - } - - /** Appends the current contents of writeBuffer as another block on the growing in-memory file */ - private int appendBlock(ResettableByteBuffersIndexOutput writeBuffer, List blocks) - { - int pos = Math.toIntExact(writeBuffer.getFilePointer()); - byte[] bytes = writeBuffer.toArrayCopy(); - writeBuffer.reset(); - blocks.add(bytes); - return pos; - } - - private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID) - { - // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or - // change the recursion while packing the index to return this left-most leaf block FP - // from each recursion instead? - // - // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing - // we call it O(N) times (N = number of leaf blocks) - while (nodeID < leafBlockFPs.length) - { - nodeID *= 2; - } - int leafID = nodeID - leafBlockFPs.length; - long result = leafBlockFPs[leafID]; - if (result < 0) - { - throw new AssertionError(result + " for leaf " + leafID); - } - return result; - } - - interface Callback - { - void writeLeafPostings(RowIDAndIndex[] leafPostings, int offset, int count); - } - - static class RowIDAndIndex - { - public int valueOrderIndex; - public long rowID; - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this) - .add("valueOrderIndex", valueOrderIndex) - .add("rowID", rowID) - .toString(); - } - } - - /** - * Responsible for writing the leaf blocks at the beginning of the balanced tree index. - */ - private class LeafWriter - { - private final IndexOutput treeOutput; - private final List leafBlockFilePointers = new ArrayList<>(); - private final List leafBlockStartValues = new ArrayList<>(); - private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue]; - private final long[] leafRowIDs = new long[maxPointsInLeafNode]; - private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode]; - private final int[] orderIndex = new int[maxPointsInLeafNode]; - private final Callback callback; - private final ByteBuffersDataOutput leafOrderIndexOutput = new ByteBuffersDataOutput(2 * 1024); - private final ByteBuffersDataOutput leafBlockOutput = new ByteBuffersDataOutput(32 * 1024); - private final byte[] packedValue = new byte[bytesPerValue]; - private final byte[] lastPackedValue = new byte[bytesPerValue]; - - private long valueCount; - private int leafValueCount; - private long lastRowID; - - LeafWriter(IndexOutput treeOutput, Callback callback) - { - assert callback != null : "Callback cannot be null in TreeWriter"; - - this.treeOutput = treeOutput; - this.callback = callback; - - for (int x = 0; x < rowIDAndIndexes.length; x++) - { - rowIDAndIndexes[x] = new RowIDAndIndex(); - } - } - - /** - * Adds a value and row ID to the current leaf block. If the leaf block is full after the addition - * the current leaf block is written to disk. - */ - void add(ByteComparable value, long rowID) throws IOException - { - ByteSourceInverse.copyBytes(value.asComparableBytes(ByteComparable.Version.OSS50), packedValue); - - if (DEBUG) - valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, rowID, lastRowID); - - System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue); - leafRowIDs[leafValueCount] = rowID; - leafValueCount++; - - if (leafValueCount == maxPointsInLeafNode) - { - // We write a block once we hit exactly the max count - writeLeafBlock(); - leafValueCount = 0; - } - - if (DEBUG) - if ((lastRowID = rowID) < 0) - throw new AssertionError("row id must be >= 0; got " + rowID); - } - - /** - * Write a leaf block if we have unwritten values and return the total number of values added - */ - public long finish() throws IOException - { - if (leafValueCount > 0) - writeLeafBlock(); - - return valueCount; - } - - private void writeLeafBlock() throws IOException - { - assert leafValueCount != 0; - if (valueCount == 0) - { - System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue); - } - System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue); - - valueCount += leafValueCount; - - if (leafBlockFilePointers.size() > 0) - { - // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end: - leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue)); - } - leafBlockFilePointers.add(treeOutput.getFilePointer()); - checkMaxLeafNodeCount(leafBlockFilePointers.size()); - - // Find the common prefix between the first and last values in the block - int commonPrefixLength = bytesPerValue; - int offset = (leafValueCount - 1) * bytesPerValue; - for (int j = 0; j < bytesPerValue; j++) - { - if (leafValues[j] != leafValues[offset + j]) - { - commonPrefixLength = j; - break; - } - } - - treeOutput.writeVInt(leafValueCount); - - for (int x = 0; x < leafValueCount; x++) - { - rowIDAndIndexes[x].valueOrderIndex = x; - rowIDAndIndexes[x].rowID = leafRowIDs[x]; - } - - final Sorter sorter = new IntroSorter() - { - RowIDAndIndex pivot; - - @Override - protected void swap(int i, int j) - { - RowIDAndIndex o = rowIDAndIndexes[i]; - rowIDAndIndexes[i] = rowIDAndIndexes[j]; - rowIDAndIndexes[j] = o; - } - - @Override - protected void setPivot(int i) - { - pivot = rowIDAndIndexes[i]; - } - - @Override - protected int comparePivot(int j) - { - return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID); - } - }; - - sorter.sort(0, leafValueCount); - - // write the leaf order index: leaf rowID -> orig index - leafOrderIndexOutput.reset(); - - // iterate in row ID order to get the row ID index for the given value order index - // place into an array to be written as packed ints - for (int x = 0; x < leafValueCount; x++) - orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x; - - LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, leafOrderIndexOutput); - - treeOutput.writeVInt((int) leafOrderIndexOutput.size()); - leafOrderIndexOutput.copyTo(treeOutput); - - callback.writeLeafPostings(rowIDAndIndexes, 0, leafValueCount); - - // Write the common prefix for the leaf block - writeCommonPrefix(treeOutput, commonPrefixLength); - - // Write the run length encoded packed values for the leaf block - leafBlockOutput.reset(); - - if (DEBUG) - valuesInOrderAndBounds(leafValueCount, - ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue), - ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue), - leafRowIDs); - - writeLeafBlockPackedValues(leafBlockOutput, commonPrefixLength, leafValueCount); - - leafBlockOutput.copyTo(treeOutput); - } - - private void checkMaxLeafNodeCount(int numLeaves) - { - if (bytesPerValue * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH) - { - throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex"); - } - } - - private void writeCommonPrefix(DataOutput treeOutput, int commonPrefixLength) throws IOException - { - treeOutput.writeVInt(commonPrefixLength); - if (commonPrefixLength > 0) - treeOutput.writeBytes(leafValues, 0, commonPrefixLength); - } - - private void writeLeafBlockPackedValues(DataOutput out, int commonPrefixLength, int count) throws IOException - { - // If all the values are the same (e.g. the common prefix length == bytes per value) then we don't - // need to write anything. Otherwise, we run length compress the values to disk. - if (commonPrefixLength != bytesPerValue) - { - int compressedByteOffset = commonPrefixLength; - commonPrefixLength++; - for (int i = 0; i < count; ) - { - // do run-length compression on the byte at compressedByteOffset - int runLen = runLen(i, Math.min(i + 0xff, count), compressedByteOffset); - assert runLen <= 0xff; - byte prefixByte = leafValues[i * bytesPerValue + compressedByteOffset]; - out.writeByte(prefixByte); - out.writeByte((byte) runLen); - writeLeafBlockPackedValuesRange(out, commonPrefixLength, i, i + runLen); - i += runLen; - assert i <= count; - } - } - } - - private void writeLeafBlockPackedValuesRange(DataOutput out, int commonPrefixLength, int start, int end) throws IOException - { - for (int i = start; i < end; ++i) - { - out.writeBytes(leafValues, i * bytesPerValue + commonPrefixLength, bytesPerValue - commonPrefixLength); - } - } - - private int runLen(int start, int end, int byteOffset) - { - byte b = leafValues[start * bytesPerValue + byteOffset]; - for (int i = start + 1; i < end; ++i) - { - byte b2 = leafValues[i * bytesPerValue + byteOffset]; - assert Byte.toUnsignedInt(b2) >= Byte.toUnsignedInt(b); - if (b != b2) - { - return i - start; - } - } - return end - start; - } - - // The following 3 methods are only used when DEBUG is true: - - private void valueInBounds(byte[] packedValues, int packedValueOffset, byte[] minPackedValue, byte[] maxPackedValue) - { - if (ByteArrayUtil.compareUnsigned(packedValues, - packedValueOffset, - minPackedValue, - 0, - bytesPerValue) < 0) - { - throw new AssertionError("value=" + new BytesRef(packedValues, packedValueOffset, bytesPerValue) + - " is < minPackedValue=" + new BytesRef(minPackedValue)); - } - - if (ByteArrayUtil.compareUnsigned(packedValues, - packedValueOffset, - maxPackedValue, 0, - bytesPerValue) > 0) - { - throw new AssertionError("value=" + new BytesRef(packedValues, packedValueOffset, bytesPerValue) + - " is > maxPackedValue=" + new BytesRef(maxPackedValue)); - } - } - - private void valuesInOrderAndBounds(int count, byte[] minPackedValue, byte[] maxPackedValue, long[] rowIds) - { - byte[] lastPackedValue = new byte[bytesPerValue]; - long lastRowId = -1; - for (int i = 0; i < count; i++) - { - valueInOrder(i, lastPackedValue, leafValues, i * bytesPerValue, rowIds[i], lastRowId); - lastRowId = rowIds[i]; - - // Make sure this value does in fact fall within this leaf cell: - valueInBounds(leafValues, i * bytesPerValue, minPackedValue, maxPackedValue); - } - } - - private void valueInOrder(long ord, byte[] lastPackedValue, byte[] packedValues, int packedValueOffset, long rowId, long lastRowId) - { - if (ord > 0) - { - int cmp = ByteArrayUtil.compareUnsigned(lastPackedValue, 0, packedValues, packedValueOffset, bytesPerValue); - if (cmp > 0) - { - throw new AssertionError("values out of order: last value=" + new BytesRef(lastPackedValue) + - " current value=" + new BytesRef(packedValues, packedValueOffset, bytesPerValue) + - " ord=" + ord); - } - if (cmp == 0 && rowId < lastRowId) - { - throw new AssertionError("row IDs out of order: last rowID=" + lastRowId + " current rowID=" + rowId + " ord=" + ord); - } - } - System.arraycopy(packedValues, packedValueOffset, lastPackedValue, 0, bytesPerValue); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/LeafOrderMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/LeafOrderMap.java deleted file mode 100644 index 8fd5bf3dc2b7..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/LeafOrderMap.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.IOException; - -import org.apache.lucene.store.DataOutput; -import org.apache.lucene.util.packed.DirectWriter; - -class LeafOrderMap -{ - static void write(final int[] array, int length, int maxValue, final DataOutput out) throws IOException - { - final int bits = DirectWriter.unsignedBitsRequired(maxValue); - final DirectWriter writer = DirectWriter.getInstance(out, length, bits); - for (int i = 0; i < length; i++) - { - assert array[i] <= maxValue; - - writer.add(array[i]); - } - writer.finish(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java deleted file mode 100644 index aedf64be1f71..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.bbtree; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; - -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.v1.segment.SegmentWriter; -import org.apache.cassandra.index.sai.utils.IndexEntry; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.util.packed.PackedInts; -import org.apache.lucene.util.packed.PackedLongValues; - -/** - * Specialized writer for values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary - * posting lists on eligible tree levels. - *

    - * Given a sorted input, the flush process is optimised because we don't need to buffer all point values to sort them. - */ -public class NumericIndexWriter implements SegmentWriter -{ - public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE; - private static final int DEFAULT_POSTINGS_SIZE = 128; - - private final BlockBalancedTreeWriter writer; - private final IndexDescriptor indexDescriptor; - private final IndexIdentifier indexIdentifier; - private final int bytesPerValue; - - public NumericIndexWriter(IndexDescriptor indexDescriptor, - IndexIdentifier indexIdentifier, - int bytesPerValue) - { - this(indexDescriptor, indexIdentifier, MAX_POINTS_IN_LEAF_NODE, bytesPerValue); - } - - @VisibleForTesting - public NumericIndexWriter(IndexDescriptor indexDescriptor, - IndexIdentifier indexIdentifier, - int maxPointsInLeafNode, - int bytesPerValue) - { - this.indexDescriptor = indexDescriptor; - this.indexIdentifier = indexIdentifier; - this.bytesPerValue = bytesPerValue; - this.writer = new BlockBalancedTreeWriter(bytesPerValue, maxPointsInLeafNode); - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this).add("indexName", indexIdentifier).add("bytesPerValue", bytesPerValue).toString(); - } - - private static class LeafCallback implements BlockBalancedTreeWriter.Callback - { - final List leafPostings = new ArrayList<>(DEFAULT_POSTINGS_SIZE); - - public int numLeaves() - { - return leafPostings.size(); - } - - @Override - public void writeLeafPostings(BlockBalancedTreeWriter.RowIDAndIndex[] leafPostings, int offset, int count) - { - PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT); - - for (int i = offset; i < count; ++i) - { - builder.add(leafPostings[i].rowID); - } - this.leafPostings.add(builder.build()); - } - } - - @Override - public SegmentMetadata.ComponentMetadataMap writeCompleteSegment(Iterator iterator) throws IOException - { - long treePosition; - - SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap(); - - LeafCallback leafCallback = new LeafCallback(); - - try (IndexOutput treeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexIdentifier, true)) - { - // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer. - long treeOffset = treeOutput.getFilePointer(); - - treePosition = writer.write(treeOutput, iterator, leafCallback); - - // If the treePosition is less than 0 then we didn't write any values out and the index is empty - if (treePosition < 0) - return components; - - long treeLength = treeOutput.getFilePointer() - treeOffset; - - Map attributes = new LinkedHashMap<>(); - attributes.put("max_points_in_leaf_node", Integer.toString(writer.getMaxPointsInLeafNode())); - attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves())); - attributes.put("num_values", Long.toString(writer.getValueCount())); - attributes.put("bytes_per_value", Long.toString(writer.getBytesPerValue())); - - components.put(IndexComponent.BALANCED_TREE, treePosition, treeOffset, treeLength, attributes); - } - - try (BlockBalancedTreeWalker reader = new BlockBalancedTreeWalker(indexDescriptor.createPerIndexFileHandle(IndexComponent.BALANCED_TREE, - indexIdentifier, - null), - treePosition); - IndexOutputWriter postingsOutput = indexDescriptor.openPerIndexOutput(IndexComponent.POSTING_LISTS, indexIdentifier, true)) - { - long postingsOffset = postingsOutput.getFilePointer(); - - BlockBalancedTreePostingsWriter postingsWriter = new BlockBalancedTreePostingsWriter(); - reader.traverse(postingsWriter); - - // The balanced tree postings writer already writes its own header & footer. - long postingsPosition = postingsWriter.finish(postingsOutput, leafCallback.leafPostings, indexIdentifier); - - Map attributes = new LinkedHashMap<>(); - attributes.put("num_leaf_postings", Integer.toString(postingsWriter.numLeafPostings)); - attributes.put("num_non_leaf_postings", Integer.toString(postingsWriter.numNonLeafPostings)); - - long postingsLength = postingsOutput.getFilePointer() - postingsOffset; - components.put(IndexComponent.POSTING_LISTS, postingsPosition, postingsOffset, postingsLength, attributes); - } - - return components; - } - - @Override - public long getNumberOfRows() - { - return writer.getValueCount(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java index 33f23acd15dd..b43b7f1611b2 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java @@ -17,75 +17,104 @@ */ package org.apache.cassandra.index.sai.disk.v1.bitpack; -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput; +import com.carrotsearch.hppc.IntObjectHashMap; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; import org.apache.cassandra.index.sai.disk.v1.LongArray; -import org.apache.lucene.store.IndexInput; +import org.apache.cassandra.index.sai.utils.SeekingRandomAccessInput; import org.apache.lucene.util.LongValues; -import org.apache.lucene.util.packed.DirectReader; -@NotThreadSafe public abstract class AbstractBlockPackedReader implements LongArray { private final int blockShift; private final int blockMask; + private final int blockSize; private final long valueCount; - private final byte[] blockBitsPerValue; + final byte[] blockBitsPerValue; // package protected for test access private final SeekingRandomAccessInput input; + private final IntObjectHashMap readers; - private long previousValue = Long.MIN_VALUE; + private long prevTokenValue = Long.MIN_VALUE; private long lastIndex; // the last index visited by token -> row ID searches - AbstractBlockPackedReader(IndexInput indexInput, byte[] blockBitsPerValue, int blockShift, int blockMask, long valueCount) + AbstractBlockPackedReader(IndexInput indexInput, byte[] blockBitsPerValue, int blockShift, int blockMask, long sstableRowId, long valueCount) { this.blockShift = blockShift; this.blockMask = blockMask; + this.blockSize = blockMask + 1; this.valueCount = valueCount; this.input = new SeekingRandomAccessInput(indexInput); this.blockBitsPerValue = blockBitsPerValue; + this.readers = new IntObjectHashMap<>(); + // start searching tokens from current index segment + this.lastIndex = sstableRowId; } protected abstract long blockOffsetAt(int block); @Override - public long get(final long valueIndex) + public long get(final long index) { - if (valueIndex < 0 || valueIndex >= valueCount) + if (index < 0 || index >= valueCount) { - throw new IndexOutOfBoundsException(String.format("Index should be between [0, %d), but was %d.", valueCount, valueIndex)); + throw new IndexOutOfBoundsException(String.format("Index should be between [0, %d), but was %d.", valueCount, index)); } - int blockIndex = (int) (valueIndex >>> blockShift); - int inBlockIndex = (int) (valueIndex & blockMask); - byte bitsPerValue = blockBitsPerValue[blockIndex]; - final LongValues subReader = bitsPerValue == 0 ? LongValues.ZEROES - : DirectReader.getInstance(input, bitsPerValue, blockOffsetAt(blockIndex)); - return delta(blockIndex, inBlockIndex) + subReader.get(inBlockIndex); + final int block = (int) (index >>> blockShift); + final int idx = (int) (index & blockMask); + return delta(block, idx) + getReader(block).get(idx); } - @Override - public long length() + private LongValues getReader(int block) { - return valueCount; + LongValues reader = readers.get(block); + if (reader == null) + { + reader = blockBitsPerValue[block] == 0 ? LongValues.ZEROES + : LuceneCompat.directReaderGetInstance(input, blockBitsPerValue[block], blockOffsetAt(block)); + readers.put(block, reader); + } + return reader; } @Override - public long indexOf(long value) + public long ceilingIndex(long targetValue) { - // If we are searching backwards, we need to reset the lastIndex. This is not normal since we normally move - // forwards when searching for tokens. We only (may) search backwards in vector searchs where we need the - // primary key ranges presented as row IDs. - if (value < previousValue) - lastIndex = 0; - // already out of range - if (lastIndex >= valueCount) + if (isOutOfRangeState()) return -1; - previousValue = value; + long index = findBlockIndex(targetValue); + lastIndex = index >= 0 ? index : -index - 1; + return isOutOfRangeState() ? -1 : lastIndex; + } + + @Override + public long indexOf(long targetValue) + { + // already out of range + if (isOutOfRangeState()) + return Long.MIN_VALUE; - int blockIndex = binarySearchBlockMinValues(value); + long index = findBlockIndex(targetValue); + lastIndex = index >= 0 ? index : -index - 1; + return isOutOfRangeState() ? Long.MIN_VALUE : index; + } + + private boolean isOutOfRangeState() + { + return lastIndex >= valueCount; + } + + private long findBlockIndex(long targetValue) + { + // We keep track previous returned value in lastIndex, so searching backward will not return correct result. + // Also it's logically wrong to search backward during token iteration in PostingListKeyRangeIterator. + if (targetValue < prevTokenValue) + throw new IllegalArgumentException(String.format("%d is smaller than prev token value %d", targetValue, prevTokenValue)); + prevTokenValue = targetValue; + + int blockIndex = binarySearchBlockMinValues(targetValue); // We need to check next block's min value on an exact match. boolean exactMatch = blockIndex >= 0; @@ -107,8 +136,7 @@ public long indexOf(long value) } // Find the global (not block-specific) index of the target token, which is equivalent to its row ID: - lastIndex = findBlockRowID(value, blockIndex, exactMatch); - return lastIndex >= valueCount ? -1 : lastIndex; + return findBlockIndex(targetValue, blockIndex, exactMatch); } /** @@ -178,7 +206,7 @@ else if (midVal > targetValue) return -low; // no exact match found } - private long findBlockRowID(long targetValue, long blockIdx, boolean exactMatch) + private long findBlockIndex(long targetValue, long blockIdx, boolean exactMatch) { // Calculate the global offset for the selected block: long offset = blockIdx << blockShift; @@ -187,7 +215,7 @@ private long findBlockRowID(long targetValue, long blockIdx, boolean exactMatch) long low = Math.max(lastIndex, offset); // The high is either the last local index in the block, or something smaller if the block isn't full: - long high = Math.min(offset + blockMask + (exactMatch ? 1 : 0), valueCount - 1); + long high = Math.min(offset + blockSize - 1 + (exactMatch ? 1 : 0), valueCount - 1); return binarySearchBlock(targetValue, low, high); } @@ -195,7 +223,7 @@ private long findBlockRowID(long targetValue, long blockIdx, boolean exactMatch) /** * binary search target value between low and high. * - * @return index if exact match is found, or *positive* insertion point if no exact match is found. + * @return index if exact match is found, or `-(insertion point) - 1` if no exact match is found. */ private long binarySearchBlock(long target, long low, long high) { @@ -232,7 +260,13 @@ else if (midVal > target) } // target not found - return low; + return -(low + 1); + } + + @Override + public long length() + { + return valueCount; } abstract long delta(int block, int idx); diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java index 767217d88f01..e2f233525df8 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java @@ -19,11 +19,13 @@ import java.io.IOException; -import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput; -import org.apache.lucene.store.IndexOutput; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.oldlucene.DirectWriterAdapter; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; +import org.apache.cassandra.index.sai.disk.oldlucene.ResettableByteBuffersIndexOutput; import org.apache.lucene.util.packed.DirectWriter; -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.checkBlockSize; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.checkBlockSize; /** * Modified copy of {@code org.apache.lucene.util.packed.AbstractBlockPackedWriter} to use {@link DirectWriter} for @@ -33,25 +35,31 @@ public abstract class AbstractBlockPackedWriter { static final int MIN_BLOCK_SIZE = 64; static final int MAX_BLOCK_SIZE = 1 << (30 - 3); + static final int MIN_VALUE_EQUALS_0 = 1; + static final int BPV_SHIFT = 1; - protected final IndexOutput indexOutput; - protected final long[] blockValues; - // This collects metadata specific to the block packed writer being used during the - // writing of the block packed data. This cached metadata is then written to the end - // of the data file when the block packed writer is finished. - protected final ResettableByteBuffersIndexOutput blockMetaWriter; - - protected int blockIndex; + protected final IndexOutput out; + protected final long[] values; + protected int off; protected boolean finished; + + final ResettableByteBuffersIndexOutput blockMetaWriter; - AbstractBlockPackedWriter(IndexOutput indexOutput, int blockSize) + AbstractBlockPackedWriter(IndexOutput out, int blockSize) { checkBlockSize(blockSize, MIN_BLOCK_SIZE, MAX_BLOCK_SIZE); - this.indexOutput = indexOutput; - this.blockMetaWriter = new ResettableByteBuffersIndexOutput(blockSize, "BlockPackedMeta"); - blockValues = new long[blockSize]; + this.out = out; + this.blockMetaWriter = LuceneCompat.getResettableByteBuffersIndexOutput(out.order(), 1024, "NumericValuesMeta", out.version()); + values = new long[blockSize]; } + private void checkNotFinished() + { + if (finished) + { + throw new IllegalStateException(String.format("[%s] Writer already finished!", out.getName())); + } + } /** * Append a new long. @@ -59,13 +67,14 @@ public abstract class AbstractBlockPackedWriter public void add(long l) throws IOException { checkNotFinished(); - if (blockIndex == blockValues.length) + if (off == values.length) { flush(); } - blockValues[blockIndex++] = l; + values[off++] = l; } + /** * Flush all buffered data to disk. This instance is not usable anymore * after this method has been called. @@ -75,24 +84,24 @@ public void add(long l) throws IOException public long finish() throws IOException { checkNotFinished(); - if (blockIndex > 0) + if (off > 0) { flush(); } - final long fp = indexOutput.getFilePointer(); - blockMetaWriter.copyTo(indexOutput); + final long fp = out.getFilePointer(); + blockMetaWriter.copyTo(out); finished = true; return fp; } - protected abstract void flushBlock() throws IOException; + protected abstract void flush() throws IOException; void writeValues(int numValues, int bitsPerValue) throws IOException { - final DirectWriter writer = DirectWriter.getInstance(indexOutput, numValues, bitsPerValue); + final DirectWriterAdapter writer = LuceneCompat.directWriterGetInstance(out.order(), out, numValues, bitsPerValue); for (int i = 0; i < numValues; ++i) { - writer.add(blockValues[i]); + writer.add(values[i]); } writer.finish(); } @@ -107,18 +116,4 @@ void writeVLong(IndexOutput out, long i) throws IOException } out.writeByte((byte) i); } - - private void flush() throws IOException - { - flushBlock(); - blockIndex = 0; - } - - private void checkNotFinished() - { - if (finished) - { - throw new IllegalStateException(String.format("[%s] Writer already finished!", indexOutput.getName())); - } - } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java index 50ce53d56cfa..50eb79ad43f5 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java @@ -19,18 +19,18 @@ import java.io.IOException; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; import org.apache.cassandra.index.sai.disk.io.IndexInputReader; -import org.apache.cassandra.index.sai.disk.v1.DirectReaders; import org.apache.cassandra.index.sai.disk.v1.LongArray; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.RandomAccessReader; -import org.apache.lucene.store.IndexInput; +import org.apache.lucene.index.CorruptIndexException; -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.checkBlockSize; -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.numBlocks; -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.readVLong; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.checkBlockSize; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.numBlocks; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.readVLong; import static org.apache.lucene.util.BitUtil.zigZagDecode; /** @@ -46,6 +46,7 @@ public class BlockPackedReader implements LongArray.Factory private final long[] blockOffsets; private final long[] minValues; + @SuppressWarnings("resource") public BlockPackedReader(FileHandle file, NumericValuesMeta meta) throws IOException { this.file = file; @@ -54,13 +55,12 @@ public BlockPackedReader(FileHandle file, NumericValuesMeta meta) throws IOExcep blockShift = checkBlockSize(meta.blockSize, AbstractBlockPackedWriter.MIN_BLOCK_SIZE, AbstractBlockPackedWriter.MAX_BLOCK_SIZE); blockMask = meta.blockSize - 1; - int numBlocks = numBlocks(valueCount, meta.blockSize); + final int numBlocks = numBlocks(valueCount, meta.blockSize); blockBitsPerValue = new byte[numBlocks]; blockOffsets = new long[numBlocks]; minValues = new long[numBlocks]; - try (RandomAccessReader reader = this.file.createReader(); - IndexInputReader in = IndexInputReader.create(reader)) + try (final IndexInputReader in = IndexInputReader.create(this.file.createReader())) { SAICodecUtils.validate(in); in.seek(meta.blockMetaOffset); @@ -68,10 +68,12 @@ public BlockPackedReader(FileHandle file, NumericValuesMeta meta) throws IOExcep for (int i = 0; i < numBlocks; ++i) { final int token = in.readByte() & 0xFF; - final int bitsPerValue = token >>> BlockPackedWriter.BPV_SHIFT; - int blockIndex = i; - DirectReaders.checkBitsPerValue(bitsPerValue, in, () -> String.format("Block %d", blockIndex)); - if ((token & BlockPackedWriter.MIN_VALUE_EQUALS_0) == 0) + final int bitsPerValue = token >>> AbstractBlockPackedWriter.BPV_SHIFT; + if (bitsPerValue > 64) + { + throw new CorruptIndexException(String.format("Block %d is corrupted. Bits per value should be no more than 64 and is %d.", i, bitsPerValue), in); + } + if ((token & AbstractBlockPackedWriter.MIN_VALUE_EQUALS_0) == 0) { long val = zigZagDecode(1L + readVLong(in)); minValues[i] = val; @@ -95,11 +97,12 @@ public BlockPackedReader(FileHandle file, NumericValuesMeta meta) throws IOExcep } } + @VisibleForTesting @Override public LongArray open() { - IndexInput indexInput = IndexFileUtils.instance.openInput(file); - return new AbstractBlockPackedReader(indexInput, blockBitsPerValue, blockShift, blockMask, valueCount) + var indexInput = IndexFileUtils.instance().openInput(file); + return new AbstractBlockPackedReader(indexInput, blockBitsPerValue, blockShift, blockMask, 0, valueCount) { @Override protected long blockOffsetAt(int block) diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java index 9dcc29c6ab62..8c570932370c 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java @@ -19,45 +19,46 @@ import java.io.IOException; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.util.packed.DirectWriter; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.oldlucene.DirectWriterAdapter; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; import static org.apache.lucene.util.BitUtil.zigZagEncode; /** * A writer for large sequences of longs. * - * Modified copy of {@link org.apache.lucene.util.packed.BlockPackedWriter} to use {@link DirectWriter} + * Modified copy of {@link org.apache.lucene.util.packed.BlockPackedWriter} to use {@link DirectWriterAdapter} * for optimised reads that doesn't require seeking through the whole file to open a thread-exclusive reader. */ public class BlockPackedWriter extends AbstractBlockPackedWriter { - static final int BPV_SHIFT = 1; - static final int MIN_VALUE_EQUALS_0 = 1; - public BlockPackedWriter(IndexOutput out, int blockSize) { super(out, blockSize); } @Override - protected void flushBlock() throws IOException + protected void flush() throws IOException { + assert off > 0; long min = Long.MAX_VALUE, max = Long.MIN_VALUE; - for (int i = 0; i < blockIndex; ++i) + for (int i = 0; i < off; ++i) { - min = Math.min(blockValues[i], min); - max = Math.max(blockValues[i], max); + min = Math.min(values[i], min); + max = Math.max(values[i], max); } - long delta = max - min; - int bitsRequired = delta == 0 ? 0 : DirectWriter.unsignedBitsRequired(delta); + final long delta = max - min; + int bitsRequired = delta == 0 ? 0 : LuceneCompat.directWriterUnsignedBitsRequired(out.order(), delta); - int shiftedBitsRequired = (bitsRequired << BPV_SHIFT) | (min == 0 ? MIN_VALUE_EQUALS_0 : 0); - blockMetaWriter.writeByte((byte) shiftedBitsRequired); + final int token = (bitsRequired << BPV_SHIFT) | (min == 0 ? MIN_VALUE_EQUALS_0 : 0); + blockMetaWriter.writeByte((byte) token); if (min != 0) { + // TODO: the min values can be delta encoded since they are read linearly + // TODO: buffer the min values so they may be written as a single block writeVLong(blockMetaWriter, zigZagEncode(min) - 1); } @@ -65,13 +66,15 @@ protected void flushBlock() throws IOException { if (min != 0) { - for (int i = 0; i < blockIndex; ++i) + for (int i = 0; i < off; ++i) { - blockValues[i] -= min; + values[i] -= min; } } - blockMetaWriter.writeVLong(indexOutput.getFilePointer()); - writeValues(blockIndex, bitsRequired); + blockMetaWriter.writeVLong(out.getFilePointer()); + writeValues(off, bitsRequired); } + + off = 0; } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java index 02071b80525e..b606cb7fd146 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java @@ -21,17 +21,15 @@ import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; import org.apache.cassandra.index.sai.disk.io.IndexInputReader; -import org.apache.cassandra.index.sai.disk.v1.DirectReaders; import org.apache.cassandra.index.sai.disk.v1.LongArray; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.RandomAccessReader; -import org.apache.lucene.store.IndexInput; +import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.util.packed.PackedInts; import org.apache.lucene.util.packed.PackedLongValues; -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.checkBlockSize; -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.numBlocks; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.checkBlockSize; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.numBlocks; /** * Provides non-blocking, random access to a stream written with {@link MonotonicBlockPackedWriter}. @@ -59,8 +57,7 @@ public MonotonicBlockPackedReader(FileHandle file, NumericValuesMeta meta) throw blockBitsPerValue = new byte[numBlocks]; this.file = file; - try (RandomAccessReader reader = this.file.createReader(); - IndexInputReader in = IndexInputReader.create(reader)) + try (final IndexInputReader in = IndexInputReader.create(this.file.createReader())) { SAICodecUtils.validate(in); @@ -70,7 +67,10 @@ public MonotonicBlockPackedReader(FileHandle file, NumericValuesMeta meta) throw minValuesBuilder.add(in.readZLong()); averages[i] = Float.intBitsToFloat(in.readInt()); final int bitsPerValue = in.readVInt(); - DirectReaders.checkBitsPerValue(bitsPerValue, in, () -> "Postings list header"); + if (bitsPerValue > 64) + { + throw new CorruptIndexException(String.format("Block %d is corrupted. Bits per value should be no more than 64 and is %d.", i, bitsPerValue), in); + } blockBitsPerValue[i] = (byte) bitsPerValue; // when bitsPerValue is 0, block offset won't be used blockOffsetsBuilder.add(bitsPerValue == 0 ? -1 : in.readVLong()); @@ -84,8 +84,8 @@ public MonotonicBlockPackedReader(FileHandle file, NumericValuesMeta meta) throw @Override public LongArray open() { - final IndexInput indexInput = IndexFileUtils.instance.openInput(file); - return new AbstractBlockPackedReader(indexInput, blockBitsPerValue, blockShift, blockMask, valueCount) + var indexInput = IndexFileUtils.instance().openInput(file); + return new AbstractBlockPackedReader(indexInput, blockBitsPerValue, blockShift, blockMask, 0, valueCount) { @Override long delta(int block, int idx) @@ -94,7 +94,7 @@ long delta(int block, int idx) } @Override - public void close() throws IOException + public void close() { indexInput.close(); } @@ -106,7 +106,13 @@ protected long blockOffsetAt(int block) } @Override - public long indexOf(long value) + public long ceilingIndex(long targetValue) + { + throw new UnsupportedOperationException(); + } + + @Override + public long indexOf(long targetValue) { throw new UnsupportedOperationException(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java index 5845aabc77ca..316118d53673 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java @@ -19,17 +19,12 @@ import java.io.IOException; -import org.apache.lucene.store.IndexOutput; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; import org.apache.lucene.util.packed.DirectWriter; /** * A writer for large monotonically increasing sequences of positive longs. * - * The writer is optimised for monotonic sequences and stores values as a series of deltas - * from an expected value. The expected value is calculated from the minimum value in the block and the average - * delta for the block. This means that stored values are generally smaller and can be packed - * into a smaller number of bits, allowing for larger block sizes. - * * Modified copy of {@link org.apache.lucene.util.packed.MonotonicBlockPackedWriter} to use {@link DirectWriter} for * optimised reads that doesn't require seeking through the whole file to open a thread-exclusive reader. */ @@ -48,30 +43,32 @@ public void add(long l) throws IOException } @Override - protected void flushBlock() throws IOException + protected void flush() throws IOException { - final float averageDelta = blockIndex == 1 ? 0f : (float) (blockValues[blockIndex - 1] - blockValues[0]) / (blockIndex - 1); - long minimumValue = blockValues[0]; - // adjust minimumValue so that all deltas will be positive - for (int index = 1; index < blockIndex; ++index) + assert off > 0; + + final float avg = off == 1 ? 0f : (float) (values[off - 1] - values[0]) / (off - 1); + long min = values[0]; + // adjust min so that all deltas will be positive + for (int i = 1; i < off; ++i) { - long actual = blockValues[index]; - long expected = MonotonicBlockPackedReader.expected(minimumValue, averageDelta, index); + final long actual = values[i]; + final long expected = MonotonicBlockPackedReader.expected(min, avg, i); if (expected > actual) { - minimumValue -= (expected - actual); + min -= (expected - actual); } } long maxDelta = 0; - for (int i = 0; i < blockIndex; ++i) + for (int i = 0; i < off; ++i) { - blockValues[i] = blockValues[i] - MonotonicBlockPackedReader.expected(minimumValue, averageDelta, i); - maxDelta = Math.max(maxDelta, blockValues[i]); + values[i] = values[i] - MonotonicBlockPackedReader.expected(min, avg, i); + maxDelta = Math.max(maxDelta, values[i]); } - blockMetaWriter.writeZLong(minimumValue); - blockMetaWriter.writeInt(Float.floatToIntBits(averageDelta)); + blockMetaWriter.writeZLong(min); + blockMetaWriter.writeInt(Float.floatToIntBits(avg)); if (maxDelta == 0) { blockMetaWriter.writeVInt(0); @@ -80,8 +77,10 @@ protected void flushBlock() throws IOException { final int bitsRequired = DirectWriter.bitsRequired(maxDelta); blockMetaWriter.writeVInt(bitsRequired); - blockMetaWriter.writeVLong(indexOutput.getFilePointer()); - writeValues(blockIndex, bitsRequired); + blockMetaWriter.writeVLong(out.getFilePointer()); + writeValues(off, bitsRequired); } + + off = 0; } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java index 62da0292fb8d..cc9a0b0db926 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java @@ -19,7 +19,7 @@ import java.io.IOException; -import org.apache.lucene.store.DataInput; +import org.apache.lucene.store.IndexInput; import org.apache.lucene.store.IndexOutput; public class NumericValuesMeta @@ -28,14 +28,21 @@ public class NumericValuesMeta public final int blockSize; public final long blockMetaOffset; - public NumericValuesMeta(DataInput input) throws IOException + public NumericValuesMeta(IndexInput input) throws IOException { valueCount = input.readLong(); blockSize = input.readInt(); blockMetaOffset = input.readVLong(); } - public static void write(IndexOutput out, long valueCount, int blockSize, long blockMetaOffset) throws IOException + public NumericValuesMeta(long valueCount, int blockSize, long blockMetaOffset) + { + this.valueCount = valueCount; + this.blockSize = blockSize; + this.blockMetaOffset = blockMetaOffset; + } + + public void write(IndexOutput out) throws IOException { out.writeLong(valueCount); out.writeInt(blockSize); diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java index 392146655d0c..d62355a2c32b 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java @@ -20,63 +20,64 @@ import java.io.Closeable; import java.io.IOException; -import javax.annotation.concurrent.NotThreadSafe; - +import org.apache.cassandra.index.sai.disk.io.IndexOutput; 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.v1.MetadataWriter; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.lucene.store.IndexOutput; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_NUMERIC_VALUES_BLOCK_SIZE; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_NUMERIC_VALUES_MONOTONIC_BLOCK_SIZE; + -@NotThreadSafe public class NumericValuesWriter implements Closeable { - public static final int MONOTONIC_BLOCK_SIZE = 16384; - public static final int BLOCK_SIZE = 128; + public static final int MONOTONIC_BLOCK_SIZE = SAI_NUMERIC_VALUES_MONOTONIC_BLOCK_SIZE.getInt(); + public static final int BLOCK_SIZE = SAI_NUMERIC_VALUES_BLOCK_SIZE.getInt(); - private final IndexOutput indexOutput; + private final IndexComponent.ForWrite components; + private final IndexOutput output; private final AbstractBlockPackedWriter writer; private final MetadataWriter metadataWriter; - private final String componentName; private final int blockSize; private long count = 0; - public NumericValuesWriter(IndexDescriptor indexDescriptor, - IndexComponent indexComponent, + public NumericValuesWriter(IndexComponent.ForWrite components, MetadataWriter metadataWriter, boolean monotonic) throws IOException { - this(indexDescriptor, indexComponent, metadataWriter, monotonic, monotonic ? MONOTONIC_BLOCK_SIZE : BLOCK_SIZE); + this(components, metadataWriter, monotonic, monotonic ? MONOTONIC_BLOCK_SIZE : BLOCK_SIZE); } - public NumericValuesWriter(IndexDescriptor indexDescriptor, - IndexComponent indexComponent, + public NumericValuesWriter(IndexComponent.ForWrite components, MetadataWriter metadataWriter, boolean monotonic, int blockSize) throws IOException { - this.componentName = indexDescriptor.componentName(indexComponent); - this.indexOutput = indexDescriptor.openPerSSTableOutput(indexComponent); - SAICodecUtils.writeHeader(indexOutput); - this.writer = monotonic ? new MonotonicBlockPackedWriter(indexOutput, blockSize) - : new BlockPackedWriter(indexOutput, blockSize); + this.components = components; + this.output = components.openOutput(); + SAICodecUtils.writeHeader(output); + + this.writer = monotonic ? new MonotonicBlockPackedWriter(output, blockSize) + : new BlockPackedWriter(output, blockSize); this.metadataWriter = metadataWriter; this.blockSize = blockSize; + } @Override public void close() throws IOException { - try (IndexOutput o = metadataWriter.builder(componentName)) + try (IndexOutput o = metadataWriter.builder(components.fileNamePart())) { - long fp = writer.finish(); - SAICodecUtils.writeFooter(indexOutput); + final long fp = writer.finish(); + SAICodecUtils.writeFooter(output); - NumericValuesMeta.write(o, count, blockSize, fp); + NumericValuesMeta meta = new NumericValuesMeta(count, blockSize, fp); + meta.write(o); } finally { - indexOutput.close(); + output.close(); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDPostingsIndex.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDPostingsIndex.java new file mode 100644 index 000000000000..5a6756a56959 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDPostingsIndex.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.IOException; + +import com.carrotsearch.hppc.IntLongHashMap; +import com.carrotsearch.hppc.IntLongMap; +import org.apache.cassandra.index.sai.disk.io.IndexInputReader; +import org.apache.cassandra.io.util.FileHandle; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.cassandra.index.sai.utils.SAICodecUtils.validate; + +/** + * Mapping between node ID and an offset to its auxiliary posting list (containing every row id from all leaves + * reachable from that node. See {@link OneDimBKDPostingsWriter}). + */ +class BKDPostingsIndex +{ + private final int size; + public final IntLongMap index = new IntLongHashMap(); + + @SuppressWarnings("resource") + BKDPostingsIndex(FileHandle postingsFileHandle, long filePosition) throws IOException + { + try (final IndexInputReader input = IndexInputReader.create(postingsFileHandle.createReader())) + { + validate(input); + input.seek(filePosition); + + size = input.readVInt(); + + for (int x = 0; x < size; x++) + { + final int node = input.readVInt(); + final long filePointer = input.readVLong(); + + index.put(node, filePointer); + } + } + } + + /** + * Returns true if given node ID has an auxiliary posting list. + */ + boolean exists(int nodeID) + { + checkArgument(nodeID > 0); + return index.containsKey(nodeID); + } + + /** + * Returns an offset within the bkd postings file to the begining of the blocks summary of given node's auxiliary + * posting list. + * + * @throws IllegalArgumentException when given nodeID doesn't have an auxiliary posting list. Check first with + * {@link #exists(int)} + */ + long getPostingsFilePointer(int nodeID) + { + checkArgument(exists(nodeID)); + return index.get(nodeID); + } + + int size() + { + return size; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDQueries.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDQueries.java new file mode 100644 index 000000000000..7b4b21f87ea8 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDQueries.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.lucene.index.PointValues.Relation; + +import static org.apache.lucene.index.PointValues.Relation.CELL_INSIDE_QUERY; + +public class BKDQueries +{ + private static final BKDReader.IntersectVisitor MATCH_ALL = new BKDReader.IntersectVisitor() + { + @Override + public boolean visit(byte[] packedValue) + { + return true; + } + + @Override + public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) + { + return CELL_INSIDE_QUERY; + } + }; + + public static BKDReader.IntersectVisitor bkdQueryFrom(Expression expression, int numDim, int bytesPerDim) + { + if (expression.lower == null && expression.upper == null) + { + return MATCH_ALL; + } + + Bound lower = null ; + if (expression.lower != null) + { + final byte[] lowerBound = toComparableBytes(numDim, bytesPerDim, expression.lower.value.encoded, expression.validator); + lower = new Bound(lowerBound, !expression.lower.inclusive); + } + + Bound upper = null; + if (expression.upper != null) + { + final byte[] upperBound = toComparableBytes(numDim, bytesPerDim, expression.upper.value.encoded, expression.validator); + upper = new Bound(upperBound, !expression.upper.inclusive); + } + + return new RangeQueryVisitor(numDim, bytesPerDim, lower, upper); + } + + private static byte[] toComparableBytes(int numDim, int bytesPerDim, ByteBuffer value, AbstractType type) + { + byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)]; + assert buffer.length == bytesPerDim * numDim; + TypeUtil.toComparableBytes(value, type, buffer); + return buffer; + } + + private static abstract class RangeQuery implements BKDReader.IntersectVisitor + { + final int numDims; + final int bytesPerDim; + + RangeQuery(int numDims, int bytesPerDim) + { + this.numDims = numDims; + this.bytesPerDim = bytesPerDim; + } + + int compareUnsigned(byte[] packedValue, int dim, Bound bound) + { + final int offset = dim * bytesPerDim; + return Arrays.compareUnsigned(packedValue, offset, offset + bytesPerDim, bound.bound, offset, offset + bytesPerDim); + } + } + + private static class Bound + { + private final byte[] bound; + private final boolean exclusive; + + Bound(byte[] bound, boolean exclusive) + { + this.bound = bound; + this.exclusive = exclusive; + } + + boolean smallerThan(int cmp) + { + return cmp > 0 || (cmp == 0 && exclusive); + } + + boolean greaterThan(int cmp) + { + return cmp < 0 || (cmp == 0 && exclusive); + } + } + + private static class RangeQueryVisitor extends RangeQuery + { + private final Bound lower; + private final Bound upper; + + private RangeQueryVisitor(int numDims, int bytesPerDim, Bound lower, Bound upper) + { + super(numDims, bytesPerDim); + this.lower = lower; + this.upper = upper; + } + + @Override + public boolean visit(byte[] packedValue) + { + for (int dim = 0; dim < numDims; dim++) + { + if (lower != null) + { + int cmp = compareUnsigned(packedValue, dim, lower); + if (lower.greaterThan(cmp)) + { + // value is too low, in this dimension + return false; + } + } + + if (upper != null) + { + int cmp = compareUnsigned(packedValue, dim, upper); + if (upper.smallerThan(cmp)) + { + // value is too high, in this dimension + return false; + } + } + } + + return true; + } + + @Override + public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) + { + boolean crosses = false; + + for (int dim = 0; dim < numDims; dim++) + { + if (lower != null) + { + int maxCmp = compareUnsigned(maxPackedValue, dim, lower); + if (lower.greaterThan(maxCmp)) + return Relation.CELL_OUTSIDE_QUERY; + + int minCmp = compareUnsigned(minPackedValue, dim, lower); + crosses |= lower.greaterThan(minCmp); + } + + if (upper != null) + { + int minCmp = compareUnsigned(minPackedValue, dim, upper); + if (upper.smallerThan(minCmp)) + return Relation.CELL_OUTSIDE_QUERY; + + int maxCmp = compareUnsigned(maxPackedValue, dim, upper); + crosses |= upper.smallerThan(maxCmp); + } + } + + if (crosses) + { + return Relation.CELL_CROSSES_QUERY; + } + else + { + return Relation.CELL_INSIDE_QUERY; + } + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDReader.java new file mode 100644 index 000000000000..055441b71145 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDReader.java @@ -0,0 +1,1008 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v1.kdtree; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; +import com.google.common.base.Stopwatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.io.CryptoUtils; +import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; +import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList; +import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList; +import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader; +import org.apache.cassandra.index.sai.metrics.QueryEventListener; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; +import org.apache.cassandra.index.sai.utils.SeekingRandomAccessInput; +import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.AbstractGuavaIterator; +import org.apache.cassandra.utils.Throwables; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.LongValues; + +/** + * Handles intersection of a multi-dimensional shape in byte[] space with a block KD-tree previously written with + * {@link BKDWriter}. + */ +public class BKDReader extends TraversingBKDReader implements Closeable +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public enum Direction { FORWARD, BACKWARD } + + private final IndexContext indexContext; + private final FileHandle postingsFile; + private final FileHandle kdtreeFile; + private final BKDPostingsIndex postingsIndex; + private final ICompressor compressor; + + /** + * Performs a blocking read. + */ + public BKDReader(IndexContext indexContext, + FileHandle kdtreeFile, + long bkdIndexRoot, + FileHandle postingsFile, + long bkdPostingsRoot) throws IOException + { + super(kdtreeFile, bkdIndexRoot); + this.indexContext = indexContext; + this.postingsFile = postingsFile; + this.kdtreeFile = kdtreeFile; + this.postingsIndex = new BKDPostingsIndex(postingsFile, bkdPostingsRoot); + this.compressor = null; + } + + public interface DocMapper + { + int oldToNew(int rowID); + } + + public IteratorState iteratorState(Direction direction, IntersectVisitor query) throws IOException + { + return new IteratorState(rowID -> rowID, direction, query); + } + + @VisibleForTesting + public IteratorState iteratorState() throws IOException + { + return iteratorState(Direction.FORWARD, null); + } + + public class IteratorState extends AbstractGuavaIterator implements Comparable, Closeable + { + public final byte[] scratch; + + private final IndexInput bkdInput; + private final IndexInput bkdPostingsInput; + private final byte[] packedValues = new byte[maxPointsInLeafNode * packedBytesLength]; + private final IntArrayList tempPostings = new IntArrayList(); + private final int[] postings = new int[maxPointsInLeafNode]; + private final DocMapper docMapper; + private final LeafCursor leafCursor; + + private int leafPointCount; + private int leafPointIndex = -1; + + private final Direction direction; + private final BKDReader.IntersectVisitor query; + + public IteratorState(DocMapper docMapper, Direction direction, BKDReader.IntersectVisitor query) throws IOException + { + this.docMapper = docMapper; + this.direction = direction; + this.query = query; + + scratch = new byte[packedBytesLength]; + + final long firstLeafFilePointer = getMinLeafBlockFP(); + bkdInput = IndexFileUtils.instance().openInput(kdtreeFile); + bkdPostingsInput = IndexFileUtils.instance().openInput(postingsFile); + bkdInput.seek(firstLeafFilePointer); + + leafCursor = new LeafCursor(direction, query); + leafPointCount = readLeaf(leafCursor.getFilePointer(), leafCursor.getNodeId(), bkdInput, packedValues, bkdPostingsInput, postings, tempPostings); + } + + @Override + public void close() + { + FileUtils.closeQuietly(bkdInput, bkdPostingsInput); + } + + @Override + public int compareTo(final IteratorState other) + { + final int cmp = Arrays.compareUnsigned(scratch, 0, packedBytesLength, other.scratch, 0, packedBytesLength); + if (cmp == 0) + { + final long rowid1 = next; + final long rowid2 = other.next; + return Long.compare(rowid1, rowid2); + } + return cmp; + } + + @Override + protected Integer computeNext() + { + while (true) + { + if (leafPointIndex == leafPointCount - 1) + { + if (!leafCursor.advance()) + return endOfData(); + + try + { + int id = leafCursor.getNodeId(); + long fp = leafCursor.getFilePointer(); + leafPointCount = readLeaf(fp, id, bkdInput, packedValues, bkdPostingsInput, postings, tempPostings); + } + catch (IOException e) + { + logger.error("Failed to read leaf during BKDTree merger", e); + throw new RuntimeException("Failed to read leaf during BKDTree merger", e); + } + leafPointIndex = -1; + } + + leafPointIndex++; + // If we're ascending, we need to read the leaf from the start, otherwise we need to read it from the end + int pointer = direction == Direction.FORWARD ? leafPointIndex : leafPointCount - leafPointIndex - 1; + + System.arraycopy(packedValues, pointer * packedBytesLength, scratch, 0, packedBytesLength); + if (query == null || query.visit(scratch)) + return docMapper.oldToNew(postings[pointer]); + } + } + } + + @SuppressWarnings("resource") + public int readLeaf(long filePointer, + int nodeID, + final IndexInput bkdInput, + final byte[] packedValues, + final IndexInput bkdPostingsInput, + int[] postings, + IntArrayList tempPostings) throws IOException + { + bkdInput.seek(filePointer); + final int count = bkdInput.readVInt(); + // loading doc ids occurred here prior + final int orderMapLength = bkdInput.readVInt(); + final long orderMapPointer = bkdInput.getFilePointer(); + + // order of the values in the posting list + final short[] origIndex = new short[maxPointsInLeafNode]; + + final int[] commonPrefixLengths = new int[numDims]; + final byte[] scratchPackedValue1 = new byte[packedBytesLength]; + + final SeekingRandomAccessInput randoInput = new SeekingRandomAccessInput(bkdInput); + LongValues orderMapReader = LuceneCompat.directReaderGetInstance(randoInput, bitsPerValue, orderMapPointer); + for (int x = 0; x < count; x++) + { + final short idx = LeafOrderMap.getValue(x, orderMapReader); + origIndex[x] = idx; + } + + IndexInput leafInput = bkdInput; + + // reused byte arrays for the decompression of leaf values + final BytesRef uncompBytes = new BytesRef(new byte[16]); + final BytesRef compBytes = new BytesRef(new byte[16]); + + // seek beyond the ordermap + leafInput.seek(orderMapPointer + orderMapLength); + + if (compressor != null) + { + // This should not throw WouldBlockException, even though we're on a TPC thread, because the + // secret key used by the underlying encryptor should be loaded at reader construction time. + leafInput = CryptoUtils.uncompress(bkdInput, compressor, compBytes, uncompBytes); + } + + final IntersectVisitor visitor = new IntersectVisitor() { + int i = 0; + + @Override + public boolean visit(byte[] packedValue) + { + System.arraycopy(packedValue, 0, packedValues, i * packedBytesLength, packedBytesLength); + i++; + return true; + } + + @Override + public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { + return Relation.CELL_CROSSES_QUERY; + } + }; + + visitDocValues(commonPrefixLengths, scratchPackedValue1, leafInput, count, visitor, null, origIndex); + + if (postingsIndex.exists(nodeID)) + { + final long pointer = postingsIndex.getPostingsFilePointer(nodeID); + final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(bkdPostingsInput, pointer); + final PostingsReader postingsReader = new PostingsReader(bkdPostingsInput, summary, QueryEventListener.PostingListEventListener.NO_OP); + + tempPostings.clear(); + + // gather the postings into tempPostings + while (true) + { + final int rowid = postingsReader.nextPosting(); + if (rowid == PostingList.END_OF_STREAM) break; + tempPostings.add(rowid); + } + + // put the postings into the array according the origIndex + for (int x = 0; x < tempPostings.size(); x++) + { + int idx = origIndex[x]; + final int rowid = tempPostings.get(idx); + + postings[x] = rowid; + } + } + else + { + throw new IllegalStateException(); + } + return count; + } + + @Override + public void close() + { + try + { + super.close(); + } + finally + { + FileUtils.closeQuietly(kdtreeFile, postingsFile); + } + } + + @SuppressWarnings("resource") + public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BKDIndexEventListener listener, QueryContext context) + { + Relation relation = visitor.compare(minPackedValue, maxPackedValue); + + if (relation == Relation.CELL_OUTSIDE_QUERY) + { + listener.onIntersectionEarlyExit(); + return PostingList.EMPTY; + } + + listener.onSegmentHit(); + IndexInput bkdInput = IndexFileUtils.instance().openInput(indexFile); + IndexInput postingsInput = IndexFileUtils.instance().openInput(postingsFile); + IndexInput postingsSummaryInput = IndexFileUtils.instance().openInput(postingsFile); + PackedIndexTree index = new PackedIndexTree(); + + Intersection completable = + relation == Relation.CELL_INSIDE_QUERY ? + new Intersection(bkdInput, postingsInput, postingsSummaryInput, index, listener, context) : + new FilteringIntersection(bkdInput, postingsInput, postingsSummaryInput, index, visitor, listener, context); + + return completable.execute(); + } + + /** + * Synchronous intersection of an multi-dimensional shape in byte[] space with a block KD-tree + * previously written with {@link BKDWriter}. + */ + class Intersection + { + private final Stopwatch queryExecutionTimer = Stopwatch.createStarted(); + final QueryContext context; + + final IndexInput bkdInput; + final SeekingRandomAccessInput bkdRandomInput; + final IndexInput postingsInput; + final IndexInput postingsSummaryInput; + final IndexTree index; + final QueryEventListener.BKDIndexEventListener listener; + + Intersection(IndexInput bkdInput, IndexInput postingsInput, IndexInput postingsSummaryInput, + IndexTree index, QueryEventListener.BKDIndexEventListener listener, QueryContext context) + { + this.bkdInput = bkdInput; + this.bkdRandomInput = new SeekingRandomAccessInput(bkdInput); + this.postingsInput = postingsInput; + this.postingsSummaryInput = postingsSummaryInput; + this.index = index; + this.listener = listener; + this.context = context; + } + + public PostingList execute() + { + try + { + var postingLists = new ArrayList(100); + executeInternal(postingLists); + + FileUtils.closeQuietly(bkdInput); + + return mergePostings(postingLists); + } + catch (Throwable t) + { + if (!(t instanceof AbortedOperationException)) + logger.error(indexContext.logMessage("kd-tree intersection failed on {}"), indexFile.path(), t); + + closeOnException(); + throw Throwables.cleaned(t); + } + } + + protected void executeInternal(final Collection postingLists) throws IOException + { + collectPostingLists(postingLists); + } + + protected void closeOnException() + { + FileUtils.closeQuietly(bkdInput, postingsInput, postingsSummaryInput); + } + + protected PostingList mergePostings(ArrayList postingLists) throws IOException + { + final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS); + + listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS); + listener.postingListsHit(postingLists.size()); + + if (!postingLists.isEmpty() && logger.isTraceEnabled()) + logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."), + indexFile.path(), elapsedMicros, postingLists.size()); + + return MergePostingList.merge(postingLists) + .onClose(() -> FileUtils.close(postingsInput, postingsSummaryInput)); + } + + public void collectPostingLists(Collection postingLists) throws IOException + { + context.checkpoint(); + + final int nodeID = index.getNodeID(); + + // if there is pre-built posting for entire subtree + if (postingsIndex.exists(nodeID)) + { + postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID))); + return; + } + + Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have kd-tree postings.", index.getNodeID()); + + // Recurse on left sub-tree: + index.pushLeft(); + collectPostingLists(postingLists); + index.pop(); + + // Recurse on right sub-tree: + index.pushRight(); + collectPostingLists(postingLists); + index.pop(); + } + + private PostingList initPostingReader(long offset) throws IOException + { + final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset); + return new PostingsReader(postingsInput, summary, listener.postingListEventListener()); + } + } + + /** + * Modified copy of BKDReader#visitDocValues() + */ + private int visitDocValues(int[] commonPrefixLengths, + byte[] scratchPackedValue1, + IndexInput in, + int count, + IntersectVisitor visitor, + FixedBitSet[] holder, + final short[] origIndex) throws IOException + { + readCommonPrefixes(commonPrefixLengths, scratchPackedValue1, in); + + int compressedDim = readCompressedDim(in); + if (compressedDim == -1) + { + return visitRawDocValues(commonPrefixLengths, scratchPackedValue1, in, count, visitor, holder, origIndex); + } + else + { + return visitCompressedDocValues(commonPrefixLengths, scratchPackedValue1, in, count, visitor, compressedDim, holder, origIndex); + } + } + + /** + * Modified copy of {@link org.apache.lucene.util.bkd.BKDReader#readCompressedDim(IndexInput)} + */ + @SuppressWarnings("JavadocReference") + private int readCompressedDim(IndexInput in) throws IOException + { + int compressedDim = in.readByte(); + if (compressedDim < -1 || compressedDim >= numDims) + { + throw new CorruptIndexException(String.format("Dimension should be in the range [-1, %d), but was %d.", numDims, compressedDim), in); + } + return compressedDim; + } + + /** + * Modified copy of BKDReader#visitCompressedDocValues() + */ + private int visitCompressedDocValues(int[] commonPrefixLengths, + byte[] scratchPackedValue, + IndexInput in, + int count, + IntersectVisitor visitor, + int compressedDim, + FixedBitSet[] holder, + final short[] origIndex) throws IOException + { + // the byte at `compressedByteOffset` is compressed using run-length compression, + // other suffix bytes are stored verbatim + final int compressedByteOffset = compressedDim * bytesPerDim + commonPrefixLengths[compressedDim]; + commonPrefixLengths[compressedDim]++; + int i, collected = 0; + + final FixedBitSet bitSet; + if (holder != null) + { + bitSet = new FixedBitSet(maxPointsInLeafNode); + } + else + { + bitSet = null; + } + + for (i = 0; i < count; ) + { + scratchPackedValue[compressedByteOffset] = in.readByte(); + final int runLen = Byte.toUnsignedInt(in.readByte()); + for (int j = 0; j < runLen; ++j) + { + for (int dim = 0; dim < numDims; dim++) + { + int prefix = commonPrefixLengths[dim]; + in.readBytes(scratchPackedValue, dim * bytesPerDim + prefix, bytesPerDim - prefix); + } + final int rowIDIndex = origIndex[i + j]; + if (visitor.visit(scratchPackedValue)) + { + if (bitSet != null) bitSet.set(rowIDIndex); + collected++; + } + } + i += runLen; + } + if (i != count) + { + throw new CorruptIndexException(String.format("Expected %d sub-blocks but read %d.", count, i), in); + } + + if (holder != null) + { + holder[0] = bitSet; + } + + return collected; + } + + /** + * Modified copy of BKDReader#visitRawDocValues() + */ + private int visitRawDocValues(int[] commonPrefixLengths, + byte[] scratchPackedValue, + IndexInput in, + int count, + IntersectVisitor visitor, + FixedBitSet[] holder, + final short[] origIndex) throws IOException + { + final FixedBitSet bitSet; + if (holder != null) + { + bitSet = new FixedBitSet(maxPointsInLeafNode); + } + else + { + bitSet = null; + } + + int collected = 0; + for (int i = 0; i < count; ++i) + { + for (int dim = 0; dim < numDims; dim++) + { + int prefix = commonPrefixLengths[dim]; + in.readBytes(scratchPackedValue, dim * bytesPerDim + prefix, bytesPerDim - prefix); + } + final int rowIDIndex = origIndex[i]; + if (visitor.visit(scratchPackedValue)) + { + if (bitSet != null) bitSet.set(rowIDIndex); + + collected++; + } + } + if (holder != null) + { + holder[0] = bitSet; + } + return collected; + } + + /** + * Copy of BKDReader#readCommonPrefixes() + */ + private void readCommonPrefixes(int[] commonPrefixLengths, byte[] scratchPackedValue, IndexInput in) throws IOException + { + for (int dim = 0; dim < numDims; dim++) + { + int prefix = in.readVInt(); + commonPrefixLengths[dim] = prefix; + if (prefix > 0) + { +// System.out.println("dim * bytesPerDim="+(dim * bytesPerDim)+" prefix="+prefix+" numDims="+numDims); + in.readBytes(scratchPackedValue, dim * bytesPerDim, prefix); + } + } + } + + private class FilteringIntersection extends Intersection + { + private final IntersectVisitor visitor; + private final byte[] scratchPackedValue1; + private final int[] commonPrefixLengths; + private final short[] origIndex; + + // reused byte arrays for the decompression of leaf values + private final BytesRef uncompBytes = new BytesRef(new byte[16]); + private final BytesRef compBytes = new BytesRef(new byte[16]); + + FilteringIntersection(IndexInput bkdInput, IndexInput postingsInput, IndexInput postingsSummaryInput, + IndexTree index, IntersectVisitor visitor, + QueryEventListener.BKDIndexEventListener listener, QueryContext context) + { + super(bkdInput, postingsInput, postingsSummaryInput, index, listener, context); + this.visitor = visitor; + this.commonPrefixLengths = new int[numDims]; + this.scratchPackedValue1 = new byte[packedBytesLength]; + this.origIndex = new short[maxPointsInLeafNode]; + } + + @Override + public void executeInternal(final Collection postingLists) throws IOException + { + collectPostingLists(postingLists, minPackedValue, maxPackedValue); + } + + public void collectPostingLists(Collection postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException + { + context.checkpoint(); + + final Relation r = visitor.compare(cellMinPacked, cellMaxPacked); + + if (r == Relation.CELL_OUTSIDE_QUERY) + { + // This cell is fully outside of the query shape: stop recursing + return; + } + + if (r == Relation.CELL_INSIDE_QUERY) + { + // This cell is fully inside of the query shape: recursively add all points in this cell without filtering + super.collectPostingLists(postingLists); + return; + } + + if (index.isLeafNode()) + { + if (index.nodeExists()) + filterLeaf(postingLists); + return; + } + + visitNode(postingLists, cellMinPacked, cellMaxPacked); + } + + @SuppressWarnings("resource") + void filterLeaf(Collection postingLists) throws IOException + { + bkdInput.seek(index.getLeafBlockFP()); + + final int count = bkdInput.readVInt(); + + // loading doc ids occurred here prior + + final FixedBitSet[] holder = new FixedBitSet[1]; + + final int orderMapLength = bkdInput.readVInt(); + + final long orderMapPointer = bkdInput.getFilePointer(); + + LongValues orderMapReader = LuceneCompat.directReaderGetInstance(bkdRandomInput, bitsPerValue, orderMapPointer); + for (int x = 0; x < count; x++) + { + origIndex[x] = LeafOrderMap.getValue(x, orderMapReader); + } + + // seek beyond the ordermap + bkdInput.seek(orderMapPointer + orderMapLength); + + IndexInput leafInput = bkdInput; + + if (compressor != null) + { + // This should not throw WouldBlockException, even though we're on a TPC thread, because the + // secret key used by the underlying encryptor should be loaded at reader construction time. + leafInput = CryptoUtils.uncompress(bkdInput, compressor, compBytes, uncompBytes); + } + + visitDocValues(commonPrefixLengths, scratchPackedValue1, leafInput, count, visitor, holder, origIndex); + + final int nodeID = index.getNodeID(); + + if (postingsIndex.exists(nodeID) && holder[0].cardinality() > 0) + { + final long pointer = postingsIndex.getPostingsFilePointer(nodeID); + postingLists.add(initFilteringPostingReader(pointer, holder[0])); + } + } + + void visitNode(Collection postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException + { + int splitDim = index.getSplitDim(); + assert splitDim >= 0 : "splitDim=" + splitDim; + assert splitDim < numDims; + + byte[] splitPackedValue = index.getSplitPackedValue(); + BytesRef splitDimValue = index.getSplitDimValue(); + assert splitDimValue.length == bytesPerDim; + + // make sure cellMin <= splitValue <= cellMax: + assert Arrays.compareUnsigned(cellMinPacked, splitDim * bytesPerDim, splitDim * bytesPerDim + bytesPerDim, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerDim) <= 0 : "bytesPerDim=" + bytesPerDim + " splitDim=" + splitDim + " numDims=" + numDims; + assert Arrays.compareUnsigned(cellMaxPacked, splitDim * bytesPerDim, splitDim * bytesPerDim + bytesPerDim, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerDim) >= 0 : "bytesPerDim=" + bytesPerDim + " splitDim=" + splitDim + " numDims=" + numDims; + + // Recurse on left sub-tree: + System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength); + System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, splitDim * bytesPerDim, bytesPerDim); + + index.pushLeft(); + collectPostingLists(postingLists, cellMinPacked, splitPackedValue); + index.pop(); + + // Restore the split dim value since it may have been overwritten while recursing: + System.arraycopy(splitPackedValue, splitDim * bytesPerDim, splitDimValue.bytes, splitDimValue.offset, bytesPerDim); + // Recurse on right sub-tree: + System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength); + System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, splitDim * bytesPerDim, bytesPerDim); + index.pushRight(); + collectPostingLists(postingLists, splitPackedValue, cellMaxPacked); + index.pop(); + } + + private PostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException + { + final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset); + return initFilteringPostingReader(filter, summary); + } + + @SuppressWarnings("resource") + private PostingList initFilteringPostingReader(FixedBitSet filter, PostingsReader.BlocksSummary header) throws IOException + { + PostingsReader postingsReader = new PostingsReader(postingsInput, header, listener.postingListEventListener()); + return new FilteringPostingList(filter, postingsReader); + } + } + + public int getNumDimensions() + { + return numDims; + } + + public int getBytesPerDimension() + { + return bytesPerDim; + } + + public long getPointCount() + { + return pointCount; + } + + /** + * We recurse the BKD tree, using a provided instance of this to guide the recursion. + */ + public interface IntersectVisitor + { + /** + * Called for all values in a leaf cell that crosses the query. The consumer + * should scrutinize the packedValue to decide whether to accept it. In the 1D case, + * values are visited in increasing order, and in the case of ties, in increasing order + * by segment row ID. + */ + boolean visit(byte[] packedValue); + + /** + * Called for non-leaf cells to test how the cell relates to the query, to + * determine how to further recurse down the tree. + */ + Relation compare(byte[] minPackedValue, byte[] maxPackedValue); + } + + /** + * Iterates the leaves of the KD-tree forward or backwards. + * Makes no heap allocations on iteration. + */ + private class LeafCursor + { + private final @Nullable IntersectVisitor query; + private final Direction direction; + + // This is not just the index tree, but actually a tree + some state like current node pointer + // This remembers the current position of the cursor + private final PackedIndexTree tree; + + // Remembers which nodes of the tree on the current path from the root were already fully explored. + // The set stores their level numbers. + // + // Because the index is a binary tree, a node can have at most 2 child nodes. + // When we visit the node for the first time, and we go down to its first child, + // and we see there is another child we must visit later, + // we consider this node as uncompleted (we're removing its level from this set). + // When we go back up to that node for the second time, we consult this set, and + // we see the node has one more child to visit. So we go down again to the second child, but this time we mark + // the node as complete, that is, we store its level in this set. So when we visit the node again for the third + // time, we know it's done, and we have to go up at least one more level. + // + // Note that we're storing levels, because we're interested only in the nodes on the current path from + // the root of the tree, as those are the only nodes that could be explored. A more obvious alternative + // would be to keep a set of all already visited node ids in the tree, but that would have worse memory + // complexity and would likely require a larger set and some heap allocations. + // + // Class invariant: this structure must contain up-to-date information + // for all the levels above the current level, up to the root. + private final BitSet completedLevels; + + /** + * Creates the cursor over the KD-tree leaves and positions it on the first leaf + * appropriate for the given query and traversal direction. + * Even if the query does not match any data, the cursor is positioned on one of the tree leaves, + * so {@link #getFilePointer()} and {@link #getNodeId()} can be always called immediately after the construction. + * + * @param query restricts the leaves to the ones that might contain the data that match the query, + * null query means the range is not restricted + */ + LeafCursor(Direction direction, @Nullable IntersectVisitor query) + { + this.query = query; + this.direction = direction; + + completedLevels = new BitSet(64); // physically impossible to have a tree bigger than 2^64 nodes + tree = new PackedIndexTree(); // this positions the tree at node id 1 and level 1 (not 0) + + if (direction == Direction.FORWARD) + pushToMinLeaf(query); + else + pushToMaxLeaf(query); + } + + /** + * Returns the id of the node the cursor is positioned at. + * Valid only immediately after construction or after a call to {@link #advance()} which returned {@code true}. + */ + int getNodeId() + { + assert tree.isLeafNode() : "Cursor not on a leaf node; end of data reached"; + return tree.nodeID; + } + + /** + * Returns the file pointer of the node the cursor is positioned at + * Valid only immediately after construction or after a call to {@link #advance()} which returned {@code true}. + */ + long getFilePointer() + { + assert tree.isLeafNode() : "Cursor not on a leaf node; end of data reached"; + return tree.getLeafBlockFP(); + } + + /** + * Advances the cursor to the next leaf. + * If there are no more leaves in the tree at all, positions the index tree at node 0. + * If there exist leaves, but they are out of the query range, positions the index tree at a non-leaf node. + * Calling this again after the cursor reached the end of the data is not allowed. + * + * @return true if the cursor was moved to the next leaf, false if there are no more leaves to iterate + */ + boolean advance() + { + assert tree.isLeafNode() : "Cursor not on a leaf node; end of data reached"; + + // Mark the current node as completed, so that the call to `popToFirstUncompletedLevel` + // won't stop on this level immediately but goes up instead. + completedLevels.set(tree.level); + + // Go up to the closest parent node that has a child we haven't visited yet. + if (!popToFirstUncompletedLevel()) + return false; + + assert tree.nodeExists() : "Node does not exist"; + assert !tree.isLeafNode() : "Expected a non-leaf node"; + assert !completedLevels.get(tree.level) : "Expected an uncompleted node"; + + // Go to the next leaf + if (direction == Direction.FORWARD) + { + if (query != null && query.compare(tree.getSplitDimValue().bytes, maxPackedValue) == Relation.CELL_OUTSIDE_QUERY) + return false; + pushRight(); + pushToMinLeaf(); + } + else // Direcion.BACKWARD + { + if (query != null && query.compare(minPackedValue, tree.getSplitDimValue().bytes) == Relation.CELL_OUTSIDE_QUERY) + return false; + pushLeft(); + pushToMaxLeaf(); + } + assert tree.isLeafNode() : "Cursor ended up on a non-leaf node"; + return true; + } + + /** + * Goes up the tree until it finds the first node for which we haven't exhausted all the paths down. + * + * @return true if uncompleted node is found, false if it reaches the top of the tree + */ + boolean popToFirstUncompletedLevel() + { + while (completedLevels.get(tree.level) && tree.level > 0) + tree.pop(); + + // 0 level is special; you cannot go down from level 0, so if we hit level 0, the traversal ended, + // so we must signal it to the caller by returning false + return tree.level != 0; + } + + /** + * Positions the index on the left-most leaf. + */ + void pushToMinLeaf() + { + pushToLeaf(Predicates.alwaysFalse()); + } + + /** + * Positions the index on the left-most leaf that intersects the query + */ + void pushToMinLeaf(BKDReader.IntersectVisitor query) + { + pushToLeaf(split -> query != null && query.compare(minPackedValue, split) == Relation.CELL_OUTSIDE_QUERY); + } + + /** + * Positions the index on the right-most leaf. + */ + void pushToMaxLeaf() + { + pushToLeaf(Predicates.alwaysTrue()); + } + + /** + * Positions the index on the right-most leaf that intersects the query + */ + void pushToMaxLeaf(BKDReader.IntersectVisitor query) + { + pushToLeaf(split -> query == null || query.compare(split, maxPackedValue) != Relation.CELL_OUTSIDE_QUERY); + } + + /** + * Recursively goes down the KD-tree until it reaches a leaf node. + * At every non-leaf node, uses the provided function to decide the direction to go. + * + * @param shouldGoRight a function that takes the split point of a non-leaf node + * and returns true if the search path should follow to the right child + */ + void pushToLeaf(Predicate shouldGoRight) + { + while (!tree.isLeafNode()) + { + // It is tempting to call index.getSplitPackedValue(), but that would return an empty array. + // It looks the user of the PackedIndexTree is supposed to build the splitPackedValue by themselves + // by assembling them from the values provided by getSplitDimValue for each dimension. + // Caution: This won't work if we ever support more than 1 dimension. + // But for 1 dimension, splitDimValue is the whole value we need. + byte[] splitPackedValue = tree.getSplitDimValue().bytes; + boolean goRight = shouldGoRight.test(splitPackedValue); + + if (goRight) + pushRight(); + else + pushLeft(); + } + } + + /** + * Goes to the right child of the current node. + * Updates the status of completeness of the current level based on the direction of the traversal. + */ + void pushRight() + { + // In FORWARD direction we process the left child before the right. + // In BACKWARD direction we process the right child before the left. + // Therefore, if we're going right in FORWARD direction, this node is completed. + // Otherwise, if we're going right in BACKWARD direction, the left child remains to be processed, so this + // node is uncompleted. + completedLevels.set(tree.level, direction == Direction.FORWARD); + tree.pushRight(); + } + + /** + * Goes to the left child of the current node. + * Updates the status of completeness of the current level based on the direction of the traversal. + */ + void pushLeft() + { + // In FORWARD direction we process the left child before the right. + // In BACKWARD direction we process the right child before the left. + // Therefore, if we're going left in BACKWARD direction, this node is completed. + // Otherwise, if we're going left in FORWARD direction, the right child remains to be processed, so this + // node is uncompleted. + completedLevels.set(tree.level, direction == Direction.BACKWARD); + tree.pushLeft(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java new file mode 100644 index 000000000000..e17257211a3f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.IOException; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import org.apache.cassandra.index.sai.disk.oldlucene.MutablePointValues; +import org.apache.lucene.util.Accountable; +import org.apache.lucene.util.ByteBlockPool; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.Counter; +import org.apache.lucene.util.packed.PackedInts; +import org.apache.lucene.util.packed.PackedLongValues; + +/** + * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}. + */ +public class BKDTreeRamBuffer implements Accountable +{ + @VisibleForTesting + public static int MAX_BLOCK_BYTE_POOL_SIZE = Integer.MAX_VALUE; + // This counter should not be used to track any other allocations, as we use it to prevent block pool overflow + private final Counter blockBytesUsed; + private final ByteBlockPool bytes; + private final int pointDimensionCount, pointNumBytes; + private final int packedBytesLength; + private final byte[] packedValue; + private final PackedLongValues.Builder docIDsBuilder; + private int numPoints; + private int numRows; + private int lastSegmentRowID = -1; + private boolean closed = false; + + public BKDTreeRamBuffer(int pointDimensionCount, int pointNumBytes) + { + this.blockBytesUsed = Counter.newCounter(); + this.pointDimensionCount = pointDimensionCount; + this.pointNumBytes = pointNumBytes; + + this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(blockBytesUsed)); + + packedValue = new byte[pointDimensionCount * pointNumBytes]; + packedBytesLength = pointDimensionCount * pointNumBytes; + + docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT); + } + + @Override + public long ramBytesUsed() + { + return docIDsBuilder.ramBytesUsed() + blockBytesUsed.get(); + } + + public boolean requiresFlush() + { + // ByteBlockPool can't handle more than Integer.MAX_VALUE bytes. These are allocated in fixed-size chunks, + // and additions are guaranteed to be smaller than the chunks. This means that the last chunk allocation will + // be triggered by an addition, and the rest of the space in the final chunk will be wasted, as the bytesUsed + // counters track block allocation, not the size of additions. This means that we can't pass this check and then + // fail to add a term. + return blockBytesUsed.get() >= MAX_BLOCK_BYTE_POOL_SIZE; + } + + public int numRows() + { + return numRows; + } + + public long numPoints() + { + return numPoints; + } + + public long addPackedValue(int segmentRowId, BytesRef value) + { + ensureOpen(); + + if (value.length != packedBytesLength) + { + throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointDimensionCount * pointNumBytes); + } + + long startingBlockBytesUsed = blockBytesUsed.get(); + long startingDocIDsBytesUsed = docIDsBuilder.ramBytesUsed(); + + docIDsBuilder.add(segmentRowId); + bytes.append(value); + + if (segmentRowId != lastSegmentRowID) + { + numRows++; + lastSegmentRowID = segmentRowId; + } + + numPoints++; + + long docIDsAllocatedBytes = docIDsBuilder.ramBytesUsed() - startingDocIDsBytesUsed; + long blockAllocatedBytes = blockBytesUsed.get() - startingBlockBytesUsed; + + return docIDsAllocatedBytes + blockAllocatedBytes; + } + + public MutableOneDimPointValues asPointValues() + { + ensureOpen(); + // building packed longs is destructive + closed = true; + final PackedLongValues docIDs = docIDsBuilder.build(); + return new MutableOneDimPointValues() + { + final int[] ords = new int[numPoints]; + + { + for (int i = 0; i < numPoints; ++i) + { + ords[i] = i; + } + } + + @Override + public void getValue(int i, BytesRef packedValue) + { + final long offset = (long) packedBytesLength * (long) ords[i]; + packedValue.length = packedBytesLength; + bytes.setRawBytesRef(packedValue, offset); + } + + @Override + public byte getByteAt(int i, int k) + { + byte[] a = new byte[1]; + final long offset = (long) packedBytesLength * (long) ords[i] + (long) k; + bytes.readBytes(offset, a, 0, 1); + return a[0]; + } + + @Override + public int getDocID(int i) + { + return Math.toIntExact(docIDs.get(ords[i])); + } + + @Override + public void swap(int i, int j) + { + int tmp = ords[i]; + ords[i] = ords[j]; + ords[j] = tmp; + } + + @Override + public void intersect(IntersectVisitor visitor) throws IOException + { + final BytesRef scratch = new BytesRef(); + for (int i = 0; i < numPoints; i++) + { + getValue(i, scratch); + assert scratch.length == packedValue.length; + System.arraycopy(scratch.bytes, scratch.offset, packedValue, 0, packedBytesLength); + visitor.visit(getDocID(i), packedValue); + } + } + + @Override + public int getNumDimensions() + { + return pointDimensionCount; + } + + @Override + public int getBytesPerDimension() + { + return pointNumBytes; + } + + @Override + public long size() + { + return numPoints; + } + + @Override + public int getDocCount() + { + return numRows; + } + }; + } + + private void ensureOpen() + { + Preconditions.checkState(!closed, "Expected open buffer."); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java new file mode 100644 index 000000000000..79b9fbdd40e8 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java @@ -0,0 +1,1042 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v1.kdtree; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.IntFunction; + +import com.google.common.base.MoreObjects; + +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.CryptoUtils; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.oldlucene.ByteBuffersDataOutputAdapter; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; +import org.apache.cassandra.index.sai.disk.oldlucene.ResettableByteBuffersIndexOutput; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.index.sai.disk.oldlucene.MutablePointValues; +import org.apache.cassandra.index.sai.disk.oldlucene.MutablePointsReaderUtils; +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.IntroSorter; +import org.apache.lucene.util.LongBitSet; +import org.apache.lucene.util.Sorter; + +// TODO +// - allow variable length byte[] (across docs and dims), but this is quite a bit more hairy +// - we could also index "auto-prefix terms" here, and use better compression, and maybe only use for the "fully contained" case so we'd +// only index docIDs +// - the index could be efficiently encoded as an FST, so we don't have wasteful +// (monotonic) long[] leafBlockFPs; or we could use MonotonicLongValues ... but then +// the index is already plenty small: 60M OSM points --> 1.1 MB with 128 points +// per leaf, and you can reduce that by putting more points per leaf +// - we could use threads while building; the higher nodes are very parallelizable + +/** + * Recursively builds a block KD-tree to assign all incoming points in N-dim space to smaller + * and smaller N-dim rectangles (cells) until the number of points in a given + * rectangle is <= maxPointsInLeafNode. The tree is + * fully balanced, which means the leaf nodes will have between 50% and 100% of + * the requested maxPointsInLeafNode. Values that fall exactly + * on a cell boundary may be in either cell. + * + *

    The number of dimensions can be 1 to 8, but every byte[] value is fixed length. + * + *

    + * See this paper for details. + * + *

    This consumes heap during writing: it allocates a LongBitSet(numPoints), + * and then uses up to the specified {@code maxMBSortInHeap} heap space for writing. + * + *

    + * NOTE: This can write at most Integer.MAX_VALUE * maxPointsInLeafNode total points. + * + * @lucene.experimental + */ + +public class BKDWriter implements Closeable +{ + /** How many bytes each docs takes in the fixed-width offline format */ + private final int bytesPerDoc; + + /** Default maximum number of point in each leaf block */ + public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024; + + /** Default maximum heap to use, before spilling to (slower) disk */ + public static final float DEFAULT_MAX_MB_SORT_IN_HEAP = 16.0f; + + /** Maximum number of dimensions */ + public static final int MAX_DIMS = 8; + + /** How many dimensions we are indexing */ + protected final int numDims; + + /** How many bytes each value in each dimension takes. */ + protected final int bytesPerDim; + + /** numDims * bytesPerDim */ + protected final int packedBytesLength; + + final BytesRef scratchBytesRef1 = new BytesRef(); + final int[] commonPrefixLengths; + + protected final LongBitSet docsSeen; + + protected final int maxPointsInLeafNode; + private final int maxPointsSortInHeap; + + /** Minimum per-dim values, packed */ + protected final byte[] minPackedValue; + + /** Maximum per-dim values, packed */ + protected final byte[] maxPackedValue; + + protected long pointCount; + + /** true if we have so many values that we must write ords using long (8 bytes) instead of int (4 bytes) */ + protected final boolean longOrds; + + /** An upper bound on how many points the caller will add (includes deletions) */ + private final long totalPointCount; + + private final long maxDoc; + + private final ICompressor compressor; + private final ByteOrder order; + private final Version version; + + // reused when writing leaf blocks + private final ByteBuffersDataOutputAdapter scratchOut; + private final ByteBuffersDataOutputAdapter scratchOut2; + + public BKDWriter(long maxDoc, int numDims, int bytesPerDim, + int maxPointsInLeafNode, double maxMBSortInHeap, long totalPointCount, boolean singleValuePerDoc, + ICompressor compressor, ByteOrder order, Version version) throws IOException + { + this(maxDoc, numDims, bytesPerDim, maxPointsInLeafNode, maxMBSortInHeap, totalPointCount, singleValuePerDoc, + totalPointCount > Integer.MAX_VALUE, compressor, order, version); + } + + protected BKDWriter(long maxDoc, int numDims, int bytesPerDim, + int maxPointsInLeafNode, double maxMBSortInHeap, long totalPointCount, + boolean singleValuePerDoc, boolean longOrds, ICompressor compressor, + ByteOrder order, Version version) throws IOException + { + verifyParams(numDims, maxPointsInLeafNode, maxMBSortInHeap, totalPointCount); + // We use tracking dir to deal with removing files on exception, so each place that + // creates temp files doesn't need crazy try/finally/sucess logic: + this.maxPointsInLeafNode = maxPointsInLeafNode; + this.numDims = numDims; + this.bytesPerDim = bytesPerDim; + this.totalPointCount = totalPointCount; + this.maxDoc = maxDoc; + this.compressor = compressor; + this.order = order; + this.version = version; + docsSeen = new LongBitSet(maxDoc); + packedBytesLength = numDims * bytesPerDim; + + commonPrefixLengths = new int[numDims]; + + minPackedValue = new byte[packedBytesLength]; + maxPackedValue = new byte[packedBytesLength]; + + // If we may have more than 1+Integer.MAX_VALUE values, then we must encode ords with long (8 bytes), else we can use int (4 bytes). + this.longOrds = longOrds; + + // dimensional values (numDims * bytesPerDim) + ord (int or long) + docID (int) + if (singleValuePerDoc) + { + // Lucene only supports up to 2.1 docs, so we better not need longOrds in this case: + assert longOrds == false; + bytesPerDoc = packedBytesLength + Integer.BYTES; + } + else if (longOrds) + { + bytesPerDoc = packedBytesLength + Long.BYTES + Integer.BYTES; + } + else + { + bytesPerDoc = packedBytesLength + Integer.BYTES + Integer.BYTES; + } + + // As we recurse, we compute temporary partitions of the data, halving the + // number of points at each recursion. Once there are few enough points, + // we can switch to sorting in heap instead of offline (on disk). At any + // time in the recursion, we hold the number of points at that level, plus + // all recursive halves (i.e. 16 + 8 + 4 + 2) so the memory usage is 2X + // what that level would consume, so we multiply by 0.5 to convert from + // bytes to points here. Each dimension has its own sorted partition, so + // we must divide by numDims as wel. + + maxPointsSortInHeap = (int) (0.5 * (maxMBSortInHeap * 1024 * 1024) / (bytesPerDoc * numDims)); + + // Finally, we must be able to hold at least the leaf node in heap during build: + if (maxPointsSortInHeap < maxPointsInLeafNode) + { + throw new IllegalArgumentException("maxMBSortInHeap=" + maxMBSortInHeap + " only allows for maxPointsSortInHeap=" + maxPointsSortInHeap + ", but this is less than maxPointsInLeafNode=" + maxPointsInLeafNode + "; either increase maxMBSortInHeap or decrease maxPointsInLeafNode"); + } + + scratchOut = LuceneCompat.getByteBuffersDataOutputAdapter(order, 32 * 1024); + scratchOut2 = LuceneCompat.getByteBuffersDataOutputAdapter(order, 2 * 1024); + } + + public static void verifyParams(int numDims, int maxPointsInLeafNode, double maxMBSortInHeap, long totalPointCount) + { + // We encode dim in a single byte in the splitPackedValues, but we only expose 4 bits for it now, in case we want to use + // remaining 4 bits for another purpose later + if (numDims < 1 || numDims > MAX_DIMS) + { + throw new IllegalArgumentException("numDims must be 1 .. " + MAX_DIMS + " (got: " + numDims + ")"); + } + if (maxPointsInLeafNode <= 0) + { + throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode); + } + if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH) + { + throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " + ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode); + } + if (maxMBSortInHeap < 0.0) + { + throw new IllegalArgumentException("maxMBSortInHeap must be >= 0.0 (got: " + maxMBSortInHeap + ")"); + } + if (totalPointCount < 0) + { + throw new IllegalArgumentException("totalPointCount must be >=0 (got: " + totalPointCount + ")"); + } + } + + /** How many points have been added so far */ + public long getPointCount() + { + return pointCount; + } + + /** + * Write a field from a {@link MutablePointValues}. This way of writing + * points is faster than regular writes with BKDWriter#add since + * there is opportunity for reordering points before writing them to + * disk. This method does not use transient disk in order to reorder points. + */ + public long writeField(IndexOutput out, MutableOneDimPointValues reader, + final OneDimensionBKDWriterCallback callback) throws IOException + { + if (numDims == 1) + { + SAICodecUtils.writeHeader(out); + final long fp = writeField1Dim(out, reader, callback); + SAICodecUtils.writeFooter(out); + return fp; + } + else + { + throw new IllegalArgumentException("Only 1 dimension is supported."); + } + } + + /* In the 1D case, we can simply sort points in ascending order and use the + * same writing logic as we use at merge time. */ + private long writeField1Dim(IndexOutput out, MutableOneDimPointValues reader, + OneDimensionBKDWriterCallback callback) throws IOException + { + // TODO: cast to int + if (reader.size() > 1) + MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), packedBytesLength, reader, 0, Math.toIntExact(reader.size())); + + final OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback); + + reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID)); + + return oneDimWriter.finish(); + } + + interface OneDimensionBKDWriterCallback + { + void writeLeafDocs(int leafNum, RowIDAndIndex[] leafDocs, int offset, int count); + } + + public static class RowIDAndIndex + { + public int valueOrderIndex; + public int rowID; + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("valueOrderIndex", valueOrderIndex) + .add("rowID", rowID) + .toString(); + } + } + + private class OneDimensionBKDWriter + { + + final IndexOutput out; + final List leafBlockFPs = new ArrayList<>(); + final List leafBlockStartValues = new ArrayList<>(); + final byte[] leafValues = new byte[maxPointsInLeafNode * packedBytesLength]; + final int[] leafDocs = new int[maxPointsInLeafNode]; + private long valueCount; + private int leafCount; + final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode]; + final int[] orderIndex = new int[maxPointsInLeafNode]; + final OneDimensionBKDWriterCallback callback; + + { + for (int x = 0; x < rowIDAndIndexes.length; x++) + { + rowIDAndIndexes[x] = new RowIDAndIndex(); + } + } + + OneDimensionBKDWriter(IndexOutput out, OneDimensionBKDWriterCallback callback) + { + if (numDims != 1) + { + throw new UnsupportedOperationException("numDims must be 1 but got " + numDims); + } + if (pointCount != 0) + { + throw new IllegalStateException("cannot mix add and merge"); + } + + this.out = out; + this.callback = callback; + + lastPackedValue = new byte[packedBytesLength]; + } + + // for asserts + final byte[] lastPackedValue; + private long lastDocID; + + void add(byte[] packedValue, int docID) throws IOException + { + assert valueInOrder(valueCount + leafCount, + 0, lastPackedValue, packedValue, 0, docID, lastDocID); + + if (valueCount + leafCount > totalPointCount) + { + throw new IllegalStateException("totalPointCount=" + totalPointCount + " was passed when we were created, but we just hit " + (valueCount + leafCount) + " values"); + } + + System.arraycopy(packedValue, 0, leafValues, leafCount * packedBytesLength, packedBytesLength); + leafDocs[leafCount] = docID; + docsSeen.set(docID); + leafCount++; + + if (leafCount == maxPointsInLeafNode) + { + // We write a block once we hit exactly the max count ... this is different from + // when we write N > 1 dimensional points where we write between max/2 and max per leaf block + writeLeafBlock(); + leafCount = 0; + } + + assert (lastDocID = docID) >= 0; // only assign when asserts are enabled + } + + public long finish() throws IOException + { + if (leafCount > 0) + { + writeLeafBlock(); + leafCount = 0; + } + + if (valueCount == 0) + { + return -1; + } + + pointCount = valueCount; + + long indexFP = out.getFilePointer(); + + int numInnerNodes = leafBlockStartValues.size(); + + //System.out.println("BKDW: now rotate numInnerNodes=" + numInnerNodes + " leafBlockStarts=" + leafBlockStartValues.size()); + + byte[] index = new byte[(1 + numInnerNodes) * (1 + bytesPerDim)]; + rotateToTree(1, 0, numInnerNodes, index, leafBlockStartValues); + long[] arr = new long[leafBlockFPs.size()]; + for (int i = 0; i < leafBlockFPs.size(); i++) + { + arr[i] = leafBlockFPs.get(i); + } + writeIndex(out, maxPointsInLeafNode, arr, index); + return indexFP; + } + + private void writeLeafBlock() throws IOException + { + assert leafCount != 0; + if (valueCount == 0) + { + System.arraycopy(leafValues, 0, minPackedValue, 0, packedBytesLength); + } + System.arraycopy(leafValues, (leafCount - 1) * packedBytesLength, maxPackedValue, 0, packedBytesLength); + + valueCount += leafCount; + + if (leafBlockFPs.size() > 0) + { + // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end: + leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, packedBytesLength)); + } + leafBlockFPs.add(out.getFilePointer()); + checkMaxLeafNodeCount(leafBlockFPs.size()); + + // Find per-dim common prefix: + int prefix = bytesPerDim; + int offset = (leafCount - 1) * packedBytesLength; + for (int j = 0; j < bytesPerDim; j++) + { + if (leafValues[j] != leafValues[offset + j]) + { + prefix = j; + break; + } + } + + commonPrefixLengths[0] = prefix; + + assert scratchOut.size() == 0; + + out.writeVInt(leafCount); + + for (int x = 0; x < leafCount; x++) + { + rowIDAndIndexes[x].valueOrderIndex = x; + rowIDAndIndexes[x].rowID = leafDocs[x]; + } + + final Sorter sorter = new IntroSorter() + { + RowIDAndIndex pivot; + + @Override + protected void swap(int i, int j) + { + RowIDAndIndex o = rowIDAndIndexes[i]; + rowIDAndIndexes[i] = rowIDAndIndexes[j]; + rowIDAndIndexes[j] = o; + } + + @Override + protected void setPivot(int i) + { + pivot = rowIDAndIndexes[i]; + } + + @Override + protected int comparePivot(int j) + { + return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID); + } + }; + + sorter.sort(0, leafCount); + + // write leaf rowID -> orig index + scratchOut2.reset(); + + // iterate in row ID order to get the row ID index for the given value order index + // place into an array to be written as packed ints + for (int x = 0; x < leafCount; x++) + { + final int valueOrderIndex = rowIDAndIndexes[x].valueOrderIndex; + orderIndex[valueOrderIndex] = x; + } + + LeafOrderMap.write(order, orderIndex, leafCount, maxPointsInLeafNode - 1, scratchOut2); + + int scratchSize = Math.toIntExact(scratchOut2.size()); + out.writeVInt(scratchSize); + out.writeBytes(scratchOut2.toArrayCopy(), 0, scratchSize); + + if (callback != null) callback.writeLeafDocs(leafBlockFPs.size() - 1, rowIDAndIndexes, 0, leafCount); + + writeCommonPrefixes(scratchOut, commonPrefixLengths, leafValues); + + scratchBytesRef1.length = packedBytesLength; + scratchBytesRef1.bytes = leafValues; + + final IntFunction packedValues = (i) -> { + scratchBytesRef1.offset = packedBytesLength * i; + return scratchBytesRef1; + }; + assert valuesInOrderAndBounds(leafCount, 0, ArrayUtil.copyOfSubArray(leafValues, 0, packedBytesLength), + ArrayUtil.copyOfSubArray(leafValues, (leafCount - 1) * packedBytesLength, leafCount * packedBytesLength), + packedValues, leafDocs, 0); + + writeLeafBlockPackedValues(scratchOut, commonPrefixLengths, leafCount, 0, packedValues); + + if (compressor == null) + { + out.writeBytes(scratchOut.toArrayCopy(), 0, Math.toIntExact(scratchOut.size())); + } + else + { + CryptoUtils.compress(new BytesRef(scratchOut.toArrayCopy(), 0, Math.toIntExact(scratchOut.size())), scratchBytesRef, out, compressor); + } + scratchOut.reset(); + } + } + + private final BytesRef scratchBytesRef = new BytesRef(new byte[128]); + + // TODO: there must be a simpler way? + private void rotateToTree(int nodeID, int offset, int count, byte[] index, List leafBlockStartValues) + { + //System.out.println("ROTATE: nodeID=" + nodeID + " offset=" + offset + " count=" + count + " bpd=" + bytesPerDim + " index.length=" + index.length); + if (count == 1) + { + // Leaf index node + //System.out.println(" leaf index node"); + //System.out.println(" index[" + nodeID + "] = blockStartValues[" + offset + "]"); + System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerDim) + 1, bytesPerDim); + } + else if (count > 1) + { + // Internal index node: binary partition of count + int countAtLevel = 1; + int totalCount = 0; + while (true) + { + int countLeft = count - totalCount; + //System.out.println(" cycle countLeft=" + countLeft + " coutAtLevel=" + countAtLevel); + if (countLeft <= countAtLevel) + { + // This is the last level, possibly partially filled: + int lastLeftCount = Math.min(countAtLevel / 2, countLeft); + assert lastLeftCount >= 0; + int leftHalf = (totalCount - 1) / 2 + lastLeftCount; + + int rootOffset = offset + leftHalf; + /* + System.out.println(" last left count " + lastLeftCount); + System.out.println(" leftHalf " + leftHalf + " rightHalf=" + (count-leftHalf-1)); + System.out.println(" rootOffset=" + rootOffset); + */ + + System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerDim) + 1, bytesPerDim); + //System.out.println(" index[" + nodeID + "] = blockStartValues[" + rootOffset + "]"); + + // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree + // under here, to save this while loop on each recursion + + // Recurse left + rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues); + + // Recurse right + rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues); + return; + } + totalCount += countAtLevel; + countAtLevel *= 2; + } + } + else + { + assert count == 0; + } + } + + // useful for debugging: + /* + private void printPathSlice(String desc, PathSlice slice, int dim) throws IOException { + System.out.println(" " + desc + " dim=" + dim + " count=" + slice.count + ":"); + try(PointReader r = slice.writer.getReader(slice.start, slice.count)) { + int count = 0; + while (r.next()) { + byte[] v = r.packedValue(); + System.out.println(" " + count + ": " + new BytesRef(v, dim*bytesPerDim, bytesPerDim)); + count++; + if (count == slice.count) { + break; + } + } + } + } + */ + + private void checkMaxLeafNodeCount(int numLeaves) + { + if ((1 + bytesPerDim) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH) + { + throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex"); + } + } + + /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */ + @SuppressWarnings("resource") + private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException + { + + int numLeaves = leafBlockFPs.length; + + // Possibly rotate the leaf block FPs, if the index not fully balanced binary tree (only happens + // if it was created by OneDimensionBKDWriter). In this case the leaf nodes may straddle the two bottom + // levels of the binary tree: + if (numDims == 1 && numLeaves > 1) + { + int levelCount = 2; + while (true) + { + if (numLeaves >= levelCount && numLeaves <= 2 * levelCount) + { + int lastLevel = 2 * (numLeaves - levelCount); + assert lastLevel >= 0; + if (lastLevel != 0) + { + // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading + // at read-time, so that we can still delta code them on disk at write: + long[] newLeafBlockFPs = new long[numLeaves]; + System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel); + System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel); + leafBlockFPs = newLeafBlockFPs; + } + break; + } + + levelCount *= 2; + } + } + + // Reused while packing the index + var writeBuffer = LuceneCompat.getResettableByteBuffersIndexOutput(order, 1024, "", version); + + // This is the "file" we append the byte[] to: + List blocks = new ArrayList<>(); + byte[] lastSplitValues = new byte[bytesPerDim * numDims]; + //System.out.println("\npack index"); + int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0l, blocks, 1, lastSplitValues, new boolean[numDims], false); + + // Compact the byte[] blocks into single byte index: + byte[] index = new byte[totalSize]; + int upto = 0; + for (byte[] block : blocks) + { + System.arraycopy(block, 0, index, upto, block.length); + upto += block.length; + } + assert upto == totalSize; + + return index; + } + + /** Appends the current contents of writeBuffer as another block on the growing in-memory file */ + private int appendBlock(ResettableByteBuffersIndexOutput writeBuffer, List blocks) throws IOException + { + int pos = writeBuffer.intSize(); + blocks.add(writeBuffer.toArrayCopy()); + writeBuffer.reset(); + return pos; + } + + /** + * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each + * inner node + */ + private int recursePackIndex(ResettableByteBuffersIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List blocks, + int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException + { + if (nodeID >= leafBlockFPs.length) + { + int leafID = nodeID - leafBlockFPs.length; + //System.out.println("recursePack leaf nodeID=" + nodeID); + + // In the unbalanced case it's possible the left most node only has one child: + if (leafID < leafBlockFPs.length) + { + long delta = leafBlockFPs[leafID] - minBlockFP; + if (isLeft) + { + assert delta == 0; + return 0; + } + else + { + assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID; + writeBuffer.writeVLong(delta); + return appendBlock(writeBuffer, blocks); + } + } + else + { + return 0; + } + } + else + { + long leftBlockFP; + if (isLeft == false) + { + leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID); + long delta = leftBlockFP - minBlockFP; + assert nodeID == 1 || delta > 0; + writeBuffer.writeVLong(delta); + } + else + { + // The left tree's left most leaf block FP is always the minimal FP: + leftBlockFP = minBlockFP; + } + + int address = nodeID * (1 + bytesPerDim); + int splitDim = splitPackedValues[address++] & 0xff; + + //System.out.println("recursePack inner nodeID=" + nodeID + " splitDim=" + splitDim + " splitValue=" + new BytesRef(splitPackedValues, address, bytesPerDim)); + + // find common prefix with last split value in this dim: + int prefix = 0; + for (; prefix < bytesPerDim; prefix++) + { + if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerDim + prefix]) + { + break; + } + } + + //System.out.println("writeNodeData nodeID=" + nodeID + " splitDim=" + splitDim + " numDims=" + numDims + " bytesPerDim=" + bytesPerDim + " prefix=" + prefix); + + int firstDiffByteDelta; + if (prefix < bytesPerDim) + { + //System.out.println(" delta byte cur=" + Integer.toHexString(splitPackedValues[address+prefix]&0xFF) + " prev=" + Integer.toHexString(lastSplitValues[splitDim * bytesPerDim + prefix]&0xFF) + " negated?=" + negativeDeltas[splitDim]); + firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerDim + prefix] & 0xFF); + if (negativeDeltas[splitDim]) + { + firstDiffByteDelta = -firstDiffByteDelta; + } + //System.out.println(" delta=" + firstDiffByteDelta); + assert firstDiffByteDelta > 0; + } + else + { + firstDiffByteDelta = 0; + } + + // pack the prefix, splitDim and delta first diff byte into a single vInt: + int code = (firstDiffByteDelta * (1 + bytesPerDim) + prefix) * numDims + splitDim; + + //System.out.println(" code=" + code); + //System.out.println(" splitValue=" + new BytesRef(splitPackedValues, address, bytesPerDim)); + + writeBuffer.writeVInt(code); + + // write the split value, prefix coded vs. our parent's split value: + int suffix = bytesPerDim - prefix; + byte[] savSplitValue = new byte[suffix]; + if (suffix > 1) + { + writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1); + } + + byte[] cmp = lastSplitValues.clone(); + + System.arraycopy(lastSplitValues, splitDim * bytesPerDim + prefix, savSplitValue, 0, suffix); + + // copy our split value into lastSplitValues for our children to prefix-code against + System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerDim + prefix, suffix); + + int numBytes = appendBlock(writeBuffer, blocks); + + // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into the right sub-tree we can + // quickly seek to its starting point + int idxSav = blocks.size(); + blocks.add(null); + + boolean savNegativeDelta = negativeDeltas[splitDim]; + negativeDeltas[splitDim] = true; + + int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true); + + if (nodeID * 2 < leafBlockFPs.length) + { + writeBuffer.writeVInt(leftNumBytes); + } + else + { + assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes; + } + byte[] bytes2 = writeBuffer.toArrayCopy(); + int numBytes2 = bytes2.length; + writeBuffer.reset(); + // replace our placeholder: + blocks.set(idxSav, bytes2); + + negativeDeltas[splitDim] = false; + int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false); + + negativeDeltas[splitDim] = savNegativeDelta; + + // restore lastSplitValues to what caller originally passed us: + System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerDim + prefix, suffix); + + assert Arrays.equals(lastSplitValues, cmp); + + return numBytes + numBytes2 + leftNumBytes + rightNumBytes; + } + } + + private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID) + { + // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or + // change the recursion while packing the index to return this left-most leaf block FP + // from each recursion instead? + // + // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing + // we call it O(N) times (N = number of leaf blocks) + while (nodeID < leafBlockFPs.length) + { + nodeID *= 2; + } + int leafID = nodeID - leafBlockFPs.length; + long result = leafBlockFPs[leafID]; + if (result < 0) + { + throw new AssertionError(result + " for leaf " + leafID); + } + return result; + } + + private void writeIndex(IndexOutput out, int countPerLeaf, long[] leafBlockFPs, byte[] splitPackedValues) throws IOException + { + byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues); + writeIndex(out, countPerLeaf, leafBlockFPs.length, packedIndex); + } + + private void writeIndex(IndexOutput out, int countPerLeaf, int numLeaves, byte[] packedIndex) throws IOException + { + out.writeVInt(numDims); + out.writeVInt(countPerLeaf); + out.writeVInt(bytesPerDim); + + assert numLeaves > 0; + out.writeVInt(numLeaves); + + if (compressor != null) + { + var ramOut = LuceneCompat.getResettableByteBuffersIndexOutput(order, 1024, "", out.version()); + ramOut.writeBytes(minPackedValue, 0, packedBytesLength); + ramOut.writeBytes(maxPackedValue, 0, packedBytesLength); + + CryptoUtils.compress(new BytesRef(ramOut.toArrayCopy(), 0, (int)ramOut.getFilePointer()), out, compressor); + } + else + { + out.writeBytes(minPackedValue, 0, packedBytesLength); + out.writeBytes(maxPackedValue, 0, packedBytesLength); + } + + out.writeVLong(pointCount); + //TODO Changing disk format + out.writeVLong(docsSeen.cardinality()); + + if (compressor != null) + { + CryptoUtils.compress(new BytesRef(packedIndex, 0, packedIndex.length), out, compressor); + } + else + { + out.writeVInt(packedIndex.length); + out.writeBytes(packedIndex, 0, packedIndex.length); + } + } + + private void writeLeafBlockPackedValues(DataOutput out, int[] commonPrefixLengths, int count, int sortedDim, IntFunction packedValues) throws IOException + { + int prefixLenSum = Arrays.stream(commonPrefixLengths).sum(); + if (prefixLenSum == packedBytesLength) + { + // all values in this block are equal + out.writeByte((byte) -1); + } + else + { + assert numDims == 1; + + assert commonPrefixLengths[sortedDim] < bytesPerDim; + out.writeByte((byte) sortedDim); + int compressedByteOffset = sortedDim * bytesPerDim + commonPrefixLengths[sortedDim]; + commonPrefixLengths[sortedDim]++; + for (int i = 0; i < count; ) + { + // do run-length compression on the byte at compressedByteOffset + int runLen = runLen(packedValues, i, Math.min(i + 0xff, count), compressedByteOffset); + assert runLen <= 0xff; + BytesRef first = packedValues.apply(i); + byte prefixByte = first.bytes[first.offset + compressedByteOffset]; + out.writeByte(prefixByte); + out.writeByte((byte) runLen); + writeLeafBlockPackedValuesRange(out, commonPrefixLengths, i, i + runLen, packedValues); + i += runLen; + assert i <= count; + } + } + } + + /** + * Return an array that contains the min and max values for the [offset, offset+length] interval + * of the given {@link BytesRef}s. + */ + private static BytesRef[] computeMinMax(int count, IntFunction packedValues, int offset, int length) + { + assert length > 0; + BytesRefBuilder min = new BytesRefBuilder(); + BytesRefBuilder max = new BytesRefBuilder(); + BytesRef first = packedValues.apply(0); + min.copyBytes(first.bytes, first.offset + offset, length); + max.copyBytes(first.bytes, first.offset + offset, length); + for (int i = 1; i < count; ++i) + { + BytesRef candidate = packedValues.apply(i); + if (Arrays.compareUnsigned(min.bytes(), 0, length, candidate.bytes, candidate.offset + offset, candidate.offset + offset + length) > 0) + { + min.copyBytes(candidate.bytes, candidate.offset + offset, length); + } + else if (Arrays.compareUnsigned(max.bytes(), 0, length, candidate.bytes, candidate.offset + offset, candidate.offset + offset + length) < 0) + { + max.copyBytes(candidate.bytes, candidate.offset + offset, length); + } + } + return new BytesRef[]{ min.get(), max.get() }; + } + + private void writeLeafBlockPackedValuesRange(DataOutput out, int[] commonPrefixLengths, int start, int end, IntFunction packedValues) throws IOException + { + for (int i = start; i < end; ++i) + { + BytesRef ref = packedValues.apply(i); + assert ref.length == packedBytesLength; + + for (int dim = 0; dim < numDims; dim++) + { + int prefix = commonPrefixLengths[dim]; + out.writeBytes(ref.bytes, ref.offset + dim * bytesPerDim + prefix, bytesPerDim - prefix); + } + } + } + + private static int runLen(IntFunction packedValues, int start, int end, int byteOffset) + { + BytesRef first = packedValues.apply(start); + byte b = first.bytes[first.offset + byteOffset]; + for (int i = start + 1; i < end; ++i) + { + BytesRef ref = packedValues.apply(i); + byte b2 = ref.bytes[ref.offset + byteOffset]; + assert Byte.toUnsignedInt(b2) >= Byte.toUnsignedInt(b); + if (b != b2) + { + return i - start; + } + } + return end - start; + } + + private void writeCommonPrefixes(DataOutput out, int[] commonPrefixes, byte[] packedValue) throws IOException + { + for (int dim = 0; dim < numDims; dim++) + { + out.writeVInt(commonPrefixes[dim]); + //System.out.println(commonPrefixes[dim] + " of " + bytesPerDim); + out.writeBytes(packedValue, dim * bytesPerDim, commonPrefixes[dim]); + } + } + + @Override + public void close() throws IOException + { + + } + + /** Called only in assert */ + private boolean valueInBounds(BytesRef packedValue, byte[] minPackedValue, byte[] maxPackedValue) + { + for (int dim = 0; dim < numDims; dim++) + { + int offset = bytesPerDim * dim; + if (Arrays.compareUnsigned(packedValue.bytes, packedValue.offset + offset, packedValue.offset + offset + bytesPerDim, minPackedValue, offset, offset + bytesPerDim) < 0) + { + return false; + } + if (Arrays.compareUnsigned(packedValue.bytes, packedValue.offset + offset, packedValue.offset + offset + bytesPerDim, maxPackedValue, offset, offset + bytesPerDim) > 0) + { + return false; + } + } + + return true; + } + + // only called from assert + private boolean valuesInOrderAndBounds(int count, int sortedDim, byte[] minPackedValue, byte[] maxPackedValue, + IntFunction values, int[] docs, int docsOffset) throws IOException + { + byte[] lastPackedValue = new byte[packedBytesLength]; + long lastDoc = -1; + for (int i = 0; i < count; i++) + { + BytesRef packedValue = values.apply(i); + assert packedValue.length == packedBytesLength; + assert valueInOrder(i, sortedDim, lastPackedValue, packedValue.bytes, packedValue.offset, + docs[docsOffset + i], lastDoc); + lastDoc = docs[docsOffset + i]; + + // Make sure this value does in fact fall within this leaf cell: + assert valueInBounds(packedValue, minPackedValue, maxPackedValue); + } + return true; + } + + // only called from assert + private boolean valueInOrder(long ord, int sortedDim, byte[] lastPackedValue, byte[] packedValue, int packedValueOffset, + long doc, long lastDoc) + { + int dimOffset = sortedDim * bytesPerDim; + if (ord > 0) + { + int cmp = Arrays.compareUnsigned(lastPackedValue, dimOffset, dimOffset + bytesPerDim, packedValue, packedValueOffset + dimOffset, packedValueOffset + dimOffset + bytesPerDim); + if (cmp > 0) + { + throw new AssertionError("values out of order: last value=" + new BytesRef(lastPackedValue) + " current value=" + new BytesRef(packedValue, packedValueOffset, packedBytesLength) + " ord=" + ord); + } + if (cmp == 0 && doc < lastDoc) + { + throw new AssertionError("docs out of order: last doc=" + lastDoc + " current doc=" + doc + " ord=" + ord); + } + } + System.arraycopy(packedValue, packedValueOffset, lastPackedValue, 0, packedBytesLength); + return true; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/ImmutableOneDimPointValues.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/ImmutableOneDimPointValues.java new file mode 100644 index 000000000000..0a81fb5260d4 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/ImmutableOneDimPointValues.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.IOException; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.TermsIterator; +import org.apache.cassandra.index.sai.disk.oldlucene.MutablePointValues; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; +import org.apache.lucene.util.bkd.BKDWriter; + +/** + * {@link MutablePointValues} that prevents buffered points from reordering, and always skips sorting phase in Lucene + * It's the responsibility of the underlying implementation to ensure that all points are correctly sorted. + *

    + * It allows to take advantage of an optimised 1-dim writer {@link BKDWriter} + * (that is enabled only for {@link MutablePointValues}), and reduce number of times we sort point values. + */ +public class ImmutableOneDimPointValues extends MutableOneDimPointValues +{ + private final TermsIterator termEnum; + private final byte[] scratch; + + private ImmutableOneDimPointValues(TermsIterator termEnum, AbstractType termComparator) + { + this.termEnum = termEnum; + this.scratch = new byte[TypeUtil.fixedSizeOf(termComparator)]; + } + + public static ImmutableOneDimPointValues fromTermEnum(TermsIterator termEnum, AbstractType termComparator) + { + return new ImmutableOneDimPointValues(termEnum, termComparator); + } + + @Override + public void intersect(IntersectVisitor visitor) throws IOException + { + while (termEnum.hasNext()) + { + ByteSourceInverse.readBytesMustFit(((ByteComparable.Preencoded) termEnum.next()).getPreencodedBytes(), + scratch); + + try (final PostingList postings = termEnum.postings()) + { + int segmentRowId; + while ((segmentRowId = postings.nextPosting()) != PostingList.END_OF_STREAM) + { + visitor.visit(segmentRowId, scratch); + } + } + } + } + + @Override + public int getBytesPerDimension() + { + return scratch.length; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/LeafOrderMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/LeafOrderMap.java new file mode 100644 index 000000000000..d74ce09aefa5 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/LeafOrderMap.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.cassandra.index.sai.disk.oldlucene.DirectWriterAdapter; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.util.LongValues; + +public class LeafOrderMap +{ + /** + * Get the value at the given index from the reader, and cast it to a short. If the value is too large to fit in a + * short, an ArithmeticException is thrown. + * @param index the index to read from + * @param reader the reader to read from + * @return the value at the given index, cast to a short + */ + public static short getValue(int index, LongValues reader) + { + var value = reader.get(index); + var result = (short) value; + if (result != value) { + throw new ArithmeticException("short overflow"); + } + return result; + } + + public static void write(ByteOrder order, final int[] array, int length, int maxValue, final DataOutput out) throws IOException + { + final int bits = LuceneCompat.directWriterUnsignedBitsRequired(order, maxValue); + final DirectWriterAdapter writer = LuceneCompat.directWriterGetInstance(order, out, length, bits); + for (int i = 0; i < length; i++) + { + assert array[i] <= maxValue; + + writer.add(array[i]); + } + writer.finish(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/MutableOneDimPointValues.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/MutableOneDimPointValues.java new file mode 100644 index 000000000000..88c3d217ff62 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/MutableOneDimPointValues.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.disk.oldlucene.MutablePointValues; +import org.apache.lucene.util.BytesRef; + +public abstract class MutableOneDimPointValues extends MutablePointValues +{ + private static final byte[] EMPTY = new byte[0]; + + public abstract void intersect(IntersectVisitor visitor) throws IOException; + + @Override + public int getDocCount() + { + throw new UnsupportedOperationException(); + } + + @Override + public long size() + { + // hack to skip sorting in Lucene + return 1; + } + + @Override + public void getValue(int i, BytesRef packedValue) + { + // no-op + } + + @Override + public byte getByteAt(int i, int k) + { + return 0; + } + + @Override + public int getDocID(int i) + { + return 0; + } + + @Override + public void swap(int i, int j) + { + throw new IllegalStateException("unexpected sorting"); + } + + @Override + public byte[] getMinPackedValue() + { + return EMPTY; + } + + @Override + public byte[] getMaxPackedValue() + { + return EMPTY; + } + + @Override + public int getNumDimensions() + { + return 1; + } + + @Override + public int getBytesPerDimension() + { + return 0; + } + + public interface IntersectVisitor + { + /** Called for all documents in a leaf cell that crosses the query. The consumer + * should scrutinize the packedValue to decide whether to accept it. In the 1D case, + * values are visited in increasing order, and in the case of ties, in increasing + * docID order. */ + void visit(int docID, byte[] packedValue) throws IOException; + } + + @Override + public int getNumIndexDimensions() throws IOException + { + return 1; + } + + @Override + public PointTree getPointTree() throws IOException + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/NumericIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/NumericIndexWriter.java new file mode 100644 index 000000000000..13d1231737ab --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/NumericIndexWriter.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.MoreObjects; + +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.oldlucene.MutablePointValues; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.packed.PackedInts; +import org.apache.lucene.util.packed.PackedLongValues; + +import static com.google.common.base.Preconditions.checkArgument; + + +/** + * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible + * tree levels. + * + * Given sorted input {@link MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to + * buffer all point values to sort them. + */ +public class NumericIndexWriter implements Closeable +{ + public static final int MAX_POINTS_IN_LEAF_NODE = BKDWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE; + private final BKDWriter writer; + private final IndexComponents.ForWrite components; + private final int bytesPerDim; + + private final IndexWriterConfig config; + + /** + * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for kd-tree + * @param totalPointCount must be greater than number of added rowIds, only used for validation. + */ + public NumericIndexWriter(IndexComponents.ForWrite components, + int bytesPerDim, + int maxSegmentRowId, + long totalPointCount, + IndexWriterConfig config) throws IOException + { + this(components, MAX_POINTS_IN_LEAF_NODE, bytesPerDim, maxSegmentRowId, totalPointCount, config); + } + + public NumericIndexWriter(IndexComponents.ForWrite components, + int maxPointsInLeafNode, + int bytesPerDim, + int maxSegmentRowId, + long totalPointCount, + IndexWriterConfig config) throws IOException + { + checkArgument(maxSegmentRowId >= 0, + "[%s] maxRowId must be non-negative value, but got %s", + config.getIndexName(), maxSegmentRowId); + + checkArgument(totalPointCount >= 0, + "[$s] totalPointCount must be non-negative value, but got %s", + config.getIndexName(), totalPointCount); + + this.components = components; + this.bytesPerDim = bytesPerDim; + this.config = config; + this.writer = new BKDWriter(maxSegmentRowId + 1L, + 1, + bytesPerDim, + maxPointsInLeafNode, + BKDWriter.DEFAULT_MAX_MB_SORT_IN_HEAP, + totalPointCount, + true, null, + components.addOrGet(IndexComponentType.KD_TREE).byteOrder(), + components.version()); + } + + @Override + public void close() throws IOException + { + IOUtils.close(writer); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("bytesPerDim", bytesPerDim) + .add("bufferedPoints", writer.getPointCount()) + .toString(); + } + + public static class LeafCallback implements BKDWriter.OneDimensionBKDWriterCallback + { + final List postings = new ArrayList<>(); + + public int numLeaves() + { + return postings.size(); + } + + @Override + public void writeLeafDocs(int leafNum, BKDWriter.RowIDAndIndex[] sortedByRowID, int offset, int count) + { + final PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT); + + for (int i = offset; i < count; ++i) + { + builder.add(sortedByRowID[i].rowID); + } + postings.add(builder.build()); + } + } + + /** + * Writes a k-d tree and posting lists from a {@link MutablePointValues}. + * + * @param values points to write + * + * @return metadata describing the location and size of this kd-tree in the overall SSTable kd-tree component file + */ + public SegmentMetadata.ComponentMetadataMap writeAll(MutableOneDimPointValues values) throws IOException + { + long bkdPosition; + final SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap(); + + final LeafCallback leafCallback = new LeafCallback(); + + try (IndexOutput bkdOutput = this.components.addOrGet(IndexComponentType.KD_TREE).openOutput(true)) + { + // The SSTable kd-tree component file is opened in append mode, so our offset is the current file pointer. + final long bkdOffset = bkdOutput.getFilePointer(); + + bkdPosition = writer.writeField(bkdOutput, values, leafCallback); + + // If the bkdPosition is less than 0 then we didn't write any values out + // and the index is empty + if (bkdPosition < 0) + return components; + + final long bkdLength = bkdOutput.getFilePointer() - bkdOffset; + + Map attributes = new LinkedHashMap<>(); + attributes.put("max_points_in_leaf_node", Integer.toString(writer.maxPointsInLeafNode)); + attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves())); + attributes.put("num_points", Long.toString(writer.pointCount)); + attributes.put("bytes_per_dim", Long.toString(writer.bytesPerDim)); + attributes.put("num_dims", Long.toString(writer.numDims)); + + components.put(IndexComponentType.KD_TREE, bkdPosition, bkdOffset, bkdLength, attributes); + } + + try (TraversingBKDReader reader = new TraversingBKDReader(this.components.get(IndexComponentType.KD_TREE).createIndexBuildTimeFileHandle(), bkdPosition); + IndexOutput postingsOutput = this.components.addOrGet(IndexComponentType.KD_TREE_POSTING_LISTS).openOutput(true)) + { + final long postingsOffset = postingsOutput.getFilePointer(); + + final OneDimBKDPostingsWriter postingsWriter = new OneDimBKDPostingsWriter(leafCallback.postings, config, this.components::logMessage); + reader.traverse(postingsWriter); + + // The kd-tree postings writer already writes its own header & footer. + final long postingsPosition = postingsWriter.finish(postingsOutput); + + Map attributes = new LinkedHashMap<>(); + attributes.put("num_leaf_postings", Integer.toString(postingsWriter.numLeafPostings)); + attributes.put("num_non_leaf_postings", Integer.toString(postingsWriter.numNonLeafPostings)); + + long postingsLength = postingsOutput.getFilePointer() - postingsOffset; + components.put(IndexComponentType.KD_TREE_POSTING_LISTS, postingsPosition, postingsOffset, postingsLength, attributes); + } + + return components; + } + + /** + * @return number of points added + */ + public long getPointCount() + { + return writer.getPointCount(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/OneDimBKDPostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/OneDimBKDPostingsWriter.java new file mode 100644 index 000000000000..b3cfa6c32b34 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/OneDimBKDPostingsWriter.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig; +import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList; +import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList; +import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.lucene.util.packed.PackedLongValues; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * Writes auxiliary posting lists for bkd tree nodes. If a node has a posting list attached, it will contain every row + * id + * from all leaves reachable from that node. + * + * Writer is stateful, because it needs to collect data from bkd index data structure first to find set of eligible + * nodes and leaf nodes reachable from them. + * + * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this + * order we pass them to the {@link BKDWriter}). That allows us to skip reading the leaves, instead just order leaf + * blocks by their offset in the index file, and correlate them with buffered posting lists. We can't make this + * assumption for multi-dim case. + */ +public class OneDimBKDPostingsWriter implements TraversingBKDReader.IndexTreeTraversalCallback +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final List postings; + private final TreeMap leafOffsetToNodeID = new TreeMap<>(Long::compareTo); + private final Multimap nodeToChildLeaves = HashMultimap.create(); + + private final IndexWriterConfig config; + private final Function logMessage; + int numNonLeafPostings = 0; + int numLeafPostings = 0; + + OneDimBKDPostingsWriter(List postings, IndexWriterConfig config, Function logMessage) + { + this.postings = postings; + this.config = config; + this.logMessage = logMessage; + } + + @Override + public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot) + { + checkArgument(!pathToRoot.containsInt(leafNodeID)); + checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1)); + + leafOffsetToNodeID.put(leafBlockFP, leafNodeID); + for (int i = 0; i < pathToRoot.size(); i++) + { + final int level = i + 1; + if (isLevelEligibleForPostingList(level)) + { + final int nodeID = pathToRoot.get(i); + nodeToChildLeaves.put(nodeID, leafNodeID); + } + } + } + + @SuppressWarnings("resource") + public long finish(IndexOutput out) throws IOException + { + checkState(postings.size() == leafOffsetToNodeID.size(), + "Expected equal number of postings lists (%s) and leaf offsets (%s).", + postings.size(), leafOffsetToNodeID.size()); + + final PostingsWriter postingsWriter = new PostingsWriter(out); + + final Iterator postingsIterator = postings.iterator(); + final Map leafToPostings = new HashMap<>(); + leafOffsetToNodeID.forEach((fp, nodeID) -> leafToPostings.put(nodeID, postingsIterator.next())); + + final long postingsRamBytesUsed = postings.stream() + .mapToLong(PackedLongValues::ramBytesUsed) + .sum(); + + final List internalNodeIDs = + nodeToChildLeaves.keySet() + .stream() + .filter(i -> nodeToChildLeaves.get(i).size() >= config.getBkdPostingsMinLeaves()) + .collect(Collectors.toList()); + + final Collection leafNodeIDs = leafOffsetToNodeID.values(); + + logger.debug(logMessage.apply("Writing posting lists for {} internal and {} leaf kd-tree nodes. Leaf postings memory usage: {}."), + internalNodeIDs.size(), + leafNodeIDs.size(), + FBUtilities.prettyPrintMemory(postingsRamBytesUsed)); + + final long startFP = out.getFilePointer(); + final Stopwatch flushTime = Stopwatch.createStarted(); + final TreeMap nodeIDToPostingsFilePointer = new TreeMap<>(); + for (int nodeID : Iterables.concat(internalNodeIDs, leafNodeIDs)) + { + Collection leaves = nodeToChildLeaves.get(nodeID); + + if (leaves.isEmpty()) + { + leaves = Collections.singletonList(nodeID); + numLeafPostings++; + } + else + { + numNonLeafPostings++; + } + + var postingLists = new ArrayList(leaves.size()); + for (Integer leaf : leaves) + postingLists.add(new PackedLongsPostingList(leafToPostings.get(leaf))); + + final PostingList mergedPostingList = MergePostingList.merge(postingLists); + final long postingFilePosition = postingsWriter.write(mergedPostingList); + // During compaction we could end up with an empty postings due to deletions. + // The writer will return a fp of -1 if no postings were written. + if (postingFilePosition >= 0) + nodeIDToPostingsFilePointer.put(nodeID, postingFilePosition); + } + flushTime.stop(); + logger.debug(logMessage.apply("Flushed {} of posting lists for kd-tree nodes in {} ms."), + FBUtilities.prettyPrintMemory(out.getFilePointer() - startFP), + flushTime.elapsed(TimeUnit.MILLISECONDS)); + + + final long indexFilePointer = out.getFilePointer(); + writeMap(nodeIDToPostingsFilePointer, out); + postingsWriter.complete(); + return indexFilePointer; + } + + private boolean isLevelEligibleForPostingList(int level) + { + return level > 1 && level % config.getBkdPostingsSkip() == 0; + } + + private void writeMap(Map map, IndexOutput out) throws IOException + { + out.writeVInt(map.size()); + + for (Map.Entry e : map.entrySet()) + { + out.writeVInt(e.getKey()); + out.writeVLong(e.getValue()); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/TraversingBKDReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/TraversingBKDReader.java new file mode 100644 index 000000000000..e163352787d6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/TraversingBKDReader.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.kdtree; + +import java.io.Closeable; +import java.util.Arrays; + +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.index.sai.disk.io.IndexInputReader; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.Throwables; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.store.ByteArrayDataInput; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.MathUtil; + +/** + * Base reader for a block KD-tree previously written with {@link BKDWriter}. + * + * Holds index tree on heap and enables it's traversal via {@link #traverse(IndexTreeTraversalCallback)}. + */ +public class TraversingBKDReader implements Closeable +{ + final FileHandle indexFile; + final int bytesPerDim; + final int numLeaves; + final byte[] minPackedValue; + final byte[] maxPackedValue; + // Packed array of byte[] holding all split values in the full binary tree: + final byte[] packedIndex; + final long pointCount; + final int leafNodeOffset; + final int numDims; + final int maxPointsInLeafNode; + final int bitsPerValue; + final int packedBytesLength; + + @SuppressWarnings("resource") + TraversingBKDReader(FileHandle indexFile, long root) + { + this.indexFile = indexFile; + + try (final IndexInputReader in = IndexInputReader.create(indexFile.createReader())) + { + SAICodecUtils.validate(in); + in.seek(root); + + numDims = in.readVInt(); + maxPointsInLeafNode = in.readVInt(); + bitsPerValue = LuceneCompat.directWriterUnsignedBitsRequired(in.order(), maxPointsInLeafNode - 1); + bytesPerDim = in.readVInt(); + packedBytesLength = numDims * bytesPerDim; + + // Read index: + numLeaves = in.readVInt(); + assert numLeaves > 0; + leafNodeOffset = numLeaves; + + minPackedValue = new byte[packedBytesLength]; + maxPackedValue = new byte[packedBytesLength]; + + in.readBytes(minPackedValue, 0, packedBytesLength); + in.readBytes(maxPackedValue, 0, packedBytesLength); + + for (int dim = 0; dim < numDims; dim++) + { + if (Arrays.compareUnsigned(minPackedValue, dim * bytesPerDim, dim * bytesPerDim + bytesPerDim, maxPackedValue, dim * bytesPerDim, dim * bytesPerDim + bytesPerDim) > 0) + { + String message = String.format("Min packed value %s is > max packed value %s for dimension %d.", + new BytesRef(minPackedValue), new BytesRef(maxPackedValue), dim); + throw new CorruptIndexException(message, in); + } + } + + pointCount = in.readVLong(); + + // docCount, unused + in.readVLong(); + + int numBytes = in.readVInt(); + packedIndex = new byte[numBytes]; + in.readBytes(packedIndex, 0, numBytes); + } + catch (Throwable t) + { + FileUtils.closeQuietly(indexFile); + throw Throwables.unchecked(t); + } + } + + public long getMinLeafBlockFP() + { + if (packedIndex != null) + { + return new ByteArrayDataInput(packedIndex).readVLong(); + } + else + { + throw new IllegalStateException(); + } + } + + public long memoryUsage() + { + return ObjectSizes.sizeOfArray(packedIndex) + + ObjectSizes.sizeOfArray(minPackedValue) + + ObjectSizes.sizeOfArray(maxPackedValue); + } + + @Override + public void close() + { + indexFile.close(); + } + + interface IndexTreeTraversalCallback + { + void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot); + } + + /** + * Copy of BKDReader.IndexTree + */ + abstract class IndexTree implements Cloneable + { + protected int nodeID; + // level is 1-based so that we can do level-1 w/o checking each time: + protected int level; + protected int splitDim; + protected final byte[][] splitPackedValueStack; + + protected IndexTree() + { + int treeDepth = getTreeDepth(); + splitPackedValueStack = new byte[treeDepth + 1][]; + nodeID = 1; + level = 1; + splitPackedValueStack[level] = new byte[packedBytesLength]; + } + + public void pushLeft() + { + nodeID *= 2; + level++; + if (splitPackedValueStack[level] == null) + { + splitPackedValueStack[level] = new byte[packedBytesLength]; + } + } + + /** Clone, but you are not allowed to pop up past the point where the clone happened. */ + public abstract IndexTree clone(); + + public void pushRight() + { + nodeID = nodeID * 2 + 1; + level++; + if (splitPackedValueStack[level] == null) + { + splitPackedValueStack[level] = new byte[packedBytesLength]; + } + } + + public void pop() + { + nodeID /= 2; + level--; + splitDim = -1; + //System.out.println(" pop nodeID=" + nodeID); + } + + public boolean isLeafNode() + { + return nodeID >= leafNodeOffset; + } + + public boolean nodeExists() + { + return nodeID - leafNodeOffset < leafNodeOffset; + } + + public int getNodeID() + { + return nodeID; + } + + public byte[] getSplitPackedValue() + { + assert !isLeafNode(); + assert splitPackedValueStack[level] != null : "level=" + level; + return splitPackedValueStack[level]; + } + + /** Only valid after pushLeft or pushRight, not pop! */ + public int getSplitDim() + { + assert !isLeafNode(); + return splitDim; + } + + /** Only valid after pushLeft or pushRight, not pop! */ + public abstract BytesRef getSplitDimValue(); + + /** Only valid after pushLeft or pushRight, not pop! */ + public abstract long getLeafBlockFP(); + } + + + /** + * Copy of BKDReader.PackedIndexTree + */ + final class PackedIndexTree extends IndexTree + { + // used to read the packed byte[] + private final ByteArrayDataInput in; + // holds the minimum (left most) leaf block file pointer for each level we've recursed to: + private final long[] leafBlockFPStack; + // holds the address, in the packed byte[] index, of the left-node of each level: + private final int[] leftNodePositions; + // holds the address, in the packed byte[] index, of the right-node of each level: + private final int[] rightNodePositions; + // holds the splitDim for each level: + private final int[] splitDims; + // true if the per-dim delta we read for the node at this level is a negative offset vs. the last split on this dim; this is a packed + // 2D array, i.e. to access array[level][dim] you read from negativeDeltas[level*numDims+dim]. this will be true if the last time we + // split on this dimension, we next pushed to the left sub-tree: + private final boolean[] negativeDeltas; + // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses: + private final byte[][] splitValuesStack; + // scratch value to return from getPackedValue: + private final BytesRef scratch; + + PackedIndexTree() + { + int treeDepth = getTreeDepth(); + leafBlockFPStack = new long[treeDepth + 1]; + leftNodePositions = new int[treeDepth + 1]; + rightNodePositions = new int[treeDepth + 1]; + splitValuesStack = new byte[treeDepth + 1][]; + splitDims = new int[treeDepth + 1]; + negativeDeltas = new boolean[numDims * (treeDepth + 1)]; + + in = new ByteArrayDataInput(packedIndex); + splitValuesStack[0] = new byte[packedBytesLength]; + readNodeData(false); + scratch = new BytesRef(); + scratch.length = bytesPerDim; + } + + @Override + public PackedIndexTree clone() + { + PackedIndexTree index = new PackedIndexTree(); + index.nodeID = nodeID; + index.level = level; + index.splitDim = splitDim; + index.leafBlockFPStack[level] = leafBlockFPStack[level]; + index.leftNodePositions[level] = leftNodePositions[level]; + index.rightNodePositions[level] = rightNodePositions[level]; + index.splitValuesStack[index.level] = splitValuesStack[index.level].clone(); + System.arraycopy(negativeDeltas, level * numDims, index.negativeDeltas, level * numDims, numDims); + index.splitDims[level] = splitDims[level]; + return index; + } + + @Override + public void pushLeft() + { + int nodePosition = leftNodePositions[level]; + super.pushLeft(); + System.arraycopy(negativeDeltas, (level - 1) * numDims, negativeDeltas, level * numDims, numDims); + assert splitDim != -1; + negativeDeltas[level * numDims + splitDim] = true; + in.setPosition(nodePosition); + readNodeData(true); + } + + @Override + public void pushRight() + { + int nodePosition = rightNodePositions[level]; + super.pushRight(); + System.arraycopy(negativeDeltas, (level - 1) * numDims, negativeDeltas, level * numDims, numDims); + assert splitDim != -1; + negativeDeltas[level * numDims + splitDim] = false; + in.setPosition(nodePosition); + readNodeData(false); + } + + @Override + public void pop() + { + super.pop(); + splitDim = splitDims[level]; + } + + @Override + public long getLeafBlockFP() + { + assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf"; + return leafBlockFPStack[level]; + } + + @Override + public BytesRef getSplitDimValue() + { + assert !isLeafNode(); + scratch.bytes = splitValuesStack[level]; + scratch.offset = splitDim * bytesPerDim; + return scratch; + } + + private void readNodeData(boolean isLeft) + { + + leafBlockFPStack[level] = leafBlockFPStack[level - 1]; + + // read leaf block FP delta + if (!isLeft) + { + leafBlockFPStack[level] += in.readVLong(); + } + + if (isLeafNode()) + { + splitDim = -1; + } + else + { + + // read split dim, prefix, firstDiffByteDelta encoded as int: + int code = in.readVInt(); + splitDim = code % numDims; + splitDims[level] = splitDim; + code /= numDims; + int prefix = code % (1 + bytesPerDim); + int suffix = bytesPerDim - prefix; + + if (splitValuesStack[level] == null) + { + splitValuesStack[level] = new byte[packedBytesLength]; + } + System.arraycopy(splitValuesStack[level - 1], 0, splitValuesStack[level], 0, packedBytesLength); + if (suffix > 0) + { + int firstDiffByteDelta = code / (1 + bytesPerDim); + if (negativeDeltas[level * numDims + splitDim]) + { + firstDiffByteDelta = -firstDiffByteDelta; + } + int oldByte = splitValuesStack[level][splitDim * bytesPerDim + prefix] & 0xFF; + splitValuesStack[level][splitDim * bytesPerDim + prefix] = (byte) (oldByte + firstDiffByteDelta); + in.readBytes(splitValuesStack[level], splitDim * bytesPerDim + prefix + 1, suffix - 1); + } + else + { + // our split value is == last split value in this dim, which can happen when there are many duplicate values + } + + int leftNumBytes; + if (nodeID * 2 < leafNodeOffset) + { + leftNumBytes = in.readVInt(); + } + else + { + leftNumBytes = 0; + } + + leftNodePositions[level] = in.getPosition(); + rightNodePositions[level] = leftNodePositions[level] + leftNumBytes; + } + } + } + + + void traverse(IndexTreeTraversalCallback callback) + { + traverse(callback, + new PackedIndexTree(), + new IntArrayList()); + } + + private void traverse(IndexTreeTraversalCallback callback, + IndexTree index, + IntArrayList pathToRoot) + { + if (index.isLeafNode()) + { + // In the unbalanced case it's possible the left most node only has one child: + if (index.nodeExists()) + { + callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot); + } + } + else + { + final int nodeID = index.getNodeID(); + final IntArrayList currentPath = new IntArrayList(); + currentPath.addAll(pathToRoot); + currentPath.add(nodeID); + + index.pushLeft(); + traverse(callback, index, currentPath); + index.pop(); + + index.pushRight(); + traverse(callback, index, currentPath); + index.pop(); + } + } + + /** + * Copy of BKDReader#getTreeDepth() + */ + private int getTreeDepth() + { + // First +1 because all the non-leave nodes makes another power + // of 2; e.g. to have a fully balanced tree with 4 leaves you + // need a depth=3 tree: + + // Second +1 because MathUtil.log computes floor of the logarithm; e.g. + // with 5 leaves you need a depth=4 tree: + return MathUtil.log(numLeaves, 2) + 2; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyLookup.java b/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyLookup.java deleted file mode 100644 index 439db532cd03..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyLookup.java +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.keystore; - -import java.io.IOException; -import javax.annotation.Nonnull; -import javax.annotation.concurrent.NotThreadSafe; - -import com.google.common.annotations.VisibleForTesting; - -import org.apache.cassandra.index.sai.disk.io.IndexInputReader; -import org.apache.cassandra.index.sai.disk.v1.LongArray; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.disk.v1.bitpack.MonotonicBlockPackedReader; -import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.utils.FastByteOperations; -import org.apache.cassandra.utils.Throwables; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.cassandra.utils.bytecomparable.ByteSource; -import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.BytesRefBuilder; - -/** - * Provides read access to an on-disk sequence of partition or clustering keys written by {@link KeyStoreWriter}. - *

    - * Care has been taken to make this structure as efficient as possible. - * Reading keys does not require allocating data heap buffers per each read operation. - * Only one key at a time is loaded to memory. - * Low complexity algorithms are used – a lookup of the key by point id is constant time, - * and a lookup of the point id by the key is logarithmic. - *

    - * Because the blocks are prefix compressed, random access applies only to the locating the whole block. - * In order to jump to a concrete key inside the block, the block keys are iterated from the block beginning. - * - * @see KeyStoreWriter - */ -@NotThreadSafe -public class KeyLookup -{ - public static final String INDEX_OUT_OF_BOUNDS = "The target point id [%d] cannot be less than 0 or greater than or equal to the key count [%d]"; - - private final FileHandle keysFileHandle; - private final KeyLookupMeta keyLookupMeta; - private final LongArray.Factory keyBlockOffsetsFactory; - - /** - * Creates a new reader based on its data components. - *

    - * It does not own the components, so you must close them separately after you're done with the reader. - * @param keysFileHandle handle to the file with a sequence of prefix-compressed blocks - * each storing a fixed number of keys - * @param keysBlockOffsets handle to the file containing an encoded sequence of the file offsets pointing to the blocks - * @param keyLookupMeta metadata object created earlier by the writer - * @param keyBlockOffsetsMeta metadata object for the block offsets - */ - public KeyLookup(@Nonnull FileHandle keysFileHandle, - @Nonnull FileHandle keysBlockOffsets, - @Nonnull KeyLookupMeta keyLookupMeta, - @Nonnull NumericValuesMeta keyBlockOffsetsMeta) throws IOException - { - this.keysFileHandle = keysFileHandle; - this.keyLookupMeta = keyLookupMeta; - this.keyBlockOffsetsFactory = new MonotonicBlockPackedReader(keysBlockOffsets, keyBlockOffsetsMeta); - } - - /** - * Opens a cursor over the keys stored in the keys file. - *

    - * This will read the first key into the key buffer and point to the first point in the keys file. - *

    - * The cursor is to be used in a single thread. - * The cursor is valid as long this object hasn't been closed. - * You must close the cursor when you no longer need it. - */ - public @Nonnull Cursor openCursor() throws IOException - { - return new Cursor(keysFileHandle, keyBlockOffsetsFactory); - } - - /** - * Allows reading the keys from the keys file. - * Can quickly seek to a random key by point id. - *

    - * This object is stateful and not thread safe. - * It maintains a position to the current key as well as a buffer that can hold one key. - */ - @NotThreadSafe - public class Cursor implements AutoCloseable - { - private final IndexInputReader keysInput; - private final int blockShift; - private final int blockMask; - private final boolean clustering; - private final long keysFilePointer; - private final LongArray blockOffsets; - - // The key the cursor currently points to. Initially empty. - private final BytesRef currentKey; - - // A temporary buffer used to hold the key at the start of the next block. - private final BytesRef nextBlockKey; - - // The point id the cursor currently points to. - private long currentPointId; - private long currentBlockIndex; - - Cursor(FileHandle keysFileHandle, LongArray.Factory blockOffsetsFactory) throws IOException - { - this.keysInput = IndexInputReader.create(keysFileHandle); - SAICodecUtils.validate(this.keysInput); - this.blockShift = this.keysInput.readVInt(); - this.blockMask = (1 << this.blockShift) - 1; - this.clustering = this.keysInput.readByte() == 1; - this.keysFilePointer = this.keysInput.getFilePointer(); - this.blockOffsets = new LongArray.DeferredLongArray(blockOffsetsFactory::open); - this.currentKey = new BytesRef(keyLookupMeta.maxKeyLength); - this.nextBlockKey = new BytesRef(keyLookupMeta.maxKeyLength); - keysInput.seek(keysFilePointer); - readKey(currentPointId, currentKey); - } - - /** - * Positions the cursor on the target point id and reads the key at the target to the current key buffer. - *

    - * It is allowed to position the cursor before the first item or after the last item; - * in these cases the internal buffer is cleared. - * - * @param pointId point id to lookup - * @return The {@link ByteSource} containing the key - * @throws IndexOutOfBoundsException if the target point id is less than -1 or greater than the number of keys - */ - public @Nonnull ByteSource seekToPointId(long pointId) - { - if (pointId < 0 || pointId >= keyLookupMeta.keyCount) - throw new IndexOutOfBoundsException(String.format(INDEX_OUT_OF_BOUNDS, pointId, keyLookupMeta.keyCount)); - - if (pointId != currentPointId) - { - long blockIndex = pointId >>> blockShift; - // We need to reset the block if the block index has changed or the pointId < currentPointId. - // We can read forward in the same block without a reset, but we can't read backwards, and token - // collision can result in us moving backwards. - if (blockIndex != currentBlockIndex || pointId < currentPointId) - { - currentBlockIndex = blockIndex; - resetToCurrentBlock(); - } - } - while (currentPointId < pointId) - { - currentPointId++; - readCurrentKey(); - updateCurrentBlockIndex(currentPointId); - } - - return ByteSource.fixedLength(currentKey.bytes, currentKey.offset, currentKey.length); - } - - /** - * Finds the pointId for a clustering key within a range of pointIds. The start and end of the range must not - * exceed the number of keys available. The keys within the range are expected to be in lexographical order. - *

    - * If the key is not in the block containing the start of the range a binary search is done to find - * the block containing the search key. That block is then searched to return the pointId that corresponds - * to the key that is either equal to or next highest to the search key. - * - * @param key The key to seek for with the partition - * @param startingPointId the inclusive starting point for the partition - * @param endingPointId the exclusive ending point for the partition. - * Note: this can be equal to the number of keys if this is the last partition - * @return a {@code long} representing the pointId of the key that is >= to the key passed to the method, or - * -1 if the key passed is > all the keys. - */ - public long clusteredSeekToKey(ByteComparable key, long startingPointId, long endingPointId) - { - assert clustering : "Cannot do a clustered seek to a key on non-clustered keys"; - - BytesRef searchKey = asBytesRef(key); - - updateCurrentBlockIndex(startingPointId); - resetToCurrentBlock(); - - // We can return immediately if the currentPointId is within the requested partition range and the keys match - if (currentPointId >= startingPointId && currentPointId < endingPointId && compareKeys(currentKey, searchKey) == 0) - return currentPointId; - - // Now do a binary search over the range if points between [lowSearchId, highSearchId) - long lowSearchId = startingPointId; - long highSearchId = endingPointId; - - // We will keep going with the binary shift while the search consists of at least one block - while ((highSearchId - lowSearchId) >>> blockShift > 0) - { - long midSearchId = lowSearchId + (highSearchId - lowSearchId) / 2; - - // See if the searchkey exists in the block containing the midSearchId or is above or below it - int position = moveToBlockAndCompareTo(midSearchId, searchKey); - - if (position == 0) - { - lowSearchId = currentPointId; - break; - } - - if (position < 0) - highSearchId = midSearchId; - else - lowSearchId = midSearchId; - } - - updateCurrentBlockIndex(lowSearchId); - resetToCurrentBlock(); - - // Depending on where we are in the block we may need to move forwards to the starting point ID - while (currentPointId < startingPointId) - { - currentPointId++; - readCurrentKey(); - updateCurrentBlockIndex(currentPointId); - } - - // Move forward to the ending point ID, returning the point ID if we find our key - while (currentPointId < endingPointId) - { - if (compareKeys(currentKey, searchKey) >= 0) - return currentPointId; - - currentPointId++; - if (currentPointId == keyLookupMeta.keyCount) - return -1; - - readCurrentKey(); - updateCurrentBlockIndex(currentPointId); - } - return endingPointId < keyLookupMeta.keyCount ? endingPointId : -1; - } - - @VisibleForTesting - public void reset() throws IOException - { - currentPointId = 0; - currentBlockIndex = 0; - keysInput.seek(keysFilePointer); - readCurrentKey(); - } - - @Override - public void close() - { - keysInput.close(); - } - - // Move to a block and see if the key is in the block using compareTo logic to indicate the keys position - // relative to the block. - // Note: It is down to the caller to position the block after a call to this method. - private int moveToBlockAndCompareTo(long pointId, BytesRef key) - { - updateCurrentBlockIndex(pointId); - resetToCurrentBlock(); - - if (compareKeys(key, currentKey) < 0) - return -1; - - // If we are in the last block we will assume for now that the key is in the last block and defer - // the final decision to later (if we can't find it). - if (currentBlockIndex == blockOffsets.length() -1) - return 0; - - // Finish by getting the starting key of the next block and comparing that with the key. - keysInput.seek(blockOffsets.get(currentBlockIndex + 1) + keysFilePointer); - readKey((currentBlockIndex + 1) << blockShift, nextBlockKey); - return compareKeys(key, nextBlockKey) < 0 ? 0 : 1; - } - - private void updateCurrentBlockIndex(long pointId) - { - currentBlockIndex = pointId >>> blockShift; - } - - // Reset currentPointId and currentKey to be at the start of the block pointed to by currentBlockIndex. - private void resetToCurrentBlock() - { - - keysInput.seek(blockOffsets.get(currentBlockIndex) + keysFilePointer); - currentPointId = currentBlockIndex << blockShift; - readCurrentKey(); - } - - private void readCurrentKey() - { - readKey(currentPointId, currentKey); - } - - // Read the next key indicated by pointId. - // - // Note: pointId is only used to determine whether we are at the start of a block. It is - // important that resetPosition is called prior to multiple calls to readKey. It is - // easy to get out of position. - private void readKey(long pointId, BytesRef key) - { - try - { - int prefixLength; - int suffixLength; - if ((pointId & blockMask) == 0L) - { - prefixLength = 0; - suffixLength = keysInput.readVInt(); - } - else - { - // Read the prefix and suffix lengths following the compression mechanism described - // in the KeyStoreWriterWriter. If the lengths contained in the starting byte are less - // than the 4 bit maximum then nothing further is read. Otherwise, the lengths in the - // following vints are added. - int compressedLengths = Byte.toUnsignedInt(keysInput.readByte()); - prefixLength = compressedLengths & 0x0F; - suffixLength = compressedLengths >>> 4; - if (prefixLength == 15) - prefixLength += keysInput.readVInt(); - if (suffixLength == 15) - suffixLength += keysInput.readVInt(); - } - - assert prefixLength + suffixLength <= keyLookupMeta.maxKeyLength; - if (prefixLength + suffixLength > 0) - { - key.length = prefixLength + suffixLength; - // The currentKey is appended to as the suffix for the current key is - // added to the existing prefix. - keysInput.readBytes(key.bytes, prefixLength, suffixLength); - } - } - catch (IOException e) - { - throw Throwables.cleaned(e); - } - } - - private int compareKeys(BytesRef left, BytesRef right) - { - return FastByteOperations.compareUnsigned(left.bytes, left.offset, left.offset + left.length, - right.bytes, right.offset, right.offset + right.length); - } - - private BytesRef asBytesRef(ByteComparable source) - { - BytesRefBuilder builder = new BytesRefBuilder(); - - ByteSource byteSource = source.asComparableBytes(ByteComparable.Version.OSS50); - int val; - while ((val = byteSource.next()) != ByteSource.END_OF_STREAM) - builder.append((byte) val); - return builder.get(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyLookupMeta.java b/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyLookupMeta.java deleted file mode 100644 index ac57e9a1a719..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyLookupMeta.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.keystore; - -import java.io.IOException; - -import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexOutput; - -/** - * Metadata produced by {@link KeyStoreWriter}, needed by {@link KeyLookup}. - */ -public class KeyLookupMeta -{ - public final long keyCount; - public final int maxKeyLength; - - public KeyLookupMeta(DataInput input) throws IOException - { - this.keyCount = input.readLong(); - this.maxKeyLength = input.readInt(); - } - - public static void write(IndexOutput output, long keyCount, int maxKeyLength) throws IOException - { - output.writeLong(keyCount); - output.writeInt(maxKeyLength); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyStoreWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyStoreWriter.java deleted file mode 100644 index b95c355e7b76..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/keystore/KeyStoreWriter.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.keystore; - -import java.io.Closeable; -import java.io.IOException; -import javax.annotation.Nonnull; -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.FastByteOperations; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.cassandra.utils.bytecomparable.ByteSource; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.BytesRefBuilder; -import org.apache.lucene.util.StringHelper; - -/** - * Writes a sequence of partition keys or clustering keys for use with {@link KeyLookup}. - *

    - * Partition keys are written unordered and clustering keys are written in ordered partitions determined by calls to - * {@link #startPartition()}. In either case keys can be of varying lengths. - *

    - * The {@link #blockShift} field is used to quickly determine the id of the current block - * based on a point id or to check if we are exactly at the beginning of the block. - *

    - * Keys are organized in blocks of (2 ^ {@link #blockShift}) keys. - *

    - * The blocks should not be too small because they allow prefix compression of the keys except the first key in a block. - *

    - * The blocks should not be too large because we can't just randomly jump to the key inside the block, but we have to - * iterate through all the keys from the start of the block. - * - * @see KeyLookup - */ -@NotThreadSafe -public class KeyStoreWriter implements Closeable -{ - private final int blockShift; - private final int blockMask; - private final boolean clustering; - private final IndexOutput keysOutput; - private final NumericValuesWriter offsetsWriter; - private final String componentName; - private final MetadataWriter metadataWriter; - - private BytesRefBuilder prevKey = new BytesRefBuilder(); - private BytesRefBuilder tempKey = new BytesRefBuilder(); - - private final long bytesStartFP; - - private boolean inPartition = false; - private int maxKeyLength = -1; - private long pointId = 0; - - /** - * Creates a new writer. - *

    - * It does not own the components, so you must close the components by yourself - * after you're done with the writer. - * - * @param componentName the component name for the {@link KeyLookupMeta} - * @param metadataWriter the {@link MetadataWriter} for storing the {@link KeyLookupMeta} - * @param keysOutput where to write the prefix-compressed keys - * @param keysBlockOffsets where to write the offsets of each block of keys - * @param blockShift the block shift that is used to determine the block size - * @param clustering determines whether the keys will be written as ordered partitions - */ - public KeyStoreWriter(String componentName, - MetadataWriter metadataWriter, - IndexOutput keysOutput, - NumericValuesWriter keysBlockOffsets, - int blockShift, - boolean clustering) throws IOException - { - this.componentName = componentName; - this.metadataWriter = metadataWriter; - SAICodecUtils.writeHeader(keysOutput); - this.blockShift = blockShift; - this.blockMask = (1 << this.blockShift) - 1; - this.clustering = clustering; - this.keysOutput = keysOutput; - this.keysOutput.writeVInt(blockShift); - this.keysOutput.writeByte((byte ) (clustering ? 1 : 0)); - this.bytesStartFP = keysOutput.getFilePointer(); - this.offsetsWriter = keysBlockOffsets; - } - - public void startPartition() - { - assert clustering : "Cannot start a partition on a non-clustering key store"; - - inPartition = false; - } - - /** - * Appends a key at the end of the sequence. - * - * @throws IOException if write to disk fails - * @throws IllegalArgumentException if the key is not greater than the previous added key - */ - public void add(final @Nonnull ByteComparable key) throws IOException - { - tempKey.clear(); - copyBytes(key, tempKey); - - BytesRef keyRef = tempKey.get(); - - if (clustering && inPartition) - { - if (compareKeys(keyRef, prevKey.get()) <= 0) - throw new IllegalArgumentException("Clustering keys must be in ascending lexographical order"); - } - - inPartition = true; - - writeKey(keyRef); - - maxKeyLength = Math.max(maxKeyLength, keyRef.length); - - BytesRefBuilder temp = this.tempKey; - this.tempKey = this.prevKey; - this.prevKey = temp; - - pointId++; - } - - private void writeKey(BytesRef key) throws IOException - { - if ((pointId & blockMask) == 0) - { - offsetsWriter.add(keysOutput.getFilePointer() - bytesStartFP); - - keysOutput.writeVInt(key.length); - keysOutput.writeBytes(key.bytes, key.offset, key.length); - } - else - { - int prefixLength = 0; - int suffixLength = 0; - - // If the key is the same as the previous key then we use prefix and suffix lengths of 0. - // This means that we store a byte of 0 and don't write any data for the key. - if (compareKeys(prevKey.get(), key) != 0) - { - prefixLength = StringHelper.bytesDifference(prevKey.get(), key); - suffixLength = key.length - prefixLength; - } - // The prefix and suffix lengths are written as a byte followed by up to 2 vints. An attempt is - // made to compress the lengths into the byte (if prefix length < 15 and/or suffix length < 15). - // If either length exceeds the compressed byte maximum, it is written as a vint following the byte. - keysOutput.writeByte((byte) (Math.min(prefixLength, 15) | (Math.min(15, suffixLength) << 4))); - - if (prefixLength + suffixLength > 0) - { - if (prefixLength >= 15) - keysOutput.writeVInt(prefixLength - 15); - if (suffixLength >= 15) - keysOutput.writeVInt(suffixLength - 15); - - keysOutput.writeBytes(key.bytes, key.offset + prefixLength, key.length - prefixLength); - } - } - } - - /** - * Flushes any in-memory buffers to the output streams. - * Does not close the output streams. - * No more writes are allowed. - */ - @Override - public void close() throws IOException - { - try (IndexOutput output = metadataWriter.builder(componentName)) - { - SAICodecUtils.writeFooter(keysOutput); - KeyLookupMeta.write(output, pointId, maxKeyLength); - } - finally - { - FileUtils.close(offsetsWriter, keysOutput); - } - } - - private int compareKeys(BytesRef left, BytesRef right) - { - return FastByteOperations.compareUnsigned(left.bytes, left.offset, left.offset + left.length, - right.bytes, right.offset, right.offset + right.length); - } - - private void copyBytes(ByteComparable source, BytesRefBuilder dest) - { - ByteSource byteSource = source.asComparableBytes(ByteComparable.Version.OSS50); - int val; - while ((val = byteSource.next()) != ByteSource.END_OF_STREAM) - dest.append((byte) val); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/FilteringPostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/FilteringPostingList.java index 9140e358c961..22433ee8d5c9 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/FilteringPostingList.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/FilteringPostingList.java @@ -19,10 +19,12 @@ import java.io.IOException; -import org.apache.cassandra.index.sai.postings.OrdinalPostingList; -import org.apache.cassandra.index.sai.postings.PostingList; +import com.google.common.base.Preconditions; + +import org.apache.cassandra.index.sai.disk.PostingList; import org.apache.lucene.util.FixedBitSet; + /** * A wrapper that iterates over a delegate {@link PostingList}, filtering out postings at * positions that are not present in a provided filter. @@ -37,12 +39,15 @@ public class FilteringPostingList implements PostingList public FilteringPostingList(FixedBitSet filter, OrdinalPostingList delegate) { cardinality = filter.cardinality(); + + Preconditions.checkArgument(cardinality > 0, "Filter must contain at least one match."); + this.filter = filter; this.delegate = delegate; } @Override - public void close() + public void close() throws IOException { delegate.close(); } @@ -52,11 +57,11 @@ public void close() * @return the segment row ID of the next match */ @Override - public long nextPosting() throws IOException + public int nextPosting() throws IOException { while (true) { - long segmentRowId = delegate.nextPosting(); + int segmentRowId = delegate.nextPosting(); if (segmentRowId == PostingList.END_OF_STREAM) { @@ -71,23 +76,23 @@ public long nextPosting() throws IOException } @Override - public long size() + public int size() { return cardinality; } @Override - public long advance(long targetRowID) throws IOException + public int advance(int targetRowID) throws IOException { - long segmentRowId = delegate.advance(targetRowID); + int segmentRowId = delegate.advance(targetRowID); if (segmentRowId == PostingList.END_OF_STREAM) { return PostingList.END_OF_STREAM; } - // these are always for leaf balanced tree postings so the max is 1024 - position = (int)delegate.getOrdinal(); + // these are always for leaf kdtree postings so the max is 1024 + position = delegate.getOrdinal(); // If the ordinal of the ID we just read satisfies the filter, just return it... if (filter.get(position - 1)) diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/IntersectingPostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/IntersectingPostingList.java new file mode 100644 index 000000000000..295796c9066f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/IntersectingPostingList.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1.postings; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.io.util.FileUtils; + +/** + * Performs intersection operations on multiple PostingLists, returning only postings + * that appear in all inputs. + */ +@NotThreadSafe +public class IntersectingPostingList implements PostingList +{ + private final Map postingsByTerm; + private final List postingLists; // so we can access by ordinal in intersection code + private final int size; + + private IntersectingPostingList(Map postingsByTerm) + { + if (postingsByTerm.isEmpty()) + throw new AssertionError(); + this.postingsByTerm = postingsByTerm; + this.postingLists = new ArrayList<>(postingsByTerm.values()); + this.size = postingLists.stream() + .mapToInt(PostingList::size) + .min() + .orElse(0); + } + + /** + * @return the intersection of the provided term-posting list mappings + */ + public static IntersectingPostingList intersect(Map postingsByTerm) + { + // TODO optimize cases where + // - we have a single postinglist + // - any posting list is empty (intersection also empty) + return new IntersectingPostingList(postingsByTerm); + } + + @Override + public int nextPosting() throws IOException + { + return findNextIntersection(Integer.MIN_VALUE, false); + } + + @Override + public int advance(int targetRowID) throws IOException + { + assert targetRowID >= 0 : targetRowID; + return findNextIntersection(targetRowID, true); + } + + @Override + public int frequency() + { + // call frequencies() instead + throw new UnsupportedOperationException(); + } + + public Map frequencies() + { + Map result = new HashMap<>(); + for (Map.Entry entry : postingsByTerm.entrySet()) + result.put(entry.getKey(), entry.getValue().frequency()); + return result; + } + + private int findNextIntersection(int targetRowID, boolean isAdvance) throws IOException + { + int maxRowId = targetRowID; + int maxRowIdIndex = -1; + + // Scan through all posting lists looking for a common row ID + for (int i = 0; i < postingLists.size(); i++) + { + // don't advance the sublist in which we found our current max + if (i == maxRowIdIndex) + continue; + + // Advance this sublist to the current max, special casing the first one as needed + PostingList list = postingLists.get(i); + int rowId = (isAdvance || maxRowIdIndex >= 0) + ? list.advance(maxRowId) + : list.nextPosting(); + if (rowId == END_OF_STREAM) + return END_OF_STREAM; + + // Update maxRowId + index if we find a larger value, or this was the first sublist evaluated + if (rowId > maxRowId || maxRowIdIndex < 0) + { + maxRowId = rowId; + maxRowIdIndex = i; + i = -1; // restart the scan with new maxRowId + } + } + + // Once we complete a full scan without finding a larger rowId, we've found an intersection + return maxRowId; + } + + @Override + public int size() + { + return size; + } + + @Override + public void close() + { + for (PostingList list : postingLists) + FileUtils.closeQuietly(list); + } +} + + diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java index 39516d8f6b8d..a4aaa916368b 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java @@ -17,140 +17,81 @@ */ package org.apache.cassandra.index.sai.disk.v1.postings; -import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.PriorityQueue; import javax.annotation.concurrent.NotThreadSafe; -import org.apache.cassandra.index.sai.postings.PeekablePostingList; -import org.apache.cassandra.index.sai.postings.PostingList; +import org.apache.cassandra.index.sai.disk.PostingList; import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.IntMerger; import static com.google.common.base.Preconditions.checkArgument; /** * Merges multiple {@link PostingList} which individually contain unique items into a single list. - * While the individual lists contain unique items, there can be duplicate items between lists so - * the class also checks for duplicates and only returns unique items in sorted order. */ @NotThreadSafe -public class MergePostingList implements PostingList +public class MergePostingList extends IntMerger implements PostingList { - private final PriorityQueue postingLists; - private final List temp; - private final Closeable onClose; - private final long minimum; - private final long maximum; - private final long size; - private long lastRowId = -1; + final int size; - private MergePostingList(PriorityQueue postingLists, Closeable onClose) + private MergePostingList(List postingLists) { - this.temp = new ArrayList<>(postingLists.size()); - this.onClose = onClose; - this.postingLists = postingLists; - long minimum = 0; - long maximum = 0; + super(postingLists, PostingList.class); + checkArgument(!postingLists.isEmpty()); long totalPostings = 0; for (PostingList postingList : postingLists) - { - minimum = Math.min(minimum, postingList.minimum()); - maximum = Math.max(maximum, postingList.maximum()); totalPostings += postingList.size(); - } - this.minimum = minimum; - this.maximum = maximum; - this.size = totalPostings; - } - - public static PostingList merge(PriorityQueue postings, Closeable onClose) - { - checkArgument(!postings.isEmpty(), "Cannot merge an empty queue of posting lists"); - return postings.size() > 1 ? new MergePostingList(postings, onClose) : postings.poll(); - } - public static PostingList merge(PriorityQueue postings) - { - return merge(postings, () -> FileUtils.close(postings)); + // We could technically "overflow" integer if enough row ids are duplicated in the source posting lists. + // The size does not affect correctness, so just use integer max if that happens. + this.size = (int) Math.min(totalPostings, Integer.MAX_VALUE); } public static PostingList merge(List postings) { - PriorityQueue postingsQueue = new PriorityQueue<>(postings.size(), Comparator.comparingLong(PeekablePostingList::peek)); - postings.stream().map(PeekablePostingList::makePeekable).forEach(postingsQueue::add); - return merge(postingsQueue); + if (postings.isEmpty()) + return PostingList.EMPTY; + + if (postings.size() == 1) + return postings.get(0); + + return new MergePostingList(postings); } @Override - public long minimum() + public int nextPosting() throws IOException { - return minimum; + return advance(); } @Override - public long maximum() + public int advance(int targetRowID) throws IOException { - return maximum; + return skipTo(targetRowID); } @Override - public long nextPosting() throws IOException + public int size() { - while (!postingLists.isEmpty()) - { - PeekablePostingList head = postingLists.poll(); - long next = head.nextPosting(); - - if (next == END_OF_STREAM) - { - // skip current posting list - continue; - } - - if (next > lastRowId) - { - lastRowId = next; - postingLists.add(head); - return next; - } - else if (next == lastRowId) - { - postingLists.add(head); - } - } - - return PostingList.END_OF_STREAM; + return size; } @Override - public long advance(long targetRowID) throws IOException + public void close() { - temp.clear(); - - while (!postingLists.isEmpty()) - { - PeekablePostingList peekable = postingLists.poll(); - peekable.advanceWithoutConsuming(targetRowID); - if (peekable.peek() != PostingList.END_OF_STREAM) - temp.add(peekable); - } - postingLists.addAll(temp); - - return nextPosting(); + applyToAllSources(FileUtils::closeQuietly); } @Override - public long size() + public int advanceSource(PostingList s) throws IOException { - return size; + return s.nextPosting(); } @Override - public void close() + protected int skipSource(PostingList s, int targetPosition) throws IOException { - FileUtils.closeQuietly(onClose); + return s.advance(targetPosition); } } diff --git a/src/java/org/apache/cassandra/index/sai/postings/OrdinalPostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/OrdinalPostingList.java similarity index 87% rename from src/java/org/apache/cassandra/index/sai/postings/OrdinalPostingList.java rename to src/java/org/apache/cassandra/index/sai/disk/v1/postings/OrdinalPostingList.java index dbc01c6c228f..ebb4d44dd43e 100644 --- a/src/java/org/apache/cassandra/index/sai/postings/OrdinalPostingList.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/OrdinalPostingList.java @@ -15,7 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.cassandra.index.sai.postings; +package org.apache.cassandra.index.sai.disk.v1.postings; + +import org.apache.cassandra.index.sai.disk.PostingList; public interface OrdinalPostingList extends PostingList { @@ -23,5 +25,5 @@ public interface OrdinalPostingList extends PostingList * * @return the ordinal of the posting that will be returned on the next call to {@link #nextPosting()} */ - long getOrdinal(); + int getOrdinal(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PackedLongsPostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PackedLongsPostingList.java index 34a6ea8e1748..0239237c0e87 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PackedLongsPostingList.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PackedLongsPostingList.java @@ -17,7 +17,9 @@ */ package org.apache.cassandra.index.sai.disk.v1.postings; -import org.apache.cassandra.index.sai.postings.PostingList; +import java.io.IOException; + +import org.apache.cassandra.index.sai.disk.PostingList; import org.apache.lucene.util.packed.PackedLongValues; /** @@ -35,11 +37,13 @@ public PackedLongsPostingList(PackedLongValues values) } @Override - public long nextPosting() + public int nextPosting() { if (iterator.hasNext()) { - return iterator.next(); + // This is assumed to be safe because we only insert segment row ids, which are always integers, + // into the packed longs object + return Math.toIntExact(iterator.next()); } else { @@ -48,13 +52,14 @@ public long nextPosting() } @Override - public long size() + public int size() { - return values.size(); + // We know that the size of the packed longs object is less than or equal to Integer.MAX_VALUE + return Math.toIntExact(values.size()); } @Override - public long advance(long targetRowID) + public int advance(int targetRowID) throws IOException { throw new UnsupportedOperationException(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingListRangeIterator.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingListRangeIterator.java deleted file mode 100644 index 813017db8d3a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingListRangeIterator.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.postings; - -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; -import javax.annotation.concurrent.NotThreadSafe; - -import com.google.common.base.Stopwatch; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.exceptions.QueryCancelledException; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.v1.segment.IndexSegmentSearcherContext; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.Throwables; - -/** - * A key iterator based on a {@link PostingList} derived from a single index segment. - * - *

      - *
    1. fetch next segment row id from posting list or skip to specific segment row id if {@link #skipTo(PrimaryKey)} is called
    2. - *
    3. add {@link IndexSegmentSearcherContext#segmentRowIdOffset} to obtain the sstable row id
    4. - *
    5. produce a {@link PrimaryKey} from {@link PrimaryKeyMap#primaryKeyFromRowId(long)} which is used - * to avoid fetching duplicated keys due to partition-level indexing on wide partition schema. - *
      - * Note: in order to reduce disk access in multi-index query, partition keys will only be fetched for intersected tokens - * in {@link org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher}. - *
    6. - *
    - * - */ - -@NotThreadSafe -public class PostingListRangeIterator extends KeyRangeIterator -{ - private static final Logger logger = LoggerFactory.getLogger(PostingListRangeIterator.class); - - private final Stopwatch timeToExhaust = Stopwatch.createStarted(); - private final QueryContext queryContext; - - private final PostingList postingList; - private final IndexIdentifier indexIdentifier; - private final PrimaryKeyMap primaryKeyMap; - private final long rowIdOffset; - - private boolean needsSkipping = false; - private PrimaryKey skipToKey = null; - - /** - * Create a direct PostingListRangeIterator where the underlying PostingList is materialised - * immediately so the posting list size can be used. - */ - public PostingListRangeIterator(IndexIdentifier indexIdentifier, - PrimaryKeyMap primaryKeyMap, - IndexSegmentSearcherContext searcherContext) - { - super(searcherContext.minimumKey, searcherContext.maximumKey, searcherContext.count(), () -> {}); - - this.indexIdentifier = indexIdentifier; - this.primaryKeyMap = primaryKeyMap; - this.postingList = searcherContext.postingList; - this.rowIdOffset = searcherContext.segmentRowIdOffset; - this.queryContext = searcherContext.context; - } - - @Override - protected void performSkipTo(PrimaryKey nextKey) - { - if (skipToKey != null && skipToKey.compareTo(nextKey, false) > 0) - return; - - skipToKey = nextKey; - needsSkipping = true; - } - - @Override - protected PrimaryKey computeNext() - { - try - { - queryContext.checkpoint(); - - // just end the iterator if we don't have a postingList or current segment is skipped - if (exhausted()) - return endOfData(); - - long rowId = getNextRowId(); - if (rowId == PostingList.END_OF_STREAM) - return endOfData(); - - return primaryKeyMap.primaryKeyFromRowId(rowId); - } - catch (Throwable t) - { - if (!(t instanceof QueryCancelledException)) - logger.error(indexIdentifier.logMessage("Unable to provide next token!"), t); - - FileUtils.closeQuietly(Arrays.asList(postingList, primaryKeyMap)); - throw Throwables.cleaned(t); - } - } - - @Override - public void close() - { - if (logger.isTraceEnabled()) - { - final long exhaustedInMills = timeToExhaust.stop().elapsed(TimeUnit.MILLISECONDS); - logger.trace(indexIdentifier.logMessage("PostingListRangeIterator exhausted after {} ms"), exhaustedInMills); - } - - FileUtils.closeQuietly(Arrays.asList(postingList, primaryKeyMap)); - } - - private boolean exhausted() - { - return needsSkipping && skipToKey.compareTo(getMaximum(), false) > 0; - } - - /** - * reads the next sstable row ID from the underlying posting list, potentially skipping to get there. - */ - private long getNextRowId() throws IOException - { - long segmentRowId; - if (needsSkipping) - { - long targetRowID = primaryKeyMap.rowIdFromPrimaryKey(skipToKey); - // skipToToken is larger than max token in token file - if (targetRowID < 0) - { - return PostingList.END_OF_STREAM; - } - - segmentRowId = postingList.advance(targetRowID - rowIdOffset); - - needsSkipping = false; - } - else - { - segmentRowId = postingList.nextPosting(); - } - - return segmentRowId != PostingList.END_OF_STREAM - ? segmentRowId + rowIdOffset - : PostingList.END_OF_STREAM; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java index bbc360445c31..39b38123f986 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java @@ -23,155 +23,231 @@ import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput; -import org.apache.cassandra.index.sai.disk.v1.DirectReaders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.io.IndexInputReader; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; import org.apache.cassandra.index.sai.disk.v1.LongArray; import org.apache.cassandra.index.sai.metrics.QueryEventListener; -import org.apache.cassandra.index.sai.postings.OrdinalPostingList; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.index.sai.utils.SeekingRandomAccessInput; import org.apache.lucene.index.CorruptIndexException; -import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.RandomAccessInput; import org.apache.lucene.util.LongValues; -import org.apache.lucene.util.packed.DirectReader; /** * Reads, decompresses and decodes postings lists written by {@link PostingsWriter}. - *

    - * Holds exactly one posting block in memory at a time. Does binary search over skip table to find a postings block to + * + * Holds exactly one postings block in memory at a time. Does binary search over skip table to find a postings block to * load. */ @NotThreadSafe public class PostingsReader implements OrdinalPostingList { - private final IndexInput input; + private static final Logger logger = LoggerFactory.getLogger(PostingsReader.class); + + protected final IndexInput input; + protected final InputCloser runOnClose; + private final int blockEntries; + private final int numPostings; + private final LongArray blockOffsets; + private final LongArray blockMaxValues; private final SeekingRandomAccessInput seekingInput; private final QueryEventListener.PostingListEventListener listener; + + // TODO: Expose more things through the summary, now that it's an actual field? private final BlocksSummary summary; - // Current block index - private int blockIndex; - // Current posting index within block - private int postingIndex; - private long totalPostingsRead; - private long actualPosting; + private int postingsBlockIdx; + private int blockIdx; // position in block + private int totalPostingsRead; + private int actualSegmentRowId; - private LongValues currentFoRValues; - private long postingsDecoded = 0; + private long currentPosition; + private LongValues currentFORValues; + private int postingsDecoded = 0; + private int currentFrequency = Integer.MIN_VALUE; + private final boolean readFrequencies; @VisibleForTesting public PostingsReader(IndexInput input, long summaryOffset, QueryEventListener.PostingListEventListener listener) throws IOException { - this(input, new BlocksSummary(input, summaryOffset), listener); + this(input, new BlocksSummary(input, summaryOffset, InputCloser.NOOP), listener); } public PostingsReader(IndexInput input, BlocksSummary summary, QueryEventListener.PostingListEventListener listener) throws IOException { + this(input, summary, false, listener, () -> { + try + { + input.close(); + } + finally + { + summary.close(); + } + }); + } + + public PostingsReader(IndexInput input, BlocksSummary summary, boolean readFrequencies, QueryEventListener.PostingListEventListener listener) throws IOException + { + this(input, summary, readFrequencies, listener, () -> { + try + { + input.close(); + } + finally + { + summary.close(); + } + }); + } + + public PostingsReader(IndexInput input, BlocksSummary summary, boolean readFrequencies, QueryEventListener.PostingListEventListener listener, InputCloser runOnClose) throws IOException + { + assert input instanceof IndexInputReader; + logger.trace("Opening postings reader for {}", input); + this.readFrequencies = readFrequencies; this.input = input; this.seekingInput = new SeekingRandomAccessInput(input); + this.blockOffsets = summary.offsets; + this.blockEntries = summary.blockEntries; + this.numPostings = summary.numPostings; + this.blockMaxValues = summary.maxValues; this.listener = listener; + this.summary = summary; + this.runOnClose = runOnClose; reBuffer(); } @Override - public long getOrdinal() + public int getOrdinal() { return totalPostingsRead; } + public interface InputCloser + { + InputCloser NOOP = () -> {}; + void close() throws IOException; + } + public static class BlocksSummary { - private final IndexInput input; - final int blockSize; + final int blockEntries; final int numPostings; final LongArray offsets; final LongArray maxValues; + private final InputCloser runOnClose; + public BlocksSummary(IndexInput input, long offset) throws IOException { - this.input = input; + this(input, offset, input::close); + } + + public BlocksSummary(IndexInput input, long offset, InputCloser runOnClose) throws IOException + { + this.runOnClose = runOnClose; + input.seek(offset); - this.blockSize = input.readVInt(); - //TODO This should need to change because we can potentially end up with postings of more than Integer.MAX_VALUE? + this.blockEntries = input.readVInt(); + // This is the count of row ids in a single posting list. For now, a segment cannot have more than + // Integer.MAX_VALUE row ids, so it is safe to use an int here. this.numPostings = input.readVInt(); - SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(input); - int numBlocks = input.readVInt(); - long maxBlockValuesLength = input.readVLong(); - long maxBlockValuesOffset = input.getFilePointer() + maxBlockValuesLength; + final SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(input); + final int numBlocks = input.readVInt(); + final long maxBlockValuesLength = input.readVLong(); + final long maxBlockValuesOffset = input.getFilePointer() + maxBlockValuesLength; - byte offsetBitsPerValue = input.readByte(); - DirectReaders.checkBitsPerValue(offsetBitsPerValue, input, () -> "Postings list header"); - LongValues lvOffsets = offsetBitsPerValue == 0 ? LongValues.ZEROES : DirectReader.getInstance(randomAccessInput, offsetBitsPerValue, input.getFilePointer()); - this.offsets = new LongArrayReader(lvOffsets, numBlocks); + final byte offsetBitsPerValue = input.readByte(); + if (offsetBitsPerValue > 64) + { + String message = String.format("Postings list header is corrupted: Bits per value for block offsets must be no more than 64 and is %d.", offsetBitsPerValue); + throw new CorruptIndexException(message, input); + } + this.offsets = new LongArrayReader(randomAccessInput, offsetBitsPerValue == 0 ? LongValues.ZEROES : LuceneCompat.directReaderGetInstance(randomAccessInput, offsetBitsPerValue, input.getFilePointer()), numBlocks); input.seek(maxBlockValuesOffset); - byte valuesBitsPerValue = input.readByte(); - DirectReaders.checkBitsPerValue(valuesBitsPerValue, input, () -> "Postings list header"); - LongValues lvValues = valuesBitsPerValue == 0 ? LongValues.ZEROES : DirectReader.getInstance(randomAccessInput, valuesBitsPerValue, input.getFilePointer()); - this.maxValues = new LongArrayReader(lvValues, numBlocks); + final byte valuesBitsPerValue = input.readByte(); + if (valuesBitsPerValue > 64) + { + String message = String.format("Postings list header is corrupted: Bits per value for values samples must be no more than 64 and is %d.", valuesBitsPerValue); + throw new CorruptIndexException(message, input); + } + this.maxValues = new LongArrayReader(randomAccessInput, valuesBitsPerValue == 0 ? LongValues.ZEROES : LuceneCompat.directReaderGetInstance(randomAccessInput, valuesBitsPerValue, input.getFilePointer()), numBlocks); } - void close() + void close() throws IOException { - FileUtils.closeQuietly(input); + runOnClose.close(); } private static class LongArrayReader implements LongArray { + private final RandomAccessInput input; private final LongValues reader; private final int length; - private LongArrayReader(LongValues reader, int length) + private LongArrayReader(RandomAccessInput input, LongValues reader, int length) { + this.input = input; this.reader = reader; this.length = length; } @Override - public long get(long idx) + public long ceilingIndex(long targetValue) { - return reader.get(idx); + throw new UnsupportedOperationException(); } @Override - public long length() + public long indexOf(long targetValue) { - return length; + throw new UnsupportedOperationException(); } @Override - public long indexOf(long value) + public long get(long idx) { - throw new UnsupportedOperationException(); + return reader.get(idx); + } + + @Override + public long length() + { + return length; } } } @Override - public void close() + public void close() throws IOException { listener.postingDecoded(postingsDecoded); - FileUtils.closeQuietly(input); - summary.close(); + runOnClose.close(); } @Override - public long size() + public int size() { - return summary.numPostings; + return numPostings; } /** * Advances to the first row ID beyond the current that is greater than or equal to the * target, and returns that row ID. Exhausts the iterator and returns {@link #END_OF_STREAM} if * the target is greater than the highest row ID. - *

    + * * Does binary search over the skip table to find the next block to load into memory. - *

    + * * Note: Callers must use the return value of this method before calling {@link #nextPosting()}, as calling * that method will return the next posting, not the one to which we have just advanced. * @@ -180,17 +256,17 @@ public long size() * @return first segment row ID which is >= the target row ID or {@link PostingList#END_OF_STREAM} if one does not exist */ @Override - public long advance(long targetRowID) throws IOException + public int advance(int targetRowID) throws IOException { listener.onAdvance(); - int block = binarySearchBlocks(targetRowID); + int block = binarySearchBlock(targetRowID); if (block < 0) { block = -block - 1; } - if (blockIndex == block + 1) + if (postingsBlockIdx == block + 1) { // we're in the same block, just iterate through return slowAdvance(targetRowID); @@ -202,11 +278,11 @@ public long advance(long targetRowID) throws IOException return slowAdvance(targetRowID); } - private long slowAdvance(long targetRowID) throws IOException + private int slowAdvance(int targetRowID) throws IOException { - while (totalPostingsRead < summary.numPostings) + while (totalPostingsRead < numPostings) { - long segmentRowId = peekNext(); + int segmentRowId = peekNext(); advanceOnePosition(segmentRowId); @@ -218,70 +294,62 @@ private long slowAdvance(long targetRowID) throws IOException return END_OF_STREAM; } - // Perform a binary search of the blocks to the find the block index - // containing the targetRowID, or, in the case of a duplicate value - // crossing blocks, the preceeding block index - private int binarySearchBlocks(long targetRowID) + private int binarySearchBlock(long targetRowID) { - int lowBlockIndex = blockIndex - 1; - int highBlockIndex = Math.toIntExact(summary.maxValues.length()) - 1; + int low = postingsBlockIdx - 1; + int high = Math.toIntExact(blockMaxValues.length()) - 1; // in current block - if (lowBlockIndex <= highBlockIndex && targetRowID <= summary.maxValues.get(lowBlockIndex)) - return lowBlockIndex; + if (low <= high && targetRowID <= blockMaxValues.get(low)) + return low; - while (lowBlockIndex <= highBlockIndex) + while (low <= high) { - int midBlockIndex = lowBlockIndex + ((highBlockIndex - lowBlockIndex) >> 1) ; + int mid = low + ((high - low) >> 1) ; - long maxValueOfMidBlock = summary.maxValues.get(midBlockIndex); + long midVal = blockMaxValues.get(mid); - if (maxValueOfMidBlock < targetRowID) + if (midVal < targetRowID) { - lowBlockIndex = midBlockIndex + 1; + low = mid + 1; } - else if (maxValueOfMidBlock > targetRowID) + else if (midVal > targetRowID) { - highBlockIndex = midBlockIndex - 1; + high = mid - 1; } else { - // At this point the maximum value of the midway block matches our target. - // - // This following check is to see if we have a duplicate value in the last entry of the - // preceeding block. This check is only going to be successful if the entire current - // block is full of duplicates. - if (midBlockIndex > 0 && summary.maxValues.get(midBlockIndex - 1) == targetRowID) + // target found, but we need to check for duplicates + if (mid > 0 && blockMaxValues.get(mid - 1L) == targetRowID) { - // there is a duplicate in the preceeding block so restrict search to finish - // at that block - highBlockIndex = midBlockIndex - 1; + // there are duplicates, pivot left + high = mid - 1; } else { // no duplicates - return midBlockIndex; + return mid; } } } - return -(lowBlockIndex + 1); // target not found + return -(low + 1); // target not found } private void lastPosInBlock(int block) { // blockMaxValues is integer only - actualPosting = summary.maxValues.get(block); + actualSegmentRowId = Math.toIntExact(blockMaxValues.get(block)); //upper bound, since we might've advanced to the last block, but upper bound is enough - totalPostingsRead += (summary.blockSize - postingIndex) + (block - blockIndex + 1) * (long)summary.blockSize; + totalPostingsRead += (blockEntries - blockIdx) + (block - postingsBlockIdx + 1) * blockEntries; - blockIndex = block + 1; - postingIndex = summary.blockSize; + postingsBlockIdx = block + 1; + blockIdx = blockEntries; } @Override - public long nextPosting() throws IOException + public int nextPosting() throws IOException { - long next = peekNext(); + final int next = peekNext(); if (next != END_OF_STREAM) { advanceOnePosition(next); @@ -289,73 +357,90 @@ public long nextPosting() throws IOException return next; } - private long peekNext() throws IOException + @VisibleForTesting + int getBlockEntries() + { + return blockEntries; + } + + private int peekNext() throws IOException { - if (totalPostingsRead >= summary.numPostings) + if (totalPostingsRead >= numPostings) { return END_OF_STREAM; } - if (postingIndex == summary.blockSize) + if (blockIdx == blockEntries) { reBuffer(); } - return actualPosting + nextFoRValue(); + return actualSegmentRowId + nextRowDelta(); } - private int nextFoRValue() + private int nextRowDelta() { - long id = currentFoRValues.get(postingIndex); + if (currentFORValues == null) + { + currentFrequency = Integer.MIN_VALUE; + return 0; + } + + long offset = readFrequencies ? 2L * blockIdx : blockIdx; + long id = currentFORValues.get(offset); + if (readFrequencies) + currentFrequency = Math.toIntExact(currentFORValues.get(offset + 1)); postingsDecoded++; return Math.toIntExact(id); } - private void advanceOnePosition(long nextPosting) + private void advanceOnePosition(int nextRowID) { - actualPosting = nextPosting; + actualSegmentRowId = nextRowID; totalPostingsRead++; - postingIndex++; + blockIdx++; } private void reBuffer() throws IOException { - long pointer = summary.offsets.get(blockIndex); - if (pointer < 4) - { + final long pointer = blockOffsets.get(postingsBlockIdx); + if (pointer < 4) { // the first 4 bytes must be CODEC_MAGIC - throw new CorruptIndexException(String.format("Invalid block offset %d for postings block idx %d", pointer, blockIndex), input); + throw new CorruptIndexException(String.format("Invalid block offset %d for postings block idx %d", pointer, postingsBlockIdx), input); } + input.seek(pointer); - long left = summary.numPostings - totalPostingsRead; + final long left = numPostings - totalPostingsRead; assert left > 0; readFoRBlock(input); - blockIndex++; - postingIndex = 0; + postingsBlockIdx++; + blockIdx = 0; } private void readFoRBlock(IndexInput in) throws IOException { - if (blockIndex == 0) - actualPosting = in.readVLong(); - - byte bitsPerValue = in.readByte(); + final byte bitsPerValue = in.readByte(); - long currentPosition = in.getFilePointer(); + currentPosition = in.getFilePointer(); if (bitsPerValue == 0) { // If bitsPerValue is 0 then all the values in the block are the same - currentFoRValues = LongValues.ZEROES; + currentFORValues = LongValues.ZEROES; return; } else if (bitsPerValue > 64) { throw new CorruptIndexException( - String.format("Postings list #%s block is corrupted. Bits per value should be no more than 64 and is %d.", blockIndex, bitsPerValue), input); + String.format("Postings list #%s block is corrupted. Bits per value should be no more than 64 and is %d.", postingsBlockIdx, bitsPerValue), input); } - currentFoRValues = DirectReader.getInstance(seekingInput, bitsPerValue, currentPosition); + currentFORValues = LuceneCompat.directReaderGetInstance(seekingInput, bitsPerValue, currentPosition); + } + + @Override + public int frequency() { + return currentFrequency; } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java index 545710c8613a..b62cacc7372a 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java @@ -24,27 +24,30 @@ import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.agrona.collections.LongArrayList; -import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput; -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.postings.PostingList; +import org.apache.cassandra.index.sai.disk.oldlucene.DirectWriterAdapter; +import org.apache.cassandra.index.sai.disk.oldlucene.LuceneCompat; +import org.apache.cassandra.index.sai.disk.oldlucene.ResettableByteBuffersIndexOutput; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; import org.apache.lucene.store.DataOutput; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.util.packed.DirectWriter; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.Math.max; +import static java.lang.Math.min; + /** * Encodes, compresses and writes postings lists to disk. - *

    - * All postings in the posting list are delta encoded, then deltas are divided into blocks for compression. - * The deltas are based on the final value of the previous block. For the first block in the posting list - * the first value in the block is written as a VLong prior to block delta encodings. + * + * All row IDs in the posting list are delta encoded, then deltas are divided into blocks for compression. *

    * In packed blocks, longs are encoded with the same bit width (FoR compression). The block size (i.e. number of * longs inside block) is fixed (currently 128). Additionally blocks that are all the same value are encoded in an @@ -56,77 +59,92 @@ * *

    * Packed blocks are favoured, meaning when the postings are long enough, {@link PostingsWriter} will try - * to encode most data as a packed block. Take a term with 259 postings as an example, the first 256 postings are encoded + * to encode most data as a packed block. Take a term with 259 row IDs as an example, the first 256 IDs are encoded * as two packed blocks, while the remaining 3 are encoded as one VLong block. *

    *

    - * Each posting list ends with a block summary containing metadata and a skip table, written right after all postings - * blocks. Skip interval is the same as block size, and each skip entry points to the end of each block. - * Skip table consist of block offsets and last values of each block, compressed as two FoR blocks. + * Each posting list ends with a meta section and a skip table, that are written right after all postings blocks. Skip + * interval is the same as block size, and each skip entry points to the end of each block. Skip table consist of + * block offsets and maximum rowids of each block, compressed as two FoR blocks. *

    * * Visual representation of the disk format: *
      *
    - * +========+========================+=====+==============+===============+===============+=====+========================+========+
    - * | HEADER | POSTINGS LIST (TERM 1)                                                      | ... | POSTINGS LIST (TERM N) | FOOTER |
    - * +========+========================+=====+==============+===============+===============+=====+========================+========+
    - *          | FIRST VALUE| FOR BLOCK (1)| ... | FOR BLOCK (N)| BLOCK SUMMARY              |
    - *          +---------------------------+-----+--------------+---------------+------------+
    - *                                                           | BLOCK SIZE    |            |
    - *                                                           | LIST SIZE     | SKIP TABLE |
    - *                                                           +---------------+------------+
    - *                                                                           | BLOCKS POS.|
    - *                                                                           | MAX VALUES |
    - *                                                                           +------------+
    + * +========+========================+=====+==============+===============+============+=====+========================+========+
    + * | HEADER | POSTINGS LIST (TERM 1)                                                   | ... | POSTINGS LIST (TERM N) | FOOTER |
    + * +========+========================+=====+==============+===============+============+=====+========================+========+
    + *          | FOR BLOCK (1)          | ... | FOR BLOCK (N)| BLOCK SUMMARY              |
    + *          +------------------------+-----+--------------+---------------+------------+
    + *                                                        | BLOCK SIZE    |            |
    + *                                                        | LIST SIZE     | SKIP TABLE |
    + *                                                        +---------------+------------+
    + *                                                                        | BLOCKS POS.|
    + *                                                                        | MAX ROWIDS |
    + *                                                                        +------------+
      *
      *  
    */ @NotThreadSafe public class PostingsWriter implements Closeable { + protected static final Logger logger = LoggerFactory.getLogger(PostingsWriter.class); + // import static org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat.BLOCK_SIZE; - private final static int BLOCK_SIZE = 128; + private final static int BLOCK_ENTRIES = 128; private static final String POSTINGS_MUST_BE_SORTED_ERROR_MSG = "Postings must be sorted ascending, got [%s] after [%s]"; private final IndexOutput dataOutput; - private final int blockSize; + private final int blockEntries; private final long[] deltaBuffer; + private final int[] freqBuffer; // frequency is capped at 255 private final LongArrayList blockOffsets = new LongArrayList(); - private final LongArrayList blockMaximumPostings = new LongArrayList(); - private final ResettableByteBuffersIndexOutput inMemoryOutput = new ResettableByteBuffersIndexOutput("blockOffsets"); + private final LongArrayList blockMaxIDs = new LongArrayList(); + private final ResettableByteBuffersIndexOutput inMemoryOutput; private final long startOffset; private int bufferUpto; - private long firstPosting = Long.MIN_VALUE; - private long lastPosting = Long.MIN_VALUE; - private long maxDelta; + private long lastSegmentRowId; + // This number is the count of row ids written to the postings for this segment. Because a segment row id can be in + // multiple postings list for the segment, this number could exceed Integer.MAX_VALUE, so we use a long. private long totalPostings; + private final boolean writeFrequencies; + + public PostingsWriter(IndexComponents.ForWrite components) throws IOException + { + this(components, BLOCK_ENTRIES); + } - public PostingsWriter(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier) throws IOException + public PostingsWriter(IndexComponents.ForWrite components, boolean writeFrequencies) throws IOException { - this(indexDescriptor, indexIdentifier, BLOCK_SIZE); + this(components.addOrGet(IndexComponentType.POSTING_LISTS).openOutput(true), BLOCK_ENTRIES, writeFrequencies); } - public PostingsWriter(IndexOutputWriter dataOutput) throws IOException + + public PostingsWriter(IndexOutput dataOutput) throws IOException { - this(dataOutput, BLOCK_SIZE); + this(dataOutput, BLOCK_ENTRIES, false); } @VisibleForTesting - PostingsWriter(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier, int blockSize) throws IOException + PostingsWriter(IndexComponents.ForWrite components, int blockEntries) throws IOException { - this(indexDescriptor.openPerIndexOutput(IndexComponent.POSTING_LISTS, indexIdentifier, true), blockSize); + this(components.addOrGet(IndexComponentType.POSTING_LISTS).openOutput(true), blockEntries, false); } - private PostingsWriter(IndexOutputWriter dataOutput, int blockSize) throws IOException + private PostingsWriter(IndexOutput dataOutput, int blockEntries, boolean writeFrequencies) throws IOException { - this.blockSize = blockSize; + assert dataOutput instanceof IndexOutputWriter; + logger.debug("Creating postings writer for output {}", dataOutput); + this.writeFrequencies = writeFrequencies; + this.blockEntries = blockEntries; this.dataOutput = dataOutput; startOffset = dataOutput.getFilePointer(); - deltaBuffer = new long[blockSize]; + deltaBuffer = new long[blockEntries]; + freqBuffer = new int[blockEntries]; + inMemoryOutput = LuceneCompat.getResettableByteBuffersIndexOutput(dataOutput.order(), 1024, "blockOffsets", dataOutput.version()); SAICodecUtils.writeHeader(dataOutput); } @@ -172,23 +190,22 @@ public long write(PostingList postings) throws IOException checkArgument(postings != null, "Expected non-null posting list."); checkArgument(postings.size() > 0, "Expected non-empty posting list."); - lastPosting = Long.MIN_VALUE; resetBlockCounters(); blockOffsets.clear(); - blockMaximumPostings.clear(); + blockMaxIDs.clear(); - long posting; + int segmentRowId; // When postings list are merged, we don't know exact size, just an upper bound. // We need to count how many postings we added to the block ourselves. int size = 0; - while ((posting = postings.nextPosting()) != PostingList.END_OF_STREAM) + while ((segmentRowId = postings.nextPosting()) != PostingList.END_OF_STREAM) { - writePosting(posting); + writePosting(segmentRowId, postings.frequency()); size++; totalPostings++; } - - assert size > 0 : "No postings were written"; + if (size == 0) + return -1; finish(); @@ -202,63 +219,56 @@ public long getTotalPostings() return totalPostings; } - private void writePosting(long posting) throws IOException - { - if (lastPosting == Long.MIN_VALUE) - { - firstPosting = posting; - deltaBuffer[bufferUpto++] = 0; - } - else - { - if (posting < lastPosting) - throw new IllegalArgumentException(String.format(POSTINGS_MUST_BE_SORTED_ERROR_MSG, posting, lastPosting)); - long delta = posting - lastPosting; - maxDelta = max(maxDelta, delta); - deltaBuffer[bufferUpto++] = delta; - } - lastPosting = posting; + private void writePosting(long segmentRowId, int freq) throws IOException { + if (!(segmentRowId >= lastSegmentRowId || lastSegmentRowId == 0)) + throw new IllegalArgumentException(String.format(POSTINGS_MUST_BE_SORTED_ERROR_MSG, segmentRowId, lastSegmentRowId)); - if (bufferUpto == blockSize) - { - addBlockToSkipTable(); - writePostingsBlock(); + assert freq > 0; + final long delta = segmentRowId - lastSegmentRowId; + deltaBuffer[bufferUpto] = delta; + freqBuffer[bufferUpto] = min(freq, 255); + bufferUpto++; + + if (bufferUpto == blockEntries) { + addBlockToSkipTable(segmentRowId); + writePostingsBlock(bufferUpto); resetBlockCounters(); } + lastSegmentRowId = segmentRowId; } private void finish() throws IOException { if (bufferUpto > 0) { - addBlockToSkipTable(); - writePostingsBlock(); + addBlockToSkipTable(lastSegmentRowId); + + writePostingsBlock(bufferUpto); } } private void resetBlockCounters() { - firstPosting = Long.MIN_VALUE; bufferUpto = 0; - maxDelta = 0; + lastSegmentRowId = 0; } - private void addBlockToSkipTable() + private void addBlockToSkipTable(long maxSegmentRowID) { blockOffsets.add(dataOutput.getFilePointer()); - blockMaximumPostings.add(lastPosting); + blockMaxIDs.add(maxSegmentRowID); } private void writeSummary(int exactSize) throws IOException { - dataOutput.writeVInt(blockSize); + dataOutput.writeVInt(blockEntries); dataOutput.writeVInt(exactSize); writeSkipTable(); } private void writeSkipTable() throws IOException { - assert blockOffsets.size() == blockMaximumPostings.size(); + assert blockOffsets.size() == blockMaxIDs.size(); dataOutput.writeVInt(blockOffsets.size()); // compressing offsets in memory first, to know the exact length (with padding) @@ -267,34 +277,32 @@ private void writeSkipTable() throws IOException writeSortedFoRBlock(blockOffsets, inMemoryOutput); dataOutput.writeVLong(inMemoryOutput.getFilePointer()); inMemoryOutput.copyTo(dataOutput); - writeSortedFoRBlock(blockMaximumPostings, dataOutput); + writeSortedFoRBlock(blockMaxIDs, dataOutput); } - private void writePostingsBlock() throws IOException - { - final int bitsPerValue = maxDelta == 0 ? 0 : DirectWriter.unsignedBitsRequired(maxDelta); - - // If we have a first posting, indicating that this is the first block in the posting list - // then write it prior to the deltas. - if (firstPosting != Long.MIN_VALUE) - dataOutput.writeVLong(firstPosting); - + private void writePostingsBlock(int entries) throws IOException { + // Find max value to determine bits needed + long maxValue = 0; + for (int i = 0; i < entries; i++) { + maxValue = max(maxValue, deltaBuffer[i]); + if (writeFrequencies) + maxValue = max(maxValue, freqBuffer[i]); + } + + // Use the maximum bits needed for either value type + final int bitsPerValue = maxValue == 0 ? 0 : LuceneCompat.directWriterUnsignedBitsRequired(dataOutput.order(), maxValue); + dataOutput.writeByte((byte) bitsPerValue); - if (bitsPerValue > 0) - { - final DirectWriter writer = DirectWriter.getInstance(dataOutput, blockSize, bitsPerValue); - for (int index = 0; index < bufferUpto; ++index) - { - writer.add(deltaBuffer[index]); - } - if (bufferUpto < blockSize) - { - // Pad the rest of the block with 0, so we don't write invalid - // values from previous blocks - for (int index = bufferUpto; index < blockSize; index++) - { - writer.add(0); - } + if (bitsPerValue > 0) { + // Write interleaved [delta][freq] pairs + final DirectWriterAdapter writer = LuceneCompat.directWriterGetInstance(dataOutput.order(), + dataOutput, + writeFrequencies ? entries * 2L : entries, + bitsPerValue); + for (int i = 0; i < entries; ++i) { + writer.add(deltaBuffer[i]); + if (writeFrequencies) + writer.add(freqBuffer[i]); } writer.finish(); } @@ -302,14 +310,14 @@ private void writePostingsBlock() throws IOException private void writeSortedFoRBlock(LongArrayList values, IndexOutput output) throws IOException { + assert !values.isEmpty(); final long maxValue = values.getLong(values.size() - 1); - assert values.size() > 0; - final int bitsPerValue = maxValue == 0 ? 0 : DirectWriter.unsignedBitsRequired(maxValue); + final int bitsPerValue = maxValue == 0 ? 0 : LuceneCompat.directWriterUnsignedBitsRequired(output.order(), maxValue); output.writeByte((byte) bitsPerValue); if (bitsPerValue > 0) { - final DirectWriter writer = DirectWriter.getInstance(output, values.size(), bitsPerValue); + final DirectWriterAdapter writer = LuceneCompat.directWriterGetInstance(output.order(), output, values.size(), bitsPerValue); for (int i = 0; i < values.size(); ++i) { writer.add(values.getLong(i)); diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/ReorderingPostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/ReorderingPostingList.java new file mode 100644 index 000000000000..f43c7c4e8dce --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/ReorderingPostingList.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1.postings; + +import java.io.IOException; +import java.util.function.ToIntFunction; + +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.lucene.util.LongHeap; + +/** + * A posting list for ANN search results. Transforms results from similarity order to rowId order. + */ +public class ReorderingPostingList implements PostingList +{ + private final LongHeap segmentRowIds; + private final int size; + + public ReorderingPostingList(CloseableIterator source, ToIntFunction rowIdTransformer) + { + segmentRowIds = new LongHeap(32); + int n = 0; + try (source) + { + while (source.hasNext()) + { + segmentRowIds.push(rowIdTransformer.applyAsInt(source.next())); + n++; + } + } + this.size = n; + } + + @Override + public int nextPosting() throws IOException + { + if (segmentRowIds.size() == 0) + return PostingList.END_OF_STREAM; + return (int) segmentRowIds.pop(); + } + + @Override + public int size() + { + return size; + } + + @Override + public int advance(int targetRowID) throws IOException + { + int rowId; + do + { + rowId = nextPosting(); + } while (rowId < targetRowID); + return rowId; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/ScanningPostingsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/ScanningPostingsReader.java new file mode 100644 index 000000000000..5c9da6169e08 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/ScanningPostingsReader.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1.postings; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.metrics.QueryEventListener; + +/** + * An sub-class of the {@code PostingsReader} that does not allow the {@code PostingList} to be + * advanced and does not support mapping row ids to primary keys. + * + * It is used during index mergers to sequentially scan the postings in order using {@code nextPosting}. + */ +public class ScanningPostingsReader extends PostingsReader +{ + public ScanningPostingsReader(IndexInput input, BlocksSummary summary, boolean readFrequencies) throws IOException + { + super(input, summary, readFrequencies, QueryEventListener.PostingListEventListener.NO_OP, InputCloser.NOOP); + } + + @Override + public int advance(int targetRowId) + { + throw new UnsupportedOperationException("Cannot advance a scanning postings reader"); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcher.java deleted file mode 100644 index 802797a682bd..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcher.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.Closeable; -import java.io.IOException; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles; -import org.apache.cassandra.index.sai.disk.v1.postings.PostingListRangeIterator; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.postings.PeekablePostingList; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.utils.CloseableIterator; - -/** - * Abstract reader for individual segments of an on-disk index. - *

    - * Accepts shared resources (token/offset file readers), and uses them to perform lookups against on-disk data - * structures. - */ -public abstract class IndexSegmentSearcher implements SegmentOrdering, Closeable -{ - final PrimaryKeyMap.Factory primaryKeyMapFactory; - final PerColumnIndexFiles indexFiles; - final SegmentMetadata metadata; - final StorageAttachedIndex index; - - IndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, - PerColumnIndexFiles perIndexFiles, - SegmentMetadata segmentMetadata, - StorageAttachedIndex index) - { - this.primaryKeyMapFactory = primaryKeyMapFactory; - this.indexFiles = perIndexFiles; - this.metadata = segmentMetadata; - this.index = index; - } - - public static IndexSegmentSearcher open(PrimaryKeyMap.Factory primaryKeyMapFactory, - SSTableId sstableId, - PerColumnIndexFiles indexFiles, - SegmentMetadata segmentMetadata, - StorageAttachedIndex index) throws IOException - { - if (index.termType().isVector()) - return new VectorIndexSegmentSearcher(primaryKeyMapFactory, sstableId, indexFiles, segmentMetadata, index); - else if (index.termType().isLiteral()) - return new LiteralIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, index); - else - return new NumericIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, index); - } - - /** - * @return memory usage of underlying on-disk data structure - */ - public abstract long indexFileCacheSize(); - - /** - * Search on-disk index synchronously. - * - * @param expression to filter on disk index - * @param queryContext to track per sstable cache and per query metrics - * - * @return {@link KeyRangeIterator} with matches for the given expression - */ - public abstract KeyRangeIterator search(Expression expression, AbstractBounds keyRange, QueryContext queryContext) throws IOException; - - /** - * Order the rows by the given expression. - * - * @param orderer the object containing the ordering logic - * @param keyRange key range specific in read command, used by ANN index - * @param context to track per sstable cache and per query metrics - * - * @return an iterator of {@link PrimaryKeyWithScore} in descending score order - */ - public CloseableIterator orderBy(Expression orderer, AbstractBounds keyRange, QueryContext context) throws IOException - { - throw new UnsupportedOperationException(); - } - - - KeyRangeIterator toPrimaryKeyIterator(PostingList postingList, QueryContext queryContext) throws IOException - { - if (postingList == null || postingList.size() == 0) - return KeyRangeIterator.empty(); - - IndexSegmentSearcherContext searcherContext = new IndexSegmentSearcherContext(metadata.minKey, - metadata.maxKey, - metadata.rowIdOffset, - queryContext, - PeekablePostingList.makePeekable(postingList)); - - return new PostingListRangeIterator(index.identifier(), primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(), searcherContext); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcherContext.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcherContext.java deleted file mode 100644 index 6cc5a1121ab9..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcherContext.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.segment; - -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.postings.PeekablePostingList; -import org.apache.cassandra.index.sai.utils.PrimaryKey; - -public class IndexSegmentSearcherContext -{ - public final QueryContext context; - public final PeekablePostingList postingList; - - public final PrimaryKey minimumKey; - public final PrimaryKey maximumKey; - public final long segmentRowIdOffset; - - public IndexSegmentSearcherContext(PrimaryKey minimumKey, - PrimaryKey maximumKey, - long segmentRowIdOffset, - QueryContext context, - PeekablePostingList postingList) - { - this.context = context; - this.postingList = postingList; - - this.segmentRowIdOffset = segmentRowIdOffset; - - this.minimumKey = minimumKey; - this.maximumKey = maximumKey; - } - - public long count() - { - return postingList.size(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentSearcher.java deleted file mode 100644 index 3aa566e65ad6..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentSearcher.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.util.Map; - -import com.google.common.base.MoreObjects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners; -import org.apache.cassandra.index.sai.metrics.QueryEventListener; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; - -/** - * Executes {@link Expression}s against the trie-based terms dictionary for an individual index segment. - */ -public class LiteralIndexSegmentSearcher extends IndexSegmentSearcher -{ - private static final Logger logger = LoggerFactory.getLogger(LiteralIndexSegmentSearcher.class); - - private final LiteralIndexSegmentTermsReader reader; - private final QueryEventListener.TrieIndexEventListener perColumnEventListener; - - LiteralIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, - PerColumnIndexFiles perIndexFiles, - SegmentMetadata segmentMetadata, - StorageAttachedIndex index) throws IOException - { - super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, index); - - long root = metadata.getIndexRoot(IndexComponent.TERMS_DATA); - assert root >= 0; - - perColumnEventListener = (QueryEventListener.TrieIndexEventListener)index.columnQueryMetrics(); - - Map map = metadata.componentMetadatas.get(IndexComponent.TERMS_DATA).attributes; - String footerPointerString = map.get(SAICodecUtils.FOOTER_POINTER); - long footerPointer = footerPointerString == null ? -1 : Long.parseLong(footerPointerString); - - reader = new LiteralIndexSegmentTermsReader(index.identifier(), indexFiles.termsData(), indexFiles.postingLists(), root, footerPointer); - } - - @Override - public long indexFileCacheSize() - { - // trie has no pre-allocated memory. - return 0; - } - - @Override - public KeyRangeIterator search(Expression expression, AbstractBounds keyRange, QueryContext queryContext) throws IOException - { - if (logger.isTraceEnabled()) - logger.trace(index.identifier().logMessage("Searching on expression '{}'..."), expression); - - if (!expression.getIndexOperator().isEquality()) - throw new IllegalArgumentException(index.identifier().logMessage("Unsupported expression: " + expression)); - - ByteComparable term = v -> index.termType().asComparableBytes(expression.lower().value.encoded, v); - QueryEventListener.TrieIndexEventListener listener = MulticastQueryEventListeners.of(queryContext, perColumnEventListener); - return toPrimaryKeyIterator(reader.exactMatch(term, listener, queryContext), queryContext); - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this).add("index", index).toString(); - } - - @Override - public void close() - { - reader.close(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentTermsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentTermsReader.java deleted file mode 100644 index 6c6f81cddf3c..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentTermsReader.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.Closeable; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.exceptions.QueryCancelledException; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; -import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader; -import org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryReader; -import org.apache.cassandra.index.sai.metrics.QueryEventListener; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.Clock; -import org.apache.cassandra.utils.Throwables; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.lucene.store.IndexInput; - -import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.validate; - -/** - * Synchronous reader of terms dictionary and postings lists to produce a {@link PostingList} with matching row ids. - * - * {@link #exactMatch(ByteComparable, QueryEventListener.TrieIndexEventListener, QueryContext)} does: - *

      - *
    • {@link TermQuery#lookupPostingsOffset(ByteComparable)}: does term dictionary lookup to find the posting list file - * position
    • - *
    • {@link TermQuery#getPostingsReader(long)}: reads posting list block summary and initializes posting read which - * reads the first block of the posting list into memory
    • - *
    - */ -public class LiteralIndexSegmentTermsReader implements Closeable -{ - private static final Logger logger = LoggerFactory.getLogger(LiteralIndexSegmentTermsReader.class); - - private final IndexIdentifier indexIdentifier; - private final FileHandle termDictionaryFile; - private final FileHandle postingsFile; - private final long termDictionaryRoot; - - public LiteralIndexSegmentTermsReader(IndexIdentifier indexIdentifier, - FileHandle termsData, - FileHandle postingLists, - long root, - long termsFooterPointer) throws IOException - { - this.indexIdentifier = indexIdentifier; - termDictionaryFile = termsData; - postingsFile = postingLists; - termDictionaryRoot = root; - - try (final IndexInput indexInput = IndexFileUtils.instance.openInput(termDictionaryFile)) - { - validate(indexInput, termsFooterPointer); - } - - try (final IndexInput indexInput = IndexFileUtils.instance.openInput(postingsFile)) - { - validate(indexInput); - } - } - - @Override - public void close() - { - FileUtils.closeQuietly(termDictionaryFile); - FileUtils.closeQuietly(postingsFile); - } - - public PostingList exactMatch(ByteComparable term, QueryEventListener.TrieIndexEventListener perQueryEventListener, QueryContext context) - { - perQueryEventListener.onSegmentHit(); - return new TermQuery(term, perQueryEventListener, context).execute(); - } - - @VisibleForTesting - public class TermQuery - { - private final IndexInput postingsInput; - private final IndexInput postingsSummaryInput; - private final QueryEventListener.TrieIndexEventListener listener; - private final long lookupStartTime; - private final QueryContext context; - private final ByteComparable term; - - TermQuery(ByteComparable term, QueryEventListener.TrieIndexEventListener listener, QueryContext context) - { - this.listener = listener; - postingsInput = IndexFileUtils.instance.openInput(postingsFile); - postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile); - this.term = term; - lookupStartTime = Clock.Global.nanoTime(); - this.context = context; - } - - public PostingList execute() - { - try - { - long postingOffset = lookupPostingsOffset(term); - if (postingOffset == PostingList.OFFSET_NOT_FOUND) - { - FileUtils.closeQuietly(postingsInput); - FileUtils.closeQuietly(postingsSummaryInput); - return null; - } - - context.checkpoint(); - - // when posting is found, resources will be closed when posting reader is closed. - return getPostingsReader(postingOffset); - } - catch (Throwable e) - { - if (!(e instanceof QueryCancelledException)) - logger.error(indexIdentifier.logMessage("Failed to execute term query"), e); - - closeOnException(); - throw Throwables.cleaned(e); - } - } - - private void closeOnException() - { - FileUtils.closeQuietly(postingsInput); - FileUtils.closeQuietly(postingsSummaryInput); - } - - public long lookupPostingsOffset(ByteComparable term) - { - try (TrieTermsDictionaryReader reader = new TrieTermsDictionaryReader(termDictionaryFile.instantiateRebufferer(null), termDictionaryRoot)) - { - final long offset = reader.exactMatch(term); - - listener.onTraversalComplete(Clock.Global.nanoTime() - lookupStartTime, TimeUnit.NANOSECONDS); - - if (offset == TrieTermsDictionaryReader.NOT_FOUND) - return PostingList.OFFSET_NOT_FOUND; - - return offset; - } - } - - public PostingsReader getPostingsReader(long offset) throws IOException - { - PostingsReader.BlocksSummary header = new PostingsReader.BlocksSummary(postingsSummaryInput, offset); - - return new PostingsReader(postingsInput, header, listener.postingListEventListener()); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/NumericIndexSegmentSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/NumericIndexSegmentSearcher.java deleted file mode 100644 index 137e635fd06c..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/NumericIndexSegmentSearcher.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.lang.invoke.MethodHandles; - -import com.google.common.base.MoreObjects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles; -import org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeReader; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners; -import org.apache.cassandra.index.sai.metrics.QueryEventListener; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.lucene.index.CorruptIndexException; - -import static org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeQueries.balancedTreeQueryFrom; - -/** - * Executes {@link Expression}s against the balanced tree for an individual index segment. - */ -public class NumericIndexSegmentSearcher extends IndexSegmentSearcher -{ - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final BlockBalancedTreeReader treeReader; - private final QueryEventListener.BalancedTreeEventListener perColumnEventListener; - - NumericIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, - PerColumnIndexFiles perIndexFiles, - SegmentMetadata segmentMetadata, - StorageAttachedIndex index) throws IOException - { - super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, index); - - final long treePosition = metadata.getIndexRoot(IndexComponent.BALANCED_TREE); - if (treePosition < 0) - throw new CorruptIndexException(index.identifier().logMessage("The tree position is less than zero."), IndexComponent.BALANCED_TREE.name); - final long postingsPosition = metadata.getIndexRoot(IndexComponent.POSTING_LISTS); - if (postingsPosition < 0) - throw new CorruptIndexException(index.identifier().logMessage("The postings position is less than zero."), IndexComponent.BALANCED_TREE.name); - - treeReader = new BlockBalancedTreeReader(index.identifier(), - indexFiles.balancedTree(), - treePosition, - indexFiles.postingLists(), - postingsPosition); - perColumnEventListener = (QueryEventListener.BalancedTreeEventListener)index.columnQueryMetrics(); - } - - @Override - public long indexFileCacheSize() - { - return treeReader.memoryUsage(); - } - - @Override - public KeyRangeIterator search(Expression exp, AbstractBounds keyRange, QueryContext context) throws IOException - { - if (logger.isTraceEnabled()) - logger.trace(index.identifier().logMessage("Searching on expression '{}'..."), exp); - - if (exp.getIndexOperator().isEqualityOrRange()) - { - final BlockBalancedTreeReader.IntersectVisitor query = balancedTreeQueryFrom(exp, treeReader.getBytesPerValue()); - QueryEventListener.BalancedTreeEventListener listener = MulticastQueryEventListeners.of(context, perColumnEventListener); - return toPrimaryKeyIterator(treeReader.intersect(query, listener, context), context); - } - else - { - throw new IllegalArgumentException(index.identifier().logMessage("Unsupported expression during index query: " + exp)); - } - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this) - .add("index", index) - .add("count", treeReader.getPointCount()) - .add("bytesPerValue", treeReader.getBytesPerValue()) - .toString(); - } - - @Override - public void close() - { - treeReader.close(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/Segment.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/Segment.java deleted file mode 100644 index 2354e798020d..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/Segment.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.Closeable; -import java.io.IOException; -import java.util.List; - -import com.google.common.annotations.VisibleForTesting; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.SSTableContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.CloseableIterator; - -/** - * Each segment represents an on-disk index structure (balanced tree/terms/postings) flushed by memory limit or token boundaries. - * It also helps to reduce resource consumption for read requests as only segments that intersect with read request data - * range need to be loaded. - */ -public class Segment implements SegmentOrdering, Closeable -{ - private final Token.KeyBound minKeyBound; - private final Token.KeyBound maxKeyBound; - - // per-segment - public final SegmentMetadata metadata; - - private final IndexSegmentSearcher index; - - public Segment(StorageAttachedIndex index, SSTableContext sstableContext, PerColumnIndexFiles indexFiles, SegmentMetadata metadata) throws IOException - { - this.minKeyBound = metadata.minKey.token().minKeyBound(); - this.maxKeyBound = metadata.maxKey.token().maxKeyBound(); - - this.metadata = metadata; - - this.index = IndexSegmentSearcher.open(sstableContext.primaryKeyMapFactory, sstableContext.sstable.getId(), indexFiles, metadata, index); - } - - @VisibleForTesting - public Segment(Token minKey, Token maxKey) - { - this.metadata = null; - this.minKeyBound = minKey.minKeyBound(); - this.maxKeyBound = maxKey.maxKeyBound(); - this.index = null; - } - - /** - * @return true if current segment intersects with query key range - */ - public boolean intersects(AbstractBounds keyRange) - { - if (keyRange instanceof Range && ((Range)keyRange).isWrapAround()) - return keyRange.contains(minKeyBound) || keyRange.contains(maxKeyBound); - - int cmp = keyRange.right.compareTo(minKeyBound); - // if right is minimum, it means right is the max token and bigger than maxKey. - // if right bound is less than minKeyBound, no intersection - if (!keyRange.right.isMinimum() && (!keyRange.inclusiveRight() && cmp == 0 || cmp < 0)) - return false; - - cmp = keyRange.left.compareTo(maxKeyBound); - // if left bound is bigger than maxKeyBound, no intersection - return (keyRange.isStartInclusive() || cmp != 0) && cmp <= 0; - } - - public long indexFileCacheSize() - { - return index == null ? 0 : index.indexFileCacheSize(); - } - - /** - * Search on-disk index synchronously - * - * @param expression to filter on disk index - * @param context to track per sstable cache and per query metrics - - * @return range iterator that matches given expression - */ - public KeyRangeIterator search(Expression expression, AbstractBounds keyRange, QueryContext context) throws IOException - { - return index.search(expression, keyRange, context); - } - - /** - * Order the on-disk index synchronously and produce an iterator in score order - * - * @param orderer the expression to use when searching the on disk index - * @param keyRange key range specific in read command, used by ANN index - * @param context to track per sstable cache and per query metrics - * @return an iterator of {@link PrimaryKeyWithScore} in score order - */ - public CloseableIterator orderBy(Expression orderer, AbstractBounds keyRange, QueryContext context) throws IOException - { - return index.orderBy(orderer, keyRange, context); - } - - @Override - public CloseableIterator orderResultsBy(QueryContext context, List results, Expression orderer) throws IOException - { - return index.orderResultsBy(context, results, orderer); - } - - @Override - public void close() - { - FileUtils.closeQuietly(index); - } - - @Override - public String toString() - { - return String.format("Segment{metadata=%s}", metadata); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java deleted file mode 100644 index cd512a756890..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.concurrent.NotThreadSafe; - -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.disk.v1.bbtree.NumericIndexWriter; -import org.apache.cassandra.index.sai.disk.v1.trie.LiteralIndexWriter; -import org.apache.cassandra.index.sai.disk.v1.vector.OnHeapGraph; -import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter; -import org.apache.cassandra.index.sai.utils.PrimaryKey; - -/** - * Creates an on-heap index data structure to be flushed to an SSTable index. - */ -@NotThreadSafe -public abstract class SegmentBuilder -{ - private static final Logger logger = LoggerFactory.getLogger(SegmentBuilder.class); - - // Served as safe net in case memory limit is not triggered or when merger merges small segments.. - public static final long LAST_VALID_SEGMENT_ROW_ID = (Integer.MAX_VALUE / 2) - 1L; - private static long testLastValidSegmentRowId = -1; - - /** The number of column indexes being built globally. */ - private static final AtomicInteger ACTIVE_BUILDER_COUNT = new AtomicInteger(0); - - /** Minimum flush size, dynamically updated as segment builds are started and completed/aborted. */ - private static volatile long minimumFlushBytes; - private final NamedMemoryLimiter limiter; - private final long lastValidSegmentRowID; - private boolean flushed = false; - private boolean active = true; - // segment metadata - private long minSSTableRowId = -1; - private long maxSSTableRowId = -1; - private long segmentRowIdOffset = 0; - - // in token order - private PrimaryKey minKey; - private PrimaryKey maxKey; - // in termComparator order - private ByteBuffer minTerm; - private ByteBuffer maxTerm; - - final StorageAttachedIndex index; - long totalBytesAllocated; - int rowCount = 0; - int maxSegmentRowId = -1; - - public static class TrieSegmentBuilder extends SegmentBuilder - { - protected final SegmentTrieBuffer segmentTrieBuffer; - - public TrieSegmentBuilder(StorageAttachedIndex index, NamedMemoryLimiter limiter) - { - super(index, limiter); - - segmentTrieBuffer = new SegmentTrieBuffer(); - totalBytesAllocated = segmentTrieBuffer.memoryUsed(); - } - - @Override - protected long addInternal(ByteBuffer term, int segmentRowId) - { - return segmentTrieBuffer.add(v -> index.termType().asComparableBytes(term, v), term.limit(), segmentRowId); - } - - @Override - protected SegmentMetadata.ComponentMetadataMap flushInternal(IndexDescriptor indexDescriptor) throws IOException - { - SegmentWriter writer = index.termType().isLiteral() ? new LiteralIndexWriter(indexDescriptor, index.identifier()) - : new NumericIndexWriter(indexDescriptor, index.identifier(), index.termType().fixedSizeOf()); - - return writer.writeCompleteSegment(segmentTrieBuffer.iterator()); - } - - @Override - public boolean isEmpty() - { - return segmentTrieBuffer.numRows() == 0; - } - } - - public static class VectorSegmentBuilder extends SegmentBuilder - { - private final OnHeapGraph graphIndex; - - public VectorSegmentBuilder(StorageAttachedIndex index, NamedMemoryLimiter limiter) - { - super(index, limiter); - graphIndex = new OnHeapGraph<>(index.termType().indexType(), index.indexWriterConfig(), null); - } - - @Override - public boolean isEmpty() - { - return graphIndex.isEmpty(); - } - - @Override - protected long addInternal(ByteBuffer term, int segmentRowId) - { - return graphIndex.add(term, segmentRowId, OnHeapGraph.InvalidVectorBehavior.IGNORE); - } - - @Override - protected SegmentMetadata.ComponentMetadataMap flushInternal(IndexDescriptor indexDescriptor) throws IOException - { - return graphIndex.writeData(indexDescriptor, index.identifier(), p -> p); - } - } - - public static int getActiveBuilderCount() - { - return ACTIVE_BUILDER_COUNT.get(); - } - - private SegmentBuilder(StorageAttachedIndex index, NamedMemoryLimiter limiter) - { - this.index = index; - this.limiter = limiter; - lastValidSegmentRowID = testLastValidSegmentRowId >= 0 ? testLastValidSegmentRowId : LAST_VALID_SEGMENT_ROW_ID; - - minimumFlushBytes = limiter.limitBytes() / ACTIVE_BUILDER_COUNT.incrementAndGet(); - } - - public SegmentMetadata flush(IndexDescriptor indexDescriptor) throws IOException - { - assert !flushed : "Cannot flush an already flushed segment"; - flushed = true; - - if (getRowCount() == 0) - { - logger.warn(index.identifier().logMessage("No rows to index during flush of SSTable {}."), indexDescriptor.sstableDescriptor); - return null; - } - - SegmentMetadata.ComponentMetadataMap indexMetas = flushInternal(indexDescriptor); - - return new SegmentMetadata(segmentRowIdOffset, rowCount, minSSTableRowId, maxSSTableRowId, minKey, maxKey, minTerm, maxTerm, indexMetas); - } - - public long add(ByteBuffer term, PrimaryKey key, long sstableRowId) - { - assert !flushed : "Cannot add to a flushed segment."; - assert sstableRowId >= maxSSTableRowId; - minSSTableRowId = minSSTableRowId < 0 ? sstableRowId : minSSTableRowId; - maxSSTableRowId = sstableRowId; - - assert maxKey == null || maxKey.compareTo(key) <= 0; - if (minKey == null) - minKey = key; - maxKey = key; - - minTerm = index.termType().min(term, minTerm); - maxTerm = index.termType().max(term, maxTerm); - - if (rowCount == 0) - { - // use first global rowId in the segment as segment rowId offset - segmentRowIdOffset = sstableRowId; - } - - rowCount++; - - // segmentRowIdOffset should encode sstableRowId into Integer - int segmentRowId = castToSegmentRowId(sstableRowId, segmentRowIdOffset); - maxSegmentRowId = Math.max(maxSegmentRowId, segmentRowId); - - long bytesAllocated = addInternal(term, segmentRowId); - totalBytesAllocated += bytesAllocated; - - return bytesAllocated; - } - - public static int castToSegmentRowId(long sstableRowId, long segmentRowIdOffset) - { - return Math.toIntExact(sstableRowId - segmentRowIdOffset); - } - - public long totalBytesAllocated() - { - return totalBytesAllocated; - } - - public boolean hasReachedMinimumFlushSize() - { - return totalBytesAllocated >= minimumFlushBytes; - } - - public long getMinimumFlushBytes() - { - return minimumFlushBytes; - } - - /** - * This method does three things: - *

    - * 1. It decrements active builder count and updates the global minimum flush size to reflect that. - * 2. It releases the builder's memory against its limiter. - * 3. It defensively marks the builder inactive to make sure nothing bad happens if we try to close it twice. - * - * @return the number of bytes used by the memory limiter after releasing this builder - */ - public long release() - { - if (active) - { - minimumFlushBytes = limiter.limitBytes() / ACTIVE_BUILDER_COUNT.getAndDecrement(); - long used = limiter.decrement(totalBytesAllocated); - active = false; - return used; - } - - logger.warn(index.identifier().logMessage("Attempted to release storage-attached index segment builder memory after builder marked inactive.")); - return limiter.currentBytesUsed(); - } - - public abstract boolean isEmpty(); - - protected abstract long addInternal(ByteBuffer term, int segmentRowId); - - protected abstract SegmentMetadata.ComponentMetadataMap flushInternal(IndexDescriptor indexDescriptor) throws IOException; - - public int getRowCount() - { - return rowCount; - } - - /** - * @return true if next SSTable row ID exceeds max segment row ID - */ - public boolean exceedsSegmentLimit(long ssTableRowId) - { - if (getRowCount() == 0) - return false; - - // To handle the case where there are many non-indexable rows. eg. rowId-1 and rowId-3B are indexable, - // the rest are non-indexable. We should flush them as 2 separate segments, because rowId-3B is going - // to cause error in on-disk index structure with 2B limitation. - return ssTableRowId - segmentRowIdOffset > lastValidSegmentRowID; - } - - @VisibleForTesting - public static void updateLastValidSegmentRowId(long lastValidSegmentRowID) - { - testLastValidSegmentRowId = lastValidSegmentRowID; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentMetadata.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentMetadata.java deleted file mode 100644 index 3ae1bd55d4de..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentMetadata.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import com.google.common.collect.ImmutableMap; - -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.v1.MetadataSource; -import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.cassandra.utils.bytecomparable.ByteSource; -import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -import org.apache.lucene.store.DataInput; -import org.apache.lucene.store.IndexOutput; - -/** - * Multiple {@link SegmentMetadata} are stored in {@link IndexComponent#META} file, each corresponds to an on-disk - * index segment. - */ -public class SegmentMetadata -{ - private static final String NAME = "SegmentMetadata"; - - /** - * Used to retrieve sstableRowId which equals to offset plus segmentRowId. - */ - public final long rowIdOffset; - - /** - * Min and max sstable rowId in current segment. - *

    - * For index generated by compaction, minSSTableRowId is the same as segmentRowIdOffset. - * But for flush, segmentRowIdOffset is taken from previous segment's maxSSTableRowId. - */ - public final long minSSTableRowId; - public final long maxSSTableRowId; - - /** - * number of indexed rows (aka. a pair of term and segmentRowId) in the current segment - */ - public final long numRows; - - /** - * Ordered by their token position in current segment - */ - public final PrimaryKey minKey; - public final PrimaryKey maxKey; - - /** - * Minimum and maximum indexed column value ordered by its {@link org.apache.cassandra.db.marshal.AbstractType}. - */ - public final ByteBuffer minTerm; - public final ByteBuffer maxTerm; - - /** - * Root, offset, length for each index structure in the segment. - *

    - * Note: postings block offsets are stored in terms dictionary, no need to worry about its root. - */ - public final ComponentMetadataMap componentMetadatas; - - public SegmentMetadata(long rowIdOffset, - long numRows, - long minSSTableRowId, - long maxSSTableRowId, - PrimaryKey minKey, - PrimaryKey maxKey, - ByteBuffer minTerm, - ByteBuffer maxTerm, - ComponentMetadataMap componentMetadatas) - { - assert numRows < Integer.MAX_VALUE; - Objects.requireNonNull(minKey); - Objects.requireNonNull(maxKey); - Objects.requireNonNull(minTerm); - Objects.requireNonNull(maxTerm); - - this.rowIdOffset = rowIdOffset; - this.minSSTableRowId = minSSTableRowId; - this.maxSSTableRowId = maxSSTableRowId; - this.numRows = numRows; - this.minKey = minKey; - this.maxKey = maxKey; - this.minTerm = minTerm; - this.maxTerm = maxTerm; - this.componentMetadatas = componentMetadatas; - } - - private SegmentMetadata(DataInput input, PrimaryKey.Factory primaryKeyFactory) throws IOException - { - this.rowIdOffset = input.readLong(); - this.numRows = input.readLong(); - this.minSSTableRowId = input.readLong(); - this.maxSSTableRowId = input.readLong(); - this.minKey = primaryKeyFactory.fromComparableBytes(ByteSource.fixedLength(readBytes(input))); - this.maxKey = primaryKeyFactory.fromComparableBytes(ByteSource.fixedLength(readBytes(input))); - this.minTerm = readBytes(input); - this.maxTerm = readBytes(input); - this.componentMetadatas = new ComponentMetadataMap(input); - } - - public int toSegmentRowId(long sstableRowId) - { - return Math.toIntExact(sstableRowId - rowIdOffset); - } - - public static List load(MetadataSource source, PrimaryKey.Factory primaryKeyFactory) throws IOException - { - DataInput input = source.get(NAME); - - int segmentCount = input.readVInt(); - - List segmentMetadata = new ArrayList<>(segmentCount); - - for (int i = 0; i < segmentCount; i++) - { - segmentMetadata.add(new SegmentMetadata(input, primaryKeyFactory)); - } - - return segmentMetadata; - } - - /** - * Writes disk metadata for the given segment list. - */ - public static void write(MetadataWriter writer, List segments) throws IOException - { - try (IndexOutput output = writer.builder(NAME)) - { - output.writeVInt(segments.size()); - - for (SegmentMetadata metadata : segments) - { - output.writeLong(metadata.rowIdOffset); - output.writeLong(metadata.numRows); - output.writeLong(metadata.minSSTableRowId); - output.writeLong(metadata.maxSSTableRowId); - - Stream.of(ByteSourceInverse.readBytes(metadata.minKey.asComparableBytes(ByteComparable.Version.OSS50)), - ByteSourceInverse.readBytes(metadata.maxKey.asComparableBytes(ByteComparable.Version.OSS50))) - .forEach(b -> writeBytes(b, output)); - Stream.of(metadata.minTerm, metadata.maxTerm).forEach(bb -> writeBytes(bb, output)); - - metadata.componentMetadatas.write(output); - } - } - } - - @Override - public String toString() - { - return "SegmentMetadata{" + - "rowIdOffset=" + rowIdOffset + - ", minSSTableRowId=" + minSSTableRowId + - ", maxSSTableRowId=" + maxSSTableRowId + - ", numRows=" + numRows + - ", componentMetadatas=" + componentMetadatas + - '}'; - } - - private static ByteBuffer readBytes(DataInput input) throws IOException - { - int len = input.readInt(); - byte[] bytes = new byte[len]; - input.readBytes(bytes, 0, len); - return ByteBuffer.wrap(bytes); - } - - private static void writeBytes(ByteBuffer buf, IndexOutput out) - { - try - { - byte[] bytes = ByteBufferUtil.getArray(buf); - out.writeInt(bytes.length); - out.writeBytes(bytes, 0, bytes.length); - } - catch (IOException e) - { - throw new UncheckedIOException(e); - } - } - - private static void writeBytes(byte[] bytes, IndexOutput out) - { - try - { - out.writeInt(bytes.length); - out.writeBytes(bytes, 0, bytes.length); - } - catch (IOException ioe) - { - throw new RuntimeException(ioe); - } - } - - long getIndexRoot(IndexComponent indexComponent) - { - return componentMetadatas.get(indexComponent).root; - } - - public static class ComponentMetadataMap - { - private final Map metas = new EnumMap<>(IndexComponent.class); - - ComponentMetadataMap(DataInput input) throws IOException - { - int size = input.readInt(); - - for (int i = 0; i < size; i++) - { - metas.put(IndexComponent.valueOf(input.readString()), new ComponentMetadata(input)); - } - } - - public ComponentMetadataMap() - { - } - - public void put(IndexComponent indexComponent, long root, long offset, long length) - { - metas.put(indexComponent, new ComponentMetadata(root, offset, length)); - } - - public void put(IndexComponent indexComponent, long root, long offset, long length, Map additionalMap) - { - metas.put(indexComponent, new ComponentMetadata(root, offset, length, additionalMap)); - } - - private void write(IndexOutput output) throws IOException - { - output.writeInt(metas.size()); - - for (Map.Entry entry : metas.entrySet()) - { - output.writeString(entry.getKey().name()); - entry.getValue().write(output); - } - } - - public ComponentMetadata get(IndexComponent indexComponent) - { - if (!metas.containsKey(indexComponent)) - throw new IllegalArgumentException(indexComponent + " ComponentMetadata not found"); - - return metas.get(indexComponent); - } - - public Map> asMap() - { - Map> metaAttributes = new HashMap<>(); - - for (Map.Entry entry : metas.entrySet()) - { - String name = entry.getKey().name(); - ComponentMetadata metadata = entry.getValue(); - - Map componentAttributes = metadata.asMap(); - - assert !metaAttributes.containsKey(name) : "Found duplicate index type: " + name; - metaAttributes.put(name, componentAttributes); - } - - return metaAttributes; - } - - @Override - public String toString() - { - return "ComponentMetadataMap{" + - "metas=" + metas + - '}'; - } - - public double indexSize() - { - return metas.values().stream().mapToLong(meta -> meta.length).sum(); - } - } - - public static class ComponentMetadata - { - public static final String ROOT = "Root"; - public static final String OFFSET = "Offset"; - public static final String LENGTH = "Length"; - - public final long root; - public final long offset; - public final long length; - public final Map attributes; - - ComponentMetadata(long root, long offset, long length) - { - this.root = root; - this.offset = offset; - this.length = length; - this.attributes = Collections.emptyMap(); - } - - ComponentMetadata(long root, long offset, long length, Map attributes) - { - this.root = root; - this.offset = offset; - this.length = length; - this.attributes = attributes; - } - - ComponentMetadata(DataInput input) throws IOException - { - this.root = input.readLong(); - this.offset = input.readLong(); - this.length = input.readLong(); - int size = input.readInt(); - - attributes = new HashMap<>(size); - for (int x=0; x < size; x++) - { - String key = input.readString(); - String value = input.readString(); - - attributes.put(key, value); - } - } - - public void write(IndexOutput output) throws IOException - { - output.writeLong(root); - output.writeLong(offset); - output.writeLong(length); - - output.writeInt(attributes.size()); - for (Map.Entry entry : attributes.entrySet()) - { - output.writeString(entry.getKey()); - output.writeString(entry.getValue()); - } - } - - @Override - public String toString() - { - return String.format("ComponentMetadata{root=%d, offset=%d, length=%d, attributes=%s}", root, offset, length, attributes.toString()); - } - - public Map asMap() - { - return ImmutableMap.builder().putAll(attributes).put(OFFSET, Long.toString(offset)).put(LENGTH, Long.toString(length)).put(ROOT, Long.toString(root)).build(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentOrdering.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentOrdering.java deleted file mode 100644 index dcb6e4273707..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentOrdering.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.util.List; - -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.utils.CloseableIterator; - -/** - * A {@link SegmentOrdering} orders an index and produces a stream of {@link PrimaryKeyWithScore}s. - * - * The limit can be used to lazily order the {@link PrimaryKey}s. Due to the possiblity for - * shadowed or updated keys, a {@link SegmentOrdering} should be able to order the whole index - * until exhausted. - * - * When using {@link SegmentOrdering} there are several steps to - * build the list of Primary Keys to be ordered: - * - * 1. Find all primary keys that match each non-ordering query predicate. - * 2. Union and intersect the results of step 1 to build a single {@link KeyRangeIterator} - * ordered by {@link PrimaryKey}. - * 3. Fan the primary keys from step 2 out to each sstable segment to order the list of primary keys. - *

    - * SegmentOrdering handles the third step. - *

    - * Note: a segment ordering is only used when a query has both ordering and non-ordering predicates. - * Where a query has only ordering predicates, the ordering is handled by - * {@link org.apache.cassandra.index.sai.disk.SSTableIndex#search(Expression, AbstractBounds, QueryContext)}. - */ -public interface SegmentOrdering -{ - /** - * Reorder, limit, and put back into original order the results from a single sstable - */ - default CloseableIterator orderResultsBy(QueryContext queryContext, List results, Expression orderer) throws IOException - { - throw new UnsupportedOperationException(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentTrieBuffer.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentTrieBuffer.java deleted file mode 100644 index 5852fb87b39d..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentTrieBuffer.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.atomic.LongAdder; -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.tries.InMemoryTrie; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.index.sai.utils.IndexEntry; -import org.apache.cassandra.utils.Throwables; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.lucene.util.packed.PackedInts; -import org.apache.lucene.util.packed.PackedLongValues; - -/** - * On-heap buffer for values that provides a sorted view of itself as an {@link Iterator}. - */ -@NotThreadSafe -public class SegmentTrieBuffer -{ - private static final int MAX_RECURSIVE_TERM_LENGTH = 128; - - private final InMemoryTrie trie; - private final PostingsAccumulator postingsAccumulator; - private int numRows; - - public SegmentTrieBuffer() - { - trie = new InMemoryTrie<>(DatabaseDescriptor.getMemtableAllocationType().toBufferType()); - postingsAccumulator = new PostingsAccumulator(); - } - - public int numRows() - { - return numRows; - } - - public long memoryUsed() - { - return trie.sizeOnHeap() + postingsAccumulator.heapAllocations(); - } - - public long add(ByteComparable term, int termLength, int segmentRowId) - { - final long initialSizeOnHeap = trie.sizeOnHeap(); - final long reducerHeapSize = postingsAccumulator.heapAllocations(); - - try - { - trie.putSingleton(term, segmentRowId, postingsAccumulator, termLength <= MAX_RECURSIVE_TERM_LENGTH); - } - catch (InMemoryTrie.SpaceExhaustedException e) - { - throw Throwables.unchecked(e); - } - - numRows++; - return (trie.sizeOnHeap() - initialSizeOnHeap) + (postingsAccumulator.heapAllocations() - reducerHeapSize); - } - - public Iterator iterator() - { - Iterator> iterator = trie.entrySet().iterator(); - - return new Iterator<>() - { - @Override - public boolean hasNext() - { - return iterator.hasNext(); - } - - @Override - public IndexEntry next() - { - Map.Entry entry = iterator.next(); - PackedLongValues postings = entry.getValue().build(); - PackedLongValues.Iterator postingsIterator = postings.iterator(); - return IndexEntry.create(entry.getKey(), new PostingList() - { - @Override - public long nextPosting() - { - if (postingsIterator.hasNext()) - return postingsIterator.next(); - return END_OF_STREAM; - } - - @Override - public long size() - { - return postings.size(); - } - - @Override - public long advance(long targetRowID) - { - throw new UnsupportedOperationException(); - } - }); - } - }; - } - - private static class PostingsAccumulator implements InMemoryTrie.UpsertTransformer - { - private final LongAdder heapAllocations = new LongAdder(); - - @Override - public PackedLongValues.Builder apply(PackedLongValues.Builder existing, Integer rowID) - { - if (existing == null) - { - existing = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT); - heapAllocations.add(existing.ramBytesUsed()); - } - long ramBefore = existing.ramBytesUsed(); - existing.add(rowID); - heapAllocations.add(existing.ramBytesUsed() - ramBefore); - return existing; - } - - long heapAllocations() - { - return heapAllocations.longValue(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentWriter.java deleted file mode 100644 index d30a84620717..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentWriter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.util.Iterator; - -import org.apache.cassandra.index.sai.utils.IndexEntry; - -public interface SegmentWriter -{ - /** - * Appends a set of terms and associated postings to their respective overall SSTable component files. - * - * @param indexEntryIterator an {@link Iterator} of {@link IndexEntry}s sorted in term order. - * - * @return metadata describing the location of this inverted index in the overall SSTable terms and postings component files - */ - SegmentMetadata.ComponentMetadataMap writeCompleteSegment(Iterator indexEntryIterator) throws IOException; - - /** - * Returns the number of rows written to the segment - * - * @return the number of rows - */ - long getNumberOfRows(); -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/VectorIndexSegmentSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/VectorIndexSegmentSearcher.java deleted file mode 100644 index 82e76de7aeca..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/VectorIndexSegmentSearcher.java +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.segment; - -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.function.IntConsumer; -import java.util.stream.Collectors; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles; -import org.apache.cassandra.index.sai.disk.v1.vector.BruteForceRowIdIterator; -import org.apache.cassandra.index.sai.disk.v1.vector.DiskAnn; -import org.apache.cassandra.index.sai.disk.v1.vector.NeighborQueueRowIdIterator; -import org.apache.cassandra.index.sai.disk.v1.vector.OnDiskOrdinalsMap; -import org.apache.cassandra.index.sai.disk.v1.vector.OptimizeFor; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.disk.v1.vector.RowIdToPrimaryKeyWithScoreIterator; -import org.apache.cassandra.index.sai.disk.v1.vector.RowIdWithScore; -import org.apache.cassandra.index.sai.disk.v1.vector.SegmentRowIdOrdinalPairs; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.memory.VectorMemoryIndex; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.AtomicRatio; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.RangeUtil; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.tracing.Tracing; -import org.apache.cassandra.utils.CloseableIterator; - -import io.github.jbellis.jvector.graph.GraphIndex; -import io.github.jbellis.jvector.graph.NeighborQueue; -import io.github.jbellis.jvector.graph.NeighborSimilarity; -import io.github.jbellis.jvector.pq.CompressedVectors; -import io.github.jbellis.jvector.util.Bits; -import io.github.jbellis.jvector.util.SparseFixedBitSet; - -import static java.lang.Math.max; -import static java.lang.Math.min; - -/** - * Executes ANN search against a vector graph for an individual index segment. - */ -public class VectorIndexSegmentSearcher extends IndexSegmentSearcher -{ - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - // If true, use brute force. If false, use graph search. If null, use the normal logic. - @VisibleForTesting - public static Boolean FORCE_BRUTE_FORCE_ANN = null; - - private final DiskAnn graph; - private final AtomicRatio actualExpectedRatio = new AtomicRatio(); - private final ThreadLocal cachedBitSets; - private final OptimizeFor optimizeFor; - private final ColumnMetadata column; - - VectorIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, - SSTableId sstableId, - PerColumnIndexFiles perIndexFiles, - SegmentMetadata segmentMetadata, - StorageAttachedIndex index) throws IOException - { - super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, index); - graph = new DiskAnn(segmentMetadata.componentMetadatas, perIndexFiles, index.indexWriterConfig(), sstableId); - cachedBitSets = ThreadLocal.withInitial(() -> new SparseFixedBitSet(graph.size())); - optimizeFor = index.indexWriterConfig().getOptimizeFor(); - column = index.termType().columnMetadata(); - } - - @Override - public long indexFileCacheSize() - { - return graph.ramBytesUsed(); - } - - @Override - public KeyRangeIterator search(Expression expression, AbstractBounds keyRange, QueryContext queryContext) throws IOException - { - throw new UnsupportedOperationException(); - } - - @Override - public CloseableIterator orderBy(Expression orderer, AbstractBounds keyRange, QueryContext context) throws IOException - { - int limit = context.limit(); - - if (logger.isTraceEnabled()) - logger.trace(index.identifier().logMessage("Searching on expression '{}'..."), orderer); - - if (orderer.getIndexOperator() != Expression.IndexOperator.ANN) - throw new IllegalArgumentException(index.identifier().logMessage("Unsupported expression during ANN index query: " + orderer)); - - int topK = optimizeFor.topKFor(limit); - - float[] queryVector = index.termType().decomposeVector(orderer.lower().value.raw.duplicate()); - CloseableIterator result = searchInternal(keyRange, queryVector, limit, topK); - return toScoreSortedIterator(result); - } - - private CloseableIterator searchInternal(AbstractBounds keyRange, float[] queryVector, int limit, int topK) throws IOException - { - try (PrimaryKeyMap primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap()) - { - // not restricted - if (RangeUtil.coversFullRing(keyRange)) - return searchInternalUnrestricted(queryVector, limit, topK); - - - // it will return the next row id if given key is not found. - long minSSTableRowId = primaryKeyMap.ceiling(keyRange.left.getToken()); - // If we didn't find the first key, we won't find the last primary key either - if (minSSTableRowId < 0) - return CloseableIterator.empty(); - long maxSSTableRowId = getMaxSSTableRowId(primaryKeyMap, keyRange.right); - - if (minSSTableRowId > maxSSTableRowId) - return CloseableIterator.empty(); - - // if it covers entire segment, skip bit set - if (minSSTableRowId <= metadata.minSSTableRowId && maxSSTableRowId >= metadata.maxSSTableRowId) - return searchInternalUnrestricted(queryVector, limit, topK); - - minSSTableRowId = Math.max(minSSTableRowId, metadata.minSSTableRowId); - maxSSTableRowId = min(maxSSTableRowId, metadata.maxSSTableRowId); - - // If num of matches are not bigger than limit, skip graph search and lazily sort by brute force. - int nRows = Math.toIntExact(maxSSTableRowId - minSSTableRowId + 1); - int maxBruteForceRows = maxBruteForceRows(limit, nRows, graph.size()); - logger.trace("Search range covers {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}", - nRows, maxBruteForceRows, graph.size(), limit); - Tracing.trace("Search range covers {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}", - nRows, maxBruteForceRows, graph.size(), limit); - boolean shouldBruteForce = FORCE_BRUTE_FORCE_ANN == null ? nRows <= maxBruteForceRows : FORCE_BRUTE_FORCE_ANN; - if (shouldBruteForce) - { - SegmentRowIdOrdinalPairs segmentOrdinalPairs = new SegmentRowIdOrdinalPairs(Math.toIntExact(nRows)); - try (OnDiskOrdinalsMap.OrdinalsView ordinalsView = graph.getOrdinalsView()) - { - for (long sstableRowId = minSSTableRowId; sstableRowId <= maxSSTableRowId; sstableRowId++) - { - int segmentRowId = metadata.toSegmentRowId(sstableRowId); - int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId); - if (ordinal >= 0) - segmentOrdinalPairs.add(segmentRowId, ordinal); - } - } - return orderByBruteForce(queryVector, segmentOrdinalPairs, limit, topK); - } - - // create a bitset of ordinals corresponding to the rows in the given key range - SparseFixedBitSet bits = bitSetForSearch(); - boolean hasMatches = false; - try (var ordinalsView = graph.getOrdinalsView()) - { - for (long sstableRowId = minSSTableRowId; sstableRowId <= maxSSTableRowId; sstableRowId++) - { - int segmentRowId = metadata.toSegmentRowId(sstableRowId); - int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId); - if (ordinal >= 0) - { - bits.set(ordinal); - hasMatches = true; - } - } - } - catch (IOException e) - { - throw new RuntimeException(e); - } - - if (!hasMatches) - return CloseableIterator.empty(); - - int expectedNodesVisited = expectedNodesVisited(limit, bits.cardinality(), graph.size()); - IntConsumer nodesVisitedConsumer = nodesVisited -> updateExpectedNodes(nodesVisited, expectedNodesVisited); - return graph.search(queryVector, topK, limit, bits, nodesVisitedConsumer); - } - } - - private CloseableIterator searchInternalUnrestricted(float[] queryVector, int limit, int topK) - { - int expectedNodesVisited = expectedNodesVisited(limit, graph.size(), graph.size()); - IntConsumer nodesVisitedConsumer = nodesVisited -> updateExpectedNodes(nodesVisited, expectedNodesVisited); - return graph.search(queryVector, topK, limit, new Bits.MatchAllBits(graph.size()), nodesVisitedConsumer); - } - - private long getMaxSSTableRowId(PrimaryKeyMap primaryKeyMap, PartitionPosition right) - { - // if the right token is the minimum token, there is no upper bound on the keyRange and - // we can save a lookup by using the maxSSTableRowId - if (right.isMinimum()) - return metadata.maxSSTableRowId; - - long max = primaryKeyMap.floor(right.getToken()); - if (max < 0) - return metadata.maxSSTableRowId; - return max; - } - - private SparseFixedBitSet bitSetForSearch() - { - SparseFixedBitSet bits = cachedBitSets.get(); - bits.clear(); - return bits; - } - - /** - * Produces a descending score ordered iterator over the rows in the given segment. Branches depending on the number - * of rows to consider and whether the graph has compressed vectors available for faster comparisons. - */ - private CloseableIterator orderByBruteForce(float[] queryVector, SegmentRowIdOrdinalPairs segmentOrdinalPairs, int limit, int topK) throws IOException - { - if (segmentOrdinalPairs.size() == 0) - return CloseableIterator.empty(); - - // If we have more than topK segmentOrdinalPairs, we do a two pass partial sort by first getting the approximate - // similarity score via the PQ vectors that are already in memory and then by hitting disk to get the full - // precision vectors to get the full precision similarity score. - if (graph.getCompressedVectors() != null && segmentOrdinalPairs.size() > topK) - return orderByBruteForceTwoPass(graph.getCompressedVectors(), queryVector, segmentOrdinalPairs, limit, topK); - - try (GraphIndex.View view = graph.getView()) - { - NeighborSimilarity.ExactScoreFunction esf = graph.getExactScoreFunction(queryVector, view); - NeighborQueue scoredRowIds = segmentOrdinalPairs.mapToSegmentRowIdScoreHeap(esf); - return new NeighborQueueRowIdIterator(scoredRowIds); - } - catch (Exception e) - { - throw new IOException(e); - } - } - - /** - * Materialize the compressed vectors for the given segment row ids, put them into a priority queue ordered by - * approximate similarity score, and then pass to the {@link BruteForceRowIdIterator} to lazily resolve the - * full resolution ordering as needed. - */ - private CloseableIterator orderByBruteForceTwoPass(CompressedVectors cv, - float[] queryVector, - SegmentRowIdOrdinalPairs segmentOrdinalPairs, - int limit, - int rerankK) - { - NeighborSimilarity.ApproximateScoreFunction scoreFunction = graph.getApproximateScoreFunction(queryVector); - // Store the index of the (rowId, ordinal) pair from the segmentOrdinalPairs in the NodeQueue so that we can - // retrieve both values with O(1) lookup when we need to resolve the full resolution score in the - // BruteForceRowIdIterator. - NeighborQueue approximateScoreHeap = segmentOrdinalPairs.mapToIndexScoreIterator(scoreFunction); - GraphIndex.View view = graph.getView(); - NeighborSimilarity.ExactScoreFunction esf = graph.getExactScoreFunction(queryVector, view); - return new BruteForceRowIdIterator(approximateScoreHeap, segmentOrdinalPairs, esf, limit, rerankK, view); - } - - @Override - public CloseableIterator orderResultsBy(QueryContext context, List results, Expression orderer) throws IOException - { - int limit = context.limit(); - // VSTODO would it be better to do a binary search to find the boundaries? - List keysInRange = results.stream() - .dropWhile(k -> k.compareTo(metadata.minKey) < 0) - .takeWhile(k -> k.compareTo(metadata.maxKey) <= 0) - .collect(Collectors.toList()); - if (keysInRange.isEmpty()) - return CloseableIterator.empty(); - - try (PrimaryKeyMap primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap()) - { - // the iterator represents keys from the whole table -- we'll only pull of those that - // are from our own token range, so we can use row ids to order the results by vector similarity. - SegmentRowIdOrdinalPairs segmentOrdinalPairs = new SegmentRowIdOrdinalPairs(keysInRange.size()); - try (OnDiskOrdinalsMap.OrdinalsView ordinalsView = graph.getOrdinalsView()) - { - for (PrimaryKey primaryKey : keysInRange) - { - long sstableRowId = primaryKeyMap.rowIdFromPrimaryKey(primaryKey); - // skip rows that are not in our segment (or more preciesely, have no vectors that were indexed) - // or are not in this segment (exactRowIdForPrimaryKey returns a negative value for not found) - if (sstableRowId < metadata.minSSTableRowId) - continue; - - // if sstable row id has exceeded current ANN segment, stop - if (sstableRowId > metadata.maxSSTableRowId) - break; - - int segmentRowId = metadata.toSegmentRowId(sstableRowId); - // VSTODO now that we know the size of keys evaluated, is it worth doing the brute - // force check eagerly to potentially skip the PK to sstable row id to ordinal lookup? - int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId); - if (ordinal >= 0) - segmentOrdinalPairs.add(segmentRowId, ordinal); - } - } - - int topK = optimizeFor.topKFor(limit); - float[] queryVector = index.termType().decomposeVector(orderer.lower().value.raw.duplicate()); - - if (shouldUseBruteForce(topK, limit, segmentOrdinalPairs.size())) - { - return toScoreSortedIterator(orderByBruteForce(queryVector, segmentOrdinalPairs, limit, topK)); - } - - SparseFixedBitSet bits = bitSetForSearch(); - segmentOrdinalPairs.forEachOrdinal(bits::set); - // else ask the index to perform a search limited to the bits we created - int expectedNodesVisited = expectedNodesVisited(limit, segmentOrdinalPairs.size(), graph.size()); - IntConsumer nodesVisitedConsumer = nodesVisited -> updateExpectedNodes(nodesVisited, expectedNodesVisited); - CloseableIterator result = graph.search(queryVector, topK, limit, bits, nodesVisitedConsumer); - return toScoreSortedIterator(result); - } - } - - private boolean shouldUseBruteForce(int topK, int limit, int numRows) - { - // if we have a small number of results then let TopK processor do exact NN computation - int maxBruteForceRows = maxBruteForceRows(topK, numRows, graph.size()); - logger.trace("SAI materialized {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}", - numRows, maxBruteForceRows, graph.size(), limit); - Tracing.trace("SAI materialized {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}", - numRows, maxBruteForceRows, graph.size(), limit); - return FORCE_BRUTE_FORCE_ANN == null ? numRows <= maxBruteForceRows - : FORCE_BRUTE_FORCE_ANN; - } - - private int maxBruteForceRows(int limit, int nPermittedOrdinals, int graphSize) - { - int expectedNodesVisited = expectedNodesVisited(limit, nPermittedOrdinals, graphSize); - int expectedComparisons = index.indexWriterConfig().getMaximumNodeConnections() * expectedNodesVisited; - // Brute force here means reading each vector from the index file on disk, comparing the row's vector to the - // search vector to get a score, and then putting the results into a priority queue to then iterate over. - // Alternatively, we search the graph, which entails comparisons and disk reads. The goal is to reduce disk - // accesses and number of comparisons. - // VSTODO the below factor is dramatically oversimplified - // larger dimension should increase this, because comparisons are more expensive - double memoryToDiskFactor = 0.25; - return (int) max(limit, memoryToDiskFactor * expectedComparisons); - } - - private int expectedNodesVisited(int limit, int nPermittedOrdinals, int graphSize) - { - double observedRatio = actualExpectedRatio.getUpdateCount() >= 10 ? actualExpectedRatio.get() : 1.0; - return (int) (observedRatio * VectorMemoryIndex.expectedNodesVisited(limit, nPermittedOrdinals, graphSize)); - } - - private void updateExpectedNodes(int actualNodesVisited, int expectedNodesVisited) - { - assert expectedNodesVisited >= 0 : expectedNodesVisited; - assert actualNodesVisited >= 0 : actualNodesVisited; - if (actualNodesVisited >= 1000 && actualNodesVisited > 2 * expectedNodesVisited || expectedNodesVisited > 2 * actualNodesVisited) - logger.trace("Predicted visiting {} nodes, but actually visited {}", expectedNodesVisited, actualNodesVisited); - actualExpectedRatio.update(actualNodesVisited, expectedNodesVisited); - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this).add("index", index).toString(); - } - - @Override - public void close() throws IOException - { - graph.close(); - } - - private CloseableIterator toScoreSortedIterator(CloseableIterator rowIdIterator) throws IOException - { - if (!rowIdIterator.hasNext()) - { - FileUtils.closeQuietly(rowIdIterator); - return CloseableIterator.empty(); - } - - return new RowIdToPrimaryKeyWithScoreIterator(column, primaryKeyMapFactory, rowIdIterator, metadata.rowIdOffset); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/DocLengthsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/DocLengthsWriter.java new file mode 100644 index 000000000000..254f577848ef --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/DocLengthsWriter.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.trie; + +import java.io.Closeable; +import java.io.IOException; + +import org.agrona.collections.Int2IntHashMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; + +/** + * Writes document length information to disk for use in text scoring + */ +public class DocLengthsWriter implements Closeable +{ + private final IndexOutputWriter output; + + private final long startOffset; + + public DocLengthsWriter(IndexComponents.ForWrite components) throws IOException + { + this.output = components.addOrGet(IndexComponentType.DOC_LENGTHS).openOutput(true); + + // Version EC skipped the header in the doc lengths component metadata. + if (Version.EC.equals(components.version())) + { + SAICodecUtils.writeHeader(output); + startOffset = output.getFilePointer(); + } + else + { + startOffset = output.getFilePointer(); + SAICodecUtils.writeHeader(output); + } + } + + public void writeDocLengths(Int2IntHashMap lengths) throws IOException + { + // Calculate max row ID from doc lengths map + int maxRowId = -1; + for (var keyIterator = lengths.keySet().iterator(); keyIterator.hasNext(); ) + { + int key = keyIterator.nextValue(); + if (key > maxRowId) + maxRowId = key; + } + + // write out the doc lengths in row order + for (int rowId = 0; rowId <= maxRowId; rowId++) + { + final int length = lengths.get(rowId); + output.writeInt(length == lengths.missingValue() ? 0 : length); + } + + SAICodecUtils.writeFooter(output); + } + + public long getFilePointer() + { + return output.getFilePointer(); + } + + /** + * @return file pointer where index structure begins (before header) + */ + public long getStartOffset() + { + return startOffset; + } + + @Override + public void close() throws IOException + { + output.close(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/InvertedIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/InvertedIndexWriter.java new file mode 100644 index 000000000000..dfaa5e3ddd30 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/InvertedIndexWriter.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v1.trie; + +import java.io.Closeable; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.commons.lang3.mutable.MutableLong; + +import org.agrona.collections.Int2IntHashMap; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.TermsIterator; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * Builds an on-disk inverted index structure: terms dictionary and postings lists. + */ +@NotThreadSafe +public class InvertedIndexWriter implements Closeable +{ + private final TrieTermsDictionaryWriter termsDictionaryWriter; + private final PostingsWriter postingsWriter; + private final DocLengthsWriter docLengthsWriter; + private long postingsAdded; + + public InvertedIndexWriter(IndexComponents.ForWrite components) throws IOException + { + this(components, false); + } + + public InvertedIndexWriter(IndexComponents.ForWrite components, boolean writeFrequencies) throws IOException + { + this.termsDictionaryWriter = new TrieTermsDictionaryWriter(components); + this.postingsWriter = new PostingsWriter(components, writeFrequencies); + this.docLengthsWriter = components.version().onOrAfter(Version.BM25_EARLIEST) ? new DocLengthsWriter(components) : null; + } + + /** + * Appends a set of terms and associated postings to their respective overall SSTable component files. + * + * @param terms an iterator of terms with their associated postings + * + * @return metadata describing the location of this inverted index in the overall SSTable + * terms and postings component files + */ + public SegmentMetadata.ComponentMetadataMap writeAll(TermsIterator terms, Int2IntHashMap docLengths) throws IOException + { + // Terms and postings writers are opened in append mode with pointers at the end of their respective files. + long termsOffset = termsDictionaryWriter.getStartOffset(); + long postingsOffset = postingsWriter.getStartOffset(); + + while (terms.hasNext()) + { + ByteComparable term = terms.next(); + try (PostingList postings = terms.postings()) + { + final long offset = postingsWriter.write(postings); + if (offset >= 0) + termsDictionaryWriter.add(term, offset); + } + } + postingsAdded = postingsWriter.getTotalPostings(); + MutableLong footerPointer = new MutableLong(); + long termsRoot = termsDictionaryWriter.complete(footerPointer); + postingsWriter.complete(); + + long termsLength = termsDictionaryWriter.getFilePointer() - termsOffset; + long postingsLength = postingsWriter.getFilePointer() - postingsOffset; + + SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap(); + + Map map = new HashMap<>(2); + map.put(SAICodecUtils.FOOTER_POINTER, "" + footerPointer.getValue()); + + // Postings list file pointers are stored directly in TERMS_DATA, so a root is not needed. + components.put(IndexComponentType.POSTING_LISTS, -1, postingsOffset, postingsLength); + components.put(IndexComponentType.TERMS_DATA, termsRoot, termsOffset, termsLength, map); + + // Write doc lengths + if (docLengthsWriter != null) + { + long docLengthsOffset = docLengthsWriter.getStartOffset(); + docLengthsWriter.writeDocLengths(docLengths); + long docLengthsLength = docLengthsWriter.getFilePointer() - docLengthsOffset; + components.put(IndexComponentType.DOC_LENGTHS, -1, docLengthsOffset, docLengthsLength); + } + + return components; + } + + @Override + public void close() throws IOException + { + postingsWriter.close(); + termsDictionaryWriter.close(); + if (docLengthsWriter != null) + docLengthsWriter.close(); + } + + /** + * @return total number of row IDs added to posting lists + */ + public long getPostingsCount() + { + return postingsAdded; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/LiteralIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/LiteralIndexWriter.java deleted file mode 100644 index 616ef603a21d..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/LiteralIndexWriter.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.disk.v1.trie; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.commons.lang3.mutable.MutableLong; - -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.v1.segment.SegmentWriter; -import org.apache.cassandra.index.sai.utils.IndexEntry; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.index.sai.postings.PostingList; - -/** - * Builds an on-disk inverted index structure: terms dictionary and postings lists. - */ -@NotThreadSafe -public class LiteralIndexWriter implements SegmentWriter -{ - private final IndexDescriptor indexDescriptor; - private final IndexIdentifier indexIdentifier; - private long postingsAdded; - - public LiteralIndexWriter(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier) - { - this.indexDescriptor = indexDescriptor; - this.indexIdentifier = indexIdentifier; - } - - @Override - public SegmentMetadata.ComponentMetadataMap writeCompleteSegment(Iterator iterator) throws IOException - { - SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap(); - - try (TrieTermsDictionaryWriter termsDictionaryWriter = new TrieTermsDictionaryWriter(indexDescriptor, indexIdentifier); - PostingsWriter postingsWriter = new PostingsWriter(indexDescriptor, indexIdentifier)) - { - // Terms and postings writers are opened in append mode with pointers at the end of their respective files. - long termsOffset = termsDictionaryWriter.getStartOffset(); - long postingsOffset = postingsWriter.getStartOffset(); - - while (iterator.hasNext()) - { - IndexEntry indexEntry = iterator.next(); - try (PostingList postings = indexEntry.postingList) - { - long offset = postingsWriter.write(postings); - termsDictionaryWriter.add(indexEntry.term, offset); - } - } - postingsAdded = postingsWriter.getTotalPostings(); - MutableLong footerPointer = new MutableLong(); - long termsRoot = termsDictionaryWriter.complete(footerPointer); - postingsWriter.complete(); - - long termsLength = termsDictionaryWriter.getFilePointer() - termsOffset; - long postingsLength = postingsWriter.getFilePointer() - postingsOffset; - - Map map = new HashMap<>(2); - map.put(SAICodecUtils.FOOTER_POINTER, footerPointer.getValue().toString()); - - // Postings list file pointers are stored directly in TERMS_DATA, so a root is not needed. - components.put(IndexComponent.POSTING_LISTS, -1, postingsOffset, postingsLength); - components.put(IndexComponent.TERMS_DATA, termsRoot, termsOffset, termsLength, map); - } - return components; - } - - @Override - public long getNumberOfRows() - { - return postingsAdded; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/ReverseTrieTermsDictionaryReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/ReverseTrieTermsDictionaryReader.java new file mode 100644 index 000000000000..ddf8fbcb20ab --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/ReverseTrieTermsDictionaryReader.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v1.trie; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.tries.ReverseValueIterator; +import org.apache.cassandra.io.util.Rebufferer; +import org.apache.cassandra.io.util.SizedInts; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * Page-aware reverse iterator reader for a trie terms dictionary written by {@link TrieTermsDictionaryWriter}. + */ +public class ReverseTrieTermsDictionaryReader extends ReverseValueIterator implements Iterator> +{ + public ReverseTrieTermsDictionaryReader(Rebufferer rebufferer, long root) + { + super(rebufferer, root, true, TypeUtil.BYTE_COMPARABLE_VERSION); + } + + @Override + public boolean hasNext() + { + return super.hasNext(); + } + + @Override + public Pair next() + { + return nextValue(this::getKeyAndPayload); + } + + private Pair getKeyAndPayload() + { + return Pair.create(collectedKey(), getPayload(buf, payloadPosition(), payloadFlags())); + } + + private static long getPayload(ByteBuffer contents, int payloadPos, int bytes) + { + return SizedInts.read(contents, payloadPos, bytes); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryReader.java index b0867da1e361..f4f6477fca7b 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryReader.java @@ -19,15 +19,17 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Iterator; import javax.annotation.concurrent.NotThreadSafe; import org.apache.cassandra.io.tries.SerializationNode; import org.apache.cassandra.io.tries.TrieNode; import org.apache.cassandra.io.tries.TrieSerializer; -import org.apache.cassandra.io.tries.Walker; +import org.apache.cassandra.io.tries.ValueIterator; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.io.util.Rebufferer; import org.apache.cassandra.io.util.SizedInts; +import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; @@ -35,13 +37,27 @@ * Page-aware random access reader for a trie terms dictionary written by {@link TrieTermsDictionaryWriter}. */ @NotThreadSafe -public class TrieTermsDictionaryReader extends Walker +public class TrieTermsDictionaryReader extends ValueIterator implements Iterator> { public static final long NOT_FOUND = -1; - public TrieTermsDictionaryReader(Rebufferer rebufferer, long root) + public TrieTermsDictionaryReader(Rebufferer rebufferer, long root, ByteComparable.Version version) { - super(rebufferer, root); + super(rebufferer, root, true, version); + } + + /** + * Creates a reader for a trie terms dictionary range. See {@link ValueIterator} for details. + */ + public TrieTermsDictionaryReader(Rebufferer source, + long root, + ByteComparable start, + ByteComparable end, + boolean inclStart, + boolean collecting, + ByteComparable.Version version) + { + super(source, root, start, end, inclStart ? LeftBoundTreatment.ADMIT_EXACT : LeftBoundTreatment.GREATER, collecting, version); } public static final TrieSerializer trieSerializer = new TrieSerializer<>() @@ -55,39 +71,150 @@ public int sizeofNode(SerializationNode node, long nodePosition) @Override public void write(DataOutputPlus dest, SerializationNode node, long nodePosition) throws IOException { - TrieNode type = TrieNode.typeFor(node, nodePosition); - Long payload = node.payload(); - int payloadBits = sizeof(payload); - type.serialize(dest, node, payloadBits, nodePosition); - + final TrieNode type = TrieNode.typeFor(node, nodePosition); + final Long payload = node.payload(); if (payload != null) + { + final int payloadBits = SizedInts.nonZeroSize(payload); + type.serialize(dest, node, payloadBits, nodePosition); SizedInts.write(dest, payload, payloadBits); + } + else + { + type.serialize(dest, node, 0, nodePosition); + } } private int sizeof(Long payload) { - return payload == null ? 0 : SizedInts.nonZeroSize(payload); + if (payload != null) + { + return SizedInts.nonZeroSize(payload); + } + return 0; } }; public long exactMatch(ByteComparable key) { - // Since we are looking for an exact match we are always expecting the follow - // to return END_OF_STREAM if the key was found. - return follow(key) == ByteSource.END_OF_STREAM ? getCurrentPayload() : NOT_FOUND; + int b = follow(key); + if (b != ByteSource.END_OF_STREAM) + { + return NOT_FOUND; + } + return getCurrentPayload(); } - private long getCurrentPayload() + /** + * Returns the position associated with the least term greater than or equal to the given key, or + * a negative value if there is no such term. In order to optimize the search, the trie is traversed + * statefully. Therefore, this method only returns correct results when called for increasing keys. + * Warning: ceiling is not idempotent. Calling ceiling() twice for the same key will return successive + * values instead of the same value. This is acceptable for the current usage of the method. + * @param key the prefix to traverse in the trie + * @return a position, if found, or a negative value if there is no such position + */ + public long ceiling(ByteComparable key) { - return getPayloadAt(buf, payloadPosition(), payloadFlags()); + skipTo(key, LeftBoundTreatment.ADMIT_EXACT); + return nextAsLong(); } - private long getPayloadAt(ByteBuffer contents, int payloadPos, int bytes) + public long nextAsLong() { - if (bytes == 0) + return nextValueAsLong(this::getCurrentPayload, NOT_FOUND); + } + + @Override + public boolean hasNext() + { + return super.hasNext(); + } + + @Override + public Pair next() + { + return nextValue(this::getKeyAndPayload); + } + + private Pair getKeyAndPayload() + { + return Pair.create(collectedKey(), getCurrentPayload()); + } + + /** + * Returns the position associated with the greatest term less than or equal to the given key, or + * a negative value if there is no such term. + * @param key the prefix to traverse in the trie + * @return a position, if found, or a negative value if there is no such position + */ + public long floor(ByteComparable key) + { + Long result = null; + try + { + result = prefixAndNeighbours(key, TrieTermsDictionaryReader::getPayload); + } + catch (IOException e) { + throw new RuntimeException(e); + } + if (result != null && result != NOT_FOUND) + return result; + if (lesserBranch == -1) return NOT_FOUND; + goMax(lesserBranch); + return getCurrentPayload(); + } + + public ByteComparable getMaxTerm() + { + final TransitionBytesCollector collector = new TransitionBytesCollector(byteComparableVersion); + go(root); + while (true) + { + int lastIdx = transitionRange() - 1; + long lastChild = transition(lastIdx); + if (lastIdx < 0) + { + return collector.toByteComparable(); + } + collector.add(transitionByte(lastIdx)); + go(lastChild); + } + } + + public ByteComparable getMinTerm() + { + final TransitionBytesCollector collector = new TransitionBytesCollector(byteComparableVersion); + go(root); + while (true) + { + int payloadBits = payloadFlags(); + if (payloadBits > 0) + { + return collector.toByteComparable(); + } + collector.add(transitionByte(0)); + go(transition(0)); } + } + + private long getCurrentPayload() + { + return getPayload(payloadPosition(), payloadFlags()); + } + + private long getPayload(int payloadPos, int bits) + { + return getPayload(buf, payloadPos, bits); + } + + private static long getPayload(ByteBuffer contents, int payloadPos, int bytes) + { + if (bytes == 0) + return NOT_FOUND; + return SizedInts.read(contents, payloadPos, bytes); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryWriter.java index df6b278a069f..767b04e96d44 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/trie/TrieTermsDictionaryWriter.java @@ -23,18 +23,16 @@ import org.apache.commons.lang3.mutable.MutableLong; -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.io.tries.IncrementalDeepTrieWriterPageAware; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; import org.apache.cassandra.io.tries.IncrementalTrieWriter; import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** - * Writes terms dictionary to disk in a trie format (see {@link IncrementalTrieWriter}). - *

    + * Writes terms dictionary to disk in a trie format (see {@link IncrementalTrieWriter}. + * * Allows for variable-length keys. Trie values are 64-bit offsets to the posting file, pointing to the beginning of * summary block for that postings list. */ @@ -45,14 +43,15 @@ public class TrieTermsDictionaryWriter implements Closeable private final IndexOutputWriter termDictionaryOutput; private final long startOffset; - TrieTermsDictionaryWriter(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier) throws IOException + TrieTermsDictionaryWriter(IndexComponents.ForWrite components) throws IOException { - termDictionaryOutput = indexDescriptor.openPerIndexOutput(IndexComponent.TERMS_DATA, indexIdentifier, true); + termDictionaryOutput = components.addOrGet(IndexComponentType.TERMS_DATA).openOutput(true); startOffset = termDictionaryOutput.getFilePointer(); SAICodecUtils.writeHeader(termDictionaryOutput); // we pass the output as SequentialWriter, but we keep IndexOutputWriter around to write footer on flush - termsDictionaryWriter = new IncrementalDeepTrieWriterPageAware<>(TrieTermsDictionaryReader.trieSerializer, termDictionaryOutput.asSequentialWriter()); + var encodingVersion = components.byteComparableVersionFor(IndexComponentType.TERMS_DATA); + termsDictionaryWriter = IncrementalTrieWriter.open(TrieTermsDictionaryReader.trieSerializer, termDictionaryOutput.asSequentialWriter(), encodingVersion); } public void add(ByteComparable term, long postingListOffset) throws IOException @@ -61,7 +60,7 @@ public void add(ByteComparable term, long postingListOffset) throws IOException } @Override - public void close() + public void close() throws IOException { termsDictionaryWriter.close(); termDictionaryOutput.close(); diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/AutoResumingNodeScoreIterator.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/AutoResumingNodeScoreIterator.java deleted file mode 100644 index 6b19a72246ed..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/AutoResumingNodeScoreIterator.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.function.IntConsumer; - -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.tracing.Tracing; -import org.apache.cassandra.utils.AbstractIterator; - -import io.github.jbellis.jvector.graph.GraphIndex; -import io.github.jbellis.jvector.graph.GraphSearcher; -import io.github.jbellis.jvector.graph.NeighborSimilarity; -import io.github.jbellis.jvector.graph.SearchResult; -import io.github.jbellis.jvector.util.Bits; -import io.github.jbellis.jvector.util.GrowableBitSet; - -/** - * An iterator over {@link SearchResult.NodeScore} backed by a {@link SearchResult} that resumes search - * when the backing {@link SearchResult} is exhausted. - */ -public class AutoResumingNodeScoreIterator extends AbstractIterator -{ - private final GraphSearcher searcher; - private final GraphIndex.View view; - private final NeighborSimilarity.ScoreFunction scoreFunction; - private final NeighborSimilarity.ReRanker reRanker; - private final int topK; - private final Bits acceptBits; - private final boolean inMemory; - private final String source; - private final IntConsumer nodesVisitedConsumer; - private Iterator nodeScores = Collections.emptyIterator(); - private int cumulativeNodesVisited; - - // Defer initialization since it is only needed if we need to resume search - private SkipVisitedBits visited = null; - private SearchResult.NodeScore[] previousResult = null; - - /** - * Create a new {@link AutoResumingNodeScoreIterator} that iterates over the provided {@link SearchResult}. - * If the {@link SearchResult} is consumed, it retrieves the next {@link SearchResult} until the search returns - * no more results. - * @param searcher the {@link GraphSearcher} to use to search and resume search. - * @param nodesVisitedConsumer a consumer that accepts the total number of nodes visited - * @param inMemory whether the graph is in memory or on disk (used for trace logging) - * @param source the source of the search (used for trace logging) - * @param view the view used to read from disk. It will be closed when the iterator is closed. - */ - public AutoResumingNodeScoreIterator(GraphSearcher searcher, - NeighborSimilarity.ScoreFunction scoreFunction, - NeighborSimilarity.ReRanker reRanker, - int topK, - Bits acceptBits, - IntConsumer nodesVisitedConsumer, - boolean inMemory, - String source, - GraphIndex.View view) - { - this.searcher = searcher; - this.scoreFunction = scoreFunction; - this.reRanker = reRanker; - this.topK = topK; - this.acceptBits = acceptBits; - - this.cumulativeNodesVisited = 0; - this.nodesVisitedConsumer = nodesVisitedConsumer; - this.inMemory = inMemory; - this.source = source; - this.view = view; - } - - @Override - protected SearchResult.NodeScore computeNext() - { - if (nodeScores.hasNext()) - return nodeScores.next(); - - // Add result from previous search to visited bits - if (previousResult != null) - { - if (visited == null) - visited = new SkipVisitedBits(acceptBits, previousResult.length); - visited.visited(previousResult); - } - Bits bits = visited == null ? acceptBits : visited; - SearchResult nextResult = searcher.search(scoreFunction, reRanker, topK, bits); - - // Record metrics (we add here instead of overwriting because re-queries are expensive proportional to the - // number of visited nodes and even though we throw away some of those results, it helps us determine the - // right path for brute force vs. ANN) - cumulativeNodesVisited += nextResult.getVisitedCount(); - - if (Tracing.isTracing()) - { - Tracing.trace("{} based ANN {} for topK {} visited {} nodes to return {} results from {}", - inMemory ? "Memory" : "Disk", previousResult == null ? "initial" : "re-query", - topK, nextResult.getVisitedCount(), nextResult.getNodes().length, source); - } - - previousResult = nextResult.getNodes(); - // If the next result is empty, we are done searching. - nodeScores = Arrays.stream(nextResult.getNodes()).iterator(); - return nodeScores.hasNext() ? nodeScores.next() : endOfData(); - } - - @Override - public void close() - { - nodesVisitedConsumer.accept(cumulativeNodesVisited); - FileUtils.closeQuietly(view); - } - - private static class SkipVisitedBits implements Bits - { - private final Bits acceptBits; - private final GrowableBitSet visited; - - SkipVisitedBits(Bits acceptBits, int initialBits) - { - this.acceptBits = acceptBits; - this.visited = new GrowableBitSet(initialBits); - } - - void visited(SearchResult.NodeScore[] nodes) - { - for (SearchResult.NodeScore nodeScore : nodes) - visited.set(nodeScore.node); - } - - @Override - public boolean get(int i) - { - return acceptBits.get(i) && !visited.get(i); - } - - @Override - public int length() - { - return acceptBits.length(); - } - } -} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/BruteForceRowIdIterator.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/BruteForceRowIdIterator.java deleted file mode 100644 index 5493555b0e94..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/BruteForceRowIdIterator.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.AbstractIterator; - -import io.github.jbellis.jvector.graph.GraphIndex; -import io.github.jbellis.jvector.graph.NeighborQueue; -import io.github.jbellis.jvector.graph.NeighborSimilarity; - - -/** - * An iterator over {@link RowIdWithScore} that lazily consumes from a {@link NeighborQueue} of approximate scores. - *

    - * The idea is that we maintain the same level of accuracy as we would get from a graph search, by re-ranking the top - * `k` best approximate scores at a time with the full resolution vectors to return the top `limit`. - *

    - * For example, suppose that limit=3 and k=5 and we have ten elements. After our first re-ranking batch, we have - * ABDEF????? - * We will return A, B, and D; if more elements are requested, we will re-rank another 5 (so three more, including - * the two remaining from the first batch). Here we uncover C, G, and H, and order them appropriately: - * CEFGH?? - * This illustrates that, also like a graph search, we only guarantee ordering of results within a re-ranking batch, - * not globally. - *

    - * Note that we deliberately do not fetch new items from the approximate list until the first batch of `limit`-many - * is consumed. We do this because we expect that most often the first limit-many will pass the final verification - * and only query more if some didn't (e.g. because the vector was deleted in a newer sstable). - *

    - * As an implementation detail, we use a heap to maintain state rather than a List and sorting. - */ -@NotThreadSafe -public class BruteForceRowIdIterator extends AbstractIterator -{ - // We use two binary heaps (NeighborQueue) because we do not need an eager ordering of - // these results. Depending on how many sstables the query hits and the relative scores of vectors from those - // sstables, we may not need to return more than the first handful of scores. - // Heap with compressed vector scores - private final NeighborQueue approximateScoreQueue; - private final SegmentRowIdOrdinalPairs segmentOrdinalPairs; - // Use the jvector NeighborQueue to avoid unnecessary object allocations - private final NeighborQueue exactScoreQueue; - private final NeighborSimilarity.ExactScoreFunction reranker; - private final GraphIndex.View view; - private final int topK; - private final int limit; - private int rerankedCount; - - /** - * @param approximateScoreQueue A heap of indexes ordered by their approximate similarity scores - * @param segmentOrdinalPairs A mapping from the index in the approximateScoreQueue to the node's rowId and ordinal - * @param reranker A function that takes a graph ordinal and returns the exact similarity score - * @param limit The query limit - * @param topK The number of vectors to resolve and score before returning results - * @param view The view of the graph, passed so we can close it when the iterator is closed - */ - public BruteForceRowIdIterator(NeighborQueue approximateScoreQueue, - SegmentRowIdOrdinalPairs segmentOrdinalPairs, - NeighborSimilarity.ExactScoreFunction reranker, - int limit, - int topK, - GraphIndex.View view) - { - this.approximateScoreQueue = approximateScoreQueue; - this.segmentOrdinalPairs = segmentOrdinalPairs; - this.exactScoreQueue = new NeighborQueue(limit, true); - this.reranker = reranker; - assert topK >= limit : "topK must be greater than or equal to limit. Found: " + topK + " < " + limit; - this.limit = limit; - this.topK = topK; - this.rerankedCount = topK; // placeholder to kick off computeNext - this.view = view; - } - - @Override - protected RowIdWithScore computeNext() - { - int consumed = rerankedCount - exactScoreQueue.size(); - if (consumed >= limit) - { - // Refill the exactScoreQueue until it reaches topK exact scores, or the approximate score queue is empty - while (approximateScoreQueue.size() > 0 && exactScoreQueue.size() < topK) - { - int segmentOrdinalIndex = approximateScoreQueue.pop(); - int rowId = segmentOrdinalPairs.getSegmentRowId(segmentOrdinalIndex); - int ordinal = segmentOrdinalPairs.getOrdinal(segmentOrdinalIndex); - float score = reranker.similarityTo(ordinal); - exactScoreQueue.add(rowId, score); - } - rerankedCount = exactScoreQueue.size(); - } - if (exactScoreQueue.size() == 0) - return endOfData(); - - float score = exactScoreQueue.topScore(); - int rowId = exactScoreQueue.pop(); - return new RowIdWithScore(rowId, score); - } - - @Override - public void close() - { - FileUtils.closeQuietly(view); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/CompactionVectorValues.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/CompactionVectorValues.java deleted file mode 100644 index af23a666a91b..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/CompactionVectorValues.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import javax.annotation.concurrent.NotThreadSafe; - -import io.github.jbellis.jvector.util.RamUsageEstimator; -import org.apache.cassandra.db.marshal.VectorType; -import org.apache.cassandra.io.util.SequentialWriter; - -@NotThreadSafe -public class CompactionVectorValues implements RamAwareVectorValues -{ - private final int dimension; - private final ArrayList values = new ArrayList<>(); - private final VectorType type; - - public CompactionVectorValues(VectorType type) - { - this.dimension = type.dimension; - this.type = type; - } - - @Override - public int size() - { - return values.size(); - } - - @Override - public int dimension() - { - return dimension; - } - - @Override - public float[] vectorValue(int i) - { - return type.composeAsFloat(values.get(i)); - } - - /** return approximate bytes used by the new vector */ - public long add(int ordinal, ByteBuffer value) - { - if (ordinal != values.size()) - throw new IllegalArgumentException(String.format("CVV requires vectors to be added in ordinal order (%d given, expected %d)", - ordinal, values.size())); - values.add(value); - return RamEstimation.concurrentHashMapRamUsed(1) + oneVectorBytesUsed(); - } - - @Override - public CompactionVectorValues copy() - { - return this; - } - - public long write(SequentialWriter writer) throws IOException - { - writer.writeInt(size()); - writer.writeInt(dimension()); - - for (int i = 0; i < size(); i++) { - ByteBuffer bb = values.get(i); - assert bb != null : "null vector at index " + i + " of " + size(); - writer.write(bb); - } - - return writer.position(); - } - - @Override - public boolean isValueShared() - { - return false; - } - - private long oneVectorBytesUsed() - { - return RamUsageEstimator.NUM_BYTES_OBJECT_REF; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/ConcurrentVectorValues.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/ConcurrentVectorValues.java deleted file mode 100644 index 89ae69251e0a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/ConcurrentVectorValues.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import org.jctools.maps.NonBlockingHashMapLong; - -public class ConcurrentVectorValues implements RamAwareVectorValues -{ - private final int dimensions; - private final NonBlockingHashMapLong values = new NonBlockingHashMapLong<>(); - - public ConcurrentVectorValues(int dimensions) - { - this.dimensions = dimensions; - } - - @Override - public int size() - { - return values.size(); - } - - @Override - public int dimension() - { - return dimensions; - } - - @Override - public float[] vectorValue(int i) - { - return values.get(i); - } - - /** return approximate bytes used by the new vector */ - public long add(int ordinal, float[] vector) - { - values.put(ordinal, vector); - return RamEstimation.concurrentHashMapRamUsed(1) + oneVectorBytesUsed(); - } - - @Override - public boolean isValueShared() - { - return false; - } - - @Override - public ConcurrentVectorValues copy() - { - // no actual copy required because we always return distinct float[] for distinct vector ordinals - return this; - } - - private long oneVectorBytesUsed() - { - return Integer.BYTES + Integer.BYTES + (long) dimension() * Float.BYTES; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/DiskAnn.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/DiskAnn.java deleted file mode 100644 index 41703715539d..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/DiskAnn.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; -import java.util.function.IntConsumer; - -import io.github.jbellis.jvector.disk.CachingGraphIndex; -import io.github.jbellis.jvector.disk.OnDiskGraphIndex; -import io.github.jbellis.jvector.graph.GraphIndex; -import io.github.jbellis.jvector.graph.GraphSearcher; -import io.github.jbellis.jvector.graph.NeighborSimilarity; -import io.github.jbellis.jvector.pq.CompressedVectors; -import io.github.jbellis.jvector.util.Bits; -import io.github.jbellis.jvector.vector.VectorSimilarityFunction; -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig; -import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.CloseableIterator; -import org.apache.cassandra.utils.Throwables; - -public class DiskAnn implements AutoCloseable -{ - private final FileHandle graphHandle; - private final OnDiskOrdinalsMap ordinalsMap; - private final CachingGraphIndex graph; - private final VectorSimilarityFunction similarityFunction; - private final String source; - - // only one of these will be not null - private final CompressedVectors compressedVectors; - - public DiskAnn(SegmentMetadata.ComponentMetadataMap componentMetadatas, PerColumnIndexFiles indexFiles, IndexWriterConfig config, SSTableId sstableId) throws IOException - { - similarityFunction = config.getSimilarityFunction(); - source = sstableId.toString(); - - SegmentMetadata.ComponentMetadata termsMetadata = componentMetadatas.get(IndexComponent.TERMS_DATA); - graphHandle = indexFiles.termsData(); - graph = new CachingGraphIndex(new OnDiskGraphIndex<>(RandomAccessReaderAdapter.createSupplier(graphHandle), termsMetadata.offset)); - - long pqSegmentOffset = componentMetadatas.get(IndexComponent.COMPRESSED_VECTORS).offset; - try (var pqFileHandle = indexFiles.compressedVectors(); var reader = new RandomAccessReaderAdapter(pqFileHandle)) - { - reader.seek(pqSegmentOffset); - boolean containsCompressedVectors = reader.readBoolean(); - if (containsCompressedVectors) - compressedVectors = CompressedVectors.load(reader, reader.getFilePointer()); - else - compressedVectors = null; - } - - SegmentMetadata.ComponentMetadata postingListsMetadata = componentMetadatas.get(IndexComponent.POSTING_LISTS); - ordinalsMap = new OnDiskOrdinalsMap(indexFiles.postingLists(), postingListsMetadata.offset, postingListsMetadata.length); - } - - public long ramBytesUsed() - { - return graph.ramBytesUsed(); - } - - public int size() - { - return graph.size(); - } - - public CompressedVectors getCompressedVectors() - { - return compressedVectors; - } - - /** - * @return Row IDs associated with the topK vectors near the query - */ - public CloseableIterator search(float[] queryVector, int topK, int limit, Bits acceptBits, IntConsumer nodesVisitedConsumer) - { - OnHeapGraph.validateIndexable(queryVector, similarityFunction); - - GraphIndex.View view = graph.getView(); - try - { - GraphSearcher searcher = new GraphSearcher.Builder<>(view).build(); - NeighborSimilarity.ScoreFunction scoreFunction; - NeighborSimilarity.ReRanker reRanker; - if (compressedVectors == null) - { - scoreFunction = (NeighborSimilarity.ExactScoreFunction) - i -> similarityFunction.compare(queryVector, view.getVector(i)); - reRanker = null; - } - else - { - scoreFunction = compressedVectors.approximateScoreFunctionFor(queryVector, similarityFunction); - reRanker = (i, map) -> similarityFunction.compare(queryVector, map.get(i)); - } - Bits acceptedBits = ordinalsMap.ignoringDeleted(acceptBits); - // Search is done within the iterator to keep track of visited nodes. The resulting iterator - // searches until the graph is exhausted. - AutoResumingNodeScoreIterator nodeScoreIterator = new AutoResumingNodeScoreIterator(searcher, scoreFunction, reRanker, topK, acceptedBits, nodesVisitedConsumer, false, source, view); - return new NodeScoreToRowIdWithScoreIterator(nodeScoreIterator, ordinalsMap.getRowIdsView()); - } - catch (Throwable e) - { - FileUtils.closeQuietly(view); - throw Throwables.unchecked(e); - } - } - - public NeighborSimilarity.ApproximateScoreFunction getApproximateScoreFunction(float[] queryVector) - { - return compressedVectors.approximateScoreFunctionFor(queryVector, similarityFunction); - } - - public NeighborSimilarity.ExactScoreFunction getExactScoreFunction(float[] queryVector, GraphIndex.View view) - { - return i -> similarityFunction.compare(queryVector, view.getVector(i)); - } - - @Override - public void close() throws IOException - { - ordinalsMap.close(); - graph.close(); - graphHandle.close(); - } - - public OnDiskOrdinalsMap.OrdinalsView getOrdinalsView() - { - return ordinalsMap.getOrdinalsView(); - } - - /** - * Get the graph view, callers must close the view. - * @return - */ - public GraphIndex.View getView() - { - return graph.getView(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/DiskBinarySearch.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/DiskBinarySearch.java deleted file mode 100644 index 017ddfd7698c..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/DiskBinarySearch.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - - -import java.util.function.Function; - -public class DiskBinarySearch -{ - /** - * Search for the target int between positions low and high, using the provided function - * to retrieve the int value at the given ordinal. - * - * Returns the position at which target is found. Raises an exception if it is not found. - * - * This will not call f() after the target is found, so if f is performing disk seeks, - * it will leave the underlying reader at the position right after reading the target. - * - * @return index if target is found; otherwise return -1 if targer is not found - */ - public static long searchInt(long low, long high, int target, Function f) - { - assert high < Long.MAX_VALUE >> 2 : "high is too large to avoid potential overflow: " + high; - assert low < high : "low must be less than high: " + low + " >= " + high; - - while (low < high) - { - long i = low + (high - low) / 2; - int value = f.apply(i); - if (target == value) - return i; - else if (target > value) - low = i + 1; - else - high = i; - } - return -1; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/NeighborQueueRowIdIterator.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/NeighborQueueRowIdIterator.java deleted file mode 100644 index cc8dfaa8731c..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/NeighborQueueRowIdIterator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import org.apache.cassandra.utils.AbstractIterator; - -import io.github.jbellis.jvector.graph.NeighborQueue; - -/** - * An iterator over {@link RowIdWithScore} that lazily consumes a {@link NeighborQueue}. - */ -public class NeighborQueueRowIdIterator extends AbstractIterator -{ - private final NeighborQueue scoreQueue; - - public NeighborQueueRowIdIterator(NeighborQueue scoreQueue) - { - this.scoreQueue = scoreQueue; - } - - @Override - protected RowIdWithScore computeNext() - { - if (scoreQueue.size() == 0) - return endOfData(); - float score = scoreQueue.topScore(); - int rowId = scoreQueue.pop(); - return new RowIdWithScore(rowId, score); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OnDiskOrdinalsMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OnDiskOrdinalsMap.java deleted file mode 100644 index 673dcc3170eb..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OnDiskOrdinalsMap.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; - -import com.google.common.base.Preconditions; - -import io.github.jbellis.jvector.util.Bits; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.RandomAccessReader; - -public class OnDiskOrdinalsMap implements AutoCloseable -{ - private final FileHandle fh; - private final long ordToRowOffset; - private final long segmentEnd; - private final int size; - // the offset where we switch from recording ordinal -> rows, to row -> ordinal - private final long rowOrdinalOffset; - private final Set deletedOrdinals; - - public OnDiskOrdinalsMap(FileHandle fh, long segmentOffset, long segmentLength) - { - deletedOrdinals = new HashSet<>(); - - this.segmentEnd = segmentOffset + segmentLength; - this.fh = fh; - try (var reader = fh.createReader()) - { - reader.seek(segmentOffset); - int deletedCount = reader.readInt(); - for (int i = 0; i < deletedCount; i++) - { - deletedOrdinals.add(reader.readInt()); - } - - this.ordToRowOffset = reader.getFilePointer(); - this.size = reader.readInt(); - reader.seek(segmentEnd - 8); - this.rowOrdinalOffset = reader.readLong(); - assert rowOrdinalOffset < segmentEnd : "rowOrdinalOffset " + rowOrdinalOffset + " is not less than segmentEnd " + segmentEnd; - } - catch (Exception e) - { - throw new RuntimeException("Error initializing OnDiskOrdinalsMap at segment " + segmentOffset, e); - } - } - - public RowIdsView getRowIdsView() - { - return new RowIdsView(); - } - - public Bits ignoringDeleted(Bits acceptBits) - { - return BitsUtil.bitsIgnoringDeleted(acceptBits, deletedOrdinals); - } - - public class RowIdsView implements AutoCloseable - { - final RandomAccessReader reader = fh.createReader(); - - public int[] getSegmentRowIdsMatching(int vectorOrdinal) throws IOException - { - Preconditions.checkArgument(vectorOrdinal < size, "vectorOrdinal %s is out of bounds %s", vectorOrdinal, size); - - // read index entry - try - { - reader.seek(ordToRowOffset + 4L + vectorOrdinal * 8L); - } - catch (Exception e) - { - throw new RuntimeException(String.format("Error seeking to index offset for ordinal %d with ordToRowOffset %d", - vectorOrdinal, ordToRowOffset), e); - } - long offset = reader.readLong(); - // seek to and read rowIds - try - { - reader.seek(offset); - } - catch (Exception e) - { - throw new RuntimeException(String.format("Error seeking to rowIds offset for ordinal %d with ordToRowOffset %d", - vectorOrdinal, ordToRowOffset), e); - } - int postingsSize = reader.readInt(); - int[] rowIds = new int[postingsSize]; - for (int i = 0; i < rowIds.length; i++) - { - rowIds[i] = reader.readInt(); - } - return rowIds; - } - - @Override - public void close() - { - reader.close(); - } - } - - public OrdinalsView getOrdinalsView() - { - return new OrdinalsView(); - } - - public class OrdinalsView implements AutoCloseable - { - final RandomAccessReader reader = fh.createReader(); - private final long high = (segmentEnd - 8 - rowOrdinalOffset) / 8; - - /** - * @return order if given row id is found; otherwise return -1 - */ - public int getOrdinalForRowId(int rowId) throws IOException - { - // Compute the offset of the start of the rowId to vectorOrdinal mapping - long index = DiskBinarySearch.searchInt(0, Math.toIntExact(high), rowId, i -> { - try - { - long offset = rowOrdinalOffset + i * 8; - reader.seek(offset); - return reader.readInt(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - }); - - // not found - if (index < 0) - return -1; - - return reader.readInt(); - } - - @Override - public void close() - { - reader.close(); - } - } - - @Override - public void close() - { - fh.close(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OnHeapGraph.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OnHeapGraph.java deleted file mode 100644 index a823943a4e77..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OnHeapGraph.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.IntStream; - -import org.cliffc.high_scale_lib.NonBlockingHashMap; -import org.cliffc.high_scale_lib.NonBlockingHashMapLong; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.github.jbellis.jvector.disk.OnDiskGraphIndex; -import io.github.jbellis.jvector.graph.GraphIndex; -import io.github.jbellis.jvector.graph.GraphIndexBuilder; -import io.github.jbellis.jvector.graph.GraphSearcher; -import io.github.jbellis.jvector.graph.NeighborSimilarity; -import io.github.jbellis.jvector.graph.RandomAccessVectorValues; -import io.github.jbellis.jvector.graph.SearchResult; -import io.github.jbellis.jvector.pq.CompressedVectors; -import io.github.jbellis.jvector.pq.ProductQuantization; -import io.github.jbellis.jvector.util.Bits; -import io.github.jbellis.jvector.util.RamUsageEstimator; -import io.github.jbellis.jvector.vector.VectorEncoding; -import io.github.jbellis.jvector.vector.VectorSimilarityFunction; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.VectorType; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.index.sai.disk.format.IndexComponent; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; -import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig; -import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.io.util.SequentialWriter; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.CloseableIterator; -import org.apache.lucene.util.StringHelper; - -public class OnHeapGraph -{ - private static final Logger logger = LoggerFactory.getLogger(OnHeapGraph.class); - - public static final int MIN_PQ_ROWS = 1024; - - private final RamAwareVectorValues vectorValues; - private final GraphIndexBuilder builder; - private final VectorType vectorType; - private final VectorSimilarityFunction similarityFunction; - private final ConcurrentMap> postingsMap; - private final NonBlockingHashMapLong> postingsByOrdinal; - private final NonBlockingHashMap vectorsByKey; - private final AtomicInteger nextOrdinal = new AtomicInteger(); - private volatile boolean hasDeletions; - private String source; - - /** - * @param termComparator the vector type - * @param indexWriterConfig the {@link IndexWriterConfig} for the graph - * @param memtable should be provided if attached to a memtable, null otherwise (i.e. compaction). Allows us to - * configure concurrent search and provide more meaningful trace logging. Concurrent search - * while building the graph; non-concurrent allows us to avoid synchronization costs. - */ - @SuppressWarnings("unchecked") - public OnHeapGraph(AbstractType termComparator, IndexWriterConfig indexWriterConfig, Memtable memtable) - { - this.vectorType = (VectorType) termComparator; - source = memtable != null - ? memtable.getClass().getSimpleName() + '@' + Integer.toHexString(memtable.hashCode()) - : "compaction"; - vectorValues = memtable != null - ? new ConcurrentVectorValues(((VectorType) termComparator).dimension) - : new CompactionVectorValues(((VectorType) termComparator)); - similarityFunction = indexWriterConfig.getSimilarityFunction(); - // We need to be able to inexpensively distinguish different vectors, with a slower path - // that identifies vectors that are equal but not the same reference. A comparison - // based Map (which only needs to look at vector elements until a difference is found) - // is thus a better option than hash-based (which has to look at all elements to compute the hash). - postingsMap = new ConcurrentSkipListMap<>(Arrays::compare); - postingsByOrdinal = new NonBlockingHashMapLong<>(); - vectorsByKey = memtable != null ? new NonBlockingHashMap<>() : null; - - builder = new GraphIndexBuilder<>(vectorValues, - VectorEncoding.FLOAT32, - similarityFunction, - indexWriterConfig.getMaximumNodeConnections(), - indexWriterConfig.getConstructionBeamWidth(), - 1.2f, - 1.4f); - } - - public int size() - { - return vectorValues.size(); - } - - public boolean isEmpty() - { - return postingsMap.values().stream().allMatch(VectorPostings::isEmpty); - } - - /** - * @return the incremental bytes ysed by adding the given vector to the index - */ - public long add(ByteBuffer term, T key, InvalidVectorBehavior behavior) - { - assert term != null && term.remaining() != 0; - - float[] vector = vectorType.composeAsFloat(term); - if (behavior == InvalidVectorBehavior.IGNORE) - { - try - { - validateIndexable(vector, similarityFunction); - } - catch (InvalidRequestException e) - { - logger.trace("Ignoring invalid vector during index build against existing data: {}", vector, e); - return 0; - } - } - else - { - assert behavior == InvalidVectorBehavior.FAIL; - validateIndexable(vector, similarityFunction); - } - - long bytesUsed = 0L; - - // Store a cached reference to the vector for brute force computations later. Because insertions - // for the same primary key are guaranteed to be sequential, there is no race condition here. - if (vectorsByKey != null) - { - vectorsByKey.put(key, vector); - // The size of the entries themselves are counted below, so just count the two extra references - bytesUsed += RamUsageEstimator.NUM_BYTES_OBJECT_REF * 2L; - } - - VectorPostings postings = postingsMap.get(vector); - // if the vector is already in the graph, all that happens is that the postings list is updated - // otherwise, we add the vector in this order: - // 1. to the postingsMap - // 2. to the vectorValues - // 3. to the graph - // This way, concurrent searches of the graph won't see the vector until it's visible - // in the other structures as well. - if (postings == null) - { - postings = new VectorPostings<>(key); - // since we are using ConcurrentSkipListMap, it is NOT correct to use computeIfAbsent here - if (postingsMap.putIfAbsent(vector, postings) == null) - { - // we won the race to add the new entry; assign it an ordinal and add to the other structures - int ordinal = nextOrdinal.getAndIncrement(); - postings.setOrdinal(ordinal); - bytesUsed += RamEstimation.concurrentHashMapRamUsed(1); // the new posting Map entry - bytesUsed += (vectorValues instanceof ConcurrentVectorValues) - ? ((ConcurrentVectorValues) vectorValues).add(ordinal, vector) - : ((CompactionVectorValues) vectorValues).add(ordinal, term); - bytesUsed += VectorPostings.emptyBytesUsed() + VectorPostings.bytesPerPosting(); - postingsByOrdinal.put(ordinal, postings); - bytesUsed += builder.addGraphNode(ordinal, vectorValues); - return bytesUsed; - } - else - { - postings = postingsMap.get(vector); - } - } - // postings list already exists, just add the new key (if it's not already in the list) - if (postings.add(key)) - { - bytesUsed += VectorPostings.bytesPerPosting(); - } - - return bytesUsed; - } - - // copied out of a Lucene PR -- hopefully committed soon - public static final float MAX_FLOAT32_COMPONENT = 1E17f; - - public static void checkInBounds(float[] v) - { - for (int i = 0; i < v.length; i++) - { - if (!Float.isFinite(v[i])) - { - throw new IllegalArgumentException("non-finite value at vector[" + i + "]=" + v[i]); - } - - if (Math.abs(v[i]) > MAX_FLOAT32_COMPONENT) - { - throw new IllegalArgumentException("Out-of-bounds value at vector[" + i + "]=" + v[i]); - } - } - } - - public static void validateIndexable(float[] vector, VectorSimilarityFunction similarityFunction) - { - try - { - checkInBounds(vector); - } - catch (IllegalArgumentException e) - { - throw new InvalidRequestException(e.getMessage()); - } - - if (similarityFunction == VectorSimilarityFunction.COSINE) - { - for (int i = 0; i < vector.length; i++) - { - if (vector[i] != 0) - return; - } - throw new InvalidRequestException("Zero vectors cannot be indexed or queried with cosine similarity"); - } - } - - public Collection keysFromOrdinal(int node) - { - return postingsByOrdinal.get(node).getPostings(); - } - - public float[] vectorForKey(T key) - { - if (vectorsByKey == null) - throw new IllegalStateException("vectorsByKey is not initialized"); - return vectorsByKey.get(key); - } - - public long remove(ByteBuffer term, T key) - { - assert term != null && term.remaining() != 0; - - float[] vector = vectorType.composeAsFloat(term); - VectorPostings postings = postingsMap.get(vector); - if (postings == null) - { - // it's possible for this to be called against a different memtable than the one - // the value was originally added to, in which case we do not expect to find - // the key among the postings for this vector - return 0; - } - - hasDeletions = true; - long bytesUsed = postings.remove(key); - - if (vectorsByKey != null) - { - // On updates to a row, we call add then remove, so we must pass the key's value to ensure we only remove - // the deleted vector from vectorsByKey. - vectorsByKey.remove(key, vector); - } - - return bytesUsed; - } - - /** - * @return keys (PrimaryKey or segment row id) associated with the topK vectors near the query - */ - public CloseableIterator search(float[] queryVector, int limit, Bits toAccept) - { - validateIndexable(queryVector, similarityFunction); - - // search() errors out when an empty graph is passed to it - if (vectorValues.size() == 0) - return CloseableIterator.empty(); - - Bits bits = hasDeletions ? BitsUtil.bitsIgnoringDeleted(toAccept, postingsByOrdinal) : toAccept; - GraphIndex graph = builder.getGraph(); - GraphIndex.View view = graph.getView(); - GraphSearcher searcher = new GraphSearcher.Builder<>(view).withConcurrentUpdates().build(); - NeighborSimilarity.ExactScoreFunction scoreFunction = node2 -> vectorCompareFunction(queryVector, node2); - return new AutoResumingNodeScoreIterator(searcher, scoreFunction, null, limit, bits, v -> {}, true, source, view); - } - - public SegmentMetadata.ComponentMetadataMap writeData(IndexDescriptor indexDescriptor, IndexIdentifier indexIdentifier, Function postingTransformer) throws IOException - { - int nInProgress = builder.insertsInProgress(); - assert nInProgress == 0 : String.format("Attempting to write graph while %d inserts are in progress", nInProgress); - assert nextOrdinal.get() == builder.getGraph().size() : String.format("nextOrdinal %d != graph size %d -- ordinals should be sequential", - nextOrdinal.get(), builder.getGraph().size()); - assert vectorValues.size() == builder.getGraph().size() : String.format("vector count %d != graph size %d", - vectorValues.size(), builder.getGraph().size()); - assert postingsMap.keySet().size() == vectorValues.size() : String.format("postings map entry count %d != vector count %d", - postingsMap.keySet().size(), vectorValues.size()); - logger.debug("Writing graph with {} rows and {} distinct vectors", postingsMap.values().stream().mapToInt(VectorPostings::size).sum(), vectorValues.size()); - - try (var pqOutput = IndexFileUtils.instance.openOutput(indexDescriptor.fileFor(IndexComponent.COMPRESSED_VECTORS, indexIdentifier), true); - var postingsOutput = IndexFileUtils.instance.openOutput(indexDescriptor.fileFor(IndexComponent.POSTING_LISTS, indexIdentifier), true); - var indexOutput = IndexFileUtils.instance.openOutput(indexDescriptor.fileFor(IndexComponent.TERMS_DATA, indexIdentifier), true)) - { - SAICodecUtils.writeHeader(pqOutput); - SAICodecUtils.writeHeader(postingsOutput); - SAICodecUtils.writeHeader(indexOutput); - - // compute and write PQ - long pqOffset = pqOutput.getFilePointer(); - long pqPosition = writePQ(pqOutput.asSequentialWriter()); - long pqLength = pqPosition - pqOffset; - - Set deletedOrdinals = new HashSet<>(); - postingsMap.values().stream().filter(VectorPostings::isEmpty).forEach(vectorPostings -> deletedOrdinals.add(vectorPostings.getOrdinal())); - // remove ordinals that don't have corresponding row ids due to partition/range deletion - for (VectorPostings vectorPostings : postingsMap.values()) - { - vectorPostings.computeRowIds(postingTransformer); - if (vectorPostings.shouldAppendDeletedOrdinal()) - deletedOrdinals.add(vectorPostings.getOrdinal()); - } - // write postings - long postingsOffset = postingsOutput.getFilePointer(); - long postingsPosition = new VectorPostingsWriter().writePostings(postingsOutput.asSequentialWriter(), vectorValues, postingsMap, deletedOrdinals); - long postingsLength = postingsPosition - postingsOffset; - - // complete (internal clean up) and write the graph - builder.complete(); - long termsOffset = indexOutput.getFilePointer(); - OnDiskGraphIndex.write(builder.getGraph(), vectorValues, indexOutput.asSequentialWriter()); - long termsLength = indexOutput.getFilePointer() - termsOffset; - - // write footers/checksums - SAICodecUtils.writeFooter(pqOutput); - SAICodecUtils.writeFooter(postingsOutput); - SAICodecUtils.writeFooter(indexOutput); - - // add components to the metadata map - SegmentMetadata.ComponentMetadataMap metadataMap = new SegmentMetadata.ComponentMetadataMap(); - metadataMap.put(IndexComponent.TERMS_DATA, -1, termsOffset, termsLength, Map.of()); - metadataMap.put(IndexComponent.POSTING_LISTS, -1, postingsOffset, postingsLength, Map.of()); - Map vectorConfigs = Map.of("SEGMENT_ID", ByteBufferUtil.bytesToHex(ByteBuffer.wrap(StringHelper.randomId()))); - metadataMap.put(IndexComponent.COMPRESSED_VECTORS, -1, pqOffset, pqLength, vectorConfigs); - return metadataMap; - } - } - - private float vectorCompareFunction(float[] queryVector, int node) - { - return similarityFunction.compare(queryVector, ((RandomAccessVectorValues) vectorValues).vectorValue(node)); - } - - private long writePQ(SequentialWriter writer) throws IOException - { - // don't bother with PQ if there are fewer than 1K vectors - int M = vectorValues.dimension() / 2; - writer.writeBoolean(vectorValues.size() >= MIN_PQ_ROWS); - if (vectorValues.size() < MIN_PQ_ROWS) - { - logger.debug("Skipping PQ for only {} vectors", vectorValues.size()); - return writer.position(); - } - - logger.debug("Computing PQ for {} vectors", vectorValues.size()); - // limit the PQ computation and encoding to one index at a time -- goal during flush is to - // evict from memory ASAP so better to do the PQ build (in parallel) one at a time - ProductQuantization pq; - byte[][] encoded; - synchronized (OnHeapGraph.class) - { - // train PQ and encode - pq = ProductQuantization.compute(vectorValues, M, false); - assert !vectorValues.isValueShared(); - encoded = IntStream.range(0, vectorValues.size()) - .parallel() - .mapToObj(i -> pq.encode(vectorValues.vectorValue(i))) - .toArray(byte[][]::new); - } - CompressedVectors cv = new CompressedVectors(pq, encoded); - // save - cv.write(writer); - return writer.position(); - } - - public enum InvalidVectorBehavior - { - IGNORE, - FAIL - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OptimizeFor.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OptimizeFor.java deleted file mode 100644 index cdc5ab5de262..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/OptimizeFor.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.util.function.Function; - -import static java.lang.Math.pow; - -/** - * Allows the vector index searches to be optimised for latency or recall. This is used by the - * {@link org.apache.cassandra.index.sai.disk.v1.segment.VectorIndexSegmentSearcher} to determine how many results to ask the graph - * to search for. If we are optimising for {@link #RECALL} we ask for more than the requested limit which - * (since it will search deeper in the graph) will tend to surface slightly better results. - */ -public enum OptimizeFor -{ - LATENCY(limit -> 0.979 + 4.021 * pow(limit, -0.761)), // f(1) = 5.0, f(100) = 1.1, f(1000) = 1.0 - RECALL(limit -> 0.509 + 9.491 * pow(limit, -0.402)); // f(1) = 10.0, f(100) = 2.0, f(1000) = 1.1 - - private final Function limitMultiplier; - - OptimizeFor(Function limitMultiplier) - { - this.limitMultiplier = limitMultiplier; - } - - public int topKFor(int limit) - { - return (int)(Math.max(1.0, limitMultiplier.apply(limit)) * limit); - } - - public static OptimizeFor fromString(String value) - { - return valueOf(value.toUpperCase()); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/PrimaryKeyWithScore.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/PrimaryKeyWithScore.java deleted file mode 100644 index c81c4ebd20df..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/PrimaryKeyWithScore.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import org.apache.cassandra.db.CellSourceIdentifier; -import org.apache.cassandra.db.rows.Cell; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sai.utils.CellWithSource; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.schema.ColumnMetadata; - -/** - * A PrimaryKey with one piece of metadata. Subclasses define the metadata, and to prevent unnecessary boxing, the - * metadata is not referenced in this class. The metadata is not used to determine equality or hash code, but it is used - * to compare the PrimaryKey objects. - * Note: this class has a natural ordering that is inconsistent with equals. - */ -public class PrimaryKeyWithScore implements Comparable -{ - protected final ColumnMetadata columnMetadata; - private final PrimaryKey primaryKey; - private final CellSourceIdentifier sourceTable; - - private final float indexScore; - - public PrimaryKeyWithScore(ColumnMetadata columnMetadata, CellSourceIdentifier sourceTable, PrimaryKey primaryKey, float indexScore) - { - this.columnMetadata = columnMetadata; - this.sourceTable = sourceTable; - this.primaryKey = primaryKey; - this.indexScore = indexScore; - } - - public PrimaryKey primaryKey() - { - return primaryKey; - } - - public boolean isIndexDataValid(Row row, long nowInSecs) - { - // If the indexed column is part of the primary key, we don't need this type of validation because we would have - // fetched the row using the indexed primary key, so they have to match. - if (columnMetadata.isPrimaryKeyColumn()) - return true; - - // If the row is static and the column is not static, or vice versa, the indexed value won't be present so we - // don't need to check if live data matches indexed data. - if (row.isStatic() != columnMetadata.isStatic()) - return true; - - Cell cell = row.getCell(columnMetadata); - if (!cell.isLive(nowInSecs)) - return false; - - assert cell instanceof CellWithSource : "Expected CellWithSource, got " + cell.getClass(); - return sourceTable.isEqualSource(((CellWithSource) cell).sourceTable()); - } - - @Override - public int compareTo(PrimaryKeyWithScore o) - { - // Descending order - return Float.compare(o.indexScore, indexScore); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RamEstimation.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RamEstimation.java deleted file mode 100644 index 4288a84e014a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RamEstimation.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import org.apache.lucene.util.RamUsageEstimator; - -public class RamEstimation -{ - /** - * @param externalNodeCount the size() of the ConcurrentHashMap - * @return an estimate of the number of bytes used - */ - public static long concurrentHashMapRamUsed(int externalNodeCount) { - long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; - long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER; - long CORES = Runtime.getRuntime().availableProcessors(); - - long chmNodeBytes = - REF_BYTES // node itself in Node[] - + 3L * REF_BYTES - + Integer.BYTES; // node internals - float chmLoadFactor = 0.75f; // this is hardcoded inside ConcurrentHashMap - // CHM has a striped counter Cell implementation, we expect at most one per core - long chmCounters = AH_BYTES + CORES * (REF_BYTES + Long.BYTES); - - double nodeCount = externalNodeCount / chmLoadFactor; - - return - (long) nodeCount * (chmNodeBytes + REF_BYTES)// nodes - + AH_BYTES // nodes array - + Long.BYTES - + 3 * Integer.BYTES - + 3 * REF_BYTES // extra internal fields - + chmCounters - + REF_BYTES; // the Map reference itself - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RandomAccessReaderAdapter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RandomAccessReaderAdapter.java deleted file mode 100644 index 95ea81f3234e..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RandomAccessReaderAdapter.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; - -import com.google.common.primitives.Ints; - -import io.github.jbellis.jvector.disk.ReaderSupplier; -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.RandomAccessReader; -import org.apache.cassandra.io.util.Rebufferer.BufferHolder; - -public class RandomAccessReaderAdapter extends RandomAccessReader implements io.github.jbellis.jvector.disk.RandomAccessReader -{ - static ReaderSupplier createSupplier(FileHandle fileHandle) - { - return () -> new RandomAccessReaderAdapter(fileHandle); - } - - RandomAccessReaderAdapter(FileHandle fileHandle) - { - super(fileHandle.instantiateRebufferer(null)); - } - - @Override - public void readFully(float[] dest) throws IOException - { - BufferHolder bh = bufferHolder; - long position = getPosition(); - - FloatBuffer floatBuffer; - if (bh.offset() == 0 && position % Float.BYTES == 0) - { - // this is a separate code path because buffer() and asFloatBuffer() both allocate - // new and relatively expensive xBuffer objects, so we want to avoid doing that - // twice, where possible - floatBuffer = bh.floatBuffer(); - floatBuffer.position(Ints.checkedCast(position / Float.BYTES)); - } - else - { - // offset is non-zero, and probably not aligned to Float.BYTES, so - // set the position before converting to FloatBuffer. - ByteBuffer bb = bh.buffer(); - bb.position(Ints.checkedCast(position - bh.offset())); - floatBuffer = bb.asFloatBuffer(); - } - - if (dest.length > floatBuffer.remaining()) - { - // slow path -- desired slice is across region boundaries - ByteBuffer bb = ByteBuffer.allocate(Float.BYTES * dest.length); - readFully(bb); - floatBuffer = bb.asFloatBuffer(); - } - - floatBuffer.get(dest); - seek(position + (long) Float.BYTES * dest.length); - } - - /** - * Read ints into an int[], starting at the current position. - * - * @param dest the array to read into - * @param offset the offset in the array at which to start writing ints - * @param count the number of ints to read - * - * Will change the buffer position. - */ - @Override - public void read(int[] dest, int offset, int count) throws IOException - { - if (count == 0) - return; - - BufferHolder bh = bufferHolder; - long position = getPosition(); - - IntBuffer intBuffer; - if (bh.offset() == 0 && position % Integer.BYTES == 0) - { - // this is a separate code path because buffer() and asIntBuffer() both allocate - // new and relatively expensive xBuffer objects, so we want to avoid doing that - // twice, where possible - intBuffer = bh.intBuffer(); - intBuffer.position(Ints.checkedCast(position / Integer.BYTES)); - } - else - { - // offset is non-zero, and probably not aligned to Integer.BYTES, so - // set the position before converting to IntBuffer. - ByteBuffer bb = bh.buffer(); - bb.position(Ints.checkedCast(position - bh.offset())); - intBuffer = bb.asIntBuffer(); - } - - if (count > intBuffer.remaining()) - { - // slow path -- desired slice is across region boundaries - ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * count); - readFully(bb); - intBuffer = bb.asIntBuffer(); - } - - intBuffer.get(dest, offset, count); - seek(position + (long) Integer.BYTES * count); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RowIdToPrimaryKeyWithScoreIterator.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RowIdToPrimaryKeyWithScoreIterator.java deleted file mode 100644 index b09579e044a6..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RowIdToPrimaryKeyWithScoreIterator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; - -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.CloseableIterator; - -/** - * An iterator over scored primary keys ordered by the score descending - * Not skippable. - */ -public class RowIdToPrimaryKeyWithScoreIterator extends AbstractIterator -{ - private final ColumnMetadata column; - private final SSTableId sstableId; - private final PrimaryKeyMap primaryKeyMap; - private final CloseableIterator scoredRowIdIterator; - private final long segmentRowIdOffset; - - public RowIdToPrimaryKeyWithScoreIterator(ColumnMetadata column, - PrimaryKeyMap.Factory primaryKeyMapFactory, - CloseableIterator scoredRowIdIterator, - long segmentRowIdOffset) throws IOException - { - this.column = column; - this.scoredRowIdIterator = scoredRowIdIterator; - this.primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(); - this.sstableId = primaryKeyMap.getSSTableId(); - this.segmentRowIdOffset = segmentRowIdOffset; - } - - @Override - protected PrimaryKeyWithScore computeNext() - { - if (!scoredRowIdIterator.hasNext()) - return endOfData(); - RowIdWithScore rowIdWithScore = scoredRowIdIterator.next(); - return rowIdWithScore.toPrimaryKeyWithScore(column, sstableId, primaryKeyMap, segmentRowIdOffset); - } - - @Override - public void close() - { - FileUtils.closeQuietly(primaryKeyMap); - FileUtils.closeQuietly(scoredRowIdIterator); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RowIdWithScore.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RowIdWithScore.java deleted file mode 100644 index 310080dd6b16..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RowIdWithScore.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.io.sstable.SSTableId; -import org.apache.cassandra.schema.ColumnMetadata; - -/** - * Represents a row id with its computed score. - */ -public class RowIdWithScore -{ - private final int segmentRowId; - private final float score; - - public RowIdWithScore(int segmentRowId, float score) - { - this.segmentRowId = segmentRowId; - this.score = score; - } - - public PrimaryKeyWithScore toPrimaryKeyWithScore(ColumnMetadata columnMetadata, - SSTableId sstableId, - PrimaryKeyMap primaryKeyMap, - long segmentRowIdOffset) - { - PrimaryKey pk = primaryKeyMap.primaryKeyFromRowId(segmentRowIdOffset + segmentRowId); - return new PrimaryKeyWithScore(columnMetadata, sstableId, pk, score); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/SegmentRowIdOrdinalPairs.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/SegmentRowIdOrdinalPairs.java deleted file mode 100644 index 6335bde14d7b..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/SegmentRowIdOrdinalPairs.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.util.function.IntConsumer; - -import io.github.jbellis.jvector.graph.NeighborQueue; -import io.github.jbellis.jvector.graph.NeighborSimilarity; - -/** - * A specialized data structure that stores segment row id to ordinal pairs efficiently. Implemented as an array of int - * pairs that avoids boxing. - */ -public class SegmentRowIdOrdinalPairs -{ - private final int capacity; - private int size; - private final int[] array; - - /** - * Create a new SegmentRowIdOrdinalPairs with the given capacity. - * @param capacity the capacity - */ - public SegmentRowIdOrdinalPairs(int capacity) - { - assert capacity < Integer.MAX_VALUE / 2 : "capacity is too large " + capacity; - this.capacity = capacity; - this.size = 0; - this.array = new int[capacity * 2]; - } - - /** - * Add a pair to the array. - * @param segmentRowId the first value - * @param ordinal the second value - */ - public void add(int segmentRowId, int ordinal) - { - if (size == capacity) - throw new ArrayIndexOutOfBoundsException(size); - array[size * 2] = segmentRowId; - array[size * 2 + 1] = ordinal; - size++; - } - - /** - * Get the row id at the given index. - * @param index the index - * @return the row id - */ - public int getSegmentRowId(int index) - { - if ( index < 0 || index >= size) - throw new ArrayIndexOutOfBoundsException(index); - return array[index * 2]; - } - - /** - * Get the ordinal at the given index. - * @param index the index - * @return the ordinal - */ - public int getOrdinal(int index) - { - if ( index < 0 || index >= size) - throw new ArrayIndexOutOfBoundsException(index); - return array[index * 2 + 1]; - } - - /** - * The number of pairs in the array. - * @return the number of pairs in the array - */ - public int size() - { - return size; - } - - /** - * Create an iterator over the segment row id and scored ordinal pairs in the array. - * @param scoreFunction the score function to use to compute the next score based on the ordinal - * @return a {@link NeighborQueue} - */ - public NeighborQueue mapToSegmentRowIdScoreHeap(NeighborSimilarity.ScoreFunction scoreFunction) - { - // TODO this could be improved using Floyd's algorithm in a later jvector version - NeighborQueue queue = new NeighborQueue(size(), true); - for (int i = 0; i < size; i++) - queue.add(array[i * 2], scoreFunction.similarityTo(array[i * 2 + 1])); // rowid, score - return queue; - } - - /** - * Create an iterator over the index and scored ordinal pairs in the array. - * @param scoreFunction the score function to use to compute the next score based on the ordinal - */ - public NeighborQueue mapToIndexScoreIterator(NeighborSimilarity.ScoreFunction scoreFunction) - { - // TODO this could be improved using Floyd's algorithm in a later jvector version - NeighborQueue queue = new NeighborQueue(size(), true); - for (int i = 0; i < size; i++) - queue.add(i, scoreFunction.similarityTo(array[i * 2 + 1])); // index, score - return queue; - } - - /** - * Calls the consumer for each right value in each pair of the array. - * @param consumer the consumer to call for each right value - */ - public void forEachOrdinal(IntConsumer consumer) - { - for (int i = 0; i < size; i++) - consumer.accept(array[i * 2 + 1]); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/VectorPostings.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/VectorPostings.java deleted file mode 100644 index 468c85037ebc..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/VectorPostings.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Function; - -import com.google.common.base.Preconditions; - -import org.agrona.collections.IntArrayList; -import org.apache.lucene.util.RamUsageEstimator; - -public class VectorPostings -{ - private final List postings; - private volatile int ordinal = -1; - - private volatile IntArrayList rowIds; - - public VectorPostings(T firstKey) - { - // we expect that the overwhelmingly most common cardinality will be 1, so optimize for reads - postings = new CopyOnWriteArrayList<>(List.of(firstKey)); - } - - /** - * Split out from constructor only to make dealing with concurrent inserts easier for CassandraOnHeapGraph. - * Should be called at most once per instance. - */ - void setOrdinal(int ordinal) - { - assert this.ordinal == -1 : String.format("ordinal already set to %d; attempted to set to %d", this.ordinal, ordinal); - this.ordinal = ordinal; - } - - public boolean add(T key) - { - for (T existing : postings) - if (existing.equals(key)) - return false; - postings.add(key); - return true; - } - - /** - * @return true if current ordinal is removed by partition/range deletion. - * Must be called after computeRowIds. - */ - public boolean shouldAppendDeletedOrdinal() - { - return !postings.isEmpty() && (rowIds != null && rowIds.isEmpty()); - } - - /** - * Compute the rowIds corresponding to the {@code } keys in this postings list. - */ - public void computeRowIds(Function postingTransformer) - { - Preconditions.checkState(rowIds == null); - - IntArrayList ids = new IntArrayList(postings.size(), -1); - for (T key : postings) - { - int rowId = postingTransformer.apply(key); - // partition deletion and range deletion won't trigger index update. There is no row id for given key during flush - if (rowId >= 0) - ids.add(rowId); - } - - rowIds = ids; - } - - /** - * @return rowIds corresponding to the {@code } keys in this postings list. - * Must be called after computeRowIds. - */ - public IntArrayList getRowIds() - { - Preconditions.checkNotNull(rowIds); - return rowIds; - } - - public long remove(T key) - { - long bytesUsed = ramBytesUsed(); - postings.remove(key); - return bytesUsed - ramBytesUsed(); - } - - public long ramBytesUsed() - { - return emptyBytesUsed() + postings.size() * bytesPerPosting(); - } - - public static long emptyBytesUsed() - { - long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; - long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER; - return Integer.BYTES + REF_BYTES + AH_BYTES; - } - - // we can't do this exactly without reflection, because keys could be Long or PrimaryKey. - // PK is larger, so we'll take that and return an upper bound. - // we already count the float[] vector in vectorValues, so leave it out here - public static long bytesPerPosting() - { - long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; - return REF_BYTES - + 2 * Long.BYTES // hashes in PreHashedDecoratedKey - + REF_BYTES; // key ByteBuffer, this is used elsewhere, so we don't take the deep size - } - - public int size() - { - return postings.size(); - } - - public List getPostings() - { - return postings; - } - - public boolean isEmpty() - { - return postings.isEmpty(); - } - - public int getOrdinal() - { - assert ordinal >= 0 : "ordinal not set"; - return ordinal; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/VectorPostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/vector/VectorPostingsWriter.java deleted file mode 100644 index b62575e6f3e9..000000000000 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/VectorPostingsWriter.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.disk.v1.vector; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.agrona.collections.IntArrayList; -import org.apache.cassandra.io.util.SequentialWriter; -import org.apache.cassandra.utils.Pair; - -public class VectorPostingsWriter -{ - public long writePostings(SequentialWriter writer, - RamAwareVectorValues vectorValues, - Map> postingsMap, - Set deletedOrdinals) throws IOException - { - writeDeletedOrdinals(writer, deletedOrdinals); - writeNodeOrdinalToRowIdMapping(writer, vectorValues, postingsMap); - writeRowIdToNodeOrdinalMapping(writer, vectorValues, postingsMap); - - return writer.position(); - } - - private void writeDeletedOrdinals(SequentialWriter writer, Set deletedOrdinals) throws IOException - { - writer.writeInt(deletedOrdinals.size()); - for (int ordinal : deletedOrdinals) { - writer.writeInt(ordinal); - } - } - - public void writeNodeOrdinalToRowIdMapping(SequentialWriter writer, - RamAwareVectorValues vectorValues, - Map> postingsMap) throws IOException - { - long ordToRowOffset = writer.getOnDiskFilePointer(); - - // total number of vectors - writer.writeInt(vectorValues.size()); - - // Write the offsets of the postings for each ordinal - long offsetsStartAt = ordToRowOffset + 4L + 8L * vectorValues.size(); - long nextOffset = offsetsStartAt; - for (int i = 0; i < vectorValues.size(); i++) { - // (ordinal is implied; don't need to write it) - writer.writeLong(nextOffset); - IntArrayList rowIds = postingsMap.get(vectorValues.vectorValue(i)).getRowIds(); - nextOffset += 4 + (rowIds.size() * 4L); // 4 bytes for size and 4 bytes for each integer in the list - } - assert writer.position() == offsetsStartAt : "writer.position()=" + writer.position() + " offsetsStartAt=" + offsetsStartAt; - - // Write postings lists - for (int i = 0; i < vectorValues.size(); i++) { - VectorPostings postings = postingsMap.get(vectorValues.vectorValue(i)); - - IntArrayList rowIds = postings.getRowIds(); - writer.writeInt(rowIds.size()); - for (int r = 0; r < rowIds.size(); r++) - writer.writeInt(rowIds.getInt(r)); - } - assert writer.position() == nextOffset; - } - - public void writeRowIdToNodeOrdinalMapping(SequentialWriter writer, - RamAwareVectorValues vectorValues, - Map> postingsMap) throws IOException - { - List> pairs = new ArrayList<>(); - - // Collect all (rowId, vectorOrdinal) pairs - for (int i = 0; i < vectorValues.size(); i++) { - IntArrayList rowIds = postingsMap.get(vectorValues.vectorValue(i)).getRowIds(); - for (int r = 0; r < rowIds.size(); r++) - pairs.add(Pair.create(rowIds.getInt(r), i)); - } - - // Sort the pairs by rowId - pairs.sort(Comparator.comparingInt(Pair::left)); - - // Write the pairs to the file - long startOffset = writer.position(); - for (Pair pair : pairs) { - writer.writeInt(pair.left); - writer.writeInt(pair.right); - } - - // write the position of the beginning of rowid -> ordinals mappings to the end - writer.writeLong(startOffset); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/DiskBinarySearch.java b/src/java/org/apache/cassandra/index/sai/disk/v2/DiskBinarySearch.java new file mode 100644 index 000000000000..4e0080bfead7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/DiskBinarySearch.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + + +public class DiskBinarySearch +{ + /** + * A function that takes a primitive long and returns a primitive int. + */ + @FunctionalInterface + public interface LongIntFunction + { + int apply(long i); + } + + /** + * Search for the target int between positions low and high, using the provided function + * to retrieve the int value at the given ordinal. + * + * Returns the position at which target is found. Raises an exception if it is not found. + * + * This will not call f() after the target is found, so if f is performing disk seeks, + * it will leave the underlying reader at the position right after reading the target. + * + * @return index if target is found; otherwise return -1 if targer is not found + */ + public static long searchInt(long low, long high, int target, LongIntFunction f) + { + return search(low, high, target, false, f); + } + + /** + * Similar to searchInt but returns index of a value greater or equal to the target, -1 if not found. + */ + public static long searchFloor(long low, long high, int target, LongIntFunction f) + { + return search(low, high, target, true, f); + } + + private static long search(long low, long high, int target, boolean floorSearch, LongIntFunction f) + { + assert high < Long.MAX_VALUE >> 2 : "high is too large to avoid potential overflow: " + high; + assert low < high : "low must be less than high: " + low + " >= " + high; + + int value = Integer.MIN_VALUE; + long i = low; + + while (low < high) + { + i = low + (high - low) / 2; + value = f.apply(i); + + if (target == value) + return i; + + if (target > value) + low = i + 1; + else + high = i; + } + return floorSearch && value >= target ? i : -1; + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java b/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java new file mode 100644 index 000000000000..35201b74a1e9 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +class PrimaryKeyWithSource implements PrimaryKey +{ + private final SSTableId sourceSstableId; + private final long sourceRowId; + private PrimaryKey delegatePrimaryKey; + private PrimaryKeyMap primaryKeyMap; + private final PrimaryKey sourceSstableMinKey; + private final PrimaryKey sourceSstableMaxKey; + + PrimaryKeyWithSource(PrimaryKeyMap primaryKeyMap, long sstableRowId, PrimaryKey sourceSstableMinKey, PrimaryKey sourceSstableMaxKey) + { + this.primaryKeyMap = primaryKeyMap; + this.sourceSstableId = primaryKeyMap.getSSTableId(); + this.sourceRowId = sstableRowId; + this.sourceSstableMinKey = sourceSstableMinKey; + this.sourceSstableMaxKey = sourceSstableMaxKey; + } + + private PrimaryKey primaryKey() + { + if (delegatePrimaryKey == null) + { + delegatePrimaryKey = primaryKeyMap.primaryKeyFromRowId(sourceRowId); + primaryKeyMap = null; // Removes the no longer needed reference to the primary key map. + } + + return delegatePrimaryKey; + } + + public long getSourceRowId() + { + return sourceRowId; + } + + public SSTableId getSourceSstableId() + { + return sourceSstableId; + } + + @Override + public PrimaryKey forStaticRow() + { + // We cannot use row awareness if we need a static row. + return primaryKey().forStaticRow(); + } + + @Override + public Token token() + { + return primaryKey().token(); + } + + @Override + public DecoratedKey partitionKey() + { + return primaryKey().partitionKey(); + } + + @Override + public Clustering clustering() + { + return primaryKey().clustering(); + } + + @Override + public PrimaryKey loadDeferred() + { + primaryKey().loadDeferred(); + return this; + } + + @Override + public ByteSource asComparableBytes(ByteComparable.Version version) + { + return primaryKey().asComparableBytes(version); + } + + @Override + public ByteSource asComparableBytesMinPrefix(ByteComparable.Version version) + { + return primaryKey().asComparableBytesMinPrefix(version); + } + + @Override + public ByteSource asComparableBytesMaxPrefix(ByteComparable.Version version) + { + return primaryKey().asComparableBytesMaxPrefix(version); + } + + @Override + public int compareTo(PrimaryKey o) + { + if (o instanceof PrimaryKeyWithSource) + { + PrimaryKeyWithSource other = (PrimaryKeyWithSource) o; + if (sourceSstableId.equals(other.sourceSstableId)) + return Long.compare(sourceRowId, other.sourceRowId); + // Compare to the other source sstable's min and max keys to determine if the keys are comparable. + // Note that these are already loaded into memory as part of the segment's metadata, so the comparison + // is cheaper than loading the actual keys. + if (sourceSstableMinKey.compareTo(other.sourceSstableMaxKey) > 0) + return 1; + if (sourceSstableMaxKey.compareTo(other.sourceSstableMinKey) < 0) + return -1; + } + else + { + if (sourceSstableMinKey.compareTo(o) > 0) + return 1; + if (sourceSstableMaxKey.compareTo(o) < 0) + return -1; + } + + return primaryKey().compareTo(o); + } + + @Override + public boolean equals(Object o) + { + if (o instanceof PrimaryKeyWithSource) + { + var other = (PrimaryKeyWithSource) o; + // If they are from the same source sstable, we can compare the row ids directly. + if (sourceSstableId.equals(other.sourceSstableId)) + return sourceRowId == other.sourceRowId; + + // If the source sstable primary key ranges do not intersect, the keys cannot be equal. + if (sourceSstableMinKey.compareTo(other.sourceSstableMaxKey) > 0 + || sourceSstableMaxKey.compareTo(other.sourceSstableMinKey) < 0) + return false; + } + + return primaryKey().equals(o); + } + + @Override + public int hashCode() + { + return primaryKey().hashCode(); + } + + @Override + public String toString() + { + return String.format("%s (source sstable: %s, %s)", delegatePrimaryKey, sourceSstableId, sourceRowId); + } + + @Override + public long ramBytesUsed() + { + // Object header + 3 references (primaryKey, sourceSstableId) + long value + return RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + + 2L * RamUsageEstimator.NUM_BYTES_OBJECT_REF + + Long.BYTES + + primaryKey().ramBytesUsed(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyFactory.java b/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyFactory.java new file mode 100644 index 000000000000..f35da6871e92 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyFactory.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * A row-aware {@link PrimaryKey.Factory}. This creates {@link PrimaryKey} instances that are + * sortable by {@link DecoratedKey} and {@link Clustering}. + */ +public class RowAwarePrimaryKeyFactory implements PrimaryKey.Factory +{ + private final ClusteringComparator clusteringComparator; + private final boolean hasClustering; + + + public RowAwarePrimaryKeyFactory(ClusteringComparator clusteringComparator) + { + this.clusteringComparator = clusteringComparator; + this.hasClustering = clusteringComparator.size() > 0; + } + + @Override + public PrimaryKey createDeferred(Token token, Supplier primaryKeySupplier) + { + return new RowAwarePrimaryKey(token, null, null, primaryKeySupplier); + } + + @Override + public PrimaryKey create(DecoratedKey partitionKey, Clustering clustering) + { + return new RowAwarePrimaryKey(partitionKey.getToken(), partitionKey, clustering, null); + } + + PrimaryKey createWithSource(PrimaryKeyMap primaryKeyMap, long sstableRowId, PrimaryKey sourceSstableMinKey, PrimaryKey sourceSstableMaxKey) + { + return new PrimaryKeyWithSource(primaryKeyMap, sstableRowId, sourceSstableMinKey, sourceSstableMaxKey); + } + + private class RowAwarePrimaryKey implements PrimaryKey + { + private Token token; + private DecoratedKey partitionKey; + private Clustering clustering; + private Supplier primaryKeySupplier; + + private RowAwarePrimaryKey(Token token, DecoratedKey partitionKey, Clustering clustering, Supplier primaryKeySupplier) + { + this.token = token; + this.partitionKey = partitionKey; + this.clustering = clustering; + this.primaryKeySupplier = primaryKeySupplier; + } + + @Override + public RowAwarePrimaryKey forStaticRow() + { + return new RowAwarePrimaryKey(token, partitionKey, Clustering.STATIC_CLUSTERING, primaryKeySupplier); + } + + @Override + public Token token() + { + return token; + } + + @Override + public DecoratedKey partitionKey() + { + loadDeferred(); + return partitionKey; + } + + @Override + public Clustering clustering() + { + loadDeferred(); + return clustering; + } + + @Override + public PrimaryKey loadDeferred() + { + if (primaryKeySupplier != null) + { + assert partitionKey == null : "While applying existing primaryKeySupplier to load deferred primaryKey the partition key was unexpectedly already set"; + PrimaryKey deferredPrimaryKey = primaryKeySupplier.get(); + this.partitionKey = deferredPrimaryKey.partitionKey(); + assert this.token.equals(this.partitionKey.getToken()) : "Deferred primary key must contain the same token"; + + // It is possible we have already set clustering to `STATIC_CLUSTERING` because this object + // is the result of a call to `forStaticRow` on some other primary key that was not yet loaded. + // In that case we must not overwrite it. Overwriting it would turn the key for the static row back into + // the key to a regular row. + if (this.clustering == null) + this.clustering = deferredPrimaryKey.clustering(); + + primaryKeySupplier = null; + } + return this; + } + + @Override + public ByteSource asComparableBytes(ByteComparable.Version version) + { + return asComparableBytes(version == ByteComparable.Version.LEGACY ? ByteSource.END_OF_STREAM : ByteSource.TERMINATOR, version, false); + } + + @Override + public ByteSource asComparableBytesMinPrefix(ByteComparable.Version version) + { + return asComparableBytes(ByteSource.LT_NEXT_COMPONENT, version, true); + } + + @Override + public ByteSource asComparableBytesMaxPrefix(ByteComparable.Version version) + { + return asComparableBytes(ByteSource.GT_NEXT_COMPONENT, version, true); + } + + private ByteSource asComparableBytes(int terminator, ByteComparable.Version version, boolean isPrefix) + { + // We need to make sure that the key is loaded before returning a + // byte comparable representation. If we don't we won't get a correct + // comparison because we potentially won't be using the partition key + // and clustering for the lookup + loadDeferred(); + + ByteSource tokenComparable = token.asComparableBytes(version); + ByteSource keyComparable = ByteSource.of(partitionKey.getKey(), version); + + // It is important that the ClusteringComparator.asBytesComparable method is used + // to maintain the correct clustering sort order + ByteSource clusteringComparable = clusteringComparator.size() == 0 || + clustering == null || + clustering.isEmpty() ? null + : clusteringComparator.asByteComparable(clustering) + .asComparableBytes(version); + + // prefix doesn't include null components + if (isPrefix && clusteringComparable == null) + return ByteSource.withTerminator(terminator, tokenComparable, keyComparable); + else + return ByteSource.withTerminator(terminator, tokenComparable, keyComparable, clusteringComparable); + } + + @Override + public int compareTo(PrimaryKey o) + { + int cmp = token().compareTo(o.token()); + + // If the tokens don't match then we don't need to compare any more of the key. + // Otherwise if either this key or given key are token only, + // then we can only compare tokens + if ((cmp != 0) || isTokenOnly() || o.isTokenOnly()) + return cmp; + + // Next compare the partition keys. If they are not equal or + // this is a single row partition key or there are no + // clusterings then we can return the result of this without + // needing to compare the clusterings + cmp = partitionKey().compareTo(o.partitionKey()); + if (cmp != 0 || !hasClustering() || !o.hasClustering()) + return cmp; + return clusteringComparator.compare(clustering(), o.clustering()); + } + + @Override + public int hashCode() + { + if (hasClustering) + return Objects.hash(token, clustering()); + else + return Objects.hash(token); + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof PrimaryKey) + return compareTo((PrimaryKey)obj) == 0; + return false; + } + + @Override + public String toString() + { + return String.format("RowAwarePrimaryKey: { token: %s, partition: %s, clustering: %s:%s} ", + token, + partitionKey, + clustering == null ? null : clustering.kind(), + clustering == null ? null : Arrays.stream(clustering.getBufferArray()) + .map(ByteBufferUtil::bytesToHex) + .collect(Collectors.joining(","))); + } + + @Override + public long ramBytesUsed() + { + // Object header + 4 references (token, partitionKey, clustering, primaryKeySupplier) + implicit outer reference + long size = RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + + 5L * RamUsageEstimator.NUM_BYTES_OBJECT_REF; + + if (token != null) + size += token.getHeapSize(); + if (partitionKey != null) + size += RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + + 2L * RamUsageEstimator.NUM_BYTES_OBJECT_REF + // token and key references + 2L * Long.BYTES; + // We don't count clustering size here as it's managed elsewhere + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java new file mode 100644 index 000000000000..c708545f75b8 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import org.apache.cassandra.db.BufferDecoratedKey; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.v1.LongArray; +import org.apache.cassandra.index.sai.disk.v1.MetadataSource; +import org.apache.cassandra.index.sai.disk.v1.bitpack.BlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsMeta; +import org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsReader; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +/** + * A row-aware {@link PrimaryKeyMap} + * + * This uses the following on-disk structures: + *

      + *
    • Block-packed structure for rowId to token lookups using {@link BlockPackedReader}. + * Uses component {@link IndexComponentType#TOKEN_VALUES}
    • + *
    • A sorted-terms structure for rowId to {@link PrimaryKey} and {@link PrimaryKey} to rowId lookups using + * {@link SortedTermsReader}. Uses components {@link IndexComponentType#PRIMARY_KEY_TRIE}, {@link IndexComponentType#PRIMARY_KEY_BLOCKS}, + * {@link IndexComponentType#PRIMARY_KEY_BLOCK_OFFSETS}
    • + *
    + * + * While the {@link RowAwarePrimaryKeyMapFactory} is threadsafe, individual instances of the {@link RowAwarePrimaryKeyMap} + * are not. + */ +@NotThreadSafe +public class RowAwarePrimaryKeyMap implements PrimaryKeyMap +{ + @ThreadSafe + public static class RowAwarePrimaryKeyMapFactory implements Factory + { + private final IndexComponents.ForRead perSSTableComponents; + private final LongArray.Factory tokenReaderFactory; + private final SortedTermsReader sortedTermsReader; + private final long count; + private final FileHandle token; + private final FileHandle termsDataBlockOffsets; + private final FileHandle termsData; + private final FileHandle termsTrie; + private final IPartitioner partitioner; + private final ClusteringComparator clusteringComparator; + private final RowAwarePrimaryKeyFactory primaryKeyFactory; + private final SSTableId sstableId; + private final boolean hasStaticColumns; + + public RowAwarePrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, RowAwarePrimaryKeyFactory primaryKeyFactory, SSTableReader sstable) + { + FileHandle token = null; + FileHandle termsDataBlockOffsets = null; + FileHandle termsData = null; + FileHandle termsTrie = null; + + try + { + MetadataSource metadataSource = MetadataSource.loadMetadata(perSSTableComponents); + NumericValuesMeta tokensMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.TOKEN_VALUES))); + this.count = tokensMeta.valueCount; + + SortedTermsMeta sortedTermsMeta = new SortedTermsMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_BLOCKS))); + NumericValuesMeta blockOffsetsMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_BLOCK_OFFSETS))); + + token = perSSTableComponents.get(IndexComponentType.TOKEN_VALUES).createFileHandle(); + this.tokenReaderFactory = new BlockPackedReader(token, tokensMeta); + + termsDataBlockOffsets = perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_BLOCK_OFFSETS).createFileHandle(); + termsData = perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_BLOCKS).createFileHandle(); + termsTrie = perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_TRIE).createFileHandle(); + this.sortedTermsReader = new SortedTermsReader(termsData, termsDataBlockOffsets, termsTrie, sortedTermsMeta, blockOffsetsMeta); + } + catch (Throwable t) + { + throw Throwables.unchecked(Throwables.close(t, token, termsData, termsDataBlockOffsets, termsTrie)); + } + this.perSSTableComponents = perSSTableComponents; + this.token = token; + this.termsDataBlockOffsets = termsDataBlockOffsets; + this.termsData = termsData; + this.termsTrie = termsTrie; + this.partitioner = sstable.metadata().partitioner; + this.primaryKeyFactory = primaryKeyFactory; + this.clusteringComparator = sstable.metadata().comparator; + this.sstableId = sstable.getId(); + this.hasStaticColumns = sstable.metadata().hasStaticColumns(); + } + + @Override + public PrimaryKeyMap newPerSSTablePrimaryKeyMap() + { + final LongArray rowIdToToken = new LongArray.DeferredLongArray(() -> tokenReaderFactory.open()); + try + { + return new RowAwarePrimaryKeyMap(rowIdToToken, + sortedTermsReader, + sortedTermsReader.openCursor(), + partitioner, + primaryKeyFactory, + clusteringComparator, + sstableId, + hasStaticColumns); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + public long count() + { + return count; + } + + @Override + public void close() throws IOException + { + FileUtils.closeQuietly(token, termsData, termsDataBlockOffsets, termsTrie); + } + } + + private final LongArray rowIdToToken; + private final SortedTermsReader sortedTermsReader; + private final SortedTermsReader.Cursor cursor; + private final IPartitioner partitioner; + private final RowAwarePrimaryKeyFactory primaryKeyFactory; + private final ClusteringComparator clusteringComparator; + private final SSTableId sstableId; + private final boolean hasStaticColumns; + + private RowAwarePrimaryKeyMap(LongArray rowIdToToken, + SortedTermsReader sortedTermsReader, + SortedTermsReader.Cursor cursor, + IPartitioner partitioner, + RowAwarePrimaryKeyFactory primaryKeyFactory, + ClusteringComparator clusteringComparator, + SSTableId sstableId, + boolean hasStaticColumns) + { + this.rowIdToToken = rowIdToToken; + this.sortedTermsReader = sortedTermsReader; + this.cursor = cursor; + this.partitioner = partitioner; + this.primaryKeyFactory = primaryKeyFactory; + this.clusteringComparator = clusteringComparator; + this.sstableId = sstableId; + this.hasStaticColumns = hasStaticColumns; + } + + @Override + public SSTableId getSSTableId() + { + return sstableId; + } + + public long count() + { + return rowIdToToken.length(); + } + + @Override + public PrimaryKey primaryKeyFromRowId(long sstableRowId) + { + long token = rowIdToToken.get(sstableRowId); + return primaryKeyFactory.createDeferred(partitioner.getTokenFactory().fromLongValue(token), () -> supplier(sstableRowId)); + } + + @Override + public PrimaryKey primaryKeyFromRowId(long sstableRowId, PrimaryKey lowerBound, PrimaryKey upperBound) + { + return hasStaticColumns ? primaryKeyFromRowId(sstableRowId) + : primaryKeyFactory.createWithSource(this, sstableRowId, lowerBound, upperBound); + } + + private long skinnyExactRowIdOrInvertedCeiling(PrimaryKey key) + { + // Fast path when there is no clustering, i.e., there is one row per partition. + // (The reason we don't just make the Factory return a PartitionAware map for this case + // is that it reads partition keys directly from the sstable using the offsets file. + // While this worked in BDP, it was not efficient and caused problems because the + // sstable reader was using 64k page sizes, and this caused page cache thrashing. + long rowId = rowIdToToken.indexOf(key.token().getLongValue()); + if (rowId < 0) + // No match found, return the inverted ceiling + return rowId; + // The first index might not have been the correct match in the case of token collisions. + return tokenCollisionDetection(key, rowId); + } + + /** + * Returns a row Id for a {@link PrimaryKey}. If there is no such term, returns the `-(next row id) - 1` where + * `next row id` is the row id of the next greatest {@link PrimaryKey} in the map. + * @param key the {@link PrimaryKey} to lookup + * @return a row id + */ + @Override + public long exactRowIdOrInvertedCeiling(PrimaryKey key) + { + if (key instanceof PrimaryKeyWithSource) + { + var pkws = (PrimaryKeyWithSource) key; + if (pkws.getSourceSstableId().equals(sstableId)) + return pkws.getSourceRowId(); + } + + if (clusteringComparator.size() == 0) + return skinnyExactRowIdOrInvertedCeiling(key); + + long pointId = cursor.getExactPointId(v -> key.asComparableBytes(v)); + if (pointId >= 0) + return pointId; + long ceiling = cursor.ceiling(v -> key.asComparableBytesMinPrefix(v)); + // Use min value since -(Long.MIN_VALUE) - 1 == Long.MAX_VALUE. + return ceiling < 0 ? Long.MIN_VALUE : -ceiling - 1; + } + + @Override + public long ceiling(PrimaryKey key) + { + if (key instanceof PrimaryKeyWithSource) + { + var pkws = (PrimaryKeyWithSource) key; + if (pkws.getSourceSstableId().equals(sstableId)) + return pkws.getSourceRowId(); + } + + if (clusteringComparator.size() == 0) + { + long rowId = skinnyExactRowIdOrInvertedCeiling(key); + if (rowId >= 0) + return rowId; + else + if (rowId == Long.MIN_VALUE) + return -1; + else + return -rowId - 1; + } + + return cursor.ceiling(key::asComparableBytesMinPrefix); + } + + @Override + public long floor(PrimaryKey key) + { + return cursor.floor(key::asComparableBytesMaxPrefix); + } + + + @Override + public void close() throws IOException + { + FileUtils.closeQuietly(cursor, rowIdToToken); + } + + private PrimaryKey supplier(long sstableRowId) + { + try + { + cursor.seekToPointId(sstableRowId); + ByteSource.Peekable peekable = ByteSource.peekable(cursor.term().asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + + Token token = partitioner.getTokenFactory().fromComparableBytes(ByteSourceInverse.nextComponentSource(peekable), + TypeUtil.BYTE_COMPARABLE_VERSION); + byte[] keyBytes = ByteSourceInverse.getUnescapedBytes(ByteSourceInverse.nextComponentSource(peekable)); + + if (keyBytes == null) + return primaryKeyFactory.createTokenOnly(token); + + DecoratedKey partitionKey = new BufferDecoratedKey(token, ByteBuffer.wrap(keyBytes)); + + Clustering clustering = clusteringComparator.size() == 0 + ? Clustering.EMPTY + : clusteringComparator.clusteringFromByteComparable(ByteBufferAccessor.instance, + v -> ByteSourceInverse.nextComponentSource(peekable), + TypeUtil.BYTE_COMPARABLE_VERSION); + + return primaryKeyFactory.create(partitionKey, clustering); + } + catch (IOException e) + { + throw Throwables.cleaned(e); + } + } + + // Look for token collision by if the ajacent token in the token array matches the + // current token. If we find a collision we need to compare the partition key instead. + protected long tokenCollisionDetection(PrimaryKey primaryKey, long rowId) + { + // Look for collisions while we haven't reached the end of the tokens and the tokens don't collide + while (rowId + 1 < rowIdToToken.length() && primaryKey.token().getLongValue() == rowIdToToken.get(rowId + 1)) + { + // If we had a collision then see if the partition key for this row is >= to the lookup partition key + if (primaryKeyFromRowId(rowId).compareTo(primaryKey) >= 0) + return rowId; + + rowId++; + } + // Note: We would normally expect to get here without going into the while loop + return rowId; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/SSTableComponentsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v2/SSTableComponentsWriter.java new file mode 100644 index 000000000000..f4bfe3c34e0e --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/SSTableComponentsWriter.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.io.IOException; + +import com.google.common.base.Stopwatch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; +import org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsWriter; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.lucene.util.IOUtils; + +public class SSTableComponentsWriter implements PerSSTableWriter +{ + protected static final Logger logger = LoggerFactory.getLogger(SSTableComponentsWriter.class); + + private final IndexComponents.ForWrite perSSTableComponents; + private final MetadataWriter metadataWriter; + private final NumericValuesWriter tokenWriter; + private final NumericValuesWriter blockFPWriter; + private final SortedTermsWriter sortedTermsWriter; + + public SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException + { + this.perSSTableComponents = perSSTableComponents; + this.metadataWriter = new MetadataWriter(perSSTableComponents); + this.tokenWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.TOKEN_VALUES), + metadataWriter, false); + + this.blockFPWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.PRIMARY_KEY_BLOCK_OFFSETS), + metadataWriter, true); + this.sortedTermsWriter = new SortedTermsWriter(perSSTableComponents.addOrGet(IndexComponentType.PRIMARY_KEY_BLOCKS), + metadataWriter, + blockFPWriter, + perSSTableComponents.addOrGet(IndexComponentType.PRIMARY_KEY_TRIE)); + } + + @Override + public void nextRow(PrimaryKey primaryKey) throws IOException + { + tokenWriter.add(primaryKey.token().getLongValue()); + sortedTermsWriter.add(v -> primaryKey.asComparableBytes(v)); + } + + @Override + public void complete(Stopwatch stopwatch) throws IOException + { + IOUtils.close(tokenWriter, sortedTermsWriter, metadataWriter); + perSSTableComponents.markComplete(); + } + + @Override + public void abort(Throwable accumulator) + { + logger.debug(perSSTableComponents.logMessage("Aborting per-SSTable index component writer for {}..."), perSSTableComponents.descriptor()); + perSSTableComponents.forceDeleteAllComponents(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java b/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java new file mode 100644 index 000000000000..aaaccab34b38 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java @@ -0,0 +1,132 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v2; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +public final class TokenOnlyPrimaryKey implements PrimaryKey +{ + private final Token token; + + public TokenOnlyPrimaryKey(Token token) + { + this.token = token; + } + + @Override + public boolean isTokenOnly() + { + return true; + } + + @Override + public Token token() + { + return token; + } + + @Override + public DecoratedKey partitionKey() + { + return null; + } + + @Override + public Clustering clustering() + { + return null; + } + + @Override + public ByteSource asComparableBytes(Version version) + { + return asComparableBytes(version == ByteComparable.Version.LEGACY ? ByteSource.END_OF_STREAM : ByteSource.TERMINATOR, version, false); + } + + @Override + public ByteSource asComparableBytesMinPrefix(Version version) + { + return asComparableBytes(ByteSource.LT_NEXT_COMPONENT, version, true); + } + + @Override + public ByteSource asComparableBytesMaxPrefix(Version version) + { + return asComparableBytes(ByteSource.GT_NEXT_COMPONENT, version, true); + } + + private ByteSource asComparableBytes(int terminator, ByteComparable.Version version, boolean isPrefix) + { + ByteSource tokenComparable = token.asComparableBytes(version); + // prefix doesn't include null components + if (isPrefix) + return ByteSource.withTerminator(terminator, tokenComparable); + else + return ByteSource.withTerminator(terminator, tokenComparable, null, null); + } + + @Override + public int compareTo(PrimaryKey o) + { + return token().compareTo(o.token()); + } + + @Override + public long ramBytesUsed() + { + // Object header + 1 reference (token) + implicit outer reference + token size + return RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + RamUsageEstimator.NUM_BYTES_OBJECT_REF + token.getHeapSize(); + } + + @Override + public PrimaryKey forStaticRow() + { + return this; + } + + @Override + public PrimaryKey loadDeferred() + { + return this; + } + + @Override + public boolean equals(Object o) + { + if (o instanceof PrimaryKey) + return compareTo((PrimaryKey) o) == 0; + return false; + } + + @Override + public int hashCode() + { + return token().hashCode(); + } + + @Override + public String toString() + { + return String.format("TokenOnlyPrimaryKey: { token: %s }", token()); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2InvertedIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2InvertedIndexSearcher.java new file mode 100644 index 000000000000..c9e471d2116c --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2InvertedIndexSearcher.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.InvertedIndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; + +/** + * The key override for this class is the use of {@link Version#BA}. + */ +class V2InvertedIndexSearcher extends InvertedIndexSearcher +{ + V2InvertedIndexSearcher(SSTableContext sstableContext, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) throws IOException + { + // We filter because the CA format wrote maps acording to a different order than their abstract type. + super(sstableContext, perIndexFiles, segmentMetadata, indexContext, Version.BA, true); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java new file mode 100644 index 000000000000..ae6baaf9e5d5 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v1.V1OnDiskFormat; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +/** + * Updates SAI OnDiskFormat to include full PK -> offset mapping, and adds vector components. + */ +public class V2OnDiskFormat extends V1OnDiskFormat +{ + private static final Set PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponentType.GROUP_COMPLETION_MARKER, + IndexComponentType.GROUP_META, + IndexComponentType.TOKEN_VALUES, + IndexComponentType.PRIMARY_KEY_TRIE, + IndexComponentType.PRIMARY_KEY_BLOCKS, + IndexComponentType.PRIMARY_KEY_BLOCK_OFFSETS); + + public static final Set VECTOR_COMPONENTS_V2 = EnumSet.of(IndexComponentType.COLUMN_COMPLETION_MARKER, + IndexComponentType.META, + IndexComponentType.VECTOR, + IndexComponentType.TERMS_DATA, + IndexComponentType.POSTING_LISTS); + + public static final V2OnDiskFormat instance = new V2OnDiskFormat(); + + private static final IndexFeatureSet v2IndexFeatureSet = new IndexFeatureSet() + { + @Override + public boolean isRowAware() + { + return true; + } + + @Override + public boolean hasTermsHistogram() + { + return false; + } + }; + + protected V2OnDiskFormat() + {} + + @Override + public IndexFeatureSet indexFeatureSet() + { + return v2IndexFeatureSet; + } + + @Override + public PrimaryKey.Factory newPrimaryKeyFactory(ClusteringComparator comparator) + { + return new RowAwarePrimaryKeyFactory(comparator); + } + + @Override + public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, PrimaryKey.Factory primaryKeyFactory, SSTableReader sstable) + { + assert primaryKeyFactory instanceof RowAwarePrimaryKeyFactory; + return new RowAwarePrimaryKeyMap.RowAwarePrimaryKeyMapFactory(perSSTableComponents, (RowAwarePrimaryKeyFactory) primaryKeyFactory, sstable); + } + + @Override + public IndexSearcher newIndexSearcher(SSTableContext sstableContext, + IndexContext indexContext, + PerIndexFiles indexFiles, + SegmentMetadata segmentMetadata) throws IOException + { + if (indexContext.isVector()) + throw new IllegalStateException("V2 (HNSW) vector index support has been removed"); + if (indexContext.isLiteral()) + return new V2InvertedIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext); + return super.newIndexSearcher(sstableContext, indexContext, indexFiles, segmentMetadata); + } + + @Override + public PerSSTableWriter newPerSSTableWriter(IndexDescriptor indexDescriptor) throws IOException + { + return new SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); + } + + @Override + public Set perIndexComponentTypes(AbstractType validator) + { + if (validator.isVector()) + return VECTOR_COMPONENTS_V2; + return super.perIndexComponentTypes(validator); + } + + @Override + public Set perSSTableComponentTypes() + { + return PER_SSTABLE_COMPONENTS; + } + + @Override + public int openFilesPerSSTable() + { + return 4; + } + + @Override + public ByteOrder byteOrderFor(IndexComponentType indexComponentType, IndexContext context) + { + // The little-endian files are written by Lucene, and the upgrade to Lucene 9 switched the byte order from big to little. + switch (indexComponentType) + { + case META: + case GROUP_META: + case TOKEN_VALUES: + case PRIMARY_KEY_BLOCK_OFFSETS: + case KD_TREE: + case KD_TREE_POSTING_LISTS: + return ByteOrder.LITTLE_ENDIAN; + case POSTING_LISTS: + return (context != null && context.isVector()) ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; + default: + return ByteOrder.BIG_ENDIAN; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskOrdinalsMap.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskOrdinalsMap.java new file mode 100644 index 000000000000..8fbe45fd6ab5 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskOrdinalsMap.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.PrimitiveIterator; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.util.Bits; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter.Structure; +import org.apache.cassandra.index.sai.disk.vector.BitsUtil; +import org.apache.cassandra.index.sai.disk.vector.OnDiskOrdinalsMap; +import org.apache.cassandra.index.sai.disk.vector.OrdinalsView; +import org.apache.cassandra.index.sai.disk.vector.RowIdsView; +import org.apache.cassandra.index.sai.utils.SingletonIntIterator; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.RandomAccessReader; + +public class V2OnDiskOrdinalsMap implements OnDiskOrdinalsMap +{ + private static final Logger logger = LoggerFactory.getLogger(V2OnDiskOrdinalsMap.class); + + private final OrdinalsView fastOrdinalsView; + private static final OneToOneRowIdsView ONE_TO_ONE_ROW_IDS_VIEW = new OneToOneRowIdsView(); + private final FileHandle fh; + private final long ordToRowOffset; + private final long segmentEnd; + private final int size; + // the offset where we switch from recording ordinal -> rows, to row -> ordinal + private final long rowOrdinalOffset; + private final Set deletedOrdinals; + + private final boolean canFastMapOrdinalsView; + private final boolean canFastMapRowIdsView; + + public V2OnDiskOrdinalsMap(FileHandle fh, long segmentOffset, long segmentLength) + { + deletedOrdinals = new HashSet<>(); + + this.segmentEnd = segmentOffset + segmentLength; + this.fh = fh; + try (var reader = fh.createReader()) + { + reader.seek(segmentOffset); + int deletedCount = reader.readInt(); + for (var i = 0; i < deletedCount; i++) + { + int ordinal = reader.readInt(); + deletedOrdinals.add(ordinal); + } + + this.ordToRowOffset = reader.getFilePointer(); + this.size = reader.readInt(); + reader.seek(segmentEnd - 8); + this.rowOrdinalOffset = reader.readLong(); + + // When rowOrdinalOffset + 8 is equal to segmentEnd, the segment has no postings. Therefore, + // we use the EmptyView. That case does not get a fastRowIdsView because we only hit that code after + // getting ordinals from the graph, and an EmptyView will not produce any ordinals to search. Importantly, + // the file format for the RowIdsView is correct, even if there are no postings. + this.canFastMapRowIdsView = deletedCount == -1; + this.canFastMapOrdinalsView = deletedCount == -1 || rowOrdinalOffset + 8 == segmentEnd; + this.fastOrdinalsView = deletedCount == -1 ? new OneToOneOrdinalsView(size) : new EmptyOrdinalsView(); + assert rowOrdinalOffset < segmentEnd : "rowOrdinalOffset " + rowOrdinalOffset + " is not less than segmentEnd " + segmentEnd; + } + catch (Exception e) + { + throw new RuntimeException("Error initializing OnDiskOrdinalsMap at segment " + segmentOffset, e); + } + } + + @Override + public Structure getStructure() + { + return canFastMapOrdinalsView ? Structure.ONE_TO_ONE : Structure.ZERO_OR_ONE_TO_MANY; + } + + @Override + public long cachedBytesUsed() + { + return 0; + } + + @Override + public RowIdsView getRowIdsView() + { + if (canFastMapRowIdsView) { + return ONE_TO_ONE_ROW_IDS_VIEW; + } + + return new FileReadingRowIdsView(); + } + + @Override + public Bits ignoringDeleted(Bits acceptBits) + { + return BitsUtil.bitsIgnoringDeleted(acceptBits, deletedOrdinals); + } + + private class FileReadingRowIdsView implements RowIdsView + { + RandomAccessReader reader = fh.createReader(); + + @Override + public PrimitiveIterator.OfInt getSegmentRowIdsMatching(int vectorOrdinal) throws IOException + { + Preconditions.checkArgument(vectorOrdinal < size, "vectorOrdinal %s is out of bounds %s", vectorOrdinal, size); + + // read index entry + try + { + reader.seek(ordToRowOffset + 4L + vectorOrdinal * 8L); + } + catch (Exception e) + { + throw new RuntimeException(String.format("Error seeking to index offset for ordinal %d with ordToRowOffset %d", + vectorOrdinal, ordToRowOffset), e); + } + var offset = reader.readLong(); + // seek to and read rowIds + try + { + reader.seek(offset); + } + catch (Exception e) + { + throw new RuntimeException(String.format("Error seeking to rowIds offset for ordinal %d with ordToRowOffset %d", + vectorOrdinal, ordToRowOffset), e); + } + var postingsSize = reader.readInt(); + + // Optimize for the most common case + if (postingsSize == 1) + return new SingletonIntIterator(reader.readInt()); + + var rowIds = new int[postingsSize]; + for (var i = 0; i < rowIds.length; i++) + { + rowIds[i] = reader.readInt(); + } + return Arrays.stream(rowIds).iterator(); + } + + @Override + public void close() + { + reader.close(); + } + } + + @Override + public OrdinalsView getOrdinalsView() + { + if (canFastMapOrdinalsView) { + return fastOrdinalsView; + } + + return new FileReadingOrdinalsView(); + } + + /** + * not thread safe + */ + private class FileReadingOrdinalsView implements OrdinalsView + { + RandomAccessReader reader = fh.createReader(); + private final long high = (segmentEnd - 8 - rowOrdinalOffset) / 8; + private int lastFoundRowId = -1; + private long lastFoundRowIdIndex = -1; + + private int lastRowId = -1; + + /** + * @return order if given row id is found; otherwise return -1 + * rowId must increase + */ + @Override + public int getOrdinalForRowId(int rowId) throws IOException + { + if (rowId <= lastRowId) + throw new IllegalArgumentException("rowId " + rowId + " is less than or equal to lastRowId " + lastRowId); + lastRowId = rowId; + + if (rowId < lastFoundRowId) // skipped row, no need to search + return -1; + + long low = 0; + if (lastFoundRowId > -1 && lastFoundRowIdIndex < high) + { + low = lastFoundRowIdIndex; + + if (lastFoundRowId == rowId) // "lastFoundRowId + 1 == rowId" case that returned -1 likely moved use here + { + long offset = rowOrdinalOffset + lastFoundRowIdIndex * 8; + reader.seek(offset); + int foundRowId = reader.readInt(); + assert foundRowId == rowId : "expected rowId " + rowId + " but found " + foundRowId; + return reader.readInt(); + } + else if (lastFoundRowId + 1 == rowId) // sequential read, skip binary search + { + long offset = rowOrdinalOffset + (lastFoundRowIdIndex + 1) * 8; + reader.seek(offset); + int foundRowId = reader.readInt(); + lastFoundRowId = foundRowId; + lastFoundRowIdIndex++; + if (foundRowId == rowId) + return reader.readInt(); + else + return -1; + } + } + final AtomicLong lastRowIdIndex = new AtomicLong(-1L); + // Compute the offset of the start of the rowId to vectorOrdinal mapping + long index = DiskBinarySearch.searchInt(low, high, rowId, i -> { + try + { + lastRowIdIndex.set(i); + long offset = rowOrdinalOffset + i * 8; + reader.seek(offset); + return reader.readInt(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + + // not found + if (index < 0) + return -1; + + lastFoundRowId = rowId; + lastFoundRowIdIndex = lastRowIdIndex.get(); + return reader.readInt(); + } + + @Override + public void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException + { + long start = DiskBinarySearch.searchFloor(0, high, startRowId, i -> { + try + { + long offset = rowOrdinalOffset + i * 8; + reader.seek(offset); + return reader.readInt(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + + if (start < 0 || start >= high) + return; + + reader.seek(rowOrdinalOffset + start * 8); + // sequential read without seeks should be fast, we expect OS to prefetch data from the disk + // binary search for starting offset of min rowid >= startRowId unlikely to be faster + for (long idx = start; idx < high; idx ++) + { + int rowId = reader.readInt(); + if (rowId > endRowId) + break; + + int ordinal = reader.readInt(); + if (rowId >= startRowId) + consumer.accept(rowId, ordinal); + } + } + + @Override + public void close() + { + reader.close(); + } + } + + @Override + public void close() + { + fh.close(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java new file mode 100644 index 000000000000..4910c470bf5a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java @@ -0,0 +1,649 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v2; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.graph.NodeQueue; +import io.github.jbellis.jvector.quantization.CompressedVectors; +import io.github.jbellis.jvector.quantization.ProductQuantization; +import io.github.jbellis.jvector.util.BitSet; +import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.util.BoundedLongHeap; +import io.github.jbellis.jvector.util.SparseBits; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v1.postings.ReorderingPostingList; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.vector.BruteForceRowIdIterator; +import org.apache.cassandra.index.sai.disk.vector.CassandraDiskAnn; +import org.apache.cassandra.index.sai.disk.vector.CloseableReranker; +import org.apache.cassandra.index.sai.disk.vector.NodeQueueRowIdIterator; +import org.apache.cassandra.index.sai.disk.vector.VectorCompression; +import org.apache.cassandra.index.sai.disk.vector.VectorMemtableIndex; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.plan.Plan.CostCoefficients; +import org.apache.cassandra.index.sai.utils.SegmentRowIdOrdinalPairs; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RangeUtil; +import org.apache.cassandra.index.sai.utils.RowIdWithMeta; +import org.apache.cassandra.index.sai.utils.RowIdWithScore; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.metrics.LinearFit; +import org.apache.cassandra.metrics.PairedSlidingWindowReservoir; +import org.apache.cassandra.metrics.QuickSlidingWindowReservoir; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.CloseableIterator; + +import static java.lang.Math.ceil; +import static java.lang.Math.min; +import static org.apache.cassandra.index.sai.plan.Plan.hrs; + +/** + * Executes ann search against the graph for an individual index segment. + */ +public class V2VectorIndexSearcher extends IndexSearcher +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + /** + * Only allow brute force if fewer than this many rows are involved. + * Not final so test can inject its own setting. + */ + @VisibleForTesting + public static int GLOBAL_BRUTE_FORCE_ROWS = Integer.MAX_VALUE; + /** + * How much more expensive is brute forcing the comparisons than going through the index? + * (brute force needs to go through the full read path to pull out the vectors from the row) + */ + @VisibleForTesting + public static double BRUTE_FORCE_EXPENSE_FACTOR = DatabaseDescriptor.getAnnBruteForceExpenseFactor(); + + @VisibleForTesting + public final CassandraDiskAnn graph; + private final PrimaryKey.Factory keyFactory; + private final PairedSlidingWindowReservoir expectedActualNodesVisited = new PairedSlidingWindowReservoir(20); + private final ThreadLocal cachedBits; + private final ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics; + + protected V2VectorIndexSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext, + CassandraDiskAnn graph) + { + super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, indexContext); + this.graph = graph; + this.keyFactory = PrimaryKey.factory(indexContext.comparator(), indexContext.indexFeatureSet()); + this.cachedBits = ThreadLocal.withInitial(SparseBits::new); + this.columnQueryMetrics = (ColumnQueryMetrics.VectorIndexMetrics) indexContext.getColumnQueryMetrics(); + } + + @Override + public long indexFileCacheSize() + { + return graph.ramBytesUsed(); + } + + public VectorCompression getCompression() + { + return graph.getCompression(); + } + + public ProductQuantization getPQ() + { + return graph.getPQ(); + } + + @Override + public KeyRangeIterator search(Expression exp, AbstractBounds keyRange, QueryContext context, boolean defer) throws IOException + { + PostingList results = searchPosting(context, exp, keyRange); + return toPrimaryKeyIterator(results, context); + } + + private PostingList searchPosting(QueryContext context, Expression exp, AbstractBounds keyRange) throws IOException + { + if (logger.isTraceEnabled()) + logger.trace(indexContext.logMessage("Searching on expression '{}'..."), exp); + + if (exp.getOp() != Expression.Op.BOUNDED_ANN) + throw new IllegalArgumentException(indexContext.logMessage("Unsupported expression during BOUNDED_ANN index query: " + exp)); + + var queryVector = vts.createFloatVector(exp.lower.value.vector); + + // this is a thresholded query, so pass graph.size() as top k to get all results satisfying the threshold + // Threshold queries do not use pruning. + var result = searchInternal(keyRange, context, queryVector, graph.size(), graph.size(), exp.getEuclideanSearchThreshold(), false); + return new ReorderingPostingList(result, RowIdWithMeta::getSegmentRowId); + } + + @Override + public CloseableIterator orderBy(Orderer orderer, Expression slice, AbstractBounds keyRange, QueryContext context, int limit) throws IOException + { + if (logger.isTraceEnabled()) + logger.trace(indexContext.logMessage("Searching on expression '{}'..."), orderer); + + if (!orderer.isANN()) + throw new IllegalArgumentException(indexContext.logMessage("Unsupported expression during ANN index query: " + orderer)); + + int rerankK = orderer.rerankKFor(limit, graph.getCompression()); + var queryVector = orderer.getVectorTerm(); + + var result = searchInternal(keyRange, context, queryVector, limit, rerankK, 0, orderer.usePruning()); + return toMetaSortedIterator(result, context); + } + + /** + * Find the closest `limit` neighbors to the given query vector, using a coarse search pass for `rerankK` + * candidates. May decide to use brute force instead of the index. + * @param keyRange the key range to search + * @param context the query context + * @param queryVector the query vector + * @param limit the limit for the query + * @param rerankK the amplified limit for the query to get more accurate results + * @param threshold the threshold for the query. When the threshold is greater than 0 and brute force logic is used, + * the results will be filtered by the threshold. + * @param usePruning whether to use pruning to speed up the ANN search + */ + private CloseableIterator searchInternal(AbstractBounds keyRange, + QueryContext context, + VectorFloat queryVector, + int limit, + int rerankK, + float threshold, + boolean usePruning) throws IOException + { + try (PrimaryKeyMap primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap()) + { + // not restricted + if (RangeUtil.coversFullRing(keyRange)) + { + var estimate = estimateCost(rerankK, graph.size()); + return graph.search(queryVector, limit, rerankK, threshold, usePruning, Bits.ALL, context, estimate::updateStatistics); + } + + PrimaryKey firstPrimaryKey = keyFactory.createTokenOnly(keyRange.left.getToken()); + + // it will return the next row id if given key is not found. + long minSSTableRowId = primaryKeyMap.ceiling(firstPrimaryKey); + // If we didn't find the first key, we won't find the last primary key either + if (minSSTableRowId < 0) + return CloseableIterator.emptyIterator(); + long maxSSTableRowId = getMaxSSTableRowId(primaryKeyMap, keyRange.right); + + if (minSSTableRowId > maxSSTableRowId) + return CloseableIterator.emptyIterator(); + + // if the range covers the entire segment, skip directly to an index search + if (minSSTableRowId <= metadata.minSSTableRowId && maxSSTableRowId >= metadata.maxSSTableRowId) + return graph.search(queryVector, limit, rerankK, threshold, usePruning, Bits.ALL, context, visited -> {}); + + minSSTableRowId = Math.max(minSSTableRowId, metadata.minSSTableRowId); + maxSSTableRowId = min(maxSSTableRowId, metadata.maxSSTableRowId); + + // Upper-bound cost based on maximum possible rows included + int nRows = Math.toIntExact(maxSSTableRowId - minSSTableRowId + 1); + var initialCostEstimate = estimateCost(rerankK, nRows); + Tracing.logAndTrace(logger, "Search range covers {} rows in index of {} nodes; estimate for LIMIT {} is {}", + nRows, graph.size(), rerankK, initialCostEstimate); + // if the range spans a small number of rows, then generate scores from the sstable rows instead of searching the index + int startSegmentRowId = metadata.toSegmentRowId(minSSTableRowId); + int endSegmentRowId = metadata.toSegmentRowId(maxSSTableRowId); + if (initialCostEstimate.shouldUseBruteForce()) + { + var maxSize = endSegmentRowId - startSegmentRowId + 1; + var segmentOrdinalPairs = new SegmentRowIdOrdinalPairs(maxSize); + try (var ordinalsView = graph.getOrdinalsView()) + { + ordinalsView.forEachOrdinalInRange(startSegmentRowId, endSegmentRowId, segmentOrdinalPairs::add); + } + + // When we have a threshold, we only need to filter the results, not order them, because it means we're + // evaluating a boolean predicate in the SAI pipeline that wants to collate by PK + if (threshold > 0) + return filterByBruteForce(queryVector, segmentOrdinalPairs, threshold); + else + return orderByBruteForce(queryVector, segmentOrdinalPairs, limit, rerankK); + } + + // create a bitset of ordinals corresponding to the rows in the given key range + final Bits bits; + try (var ordinalsView = graph.getOrdinalsView()) + { + bits = ordinalsView.buildOrdinalBits(startSegmentRowId, endSegmentRowId, this::bitSetForSearch); + } + // the set of ordinals may be empty if no rows in the range had a vector associated with them + int cardinality = bits instanceof SparseBits ? ((SparseBits) bits).cardinality() : ((BitSet) bits).cardinality(); + if (cardinality == 0) + return CloseableIterator.emptyIterator(); + // Rows are many-to-one wrt index ordinals, so the actual number of ordinals involved (`cardinality`) + // could be less than the number of rows in the range (`nRows`). In that case we should update the cost + // so that we don't pollute the planner with incorrectly pessimistic estimates. + // + // Technically, we could also have another `shouldUseBruteForce` branch here, but we don't have + // the code to generate rowids from ordinals, and it's a rare enough case that it doesn't seem worth + // the trouble to add it. + var betterCostEstimate = estimateCost(rerankK, cardinality); + + return graph.search(queryVector, limit, rerankK, threshold, usePruning, bits, context, betterCostEstimate::updateStatistics); + } + } + + private CloseableIterator orderByBruteForce(VectorFloat queryVector, SegmentRowIdOrdinalPairs segmentOrdinalPairs, int limit, int rerankK) throws IOException + { + if (segmentOrdinalPairs.size() == 0) + return CloseableIterator.emptyIterator(); + + // We allow for negative rerankK, but for our cost calculations, it only makes sense to use 0 here. + rerankK = Math.max(0, rerankK); + // If we use compressed vectors, we still have to order rerankK results using full resolution similarity + // scores, so only use the compressed vectors when there are enough vectors to make it worthwhile. + double twoPassCost = segmentOrdinalPairs.size() * CostCoefficients.ANN_SIMILARITY_COST + + rerankK * hrs(CostCoefficients.ANN_SCORED_KEY_COST); + double onePassCost = segmentOrdinalPairs.size() * hrs(CostCoefficients.ANN_SCORED_KEY_COST); + if (graph.getCompressedVectors() != null && twoPassCost < onePassCost) + return orderByBruteForce(graph.getCompressedVectors(), queryVector, segmentOrdinalPairs, limit, rerankK); + return orderByBruteForce(queryVector, segmentOrdinalPairs); + } + + /** + * Materialize the compressed vectors for the given segment row ids, put them into a priority queue ordered by + * approximate similarity score, and then pass to the {@link BruteForceRowIdIterator} to lazily resolve the + * full resolution ordering as needed. + */ + private CloseableIterator orderByBruteForce(CompressedVectors cv, + VectorFloat queryVector, + SegmentRowIdOrdinalPairs segmentOrdinalPairs, + int limit, + int rerankK) + { + // Use the jvector NodeQueue to avoid unnecessary object allocations since this part of the code operates on + // many rows. + var approximateScores = new NodeQueue(new BoundedLongHeap(segmentOrdinalPairs.size()), NodeQueue.Order.MAX_HEAP); + var similarityFunction = indexContext.getIndexWriterConfig().getSimilarityFunction(); + var scoreFunction = cv.precomputedScoreFunctionFor(queryVector, similarityFunction); + columnQueryMetrics.onBruteForceNodesVisited(segmentOrdinalPairs.size()); + + if (rerankK <= 0) + { + // Rerankless search, so we go straight to the NodeQueueRowIdIterator. + var iter = segmentOrdinalPairs.mapToSegmentRowIdScoreIterator(scoreFunction); + approximateScores.pushMany(iter, segmentOrdinalPairs.size()); + return new NodeQueueRowIdIterator(approximateScores, true); + } + + // Store the index of the (rowId, ordinal) pair from the segmentOrdinalPairs in the NodeQueue so that we can + // retrieve both values with O(1) lookup when we need to resolve the full resolution score in the + // BruteForceRowIdIterator. + var iter = segmentOrdinalPairs.mapToIndexScoreIterator(scoreFunction); + approximateScores.pushMany(iter, segmentOrdinalPairs.size()); + var reranker = new CloseableReranker(similarityFunction, queryVector, graph.getView()); + return new BruteForceRowIdIterator(approximateScores, segmentOrdinalPairs, reranker, limit, rerankK, graph.usesNVQ(), columnQueryMetrics); + } + + /** + * Produces a correct ranking of the rows in the given segment. Because this graph does not have compressed + * vectors, read all vectors and put them into a priority queue to rank them lazily. It is assumed that the whole + * PQ will often not be needed. + */ + private CloseableIterator orderByBruteForce(VectorFloat queryVector, SegmentRowIdOrdinalPairs segmentOrdinalPairs) throws IOException + { + var scoredRowIds = new NodeQueue(new BoundedLongHeap(segmentOrdinalPairs.size()), NodeQueue.Order.MAX_HEAP); + try (var vectorsView = graph.getView()) + { + var similarityFunction = indexContext.getIndexWriterConfig().getSimilarityFunction(); + var esf = vectorsView.rerankerFor(queryVector, similarityFunction); + // Because the scores are exact, we only store the rowid, score pair. + var iter = segmentOrdinalPairs.mapToSegmentRowIdScoreIterator(esf); + scoredRowIds.pushMany(iter, segmentOrdinalPairs.size()); + columnQueryMetrics.onBruteForceNodesReranked(segmentOrdinalPairs.size()); + return new NodeQueueRowIdIterator(scoredRowIds, graph.usesNVQ()); + } + } + + /** + * Materialize the full resolution vector for each row id, compute the similarity score, filter + * out rows that do not meet the threshold, and then return them in an iterator. + * NOTE: because the threshold is not used for ordering, the result is returned in PK order, not score order. + */ + private CloseableIterator filterByBruteForce(VectorFloat queryVector, + SegmentRowIdOrdinalPairs segmentOrdinalPairs, + float threshold) throws IOException + { + var results = new ArrayList(segmentOrdinalPairs.size()); + try (var vectorsView = graph.getView()) + { + var similarityFunction = indexContext.getIndexWriterConfig().getSimilarityFunction(); + var esf = vectorsView.rerankerFor(queryVector, similarityFunction); + final boolean isRerankerApproximate = graph.usesNVQ(); + segmentOrdinalPairs.forEachSegmentRowIdOrdinalPair((segmentRowId, ordinal) -> { + var score = esf.similarityTo(ordinal); + if (score >= threshold) + results.add(new RowIdWithScore(segmentRowId, score, isRerankerApproximate)); + }); + columnQueryMetrics.onBruteForceNodesReranked(segmentOrdinalPairs.size()); + } + return CloseableIterator.wrap(results.iterator()); + } + + private long getMaxSSTableRowId(PrimaryKeyMap primaryKeyMap, PartitionPosition right) + { + // if the right token is the minimum token, there is no upper bound on the keyRange and + // we can save a lookup by using the maxSSTableRowId + if (right.isMinimum()) + return metadata.maxSSTableRowId; + + PrimaryKey lastPrimaryKey = keyFactory.createTokenOnly(right.getToken()); + long max = primaryKeyMap.floor(lastPrimaryKey); + if (max < 0) + return metadata.maxSSTableRowId; + return max; + } + + public V5VectorPostingsWriter.Structure getPostingsStructure() + { + return graph.getPostingsStructure(); + } + + private class CostEstimate + { + private final int candidates; + private final int rawExpectedNodesVisited; + private final int expectedNodesVisited; + + public CostEstimate(int candidates, int rawExpectedNodesVisited, int expectedNodesVisited) + { + assert rawExpectedNodesVisited >= 0 : rawExpectedNodesVisited; + assert expectedNodesVisited >= 0 : expectedNodesVisited; + + this.candidates = candidates; + this.rawExpectedNodesVisited = rawExpectedNodesVisited; + this.expectedNodesVisited = expectedNodesVisited; + } + + public boolean shouldUseBruteForce() + { + if (candidates > GLOBAL_BRUTE_FORCE_ROWS) + return false; + return bruteForceCost() <= indexScanCost(); + } + + private double indexScanCost() + { + return expectedNodesVisited + * (CostCoefficients.ANN_SIMILARITY_COST + hrs(CostCoefficients.ANN_EDGELIST_COST) / graph.maxDegree()); + } + + private double bruteForceCost() + { + // VSTODO we don't have rerankK available here, so we only calculate the two pass cost + // out of the options in orderByBruteForce. (The rerank cost is roughly equal for both + // indexScanCost and bruteForceCost so we can leave it out of both.) + return candidates * CostCoefficients.ANN_SIMILARITY_COST; + } + + public void updateStatistics(int actualNodesVisited) + { + assert actualNodesVisited >= 0 : actualNodesVisited; + expectedActualNodesVisited.update(rawExpectedNodesVisited, actualNodesVisited); + + if (actualNodesVisited >= 1000 && (actualNodesVisited > 2 * expectedNodesVisited || actualNodesVisited < 0.5 * expectedNodesVisited)) + Tracing.logAndTrace(logger, "Predicted visiting {} nodes ({} raw), but actually visited {}", + expectedNodesVisited, rawExpectedNodesVisited, actualNodesVisited); + } + + @Override + public String toString() + { + return String.format("{brute force(%d) = %.2f, index scan(%d) = %.2f}", + candidates, bruteForceCost(), expectedNodesVisited, indexScanCost()); + } + + public double cost() + { + return min(bruteForceCost(), indexScanCost()); + } + } + + public double estimateAnnSearchCost(int rerankK, int candidates) + { + var estimate = estimateCost(rerankK, candidates); + return estimate.cost(); + } + + private CostEstimate estimateCost(int rerankK, int candidates) + { + int rawExpectedNodes = getRawExpectedNodes(rerankK, candidates); + // update the raw expected value with a linear interpolation based on observed data + var observedValues = expectedActualNodesVisited.getSnapshot().values; + int expectedNodes; + if (observedValues.length >= 10) + { + var interceptSlope = LinearFit.interceptSlopeFor(observedValues); + expectedNodes = (int) (interceptSlope.left + interceptSlope.right * rawExpectedNodes); + } + else + { + expectedNodes = rawExpectedNodes; + } + + int sanitizedEstimate = VectorMemtableIndex.ensureSaneEstimate(expectedNodes, rerankK, graph.size()); + return new CostEstimate(candidates, rawExpectedNodes, sanitizedEstimate); + } + + private SparseBits bitSetForSearch() + { + var bits = cachedBits.get(); + bits.clear(); + return bits; + } + + @Override + public CloseableIterator orderResultsBy(SSTableReader reader, + QueryContext context, + List keys, + Orderer orderer, + int limit) throws IOException + { + if (keys.isEmpty()) + return CloseableIterator.emptyIterator(); + + int rerankK = orderer.rerankKFor(limit, graph.getCompression()); + // Convert PKs to segment row ids and map to ordinals, skipping any that don't exist in this segment + var segmentOrdinalPairs = flatmapPrimaryKeysToBitsAndRows(keys); + var numRows = segmentOrdinalPairs.size(); + final CostEstimate cost = estimateCost(rerankK, numRows); + Tracing.logAndTrace(logger, "{} relevant rows out of {} in range in index of {} nodes; estimate for LIMIT {} is {}", + numRows, keys.size(), graph.size(), limit, cost); + if (numRows == 0) + return CloseableIterator.emptyIterator(); + + if (cost.shouldUseBruteForce()) + { + // brute force using the in-memory compressed vectors to cut down the number of results returned + var queryVector = orderer.getVectorTerm(); + return toMetaSortedIterator(this.orderByBruteForce(queryVector, segmentOrdinalPairs, limit, rerankK), context); + } + // Create bits from the mapping + var bits = bitSetForSearch(); + segmentOrdinalPairs.forEachOrdinal(bits::set); + // else ask the index to perform a search limited to the bits we created + var queryVector = orderer.getVectorTerm(); + var results = graph.search(queryVector, limit, rerankK, 0, orderer.usePruning(), bits, context, cost::updateStatistics); + return toMetaSortedIterator(results, context); + } + + + /** + * Build a mapping of segment row id to ordinal for the given primary keys, skipping any that don't exist in this + * segment. + * @param keysInRange the primary keys to map + * @return a mapping of segment row id to ordinal + * @throws IOException + */ + private SegmentRowIdOrdinalPairs flatmapPrimaryKeysToBitsAndRows(List keysInRange) throws IOException + { + var segmentOrdinalPairs = new SegmentRowIdOrdinalPairs(keysInRange.size()); + int lastSegmentRowId = -1; + try (var primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(); + var ordinalsView = graph.getOrdinalsView()) + { + // track whether we are saving comparisons by using binary search to skip ahead + // (if most of the keys belong to this sstable, bsearch will actually be slower) + var comparisonsSavedByBsearch = new QuickSlidingWindowReservoir(10); + boolean preferSeqScanToBsearch = false; + + for (int i = 0; i < keysInRange.size();) + { + // turn the pk back into a row id, with a fast path for the case where the pk is from this sstable + PrimaryKey primaryKey = keysInRange.get(i); + long sstableRowId = primaryKeyMap.exactRowIdOrInvertedCeiling(primaryKey); + + if (sstableRowId < 0) + { + // The given PK doesn't exist in this sstable, so sstableRowId represents the negation + // of the next-highest. Turn that back into a PK so we can skip ahead in keysInRange. + long ceilingRowId = - sstableRowId - 1; + if (ceilingRowId > metadata.maxSSTableRowId) + { + // The next greatest primary key is greater than all the primary keys in this segment + break; + } + PrimaryKey ceilingPrimaryKey = primaryKeyMap.primaryKeyFromRowId(ceilingRowId); + + boolean ceilingPrimaryKeyMatchesKeyInRange = false; + // adaptively choose either seq scan or bsearch to skip ahead in keysInRange until + // we find one at least as large as the ceiling key + if (preferSeqScanToBsearch) + { + int keysToSkip = 1; // We already know that the PK at index i is not equal to the ceiling PK. + int cmp = 1; // Need to initialize. The value is irrelevant. + for ( ; i + keysToSkip < keysInRange.size(); keysToSkip++) + { + var nextPrimaryKey = keysInRange.get(i + keysToSkip); + cmp = nextPrimaryKey.compareTo(ceilingPrimaryKey); + if (cmp >= 0) + break; + } + comparisonsSavedByBsearch.update(keysToSkip - (int) ceil(logBase2(keysInRange.size() - i))); + i += keysToSkip; + ceilingPrimaryKeyMatchesKeyInRange = cmp == 0; + } + else + { + // Use a sublist to only search the remaining primary keys in range. + List keysRemaining = keysInRange.subList(i, keysInRange.size()); + int nextIndexForCeiling = Collections.binarySearch(keysRemaining, ceilingPrimaryKey); + if (nextIndexForCeiling < 0) + // We got: -(insertion point) - 1. Invert it so we get the insertion point. + nextIndexForCeiling = -nextIndexForCeiling - 1; + else + ceilingPrimaryKeyMatchesKeyInRange = true; + + comparisonsSavedByBsearch.update(nextIndexForCeiling - (int) ceil(logBase2(keysRemaining.size()))); + i += nextIndexForCeiling; + } + + // update our estimate + preferSeqScanToBsearch = comparisonsSavedByBsearch.size() >= 10 + && comparisonsSavedByBsearch.getMean() < 0; + if (ceilingPrimaryKeyMatchesKeyInRange) + sstableRowId = ceilingRowId; + else + continue; // without incrementing i further. ceilingPrimaryKey is less than the PK at index i. + } + // Increment here to simplify the sstableRowId < 0 logic. + i++; + + // During compaction, the SegmentMetadata is written based on the rows with vector values. Therefore, + // we can find a row that has a row id but is outside the min/max range of the segment. We can ignore + // these rows here and skip the row id to ordinal conversion that would result in a -1 ordinal. + if (sstableRowId < metadata.minSSTableRowId || sstableRowId > metadata.maxSSTableRowId) + continue; + + // convert the global row id to segment row id and from segment row id to graph ordinal + int segmentRowId = metadata.toSegmentRowId(sstableRowId); + // This requirement is required by the ordinals view. There are cases where we have broken this + // requirement, and in order to make future debugging easier, we check here and throw an exception + // with additional detail. + if (segmentRowId <= lastSegmentRowId) + throw new IllegalStateException("Row ids must ascend monotonically. Got " + segmentRowId + " after " + lastSegmentRowId + + " for " + primaryKey + " on sstable " + primaryKeyMap.getSSTableId()); + lastSegmentRowId = segmentRowId; + int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId); + if (ordinal >= 0) + segmentOrdinalPairs.add(segmentRowId, ordinal); + } + } + return segmentOrdinalPairs; + } + + public static double logBase2(double number) { + return Math.log(number) / Math.log(2); + } + + private int getRawExpectedNodes(int rerankK, int nPermittedOrdinals) + { + return VectorMemtableIndex.expectedNodesVisited(rerankK, nPermittedOrdinals, graph.size()); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("indexContext", indexContext) + .toString(); + } + + @Override + public void close() throws IOException + { + graph.close(); + } + + public boolean containsUnitVectors() + { + return graph.containsUnitVectors(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorPostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorPostingsWriter.java new file mode 100644 index 000000000000..a53e680c9049 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorPostingsWriter.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.IntUnaryOperator; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; + +import io.github.jbellis.jvector.graph.RandomAccessVectorValues; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import org.agrona.collections.Int2IntHashMap; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.vector.VectorPostings; +import org.apache.cassandra.io.util.SequentialWriter; +import org.apache.cassandra.utils.Pair; + +public class V2VectorPostingsWriter +{ + // true if vectors rows are 1:1 (all vectors are associated with exactly 1 row, and each row has a non-null vector) + private final boolean oneToOne; + // the size of the post-cleanup graph (so NOT necessarily the same as the VectorValues size, which contains entries for obsoleted ordinals) + private final int graphSize; + // given a "new" ordinal (0..size), return the ordinal it corresponds to in the original graph and VectorValues + private final IntUnaryOperator newToOldMapper; + + public V2VectorPostingsWriter(boolean oneToOne, int graphSize, IntUnaryOperator mapper) { + this.oneToOne = oneToOne; + this.graphSize = graphSize; + this.newToOldMapper = mapper; + } + + public long writePostings(SequentialWriter writer, + RandomAccessVectorValues vectorValues, + Map, ? extends VectorPostings> postingsMap, + Set deletedOrdinals) throws IOException + { + writeDeletedOrdinals(writer, deletedOrdinals); + writeNodeOrdinalToRowIdMapping(writer, vectorValues, postingsMap); + writeRowIdToNodeOrdinalMapping(writer, vectorValues, postingsMap); + + return writer.position(); + } + + private void writeDeletedOrdinals(SequentialWriter writer, Set deletedOrdinals) throws IOException + { + if (oneToOne) { + assert deletedOrdinals.isEmpty(); + // -1 indicates that fast mapping of ordinal to rowId can be used + writer.writeInt(-1); + return; + } + + writer.writeInt(deletedOrdinals.size()); + for (Integer ordinal : deletedOrdinals) { + writer.writeInt(ordinal); + } + } + + public void writeNodeOrdinalToRowIdMapping(SequentialWriter writer, + RandomAccessVectorValues vectorValues, + Map, ? extends VectorPostings> postingsMap) throws IOException + { + long ordToRowOffset = writer.getOnDiskFilePointer(); + + // total number of vectors + writer.writeInt(graphSize); + + // Write the offsets of the postings for each ordinal + var offsetsStartAt = ordToRowOffset + 4L + 8L * graphSize; + var nextOffset = offsetsStartAt; + for (var i = 0; i < graphSize; i++) { + // (ordinal is implied; don't need to write it) + writer.writeLong(nextOffset); + int postingListSize; + if (oneToOne) + { + postingListSize = 1; + } + else + { + var originalOrdinal = newToOldMapper.applyAsInt(i); + var rowIds = postingsMap.get(vectorValues.getVector(originalOrdinal)).getRowIds(); + postingListSize = rowIds.size(); + } + nextOffset += 4 + (postingListSize * 4L); // 4 bytes for size and 4 bytes for each integer in the list + } + assert writer.position() == offsetsStartAt : "writer.position()=" + writer.position() + " offsetsStartAt=" + offsetsStartAt; + + // Write postings lists + for (var i = 0; i < graphSize; i++) { + if (oneToOne) + { + writer.writeInt(1); + writer.writeInt(i); + } + else + { + var originalOrdinal = newToOldMapper.applyAsInt(i); + var rowIds = postingsMap.get(vectorValues.getVector(originalOrdinal)).getRowIds(); + writer.writeInt(rowIds.size()); + for (int r = 0; r < rowIds.size(); r++) + writer.writeInt(rowIds.getInt(r)); + } + } + assert writer.position() == nextOffset; + } + + public void writeRowIdToNodeOrdinalMapping(SequentialWriter writer, + RandomAccessVectorValues vectorValues, + Map, ? extends VectorPostings> postingsMap) throws IOException + { + long startOffset = writer.position(); + + if (oneToOne) + { + for (var i = 0; i < graphSize; i++) + { + writer.writeInt(i); + writer.writeInt(i); + } + } + else + { + // Collect all (rowId, vectorOrdinal) pairs + List> pairs = new ArrayList<>(); + for (var newOrdinal = 0; newOrdinal < graphSize; newOrdinal++) { + int oldOrdinal = newToOldMapper.applyAsInt(newOrdinal); + // if it's an on-disk Map then this is an expensive assert, only do it when in memory + if (postingsMap instanceof ConcurrentSkipListMap) + assert postingsMap.get(vectorValues.getVector(oldOrdinal)).getOrdinal() == oldOrdinal; + + var rowIds = postingsMap.get(vectorValues.getVector(oldOrdinal)).getRowIds(); + for (int r = 0; r < rowIds.size(); r++) + pairs.add(Pair.create(rowIds.getInt(r), newOrdinal)); + } + + // Sort the pairs by rowId + pairs.sort(Comparator.comparingInt(Pair::left)); + + // Write the pairs to the file + for (var pair : pairs) { + writer.writeInt(pair.left); + writer.writeInt(pair.right); + } + } + + // write the position of the beginning of rowid -> ordinals mappings to the end + writer.writeLong(startOffset); + } + + /** + * @return a map of vector ordinal to row id and the largest rowid, or null if the vectors are not 1:1 with rows + */ + private static Pair, Integer> buildOrdinalMap(Map, ? extends VectorPostings> postingsMap) + { + BiMap ordinalMap = HashBiMap.create(); + int minRow = Integer.MAX_VALUE; + int maxRow = Integer.MIN_VALUE; + for (VectorPostings vectorPostings : postingsMap.values()) + { + if (vectorPostings.getRowIds().size() != 1) + { + // multiple rows associated with this vector + return null; + } + int rowId = vectorPostings.getRowIds().getInt(0); + int ordinal = vectorPostings.getOrdinal(); + minRow = Math.min(minRow, rowId); + maxRow = Math.max(maxRow, rowId); + assert !ordinalMap.containsKey(ordinal); // vector <-> ordinal should be unique + ordinalMap.put(ordinal, rowId); + } + + if (minRow != 0 || maxRow != postingsMap.values().size() - 1) + { + // not every row had a vector associated with it + return null; + } + return Pair.create(ordinalMap, maxRow); + } + + public static V5VectorPostingsWriter.RemappedPostings remapForMemtable(Map, ? extends VectorPostings> postingsMap, + boolean containsDeletes) + { + var p = buildOrdinalMap(postingsMap); + int maxNewOrdinal = postingsMap.size() - 1; // no in-graph deletes in v2 + if (p == null || containsDeletes) + return V5VectorPostingsWriter.createGenericIdentityMapping(postingsMap); + + var ordinalMap = p.left; + var maxRow = p.right; + return new V5VectorPostingsWriter.RemappedPostings(V5VectorPostingsWriter.Structure.ONE_TO_ONE, + maxNewOrdinal, + maxRow, + ordinalMap, + new Int2IntHashMap(Integer.MIN_VALUE), + new V5VectorPostingsWriter.BiMapMapper(maxNewOrdinal, ordinalMap)); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsMeta.java b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsMeta.java new file mode 100644 index 000000000000..4350eb564209 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsMeta.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2.sortedterms; + +import java.io.IOException; + +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; + +/** + * Metadata produced by {@link SortedTermsWriter}, needed by {@link SortedTermsReader}. + */ +public class SortedTermsMeta +{ + public final long trieFP; + /** Number of terms */ + public final long count; + public final int maxTermLength; + + public SortedTermsMeta(IndexInput input) throws IOException + { + this.trieFP = input.readLong(); + this.count = input.readLong(); + this.maxTermLength = input.readInt(); + } + + public SortedTermsMeta(long trieFP, long count, int maxTermLength) + { + this.trieFP = trieFP; + this.count = count; + this.maxTermLength = maxTermLength; + } + + public void write(IndexOutput output) throws IOException + { + output.writeLong(trieFP); + output.writeLong(count); + output.writeInt(maxTermLength); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsReader.java new file mode 100644 index 000000000000..14c011fafc89 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsReader.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2.sortedterms; + +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.index.sai.disk.io.IndexInputReader; +import org.apache.cassandra.index.sai.disk.v1.LongArray; +import org.apache.cassandra.index.sai.disk.v1.bitpack.MonotonicBlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryReader; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.ReadPattern; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.util.BytesRef; + +import static org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsWriter.TERMS_DICT_BLOCK_MASK; +import static org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsWriter.TERMS_DICT_BLOCK_SHIFT; + +/** + * Provides read access to a sorted on-disk sequence of terms. + *

    + * Offers the following features: + *

      + *
    • forward iterating over all terms sequentially with a cursor
    • + *
    • constant-time look up of the term at a given point id
    • + *
    • log-time lookup of the point id of a term
    • + *
    + *

    + * Care has been taken to make this structure as efficient as possible. + * Reading terms does not require allocating data heap buffers per each read operation. + * Only one term at a time is loaded to memory. + * Low complexity algorithms are used – a lookup of the term by point id is constant time, + * and a lookup of the point id by the term is logarithmic. + * + *

    + * Because the blocks are prefix compressed, random access applies only to the locating the whole block. + * In order to jump to a concrete term inside the block, the block terms are iterated from the block beginning. + * Expect random access by {@link Cursor#seekToPointId(long)} to be slower + * than just moving to the next term with {@link Cursor#advance()}. + *

    + * For documentation of the underlying on-disk data structures, see the package documentation. + * + * @see SortedTermsWriter + * @see org.apache.cassandra.index.sai.disk.v2.sortedterms + */ +@ThreadSafe +public class SortedTermsReader +{ + private final FileHandle termsData; + private final SortedTermsMeta meta; + private final FileHandle termsTrie; + private final LongArray.Factory blockOffsetsFactory; + + /** + * Creates a new reader based on its data components. + *

    + * It does not own the components, so you must close them separately after you're done with the reader. + * @param termsData handle to the file with a sequence of prefix-compressed blocks + * each storing a fixed number of terms + * @param termsDataBlockOffsets handle to the file containing an encoded sequence of the file offsets pointing to the blocks + * @param termsTrie handle to the file storing the trie with the term-to-point-id mapping + * @param meta metadata object created earlier by the writer + * @param blockOffsetsMeta metadata object for the block offsets + */ + public SortedTermsReader(@Nonnull FileHandle termsData, + @Nonnull FileHandle termsDataBlockOffsets, + @Nonnull FileHandle termsTrie, + @Nonnull SortedTermsMeta meta, + @Nonnull NumericValuesMeta blockOffsetsMeta) throws IOException + { + this.termsData = termsData; + this.termsTrie = termsTrie; + try (IndexInput trieInput = IndexInputReader.create(termsTrie)) + { + SAICodecUtils.validate(trieInput); + } + this.meta = meta; + this.blockOffsetsFactory = new MonotonicBlockPackedReader(termsDataBlockOffsets, blockOffsetsMeta); + } + + /** + * Returns the total number of terms. + */ + public long count() + { + return meta.count; + } + + /** + * Opens a cursor over the terms stored in the terms file. + *

    + * This does not read any data yet. + * The cursor is initially positioned before the first item. + *

    + * The cursor is to be used in a single thread. + * The cursor is valid as long this object hasn't been closed. + * You must close the cursor when you no longer need it. + */ + public @Nonnull Cursor openCursor() throws IOException + { + return new Cursor(termsData, blockOffsetsFactory); + } + + /** + * Allows reading the terms from the terms file. + * Can quickly seek to a random term by pointId. + *

    + * This object is stateful and not thread safe. + * It maintains a position to the current term as well as a buffer that can hold one term. + */ + @NotThreadSafe + public class Cursor implements AutoCloseable + { + private final IndexInputReader termsData; + private final long termsDataFp; + private final LongArray blockOffsets; + + // The term the cursor currently points to. Initially empty. + private final BytesRef currentTerm; + + // The point id the cursor currently points to. -1 means before the first item. + private long pointId = -1; + + private TrieTermsDictionaryReader reader; + + Cursor(FileHandle termsData, LongArray.Factory blockOffsetsFactory) throws IOException + { + try + { + this.termsData = IndexInputReader.create(termsData); + SAICodecUtils.validate(this.termsData); + this.termsDataFp = this.termsData.getFilePointer(); + this.blockOffsets = new LongArray.DeferredLongArray(blockOffsetsFactory::open); + this.currentTerm = new BytesRef(Math.max(meta.maxTermLength, 0)); // maxTermLength can be negative if meta.count == 0 + this.reader = new TrieTermsDictionaryReader(termsTrie.instantiateRebufferer(null, ReadPattern.SEQUENTIAL), meta.trieFP, TypeUtil.BYTE_COMPARABLE_VERSION); + } + catch (Throwable t) + { + if (termsData != null) + termsData.close(); + throw t; + } + } + + /** + * Returns the point id (ordinal) associated with the least term greater than or equal to the given term, or + * a negative value if there is no such term. + * @param term + * @return + */ + public long ceiling(@Nonnull ByteComparable term) + { + Preconditions.checkNotNull(term, "term null"); + return reader.ceiling(term); + } + + /** + * Returns the point id (ordinal) of the target term or a negative value if there is no such term. + * Complexity of this operation is O(log n). + * + * @param term target term to lookup + */ + public long getExactPointId(@Nonnull ByteComparable term) + { + Preconditions.checkNotNull(term, "term null"); + return reader.exactMatch(term); + } + + /** + * Returns the point id (ordinal) associated with the greatest term less than or equal to the given term, or + * a negative value if there is no such term. + * Complexity of this operation is O(log n). + * + * @param term target term to lookup + */ + public long floor(@Nonnull ByteComparable term) + { + Preconditions.checkNotNull(term, "term null"); + return reader.floor(term); + } + + /** + * Returns the number of terms + */ + public long count() + { + return SortedTermsReader.this.count(); + } + + /** + * Returns the current position of the cursor. + * Initially, before the first call to {@link Cursor#advance}, the cursor is positioned at -1. + * After reading all the items, the cursor is positioned at index one + * greater than the position of the last item. + */ + public long pointId() + { + return pointId; + } + + /** + * Returns the current term data as ByteComparable referencing the internal term buffer. + * The term data stored behind that reference is valid only until the next call to + * {@link Cursor#advance} or {@link Cursor#seekToPointId(long)}. + */ + public @Nonnull ByteComparable term() + { + return ByteComparable.preencoded(reader.byteComparableVersion, currentTerm.bytes, currentTerm.offset, currentTerm.length); + } + + /** + * Advances the cursor to the next term and reads it into the current term buffer. + *

    + * If there are no more available terms, clears the term buffer and the cursor's position will point to the + * one behind the last item. + *

    + * This method has constant time complexity. + * + * @return true if the cursor was advanced successfully, false if the end of file was reached + * @throws IOException if a read from the terms file fails + */ + public boolean advance() throws IOException + { + if (pointId >= meta.count || ++pointId >= meta.count) + { + currentTerm.length = 0; + return false; + } + + int prefixLength; + int suffixLength; + if ((pointId & TERMS_DICT_BLOCK_MASK) == 0L) + { + prefixLength = 0; + suffixLength = termsData.readVInt(); + } + else + { + final int token = Byte.toUnsignedInt(termsData.readByte()); + prefixLength = token & 0x0F; + suffixLength = 1 + (token >>> 4); + if (prefixLength == 15) + prefixLength += termsData.readVInt(); + if (suffixLength == 16) + suffixLength += termsData.readVInt(); + } + + assert prefixLength + suffixLength <= meta.maxTermLength; + currentTerm.length = prefixLength + suffixLength; + termsData.readBytes(currentTerm.bytes, prefixLength, suffixLength); + return true; + } + + /** + * Positions the cursor on the target point id and reads the term at target to the current term buffer. + *

    + * It is allowed to position the cursor before the first item or after the last item; + * in these cases the internal buffer is cleared. + *

    + * This method has constant complexity. + * + * @param target point id to lookup + * @throws IOException if a seek and read from the terms file fails + * @throws IndexOutOfBoundsException if the target point id is less than -1 or greater than {@link Cursor#count}. + */ + public void seekToPointId(long target) throws IOException + { + if (target < -1 || target > meta.count) + throw new IndexOutOfBoundsException(); + + if (target == -1 || target == meta.count) + { + termsData.seek(termsDataFp); // matters only if target is -1 + pointId = target; + currentTerm.length = 0; + } + else + { + final long blockIndex = target >>> TERMS_DICT_BLOCK_SHIFT; + final long blockAddress = blockOffsets.get(blockIndex); + termsData.seek(blockAddress + termsDataFp); + pointId = (blockIndex << TERMS_DICT_BLOCK_SHIFT) - 1; + while (pointId < target) + { + boolean advanced = advance(); + assert advanced : "unexpected eof"; // must return true because target is in range + } + } + } + + /** + * Resets the cursor to its initial position before the first item. + */ + public void reset() throws IOException + { + seekToPointId(-1); + } + + @Override + public void close() throws IOException + { + blockOffsets.close(); + termsData.close(); + this.reader.close(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsWriter.java new file mode 100644 index 000000000000..97f856fe3aef --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsWriter.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v2.sortedterms; + +import java.io.Closeable; +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.base.Preconditions; + +import io.micrometer.core.lang.NonNull; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; +import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.tries.IncrementalTrieWriter; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.StringHelper; + +import static org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryReader.trieSerializer; + +/** + * Writes an ordered sequence of terms for use with {@link SortedTermsReader}. + *

    + * Terms must be added in lexicographical ascending order. + * Terms can be of varying lengths. + * + *

    + * Important implementation note: SAI blocked packed readers are slow, + * and Lucene MonotonicBlockPackedReader is slow. Using them + * will cause this class to slow considerably. + * + * For documentation of the underlying on-disk data structures, see the package documentation. + * + * @see SortedTermsReader + * @see org.apache.cassandra.index.sai.disk.v2.sortedterms + */ +@NotThreadSafe +public class SortedTermsWriter implements Closeable +{ + // The TERMS_DICT_ constants allow for quickly determining the id of the current block based on a point id + // or to check if we are exactly at the beginning of the block. + // Terms data are organized in blocks of (2 ^ TERMS_DICT_BLOCK_SHIFT) terms. + // The blocks should not be too small because they allow prefix compression of + // the terms except the first term in a block. + // The blocks should not be too large because we can't just ranfomly jump to the term inside the block, + // but we have to iterate through all the terms from the start of the block. + static final int TERMS_DICT_BLOCK_SHIFT = 4; + static final int TERMS_DICT_BLOCK_SIZE = 1 << TERMS_DICT_BLOCK_SHIFT; + static final int TERMS_DICT_BLOCK_MASK = TERMS_DICT_BLOCK_SIZE - 1; + + static final int DIRECT_MONOTONIC_BLOCK_SHIFT = 16; + + private final IncrementalTrieWriter trieWriter; + private final IndexOutputWriter trieOutput; + private final IndexOutput termsOutput; + private final NumericValuesWriter offsetsWriter; + private final String componentName; + private final MetadataWriter metadataWriter; + + private BytesRefBuilder prevTerm = new BytesRefBuilder(); + private BytesRefBuilder tempTerm = new BytesRefBuilder(); + + private final long bytesStartFP; + + private int maxLength = -1; + private long pointId = 0; + + /** + * Creates a new writer. + *

    + * It does not own the components, so you must close the components by yourself + * after you're done with the writer. + * + * @param termsDataComponent component builder for the prefix-compressed terms data + * @param metadataWriter the MetadataWriter for storing the SortedTermsMeta + * @param termsDataBlockOffsets where to write the offsets of each block of terms data + * @param trieComponent component where to write the trie that maps the terms to point ids + */ + public SortedTermsWriter(@NonNull IndexComponent.ForWrite termsDataComponent, + @NonNull MetadataWriter metadataWriter, + @Nonnull NumericValuesWriter termsDataBlockOffsets, + @Nonnull IndexComponent.ForWrite trieComponent) throws IOException + { + this.componentName = termsDataComponent.fileNamePart(); + this.metadataWriter = metadataWriter; + this.trieOutput = trieComponent.openOutput(); + SAICodecUtils.writeHeader(this.trieOutput); + this.trieWriter = IncrementalTrieWriter.open(trieSerializer, trieOutput.asSequentialWriter(), TypeUtil.BYTE_COMPARABLE_VERSION); + this.termsOutput = termsDataComponent.openOutput(); + SAICodecUtils.writeHeader(termsOutput, termsDataComponent.parent().version()); + this.bytesStartFP = termsOutput.getFilePointer(); + this.offsetsWriter = termsDataBlockOffsets; + } + + /** + * Appends a term at the end of the sequence. + * Terms must be added in lexicographic order. + * + * @throws IOException if write to disk fails + * @throws IllegalArgumentException if the term is not greater than the previous added term + */ + public void add(final @Nonnull ByteComparable term) throws IOException + { + tempTerm.clear(); + copyBytes(term, tempTerm); + + final BytesRef termRef = tempTerm.get(); + final BytesRef prevTermRef = this.prevTerm.get(); + + Preconditions.checkArgument(prevTermRef.length == 0 || prevTermRef.compareTo(termRef) < 0, + "Terms must be added in lexicographic ascending order."); + writeTermData(termRef); + writeTermToTrie(term); + + maxLength = Math.max(maxLength, termRef.length); + swapTempWithPrevious(); + pointId++; + } + + private void writeTermToTrie(ByteComparable term) throws IOException + { + trieWriter.add(term, pointId); + } + + private void writeTermData(BytesRef term) throws IOException + { + if ((pointId & TERMS_DICT_BLOCK_MASK) == 0) + { + offsetsWriter.add(termsOutput.getFilePointer() - bytesStartFP); + + termsOutput.writeVInt(term.length); + termsOutput.writeBytes(term.bytes, term.offset, term.length); + } + else + { + final int prefixLength = StringHelper.bytesDifference(prevTerm.get(), term); + final int suffixLength = term.length - prefixLength; + assert suffixLength > 0: "terms must be unique"; + + termsOutput.writeByte((byte) (Math.min(prefixLength, 15) | (Math.min(15, suffixLength - 1) << 4))); + if (prefixLength >= 15) + termsOutput.writeVInt(prefixLength - 15); + if (suffixLength >= 16) + termsOutput.writeVInt(suffixLength - 16); + + termsOutput.writeBytes(term.bytes, term.offset + prefixLength, term.length - prefixLength); + } + } + + /** + * Flushes any in-memory buffers to the output streams. + * Does not close the output streams. + * No more writes are allowed. + */ + @Override + public void close() throws IOException + { + try (IndexOutput output = metadataWriter.builder(componentName)) + { + final long trieFP = this.trieWriter.complete(); + SAICodecUtils.writeFooter(trieOutput); + SAICodecUtils.writeFooter(termsOutput); + SortedTermsMeta sortedTermsMeta = new SortedTermsMeta(trieFP, pointId, maxLength); + sortedTermsMeta.write(output); + } + finally + { + FileUtils.closeQuietly(trieWriter, trieOutput, termsOutput, offsetsWriter); + } + } + + /** + * Copies bytes from source to dest. + */ + private void copyBytes(ByteComparable source, BytesRefBuilder dest) + { + ByteSource byteSource = source.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION); + int val; + while ((val = byteSource.next()) != ByteSource.END_OF_STREAM) + dest.append((byte) val); + } + + /** + * Swaps this.temp with this.previous. + * It is faster to swap the pointers instead of copying the data. + */ + private void swapTempWithPrevious() + { + BytesRefBuilder temp = this.tempTerm; + this.tempTerm = this.prevTerm; + this.prevTerm = temp; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/package-info.java b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/package-info.java new file mode 100644 index 000000000000..d5f1cf9f3eef --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/sortedterms/package-info.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Space-efficient on-disk data structure for storing a sorted sequence of terms. + * Provides efficient lookup of terms by their point id, as well as locating them by contents. + *

    + * All the code in the package uses the following teminology: + *

      + *
    • Term: arbitrary data provided by the user as a bunch of bytes. Terms can be of variable length.
    • + *
    • Point id: the ordinal position of a term in the sequence, 0-based.
    • + *
    + * + * Terms are stored in ByteComparable strictly ascending order. + * Duplicates are not allowed. + * + *

    + * The structure is immutable, i.e. cannot be modified nor appended after writing to disk is completed. + * You build it by adding terms in the ascending order using + * {@link org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsWriter}. + * Once saved to disk, you can open it for lookups with + * {@link org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsReader}. + * + *

    + * The data structure comprises of the following components, each stored in a separate file: + *

      + *
    • terms data, organized as a sequence of prefix-compressed blocks each storing 16 terms
    • + *
    • a monotonic list of file offsets of the blocks; this component allows to quickly locate the block + * that contains the term with a given point id
    • + *
    • a trie indexed by terms, with a long payload for the point id, + * to quickly locate the point id of a term by the term contents + *
    • + *
    + *

    + * + * The implementation has been based on code from Lucene version 7.5 SortedDocValues. + * Prefix compression and bitpacking are used extensively to save space. + */ +package org.apache.cassandra.index.sai.disk.v2.sortedterms; \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/v3/V3InvertedIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v3/V3InvertedIndexSearcher.java new file mode 100644 index 000000000000..d62394673857 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v3/V3InvertedIndexSearcher.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v3; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.InvertedIndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; + +/** + * The key override for this class is the use of {@link Version#CA}. + */ +class V3InvertedIndexSearcher extends InvertedIndexSearcher +{ + V3InvertedIndexSearcher(SSTableContext sstableContext, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) throws IOException + { + // We filter because the CA format wrote maps acording to a different order than their abstract type. + super(sstableContext, perIndexFiles, segmentMetadata, indexContext, Version.CA, true); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v3/V3OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v3/V3OnDiskFormat.java new file mode 100644 index 000000000000..cf277f71fb2d --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v3/V3OnDiskFormat.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v3; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.EnumSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v2.V2OnDiskFormat; + +import static org.apache.cassandra.config.CassandraRelevantProperties.*; + +/** + * Different vector components compared to V2OnDiskFormat (supporting DiskANN/jvector instead of HNSW/lucene). + */ +public class V3OnDiskFormat extends V2OnDiskFormat +{ + public static final boolean REDUCE_TOPK_ACROSS_SSTABLES = SAI_REDUCE_TOPK_ACROSS_SSTABLES.getBoolean(); + public static final boolean ENABLE_RERANK_FLOOR = SAI_ENABLE_RERANK_FLOOR.getBoolean(); + public static final boolean ENABLE_EDGES_CACHE = SAI_ENABLE_EDGES_CACHE.getBoolean(); + public static final boolean ENABLE_JVECTOR_DELETES = SAI_ENABLE_JVECTOR_DELETES.getBoolean(); + + public static volatile boolean WRITE_JVECTOR3_FORMAT = SAI_WRITE_JVECTOR3_FORMAT.getBoolean(); + public static final boolean ENABLE_LTM_CONSTRUCTION = SAI_ENABLE_LTM_CONSTRUCTION.getBoolean(); + // JVector doesn't give us a way to access its default, so we set it here, but allow it to be overridden. + public static boolean JVECTOR_USE_PRUNING_DEFAULT = SAI_VECTOR_USE_PRUNING_DEFAULT.getBoolean(); + + // We allow the version to be configured via a system property because of some legacy use cases, but it is + // generally not recommended to change this directly. Instead, use the cassandra.sai.latest.version system property + // to control the on-disk format version. + private final static int JVECTOR_VERSION = SAI_JVECTOR_VERSION.getInt(); + static + { + // JVector 3 is not compatible with the latest jvector changes, so we fail fast if the config is enabled. + assert JVECTOR_VERSION != 3 : "JVector version 3 is no longer suppoerted"; + assert !WRITE_JVECTOR3_FORMAT : "JVector version 3 is no longer suppoerted"; + } + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final V3OnDiskFormat instance = new V3OnDiskFormat(); + + public static final Set VECTOR_COMPONENTS_V3 = EnumSet.of(IndexComponentType.COLUMN_COMPLETION_MARKER, + IndexComponentType.META, + IndexComponentType.PQ, + IndexComponentType.TERMS_DATA, + IndexComponentType.POSTING_LISTS); + + private static final IndexFeatureSet v3IndexFeatureSet = new IndexFeatureSet() + { + @Override + public boolean isRowAware() + { + return true; + } + + @Override + public boolean hasTermsHistogram() + { + return false; + } + }; + + @Override + public IndexFeatureSet indexFeatureSet() + { + return v3IndexFeatureSet; + } + + @Override + public IndexSearcher newIndexSearcher(SSTableContext sstableContext, + IndexContext indexContext, + PerIndexFiles indexFiles, + SegmentMetadata segmentMetadata) throws IOException + { + if (indexContext.isVector()) + return new V3VectorIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext); + if (indexContext.isLiteral()) + return new V3InvertedIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext); + return super.newIndexSearcher(sstableContext, indexContext, indexFiles, segmentMetadata); + } + + @Override + public Set perIndexComponentTypes(AbstractType validator) + { + // VSTODO add checksums and actual validation + if (validator.isVector()) + return VECTOR_COMPONENTS_V3; + return super.perIndexComponentTypes(validator); + } + + @Override + public int jvectorFileFormatVersion() + { + return JVECTOR_VERSION; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v3/V3VectorIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v3/V3VectorIndexSearcher.java new file mode 100644 index 000000000000..3dcff1023e40 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v3/V3VectorIndexSearcher.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v3; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v2.V2OnDiskOrdinalsMap; +import org.apache.cassandra.index.sai.disk.v2.V2VectorIndexSearcher; +import org.apache.cassandra.index.sai.disk.vector.CassandraDiskAnn; + +/** + * Executes ann search against the graph for an individual index segment. + */ +public class V3VectorIndexSearcher extends V2VectorIndexSearcher +{ + public V3VectorIndexSearcher(SSTableContext sstableContext, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) throws IOException + { + super(sstableContext.primaryKeyMapFactory(), + perIndexFiles, + segmentMetadata, + indexContext, + new CassandraDiskAnn(sstableContext, segmentMetadata, perIndexFiles, indexContext, V2OnDiskOrdinalsMap::new)); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v4/V4InvertedIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v4/V4InvertedIndexSearcher.java new file mode 100644 index 000000000000..f819b6e1ee6f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v4/V4InvertedIndexSearcher.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v4; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.InvertedIndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; + +/** + * The key override for this class is the use of {@link Version#DB}, which allows us to skip filtering range results. + */ +class V4InvertedIndexSearcher extends InvertedIndexSearcher +{ + V4InvertedIndexSearcher(SSTableContext sstableContext, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) throws IOException + { + super(sstableContext, perIndexFiles, segmentMetadata, indexContext, segmentMetadata.version, false); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v4/V4OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v4/V4OnDiskFormat.java new file mode 100644 index 000000000000..705f893c9fef --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v4/V4OnDiskFormat.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v4; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v3.V3OnDiskFormat; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +public class V4OnDiskFormat extends V3OnDiskFormat +{ + public static final V4OnDiskFormat instance = new V4OnDiskFormat(); + + @Override + public IndexSearcher newIndexSearcher(SSTableContext sstableContext, + IndexContext indexContext, + PerIndexFiles indexFiles, + SegmentMetadata segmentMetadata) throws IOException + { + if (indexContext.isVector()) + return super.newIndexSearcher(sstableContext, indexContext, indexFiles, segmentMetadata); + if (indexContext.isLiteral()) + return new V4InvertedIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext); + return super.newIndexSearcher(sstableContext, indexContext, indexFiles, segmentMetadata); + } + + @Override + public ByteComparable encodeForTrie(ByteBuffer input, AbstractType type) + { + // Composite types use their individual type to ensure they sorted correctly in the trie so we can do + // range queries over entries. + return TypeUtil.isLiteral(type) && !TypeUtil.isComposite(type) + ? v -> ByteSource.preencoded(input) + : TypeUtil.asComparableBytes(input, type); + } + + @Override + public ByteBuffer decodeFromTrie(ByteComparable value, AbstractType type) + { + return TypeUtil.isLiteral(type) && !TypeUtil.isComposite(type) + ? ByteBuffer.wrap(ByteSourceInverse.readBytes(value.asComparableBytes(ByteComparable.Version.OSS41))) + : TypeUtil.fromComparableBytes(value, type, ByteComparable.Version.OSS41); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskFormat.java new file mode 100644 index 000000000000..449c6e7cbd4b --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskFormat.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v5; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v4.V4OnDiskFormat; + +public class V5OnDiskFormat extends V4OnDiskFormat +{ + public static final V5OnDiskFormat instance = new V5OnDiskFormat(); + + public static boolean writeV5VectorPostings(Version version) + { + return version.onOrAfter(Version.DC); + } + + @Override + public IndexSearcher newIndexSearcher(SSTableContext sstableContext, + IndexContext indexContext, + PerIndexFiles indexFiles, + SegmentMetadata segmentMetadata) throws IOException + { + if (indexContext.isVector()) + return new V5VectorIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext); + return super.newIndexSearcher(sstableContext, indexContext, indexFiles, segmentMetadata); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java b/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java new file mode 100644 index 000000000000..677abff6450f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v5; + +import java.io.IOException; +import java.util.Arrays; +import java.util.PrimitiveIterator; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.agrona.collections.Int2ObjectHashMap; +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.index.sai.disk.vector.OnDiskOrdinalsMap; +import org.apache.cassandra.index.sai.disk.vector.OrdinalsView; +import org.apache.cassandra.index.sai.disk.vector.RowIdsView; +import org.apache.cassandra.index.sai.utils.SingletonIntIterator; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.RandomAccessReader; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class V5OnDiskOrdinalsMap implements OnDiskOrdinalsMap +{ + private static final Logger logger = LoggerFactory.getLogger(V5OnDiskOrdinalsMap.class); + + private static final OneToOneRowIdsView ONE_TO_ONE_ROW_IDS_VIEW = new OneToOneRowIdsView(); + private static final EmptyOrdinalsView EMPTY_ORDINALS_VIEW = new EmptyOrdinalsView(); + private static final EmptyRowIdsView EMPTY_ROW_IDS_VIEW = new EmptyRowIdsView(); + + private final FileHandle fh; + private final long ordToRowOffset; + private final long segmentEnd; + private final int maxOrdinal; + private final int maxRowId; + private final long rowToOrdinalOffset; + @VisibleForTesting + final V5VectorPostingsWriter.Structure structure; + + private final Supplier ordinalsViewSupplier; + private final Supplier rowIdsViewSupplier; + + // cached values for OneToMany structure + private Int2ObjectHashMap extraRowsByOrdinal = null; + private int[] extraRowIds = null; + private int[] extraOrdinals = null; + + + public V5OnDiskOrdinalsMap(FileHandle fh, long segmentOffset, long segmentLength) + { + this.segmentEnd = segmentOffset + segmentLength; + this.fh = fh; + try (var reader = fh.createReader()) + { + reader.seek(segmentOffset); + int magic = reader.readInt(); + if (magic != V5VectorPostingsWriter.MAGIC) + { + throw new RuntimeException("Invalid magic number in V5OnDiskOrdinalsMap"); + } + this.structure = V5VectorPostingsWriter.Structure.values()[reader.readInt()]; + this.maxOrdinal = reader.readInt(); + this.maxRowId = reader.readInt(); + this.ordToRowOffset = reader.getFilePointer(); + if (structure == V5VectorPostingsWriter.Structure.ONE_TO_ONE) + { + this.rowToOrdinalOffset = segmentEnd; + } + else + { + reader.seek(segmentEnd - 8); + this.rowToOrdinalOffset = reader.readLong(); + } + + if (maxOrdinal < 0) + { + this.rowIdsViewSupplier = () -> EMPTY_ROW_IDS_VIEW; + this.ordinalsViewSupplier = () -> EMPTY_ORDINALS_VIEW; + } + else if (structure == V5VectorPostingsWriter.Structure.ONE_TO_ONE) + { + this.rowIdsViewSupplier = () -> ONE_TO_ONE_ROW_IDS_VIEW; + this.ordinalsViewSupplier = () -> new OneToOneOrdinalsView(maxOrdinal + 1); + } + else if (structure == V5VectorPostingsWriter.Structure.ONE_TO_MANY) + { + cacheExtraRowIds(reader); + cacheExtraRowOrdinals(reader); + this.rowIdsViewSupplier = OneToManyRowIdsView::new; + this.ordinalsViewSupplier = OneToManyOrdinalsView::new; + } + else + { + this.rowIdsViewSupplier = GenericRowIdsView::new; + this.ordinalsViewSupplier = GenericOrdinalsView::new; + } + + assert rowToOrdinalOffset <= segmentEnd : "rowOrdinalOffset " + rowToOrdinalOffset + " is not less than or equal to segmentEnd " + segmentEnd; + } + catch (Exception e) + { + throw new RuntimeException("Error initializing OnDiskOrdinalsMap at segment " + segmentOffset, e); + } + } + + @Override + public V5VectorPostingsWriter.Structure getStructure() + { + return structure; + } + + private void cacheExtraRowIds(RandomAccessReader reader) throws IOException + { + extraRowsByOrdinal = new Int2ObjectHashMap<>(); + reader.seek(ordToRowOffset); + int entryCount = reader.readInt(); + for (int i = 0; i < entryCount; i++) + { + int ordinal = reader.readInt(); + int postingsSize = reader.readInt(); + if (postingsSize > 0) + postingsSize++; // add the ordinal itself + int[] rowIds = new int[postingsSize]; + if (postingsSize > 0) + { + rowIds[0] = ordinal; + for (int j = 1; j < postingsSize; j++) + rowIds[j] = reader.readInt(); + } + extraRowsByOrdinal.put(ordinal, rowIds); + } + } + + private void cacheExtraRowOrdinals(RandomAccessReader reader) throws IOException + { + var extraRowIdsList = new IntArrayList(); + var extraOrdinalsList = new IntArrayList(); + reader.seek(rowToOrdinalOffset); + while (reader.getFilePointer() < segmentEnd - 8) + { + extraRowIdsList.add(reader.readInt()); + extraOrdinalsList.add(reader.readInt()); + } + + extraRowIds = extraRowIdsList.toIntArray(); + extraOrdinals = extraOrdinalsList.toIntArray(); + } + + public RowIdsView getRowIdsView() + { + return rowIdsViewSupplier.get(); + } + + private class GenericRowIdsView implements RowIdsView + { + RandomAccessReader reader = fh.createReader(); + + @Override + public PrimitiveIterator.OfInt getSegmentRowIdsMatching(int vectorOrdinal) throws IOException + { + Preconditions.checkArgument(vectorOrdinal <= maxOrdinal, "vectorOrdinal %s is out of bounds %s", vectorOrdinal, maxOrdinal); + + // read index entry + try + { + reader.seek(ordToRowOffset + vectorOrdinal * 8L); + } + catch (Exception e) + { + throw new RuntimeException(String.format("Error seeking to index offset for ordinal %d with ordToRowOffset %d", + vectorOrdinal, ordToRowOffset), e); + } + var offset = reader.readLong(); + // seek to and read rowIds + try + { + reader.seek(offset); + } + catch (Exception e) + { + throw new RuntimeException(String.format("Error seeking to rowIds offset for ordinal %d with ordToRowOffset %d", + vectorOrdinal, ordToRowOffset), e); + } + var postingsSize = reader.readInt(); + + // Optimize for the most common case + if (postingsSize == 1) + return new SingletonIntIterator(reader.readInt()); + + var rowIds = new int[postingsSize]; + for (var i = 0; i < rowIds.length; i++) + { + rowIds[i] = reader.readInt(); + } + return Arrays.stream(rowIds).iterator(); + } + + @Override + public void close() + { + reader.close(); + } + } + + public OrdinalsView getOrdinalsView() + { + return ordinalsViewSupplier.get(); + } + + @NotThreadSafe + private class GenericOrdinalsView implements OrdinalsView + { + RandomAccessReader reader = fh.createReader(); + + /** + * @return ordinal if given row id is found; otherwise return -1 + * rowId must increase + */ + @Override + public int getOrdinalForRowId(int rowId) throws IOException + { + long offset = rowToOrdinalOffset + (long) rowId * 8; + if (offset >= segmentEnd - 8) + return -1; + + reader.seek(offset); + int foundRowId = reader.readInt(); + assert foundRowId == rowId : "foundRowId=" + foundRowId + " instead of rowId=" + rowId; + + return reader.readInt(); + } + + @Override + public void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException + { + long startOffset = max(rowToOrdinalOffset, rowToOrdinalOffset + (long) startRowId * 8); + if (startOffset >= segmentEnd - 8) + return; // start rowid is larger than any rowId that has an associated vector ordinal + + reader.seek(startOffset); + + while (reader.getFilePointer() < segmentEnd - 8) + { + int rowId = reader.readInt(); + int ordinal = reader.readInt(); + + if (rowId > endRowId) + break; + + if (ordinal != -1) + consumer.accept(rowId, ordinal); + } + } + + @Override + public void close() + { + reader.close(); + } + } + + public void close() + { + fh.close(); + } + + private class OneToManyRowIdsView implements RowIdsView + { + @Override + public PrimitiveIterator.OfInt getSegmentRowIdsMatching(int ordinal) + { + Preconditions.checkArgument(ordinal <= maxOrdinal, "vectorOrdinal %s is out of bounds %s", ordinal, maxOrdinal); + + int[] rowIds = extraRowsByOrdinal.get(ordinal); + // no entry means there is just one rowid matching the ordinal + if (rowIds == null) + return new SingletonIntIterator(ordinal); + // zero-length entry means it's a hole + if (rowIds.length == 0) + return IntStream.empty().iterator(); + // otherwise return the rowIds + return Arrays.stream(rowIds).iterator(); + } + + @Override + public void close() + { + // no-op + } + } + + private class OneToManyOrdinalsView implements OrdinalsView { + @Override + public int getOrdinalForRowId(int rowId) + { + assert rowId >= 0 : rowId; + if (rowId > maxRowId) { + return -1; + } + + int index = Arrays.binarySearch(extraRowIds, rowId); + if (index >= 0) { + // Found in extra rows + return extraOrdinals[index]; + } + + // If it's not an "extra" row then the ordinal is the same as the rowId + return rowId; + } + + @Override + public void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException + { + int rawIndex = Arrays.binarySearch(extraRowIds, startRowId); + int extraIndex = rawIndex >= 0 ? rawIndex : -rawIndex - 1; + for (int rowId = max(0, startRowId); rowId <= min(endRowId, maxRowId); rowId++) + { + if (extraIndex < extraRowIds.length && extraRowIds[extraIndex] == rowId) + { + consumer.accept(extraRowIds[extraIndex], extraOrdinals[extraIndex]); + extraIndex++; + } + else + { + consumer.accept(rowId, rowId); + } + } + } + + @Override + public void close() { + // no-op + } + } + + @Override + public long cachedBytesUsed() + { + if (structure != V5VectorPostingsWriter.Structure.ONE_TO_MANY) { + return 0; + } + + long bytes = 0; + if (extraRowIds != null) { + bytes += extraRowIds.length * 4L; + } + if (extraOrdinals != null) { + bytes += extraOrdinals.length * 4L; + } + if (extraRowsByOrdinal != null) { + for (int[] rowIds : extraRowsByOrdinal.values()) { + bytes += rowIds.length * 4L; + } + } + return bytes; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v5/V5VectorIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v5/V5VectorIndexSearcher.java new file mode 100644 index 000000000000..2c5cd8b479f5 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v5/V5VectorIndexSearcher.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.disk.v5; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v2.V2VectorIndexSearcher; +import org.apache.cassandra.index.sai.disk.vector.CassandraDiskAnn; + +/** + * Executes ann search against the graph for an individual index segment. + */ +public class V5VectorIndexSearcher extends V2VectorIndexSearcher +{ + public V5VectorIndexSearcher(SSTableContext sstableContext, + PerIndexFiles perIndexFiles, + SegmentMetadata segmentMetadata, + IndexContext indexContext) throws IOException + { + // inherits from V2 instead of V3 because the difference between V5 and V3 is the OnDiskOrdinalsMap that they use + super(sstableContext.primaryKeyMapFactory(), + perIndexFiles, + segmentMetadata, + indexContext, + new CassandraDiskAnn(sstableContext, segmentMetadata, perIndexFiles, indexContext, V5OnDiskOrdinalsMap::new)); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v5/V5VectorPostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v5/V5VectorPostingsWriter.java new file mode 100644 index 000000000000..bc2d1028eeb1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v5/V5VectorPostingsWriter.java @@ -0,0 +1,567 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v5; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.function.IntPredicate; +import java.util.function.IntUnaryOperator; +import java.util.stream.IntStream; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.graph.RandomAccessVectorValues; +import io.github.jbellis.jvector.graph.disk.OrdinalMapper; +import io.github.jbellis.jvector.util.FixedBitSet; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import net.openhft.chronicle.map.ChronicleMap; +import org.agrona.collections.Int2IntHashMap; +import org.agrona.collections.Int2ObjectHashMap; +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.vector.VectorPostings; +import org.apache.cassandra.io.util.SequentialWriter; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class V5VectorPostingsWriter +{ + private static final Logger logger = LoggerFactory.getLogger(V5VectorPostingsWriter.class); + + /** + * Write a one-to-many mapping if the number of "holes" in the resulting ordinal sequence + * is less than this fraction of the total rows. Holes have two effects that make us not + * want to overuse them: + * (1) We read the list of rowids associated with the holes into memory + * (2) The holes make the terms component (the vector index) less cache-efficient + *
    + * In the Cohere wikipedia dataset, we observe 0.014% vectors with multiple rows, so this + * almost two orders of magnitude higher than the observed rate of holes in the same dataset. + */ + @VisibleForTesting + public static double GLOBAL_HOLES_ALLOWED = CassandraRelevantProperties.SAI_VECTOR_ORDINAL_HOLE_DENSITY_LIMIT.getDouble(); + + public static int MAGIC = 0x90571265; // POSTINGS + + public enum Structure + { + /** + * The mapping from vector ordinals to row ids is a bijection, i.e. each vector has exactly one row associated + * with it and each row has exactly one vector associated with it. No additional mappings need to be written, + * and reads can happen without consulting disk. + */ + ONE_TO_ONE, + + /** + * Every row has a vector and at least one vector has multiple rows. The ratio of rows without a unique vector + * to total rows is smaller than {@link #GLOBAL_HOLES_ALLOWED}. Only special cases (where the row id + * cannot be mapped to the same vector ordinal) are written; since this is a small fraction of total + * rows, these special cases are read into memory and reads can happen without consulting disk. + * When this format is used, level 0 of the graph contains a node for every row id, however, the only nodes + * that are actually reachable are the first node for each vector. + */ + ONE_TO_MANY, + + /** + * Either: + * 1. There is at least one row without a vector, or + * 2. The mapping would be {@link #ONE_TO_MANY}, but the ratio of rows without a unique vector to total rows is larger + * than {@link #GLOBAL_HOLES_ALLOWED}. + * Explicit mappings from each row id to vector ordinal and vice versa are written. Reads must consult disk. + */ + ZERO_OR_ONE_TO_MANY + } + + private final RemappedPostings remappedPostings; + + /** + * If Structure is ONE_TO_MANY then extraPostings should be the rowid -> ordinal map for the "extra" rows + * as determined by CassandraOnHeapGraph::buildOrdinalMap; otherwise it should be null + */ + public V5VectorPostingsWriter(RemappedPostings remappedPostings) + { + this.remappedPostings = remappedPostings; + } + + /** + * This method describes the mapping done during construction of the graph so that we can easily create + * an appropriate V5VectorPostingsWriter. No ordinal remapping is performed because (V5) compaction writes + * vectors to disk as they are added to the graph, so there is no opportunity to reorder the way there is + * in a Memtable index. + */ + public static RemappedPostings describeForCompaction(Structure structure, int graphSize, int maxRowId, int maxOrdinal, ChronicleMap, VectorPostings.CompactionVectorPostings> postingsMap) + { + assert !postingsMap.isEmpty(); // flush+compact should skip writing an index component in this case + + if (structure == Structure.ONE_TO_ONE) + { + return new RemappedPostings(Structure.ONE_TO_ONE, + graphSize - 1, + graphSize - 1, + null, + null, + new OrdinalMapper.IdentityMapper(graphSize - 1)); + } + + if (structure == Structure.ONE_TO_MANY) + { + // compute extraOrdinals from the postingsMap + var extraOrdinals = new Int2IntHashMap(Integer.MIN_VALUE); + postingsMap.forEachEntry(entry -> { + VectorPostings.CompactionVectorPostings.Marshaller.recordExtraOrdinals(entry, extraOrdinals); + }); + + var skippedOrdinals = extraOrdinals.keySet(); + return new RemappedPostings(Structure.ONE_TO_MANY, + maxOrdinal, + maxRowId, + null, + extraOrdinals, + new OmissionAwareIdentityMapper(maxOrdinal, skippedOrdinals::contains)); + } + + assert structure == Structure.ZERO_OR_ONE_TO_MANY : structure; + return createGenericIdentityMapping(postingsMap, maxRowId, maxOrdinal); + } + + public long writePostings(SequentialWriter writer, + RandomAccessVectorValues vectorValues, + Map, ? extends VectorPostings> postingsMap) throws IOException + { + var structure = remappedPostings.structure; + + writer.writeInt(MAGIC); + writer.writeInt(structure.ordinal()); + writer.writeInt(remappedPostings.maxNewOrdinal); + writer.writeInt(remappedPostings.maxRowId); + + if (structure == Structure.ONE_TO_ONE || remappedPostings.maxNewOrdinal < 0) + { + // nothing more to do + } + else if (structure == Structure.ONE_TO_MANY) + { + writeOneToManyOrdinalMapping(writer); + writeOneToManyRowIdMapping(writer); + } + else + { + assert structure == Structure.ZERO_OR_ONE_TO_MANY; + writeGenericOrdinalToRowIdMapping(writer, vectorValues, postingsMap); + writeGenericRowIdMapping(writer, vectorValues, postingsMap); + } + + return writer.position(); + } + + private void writeOneToManyOrdinalMapping(SequentialWriter writer) throws IOException + { + // make sure we're in the right branch + assert !remappedPostings.extraPostings.isEmpty(); + + // Create a map of (original) ordinals to their extra rowids + var ordinalToExtraRowIds = new Int2ObjectHashMap(); + for (var entry : remappedPostings.extraPostings.entrySet()) { + int rowId = entry.getKey(); + int ordinal = entry.getValue(); + ordinalToExtraRowIds.computeIfAbsent(ordinal, k -> new IntArrayList()).add(rowId); + } + + // Write the ordinals and their extra rowids + int holeCount = (int) IntStream.range(0, remappedPostings.maxNewOrdinal + 1) + .map(remappedPostings.ordinalMapper::newToOld) + .filter(i -> i == OrdinalMapper.OMITTED) + .count(); + writer.writeInt(holeCount + ordinalToExtraRowIds.size()); + int entries = 0; + for (int newOrdinal = 0; newOrdinal <= remappedPostings.maxNewOrdinal; newOrdinal++) { + // write the "holes" so they are not incorrectly associated with the corresponding rowId + int oldOrdinal = remappedPostings.ordinalMapper.newToOld(newOrdinal); + if (oldOrdinal == OrdinalMapper.OMITTED) + { + writer.writeInt(newOrdinal); + writer.writeInt(0); + entries++; + continue; + } + + // write the ordinals with multiple rows + var extraRowIds = ordinalToExtraRowIds.get(oldOrdinal); + if (extraRowIds != null) + { + writer.writeInt(newOrdinal); + writer.writeInt(extraRowIds.size()); + for (int rowId : extraRowIds) { + writer.writeInt(rowId); + } + entries++; + } + } + assert entries == holeCount + ordinalToExtraRowIds.size(); + } + + private void writeOneToManyRowIdMapping(SequentialWriter writer) throws IOException + { + long startOffset = writer.position(); + + // make sure we're in the right branch + assert !remappedPostings.extraPostings.isEmpty(); + + // sort the extra rowids. this boxes, but there isn't a good way to avoid that + var extraRowIds = remappedPostings.extraPostings.keySet().stream().sorted().mapToInt(i -> i).toArray(); + // only write the extra postings, everything else can be determined from those + int lastExtraRowId = -1; + for (int i = 0; i < extraRowIds.length; i++) + { + int rowId = extraRowIds[i]; + int originalOrdinal = remappedPostings.extraPostings.get(rowId); + writer.writeInt(rowId); + writer.writeInt(remappedPostings.ordinalMapper.oldToNew(originalOrdinal)); + // validate that we do in fact have contiguous rowids in the non-extra mapping + assert IntStream.range(lastExtraRowId + 1, rowId) + .allMatch(j -> remappedPostings.ordinalMapper.newToOld(j) != OrdinalMapper.OMITTED) : "Non-contiguous rowids found in non-extra mapping"; + lastExtraRowId = rowId; + } + + // Write the position of the beginning of rowid -> ordinals mappings to the end + writer.writeLong(startOffset); + } + + // VSTODO add missing row information to remapping so we don't have to go through the vectorValues again + public void writeGenericOrdinalToRowIdMapping(SequentialWriter writer, + RandomAccessVectorValues vectorValues, + Map, ? extends VectorPostings> postingsMap) throws IOException + { + long ordToRowOffset = writer.getOnDiskFilePointer(); + + var newToOldMapper = (IntUnaryOperator) remappedPostings.ordinalMapper::newToOld; + int ordinalCount = remappedPostings.maxNewOrdinal + 1; // may include unmapped ordinals + // Write the offsets of the postings for each ordinal + var offsetsStartAt = ordToRowOffset + 8L * ordinalCount; + var nextOffset = offsetsStartAt; + for (var i = 0; i < ordinalCount; i++) { + // (ordinal is implied; don't need to write it) + writer.writeLong(nextOffset); + int originalOrdinal = newToOldMapper.applyAsInt(i); + int postingListSize; + if (originalOrdinal == OrdinalMapper.OMITTED) + { + assert remappedPostings.structure == Structure.ZERO_OR_ONE_TO_MANY; + postingListSize = 0; + } + else + { + var rowIds = postingsMap.get(vectorValues.getVector(originalOrdinal)).getRowIds(); + postingListSize = rowIds.size(); + } + nextOffset += 4 + (postingListSize * 4L); // 4 bytes for size and 4 bytes for each integer in the list + } + assert writer.position() == offsetsStartAt : "writer.position()=" + writer.position() + " offsetsStartAt=" + offsetsStartAt; + + // Write postings lists + for (var i = 0; i < ordinalCount; i++) { + int originalOrdinal = newToOldMapper.applyAsInt(i); + if (originalOrdinal == OrdinalMapper.OMITTED) + { + assert remappedPostings.structure == Structure.ZERO_OR_ONE_TO_MANY; + writer.writeInt(0); + continue; + } + var rowIds = postingsMap.get(vectorValues.getVector(originalOrdinal)).getRowIds(); + writer.writeInt(rowIds.size()); + for (int r = 0; r < rowIds.size(); r++) + writer.writeInt(rowIds.getInt(r)); + } + assert writer.position() == nextOffset; + } + + public void writeGenericRowIdMapping(SequentialWriter writer, + RandomAccessVectorValues vectorValues, + Map, ? extends VectorPostings> postingsMap) throws IOException + { + long startOffset = writer.position(); + + // Create a Map of rowId -> ordinal + int maxRowId = -1; + var rowIdToOrdinalMap = new Int2IntHashMap(remappedPostings.maxNewOrdinal, 0.65f, OrdinalMapper.OMITTED); + for (int i = 0; i <= remappedPostings.maxNewOrdinal; i++) { + int ord = remappedPostings.ordinalMapper.newToOld(i); + if (ord == OrdinalMapper.OMITTED) + continue; + + var rowIds = postingsMap.get(vectorValues.getVector(ord)).getRowIds(); + for (int r = 0; r < rowIds.size(); r++) + { + var rowId = rowIds.getInt(r); + rowIdToOrdinalMap.put(rowId, i); + maxRowId = max(maxRowId, rowId); + } + } + + // Write rowId -> ordinal mappings, filling in missing rowIds with -1 + for (int currentRowId = 0; currentRowId <= maxRowId; currentRowId++) { + writer.writeInt(currentRowId); + if (rowIdToOrdinalMap.containsKey(currentRowId)) + writer.writeInt(rowIdToOrdinalMap.get(currentRowId)); + else + writer.writeInt(-1); // no corresponding ordinal + } + + // write the position of the beginning of rowid -> ordinals mappings to the end + writer.writeLong(startOffset); + } + + /** + * RemappedPostings is a + * - BiMap of original vector ordinal to the first row id it is associated with + * - Map of row id to original vector ordinal for rows that are NOT the first row associated with their vector + *

    + * Example, using digits as ordianls and letters as row ids. Postings map contains + * 0 -> B, C + * 1 -> A + * 2 -> D + *

    + * The returned ordinalMap would be {0 <-> B, 1 <-> A, 2 <-> D} and the extraPostings would be {C -> 0} + */ + public static class RemappedPostings + { + /** relationship of vector ordinals to row ids */ + public final Structure structure; + /** the largest vector ordinal in the postings (inclusive) */ + public final int maxNewOrdinal; + /** the largest rowId in the postings (inclusive) */ + public final int maxRowId; + /** map from rowId to [original] vector ordinal */ + @Nullable + private final Int2IntHashMap extraPostings; + /** public api */ + public final OrdinalMapper ordinalMapper; + + /** visible for V2VectorPostingsWriter.remapPostings, everyone else should use factory methods */ + public RemappedPostings(Structure structure, int maxNewOrdinal, int maxRowId, BiMap ordinalMap, Int2IntHashMap extraPostings, OrdinalMapper ordinalMapper) + { + this.structure = structure; + this.maxNewOrdinal = maxNewOrdinal; + this.maxRowId = maxRowId; + this.extraPostings = extraPostings; + this.ordinalMapper = ordinalMapper; + } + } + + /** + * @see RemappedPostings + */ + public static RemappedPostings remapForMemtable(Map, ? extends VectorPostings> postingsMap, Version version) + { + assert V5OnDiskFormat.writeV5VectorPostings(version); + + BiMap ordinalMap = HashBiMap.create(); + Int2IntHashMap extraPostings = new Int2IntHashMap(Integer.MIN_VALUE); + int minRow = Integer.MAX_VALUE; + int maxRow = Integer.MIN_VALUE; + int maxNewOrdinal = Integer.MIN_VALUE; + int maxOldOrdinal = Integer.MIN_VALUE; + int totalRowsAssigned = 0; + + // build the ordinalMap and extraPostings + for (var vectorPostings : postingsMap.values()) + { + assert !vectorPostings.isEmpty(); // deleted vectors should be cleaned out before remapping + var a = vectorPostings.getRowIds().toIntArray(); + Arrays.sort(a); + int rowId = a[0]; + int oldOrdinal = vectorPostings.getOrdinal(); + maxOldOrdinal = max(maxOldOrdinal, oldOrdinal); + minRow = min(minRow, rowId); + maxRow = max(maxRow, a[a.length - 1]); + assert !ordinalMap.containsKey(oldOrdinal); // vector <-> ordinal should be unique + ordinalMap.put(oldOrdinal, rowId); + maxNewOrdinal = max(maxNewOrdinal, rowId); + totalRowsAssigned += a.length; // all row ids should also be unique, but we can't easily check that + if (a.length > 1) + { + for (int i = 1; i < a.length; i++) + extraPostings.put(a[i], oldOrdinal); + } + } + assert totalRowsAssigned == 0 || totalRowsAssigned <= maxRow + 1: "rowids are not unique -- " + totalRowsAssigned + " >= " + maxRow; + + // derive the correct structure + Structure structure; + if (totalRowsAssigned > 0 && (minRow != 0 || totalRowsAssigned < maxRow + 1)) + { + logger.debug("Not all rows are assigned vectors, cannot remap one-to-many"); + structure = Structure.ZERO_OR_ONE_TO_MANY; + } + else + { + structure = extraPostings.isEmpty() + ? Structure.ONE_TO_ONE + : Structure.ONE_TO_MANY; + // override one-to-many to generic if there are too many holes + if (structure == Structure.ONE_TO_MANY && tooManyOrdinalMappingHoles(postingsMap.size(), maxRow)) + structure = Structure.ZERO_OR_ONE_TO_MANY; + logger.debug("Remapped postings include {} unique vectors and {} 'extra' rows sharing them. Structure is {}", + ordinalMap.size(), extraPostings.size(), structure); + } + + // create the mapping + if (structure == Structure.ZERO_OR_ONE_TO_MANY) + return createGenericRenumberedMapping(ordinalMap.keySet(), maxOldOrdinal, maxRow); + var ordinalMapper = new BiMapMapper(maxNewOrdinal, ordinalMap); + return new RemappedPostings(structure, maxNewOrdinal, maxRow, ordinalMap, extraPostings, ordinalMapper); + } + + /** + * Given the number of vectors and rows indexed, determine if there are too many holes for a one to many mapping. + * @param totalVectorsIndexed the number of unique vectors in the index segment + * @param totalRowsIndexed the number of rows in the index segment + * @return true if there are too many holes, false otherwise + */ + public static boolean tooManyOrdinalMappingHoles(int totalVectorsIndexed, int totalRowsIndexed) + { + return totalRowsIndexed - totalVectorsIndexed > GLOBAL_HOLES_ALLOWED * totalRowsIndexed; + } + + /** + * return an exhaustive zero-to-many mapping with the live ordinals renumbered sequentially + */ + private static RemappedPostings createGenericRenumberedMapping(Set liveOrdinals, int maxOldOrdinal, int maxRow) + { + var oldToNew = new Int2IntHashMap(maxOldOrdinal, 0.65f, Integer.MIN_VALUE); + int nextOrdinal = 0; + for (int i = 0; i <= maxOldOrdinal; i++) { + if (liveOrdinals.contains(i)) + oldToNew.put(i, nextOrdinal++); + } + return new RemappedPostings(Structure.ZERO_OR_ONE_TO_MANY, + nextOrdinal - 1, + maxRow, + null, + null, + new OrdinalMapper.MapMapper(oldToNew)); + } + + /** + * return an exhaustive zero-to-many mapping with no renumbering + */ + public static RemappedPostings createGenericIdentityMapping(Map, ? extends VectorPostings> postingsMap) + { + var maxOldOrdinal = postingsMap.values().stream().mapToInt(VectorPostings::getOrdinal).max().orElseThrow(); + int maxRow = postingsMap.values().stream().flatMap(p -> p.getRowIds().stream()).mapToInt(i -> i).max().orElseThrow(); + var presentOrdinals = new FixedBitSet(maxOldOrdinal + 1); + for (var entry : postingsMap.entrySet()) + presentOrdinals.set(entry.getValue().getOrdinal()); + return new RemappedPostings(Structure.ZERO_OR_ONE_TO_MANY, + maxOldOrdinal, + maxRow, + null, + null, + new OmissionAwareIdentityMapper(maxOldOrdinal, i -> !presentOrdinals.get(i))); + } + + /** + * return an exhaustive zero-to-many mapping with no renumbering + */ + public static RemappedPostings createGenericIdentityMapping(ChronicleMap, VectorPostings.CompactionVectorPostings> postingsMap, int maxRowId, int maxOldOrdinal) + { + var presentOrdinals = new FixedBitSet(maxOldOrdinal + 1); + + // Iterate the whole map using the low level API that avoids deserialization penalties. + postingsMap.forEachEntry(entry -> { + presentOrdinals.set(VectorPostings.CompactionVectorPostings.Marshaller.extractOrdinal(entry)); + }); + + return new RemappedPostings(Structure.ZERO_OR_ONE_TO_MANY, + maxOldOrdinal, + maxRowId, + null, + null, + new OmissionAwareIdentityMapper(maxOldOrdinal, i -> !presentOrdinals.get(i))); + } + + public static class BiMapMapper implements OrdinalMapper + { + private final int maxOrdinal; + private final BiMap ordinalMap; + + public BiMapMapper(int maxNewOrdinal, BiMap ordinalMap) + { + this.maxOrdinal = maxNewOrdinal; + this.ordinalMap = ordinalMap; + } + + @Override + public int maxOrdinal() + { + return maxOrdinal; + } + + @Override + public int oldToNew(int i) + { + return ordinalMap.get(i); + } + + @Override + public int newToOld(int i) + { + return ordinalMap.inverse().getOrDefault(i, OMITTED); + } + } + + private static class OmissionAwareIdentityMapper implements OrdinalMapper + { + private final int maxVectorOrdinal; + private final IntPredicate toSkip; + + public OmissionAwareIdentityMapper(int maxVectorOrdinal, IntPredicate toSkip) + { + this.maxVectorOrdinal = maxVectorOrdinal; + this.toSkip = toSkip; + } + + @Override + public int maxOrdinal() + { + return maxVectorOrdinal; + } + + @Override + public int oldToNew(int i) + { + return i; + } + + @Override + public int newToOld(int i) + { + return toSkip.test(i) ? OrdinalMapper.OMITTED : i; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v6/TermsDistribution.java b/src/java/org/apache/cassandra/index/sai/disk/v6/TermsDistribution.java new file mode 100644 index 000000000000..0a066d8393ae --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v6/TermsDistribution.java @@ -0,0 +1,591 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v6; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.PriorityQueue; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.NumberType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexInput; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +/** + * Approximates a statistical distribution of term values in a sstable index segment. + * It is used to quickly estimate how many rows match a given term value or a range of values, + * without performing the search using the index (which would be more costly). + *

    + * Comprises a histogram and a most frequent term table. + *

    + * To build instances of this class, use the nested {@link Builder} class. + * + * @see SegmentMetadata + */ +@ThreadSafe +@Immutable +public class TermsDistribution +{ + // Special virtual bucket placed before all the other buckets of the histogram. + // Can be considered a bucket at index -1. The existence of this instance allows us to never return null + // when looking up the bucket by an index and simplifies the code. + private static final Bucket MIN_BUCKET = new Bucket(null, 0, 0); + + private static final int MAGIC = 0xd57a75; // STATS ;) + + public final AbstractType termType; + public final Version indexVersion; + public final ByteComparable.Version byteComparableVersion; + + final ByteComparable minTerm; + final ByteComparable maxTerm; + final List histogram; + final NavigableMap mostFrequentTerms; + + public final long numPoints; + final long numRows; + + private TermsDistribution(AbstractType termType, + List histogram, + NavigableMap mostFrequentTerms, + Version indexVersion, + ByteComparable.Version byteComparableVersion) + { + this.termType = termType; + this.indexVersion = indexVersion; + this.byteComparableVersion = byteComparableVersion; + this.histogram = histogram; + this.mostFrequentTerms = mostFrequentTerms; + + this.numRows = histogram.isEmpty() ? 0 : histogram.get(histogram.size() - 1).cumulativeRowCount; + this.numPoints = histogram.isEmpty() ? 0 : histogram.get(histogram.size() - 1).cumulativePointCount; + this.minTerm = histogram.isEmpty() ? null : histogram.get(0).term; + this.maxTerm = histogram.isEmpty() ? null : histogram.get(histogram.size() - 1).term; + } + + /** + * Estimates the number of values equal to the given term. + * + * @param term term encoded as byte-comparable the same way as stored by the index on-disk + */ + public long estimateNumRowsMatchingExact(ByteComparable term) + { + Long count = mostFrequentTerms.get(term); + if (count != null) + return count; + + int index = indexOfBucketContaining(term); + Bucket low = getBucket(index - 1); + Bucket high = getBucket(index); + + // The histogram buckets include all most frequent terms, + // but if we're here, we know our term is *not* any of the frequent values. + // Therefore, we should subtract frequent values to get better precision: + var mft = mostFrequentTermsInRange(low.term, high.term); + + long points = high.cumulativePointCount - low.cumulativePointCount - mft.size(); + long rows = high.cumulativeRowCount - low.cumulativeRowCount - sumValues(mft); + return rows == 0 ? 0 : Math.round((double) rows / points); + } + + /** + * Estimates the number of rows with a value in given range. + * Allows to specify inclusiveness/exclusiveness of bounds. + * Bounds must be encoded as byte-comparable the same way as stored by the index on-disk. + */ + public long estimateNumRowsInRange(ByteComparable min, boolean minInclusive, ByteComparable max, boolean maxInclusive) + { + long rowCount = estimateNumRowsInRange(min, max); + + if (minInclusive && min != null) + rowCount += estimateNumRowsMatchingExact(min); + if (!maxInclusive && max != null) + rowCount = Math.max(0, rowCount - estimateNumRowsMatchingExact(max)); + + return rowCount; + } + + /** + * Estimates the number of rows with a value in given range. + * Bounds must be encoded as byte-comparable the same way as stored by the index on-disk. + * + * @param min exclusive minimum bound + * @param max inclusive maximum bound + */ + public long estimateNumRowsInRange(ByteComparable min, ByteComparable max) + { + Bucket low = (min != null) ? interpolate(min) : getBucket(-1); + Bucket high = (max != null) ? interpolate(max) : getBucket(histogram.size()); + return Math.max(0, high.cumulativeRowCount - low.cumulativeRowCount); + } + + /** + * Returns cumulative point count and cumulative row count for given term + * by linear interpolation of two adjacent histogram buckets. + *

    + * The information from the most frequent terms map is also included, + * so if any of the frequent terms are lower or equal to the given term, their + * row counts will be also added. + *

    + * + * Example - Let's assume the following histogram: + *

    +     * bucket index  | term      |  cumulativePointCount  |  cumulativeRowCount
    +     * --------------+-----------+------------------------+----------------------
    +     * -1            |  null     |                     0  |                   0
    +     *  0            |  "2.0"    |                   100  |               10000
    +     *  1            |  "3.0"    |                   140  |               20000
    +     * 
    + * The results of calling this function are as follows: + *
    +     * interpolate("1.0") = Bucket("1.0", 0, 0)
    +     * interpolate("1.9") = Bucket("1.9", 0, 0)
    +     * interpolate("2.0") = Bucket("2.0", 100, 10000)
    +     * interpolate("2.5") = Bucket("2.5", 120, 15000)
    +     * interpolate("3.0") = Bucket("3.0", 140, 20000)
    +     * interpolate("4.0") = Bucket("4.0", 140, 20000)
    +     * 
    + */ + private @Nonnull Bucket interpolate(@Nonnull ByteComparable term) + { + int bucketIndex = indexOfBucketContaining(term); + Bucket bucket = getBucket(bucketIndex); + Bucket prevBucket = getBucket(bucketIndex - 1); + + if (prevBucket.term == null) + return new Bucket(term, bucket.cumulativePointCount, bucket.cumulativeRowCount); + + ByteComparable bucketMinTerm = prevBucket.term; + ByteComparable bucketMaxTerm = bucket.term; + + BigDecimal bucketMinValue = toBigDecimal(bucketMinTerm); + BigDecimal bucketMaxValue = toBigDecimal(bucketMaxTerm); + + // Estimate the fraction of the bucket on the left side of the term. + // We assume terms are distributed evenly. + BigDecimal termValue = toBigDecimal(term).min(bucketMaxValue).max(bucketMinValue); + double termDistance = termValue.subtract(bucketMinValue).doubleValue(); + double bucketSize = bucketMaxValue.subtract(bucketMinValue).doubleValue(); + + // Edge case: this can theoretically happen if our big decimals have insufficient resolution + // to distinguish terms. If we didn't return early in this case, + // the later interpolation logic would divide by 0. + if (bucketSize < Double.MIN_NORMAL) + return new Bucket(term, bucket.cumulativePointCount, bucket.cumulativeRowCount); + + double fraction = termDistance / bucketSize; + assert fraction >= 0.0 && fraction <= 1.0: "Invalid fraction value: " + fraction; + + // Total number of points and rows in this bucket: + long pointCount = bucket.cumulativePointCount - prevBucket.cumulativePointCount; + long rowCount = bucket.cumulativeRowCount - prevBucket.cumulativeRowCount; + + // We need those to include precise information about most frequent terms in the calculation. + // For most frequent terms we know the exact number of rows, so if we're matching any most frequent + // terms, those will be added at the end to the final row count estimate. + SortedMap bucketMft = mostFrequentTermsInRange(prevBucket.term, bucket.term); + SortedMap matchedMft = mostFrequentTermsInRange(prevBucket.term, term); + long matchedMftPointCount = matchedMft.size(); + long matchedMftRowCount = sumValues(matchedMft); + + // We likely don't have the information on all the points in the MFT table. + // Compute the average number of rows per point for all the non-MFT points, that is + // as if all the most frequent terms didn't exist. + // Then we'll multiply this value by the number of matching non-MFT points to get + // a reasonable row count estimate for the non-MFT points. + long nonMftPointCount = pointCount - bucketMft.size(); + long nonMftRowCount = rowCount - sumValues(bucketMft); + assert nonMftPointCount >= 0 : "point count cannot be negative"; + assert nonMftRowCount >= 0 : "row count cannot be negative"; + double rowsPerPoint = nonMftPointCount == 0 ? 0.0 : (double) nonMftRowCount / nonMftPointCount; + + // We assume points are distributed evenly; therefore we use total pointCount here: + double matchedPointCount = fraction * pointCount; + + double matchedNonMftRowCount = Math.max(0.0, matchedPointCount - matchedMftPointCount) * rowsPerPoint; + double matchedRowCount = matchedNonMftRowCount + matchedMftRowCount; + + long cumulativePointCount = prevBucket.cumulativePointCount + Math.round(matchedPointCount); + long cumulativeRowCount = prevBucket.cumulativeRowCount + Math.round(matchedRowCount); + return new Bucket(term, cumulativePointCount, cumulativeRowCount); + } + + + /** + * @see #toBigDecimal(ByteComparable, AbstractType, Version, ByteComparable.Version) + */ + private BigDecimal toBigDecimal(ByteComparable value) + { + return toBigDecimal(value, termType, indexVersion, byteComparableVersion); + } + + /** + * Converts the term value stored in the index to a big decimal value. Preserves order. + * If the type represents a number, the correspondence is linear. + * For non-number types, it reinterprets a bytecomparable serialization as a number, + * so it is not necessarily linear, but still preserves the order. + */ + public static BigDecimal toBigDecimal(ByteComparable value, + AbstractType termType, + Version indexVersion, + ByteComparable.Version byteComparableVersion) + { + if (termType instanceof NumberType) + { + // For numbers we decode the number back to the raw C* representation and then convert it to BigDecimal + var numberType = (NumberType) termType; + var saiEncoded = indexVersion.onDiskFormat().decodeFromTrie(value, termType); + var raw = TypeUtil.decode(saiEncoded, termType); + return DecimalType.instance.toBigDecimal(numberType.compose(raw)); + } + + // For non numbers we just reinterpret the bytecomparable representation as decimal of fixed width. + // Therefore, we don't need to decode anything. + byte[] fixedLengthBytes = Arrays.copyOf(ByteSourceInverse.readBytes(value.asComparableBytes(byteComparableVersion)), 20); + // Flip the first bit to get a correct order for negative values, + // because the first bit is interpreted by BigInteger as a sign bit, but bytecomparable interpret all byts as unsigned. + // By flipping it, we correctly get values starting with 0 bit smaller than the ones starting with 1. + fixedLengthBytes[0] ^= (byte) 0x80; + return new BigDecimal(new BigInteger(fixedLengthBytes)); + } + + /** + * Finds the bucket at given index. + * Saturates at edges, so never returns null. + * If index is negative, returns {@link this#MIN_BUCKET}. + * If index >= histogram.size(), returns the last (highest) bucket. + */ + private @Nonnull Bucket getBucket(int index) + { + if (index < 0 || histogram.isEmpty()) + return MIN_BUCKET; + if (index >= histogram.size()) + return histogram.get(histogram.size() - 1); + + return histogram.get(index); + } + + /** + * Returns the index of the highest bucket whose term value is equal or greater than the given value. + * If the value is lower than {@link this#minTerm}, returns -1. + * If the value is higher than {@link this#maxTerm}, returns {@code histogram.size()}. + */ + private int indexOfBucketContaining(@Nonnull ByteComparable b) + { + Bucket needle = new Bucket(b, 0, 0); + int index = Collections.binarySearch(histogram, needle, (b1, b2) -> ByteComparable.compare(b1.term, b2.term, byteComparableVersion)); + return (index >= -1) ? index : -(index + 1); + } + + /** + * Helper function to return the sum of values in a map + */ + private static long sumValues(Map map) + { + return map.values().stream().mapToLong(Long::longValue).sum(); + } + + /** + * Returns a subtree of {@code mostFrequentTerms} map with values between given range. + * A null term means a term before the lowest term. + * + * @param min exclusive lower bound + * @param max inclusive upper bound + */ + private SortedMap mostFrequentTermsInRange(@Nullable ByteComparable min, @Nullable ByteComparable max) + { + if (max == null) + return Collections.emptySortedMap(); + if (min == null) + return mostFrequentTerms.headMap(max); + + return mostFrequentTerms.subMap(min, false, max, true); + } + + public void write(IndexOutput out) throws IOException + { + out.writeInt(MAGIC); + + // Reserved for future use. + // Writing a few zeroes doesn't cost us much, and we could use those for flags or other important + // stuff in the future, so we can keep backwards compatibility between minor index versions + out.writeLong(0); + out.writeLong(0); + out.writeLong(0); + out.writeLong(0); + + out.writeString(indexVersion.toString()); + out.writeString(byteComparableVersion.toString()); + out.writeShort((short) histogram.size()); + for (Bucket b : histogram) + { + var term = ByteBuffer.wrap(b.term.asByteComparableArray(byteComparableVersion)); + out.writeBytes(term); + out.writeVLong(b.cumulativePointCount); + out.writeVLong(b.cumulativeRowCount); + } + out.writeShort((short) mostFrequentTerms.size()); + for (Map.Entry entry : mostFrequentTerms.entrySet()) + { + var term = ByteBuffer.wrap(entry.getKey().asByteComparableArray(byteComparableVersion)); + out.writeBytes(term); + out.writeVLong(entry.getValue()); + } + } + + public static TermsDistribution read(IndexInput input, AbstractType termType) throws IOException + { + long magic = input.readInt(); + if (magic != MAGIC) + throw new IOException(String.format( + "Invalid TermsDistribution header. Expected MAGIC = 0x%08x but read 0x%08x instead", MAGIC, magic)); + + input.readLong(); // reserved + input.readLong(); // reserved + input.readLong(); // reserved + input.readLong(); // reserved + + Version indexVersion = decodeIndexVersion(input.readString()); + ByteComparable.Version bcVersion = decodeByteComparableVersion(input.readString()); + + int bucketCount = input.readShort(); + if (bucketCount < 0) + throw new IOException("Number of buckets cannot be negative: " + bucketCount); + + List buckets = new ArrayList<>(bucketCount); + for (int i = 0; i < bucketCount; i++) + { + ByteBuffer termBytes = input.readBytes(); + ByteComparable term = ByteComparable.preencoded(bcVersion, termBytes); + long cumulativePointCount = input.readVLong(); + long cumulativeRowCount = input.readVLong(); + buckets.add(new Bucket(term, cumulativePointCount, cumulativeRowCount)); + } + + int mostFrequentTermsCount = input.readShort(); + if (mostFrequentTermsCount < 0) + throw new IOException("Number of most frequent terms cannot be negative: " + mostFrequentTermsCount); + + NavigableMap mostFrequentTerms = new TreeMap<>((b1, b2) -> ByteComparable.compare(b1, b2, bcVersion)); + for (int i = 0; i < mostFrequentTermsCount; i++) + { + ByteBuffer termBytes = input.readBytes(); + ByteComparable term = ByteComparable.preencoded(bcVersion, termBytes); + long rowCount = input.readVLong(); + mostFrequentTerms.put(term, rowCount); + } + + return new TermsDistribution(termType, buckets, mostFrequentTerms, indexVersion, bcVersion); + } + + private static ByteComparable.Version decodeByteComparableVersion(String versionStr) throws IOException + { + try + { + return ByteComparable.Version.valueOf(versionStr); + } + catch (IllegalArgumentException e) + { + throw new IOException("Unrecognized ByteComparable version " + versionStr); + } + } + + private static Version decodeIndexVersion(String versionStr) throws IOException + { + try + { + return Version.parse(versionStr); + } + catch (IllegalArgumentException e) + { + throw new IOException("Unrecognized index version " + versionStr); + } + } + + + @NotThreadSafe + public static class Builder + { + final AbstractType termType; + final Version version; + final ByteComparable.Version byteComparableVersion; + final int histogramSize; + final int mostFrequentTermsTableSize; + + long maxRowsPerBucket; + + List buckets = new ArrayList<>(); + PriorityQueue mostFrequentTerms = new PriorityQueue<>(); + + ByteComparable lastTerm; + long cumulativePointCount; + long cumulativeRowCount; + + public Builder(AbstractType termType, + ByteComparable.Version byteComparableVersion, + int histogramSize, + int mostFrequentTermsTableSize, + Version version) + { + this.termType = termType; + this.byteComparableVersion = byteComparableVersion; + this.histogramSize = histogramSize; + this.mostFrequentTermsTableSize = mostFrequentTermsTableSize; + this.version = version; + + // Let's start with adding buckets for every point. + // This will be corrected to a higher value once the histogram gets too large and we'll do shrinking. + this.maxRowsPerBucket = 1; + } + + /** + * Adds a point to the histogram. + * Terms must be added in ascending order of term values matching the order of the index. + * Terms must be encoded as byte-comparable, because they are compared lexicographically by unsigned bytes. + * If the order is not preserved, the behavior is undefined. + * + * @param term encoded term + */ + public void add(ByteComparable term, long rowCount) + { + mostFrequentTerms.add(new Point(term, rowCount)); + if (mostFrequentTerms.size() > mostFrequentTermsTableSize) + mostFrequentTerms.poll(); + + cumulativePointCount += 1; + cumulativeRowCount += rowCount; + lastTerm = term; + + if (buckets.isEmpty() || cumulativeRowCount > buckets.get(buckets.size() - 1).cumulativeRowCount + maxRowsPerBucket) + { + buckets.add(new Bucket(lastTerm, cumulativePointCount, cumulativeRowCount)); + lastTerm = null; + + if (buckets.size() > histogramSize * 2) + shrink(); + } + } + + public TermsDistribution build() + { + if (lastTerm != null) + buckets.add(new Bucket(lastTerm, cumulativePointCount, cumulativeRowCount)); + + shrink(); + + var mft = new TreeMap((b1, b2) -> ByteComparable.compare(b1, b2, byteComparableVersion)); + for (Point point : mostFrequentTerms) { + mft.put(point.term, point.rowCount); + } + + return new TermsDistribution(termType, buckets, mft, version, byteComparableVersion); + } + + /** + * Shrinks the histogram to fit in the histogramSize limit, by removing some points. + * Tries to keep uniform granulatiry in terms of the number of rows. + * Runs in O(n) time. + * Needed because in some cases we don't know the number of points added to the histogram in advance, + * so we have to build it incrementally. + */ + private void shrink() + { + if (buckets.size() < histogramSize) + return; + + maxRowsPerBucket = buckets.get(buckets.size() - 1).cumulativeRowCount / histogramSize; + int targetIndex = 1; + for (int candidateIndex = 1; candidateIndex < buckets.size(); candidateIndex++) + { + Bucket last = buckets.get(targetIndex - 1); + Bucket candidate = buckets.get(candidateIndex); + if (candidate.cumulativeRowCount - last.cumulativeRowCount > maxRowsPerBucket || candidateIndex == buckets.size() - 1) + { + buckets.set(targetIndex, candidate); + targetIndex++; + } + } + buckets.subList(targetIndex, buckets.size()).clear(); + } + } + + /** + * A histogram bucket - keeps the cumulative point and row counts for all the terms smaller or equal given term. + */ + @ThreadSafe + @Immutable + static class Bucket + { + final ByteComparable term; + final long cumulativePointCount; + final long cumulativeRowCount; + + Bucket(ByteComparable term, long cumulativePointCount, long cumulativeRowCount) + { + this.term = term; + this.cumulativePointCount = cumulativePointCount; + this.cumulativeRowCount = cumulativeRowCount; + } + } + + /** + * A helper class for building the most frequent terms queue. + * Associates the term with the row count and provides a natural ordering by row count. + */ + static class Point implements Comparable + { + final ByteComparable term; + final long rowCount; + + Point(ByteComparable term, long rowCount) + { + this.term = term; + this.rowCount = rowCount; + } + + @Override + public int compareTo(Point o) + { + return Long.compare(rowCount, o.rowCount); + } + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v6/V6OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v6/V6OnDiskFormat.java new file mode 100644 index 000000000000..a0651bc078ba --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v6/V6OnDiskFormat.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v6; + +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.v5.V5OnDiskFormat; + +public class V6OnDiskFormat extends V5OnDiskFormat +{ + public static final V6OnDiskFormat instance = new V6OnDiskFormat(); + + private static final IndexFeatureSet v6IndexFeatureSet = new IndexFeatureSet() + { + @Override + public boolean isRowAware() + { + return true; + } + + @Override + public boolean hasTermsHistogram() + { + return true; + } + }; + + @Override + public IndexFeatureSet indexFeatureSet() + { + return v6IndexFeatureSet; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/v7/V7OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v7/V7OnDiskFormat.java new file mode 100644 index 000000000000..99e1675e077f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v7/V7OnDiskFormat.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v7; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.util.EnumSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v6.V6OnDiskFormat; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.Throwables; +import org.apache.lucene.store.IndexInput; + +public class V7OnDiskFormat extends V6OnDiskFormat +{ + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final V7OnDiskFormat instance = new V7OnDiskFormat(); + + private static final Set LITERAL_COMPONENTS = EnumSet.of(IndexComponentType.COLUMN_COMPLETION_MARKER, + IndexComponentType.META, + IndexComponentType.TERMS_DATA, + IndexComponentType.POSTING_LISTS, + IndexComponentType.DOC_LENGTHS); + + @Override + public Set perIndexComponentTypes(AbstractType validator) + { + // Vector types are technically "literal" (frozen) but should not have DOC_LENGTHS + // which is only for text search BM25 functionality + if (validator.isVector()) + return super.perIndexComponentTypes(validator); + if (TypeUtil.isLiteral(validator)) + return LITERAL_COMPONENTS; + return super.perIndexComponentTypes(validator); + } + + @Override + public int jvectorFileFormatVersion() + { + // Before version EC, we write JVector format 2. Version EB introduced the ability for jvector to read format 4, + // so we can safely start writing it for versions EC (V7) and later while maintaining proper backward + // compatibility. + return 4; + } + + @Override + public void validateIndexComponent(IndexComponent.ForRead component, boolean checksum) + { + if (component.isCompletionMarker()) + return; + + IndexContext context = component.parent().context(); + if (context != null && context.isVector() && component.parent().version().onOrAfter(Version.EC)) + { + try (IndexInput input = component.openInput()) + { + // We can't validate TERMS_DATA with checksum because the checksum was computed incorrectly through + // V7. See https://github.com/riptano/cndb/issues/14656. We can still call the basic validate method + // which does not check the checksum. (The issue is in the way the checksum was computed. It didn't + // include the header/footer bytes, and for multi-segment builds, it didn't include the bytes from + // all previous segments, which is the design for all index components to date.) + if (!checksum || component.componentType() == IndexComponentType.TERMS_DATA) + SAICodecUtils.validate(input, getExpectedEarliestVersion(context, component.componentType())); + else + SAICodecUtils.validateChecksum(input, getExpectedEarliestVersion(context, component.componentType())); + } + catch (Throwable e) + { + logger.warn(component.parent().logMessage("{} failed for index component {} on SSTable {}"), + (checksum ? "Checksum validation" : "Validation"), + component, + component.parent().descriptor(), + e); + if (e instanceof IOException) + throw new UncheckedIOException((IOException) e); + if (e.getCause() instanceof IOException) + throw new UncheckedIOException((IOException) e.getCause()); + throw Throwables.unchecked(e); + } + } + else + { + super.validateIndexComponent(component, checksum); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v8/V8OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v8/V8OnDiskFormat.java new file mode 100644 index 000000000000..27243244d82e --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v8/V8OnDiskFormat.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.v8; + +import org.apache.cassandra.index.sai.disk.v7.V7OnDiskFormat; + +public class V8OnDiskFormat extends V7OnDiskFormat +{ + public static final V8OnDiskFormat instance = new V8OnDiskFormat(); + + @Override + public int jvectorFileFormatVersion() + { + return 6; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/AbstractMemtableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/vector/AbstractMemtableIndex.java new file mode 100644 index 000000000000..ce9e71f34306 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/AbstractMemtableIndex.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.memory.MemtableIndex; + +public abstract class AbstractMemtableIndex implements MemtableIndex +{ + protected final IndexContext indexContext; + protected final Memtable memtable; + protected final Version version; + + private final int flushThresholdMaxRows; + + public AbstractMemtableIndex(IndexContext indexContext, Memtable memtable) + { + this.indexContext = indexContext; + this.memtable = memtable; + this.flushThresholdMaxRows = indexContext.isVector() ? CassandraRelevantProperties.SAI_VECTOR_FLUSH_THRESHOLD_MAX_ROWS.getInt() + : CassandraRelevantProperties.SAI_NON_VECTOR_FLUSH_THRESHOLD_MAX_ROWS.getInt(); + this.version = indexContext.version(); + } + + /** + * Called when index is updated + */ + protected void onIndexUpdated() + { + if (flushThresholdMaxRows > 0 && getRowCount() >= flushThresholdMaxRows) + memtable.signalFlushRequired(ColumnFamilyStore.FlushReason.INDEX_MEMTABLE_LIMIT, true); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/AutoResumingNodeScoreIterator.java b/src/java/org/apache/cassandra/index/sai/disk/vector/AutoResumingNodeScoreIterator.java new file mode 100644 index 000000000000..ab586b0ce025 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/AutoResumingNodeScoreIterator.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.function.IntConsumer; + +import io.github.jbellis.jvector.graph.GraphSearcher; +import io.github.jbellis.jvector.graph.SearchResult; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.AbstractIterator; + +import static java.lang.Math.max; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +/** + * An iterator over {@link SearchResult.NodeScore} backed by a {@link SearchResult} that resumes search + * when the backing {@link SearchResult} is exhausted. + */ +public class AutoResumingNodeScoreIterator extends AbstractIterator +{ + private final GraphSearcher searcher; + private final GraphSearcherAccessManager accessManager; + private final int limit; + private final int rerankK; + private final boolean inMemory; + private final String source; + private final QueryContext context; + private final ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics; + private final IntConsumer nodesVisitedConsumer; + private Iterator nodeScores; + private int cumulativeNodesVisited; + + /** + * Create a new {@link AutoResumingNodeScoreIterator} that iterates over the provided {@link SearchResult}. + * If the {@link SearchResult} is consumed, it retrieves the next {@link SearchResult} until the search returns + * no more results. + * @param searcher the {@link GraphSearcher} to use to resume search. + * @param result the first {@link SearchResult} to iterate over + * @param context the {@link QueryContext} to use to record metrics + * @param columnQueryMetrics object to record metrics + * @param nodesVisitedConsumer a consumer that accepts the total number of nodes visited + * @param limit the limit to pass to the {@link GraphSearcher} when resuming search + * @param rerankK the rerankK to pass to the {@link GraphSearcher} when resuming search + * @param inMemory whether the graph is in memory or on disk (used for trace logging) + * @param source the source of the search (used for trace logging) + */ + public AutoResumingNodeScoreIterator(GraphSearcher searcher, + GraphSearcherAccessManager accessManager, + SearchResult result, + QueryContext context, + ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics, + IntConsumer nodesVisitedConsumer, + int limit, + int rerankK, + boolean inMemory, + String source) + { + this.searcher = searcher; + this.accessManager = accessManager; + this.nodeScores = Arrays.stream(result.getNodes()).iterator(); + this.context = context; + this.columnQueryMetrics = columnQueryMetrics; + this.cumulativeNodesVisited = 0; + this.nodesVisitedConsumer = nodesVisitedConsumer; + this.limit = max(1, limit / 2); // we shouldn't need as many results on resume + this.rerankK = rerankK; + this.inMemory = inMemory; + this.source = source; + } + + @Override + protected SearchResult.NodeScore computeNext() + { + if (nodeScores.hasNext()) + return nodeScores.next(); + + long start = nanoTime(); + + // Search deeper into the graph + var nextResult = searcher.resume(limit, rerankK); + + // Record metrics + long elapsed = nanoTime() - start; + columnQueryMetrics.onSearchResult(nextResult, elapsed, true); + context.addAnnGraphSearchLatency(elapsed); + cumulativeNodesVisited += nextResult.getVisitedCount(); + + if (Tracing.isTracing()) + { + String msg = inMemory ? "Memory based ANN resume for {}/{} visited {} nodes, reranked {} to return {} results from {}" + : "Disk based ANN resume for {}/{} visited {} nodes, reranked {} to return {} results from {}"; + Tracing.trace(msg, limit, rerankK, nextResult.getVisitedCount(), nextResult.getRerankedCount(), nextResult.getNodes().length, source); + } + + // If the next result is empty, we are done searching. + nodeScores = Arrays.stream(nextResult.getNodes()).iterator(); + return nodeScores.hasNext() ? nodeScores.next() : endOfData(); + } + + @Override + public void close() + { + nodesVisitedConsumer.accept(cumulativeNodesVisited); + accessManager.release(); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/BitsUtil.java b/src/java/org/apache/cassandra/index/sai/disk/vector/BitsUtil.java similarity index 78% rename from src/java/org/apache/cassandra/index/sai/disk/v1/vector/BitsUtil.java rename to src/java/org/apache/cassandra/index/sai/disk/vector/BitsUtil.java index 603efeb21abf..9564989dbfc1 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/BitsUtil.java +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/BitsUtil.java @@ -16,13 +16,12 @@ * limitations under the License. */ -package org.apache.cassandra.index.sai.disk.v1.vector; +package org.apache.cassandra.index.sai.disk.vector; import java.util.Set; -import org.cliffc.high_scale_lib.NonBlockingHashMapLong; - import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.util.DenseIntMap; public class BitsUtil { @@ -30,12 +29,12 @@ public static Bits bitsIgnoringDeleted(Bits toAccept, Set deletedOrdina { return deletedOrdinals.isEmpty() ? toAccept - : toAccept == null ? new NoDeletedBits(deletedOrdinals) : new NoDeletedIntersectingBits(toAccept, deletedOrdinals); + : toAccept == Bits.ALL ? new NoDeletedBits(deletedOrdinals) : new NoDeletedIntersectingBits(toAccept, deletedOrdinals); } - public static Bits bitsIgnoringDeleted(Bits toAccept, NonBlockingHashMapLong> postings) + public static Bits bitsIgnoringDeleted(Bits toAccept, DenseIntMap> postings) { - return toAccept == null ? new NoDeletedPostings<>(postings) : new NoDeletedIntersectingPostings<>(toAccept, postings); + return toAccept == Bits.ALL ? new NoDeletedPostings(postings) : new NoDeletedIntersectingPostings(toAccept, postings); } private static abstract class BitsWithoutLength implements Bits, org.apache.lucene.util.Bits @@ -84,9 +83,9 @@ public boolean get(int i) private static class NoDeletedPostings extends BitsWithoutLength { - private final NonBlockingHashMapLong> postings; + private final DenseIntMap> postings; - public NoDeletedPostings(NonBlockingHashMapLong> postings) + public NoDeletedPostings(DenseIntMap> postings) { this.postings = postings; } @@ -103,9 +102,9 @@ public boolean get(int i) private static class NoDeletedIntersectingPostings extends BitsWithoutLength { private final Bits toAccept; - private final NonBlockingHashMapLong> postings; + private final DenseIntMap> postings; - public NoDeletedIntersectingPostings(Bits toAccept, NonBlockingHashMapLong> postings) + public NoDeletedIntersectingPostings(Bits toAccept, DenseIntMap> postings) { this.toAccept = toAccept; this.postings = postings; diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/BruteForceRowIdIterator.java b/src/java/org/apache/cassandra/index/sai/disk/vector/BruteForceRowIdIterator.java new file mode 100644 index 000000000000..131fbed2a6f7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/BruteForceRowIdIterator.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import io.github.jbellis.jvector.graph.NodeQueue; +import io.github.jbellis.jvector.util.BoundedLongHeap; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.index.sai.utils.SegmentRowIdOrdinalPairs; +import org.apache.cassandra.index.sai.utils.RowIdWithMeta; +import org.apache.cassandra.index.sai.utils.RowIdWithScore; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.AbstractIterator; + + +/** + * An iterator over {@link RowIdWithMeta} that lazily consumes from a {@link NodeQueue} of approximate scores. + *

    + * The idea is that we maintain the same level of accuracy as we would get from a graph search, by re-ranking the top + * `k` best approximate scores at a time with the full resolution vectors to return the top `limit`. + *

    + * For example, suppose that limit=3 and k=5 and we have ten elements. After our first re-ranking batch, we have + * ABDEF????? + * We will return A, B, and D; if more elements are requested, we will re-rank another 5 (so three more, including + * the two remaining from the first batch). Here we uncover C, G, and H, and order them appropriately: + * CEFGH?? + * This illustrates that, also like a graph search, we only guarantee ordering of results within a re-ranking batch, + * not globally. + *

    + * Note that we deliberately do not fetch new items from the approximate list until the first batch of `limit`-many + * is consumed. We do this because we expect that most often the first limit-many will pass the final verification + * and only query more if some didn't (e.g. because the vector was deleted in a newer sstable). + *

    + * As an implementation detail, we use a heap to maintain state rather than a List and sorting. + */ +public class BruteForceRowIdIterator extends AbstractIterator +{ + // We use two binary heaps (NodeQueue) because we do not need an eager ordering of + // these results. Depending on how many sstables the query hits and the relative scores of vectors from those + // sstables, we may not need to return more than the first handful of scores. + // Heap with compressed vector scores + private final NodeQueue approximateScoreQueue; + private final SegmentRowIdOrdinalPairs segmentOrdinalPairs; + // Use the jvector NodeQueue to avoid unnecessary object allocations + private final NodeQueue exactScoreQueue; + private final CloseableReranker reranker; + private final int topK; + private final int limit; + private final boolean isScoreApproximate; + private final ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics; + private int rerankedCount; + + /** + * @param approximateScoreQueue A heap of indexes ordered by their approximate similarity scores + * @param segmentOrdinalPairs A mapping from the index in the approximateScoreQueue to the node's rowId and ordinal + * @param reranker A function that takes a graph ordinal and returns the exact similarity score + * @param limit The query limit + * @param topK The number of vectors to resolve and score before returning results + * @param isScoreApproximate Whether the scores are approximate or exact + * @param columnQueryMetrics object to record metrics + */ + public BruteForceRowIdIterator(NodeQueue approximateScoreQueue, + SegmentRowIdOrdinalPairs segmentOrdinalPairs, + CloseableReranker reranker, + int limit, + int topK, + boolean isScoreApproximate, + ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics) + { + this.approximateScoreQueue = approximateScoreQueue; + this.segmentOrdinalPairs = segmentOrdinalPairs; + this.exactScoreQueue = new NodeQueue(new BoundedLongHeap(topK), NodeQueue.Order.MAX_HEAP); + this.reranker = reranker; + assert topK >= limit : "topK must be greater than or equal to limit. Found: " + topK + " < " + limit; + this.limit = limit; + this.topK = topK; + this.isScoreApproximate = isScoreApproximate; + this.columnQueryMetrics = columnQueryMetrics; + this.rerankedCount = topK; // placeholder to kick off computeNext + } + + @Override + protected RowIdWithScore computeNext() { + int consumed = rerankedCount - exactScoreQueue.size(); + if (consumed >= limit) { + int exactComparisons = 0; + // Refill the exactScoreQueue until it reaches topK exact scores, or the approximate score queue is empty + while (approximateScoreQueue.size() > 0 && exactScoreQueue.size() < topK) { + int segmentOrdinalIndex = approximateScoreQueue.pop(); + int rowId = segmentOrdinalPairs.getSegmentRowId(segmentOrdinalIndex); + int ordinal = segmentOrdinalPairs.getOrdinal(segmentOrdinalIndex); + float score = reranker.similarityTo(ordinal); + exactComparisons++; + exactScoreQueue.push(rowId, score); + } + columnQueryMetrics.onBruteForceNodesReranked(exactComparisons); + rerankedCount = exactScoreQueue.size(); + } + if (exactScoreQueue.size() == 0) + return endOfData(); + + float score = exactScoreQueue.topScore(); + int rowId = exactScoreQueue.pop(); + return new RowIdWithScore(rowId, score, isScoreApproximate); + } + + @Override + public void close() + { + FileUtils.closeQuietly(reranker); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java new file mode 100644 index 000000000000..d651324f47c2 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.function.IntConsumer; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.graph.ImmutableGraphIndex; +import io.github.jbellis.jvector.graph.GraphSearcher; +import io.github.jbellis.jvector.graph.disk.feature.FeatureId; +import io.github.jbellis.jvector.graph.disk.OnDiskGraphIndex; +import io.github.jbellis.jvector.graph.similarity.DefaultSearchScoreProvider; +import io.github.jbellis.jvector.graph.similarity.SearchScoreProvider; +import io.github.jbellis.jvector.quantization.CompressedVectors; +import io.github.jbellis.jvector.quantization.PQVectors; +import io.github.jbellis.jvector.quantization.ProductQuantization; +import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.util.ExplicitThreadLocal; +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.v1.PerIndexFiles; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v3.V3OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter.Structure; +import org.apache.cassandra.index.sai.disk.vector.CassandraOnHeapGraph.PQVersion; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.index.sai.utils.RowIdWithScore; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.CloseableIterator; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + + +public class CassandraDiskAnn +{ + private static final Logger logger = LoggerFactory.getLogger(CassandraDiskAnn.class.getName()); + + public static final int PQ_MAGIC = 0xB011A61C; // PQ_MAGIC, with a lot of liberties taken + protected final PerIndexFiles indexFiles; + private final ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics; + protected final SegmentMetadata.ComponentMetadataMap componentMetadatas; + + private final SSTableId source; + private final FileHandle graphHandle; + private final OnDiskOrdinalsMap ordinalsMap; + private final Set features; + private final ImmutableGraphIndex graph; + private final boolean usesNVQ; + private final VectorSimilarityFunction similarityFunction; + @Nullable + private final CompressedVectors compressedVectors; + @Nullable + private final ProductQuantization pq; + private final VectorCompression compression; + final boolean pqUnitVectors; + + private final ExplicitThreadLocal searchers; + + public CassandraDiskAnn(SSTableContext sstableContext, SegmentMetadata segmentMetadata, PerIndexFiles indexFiles, IndexContext context, OrdinalsMapFactory omFactory) throws IOException + { + this.source = sstableContext.sstable().getId(); + this.componentMetadatas = segmentMetadata.componentMetadatas; + this.indexFiles = indexFiles; + this.columnQueryMetrics = (ColumnQueryMetrics.VectorIndexMetrics) context.getColumnQueryMetrics(); + + similarityFunction = context.getIndexWriterConfig().getSimilarityFunction(); + + SegmentMetadata.ComponentMetadata termsMetadata = this.componentMetadatas.get(IndexComponentType.TERMS_DATA); + graphHandle = indexFiles.termsData(); + var rawGraph = OnDiskGraphIndex.load(graphHandle::createReader, termsMetadata.offset, false); + features = rawGraph.getFeatureSet(); + graph = rawGraph; + usesNVQ = features.contains(FeatureId.NVQ_VECTORS); + + // This is helpful for understanding what features are enabled for a given index. Features is an EnumSet + // so the toString() method will print all the enabled features. + logger.debug("Opened graph for {} for sstable row id offset {} with {} features", source, segmentMetadata.segmentRowIdOffset, features); + + long pqSegmentOffset = this.componentMetadatas.get(IndexComponentType.PQ).offset; + try (var pqFile = indexFiles.pq(); + var reader = pqFile.createReader()) + { + reader.seek(pqSegmentOffset); + var version = PQVersion.V0; + if (reader.readInt() == PQ_MAGIC) + { + version = PQVersion.values()[reader.readInt()]; + assert PQVersion.V1.compareTo(version) >= 0 : String.format("Old PQ version %s written with PQ_MAGIC!?", version); + pqUnitVectors = reader.readBoolean(); + } + else + { + pqUnitVectors = true; + reader.seek(pqSegmentOffset); + } + + VectorCompression.CompressionType compressionType = VectorCompression.CompressionType.values()[reader.readByte()]; + if (features.contains(FeatureId.FUSED_PQ)) + { + assert compressionType == VectorCompression.CompressionType.PRODUCT_QUANTIZATION; + compressedVectors = null; + // don't load full PQVectors, all we need is the metadata from the PQ at the start + pq = ProductQuantization.load(reader); + compression = new VectorCompression(VectorCompression.CompressionType.PRODUCT_QUANTIZATION, + graph.getDimension() * Float.BYTES, + pq.compressedVectorSize()); + } + else + { + if (compressionType == VectorCompression.CompressionType.PRODUCT_QUANTIZATION) + { + compressedVectors = PQVectors.load(reader, reader.getFilePointer()); + pq = ((PQVectors) compressedVectors).getCompressor(); + compression = new VectorCompression(compressionType, + compressedVectors.getOriginalSize(), + compressedVectors.getCompressedSize()); + } + else + { + compressedVectors = null; + pq = null; + compression = VectorCompression.NO_COMPRESSION; + } + } + } + + SegmentMetadata.ComponentMetadata postingListsMetadata = this.componentMetadatas.get(IndexComponentType.POSTING_LISTS); + ordinalsMap = omFactory.create(indexFiles.postingLists(), postingListsMetadata.offset, postingListsMetadata.length); + if (ordinalsMap.getStructure() == Structure.ZERO_OR_ONE_TO_MANY) + logger.warn("Index {} has structure ZERO_OR_ONE_TO_MANY, which requires on reading the on disk row id" + + " to ordinal mapping for each search. This will be slower.", source); + + searchers = ExplicitThreadLocal.withInitial(() -> new GraphSearcherAccessManager(new GraphSearcher(graph))); + + // Record metrics for this graph + columnQueryMetrics.onGraphLoaded(compressedVectors == null ? 0 : compressedVectors.ramBytesUsed(), + ordinalsMap.cachedBytesUsed(), + graph.size(0)); + } + + public Structure getPostingsStructure() + { + return ordinalsMap.getStructure(); + } + + @FunctionalInterface + public interface OrdinalsMapFactory { + OnDiskOrdinalsMap create(FileHandle handle, long offset, long length); + } + + public ProductQuantization getPQ() + { + assert compression.type == VectorCompression.CompressionType.PRODUCT_QUANTIZATION; + assert pq != null; + return pq; + } + + public long ramBytesUsed() + { + return graph.ramBytesUsed() + compressedVectorBytes(); + } + + private long compressedVectorBytes() + { + // compressedVectors counts the pq internally, so only count pq if compressedVectors is null. + return compressedVectors == null + ? pq == null ? 0 : pq.ramBytesUsed() + : compressedVectors.ramBytesUsed(); + } + + public int size() + { + // The base layer of the graph has all nodes. + return graph.size(0); + } + + /** + * @param queryVector the query vector + * @param limit the number of results to look for in the index (>= limit) + * @param rerankK the number of quantized results to look for in the index (>= limit or <= 0). If rerankK is + * non-positive, then we will use limit as the value and will skip reranking. Rerankless search + * only applies when the graph has compressed vectors. + * @param threshold the minimum similarity score to accept + * @param usePruning whether to use pruning to speed up the search + * @param acceptBits a Bits indicating which row IDs are acceptable, or null if no constraints + * @param context unused (vestige from HNSW, retained in signature to allow calling both easily) + * @param nodesVisitedConsumer a consumer that will be called with the number of nodes visited during the search + * @return Iterator of Row IDs associated with the vectors near the query. If a threshold is specified, only vectors + * with a similarity score >= threshold will be returned. + */ + public CloseableIterator search(VectorFloat queryVector, + int limit, + int rerankK, + float threshold, + boolean usePruning, + Bits acceptBits, + QueryContext context, + IntConsumer nodesVisitedConsumer) + { + VectorValidation.validateIndexable(queryVector, similarityFunction); + boolean isRerankless = rerankK <= 0; + if (isRerankless) + rerankK = limit; + + var graphAccessManager = searchers.get(); + var searcher = graphAccessManager.get(); + // This searcher is reused across searches. We set here every time to ensure it is configured correctly + // for this search. Note that resume search in AutoResumingNodeScoreIterator will continue to use this setting. + searcher.usePruning(usePruning); + try + { + var view = (ImmutableGraphIndex.ScoringView) searcher.getView(); + SearchScoreProvider ssp; + if (features.contains(FeatureId.FUSED_PQ)) + { + var asf = view.approximateScoreFunctionFor(queryVector, similarityFunction); + var rr = isRerankless ? null : view.rerankerFor(queryVector, similarityFunction); + ssp = new DefaultSearchScoreProvider(asf, rr); + } + else if (compressedVectors == null) + { + // no compression, so we ignore isRerankless (except for setting rerankK to limit) + ssp = new DefaultSearchScoreProvider(view.rerankerFor(queryVector, similarityFunction)); + } + else + { + // unit vectors defined with dot product should switch to cosine similarity for compressed + // comparisons, since the compression does not maintain unit length + var sf = pqUnitVectors && similarityFunction == VectorSimilarityFunction.DOT_PRODUCT + ? VectorSimilarityFunction.COSINE + : similarityFunction; + var asf = compressedVectors.precomputedScoreFunctionFor(queryVector, sf); + var rr = isRerankless ? null : view.rerankerFor(queryVector, similarityFunction); + ssp = new DefaultSearchScoreProvider(asf, rr); + } + long start = nanoTime(); + var result = searcher.search(ssp, limit, rerankK, threshold, context.getAnnRerankFloor(), ordinalsMap.ignoringDeleted(acceptBits)); + long elapsed = nanoTime() - start; + if (V3OnDiskFormat.ENABLE_RERANK_FLOOR) + context.updateAnnRerankFloor(result.getWorstApproximateScoreInTopK()); + Tracing.trace("DiskANN search for {}/{} rerankless={}, usePruning={} visited {} nodes, reranked {} to return {} results from {}", + limit, rerankK, isRerankless, usePruning, result.getVisitedCount(), result.getRerankedCount(), result.getNodes().length, source); + columnQueryMetrics.onSearchResult(result, elapsed, false); + context.addAnnGraphSearchLatency(elapsed); + boolean isScoreApproximate = usesNVQ || isRerankless; + if (threshold > 0) + { + // Threshold based searches are comprehensive and do not need to resume the search. + graphAccessManager.release(); + nodesVisitedConsumer.accept(result.getVisitedCount()); + var nodeScores = CloseableIterator.wrap(Arrays.stream(result.getNodes()).iterator()); + return new NodeScoreToRowIdWithScoreIterator(nodeScores, ordinalsMap.getRowIdsView(), isScoreApproximate); + } + else + { + var nodeScores = new AutoResumingNodeScoreIterator(searcher, graphAccessManager, result, context, columnQueryMetrics, nodesVisitedConsumer, limit, rerankK, false, source.toString()); + return new NodeScoreToRowIdWithScoreIterator(nodeScores, ordinalsMap.getRowIdsView(), isScoreApproximate); + } + } + catch (Throwable t) + { + // If we don't release it, we'll never be able to aquire it, so catch and rethrow Throwable. + graphAccessManager.forceRelease(); + throw t; + } + } + + public VectorCompression getCompression() + { + return compression; + } + + public CompressedVectors getCompressedVectors() + { + return compressedVectors; + } + + public void close() throws IOException + { + FileUtils.close(ordinalsMap, searchers, graph, graphHandle); + columnQueryMetrics.onGraphClosed(compressedVectors == null ? 0 : compressedVectors.ramBytesUsed(), + ordinalsMap.cachedBytesUsed(), + graph.size(0)); + } + + public OrdinalsView getOrdinalsView() + { + return ordinalsMap.getOrdinalsView(); + } + + public ImmutableGraphIndex.ScoringView getView() + { + return (ImmutableGraphIndex.ScoringView) graph.getView(); + } + + public boolean usesNVQ() + { + return usesNVQ; + } + + public boolean containsUnitVectors() + { + return pqUnitVectors; + } + + public int maxDegree() + { + return graph.maxDegree(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java new file mode 100644 index 000000000000..00019e73a7b7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java @@ -0,0 +1,790 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import java.io.DataOutput; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.IntUnaryOperator; +import java.util.function.ToIntFunction; + +import com.google.common.annotations.VisibleForTesting; +import org.cliffc.high_scale_lib.NonBlockingHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.graph.GraphIndexBuilder; +import io.github.jbellis.jvector.graph.GraphSearcher; +import io.github.jbellis.jvector.graph.ImmutableGraphIndex; +import io.github.jbellis.jvector.graph.RandomAccessVectorValues; +import io.github.jbellis.jvector.graph.SearchResult; +import io.github.jbellis.jvector.graph.disk.OnDiskGraphIndexWriter; +import io.github.jbellis.jvector.graph.disk.OrdinalMapper; +import io.github.jbellis.jvector.graph.disk.feature.Feature; +import io.github.jbellis.jvector.graph.disk.feature.FeatureId; +import io.github.jbellis.jvector.graph.disk.feature.FusedPQ; +import io.github.jbellis.jvector.graph.disk.feature.InlineVectors; +import io.github.jbellis.jvector.graph.disk.feature.NVQ; +import io.github.jbellis.jvector.graph.similarity.DefaultSearchScoreProvider; +import io.github.jbellis.jvector.quantization.CompressedVectors; +import io.github.jbellis.jvector.quantization.PQVectors; +import io.github.jbellis.jvector.quantization.NVQuantization; +import io.github.jbellis.jvector.quantization.ProductQuantization; +import io.github.jbellis.jvector.quantization.VectorCompressor; +import io.github.jbellis.jvector.util.Accountable; +import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.util.DenseIntMap; +import io.github.jbellis.jvector.util.RamUsageEstimator; +import io.github.jbellis.jvector.vector.ArrayVectorFloat; +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.VectorUtil; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.agrona.collections.IntHashSet; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.db.marshal.VectorType; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.Segment; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v2.V2VectorIndexSearcher; +import org.apache.cassandra.index.sai.disk.v2.V2VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.v5.V5OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter.Structure; +import org.apache.cassandra.index.sai.disk.vector.VectorCompression.CompressionType; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.SequentialWriter; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.lucene.util.StringHelper; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import static org.apache.cassandra.index.sai.disk.vector.JVectorVersionUtil.NUM_SUB_VECTORS; + +public class CassandraOnHeapGraph implements Accountable +{ + // Cassandra's PQ features, independent of JVector's + public enum PQVersion { + V0, // initial version + V1, // includes unit vector calculation + } + + /** minimum number of rows to perform PQ codebook generation */ + public static final int MIN_PQ_ROWS = 1024; + + private static final Logger logger = LoggerFactory.getLogger(CassandraOnHeapGraph.class); + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + // We use the metable reference for easier tracing. + private final String source; + private final ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics; + private final ConcurrentVectorValues vectorValues; + private final GraphIndexBuilder builder; + private final VectorType.VectorSerializer serializer; + private final VectorSimilarityFunction similarityFunction; + private final ConcurrentMap, VectorPostings> postingsMap; + private final DenseIntMap> postingsByOrdinal; + private final NonBlockingHashMap> vectorsByKey; + private final AtomicInteger nextOrdinal = new AtomicInteger(); + private final VectorSourceModel sourceModel; + private final InvalidVectorBehavior invalidVectorBehavior; + private final IntHashSet deletedOrdinals; + private volatile boolean hasDeletions; + private volatile boolean allVectorsAreUnitLength; + + // we don't need to explicitly close these since only on-heap resources are involved + private final ThreadLocal searchers; + + private final boolean writeNvq; + + /** + * @param forSearching if true, vectorsByKey will be initialized and populated with vectors as they are added + */ + public CassandraOnHeapGraph(IndexContext context, boolean forSearching, Memtable memtable) + { + this.source = memtable == null + ? "null" + : memtable.getClass().getSimpleName() + '@' + Integer.toHexString(memtable.hashCode()); + this.columnQueryMetrics = (ColumnQueryMetrics.VectorIndexMetrics) context.getColumnQueryMetrics(); + var indexConfig = context.getIndexWriterConfig(); + var termComparator = context.getValidator(); + serializer = (VectorType.VectorSerializer) termComparator.getSerializer(); + var dimension = ((VectorType) termComparator).dimension; + vectorValues = new ConcurrentVectorValues(dimension); + similarityFunction = indexConfig.getSimilarityFunction(); + sourceModel = indexConfig.getSourceModel(); + // We need to be able to inexpensively distinguish different vectors, with a slower path + // that identifies vectors that are equal but not the same reference. A comparison- + // based Map (which only needs to look at vector elements until a difference is found) + // is thus a better option than hash-based (which has to look at all elements to compute the hash). + postingsMap = new ConcurrentSkipListMap<>((a, b) -> { + return Arrays.compare(((ArrayVectorFloat) a).get(), ((ArrayVectorFloat) b).get()); + }); + postingsByOrdinal = new DenseIntMap<>(1024); + deletedOrdinals = new IntHashSet(); + vectorsByKey = forSearching ? new NonBlockingHashMap<>() : null; + invalidVectorBehavior = forSearching ? InvalidVectorBehavior.FAIL : InvalidVectorBehavior.IGNORE; + + int jvectorVersion = context.version().onDiskFormat().jvectorFileFormatVersion(); + // Assume true until we observe otherwise. + allVectorsAreUnitLength = true; + + // NVQ is only written during compaction to save on compute costs + writeNvq = JVectorVersionUtil.shouldWriteNVQ(dimension, context.version()) && !forSearching; + + // This is only a warning since it's not a fatal error to write without hierarchy + if (indexConfig.isHierarchyEnabled() && jvectorVersion < 4) + logger.warn("Hierarchical graphs configured but node configured with V3OnDiskFormat.JVECTOR_VERSION {}. " + + "Skipping setting for {}", jvectorVersion, indexConfig.getIndexName()); + + builder = new GraphIndexBuilder(vectorValues, + similarityFunction, + indexConfig.getAnnMaxDegree(), + indexConfig.getConstructionBeamWidth(), + indexConfig.getNeighborhoodOverflow(1.0f), // no overflow means add will be a bit slower but flush will be faster + indexConfig.getAlpha(dimension > 3 ? 1.2f : 2.0f), + indexConfig.isHierarchyEnabled() && jvectorVersion >= 4); + searchers = ThreadLocal.withInitial(() -> new GraphSearcherAccessManager(new GraphSearcher(builder.getGraph()))); + } + + public int size() + { + return vectorValues.size(); + } + + public boolean isEmpty() + { + return postingsMap.values().stream().allMatch(VectorPostings::isEmpty); + } + + /** + * @return the ordinal of the vector in the graph, or -1 if the vector is not in the graph + */ + public int getOrdinal(VectorFloat vector) + { + VectorPostings postings = postingsMap.get(vector); + // There is a small race from when the postings list is created to when it is assigned an ordinal, + // so we do not assert that the ordinal is set here + return postings == null ? -1 : postings.getOrdinal(false); + } + + /** + * @return the incremental bytes used by adding the given vector to the index + */ + public long add(ByteBuffer term, T key) + { + assert term != null && term.remaining() != 0; + + var vector = vts.createFloatVector(serializer.deserializeFloatArray(term)); + // Validate the vector. Almost always, this is called at insert time (which sets invalid behavior to FAIL, + // resulting in the insert being aborted if the vector is invalid), or while writing out an sstable + // from flush or compaction (which sets invalid behavior to IGNORE, since we can't just rip existing data out of + // the table). + // + // However, it's also possible for this to be called during commitlog replay if the node previously crashed + // AFTER processing CREATE INDEX, but BEFORE flushing active memtables. Commitlog replay will then follow + // the normal insert code path, (which would set behavior to FAIL) so we special-case it here; see VECTOR-269. + var behavior = invalidVectorBehavior; + if (!StorageService.instance.isInitialized()) + behavior = InvalidVectorBehavior.IGNORE; // we're replaying the commitlog so force IGNORE + if (behavior == InvalidVectorBehavior.IGNORE) + { + try + { + VectorValidation.validateIndexable(vector, similarityFunction); + } + catch (InvalidRequestException e) + { + if (StorageService.instance.isInitialized()) + logger.trace("Ignoring invalid vector during index build against existing data: {}", (Object) e); + else + logger.trace("Ignoring invalid vector during commitlog replay: {}", (Object) e); + return 0; + } + } + else + { + assert behavior == InvalidVectorBehavior.FAIL; + VectorValidation.validateIndexable(vector, similarityFunction); + } + + var bytesUsed = 0L; + + // Store a cached reference to the vector for brute force computations later. There is a small race + // condition here: if inserts for the same PrimaryKey add different vectors, vectorsByKey might + // become out of sync with the graph. + if (vectorsByKey != null) + { + vectorsByKey.put(key, vector); + // The size of the entries themselves are counted below, so just count the two extra references + bytesUsed += RamUsageEstimator.NUM_BYTES_OBJECT_REF * 2L; + } + + VectorPostings postings = postingsMap.get(vector); + // if the vector is already in the graph, all that happens is that the postings list is updated + // otherwise, we add the vector in this order: + // 1. to the postingsMap + // 2. to the vectorValues + // 3. to the graph + // This way, concurrent searches of the graph won't see the vector until it's visible + // in the other structures as well. + if (postings == null) + { + postings = new VectorPostings<>(key); + // since we are using ConcurrentSkipListMap, it is NOT correct to use computeIfAbsent here + if (postingsMap.putIfAbsent(vector, postings) == null) + { + // we won the race to add the new entry; assign it an ordinal and add to the other structures + int ordinal = nextOrdinal.getAndIncrement(); + postings.setOrdinal(ordinal); + bytesUsed += RamEstimation.concurrentHashMapRamUsed(1); // the new posting Map entry + bytesUsed += vectorValues.add(ordinal, vector); + bytesUsed += postings.ramBytesUsed(); + var success = postingsByOrdinal.compareAndPut(ordinal, null, postings); + assert success : "postingsByOrdinal already contains an entry for ordinal " + ordinal; + bytesUsed += builder.addGraphNode(ordinal, vector); + + // If necessary, check if the vector is unit length. + if (!sourceModel.hasKnownUnitLengthVectors() && allVectorsAreUnitLength) + if (!(Math.abs(VectorUtil.dotProduct(vector, vector) - 1.0f) < 0.01)) + allVectorsAreUnitLength = false; + + return bytesUsed; + } + else + { + postings = postingsMap.get(vector); + } + } + // postings list already exists, just add the new key (if it's not already in the list) + if (postings.add(key)) + { + bytesUsed += postings.bytesPerPosting(); + } + + return bytesUsed; + } + + public Collection keysFromOrdinal(int node) + { + return postingsByOrdinal.get(node).getPostings(); + } + + public VectorFloat vectorForKey(T key) + { + if (vectorsByKey == null) + throw new IllegalStateException("vectorsByKey is not initialized"); + return vectorsByKey.get(key); + } + + public void remove(ByteBuffer term, T key) + { + assert term != null && term.remaining() != 0; + + var rawVector = serializer.deserializeFloatArray(term); + VectorFloat v = vts.createFloatVector(rawVector); + var postings = postingsMap.get(v); + if (postings == null) + { + // it's possible for this to be called against a different memtable than the one + // the value was originally added to, in which case we do not expect to find + // the key among the postings for this vector + return; + } + + hasDeletions = true; + postings.remove(key); + if (vectorsByKey != null) + // On updates to a row, we call add then remove, so we must pass the key's value to ensure we only remove + // the deleted vector from vectorsByKey + vectorsByKey.remove(key, v); + } + + /** + * @return an itererator over {@link PrimaryKeyWithSortKey} in the graph's {@link SearchResult} order + */ + public CloseableIterator search(QueryContext context, VectorFloat queryVector, int limit, int rerankK, float threshold, boolean usePruning, Bits toAccept) + { + VectorValidation.validateIndexable(queryVector, similarityFunction); + + // search() errors out when an empty graph is passed to it + if (vectorValues.size() == 0) + return CloseableIterator.emptyIterator(); + // This configuration indicates rerankless search, but that is only applicable to disk search, so we set + // rerankK to limit and otherwise ignore the setting. + if (rerankK <= 0) + rerankK = limit; + + Bits bits = hasDeletions ? BitsUtil.bitsIgnoringDeleted(toAccept, postingsByOrdinal) : toAccept; + var graphAccessManager = searchers.get(); + var searcher = graphAccessManager.get(); + searcher.usePruning(usePruning); + searcher.setView(builder.getGraph().getView()); + try + { + var ssf = DefaultSearchScoreProvider.exact(queryVector, similarityFunction, vectorValues); + long start = nanoTime(); + var result = searcher.search(ssf, limit, rerankK, threshold, 0.0f, bits); + long elapsed = nanoTime() - start; + Tracing.trace("ANN search for {}/{} (usePruning: {}) visited {} nodes, reranked {} to return {} results from {}", + limit, rerankK, usePruning, result.getVisitedCount(), result.getRerankedCount(), result.getNodes().length, source); + columnQueryMetrics.onSearchResult(result, elapsed, false); + context.addAnnGraphSearchLatency(elapsed); + if (threshold > 0) + { + // Threshold based searches do not support resuming the search. + graphAccessManager.release(); + return CloseableIterator.wrap(Arrays.stream(result.getNodes()).iterator()); + } + return new AutoResumingNodeScoreIterator(searcher, graphAccessManager, result, context, columnQueryMetrics, visited -> {}, limit, rerankK, true, source); + } + catch (Throwable t) + { + // If we don't release it, we'll never be able to aquire it, so catch and rethrow Throwable. + graphAccessManager.forceRelease(); + throw t; + } + } + + /** + * Prepare for flushing by doing a bunch of housekeeping: + * 1. Compute row ids for each vector in the postings map + * 2. Remove any vectors that are no longer in use and populate `deletedOrdinals`, including for range deletions + * 3. Return true if the caller should proceed to invoke flush, or false if everything was deleted + *

    + * This is split out from flush per se because of (3); we don't want to flush empty + * index segments, but until we do (1) and (2) we don't know if the segment is empty. + */ + public boolean preFlush(ToIntFunction postingTransformer) + { + var it = postingsMap.entrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + var vp = entry.getValue(); + vp.computeRowIds(postingTransformer); + if (vp.isEmpty() || vp.shouldAppendDeletedOrdinal()) + deletedOrdinals.add(vp.getOrdinal()); + } + return deletedOrdinals.size() < builder.getGraph().size(); + } + + public SegmentMetadata.ComponentMetadataMap flush(IndexComponents.ForWrite perIndexComponents) throws IOException + { + int nInProgress = builder.insertsInProgress(); + assert nInProgress == 0 : String.format("Attempting to write graph while %d inserts are in progress", nInProgress); + assert nextOrdinal.get() == builder.getGraph().size() : String.format("nextOrdinal %d != graph size %d -- ordinals should be sequential", + nextOrdinal.get(), builder.getGraph().size()); + assert vectorValues.size() == builder.getGraph().size() : String.format("vector count %d != graph size %d", + vectorValues.size(), builder.getGraph().size()); + logger.debug("Writing graph with {} rows and {} distinct vectors", postingsMap.values().stream().mapToInt(VectorPostings::size).sum(), vectorValues.size()); + + // compute the remapping of old ordinals to new (to fill in holes from deletion and/or to create a + // closer correspondance to rowids, simplifying postings lookups later) + V5VectorPostingsWriter.RemappedPostings remappedPostings; + if (V5OnDiskFormat.writeV5VectorPostings(perIndexComponents.version())) + { + // remove postings corresponding to marked-deleted vectors + var it = postingsMap.entrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + var vp = entry.getValue(); + if (deletedOrdinals.contains(vp.getOrdinal())) + it.remove(); + } + + assert postingsMap.keySet().size() + deletedOrdinals.size() == vectorValues.size() + : String.format("postings map entry count %d + deleted count %d != vector count %d", + postingsMap.keySet().size(), deletedOrdinals.size(), vectorValues.size()); + // remove deleted ordinals from the graph. this is not done at remove() time, because the same vector + // could be added back again, "undeleting" the ordinal, and the concurrency gets tricky + deletedOrdinals.stream().parallel().forEach(builder::markNodeDeleted); + deletedOrdinals.clear(); + builder.cleanup(); + remappedPostings = V5VectorPostingsWriter.remapForMemtable(postingsMap, perIndexComponents.version()); + } + else + { + assert postingsMap.keySet().size() == vectorValues.size() : String.format("postings map entry count %d != vector count %d", + postingsMap.keySet().size(), vectorValues.size()); + builder.cleanup(); + remappedPostings = V2VectorPostingsWriter.remapForMemtable(postingsMap, !deletedOrdinals.isEmpty()); + } + + OrdinalMapper ordinalMapper = remappedPostings.ordinalMapper; + + IndexComponent.ForWrite termsDataComponent = perIndexComponents.addOrGet(IndexComponentType.TERMS_DATA); + var indexFile = termsDataComponent.file(); + long termsOffset = SAICodecUtils.headerSize(); + if (indexFile.exists()) + termsOffset += indexFile.length(); + try (var pqOutput = perIndexComponents.addOrGet(IndexComponentType.PQ).openOutput(true); + var postingsOutput = perIndexComponents.addOrGet(IndexComponentType.POSTING_LISTS).openOutput(true)) + { + SAICodecUtils.writeHeader(pqOutput); + SAICodecUtils.writeHeader(postingsOutput); + + // Write fused unless we don't meet some criteria (will be determined in the writePQ method) + boolean writeFusedPQ = JVectorVersionUtil.shouldWriteFused(perIndexComponents.version()); + + // compute and write PQ + long pqOffset = pqOutput.getFilePointer(); + var compressor = writePQ(pqOutput.asSequentialWriter(), remappedPostings, perIndexComponents.context(), writeFusedPQ); + long pqLength = pqOutput.getFilePointer() - pqOffset; + + // write postings + long postingsOffset = postingsOutput.getFilePointer(); + long postingsPosition; + if (V5OnDiskFormat.writeV5VectorPostings(perIndexComponents.version())) + { + assert deletedOrdinals.isEmpty(); // V5 format does not support recording deleted ordinals + postingsPosition = new V5VectorPostingsWriter(remappedPostings) + .writePostings(postingsOutput.asSequentialWriter(), vectorValues, postingsMap); + } + else + { + IntUnaryOperator newToOldMapper = remappedPostings.ordinalMapper::newToOld; + postingsPosition = new V2VectorPostingsWriter(remappedPostings.structure == Structure.ONE_TO_ONE, builder.getGraph().size(), newToOldMapper) + .writePostings(postingsOutput.asSequentialWriter(), vectorValues, postingsMap, deletedOrdinals); + } + long postingsLength = postingsPosition - postingsOffset; + + // Write the NVQ feature. We could compute this at insert time, but because the graph allows for parallel + // insertions, it would be a bit more complicated. All vectors are in memory, so the computation to build the + // mean vector should be pretty fast, and this path is only used when we don't have an existing + // ProductQuantization. + NVQuantization nvq = writeNvq ? NVQuantization.compute(vectorValues, NUM_SUB_VECTORS) : null; + + try (var indexWriter = createIndexWriter(indexFile, termsOffset, perIndexComponents.context(), ordinalMapper, compressor, nvq); + var view = builder.getGraph().getView()) + { + indexWriter.getOutput().seek(indexFile.length()); // position at the end of the previous segment before writing our own header + SAICodecUtils.writeHeader(SAICodecUtils.toLuceneOutput(indexWriter.getOutput()), perIndexComponents.version()); + assert indexWriter.getOutput().position() == termsOffset : "termsOffset " + termsOffset + " != " + indexWriter.getOutput().position(); + + // write the graph + var start = nanoTime(); + indexWriter.write(suppliers(view, compressor, nvq, writeFusedPQ)); + SAICodecUtils.writeFooter(indexWriter.getOutput(), indexWriter.checksum()); + logger.info("Writing graph took {}ms", (nanoTime() - start) / 1_000_000); + long termsLength = indexWriter.getOutput().position() - termsOffset; + + // write remaining footers/checksums + SAICodecUtils.writeFooter(pqOutput); + SAICodecUtils.writeFooter(postingsOutput); + + // add components to the metadata map + return createMetadataMap(termsOffset, termsLength, postingsOffset, postingsLength, pqOffset, pqLength); + } + } + } + + private OnDiskGraphIndexWriter createIndexWriter(File indexFile, long termsOffset, IndexContext context, OrdinalMapper ordinalMapper, VectorCompressor compressor, NVQuantization nvq) throws IOException + { + var indexWriterBuilder = new OnDiskGraphIndexWriter.Builder(builder.getGraph(), indexFile.toPath()) + .withStartOffset(termsOffset) + .withVersion(context.version().onDiskFormat().jvectorFileFormatVersion()) + .withMapper(ordinalMapper) + .with(nvq != null ? new NVQ(nvq) : new InlineVectors(vectorValues.dimension())); + + if (compressor instanceof ProductQuantization && JVectorVersionUtil.shouldWriteFused(context.version())) + indexWriterBuilder.with(new FusedPQ(context.getIndexWriterConfig().getAnnMaxDegree(), (ProductQuantization) compressor)); + + return indexWriterBuilder.build(); + } + + static SegmentMetadata.ComponentMetadataMap createMetadataMap(long termsOffset, long termsLength, long postingsOffset, long postingsLength, long pqOffset, long pqLength) + { + SegmentMetadata.ComponentMetadataMap metadataMap = new SegmentMetadata.ComponentMetadataMap(); + metadataMap.put(IndexComponentType.TERMS_DATA, -1, termsOffset, termsLength, Map.of()); + metadataMap.put(IndexComponentType.POSTING_LISTS, -1, postingsOffset, postingsLength, Map.of()); + Map vectorConfigs = Map.of("SEGMENT_ID", ByteBufferUtil.bytesToHex(ByteBuffer.wrap(StringHelper.randomId()))); + metadataMap.put(IndexComponentType.PQ, -1, pqOffset, pqLength, vectorConfigs); + return metadataMap; + } + + private EnumMap> suppliers(ImmutableGraphIndex.View view, VectorCompressor compressor, NVQuantization nvq, boolean writeFusedPQ) + { + var features = new EnumMap>(FeatureId.class); + + // We either write NVQ or inline (full precision) vectors in the graph. nvq is null when it is not enabled. + if (nvq != null) + features.put(FeatureId.NVQ_VECTORS, nodeId -> new NVQ.State(nvq.encode(vectorValues.getVector(nodeId)))); + else + features.put(FeatureId.INLINE_VECTORS, nodeId -> new InlineVectors.State(vectorValues.getVector(nodeId))); + + if (compressor instanceof ProductQuantization && writeFusedPQ) + { + // This block is an extension of an already present design that limits the PQ computation and encoding + // to one index at a time -- goal during flush is to evict from memory ASAP so better to do the PQ build + // (in parallel) one at a time. We have https://github.com/riptano/cndb/issues/12110 to encode the + // PQ iteratively, but since that isn't implemented, we keep the same, fairly brittle pattern. + final PQVectors pqVectors; + synchronized (CassandraOnHeapGraph.class) + { + // Note: the features implementation expects the pqVectors to be addressable on their old ordinal + // index, so we use the original vectorValues as the source without performing any remapping. + pqVectors = (PQVectors) compressor.encodeAll(vectorValues); + } + features.put(FeatureId.FUSED_PQ, nodeId -> new FusedPQ.State(view, pqVectors::get, nodeId)); + } + + return features; + } + + /** + * Return the best previous CompressedVectors for this column that matches the `matcher` predicate. + * "Best" means the most recent one that hits the row count target of {@link ProductQuantization#MAX_PQ_TRAINING_SET_SIZE}, + * or the one with the most rows if none are larger than that. + */ + public static PqInfo getPqIfPresent(IndexContext indexContext, Function matcher) + { + // Retrieve the first compressed vectors for a segment with at least MAX_PQ_TRAINING_SET_SIZE rows + // or the one with the most rows if none reach that size + var view = indexContext.getReferencedView(TimeUnit.SECONDS.toNanos(5)); + if (view == null) + { + logger.warn("Unable to get view of already built indexes for {}", indexContext); + return null; + } + + try + { + var indexes = new ArrayList<>(view.getIndexes()); + indexes.sort(Comparator.comparing(SSTableIndex::getSSTable, CompactionSSTable.maxTimestampDescending)); + + PqInfo cvi = null; + long maxRows = 0; + for (SSTableIndex index : indexes) + { + for (Segment segment : index.getSegments()) + { + if (segment.metadata.numRows < maxRows) + continue; + + var searcher = (V2VectorIndexSearcher) segment.getIndexSearcher(); + var cv = searcher.getCompression(); + if (matcher.apply(cv)) + { + // We can exit now because we won't find a better candidate + var candidate = new PqInfo(searcher.getPQ(), searcher.containsUnitVectors(), segment.metadata.numRows); + if (segment.metadata.numRows >= ProductQuantization.MAX_PQ_TRAINING_SET_SIZE) + return candidate; + + cvi = candidate; + maxRows = segment.metadata.numRows; + } + } + } + return cvi; + } + finally + { + view.release(); + } + } + + private VectorCompressor writePQ(SequentialWriter writer, V5VectorPostingsWriter.RemappedPostings remapped, IndexContext indexContext, boolean writeFusedPQ) throws IOException + { + var preferredCompression = sourceModel.compressionProvider.apply(vectorValues.dimension()); + + // Build encoder and compress vectors + VectorCompressor compressor = null; // will be null if we can't compress + CompressedVectors cv = null; + // limit the PQ computation and encoding to one index at a time -- goal during flush is to + // evict from memory ASAP so better to do the PQ build (in parallel) one at a time + synchronized (CassandraOnHeapGraph.class) + { + // build encoder (expensive for PQ) + if (preferredCompression.type == CompressionType.PRODUCT_QUANTIZATION) + { + var pqi = getPqIfPresent(indexContext, preferredCompression::equals); + compressor = computeOrRefineFrom(pqi, preferredCompression); + } + assert !vectorValues.isValueShared(); + // encode (compress) the vectors to save + if (compressor != null && !writeFusedPQ) + cv = compressor.encodeAll(new RemappedVectorValues(remapped, remapped.maxNewOrdinal, vectorValues)); + } + + var actualType = compressor == null ? CompressionType.NONE : preferredCompression.type; + writePqHeader(writer, allVectorsAreUnitLength, actualType, indexContext.version()); + if (actualType == CompressionType.NONE) + return null; + + if (writeFusedPQ) + { + compressor.write(writer, indexContext.version().onDiskFormat().jvectorFileFormatVersion()); + return compressor; + } + + // save (outside the synchronized block, this is io-bound not CPU) + cv.write(writer, indexContext.version().onDiskFormat().jvectorFileFormatVersion()); + return null; // Don't need compressor in this case + } + + static void writePqHeader(DataOutput writer, boolean unitVectors, CompressionType type, Version version) + throws IOException + { + if (version.onDiskFormat().jvectorFileFormatVersion() >= 3) + { + // version and optional fields + writer.writeInt(CassandraDiskAnn.PQ_MAGIC); + writer.writeInt(PQVersion.V1.ordinal()); + writer.writeBoolean(unitVectors); + } + + // write the compression type + writer.writeByte(type.ordinal()); + } + + ProductQuantization computeOrRefineFrom(PqInfo existingInfo, VectorCompression preferredCompression) + { + if (existingInfo == null) + { + // no previous PQ, compute a new one if we have enough rows to do it + if (vectorValues.size() < MIN_PQ_ROWS) + return null; + else + return ProductQuantization.compute(vectorValues, preferredCompression.getCompressedSize(), 256, false); + } + + // use the existing one unmodified if we either don't have enough rows to fine-tune, or + // the existing one was built with a large enough set + var existingPQ = existingInfo.pq; + if (vectorValues.size() < MIN_PQ_ROWS || existingInfo.rowCount >= ProductQuantization.MAX_PQ_TRAINING_SET_SIZE) + return existingPQ; + + // refine the existing one + return existingPQ.refine(vectorValues); + } + + public long ramBytesUsed() + { + return postingsBytesUsed() + vectorValues.ramBytesUsed() + builder.getGraph().ramBytesUsed(); + } + + private long postingsBytesUsed() + { + return RamEstimation.denseIntMapRamUsed(postingsByOrdinal.size()) + + 3 * RamEstimation.concurrentHashMapRamUsed(postingsMap.size()) // CSLM is much less efficient than CHM + + postingsMap.values().stream().mapToLong(VectorPostings::ramBytesUsed).sum(); + } + + public enum InvalidVectorBehavior + { + IGNORE, + FAIL + } + + public static class PqInfo + { + public final ProductQuantization pq; + /** an empty Optional indicates that the index was written with an older version that did not record this information */ + public final boolean unitVectors; + public final long rowCount; + + public PqInfo(ProductQuantization pq, boolean unitVectors, long rowCount) + { + this.pq = pq; + this.unitVectors = unitVectors; + this.rowCount = rowCount; + } + } + + /** ensures that the graph is connected -- normally not necessary but it can help tests reason about the state */ + @VisibleForTesting + public void cleanup() + { + builder.cleanup(); + } + + /** + * A simple wrapper that remaps the ordinals in the vector values to the new ordinals + */ + private static class RemappedVectorValues implements RandomAccessVectorValues + { + final V5VectorPostingsWriter.RemappedPostings remapped; + final int maxNewOrdinal; + final RandomAccessVectorValues vectorValues; + + RemappedVectorValues(V5VectorPostingsWriter.RemappedPostings remapped, int maxNewOrdinal, RandomAccessVectorValues vectorValues) + { + this.remapped = remapped; + this.maxNewOrdinal = maxNewOrdinal; + this.vectorValues = vectorValues; + } + + @Override + public int size() + { + return maxNewOrdinal + 1; + } + + @Override + public int dimension() + { + return vectorValues.dimension(); + } + + @Override + public VectorFloat getVector(int i) + { + var oldOrdinal = remapped.ordinalMapper.newToOld(i); + return oldOrdinal == OrdinalMapper.OMITTED ? null : vectorValues.getVector(oldOrdinal); + } + + @Override + public boolean isValueShared() + { + return vectorValues.isValueShared(); + } + + @Override + public RandomAccessVectorValues copy() + { + return new RemappedVectorValues(remapped, maxNewOrdinal, vectorValues.copy()); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/CloseableReranker.java b/src/java/org/apache/cassandra/index/sai/disk/vector/CloseableReranker.java new file mode 100644 index 000000000000..0415b9eb4bcc --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/CloseableReranker.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.Closeable; + +import io.github.jbellis.jvector.graph.ImmutableGraphIndex; +import io.github.jbellis.jvector.graph.similarity.ScoreFunction; +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import org.apache.cassandra.io.util.FileUtils; + +/** + * An ExactScoreFunction that closes the underlying {@link ImmutableGraphIndex.ScoringView} when closed. + */ +public class CloseableReranker implements ScoreFunction.ExactScoreFunction, Closeable +{ + private final ImmutableGraphIndex.ScoringView view; + private final ExactScoreFunction scoreFunction; + + public CloseableReranker(VectorSimilarityFunction similarityFunction, VectorFloat queryVector, ImmutableGraphIndex.ScoringView view) + { + this.view = view; + this.scoreFunction = view.rerankerFor(queryVector, similarityFunction); + } + + @Override + public float similarityTo(int i) + { + return scoreFunction.similarityTo(i); + } + + @Override + public void close() + { + FileUtils.closeQuietly(view); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/CompactionGraph.java b/src/java/org/apache/cassandra/index/sai/disk/vector/CompactionGraph.java new file mode 100644 index 000000000000..45c7c7e47bab --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/CompactionGraph.java @@ -0,0 +1,660 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.IntFunction; +import java.util.stream.IntStream; + +import com.google.common.annotations.VisibleForTesting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.graph.GraphIndexBuilder; +import io.github.jbellis.jvector.graph.ListRandomAccessVectorValues; +import io.github.jbellis.jvector.graph.RandomAccessVectorValues; +import io.github.jbellis.jvector.graph.disk.OnDiskParallelGraphIndexWriter; +import io.github.jbellis.jvector.graph.disk.RandomAccessOnDiskGraphIndexWriter; +import io.github.jbellis.jvector.graph.disk.feature.Feature; +import io.github.jbellis.jvector.graph.disk.feature.FeatureId; +import io.github.jbellis.jvector.graph.disk.feature.FusedPQ; +import io.github.jbellis.jvector.graph.disk.feature.InlineVectors; +import io.github.jbellis.jvector.graph.disk.OnDiskGraphIndexWriter; +import io.github.jbellis.jvector.graph.disk.OrdinalMapper; +import io.github.jbellis.jvector.graph.disk.feature.NVQ; +import io.github.jbellis.jvector.graph.similarity.BuildScoreProvider; +import io.github.jbellis.jvector.quantization.MutableCompressedVectors; +import io.github.jbellis.jvector.quantization.MutablePQVectors; +import io.github.jbellis.jvector.quantization.NVQuantization; +import io.github.jbellis.jvector.quantization.PQVectors; +import io.github.jbellis.jvector.quantization.ProductQuantization; +import io.github.jbellis.jvector.quantization.VectorCompressor; +import io.github.jbellis.jvector.util.Accountable; +import io.github.jbellis.jvector.util.RamUsageEstimator; +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.VectorUtil; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import net.openhft.chronicle.bytes.Bytes; +import net.openhft.chronicle.hash.Data; +import net.openhft.chronicle.hash.serialization.BytesReader; +import net.openhft.chronicle.hash.serialization.BytesWriter; +import net.openhft.chronicle.hash.serialization.SizeMarshaller; +import net.openhft.chronicle.map.ChronicleMap; +import net.openhft.chronicle.map.ChronicleMapBuilder; +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.agrona.collections.Int2ObjectHashMap; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.marshal.VectorType; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.disk.v2.V2VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.v5.V5OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter.Structure; +import org.apache.cassandra.index.sai.disk.vector.VectorPostings.CompactionVectorPostings; +import org.apache.cassandra.index.sai.utils.LowPriorityThreadFactory; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +import static java.lang.Math.max; +import static java.lang.Math.min; + + +public class CompactionGraph implements Closeable, Accountable +{ + private static final Logger logger = LoggerFactory.getLogger(CompactionGraph.class); + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + private static final ForkJoinPool compactionFjp = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), // checkstyle: permit this instantiation + new LowPriorityThreadFactory(), + null, + false); + // see comments to JVector PhysicalCoreExecutor -- HT tends to cause contention for the SIMD units + private static final ForkJoinPool compactionSimdPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors() / 2, // checkstyle: permit this instantiation + new LowPriorityThreadFactory(), + null, + false); + + @VisibleForTesting + public static int PQ_TRAINING_SIZE = ProductQuantization.MAX_PQ_TRAINING_SET_SIZE; + + private static boolean PARALLEL_ENCODING_WRITING = CassandraRelevantProperties.SAI_ENCODE_AND_WRITE_VECTOR_GRAPH_IN_PARALLEL_ENABLED.getBoolean(); + private static int PARALLEL_ENCODING_WRITING_NUM_THREADS = CassandraRelevantProperties.SAI_ENCODE_AND_WRITE_VECTOR_GRAPH_IN_PARALLEL_NUM_THREADS.getInt(); + private static boolean PARALLEL_ENCODING_WRITING_USE_DIRECT_BUFFERS = CassandraRelevantProperties.SAI_ENCODE_AND_WRITE_VECTOR_GRAPH_IN_PARALLEL_USE_DIRECT_BUFFERS.getBoolean(); + + private final VectorType.VectorSerializer serializer; + private final VectorSimilarityFunction similarityFunction; + private final ChronicleMap, CompactionVectorPostings> postingsMap; + private final IndexComponents.ForWrite perIndexComponents; + private final IndexContext context; + private final boolean unitVectors; + private final int postingsEntriesAllocated; + private final File postingsFile; + private final File vectorsByOrdinalTmpFile; + private final OnDiskVectorValuesWriter onDiskVectorValuesWriter; + private final File termsFile; + private final int dimension; + private Structure postingsStructure; + private final long termsOffset; + private int lastRowId = -1; + private int rowsAdded = 0; + private int maxOrdinal = -1; // Inclusive + // if `useSyntheticOrdinals` is true then we use `nextOrdinal` to avoid holes, otherwise use rowId as source of ordinals + private final boolean useSyntheticOrdinals; + private int nextOrdinal = 0; + + // protects the fine-tuning changes (done in maybeAddVector) from addGraphNode threads + // (and creates happens-before events so we don't need to mark the other fields volatile) + private final ReadWriteLock trainingLock = new ReentrantReadWriteLock(); + private boolean pqFinetuned = false; + // not final; will be updated to different objects after fine-tuning + private VectorCompressor compressor; + private MutableCompressedVectors compressedVectors; + private GraphIndexBuilder builder; + + private final VectorFloat globalMean; + + public CompactionGraph(IndexComponents.ForWrite perIndexComponents, VectorCompressor compressor, boolean unitVectors, long keyCount, boolean allRowsHaveVectors) throws IOException + { + this.perIndexComponents = perIndexComponents; + this.context = perIndexComponents.context(); + this.unitVectors = unitVectors; + var indexConfig = context.getIndexWriterConfig(); + var termComparator = context.getValidator(); + dimension = ((VectorType) termComparator).dimension; + + // We need to tell Chronicle Map (CM) how many entries to expect. it's critical not to undercount, + // or CM will crash. However, we don't want to just pass in a max entries count of 2B, since it eagerly + // allocated segments for that many entries, which takes about 25s. + // + // If our estimate turns out to be too small, it's not the end of the world, we'll flush this segment + // and start another to avoid crashing CM. But we'd rather not do this because the whole goal of + // CompactionGraph is to write one segment only. + var dd = perIndexComponents.descriptor(); + var rowsPerKey = max(1, Keyspace.open(dd.ksname).getColumnFamilyStore(dd.cfname).getMeanRowsPerPartition()); + long estimatedRows = (long) (1.1 * keyCount * rowsPerKey); // 10% fudge factor + int maxRowsInGraph = Integer.MAX_VALUE - 100_000; // leave room for a few more async additions until we flush + postingsEntriesAllocated = max(1000, (int) min(estimatedRows, maxRowsInGraph)); + + serializer = (VectorType.VectorSerializer) termComparator.getSerializer(); + similarityFunction = indexConfig.getSimilarityFunction(); + postingsStructure = Structure.ONE_TO_ONE; // until proven otherwise + this.compressor = compressor; + // `allRowsHaveVectors` only tells us about data for which we have already built indexes; if we + // are adding previously unindexed data then we could still encounter rows with null vectors, + // so this is just a best guess. If the guess is wrong then the penalty is that we end up + // with "holes" in the ordinal sequence (and pq and data files) which we would prefer to avoid + // (hence the effort to predict `allRowsHaveVectors`) but will not cause correctness issues, + // and the next compaction will fill in the holes. + this.useSyntheticOrdinals = !V5OnDiskFormat.writeV5VectorPostings(context.version()) || !allRowsHaveVectors; + + // the extension here is important to signal to CFS.scrubDataDirectories that it should be removed if present at restart + postingsFile = perIndexComponents.tmpFileFor("postings_chonicle_map"); + postingsMap = ChronicleMapBuilder.of((Class>) (Class) VectorFloat.class, (Class) (Class) CompactionVectorPostings.class) + .averageKeySize(dimension * Float.BYTES) + .keySizeMarshaller(SizeMarshaller.constant((long) dimension * Float.BYTES)) + .averageValueSize(VectorPostings.emptyBytesUsed() + RamUsageEstimator.NUM_BYTES_OBJECT_REF + 2 * Integer.BYTES) + .keyMarshaller(new VectorFloatMarshaller(dimension)) + .valueMarshaller(new VectorPostings.Marshaller()) + .entries(postingsEntriesAllocated) + .createPersistedTo(postingsFile.toJavaIOFile()); + + // Formatted so that the full resolution vector is written at the ordinal * vector dimension offset + vectorsByOrdinalTmpFile = perIndexComponents.tmpFileFor("vectors_by_ordinal"); + onDiskVectorValuesWriter = new OnDiskVectorValuesWriter(vectorsByOrdinalTmpFile, dimension); + + BuildScoreProvider bsp; + if (compressor instanceof ProductQuantization) + { + compressedVectors = new MutablePQVectors((ProductQuantization) compressor); + bsp = BuildScoreProvider.pqBuildScoreProvider(similarityFunction, (PQVectors) compressedVectors); + } + else + { + throw new IllegalArgumentException("Unsupported compressor: " + compressor); + } + int jvectorVersion = context.version().onDiskFormat().jvectorFileFormatVersion(); + if (indexConfig.isHierarchyEnabled() && jvectorVersion < 4) + logger.warn("Hierarchical graphs configured but node configured with V3OnDiskFormat.JVECTOR_VERSION {}. " + + "Skipping setting for {}", jvectorVersion, indexConfig.getIndexName()); + + builder = new GraphIndexBuilder(bsp, + dimension, + indexConfig.getAnnMaxDegree(), + indexConfig.getConstructionBeamWidth(), + indexConfig.getNeighborhoodOverflow(1.2f), + indexConfig.getAlpha(dimension > 3 ? 1.2f : 1.4f), + indexConfig.isHierarchyEnabled() && jvectorVersion >= 4, + true, // We always refine during compaction + compactionSimdPool, + compactionFjp); + + termsFile = perIndexComponents.addOrGet(IndexComponentType.TERMS_DATA).file(); + termsOffset = (termsFile.exists() ? termsFile.length() : 0) + + SAICodecUtils.headerSize(); + + globalMean = JVectorVersionUtil.shouldWriteNVQ(dimension, context.version()) ? vts.createFloatVector(new float[dimension]) + : null; + } + + private RandomAccessOnDiskGraphIndexWriter createTermsWriter(OrdinalMapper ordinalMapper, NVQuantization nvq) throws IOException + { + // We call termsFile.toJavaIOFile().toPath() to get a local file. + var path = termsFile.toJavaIOFile().toPath(); + var graph = builder.getGraph(); + + var writerBuilder = PARALLEL_ENCODING_WRITING + ? new OnDiskParallelGraphIndexWriter.Builder(graph, path) + .withStartOffset(termsOffset) + .withParallelWorkerThreads(PARALLEL_ENCODING_WRITING_NUM_THREADS) + .withParallelDirectBuffers(PARALLEL_ENCODING_WRITING_USE_DIRECT_BUFFERS) + : new OnDiskGraphIndexWriter.Builder(graph, path).withStartOffset(termsOffset); + + writerBuilder.with(nvq != null ? new NVQ(nvq) : new InlineVectors(dimension)) + .withVersion(context.version().onDiskFormat().jvectorFileFormatVersion()) + .withMapper(ordinalMapper); + if (compressor instanceof ProductQuantization && JVectorVersionUtil.shouldWriteFused(context.version())) + writerBuilder.with(new FusedPQ(context.getIndexWriterConfig().getAnnMaxDegree(), (ProductQuantization) compressor)); + return writerBuilder.build(); + } + + @Override + public void close() throws IOException + { + // this gets called in `finally` blocks, so use closeQuietly to avoid generating additional exceptions + FileUtils.closeQuietly(postingsMap); + FileUtils.closeQuietly(onDiskVectorValuesWriter); + Files.delete(postingsFile.toJavaIOFile().toPath()); + Files.delete(vectorsByOrdinalTmpFile.toJavaIOFile().toPath()); + } + + public int size() + { + return builder.getGraph().size(); + } + + public boolean isEmpty() + { + return rowsAdded == 0; + } + + /** + * @return the result of adding the given (vector) term; see {@link InsertionResult} + */ + public InsertionResult maybeAddVector(ByteBuffer term, int segmentRowId) throws IOException + { + assert term != null && term.remaining() != 0; + + var vector = vts.createFloatVector(serializer.deserializeFloatArray(term)); + // Validate the vector. Since we are compacting, invalid vectors are ignored instead of failing the operation. + try + { + VectorValidation.validateIndexable(vector, similarityFunction); + } + catch (InvalidRequestException e) + { + if (StorageService.instance.isInitialized()) + logger.trace("Ignoring invalid vector during index build against existing data: {}", (Object) e); + else + logger.trace("Ignoring invalid vector during commitlog replay: {}", (Object) e); + return new InsertionResult(0); + } + + // if we don't see sequential rowids, it means the skipped row(s) have null vectors + if (segmentRowId != lastRowId + 1) + postingsStructure = Structure.ZERO_OR_ONE_TO_MANY; + lastRowId = segmentRowId; + rowsAdded++; + + var bytesUsed = 0L; + // QueryContext allows us to avoid re-serializing the vector. Closing the queryContext releases the lock. + // Note that a normal put operation follows this flow, so the overhead of acquiring the lock is required. + try (var postingsQueryContext = postingsMap.queryContext(vector)) + { + // Closing the query context releases the lock. + //noinspection LockAcquiredButNotSafelyReleased + postingsQueryContext.writeLock().lock(); + var absentEntry = postingsQueryContext.absentEntry(); + if (absentEntry != null) + { + // add a new entry + // this all runs on the same compaction thread, so we don't need to worry about concurrency + int ordinal = useSyntheticOrdinals ? nextOrdinal++ : segmentRowId; + assert ordinal > maxOrdinal : "Unexpected ordinal " + ordinal + " previous max " + maxOrdinal; + maxOrdinal = ordinal; + CompactionVectorPostings postings = new CompactionVectorPostings(ordinal, segmentRowId); + Data data = postingsQueryContext.wrapValueAsData(postings); + absentEntry.doInsert(data); + + // fine-tune the PQ if we've collected enough vectors + if (compressor instanceof ProductQuantization && !pqFinetuned && postingsMap.size() >= PQ_TRAINING_SIZE) + { + // walk the on-disk Postings once to build (1) a dense list of vectors with no missing entries or zeros + // and (2) a map of vectors keyed by ordinal + var trainingVectors = new ArrayList>(postingsMap.size()); + var vectorsByOrdinal = new Int2ObjectHashMap>(); + postingsMap.forEachEntry(entry -> { + // We copy here to be extra safe because at the time of writing, I couldn't find definitive + // proof that forEachEntry doesn't reuse the float[] backing the instance. Since this is only + // ever called for MAX_PQ_TRAINING_SET_SIZE vectors at a time, the cost is essentially fixed + // and is unlikely to contribute much to compaction duration. + var vectorClone = entry.key().get().copy(); + trainingVectors.add(vectorClone); + vectorsByOrdinal.put(VectorPostings.Marshaller.extractOrdinal(entry), vectorClone); + }); + + // lock the addGraphNode threads out so they don't try to use old pq codepoints against the new codebook + trainingLock.writeLock().lock(); + try + { + // Fine tune the pq codebook + compressor = ((ProductQuantization) compressor).refine(new ListRandomAccessVectorValues(trainingVectors, dimension)); + trainingVectors.clear(); // don't need these anymore so let GC reclaim if it wants to + + long originalBytesUsed = compressedVectors.ramBytesUsed(); + // re-encode the vectors added so far + int encodedVectorCount = compressedVectors.count(); + compressedVectors = new MutablePQVectors((ProductQuantization) compressor); + compactionFjp.submit(() -> { + IntStream.range(0, encodedVectorCount) + .parallel() + .forEach(i -> { + var v = vectorsByOrdinal.get(i); + if (v == null) + compressedVectors.setZero(i); + else + compressedVectors.encodeAndSet(i, v); + }); + }).join(); + + // Update bytes to account for new encoding. This isn't expected to change, but just + // in case it does, we track it here. + bytesUsed += (compressedVectors.ramBytesUsed() - originalBytesUsed); + + // Keep the existing edges but recompute their scores + builder = GraphIndexBuilder.rescore(builder, BuildScoreProvider.pqBuildScoreProvider(similarityFunction, (PQVectors) compressedVectors)); + } + finally + { + trainingLock.writeLock().unlock(); + } + pqFinetuned = true; + } + + // Update the global mean, if we're tracking it (which is currently only done when we will write using NVQ) + if (globalMean != null) + VectorUtil.addInPlace(globalMean, vector); + + // Store the vector on disk in a mapping from ordinal -> vector for fast retrieval later. This mapping + // is only needed during index build. It is a temp file. + onDiskVectorValuesWriter.write(ordinal, vector); + + // Track the bytes used as a result of this operation + long compressedVectorsBytesUsed = compressedVectors.ramBytesUsed(); + // Fill in any holes in the pqVectors (setZero has the side effect of increasing the count) + while (compressedVectors.count() < ordinal) + compressedVectors.setZero(compressedVectors.count()); + compressedVectors.encodeAndSet(ordinal, vector); + + bytesUsed += postings.ramBytesUsed(); + bytesUsed += (compressedVectors.ramBytesUsed() - compressedVectorsBytesUsed); + return new InsertionResult(bytesUsed, ordinal, vector); + } + + // postings list already exists, just add the new key + if (postingsStructure == Structure.ONE_TO_ONE) + postingsStructure = Structure.ONE_TO_MANY; + + var postingsEntry = postingsQueryContext.entry(); + assert postingsEntry != null; + var postings = postingsEntry.value().get(); + var newPosting = postings.add(segmentRowId); + assert newPosting; + bytesUsed += postings.bytesPerPosting(); + Data updatedPostings = postingsQueryContext.wrapValueAsData(postings); + postingsEntry.doReplaceValue(updatedPostings); // re-serialize value to disk + + return new InsertionResult(bytesUsed); + } + } + + public long addGraphNode(InsertionResult result) + { + trainingLock.readLock().lock(); + try + { + return builder.addGraphNode(result.ordinal, result.vector); + } + finally + { + trainingLock.readLock().unlock(); + } + } + + public SegmentMetadata.ComponentMetadataMap flush() throws IOException + { + // Close the temporary file so the reader will know it is the end of the file. + onDiskVectorValuesWriter.close(); + + int nInProgress = builder.insertsInProgress(); + assert nInProgress == 0 : String.format("Attempting to write graph while %d inserts are in progress", nInProgress); + assert !useSyntheticOrdinals || nextOrdinal == builder.getGraph().size() : String.format("nextOrdinal %d != graph size %d -- ordinals should be sequential", + nextOrdinal, builder.getGraph().size()); + assert compressedVectors.count() == builder.getGraph().getIdUpperBound() : String.format("Largest vector id %d != largest graph id %d", + compressedVectors.count(), builder.getGraph().getIdUpperBound()); + assert postingsMap.keySet().size() == builder.getGraph().size() : String.format("postings map entry count %d != vector count %d", + postingsMap.keySet().size(), builder.getGraph().size()); + if (logger.isDebugEnabled()) + { + logger.debug("Writing graph with {} rows and {} distinct vectors", rowsAdded, builder.getGraph().size()); + logger.debug("Estimated size is {} + {}", compressedVectors.ramBytesUsed(), builder.getGraph().ramBytesUsed()); + } + + try (var postingsOutput = perIndexComponents.addOrGet(IndexComponentType.POSTING_LISTS).openOutput(true); + var pqOutput = perIndexComponents.addOrGet(IndexComponentType.PQ).openOutput(true)) + { + SAICodecUtils.writeHeader(postingsOutput); + SAICodecUtils.writeHeader(pqOutput); + + // write PQ (time to do this is negligible, don't bother doing it async) + long pqOffset = pqOutput.getFilePointer(); + Version version = context.version(); + CassandraOnHeapGraph.writePqHeader(pqOutput.asSequentialWriter(), unitVectors, VectorCompression.CompressionType.PRODUCT_QUANTIZATION, version); + compressedVectors.write(pqOutput.asSequentialWriter(), version.onDiskFormat().jvectorFileFormatVersion()); + long pqLength = pqOutput.getFilePointer() - pqOffset; + + // write postings asynchronously while we run cleanup() + var ordinalMapper = new AtomicReference(); + long postingsOffset = postingsOutput.getFilePointer(); + var es = ExecutorFactory.Global.executorFactory().sequential("CompactionGraphPostingsWriter"); + var postingsFuture = es.submit(() -> { + // V2 doesn't support ONE_TO_MANY so force it to ZERO_OR_ONE_TO_MANY if necessary; + // similarly, if we've been using synthetic ordinals then we can't map to ONE_TO_MANY + // (ending up at ONE_TO_MANY when the source sstables were not is unusual, but possible, + // if a row with null vector in sstable A gets updated with a vector in sstable B) + // If there are too many holes, we leave the mapping on the disk. + if (postingsStructure == Structure.ONE_TO_MANY + && (!V5OnDiskFormat.writeV5VectorPostings(version) + || useSyntheticOrdinals + || V5VectorPostingsWriter.tooManyOrdinalMappingHoles(postingsMap.size(), rowsAdded))) + { + postingsStructure = Structure.ZERO_OR_ONE_TO_MANY; + } + var rp = V5VectorPostingsWriter.describeForCompaction(postingsStructure, + builder.getGraph().size(), + lastRowId, + maxOrdinal, + postingsMap); + ordinalMapper.set(rp.ordinalMapper); + try (var vectorValues = new OnDiskVectorValues(vectorsByOrdinalTmpFile, dimension)) + { + return writePostings(version, rp, postingsOutput, vectorValues); + } + }); + + // complete internal graph clean up + builder.cleanup(); + + // wait for postings to finish writing and clean up related resources + long postingsEnd = postingsFuture.get(); + long postingsLength = postingsEnd - postingsOffset; + es.shutdown(); + + // write the graph edge lists and optionally fused adc features + var start = nanoTime(); + + // Null if we not using nvq + NVQuantization nvq = createNVQ(); + long termsLength; + try(var writer = createTermsWriter(ordinalMapper.get(), nvq)) + { + writer.getOutput().seek(termsFile.length()); // position at the end of the previous segment before writing our own header + SAICodecUtils.writeHeader(SAICodecUtils.toLuceneOutput(writer.getOutput()), perIndexComponents.version()); + // OnDiskVectorValues is thread safe, making it safe to close over it. + try (var vectorValues = new OnDiskVectorValues(vectorsByOrdinalTmpFile, dimension)) + { + EnumMap> supplier; + if (nvq != null) + { + supplier = Feature.singleStateFactory(FeatureId.NVQ_VECTORS, ordinal -> { + return new NVQ.State(nvq.encode(vectorValues.getVector(ordinal))); + }); + } + else + { + supplier = Feature.singleStateFactory(FeatureId.INLINE_VECTORS, ordinal -> { + return new InlineVectors.State(vectorValues.getVector(ordinal)); + }); + } + if (writer.getFeatureSet().contains(FeatureId.FUSED_PQ)) + { + try (var view = builder.getGraph().getView()) + { + supplier.put(FeatureId.FUSED_PQ, ordinal -> new FusedPQ.State(view, (PQVectors) compressedVectors, ordinal)); + writer.write(supplier); + } + } + else + { + writer.write(supplier); + } + } + catch (Exception e) + { + // Closing threadLocalReaders can throw Exception, but we don't expect it to. + throw new RuntimeException(e); + } + + SAICodecUtils.writeFooter(writer.getOutput(), writer.checksum()); + logger.info("Writing graph took {}ms", (nanoTime() - start) / 1_000_000); + termsLength = writer.getOutput().position() - termsOffset; + } + // write remaining footers/checksums + SAICodecUtils.writeFooter(pqOutput); + SAICodecUtils.writeFooter(postingsOutput); + + // add components to the metadata map + return CassandraOnHeapGraph.createMetadataMap(termsOffset, termsLength, postingsOffset, postingsLength, pqOffset, pqLength); + } + catch (ExecutionException | InterruptedException e) + { + throw new RuntimeException(e); + } + } + + private NVQuantization createNVQ() + { + // If we don't have a global mean, we are not using NVQ + if (globalMean == null) + return null; + // Scale in place then create the NVQ + VectorUtil.scale(globalMean, 1.0f / compressedVectors.count()); + return NVQuantization.create(globalMean, JVectorVersionUtil.NUM_SUB_VECTORS); + } + + private long writePostings(Version version, V5VectorPostingsWriter.RemappedPostings rp, IndexOutputWriter postingsOutput, + RandomAccessVectorValues vectorValues) throws IOException + { + if (V5OnDiskFormat.writeV5VectorPostings(version)) + { + return new V5VectorPostingsWriter(rp).writePostings(postingsOutput.asSequentialWriter(), vectorValues, postingsMap); + } + else + { + assert postingsStructure == Structure.ONE_TO_ONE || postingsStructure == Structure.ZERO_OR_ONE_TO_MANY; + return new V2VectorPostingsWriter(postingsStructure == Structure.ONE_TO_ONE, builder.getGraph().size(), rp.ordinalMapper::newToOld) + .writePostings(postingsOutput.asSequentialWriter(), vectorValues, postingsMap, Set.of()); + } + } + + public long ramBytesUsed() + { + return compressedVectors.ramBytesUsed() + builder.getGraph().ramBytesUsed(); + } + + public boolean requiresFlush() + { + return builder.getGraph().size() >= postingsEntriesAllocated; + } + + public static class VectorFloatMarshaller implements BytesReader>, BytesWriter> { + + private final int dimension; + + public VectorFloatMarshaller(int dimension) + { + this.dimension = dimension; + } + + @Override + public void write(Bytes out, VectorFloat vector) { + for (int i = 0; i < vector.length(); i++) { + out.writeFloat(vector.get(i)); + } + } + + @Override + public VectorFloat read(Bytes in, VectorFloat using) { + if (using == null) { + float[] data = new float[dimension]; + for (int i = 0; i < dimension; i++) { + data[i] = in.readFloat(); + } + return vts.createFloatVector(data); + } + + for (int i = 0; i < dimension; i++) { + using.set(i, in.readFloat()); + } + return using; + } + } + + /** + * AddResult is a container for the result of maybeAddVector. If this call resulted in a new + * vector being added to the graph, then `ordinal` and `vector` fields will be populated, otherwise + * they will be null. + *

    + * bytesUsed is always populated and always non-negative (it will be smaller, but not zero, + * when adding a vector that already exists in the graph to a new row). + */ + public static class InsertionResult + { + public final long bytesUsed; + public final Integer ordinal; + public final VectorFloat vector; + + public InsertionResult(long bytesUsed, Integer ordinal, VectorFloat vector) + { + this.bytesUsed = bytesUsed; + this.ordinal = ordinal; + this.vector = vector; + } + + public InsertionResult(long bytesUsed) + { + this(bytesUsed, null, null); + } + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/ConcurrentVectorValues.java b/src/java/org/apache/cassandra/index/sai/disk/vector/ConcurrentVectorValues.java new file mode 100644 index 000000000000..db77f95dd797 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/ConcurrentVectorValues.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.function.IntUnaryOperator; + +import com.google.common.annotations.VisibleForTesting; + +import io.github.jbellis.jvector.util.DenseIntMap; +import io.github.jbellis.jvector.util.RamUsageEstimator; +import io.github.jbellis.jvector.vector.ArrayVectorFloat; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import org.apache.cassandra.io.util.SequentialWriter; + +public class ConcurrentVectorValues implements RamAwareVectorValues +{ + private final int dimensions; + private final DenseIntMap> values = new DenseIntMap<>(1024); + + public ConcurrentVectorValues(int dimensions) + { + this.dimensions = dimensions; + } + + @Override + public int size() + { + return values.size(); + } + + @Override + public int dimension() + { + return dimensions; + } + + @Override + public VectorFloat getVector(int i) + { + return values.get(i); + } + + /** return approximate bytes used by the new vector */ + public long add(int ordinal, VectorFloat vector) + { + if (!values.compareAndPut(ordinal, null, vector)) + throw new IllegalStateException("Vector already exists for ordinal " + ordinal); + return RamUsageEstimator.NUM_BYTES_OBJECT_REF + oneVectorBytesUsed(); + } + + @Override + public boolean isValueShared() + { + return false; + } + + @Override + public ConcurrentVectorValues copy() + { + // no actual copy required because we always return distinct float[] for distinct vector ordinals + return this; + } + + public long ramBytesUsed() + { + long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; + return 2 * REF_BYTES + + RamEstimation.denseIntMapRamUsed(values.size()) + + values.size() * oneVectorBytesUsed(); + } + + private long oneVectorBytesUsed() + { + return Integer.BYTES + Integer.BYTES + (long) dimension() * Float.BYTES; + } + + @VisibleForTesting + public long write(SequentialWriter writer, IntUnaryOperator ordinalMapper) throws IOException + { + writer.writeInt(size()); + writer.writeInt(dimension()); + + for (var i = 0; i < size(); i++) { + int ord = ordinalMapper.applyAsInt(i); + var fb = FloatBuffer.wrap(((ArrayVectorFloat) values.get(ord)).get()); + var bb = ByteBuffer.allocate(fb.capacity() * Float.BYTES); + bb.asFloatBuffer().put(fb); + writer.write(bb); + } + + return writer.position(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/GraphSearcherAccessManager.java b/src/java/org/apache/cassandra/index/sai/disk/vector/GraphSearcherAccessManager.java new file mode 100644 index 000000000000..0dc514a22848 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/GraphSearcherAccessManager.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.concurrent.NotThreadSafe; + +import io.github.jbellis.jvector.graph.GraphSearcher; + +/** + * Manages access to a {@link GraphSearcher} instance, validating that we respect the contract of GraphSearcher + * to only use it in a single search at a time. + */ +@NotThreadSafe +public class GraphSearcherAccessManager +{ + private final GraphSearcher searcher; + private final AtomicBoolean locked; + + public GraphSearcherAccessManager(GraphSearcher searcher) + { + this.searcher = searcher; + this.locked = new AtomicBoolean(false); + } + + /** + * Get the {@link GraphSearcher} instance, locking it to the current in-progress search. + */ + public GraphSearcher get() + { + if (!locked.compareAndSet(false, true)) + throw new IllegalStateException("GraphAccessManager is already locked"); + return searcher; + } + + /** + * Release the {@link GraphSearcher} instance, allowing it to be used in another search. + */ + public void release() + { + if (!locked.compareAndSet(true, false)) + throw new IllegalStateException("GraphAccessManager is already unlocked"); + } + + /** + * Release the {@link GraphSearcher} instance, allowing it to be used in another search, + * without confirming its state. Inteaded for use in exceptional code paths. + */ + public void forceRelease() + { + locked.set(false); + } + + /** + * Close the {@link GraphSearcher} instance. It cannot be used again after being closed. + */ + public void close() throws IOException + { + searcher.close(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/JVectorVersionUtil.java b/src/java/org/apache/cassandra/index/sai/disk/vector/JVectorVersionUtil.java new file mode 100644 index 000000000000..13078930e230 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/JVectorVersionUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.index.sai.disk.format.Version; + +public class JVectorVersionUtil +{ + /** + * Variables are volatile to allow for changing in unit tests. They are only accessed on flush and compaction, + * so their access is infrequent. + */ + public static volatile boolean ENABLE_NVQ = CassandraRelevantProperties.SAI_VECTOR_ENABLE_NVQ.getBoolean(); + public static final int NUM_SUB_VECTORS = CassandraRelevantProperties.SAI_VECTOR_NVQ_NUM_SUB_VECTORS.getInt(); + + /** + * Decide whether we should write NVQ vectors to disk. + * With NVQ, we use M * (7 + D / M) bytes, where D is the number of dimensions and M is the number of subvectors. + * For FP vectors, we trivially use 4D bytes + * @param dimension vector dimension for the index + * @param version SAI on disk version, which internally determines the jvector version + * @return true if NVQ should be used for the graph or false otherwise + */ + public static boolean shouldWriteNVQ(int dimension, Version version) + { + return ENABLE_NVQ && versionSupportsNVQ(version) && NUM_SUB_VECTORS * (7 + dimension / NUM_SUB_VECTORS) < 4 * dimension; + } + + public static boolean versionSupportsNVQ(Version version) + { + return version.onDiskFormat().jvectorFileFormatVersion() >= 4; + } + + /** + * Decide whether to attempt to write the quantized vectors as fused parts of the graph. Note that this method + * does not take into account whether the graph has enough information to build a quantization, as that depends on + * external factors. + *

    + * FusedPQ is automatically enabled for all indexes using version FA or later (jvector file format version 6+). + * The deprecated ENABLE_FUSED property is ignored for these versions. + * + * @param version the SAI on disk format to use when writing to disk + * @return true if conditions are met, false otherwise + */ + public static boolean shouldWriteFused(Version version) + { + // For FA version and later, FusedPQ is always enabled (tied to the version) + return versionSupportsFused(version); + } + + public static boolean versionSupportsFused(Version version) + { + return version.onDiskFormat().jvectorFileFormatVersion() >= 6; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/NodeQueueRowIdIterator.java b/src/java/org/apache/cassandra/index/sai/disk/vector/NodeQueueRowIdIterator.java new file mode 100644 index 000000000000..a552d3799133 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/NodeQueueRowIdIterator.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import io.github.jbellis.jvector.graph.NodeQueue; +import org.apache.cassandra.index.sai.utils.RowIdWithScore; +import org.apache.cassandra.utils.AbstractIterator; + +/** + * An iterator over {@link RowIdWithScore} that lazily consumes a {@link NodeQueue}. + */ +public class NodeQueueRowIdIterator extends AbstractIterator +{ + private final NodeQueue scoreQueue; + private final boolean isScoreApproximate; + + public NodeQueueRowIdIterator(NodeQueue scoreQueue, boolean isScoreApproximate) + { + this.scoreQueue = scoreQueue; + this.isScoreApproximate = isScoreApproximate; + } + + @Override + protected RowIdWithScore computeNext() + { + if (scoreQueue.size() == 0) + return endOfData(); + float score = scoreQueue.topScore(); + int rowId = scoreQueue.pop(); + return new RowIdWithScore(rowId, score, isScoreApproximate); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/NodeScoreToRowIdWithScoreIterator.java b/src/java/org/apache/cassandra/index/sai/disk/vector/NodeScoreToRowIdWithScoreIterator.java similarity index 78% rename from src/java/org/apache/cassandra/index/sai/disk/v1/vector/NodeScoreToRowIdWithScoreIterator.java rename to src/java/org/apache/cassandra/index/sai/disk/vector/NodeScoreToRowIdWithScoreIterator.java index 4dfa46e4a64a..b861236e64cc 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/NodeScoreToRowIdWithScoreIterator.java +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/NodeScoreToRowIdWithScoreIterator.java @@ -16,19 +16,18 @@ * limitations under the License. */ -package org.apache.cassandra.index.sai.disk.v1.vector; +package org.apache.cassandra.index.sai.disk.vector; import java.io.IOException; -import java.util.Arrays; import java.util.PrimitiveIterator; import java.util.stream.IntStream; +import io.github.jbellis.jvector.graph.SearchResult; +import org.apache.cassandra.index.sai.utils.RowIdWithScore; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.CloseableIterator; -import io.github.jbellis.jvector.graph.SearchResult; - /** * An iterator over {@link RowIdWithScore} sorted by score descending. The iterator converts ordinals (node ids) to * segment row ids and pairs them with the score given by the index. @@ -36,16 +35,19 @@ public class NodeScoreToRowIdWithScoreIterator extends AbstractIterator { private final CloseableIterator nodeScores; - private final OnDiskOrdinalsMap.RowIdsView rowIdsView; + private final RowIdsView rowIdsView; + private final boolean isScoreApproximate; private PrimitiveIterator.OfInt segmentRowIdIterator = IntStream.empty().iterator(); private float currentScore; public NodeScoreToRowIdWithScoreIterator(CloseableIterator nodeScores, - OnDiskOrdinalsMap.RowIdsView rowIdsView) + RowIdsView rowIdsView, + boolean isScoreApproximate) { this.nodeScores = nodeScores; this.rowIdsView = rowIdsView; + this.isScoreApproximate = isScoreApproximate; } @Override @@ -54,16 +56,16 @@ protected RowIdWithScore computeNext() try { if (segmentRowIdIterator.hasNext()) - return new RowIdWithScore(segmentRowIdIterator.nextInt(), currentScore); + return new RowIdWithScore(segmentRowIdIterator.nextInt(), currentScore, isScoreApproximate); while (nodeScores.hasNext()) { SearchResult.NodeScore result = nodeScores.next(); currentScore = result.score; - int ordinal = result.node; - segmentRowIdIterator = Arrays.stream(rowIdsView.getSegmentRowIdsMatching(ordinal)).iterator(); + var ordinal = result.node; + segmentRowIdIterator = rowIdsView.getSegmentRowIdsMatching(ordinal); if (segmentRowIdIterator.hasNext()) - return new RowIdWithScore(segmentRowIdIterator.nextInt(), currentScore); + return new RowIdWithScore(segmentRowIdIterator.nextInt(), currentScore, isScoreApproximate); } return endOfData(); } @@ -76,7 +78,6 @@ protected RowIdWithScore computeNext() @Override public void close() { - FileUtils.closeQuietly(rowIdsView); - FileUtils.closeQuietly(nodeScores); + FileUtils.closeQuietly(rowIdsView, nodeScores); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskOrdinalsMap.java b/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskOrdinalsMap.java new file mode 100644 index 000000000000..87d22d785718 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskOrdinalsMap.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.IOException; +import java.util.PrimitiveIterator; +import java.util.function.Supplier; + +import io.github.jbellis.jvector.util.BitSet; +import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.util.SparseBits; +import org.apache.cassandra.index.sai.disk.v5.V5VectorPostingsWriter; +import org.apache.cassandra.index.sai.utils.SingletonIntIterator; + +public interface OnDiskOrdinalsMap extends AutoCloseable +{ + /** maps from vector ordinals returned by index search to rowids in the sstable */ + RowIdsView getRowIdsView(); + + default Bits ignoringDeleted(Bits acceptBits) { + return acceptBits; + } + + /** maps from rowids to their associated ordinals, for setting up the ordinals-to-accept in a restricted search */ + OrdinalsView getOrdinalsView(); + + void close(); + + V5VectorPostingsWriter.Structure getStructure(); + + /** + * Ignoring the constant overhead of the object, return the variable overhead of the object. This helps + * identify the cost of caching. + */ + long cachedBytesUsed(); + + class OneToOneRowIdsView implements RowIdsView { + + @Override + public PrimitiveIterator.OfInt getSegmentRowIdsMatching(int vectorOrdinal) throws IOException + { + return new SingletonIntIterator(vectorOrdinal); + } + + @Override + public void close() + { + // noop + } + } + + class EmptyRowIdsView implements RowIdsView + { + @Override + public PrimitiveIterator.OfInt getSegmentRowIdsMatching(int vectorOrdinal) throws IOException + { + return new PrimitiveIterator.OfInt() + { + @Override + public int nextInt() + { + throw new IllegalStateException(); + } + + @Override + public boolean hasNext() + { + return false; + } + }; + } + + @Override + public void close() + { + // noop + } + } + + /** + * An OrdinalsView that always returns -1 for all rowIds. This is used when the segment has no postings, which + * can happen if all the graph's ordinals are in the deletedOrdinals set. + */ + class EmptyOrdinalsView implements OrdinalsView + { + @Override + public int getOrdinalForRowId(int rowId) throws IOException + { + return -1; + } + + @Override + public void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException + { + // noop + } + + @Override + public Bits buildOrdinalBits(int startRowId, int endRowId, Supplier bitsSupplier) throws IOException + { + // Get an empty bitset + return bitsSupplier.get(); + } + + @Override + public void close() + { + // noop + } + } + + /** Bits matching the given range, inclusively. */ + class MatchRangeBits extends BitSet + { + final int lowerBound; + final int upperBound; + + public MatchRangeBits(int lowerBound, int upperBound) { + // bitset is empty if lowerBound > upperBound + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + @Override + public boolean get(int index) { + return lowerBound <= index && index <= upperBound; + } + + @Override + public int length() { + if (lowerBound > upperBound) + return 0; + return upperBound - lowerBound + 1; + } + + @Override + public void set(int i) + { + throw new UnsupportedOperationException("not supported"); + } + + @Override + public boolean getAndSet(int i) + { + throw new UnsupportedOperationException("not supported"); + } + + @Override + public void clear(int i) + { + throw new UnsupportedOperationException("not supported"); + } + + @Override + public void clear(int i, int i1) + { + throw new UnsupportedOperationException("not supported"); + } + + @Override + public int cardinality() + { + return length(); + } + + @Override + public int approximateCardinality() + { + return length(); + } + + @Override + public int prevSetBit(int i) + { + throw new UnsupportedOperationException("not supported"); + } + + @Override + public int nextSetBit(int i) + { + throw new UnsupportedOperationException("not supported"); + } + + @Override + public long ramBytesUsed() + { + return 2 * Integer.BYTES; + } + } + + class OneToOneOrdinalsView implements OrdinalsView + { + // The number of ordinals in the segment. If we see a rowId greater than or equal to this, we know it's not in + // the graph. + private final int size; + + public OneToOneOrdinalsView(int size) + { + this.size = size; + } + + @Override + public int getOrdinalForRowId(int rowId) throws IOException + { + if (rowId >= size) + return -1; + return rowId; + } + + @Override + public void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException + { + // risk of overflow + assert endRowId < Integer.MAX_VALUE : "endRowId must be less than Integer.MAX_VALUE"; + assert endRowId >= startRowId : "endRowId must be greater than or equal to startRowId"; + + int start = Math.max(startRowId, 0); + int end = Math.min(endRowId + 1, size); + for (int rowId = start; rowId < end; rowId++) + consumer.accept(rowId, rowId); + } + + @Override + public BitSet buildOrdinalBits(int startRowId, int endRowId, Supplier unused) throws IOException + { + int start = Math.max(startRowId, 0); + int end = Math.min(endRowId, size - 1); + + return new MatchRangeBits(start, end); + } + + @Override + public void close() + { + // noop + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskVectorValues.java b/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskVectorValues.java new file mode 100644 index 000000000000..9968f6a9d534 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskVectorValues.java @@ -0,0 +1,152 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import java.io.IOException; + +import io.github.jbellis.jvector.graph.RandomAccessVectorValues; +import io.github.jbellis.jvector.util.ExplicitThreadLocal; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.io.util.RandomAccessReader; + +/** + * Reads vectors from a file indexed by ordinal position. + *

    + * This class provides random access to vectors stored on disk by ordinal. + * Vectors are expected to be stored at positions calculated as: ordinal * dimension * Float.BYTES. + *

    + * The reader supports: + * - Random access by ordinal via getVector(int) + * - Determining the total number of vectors in the file + * - Creating independent copies for concurrent access + *

    + * This class is thread-safe. + * It should only be used within the vector index package. + */ +public class OnDiskVectorValues implements RandomAccessVectorValues, AutoCloseable +{ + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + // Because of the way RandomAccessVectorValues are used within jvector, this is the safest solution + // at the moment. See https://github.com/datastax/jvector/issues/635. + private final ExplicitThreadLocal threadLocalRandomAccessReader; + private final int dimension; + private final long vectorSize; + + /** + * Creates a new reader for vectors of the specified dimension. + * + * @param file the file containing vectors written by VectorByOrdinalWriter + * @param dimension the dimension of vectors in the file + */ + public OnDiskVectorValues(File file, int dimension) + { + this.threadLocalRandomAccessReader = ExplicitThreadLocal.withInitial(() -> RandomAccessReader.open(file)); + this.dimension = dimension; + this.vectorSize = (long) dimension * Float.BYTES; + } + + /** + * Returns the total number of vectors in the file. + * This is calculated based on the file size and vector dimension. + */ + @Override + public int size() + { + return (int) (threadLocalRandomAccessReader.get().length() / vectorSize); + } + + /** + * Returns the dimension of vectors in this file. + */ + @Override + public int dimension() + { + return dimension; + } + + /** + * Reads and returns the vector at the specified ordinal position. + * + * @param ordinal the ordinal position to read from + * @return the vector at the specified position + * @throws RuntimeException if an I/O error occurs + */ + @Override + public VectorFloat getVector(int ordinal) + { + try + { + var reader = threadLocalRandomAccessReader.get(); + reader.seek(ordinal * vectorSize); + return vts.readFloatVector(reader, dimension); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + /** + * Returns false, indicating that vectors are not shared between calls to getVector. + */ + @Override + public boolean isValueShared() + { + return false; + } + + /** + * Returns an instance of self since the implementation is completely thread safe. + * + * @return self + */ + @Override + public RandomAccessVectorValues copy() + { + // The only shared state are thread local readers only used within this class, so it is safe to share them + return this; + } + + /** + * Returns the underlying file being read. + */ + File getFile() + { + return threadLocalRandomAccessReader.get().getFile(); + } + + /** + * Returns the size in bytes of each vector in the file. + */ + long getVectorSize() + { + return vectorSize; + } + + @Override + public void close() + { + // Safely closes all readers, which is important because jvector doesn't handle closing them correctly + // at the moment. + FileUtils.closeQuietly(threadLocalRandomAccessReader); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskVectorValuesWriter.java b/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskVectorValuesWriter.java new file mode 100644 index 000000000000..9c6c36769258 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/OnDiskVectorValuesWriter.java @@ -0,0 +1,124 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import java.io.Closeable; +import java.io.IOException; + +import io.github.jbellis.jvector.disk.BufferedRandomAccessWriter; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import org.apache.cassandra.io.util.File; + +/** + * Writes vectors to a file indexed by ordinal position. + *

    + * This class provides efficient sequential and sparse writing of vectors to disk. + * Vectors are stored at positions calculated as: ordinal * dimension * Float.BYTES. + * This allows for direct random access reading by ordinal. + *

    + * The writer supports: + * - Sequential writes (ordinal increases by 1 each time) + * - Sparse writes (ordinals can have gaps, which are left as zeros) + * - Efficient buffering via BufferedRandomAccessWriter + * This class is not thread-safe and should only be used within the vector index package. + */ +public class OnDiskVectorValuesWriter implements Closeable +{ + private final int dimension; + private final BufferedRandomAccessWriter bufferedWriter; + private int lastOrdinal; + + /** + * Creates a new writer for vectors of the specified dimension. + * + * @param file the file to write vectors to + * @param dimension the dimension of vectors to be written + * @throws IOException if an I/O error occurs + */ + public OnDiskVectorValuesWriter(File file, int dimension) throws IOException + { + this.bufferedWriter = new BufferedRandomAccessWriter(file.toPath()); + this.dimension = dimension; + this.lastOrdinal = -1; + } + + /** + * Writes a vector at the specified ordinal position. + *

    + * Ordinals must be written in increasing order. If there are gaps between ordinals, + * the file will contain zeros at those positions. + * + * @param ordinal the ordinal position for this vector (must be greater than the last written ordinal) + * @param vector the vector to write (must have the same dimension as specified in constructor) + * @throws IOException if an I/O error occurs + * @throws AssertionError if ordinal is not greater than the last written ordinal, or if seeking backwards + */ + public void write(int ordinal, VectorFloat vector) throws IOException + { + assert ordinal > lastOrdinal : "Unexpected ordinal " + ordinal + " must be greater than " + lastOrdinal; + assert vector != null : "Vector is null"; + assert vector.length() == dimension : "Incorrect vector dimension " + vector.length() + " != " + dimension; + + // We are careful to only skip or call position() when necessary because the BufferedRandomAccessWriter always + // flushes the buffer for each of those operations. See https://github.com/datastax/jvector/issues/562. + if (ordinal != lastOrdinal + 1) + { + // Skip to the correct position (ensuring that we only skip forward). Note that if the ordinal + // is the segmentRowId and there are duplicates, we will skip some positions. This works in conjunction + // with the posting list logic. + long targetPosition = ordinal * Float.BYTES * (long) dimension; + assert bufferedWriter.position() <= targetPosition : "bufferedWriter.position()=" + bufferedWriter.position() + " > targetPosition=" + targetPosition; + bufferedWriter.seek(targetPosition); + } + + // Update the last ordinal + lastOrdinal = ordinal; + + // Write the vector data + vector.writeTo(bufferedWriter); + } + + /** + * Returns the dimension of vectors being written. + */ + int getDimension() + { + return dimension; + } + + /** + * Returns the last ordinal that was written, or -1 if no vectors have been written yet. + */ + public int getLastOrdinal() + { + return lastOrdinal; + } + + /** + * Returns the current position in the file. + */ + long position() throws IOException + { + return bufferedWriter.position(); + } + + @Override + public void close() throws IOException + { + bufferedWriter.close(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/OptimizeFor.java b/src/java/org/apache/cassandra/index/sai/disk/vector/OptimizeFor.java new file mode 100644 index 000000000000..7c6ab72f6cb7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/OptimizeFor.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +public enum OptimizeFor +{ + LATENCY, + RECALL; + + public static OptimizeFor fromString(String value) + { + return valueOf(value.toUpperCase()); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/OrdinalsView.java b/src/java/org/apache/cassandra/index/sai/disk/vector/OrdinalsView.java new file mode 100644 index 000000000000..60890730a7b7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/OrdinalsView.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.IOException; +import java.util.function.Supplier; + +import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.util.SparseBits; + +public interface OrdinalsView extends AutoCloseable +{ + interface OrdinalConsumer + { + void accept(int rowId, int ordinal) throws IOException; + } + + /** return the vector ordinal associated with the given row, or -1 if no vectors are associated with it */ + int getOrdinalForRowId(int rowId) throws IOException; + + /** + * iterates over all ordinals in the view. order of iteration is undefined. Only calls consumer for valid mappings + * from row id to ordinal. + */ + void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException; + + default Bits buildOrdinalBits(int startRowId, int endRowId, Supplier bitsSupplier) throws IOException + { + var bits = bitsSupplier.get(); + this.forEachOrdinalInRange(startRowId, endRowId, (segmentRowId, ordinal) -> { + bits.set(ordinal); + }); + return bits; + } + + @Override + void close(); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RamAwareVectorValues.java b/src/java/org/apache/cassandra/index/sai/disk/vector/RamAwareVectorValues.java similarity index 90% rename from src/java/org/apache/cassandra/index/sai/disk/v1/vector/RamAwareVectorValues.java rename to src/java/org/apache/cassandra/index/sai/disk/vector/RamAwareVectorValues.java index 4e76b443f4cb..d7ee9b0e7d9e 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/vector/RamAwareVectorValues.java +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/RamAwareVectorValues.java @@ -16,11 +16,11 @@ * limitations under the License. */ -package org.apache.cassandra.index.sai.disk.v1.vector; +package org.apache.cassandra.index.sai.disk.vector; import io.github.jbellis.jvector.graph.RandomAccessVectorValues; -public interface RamAwareVectorValues extends RandomAccessVectorValues +public interface RamAwareVectorValues extends RandomAccessVectorValues { - float[] vectorValue(int i); + long ramBytesUsed(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/RamEstimation.java b/src/java/org/apache/cassandra/index/sai/disk/vector/RamEstimation.java new file mode 100644 index 000000000000..bb1bee76a5f1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/RamEstimation.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import io.github.jbellis.jvector.util.RamUsageEstimator; + +public class RamEstimation +{ + /** + * @param externalNodeCount the size() of the ConcurrentHashMap + * @return an estimate of the number of bytes used + */ + public static long concurrentHashMapRamUsed(int externalNodeCount) { + long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; + long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER; + long CORES = Runtime.getRuntime().availableProcessors(); + + long chmNodeBytes = + REF_BYTES // node itself in Node[] + + 3L * REF_BYTES + + Integer.BYTES; // node internals + float chmLoadFactor = 0.75f; // this is hardcoded inside ConcurrentHashMap + // CHM has a striped counter Cell implementation, we expect at most one per core + long chmCounters = AH_BYTES + CORES * (REF_BYTES + Long.BYTES); + + double nodeCount = externalNodeCount / chmLoadFactor; + + return + (long) nodeCount * (chmNodeBytes + REF_BYTES)// nodes + + AH_BYTES // nodes array + + Long.BYTES + + 3 * Integer.BYTES + + 3 * REF_BYTES // extra internal fields + + chmCounters + + REF_BYTES; // the Map reference itself + } + + /** + * @param elementCount the size() of the DenseIntMap + * @return an estimate of the number of bytes used by a DenseIntMap + */ + public static long denseIntMapRamUsed(int elementCount) { + long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; + long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER; + long RWLOCK_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + 3 * REF_BYTES; // Approx. size for ReadWriteLock + long ATOMIC_INT_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + Integer.BYTES + REF_BYTES; // AtomicInteger overhead + + // Find power of 2 greater than or equal to elementCount + int capacity = 1; + while (capacity < elementCount) { + capacity <<= 1; + } + + // Calculate size for AtomicReferenceArray with for capacity elements + long atomicRefArrayBytes = AH_BYTES + capacity * REF_BYTES; + + return RWLOCK_BYTES // Size of the ReadWriteLock object + + ATOMIC_INT_BYTES // Size of the AtomicInteger + + atomicRefArrayBytes; // Size of the AtomicReferenceArray structure + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/RowIdsView.java b/src/java/org/apache/cassandra/index/sai/disk/vector/RowIdsView.java new file mode 100644 index 000000000000..16d5db1ee6fe --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/RowIdsView.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.io.IOException; +import java.util.PrimitiveIterator; + +public interface RowIdsView extends AutoCloseable +{ + PrimitiveIterator.OfInt getSegmentRowIdsMatching(int vectorOrdinal) throws IOException; + + @Override + void close(); +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/VectorCompression.java b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorCompression.java new file mode 100644 index 000000000000..d1d034b34bc2 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorCompression.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.util.Objects; + +public class VectorCompression +{ + public static final VectorCompression NO_COMPRESSION = new VectorCompression(CompressionType.NONE, -1, -1); + + public final CompressionType type; + private final int originalSize; // in bytes + private final int compressedSize; // in bytes + + public VectorCompression(CompressionType type, int dimension, double ratio) + { + this.type = type; + this.originalSize = dimension * Float.BYTES; + this.compressedSize = (int) (originalSize * ratio); + } + + public VectorCompression(CompressionType type, int originalSize, int compressedSize) + { + this.type = type; + this.originalSize = originalSize; + this.compressedSize = compressedSize; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VectorCompression that = (VectorCompression) o; + if (type == CompressionType.NONE) + return that.type == CompressionType.NONE; + return originalSize == that.originalSize && compressedSize == that.compressedSize && type == that.type; + } + + @Override + public int hashCode() + { + return Objects.hash(type, getOriginalSize(), getCompressedSize()); + } + + public String toString() + { + return String.format("VectorCompression(%s, %d->%d)", type, originalSize, compressedSize); + } + + public int getOriginalSize() + { + if (type == CompressionType.NONE) + throw new UnsupportedOperationException(); + return originalSize; + } + + public int getCompressedSize() + { + if (type == CompressionType.NONE) + throw new UnsupportedOperationException(); + return compressedSize; + } + + public enum CompressionType + { + NONE, + PRODUCT_QUANTIZATION, + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/VectorMemtableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorMemtableIndex.java new file mode 100644 index 000000000000..8ec79f7027f6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorMemtableIndex.java @@ -0,0 +1,619 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.disk.vector; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NavigableSet; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +import com.google.common.util.concurrent.Runnables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.graph.SearchResult; +import io.github.jbellis.jvector.util.Bits; +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.agrona.collections.IntHashSet; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.v1.SegmentMetadata; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.memory.MemoryIndex; +import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyListUtil; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithScore; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RangeUtil; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.SortingIterator; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.concurrent.OpOrder; + +import static java.lang.Math.log; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.pow; + +public class VectorMemtableIndex extends AbstractMemtableIndex +{ + private static final Logger logger = LoggerFactory.getLogger(VectorMemtableIndex.class); + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + public static int GLOBAL_BRUTE_FORCE_ROWS = Integer.MAX_VALUE; // not final so test can inject its own setting + + private final ColumnQueryMetrics.VectorIndexMetrics columnQueryMetrics; + private final CassandraOnHeapGraph graph; + private final LongAdder writeCount = new LongAdder(); + private final LongAdder overwriteCount = new LongAdder(); + private final LongAdder removedCount = new LongAdder(); + + private PrimaryKey minimumKey; + private PrimaryKey maximumKey; + + private final NavigableSet primaryKeys = new ConcurrentSkipListSet<>(); + + public VectorMemtableIndex(IndexContext indexContext, Memtable mt) + { + super(indexContext, mt); + this.columnQueryMetrics = (ColumnQueryMetrics.VectorIndexMetrics) indexContext.getColumnQueryMetrics(); + this.graph = new CassandraOnHeapGraph<>(indexContext, true, mt); + } + + @Override + public Memtable getMemtable() + { + return memtable; + } + + @Override + public void index(DecoratedKey key, Clustering clustering, ByteBuffer value, Memtable memtable, OpOrder.Group opGroup) + { + if (value == null || value.remaining() == 0) + return; + + PrimaryKey primaryKey = indexContext.keyFactory().create(key, clustering); + long allocatedBytes = index(primaryKey, value); + memtable.markExtraOnHeapUsed(allocatedBytes, opGroup); + onIndexUpdated(); + } + + private long index(PrimaryKey primaryKey, ByteBuffer value) + { + if (value == null || value.remaining() == 0) + return 0; + + updateKeyBounds(primaryKey); + + writeCount.increment(); + primaryKeys.add(primaryKey); + return graph.add(value, primaryKey); + } + + @Override + public void update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue, Memtable memtable, OpOrder.Group opGroup) + { + int oldRemaining = oldValue == null ? 0 : oldValue.remaining(); + int newRemaining = newValue == null ? 0 : newValue.remaining(); + if (oldRemaining == 0 && newRemaining == 0) + return; + + boolean different; + if (oldRemaining != newRemaining) + { + assert oldRemaining == 0 || newRemaining == 0; // one of them is null + different = true; + } + else + { + different = indexContext.getValidator().compare(oldValue, newValue) != 0; + } + + if (different) + { + PrimaryKey primaryKey = indexContext.keyFactory().create(key, clustering); + // update bounds because only rows with vectors are included in the key bounds, + // so if the vector was null before, we won't have included it + updateKeyBounds(primaryKey); + + // make the changes in this order so we don't have a window where the row is not in the index at all + if (newRemaining > 0) + { + graph.add(newValue, primaryKey); + overwriteCount.increment(); + } + if (oldRemaining > 0) + graph.remove(oldValue, primaryKey); + + // remove primary key if it's no longer indexed + if (newRemaining <= 0 && oldRemaining > 0) + { + primaryKeys.remove(primaryKey); + removedCount.increment(); + } + + onIndexUpdated(); + } + } + + @Override + public void update(DecoratedKey key, Clustering clustering, Iterator oldValues, Iterator newValues, Memtable memtable, OpOrder.Group opGroup) + { + throw new UnsupportedOperationException("Vector index does not support multi-value updates"); + } + + private void updateKeyBounds(PrimaryKey primaryKey) { + if (minimumKey == null) + minimumKey = primaryKey; + else if (primaryKey.compareTo(minimumKey) < 0) + minimumKey = primaryKey; + if (maximumKey == null) + maximumKey = primaryKey; + else if (primaryKey.compareTo(maximumKey) > 0) + maximumKey = primaryKey; + } + + @Override + public KeyRangeIterator search(QueryContext context, Expression expr, AbstractBounds keyRange) + { + if (expr.getOp() != Expression.Op.BOUNDED_ANN) + throw new IllegalArgumentException(indexContext.logMessage("Only BOUNDED_ANN is supported, received: " + expr)); + var qv = vts.createFloatVector(expr.lower.value.vector); + float threshold = expr.getEuclideanSearchThreshold(); + + SortingIterator.Builder keyQueue; + // Threshold queries do not use pruning. + try (var pkIterator = searchInternal(context, qv, keyRange, graph.size(), graph.size(), threshold, false)) + { + keyQueue = new SortingIterator.Builder<>(); + while (pkIterator.hasNext()) + keyQueue.add(pkIterator.next().primaryKey()); + } + + if (keyQueue.size() == 0) + return KeyRangeIterator.empty(); + return new ReorderingKeyRangeIterator(keyQueue.build(Comparator.naturalOrder()), keyQueue.size()); + } + + @Override + public long estimateMatchingRowsCount(Expression expression) + { + // For BOUNDED_ANN we use the old way of estimating cardinality - by running the search. + throw new UnsupportedOperationException("Cardinality estimation not supported by vector indexes"); + } + + @Override + public List> orderBy(QueryContext context, + Orderer orderer, + Expression slice, + AbstractBounds keyRange, + int limit) + { + assert slice == null : "ANN does not support index slicing"; + assert orderer.isANN() : "Only ANN is supported for vector search, received " + orderer.operator; + + var qv = orderer.getVectorTerm(); + var rerankK = orderer.rerankKFor(limit, VectorCompression.NO_COMPRESSION); + + return List.of(searchInternal(context, qv, keyRange, limit, rerankK, 0, orderer.usePruning())); + } + + private CloseableIterator searchInternal(QueryContext context, + VectorFloat queryVector, + AbstractBounds keyRange, + int limit, + int rerankK, + float threshold, + boolean usePruning) + { + Bits bits; + if (RangeUtil.coversFullRing(keyRange)) + { + bits = Bits.ALL; + } + else + { + // if left bound is MIN_BOUND or KEY_BOUND, we need to include all token-only PrimaryKeys with same token + boolean leftInclusive = keyRange.left.kind() != PartitionPosition.Kind.MAX_BOUND; + // if right bound is MAX_BOUND or KEY_BOUND, we need to include all token-only PrimaryKeys with same token + boolean rightInclusive = keyRange.right.kind() != PartitionPosition.Kind.MIN_BOUND; + // if right token is MAX (Long.MIN_VALUE), there is no upper bound + boolean isMaxToken = keyRange.right.getToken().isMinimum(); // max token + + PrimaryKey left = indexContext.keyFactory().createTokenOnly(keyRange.left.getToken()); // lower bound + PrimaryKey right = isMaxToken ? null : indexContext.keyFactory().createTokenOnly(keyRange.right.getToken()); // upper bound + + NavigableSet resultKeys = isMaxToken ? primaryKeys.tailSet(left, leftInclusive) + : primaryKeys.subSet(left, leftInclusive, right, rightInclusive); + + if (resultKeys.isEmpty()) + return CloseableIterator.emptyIterator(); + + int bruteForceRows = maxBruteForceRows(rerankK, resultKeys.size(), graph.size()); + logger.trace("Search range covers {} rows; max brute force rows is {} for memtable index with {} nodes, rerankK {}, LIMIT {}", + resultKeys.size(), bruteForceRows, graph.size(), rerankK, limit); + Tracing.trace("Search range covers {} rows; max brute force rows is {} for memtable index with {} nodes, rerankK {}, LIMIT {}", + resultKeys.size(), bruteForceRows, graph.size(), rerankK, limit); + if (resultKeys.size() <= bruteForceRows) + { + // When we have a threshold, we only need to filter the results, not order them, because it means we're + // evaluating a boolean predicate in the SAI pipeline that wants to collate by PK + if (threshold > 0) + return filterByBruteForce(queryVector, threshold, resultKeys); + else + return orderByBruteForce(queryVector, resultKeys); + } + else + { + bits = new KeyRangeFilteringBits(keyRange); + } + } + + var nodeScoreIterator = graph.search(context, queryVector, limit, rerankK, threshold, usePruning, bits); + return new NodeScoreToScoredPrimaryKeyIterator(nodeScoreIterator); + } + + + @Override + public CloseableIterator orderResultsBy(QueryContext context, List keys, Orderer orderer, int limit) + { + if (minimumKey == null) + // This case implies maximumKey is empty too. + return CloseableIterator.emptyIterator(); + + assert orderer.isANN() : "Only ANN is supported for vector search, received " + orderer; + // Compute the keys that exist in the current memtable and their corresponding graph ordinals + var keysInGraph = new HashSet(); + var relevantOrdinals = new IntHashSet(); + + var keysInRange = PrimaryKeyListUtil.getKeysInRange(keys, minimumKey, maximumKey); + boolean isStatic = indexContext.getDefinition().isStatic(); + + keysInRange.forEach(k -> + { + // if the indexed column is static, we need to get the static row associated with the non-static row that + // might be referenced by the key + if (isStatic) + k = k.forStaticRow(); + + var v = graph.vectorForKey(k); + if (v == null) + return; + var i = graph.getOrdinal(v); + if (i < 0) + // might happen if the vector and/or its postings have been removed in the meantime between getting the + // vector and getting the ordinal (graph#vectorForKey and graph#getOrdinal are not synchronized) + return; + keysInGraph.add(k); + relevantOrdinals.add(i); + }); + + int rerankK = orderer.rerankKFor(limit, VectorCompression.NO_COMPRESSION); + int maxBruteForceRows = maxBruteForceRows(rerankK, relevantOrdinals.size(), graph.size()); + Tracing.logAndTrace(logger, "{} rows relevant to current memtable out of {} materialized by SAI; max brute force rows is {} for memtable index with {} nodes, rerankK {}", + relevantOrdinals.size(), keys.size(), maxBruteForceRows, graph.size(), rerankK); + + // convert the expression value to query vector + var qv = orderer.getVectorTerm(); + // brute force path + if (keysInGraph.size() <= maxBruteForceRows) + { + if (keysInGraph.isEmpty()) + return CloseableIterator.emptyIterator(); + return orderByBruteForce(qv, keysInGraph); + } + // indexed path + var nodeScoreIterator = graph.search(context, qv, limit, rerankK, 0, orderer.usePruning(), relevantOrdinals::contains); + return new NodeScoreToScoredPrimaryKeyIterator(nodeScoreIterator); + } + + /** + * Filter the keys in the provided set by comparing their vectors to the query vector and returning only those + * that have a similarity score >= the provided threshold. + * NOTE: because the threshold is not used for ordering, the result is returned in PK order, not score order. + * @param queryVector the query vector + * @param threshold the minimum similarity score to accept + * @param keys the keys to filter + * @return an iterator over the keys that pass the filter in PK order + */ + private CloseableIterator filterByBruteForce(VectorFloat queryVector, float threshold, NavigableSet keys) + { + columnQueryMetrics.onBruteForceNodesReranked(keys.size()); + // Keys are already ordered in ascending PK order, so just use an ArrayList to collect the results. + var results = new ArrayList(keys.size()); + scoreKeysAndAddToCollector(queryVector, keys, threshold, results); + return CloseableIterator.wrap(results.iterator()); + } + + private CloseableIterator orderByBruteForce(VectorFloat queryVector, Collection keys) + { + columnQueryMetrics.onBruteForceNodesReranked(keys.size()); + // Use a sorting iterator because we often don't need to consume the entire iterator + var similarityFunction = indexContext.getIndexWriterConfig().getSimilarityFunction(); + return SortingIterator.createCloseable(Comparator.naturalOrder(), + keys, + key -> scoreKey(similarityFunction, queryVector, key, 0), + Runnables.doNothing()); + } + + private void scoreKeysAndAddToCollector(VectorFloat queryVector, + Collection keys, + float threshold, + Collection collector) + { + var similarityFunction = indexContext.getIndexWriterConfig().getSimilarityFunction(); + for (var key : keys) + { + var scored = scoreKey(similarityFunction, queryVector, key, threshold); + if (scored != null) + collector.add(scored); + } + } + + private PrimaryKeyWithScore scoreKey(VectorSimilarityFunction similarityFunction, VectorFloat queryVector, PrimaryKey key, float threshold) + { + var vector = graph.vectorForKey(key); + if (vector == null) + return null; + var score = similarityFunction.compare(queryVector, vector); + if (score < threshold) + return null; + return new PrimaryKeyWithScore(indexContext, memtable, key, score); + } + + private int maxBruteForceRows(int rerankK, int nPermittedOrdinals, int graphSize) + { + int expectedNodesVisited = expectedNodesVisited(rerankK, nPermittedOrdinals, graphSize); + return min(max(rerankK, expectedNodesVisited), GLOBAL_BRUTE_FORCE_ROWS); + } + + public int estimateAnnNodesVisited(int rerankK, int nPermittedOrdinals) + { + return expectedNodesVisited(rerankK, nPermittedOrdinals, graph.size()); + } + + /** + * All parameters must be greater than zero. nPermittedOrdinals may be larger than graphSize. + *

    + * Returns the expected number of nodes visited by an ANN search. + * !!! + * !!! "Visted" means we compute the coarse similarity with the query vector. This is + * !!! roughly `degree` times larger than the number of nodes whose edge lists we load! + * !!! + */ + public static int expectedNodesVisited(int rerankK, int nPermittedOrdinals, int graphSize) + { + var K = rerankK; + var B = min(nPermittedOrdinals, graphSize); + var N = graphSize; + // These constants come from running many searches on a variety of datasets and graph sizes. + // * It is very consistent that the visited count is slightly less than linear wrt K, for both + // unconstrained (B = N) and constrained (B < N) searches. + // * The behavior wrt B is hard to characterize. Graphing the result F vs N/B shows ranges of + // growth very close to linear, interspersed with sharp jumps up to a higher visit count. Overall, + // approximating it as linear is in the right ballpark. + // * For unconstrained searches, the visited count is closest to log(N) but for constrained searches + // it is closer to log(N)**2 (or a higher exponent), perhaps as a result of N/B being too small. + // + // If we need to make this even more accurate, the relationship to B and to log(N) may be the best + // places to start. + var raw = (int) (100 + 0.025 * pow(log(N), 2) * pow(K, 0.95) * ((double) N / B)); + return ensureSaneEstimate(raw, rerankK, graphSize); + } + + public static int ensureSaneEstimate(int rawEstimate, int rerankK, int graphSize) + { + // we will always visit at least min(rerankK, graphSize) nodes, and we can't visit more nodes than exist in the graph + return min(max(rawEstimate, min(rerankK, graphSize)), graphSize); + } + + @Override + public Iterator>> iterator(DecoratedKey min, DecoratedKey max) + { + // This method is only used when merging an in-memory index with a RowMapping. This is done a different + // way with the graph using the writeData method below. + throw new UnsupportedOperationException(); + } + + /** returns true if the index is non-empty and should be flushed */ + public boolean preFlush(ToIntFunction ordinalMapper) + { + return graph.preFlush(ordinalMapper); + } + + @Override + public int getRowCount() + { + return graph.size(); + } + + @Override + public long getApproximateTermCount() + { + throw new UnsupportedOperationException("Getting number of terms not supported by vector indexes"); + } + + public SegmentMetadata.ComponentMetadataMap writeData(IndexComponents.ForWrite perIndexComponents) throws IOException + { + // Note that range deletions won't show up in the removed count, which is why it's just named removedCount and + // not deleted count. + logger.debug("Writing {} nodes to disk after {} inserts, {} overwrites, and {} removals for {}", graph.size(), + writeCount.longValue(), overwriteCount.longValue(), removedCount.longValue(), perIndexComponents.descriptor()); + return graph.flush(perIndexComponents); + } + + @Override + public long writeCount() + { + return writeCount.longValue() + overwriteCount.longValue(); + } + + @Override + public long estimatedOnHeapMemoryUsed() + { + return graph.ramBytesUsed(); + } + + @Override + public long estimatedOffHeapMemoryUsed() + { + return 0; + } + + @Override + public boolean isEmpty() + { + return graph.isEmpty(); + } + + @Nullable + @Override + public ByteBuffer getMinTerm() + { + return null; + } + + @Nullable + @Override + public ByteBuffer getMaxTerm() + { + return null; + } + + /* + * A {@link Bits} implementation that filters out all ordinals that do not correspond to a {@link PrimaryKey} + * in the provided {@link AbstractBounds}. + */ + private class KeyRangeFilteringBits implements Bits + { + private final AbstractBounds keyRange; + + public KeyRangeFilteringBits(AbstractBounds keyRange) + { + this.keyRange = keyRange; + } + + @Override + public boolean get(int ordinal) + { + var keys = graph.keysFromOrdinal(ordinal); + return keys.stream().anyMatch(k -> keyRange.contains(k.partitionKey())); + } + } + + private class ReorderingKeyRangeIterator extends KeyRangeIterator + { + private final SortingIterator keyQueue; + + ReorderingKeyRangeIterator(SortingIterator keyQueue, int expectedSize) + { + super(minimumKey, maximumKey, expectedSize); + this.keyQueue = keyQueue; + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + keyQueue.skipTo(nextKey); + } + + @Override + public void close() {} + + @Override + protected PrimaryKey computeNext() + { + if (!keyQueue.hasNext()) + return endOfData(); + return keyQueue.next(); + } + } + + /** + * An iterator over {@link PrimaryKeyWithSortKey} sorted by score descending. The iterator converts ordinals (node ids) + * to {@link PrimaryKey}s and pairs them with the score given by the index. + */ + private class NodeScoreToScoredPrimaryKeyIterator extends AbstractIterator + { + private final CloseableIterator nodeScores; + private Iterator primaryKeysForNode = Collections.emptyIterator(); + + NodeScoreToScoredPrimaryKeyIterator(CloseableIterator nodeScores) + { + this.nodeScores = nodeScores; + } + + @Override + protected PrimaryKeyWithSortKey computeNext() + { + if (primaryKeysForNode.hasNext()) + return primaryKeysForNode.next(); + + while (nodeScores.hasNext()) + { + SearchResult.NodeScore nodeScore = nodeScores.next(); + primaryKeysForNode = graph.keysFromOrdinal(nodeScore.node) + .stream() + .map(pk -> new PrimaryKeyWithScore(indexContext, memtable, pk, nodeScore.score)) + .iterator(); + if (primaryKeysForNode.hasNext()) + return primaryKeysForNode.next(); + } + + return endOfData(); + } + + @Override + public void close() + { + FileUtils.closeQuietly(nodeScores); + } + } + + /** ensures that the graph is connected -- normally not necessary but it can help tests reason about the state */ + public void cleanup() + { + graph.cleanup(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/VectorPostings.java b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorPostings.java new file mode 100644 index 000000000000..590e5aab456f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorPostings.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.ToIntFunction; + +import com.google.common.base.Preconditions; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import net.openhft.chronicle.bytes.Bytes; +import net.openhft.chronicle.hash.serialization.BytesReader; +import net.openhft.chronicle.hash.serialization.BytesWriter; +import net.openhft.chronicle.map.MapEntry; +import org.agrona.collections.Int2IntHashMap; +import org.agrona.collections.IntArrayList; + +public class VectorPostings +{ + // we expect that the overwhelmingly most common cardinality will be 1, so optimize for reads using COWAL + final CopyOnWriteArrayList postings; + volatile int ordinal = -1; + + private volatile IntArrayList rowIds; // initially null; gets filled in on flush by computeRowIds + + public VectorPostings(T firstKey) + { + postings = new CopyOnWriteArrayList<>(List.of(firstKey)); + } + + public VectorPostings(List raw) + { + postings = new CopyOnWriteArrayList<>(raw); + } + + /** + * Split out from constructor only to make dealing with concurrent inserts easier for CassandraOnHeapGraph. + * Should be called at most once per instance. + */ + public void setOrdinal(int ordinal) + { + assert this.ordinal == -1 : String.format("ordinal already set to %d; attempted to set to %d", this.ordinal, ordinal); + this.ordinal = ordinal; + } + + public boolean add(T key) + { + for (T existing : postings) + if (existing.equals(key)) + return false; + postings.add(key); + return true; + } + + /** + * @return true if current ordinal is removed by partition/range deletion. + * Must be called after computeRowIds. + */ + public boolean shouldAppendDeletedOrdinal() + { + return !postings.isEmpty() && (rowIds != null && rowIds.isEmpty()); + } + + /** + * Compute the rowIds corresponding to the < T > keys in this postings list. + */ + public void computeRowIds(ToIntFunction postingTransformer) + { + Preconditions.checkState(rowIds == null); + + IntArrayList ids = new IntArrayList(postings.size(), -1); + for (T key : postings) + { + int rowId = postingTransformer.applyAsInt(key); + // partition deletion and range deletion won't trigger index update. There is no row id for given key during flush + if (rowId >= 0) + ids.add(rowId); + } + + rowIds = ids; + } + + /** + * @return rowIds corresponding to the < T > keys in this postings list. + * Must be called after computeRowIds. + */ + public IntArrayList getRowIds() + { + Preconditions.checkNotNull(rowIds); + return rowIds; + } + + public void remove(T key) + { + postings.remove(key); + } + + public long ramBytesUsed() + { + return emptyBytesUsed() + postings.size() * bytesPerPosting(); + } + + public static long emptyBytesUsed() + { + long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; + long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER; + return Integer.BYTES + REF_BYTES + AH_BYTES; + } + + // we can't do this exactly without reflection, because keys could be Integer or PrimaryKey. + // PK is larger, so we'll take that and return an upper bound. + // we already count the float[] vector in vectorValues, so leave it out here + public long bytesPerPosting() + { + long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; + return REF_BYTES + + 2 * Long.BYTES // hashes in PreHashedDecoratedKey + + REF_BYTES; // key ByteBuffer, this is used elsewhere so we don't take the deep size + } + + public int size() + { + return postings.size(); + } + + public List getPostings() + { + return postings; + } + + public boolean isEmpty() + { + return postings.isEmpty(); + } + + public int getOrdinal() + { + return getOrdinal(true); + } + + public int getOrdinal(boolean assertSet) + { + assert !assertSet || ordinal >= 0 : "ordinal not set"; + return ordinal; + } + + public static class CompactionVectorPostings extends VectorPostings { + public CompactionVectorPostings(int ordinal, List raw) + { + super(raw); + this.ordinal = ordinal; + } + + public CompactionVectorPostings(int ordinal, int firstKey) + { + super(firstKey); + this.ordinal = ordinal; + } + + @Override + public void setOrdinal(int ordinal) + { + throw new UnsupportedOperationException(); + } + + @Override + public IntArrayList getRowIds() + { + var L = new IntArrayList(size(), -1); + for (var i : postings) + L.addInt(i); + return L; + } + + // CVP always contains int keys, so we don't have to be pessimistic on size like super does + @Override + public long bytesPerPosting() + { + long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF; + return REF_BYTES + Integer.BYTES; + } + } + + public static class Marshaller implements BytesReader, BytesWriter + { + @Override + public void write(Bytes out, CompactionVectorPostings postings) { + out.writeInt(postings.ordinal); + out.writeInt(postings.size()); + for (Integer posting : postings.getPostings()) { + out.writeInt(posting); + } + } + + @Override + public CompactionVectorPostings read(Bytes in, CompactionVectorPostings using) { + int ordinal = in.readInt(); + int size = in.readInt(); + assert size >= 0 : size; + CompactionVectorPostings cvp; + if (size == 1) { + cvp = new CompactionVectorPostings(ordinal, in.readInt()); + } + else + { + var postingsList = new IntArrayList(size, -1); + for (int i = 0; i < size; i++) + { + postingsList.add(in.readInt()); + } + cvp = new CompactionVectorPostings(ordinal, postingsList); + } + return cvp; + } + + /** + * Optimized method to extract the ordinal from the provided entry. Avoids unnecessary deserialization of + * the value. + * @param entry map entry to use when extracting the value's ordinal + * @return an ordinal + */ + public static int extractOrdinal(MapEntry entry) { + long offset = entry.value().offset(); + return entry.value().bytes().readInt(offset); + } + + /** + * Optimized method to extract the ordinal and row ids from the posting list, then insert the extras into + * the provided extraOrdinals map. The first row id must be the same as the ordinal. Avoids unnecessary + * allocations by iteratively reading integers from the entry's bytes. + * @param entry the entry from which to extract the ordinal and row ids + * @param extraOrdinals the map to add the row id to ordinal mapping + */ + public static void recordExtraOrdinals(MapEntry entry, Int2IntHashMap extraOrdinals) { + long offset = entry.value().offset(); + var postings = entry.value().bytes(); + int ordinal = postings.readInt(offset); + int size = postings.readInt(offset + 4); + int firstRowId = postings.readInt(offset + 8); + + assert ordinal == firstRowId : ordinal + " != " + firstRowId; // synthetic ordinals not allowed in ONE_TO_MANY + for (int i = 1; i < size; i++) + { + extraOrdinals.put(postings.readInt(offset + 8 + (4L * i)), ordinal); + } + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/VectorSourceModel.java b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorSourceModel.java new file mode 100644 index 000000000000..c55ff1a2e564 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorSourceModel.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import java.util.function.Function; + +import com.google.common.annotations.VisibleForTesting; + +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; + +import static io.github.jbellis.jvector.vector.VectorSimilarityFunction.COSINE; +import static io.github.jbellis.jvector.vector.VectorSimilarityFunction.DOT_PRODUCT; +import static java.lang.Math.max; +import static java.lang.Math.pow; +import static org.apache.cassandra.index.sai.disk.vector.VectorCompression.CompressionType.NONE; +import static org.apache.cassandra.index.sai.disk.vector.VectorCompression.CompressionType.PRODUCT_QUANTIZATION; +public enum VectorSourceModel +{ + ADA002((dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.125), 1.25, true), + OPENAI_V3_SMALL((dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.0625), 1.5, true), + OPENAI_V3_LARGE((dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.0625), 1.25, true), + // BERT is not known to have unit length vectors in all cases + BERT(COSINE, (dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.25), __ -> 1.0, false), + GECKO((dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.125), 1.25, true), + NV_QA_4((dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.125), 1.25, false), + // Cohere does not officially say they have unit length vectors, but some users report that they do + COHERE_V3((dimension) -> new VectorCompression(PRODUCT_QUANTIZATION, dimension, 0.0625), 1.25, false), + + OTHER(COSINE, VectorSourceModel::genericCompressionFor, VectorSourceModel::genericOverquery, false); + + /** + * Default similarity function for this model. + */ + public final VectorSimilarityFunction defaultSimilarityFunction; + /** + * Compression provider optimized for this model. + */ + public final Function compressionProvider; + /** + * Factor by which to multiply the top K requested by to search deeper in the graph. + * This is IN ADDITION to the tapered 2x applied by OverqueryUtils. + */ + public final Function overqueryProvider; + + /** + * Indicates that the model is known to have unit length vectors. When false, the runtime checks per graph + * until a non-unit length vector is found. + */ + private final boolean knownUnitLength; + + VectorSourceModel(Function compressionProvider, + double overqueryFactor, + boolean knownUnitLength) + { + this(DOT_PRODUCT, compressionProvider, __ -> overqueryFactor, knownUnitLength); + } + + VectorSourceModel(VectorSimilarityFunction defaultSimilarityFunction, + Function compressionProvider, + Function overqueryProvider, + boolean knownUnitLength) + { + this.defaultSimilarityFunction = defaultSimilarityFunction; + this.compressionProvider = compressionProvider; + this.overqueryProvider = overqueryProvider; + this.knownUnitLength = knownUnitLength; + } + + public boolean hasKnownUnitLengthVectors() + { + return knownUnitLength; + } + + public static VectorSourceModel fromString(String value) + { + return valueOf(value.toUpperCase()); + } + + private static VectorCompression genericCompressionFor(int dimension) + { + // Model is unspecified / unknown, so we guess. + return new VectorCompression(PRODUCT_QUANTIZATION, dimension * Float.BYTES, defaultPQBytesFor(dimension)); + } + + private static int defaultPQBytesFor(int originalDimension) + { + // the idea here is that higher dimensions compress well, but not so well that we should use fewer bits + // than a lower-dimension vector, which is what you could get with cutoff points to switch between (e.g.) + // D*0.5 and D*0.25. Thus, the following ensures that bytes per vector is strictly increasing with D. + int compressedBytes; + if (originalDimension <= 32) { + // We are compressing from 4-byte floats to single-byte codebook indexes, + // so this represents compression of 4x + // * GloVe-25 needs 25 BPV to achieve good recall + compressedBytes = originalDimension; + } + else if (originalDimension <= 64) { + // * GloVe-50 performs fine at 25 + compressedBytes = 32; + } + else if (originalDimension <= 200) { + // * GloVe-100 and -200 perform well at 50 and 100 BPV, respectively + compressedBytes = (int) (originalDimension * 0.5); + } + else if (originalDimension <= 400) { + // * NYTimes-256 actually performs fine at 64 BPV but we'll be conservative + // since we don't want BPV to decrease + compressedBytes = 100; + } + else if (originalDimension <= 768) { + // allow BPV to increase linearly up to 192 + compressedBytes = (int) (originalDimension * 0.25); + } + else if (originalDimension <= 1536) { + // * ada002 vectors have good recall even at 192 BPV = compression of 32x + compressedBytes = 192; + } + else { + // We have not tested recall with larger vectors than this, let's let it increase linearly + compressedBytes = (int) (originalDimension * 0.125); + } + return compressedBytes; + } + + private static double genericOverquery(VectorCompression vc) + { + assert vc != null; + // we compress extra-large vectors more aggressively, so we need to bump up the limit for those. + if ((double) vc.getOriginalSize() / vc.getCompressedSize() > 16.0) + return 1.5; + else + return 1.0; + } + + /** + * @param limit the number of results the user asked for + * @param vc compression information about vectors being queried + * @return the topK >= `limit` results to ask the index to search for, forcing + * the greedy search deeper into the graph. This serves two purposes: + * 1. Smoothes out the relevance difference between small LIMIT and large + * 2. Compensates for using lossily-compressed vectors during the search + */ + public int rerankKFor(int limit, VectorCompression vc) + { + // if the vectors are uncompressed, bump up the limit a bit to start with but decay it rapidly + if (vc.type == NONE) + { + var n = max(1.0, 0.979 + 4.021 * pow(limit, -0.761)); // f(1) = 5.0, f(100) = 1.1, f(1000) = 1.0 + return (int) (n * limit); + } + + // Most compressed vectors should be queried at ~2x as much as uncompressed vectors. (Our compression + // is tuned so that this should give us approximately the same recall as using uncompressed.) + // Again, we do want this to decay as we go to very large limits. + var n = tapered2x(limit); + + // per-model adjustment on top of the ~2x factor + int originalDimension = vc.getOriginalSize() / 4; + if (compressionProvider.apply(originalDimension).equals(vc)) + { + n *= overqueryProvider.apply(vc); + } + else + { + // we're using an older CV that wasn't created with the currently preferred parameters, + // so use the generic defaults instead + n *= OTHER.overqueryProvider.apply(vc); + } + + return (int) (n * limit); + } + + @VisibleForTesting + public static double tapered2x(int limit) + { + return max(1.0, 0.509 + 9.491 * pow(limit, -0.402)); // f(1) = 10.0, f(100) = 2.0, f(1000) = 1.1 + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/VectorValidation.java b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorValidation.java new file mode 100644 index 000000000000..b3130f8e3423 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/vector/VectorValidation.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.vector; + +import io.github.jbellis.jvector.vector.VectorSimilarityFunction; +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.apache.cassandra.exceptions.InvalidRequestException; + +public class VectorValidation +{ + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + // chosen to make sure dot products don't overflow + public static final float MAX_FLOAT32_COMPONENT = 1E17f; + + public static void checkInBounds(VectorFloat v) + { + for (int i = 0; i < v.length(); i++) + { + if (!Float.isFinite(v.get(i))) + { + throw new IllegalArgumentException("non-finite value at vector[" + i + "]=" + v.get(i)); + } + + if (Math.abs(v.get(i)) > MAX_FLOAT32_COMPONENT) + { + throw new IllegalArgumentException("Out-of-bounds value at vector[" + i + "]=" + v.get(i)); + } + } + } + + /** use with caution, it allocates a temporary VectorFloat */ + public static void validateIndexable(float[] raw, VectorSimilarityFunction similarityFunction) + { + validateIndexable(vts.createFloatVector(raw), similarityFunction); + } + + public static void validateIndexable(VectorFloat vector, VectorSimilarityFunction similarityFunction) + { + try + { + checkInBounds(vector); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException(e.getMessage()); + } + + if (similarityFunction == VectorSimilarityFunction.COSINE) + { + if (isEffectivelyZero(vector)) + throw new InvalidRequestException("Zero and near-zero vectors cannot be indexed or queried with cosine similarity"); + } + } + + public static boolean isEffectivelyZero(VectorFloat vector) + { + for (int i = 0; i < vector.length(); i++) + { + if (vector.get(i) < -1E-6 || vector.get(i) > 1E-6) + return false; + } + return true; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeAntiJoinIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeAntiJoinIterator.java new file mode 100644 index 000000000000..6743f0f5cc9f --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeAntiJoinIterator.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.iterators; + +import java.io.IOException; + +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.util.FileUtils; + +/** + * An iterator wrapper that wraps two iterators (left and right) and returns the primary keys from the left iterator + * that do not match the primary keys from the right iterator. The keys returned by the wrapped iterators must + * follow token-clustering order. + */ +public class KeyRangeAntiJoinIterator extends KeyRangeIterator +{ + final KeyRangeIterator left; + final KeyRangeIterator right; + + private PrimaryKey nextKeyToSkip = null; + + private KeyRangeAntiJoinIterator(KeyRangeIterator left, KeyRangeIterator right) + { + super(left.getMinimum(), left.getMaximum(), left.getMaxKeys()); + this.left = left; + this.right = right; + } + + public static KeyRangeAntiJoinIterator create(KeyRangeIterator left, KeyRangeIterator right) + { + return new KeyRangeAntiJoinIterator(left, right); + } + + protected void performSkipTo(PrimaryKey nextKey) + { + left.skipTo(nextKey); + + if (nextKeyToSkip == null || nextKeyToSkip.compareTo(nextKey) < 0) + right.skipTo(nextKey); + } + + public void close() throws IOException + { + FileUtils.close(left, right); + } + + protected PrimaryKey computeNext() + { + if (nextKeyToSkip == null) + nextKeyToSkip = right.nextOrNull(); + + PrimaryKey key = left.nextOrNull(); + int cmp = compare(key, nextKeyToSkip); + + while (key != null && cmp >= 0) + { + if (cmp == 0) + { + key = left.nextOrNull(); + } + else + { + right.skipTo(key); + } + nextKeyToSkip = right.nextOrNull(); + cmp = compare(key, nextKeyToSkip); + } + + return key != null ? key : endOfData(); + } + + private int compare(PrimaryKey key1, PrimaryKey key2) + { + return (key1 == null || key2 == null) ? -1 : key1.compareTo(key2); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeCollectionIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeCollectionIterator.java new file mode 100644 index 000000000000..a8d86441c4ed --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeCollectionIterator.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.iterators; + +import java.util.List; +import java.util.SortedSet; + +import com.google.common.collect.Iterators; +import com.google.common.collect.PeekingIterator; + +import org.apache.cassandra.index.sai.utils.PrimaryKey; + +/** + * A {@link KeyRangeIterator} that iterates over a collection of {@link PrimaryKey}s without modifying the underlying list. + */ +public class KeyRangeCollectionIterator extends KeyRangeIterator +{ + private final PeekingIterator keyQueue; + + /** + * Create a new {@link KeyRangeCollectionIterator} that iterates over the provided list of keys. + * @param minimumKey the minimum key for the provided list of keys + * @param maximumKey the maximum key for the provided list of keys + * @param keys the list of keys to iterate over + */ + public KeyRangeCollectionIterator(PrimaryKey minimumKey, PrimaryKey maximumKey, List keys) + { + super(minimumKey, maximumKey, keys.size()); + this.keyQueue = Iterators.peekingIterator(keys.iterator()); + } + + /** + * Create a new {@link KeyRangeCollectionIterator} that iterates over the provided set of keys. + * @param keys the sorted set of keys to iterate over + */ + public KeyRangeCollectionIterator(SortedSet keys) + { + super(keys.first(), keys.last(), keys.size()); + this.keyQueue = Iterators.peekingIterator(keys.iterator()); + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + while (keyQueue.hasNext()) + { + if (keyQueue.peek().compareTo(nextKey) >= 0) + break; + keyQueue.next(); + } + } + + @Override + public void close() {} + + @Override + protected PrimaryKey computeNext() + { + return keyQueue.hasNext() ? keyQueue.next() : endOfData(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeConcatIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeConcatIterator.java index cd47ff219be4..3a2bc21e2bc5 100644 --- a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeConcatIterator.java +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeConcatIterator.java @@ -17,175 +17,153 @@ */ package org.apache.cassandra.index.sai.iterators; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; import java.util.List; -import com.google.common.annotations.VisibleForTesting; - import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.util.FileUtils; /** - * {@link KeyRangeConcatIterator} takes a list of sorted range iterators and concatenates them, leaving duplicates in + * {@link KeyRangeConcatIterator} takes a list of sorted range iterator and concatenates them, leaving duplicates in * place, to produce a new stably sorted iterator. Duplicates are eliminated later in * {@link org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher} * as results from multiple SSTable indexes and their respective segments are consumed. - *

    + * * ex. (1, 2, 3) + (3, 3, 4, 5) -> (1, 2, 3, 3, 3, 4, 5) * ex. (1, 2, 2, 3) + (3, 4, 4, 6, 6, 7) -> (1, 2, 2, 3, 3, 4, 4, 6, 6, 7) + * */ public class KeyRangeConcatIterator extends KeyRangeIterator { public static final String MUST_BE_SORTED_ERROR = "RangeIterator must be sorted, previous max: %s, next min: %s"; - private final List ranges; + private final Iterator ranges; + private KeyRangeIterator currentRange; + private final List toRelease; - private int current; - - protected KeyRangeConcatIterator(KeyRangeIterator.Builder.Statistics statistics, List ranges, Runnable onClose) + protected KeyRangeConcatIterator(KeyRangeIterator.Builder.Statistics statistics, List ranges) { - super(statistics, onClose); + super(statistics); if (ranges.isEmpty()) throw new IllegalArgumentException("Cannot concatenate empty list of ranges"); - - this.current = 0; - this.ranges = ranges; + this.ranges = ranges.iterator(); + currentRange = this.ranges.next(); + this.toRelease = ranges; } @Override - protected void performSkipTo(PrimaryKey nextKey) + protected void performSkipTo(PrimaryKey primaryKey) { - while (current < ranges.size()) + while (true) { - KeyRangeIterator currentIterator = ranges.get(current); - - if (currentIterator.hasNext() && currentIterator.peek().compareTo(nextKey, false) >= 0) - break; - - if (currentIterator.getMaximum().compareTo(nextKey, false) >= 0) + if (currentRange.getMaximum().compareTo(primaryKey) >= 0) { - currentIterator.skipTo(nextKey); - break; + currentRange.skipTo(primaryKey); + return; } - - current++; + if (!ranges.hasNext()) + { + currentRange.skipTo(primaryKey); + return; + } + currentRange = ranges.next(); } } @Override protected PrimaryKey computeNext() { - while (current < ranges.size()) + while (!currentRange.hasNext()) { - KeyRangeIterator currentIterator = ranges.get(current); - - if (currentIterator.hasNext()) - return currentIterator.next(); + if (!ranges.hasNext()) + return endOfData(); - current++; + currentRange = ranges.next(); } - - return endOfData(); + return currentRange.next(); } @Override - public void close() + public void close() throws IOException { - super.close(); - // due to lazy key fetching, we cannot close iterator immediately - FileUtils.closeQuietly(ranges); + toRelease.forEach(FileUtils::closeQuietly); + } + + public static Builder builder() + { + return builder(1); } public static Builder builder(int size) { - return builder(size, () -> {}); + return new Builder(size); } - public static Builder builder(int size, Runnable onClose) + public static KeyRangeIterator build(List tokens) { - return new Builder(size, onClose); + return new Builder(tokens.size()).add(tokens).build(); } - @VisibleForTesting public static class Builder extends KeyRangeIterator.Builder { // We can use a list because the iterators are already in order - private final List ranges; + private final List rangeIterators; + public Builder(int size) + { + super(IteratorType.CONCAT); + this.rangeIterators = new ArrayList<>(size); + } - Builder(int size, Runnable onClose) + @Override + public int rangeCount() + { + return rangeIterators.size(); + } + + @Override + public Collection ranges() { - super(new ConcatStatistics(), onClose); - this.ranges = new ArrayList<>(size); + return rangeIterators; } @Override - public KeyRangeIterator.Builder add(KeyRangeIterator range) + public Builder add(KeyRangeIterator range) { if (range == null) return this; if (range.getMaxKeys() > 0) - ranges.add(range); + { + rangeIterators.add(range); + statistics.update(range); + } else FileUtils.closeQuietly(range); - statistics.update(range); return this; } @Override - public int rangeCount() + public KeyRangeIterator.Builder add(List ranges) { - return ranges.size(); - } + if (ranges == null || ranges.isEmpty()) + return this; - @Override - public void cleanup() - { - super.cleanup(); - FileUtils.closeQuietly(ranges); + ranges.forEach(this::add); + return this; } - @Override protected KeyRangeIterator buildIterator() { if (rangeCount() == 0) - { - onClose.run(); return empty(); - } if (rangeCount() == 1) - { - KeyRangeIterator single = ranges.get(0); - single.setOnClose(onClose); - return single; - } - - return new KeyRangeConcatIterator(statistics, ranges, onClose); - } - } - - private static class ConcatStatistics extends KeyRangeIterator.Builder.Statistics - { - @Override - public void update(KeyRangeIterator range) - { - // range iterators should be sorted, but previous max must not be greater than next min. - if (range.getMaxKeys() > 0) - { - if (count == 0) - { - min = range.getMinimum(); - } - else if (count > 0 && max.compareTo(range.getMinimum(), false) > 0) - { - throw new IllegalArgumentException(String.format(MUST_BE_SORTED_ERROR, max, range.getMinimum())); - } - - max = range.getMaximum(); - count += range.getMaxKeys(); - } + return rangeIterators.get(0); + return new KeyRangeConcatIterator(statistics, rangeIterators); } } } diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIntersectionIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIntersectionIterator.java index 4909033bf86e..d96a917ac0cc 100644 --- a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIntersectionIterator.java +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIntersectionIterator.java @@ -17,247 +17,191 @@ */ package org.apache.cassandra.index.sai.iterators; +import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.ArrayList; -import java.util.Comparator; +import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; -import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.PrimaryKey.Kind; import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.tracing.Tracing; -import javax.annotation.Nullable; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT; /** * A simple intersection iterator that makes no real attempts at optimising the iteration apart from - * initially sorting the ranges. This implementation also supports an intersection limit via - * {@code CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT} which limits the number of ranges that will - * be included in the intersection. This currently defaults to 2. - *

    - * Intersection only works for ranges that are compatible according to {@link PrimaryKey.Kind#isIntersectable(Kind)}. + * initially sorting the ranges. This implementation also supports an intersection limit which limits + * the number of ranges that will be included in the intersection. This currently defaults to 2. */ public class KeyRangeIntersectionIterator extends KeyRangeIterator { - private static final Logger logger = LoggerFactory.getLogger(KeyRangeIntersectionIterator.class); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // The cassandra.sai.intersection_clause_limit (default: 2) controls the maximum number of range iterator that + // will be used in the final intersection of a query operation. + public static final int INTERSECTION_CLAUSE_LIMIT = SAI_INTERSECTION_CLAUSE_LIMIT.getInt(); static { - logger.info(String.format("Storage attached index intersection clause limit is %d", CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.getInt())); + logger.info(String.format("Storage attached index intersection clause limit is %d", + CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.getInt())); } - private final List ranges; - private PrimaryKey highestKey; + public final List ranges; + private final int[] rangeStats; - private KeyRangeIntersectionIterator(Builder.Statistics statistics, List ranges, Runnable onClose) + private KeyRangeIntersectionIterator(Builder.Statistics statistics, List ranges) { - super(statistics, onClose); + super(statistics); this.ranges = ranges; - this.highestKey = null; + this.rangeStats = new int[ranges.size()]; } - @Override protected PrimaryKey computeNext() { - if (highestKey == null) - highestKey = computeHighestKey(); + // The highest primary key seen on any range iterator so far. + // It can become null when we reach the end of the iterator. + PrimaryKey highestKey = ranges.get(0).hasNext() ? ranges.get(0).peek() : null; + // Index of the range iterator that has advanced beyond the others + int indexOfHighestKey = 0; + rangeStats[0]++; outer: - // After advancing one iterator, we must try to advance all the other iterators that got behind, - // so they catch up to it. Note that we will not advance the iterators for static columns - // as long as they point to the partition of the highest key. (This is because STATIC primary keys - // compare to other keys only by partition.) This loop continues until all iterators point to the same key, - // or if we run out of keys on any of them, or if we exceed the maximum key. - // There is no point in iterating after maximum, because no keys will match beyond that point. - while (highestKey != null && highestKey.compareTo(getMaximum(), false) <= 0) + while (highestKey != null) { - // Try to advance all iterators to the highest key seen so far. + // Try advance all iterators to the highest key seen so far. // Once this inner loop finishes normally, all iterators are guaranteed to be at the same value. - for (KeyRangeIterator range : ranges) + for (int index = 0; index < ranges.size(); index++) { - if (!range.hasNext()) - return endOfData(); - - if (range.peek().compareTo(highestKey, false) < 0) + if (index != indexOfHighestKey) { - PrimaryKey nextKey = skipToHighestKey(range); + KeyRangeIterator range = ranges.get(index); + + range.skipTo(highestKey); + PrimaryKey nextKey = range.hasNext() ? range.peek() : null; - // We use strict comparison here, since it orders WIDE primary keys after STATIC primary keys - // in the same partition. When WIDE keys are present, we want to return them rather than STATIC - // keys to avoid retrieving and post-filtering entire partitions. - if (nextKey == null || nextKey.compareTo(highestKey, true) > 0) + rangeStats[index]++; + int comparisonResult; + if (nextKey == null || (comparisonResult = nextKey.compareTo(highestKey)) > 0) { // We jumped over the highest key seen so far, so make it the new highest key. highestKey = nextKey; - - // This iterator jumped over, so the other iterators might be lagging behind now, + // Remember this iterator to avoid advancing it again, because it is already at the highest key + indexOfHighestKey = index; + // This iterator jumped over, so the other iterators are lagging behind now, // including the ones already advanced in the earlier cycles of the inner loop. - // Therefore, restart the inner loop in order to advance the lagging iterators. + // Therefore, restart the inner loop in order to advance + // the other iterators except this one to match the new highest key. continue outer; } - assert nextKey.compareTo(highestKey, false) == 0 : - String.format("Skipped to a key smaller than the target! " + - "iterator: %s, target key: %s, returned key: %s", range, highestKey, nextKey); - } - } - // If we get here, all iterators have been advanced to the same key. When STATIC and WIDE keys are - // mixed, this means WIDE keys point to exactly the same row, and STATIC keys the same partition. - PrimaryKey result = highestKey; - - // Advance one iterator to the next key and remember the key as the highest seen so far. - // It can become null when we reach the end of the iterator. - // If there are both static and non-static keys being iterated here, we advance a non-static one, - // regardless of the order of ranges in the ranges list. - highestKey = advanceOneRange(); - - // If we get here, all iterators have been advanced to the same key. When STATIC and WIDE keys are - // mixed, this means WIDE keys point to exactly the same row, and STATIC keys the same partition. - return result; - } - - return endOfData(); - } - - private PrimaryKey skipToHighestKey(KeyRangeIterator range) - { - if (range.peek().kind() == highestKey.kind()) - return skipAndPeek(range, highestKey); - - if (range.peek().kind() == Kind.STATIC) - { - // If we advance a STATIC key, then we must advance it to the same partition as the highestKey. - // Advancing a STATIC key to a WIDE key directly (without throwing away the clustering) would - // go too far, as WIDE keys are stored after STATIC in the posting list. - PrimaryKey nextKey = skipAndPeek(range, highestKey.toStatic()); - - if (nextKey != null && nextKey.compareTo(highestKey, true) < 0 && nextKey.kind() == Kind.WIDE) - // This iterator may have mixed STATIC and non-STATIC postings. Advance again if we've - // landed on a WIDE key that sorts lower in the same partition. - nextKey = skipAndPeek(range, highestKey); - - return nextKey; - } - - return skipAndPeek(range, highestKey); - } + assert comparisonResult == 0 : + String.format("skipTo skipped to an item smaller than the target; " + + "iterator: %s, target key: %s, returned key: %s", range, highestKey, nextKey); - /** - * Advances the iterator of one range to the next item, which becomes the highest seen so far. - * Iterators pointing to STATIC keys are advanced only if no non-STATIC keys have been advanced. - * - * @return the next highest key or null if the iterator has reached the end - */ - private @Nullable PrimaryKey advanceOneRange() - { - for (KeyRangeIterator range : ranges) - if (range.peek().kind() != Kind.STATIC) + // More specific keys should win over full partitions, + // because they match a single row instead of the whole partition. + // However, because this key matches with the earlier keys, we can continue the inner loop. + if (nextKey.hasClustering()) + { + highestKey = nextKey; + indexOfHighestKey = index; + } + } + } + // If we reached here, we have a match - all iterators are at the same key == highestKey. + + // Now we need to advance the iterators to avoid returning the same key again. + // This is tricky because of empty clustering keys that match the whole partition. + // We must not advance ranges at keys with no clustering because they + // may still match the next keys returned by other iterators in the next cycles. + // However, if all ranges are at the same partition with no clustering (!highestKey.hasClustering()), + // we must advance all of them, because we return the key for the whole partition and that partition is done. + for (var range : ranges) { - range.next(); - return range.hasNext() ? range.peek() : null; + if (!highestKey.hasClustering() || range.peek().hasClustering()) + range.next(); } - - for (KeyRangeIterator range : ranges) - if (range.peek().kind() == Kind.STATIC) + + // Move the iterator that was called the least times to the start of the list. + // This is an optimisation assuming that iterator is likely a more selective one. + // E.g.if the first range produces (1, 2, 3, ... 100) and the second one (10, 20, 30, .. 100) + // we'd want to start with the second. + int idxOfSmallest = getIdxOfSmallest(rangeStats); + + if (idxOfSmallest != 0) { - range.next(); - return range.hasNext() ? range.peek() : null; + Collections.swap(ranges, 0, idxOfSmallest); + // swap stats as well + int a = rangeStats[0]; + int b = rangeStats[idxOfSmallest]; + rangeStats[0] = b; + rangeStats[idxOfSmallest] = a; } - throw new IllegalStateException("There should be at least one range to advance!"); + return highestKey; + } + return endOfData(); } - private @Nullable PrimaryKey computeHighestKey() + private static int getIdxOfSmallest(int[] rangeStats) { - PrimaryKey max = getMinimum(); - for (KeyRangeIterator range : ranges) + int idxOfSmallest = 0; + for (int i = 1; i < rangeStats.length; i++) { - if (!range.hasNext()) - return null; - if (range.peek().compareTo(max, true) > 0) - max = range.peek(); + if (rangeStats[i] < rangeStats[idxOfSmallest]) + idxOfSmallest = i; } - return max; + return idxOfSmallest; } - @Override - protected void performSkipTo(PrimaryKey nextKey) + protected void performSkipTo(PrimaryKey nextToken) { // Resist the temptation to call range.hasNext before skipTo: this is a pessimisation, hasNext will invoke // computeNext under the hood, which is an expensive operation to produce a value that we plan to throw away. // Instead, it is the responsibility of the child iterators to make skipTo fast when the iterator is exhausted. for (KeyRangeIterator range : ranges) - range.skipTo(nextKey); - - // Force recomputing the highest key on the next call to computeNext() - highestKey = null; - } - - @Override - public void close() - { - super.close(); - FileUtils.closeQuietly(ranges); + range.skipTo(nextToken); } - /** - * Fetches the next available item from the iterator, such that the item is not lower than the given key. - * If no such items are available, returns null. - */ - private PrimaryKey skipAndPeek(KeyRangeIterator iterator, PrimaryKey minKey) + public void close() throws IOException { - iterator.skipTo(minKey); - return iterator.hasNext() ? iterator.peek() : null; + ranges.forEach(FileUtils::closeQuietly); } - public static Builder builder(int size, int limit) + public static Builder builder(List ranges) { - return builder(size, limit, () -> {}); + var builder = new Builder(ranges.size()); + for (var range : ranges) + builder.add(range); + return builder; } - public static Builder builder(int size, Runnable onClose) + public static Builder builder(int size) { - return new Builder(size, onClose); + return new Builder(size); } - @VisibleForTesting - public static Builder builder(int size, int limit, Runnable onClose) + public static Builder builder() { - return new Builder(size, limit, onClose); + return builder(4); } - @VisibleForTesting public static class Builder extends KeyRangeIterator.Builder { - // This controls the maximum number of range iterators that will be used in the final - // intersection of a query operation. It is set from cassandra.sai.intersection_clause_limit - // and defaults to 2 - private final int limit; - // tracks if any of the added ranges are disjoint with the other ranges, which is useful - // in case of intersection, as it gives a direct answer whether the iterator is going - // to produce any results. - private boolean isDisjoint; - - protected final List rangeIterators; - - Builder(int size, Runnable onClose) - { - this(size, CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.getInt(), onClose); - } + protected List rangeIterators; - Builder(int size, int limit, Runnable onClose) + private Builder(int size) { - super(new IntersectionStatistics(), onClose); + super(IteratorType.INTERSECTION); rangeIterators = new ArrayList<>(size); - this.limit = limit; } - @Override public KeyRangeIterator.Builder add(KeyRangeIterator range) { if (range == null) @@ -267,156 +211,46 @@ public KeyRangeIterator.Builder add(KeyRangeIterator range) rangeIterators.add(range); else FileUtils.closeQuietly(range); + statistics.update(range); - updateStatistics(statistics, range); + return this; + } + + public KeyRangeIterator.Builder add(List ranges) + { + if (ranges == null || ranges.isEmpty()) + return this; + ranges.forEach(this::add); return this; } - @Override public int rangeCount() { return rangeIterators.size(); } @Override - public void cleanup() + public Collection ranges() { - super.cleanup(); - FileUtils.closeQuietly(rangeIterators); + return rangeIterators; } - @Override protected KeyRangeIterator buildIterator() { - rangeIterators.sort(Comparator.comparingLong(KeyRangeIterator::getMaxKeys)); - int initialSize = rangeIterators.size(); - // all ranges will be included - if (limit >= rangeIterators.size() || limit <= 0) - return buildIterator(statistics, rangeIterators); - - // Apply most selective iterators during intersection, because larger number of iterators will result lots of disk seek. - Statistics selectiveStatistics = new IntersectionStatistics(); - isDisjoint = false; - for (int i = rangeIterators.size() - 1; i >= 0 && i >= limit; i--) - FileUtils.closeQuietly(rangeIterators.remove(i)); - - rangeIterators.forEach(range -> updateStatistics(selectiveStatistics, range)); - - if (Tracing.isTracing()) - Tracing.trace("Selecting {} {} of {} out of {} indexes", - rangeIterators.size(), - rangeIterators.size() > 1 ? "indexes with cardinalities" : "index with cardinality", - rangeIterators.stream().map(KeyRangeIterator::getMaxKeys).map(Object::toString).collect(Collectors.joining(", ")), - initialSize); - - return buildIterator(selectiveStatistics, rangeIterators); - } - - public boolean isDisjoint() - { - return isDisjoint; - } - - private KeyRangeIterator buildIterator(Statistics statistics, List ranges) - { - // if the ranges are disjoint, or we have an intersection with an empty set, + // if the range is disjoint or we have an intersection with an empty set, // we can simply return an empty iterator, because it's not going to produce any results. - if (isDisjoint) + if (statistics.isEmptyOrDisjoint()) { - FileUtils.closeQuietly(ranges); - onClose.run(); + // release posting lists + FileUtils.closeQuietly(rangeIterators); return KeyRangeIterator.empty(); } - if (ranges.size() == 1) - { - KeyRangeIterator single = ranges.get(0); - single.setOnClose(onClose); - return single; - } - - // Make sure intersection is supported on the ranges provided: - PrimaryKey.Kind firstKind = null; - - for (KeyRangeIterator range : ranges) - { - PrimaryKey key; - if(range.hasNext()) - key = range.peek(); - else - key = range.getMaximum(); - - if (key != null) - if (firstKind == null) - firstKind = key.kind(); - else if (!firstKind.isIntersectable(key.kind())) - throw new IllegalArgumentException("Cannot intersect " + firstKind + " and " + key.kind() + " ranges!"); - } - - return new KeyRangeIntersectionIterator(statistics, ranges, onClose); - } - - private void updateStatistics(Statistics statistics, KeyRangeIterator range) - { - statistics.update(range); - isDisjoint |= isDisjointInternal(statistics.min, statistics.max, range); - } - } - - private static class IntersectionStatistics extends KeyRangeIterator.Builder.Statistics - { - private boolean empty = true; + if (rangeCount() == 1) + return rangeIterators.get(0); - @Override - public void update(KeyRangeIterator range) - { - // minimum of the intersection is the biggest minimum of individual iterators - min = nullSafeMax(min, range.getMinimum()); - // maximum of the intersection is the smallest maximum of individual iterators - max = nullSafeMin(max, range.getMaximum()); - - // With STATIC keys, it is possible for the min to overtake the max, which must be corrected. - min = nullSafeMin(min, max); - - if (empty) - { - empty = false; - count = range.getMaxKeys(); - } - else - { - count = Math.min(count, range.getMaxKeys()); - } + return new KeyRangeIntersectionIterator(statistics, rangeIterators); } } - - @VisibleForTesting - protected static boolean isDisjoint(KeyRangeIterator a, KeyRangeIterator b) - { - return isDisjointInternal(a.peek(), a.getMaximum(), b); - } - - /** - * Ranges are overlapping the following cases: - *

    - * * When they have a common subrange: - *

    - * min b.current max b.max - * +---------|--------------+------------| - *

    - * b.current min max b.max - * |--------------+---------+------------| - *

    - * min b.current b.max max - * +----------|-------------|------------+ - *

    - * - * If either range is empty, they're disjoint. - */ - private static boolean isDisjointInternal(PrimaryKey min, PrimaryKey max, KeyRangeIterator b) - { - return min == null || max == null || b.getMaxKeys() == 0 - || min.compareTo(b.getMaximum(), false) > 0 || (b.hasNext() && b.peek().compareTo(max, false) > 0); - } } diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIterator.java index a4ada8ec5765..9e9b9e54deec 100644 --- a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIterator.java +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeIterator.java @@ -18,63 +18,46 @@ package org.apache.cassandra.index.sai.iterators; import java.io.Closeable; +import java.util.Collection; +import java.util.List; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.Iterables; -import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.utils.AbstractGuavaIterator; - -import javax.annotation.concurrent.NotThreadSafe; +import org.apache.cassandra.index.sai.utils.PrimaryKey; /** - * An abstract implementation of {@link AbstractGuavaIterator} that supports the building and management of - * concatanation, union and intersection iterators. - *

    * Range iterators contain primary keys, in sorted order, with no duplicates. They also * know their minimum and maximum keys, and an upper bound on the number of keys they contain. - *

    - * Only certain methods are designed to be overriden. The others are marked private or final. */ -@NotThreadSafe public abstract class KeyRangeIterator extends AbstractGuavaIterator implements Closeable { + private static final Builder.EmptyRangeIterator EMPTY = new Builder.EmptyRangeIterator(); + private final PrimaryKey min, max; private final long count; - private Runnable onClose; - protected KeyRangeIterator(Builder.Statistics statistics, Runnable onClose) + protected KeyRangeIterator(Builder.Statistics statistics) { - this(statistics.min, statistics.max, statistics.count, onClose); + this(statistics.min, statistics.max, statistics.tokenCount); } - public KeyRangeIterator(KeyRangeIterator range, Runnable onClose) + public KeyRangeIterator(KeyRangeIterator range) { - this(range == null ? null : range.min, - range == null ? null : range.max, - range == null ? -1 : range.count, - onClose); + this(range == null ? null : range.min, range == null ? null : range.max, range == null ? -1 : range.count); } public KeyRangeIterator(PrimaryKey min, PrimaryKey max, long count) { - this(min, max, count, () -> {}); - } - - public KeyRangeIterator(PrimaryKey min, PrimaryKey max, long count, Runnable onClose) - { - boolean isComplete = min != null && max != null && count != 0; - boolean isEmpty = min == null && max == null && (count == 0 || count == -1); - Preconditions.checkArgument(isComplete || isEmpty, "Range: [%s,%s], Count: %d", min, max, count); - - if (isEmpty) - endOfData(); + if (min == null || max == null || count == 0) + { + assert min == null && max == null && (count == 0 || count == -1) : min + " - " + max + " " + count; + endOfData(); + } this.min = min; this.max = max; this.count = count; - this.onClose = onClose; } public final PrimaryKey getMinimum() @@ -95,76 +78,58 @@ public final long getMaxKeys() return count; } + public final PrimaryKey nextOrNull() + { + return hasNext() ? next() : null; + } + /** - * When called, this iterator's current position will + * When called, this iterators current position will * be skipped forwards until finding either: - * 1) an element equal to or bigger than nextKey + * 1) an element equal to or bigger than next * 2) the end of the iterator * - * @param nextKey value to skip the iterator forward until matching + * @param nextToken value to skip the iterator forward until matching */ - public final void skipTo(PrimaryKey nextKey) + public final void skipTo(PrimaryKey nextToken) { if (state == State.DONE) return; - if (state == State.READY && next.compareTo(nextKey, false) >= 0) + if (state == State.READY && next.compareTo(nextToken) >= 0) return; - if (max.compareTo(nextKey, false) < 0) - { - endOfData(); - return; - } - - performSkipTo(nextKey); + performSkipTo(nextToken); state = State.NOT_READY; } /** - * Skip to nextKey. - *

    - * That is, implementations should set up the iterator state such that - * calling computeNext() will return nextKey if present, - * or the first one after it if not present. + * Skip up to nextKey, but leave the internal state in a position where + * calling computeNext() will return nextKey or the first one after it. */ protected abstract void performSkipTo(PrimaryKey nextKey); - public void setOnClose(Runnable onClose) - { - this.onClose = onClose; - } - - @Override - public void close() - { - onClose.run(); - } - public static KeyRangeIterator empty() { - return EmptyRangeIterator.instance; - } - - private static class EmptyRangeIterator extends KeyRangeIterator - { - static final KeyRangeIterator instance = new EmptyRangeIterator(); - EmptyRangeIterator() { super(null, null, 0, () -> {}); } - public PrimaryKey computeNext() { return endOfData(); } - protected void performSkipTo(PrimaryKey nextKey) { } - public void close() { } + return EMPTY; } - @VisibleForTesting public static abstract class Builder { + public enum IteratorType + { + CONCAT, + UNION, + INTERSECTION + } + + @VisibleForTesting protected final Statistics statistics; - protected final Runnable onClose; - public Builder(Statistics statistics, Runnable onClose) + + public Builder(IteratorType type) { - this.statistics = statistics; - this.onClose = onClose; + statistics = new Statistics(type); } public PrimaryKey getMinimum() @@ -177,54 +142,176 @@ public PrimaryKey getMaximum() return statistics.max; } - public long getCount() + public long getTokenCount() { - return statistics.count; + return statistics.tokenCount; } - public Builder add(Iterable ranges) - { - if (ranges == null || Iterables.isEmpty(ranges)) - return this; + public abstract int rangeCount(); - ranges.forEach(this::add); - return this; - } + public abstract Collection ranges(); + + // Implementation takes ownership of the range iterator. If the implementation decides not to include it, such + // that `rangeCount` may return 0, it must close the range iterator. + public abstract Builder add(KeyRangeIterator range); + + public abstract Builder add(List ranges); public final KeyRangeIterator build() { if (rangeCount() == 0) - { - onClose.run(); - return empty(); - } + return new EmptyRangeIterator(); else - { return buildIterator(); - } } - public abstract Builder add(KeyRangeIterator range); - - public abstract int rangeCount(); - - public void cleanup() + public static class EmptyRangeIterator extends KeyRangeIterator { - onClose.run(); + EmptyRangeIterator() { super(null, null, 0); } + public org.apache.cassandra.index.sai.utils.PrimaryKey computeNext() { return endOfData(); } + protected void performSkipTo(org.apache.cassandra.index.sai.utils.PrimaryKey nextToken) { } + public void close() { } } protected abstract KeyRangeIterator buildIterator(); - public static abstract class Statistics + public static class Statistics { - protected PrimaryKey min, max; - protected long count; + protected final IteratorType iteratorType; + + protected org.apache.cassandra.index.sai.utils.PrimaryKey min, max; + protected long tokenCount; + + // iterator with the least number of items + protected KeyRangeIterator minRange; + // iterator with the most number of items + protected KeyRangeIterator maxRange; + + + private boolean hasRange = false; + + public Statistics(IteratorType iteratorType) + { + this.iteratorType = iteratorType; + } + + /** + * Update statistics information with the given range. + * + * Updates min/max of the combined range, token count and + * tracks range with the least/most number of tokens. + * + * @param range The range to update statistics with. + */ + public void update(KeyRangeIterator range) + { + switch (iteratorType) + { + case CONCAT: + // range iterators should be sorted, but previous max must not be greater than next min. + if (range.getMaxKeys() > 0) + { + if (tokenCount == 0) + { + min = range.getMinimum(); + } + else if (tokenCount > 0 && max.compareTo(range.getMinimum()) > 0) + { + throw new IllegalArgumentException(String.format(KeyRangeConcatIterator.MUST_BE_SORTED_ERROR, max, range.getMinimum())); + } + + max = range.getMaximum(); + } + tokenCount += range.getMaxKeys(); + break; + + case UNION: + min = nullSafeMin(min, range.getMinimum()); + max = nullSafeMax(max, range.getMaximum()); + tokenCount += range.getMaxKeys(); + break; + + case INTERSECTION: + // minimum of the intersection is the biggest minimum of individual iterators + min = nullSafeMax(min, range.getMinimum()); + // maximum of the intersection is the smallest maximum of individual iterators + max = nullSafeMin(max, range.getMaximum()); + if (hasRange) + tokenCount = Math.min(tokenCount, range.getMaxKeys()); + else + tokenCount = range.getMaxKeys(); + + break; + + default: + throw new IllegalStateException("Unknown iterator type: " + iteratorType); + } + + minRange = minRange == null ? range : min(minRange, range); + maxRange = maxRange == null ? range : max(maxRange, range); + + hasRange = true; + } + + private KeyRangeIterator min(KeyRangeIterator a, KeyRangeIterator b) + { + return a.getMaxKeys() > b.getMaxKeys() ? b : a; + } + + private KeyRangeIterator max(KeyRangeIterator a, KeyRangeIterator b) + { + return a.getMaxKeys() > b.getMaxKeys() ? a : b; + } + + /** + * Returns true if the final range is not going to produce any results, + * so we can cleanup range storage and never added anything to it. + */ + public boolean isEmptyOrDisjoint() + { + // max < min if intersected ranges are disjoint + return tokenCount == 0 || min.compareTo(max) > 0; + } - public abstract void update(KeyRangeIterator range); + public double sizeRatio() + { + return minRange.getMaxKeys() * 1d / maxRange.getMaxKeys(); + } } } - protected static PrimaryKey nullSafeMin(PrimaryKey a, PrimaryKey b) + @VisibleForTesting + protected static > boolean isOverlapping(KeyRangeIterator a, KeyRangeIterator b) + { + return isOverlapping(a.peek(), a.getMaximum(), b); + } + + /** + * Ranges are overlapping the following cases: + * + * * When they have a common subrange: + * + * min b.current max b.max + * +---------|--------------+------------| + * + * b.current min max b.max + * |--------------+---------+------------| + * + * min b.current b.max max + * +----------|-------------|------------+ + * + * + * If either range is empty, they're disjoint. + */ + @VisibleForTesting + protected static boolean isOverlapping(PrimaryKey min, PrimaryKey max, KeyRangeIterator b) + { + return (min != null && max != null) && + b.hasNext() && min.compareTo(b.getMaximum()) <= 0 && b.peek().compareTo(max) <= 0; + } + + @SuppressWarnings("unchecked") + private static T nullSafeMin(T a, T b) { if (a == null) return b; if (b == null) return a; @@ -232,18 +319,12 @@ protected static PrimaryKey nullSafeMin(PrimaryKey a, PrimaryKey b) return a.compareTo(b) > 0 ? b : a; } - protected static PrimaryKey nullSafeMax(PrimaryKey a, PrimaryKey b) + @SuppressWarnings("unchecked") + private static T nullSafeMax(T a, T b) { if (a == null) return b; if (b == null) return a; - // The STATIC key sorts before WIDE keys in its partition, but to avoid missing rows while - // intersecting, the STATIC key must override any WIDE key. - if (a.kind() == PrimaryKey.Kind.STATIC && b.kind() == PrimaryKey.Kind.WIDE) - return a.compareTo(b, false) >= 0 ? a : b; - else if (b.kind() == PrimaryKey.Kind.STATIC && a.kind() == PrimaryKey.Kind.WIDE) - return b.compareTo(a, false) >= 0 ? b : a; - return a.compareTo(b) > 0 ? a : b; } } diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeLazyIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeLazyIterator.java new file mode 100644 index 000000000000..41dd2e92e58c --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeLazyIterator.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.iterators; + +import java.io.IOException; +import java.util.function.Supplier; + +import org.apache.cassandra.index.sai.utils.PrimaryKey; + +/** + * Delays creating an iterator to the first use. + */ +public class KeyRangeLazyIterator extends KeyRangeIterator +{ + private KeyRangeIterator inner; + private final Supplier factory; + + public KeyRangeLazyIterator(Supplier factory, PrimaryKey min, PrimaryKey max, long count) + { + super(min, max, count); + this.factory = factory; + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + maybeInitialize(); + inner.skipTo(nextKey); + } + + @Override + protected PrimaryKey computeNext() + { + maybeInitialize(); + return inner.hasNext() ? inner.next() : endOfData(); + } + + @Override + public void close() throws IOException + { + if (inner != null) + inner.close(); + } + + private void maybeInitialize() + { + if (inner == null) + { + inner = factory.get(); + assert inner != null; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeTermIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeTermIterator.java new file mode 100644 index 000000000000..8e21d1cd3ee4 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeTermIterator.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.iterators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.QueryView; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Throwables; + +/** + * KeyRangeTermIterator wraps KeyRangeUnionIterator with code that tracks and releases the referenced indexes, + * and adds timeout checkpoints around expensive operations. + */ +public class KeyRangeTermIterator extends KeyRangeIterator +{ + private static final Logger logger = LoggerFactory.getLogger(KeyRangeTermIterator.class); + + private final QueryContext context; + + private final KeyRangeIterator union; + private final Set referencedIndexes; + + private KeyRangeTermIterator(KeyRangeIterator union, Set referencedIndexes, QueryContext queryContext) + { + super(union.getMinimum(), union.getMaximum(), union.getMaxKeys()); + + this.union = union; + this.referencedIndexes = referencedIndexes; + this.context = queryContext; + + for (SSTableIndex index : referencedIndexes) + { + boolean success = index.reference(); + // Won't happen, because the indexes we get here must be already referenced by the query view + assert success : "Failed to reference the index " + index; + } + } + + + @SuppressWarnings("resource") + public static KeyRangeTermIterator build(final Expression e, QueryView view, AbstractBounds keyRange, QueryContext queryContext, boolean defer) + { + KeyRangeIterator rangeIterator = buildRangeIterator(e, view, keyRange, queryContext, defer); + return new KeyRangeTermIterator(rangeIterator, view.sstableIndexes, queryContext); + } + + private static KeyRangeIterator buildRangeIterator(final Expression e, QueryView view, AbstractBounds keyRange, QueryContext queryContext, boolean defer) + { + final List tokens = new ArrayList<>(1 + view.sstableIndexes.size()); + + KeyRangeIterator memtableIterator = e.context.searchMemtable(queryContext, view.memtableIndexes, e, keyRange); + if (memtableIterator != null) + tokens.add(memtableIterator); + + for (final SSTableIndex index : view.sstableIndexes) + { + try + { + queryContext.checkpoint(); + queryContext.addSstablesHit(1); + assert !index.isReleased(); + + KeyRangeIterator keyIterator = index.search(e, keyRange, queryContext, defer); + + if (keyIterator == null || !keyIterator.hasNext()) + continue; + + tokens.add(keyIterator); + } + catch (Throwable e1) + { + if (logger.isDebugEnabled() && !(e1 instanceof AbortedOperationException)) + logger.debug(String.format("Failed search an index %s, skipping.", index.getSSTable()), e1); + + // Close the iterators that were successfully opened before the error + FileUtils.closeQuietly(tokens); + + throw Throwables.cleaned(e1); + } + } + + return KeyRangeUnionIterator.build(tokens); + } + + protected PrimaryKey computeNext() + { + try + { + return union.hasNext() ? union.next() : endOfData(); + } + finally + { + context.checkpoint(); + } + } + + protected void performSkipTo(PrimaryKey nextKey) + { + try + { + union.skipTo(nextKey); + } + finally + { + context.checkpoint(); + } + } + + public void close() + { + FileUtils.closeQuietly(union); + referencedIndexes.forEach(KeyRangeTermIterator::releaseQuietly); + } + + private static void releaseQuietly(SSTableIndex index) + { + try + { + index.release(); + } + catch (Throwable e) + { + logger.error(String.format("Failed to release index %s", index.getSSTable()), e); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeUnionIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeUnionIterator.java index 340333f2af2b..57e28e1d5511 100644 --- a/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeUnionIterator.java +++ b/src/java/org/apache/cassandra/index/sai/iterators/KeyRangeUnionIterator.java @@ -17,136 +17,158 @@ */ package org.apache.cassandra.index.sai.iterators; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterables; +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.util.FileUtils; /** - * Range Union Iterator is used to return sorted stream of elements from multiple RangeIterator instances. + * Range Union Iterator is used to return sorted stream of elements from multiple KeyRangeIterator instances. + * Keys are sorted by natural order of PrimaryKey, however if two keys are equal by their natural order, + * the one with an empty clustering always wins. */ +@SuppressWarnings("resource") public class KeyRangeUnionIterator extends KeyRangeIterator { - private final List ranges; - private final List candidates; + public final List ranges; - private KeyRangeUnionIterator(Builder.Statistics statistics, List ranges, Runnable onClose) + // If set, we must first skip this partition. + private DecoratedKey partitionToSkip = null; + + private KeyRangeUnionIterator(Builder.Statistics statistics, List ranges) { - super(statistics, onClose); - this.ranges = ranges; - this.candidates = new ArrayList<>(ranges.size()); + super(statistics); + this.ranges = new ArrayList<>(ranges); } - @Override public PrimaryKey computeNext() { - // the design is to find the next best value from all the ranges, - // and then advance all the ranges that have the same value. - candidates.clear(); - PrimaryKey candidateKey = null; + // If we already emitted a partition key for the whole partition (== pk with empty clustering), + // we should not emit any more keys from this partition. + maybeSkipCurrentPartition(); + + // Keep track of the next best candidate. If another candidate has the same value, advance it to prevent + // duplicate results. This design avoids unnecessary list operations. + KeyRangeIterator candidate = null; for (KeyRangeIterator range : ranges) { if (!range.hasNext()) continue; - if (candidateKey == null) + if (candidate == null) { - candidateKey = range.peek(); - candidates.add(range); + candidate = range; } else { - PrimaryKey peeked = range.peek(); - - int cmp = candidateKey.compareTo(peeked, false); - + int cmp = candidate.peek().compareTo(range.peek()); if (cmp == 0) { - // Replace any existing candidate key if this one is STATIC: - if (peeked.kind() == PrimaryKey.Kind.STATIC) - candidateKey = peeked; - - candidates.add(range); + // Due to the way how we compare PrimaryKeys with empty clusterings which is hard to change now, + // the fact that two primary keys compare the same doesn't guarantee they have the same clustering. + // The clustering information is ignored if one key has empty clustering, so a key with an empty + // clustering will match any key with a non-empty clustering (as long as the partition keys are the same). + // So we may end up in a situation when one or more ranges have empty clustering and the others + // have non-empty. This situation is likely if we mix row-aware (DC, EC, ...) indexes with older + // non-row-aware (AA) indexes. + // In that case we absolutely *must* pick the key with an empty clustering, + // as it matches all rows in the partition + // (and hence, it includes all the rows matched by the keys from the other candidates). + // Thanks to postfiltering, we are allowed to return more rows than necessary in SAI, but not less. + // If we chose one of the specific keys with non-empty clustering (e.g. pick the first one we see), + // we may miss rows matched by the non-row-aware index, as well as the rows matched by + // the other row-aware indexes. + if (!range.peek().hasClustering() && candidate.peek().hasClustering()) + candidate = range; + else + range.next(); // truly equal by partition and clustering, so we can just get rid of one } else if (cmp > 0) { - // we found a new best candidate, throw away the old ones - candidates.clear(); - candidateKey = peeked; - candidates.add(range); + candidate = range; } - // else, existing candidate is less than the next in this range } } - if (candidates.isEmpty()) + + if (candidate == null) return endOfData(); - for (KeyRangeIterator candidate : candidates) + var result = candidate.next(); + + // If the winning candidate has an empty clustering, this means it selects the whole partition, so + // advance all other ranges to the end of this partition to avoid duplicates. + // We delay that to the next call to computeNext() though, because if we have a wide partition, it's better + // to first let the caller consume all the rows from this partition - maybe they won't call again. + if (!result.hasClustering()) + partitionToSkip = result.partitionKey(); + + return result; + } + + private void maybeSkipCurrentPartition() + { + if (partitionToSkip != null) { - do - { - // Consume the remaining values equal to the candidate key: - candidate.next(); - } - while (candidate.hasNext() && candidate.peek().compareTo(candidateKey, false) == 0); + for (KeyRangeIterator range : ranges) + skipPartition(range, partitionToSkip); + + partitionToSkip = null; } + } - return candidateKey; + private void skipPartition(KeyRangeIterator iterator, DecoratedKey partitionKey) + { + // TODO: Push this logic down to the iterator where it can be more efficient + while (iterator.hasNext() && !iterator.peek().isTokenOnly() && iterator.peek().partitionKey().compareTo(partitionKey) <= 0) + iterator.next(); } - @Override protected void performSkipTo(PrimaryKey nextKey) { + // Resist the temptation to call range.hasNext before skipTo: this is a pessimisation, hasNext will invoke + // computeNext under the hood, which is an expensive operation to produce a value that we plan to throw away. + // Instead, it is the responsibility of the child iterators to make skipTo fast when the iterator is exhausted. for (KeyRangeIterator range : ranges) - { - if (range.hasNext()) - range.skipTo(nextKey); - } + range.skipTo(nextKey); } - @Override - public void close() + public void close() throws IOException { - super.close(); - // Due to lazy key fetching, we cannot close iterator immediately - FileUtils.closeQuietly(ranges); + ranges.forEach(FileUtils::closeQuietly); } public static Builder builder(int size) { - return builder(size, () -> {}); + return new Builder(size); } - public static Builder builder(int size, Runnable onClose) + public static Builder builder() { - return new Builder(size, onClose); + return builder(10); } - public static KeyRangeIterator build(List keys, Runnable onClose) - { - return new Builder(keys.size(), onClose).add(keys).build(); - } - public static KeyRangeIterator build(List keys) + public static KeyRangeIterator build(Iterable tokens) { - return build(keys, () -> {}); + return KeyRangeUnionIterator.builder(Iterables.size(tokens)).add(tokens).build(); } - @VisibleForTesting public static class Builder extends KeyRangeIterator.Builder { - protected final List rangeIterators; + protected List rangeIterators; - Builder(int size, Runnable onClose) + public Builder(int size) { - super(new UnionStatistics(), onClose); + super(IteratorType.UNION); this.rangeIterators = new ArrayList<>(size); } - @Override public KeyRangeIterator.Builder add(KeyRangeIterator range) { if (range == null) @@ -158,48 +180,66 @@ public KeyRangeIterator.Builder add(KeyRangeIterator range) statistics.update(range); } else - { FileUtils.closeQuietly(range); - } return this; } @Override + public KeyRangeIterator.Builder add(List ranges) + { + if (ranges == null || ranges.isEmpty()) + return this; + + ranges.forEach(this::add); + return this; + } + + public KeyRangeIterator.Builder add(Iterable ranges) + { + if (ranges == null || Iterables.isEmpty(ranges)) + return this; + + ranges.forEach(this::add); + return this; + } + public int rangeCount() { return rangeIterators.size(); } @Override - public void cleanup() + public Collection ranges() { - super.cleanup(); - FileUtils.closeQuietly(rangeIterators); + return rangeIterators; } - @Override protected KeyRangeIterator buildIterator() { - if (rangeCount() == 1) + switch (rangeCount()) { - KeyRangeIterator single = rangeIterators.get(0); - single.setOnClose(onClose); - return single; + case 1: + return rangeIterators.get(0); + + default: + rangeIterators.sort((a, b) -> a.getMinimum().compareTo(b.getMinimum())); + boolean isDisjoint = true; + for (int i = 0; i < rangeIterators.size() - 1; i++) + { + // If a's max is greater than or equal to b's min, then the ranges are not disjoint + var a = rangeIterators.get(i); + var b = rangeIterators.get(i + 1); + if (a.getMaximum().compareTo(b.getMinimum()) >= 0) + { + isDisjoint = false; + break; + } + } + // If the iterators are not overlapping, then we can use the concat iterator which is more efficient + return isDisjoint ? new KeyRangeConcatIterator(statistics, rangeIterators) + : new KeyRangeUnionIterator(statistics, rangeIterators); } - - return new KeyRangeUnionIterator(statistics, rangeIterators, onClose); - } - } - - private static class UnionStatistics extends KeyRangeIterator.Builder.Statistics - { - @Override - public void update(KeyRangeIterator range) - { - min = nullSafeMin(min, range.getMinimum()); - max = nullSafeMax(max, range.getMaximum()); - count += range.getMaxKeys(); } } } diff --git a/src/java/org/apache/cassandra/index/sai/iterators/RowIdToPrimaryKeyWithSortKeyIterator.java b/src/java/org/apache/cassandra/index/sai/iterators/RowIdToPrimaryKeyWithSortKeyIterator.java new file mode 100644 index 000000000000..3f3c6dd07eb6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/iterators/RowIdToPrimaryKeyWithSortKeyIterator.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.iterators; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.IndexSearcherContext; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RowIdWithMeta; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.CloseableIterator; + +/** + * An iterator over scored primary keys ordered by the score descending + * Not skippable. + */ +public class RowIdToPrimaryKeyWithSortKeyIterator extends AbstractIterator +{ + private final IndexContext indexContext; + private final SSTableId sstableId; + private final PrimaryKeyMap primaryKeyMap; + private final CloseableIterator scoredRowIdIterator; + private final IndexSearcherContext searcherContext; + + public RowIdToPrimaryKeyWithSortKeyIterator(IndexContext indexContext, + SSTableId sstableId, + CloseableIterator scoredRowIdIterator, + PrimaryKeyMap primaryKeyMap, + IndexSearcherContext context) + { + this.indexContext = indexContext; + this.sstableId = sstableId; + this.scoredRowIdIterator = scoredRowIdIterator; + this.primaryKeyMap = primaryKeyMap; + this.searcherContext = context; + } + + @Override + protected PrimaryKeyWithSortKey computeNext() + { + if (!scoredRowIdIterator.hasNext()) + return endOfData(); + var rowIdWithMeta = scoredRowIdIterator.next(); + return rowIdWithMeta.buildPrimaryKeyWithSortKey(indexContext, sstableId, primaryKeyMap, searcherContext.getSegmentRowIdOffset()); + } + + @Override + public void close() + { + FileUtils.closeQuietly(primaryKeyMap); + FileUtils.closeQuietly(scoredRowIdIterator); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/memory/FilteringInMemoryKeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/memory/FilteringInMemoryKeyRangeIterator.java deleted file mode 100644 index d54823546a96..000000000000 --- a/src/java/org/apache/cassandra/index/sai/memory/FilteringInMemoryKeyRangeIterator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.memory; - -import java.util.SortedSet; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.utils.PrimaryKey; - -/** - * An {@link InMemoryKeyRangeIterator} that filters the returned {@link PrimaryKey}s based on the provided keyRange - */ -public class FilteringInMemoryKeyRangeIterator extends InMemoryKeyRangeIterator -{ - private final AbstractBounds keyRange; - - public FilteringInMemoryKeyRangeIterator(SortedSet keys, AbstractBounds keyRange) - { - super(keys); - this.keyRange = keyRange; - } - - @Override - protected PrimaryKey computeNext() - { - PrimaryKey key = computeNextKey(); - while (key != null && !keyRange.contains(key.partitionKey())) - key = computeNextKey(); - return key == null ? endOfData() : key; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/memory/FilteringKeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/memory/FilteringKeyRangeIterator.java new file mode 100644 index 000000000000..bda41c727571 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/memory/FilteringKeyRangeIterator.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.memory; + +import java.io.IOException; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.utils.PrimaryKey; + +/** + * A {@link KeyRangeIterator} that filters the returned {@link PrimaryKey}s based on the provided keyRange + */ +public class FilteringKeyRangeIterator extends KeyRangeIterator +{ + private final AbstractBounds keyRange; + private final KeyRangeIterator source; + + public FilteringKeyRangeIterator(KeyRangeIterator source, AbstractBounds keyRange) + { + super(source.getMinimum(), source.getMaximum(), source.getMaxKeys()); + this.keyRange = keyRange; + this.source = source; + } + + @Override + protected PrimaryKey computeNext() + { + while (source.hasNext()) + { + PrimaryKey key = source.next(); + if (keyRange.contains(key.partitionKey())) + return key; + } + return endOfData(); + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + source.skipTo(nextKey); + } + + @Override + public void close() throws IOException + { + } +} diff --git a/src/java/org/apache/cassandra/index/sai/memory/InMemoryKeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/memory/InMemoryKeyRangeIterator.java deleted file mode 100644 index 5131965278f0..000000000000 --- a/src/java/org/apache/cassandra/index/sai/memory/InMemoryKeyRangeIterator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.memory; - -import java.util.PriorityQueue; -import java.util.SortedSet; -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.utils.PrimaryKey; - -@NotThreadSafe -public class InMemoryKeyRangeIterator extends KeyRangeIterator -{ - private final PriorityQueue keys; - private final boolean uniqueKeys; - private PrimaryKey lastKey; - - /** - * An in-memory {@link KeyRangeIterator} that uses a {@link PriorityQueue} built from a {@link SortedSet} - * which has no duplication as its backing store. - */ - public InMemoryKeyRangeIterator(SortedSet keys) - { - super(keys.first(), keys.last(), keys.size(), () -> {}); - this.keys = new PriorityQueue<>(keys); - this.uniqueKeys = true; - } - - /** - * An in-memory {@link KeyRangeIterator} that uses a {@link PriorityQueue} which may - * contain duplicated keys as its backing store. - */ - public InMemoryKeyRangeIterator(PrimaryKey min, PrimaryKey max, PriorityQueue keys) - { - super(min, max, keys.size(), () -> {}); - this.keys = keys; - this.uniqueKeys = false; - } - - @Override - protected PrimaryKey computeNext() - { - PrimaryKey key = computeNextKey(); - return key == null ? endOfData() : key; - } - - protected PrimaryKey computeNextKey() - { - PrimaryKey next = null; - - while (!keys.isEmpty()) - { - PrimaryKey key = keys.poll(); - if (uniqueKeys) - return key; - - if (lastKey == null || lastKey.compareTo(key, false) != 0) - { - next = key; - lastKey = key; - break; - } - } - - return next; - } - - @Override - protected void performSkipTo(PrimaryKey nextKey) - { - while (!keys.isEmpty()) - { - PrimaryKey key = keys.peek(); - if (key.compareTo(nextKey, false) >= 0) - break; - - // consume smaller key - keys.poll(); - } - } - - @Override - public void close() - {} -} diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemoryIndex.java b/src/java/org/apache/cassandra/index/sai/memory/MemoryIndex.java index 4307727b88ae..a6fc644bb7e3 100644 --- a/src/java/org/apache/cassandra/index/sai/memory/MemoryIndex.java +++ b/src/java/org/apache/cassandra/index/sai/memory/MemoryIndex.java @@ -18,54 +18,92 @@ package org.apache.cassandra.index.sai.memory; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; +import java.util.function.LongConsumer; + import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.PrimaryKeys; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.utils.CloseableIterator; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Iterator; -import java.util.function.Function; - -public abstract class MemoryIndex implements MemtableOrdering +public abstract class MemoryIndex { - protected final StorageAttachedIndex index; + protected final IndexContext indexContext; - protected MemoryIndex(StorageAttachedIndex index) + protected MemoryIndex(IndexContext indexContext) { - this.index = index; + this.indexContext = indexContext; } - public abstract long add(DecoratedKey key, Clustering clustering, ByteBuffer value); + public abstract void add(DecoratedKey key, + Clustering clustering, + ByteBuffer value, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker); - public abstract long update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue); + /** + * Update the index value for the given key and clustering by removing the old value and adding the new value. + * This is meant to be used when the indexed column is any type other than a non-frozen collection. + */ + public abstract void update(DecoratedKey key, + Clustering clustering, + ByteBuffer oldValue, + ByteBuffer newValue, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker); - public abstract KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds keyRange); + /** + * Update the index value for the given key and clustering by removing the old values and adding the new values. + * This is meant to be used when the indexed column is a non-frozen collection. + */ + public abstract void update(DecoratedKey key, + Clustering clustering, + Iterator oldValues, + Iterator newValues, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker); - public abstract boolean isEmpty(); + public abstract CloseableIterator orderBy(Orderer orderer, Expression slice); + + public abstract KeyRangeIterator search(Expression expression, AbstractBounds keyRange); + + public abstract long estimateMatchingRowsCount(Expression expression); public abstract ByteBuffer getMinTerm(); public abstract ByteBuffer getMaxTerm(); + /** + * @return num of rows in the memory index + */ + public abstract int indexedRows(); + /** * Iterate all Term->PrimaryKeys mappings in sorted order */ - public abstract Iterator> iterator(); + public abstract Iterator>> iterator(); - public abstract SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor, - IndexIdentifier indexIdentifier, - Function postingTransformer) throws IOException; + + public static class PkWithFrequency + { + public final PrimaryKey pk; + public final int frequency; + + public PkWithFrequency(PrimaryKey pk, int frequency) + { + this.pk = pk; + this.frequency = frequency; + } + } } diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java index 4ebee9318b7f..a9abaf4d8bc9 100644 --- a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java +++ b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java @@ -18,117 +18,91 @@ package org.apache.cassandra.index.sai.memory; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.Iterator; import java.util.List; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Function; +import javax.annotation.Nullable; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; +import org.apache.cassandra.index.sai.disk.vector.VectorMemtableIndex; import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.PrimaryKeys; -import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.index.sai.utils.MemtableOrdering; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.concurrent.OpOrder; -public class MemtableIndex implements MemtableOrdering +public interface MemtableIndex extends MemtableOrdering { - private final MemoryIndex memoryIndex; - private final LongAdder writeCount = new LongAdder(); - private final LongAdder estimatedMemoryUsed = new LongAdder(); - private final Memtable memtable; + Memtable getMemtable(); - public MemtableIndex(StorageAttachedIndex index, Memtable memtable) - { - this.memoryIndex = index.termType().isVector() ? new VectorMemoryIndex(index, memtable) : new TrieMemoryIndex(index); - this.memtable = memtable; - } - - public long writeCount() - { - return writeCount.sum(); - } - - public long estimatedMemoryUsed() - { - return estimatedMemoryUsed.sum(); - } + long writeCount(); - public boolean isEmpty() - { - return memoryIndex.isEmpty(); - } + long estimatedOnHeapMemoryUsed(); - public Memtable getMemtable() - { - return memtable; - } + long estimatedOffHeapMemoryUsed(); - public ByteBuffer getMinTerm() - { - return memoryIndex.getMinTerm(); - } + boolean isEmpty(); - public ByteBuffer getMaxTerm() - { - return memoryIndex.getMaxTerm(); - } + // Returns the minimum indexed term in the combined memory indexes. + // This can be null if the indexed memtable was empty. Users of the + // {@code MemtableIndex} requiring a non-null minimum term should + // use the {@link MemtableIndex#isEmpty} method. + // Note: Individual index shards can return null here if the index + // didn't receive any terms within the token range of the shard + @Nullable + ByteBuffer getMinTerm(); - public long index(DecoratedKey key, Clustering clustering, ByteBuffer value) - { - if (value == null || (value.remaining() == 0 && memoryIndex.index.termType().skipsEmptyValue())) - return 0; + // Returns the maximum indexed term in the combined memory indexes. + // This can be null if the indexed memtable was empty. Users of the + // {@code MemtableIndex} requiring a non-null maximum term should + // use the {@link MemtableIndex#isEmpty} method. + // Note: Individual index shards can return null here if the index + // didn't receive any terms within the token range of the shard + @Nullable + ByteBuffer getMaxTerm(); - long ram = memoryIndex.add(key, clustering, value); - writeCount.increment(); - estimatedMemoryUsed.add(ram); - return ram; - } + void index(DecoratedKey key, Clustering clustering, ByteBuffer value, Memtable memtable, OpOrder.Group opGroup); - public long update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue) - { - return memoryIndex.update(key, clustering, oldValue, newValue); - } + void update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue, Memtable memtable, OpOrder.Group opGroup); + void update(DecoratedKey key, Clustering clustering, Iterator oldValues, Iterator newValues, Memtable memtable, OpOrder.Group opGroup); - public KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds keyRange) - { - return memoryIndex.search(queryContext, expression, keyRange); - } + KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds keyRange); - public Iterator> iterator() - { - return memoryIndex.iterator(); - } + /** + * Estimates the number of rows that would be returned by this index given the predicate. + * The estimate is intended for query plan optimization and is + * not guaranteed to be very accurate, but should be usually at the right + * level of magnitude. Being off by +/-50% is not a bug. + * + * @param expression predicate to match + * @return an approximate number of the matching rows + */ + long estimateMatchingRowsCount(Expression expression); - public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor, - IndexIdentifier indexIdentifier, - Function postingTransformer) throws IOException - { - return memoryIndex.writeDirect(indexDescriptor, indexIdentifier, postingTransformer); - } + Iterator>> iterator(DecoratedKey min, DecoratedKey max); - @Override - public CloseableIterator orderBy(QueryContext queryContext, Expression orderer, AbstractBounds keyRange) + static MemtableIndex createIndex(IndexContext indexContext, Memtable mt) { - return memoryIndex.orderBy(queryContext, orderer, keyRange); + return indexContext.isVector() ? new VectorMemtableIndex(indexContext, mt) : new TrieMemtableIndex(indexContext, mt); } - @Override - public CloseableIterator orderResultsBy(QueryContext queryContext, List results, Expression orderer) - { - return memoryIndex.orderResultsBy(queryContext, results, orderer); - } + /** + * @return num of rows in the memtable index + */ + int getRowCount(); + + /** + * Approximate total count of terms in the memtable index. + * The count is approximate because some deletions are not accounted for in the current implementation. + * + * @return total count of terms for indexes rows. + */ + long getApproximateTermCount(); } diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java deleted file mode 100644 index 855f5a20c7da..000000000000 --- a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.memory; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - -import com.google.common.annotations.VisibleForTesting; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.utils.Clock; -import org.apache.cassandra.utils.FBUtilities; - -public class MemtableIndexManager -{ - private final StorageAttachedIndex index; - private final ConcurrentMap liveMemtableIndexMap; - - public MemtableIndexManager(StorageAttachedIndex index) - { - this.index = index; - this.liveMemtableIndexMap = new ConcurrentHashMap<>(); - } - - public void maybeInitializeMemtableIndex(Memtable memtable) - { - if (index.termType().isVector()) - initializeMemtableIndex(memtable); - } - - private MemtableIndex initializeMemtableIndex(Memtable mt) - { - MemtableIndex current = liveMemtableIndexMap.get(mt); - - // We expect the relevant IndexMemtable to be present most of the time, so only make the - // call to computeIfAbsent() if it's not. (see https://bugs.openjdk.java.net/browse/JDK-8161372) - return current != null ? current - : liveMemtableIndexMap.computeIfAbsent(mt, memtable -> new MemtableIndex(index, memtable)); - } - - public long index(DecoratedKey key, Row row, Memtable mt) - { - MemtableIndex target = initializeMemtableIndex(mt); - - long start = Clock.Global.nanoTime(); - - long bytes = 0; - - if (index.termType().isNonFrozenCollection()) - { - Iterator bufferIterator = index.termType().valuesOf(row, FBUtilities.nowInSeconds()); - if (bufferIterator != null) - { - while (bufferIterator.hasNext()) - { - ByteBuffer value = bufferIterator.next(); - bytes += target.index(key, row.clustering(), value); - } - } - } - else - { - ByteBuffer value = index.termType().valueOf(key, row, FBUtilities.nowInSeconds()); - bytes += target.index(key, row.clustering(), value); - } - index.indexMetrics().memtableIndexWriteLatency.update(Clock.Global.nanoTime() - start, TimeUnit.NANOSECONDS); - return bytes; - } - - public long update(DecoratedKey key, Row oldRow, Row newRow, Memtable memtable) - { - if (!index.termType().isVector()) - { - return index(key, newRow, memtable); - } - - // Updates should only be able to happen on memtables that were already created and that are still live. - MemtableIndex target = liveMemtableIndexMap.get(memtable); - assert target != null : "Memtable for " + memtable.metadata().getTableName() + " not found"; - - ByteBuffer oldValue = index.termType().valueOf(key, oldRow, FBUtilities.nowInSeconds()); - ByteBuffer newValue = index.termType().valueOf(key, newRow, FBUtilities.nowInSeconds()); - return target.update(key, oldRow.clustering(), oldValue, newValue); - } - - public void renewMemtable(Memtable renewed) - { - for (Memtable memtable : liveMemtableIndexMap.keySet()) - { - // remove every index but the one that corresponds to the post-truncate Memtable - if (renewed != memtable) - { - liveMemtableIndexMap.remove(memtable); - } - } - } - - public void discardMemtable(Memtable discarded) - { - liveMemtableIndexMap.remove(discarded); - } - - @Nullable - public MemtableIndex getPendingMemtableIndex(LifecycleNewTracker tracker) - { - return liveMemtableIndexMap.keySet().stream() - .filter(m -> tracker.equals(m.getFlushTransaction())) - .findFirst() - .map(liveMemtableIndexMap::get) - .orElse(null); - } - - public long liveMemtableWriteCount() - { - return liveMemtableIndexMap.values().stream().mapToLong(MemtableIndex::writeCount).sum(); - } - - public Collection getLiveMemtableIndexesSnapshot() - { - Collection memtableIndexes = liveMemtableIndexMap.values(); - if (memtableIndexes.isEmpty()) - return Collections.emptyList(); - - // Copy the values. Otherwise, we'll only have a view of the map's values which is subject to change. - return new ArrayList<>(memtableIndexes); - } - - public long estimatedMemIndexMemoryUsed() - { - return liveMemtableIndexMap.values().stream().mapToLong(MemtableIndex::estimatedMemoryUsed).sum(); - } - - @VisibleForTesting - public int size() - { - return liveMemtableIndexMap.size(); - } - - public void invalidate() - { - liveMemtableIndexMap.clear(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableKeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/memory/MemtableKeyRangeIterator.java new file mode 100644 index 000000000000..568af11b3cca --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/memory/MemtableKeyRangeIterator.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.memory; + +import java.io.IOException; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DataRange; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.schema.TableMetadata; + +/** + * Iterates over primary keys in a memtable + */ +public class MemtableKeyRangeIterator extends KeyRangeIterator +{ + private final Memtable memtable; + private final PrimaryKey.Factory pkFactory; + private final AbstractBounds keyRange; + private final ColumnFilter columns; + private UnfilteredPartitionIterator partitionIterator; + private UnfilteredRowIterator rowIterator; + + public MemtableKeyRangeIterator(Memtable memtable, + PrimaryKey.Factory pkFactory, + AbstractBounds keyRange) + { + super(minKey(memtable, pkFactory), + maxKey(memtable, pkFactory), + memtable.operationCount()); + + TableMetadata metadata = memtable.metadata(); + this.memtable = memtable; + this.pkFactory = pkFactory; + this.keyRange = keyRange; + this.columns = ColumnFilter.selectionBuilder() + .addAll(metadata.partitionKeyColumns()) + .addAll(metadata.clusteringColumns()) + .addAll(metadata.regularColumns()) + .build(); + + DataRange dataRange = new DataRange(keyRange, new ClusteringIndexSliceFilter(Slices.ALL, false)); + this.partitionIterator = memtable.partitionIterator(columns, dataRange, SSTableReadsListener.NOOP_LISTENER); + this.rowIterator = null; + } + + private static PrimaryKey minKey(Memtable memtable, PrimaryKey.Factory factory) + { + DecoratedKey pk = memtable.minPartitionKey(); + return pk != null ? factory.createPartitionKeyOnly(pk) : null; + } + + private static PrimaryKey maxKey(Memtable memtable, PrimaryKey.Factory factory) + { + DecoratedKey pk = memtable.maxPartitionKey(); + return pk != null ? factory.createPartitionKeyOnly(pk) : null; + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + PartitionPosition start = nextKey.isTokenOnly() + ? nextKey.token().minKeyBound() + : nextKey.partitionKey(); + if (!keyRange.right.isMinimum() && start.compareTo(keyRange.right) > 0) + { + partitionIterator = EmptyIterators.unfilteredPartition(memtable.metadata()); + rowIterator = null; + return; + } + + AbstractBounds partitionBounds = AbstractBounds.bounds(start, true, keyRange.right, true); + DataRange dataRange = new DataRange(partitionBounds, new ClusteringIndexSliceFilter(Slices.ALL, false)); + FileUtils.closeQuietly(partitionIterator); + partitionIterator = memtable.partitionIterator(columns, dataRange, SSTableReadsListener.NOOP_LISTENER); + if (partitionIterator.hasNext()) + { + this.rowIterator = partitionIterator.next(); + if (nextKey.hasClustering() && rowIterator.partitionKey().equals(nextKey.partitionKey())) + { + Slice slice = Slice.make(nextKey.clustering(), Clustering.EMPTY); + Slices slices = Slices.with(memtable.metadata().comparator, slice); + FileUtils.closeQuietly(rowIterator); + rowIterator = memtable.getPartition(nextKey.partitionKey()).unfilteredIterator(columns, slices, false); + } + } + } + + @Override + public void close() throws IOException + { + FileUtils.close(partitionIterator, rowIterator); + } + + @Override + protected PrimaryKey computeNext() + { + while (hasNextRow(rowIterator) || partitionIterator.hasNext()) + { + if (!hasNextRow(rowIterator)) + { + FileUtils.closeQuietly(rowIterator); + rowIterator = partitionIterator.next(); + continue; + } + + Unfiltered unfiltered = rowIterator.next(); + if (unfiltered.isRow()) + { + Row row = (Row) unfiltered; + return pkFactory.create(rowIterator.partitionKey(), row.clustering()); + } + } + return endOfData(); + } + + private static boolean hasNextRow(UnfilteredRowIterator rowIterator) + { + return rowIterator != null && rowIterator.hasNext(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableOrdering.java b/src/java/org/apache/cassandra/index/sai/memory/MemtableOrdering.java deleted file mode 100644 index d437dde74d5a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/memory/MemtableOrdering.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.memory; - -import java.util.List; - -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.utils.CloseableIterator; - -/** - * Analogue of {@link org.apache.cassandra.index.sai.disk.v1.segment.SegmentOrdering}, but for memtables. - */ -public interface MemtableOrdering -{ - /** - * Order the index based on the given orderer (expression). - * - * @param queryContext - the query context - * @param orderer - the expression to order by - * @param keyRange - the key range to search - * @return an iterator over the results in score order. - */ - CloseableIterator orderBy(QueryContext queryContext, - Expression orderer, - AbstractBounds keyRange); - - /** - * Order the given list of {@link PrimaryKey} results corresponding to the given orderer. - * Returns an iterator over the results in score order. - * - * Assumes that the given spans the same rows as the implementing index's segment. - */ - CloseableIterator orderResultsBy(QueryContext context, List results, Expression orderer); -} diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableTermsIterator.java b/src/java/org/apache/cassandra/index/sai/memory/MemtableTermsIterator.java deleted file mode 100644 index 638b0d218339..000000000000 --- a/src/java/org/apache/cassandra/index/sai/memory/MemtableTermsIterator.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.memory; - -import java.nio.ByteBuffer; -import java.util.Iterator; - -import com.google.common.base.Preconditions; - -import com.carrotsearch.hppc.LongArrayList; -import com.carrotsearch.hppc.cursors.LongCursor; -import org.apache.cassandra.index.sai.utils.IndexEntry; -import org.apache.cassandra.index.sai.utils.TermsIterator; -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; - -/** - * Iterator over a token range bounded segment of a Memtable index. Used to flush Memtable index segments to disk. - */ -public class MemtableTermsIterator implements TermsIterator -{ - private final ByteBuffer minTerm; - private final ByteBuffer maxTerm; - private final Iterator> iterator; - - private Pair current; - - private long maxSSTableRowId = -1; - private long minSSTableRowId = Long.MAX_VALUE; - - public MemtableTermsIterator(ByteBuffer minTerm, - ByteBuffer maxTerm, - Iterator> iterator) - { - Preconditions.checkArgument(iterator != null); - this.minTerm = minTerm; - this.maxTerm = maxTerm; - this.iterator = iterator; - } - - @Override - public ByteBuffer getMinTerm() - { - return minTerm; - } - - @Override - public ByteBuffer getMaxTerm() - { - return maxTerm; - } - - @Override - public void close() {} - - @Override - public boolean hasNext() - { - return iterator.hasNext(); - } - - @Override - public IndexEntry next() - { - current = iterator.next(); - return IndexEntry.create(current.left, postings()); - } - - public long getMaxSSTableRowId() - { - return maxSSTableRowId; - } - - public long getMinSSTableRowId() - { - return minSSTableRowId; - } - - private PostingList postings() - { - final LongArrayList list = current.right; - - assert list.size() > 0; - - final long minSegmentRowID = list.get(0); - final long maxSegmentRowID = list.get(list.size() - 1); - - minSSTableRowId = Math.min(minSSTableRowId, minSegmentRowID); - maxSSTableRowId = Math.max(maxSSTableRowId, maxSegmentRowID); - - final Iterator it = list.iterator(); - - return new PostingList() - { - @Override - public long nextPosting() - { - if (!it.hasNext()) - { - return END_OF_STREAM; - } - - return it.next().value; - } - - @Override - public long size() - { - return list.size(); - } - - @Override - public long advance(long targetRowID) - { - throw new UnsupportedOperationException(); - } - }; - } -} diff --git a/src/java/org/apache/cassandra/index/sai/memory/RowMapping.java b/src/java/org/apache/cassandra/index/sai/memory/RowMapping.java new file mode 100644 index 000000000000..a0bc124c4ae7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/memory/RowMapping.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.memory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.utils.AbstractGuavaIterator; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +/** + * In memory representation of {@link PrimaryKey} to row ID mappings which only contains + * {@link Row} regardless it's live or deleted. ({@link RangeTombstoneMarker} is not included.) + * + * For JBOD, we can make use of sstable min/max partition key to filter irrelevant {@link TrieMemtableIndex} subranges. + * For Tiered Storage, in most cases, it flushes to tiered 0. + */ +public class RowMapping +{ + public static final RowMapping DUMMY = new RowMapping() + { + @Override + public Iterator>> merge(MemtableIndex index) { return Collections.emptyIterator(); } + + @Override + public void complete() {} + + @Override + public void add(PrimaryKey key, long sstableRowId) {} + + @Override + public int get(PrimaryKey key) + { + return -1; + } + + @Override + public int size() + { + return 0; + } + }; + + private final InMemoryTrie rowMapping = InMemoryTrie.shortLived(TypeUtil.BYTE_COMPARABLE_VERSION); + + private volatile boolean complete = false; + + public PrimaryKey minKey; + public PrimaryKey maxKey; + + public int maxSegmentRowId = -1; + + public int count; + + private RowMapping() + {} + + /** + * Create row mapping for FLUSH operation only. + */ + public static RowMapping create(OperationType opType) + { + if (opType == OperationType.FLUSH) + return new RowMapping(); + return DUMMY; + } + + public static class RowIdWithFrequency { + public final int rowId; + public final int frequency; + + public RowIdWithFrequency(int rowId, int frequency) { + this.rowId = rowId; + this.frequency = frequency; + } + } + + /** + * Merge IndexMemtable(index term to PrimaryKeys mappings) with row mapping of a sstable + * (PrimaryKey to RowId mappings). + * + * @param index a Memtable-attached column index + * + * @return iterator of index term to postings mapping exists in the sstable + */ + public Iterator>> merge(MemtableIndex index) + { + assert complete : "RowMapping is not built."; + + var it = index.iterator(minKey.partitionKey(), maxKey.partitionKey()); + return new AbstractGuavaIterator<>() + { + @Override + protected Pair> computeNext() + { + while (it.hasNext()) + { + var pair = it.next(); + + List postings = null; + var primaryKeysWithFreq = pair.right; + + for (var pkWithFreq : primaryKeysWithFreq) + { + ByteComparable byteComparable = pkWithFreq.pk::asComparableBytes; + Integer segmentRowId = rowMapping.get(byteComparable); + + if (segmentRowId != null) + { + postings = postings == null ? new ArrayList<>() : postings; + postings.add(new RowIdWithFrequency(segmentRowId, pkWithFreq.frequency)); + } + } + if (postings != null && !postings.isEmpty()) + return Pair.create(pair.left, postings); + } + return endOfData(); + } + }; + } + + /** + * Complete building in memory RowMapping, mark it as immutable. + */ + public void complete() + { + assert !complete : "RowMapping can only be built once."; + this.complete = true; + } + + /** + * Include PrimaryKey to RowId mapping + */ + public void add(PrimaryKey key, long sstableRowId) throws TrieSpaceExhaustedException + { + assert !complete : "Cannot modify built RowMapping."; + + if (sstableRowId > Integer.MAX_VALUE) + throw new IllegalArgumentException("RowId must be less than or equal to Integer.MAX_VALUE"); + + // We only build this mapping for memtables, and because those only have a single segment, we know + // that the segment row id is the same as the sstable row id. + int segmentRowId = (int) sstableRowId; + + ByteComparable byteComparable = v -> key.asComparableBytes(v); + rowMapping.putSingleton(byteComparable, segmentRowId, (existing, neww) -> neww); + + maxSegmentRowId = Math.max(maxSegmentRowId, segmentRowId); + + // data is written in token sorted order + if (minKey == null) + minKey = key; + maxKey = key; + count++; + } + + public int get(PrimaryKey key) + { + Integer sstableRowId = rowMapping.get(v -> key.asComparableBytes(v)); + return sstableRowId == null ? -1 : sstableRowId; + } + + public int size() + { + return count; + } + + public boolean hasRows() + { + return size() > 0; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/memory/TrieMemoryIndex.java b/src/java/org/apache/cassandra/index/sai/memory/TrieMemoryIndex.java index c448997c2867..4e7b36f84bd5 100644 --- a/src/java/org/apache/cassandra/index/sai/memory/TrieMemoryIndex.java +++ b/src/java/org/apache/cassandra/index/sai/memory/TrieMemoryIndex.java @@ -1,3 +1,9 @@ +/* + * All changes to the original code are Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,319 +24,793 @@ package org.apache.cassandra.index.sai.memory; +import java.io.IOException; +import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.PriorityQueue; +import java.util.Set; import java.util.SortedSet; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.LongAdder; -import java.util.function.Function; +import java.util.function.LongConsumer; +import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterators; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.github.jbellis.jvector.util.RamUsageEstimator; +import io.netty.util.concurrent.FastThreadLocal; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.memtable.TrieMemtable; import org.apache.cassandra.db.tries.InMemoryTrie; +import org.apache.cassandra.db.tries.Direction; import org.apache.cassandra.db.tries.Trie; +import org.apache.cassandra.db.tries.TrieSpaceExhaustedException; import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.disk.v6.TermsDistribution; import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; +import org.apache.cassandra.index.sai.plan.Orderer; import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithByteComparable; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; import org.apache.cassandra.index.sai.utils.PrimaryKeys; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.AbstractGuavaIterator; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.BinaryHeap; import org.apache.cassandra.utils.CloseableIterator; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; -/** - * This is an in-memory index using the {@link InMemoryTrie} to store a {@link ByteComparable} - * representation of the indexed values. Data is stored on-heap or off-heap and follows the - * settings of the {@link TrieMemtable} to determine where. - */ public class TrieMemoryIndex extends MemoryIndex { private static final Logger logger = LoggerFactory.getLogger(TrieMemoryIndex.class); + private static final int MINIMUM_QUEUE_SIZE = 128; private static final int MAX_RECURSIVE_KEY_LENGTH = 128; - private static final int MINIMUM_PRIORITY_QUEUE_SIZE = 128; private final InMemoryTrie data; - private final PrimaryKeysReducer primaryKeysReducer; + private final LongAdder primaryKeysHeapAllocations; + private final PrimaryKeysAccumulator primaryKeysAccumulator; + private final PrimaryKeysRemover primaryKeysRemover; + private final boolean analyzerTransformsValue; + private final Map docLengths = new HashMap<>(); + private volatile int indexedRows = 0; + private volatile long totalTermCount = 0; + + private final Memtable memtable; + private AbstractBounds keyBounds; private ByteBuffer minTerm; private ByteBuffer maxTerm; + private final Version version; - // Maintain the last queue size used on this index to use for the next range match. - // This allows for receiving a stream of wide range queries where the queue size - // is larger than we would want to default the size to. - private final AtomicInteger lastPriorityQueueSize = new AtomicInteger(MINIMUM_PRIORITY_QUEUE_SIZE); + private static final FastThreadLocal lastQueueSize = new FastThreadLocal() + { + protected Integer initialValue() + { + return MINIMUM_QUEUE_SIZE; + } + }; + + @VisibleForTesting + public TrieMemoryIndex(IndexContext indexContext) + { + this(indexContext, null, AbstractBounds.unbounded(indexContext.getPartitioner())); + } + + public TrieMemoryIndex(IndexContext indexContext, Memtable memtable, AbstractBounds keyBounds) + { + super(indexContext); + this.version = indexContext.version(); + this.keyBounds = keyBounds; + this.primaryKeysHeapAllocations = new LongAdder(); + this.primaryKeysAccumulator = new PrimaryKeysAccumulator(primaryKeysHeapAllocations); + this.primaryKeysRemover = new PrimaryKeysRemover(primaryKeysHeapAllocations); + this.analyzerTransformsValue = indexContext.getAnalyzerFactory().create().transformValue(); + this.data = InMemoryTrie.longLived(TypeUtil.byteComparableVersionForTermsData(indexContext.version()), TrieMemtable.BUFFER_TYPE, indexContext.columnFamilyStore().readOrdering()); + this.memtable = memtable; + } + + public synchronized Map getDocLengths() + { + return docLengths; + } - public TrieMemoryIndex(StorageAttachedIndex index) + @Override + public int indexedRows() { - super(index); - this.data = new InMemoryTrie<>(TrieMemtable.BUFFER_TYPE); - this.primaryKeysReducer = new PrimaryKeysReducer(); + return indexedRows; } /** - * Adds an index value to the in-memory index + * The count of terms for indexed rows is maintained during insertions and updates. + * Deletes are not accounted for. Thus, the count is approximated. * - * @param key partition key for the indexed value - * @param clustering clustering for the indexed value - * @param value indexed value - * @return amount of heap allocated by the new value + * @return the total number of terms in the indexed rows */ - @Override - public synchronized long add(DecoratedKey key, Clustering clustering, ByteBuffer value) + public long approximateTotalTermCount() { - value = index.termType().asIndexBytes(value); - final PrimaryKey primaryKey = index.hasClustering() ? index.keyFactory().create(key, clustering) - : index.keyFactory().create(key); - final long initialSizeOnHeap = data.sizeOnHeap(); - final long initialSizeOffHeap = data.sizeOffHeap(); - final long reducerHeapSize = primaryKeysReducer.heapAllocations(); + return totalTermCount; + } - if (index.hasAnalyzer()) + public synchronized void add(DecoratedKey key, + Clustering clustering, + ByteBuffer value, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker) + { + final PrimaryKey primaryKey = indexContext.keyFactory().create(key, clustering); + applyTransformer(primaryKey, value, onHeapAllocationsTracker, offHeapAllocationsTracker, primaryKeysAccumulator); + } + + public synchronized void update(DecoratedKey key, + Clustering clustering, + ByteBuffer oldValue, + ByteBuffer newValue, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker) + { + final PrimaryKey primaryKey = indexContext.keyFactory().create(key, clustering); + try { - AbstractAnalyzer analyzer = index.analyzer(); - try + if (analyzerTransformsValue) { - analyzer.reset(value); - while (analyzer.hasNext()) - { - addTerm(primaryKey, analyzer.next()); - } + // Because an update can add and remove the same term, we collect the set of the seen PrimaryKeys + // objects touched by the new values and pass it to the remover to prevent removing the PrimaryKey from + // the PrimaryKeys object if it was updated during the add part of this update. + var seenPrimaryKeys = new HashSet(); + primaryKeysAccumulator.setSeenPrimaryKeys(seenPrimaryKeys); + primaryKeysRemover.setSeenPrimaryKeys(seenPrimaryKeys); + } + + // Add before removing to prevent a period where the value is not available in the index + if (newValue != null && newValue.hasRemaining()) + applyTransformer(primaryKey, newValue, onHeapAllocationsTracker, offHeapAllocationsTracker, primaryKeysAccumulator); + if (oldValue != null && oldValue.hasRemaining()) + applyTransformer(primaryKey, oldValue, onHeapAllocationsTracker, offHeapAllocationsTracker, primaryKeysRemover); + } + finally + { + // Return the accumulator and remover to their default state. + primaryKeysAccumulator.setSeenPrimaryKeys(null); + primaryKeysRemover.setSeenPrimaryKeys(null); + } + } + + public synchronized void update(DecoratedKey key, + Clustering clustering, + Iterator oldValues, + Iterator newValues, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker) + { + final PrimaryKey primaryKey = indexContext.keyFactory().create(key, clustering); + try + { + // Because an update can add and remove the same term, we collect the set of the seen PrimaryKeys + // objects touched by the new values and pass it to the remover to prevent removing the PrimaryKey from + // the PrimaryKeys object if it was updated during the add part of this update. + var seenPrimaryKeys = new HashSet(); + primaryKeysAccumulator.setSeenPrimaryKeys(seenPrimaryKeys); + primaryKeysRemover.setSeenPrimaryKeys(seenPrimaryKeys); + + // Add before removing to prevent a period where the values are not available in the index + while (newValues != null && newValues.hasNext()) + { + ByteBuffer newValue = newValues.next(); + if (newValue != null && newValue.hasRemaining()) + applyTransformer(primaryKey, newValue, onHeapAllocationsTracker, offHeapAllocationsTracker, primaryKeysAccumulator); } - finally + + while (oldValues != null && oldValues.hasNext()) { - analyzer.end(); + ByteBuffer oldValue = oldValues.next(); + if (oldValue != null && oldValue.hasRemaining()) + applyTransformer(primaryKey, oldValue, onHeapAllocationsTracker, offHeapAllocationsTracker, primaryKeysRemover); } } - else + finally { - addTerm(primaryKey, value); + // Return the accumulator and remover to their default state. + primaryKeysAccumulator.setSeenPrimaryKeys(null); + primaryKeysRemover.setSeenPrimaryKeys(null); } - long onHeap = data.sizeOnHeap(); - long offHeap = data.sizeOffHeap(); - long heapAllocations = primaryKeysReducer.heapAllocations(); - return (onHeap - initialSizeOnHeap) + (offHeap - initialSizeOffHeap) + (heapAllocations - reducerHeapSize); } - @Override - public long update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue) + private void applyTransformer(PrimaryKey primaryKey, + ByteBuffer value, + LongConsumer onHeapAllocationsTracker, + LongConsumer offHeapAllocationsTracker, + InMemoryTrie.UpsertTransformer transformer) { - throw new UnsupportedOperationException(); + AbstractAnalyzer analyzer = indexContext.getAnalyzerFactory().create(); + try + { + value = TypeUtil.asIndexBytes(value, indexContext.getValidator()); + analyzer.reset(value); + final long initialSizeOnHeap = data.usedSizeOnHeap(); + final long initialSizeOffHeap = data.usedSizeOffHeap(); + final long initialPrimaryKeysHeapAllocations = primaryKeysHeapAllocations.longValue(); + + int tokenCount = 0; + while (analyzer.hasNext()) + { + final ByteBuffer term = analyzer.next(); + if (!indexContext.validateMaxTermSize(primaryKey.partitionKey(), term)) + continue; + + tokenCount++; + + // Note that this term is already encoded once by the TypeUtil.encode call above. + setMinMaxTerm(term.duplicate()); + + final ByteComparable encodedTerm = asByteComparable(term.duplicate()); + + try + { + data.putSingleton(encodedTerm, primaryKey, transformer, term.remaining() <= MAX_RECURSIVE_KEY_LENGTH); + } + catch (TrieSpaceExhaustedException e) + { + Throwables.throwAsUncheckedException(e); + } + } + + Object prev = docLengths.put(primaryKey, tokenCount); + if (prev != null) + { + // An update first transforms with Accumulator to the new value, + // then transforms with Remover from the old value. + if (transformer instanceof PrimaryKeysAccumulator) + totalTermCount += tokenCount; + if (transformer instanceof PrimaryKeysRemover) + totalTermCount -= tokenCount; + // heap used for doc lengths + long heapUsed = RamUsageEstimator.HASHTABLE_RAM_BYTES_PER_ENTRY + + primaryKey.ramBytesUsed() // TODO do we count these bytes? + + Integer.BYTES; + onHeapAllocationsTracker.accept(heapUsed); + } + else + { + indexedRows++; + totalTermCount += tokenCount; + } + + // memory used by the trie + onHeapAllocationsTracker.accept((data.usedSizeOnHeap() - initialSizeOnHeap) + + (primaryKeysHeapAllocations.longValue() - initialPrimaryKeysHeapAllocations)); + offHeapAllocationsTracker.accept(data.usedSizeOffHeap() - initialSizeOffHeap); + } + finally + { + analyzer.end(); + } } - /** - * Search for an expression in the in-memory index within the {@link AbstractBounds} defined - * by keyRange. This can either be an exact match or a range match. - *

    - * @param expression the {@link Expression} to search for - * @param keyRange the {@link AbstractBounds} containing the key range to restrict the search to - * @return a {@link KeyRangeIterator} containing the search results - */ - public KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds keyRange) + @Override + public KeyRangeIterator search(Expression expression, AbstractBounds keyRange) { if (logger.isTraceEnabled()) logger.trace("Searching memtable index on expression '{}'...", expression); - switch (expression.getIndexOperator()) + switch (expression.getOp()) { + case MATCH: case EQ: case CONTAINS_KEY: case CONTAINS_VALUE: return exactMatch(expression, keyRange); case RANGE: - KeyRangeIterator keyIterator = rangeMatch(expression, keyRange); - int keyCount = (int) keyIterator.getMaxKeys(); - if (keyCount > MINIMUM_PRIORITY_QUEUE_SIZE) - lastPriorityQueueSize.set(keyCount); - return keyIterator; + return rangeMatch(expression, keyRange); default: throw new IllegalArgumentException("Unsupported expression: " + expression); } } - /** - * Returns an {@link Iterator} over the entire dataset contained in the trie. This is used - * when the index is flushed to disk. - * - * @return the iterator containing the trie data - */ @Override - public Iterator> iterator() + public Iterator>> iterator() { - Iterator> iterator = data.entrySet().iterator(); - return new Iterator<>() + Iterator> iterator = data.entrySet().iterator(); + return new AbstractGuavaIterator<>() { @Override - public boolean hasNext() - { - return iterator.hasNext(); - } - - @Override - public Pair next() + public Pair> computeNext() { - Map.Entry entry = iterator.next(); - return Pair.create(entry.getKey(), entry.getValue()); + while (iterator.hasNext()) + { + Map.Entry entry = iterator.next(); + PrimaryKeys primaryKeys = entry.getValue(); + if (primaryKeys.isEmpty()) + continue; + + var pairs = new ArrayList(primaryKeys.size()); + Iterators.addAll(pairs, primaryKeys.iterator()); + return Pair.create(entry.getKey(), pairs); + } + return endOfData(); } }; } - @Override - public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor, - IndexIdentifier indexIdentifier, - Function postingTransformer) + @VisibleForTesting + long estimatedTrieValuesMemoryUsed() { - throw new UnsupportedOperationException(); + return primaryKeysHeapAllocations.longValue(); } @Override - public boolean isEmpty() + public CloseableIterator orderBy(Orderer orderer, @Nullable Expression slice) { - return minTerm == null; + if (data.isEmpty()) + return CloseableIterator.emptyIterator(); + + Trie subtrie = getSubtrie(slice); + var iter = subtrie.entrySet(orderer.isAscending() ? Direction.FORWARD : Direction.REVERSE).iterator(); + return new AllTermsIterator(iter); } - @Override - public ByteBuffer getMinTerm() + private ByteComparable asByteComparable(ByteBuffer input) { - return minTerm; + return version.onDiskFormat().encodeForTrie(input, indexContext.getValidator()); } - @Override - public ByteBuffer getMaxTerm() + public KeyRangeIterator exactMatch(Expression expression, AbstractBounds keyRange) { - return maxTerm; + final ByteComparable prefix = expression.lower == null ? ByteComparable.EMPTY : asByteComparable(expression.lower.value.encoded); + final PrimaryKeys primaryKeys = data.get(prefix); + if (primaryKeys == null || primaryKeys.keys().isEmpty()) + { + return KeyRangeIterator.empty(); + } + return new FilteringKeyRangeIterator(new SortedSetKeyRangeIterator(primaryKeys.keys()), keyRange); } - private void addTerm(PrimaryKey primaryKey, ByteBuffer term) + /** + * Accumulator that adds a primary key to the primary keys set. + */ + static class PrimaryKeysAccumulator implements InMemoryTrie.UpsertTransformer { - if (index.validateTermSize(primaryKey.partitionKey(), term, false, null)) + private final LongAdder heapAllocations; + private HashSet seenPrimaryKeys; + + PrimaryKeysAccumulator(LongAdder heapAllocations) { - setMinMaxTerm(term.duplicate()); + this.heapAllocations = heapAllocations; + } - final ByteComparable comparableBytes = asComparableBytes(term); + /** + * Set the PrimaryKeys set to check for each PrimaryKeys object updated by this transformer. + * Warning: This method is not thread-safe and should only be called from within the synchronized block + * of the TrieMemoryIndex class. + * @param seenPrimaryKeys the set of PrimaryKeys objects updated so far + */ + private void setSeenPrimaryKeys(HashSet seenPrimaryKeys) + { + this.seenPrimaryKeys = seenPrimaryKeys; + } - try - { - if (term.limit() <= MAX_RECURSIVE_KEY_LENGTH) - { - data.putRecursive(comparableBytes, primaryKey, primaryKeysReducer); - } - else - { - data.apply(Trie.singleton(comparableBytes, primaryKey), primaryKeysReducer); - } - } - catch (InMemoryTrie.SpaceExhaustedException e) + @Override + public PrimaryKeys apply(PrimaryKeys existing, PrimaryKey neww) + { + if (existing == null) { - throw new RuntimeException(e); + existing = new PrimaryKeys(); + heapAllocations.add(PrimaryKeys.unsharedHeapSize()); } + + // If we are tracking PrimaryKeys via the seenPrimaryKeys set, then we need to reset the + // counter on the first time seeing each PrimaryKeys object since an update means that the + // frequency should be reset. + boolean shouldResetFrequency = false; + if (seenPrimaryKeys != null) + shouldResetFrequency = seenPrimaryKeys.add(existing); + + long bytesAdded = shouldResetFrequency ? existing.addAndResetFrequency(neww) + : existing.addAndIncrementFrequency(neww); + heapAllocations.add(bytesAdded); + return existing; } } - private void setMinMaxTerm(ByteBuffer term) + /** + * Transformer that removes a primary key from the primary keys set, if present. + */ + static class PrimaryKeysRemover implements InMemoryTrie.UpsertTransformer { - assert term != null; + private final LongAdder heapAllocations; + private Set seenPrimaryKeys; - minTerm = index.termType().min(term, minTerm); - maxTerm = index.termType().max(term, maxTerm); - } + PrimaryKeysRemover(LongAdder heapAllocations) + { + this.heapAllocations = heapAllocations; + } - private ByteComparable asComparableBytes(ByteBuffer input) - { - return version -> index.termType().asComparableBytes(input, version); - } + /** + * Set the set of seenPrimaryKeys. + * Warning: This method is not thread-safe and should only be called from within the synchronized block + * of the TrieMemoryIndex class. + * @param seenPrimaryKeys + */ + private void setSeenPrimaryKeys(Set seenPrimaryKeys) + { + this.seenPrimaryKeys = seenPrimaryKeys; + } - private KeyRangeIterator exactMatch(Expression expression, AbstractBounds keyRange) - { - ByteComparable comparableMatch = expression.lower() == null ? ByteComparable.EMPTY - : asComparableBytes(expression.lower().value.encoded); - PrimaryKeys primaryKeys = data.get(comparableMatch); - return primaryKeys == null ? KeyRangeIterator.empty() - : new FilteringInMemoryKeyRangeIterator(primaryKeys.keys(), keyRange); - } + @Override + public PrimaryKeys apply(PrimaryKeys existing, PrimaryKey neww) + { + if (existing == null) + return null; - @Override - public CloseableIterator orderBy(QueryContext queryContext, Expression orderer, AbstractBounds keyRange) - { - throw new UnsupportedOperationException(); + // This PrimaryKeys object was already seen during the add part of this update, + // so we skip removing the PrimaryKey from the PrimaryKeys class. + if (seenPrimaryKeys != null && seenPrimaryKeys.contains(existing)) + return existing; + + heapAllocations.add(existing.remove(neww)); + return existing; + } } - @Override - public CloseableIterator orderResultsBy(QueryContext context, List results, Expression orderer) + /** + * A sorting iterator over items that can either be singleton PrimaryKey or a SortedSetKeyRangeIterator. + */ + static class SortingSingletonOrSetIterator extends BinaryHeap { - throw new UnsupportedOperationException(); + public SortingSingletonOrSetIterator(Collection data) + { + super(data.toArray()); + heapify(); + } + + @Override + protected boolean greaterThan(Object a, Object b) + { + if (a == null || b == null) + return b != null; + + return peek(a).compareTo(peek(b)) > 0; + } + + public PrimaryKey nextOrNull() + { + Object key = top(); + if (key == null) + return null; + PrimaryKey result = peek(key); + assert result != null; + replaceTop(advanceItem(key)); + return result; + } + + public void skipTo(PrimaryKey target) + { + advanceTo(target); + } + + /** + * Advance the given keys object to the next key. + * If the keys object contains a single key, null is returned. + * If the keys object contains more than one key, the first key is dropped and the iterator to the + * remaining keys is returned. + */ + @Override + protected @Nullable Object advanceItem(Object keys) + { + if (keys instanceof PrimaryKey) + return null; + + SortedSetKeyRangeIterator iterator = (SortedSetKeyRangeIterator) keys; + assert iterator.hasNext(); + iterator.next(); + return iterator.hasNext() ? iterator : null; + } + + /** + * Advance the given keys object to the first element that is greater than or equal to the target key. + * This is only called when the given item is known to be before the target key. + * If the keys object contains a single key, null is returned. + * If the keys object contains more than one key, it is skipped to the given target and the iterator to the + * remaining keys is returned. + */ + @Override + protected @Nullable Object advanceItemTo(Object keys, Object target) + { + if (keys instanceof PrimaryKey) + return null; + + SortedSetKeyRangeIterator iterator = (SortedSetKeyRangeIterator) keys; + iterator.skipTo((PrimaryKey) target); + return iterator.hasNext() ? iterator : null; + } + + /** + * Resolve a keys object to either its singleton value or the current element in the iterator. + */ + static PrimaryKey peek(Object keys) + { + if (keys instanceof PrimaryKey) + return (PrimaryKey) keys; + if (keys instanceof SortedSetKeyRangeIterator) + return ((SortedSetKeyRangeIterator) keys).peek(); + + throw new AssertionError("Unreachable"); + } } - private static class Collector + static class MergingKeyRangeIterator extends KeyRangeIterator { - final PriorityQueue mergedKeys; - final AbstractBounds keyRange; + // A sorting iterator of items that can be either singletons or SortedSetKeyRangeIterator + SortingSingletonOrSetIterator keySets; // class invariant: each object placed in this queue contains at least one key - PrimaryKey maximumKey = null; + MergingKeyRangeIterator(Collection keySets, + PrimaryKey minKey, + PrimaryKey maxKey, + long count) + { + super(minKey, maxKey, count); - public Collector(AbstractBounds keyRange, int expectedKeys) + this.keySets = new SortingSingletonOrSetIterator(keySets); + } + + static Builder builder(AbstractBounds keyRange, PrimaryKey.Factory factory, int capacity) { - this.keyRange = keyRange; - this.mergedKeys = new PriorityQueue<>(expectedKeys); + return new Builder(keyRange, factory, capacity); } - public void processContent(PrimaryKeys keys) + @Override + protected void performSkipTo(PrimaryKey nextKey) { - if (keys.isEmpty()) - return; + keySets.skipTo(nextKey); + } + + @Override + protected PrimaryKey computeNext() + { + PrimaryKey result = keySets.nextOrNull(); + if (result == null) + return endOfData(); + else + return result; + } + + @Override + public void close() throws IOException + { + } + + static class Builder + { + final List keySets; - SortedSet primaryKeys = keys.keys(); + private final PrimaryKey min; + private final PrimaryKey max; + private long count; - // shortcut to avoid generating iterator - if (primaryKeys.size() == 1) + + Builder(AbstractBounds keyRange, PrimaryKey.Factory factory, int capacity) { - processKey(primaryKeys.first()); - return; + this.min = factory.createTokenOnly(keyRange.left.getToken()); + this.max = factory.createTokenOnly(keyRange.right.getToken()); + this.keySets = new ArrayList<>(capacity); } - // skip entire partition keys if they don't overlap - if (!keyRange.right.isMinimum() && primaryKeys.first().partitionKey().compareTo(keyRange.right) > 0 - || primaryKeys.last().partitionKey().compareTo(keyRange.left) < 0) - return; + public void add(PrimaryKeys primaryKeys) + { + if (primaryKeys.isEmpty()) + return; - for (PrimaryKey primaryKey : primaryKeys) - processKey(primaryKey); - } + int size = primaryKeys.size(); + SortedSet keys = primaryKeys.keys(); + if (size == 1) + keySets.add(keys.first()); + else + keySets.add(new SortedSetKeyRangeIterator(keys, min, max, size)); - private void processKey(PrimaryKey key) - { - if (keyRange.contains(key.partitionKey())) + count += size; + } + + public int size() + { + return keySets.size(); + } + + public boolean isEmpty() { - mergedKeys.add(key); + return keySets.isEmpty(); + } - // We only track the maximum key, as the minimum can be peeked in constant time on the PQ itself. - maximumKey = maximumKey == null ? key : key.compareTo(maximumKey) > 0 ? key : maximumKey; + public MergingKeyRangeIterator build() + { + return new MergingKeyRangeIterator(keySets, min, max, count); } } } + static class SortedSetKeyRangeIterator extends KeyRangeIterator + { + private SortedSet primaryKeySet; + private Iterator iterator; + private PrimaryKey lastComputedKey; + + public SortedSetKeyRangeIterator(SortedSet source) + { + super(source.first(), source.last(), source.size()); + this.primaryKeySet = source; + } + + private SortedSetKeyRangeIterator(SortedSet source, PrimaryKey min, PrimaryKey max, long count) + { + super(min, max, count); + this.primaryKeySet = source; + } + + + @Override + protected PrimaryKey computeNext() + { + // Skip can be called multiple times in a row, so defer iterator creation until needed + if (iterator == null) + iterator = primaryKeySet.iterator(); + lastComputedKey = iterator.hasNext() ? iterator.next() : endOfData(); + return lastComputedKey; + } + + @Override + protected void performSkipTo(PrimaryKey nextKey) + { + // Avoid going backwards + if (lastComputedKey != null && nextKey.compareTo(lastComputedKey) <= 0) + return; + + primaryKeySet = primaryKeySet.tailSet(nextKey); + iterator = null; + } + + @Override + public void close() throws IOException + { + } + } + private KeyRangeIterator rangeMatch(Expression expression, AbstractBounds keyRange) { + Trie subtrie = getSubtrie(expression); + + var capacity = Math.max(MINIMUM_QUEUE_SIZE, lastQueueSize.get()); + var mergingIteratorBuilder = MergingKeyRangeIterator.builder(keyBounds, indexContext.keyFactory(), capacity); + lastQueueSize.set(mergingIteratorBuilder.size()); + + if (!version.onOrAfter(Version.DB) && TypeUtil.isComposite(expression.validator)) + subtrie.entrySet().forEach(entry -> { + // Before version DB, we encoded composite types using a non order-preserving function. In order to + // perform a range query on a map, we use the bounds to get all entries for a given map key and then + // only keep the map entries that satisfy the expression. + assert entry.getKey().encodingVersion() == TypeUtil.BYTE_COMPARABLE_VERSION || version == Version.AA; + byte[] key = ByteSourceInverse.readBytes(entry.getKey().getPreencodedBytes()); + if (expression.isSatisfiedBy(ByteBuffer.wrap(key))) + mergingIteratorBuilder.add(entry.getValue()); + }); + else + subtrie.values().forEach(mergingIteratorBuilder::add); + + return mergingIteratorBuilder.isEmpty() + ? KeyRangeIterator.empty() + : new FilteringKeyRangeIterator(mergingIteratorBuilder.build(), keyRange); + } + + @Override + public long estimateMatchingRowsCount(Expression expression) + { + switch (expression.getOp()) + { + case MATCH: + case EQ: + case CONTAINS_KEY: + case CONTAINS_VALUE: + return estimateNumRowsMatchingExact(expression); + case NOT_EQ: + case NOT_CONTAINS_KEY: + case NOT_CONTAINS_VALUE: + if (TypeUtil.supportsRounding(expression.validator)) + return Memtable.estimateRowCount(memtable); + else + // need to clamp at 0, because row count is imprecise + return Math.max(0, Memtable.estimateRowCount(memtable) - estimateNumRowsMatchingExact(expression)); + case RANGE: + return estimateNumRowsMatchingRange(expression); + default: + throw new IllegalArgumentException("Unsupported expression: " + expression); + } + } + + + private int estimateNumRowsMatchingExact(Expression expression) + { + final ByteComparable prefix = expression.lower == null ? ByteComparable.EMPTY : asByteComparable(expression.lower.value.encoded); + final PrimaryKeys primaryKeys = data.get(prefix); + return primaryKeys == null ? 0 : primaryKeys.size(); + } + + private long estimateNumRowsMatchingRange(Expression expression) + { + if (minTerm == null || maxTerm == null) + return 0; + + AbstractType termType = indexContext.getValidator(); + ByteComparable minTermComparable = version.onDiskFormat().encodeForTrie(minTerm, termType); + ByteComparable maxTermComparable = version.onDiskFormat().encodeForTrie(maxTerm, termType); + BigDecimal indexLowerBound = toBigDecimal(minTermComparable); + BigDecimal indexUpperBound = toBigDecimal(maxTermComparable); + + BigDecimal queryLowerBound = expression.lower != null + ? toBigDecimal(expression.getEncodedLowerBoundByteComparable(version)) + : indexLowerBound; + BigDecimal queryUpperBound = expression.upper != null + ? toBigDecimal(expression.getEncodedUpperBoundByteComparable(version)) + : indexUpperBound; + + if (queryLowerBound.compareTo(indexUpperBound) > 0 || queryUpperBound.compareTo(indexLowerBound) < 0) + return 0; + if (queryLowerBound.compareTo(indexUpperBound) == 0 && expression.lower != null && !expression.lower.inclusive) + return 0; + if (queryUpperBound.compareTo(indexLowerBound) == 0 && expression.upper != null && !expression.upper.inclusive) + return 0; + if (queryLowerBound.compareTo(indexLowerBound) <= 0 && queryUpperBound.compareTo(indexUpperBound) >= 0) + return indexedRows; + + queryUpperBound = queryUpperBound.min(indexUpperBound).max(indexLowerBound); + queryLowerBound = queryLowerBound.max(indexLowerBound).min(indexUpperBound); + assert queryLowerBound.compareTo(queryUpperBound) <= 0 + : "query lower bound (" + queryLowerBound + ") should be less than or equal to query upper bound (" + queryUpperBound + ')'; + + double indexRangeSize = indexUpperBound.subtract(indexLowerBound).doubleValue() + Double.MIN_NORMAL; + double queryRangeSize = queryUpperBound.subtract(queryLowerBound).doubleValue() + Double.MIN_NORMAL; + double selectivity = queryRangeSize / indexRangeSize; + assert selectivity >= 0.0 && selectivity <= 1.0 : "selectivity (" + selectivity + ") should be between 0.0 and 1.0"; + return Math.round(selectivity * indexedRows); + } + + /** + * Converts the term to a BigDecimal in a way that it keeps the sort order + * (so terms comparing larger yield larger numbers). + * @see TermsDistribution#toBigDecimal(ByteComparable, AbstractType, Version, ByteComparable.Version) + */ + private BigDecimal toBigDecimal(ByteComparable term) + { + AbstractType type = indexContext.getValidator(); + return TermsDistribution.toBigDecimal(term, type, version, TypeUtil.BYTE_COMPARABLE_VERSION); + } + + private Trie getSubtrie(@Nullable Expression expression) + { + if (expression == null) + return data; + ByteComparable lowerBound, upperBound; boolean lowerInclusive, upperInclusive; - if (expression.lower() != null) + if (expression.lower != null) { - lowerBound = asComparableBytes(expression.lower().value.encoded); - lowerInclusive = expression.lower().inclusive; + lowerBound = expression.getEncodedLowerBoundByteComparable(version); + lowerInclusive = expression.lower.inclusive; } else { @@ -338,10 +818,10 @@ private KeyRangeIterator rangeMatch(Expression expression, AbstractBounds values = data.subtrie(lowerBound, lowerInclusive, upperBound, upperInclusive).valueIterator(); + return data.subtrie(lowerBound, lowerInclusive, upperBound, upperInclusive); + } - while (values.hasNext()) - cd.processContent(values.next()); + @Override + public ByteBuffer getMinTerm() + { + return minTerm; + } - if (cd.mergedKeys.isEmpty()) - return KeyRangeIterator.empty(); + @Override + public ByteBuffer getMaxTerm() + { + return maxTerm; + } + + private void setMinMaxTerm(ByteBuffer term) + { + assert term != null; - return new InMemoryKeyRangeIterator(cd.mergedKeys.peek(), cd.maximumKey, cd.mergedKeys); + // Note that an update to a term could make these inaccurate, but they err in the correct direction. + // An alternative solution could use the trie to find the min/max term, but the trie has ByteComparable + // objects, not the ByteBuffer, and we would need to implement a custom decoder to undo the encodeForTrie + // mapping. + minTerm = TypeUtil.min(term, minTerm, indexContext.getValidator(), version); + maxTerm = TypeUtil.max(term, maxTerm, indexContext.getValidator(), version); } - private static class PrimaryKeysReducer implements InMemoryTrie.UpsertTransformer + /** + * Iterator that provides ordered access to all indexed terms and their associated primary keys + * in the TrieMemoryIndex. For each term in the index, yields PrimaryKeyWithSortKey objects that + * combine a primary key with its associated term. + *

    + * A more verbose name could be KeysMatchingTermsByTermIterator. + */ + private class AllTermsIterator extends AbstractIterator { - private final LongAdder heapAllocations = new LongAdder(); + private final Iterator> iterator; + private Iterator primaryKeysIterator = CloseableIterator.emptyIterator(); + private ByteComparable.Preencoded byteComparableTerm = null; - @Override - public PrimaryKeys apply(PrimaryKeys existing, PrimaryKey neww) + public AllTermsIterator(Iterator> iterator) { - if (existing == null) - { - existing = new PrimaryKeys(); - heapAllocations.add(existing.unsharedHeapSize()); - } - heapAllocations.add(existing.add(neww)); - return existing; + this.iterator = iterator; } - long heapAllocations() + @Override + protected PrimaryKeyWithSortKey computeNext() { - return heapAllocations.longValue(); + assert memtable != null; + if (primaryKeysIterator.hasNext()) + return new PrimaryKeyWithByteComparable(indexContext, memtable, primaryKeysIterator.next(), byteComparableTerm); + + while (iterator.hasNext()) + { + var entry = iterator.next(); + primaryKeysIterator = entry.getValue().keys().iterator(); + if (!primaryKeysIterator.hasNext()) + continue; + byteComparableTerm = entry.getKey(); + return new PrimaryKeyWithByteComparable(indexContext, memtable, primaryKeysIterator.next(), byteComparableTerm); + } + return endOfData(); } } } diff --git a/src/java/org/apache/cassandra/index/sai/memory/TrieMemtableIndex.java b/src/java/org/apache/cassandra/index/sai/memory/TrieMemtableIndex.java new file mode 100644 index 000000000000..01c3f4a25099 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/memory/TrieMemtableIndex.java @@ -0,0 +1,585 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.memory; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.LongAdder; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Streams; +import com.google.common.util.concurrent.Runnables; + +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.memtable.AbstractShardedMemtable; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.memtable.ShardBoundaries; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; +import org.apache.cassandra.index.sai.disk.vector.AbstractMemtableIndex; +import org.apache.cassandra.index.sai.iterators.KeyRangeConcatIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeIntersectionIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeLazyIterator; +import org.apache.cassandra.index.sai.memory.MemoryIndex.PkWithFrequency; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.index.sai.utils.BM25Utils; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithByteComparable; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; +import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.Reducer; +import org.apache.cassandra.utils.SortingIterator; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.concurrent.OpOrder; + +public class TrieMemtableIndex extends AbstractMemtableIndex +{ + private final ShardBoundaries boundaries; + private final MemoryIndex[] rangeIndexes; + private final IndexContext indexContext; + private final AbstractType validator; + private final LongAdder writeCount = new LongAdder(); + private final LongAdder estimatedOnHeapMemoryUsed = new LongAdder(); + private final LongAdder estimatedOffHeapMemoryUsed = new LongAdder(); + + private final Context sensorContext; + private final RequestTracker requestTracker; + + public TrieMemtableIndex(IndexContext indexContext, Memtable memtable) + { + this(indexContext, memtable, AbstractShardedMemtable.getDefaultShardCount()); + } + + @VisibleForTesting + public TrieMemtableIndex(IndexContext indexContext, Memtable memtable, int shardCount) + { + super(indexContext, memtable); + this.boundaries = indexContext.columnFamilyStore().localRangeSplits(shardCount); + this.rangeIndexes = new MemoryIndex[boundaries.shardCount()]; + this.indexContext = indexContext; + this.validator = indexContext.getValidator(); + for (int shard = 0; shard < boundaries.shardCount(); shard++) + { + this.rangeIndexes[shard] = new TrieMemoryIndex(indexContext, memtable, boundaries.getBounds(shard)); + } + this.sensorContext = Context.from(indexContext); + this.requestTracker = RequestTracker.instance; + } + + @Override + public Memtable getMemtable() + { + return memtable; + } + + @Override + public int getRowCount() + { + int size = 0; + for (MemoryIndex memoryIndex : rangeIndexes) + size += memoryIndex.indexedRows(); + return size; + } + + /** + * Approximate total count of terms in the memory index. + * The count is approximate because some deletions are not accounted for in the current implementation. + * + * @return total count of terms for indexes rows. + */ + @Override + public long getApproximateTermCount() + { + long count = 0; + for (MemoryIndex memoryIndex : rangeIndexes) + { + assert memoryIndex instanceof TrieMemoryIndex; + count += ((TrieMemoryIndex) memoryIndex).approximateTotalTermCount(); + } + return count; + } + + @VisibleForTesting + public int shardCount() + { + return rangeIndexes.length; + } + + @Override + public long writeCount() + { + return writeCount.sum(); + } + + @Override + public long estimatedOnHeapMemoryUsed() + { + return estimatedOnHeapMemoryUsed.sum(); + } + + @Override + public long estimatedOffHeapMemoryUsed() + { + return estimatedOffHeapMemoryUsed.sum(); + } + + @Override + public boolean isEmpty() + { + return getMinTerm() == null; + } + + // Returns the minimum indexed term in the combined memory indexes. + // This can be null if the indexed memtable was empty. Users of the + // {@code MemtableIndex} requiring a non-null minimum term should + // use the {@link MemtableIndex#isEmpty} method. + // Note: Individual index shards can return null here if the index + // didn't receive any terms within the token range of the shard + @Override + @Nullable + public ByteBuffer getMinTerm() + { + return Arrays.stream(rangeIndexes) + .map(MemoryIndex::getMinTerm) + .filter(Objects::nonNull) + .reduce((a, b) -> TypeUtil.min(a, b, validator, version)) + .orElse(null); + } + + // Returns the maximum indexed term in the combined memory indexes. + // This can be null if the indexed memtable was empty. Users of the + // {@code MemtableIndex} requiring a non-null maximum term should + // use the {@link MemtableIndex#isEmpty} method. + // Note: Individual index shards can return null here if the index + // didn't receive any terms within the token range of the shard + @Override + @Nullable + public ByteBuffer getMaxTerm() + { + return Arrays.stream(rangeIndexes) + .map(MemoryIndex::getMaxTerm) + .filter(Objects::nonNull) + .reduce((a, b) -> TypeUtil.max(a, b, validator, version)) + .orElse(null); + } + + @Override + public void index(DecoratedKey key, Clustering clustering, ByteBuffer value, Memtable memtable, OpOrder.Group opGroup) + { + if (value == null || (value.remaining() == 0 && TypeUtil.skipsEmptyValue(validator))) + return; + + RequestSensors sensors = requestTracker.get(); + if (sensors != null) + sensors.registerSensor(sensorContext, Type.INDEX_WRITE_BYTES); + rangeIndexes[boundaries.getShardForKey(key)].add(key, + clustering, + value, + allocatedBytes -> { + memtable.markExtraOnHeapUsed(allocatedBytes, opGroup); + estimatedOnHeapMemoryUsed.add(allocatedBytes); + if (sensors != null) + sensors.incrementSensor(sensorContext, Type.INDEX_WRITE_BYTES, allocatedBytes); + }, + allocatedBytes -> { + memtable.markExtraOffHeapUsed(allocatedBytes, opGroup); + estimatedOffHeapMemoryUsed.add(allocatedBytes); + if (sensors != null) + sensors.incrementSensor(sensorContext, Type.INDEX_WRITE_BYTES, allocatedBytes); + }); + writeCount.increment(); + onIndexUpdated(); + } + + @Override + public void update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue, Memtable memtable, OpOrder.Group opGroup) + { + int oldRemaining = oldValue == null ? 0 : oldValue.remaining(); + int newRemaining = newValue == null ? 0 : newValue.remaining(); + if (oldRemaining == 0 && newRemaining == 0) + return; + + if (oldRemaining == newRemaining && validator.compare(oldValue, newValue) == 0) + return; + + // The terms inserted into the index could still be the same in the case of certain analyzer configs. + // We don't know yet though, and instead of eagerly determining it, we leave it to the index to handle it. + rangeIndexes[boundaries.getShardForKey(key)].update(key, + clustering, + oldValue, + newValue, + allocatedBytes -> { + memtable.markExtraOnHeapUsed(allocatedBytes, opGroup); + estimatedOnHeapMemoryUsed.add(allocatedBytes); + }, + allocatedBytes -> { + memtable.markExtraOffHeapUsed(allocatedBytes, opGroup); + estimatedOffHeapMemoryUsed.add(allocatedBytes); + }); + writeCount.increment(); + onIndexUpdated(); + } + + @Override + public void update(DecoratedKey key, Clustering clustering, Iterator oldValues, Iterator newValues, Memtable memtable, OpOrder.Group opGroup) + { + // We defer on comparing old and new values here. Instead, we rely on the index to do the comparison and then + // have custom logic in the aggregator to ensure that we properly add/keep new values and remove old values + // that are not present in the new values. + rangeIndexes[boundaries.getShardForKey(key)].update(key, + clustering, + oldValues, + newValues, + allocatedBytes -> { + memtable.markExtraOnHeapUsed(allocatedBytes, opGroup); + estimatedOnHeapMemoryUsed.add(allocatedBytes); + }, + allocatedBytes -> { + memtable.markExtraOffHeapUsed(allocatedBytes, opGroup); + estimatedOffHeapMemoryUsed.add(allocatedBytes); + }); + writeCount.increment(); + } + + public KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds keyRange) + { + int startShard = boundaries.getShardForToken(keyRange.left.getToken()); + int endShard = getEndShardForBounds(keyRange); + + KeyRangeConcatIterator.Builder builder = KeyRangeConcatIterator.builder(endShard - startShard + 1); + + // We want to run the search on the first shard only to get the estimate on the number of matching keys. + // But we don't want to run the search on the other shards until the user polls more items from the + // result iterator. Therefore, the first shard search is special - we run the search eagerly, + // but the rest of the iterators are create lazily in the loop below. + assert rangeIndexes[startShard] != null; + KeyRangeIterator firstIterator = rangeIndexes[startShard].search(expression, keyRange); + // Assume all shards are the same size, but we must not pass 0 because of some checks in KeyRangeIterator + // that assume 0 means empty iterator and could fail. + var keyCount = Math.max(1, firstIterator.getMaxKeys()); + builder.add(firstIterator); + + // Prepare the search on the remaining shards, but wrap them in KeyRangeLazyIterator, so they don't run + // until the user exhaust the results given from the first shard. + for (int shard = startShard + 1; shard <= endShard; ++shard) + { + assert rangeIndexes[shard] != null; + var index = rangeIndexes[shard]; + var shardRange = boundaries.getBounds(shard); + var minKey = index.indexContext.keyFactory().createTokenOnly(shardRange.left.getToken()); + var maxKey = index.indexContext.keyFactory().createTokenOnly(shardRange.right.getToken()); + builder.add(new KeyRangeLazyIterator(() -> index.search(expression, keyRange), minKey, maxKey, keyCount)); + } + + return builder.build(); + } + + public KeyRangeIterator eagerSearch(Expression expression, AbstractBounds keyRange) + { + int startShard = boundaries.getShardForToken(keyRange.left.getToken()); + int endShard = getEndShardForBounds(keyRange); + + KeyRangeConcatIterator.Builder builder = KeyRangeConcatIterator.builder(endShard - startShard + 1); + for (int shard = startShard; shard <= endShard; ++shard) + { + assert rangeIndexes[shard] != null; + builder.add(rangeIndexes[shard].search(expression, keyRange)); + } + return builder.build(); + } + + @Override + public List> orderBy(QueryContext queryContext, + Orderer orderer, + Expression slice, + AbstractBounds keyRange, + int limit) + { + int startShard = boundaries.getShardForToken(keyRange.left.getToken()); + int endShard = getEndShardForBounds(keyRange); + + if (orderer.isBM25()) + { + // Intersect iterators to find documents containing all terms + List queryTerms = orderer.getQueryTerms(); + List termIterators = new ArrayList<>(queryTerms.size()); + for (ByteBuffer term : queryTerms) + { + Expression expr = new Expression(indexContext).add(Operator.ANALYZER_MATCHES, term); + KeyRangeIterator iterator = eagerSearch(expr, keyRange); + termIterators.add(iterator); + } + KeyRangeIterator intersectedIterator = KeyRangeIntersectionIterator.builder(termIterators).build(); + + return List.of(orderByBM25(Streams.stream(intersectedIterator), orderer)); + } + else + { + var iterators = new ArrayList>(endShard - startShard + 1); + for (int shard = startShard; shard <= endShard; ++shard) + { + assert rangeIndexes[shard] != null; + iterators.add(rangeIndexes[shard].orderBy(orderer, slice)); + } + return iterators; + } + } + + /** + * Estimates the number of rows matching the given query predicate. + *

    + * This method provides a fast approximation by calculating the matching row count from a subset of shards. + * The number of shards taken for the computation is determined dynamically depending on the number of indexed + * and matching rows – the more rows found in the shard, the fewer shards are needed to get a reliable estimate. + *

    + * This approach assumes that data is uniformly distributed across shards, which may not + * always be accurate but provides a quick estimate with minimal computational overhead. + * The estimate is particularly useful for query planning and optimization decisions where + * speed is more important than precision.

    + * + * @param expression the search expression/predicate to match against indexed terms + * @return an estimated number of matching rows extrapolated from the first few shards; + */ + @Override + public long estimateMatchingRowsCount(Expression expression) + { + // Control how many shards are taken for estimating the number of keys matching the query expression. + // Shards are taken until we reach at least MIN_MATCHING_ROWS matching rows + // or the total number of indexed rows in all the shards we considered reaches MIN_INDEXED_ROWS. + // Those constants do not affect query correctness, and can be safely to set to any value. + // They navigate the tradeoff between the cardinality estimation speed and accuracy. The higher the values are, + // the more shards will be considered for the estimation, which will increase accuracy but also + // increase the time it takes to estimate. + // If set to MAX_VALUE, all shards will be considered. + // If set to 0, only the first shard will be considered. + final int MIN_MATCHING_ROWS = 100; + final int MIN_INDEXED_ROWS = 100000; + + long matchingRows = 0; + long indexedRows = 0; + int processedShards = 0; + + for (int shard = 0; shard < shardCount(); ++shard) + { + assert rangeIndexes[shard] != null; + matchingRows += rangeIndexes[shard].estimateMatchingRowsCount(expression); + indexedRows += rangeIndexes[shard].indexedRows(); + processedShards++; + + if (matchingRows >= MIN_MATCHING_ROWS) + break; + if (indexedRows >= MIN_INDEXED_ROWS) + break; + } + + assert processedShards >= 1 : "Must process at least one shard for estimating matching rows count"; + return Math.round(matchingRows * (double) (shardCount()) / processedShards); + } + + @Override + public CloseableIterator orderResultsBy(QueryContext queryContext, List keys, Orderer orderer, int limit) + { + if (keys.isEmpty()) + return CloseableIterator.emptyIterator(); + + if (orderer.isBM25()) + return orderByBM25(keys.stream(), orderer); + else + return SortingIterator.createCloseable( + orderer.getComparator(), + keys, + key -> + { + var partition = memtable.getPartition(key.partitionKey()); + if (partition == null) + return null; + var row = partition.getRow(key.clustering()); + if (row == null) + return null; + var cell = row.getCell(indexContext.getDefinition()); + if (cell == null) + return null; + + // We do two kinds of encoding... it'd be great to make this more straight forward, but this is what + // we have for now. I leave it to the reader to inspect the two methods to see the nuanced differences. + var encoding = encode(TypeUtil.asIndexBytes(cell.buffer(), validator)); + return new PrimaryKeyWithByteComparable(indexContext, memtable, key, encoding); + }, + Runnables.doNothing() + ); + } + + private CloseableIterator orderByBM25(Stream stream, Orderer orderer) + { + assert orderer.isBM25(); + List queryTerms = orderer.getQueryTerms(); + AbstractAnalyzer analyzer = indexContext.getAnalyzerFactory().create(); + Iterator it = stream + .map(pk -> BM25Utils.EagerDocTF.createFromDocument(pk, getCellForKey(pk), analyzer, queryTerms)) + .filter(Objects::nonNull) + .iterator(); + return BM25Utils.computeScores(CloseableIterator.wrap(it), + queryTerms, + orderer.bm25stats, + indexContext, + memtable, + false); + } + + + @Nullable + private org.apache.cassandra.db.rows.Cell getCellForKey(PrimaryKey key) + { + var partition = memtable.getPartition(key.partitionKey()); + if (partition == null) + return null; + var row = partition.getRow(key.clustering()); + if (row == null) + return null; + return row.getCell(indexContext.getDefinition()); + } + + private ByteComparable encode(ByteBuffer input) + { + return version.onDiskFormat().encodeForTrie(input, indexContext.getValidator()); + } + + private int getEndShardForBounds(AbstractBounds bounds) + { + PartitionPosition position = bounds.right; + return position.isMinimum() ? boundaries.shardCount() - 1 + : boundaries.getShardForToken(position.getToken()); + } + + /** + * NOTE: returned data may contain partition key not within the provided min and max which are only used to find + * corresponding subranges. We don't do filtering here to avoid unnecessary token comparison. In case of JBOD, + * min/max should align exactly at token boundaries. In case of tiered-storage, keys within min/max may not + * belong to the given sstable. + * + * @param min minimum partition key used to find min subrange + * @param max maximum partition key used to find max subrange + * + * @return iterator of indexed term to primary keys mapping in sorted by indexed term and primary key. + */ + @Override + public Iterator>> iterator(DecoratedKey min, DecoratedKey max) + { + int minSubrange = min == null ? 0 : boundaries.getShardForKey(min); + int maxSubrange = max == null ? rangeIndexes.length - 1 : boundaries.getShardForKey(max); + + List>>> rangeIterators = new ArrayList<>(maxSubrange - minSubrange + 1); + for (int i = minSubrange; i <= maxSubrange; i++) + rangeIterators.add(rangeIndexes[i].iterator()); + + return MergeIterator.get(rangeIterators, + (o1, o2) -> ByteComparable.compare(o1.left, o2.left), + new PrimaryKeysMergeReducer(rangeIterators.size())); + } + + /** + * Used to merge sorted primary keys from multiple TrieMemoryIndex shards for a given indexed term. + * For each term that appears in multiple shards, the reducer: + * 1. Receives exactly one call to reduce() per shard containing that term + * 2. Merges all the primary keys for that term via getReduced() + * 3. Resets state via onKeyChange() before processing the next term + *

    + * While this follows the Reducer pattern, its "reduction" operation is a simple merge since each term + * appears at most once per shard, and each key will only be found in a given shard, so there are no values to aggregate; + * we simply combine and sort the primary keys from each shard that contains the term. + */ + private static class PrimaryKeysMergeReducer extends Reducer>, Pair>> + { + private final Pair>[] rangeIndexEntriesToMerge; + private final Comparator comparator; + + private ByteComparable.Preencoded term; + + @SuppressWarnings("unchecked") + // The size represents the number of range indexes that have been selected for the merger + PrimaryKeysMergeReducer(int size) + { + this.rangeIndexEntriesToMerge = new Pair[size]; + this.comparator = PrimaryKey::compareTo; + } + + @Override + // Receive the term entry for a range index. This should only be called once for each + // range index before reduction. + public void reduce(int index, Pair> termPair) + { + Preconditions.checkArgument(rangeIndexEntriesToMerge[index] == null, "Terms should be unique in the memory index"); + + rangeIndexEntriesToMerge[index] = termPair; + if (termPair != null && term == null) + term = termPair.left; + } + + @Override + // Return a merger of the term keys for the term. + public Pair> getReduced() + { + Preconditions.checkArgument(term != null, "The term must exist in the memory index"); + + var merged = new ArrayList(); + for (var p : rangeIndexEntriesToMerge) + if (p != null && p.right != null) + merged.addAll(p.right); + + merged.sort((o1, o2) -> comparator.compare(o1.pk, o2.pk)); + return Pair.create(term, merged); + } + + @Override + public void onKeyChange() + { + Arrays.fill(rangeIndexEntriesToMerge, null); + term = null; + } + } + + @VisibleForTesting + public MemoryIndex[] getRangeIndexes() + { + return rangeIndexes; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/memory/VectorMemoryIndex.java b/src/java/org/apache/cassandra/index/sai/memory/VectorMemoryIndex.java deleted file mode 100644 index c9d5aa28895b..000000000000 --- a/src/java/org/apache/cassandra/index/sai/memory/VectorMemoryIndex.java +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.memory; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.NavigableSet; -import java.util.PriorityQueue; -import java.util.Set; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Function; -import java.util.stream.Collectors; -import javax.annotation.Nullable; - -import io.github.jbellis.jvector.graph.SearchResult; -import io.github.jbellis.jvector.util.Bits; -import io.github.jbellis.jvector.vector.VectorSimilarityFunction; -import org.apache.cassandra.db.Clustering; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; -import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; -import org.apache.cassandra.index.sai.disk.v1.vector.OnHeapGraph; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.iterators.PriorityQueueIterator; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.PrimaryKeys; -import org.apache.cassandra.index.sai.utils.RangeUtil; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.tracing.Tracing; -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.CloseableIterator; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; - -import static java.lang.Math.log; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static java.lang.Math.pow; - -public class VectorMemoryIndex extends MemoryIndex -{ - private final OnHeapGraph graph; - private final Memtable memtable; - private final LongAdder writeCount = new LongAdder(); - - private PrimaryKey minimumKey; - private PrimaryKey maximumKey; - - private final NavigableSet primaryKeys = new ConcurrentSkipListSet<>(); - - public VectorMemoryIndex(StorageAttachedIndex index, Memtable memtable) - { - super(index); - this.graph = new OnHeapGraph<>(index.termType().indexType(), index.indexWriterConfig(), memtable); - this.memtable = memtable; - } - - @Override - public synchronized long add(DecoratedKey key, Clustering clustering, ByteBuffer value) - { - if (value == null || value.remaining() == 0 || !index.validateTermSize(key, value, false, null)) - return 0; - - PrimaryKey primaryKey = index.hasClustering() ? index.keyFactory().create(key, clustering) - : index.keyFactory().create(key); - return index(primaryKey, value); - } - - private long index(PrimaryKey primaryKey, ByteBuffer value) - { - updateKeyBounds(primaryKey); - - writeCount.increment(); - primaryKeys.add(primaryKey); - return graph.add(value, primaryKey, OnHeapGraph.InvalidVectorBehavior.FAIL); - } - - @Override - public long update(DecoratedKey key, Clustering clustering, ByteBuffer oldValue, ByteBuffer newValue) - { - int oldRemaining = oldValue == null ? 0 : oldValue.remaining(); - int newRemaining = newValue == null ? 0 : newValue.remaining(); - if (oldRemaining == 0 && newRemaining == 0) - return 0; - - boolean different; - if (oldRemaining != newRemaining) - { - assert oldRemaining == 0 || newRemaining == 0; // one of them is null - different = true; - } - else - { - different = index.termType().compare(oldValue, newValue) != 0; - } - - long bytesUsed = 0; - if (different) - { - PrimaryKey primaryKey = index.hasClustering() ? index.keyFactory().create(key, clustering) - : index.keyFactory().create(key); - // update bounds because only rows with vectors are included in the key bounds, - // so if the vector was null before, we won't have included it - updateKeyBounds(primaryKey); - - // make the changes in this order, so we don't have a window where the row is not in the index at all - if (newRemaining > 0) - bytesUsed += graph.add(newValue, primaryKey, OnHeapGraph.InvalidVectorBehavior.FAIL); - if (oldRemaining > 0) - bytesUsed -= graph.remove(oldValue, primaryKey); - - // remove primary key if it's no longer indexed - if (newRemaining <= 0 && oldRemaining > 0) - primaryKeys.remove(primaryKey); - } - return bytesUsed; - } - - private void updateKeyBounds(PrimaryKey primaryKey) - { - if (minimumKey == null) - minimumKey = primaryKey; - else if (primaryKey.compareTo(minimumKey) < 0) - minimumKey = primaryKey; - if (maximumKey == null) - maximumKey = primaryKey; - else if (primaryKey.compareTo(maximumKey) > 0) - maximumKey = primaryKey; - } - - @Override - public KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds keyRange) - { - throw new UnsupportedOperationException(); - } - - @Override - public CloseableIterator orderBy(QueryContext queryContext, Expression expr, AbstractBounds keyRange) - { - assert expr.getIndexOperator() == Expression.IndexOperator.ANN : "Only ANN is supported for vector search, received " + expr.getIndexOperator(); - - ByteBuffer buffer = expr.lower().value.raw; - float[] qv = index.termType().decomposeVector(buffer); - - Bits bits; - if (!RangeUtil.coversFullRing(keyRange)) - { - // if left bound is MIN_BOUND or KEY_BOUND, we need to include all token-only PrimaryKeys with same token - boolean leftInclusive = keyRange.left.kind() != PartitionPosition.Kind.MAX_BOUND; - // if right bound is MAX_BOUND or KEY_BOUND, we need to include all token-only PrimaryKeys with same token - boolean rightInclusive = keyRange.right.kind() != PartitionPosition.Kind.MIN_BOUND; - // if right token is MAX (Long.MIN_VALUE), there is no upper bound - boolean isMaxToken = keyRange.right.getToken().isMinimum(); // max token - - PrimaryKey left = index.keyFactory().create(keyRange.left.getToken()); // lower bound - PrimaryKey right = isMaxToken ? null : index.keyFactory().create(keyRange.right.getToken()); // upper bound - - Set resultKeys = isMaxToken ? primaryKeys.tailSet(left, leftInclusive) : primaryKeys.subSet(left, leftInclusive, right, rightInclusive); - - if (resultKeys.isEmpty()) - return CloseableIterator.empty(); - - int bruteForceRows = maxBruteForceRows(queryContext.limit(), resultKeys.size(), graph.size()); - Tracing.trace("Search range covers {} rows; max brute force rows is {} for memtable index with {} nodes, LIMIT {}", - resultKeys.size(), bruteForceRows, graph.size(), queryContext.limit()); - if (resultKeys.size() < Math.max(queryContext.limit(), bruteForceRows)) - return orderByBruteForce(qv, resultKeys); - else - bits = new KeyRangeFilteringBits(keyRange, null); - } - else - { - // Accept all bits - bits = new Bits.MatchAllBits(Integer.MAX_VALUE); - } - - CloseableIterator iterator = graph.search(qv, queryContext.limit(), bits); - return new NodeScoreToScoredPrimaryKeyIterator(iterator); - } - - @Override - public CloseableIterator orderResultsBy(QueryContext queryContext, List results, Expression orderer) - { - if (minimumKey == null) - // This case implies maximumKey is empty too. - return CloseableIterator.empty(); - - int limit = queryContext.limit(); - - List resultsInRange = results.stream() - .dropWhile(k -> k.compareTo(minimumKey) < 0) - .takeWhile(k -> k.compareTo(maximumKey) <= 0) - .collect(Collectors.toList()); - - int maxBruteForceRows = maxBruteForceRows(limit, resultsInRange.size(), graph.size()); - Tracing.trace("SAI materialized {} rows; max brute force rows is {} for memtable index with {} nodes, LIMIT {}", - resultsInRange.size(), maxBruteForceRows, graph.size(), limit); - - if (resultsInRange.isEmpty()) - return CloseableIterator.empty(); - - ByteBuffer buffer = orderer.lower().value.raw; - float[] qv = index.termType().decomposeVector(buffer); - - if (resultsInRange.size() <= maxBruteForceRows) - return orderByBruteForce(qv, resultsInRange); - - // Search the graph for the topK vectors near the query - KeyFilteringBits bits = new KeyFilteringBits(resultsInRange); - CloseableIterator nodeScores = graph.search(qv, limit, bits); - return new NodeScoreToScoredPrimaryKeyIterator(nodeScores); - } - - private int maxBruteForceRows(int limit, int nPermittedOrdinals, int graphSize) - { - int expectedNodesVisited = expectedNodesVisited(limit, nPermittedOrdinals, graphSize); - // ANN index will do a bunch of extra work besides the full comparisons - // VSTODO I'm not sure which one is more expensive, but since the graph is in memory and the vectors are - // full precision, the goal here is simple: minimize the number of vector comparisons (aka nodes visited). - // As such, the cost function weights them at a 1:1 ratio for now. - return max(limit, expectedNodesVisited); - } - - private CloseableIterator orderByBruteForce(float[] queryVector, Collection keys) - { - VectorSimilarityFunction similarityFunction = index.indexWriterConfig().getSimilarityFunction(); - List scoredKeys = new ArrayList<>(keys.size()); - for (PrimaryKey key : keys) - { - PrimaryKeyWithScore scoredKey = scoreKey(similarityFunction, queryVector, key); - if (scoredKey != null) - scoredKeys.add(scoredKey); - } - // Because we merge iterators from all sstables and memtables, we do not need a complete sort of these - // elements, so a priority queue provides good performance. - return new PriorityQueueIterator<>(new PriorityQueue<>(scoredKeys)); - } - - private PrimaryKeyWithScore scoreKey(VectorSimilarityFunction similarityFunction, float[] queryVector, PrimaryKey key) - { - float[] vector = graph.vectorForKey(key); - if (vector == null) - return null; - float score = similarityFunction.compare(queryVector, vector); - return new PrimaryKeyWithScore(index.termType().columnMetadata(), memtable, key, score); - } - - /** - * All parameters must be greater than zero. nPermittedOrdinals may be larger than graphSize. - */ - public static int expectedNodesVisited(int limit, int nPermittedOrdinals, int graphSize) - { - // constants are computed by Code Interpreter based on observed comparison counts in tests - // https://chat.openai.com/share/2b1d7195-b4cf-4a45-8dce-1b9b2f893c75 - int sizeRestriction = min(nPermittedOrdinals, graphSize); - int raw = (int) (0.7 * pow(log(graphSize), 2) * - pow(graphSize, 0.33) * - pow(log(limit), 2) * - pow(log((double) graphSize / sizeRestriction), 2) / pow(sizeRestriction, 0.13)); - // we will always visit at least min(limit, graphSize) nodes, and we can't visit more nodes than exist in the graph - return min(max(raw, min(limit, graphSize)), graphSize); - } - - @Override - public Iterator> iterator() - { - // This method is only used when merging an in-memory index with a RowMapping. This is done a different - // way with the graph using the writeData method below. - throw new UnsupportedOperationException(); - } - - public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor, - IndexIdentifier indexIdentifier, - Function postingTransformer) throws IOException - { - return graph.writeData(indexDescriptor, indexIdentifier, postingTransformer); - } - - @Override - public boolean isEmpty() - { - return graph.isEmpty(); - } - - @Nullable - @Override - public ByteBuffer getMinTerm() - { - return null; - } - - @Nullable - @Override - public ByteBuffer getMaxTerm() - { - return null; - } - - private class KeyRangeFilteringBits implements Bits - { - private final AbstractBounds keyRange; - @Nullable - private final Bits bits; - - public KeyRangeFilteringBits(AbstractBounds keyRange, @Nullable Bits bits) - { - this.keyRange = keyRange; - this.bits = bits; - } - - @Override - public boolean get(int ordinal) - { - if (bits != null && !bits.get(ordinal)) - return false; - - Collection keys = graph.keysFromOrdinal(ordinal); - return keys.stream().anyMatch(k -> keyRange.contains(k.partitionKey())); - } - - @Override - public int length() - { - return graph.size(); - } - } - - private class KeyFilteringBits implements Bits - { - private final List results; - - public KeyFilteringBits(List results) - { - this.results = results; - } - - @Override - public boolean get(int i) - { - Collection pk = graph.keysFromOrdinal(i); - return results.stream().anyMatch(pk::contains); - } - - @Override - public int length() - { - return results.size(); - } - } - - /** - * An iterator over {@link PrimaryKeyWithScore} sorted by score descending. The iterator converts ordinals (node ids) - * to {@link PrimaryKey}s and pairs them with the score given by the index. - */ - private class NodeScoreToScoredPrimaryKeyIterator extends AbstractIterator - { - private final CloseableIterator nodeScores; - private Iterator primaryKeysForNode = Collections.emptyIterator(); - - NodeScoreToScoredPrimaryKeyIterator(CloseableIterator nodeScores) - { - this.nodeScores = nodeScores; - } - - @Override - protected PrimaryKeyWithScore computeNext() - { - if (primaryKeysForNode.hasNext()) - return primaryKeysForNode.next(); - - while (nodeScores.hasNext()) - { - SearchResult.NodeScore nodeScore = nodeScores.next(); - primaryKeysForNode = graph.keysFromOrdinal(nodeScore.node) - .stream() - .map(pk -> new PrimaryKeyWithScore(index.termType().columnMetadata(), memtable, pk, nodeScore.score)) - .iterator(); - if (primaryKeysForNode.hasNext()) - return primaryKeysForNode.next(); - } - - return endOfData(); - } - - @Override - public void close() - { - FileUtils.closeQuietly(nodeScores); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java index 1bb0a7666ac3..9d0568f2028f 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.List; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; import org.apache.cassandra.metrics.CassandraMetricsRegistry; import org.apache.cassandra.metrics.DefaultNameFactory; @@ -36,11 +35,6 @@ public abstract class AbstractMetrics private final String scope; protected final List tracked = new ArrayList<>(); - AbstractMetrics(IndexIdentifier indexIdentifier, String scope) - { - this(indexIdentifier.keyspaceName, indexIdentifier.tableName, indexIdentifier.indexName, scope); - } - AbstractMetrics(String keyspace, String table, String scope) { this(keyspace, table, null, scope); @@ -48,7 +42,7 @@ public abstract class AbstractMetrics AbstractMetrics(String keyspace, String table, String index, String scope) { - assert keyspace != null && table != null : "SAI metrics must include keyspace and table"; + assert keyspace != null && table != null : "SAI metrics must include table metadata"; this.keyspace = keyspace; this.table = table; this.index = index; @@ -68,12 +62,12 @@ protected CassandraMetricsRegistry.MetricName createMetricName(String name) protected CassandraMetricsRegistry.MetricName createMetricName(String name, String scope) { - String metricScope = keyspace + '.' + table; + String metricScope = keyspace + "." + table; if (index != null) { - metricScope += '.' + index; + metricScope += "." + index; } - metricScope += '.' + scope; + metricScope += "." + scope; CassandraMetricsRegistry.MetricName metricName = new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME, TYPE, name, metricScope, createMBeanName(name, scope)); diff --git a/src/java/org/apache/cassandra/index/sai/metrics/ColumnQueryMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/ColumnQueryMetrics.java index f922231d0b37..ffaed5c0b6c5 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/ColumnQueryMetrics.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/ColumnQueryMetrics.java @@ -17,19 +17,23 @@ */ package org.apache.cassandra.index.sai.metrics; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; +import com.codahale.metrics.Counter; import com.codahale.metrics.Meter; import com.codahale.metrics.Timer; -import org.apache.cassandra.index.sai.utils.IndexIdentifier; +import io.github.jbellis.jvector.graph.SearchResult; +import org.apache.cassandra.config.CassandraRelevantProperties; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; public abstract class ColumnQueryMetrics extends AbstractMetrics { - protected ColumnQueryMetrics(IndexIdentifier indexIdentifier) + protected ColumnQueryMetrics(String keyspace, String table, String indexName) { - super(indexIdentifier, "ColumnQueryMetrics"); + super(keyspace, table, indexName, "ColumnQueryMetrics"); } public static class TrieIndexMetrics extends ColumnQueryMetrics implements QueryEventListener.TrieIndexEventListener @@ -39,15 +43,18 @@ public static class TrieIndexMetrics extends ColumnQueryMetrics implements Query /** * Trie index metrics. */ - private final Timer termsTraversalTotalTime; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public final Optional termsTraversalTotalTime; - private final QueryEventListener.PostingListEventListener postingsListener; + public final QueryEventListener.PostingListEventListener postingsListener; - public TrieIndexMetrics(IndexIdentifier indexIdentifier) + public TrieIndexMetrics(String keyspace, String table, String indexName) { - super(indexIdentifier); + super(keyspace, table, indexName); - termsTraversalTotalTime = Metrics.timer(createMetricName("TermsLookupLatency")); + termsTraversalTotalTime = CassandraRelevantProperties.SAI_HISTOGRAMS_ENABLED.getBoolean() + ? Optional.of(Metrics.timer(createMetricName("TermsLookupLatency"))) + : Optional.empty(); Meter postingDecodes = Metrics.meter(createMetricName("PostingDecodes", TRIE_POSTINGS_TYPE)); @@ -60,7 +67,7 @@ public void onSegmentHit() { } @Override public void onTraversalComplete(long traversalTotalTime, TimeUnit unit) { - termsTraversalTotalTime.update(traversalTotalTime, unit); + termsTraversalTotalTime.ifPresent(timer -> timer.update(traversalTotalTime, unit)); } @Override @@ -70,29 +77,32 @@ public QueryEventListener.PostingListEventListener postingListEventListener() } } - public static class BalancedTreeIndexMetrics extends ColumnQueryMetrics implements QueryEventListener.BalancedTreeEventListener + public static class BKDIndexMetrics extends ColumnQueryMetrics implements QueryEventListener.BKDIndexEventListener { - private static final String BALANCED_TREE_POSTINGS_TYPE = "BalancedTreePostings"; + private static final String BKD_POSTINGS_TYPE = "KDTreePostings"; /** - * Balanced Tree index metrics. + * BKD index metrics. */ - private final Timer intersectionLatency; - private final Meter postingsNumPostings; - private final Meter intersectionEarlyExits; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public final Optional intersectionLatency; + public final Meter postingsNumPostings; + public final Meter intersectionEarlyExits; private final QueryEventListener.PostingListEventListener postingsListener; - public BalancedTreeIndexMetrics(IndexIdentifier indexIdentifier) + public BKDIndexMetrics(String keyspace, String table, String indexName) { - super(indexIdentifier); + super(keyspace, table, indexName); - intersectionLatency = Metrics.timer(createMetricName("BalancedTreeIntersectionLatency")); - intersectionEarlyExits = Metrics.meter(createMetricName("BalancedTreeIntersectionEarlyExits")); + intersectionLatency = CassandraRelevantProperties.SAI_HISTOGRAMS_ENABLED.getBoolean() + ? Optional.of(Metrics.timer(createMetricName("KDTreeIntersectionLatency"))) + : Optional.empty(); + intersectionEarlyExits = Metrics.meter(createMetricName("KDTreeIntersectionEarlyExits")); - postingsNumPostings = Metrics.meter(createMetricName("NumPostings", BALANCED_TREE_POSTINGS_TYPE)); + postingsNumPostings = Metrics.meter(createMetricName("NumPostings", BKD_POSTINGS_TYPE)); - Meter postingDecodes = Metrics.meter(createMetricName("PostingDecodes", BALANCED_TREE_POSTINGS_TYPE)); + Meter postingDecodes = Metrics.meter(createMetricName("PostingDecodes", BKD_POSTINGS_TYPE)); postingsListener = new PostingListEventsMetrics(postingDecodes); } @@ -100,7 +110,7 @@ public BalancedTreeIndexMetrics(IndexIdentifier indexIdentifier) @Override public void onIntersectionComplete(long intersectionTotalTime, TimeUnit unit) { - intersectionLatency.update(intersectionTotalTime, unit); + intersectionLatency.ifPresent(timer -> timer.update(intersectionTotalTime, unit)); } @Override @@ -143,4 +153,102 @@ public void postingDecoded(long postingsDecoded) postingDecodes.mark(postingsDecoded); } } + + /** + * Example VectorIndexMetrics for tracking ANN/Vector index–related metrics. + * You will also need a corresponding QueryEventListener implementation + * (e.g. VectorIndexEventListener) that calls these methods. + */ + public static class VectorIndexMetrics extends ColumnQueryMetrics implements QueryEventListener.VectorIndexEventListener + { + // Vector index meatrics + // Note that the counters will essentially give us a number of operations per second. We lose the notion + // of per query counts, but we can back into an average by dividing by the number of queries. + public final Counter annNodesVisited; + public final Counter annNodesReranked; + public final Counter annNodesExpanded; + public final Counter annNodesExpandedBaseLayer; + public final Counter annGraphSearches; + public final Counter annGraphResumes; + public final Timer annGraphSearchLatency; // Note that this timer measures individual graph search latency + + public final Counter bruteForceNodesVisited; + public final Counter bruteForceNodesReranked; + + // While not query metrics, these are vector specific metrics for the column. + public final LongAdder quantizationMemoryBytes; + public final LongAdder ordinalsMapMemoryBytes; + public final LongAdder onDiskGraphsCount; + public final LongAdder onDiskGraphVectorsCount; + + public VectorIndexMetrics(String keyspace, String table, String indexName) + { + super(keyspace, table, indexName); + + // Initialize Counters and Timer for ANN search + annNodesVisited = Metrics.counter(createMetricName("ANNNodesVisited")); + annNodesReranked = Metrics.counter(createMetricName("ANNNodesReranked")); + annNodesExpanded = Metrics.counter(createMetricName("ANNNodesExpanded")); + annNodesExpandedBaseLayer = Metrics.counter(createMetricName("ANNNodesExpandedBaseLayer")); + annGraphSearches = Metrics.counter(createMetricName("ANNGraphSearches")); + annGraphResumes = Metrics.counter(createMetricName("ANNGraphResumes")); + annGraphSearchLatency = Metrics.timer(createMetricName("ANNGraphSearchLatency")); + + // Initialize Counters for brute-force fallback (if applicable) + bruteForceNodesVisited = Metrics.counter(createMetricName("BruteForceNodesVisited")); + bruteForceNodesReranked = Metrics.counter(createMetricName("BruteForceNodesReranked")); + + // Initialize Gauge for PQ bytes. Ignoring codahale metrics for now. + quantizationMemoryBytes = new LongAdder(); + ordinalsMapMemoryBytes = new LongAdder(); + onDiskGraphVectorsCount = new LongAdder(); + onDiskGraphsCount = new LongAdder(); + } + + @Override + public void onGraphLoaded(long quantizationBytes, long ordinalsMapCachedBytes, long vectorsLoaded) + { + this.quantizationMemoryBytes.add(quantizationBytes); + this.ordinalsMapMemoryBytes.add(ordinalsMapCachedBytes); + this.onDiskGraphVectorsCount.add(vectorsLoaded); + this.onDiskGraphsCount.increment(); + } + + @Override + public void onGraphClosed(long quantizationBytes, long ordinalsMapCachedBytes, long vectorsLoaded) + { + this.quantizationMemoryBytes.add(-quantizationBytes); + this.ordinalsMapMemoryBytes.add(-ordinalsMapCachedBytes); + this.onDiskGraphVectorsCount.add(-vectorsLoaded); + this.onDiskGraphsCount.decrement(); + } + + @Override + public void onSearchResult(SearchResult result, long latencyNs, boolean isResume) + { + annNodesVisited.inc(result.getVisitedCount()); + annNodesReranked.inc(result.getRerankedCount()); + annNodesExpanded.inc(result.getExpandedCount()); + annNodesExpandedBaseLayer.inc(result.getExpandedCountBaseLayer()); + annGraphSearchLatency.update(latencyNs, TimeUnit.NANOSECONDS); + if (isResume) + annGraphResumes.inc(); + else + annGraphSearches.inc(); + } + + // These are the approximate similarity comparisons + @Override + public void onBruteForceNodesVisited(int visited) + { + bruteForceNodesVisited.inc(visited); + } + + // These are the exact similarity comparisons + @Override + public void onBruteForceNodesReranked(int visited) + { + bruteForceNodesReranked.inc(visited); + } + } } diff --git a/src/java/org/apache/cassandra/index/sai/metrics/IndexGroupMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/IndexGroupMetrics.java index fff6291ab062..6a1bf09274aa 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/IndexGroupMetrics.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/IndexGroupMetrics.java @@ -26,11 +26,15 @@ public class IndexGroupMetrics extends AbstractMetrics { public static final String INDEX_GROUP_METRICS_TYPE = "IndexGroupMetrics"; + public final Gauge openIndexFiles; + public final Gauge diskUsedBytes; + public IndexGroupMetrics(TableMetadata table, StorageAttachedIndexGroup group) { super(table.keyspace, table.name, INDEX_GROUP_METRICS_TYPE); - Metrics.register(createMetricName("OpenIndexFiles"), (Gauge) group::openIndexFiles); - Metrics.register(createMetricName("DiskUsedBytes"), (Gauge) group::diskUsage); + openIndexFiles = Metrics.register(createMetricName("OpenIndexFiles"), group::openIndexFiles); + + diskUsedBytes = Metrics.register(createMetricName("DiskUsedBytes"), group::diskUsage); } } diff --git a/src/java/org/apache/cassandra/index/sai/metrics/IndexMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/IndexMetrics.java index 575fb79b9b9d..53cd2edcba6c 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/IndexMetrics.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/IndexMetrics.java @@ -17,46 +17,63 @@ */ package org.apache.cassandra.index.sai.metrics; +import java.util.Optional; + import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Timer; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.memory.MemtableIndexManager; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.index.sai.IndexContext; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; public class IndexMetrics extends AbstractMetrics { - public final Timer memtableIndexWriteLatency; - + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public final Optional memtableIndexWriteLatency; + + public final Gauge ssTableCellCount; + public final Gauge liveMemtableIndexWriteCount; + public final Gauge diskUsedBytes; + public final Gauge memtableOnHeapIndexBytes; + public final Gauge memtableOffHeapIndexBytes; + public final Gauge indexFileCacheBytes; + public final Counter memtableIndexFlushCount; public final Counter compactionCount; + public final Counter compactionTermsProcessedCount; public final Counter memtableIndexFlushErrors; public final Counter segmentFlushErrors; - + public final Counter queriesCount; + public final Histogram memtableFlushCellsPerSecond; public final Histogram segmentsPerCompaction; public final Histogram compactionSegmentCellsPerSecond; public final Histogram compactionSegmentBytesPerSecond; - public IndexMetrics(StorageAttachedIndex index, MemtableIndexManager memtableIndexManager) + public IndexMetrics(IndexContext context) { - super(index.identifier(), "IndexMetrics"); + super(context.getKeyspace(), context.getTable(), context.getIndexName(), "IndexMetrics"); - memtableIndexWriteLatency = Metrics.timer(createMetricName("MemtableIndexWriteLatency")); + memtableIndexWriteLatency = CassandraRelevantProperties.SAI_HISTOGRAMS_ENABLED.getBoolean() + ? Optional.of(Metrics.timer(createMetricName("MemtableIndexWriteLatency"))) + : Optional.empty(); compactionSegmentCellsPerSecond = Metrics.histogram(createMetricName("CompactionSegmentCellsPerSecond"), false); compactionSegmentBytesPerSecond = Metrics.histogram(createMetricName("CompactionSegmentBytesPerSecond"), false); memtableFlushCellsPerSecond = Metrics.histogram(createMetricName("MemtableIndexFlushCellsPerSecond"), false); segmentsPerCompaction = Metrics.histogram(createMetricName("SegmentsPerCompaction"), false); + ssTableCellCount = Metrics.register(createMetricName("SSTableCellCount"), context::getCellCount); memtableIndexFlushCount = Metrics.counter(createMetricName("MemtableIndexFlushCount")); compactionCount = Metrics.counter(createMetricName("CompactionCount")); + compactionTermsProcessedCount = Metrics.counter(createMetricName("CompactionTermsProcessedCount")); memtableIndexFlushErrors = Metrics.counter(createMetricName("MemtableIndexFlushErrors")); segmentFlushErrors = Metrics.counter(createMetricName("CompactionSegmentFlushErrors")); - Metrics.register(createMetricName("SSTableCellCount"), (Gauge) index::cellCount); - Metrics.register(createMetricName("LiveMemtableIndexWriteCount"), (Gauge) memtableIndexManager::liveMemtableWriteCount); - Metrics.register(createMetricName("MemtableIndexBytes"), (Gauge) memtableIndexManager::estimatedMemIndexMemoryUsed); - Metrics.register(createMetricName("DiskUsedBytes"), (Gauge) index::diskUsage); - Metrics.register(createMetricName("IndexFileCacheBytes"), (Gauge) index::indexFileCacheSize); + queriesCount = Metrics.counter(createMetricName("QueriesCount")); + liveMemtableIndexWriteCount = Metrics.register(createMetricName("LiveMemtableIndexWriteCount"), context::liveMemtableWriteCount); + memtableOnHeapIndexBytes = Metrics.register(createMetricName("MemtableOnHeapIndexBytes"), context::estimatedOnHeapMemIndexMemoryUsed); + memtableOffHeapIndexBytes = Metrics.register(createMetricName("MemtableOffHeapIndexBytes"), context::estimatedOffHeapMemIndexMemoryUsed); + diskUsedBytes = Metrics.register(createMetricName("DiskUsedBytes"), context::diskUsage); + indexFileCacheBytes = Metrics.register(createMetricName("IndexFileCacheBytes"), context::indexFileCacheSize); } } diff --git a/src/java/org/apache/cassandra/index/sai/metrics/MulticastQueryEventListeners.java b/src/java/org/apache/cassandra/index/sai/metrics/MulticastQueryEventListeners.java index 5495f2f2322c..1be76175a130 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/MulticastQueryEventListeners.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/MulticastQueryEventListeners.java @@ -28,9 +28,9 @@ public static QueryEventListener.TrieIndexEventListener of(QueryContext ctx, Que return new Multicast2TrieIndexEventListener(ctx, listener); } - public static QueryEventListener.BalancedTreeEventListener of(QueryContext ctx, QueryEventListener.BalancedTreeEventListener listener) + public static QueryEventListener.BKDIndexEventListener of(QueryContext ctx, QueryEventListener.BKDIndexEventListener listener) { - return new Multicast2BalancedTreeEventListener(ctx, listener); + return new Multicast2BKDIndexEventListener(ctx, listener); } public static class Multicast2TrieIndexEventListener implements QueryEventListener.TrieIndexEventListener @@ -49,8 +49,8 @@ private Multicast2TrieIndexEventListener(QueryContext ctx, QueryEventListener.Tr @Override public void onSegmentHit() { - ctx.segmentsHit++; - ctx.trieSegmentsHit++; + ctx.addSegmentsHit(1); + ctx.addTrieSegmentsHit(1); listener.onSegmentHit(); } @@ -67,17 +67,17 @@ public QueryEventListener.PostingListEventListener postingListEventListener() } } - public static class Multicast2BalancedTreeEventListener implements QueryEventListener.BalancedTreeEventListener + public static class Multicast2BKDIndexEventListener implements QueryEventListener.BKDIndexEventListener { private final QueryContext ctx; - private final QueryEventListener.BalancedTreeEventListener listener; - private final Multicast2BalancedTreePostingListEventListener postingListEventListener; + private final QueryEventListener.BKDIndexEventListener listener; + private final Multicast2BKDPostingListEventListener postingListEventListener; - private Multicast2BalancedTreeEventListener(QueryContext ctx, QueryEventListener.BalancedTreeEventListener listener) + private Multicast2BKDIndexEventListener(QueryContext ctx, QueryEventListener.BKDIndexEventListener listener) { this.ctx = ctx; this.listener = listener; - this.postingListEventListener = new Multicast2BalancedTreePostingListEventListener(ctx, listener.postingListEventListener()); + this.postingListEventListener = new Multicast2BKDPostingListEventListener(ctx, listener.postingListEventListener()); } @Override @@ -95,15 +95,15 @@ public void onIntersectionEarlyExit() @Override public void postingListsHit(int count) { - ctx.balancedTreePostingListsHit++; + ctx.addBkdPostingListsHit(1); listener.postingListsHit(count); } @Override public void onSegmentHit() { - ctx.segmentsHit++; - ctx.balancedTreeSegmentsHit++; + ctx.addSegmentsHit(1); + ctx.addBkdSegmentsHit(1); listener.onSegmentHit(); } @@ -114,12 +114,12 @@ public QueryEventListener.PostingListEventListener postingListEventListener() } } - public static class Multicast2BalancedTreePostingListEventListener implements QueryEventListener.PostingListEventListener + public static class Multicast2BKDPostingListEventListener implements QueryEventListener.PostingListEventListener { private final QueryContext ctx; private final QueryEventListener.PostingListEventListener listener; - Multicast2BalancedTreePostingListEventListener(QueryContext ctx, QueryEventListener.PostingListEventListener listener) + Multicast2BKDPostingListEventListener(QueryContext ctx, QueryEventListener.PostingListEventListener listener) { this.ctx = ctx; this.listener = listener; @@ -128,14 +128,14 @@ public static class Multicast2BalancedTreePostingListEventListener implements Qu @Override public void onAdvance() { - ctx.balancedTreePostingsSkips++; + ctx.addBkdPostingsSkips(1); listener.onAdvance(); } @Override public void postingDecoded(long postingDecoded) { - ctx.balancedTreePostingsDecodes += postingDecoded; + ctx.addBkdPostingsDecodes(postingDecoded); listener.postingDecoded(postingDecoded); } } @@ -154,14 +154,14 @@ public static class Multicast2TriePostingListEventListener implements QueryEvent @Override public void onAdvance() { - ctx.triePostingsSkips++; + ctx.addTriePostingsSkips(1); listener.onAdvance(); } @Override public void postingDecoded(long postingDecoded) { - ctx.triePostingsDecodes += postingDecoded; + ctx.addTriePostingsDecodes(postingDecoded); listener.postingDecoded(postingDecoded); } } diff --git a/src/java/org/apache/cassandra/index/sai/metrics/QueryEventListener.java b/src/java/org/apache/cassandra/index/sai/metrics/QueryEventListener.java index db583d8b402f..41257169a0ab 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/QueryEventListener.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/QueryEventListener.java @@ -19,18 +19,30 @@ import java.util.concurrent.TimeUnit; +import io.github.jbellis.jvector.graph.SearchResult; + /** * Listener that gets notified during storage-attached index query execution. */ public interface QueryEventListener { /** - * Collector for balanced tree file related metrics. + * Returns listener for bkd index events. + */ + BKDIndexEventListener bkdIndexEventListener(); + + /** + * Returns listener for trie index events. */ - interface BalancedTreeEventListener + TrieIndexEventListener trieIndexEventListener(); + + /** + * Collector for kd-tree index file related metrics. + */ + interface BKDIndexEventListener { /** - * Per-segment balanced tree index intersection time in given units. Recorded when intersection completes. + * Per-segment kd-tree index intersection time in given units. Recorded when intersection completes. */ void onIntersectionComplete(long intersectionTotalTime, TimeUnit unit); @@ -40,17 +52,17 @@ interface BalancedTreeEventListener void onIntersectionEarlyExit(); /** - * How many balanced tree posting list were matched during the intersection. + * How many bkd posting list were matched during the intersection. */ void postingListsHit(int count); /** - * When query potentially matches value range within a segment, and we need to do a traversal. + * When query potentially matches value range within a segment and we need to do a traversal. */ void onSegmentHit(); /** - * Returns events listener for balanced tree postings. + * Returns events listener for bkd postings. */ PostingListEventListener postingListEventListener(); } @@ -58,7 +70,7 @@ interface BalancedTreeEventListener interface TrieIndexEventListener { /** - * When query potentially matches value range within a segment, and we need to do a traversal. + * When query potentially matches value range within a segment and we need to do a traversal. */ void onSegmentHit(); @@ -93,12 +105,27 @@ interface PostingListEventListener @Override public void onAdvance() { + } @Override public void postingDecoded(long postingsDecoded) { + } }; } + + interface VectorIndexEventListener + { + void onGraphLoaded(long quantizationBytes, long ordinalsMapCachedBytes, long vectorsLoaded); + + void onGraphClosed(long pqBytes, long ordinalsMapCachedBytes, long vectorsLoaded); + + void onSearchResult(SearchResult result, long latencyNs, boolean isResume); + + void onBruteForceNodesVisited(int visited); + + void onBruteForceNodesReranked(int visited); + } } diff --git a/src/java/org/apache/cassandra/index/sai/metrics/TableQueryMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/TableQueryMetrics.java index bbfbe1d28701..7f1044cd86a8 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/TableQueryMetrics.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/TableQueryMetrics.java @@ -17,162 +17,542 @@ */ package org.apache.cassandra.index.sai.metrics; +import java.util.EnumMap; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; import com.codahale.metrics.Counter; import com.codahale.metrics.Histogram; import com.codahale.metrics.Timer; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.index.sai.QueryContext; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.tracing.Tracing; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; -public class TableQueryMetrics extends AbstractMetrics +/** + * Table query metrics for different kinds of query. The metrics for each type of query are divided into two groups: + *

      + *
    • Per table counters ({@link PerTable}).
    • + *
    • Per query timers and histograms ({@link PerQuery}).
    • + *
    + * The following kinds of query are tracked: + *
      + *
    • All SAI queries.
    • + *
    • Single-partition filter queries (filtering only, no top-k).
    • + *
    • Multi-partition filter queries (filtering only, no top-k).
    • + *
    • Single-partition top-k queries (top-k only, no filtering).
    • + *
    • Multi-partition top-k queries (top-k only, no filtering).
    • + *
    • Single-partition hybrid queries (both filtering and top-k).
    • + *
    • Multi-partition hybrid queries (both filtering and top-k).
    • + *
    + * The general metrics for all SAI queries are always recorded. The other kinds of queries are recorded only if they are + * enabled via the {@link CassandraRelevantProperties#SAI_QUERY_KIND_PER_TABLE_METRICS_ENABLED} and + * {@link CassandraRelevantProperties#SAI_QUERY_KIND_PER_QUERY_METRICS_ENABLED} system properties. + */ +public class TableQueryMetrics { - public static final String TABLE_QUERY_METRIC_TYPE = "TableQueryMetrics"; - - public final Timer postFilteringReadLatency; + /** Per table metrics for all kinds of queries (counters). */ + public final EnumMap perTableMetrics = new EnumMap<>(QueryKind.class); - private final PerQueryMetrics perQueryMetrics; - - private final Counter totalQueryTimeouts; - private final Counter totalPartitionReads; - private final Counter totalRowsFiltered; - private final Counter totalQueriesCompleted; + /** Per query metrics for all kinds of queries (timers and histograms). */ + public final EnumMap perQueryMetrics = new EnumMap<>(QueryKind.class); public TableQueryMetrics(TableMetadata table) { - super(table.keyspace, table.name, TABLE_QUERY_METRIC_TYPE); + addMetrics(table, QueryKind.ALL, cmd -> true); + addMetrics(table, QueryKind.SP_FILTER_ONLY, cmd -> !cmd.isTopK() && cmd.usesIndexFiltering() && cmd.isSinglePartition()); // single-partition-queries that are filtering only + addMetrics(table, QueryKind.MP_FILTER_ONLY, cmd -> !cmd.isTopK() && cmd.usesIndexFiltering() && !cmd.isSinglePartition()); // multi-partition queries that are filtering only + addMetrics(table, QueryKind.SP_TOPK_ONLY, cmd -> cmd.isTopK() && !cmd.usesIndexFiltering() && cmd.isSinglePartition()); // single-partition queries that are top-k only + addMetrics(table, QueryKind.MP_TOPK_ONLY, cmd -> cmd.isTopK() && !cmd.usesIndexFiltering() && !cmd.isSinglePartition()); // multi-partition queries that are top-k only + addMetrics(table, QueryKind.SP_HYBRID, cmd -> cmd.isTopK() && cmd.usesIndexFiltering() && cmd.isSinglePartition()); // single-partition queries that are both filtering and top-k + addMetrics(table, QueryKind.MP_HYBRID, cmd -> cmd.isTopK() && cmd.usesIndexFiltering() && !cmd.isSinglePartition()); // multi-partition queries that are both filtering and top-k + } - perQueryMetrics = new PerQueryMetrics(table); + public enum QueryKind + { + ALL(""), + SP_FILTER_ONLY("SinglePartitionFilterOnly"), + MP_FILTER_ONLY("MultiPartitionFilterOnly"), + SP_TOPK_ONLY("SinglePartitionTopKOnly"), + MP_TOPK_ONLY("MultiPartitionTopKOnly"), + SP_HYBRID("SinglePartitionHybrid"), + MP_HYBRID("MultiPartitionHybrid"); - postFilteringReadLatency = Metrics.timer(createMetricName("PostFilteringReadLatency")); + private final String name; - totalPartitionReads = Metrics.counter(createMetricName("TotalPartitionReads")); - totalRowsFiltered = Metrics.counter(createMetricName("TotalRowsFiltered")); - totalQueriesCompleted = Metrics.counter(createMetricName("TotalQueriesCompleted")); - totalQueryTimeouts = Metrics.counter(createMetricName("TotalQueryTimeouts")); + QueryKind(String name) + { + this.name = name; + } } - public void record(QueryContext queryContext) + private void addMetrics(TableMetadata table, QueryKind queryKind, Predicate filter) { - if (queryContext.queryTimedOut) - totalQueryTimeouts.inc(); + if (queryKind == QueryKind.ALL) + { + perTableMetrics.put(queryKind, new PerTableAll(table, queryKind, filter)); + perQueryMetrics.put(queryKind, new PerQuery(table, queryKind, filter)); + } + else + { + if (CassandraRelevantProperties.SAI_QUERY_KIND_PER_TABLE_METRICS_ENABLED.getBoolean()) + perTableMetrics.put(queryKind, new PerTable(table, queryKind, filter)); - perQueryMetrics.record(queryContext); + if (CassandraRelevantProperties.SAI_QUERY_KIND_PER_QUERY_METRICS_ENABLED.getBoolean()) + perQueryMetrics.put(queryKind, new PerQuery(table, queryKind, filter)); + } } + /** + * Records metrics for a single query. + * + * @param context the stats relevant to the execution of a single query + * @param command the query command + */ + public void record(QueryContext context, ReadCommand command) + { + QueryContext.Snapshot snapshot = context.snapshot(); + perTableMetrics.values().forEach(m -> m.record(snapshot, command)); + perQueryMetrics.values().forEach(m -> m.record(snapshot, command)); + + if (Tracing.isTracing()) + { + final long queryLatencyMicros = TimeUnit.NANOSECONDS.toMicros(snapshot.totalQueryTimeNs); + + if (snapshot.queryPlanInfo != null && snapshot.queryPlanInfo.searchExecutedBeforeOrder) + { + Tracing.trace("Index query accessed memtable indexes, {}, and {}, selected {} before ranking, " + + "post-filtered {} in {}, and took {} microseconds.", + pluralize(snapshot.sstablesHit, "SSTable index", "es"), + pluralize(snapshot.segmentsHit, "segment", "s"), + pluralize(snapshot.rowsFetched, "row", "s"), + pluralize(snapshot.rowsReturned, "row", "s"), + pluralize(snapshot.partitionsReturned, "partition", "s"), + queryLatencyMicros); + } + else + { + Tracing.trace("Index query accessed memtable indexes, {}, and {}, post-filtered {} in {}, " + + "and took {} microseconds.", + pluralize(snapshot.sstablesHit, "SSTable index", "es"), + pluralize(snapshot.segmentsHit, "segment", "s"), + pluralize(snapshot.rowsReturned, "row", "s"), + pluralize(snapshot.partitionsReturned, "partition", "s"), + queryLatencyMicros); + } + } + } + + /** + * Releases all the resources used by these metrics. + */ public void release() { - super.release(); - perQueryMetrics.release(); + perTableMetrics.values().forEach(PerTable::release); + perQueryMetrics.values().forEach(PerQuery::release); } - public class PerQueryMetrics extends AbstractMetrics + private static String pluralize(long count, String root, String plural) { - public static final String PER_QUERY_METRICS_TYPE = "PerQuery"; + return count == 1 ? String.format("1 %s", root) : String.format("%d %s%s", count, root, plural); + } - private final Timer queryLatency; + /** + * Family of metrics for a specific kind of query. + */ + public abstract static class AbstractQueryMetrics extends AbstractMetrics + { + private static final Pattern PATTERN = Pattern.compile("Query"); - /** - * Global metrics for all indices hit during the query. - */ - private final Histogram sstablesHit; - private final Histogram segmentsHit; - private final Histogram partitionReads; - private final Histogram rowsFiltered; + private final Predicate filter; + + private AbstractQueryMetrics(String keyspace, String table, String scope, QueryKind queryKind, Predicate filter) + { + super(keyspace, table, makeName(scope, queryKind)); + this.filter = filter; + } + + public void record(QueryContext.Snapshot snapshot, ReadCommand command) + { + if (filter.test(command)) + record(snapshot); + } + + protected abstract void record(QueryContext.Snapshot snapshot); + + public static String makeName(String scope, QueryKind queryKind) + { + return PATTERN.matcher(scope).replaceFirst(queryKind.name + "Query"); + } + } + + /** + * Per table metrics for a specific kind of query. These metrics are always counters. + */ + public static class PerTable extends AbstractQueryMetrics + { + public static final String METRIC_TYPE = "TableQueryMetrics"; + + /** Total number of queries that have timed out. */ + public final Counter totalQueryTimeouts; + + /** Total number of partition/row keys fetched from the indexes. */ + public final Counter totalKeysFetched; + + /** Total number of live partitions fetched from the storage engine, before post-filtering. */ + public final Counter totalPartitionsFetched; + + /** Total number of live partitions returned to the coordinator, after post-filtering. */ + public final Counter totalPartitionsReturned; + + /** Total number of deleted partitions that are fetched. */ + public final Counter totalPartitionTombstonesFetched; + + /** Total number of live rows fetched from the storage engine, before post-filtering. */ + public final Counter totalRowsFetched; + + /** Total number of live rows returned to the coordinator, after post-filtering. */ + public final Counter totalRowsReturned; + + /** Total number of deleted individual rows or ranges of rows that are fetched. */ + public final Counter totalRowTombstonesFetched; + + /** Total number of completed queries. */ + public final Counter totalQueriesCompleted; /** - * Balanced tree index metrics. - */ - private final Histogram balancedTreePostingsNumPostings; - /** - * Balanced tree index posting lists metrics. + * Aggregated metrics about the query plans, {@code null} if not enbaled in + * {@link CassandraRelevantProperties#SAI_QUERY_PLAN_METRICS_ENABLED}. */ - private final Histogram balancedTreePostingsSkips; - private final Histogram balancedTreePostingsDecodes; + @Nullable + public final QueryPlanMetrics queryPlanMetrics; /** - * Trie index posting lists metrics. + * @param table the table to measure metrics for + * @param queryKind an identifier for the kind of query which metrics are being recorded for + * @param filter a predicate that determines whether a given query should be recorded */ - private final Histogram postingsSkips; - private final Histogram postingsDecodes; + public PerTable(TableMetadata table, QueryKind queryKind, Predicate filter) + { + super(table.keyspace, table.name, METRIC_TYPE, queryKind, filter); + + totalKeysFetched = Metrics.counter(createMetricName("TotalKeysFetched")); + totalPartitionsFetched = Metrics.counter(createMetricName("TotalPartitionsFetched")); + totalPartitionsReturned = Metrics.counter(createMetricName("TotalPartitionsReturned")); + totalPartitionTombstonesFetched = Metrics.counter(createMetricName("TotalPartitionTombstonesFetched")); + totalRowsFetched = Metrics.counter(createMetricName("TotalRowsFetched")); + totalRowsReturned = Metrics.counter(createMetricName("TotalRowsReturned")); + totalRowTombstonesFetched = Metrics.counter(createMetricName("TotalRowTombstonesFetched")); + totalQueriesCompleted = Metrics.counter(createMetricName("TotalQueriesCompleted")); + totalQueryTimeouts = Metrics.counter(createMetricName("TotalQueryTimeouts")); + queryPlanMetrics = (CassandraRelevantProperties.SAI_QUERY_PLAN_METRICS_ENABLED.getBoolean()) + ? new QueryPlanMetrics() + : null; + } - public PerQueryMetrics(TableMetadata table) + @Override + public void record(QueryContext.Snapshot snapshot) { - super(table.keyspace, table.name, PER_QUERY_METRICS_TYPE); + if (snapshot.queryTimedOut) + { + totalQueryTimeouts.inc(); + } - queryLatency = Metrics.timer(createMetricName("QueryLatency")); + totalQueriesCompleted.inc(); + totalKeysFetched.inc(snapshot.keysFetched); + totalPartitionsFetched.inc(snapshot.partitionsFetched); + totalPartitionsReturned.inc(snapshot.partitionsReturned); + totalPartitionTombstonesFetched.inc(snapshot.partitionTombstonesFetched); + totalRowsFetched.inc(snapshot.rowsFetched); + totalRowsReturned.inc(snapshot.rowsReturned); + totalRowTombstonesFetched.inc(snapshot.rowTombstonesFetched); + + QueryContext.PlanInfo queryPlanInfo = snapshot.queryPlanInfo; + if (queryPlanInfo != null && queryPlanMetrics != null) + { + queryPlanMetrics.totalCostEstimated.inc(queryPlanInfo.costEstimated); + queryPlanMetrics.totalRowsToReturnEstimated.inc(queryPlanInfo.rowsToReturnEstimated); + queryPlanMetrics.totalRowsToFetchEstimated.inc(queryPlanInfo.rowsToFetchEstimated); + queryPlanMetrics.totalKeysToIterateEstimated.inc(queryPlanInfo.keysToIterateEstimated); + + if (queryPlanInfo.filterExecutedAfterOrderedScan) + queryPlanMetrics.sortThenFilterQueriesCompleted.inc(); + if (queryPlanInfo.searchExecutedBeforeOrder) + queryPlanMetrics.filterThenSortQueriesCompleted.inc(); + } + } - sstablesHit = Metrics.histogram(createMetricName("SSTableIndexesHit"), false); - segmentsHit = Metrics.histogram(createMetricName("IndexSegmentsHit"), false); + public class QueryPlanMetrics + { + public final Counter totalRowsToReturnEstimated; + public final Counter totalRowsToFetchEstimated; + public final Counter totalKeysToIterateEstimated; + public final Counter totalCostEstimated; - balancedTreePostingsSkips = Metrics.histogram(createMetricName("BalancedTreePostingsSkips"), false); + public final Counter sortThenFilterQueriesCompleted; + public final Counter filterThenSortQueriesCompleted; - balancedTreePostingsNumPostings = Metrics.histogram(createMetricName("BalancedTreePostingsNumPostings"), false); - balancedTreePostingsDecodes = Metrics.histogram(createMetricName("BalancedTreePostingsDecodes"), false); - postingsSkips = Metrics.histogram(createMetricName("PostingsSkips"), false); - postingsDecodes = Metrics.histogram(createMetricName("PostingsDecodes"), false); + public QueryPlanMetrics() + { + totalRowsToReturnEstimated = Metrics.counter(createMetricName("TotalRowsToReturnEstimated")); + totalRowsToFetchEstimated = Metrics.counter(createMetricName("TotalRowsToFetchEstimated")); + totalKeysToIterateEstimated = Metrics.counter(createMetricName("TotalKeysToIterateEstimated")); + totalCostEstimated = Metrics.counter(createMetricName("TotalCostEstimated")); + + sortThenFilterQueriesCompleted = Metrics.counter(createMetricName("SortThenFilterQueriesCompleted")); + filterThenSortQueriesCompleted = Metrics.counter(createMetricName("FilterThenSortQueriesCompleted")); + } + } + } + + public static class PerTableAll extends PerTable + { + /** Total number of completed BM25 queries. */ + public final Counter totalBM25QueriesCompleted; - partitionReads = Metrics.histogram(createMetricName("PartitionReads"), false); - rowsFiltered = Metrics.histogram(createMetricName("RowsFiltered"), false); + public PerTableAll(TableMetadata table, QueryKind queryKind, Predicate filter) + { + super(table, queryKind, filter); + totalBM25QueriesCompleted = Metrics.counter(createMetricName("TotalBM25QueriesCompleted")); } - private void recordStringIndexCacheMetrics(QueryContext events) + @Override + public void record(QueryContext.Snapshot snapshot) { - postingsSkips.update(events.triePostingsSkips); - postingsDecodes.update(events.triePostingsDecodes); + super.record(snapshot); } - private void recordNumericIndexCacheMetrics(QueryContext events) + @Override + public final void record(QueryContext.Snapshot snapshot, ReadCommand command) { - balancedTreePostingsNumPostings.update(events.balancedTreePostingListsHit); + super.record(snapshot, command); - balancedTreePostingsSkips.update(events.balancedTreePostingsSkips); - balancedTreePostingsDecodes.update(events.balancedTreePostingsDecodes); + if (command.isBM25()) + totalBM25QueriesCompleted.inc(); } + } + + /** + * Per query metrics for a specific kind of query. These metrics are always timers and histograms. + */ + public static class PerQuery extends AbstractQueryMetrics + { + public static final String METRIC_TYPE = "PerQuery"; + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public final Optional queryLatency; + + /** Number of sstables visited by the query. */ + public final Histogram sstablesHit; + + /** Number of index segments having results for the query. */ + public final Histogram segmentsHit; + + /** Number of partition/row keys fetched from the indexes. */ + public final Histogram keysFetched; + + /** Number of live partitions fetched from the storage engine, before post-filtering. */ + public final Histogram partitionsFetched; + + /** Number of live partitions returned to the coordinator, after post-filtering. */ + public final Histogram partitionsReturned; + + /** Number of deleted partitions that have been fetched. */ + public final Histogram partitionTombstonesFetched; + + /** Number of live rows fetched from the storage engine, before post-filtering. */ + public final Histogram rowsFetched; + + /** Number of live rows returned to the coordinator, after post-filtering. */ + public final Histogram rowsReturned; + + /** Number of deleted individual rows or ranges of rows that have been fetched. */ + public final Histogram rowTombstonesFetched; + + /** Number of times the query has jumped to the position of a row ID within a trie (literal or key) posting list. */ + public final Histogram postingsSkips; + + /** Number of times the query has advanced into a trie (literal or key) posting list. */ + public final Histogram postingsDecodes; + + /** Number of BKD (numeric) merged posting lists visited by the query. */ + public final Histogram kdTreePostingsNumPostings; + + /** Number of times the query has jumped to the position of a row ID within a BKD (numeric) posting list. */ + public final Histogram kdTreePostingsSkips; + + /** Number of times the query has advanced into a BKD (numeric) posting list. */ + public final Histogram kdTreePostingsDecodes; - public void record(QueryContext queryContext) + /** + * Cumulative time spent searching ANN graph. + */ + public final Timer annGraphSearchLatency; + + public final Timer postFilteringReadLatency; + + /** + * Aggregated metrics about the query plans, {@code null} if not enbaled in + * {@link CassandraRelevantProperties#SAI_QUERY_PLAN_METRICS_ENABLED}. + */ + @Nullable + public final QueryPlanMetrics queryPlanMetrics; + + /** + * @param table the table to measure metrics for + * @param queryKind an identifier for the kind of query which metrics are being recorded for + * @param filter a predicate that determines whether a given query should be recorded + */ + public PerQuery(TableMetadata table, QueryKind queryKind, Predicate filter) { - final long totalQueryTimeNs = queryContext.totalQueryTimeNs(); - queryLatency.update(totalQueryTimeNs, TimeUnit.NANOSECONDS); - final long queryLatencyMicros = TimeUnit.NANOSECONDS.toMicros(totalQueryTimeNs); + super(table.keyspace, table.name, METRIC_TYPE, queryKind, filter); + + queryLatency = CassandraRelevantProperties.SAI_HISTOGRAMS_ENABLED.getBoolean() + ? Optional.of(Metrics.timer(createMetricName("QueryLatency"))) + : Optional.empty(); + + sstablesHit = Metrics.histogram(createMetricName("SSTableIndexesHit"), false); + segmentsHit = Metrics.histogram(createMetricName("IndexSegmentsHit"), false); + keysFetched = Metrics.histogram(createMetricName("KeysFetched"), false); + partitionsFetched = Metrics.histogram(createMetricName("PartitionsFetched"), false); + partitionsReturned = Metrics.histogram(createMetricName("PartitionsReturned"), false); + partitionTombstonesFetched = Metrics.histogram(createMetricName("PartitionTombstonesFetched"), false); + rowsFetched = Metrics.histogram(createMetricName("RowsFetched"), false); + rowsReturned = Metrics.histogram(createMetricName("RowsReturned"), false); + rowTombstonesFetched = Metrics.histogram(createMetricName("RowTombstonesFetched"), false); + + postingsSkips = Metrics.histogram(createMetricName("PostingsSkips"), true); + postingsDecodes = Metrics.histogram(createMetricName("PostingsDecodes"), false); + + kdTreePostingsSkips = Metrics.histogram(createMetricName("KDTreePostingsSkips"), true); + kdTreePostingsNumPostings = Metrics.histogram(createMetricName("KDTreePostingsNumPostings"), false); + kdTreePostingsDecodes = Metrics.histogram(createMetricName("KDTreePostingsDecodes"), false); - sstablesHit.update(queryContext.sstablesHit); - segmentsHit.update(queryContext.segmentsHit); + // Key vector metrics that translate to performance + annGraphSearchLatency = Metrics.timer(createMetricName("ANNGraphSearchLatency")); + postFilteringReadLatency = Metrics.timer(createMetricName("PostFilteringReadLatency")); - partitionReads.update(queryContext.partitionsRead); - totalPartitionReads.inc(queryContext.partitionsRead); + queryPlanMetrics = CassandraRelevantProperties.SAI_QUERY_PLAN_METRICS_ENABLED.getBoolean() + ? new QueryPlanMetrics() + : null; + } - rowsFiltered.update(queryContext.rowsFiltered); - totalRowsFiltered.inc(queryContext.rowsFiltered); + @Override + public void record(QueryContext.Snapshot snapshot) + { + queryLatency.ifPresent(timer -> timer.update(snapshot.totalQueryTimeNs, TimeUnit.NANOSECONDS)); + sstablesHit.update(snapshot.sstablesHit); + segmentsHit.update(snapshot.segmentsHit); + keysFetched.update(snapshot.keysFetched); + partitionsFetched.update(snapshot.partitionsFetched); + partitionsReturned.update(snapshot.partitionsReturned); + partitionTombstonesFetched.update(snapshot.partitionTombstonesFetched); + rowsFetched.update(snapshot.rowsFetched); + rowsReturned.update(snapshot.rowsReturned); + rowTombstonesFetched.update(snapshot.rowTombstonesFetched); + + // Record literal index cache metrics. + if (snapshot.trieSegmentsHit > 0) + { + postingsSkips.update(snapshot.triePostingsSkips); + postingsDecodes.update(snapshot.triePostingsDecodes); + } - if (Tracing.isTracing()) + // Record numeric index cache metrics. + if (snapshot.bkdSegmentsHit > 0) { - Tracing.trace("Index query accessed memtable indexes, {}, and {}, post-filtered {} in {}, and took {} microseconds.", - pluralize(queryContext.sstablesHit, "SSTable index", "es"), pluralize(queryContext.segmentsHit, "segment", "s"), - pluralize(queryContext.rowsFiltered, "row", "s"), pluralize(queryContext.partitionsRead, "partition", "s"), - queryLatencyMicros); + kdTreePostingsNumPostings.update(snapshot.bkdPostingListsHit); + kdTreePostingsSkips.update(snapshot.bkdPostingsSkips); + kdTreePostingsDecodes.update(snapshot.bkdPostingsDecodes); } - if (queryContext.trieSegmentsHit > 0) + // Record vector index metrics. + // If ann brute forced the whole search, this is 0. We don't measure brute force latency. Maybe we should? + // At the very least, we collect brute force comparison metrics, which should give a reasonable indicator + // of work done. + if (snapshot.annGraphSearchLatency > 0) { - recordStringIndexCacheMetrics(queryContext); + annGraphSearchLatency.update(snapshot.annGraphSearchLatency, TimeUnit.NANOSECONDS); } + postFilteringReadLatency.update(snapshot.postFilteringReadLatency, TimeUnit.NANOSECONDS); - if (queryContext.balancedTreeSegmentsHit > 0) + QueryContext.PlanInfo queryPlanInfo = snapshot.queryPlanInfo; + if (queryPlanInfo != null && queryPlanMetrics != null) { - recordNumericIndexCacheMetrics(queryContext); + queryPlanMetrics.costEstimated.update(queryPlanInfo.costEstimated); + queryPlanMetrics.rowsToReturnEstimated.update(queryPlanInfo.rowsToReturnEstimated); + queryPlanMetrics.rowsToFetchEstimated.update(queryPlanInfo.rowsToFetchEstimated); + queryPlanMetrics.keysToIterateEstimated.update(queryPlanInfo.keysToIterateEstimated); + queryPlanMetrics.logSelectivityEstimated.update(queryPlanInfo.logSelectivityEstimated); + queryPlanMetrics.indexReferencesInQuery.update(queryPlanInfo.indexReferencesInQuery); + queryPlanMetrics.indexReferencesInPlan.update(queryPlanInfo.indexReferencesInPlan); } + } - totalQueriesCompleted.inc(); + /// Metrics related to query planning. + /// Moved to separate class so they can be enabled/disabled as a group. + public class QueryPlanMetrics + { + /** + * Query execution cost as estimated by the planner + */ + public final Histogram costEstimated; + + /** + * Number of rows to be returned from the query as estimated by the planner + */ + public final Histogram rowsToReturnEstimated; + + /** + * Number of rows to be fetched by the query as estimated by the planner + */ + public final Histogram rowsToFetchEstimated; + + /** + * Number of keys to be iterated by the query as estimated by the planner + */ + public final Histogram keysToIterateEstimated; + + /** + * Negative decimal logarithm of selectivity of the query, before applying the LIMIT clause. + * We use logarithm because selectivity values can be very small (e.g. 10^-9). + */ + public final Histogram logSelectivityEstimated; + + /** + * Number of indexes referenced by the optimized query plan. + * The same index referenced from unrelated query clauses, + * leading to separate index searches, are counted separately. + */ + public final Histogram indexReferencesInPlan; + + /** + * Number of indexes referenced by the original query plan before optimization (as stated in the query text) + */ + public final Histogram indexReferencesInQuery; + + QueryPlanMetrics() + { + costEstimated = Metrics.histogram(createMetricName("CostEstimated"), false); + rowsToReturnEstimated = Metrics.histogram(createMetricName("RowsToReturnEstimated"), true); + rowsToFetchEstimated = Metrics.histogram(createMetricName("RowsToFetchEstimated"), true); + keysToIterateEstimated = Metrics.histogram(createMetricName("KeysToIterateEstimated"), true); + logSelectivityEstimated = Metrics.histogram(createMetricName("LogSelectivityEstimated"), true); + indexReferencesInPlan = Metrics.histogram(createMetricName("IndexReferencesInPlan"), true); + indexReferencesInQuery = Metrics.histogram(createMetricName("IndexReferencesInQuery"), false); + } } - } - private String pluralize(long count, String root, String plural) - { - return count == 1 ? String.format("1 %s", root) : String.format("%d %s%s", count, root, plural); } + + } diff --git a/src/java/org/apache/cassandra/index/sai/metrics/TableStateMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/TableStateMetrics.java index f7b64055206d..482b0d2f6d5b 100644 --- a/src/java/org/apache/cassandra/index/sai/metrics/TableStateMetrics.java +++ b/src/java/org/apache/cassandra/index/sai/metrics/TableStateMetrics.java @@ -28,19 +28,26 @@ public class TableStateMetrics extends AbstractMetrics { public static final String TABLE_STATE_METRIC_TYPE = "TableStateMetrics"; + // Visible for CNDB + public final Gauge diskUsageBytes; + private final Gauge diskUsagePercentageOfBaseTable; + private final Gauge totalIndexCount; + private final Gauge totalIndexBuildsInProgress; + private final Gauge totalQueryableIndexCount; + public TableStateMetrics(TableMetadata table, StorageAttachedIndexGroup group) { super(table.keyspace, table.name, TABLE_STATE_METRIC_TYPE); - Metrics.register(createMetricName("DiskUsedBytes"), (Gauge) group::totalDiskUsage); - Metrics.register(createMetricName("DiskPercentageOfBaseTable"), (Gauge) new RatioGauge() { + totalQueryableIndexCount = Metrics.register(createMetricName("TotalQueryableIndexCount"), group::totalQueryableIndexCount); + totalIndexCount = Metrics.register(createMetricName("TotalIndexCount"), group::totalIndexCount); + totalIndexBuildsInProgress = Metrics.register(createMetricName("TotalIndexBuildsInProgress"), group::totalIndexBuildsInProgress); + diskUsageBytes = Metrics.register(createMetricName("DiskUsedBytes"), group::totalDiskUsage); + diskUsagePercentageOfBaseTable = Metrics.register(createMetricName("DiskPercentageOfBaseTable"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(group.totalDiskUsage(), group.table().metric.liveDiskSpaceUsed.getCount()); } }); - Metrics.register(createMetricName("TotalIndexCount"), (Gauge) group::totalIndexCount); - Metrics.register(createMetricName("TotalQueryableIndexCount"), (Gauge) group::totalQueryableIndexCount); - Metrics.register(createMetricName("TotalIndexBuildsInProgress"), (Gauge) group::totalIndexBuildsInProgress); } } diff --git a/src/java/org/apache/cassandra/index/sai/plan/CountFetchedTransformation.java b/src/java/org/apache/cassandra/index/sai/plan/CountFetchedTransformation.java new file mode 100644 index 000000000000..443462d5a077 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/CountFetchedTransformation.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.plan; + +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.index.sai.QueryContext; + +/** + * Counts the number of partitions, rows and tombstones fetched by an index query, before post-filtering and sorting. + */ +class CountFetchedTransformation extends Transformation +{ + private final QueryContext queryContext; + private final long nowInSec; + + CountFetchedTransformation(QueryContext queryContext, long nowInSec) + { + this.queryContext = queryContext; + this.nowInSec = nowInSec; + } + + /** + * Updates the query context metrics about the number of fetched partitions, rows and tombstones + * with the contents of the provided partition iterator. + * + * @param partition the results of querying the base table with the indexed keys, before applying post-filtering and sorting + * @return a copy of the provided row iterator, which will populate the query context as it is consumed + */ + UnfilteredRowIterator apply(UnfilteredRowIterator partition) + { + return Transformation.apply(partition, this); + } + + @Override + protected DeletionTime applyToDeletion(DeletionTime deletionTime) + { + queryContext.checkpoint(); + if (deletionTime.deletes(nowInSec)) + queryContext.addPartitionTombstonesFetched(1); + else + queryContext.addPartitionsFetched(1); + return deletionTime; + } + + @Override + protected Row applyToRow(Row row) + { + queryContext.checkpoint(); + if (row.hasLiveData(nowInSec, false)) + queryContext.addRowsFetched(1); + else + queryContext.addRowTombstonesFetched(1); + return row; + } + + @Override + protected RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker) + { + queryContext.checkpoint(); + queryContext.addRowTombstonesFetched(1); + return marker; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/CountReturnedTransformation.java b/src/java/org/apache/cassandra/index/sai/plan/CountReturnedTransformation.java new file mode 100644 index 000000000000..d442e93a4b84 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/CountReturnedTransformation.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.plan; + +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.index.sai.QueryContext; + +/** + * Counts the final number of partitions and rows returned by a query to the coordinator, after post-filtering and sorting. + * Tombstones are not counted because they are not returned to the coordinator. + */ +class CountReturnedTransformation extends Transformation +{ + private final QueryContext queryContext; + private final Runnable onClose; + private final Transformation rowCounter; + + private CountReturnedTransformation(QueryContext queryContext, Runnable onClose) + { + this.queryContext = queryContext; + this.onClose = onClose; + rowCounter = new Transformation<>() { + @Override + protected Row applyToRow(Row row) + { + queryContext.checkpoint(); + queryContext.addRowsReturned(1); + return row; + } + }; + } + + /** + * Updates the query context metrics about the number of partitions and rows returned to the coordinator + * with the contents of the provided partition iterator. + * + * @param partition the partition iterator containing the final results to return to the coordinator + * @param queryContext the query context to update with the metrics + * @param onClose a callback to run when the transformation is closed + * @return a copy of the provided partition iterator, which will populate the query context as it is consumed + */ + static UnfilteredPartitionIterator apply(UnfilteredPartitionIterator partition, QueryContext queryContext, Runnable onClose) + { + return Transformation.apply(partition, new CountReturnedTransformation(queryContext, onClose)); + } + + @Override + protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) + { + queryContext.checkpoint(); + queryContext.addPartitionsReturned(1); + return Transformation.apply(partition, rowCounter); + } + + @Override + protected void onClose() + { + onClose.run(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/Expression.java b/src/java/org/apache/cassandra/index/sai/plan/Expression.java index 262fc3a9cc4c..def6fac7b6b3 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/Expression.java +++ b/src/java/org/apache/cassandra/index/sai/plan/Expression.java @@ -1,3 +1,9 @@ +/* + * All changes to the original code are Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -19,87 +25,90 @@ package org.apache.cassandra.index.sai.plan; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.Objects; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterators; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sai.utils.IndexTermType; - -/** - * An {@link Expression} is an internal representation of an index query operation. They are built from - * CQL {@link Operator} and {@link ByteBuffer} value pairs for a single column. - *

    - * Each {@link Expression} consists of an {@link IndexOperator} and optional lower and upper {@link Bound}s. - *

    - * The {@link IndexedExpression} has a backing {@link StorageAttachedIndex} for the index query but order to support - * CQL expressions on columns that do not have indexes or use operators that are not supported by the index there is - * an {@link UnindexedExpression} that does not provide a {@link StorageAttachedIndex} and can only be used for - * post-filtering - */ -public abstract class Expression +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.utils.GeoUtil; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.lucene.util.SloppyMath; + +public class Expression { - Logger logger = LoggerFactory.getLogger(Expression.class); - - private final IndexTermType indexTermType; - protected IndexOperator operator; - - public Bound lower, upper; - // The upperInclusive and lowerInclusive flags are maintained separately to the inclusive flags - // in the upper and lower bounds because the upper and lower bounds have their inclusivity relaxed - // if the datatype being filtered is rounded in the index. These flags are used in the post-filtering - // process to remove values equal to the bounds. - public boolean upperInclusive, lowerInclusive; - - Expression(IndexTermType indexTermType) - { - this.indexTermType = indexTermType; - } + private static final Logger logger = LoggerFactory.getLogger(Expression.class); - public static Expression create(StorageAttachedIndex index) + public enum Op { - return new IndexedExpression(index); - } - - public static Expression create(IndexTermType indexTermType) - { - return new UnindexedExpression(indexTermType); - } + EQ, MATCH, PREFIX, NOT_EQ, RANGE, + CONTAINS_KEY, CONTAINS_VALUE, + NOT_CONTAINS_VALUE, NOT_CONTAINS_KEY, + IN, ORDER_BY, BOUNDED_ANN; - public static boolean supportsOperator(Operator operator) - { - return IndexOperator.valueOf(operator) != null; - } - - public enum IndexOperator - { - EQ, RANGE, CONTAINS_KEY, CONTAINS_VALUE, ANN; - - public static IndexOperator valueOf(Operator operator) + public static Op valueOf(Operator operator) { switch (operator) { case EQ: return EQ; + case NEQ: + return NOT_EQ; + case CONTAINS: return CONTAINS_VALUE; // non-frozen map: value contains term; case CONTAINS_KEY: return CONTAINS_KEY; // non-frozen map: value contains key term; + case NOT_CONTAINS: + return NOT_CONTAINS_VALUE; + + case NOT_CONTAINS_KEY: + return NOT_CONTAINS_KEY; + case LT: case GT: case LTE: case GTE: return RANGE; + case LIKE_PREFIX: + return PREFIX; + + case LIKE_MATCHES: + case ANALYZER_MATCHES: + return MATCH; + + case IN: + return IN; + case ANN: - return ANN; + case BM25: + case ORDER_BY_ASC: + case ORDER_BY_DESC: + return ORDER_BY; + + case BOUNDED_ANN: + return BOUNDED_ANN; default: return null; @@ -115,44 +124,52 @@ public boolean isEqualityOrRange() { return isEquality() || this == RANGE; } - } - public abstract boolean isNotIndexed(); + public boolean isNonEquality() + { + return this == NOT_EQ || this == NOT_CONTAINS_KEY || this == NOT_CONTAINS_VALUE; + } - public abstract StorageAttachedIndex getIndex(); + public boolean isContains() + { + return this == CONTAINS_KEY + || this == CONTAINS_VALUE + || this == NOT_CONTAINS_KEY + || this == NOT_CONTAINS_VALUE; + } + } - abstract boolean hasAnalyzer(); + public final AbstractAnalyzer.AnalyzerFactory analyzerFactory; - abstract AbstractAnalyzer getAnalyzer(); + public final IndexContext context; + public final AbstractType validator; - public IndexOperator getIndexOperator() - { - return operator; - } + @VisibleForTesting + protected Op operation; - public IndexTermType getIndexTermType() - { - return indexTermType; - } + public Bound lower, upper; + private float boundedAnnEuclideanDistanceThreshold = 0; + private float searchRadiusMeters = 0; + private float searchRadiusDegreesSquared = 0; + public int topK; + // These variables are only meant to be used for final validation of the range search. They are not + // meant to be used when searching the index. See the 'add' method below for additional explanation. + private boolean upperInclusive, lowerInclusive; - public Bound lower() + final List exclusions = new ArrayList<>(); + + public Expression(IndexContext indexContext) { - return lower; + this.context = indexContext; + this.analyzerFactory = indexContext.getAnalyzerFactory(); + this.validator = indexContext.getValidator(); } - public Bound upper() + public boolean isLiteral() { - return upper; + return context.isLiteral(); } - /** - * This adds an operation to the current {@link Expression} instance and - * returns the current instance. - * - * @param op the CQL3 operation - * @param value the expression value - * @return the current expression with the added operation - */ public Expression add(Operator op, ByteBuffer value) { boolean lowerInclusive, upperInclusive; @@ -160,19 +177,38 @@ public Expression add(Operator op, ByteBuffer value) // range search is always inclusive, otherwise we run the risk of // missing values that are within the exclusive range but are rejected // because their rounded value is the same as the value being queried. - lowerInclusive = upperInclusive = indexTermType.supportsRounding(); + lowerInclusive = upperInclusive = TypeUtil.supportsRounding(validator); switch (op) { + case LIKE_PREFIX: + case LIKE_MATCHES: + case ANALYZER_MATCHES: case EQ: case CONTAINS: case CONTAINS_KEY: - lower = new Bound(value, indexTermType, true); + case NOT_CONTAINS: + case NOT_CONTAINS_KEY: + lower = new Bound(value, validator, true); upper = lower; - operator = IndexOperator.valueOf(op); + operation = Op.valueOf(op); + break; + + case NEQ: + // index expressions are priority sorted + // and NOT_EQ is the lowest priority, which means that operation type + // is always going to be set before reaching it in case of RANGE or EQ. + if (operation == null) + { + operation = Op.NOT_EQ; + lower = new Bound(value, validator, true); + upper = lower; + } + else + exclusions.add(value); break; case LTE: - if (indexTermType.isReversed()) + if (context.getDefinition().isReversedType()) { this.lowerInclusive = true; lowerInclusive = true; @@ -183,15 +219,15 @@ public Expression add(Operator op, ByteBuffer value) upperInclusive = true; } case LT: - operator = IndexOperator.RANGE; - if (indexTermType.isReversed()) - lower = new Bound(value, indexTermType, lowerInclusive); + operation = Op.RANGE; + if (context.getDefinition().isReversedType()) + lower = new Bound(value, validator, lowerInclusive); else - upper = new Bound(value, indexTermType, upperInclusive); + upper = new Bound(value, validator, upperInclusive); break; case GTE: - if (indexTermType.isReversed()) + if (context.getDefinition().isReversedType()) { this.upperInclusive = true; upperInclusive = true; @@ -202,56 +238,81 @@ public Expression add(Operator op, ByteBuffer value) lowerInclusive = true; } case GT: - operator = IndexOperator.RANGE; - if (indexTermType.isReversed()) - upper = new Bound(value, indexTermType, upperInclusive); + operation = Op.RANGE; + if (context.getDefinition().isReversedType()) + upper = new Bound(value, validator, upperInclusive); else - lower = new Bound(value, indexTermType, lowerInclusive); + lower = new Bound(value, validator, lowerInclusive); + break; + case BOUNDED_ANN: + operation = Op.BOUNDED_ANN; + lower = new Bound(value, validator, true); + assert upper != null; + searchRadiusMeters = FloatType.instance.compose(upper.value.raw); + boundedAnnEuclideanDistanceThreshold = GeoUtil.amplifiedEuclideanSimilarityThreshold(lower.value.vector, searchRadiusMeters); break; case ANN: - operator = IndexOperator.ANN; - lower = new Bound(value, indexTermType, true); - upper = lower; + case BM25: + case ORDER_BY_ASC: + case ORDER_BY_DESC: + // If we alread have an operation on the column, we don't need to set the ORDER_BY op because + // it is only used to force validation on a column, and the presence of another operation will do that. + if (operation == null) + operation = Op.ORDER_BY; break; default: - throw new IllegalArgumentException("Index does not support the " + op + " operator"); + throw new UnsupportedOperationException("Unsupported operator: " + op); } + assert operation != null; + return this; } - /** - * Used in post-filtering to determine is an indexed value matches the expression - */ + // VSTODO seems like we could optimize for CompositeType here since we know we have a key match public boolean isSatisfiedBy(ByteBuffer columnValue) { - // If the expression represents an ANN ordering then we return true because the actual result - // is approximate and will rarely / never match the expression value - if (indexTermType.isVector()) + if (columnValue == null) + return false; + + // ORDER_BY is not indepently verifiable, so we always return true + if (operation == Op.ORDER_BY) return true; - if (!indexTermType.isValid(columnValue)) + if (!TypeUtil.isValid(columnValue, validator)) { - logger.error("Value is not valid for indexed column {} with {}", indexTermType.columnName(), indexTermType.indexType()); + logger.error(context.logMessage("Value is not valid for indexed column {} with {}"), context.getColumnName(), validator); return false; } - Value value = new Value(columnValue, indexTermType); + Value value = new Value(columnValue, validator); + + if (operation == Op.BOUNDED_ANN) + { + double haversineDistance = SloppyMath.haversinMeters(lower.value.vector[0], lower.value.vector[1], value.vector[0], value.vector[1]); + return upperInclusive ? haversineDistance <= searchRadiusMeters : haversineDistance < searchRadiusMeters; + } if (lower != null) { // suffix check - if (indexTermType.isLiteral()) - return validateStringValue(value.raw, lower.value.raw); + if (TypeUtil.isLiteral(validator)) + { + if (!validateStringValue(value.raw, lower.value.raw)) + return false; + } else { // range or (not-)equals - (mainly) for numeric values - int cmp = indexTermType.comparePostFilter(lower.value, value); + int cmp = TypeUtil.comparePostFilter(lower.value, value, validator); - // in case of EQ lower == upper - if (operator == IndexOperator.EQ || operator == IndexOperator.CONTAINS_KEY || operator == IndexOperator.CONTAINS_VALUE) + // in case of (NOT_)EQ lower == upper + if (operation == Op.EQ || operation == Op.CONTAINS_KEY || operation == Op.CONTAINS_VALUE) return cmp == 0; + if (operation == Op.NOT_EQ || operation == Op.NOT_CONTAINS_KEY || operation == Op.NOT_CONTAINS_VALUE) + return cmp != 0; + if (cmp > 0 || (cmp == 0 && !lowerInclusive)) return false; } @@ -260,60 +321,183 @@ public boolean isSatisfiedBy(ByteBuffer columnValue) if (upper != null && lower != upper) { // string (prefix or suffix) check - if (indexTermType.isLiteral()) - return validateStringValue(value.raw, upper.value.raw); + if (TypeUtil.isLiteral(validator)) + { + if (!validateStringValue(value.raw, upper.value.raw)) + return false; + } else { // range - mainly for numeric values - int cmp = indexTermType.comparePostFilter(upper.value, value); - return (cmp > 0 || (cmp == 0 && upperInclusive)); + int cmp = TypeUtil.comparePostFilter(upper.value, value, validator); + if (cmp < 0 || (cmp == 0 && !upperInclusive)) + return false; } } + // as a last step let's check exclusions for the given field, + // this covers EQ/RANGE with exclusions. + for (ByteBuffer term : exclusions) + { + if (TypeUtil.isLiteral(validator) && validateStringValue(value.raw, term) || + TypeUtil.comparePostFilter(new Value(term, validator), value, validator) == 0) + return false; + } + return true; } + /** + * Returns the lower bound of the expression as a ByteComparable with an encoding based on the version and the + * validator. + * @param version the version of the index + * @return + */ + public ByteComparable getEncodedLowerBoundByteComparable(Version version) + { + // Note: this value was encoded using the TypeUtil.encode method, but it wasn't + var bound = getPartiallyEncodedLowerBound(version); + if (bound == null) + return null; + // If the lower bound is inclusive, we use the LT_NEXT_COMPONENT terminator to make sure the bound is not a + // prefix of some other key. This ensures reverse iteration works correctly too. + var terminator = lower.inclusive ? ByteSource.LT_NEXT_COMPONENT : ByteSource.GT_NEXT_COMPONENT; + return getBoundByteComparable(bound, version, terminator); + } + + /** + * Returns the upper bound of the expression as a ByteComparable with an encoding based on the version and the + * validator. + * @param version the version of the index + * @return + */ + public ByteComparable getEncodedUpperBoundByteComparable(Version version) + { + var bound = getPartiallyEncodedUpperBound(version); + if (bound == null) + return null; + // If the upper bound is inclusive, we use the LT_NEXT_COMPONENT terminator to make sure the bound is not a + // prefix of some other key. This ensures reverse iteration works correctly too. + var terminator = upper.inclusive ? ByteSource.GT_NEXT_COMPONENT : ByteSource.LT_NEXT_COMPONENT; + return getBoundByteComparable(bound, version, terminator); + } + + // This call encodes the byte buffer into a ByteComparable object based on the version of the index, the validator, + // and whether the expression is in memory or on disk. + private ByteComparable getBoundByteComparable(ByteBuffer unencodedBound, Version version, int terminator) + { + if (TypeUtil.isComposite(validator) && version.onOrAfter(Version.DB)) + // Note that for ranges that have one unrestricted bound, we technically do not need the terminator + // because we use the 0 or the 1 at the end of the first component as the bound. However, it works + // with the terminator, so we use it for simplicity. + return TypeUtil.asComparableBytes(unencodedBound, terminator, (CompositeType) validator); + else + return version.onDiskFormat().encodeForTrie(unencodedBound, validator); + } + + /** + * This is partially encoded because it uses the {@link TypeUtil#encode(ByteBuffer, AbstractType)} method on the + * {@link ByteBuffer}, but it does not apply the validator's encoding. We do this because we apply + * {@link TypeUtil#encode(ByteBuffer, AbstractType)} before we find the min/max on an index and this method is + * exposed publicly for determining if a bound is within an index's min/max. + * @param version + * @return + */ + public ByteBuffer getPartiallyEncodedLowerBound(Version version) + { + return getBound(lower, true, version); + } + + /** + * This is partially encoded because it uses the {@link TypeUtil#encode(ByteBuffer, AbstractType)} method on the + * {@link ByteBuffer}, but it does not apply the validator's encoding. We do this because we apply + * {@link TypeUtil#encode(ByteBuffer, AbstractType)} before we find the min/max on an index and this method is + * exposed publicly for determining if a bound is within an index's min/max. + * @param version + * @return + */ + public ByteBuffer getPartiallyEncodedUpperBound(Version version) + { + return getBound(upper, false, version); + } + + private ByteBuffer getBound(Bound bound, boolean isLowerBound, Version version) + { + if (bound == null) + return null; + // Composite types are currently only used in maps. + // Before DB, we need to extract the first component of the composite type to use as the trie search prefix. + // After DB, we can use the encoded value directly because the trie is encoded in order so the range + // correctly gets all relevant values. + if (!version.onOrAfter(Version.DB) && validator instanceof CompositeType) + return CompositeType.extractFirstComponentAsTrieSearchPrefix(bound.value.encoded, isLowerBound); + return bound.value.encoded; + } + + public boolean isSatisfiedBy(Iterator values) + { + if (values == null) + values = Collections.emptyIterator(); + + boolean success = operation.isNonEquality(); + while (values.hasNext()) + { + ByteBuffer v = values.next(); + if (isSatisfiedBy(v) ^ success) + return !success; + } + return success; + } + private boolean validateStringValue(ByteBuffer columnValue, ByteBuffer requestedValue) { - if (hasAnalyzer()) + AbstractAnalyzer analyzer = analyzerFactory.create(); + analyzer.reset(columnValue); + try { - AbstractAnalyzer analyzer = getAnalyzer(); - analyzer.reset(columnValue.duplicate()); - try + while (analyzer.hasNext()) { - while (analyzer.hasNext()) + final ByteBuffer term = analyzer.next(); + + boolean isMatch = false; + switch (operation) { - if (termMatches(analyzer.next(), requestedValue)) - return true; + case EQ: + case MATCH: + // Operation.isSatisfiedBy handles conclusion on !=, + // here we just need to make sure that term matched it + case CONTAINS_KEY: + case CONTAINS_VALUE: + isMatch = validator.compare(term, requestedValue) == 0; + break; + case NOT_EQ: + case NOT_CONTAINS_KEY: + case NOT_CONTAINS_VALUE: + isMatch = validator.compare(term, requestedValue) != 0; + break; + case RANGE: + isMatch = isLowerSatisfiedBy(term) && isUpperSatisfiedBy(term); + break; + + case PREFIX: + isMatch = ByteBufferUtil.startsWith(term, requestedValue); + break; } - return false; - } - finally - { - analyzer.end(); + + if (isMatch) + return true; } + return false; } - else + finally { - return termMatches(columnValue, requestedValue); + analyzer.end(); } } - private boolean termMatches(ByteBuffer term, ByteBuffer requestedValue) + public Op getOp() { - boolean isMatch = false; - switch (operator) - { - case EQ: - case CONTAINS_KEY: - case CONTAINS_VALUE: - isMatch = indexTermType.compare(term, requestedValue) == 0; - break; - case RANGE: - isMatch = isLowerSatisfiedBy(term) && isUpperSatisfiedBy(term); - break; - } - return isMatch; + return operation; } private boolean hasLower() @@ -331,7 +515,7 @@ private boolean isLowerSatisfiedBy(ByteBuffer value) if (!hasLower()) return true; - int cmp = indexTermType.indexType().compare(value, lower.value.raw); + int cmp = validator.compare(value, lower.value.raw); return cmp > 0 || cmp == 0 && lower.inclusive; } @@ -340,31 +524,47 @@ private boolean isUpperSatisfiedBy(ByteBuffer value) if (!hasUpper()) return true; - int cmp = indexTermType.indexType().compare(value, upper.value.raw); + int cmp = validator.compare(value, upper.value.raw); return cmp < 0 || cmp == 0 && upper.inclusive; } + public float getEuclideanSearchThreshold() + { + return boundedAnnEuclideanDistanceThreshold; + } + @Override public String toString() { - return String.format("Expression{name: %s, op: %s, lower: (%s, %s), upper: (%s, %s)}", - indexTermType.columnName(), - operator, - lower == null ? "null" : indexTermType.asString(lower.value.raw), + return toString(false); + } + + public String toString(boolean redact) + { + return String.format("Expression{name: %s, op: %s, lower: (%s, %s), upper: (%s, %s), exclusions: %s}", + context.getColumnName(), + operation, + lower == null ? "null" : validator.getString(lower.value.raw, redact ? Redaction.REDACT : Redaction.NONE), lower != null && lower.inclusive, - upper == null ? "null" : indexTermType.asString(upper.value.raw), - upper != null && upper.inclusive); + upper == null ? "null" : validator.getString(upper.value.raw, redact ? Redaction.REDACT : Redaction.NONE), + upper != null && upper.inclusive, + Iterators.toString(Iterators.transform(exclusions.iterator(), x -> validator.getString(x, redact ? Redaction.REDACT : Redaction.NONE)))); + } + + public String getIndexName() + { + return context.getIndexName(); } - @Override public int hashCode() { - return new HashCodeBuilder().append(indexTermType) - .append(operator) - .append(lower).append(upper).build(); + return new HashCodeBuilder().append(context.getColumnName()) + .append(operation) + .append(validator) + .append(lower).append(upper) + .append(exclusions).build(); } - @Override public boolean equals(Object other) { if (!(other instanceof Expression)) @@ -375,77 +575,38 @@ public boolean equals(Object other) Expression o = (Expression) other; - return Objects.equals(indexTermType, o.indexTermType) - && operator == o.operator - && Objects.equals(lower, o.lower) - && Objects.equals(upper, o.upper); - } - - public static class IndexedExpression extends Expression - { - private final StorageAttachedIndex index; - - public IndexedExpression(StorageAttachedIndex index) - { - super(index.termType()); - this.index = index; - } - - @Override - public boolean isNotIndexed() - { - return false; - } - - @Override - public StorageAttachedIndex getIndex() - { - return index; - } - - @Override - boolean hasAnalyzer() - { - return index.hasAnalyzer(); - } - - @Override - AbstractAnalyzer getAnalyzer() - { - return index.analyzer(); - } + return Objects.equals(context.getColumnName(), o.context.getColumnName()) + && validator.equals(o.validator) + && operation == o.operation + && Objects.equals(lower, o.lower) + && Objects.equals(upper, o.upper) + && exclusions.equals(o.exclusions); } - public static class UnindexedExpression extends Expression + /** + * Returns an expression that matches keys not matched by this expression. + */ + public Expression negated() { - private UnindexedExpression(IndexTermType indexTermType) - { - super(indexTermType); - } - - @Override - public boolean isNotIndexed() - { - return true; - } + Expression result = new Expression(context); + result.lower = lower; + result.upper = upper; - @Override - public StorageAttachedIndex getIndex() - { - throw new UnsupportedOperationException(); - } - - @Override - boolean hasAnalyzer() + switch (operation) { - return false; - } - - @Override - AbstractAnalyzer getAnalyzer() - { - throw new UnsupportedOperationException(); + case NOT_EQ: + result.operation = Op.EQ; + break; + case NOT_CONTAINS_KEY: + result.operation = Op.CONTAINS_KEY; + break; + case NOT_CONTAINS_VALUE: + result.operation = Op.CONTAINS_VALUE; + break; + default: + throw new UnsupportedOperationException(String.format("Negation of operator %s not supported", operation)); } + return result; } /** @@ -456,10 +617,17 @@ public static class Value public final ByteBuffer raw; public final ByteBuffer encoded; - public Value(ByteBuffer value, IndexTermType indexTermType) + /** + * The native representation of our vector indexes is float[], so we cache that here as well + * to avoid repeated expensive conversions. Always null for non-vector types. + */ + public final float[] vector; + + public Value(ByteBuffer value, AbstractType type) { this.raw = value; - this.encoded = indexTermType.asIndexBytes(value); + this.encoded = TypeUtil.asIndexBytes(value, type); + this.vector = type.isVector() ? TypeUtil.decomposeVector(type, raw) : null; } @Override @@ -487,9 +655,9 @@ public static class Bound public final Value value; public final boolean inclusive; - public Bound(ByteBuffer value, IndexTermType indexTermType, boolean inclusive) + public Bound(ByteBuffer value, AbstractType type, boolean inclusive) { - this.value = new Value(value, indexTermType); + this.value = new Value(value, type); this.inclusive = inclusive; } diff --git a/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java b/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java index 15ea273145e0..84c6d351f95d 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java +++ b/src/java/org/apache/cassandra/index/sai/plan/FilterTree.java @@ -19,40 +19,44 @@ import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.Set; + +import com.google.common.collect.ListMultimap; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.ColumnMetadata.Kind; import org.apache.cassandra.utils.FBUtilities; -import static org.apache.cassandra.index.sai.plan.Operation.BooleanOperator; +import static org.apache.cassandra.index.sai.plan.Operation.OperationType; /** * Tree-like structure to filter base table data using indexed expressions and non-user-defined filters. - *

    + * * This is needed because: * 1. SAI doesn't index tombstones, base data may have been shadowed. - * 2. Replica filter protecting may fetch data that doesn't match index expressions. + * 2. SAI indexes partition offset, not all rows in partition match index condition. + * 3. Replica filter protecting may fetch data that doesn't match index expressions. */ public class FilterTree { - protected final BooleanOperator baseOperator; - protected final Operation.Expressions expressions; + protected final OperationType op; + protected final ListMultimap expressions; protected final List children = new ArrayList<>(); - private final boolean isStrict; - private final QueryContext context; - FilterTree(BooleanOperator baseOperator, Operation.Expressions expressions, boolean isStrict, QueryContext context) + FilterTree(OperationType operation, + ListMultimap expressions) { - this.baseOperator = baseOperator; + this.op = operation; this.expressions = expressions; - this.isStrict = isStrict; - this.context = context; } void addChild(FilterTree child) @@ -60,133 +64,91 @@ void addChild(FilterTree child) children.add(child); } - /** - * @return true if this node of the tree or any of its children filter a non-static column - */ - public boolean restrictsNonStaticRow() + public boolean isSatisfiedBy(DecoratedKey key, Unfiltered currentCluster, Row staticRow) { - for (ColumnMetadata column : expressions.columns()) - if (!column.isStatic()) - return true; + boolean result = localSatisfiedBy(key, currentCluster, staticRow); - for (FilterTree child : children) - if (child.restrictsNonStaticRow()) - return true; - - return false; - } - - public boolean isSatisfiedBy(DecoratedKey key, Row row, Row staticRow) - { - boolean result = localSatisfiedBy(key, row, staticRow); + if (shouldReturnNow(result)) + return result; for (FilterTree child : children) - result = baseOperator.apply(result, child.isSatisfiedBy(key, row, staticRow)); + { + result = op.apply(result, child.isSatisfiedBy(key, currentCluster, staticRow)); + if (shouldReturnNow(result)) + return result; + } return result; } - private boolean localSatisfiedBy(DecoratedKey key, Row row, Row staticRow) + private boolean localSatisfiedBy(DecoratedKey key, Unfiltered currentCluster, Row staticRow) { - if (row == null) + if (currentCluster == null || !currentCluster.isRow()) return false; final long now = FBUtilities.nowInSeconds(); - // Downgrade AND to OR unless the coordinator indicates strict filtering is safe or all matches are repaired: - BooleanOperator localOperator = (isStrict || !context.hasUnrepairedMatches) ? baseOperator : BooleanOperator.OR; - boolean result = localOperator == BooleanOperator.AND; - - // If all matches on indexed columns are repaired, strict filtering is not allowed, and there are multiple - // unindexed column expressions, isolate the expressions on unindexed columns and union their results: - boolean isolateUnindexed = !context.hasUnrepairedMatches && !isStrict && expressions.hasMultipleUnindexedColumns(); - boolean unindexedResult = false; + boolean result = op == OperationType.AND; - Iterator columnIterator = expressions.columns().iterator(); - while (columnIterator.hasNext()) + Iterator columnIterator = expressions.keySet().iterator(); + while(columnIterator.hasNext()) { ColumnMetadata column = columnIterator.next(); - Row localRow = column.kind == Kind.STATIC ? staticRow : row; + Row row = column.kind == Kind.STATIC ? staticRow : (Row)currentCluster; - // If there is a column with multiple expressions that can mean an OR, or (in the case of map + // If there is a column with multiple expressions that can mean an OR or (in the case of map // collections) it can mean different map indexes. - List filters = expressions.expressionsFor(column); + List filters = expressions.get(column); // We do a reverse iteration over the filters because NOT_EQ operations will be at the end - // of the filter list, and we want to check them first. + // of the filter list and we want to check them first. ListIterator filterIterator = filters.listIterator(filters.size()); - - if (isolateUnindexed && expressions.isUnindexed(column)) + while(filterIterator.hasPrevious()) { - // If we isolate unindexed column expressions, we're implicitly calculating the union of those - // expressions. Once we've matched on any column, we can skip the rest, if any exist. - if (unindexedResult) - continue; + Expression filter = filterIterator.previous(); - while (filterIterator.hasPrevious()) + if (TypeUtil.isNonFrozenCollection(column.type)) { - Expression filter = filterIterator.previous(); - unindexedResult = applyFilter(key, now, BooleanOperator.OR, unindexedResult, localRow, filter); + Iterator valueIterator = filter.context.getValuesOf(row, now); + result = op.apply(result, filter.isSatisfiedBy(valueIterator)); } - } - else - { - while (filterIterator.hasPrevious()) + else { - Expression filter = filterIterator.previous(); - result = applyFilter(key, now, localOperator, result, localRow, filter); - - // If the operation is an AND then exit early if we get a single false - if ((localOperator == BooleanOperator.AND) && !result) - return false; - - // If the operation is an OR then exit early if we get a single true - if (localOperator == BooleanOperator.OR && result) - return true; + ByteBuffer value = filter.context.getValueOf(key, row, now); + result = op.apply(result, filter.isSatisfiedBy(value)); } + + if (shouldReturnNow(result)) + return result; } } - - if (isolateUnindexed) - // If we had to isolate the unindexed column expressions, combine with the indexed column result. Note that - // the indexed result must be true at this point if it was evaluated with the AND operator: - return localOperator == BooleanOperator.AND ? unindexedResult : result || unindexedResult; - return result; } - private boolean applyFilter(DecoratedKey key, long now, BooleanOperator operator, boolean result, Row row, Expression expression) - { - if (expression.getIndexTermType().isNonFrozenCollection()) - { - Iterator valueIterator = expression.getIndexTermType().valuesOf(row, now); - return operator.apply(result, collectionMatch(valueIterator, expression)); - } - else - { - ByteBuffer value = expression.getIndexTermType().valueOf(key, row, now); - return operator.apply(result, singletonMatch(value, expression)); - } + /** + * When evaluating an AND expression, if the current result is false, we can return immediately. + * When evaluating an OR expression, if the current result is true, we can return immediately. + * @param result the current result + * @return true if it is valid to return the current result + */ + private boolean shouldReturnNow(boolean result) { + return (op == OperationType.AND && !result) || (op == OperationType.OR && result); } - private boolean singletonMatch(ByteBuffer value, Expression filter) + /** + * @return the number of unique SSTable indexes that are referenced by the expressions in this filter tree. + */ + public int numSSTableIndexes() { - return value != null && filter.isSatisfiedBy(value); + Set referencedIndexes = new HashSet<>(); + sstableIndexes(referencedIndexes); + return referencedIndexes.size(); } - private boolean collectionMatch(Iterator valueIterator, Expression filter) + private void sstableIndexes(Set indexes) { - if (valueIterator == null) - return false; - - while (valueIterator.hasNext()) - { - ByteBuffer value = valueIterator.next(); - if (value == null) - continue; - - if (filter.isSatisfiedBy(value)) - return true; - } - return false; + for (Expression expression : expressions.values()) + indexes.addAll(expression.context.getView().getIndexes()); + for (FilterTree child : children) + child.sstableIndexes(indexes); } } diff --git a/src/java/org/apache/cassandra/index/sai/plan/Operation.java b/src/java/org/apache/cassandra/index/sai/plan/Operation.java index 580bf8b6e9bc..c85d570b6486 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/Operation.java +++ b/src/java/org/apache/cassandra/index/sai/plan/Operation.java @@ -20,12 +20,10 @@ import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; -import java.util.Set; -import java.util.function.BiFunction; +import java.util.Map; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; @@ -33,94 +31,48 @@ import com.google.common.collect.ListMultimap; import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.utils.TreeFormatter; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.serializers.ListSerializer; public class Operation { - public enum BooleanOperator + public enum OperationType { - AND((a, b) -> a & b), - OR((a, b) -> a | b); - - private final BiFunction func; - - BooleanOperator(BiFunction func) - { - this.func = func; - } + AND, OR; public boolean apply(boolean a, boolean b) { - return func.apply(a, b); - } - } - - public static class Expressions - { - final ListMultimap expressions; - final Set unindexedColumns; - - Expressions(ListMultimap expressions, Set unindexedColumns) - { - this.expressions = expressions; - this.unindexedColumns = unindexedColumns; - } - - Set columns() - { - return expressions.keySet(); - } - - Collection all() - { - return expressions.values(); - } - - List expressionsFor(ColumnMetadata column) - { - return expressions.get(column); - } - - boolean isEmpty() - { - return expressions.isEmpty(); - } - - int size() - { - return expressions.size(); - } + switch (this) + { + case OR: + return a | b; - boolean isUnindexed(ColumnMetadata column) - { - return unindexedColumns.contains(column); - } + case AND: + return a & b; - boolean hasMultipleUnindexedColumns() - { - return unindexedColumns.size() > 1; + default: + throw new AssertionError(); + } } } @VisibleForTesting - protected static Expressions buildIndexExpressions(QueryController queryController, List expressions) + protected static ListMultimap analyzeGroup(QueryController controller, + OperationType op, + List expressions) { ListMultimap analyzed = ArrayListMultimap.create(); - Set unindexedColumns = Collections.emptySet(); + Map columnIsMultiExpression = new HashMap<>(); - // sort all the expressions in the operation by name and priority of the logical operator + // sort all of the expressions in the operation by name and priority of the logical operator // this gives us an efficient way to handle inequality and combining into ranges without extra processing // and converting expressions from one type to another. expressions.sort((a, b) -> { @@ -128,88 +80,86 @@ protected static Expressions buildIndexExpressions(QueryController queryControll return cmp == 0 ? -Integer.compare(getPriority(a.operator()), getPriority(b.operator())) : cmp; }); - for (final RowFilter.Expression expression : expressions) + for (final RowFilter.Expression e : expressions) { - if (Expression.supportsOperator(expression.operator())) - { - StorageAttachedIndex index = queryController.indexFor(expression); - List perColumn = analyzed.get(expression.column()); + IndexContext indexContext = controller.getContext(e); + List perColumn = analyzed.get(e.column()); - if (index == null) + AbstractAnalyzer.AnalyzerFactory analyzerFactory = indexContext.getQueryAnalyzerFactory(); + AbstractAnalyzer analyzer = analyzerFactory.create(); + try + { + analyzer.reset(e.getIndexValue()); + + // EQ/LIKE_*/NOT_EQ can have multiple expressions e.g. text = "Hello World", + // becomes text = "Hello" AND text = "World" because "space" is always interpreted as a split point (by analyzer), + // CONTAINS/CONTAINS_KEY are always treated as multiple expressions since they currently only targetting + // collections, NOT_EQ is made an independent expression only in case of pre-existing multiple EQ expressions, or + // if there is no EQ operations and NOT_EQ is met or a single NOT_EQ expression present, + // in such case we know exactly that there would be no more EQ/RANGE expressions for given column + // since NOT_EQ has the lowest priority. + boolean isMultiExpression = columnIsMultiExpression.getOrDefault(e.column(), Boolean.FALSE); + switch (e.operator()) { - buildUnindexedExpression(queryController, expression, perColumn); - - if (!expression.column().isPrimaryKeyColumn()) - { - if (unindexedColumns.isEmpty()) - unindexedColumns = new HashSet<>(3); + // case BM25: leave it at the default of `false` + case EQ: + // EQ operator will always be a multiple expression because it is being used by map entries + isMultiExpression = indexContext.isNonFrozenCollection(); - unindexedColumns.add(expression.column()); - } - } - else - { - buildIndexedExpression(index, expression, perColumn); + // EQ wil behave like ANALYZER_MATCHES for analyzed columns if the analyzer supports EQ queries + isMultiExpression |= indexContext.isAnalyzed() && analyzerFactory.supportsEquals(); + break; + case CONTAINS: + case CONTAINS_KEY: + case NOT_CONTAINS: + case NOT_CONTAINS_KEY: + case LIKE_PREFIX: + case LIKE_MATCHES: + case ANALYZER_MATCHES: + isMultiExpression = true; + break; + case NEQ: + // NEQ operator will always be a multiple expression if it is the only operator + // (e.g. multiple NEQ expressions) + isMultiExpression = isMultiExpression || perColumn.isEmpty(); + break; } - } - } + columnIsMultiExpression.put(e.column(), isMultiExpression); - return new Expressions(analyzed, unindexedColumns); - } - - private static void buildUnindexedExpression(QueryController queryController, - RowFilter.Expression expression, - List perColumn) - { - IndexTermType indexTermType = IndexTermType.create(expression.column(), - queryController.metadata().partitionKeyColumns(), - determineIndexTargetType(expression)); - if (indexTermType.isMultiExpression(expression)) - { - perColumn.add(Expression.create(indexTermType).add(expression.operator(), expression.getIndexValue().duplicate())); - } - else - { - Expression range; - if (perColumn.size() == 0) - { - range = Expression.create(indexTermType); - perColumn.add(range); - } - else - { - range = Iterables.getLast(perColumn); - } - range.add(expression.operator(), expression.getIndexValue().duplicate()); - } - } - - private static void buildIndexedExpression(StorageAttachedIndex index, RowFilter.Expression expression, List perColumn) - { - if (index.hasAnalyzer()) - { - AbstractAnalyzer analyzer = index.analyzer(); - try - { - analyzer.reset(expression.getIndexValue().duplicate()); - - if (index.termType().isMultiExpression(expression)) + if (isMultiExpression) { - while (analyzer.hasNext()) + if (!analyzer.hasNext()) { - final ByteBuffer token = analyzer.next(); - perColumn.add(Expression.create(index).add(expression.operator(), token.duplicate())); + perColumn.add(new Expression(indexContext).add(e.operator(), ByteBuffer.allocate(0))); + } + else + { + // The hasNext implementation has a side effect, so we need to call next before calling hasNext + do + { + final ByteBuffer token = analyzer.next(); + perColumn.add(new Expression(indexContext).add(e.operator(), token.duplicate())); + } + while (analyzer.hasNext()); } } + else if (e instanceof RowFilter.GeoDistanceExpression) + { + RowFilter.GeoDistanceExpression distance = ((RowFilter.GeoDistanceExpression) e); + Expression expression = new Expression(indexContext) + .add(distance.getDistanceOperator(), distance.getDistance().duplicate()) + .add(Operator.BOUNDED_ANN, e.getIndexValue().duplicate()); + perColumn.add(expression); + } else // "range" or not-equals operator, combines both bounds together into the single expression, // if operation of the group is AND, otherwise we are forced to create separate expressions, // not-equals is combined with the range iff operator is AND. { Expression range; - if (perColumn.size() == 0) + if (perColumn.size() == 0 || op != OperationType.AND || e instanceof RowFilter.MapComparisonExpression) { - range = Expression.create(index); + range = new Expression(indexContext); perColumn.add(range); } else @@ -217,17 +167,40 @@ private static void buildIndexedExpression(StorageAttachedIndex index, RowFilter range = Iterables.getLast(perColumn); } - if (index.termType().isLiteral()) + if (!TypeUtil.isLiteral(indexContext.getValidator())) { - while (analyzer.hasNext()) - { - ByteBuffer term = analyzer.next(); - range.add(expression.operator(), term.duplicate()); + range.add(e.operator(), e.getIndexValue().duplicate()); + } + else if (e instanceof RowFilter.MapComparisonExpression) + { + var map = (RowFilter.MapComparisonExpression) e; + var operator = map.operator(); + switch (operator) { + case EQ: + case NEQ: + range.add(operator, map.getIndexValue().duplicate()); + break; + case GT: + case GTE: + range.add(operator, map.getLowerBound().duplicate()); + range.add(Operator.LTE, map.getUpperBound().duplicate()); + break; + case LT: + case LTE: + range.add(Operator.GTE, map.getLowerBound().duplicate()); + range.add(operator, map.getUpperBound().duplicate()); + break; + default: + throw new InvalidRequestException("Unexpected operator: " + operator); } } else { - range.add(expression.operator(), expression.getIndexValue().duplicate()); + while (analyzer.hasNext()) + { + ByteBuffer term = analyzer.next(); + range.add(e.operator(), term.duplicate()); + } } } } @@ -236,130 +209,52 @@ private static void buildIndexedExpression(StorageAttachedIndex index, RowFilter analyzer.end(); } } - else - { - if (index.termType().isMultiExpression(expression)) - { - perColumn.add(Expression.create(index).add(expression.operator(), expression.getIndexValue().duplicate())); - } - else - { - Expression range; - if (perColumn.size() == 0) - { - range = Expression.create(index); - perColumn.add(range); - } - else - { - range = Iterables.getLast(perColumn); - } - range.add(expression.operator(), expression.getIndexValue().duplicate()); - } - } - } - /** - * Determines the {@link IndexTarget.Type} for the expression. In this case we are only interested in map types and - * the operator being used in the expression. - */ - private static IndexTarget.Type determineIndexTargetType(RowFilter.Expression expression) - { - AbstractType type = expression.column().type; - IndexTarget.Type indexTargetType = IndexTarget.Type.SIMPLE; - if (type.isCollection() && type.isMultiCell()) - { - CollectionType collection = ((CollectionType) type); - if (collection.kind == CollectionType.Kind.MAP) - { - switch (expression.operator()) - { - case EQ: - indexTargetType = IndexTarget.Type.KEYS_AND_VALUES; - break; - case CONTAINS: - indexTargetType = IndexTarget.Type.VALUES; - break; - case CONTAINS_KEY: - indexTargetType = IndexTarget.Type.KEYS; - break; - default: - throw new InvalidRequestException("Invalid operator"); - } - } - } - return indexTargetType; + return analyzed; } - private static int getPriority(Operator op) + private static int getPriority(org.apache.cassandra.cql3.Operator op) { switch (op) { case EQ: + return 7; + case CONTAINS: case CONTAINS_KEY: + return 6; + + case LIKE_PREFIX: + case LIKE_MATCHES: return 5; case GTE: case GT: - return 3; + return 4; case LTE: case LT: + return 3; + + case NOT_CONTAINS: + case NOT_CONTAINS_KEY: return 2; + case NEQ: + return 1; + default: return 0; } } - /** - * Converts expressions into filter tree for query. - * - * @return a KeyRangeIterator over the index query results - */ - static KeyRangeIterator buildIterator(QueryController controller) - { - return Node.buildTree(controller.indexFilter()).analyzeTree(controller).rangeIterator(controller); - } - - /** - * Converts expressions into filter tree for query. - * - * @return a KeyRangeIterator over the index query results - */ - static CloseableIterator buildIteratorForOrder(QueryController controller, QueryViewBuilder.QueryExpressionView view) + public static abstract class Node { - if (controller.indexFilter().getExpressions().size() == 1) - // If we only have one expression, we just use the ANN index to order and limit. - return controller.getTopKRows(view); - - // Otherwise, we need to search first, then order. - KeyRangeIterator iterator = buildIterator(controller); - return controller.getTopKRows(iterator, view); - } - - /** - * Converts expressions into filter tree (which is currently just a single AND). - *

    - * Filter tree allows us to do a couple of important optimizations - * namely, group flattening for AND operations (query rewrite), expression bounds checks, - * "satisfies by" checks for resulting rows with an early exit. - * - * @return root of the filter tree. - */ - static FilterTree buildFilter(QueryController controller, boolean strict) - { - return Node.buildTree(controller.indexFilter()).buildFilter(controller, strict); - } - - static abstract class Node - { - Expressions expressions; + ListMultimap expressionMap; boolean canFilter() { - return (expressions != null && !expressions.isEmpty()) || !children().isEmpty(); + return (expressionMap != null && !expressionMap.isEmpty()) || !children().isEmpty() ; } List children() @@ -377,61 +272,94 @@ RowFilter.Expression expression() throw new UnsupportedOperationException(); } - abstract void analyze(List expressionList, QueryController controller); + /** + * Analyze the tree, potentially flattening it and storing the result in expressionMap. + */ + abstract void analyze(QueryController controller); - abstract FilterTree filterTree(boolean strict, QueryContext context); + abstract FilterTree filterTree(); - abstract KeyRangeIterator rangeIterator(QueryController controller); + abstract Plan.KeysIteration plan(QueryController controller); - static Node buildTree(RowFilter filterOperation) + static Node buildTree(QueryController controller, List expressions, List children, boolean isDisjunction) { - OperatorNode node = new AndNode(); - for (RowFilter.Expression expression : filterOperation.getExpressions()) - node.add(buildExpression(expression)); + OperatorNode node = isDisjunction ? new OrNode() : new AndNode(); + for (RowFilter.Expression expression : expressions) + node.add(buildExpression(controller, expression, isDisjunction)); + for (RowFilter.FilterElement child : children) + node.add(buildTree(controller, child)); return node; } - static Node buildExpression(RowFilter.Expression expression) + static Node buildTree(QueryController controller, RowFilter.FilterElement filterOperation) { - return new ExpressionNode(expression); + return buildTree(controller, filterOperation.expressions(), filterOperation.children(), filterOperation.isDisjunction()); + } + + static Node buildExpression(QueryController controller, RowFilter.Expression expression, boolean isDisjunction) + { + if (expression.operator() == Operator.IN) + { + OperatorNode node = new OrNode(); + int size = ListSerializer.readCollectionSize(expression.getIndexValue(), ByteBufferAccessor.instance); + int offset = ListSerializer.sizeOfCollectionSize(); + for (int index = 0; index < size; index++) + { + node.add(new ExpressionNode(new RowFilter.SimpleExpression(expression.column(), + Operator.EQ, + ListSerializer.readValue(expression.getIndexValue(), + ByteBufferAccessor.instance, + offset), + expression.analyzer(), + expression.annOptions()))); + offset += TypeSizes.INT_SIZE + ByteBufferAccessor.instance.getInt(expression.getIndexValue(), offset); + } + if (node.children().size() == 1) + return node.children().get(0); + if (node.children().isEmpty()) + return new EmptyNode(); + return node; + } + else if (isDisjunction && (expression.operator() == Operator.ANALYZER_MATCHES || + expression.operator() == Operator.EQ && controller.getContext(expression).isAnalyzed())) + { + // In case of having a tokenizing query_analyzer (such as NGram) with OR, we need to split the + // expression into multiple expressions and intersect them. + // The additional node in case of no tokenization will be taken care of in Plan.Factory#intersection() + OperatorNode node = new AndNode(); + node.add(new ExpressionNode(expression)); + return node; + } + else + return new ExpressionNode(expression); } Node analyzeTree(QueryController controller) { - List expressionList = new ArrayList<>(); - doTreeAnalysis(this, expressionList, controller); - if (!expressionList.isEmpty()) - this.analyze(expressionList, controller); + analyze(controller); return this; } - void doTreeAnalysis(Node node, List expressions, QueryController controller) + @VisibleForTesting + FilterTree buildFilter(QueryController controller) { - if (node.children().isEmpty()) - expressions.add(node.expression()); - else - { - List expressionList = new ArrayList<>(); - for (Node child : node.children()) - doTreeAnalysis(child, expressionList, controller); - node.analyze(expressionList, controller); - } + analyze(controller); + return filterTree(); } - FilterTree buildFilter(QueryController controller, boolean isStrict) + /** + * Formats the whole operation tree as a pretty tree. + */ + public final String toStringRecursive() { - analyzeTree(controller); - FilterTree tree = filterTree(isStrict, controller.queryContext); - for (Node child : children()) - if (child.canFilter()) - tree.addChild(child.buildFilter(controller, isStrict)); - return tree; + TreeFormatter formatter = new TreeFormatter<>(Node::toString, Node::children); + return formatter.format(this); } } static abstract class OperatorNode extends Node { - final List children = new ArrayList<>(); + List children = new ArrayList<>(); @Override public List children() @@ -444,52 +372,107 @@ public void add(Node child) { children.add(child); } - } - static class AndNode extends OperatorNode - { + abstract protected OperationType operationType(); + abstract protected Plan.Builder planBuilder(QueryController controller); + + // expression list is the children that are leaf nodes... we could figure that out here... @Override - public void analyze(List expressionList, QueryController controller) + public void analyze(QueryController controller) { - expressions = buildIndexExpressions(controller, expressionList); + // This operation flattens the tree where possible and stores the result in expressionMap + List expressionList = new ArrayList<>(); + for (Node child : children) + { + if (child instanceof ExpressionNode) + expressionList.add(child.expression()); + else + child.analyze(controller); + } + expressionMap = analyzeGroup(controller, operationType(), expressionList); } @Override - FilterTree filterTree(boolean isStrict, QueryContext context) + FilterTree filterTree() { - return new FilterTree(BooleanOperator.AND, expressions, isStrict, context); + assert expressionMap != null; + var tree = new FilterTree(operationType(), expressionMap); + for (Node child : children()) + if (child.canFilter()) + tree.addChild(child.filterTree()); + return tree; } @Override - KeyRangeIterator rangeIterator(QueryController controller) + Plan.KeysIteration plan(QueryController controller) { - KeyRangeIterator.Builder builder = controller.getIndexQueryResults(expressions.all()); + var builder = planBuilder(controller); + if (!expressionMap.isEmpty()) + controller.buildPlanForExpressions(builder, expressionMap.values()); for (Node child : children) - { - boolean canFilter = child.canFilter(); - if (canFilter) - builder.add(child.rangeIterator(controller)); - } + if (child.canFilter()) + builder.add(child.plan(controller)); return builder.build(); } } - static class ExpressionNode extends Node + public static class AndNode extends OperatorNode + { + @Override + protected OperationType operationType() + { + return OperationType.AND; + } + + @Override + protected Plan.Builder planBuilder(QueryController controller) + { + return controller.planFactory.intersectionBuilder(); + } + + @Override + public String toString() + { + return "AndNode"; + } + } + + public static class OrNode extends OperatorNode { - final RowFilter.Expression expression; + @Override + protected OperationType operationType() + { + return OperationType.OR; + } @Override - public void analyze(List expressionList, QueryController controller) + protected Plan.Builder planBuilder(QueryController controller) { - expressions = buildIndexExpressions(controller, expressionList); - assert expressions.size() == 1 : "Expression nodes should only have a single expression!"; + return controller.planFactory.unionBuilder(); } @Override - FilterTree filterTree(boolean isStrict, QueryContext context) + public String toString() { - // There should only be one expression, so AND/OR would both work here. - return new FilterTree(BooleanOperator.AND, expressions, isStrict, context); + return "OrNode"; + } + } + + public static class ExpressionNode extends Node + { + RowFilter.Expression expression; + + @Override + public void analyze(QueryController controller) + { + expressionMap = analyzeGroup(controller, OperationType.AND, Collections.singletonList(expression)); + } + + @Override + FilterTree filterTree() + { + assert expressionMap != null; + return new FilterTree(OperationType.AND, expressionMap); } public ExpressionNode(RowFilter.Expression expression) @@ -504,11 +487,54 @@ public RowFilter.Expression expression() } @Override - KeyRangeIterator rangeIterator(QueryController controller) + Plan.KeysIteration plan(QueryController controller) { assert canFilter() : "Cannot process query with no expressions"; + Plan.Builder builder = controller.planFactory.intersectionBuilder(); + controller.buildPlanForExpressions(builder, expressionMap.values()); + return builder.build(); + } - return controller.getIndexQueryResults(expressions.all()).build(); + @Override + public String toString() + { + return "ExpressionNode{expression=" + expression + '}'; + } + } + + public static class EmptyNode extends Node + { + // A FilterTree that filters out all rows + private static final FilterTree EMPTY_TREE = new FilterTree(OperationType.OR, ArrayListMultimap.create()); + + @Override + boolean canFilter() + { + return true; + } + + @Override + void analyze(QueryController controller) + { + } + + @Override + FilterTree filterTree() + { + return EMPTY_TREE; + } + + @Override + Plan.KeysIteration plan(QueryController controller) + { + return controller.planFactory.nothing; + } + + @Override + public String toString() + { + return "EmptyNode"; } } + } diff --git a/src/java/org/apache/cassandra/index/sai/plan/Orderer.java b/src/java/org/apache/cassandra/index/sai/plan/Orderer.java new file mode 100644 index 000000000000..31b3ea576e23 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/Orderer.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.plan; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.filter.ANNOptions; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.db.marshal.RedactionUtil; +import org.apache.cassandra.index.SecondaryIndexManager; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.disk.vector.VectorCompression; +import org.apache.cassandra.index.sai.utils.DocBm25Stats; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; + +import static org.apache.cassandra.index.sai.disk.v3.V3OnDiskFormat.JVECTOR_USE_PRUNING_DEFAULT; + +/** + * An SAI Orderer represents an index based order by clause. + */ +public class Orderer +{ + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + // The list of operators that are valid for order by clauses. + static final EnumSet ORDER_BY_OPERATORS = EnumSet.of(Operator.ANN, + Operator.BM25, + Operator.ORDER_BY_ASC, + Operator.ORDER_BY_DESC); + + public final IndexContext context; + public final Operator operator; + public final ByteBuffer term; + + // Vector search parameters + private float[] rawVector; + private VectorFloat vector; + private final ANNOptions annOptions; + + // BM25 search parameter + private List queryTerms; + + // BM25 aggregated statistics + public final DocBm25Stats bm25stats = new DocBm25Stats(); + + /** + * Create an orderer for the given index context, operator, and term. + * @param context the index context, used to build the view of memtables and sstables for query execution. + * @param operator the operator for the order by clause. + * @param term the term to order by (not always relevant) + * @param annOptions optional options for ANN queries + */ + public Orderer(IndexContext context, Operator operator, ByteBuffer term, ANNOptions annOptions) + { + this.context = context; + assert ORDER_BY_OPERATORS.contains(operator) : "Invalid operator for order by clause " + operator; + this.operator = operator; + this.annOptions = annOptions; + this.term = term; + } + + public String getIndexName() + { + return context.getIndexName(); + } + + public boolean isAscending() + { + // Note: ANN is always descending. + return operator == Operator.ORDER_BY_ASC; + } + + public Comparator getComparator() + { + // ANN/BM25's PrimaryKeyWithSortKey is always descending, so we use the natural order for the priority queue + return (isAscending() || isANN() || isBM25()) ? Comparator.naturalOrder() : Comparator.reverseOrder(); + } + + public boolean isLiteral() + { + return context.isLiteral(); + } + + public boolean isANN() + { + return operator == Operator.ANN; + } + + /** + * Provide rerankK for ANN queries. Use the user provided rerankK if available, otherwise use the model's default + * based on the limit and compression type. + * + * @param limit the query limit or the proportional segment limit to use when calculating a reasonable rerankK + * default value + * @param vc the compression type of the vectors in the index + * @return the rerankK value to use in ANN search + */ + public int rerankKFor(int limit, VectorCompression vc) + { + assert isANN() : "rerankK is only valid for ANN queries"; + return annOptions.rerankK != null + ? annOptions.rerankK + : context.getIndexWriterConfig().getSourceModel().rerankKFor(limit, vc); + } + + /** + * Whether to use pruning to speed up the ANN search. If the AnnOption does not specify a value for usePruning, + * we use the default value, which is currently configured as an environment variable. + * + * @return the usePruning value to use in ANN search + */ + public boolean usePruning() + { + assert isANN() : "usePruning is only valid for ANN queries"; + return annOptions.usePruning != null ? annOptions.usePruning : JVECTOR_USE_PRUNING_DEFAULT; + } + + public boolean isBM25() + { + return operator == Operator.BM25; + } + + @Nullable + public static Orderer from(SecondaryIndexManager indexManager, RowFilter filter) + { + var expressions = filter.root.expressions().stream().filter(Orderer::isFilterExpressionOrderer).collect(Collectors.toList()); + if (expressions.isEmpty()) + return null; + var orderExpression = expressions.get(0); + var index = indexManager.getBestIndexFor(orderExpression, filter.indexHints, StorageAttachedIndex.class) + .orElseThrow(() -> new IllegalStateException("No index found for order by clause")); + + return new Orderer(index.getIndexContext(), orderExpression.operator(), orderExpression.getIndexValue(), filter.annOptions()); + } + + public static boolean isFilterExpressionOrderer(RowFilter.Expression expression) + { + return ORDER_BY_OPERATORS.contains(expression.operator()); + } + + @Override + public String toString() + { + return toString(Redaction.NONE); + } + + public String toString(Redaction redaction) + { + String direction = isAscending() ? "ASC" : "DESC"; + String annOptionsString = annOptions != null ? annOptions.toCQLString() : ""; + if (isANN()) + return context.getColumnName() + " ANN OF " + getVectorTermAsString(redaction) + ' ' + direction + annOptionsString; + if (isBM25()) + return context.getColumnName() + " BM25 OF " + context.getValidator().toCQLString(term, redaction) + ' ' + direction; + return context.getColumnName() + ' ' + direction; + } + + public String getVectorTermAsString(Redaction redaction) + { + return redaction == Redaction.REDACT + ? RedactionUtil.redact(0) + : Arrays.toString(getRawVectorTerm()); + } + + public VectorFloat getVectorTerm() + { + if (vector == null) + vector = vts.createFloatVector(getRawVectorTerm()); + return vector; + } + + private float[] getRawVectorTerm() + { + if (rawVector == null) + rawVector = TypeUtil.decomposeVector(context.getValidator(), term); + return rawVector; + } + + public float score(ByteBuffer otherVector) + { + if (!context.isVector()) + throw new IllegalStateException("Cannot score non-vector index"); + var floatVector = vts.createFloatVector(TypeUtil.decomposeVector(context.getValidator(), otherVector)); + return context.getIndexWriterConfig().getSimilarityFunction().compare(getVectorTerm(), floatVector); + } + + public List getQueryTerms() + { + if (queryTerms != null) + return queryTerms; + + var queryAnalyzer = context.getQueryAnalyzerFactory().create(); + // Split query into terms + var uniqueTerms = new HashSet(); + queryAnalyzer.reset(term); + try + { + queryAnalyzer.forEachRemaining(uniqueTerms::add); + } + finally + { + queryAnalyzer.end(); + } + queryTerms = new ArrayList<>(uniqueTerms); + return queryTerms; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/Plan.java b/src/java/org/apache/cassandra/index/sai/plan/Plan.java new file mode 100644 index 000000000000..d14da4353b75 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/Plan.java @@ -0,0 +1,2554 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.plan; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.mutable.MutableDouble; +import org.apache.commons.lang3.mutable.MutableInt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cache.ChunkCache; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.iterators.KeyRangeIntersectionIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TreeFormatter; +import org.apache.cassandra.io.util.FileUtils; + +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.round; +import static org.apache.cassandra.index.sai.plan.Plan.CostCoefficients.*; + +/** + * The common base class for query execution plan nodes. + * The top-level node is considered to be the execution plan of the query. + * + *

    Structure

    + * A query plan is an immutable tree constisting of nodes representing physical data operations, + * e.g. index scans, intersections, unions, filtering, limiting, sorting, etc. + * Nodes of type {@link KeysIteration} operate on streams of keys, and nodes of type {@link RowsIteration} operate + * on streams of rows. You should build a plan bottom-up by using static methods in {@link Plan.Factory}. + * Nodes don't have pointers to parent nodes on purpose – this way multiple plans can share subtrees. + * + *

    Cost estimation

    + * A plan can estimate its execution cost and result set size which is useful to select the best plan among + * the semantically equivalent candidate plans. Operations represented by nodes may be pipelined, so their actual + * runtime cost may depend on how many rows are read from the top level node. Upon construction, each plan node + * gets an {@link Access} object which describes the way how the node results are going to be used by the parent nodes: + * how many rows will be requested or what skip operations are going to be performed on the iterator. + * The access objects get propagated down the tree to the leaves. This way we get an accurate cost of execution + * at the leave nodes, taking into account any top-level limit or intersections. + *

    + * Some nodes cannot be pipelined, e.g. nodes that represent sorting. To make cost estimation for such nodes possible, + * each node maintains an initial cost (initCost) of the operation - that is the cost of preparation before the first + * result row or key can be returned. Sorting nodes can have that cost very high. + * + *

    Optimization

    + * This class also offers a few methods for modifying the plans (e.g. removing nodes) and a method allowing + * to automatically improve the plan – see {@link #optimize()}. Whenever we talk about "modification" or "updates" + * we always mean constructing a new plan. All updates are non-destructive. Each node has a unique numeric + * identifier in the tree. Because a modification requires creating some new nodes, identifiers allow to find + * corresponding nodes in the modified plan, even if they addresses changed (they are different java objects). + * + *

    Execution

    + * The plan tree may store additional context information to be executable, i.e. to produce the iterator over the result + * keys or rows - see {@link KeysIteration#execute}. However, the purpose of the plan nodes is not to perform + * the actual computation of the result set. Instead, it should delegate the control to other modules responsible + * for data retrieval. The plan only sets up the execution, but must not contain the execution logic. + * For the sake of good testability, plan trees must be creatable, estimatable and optimizable also without + * creating any of the objects used by the execution engine. + * + *

    Example

    + * The CQL query + *
    + * SELECT * FROM table WHERE  a < 0.01 AND b < 0.2 LIMIT 10
    + * 
    + * + * can be represented by the following query execution plan: + *
    + * Limit 10 (rows: 10.0, cost/row: 265.2, cost: 80.0..2732.2)
    + *  └─ Filter a < 0.2 AND b < 0.01 (sel: 1.000000000) (rows: 10.0, cost/row: 265.2, cost: 80.0..2732.2)
    + *      └─ Fetch (rows: 10.0, cost/row: 265.2, cost: 80.0..2732.2)
    + *          └─ Intersection (keys: 10.0, cost/key: 58.2, cost: 80.0..662.1)
    + *              ├─ NumericIndexScan of vector_b_idx using Expression{ ... } (sel: 0.010010000, step: 1.0) (keys: 50.2, cost/key: 1.0, cost: 40.0..90.2)
    + *              └─ NumericIndexScan of vector_a_idx using Expression{ ... } (sel: 0.199230000, step: 19.9) (keys: 50.2, cost/key: 10.6, cost: 40.0..571.9)
    + * 
    + */ +@NotThreadSafe +abstract public class Plan +{ + private static final Logger logger = LoggerFactory.getLogger(Plan.class); + + @VisibleForTesting + static DoubleSupplier hitRateSupplier = () -> { + // cache hit rate with reasonable defaults if we have no data + double hitRate = ChunkCache.instance == null ? 1.0 : ChunkCache.instance.metrics.hitRate(); + return Double.isFinite(hitRate) ? hitRate : 1.0; + }; + + /** + * Identifier of the plan tree node. + * Used to identify the nodes of the plan. + * Preserved during plan transformations. + *

    + * Identifiers are more useful than object's identity (address) because plans can be transformed functionally + * and as the result of that process we may get new node objects. + * Identifiers allow us to match nodes in the transformed plan to the original. + */ + final int id; + + /** + * Reference to the factory gives access to common data shared among all nodes, + * e.g. total number of keys in the table and the cost parameters. + * It also allows to modify plan trees, e.g. create new nodes or recreate this node with different parameters. + */ + final Factory factory; + + /** + * Describes how this node is going to be used. + * Very likely affects the cost. + */ + final Access access; + + /** + * Lazily caches the estimated fraction of the table data that the result of this plan is expected to match. + */ + private double selectivity = -1; + + + private Plan(Factory factory, int id, Access access) + { + this.id = id; + this.factory = factory; + this.access = access; + } + + /** + * Returns the order of the keys / rows returned by this plan. + */ + protected abstract @Nullable Orderer ordering(); + + /** selectivity comparisons to 0 will probably cause bugs, use this instead */ + protected static boolean isEffectivelyZero(double a) { + assert a >= 0; + return a < 1e-9; + } + + /** dividing by extremely tiny numbers can cause overflow so clamp the minimum to 1e-9 */ + protected static double boundedSelectivity(double selectivity) { + assert 0 <= selectivity && selectivity <= 1.0; + return Math.max(1e-9, selectivity); + } + + /** + * Returns a new list containing subplans of this node. + * The list can be later freely modified by the caller and does not affect the original plan. + *

    + * Performance warning: This allocates a fresh list on the heap. + * If you only want to iterate the subplan nodes, it is recommended to use {@link #forEachSubplan(Function)} + * or {@link #withUpdatedSubplans(Function)} which offer better performance and less GC pressure. + */ + final List subplans() + { + List result = new ArrayList<>(); + forEachSubplan(subplan -> { + result.add(subplan); + return ControlFlow.Continue; + }); + return result; + } + + /** + * Returns a new list of nodes of given type. + * The tree is traversed in depth-first order. + * This node is included in the search. + * The list can be later freely modified by the caller and does not affect the original plan. + *

    + * Performance warning: This allocates a fresh list on the heap. + * If you only want to iterate the subplan nodes, it is recommended to use {@link #forEachSubplan(Function)} + * which should offer better performance and less GC pressure. + */ + @SuppressWarnings("unchecked") + final List nodesOfType(Class nodeType) + { + List result = new ArrayList<>(); + forEach(node -> { + if (nodeType.isAssignableFrom(node.getClass())) + result.add((T) node); + return ControlFlow.Continue; + }); + return result; + } + + /** + * Returns the first node of the given type. + * Searches the tree in depth-first order. + * This node is included in the search. + * If node of given type is not found, returns null. + */ + @SuppressWarnings("unchecked") + public final @Nullable T firstNodeOfType(Class nodeType) + { + Plan[] result = new Plan[] { null }; + forEach(node -> { + if (nodeType.isAssignableFrom(node.getClass())) + { + result[0] = node; + return ControlFlow.Break; + } + return ControlFlow.Continue; + }); + return (T) result[0]; + } + + /** + * Calls a function recursively for each node of given type in the tree. + * If the function returns {@link ControlFlow#Break} then the traversal is aborted. + * @return {@link ControlFlow#Continue} if traversal hasn't been aborted, {@link ControlFlow#Break} otherwise. + */ + final ControlFlow forEach(Function function) + { + return (function.apply(this) == ControlFlow.Continue) + ? forEachSubplan(subplan -> subplan.forEach(function)) + : ControlFlow.Break; + } + + /** + * Calls a function for each child node of this plan. + * The function should return {@link ControlFlow#Continue} to indicate the iteration should be continued + * and {@link ControlFlow#Break} to abort it. + * + * @return the value returned by the last invocation of the function + */ + abstract ControlFlow forEachSubplan(Function function); + + + /** Controls tree traversals, see {@link #forEach(Function)} and {@link #forEachSubplan(Function)} */ + enum ControlFlow { Continue, Break } + + /** + * Runs the updater function on each subplan and if the updater returns a new subplan, then reconstructs this + * plan from the modified subplans. + *

    + * Accepting a list of sub-plans would be a valid alternative design of this API, + * but that would require constructing a list on the heap by the caller for each updated node, + * and that would be potentially wasteful as most of the node types have at most one subplan and don't use + * lists internally. + * + * @param updater a function to be called on each subplan; if no update is needed, should return the argument + * @return a new plan if any of the subplans has been replaced, this otherwise + */ + protected abstract Plan withUpdatedSubplans(Function updater); + + /** + * Returns an object describing detailed cost information about running this plan. + * The actual type of the Cost depends in practice on the type of the result set returned by the node. + * The results of this method are supposed to be cached. The method is idempotent. + * The cost usually depends on the Access value. + */ + protected abstract Cost cost(); + + /** + * Estimates the probability of a random key or row of the table to be included in the result set + * if the result was iterated fully with no skipping and if it did not have any limits. + * This property is independent of the way how result set is used. + */ + protected abstract double estimateSelectivity(); + + /** + * Formats the whole plan as a pretty tree. + * + * @param redaction whether to redact the queried column values. + */ + public String toStringRecursive(Redaction redaction) + { + return toStringRecursive(redaction, null); + } + + /** + * Formats the whole plan as a pretty tree, with indentation + * + * @param redaction whether to redact the queried column values. + * @param indent a string used for indentation + */ + public String toStringRecursive(Redaction redaction, String indent) + { + TreeFormatter formatter = new TreeFormatter<>(plan -> plan.toString(redaction), Plan::subplans, indent); + return formatter.format(this); + } + + /** + * Returns the string representation of this node only, without redacting the queried column values. + * @see #toString(Redaction) + */ + @Override + public final String toString() + { + return toString(Redaction.NONE); + } + + /** + * Returns the string representation of this node only + * + * @param redaction whether to redact the queried column values. + */ + public final String toString(Redaction redaction) + { + String title = title(redaction); + String description = description(redaction); + return (title.isEmpty()) + ? String.format("%s (%s)\n%s", getClass().getSimpleName(), cost(), description).stripTrailing() + : String.format("%s %s (%s)\n%s", getClass().getSimpleName(), title, cost(), description).stripTrailing(); + } + + /** + * Returns additional information specific to the node displayed in the first line. + * The information is included in the output of {@link #toString()} and {@link #toRedactedStringRecursive()}. + * It is up to subclasses to implement it. + */ + protected String title(Redaction redaction) + { + return ""; + } + + /** + * Returns additional information specific to the node, displayed below the title. + * The information is included in the output of {@link #toString()} and {@link #toRedactedStringRecursive()}. + * It is up to subclasses to implement it. + */ + protected String description(Redaction redaction) + { + return ""; + } + + /** + * Traverses the tree recursively and calls the consumer for each index used in the plan. + */ + public final void visitIndexesRecursive(Consumer consumer) + { + forEach(node -> { + node.visitIndexes(consumer); + return Plan.ControlFlow.Continue; + }); + } + + /** + * Non-recursive auxiliary method for {@link #visitIndexesRecursive(Consumer)} + * that calls the consumer for the index(es) in the current node. + */ + protected void visitIndexes(Consumer consumer) + { + // By default, a node does not contain an index. + } + + /** + * Returns an optimized plan. + *

    + * The current optimization algorithm repeatedly cuts down one leaf of the plan tree + * and recomputes the nodes above it. Then it returns the best plan from candidates obtained that way. + * The expected running time is proportional to the height of the plan tree multiplied by the number of the leaves. + */ + protected Plan optimize() + { + if (logger.isTraceEnabled()) + logger.trace("Optimizing plan:\n{}", toStringRecursive(Redaction.REDACT)); + + Plan bestPlanSoFar = this; + List leaves = nodesOfType(Leaf.class); + + // Remove leaves one by one, starting from the ones with the worst selectivity + leaves.sort(Comparator.comparingDouble(Plan::selectivity).reversed()); + for (Leaf leaf : leaves) + { + // We won't try to skip leaves with a preferred index + if (leaf.usesIncludedIndex()) + continue; + + Plan candidate = bestPlanSoFar.removeRestriction(leaf.id); + if (logger.isTraceEnabled()) + logger.trace("Candidate query plan:\n{}", candidate.toStringRecursive(Redaction.REDACT)); + + if (candidate.fullCost() <= bestPlanSoFar.fullCost()) + bestPlanSoFar = candidate; + } + + if (logger.isTraceEnabled()) + logger.trace("Optimized plan:\n{}", bestPlanSoFar.toStringRecursive(Redaction.REDACT)); + return bestPlanSoFar; + } + + /** + * Modifies all intersections to not intersect more clauses than the given limit. + * Retains the most selective clauses. + */ + protected Plan limitIntersectedClauses(int clauseLimit) + { + Plan result = this; + if (result instanceof Intersection) + { + Plan.Intersection intersection = (Plan.Intersection) result; + result = intersection.stripSubplans(clauseLimit); + } + return result.withUpdatedSubplans(p -> p.limitIntersectedClauses(clauseLimit)); + } + + /** Returns true if the plan contains a node matching the condition */ + public final boolean contains(Function condition) + { + ControlFlow res = forEach(node -> (condition.apply(node)) ? ControlFlow.Break : ControlFlow.Continue); + return res == ControlFlow.Break; + } + + /** + * Returns true if the plan represents a hybrid query that first selects the matching + * rows by index-based search and then orders the matching rows in memory. This order of execution + * is best when the query contains a predicate that matches only a very small number of rows. + * Returns false in other cases, including when the query is not a hybrid query. + */ + public final boolean isSearchThenOrderHybrid() + { + return contains(node -> node instanceof KeysSort); + } + + /** + * Returns true if the plan represents a hybrid query that first scans the index in + * the index term-order (sorted by the index terms) and then filters the matching rows in memory. + * This order of execution is best when the query contains a predicate with a poor selectivity. + * Returns false in other cases, including when the query is not a hybrid query. + */ + public final boolean isOrderedScanThenFilterHybrid() + { + return (contains(node -> node instanceof Filter) + && contains(node -> node instanceof IndexScan && ((IndexScan) node).ordering != null + || node instanceof ScoredIndexScan)); + } + + /** + * Returns a new plan with the given node filtering restriction removed. + * Searches for the subplan to remove recursively down the tree. + * If the new plan is different, its estimates are also recomputed. + * If *this* plan matches the id, then the {@link Everything} node is returned. + * + *

    + * The purpose of this method is to optimise the plan. + * Sometimes not doing an intersection and post-filtering instead can be faster, so by removing child nodes from + * intersections we can potentially get a better plan. + */ + final Plan removeRestriction(int id) + { + if (this.id != id) + return withUpdatedSubplans(subplan -> subplan.removeRestriction(id)); + + // If id is the same, replace this node with "everything" + // because a query with no filter expression returns all rows + // (removing restrictions should widen the result set). + // Beware we must not remove ordering because that would change the semantics of the query. + Orderer ordering = this.ordering(); + return (ordering != null) + ? factory.sort(factory.everything, ordering) + : factory.everything; + } + + /** + * Returns the estimated cost of preparation steps + * that must be done before returning the first row / key + */ + public final double initCost() + { + return cost().initCost(); + } + + public final double iterCost() + { + return cost().iterCost(); + } + + /** + * Returns the estimated cost of running the plan to completion, i.e. exhausting + * the key or row iterator returned by it + */ + public final double fullCost() + { + return cost().fullCost(); + } + + /** + * Returns the estimated fraction of the table data that the result of this plan is expected to match + */ + public final double selectivity() + { + if (selectivity == -1) + selectivity = estimateSelectivity(); + assert 0.0 <= selectivity && selectivity <= 1.0 : "Invalid selectivity: " + selectivity; + return selectivity; + } + + /** + * Returns the number of indexes referenced by this plan. + * The same index referenced from unrelated query clauses, + * leading to separate index searches, are counted separately. + */ + public final int referencedIndexCount() + { + MutableInt count = new MutableInt(0); + visitIndexesRecursive(index -> count.increment()); + return count.intValue(); + } + + /** + * Returns the estimated number of rows to be fetched from storage. + */ + public final double estimatedRowsToFetch() + { + Fetch fetch = firstNodeOfType(Plan.Fetch.class); + return fetch != null ? fetch.expectedRows() : 0.0; + } + + /** + * Returns the estimated number of primary keys to be iterated by all index iterators. + * This may be larger than the number of rows to fetch because of intersections. + */ + public final double estimatedKeysToIterate() + { + MutableDouble total = new MutableDouble(0.0); + forEach(node -> { + if (node instanceof Leaf) + { + total.add(((Leaf) node).expectedKeys()); + } + return ControlFlow.Continue; + }); + return total.doubleValue(); + } + + protected interface Cost + { + /** + * Initialization cost: cannot be reduced later. + */ + double initCost(); + + /** + * Cost to iterate over all the expected keys or rows. May be reduced by LIMIT. + */ + double iterCost(); + + default double fullCost() + { + return initCost() + iterCost(); + } + } + + protected static final class KeysIterationCost implements Cost + { + final double expectedKeys; + final double initCost; + final double iterCost; + + /** + * @param expectedKeys number of keys expected to be iterated over + * @param initCost cost to set up the iteration + * @param iterCost *total* cost of iterating over the expected number of keys + */ + public KeysIterationCost(double expectedKeys, double initCost, double iterCost) + { + this.expectedKeys = expectedKeys; + this.initCost = initCost; + this.iterCost = iterCost; + } + + @Override + public double initCost() + { + return initCost; + } + + @Override + public double iterCost() + { + return iterCost; + } + + public double costPerKey() + { + return expectedKeys == 0 ? 0.0 : iterCost / expectedKeys; + } + + public String toString() + { + return String.format("keys: %.1f, cost/key: %.1f, cost: %.1f..%.1f", + expectedKeys, costPerKey(), initCost, fullCost()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeysIterationCost that = (KeysIterationCost) o; + return Double.compare(expectedKeys, that.expectedKeys) == 0 + && Double.compare(initCost, that.initCost) == 0 + && Double.compare(iterCost, that.iterCost) == 0; + } + + @Override + public int hashCode() + { + return Objects.hash(expectedKeys, initCost, iterCost); + } + } + + protected static final class RowsIterationCost implements Cost + { + final double expectedRows; + final double initCost; + final double iterCost; + + public RowsIterationCost(double expectedRows, double initCost, double iterCost) + { + this.expectedRows = expectedRows; + this.initCost = initCost; + this.iterCost = iterCost; + } + + @Override + public double initCost() + { + return initCost; + } + + @Override + public double iterCost() + { + return iterCost; + } + + public double costPerRow() + { + return expectedRows == 0 ? 0.0 : iterCost / expectedRows; + } + + public String toString() + { + return String.format("rows: %.1f, cost/row: %.1f, cost: %.1f..%.1f", + expectedRows, costPerRow(), initCost, fullCost()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RowsIterationCost that = (RowsIterationCost) o; + return Double.compare(expectedRows, that.expectedRows) == 0 + && Double.compare(initCost, that.initCost) == 0 + && Double.compare(iterCost, that.iterCost) == 0; + } + + @Override + public int hashCode() + { + return Objects.hash(expectedRows, initCost, iterCost); + } + } + + /** + * Common base class for all plan nodes that iterate over primary keys. + */ + public abstract static class KeysIteration extends Plan + { + /** + * Caches the estimated cost to avoid frequent recomputation + */ + private KeysIterationCost cost; + + protected KeysIteration(Factory factory, int id, Access access) + { + super(factory, id, access); + } + + @Override + protected final KeysIterationCost cost() + { + if (cost == null) + cost = estimateCost(); + return cost; + } + + protected abstract KeysIterationCost estimateCost(); + + /** + * Executes the operation represented by this node. + * The node itself isn't supposed for doing the actual work, but rather serves as a director which + * delegates the work to the query controller through the passed Executor. + * + * @param executor does all the hard work like fetching keys from the indexes or ANN sort + */ + protected abstract Iterator execute(Executor executor); + + protected abstract KeysIteration withAccess(Access patterns); + + final double expectedKeys() + { + return cost().expectedKeys; + } + + final double costPerKey() + { + return cost().costPerKey(); + } + + @Override + public final KeysIteration optimize() + { + return (KeysIteration) super.optimize(); + } + + @Override + public final KeysIteration limitIntersectedClauses(int clauseLimit) + { + return (KeysIteration) super.limitIntersectedClauses(clauseLimit); + } + + protected abstract boolean usesIncludedIndex(); + } + + /** + * Leaves of the plan tree cannot have subplans. + * This class exists purely for DRY purpose. + */ + abstract static class Leaf extends KeysIteration + { + protected Leaf(Factory factory, int id, Access accesses) + { + super(factory, id, accesses); + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + return ControlFlow.Continue; + } + + @Override + protected final Plan withUpdatedSubplans(Function updater) + { + // There are no subplans so it is a noop + return this; + } + } + + /** + * Represents an index scan that returns an empty range + */ + static class Nothing extends Leaf + { + protected Nothing(int id, Factory factory) + { + super(factory, id, null); + } + + @Nonnull + @Override + protected KeysIterationCost estimateCost() + { + return new KeysIterationCost(0, 0.0, 0.0); + } + + @Override + protected boolean usesIncludedIndex() + { + return false; + } + + @Nullable + @Override + protected Orderer ordering() + { + return null; + } + + @Override + protected double estimateSelectivity() + { + return 0; + } + + @Override + protected KeyRangeIterator execute(Executor executor) + { + return KeyRangeIterator.empty(); + } + + @Override + protected Nothing withAccess(Access patterns) + { + // limit does not matter for Nothing node because it always returns 0 keys + return this; + } + } + + /** + * Represents an index scan that returns all keys in the table. + * This is a virtual node that has no real representation in the database system. + * It is useful in query optimization. + */ + static class Everything extends Leaf + { + protected Everything(int id, Factory factory, Access accesses) + { + super(factory, id, accesses); + } + + @Nonnull + @Override + protected KeysIterationCost estimateCost() + { + // We set the cost to infinity so this node is never present in the optimized plan. + // We don't want to have those nodes in the final plan, + // because currently we have no way to execute it efficiently. + // In the future we may want to change it, when we have a way to return all rows without using an index. + return new KeysIterationCost(access.expectedAccessCount(factory.tableMetrics.rows), + Double.POSITIVE_INFINITY, + Double.POSITIVE_INFINITY); + } + + @Override + protected boolean usesIncludedIndex() + { + return false; + } + + @Nullable + @Override + protected Orderer ordering() + { + return null; + } + + @Override + protected double estimateSelectivity() + { + return 1.0; + } + + @Override + protected KeyRangeIterator execute(Executor executor) + { + // Not supported because it doesn't make a lot of sense. + // A direct scan of table data would be certainly faster. + // Everything node is not supposed to be executed. However, it is useful for analyzing various plans, + // e.g. we may get such node after removing some nodes from a valid, executable plan. + throw new UnsupportedOperationException("Returning an iterator over all keys is not supported."); + } + + @Override + protected Everything withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Everything(id, factory, access); + } + } + + abstract static class IndexScan extends Leaf + { + @Nullable + protected final Expression predicate; + @Nullable + protected final Orderer ordering; + + protected final long matchingKeysCount; + + public IndexScan(Factory factory, int id, Expression predicate, long matchingKeysCount, Access access, Orderer ordering) + { + super(factory, id, access); + Preconditions.checkArgument(predicate != null || ordering != null, + "Either predicate or ordering must be set"); + Preconditions.checkArgument(predicate == null + || ordering == null + || predicate.getIndexName().equals(ordering.getIndexName()), + "Ordering must use the same index as the predicate"); + this.predicate = predicate; + // If we match by equality, ordering makes no sense because all term values would be the same. + this.ordering = (predicate == null || predicate.getOp() != Expression.Op.EQ) ? ordering : null; + this.matchingKeysCount = matchingKeysCount; + } + + @Override + protected final String title(Redaction redaction) + { + return String.format("of %s (sel: %.9f, step: %.1f)", + getIndexName(), selectivity(), access.meanDistance()); + } + + @Override + protected String description(Redaction redaction) + { + StringBuilder sb = new StringBuilder(); + if (predicate != null) + { + sb.append("predicate: "); + sb.append(predicate.toString(redaction == Redaction.REDACT)); + sb.append('\n'); + } + if (ordering != null) + { + sb.append("ordering: "); + sb.append(ordering.toString(redaction)); + sb.append('\n'); + } + return sb.toString(); + } + + @Nullable + @Override + protected final Orderer ordering() + { + return ordering; + } + + @Override + protected final KeysIterationCost estimateCost() + { + double expectedKeys = access.expectedAccessCount(matchingKeysCount); + double costPerKey = access.unitCost(SAI_KEY_COST, this::estimateCostPerSkip); + // we hit-rate-scale the open cost, but not the per-key cost, under the assumption that + // readahead against the postings file will mostly amortize the penalty when hitting disk per key + double initCost = hrs(SAI_OPEN_COST) * factory.tableMetrics.sstables; + double iterCost = expectedKeys * costPerKey; + return new KeysIterationCost(expectedKeys, initCost, iterCost); + } + + @Override + protected double estimateSelectivity() + { + return factory.tableMetrics.rows > 0 + ? ((double) matchingKeysCount / factory.tableMetrics.rows) + : 0.0; + } + + @Override + protected boolean usesIncludedIndex() + { + return factory.hints.includes(getIndexName()); + } + + private double estimateCostPerSkip(double step) + { + // This is the first very rough approximation of the cost model for skipTo operation. + // It is likely not a very accurate model. + // We know for sure that the cost goes up the bigger the skip distance (= the more key we skip over) + // and also the bigger the merged posting list is. A range scan of the numeric + // index may require merging posting lists from many index nodes. Intuitively, the more keys we match, + // the higher number of posting lists are merged. Also, the further we skip, the higher number + // of posting lists must be advanced, and we're also more likely to hit a non-cached chunk. + // From a few experiments I did, I conclude those costs grow sublinearly. + // In the future we probably will need to take more index metrics into account + // (e.g. number of distinct values). + + double keysPerSSTable = (double) matchingKeysCount / factory.tableMetrics.sstables; + + double skipCostFactor; + double postingsCountFactor; + double postingsCountExponent; + double skipDistanceFactor; + double skipDistanceExponent; + if (predicate == null || predicate.getOp() == Expression.Op.RANGE) + { + skipCostFactor = RANGE_SCAN_SKIP_COST; + postingsCountFactor = RANGE_SCAN_SKIP_COST_POSTINGS_COUNT_FACTOR; + postingsCountExponent = RANGE_SCAN_SKIP_COST_POSTINGS_COUNT_EXPONENT; + skipDistanceFactor = RANGE_SCAN_SKIP_COST_DISTANCE_FACTOR; + skipDistanceExponent = RANGE_SCAN_SKIP_COST_DISTANCE_EXPONENT; + } + else + { + skipCostFactor = POINT_LOOKUP_SKIP_COST; + postingsCountFactor = 0.0; + postingsCountExponent = 1.0; + skipDistanceFactor = POINT_LOOKUP_SKIP_COST_DISTANCE_FACTOR; + skipDistanceExponent = POINT_LOOKUP_SKIP_COST_DISTANCE_EXPONENT; + } + + // divide by exponent so the derivative at 1.0 equals postingsCountFactor + double dKeys = postingsCountFactor / postingsCountExponent; + double postingsCountPenalty = dKeys * Math.pow(keysPerSSTable, postingsCountExponent); + + // divide by exponent so the derivative at 1.0 equals skipDistanceFactor + double dPostings = skipDistanceFactor / skipDistanceExponent; + double distancePenalty = dPostings * Math.pow(step, skipDistanceExponent); + + return skipCostFactor + * (1.0 + distancePenalty) + * (1.0 + postingsCountPenalty) + * factory.tableMetrics.sstables; + } + + @Override + protected Iterator execute(Executor executor) + { + return (ordering != null) + ? executor.getTopKRows(predicate, max(1, round((float) access.expectedAccessCount(factory.tableMetrics.rows)))) + : executor.getKeysFromIndex(predicate); + } + + public String getIndexName() + { + assert predicate != null || ordering != null; + return predicate != null ? predicate.getIndexName() : ordering.getIndexName(); + } + + @Override + final protected void visitIndexes(Consumer consumer) + { + assert predicate != null || ordering != null; + consumer.accept(predicate != null ? predicate.context : ordering.context); + } + } + /** + * Represents a scan over a numeric storage attached index. + */ + static class NumericIndexScan extends IndexScan + { + public NumericIndexScan(Factory factory, int id, Expression predicate, long matchingKeysCount, Access access, Orderer ordering) + { + super(factory, id, predicate, matchingKeysCount, access, ordering); + } + + @Override + protected NumericIndexScan withAccess(Access access) + { + return Objects.equals(this.access, access) + ? this + : new NumericIndexScan(factory, id, predicate, matchingKeysCount, access, ordering); + } + } + + /** + * Represents a scan over a literal storage attached index + */ + static class LiteralIndexScan extends IndexScan + { + public LiteralIndexScan(Factory factory, int id, Expression predicate, long matchingKeysCount, Access access, Orderer ordering) + { + super(factory, id, predicate, matchingKeysCount, access, ordering); + } + + @Override + protected LiteralIndexScan withAccess(Access access) + { + return Objects.equals(this.access, access) + ? this + : new LiteralIndexScan(factory, id, predicate, matchingKeysCount, this.access, ordering); + } + } + + /** + * Union of multiple primary key streams. + * This is a fairly cheap operation - its cost is basically a sum of costs of the subplans. + */ + static final class Union extends KeysIteration + { + private final LazyTransform> subplansSupplier; + private final boolean disjoint; + + Union(Factory factory, int id, List subplans, boolean disjoint, Access access) + { + super(factory, id, access); + Preconditions.checkArgument(!subplans.isEmpty(), "Subplans must not be empty"); + + this.disjoint = disjoint; + + // We propagate Access lazily just before we need the subplans. + // This is because there may be several requests to change the access pattern from the top, + // and we don't want to reconstruct the whole subtree each time + this.subplansSupplier = new LazyTransform<>(subplans, this::propagateAccess); + } + + /** + * Adjusts the counts for each subplan to account for the other subplans. + * As explained in `estimateSelectivity`, the union of (for instance) two subplans + * that each select 50% of the keys is 75%, not 100%. Thus, we need to reduce the counts + * to remove estimated overlapping keys. + */ + private List propagateAccess(List subplans) + { + if (isEffectivelyZero(selectivity())) + { + // all subplan selectivity should also be ~0 + for (var subplan: subplans) + assert isEffectivelyZero(subplan.selectivity()); + return subplans; + } + + ArrayList newSubplans = new ArrayList<>(subplans.size()); + for (KeysIteration subplan : subplans) + { + Access access = this.access.scaleCount(subplan.selectivity() / selectivity()); + newSubplans.add(subplan.withAccess(access)); + } + return newSubplans; + } + + @Override + protected double estimateSelectivity() + { + if (disjoint) + { + // If we know the subplans are disjoint, we can just sum their selectivities. + double selectivity = 0.0; + for (KeysIteration plan : subplansSupplier.orig) + selectivity += plan.selectivity(); + return Math.min(1.0, selectivity); + } + else + { + // Assume independence (lack of correlation) of subplans. + // We multiply the probabilities of *not* selecting a key. + // Because selectivity is usage-independent, we can use the original subplans, + // to avoid forcing pushdown of Access information down. + double inverseSelectivity = 1.0; + for (KeysIteration plan : subplansSupplier.orig) + inverseSelectivity *= (1.0 - plan.selectivity()); + return 1.0 - inverseSelectivity; + } + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + for (Plan s : subplansSupplier.get()) + { + if (function.apply(s) == ControlFlow.Break) + return ControlFlow.Break; + } + return ControlFlow.Continue; + } + + @Override + protected Plan withUpdatedSubplans(Function updater) + { + List subplans = subplansSupplier.get(); + ArrayList newSubplans = new ArrayList<>(subplans.size()); + for (Plan subplan : subplans) + newSubplans.add((KeysIteration) updater.apply(subplan)); + + return newSubplans.equals(subplans) + ? this + : factory.union(newSubplans, disjoint, id).withAccess(access); + } + + @Override + protected Union withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Union(factory, id, subplansSupplier.orig, disjoint, access); + } + + @Override + protected KeysIterationCost estimateCost() + { + double initCost = 0.0; + double iterCost = 0.0; + List subplans = subplansSupplier.get(); + for (int i = 0; i < subplans.size(); i++) + { + KeysIteration subplan = subplans.get(i); + // Initialization must be done all branches before we can start iterating + initCost += subplan.initCost(); + iterCost += subplan.iterCost(); + } + double expectedKeys = access.expectedAccessCount(factory.tableMetrics.rows * selectivity()); + return new KeysIterationCost(expectedKeys, initCost, iterCost); + } + + @Nullable + @Override + protected Orderer ordering() + { + return subplansSupplier.get().get(0).ordering(); + } + + @Override + protected KeyRangeIterator execute(Executor executor) + { + KeyRangeIterator.Builder builder = KeyRangeUnionIterator.builder(); + try + { + for (KeysIteration plan : subplansSupplier.get()) + builder.add((KeyRangeIterator) plan.execute(executor)); + return builder.build(); + } + catch (Throwable t) + { + FileUtils.closeQuietly(builder.ranges()); + throw t; + } + } + + @Override + protected boolean usesIncludedIndex() + { + return subplansSupplier.get().stream().anyMatch(KeysIteration::usesIncludedIndex); + } + } + + /** + * Intersection of multiple primary key streams. + * This is quite complex operation where many keys from all underlying streams must be read in order + * to return one matching key. Therefore, expect the cost of this operation to be significantly higher than + * the costs of the subplans. + */ + static final class Intersection extends KeysIteration + { + private final LazyTransform> subplansSupplier; + + private Intersection(Factory factory, int id, List subplans, Access access) + { + super(factory, id, access); + Preconditions.checkArgument(!subplans.isEmpty(), "Subplans must not be empty"); + + // We propagate Access lazily just before we need the subplans. + // This is because there may be several requests to change the access pattern from the top, + // and we don't want to reconstruct the whole subtree each time + this.subplansSupplier = new LazyTransform<>(subplans, this::propagateAccess); + } + + /** + * In an intersection operation, the goal is to find the common elements between the results + * of multiple subplans. This requires taking into account not only the selectivity but also + * the match probabilities between subplans. + *

    + * VSTODO explain what's going on in more detail. + */ + private ArrayList propagateAccess(List subplans) + { + double loops = subplans.get(0).selectivity() / boundedSelectivity(selectivity()); + ArrayList newSubplans = new ArrayList<>(subplans.size()); + KeysIteration s0 = subplans.get(0).withAccess(access.scaleDistance(loops).convolute(loops, 1.0)); + newSubplans.add(s0); + + // We may run out of keys while iterating the first iterator, and then we just break the loop early + loops = Math.min(s0.expectedKeys(), loops); + + double matchProbability = 1.0; + for (int i = 1; i < subplans.size(); i++) + { + KeysIteration subplan = subplans.get(i); + double cumulativeSelectivity = subplans.get(0).selectivity() * matchProbability; + double skipDistance = subplan.selectivity() / boundedSelectivity(cumulativeSelectivity); + Access subAccess = access.scaleDistance(subplan.selectivity() / boundedSelectivity(selectivity())) + .convolute(loops * matchProbability, skipDistance) + .forceSkip(); + newSubplans.add(subplan.withAccess(subAccess)); + matchProbability *= subplan.selectivity(); + } + return newSubplans; + } + + @Override + protected double estimateSelectivity() + { + double selectivity = 1.0; + for (KeysIteration plan : subplansSupplier.orig) + selectivity *= plan.selectivity(); + return selectivity; + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + for (Plan s : subplansSupplier.get()) + { + if (function.apply(s) == ControlFlow.Break) + return ControlFlow.Break; + } + return ControlFlow.Continue; + } + + @Override + protected Plan withUpdatedSubplans(Function updater) + { + List subplans = subplansSupplier.get(); + ArrayList newSubplans = new ArrayList<>(subplans.size()); + for (Plan subplan : subplans) + newSubplans.add((KeysIteration) updater.apply(subplan)); + + return newSubplans.equals(subplans) + ? this + : factory.intersection(newSubplans, id).withAccess(access); + } + + @Override + protected Intersection withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Intersection(factory, id, subplansSupplier.orig, access); + } + + @Nullable + @Override + protected Orderer ordering() + { + return subplansSupplier.get().get(0).ordering(); + } + + @Override + protected KeysIterationCost estimateCost() + { + List subplans = subplansSupplier.get(); + assert !subplans.isEmpty() : "Expected at least one subplan here. An intersection of 0 plans should have been optimized out."; + + double initCost = 0.0; + double iterCost = 0.0; + for (KeysIteration subplan : subplans) + { + initCost += subplan.initCost(); + iterCost += subplan.iterCost(); + } + double expectedKeyCount = access.expectedAccessCount(factory.tableMetrics.rows * selectivity()); + return new KeysIterationCost(expectedKeyCount, initCost, iterCost); + } + + @Override + protected KeyRangeIterator execute(Executor executor) + { + KeyRangeIterator.Builder builder = KeyRangeIntersectionIterator.builder(); + try + { + for (KeysIteration plan : subplansSupplier.get()) + builder.add((KeyRangeIterator) plan.execute(executor)); + + return builder.build(); + } + catch (Throwable t) + { + FileUtils.closeQuietly(builder.ranges()); + throw t; + } + } + + @Override + protected boolean usesIncludedIndex() + { + return subplansSupplier.get().stream().anyMatch(KeysIteration::usesIncludedIndex); + } + + /** + * Limits the number of intersected subplans + */ + public Plan stripSubplans(int clauseLimit) + { + if (subplansSupplier.orig.size() <= clauseLimit) + return this; + + if (factory.hints.included.isEmpty()) + return withNewSubplans(new ArrayList<>(subplansSupplier.orig.subList(0, clauseLimit))); + + List newSubplans = new ArrayList<>(clauseLimit); + List optionalNewSubplans = new ArrayList<>(clauseLimit); + for (KeysIteration keysIteration : subplansSupplier.orig) + { + if (keysIteration.usesIncludedIndex()) + newSubplans.add(keysIteration); + else + optionalNewSubplans.add(keysIteration); + } + for (KeysIteration keysIteration : optionalNewSubplans) + { + if (newSubplans.size() >= clauseLimit) + break; + newSubplans.add(keysIteration); + } + + return withNewSubplans(newSubplans); + } + + private Plan withNewSubplans(List newSubplans) + { + return factory.intersection(newSubplans, id).withAccess(access); + } + } + + /** + * Sorts keys in ANN order. + * Must fetch all keys from the source before sorting, so it has a high initial cost. + */ + static final class KeysSort extends KeysIteration + { + private final KeysIteration source; + final Orderer ordering; + + KeysSort(Factory factory, int id, KeysIteration source, Access access, Orderer ordering) + { + super(factory, id, access); + this.source = source; + this.ordering = ordering; + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + return function.apply(source); + } + + @Override + protected Plan withUpdatedSubplans(Function updater) + { + return factory.sort((KeysIteration) updater.apply(source), ordering, id).withAccess(access); + } + + @Override + protected double estimateSelectivity() + { + return source.selectivity(); + } + + @Override + protected KeysIterationCost estimateCost() + { + if (ordering.isANN()) + return estimateAnnSortCost(); + else if (ordering.isBM25()) + return estimateBm25SortCost(); + else + return estimateGlobalSortCost(); + } + + private KeysIterationCost estimateAnnSortCost() + { + double expectedKeys = access.expectedAccessCount(source.expectedKeys()); + int expectedKeysInt = max(1, (int) Math.ceil(expectedKeys)); + int expectedSourceKeysInt = max(1, (int) Math.ceil(source.expectedKeys())); + double initCost = annSortOpenCost(factory.keyspace) * factory.tableMetrics.sstables + + source.fullCost() + + source.expectedKeys() * CostCoefficients.annSortKeyCost(factory.keyspace); + double searchCost = factory.costEstimator.estimateAnnSearchCost(ordering, + expectedKeysInt, + expectedSourceKeysInt); + return new KeysIterationCost(expectedKeys, initCost, searchCost); + } + + private KeysIterationCost estimateBm25SortCost() + { + double expectedKeys = access.expectedAccessCount(source.expectedKeys()); + + int termCount = ordering.getQueryTerms().size(); + // all of the cost for BM25 is up front since the index doesn't give us the information we need + // to return results in order, in isolation. The big cost is reading the indexed cells out of + // the sstables. + // VSTODO if we had stats on cell size _per column_ we could usefully include ROW_BYTE_COST + double initCost = source.fullCost() + + source.expectedKeys() * (hrs(ROW_CELL_COST) + ROW_CELL_COST) + + termCount * BM25_SCORE_COST; + return new KeysIterationCost(expectedKeys, initCost, 0); + } + + private KeysIterationCost estimateGlobalSortCost() + { + return new KeysIterationCost(source.expectedKeys(), + source.fullCost() + source.expectedKeys() * hrs(ROW_COST), + source.expectedKeys() * SAI_KEY_COST); + + } + + @Nullable + @Override + protected Orderer ordering() + { + return ordering; + } + + @Override + protected Iterator execute(Executor executor) + { + KeyRangeIterator sourceIterator = (KeyRangeIterator) source.execute(executor); + int softLimit = max(1, round((float) access.expectedAccessCount(factory.tableMetrics.rows))); + return executor.getTopKRows(sourceIterator, softLimit); + } + + @Override + protected KeysSort withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new KeysSort(factory, id, source, access, ordering); + } + + @Override + protected boolean usesIncludedIndex() + { + return source.usesIncludedIndex(); + } + + @Override + protected String description(Redaction redaction) + { + return ordering.toString(redaction); + } + } + + /** + * Base class for index scans that return results in a computed order (ANN, BM25) + * rather than the natural index order. + */ + abstract static class ScoredIndexScan extends Leaf + { + final Orderer ordering; + + protected ScoredIndexScan(Factory factory, int id, Access access, Orderer ordering) + { + super(factory, id, access); + this.ordering = ordering; + } + + @Nullable + @Override + protected Orderer ordering() + { + return ordering; + } + + @Override + protected double estimateSelectivity() + { + return 1.0; + } + + @Override + protected boolean usesIncludedIndex() + { + return factory.hints.includes(getIndexName()); + } + + @Override + protected Iterator execute(Executor executor) + { + int softLimit = max(1, round((float) access.expectedAccessCount(factory.tableMetrics.rows))); + return executor.getTopKRows((Expression) null, softLimit); + } + + public String getIndexName() + { + return ordering.getIndexName(); + } + } + + /** + * Returns all keys in ANN order. + * Contrary to {@link KeysSort}, there is no input node here and the output is generated lazily. + */ + final static class AnnIndexScan extends ScoredIndexScan + { + protected AnnIndexScan(Factory factory, int id, Access access, Orderer ordering) + { + super(factory, id, access, ordering); + } + + @Override + protected KeysIterationCost estimateCost() + { + double expectedKeys = access.expectedAccessCount(factory.tableMetrics.rows); + int expectedKeysInt = Math.max(1, (int) Math.ceil(expectedKeys)); + double searchCost = factory.costEstimator.estimateAnnSearchCost(ordering, + expectedKeysInt, + factory.tableMetrics.rows); + double initCost = 0; // negligible + return new KeysIterationCost(expectedKeys, initCost, searchCost); + } + + @Override + protected KeysIteration withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new AnnIndexScan(factory, id, access, ordering); + } + + @Override + protected void visitIndexes(Consumer consumer) + { + consumer.accept(ordering.context); + } + + @Override + protected String description(Redaction redaction) + { + return ordering.toString(redaction); + } + } + + /** + * Returns all keys in BM25 order. + * Like AnnIndexScan, this generates results lazily without an input node. + */ + final static class Bm25IndexScan extends ScoredIndexScan + { + protected Bm25IndexScan(Factory factory, int id, Access access, Orderer ordering) + { + super(factory, id, access, ordering); + } + + @Nonnull + @Override + protected KeysIterationCost estimateCost() + { + double expectedKeys = access.expectedAccessCount(factory.tableMetrics.rows); + int expectedKeysInt = Math.max(1, (int) Math.ceil(expectedKeys)); + + int termCount = ordering.getQueryTerms().size(); + double initCost = expectedKeysInt * (hrs(ROW_CELL_COST) + ROW_CELL_COST) + + termCount * BM25_SCORE_COST; + + return new KeysIterationCost(expectedKeys, initCost, 0); + } + + @Override + protected KeysIteration withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Bm25IndexScan(factory, id, access, ordering); + } + + @Override + protected void visitIndexes(Consumer consumer) + { + consumer.accept(ordering.context); + } + + @Override + protected String description(Redaction redaction) + { + return ordering.toString(redaction); + } + } + + abstract public static class RowsIteration extends Plan + { + private RowsIterationCost cost; + + private RowsIteration(Factory factory, int id, Access access) + { + super(factory, id, access); + } + + @Override + protected RowsIterationCost cost() + { + if (cost == null) + cost = estimateCost(); + return cost; + } + + protected abstract RowsIterationCost estimateCost(); + + protected abstract RowsIteration withAccess(Access patterns); + + final double costPerRow() + { + return cost().costPerRow(); + } + + @VisibleForTesting + public final double expectedRows() + { + return cost().expectedRows; + } + + @Override + public final RowsIteration optimize() + { + return (RowsIteration) super.optimize(); + } + + @Override + public final RowsIteration limitIntersectedClauses(int clauseLimit) + { + return (RowsIteration) super.limitIntersectedClauses(clauseLimit); + } + } + + /** + * Retrieves rows from storage based on the stream of primary keys + */ + static final class Fetch extends RowsIteration + { + private final LazyTransform source; + + private Fetch(Factory factory, int id, KeysIteration keysIteration, Access access) + { + super(factory, id, access); + this.source = new LazyTransform<>(keysIteration, k -> k.withAccess(access)); + } + + @Nullable + @Override + protected Orderer ordering() + { + return source.get().ordering(); + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + return function.apply(source.get()); + } + + @Override + protected Fetch withUpdatedSubplans(Function updater) + { + Plan.KeysIteration updatedSource = (KeysIteration) updater.apply(source.get()); + return updatedSource == source.get() ? this : new Fetch(factory, id, updatedSource, access); + } + + @Override + protected double estimateSelectivity() + { + return source.orig.selectivity(); + } + + @Override + protected RowsIterationCost estimateCost() + { + // VSTODO this assumes we will need to deserialize the entire row for any fetch. + // For vector rows where we need to check a non-vector field for a predicate, + // this is a very pessimistic assumption since the vectors (that we don't read) + // are by far the majority of the row size. + double rowFetchCost = hrs(CostCoefficients.ROW_COST) + + CostCoefficients.ROW_CELL_COST * factory.tableMetrics.avgCellsPerRow + + CostCoefficients.ROW_BYTE_COST * factory.tableMetrics.avgBytesPerRow; + + KeysIteration src = source.get(); + double expectedKeys = access.expectedAccessCount(src.expectedKeys()); + return new RowsIterationCost(expectedKeys, + src.initCost(), + src.iterCost() + expectedKeys * rowFetchCost); + } + + @Override + protected Fetch withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Fetch(factory, id, source.orig, access); + } + } + + /** + * Filters rows. + * In order to return one row in the result set it may need to retrieve many rows from the source node. + * Hence, it will typically have higher cost-per-row than the source node, and will return fewer rows. + */ + static class Filter extends RowsIteration + { + private final RowFilter filter; + private final LazyTransform source; + private final double targetSelectivity; + + Filter(Factory factory, int id, RowFilter filter, RowsIteration source, double targetSelectivity, Access access) + { + super(factory, id, access); + this.filter = filter; + this.source = new LazyTransform<>(source, this::propagateAccess); + this.targetSelectivity = targetSelectivity; + } + + @Nullable + @Override + protected Orderer ordering() + { + return source.get().ordering(); + } + + /** + * Scale the access pattern of the source to reflect that we will need + * to keep pulling rows from it until the Filter is satisfied. + */ + private RowsIteration propagateAccess(RowsIteration source) + { + Access scaledAccess = access.scaleCount(source.selectivity() / boundedSelectivity(targetSelectivity)); + return source.withAccess(scaledAccess); + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + return function.apply(source.get()); + } + + @Override + protected Plan withUpdatedSubplans(Function updater) + { + Plan.RowsIteration updatedSource = (RowsIteration) updater.apply(source.get()); + return updatedSource == source.get() + ? this + : new Filter(factory, id, filter, updatedSource, targetSelectivity, access); + } + + @Override + protected double estimateSelectivity() + { + return targetSelectivity; + } + + @Override + protected RowsIterationCost estimateCost() + { + double expectedRows = access.expectedAccessCount(factory.tableMetrics.rows * targetSelectivity); + return new RowsIterationCost(expectedRows, + source.get().initCost(), + source.get().iterCost()); + } + + @Override + protected Filter withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Filter(factory, id, filter, source.orig, targetSelectivity, access); + } + + @Override + protected String title(Redaction redaction) + { + return String.format("%s (sel: %.9f)", filter.toCQLString(redaction), selectivity() / source.get().selectivity()); + } + } + + /** + * Limits the number of returned rows to a fixed number. + * Unlike {@link Filter} it does not affect the cost-per-row. + */ + static class Limit extends RowsIteration + { + private final LazyTransform source; + final int limit; + + private Limit(Factory factory, int id, RowsIteration source, int limit, Access access) + { + super(factory, id, access); + this.limit = limit; + this.source = new LazyTransform<>(source, s -> s.withAccess(access.limit(limit))); + } + + @Nullable + @Override + protected Orderer ordering() + { + return source.get().ordering(); + } + + @Override + protected ControlFlow forEachSubplan(Function function) + { + return function.apply(source.get()); + } + + @Override + protected Plan withUpdatedSubplans(Function updater) + { + Plan.RowsIteration updatedSource = (RowsIteration) updater.apply(source.get()); + return updatedSource == source.get() ? this : new Limit(factory, id, updatedSource, limit, access); + } + + @Override + protected double estimateSelectivity() + { + return source.orig.selectivity(); + } + + @Override + protected RowsIterationCost estimateCost() + { + RowsIteration src = source.get(); + double expectedRows = access.expectedAccessCount(src.expectedRows()); + double iterCost = (limit >= src.expectedRows()) + ? src.iterCost() + : src.iterCost() * limit / src.expectedRows(); + return new RowsIterationCost(expectedRows, src.initCost(), iterCost); + } + + @Override + protected RowsIteration withAccess(Access access) + { + return Objects.equals(access, this.access) + ? this + : new Limit(factory, id, source.orig, limit, access); + } + + @Override + protected String title(Redaction redaction) + { + return "" + limit; + } + } + + /** + * Constructs plan nodes. + * Contains data common for all plan nodes. + * Performs very lightweight local optimizations. + * E.g. requesting an intersection/union of only one subplan will result in returning the subplan directly + * and no intersection/union will be created. + */ + @NotThreadSafe + public static final class Factory + { + public final String keyspace; + + /** Table metrics that affect cost estimates, e.g. row count, sstable count etc */ + public final TableMetrics tableMetrics; + + public final CostEstimator costEstimator; + + public final IndexHints hints; + + /** A plan returning no keys */ + public final KeysIteration nothing; + + /** A plan returning all keys in the table */ + public final KeysIteration everything; + + /** Default access pattern is to read all rows/keys without skipping until the end of the iterator */ + private final Access defaultAccess; + + /** Id of the next new node created by this factory */ + private int nextId = 0; + + /** + * Creates a factory that produces Plan nodes. + * + * @param tableMetrics allows the planner to adapt the cost estimates to the actual amount of data stored in the table + * @param costEstimator a cost estimator + * @param hints the user-provided index hints, the plan should try to respect them + */ + public Factory(String keyspace, TableMetrics tableMetrics, CostEstimator costEstimator, IndexHints hints) + { + this.keyspace = keyspace; + this.tableMetrics = tableMetrics; + this.costEstimator = costEstimator; + this.hints = hints; + this.nothing = new Nothing(-1, this); + this.defaultAccess = Access.sequential(tableMetrics.rows); + this.everything = new Everything(-1, this, defaultAccess); + } + + /** + * Constructs a plan node representing a direct scan of an index. + * + * @param predicate the expression matching the rows that we want to search in the index; + * this is needed for identifying this node, it doesn't affect the cost + * @param matchingKeysCount the number of row keys expected to be returned by the index scan, + * i.e. keys of rows that match the search predicate + */ + public KeysIteration indexScan(@Nullable Expression predicate, long matchingKeysCount) + { + Preconditions.checkArgument(matchingKeysCount >= 0, "matchingKeyCount must not be negative"); + Preconditions.checkArgument(matchingKeysCount <= tableMetrics.rows, "matchingKeyCount must not exceed totalKeyCount"); + return indexScan(predicate, matchingKeysCount, null, nextId++); + } + + private KeysIteration indexScan(Expression predicate, long matchingKeysCount, Orderer ordering, int id) + { + if (predicate == null && ordering == null) + { + assert matchingKeysCount == tableMetrics.rows; + return everything; + } + + if (ordering != null) + if (ordering.isANN()) + return new AnnIndexScan(this, id, defaultAccess, ordering); + else if (ordering.isBM25()) + return new Bm25IndexScan(this, id, defaultAccess, ordering); + else if (ordering.isLiteral()) + return new LiteralIndexScan(this, id, predicate, matchingKeysCount, defaultAccess, ordering); + else + return new NumericIndexScan(this, id, predicate, matchingKeysCount, defaultAccess, ordering); + + Preconditions.checkNotNull(predicate, "predicate must not be null"); + Preconditions.checkArgument(matchingKeysCount >= 0, "matchingKeyCount must not be negative"); + Preconditions.checkArgument(matchingKeysCount <= tableMetrics.rows, "matchingKeyCount must not exceed totalKeyCount"); + return predicate.isLiteral() + ? new LiteralIndexScan(this, id, predicate, matchingKeysCount, defaultAccess, null) + : new NumericIndexScan(this, id, predicate, matchingKeysCount, defaultAccess, null); + } + + public KeysIteration fullIndexScan(IndexContext context) + { + Expression everythingExpression = new Expression(context); + everythingExpression.operation = Expression.Op.RANGE; + return indexScan(everythingExpression, tableMetrics.rows); + } + + /** + * Constructs a plan node representing a union of two key sets. + * Key sets are assumed to be non-correlated and may overlap. + * @param subplans a list of subplans for unioned key sets + */ + public KeysIteration union(List subplans) + { + return union(subplans, false, nextId++); + } + + /** + * Constructs a plan node representing a union of two key sets. + * + * @param subplans a list of subplans for unioned key sets + * @param disjoint hint to the planner that the subplans are disjoint; used for better row count estimation + */ + public KeysIteration union(List subplans, boolean disjoint) + { + return union(subplans, disjoint, nextId++); + } + + + private KeysIteration union(List subplans, boolean disjoint, int id) + { + if (subplans.contains(everything)) + return everything; + if (subplans.contains(nothing)) + subplans.removeIf(s -> s == nothing); + if (subplans.size() == 1) + return subplans.get(0); + if (subplans.isEmpty()) + return nothing; + + return new Union(this, id, subplans, disjoint, defaultAccess); + } + + /** + * Constructs a plan node representing an intersection of key sets. + * The subplans will be sorted by selectivity from the most selective to the least selective ones. + * @param subplans a list of subplans for intersected key sets + */ + public KeysIteration intersection(List subplans) + { + return intersection(subplans, nextId++); + } + + private KeysIteration intersection(List subplans, int id) + { + if (subplans.contains(nothing)) + return nothing; + if (subplans.contains(everything)) + subplans.removeIf(c -> c == everything); + if (subplans.size() == 1) + return subplans.get(0); + if (subplans.isEmpty()) + return everything; + + subplans.sort(Comparator.comparing(KeysIteration::selectivity)); + return new Intersection(this, id, subplans, defaultAccess); + } + + public Builder unionBuilder() + { + return new Builder(this, Operation.OperationType.OR); + } + + public Builder intersectionBuilder() + { + return new Builder(this, Operation.OperationType.AND); + } + + /** + * Constructs a node that sorts keys using an index + */ + public KeysIteration sort(@Nonnull KeysIteration source, @Nonnull Orderer ordering) + { + return sort(source, ordering, nextId++); + } + + private KeysIteration sort(@Nonnull KeysIteration source, @Nonnull Orderer ordering, int id) + { + if (source instanceof IndexScan) + { + // Optimization + // If we want to sort on the same column as the index scan we already have, + // then we collapse sorting with filtering in a single plan node as the index + // is already sorted. + IndexScan indexScan = (IndexScan) source; + if (indexScan.getIndexName().equals(ordering.getIndexName())) + return indexScan(indexScan.predicate, indexScan.matchingKeysCount, ordering, id); + } + + return (source instanceof Everything) + ? indexScan(null, tableMetrics.rows, ordering, id) + : new KeysSort(this, id, source, defaultAccess, ordering); + } + + /** + * Constructs a node that lazily fetches the rows from storage, based on the primary key iterator. + */ + public RowsIteration fetch(@Nonnull KeysIteration keysIterationPlan) + { + return new Fetch(this, nextId++, keysIterationPlan, defaultAccess); + } + + /** + * Constructs a filter node with fixed target selectivity set to the selectivity of the source node. + * @see Plan.Factory#filter + */ + public RowsIteration recheckFilter(@Nonnull RowFilter filter, @Nonnull RowsIteration source) + { + return filter.isEmpty() + ? source + : new Filter(this, nextId++, filter, source, source.selectivity(), defaultAccess); + } + + /** + * Constructs a filter node with fixed target selectivity. + *

    + * Fixed target selectivity means that the expected number of rows returned by this node is always + * targetSelectivity/totalRows, regardless of the number of the input rows. + * Changing the number of the input rows by replacing the subplan + * with a subplan of different selectivity does not cause this node to return a different number + * of rows (however, it may change the cost per row estimate). + *

    + * This property is useful for constructing so-called "recheck filters" – filters that + * are not any weaker than the filters in the subplan. If a recheck filter is present, we can freely reduce + * selectivity of the subplan by e.g. removing intersection nodes, and we still get exactly same number of rows + * in the result set. + *

    + * @param filter defines which rows are accepted + * @param source source plan providing the input rows + * @param targetSelectivity a value in range [0.0, 1.0], but not greater than the selectivity of source + */ + public RowsIteration filter(@Nonnull RowFilter filter, @Nonnull RowsIteration source, double targetSelectivity) + { + Preconditions.checkArgument(targetSelectivity >= 0.0, "selectivity must not be negative"); + Preconditions.checkArgument(targetSelectivity <= source.selectivity(), "selectivity must not exceed source selectivity of " + source.selectivity()); + return new Filter(this, nextId++, filter, source, targetSelectivity, defaultAccess); + } + + /** + * Constructs a plan node that fetches only a limited number of rows. + * It is likely going to have lower fullCost than the fullCost of its input. + */ + public RowsIteration limit(@Nonnull RowsIteration source, int limit) + { + return new Limit(this, nextId++, source, limit, defaultAccess); + } + } + + public static class TableMetrics + { + public final long rows; + public final double avgCellsPerRow; + public final double avgBytesPerRow; + public final int sstables; + + public TableMetrics(long rows, double avgCellsPerRow, double avgBytesPerRow, int sstables) + { + this.rows = rows; + this.avgCellsPerRow = avgCellsPerRow; + this.avgBytesPerRow = avgBytesPerRow; + this.sstables = sstables; + } + } + + /** + * Executes the plan + */ + public interface Executor + { + Iterator getKeysFromIndex(Expression predicate); + Iterator getTopKRows(Expression predicate, int softLimit); + Iterator getTopKRows(KeyRangeIterator keys, int softLimit); + } + + /** + * Outsources more complex cost estimates to external components. + * Some components may collect stats on previous data execution and deliver more accurate estimates based + * on that state. + */ + public interface CostEstimator + { + /** + * Returns the expected number of ANN index nodes that must be visited to get the list of candidates for top K. + * + * @param ordering allows to identify the proper index + * @param limit number of rows to fetch; must be > 0 + * @param candidates number of candidate rows that satisfy the expression predicates + */ + double estimateAnnSearchCost(Orderer ordering, int limit, long candidates); + } + + /** + * Data-independent cost coefficients. + * They are likely going to change whenever storage engine algorithms change. + */ + public static class CostCoefficients + { + /** The constant cost of performing skipTo on posting lists returned from range scans */ + public final static double RANGE_SCAN_SKIP_COST = 0.2; + + /** The coefficient controlling the increase of the skip cost with the distance of the skip. */ + public final static double RANGE_SCAN_SKIP_COST_DISTANCE_FACTOR = 0.1; + public final static double RANGE_SCAN_SKIP_COST_DISTANCE_EXPONENT = 0.5; + + /** The coefficient controlling the increase of the skip cost with the total size of the posting list. */ + public final static double RANGE_SCAN_SKIP_COST_POSTINGS_COUNT_FACTOR = 0.03; + public final static double RANGE_SCAN_SKIP_COST_POSTINGS_COUNT_EXPONENT = 0.33; + + /** The constant cost of performing skipTo on literal indexes */ + public final static double POINT_LOOKUP_SKIP_COST = 0.5; + + /** The coefficient controlling the increase of the skip cost with the total size of the posting list for point lookup queries. */ + public final static double POINT_LOOKUP_SKIP_COST_DISTANCE_FACTOR = 0.1; + public final static double POINT_LOOKUP_SKIP_COST_DISTANCE_EXPONENT = 0.5; + + /** Cost to open the per-sstable index, read metadata and obtain the iterators. Affected by cache hit rate. */ + public final static double SAI_OPEN_COST = 1500.0; + + /** Cost to advance the index iterator to the next key and load the key. Common for literal and numeric indexes. */ + public final static double SAI_KEY_COST = 0.1; + + /** Cost to get a scored key from DiskANN (~rerank cost). Affected by cache hit rate */ + public final static double ANN_SCORED_KEY_COST = 15; + + /** Cost to perform a coarse (PQ) in-memory similarity computation */ + public final static double ANN_SIMILARITY_COST = 0.5; + + /** Cost to load the neighbor list for a DiskANN node. Affected by cache hit rate */ + public final static double ANN_EDGELIST_COST = 20.0; + + /** Cost to fetch one row from storage. Affected by cache hit rate */ + public final static double ROW_COST = 100.0; + + /** Additional cost added to row fetch cost per each row cell */ + public final static double ROW_CELL_COST = 0.4; + + /** Additional cost added to row fetch cost per each serialized byte of the row */ + public final static double ROW_BYTE_COST = 0.005; + + /** Cost to perform BM25 scoring, per query term */ + public final static double BM25_SCORE_COST = 0.5; + + /** Cost to begin processing PKs into index ordinals for estimateAnnSortCost */ + // DC introduced the one-to-many ordinal mapping optimization + public static double annSortOpenCost(String keyspace) + { + return Version.current(keyspace).onOrAfter(Version.DC) ? 370 : 4200; + } + + /** Additional overhead needed to process each input key fed to the ANN index searcher */ + // DC introduced the one-to-many ordinal mapping optimization + public static double annSortKeyCost(String keyspace) + { + return Version.current(keyspace).onOrAfter(Version.DC) ? 0.03 : 0.2; + } + + } + + /** Convenience builder for building intersection and union nodes */ + public static class Builder + { + final Factory factory; + final Operation.OperationType type; + final List subplans; + + Builder(Factory context, Operation.OperationType type) + { + this.factory = context; + this.type = type; + this.subplans = new ArrayList<>(4); + } + + public Builder add(KeysIteration subplan) + { + subplans.add(subplan); + return this; + } + + public KeysIteration build() + { + if (type == Operation.OperationType.AND) + return factory.intersection(subplans); + if (type == Operation.OperationType.OR) + return factory.union(subplans); + + // Should never hit this + throw new AssertionError("Unexpected builder type: " + type); + } + } + + /** hit-rate-scale the raw cost */ + public static double hrs(double raw) + { + double multiplier = min(1000.0, 1 / hitRateSupplier.getAsDouble()); + return raw * multiplier; + } + + /** + * Describes the expected data access patterns for a plan node. + *
    + * Each access pattern is assumed to follow uniform distribution and + * is represented by a pair of values: + * - a count (number of expected occurrences) and + * - an average distance to next occurrence (skip distance). + * For performance, these are split into arrays of the primitives. + *
    + * For example, given: + * counts = [100, 50, 10] + * distances = [1.0, 2.0, 5.0] + * This represents: + * - 100 sequential accesses (distance 1.0) + * - 50 accesses with a skip distance of 2.0 + * - 10 accesses with a skip distance of 5.0 + *
    + * This information is used to optimize query execution plans by predicting + * how data will be accessed. + */ + protected static final class Access + { + /** Represents an empty access pattern. */ + final static Access EMPTY = Access.sequential(0); + + /** + * Array of expected occurrence counts for each access pattern. + * Each element represents the number of times a particular access pattern + * is expected to occur. + */ + final double[] counts; + + /** + * Array of skip distances for each access pattern. + * Each element represents the skip distance for a particular access pattern. + * A distance of 1.0 indicates sequential access. Smaller distances than 1.0 do not make sense. + */ + final double[] distances; + + /** + * The total count of expected accesses across all patterns. + * This is the sum of all elements in the counts array. + */ + final double totalCount; + + /** + * The total weighted distance of all access patterns. + * Calculated as the sum of (count * distance) for all patterns. + */ + final double totalDistance; + + /** + * Flag indicating whether to force the use of skip operations. + * When true, skip operations are used even for small distances (1.0). + */ + final boolean forceSkip; + + private Access(double[] count, double[] distance, boolean forceSkip) + { + assert count.length == distance.length; + this.counts = count; + this.distances = distance; + this.forceSkip = forceSkip; + + double totalDistance = 0.0; + double totalCount = 0.0; + for (int i = 0; i < counts.length; i++) + { + totalCount += counts[i]; + totalDistance += counts[i] * distances[i]; + } + + this.totalDistance = totalDistance; + this.totalCount = totalCount; + } + + static Access sequential(double count) + { + return new Access(new double[] { count }, new double[] { 1.0 }, false); + } + + /** Scales the counts so that the total count does not exceed given limit */ + Access limit(long limit) + { + double totalCount = 0.0; + for (int i = 0; i < counts.length; i++) + totalCount += counts[i]; + + return limit > totalCount + ? this + : this.scaleCount(limit / totalCount); + } + + /** Multiplies all counts by a constant without changing the distribution */ + Access scaleCount(double factor) + { + assert Double.isFinite(factor) : "Count multiplier must not be finite; got " + factor; + + double[] counts = Arrays.copyOf(this.counts, this.counts.length); + double[] skipDistances = Arrays.copyOf(this.distances, this.distances.length); + for (int i = 0; i < counts.length; i++) + counts[i] *= factor; + return new Access(counts, skipDistances, forceSkip); + } + + /** + * Multiplies all skip distances by a constant + * (if constant is > 1, it spreads accesses further away from each other) + */ + Access scaleDistance(double factor) + { + assert Double.isFinite(factor) : "Distance multiplier must not be finite; got " + factor; + + double[] counts = Arrays.copyOf(this.counts, this.counts.length); + double[] skipDistances = Arrays.copyOf(this.distances, this.distances.length); + for (int i = 0; i < counts.length; i++) + skipDistances[i] *= factor; + return new Access(counts, skipDistances, forceSkip); + } + + /** + * Returns a new Access pattern derived by applying a repeated access pattern to the current one, + * to represent the effect of intersecting with another predicate. That is, given "x intersect y," + * we apply `convolute` to y's Access pattern to account for the skips introduced by x. + *

    + * Example (a star denotes a single access): + *

    +         * Access.sequential(4).scaleDistance(6):
    +         * *     *     *     *
    +         * Access.sequential(4).scaleDistance(6).convolute(3, 1):
    +         * ***   ***   ***   ***
    +         * 
    + * */ + Access convolute(double count, double skipDistance) + { + assert !Double.isNaN(count) : "Count must not be NaN"; + assert !Double.isNaN(skipDistance) : "Skip distance must not be NaN"; + + if (count <= 1.0) + return scaleCount(count); + + double[] counts = Arrays.copyOf(this.counts, this.counts.length + 1); + double[] skipDistances = Arrays.copyOf(this.distances, this.distances.length + 1); + + counts[counts.length - 1] = (count - 1) * totalCount; + skipDistances[skipDistances.length - 1] = skipDistance; + + // Because we added new accesses, we need to adjust the distance of the remaining points + // in a way that the total distance stays the same: + for (int i = 0; i < skipDistances.length - 1; i++) + skipDistances[i] -= (count - 1) * skipDistance; + + return new Access(counts, skipDistances, forceSkip); + } + + /** Forces using skipTo cost even if skipping distance is not greater than 1 item */ + Access forceSkip() + { + return new Access(counts, distances, true); + } + + /** Returns the total expected number of items (rows or keys) to be retrieved from the node */ + double expectedAccessCount(double availableCount) + { + return totalCount == 0 || totalDistance <= availableCount + ? totalCount + : availableCount / totalDistance * totalCount; + } + + /** + * Computes the expected cost of fetching one item (row or key). + * This is computed as an arithmetic mean of costs of skipping by each distance, weighted by counts. + * @param nextCost the cost of fetching one item from the plan node as a function of the skip distance + * (measured in rows or keys) + */ + double unitCost(double nextCost, Function skipCostFn) + { + if (totalCount == 0) + return 0.0; // we don't want NaNs ;) + + double totalCost = 0.0; + double totalWeight = 0.0; + for (int i = 0; i < counts.length; i++) + { + double skipCost = (distances[i] > 1.0 || forceSkip) ? skipCostFn.apply(distances[i]) : 0.0; + totalCost += counts[i] * (nextCost + skipCost); + totalWeight += counts[i]; + } + return totalCost / totalWeight; + } + + public double meanDistance() + { + return totalCount > 0.0 ? totalDistance / totalCount : 0.0; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Access that = (Access) o; + return Arrays.equals(counts, that.counts) && Arrays.equals(distances, that.distances); + } + + @Override + public int hashCode() + { + return Objects.hash(Arrays.hashCode(counts), Arrays.hashCode(distances)); + } + } + + /** + * Applies given function to given object lazily, only when the result is needed. + * Caches the result for subsequent executions. + */ + static class LazyTransform + { + final T orig; + final Function transform; + private T result; + + LazyTransform(T orig, Function transform) + { + this.orig = orig; + this.transform = transform; + } + + public T get() + { + if (result == null) + result = transform.apply(orig); + return result; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java index 89682713ad5f..b0b646ab6487 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java @@ -18,25 +18,42 @@ package org.apache.cassandra.index.sai.plan; +import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.NavigableSet; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; - -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.db.CellSourceIdentifier; +import com.google.common.collect.Multimap; import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.index.FeatureNeedsIndexRebuildException; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.metrics.TableQueryMetrics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.restrictions.StatementRestrictions; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.MessageParams; +import org.apache.cassandra.db.MultiRangeReadCommand; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.PartitionRangeReadCommand; import org.apache.cassandra.db.ReadCommand; @@ -44,87 +61,146 @@ import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.rows.BaseRowIterator; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableIndex; import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.IndexSearchResultIterator; -import org.apache.cassandra.index.sai.disk.SSTableIndex; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.iterators.KeyRangeIntersectionIterator; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.v1.Segment; +import org.apache.cassandra.index.sai.disk.vector.VectorCompression; +import org.apache.cassandra.index.sai.disk.vector.VectorMemtableIndex; import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; -import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator; +import org.apache.cassandra.index.sai.iterators.KeyRangeTermIterator; import org.apache.cassandra.index.sai.memory.MemtableIndex; -import org.apache.cassandra.index.sai.utils.MergePrimaryKeyWithScoreIterator; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; import org.apache.cassandra.index.sai.utils.PrimaryKey; -import org.apache.cassandra.index.sai.utils.RowWithSource; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; +import org.apache.cassandra.index.sai.utils.RowWithSourceTable; +import org.apache.cassandra.index.sai.utils.RangeUtil; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.net.ParamType; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.InsertionOrderedNavigableSet; +import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.Throwables; -public class QueryController +import static java.lang.Math.max; +import org.apache.cassandra.config.CassandraRelevantProperties; +import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_QUERY_OPT_LEVEL; +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +public class QueryController implements Plan.Executor, Plan.CostEstimator { - // Transforms a row to include its source table, which is then used for ANN query validation. - private final static Function>> SOURCE_TABLE_ROW_TRANSFORMER = (CellSourceIdentifier sourceTable) -> new Transformation<>() - { - @Override - protected Row applyToStatic(Row row) - { - return new RowWithSource(row, sourceTable); - } - @Override - protected Row applyToRow(Row row) - { - return new RowWithSource(row, sourceTable); - } - }; + public static final String INDEX_MAY_HAVE_BEEN_DROPPED = "An index may have been dropped. " + + StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE; + public static final String INDEX_VERSION_DOES_NOT_SUPPORT_BM25 = "%s does not support BM25 scoring until it is rebuilt"; + private static final Logger logger = LoggerFactory.getLogger(QueryController.class); /** - * The maximum number of primary keys we will materialize when performing hybrid vector search. If this limit is - * exceeded, we switch to an order-by-then-filter execution path + * Controls whether we optimize query plans. + * 0 disables the optimizer. As a side effect, hybrid ANN queries will default to FilterSortOrder.SCAN_THEN_FILTER. + * 1 enables the optimizer. + * Note: the config is not final to simplify testing. */ - public static int MAX_MATERIALIZED_KEYS = CassandraRelevantProperties.SAI_VECTOR_SEARCH_MAX_MATERIALIZE_KEYS.getInt(); + @VisibleForTesting + public static int QUERY_OPT_LEVEL = SAI_QUERY_OPT_LEVEL.getInt(); - final QueryContext queryContext; + public static volatile boolean QUERY_OPT_USE_TERM_STATS = CassandraRelevantProperties.SAI_QUERY_OPTIMIZATION_USE_TERM_STATISTICS.getBoolean(); private final ColumnFamilyStore cfs; private final ReadCommand command; - private final RowFilter indexFilter; + private final Orderer orderer; + private final QueryContext queryContext; + private final TableQueryMetrics tableQueryMetrics; + private final IndexFeatureSet indexFeatureSet; private final List ranges; private final AbstractBounds mergeRange; + private final PrimaryKey.Factory keyFactory; private final PrimaryKey firstPrimaryKey; - private final PrimaryKey lastPrimaryKey; private final NavigableSet> nextClusterings; + final Plan.Factory planFactory; + + /** + * Holds the primary key iterators for indexed expressions in the query (i.e. leaves of the expression tree). + * We will construct the final iterator from those. + * We need a MultiMap because the same Expression can occur more than once in a query. + *

    + * Longer explanation why this is needed: + * In order to construct a Plan for a query, we need predicate selectivity estimates. But at the moment + * of writing this code, the only way to estimate an index predicate selectivity is to look at the posting + * list(s) in the index, by obtaining a {@link KeyRangeIterator} and callling {@link KeyRangeIterator#getMaxKeys()} on it. + * Hence, we need to create the iterators before creating the Plan. + * But later when we assemble the final key iterator according to the optimized Plan, we need those iterators + * again. In order to avoid recreating them, which would be costly, we just keep them here in this map. + */ + private final Multimap keyIterators = ArrayListMultimap.create(); + + private final Map queryViews = new HashMap<>(); + + static + { + logger.info(String.format("Query plan optimization is %s (level = %d)", + QUERY_OPT_LEVEL > 0 ? "enabled" : "disabled", + QUERY_OPT_LEVEL)); + } + + @VisibleForTesting public QueryController(ColumnFamilyStore cfs, ReadCommand command, - RowFilter indexFilter, - QueryContext queryContext) + IndexFeatureSet indexFeatureSet, + QueryContext queryContext, + TableQueryMetrics tableQueryMetrics) + { + this(cfs, command, null, indexFeatureSet, queryContext, tableQueryMetrics); + } + + public QueryController(ColumnFamilyStore cfs, + ReadCommand command, + Orderer orderer, + IndexFeatureSet indexFeatureSet, + QueryContext queryContext, + TableQueryMetrics tableQueryMetrics) { this.cfs = cfs; this.command = command; + this.orderer = orderer; this.queryContext = queryContext; - this.indexFilter = indexFilter; + this.tableQueryMetrics = tableQueryMetrics; + this.indexFeatureSet = indexFeatureSet; this.ranges = dataRanges(command); - DataRange first = ranges.get(0); - DataRange last = ranges.get(ranges.size() - 1); - this.mergeRange = ranges.size() == 1 ? first.keyRange() : first.keyRange().withNewRight(last.keyRange().right); - this.keyFactory = new PrimaryKey.Factory(cfs.getPartitioner(), cfs.getComparator()); - this.firstPrimaryKey = keyFactory.create(mergeRange.left.getToken()); - this.lastPrimaryKey = keyFactory.create(mergeRange.right.getToken()); + this.mergeRange = merge(ranges); + + this.keyFactory = PrimaryKey.factory(cfs.metadata().comparator, indexFeatureSet); + this.firstPrimaryKey = keyFactory.createTokenOnly(mergeRange.left.getToken()); this.nextClusterings = new InsertionOrderedNavigableSet<>(cfs.metadata().comparator); + var tableMetrics = new Plan.TableMetrics(estimateTotalAvailableRows(ranges), + avgCellsPerRow(), + avgRowSizeInBytes(), + cfs.getLiveSSTables().size()); + this.planFactory = new Plan.Factory(cfs.metadata.keyspace, tableMetrics, this, command.rowFilter().indexHints); } public PrimaryKey.Factory primaryKeyFactory() @@ -132,345 +208,654 @@ public PrimaryKey.Factory primaryKeyFactory() return keyFactory; } - public PrimaryKey firstPrimaryKeyInRange() + public PrimaryKey firstPrimaryKey() { return firstPrimaryKey; } - public PrimaryKey lastPrimaryKeyInRange() - { - return lastPrimaryKey; - } - public TableMetadata metadata() { return command.metadata(); } - public RowFilter indexFilter() + public ReadCommand command() { - return this.indexFilter; + return command; } - - public boolean usesStrictFiltering() + + RowFilter.FilterElement filterOperation() { - return command.rowFilter().isStrict(); + // NOTE: we cannot remove the order by filter expression here yet because it is used in the FilterTree class + // to filter out shadowed rows. + return this.command.rowFilter().root; } /** * @return token ranges used in the read command */ - public List dataRanges() + List dataRanges() { return ranges; } - public AbstractBounds mergeRange() + /** + * Note: merged range may contain subrange that no longer belongs to the local node after range movement. + * It should only be used as an optimization to reduce search space. Use {@link #dataRanges()} instead to filter data. + * + * @return merged token range + */ + AbstractBounds mergeRange() { return mergeRange; } - @Nullable - public StorageAttachedIndex indexFor(RowFilter.Expression expression) + /** + * @return indexed {@code ColumnContext} if index is found; otherwise return non-indexed {@code ColumnContext}. + */ + public IndexContext getContext(RowFilter.Expression expression) { - return cfs.indexManager.getBestIndexFor(expression, StorageAttachedIndex.class).orElse(null); + StorageAttachedIndex index = getBestIndexFor(expression); + + if (index != null) + return index.getIndexContext(); + + return new IndexContext(cfs.metadata().keyspace, + cfs.metadata().name, + cfs.metadata().id, + cfs.metadata().partitionKeyType, + cfs.metadata().comparator, + expression.column(), + determineIndexTargetType(expression), + null, + cfs); } - public boolean hasAnalyzer(RowFilter.Expression expression) + /** + * Determines the {@link IndexTarget.Type} for the expression. In this case we are only interested in map types and + * the operator being used in the expression. + */ + public static IndexTarget.Type determineIndexTargetType(RowFilter.Expression expression) { - StorageAttachedIndex index = indexFor(expression); - return index != null && index.hasAnalyzer(); + AbstractType type = expression.column().type; + IndexTarget.Type indexTargetType = IndexTarget.Type.SIMPLE; + if (type.isCollection() && type.isMultiCell()) + { + CollectionType collection = ((CollectionType) type); + if (collection.kind == CollectionType.Kind.MAP) + { + Operator operator = expression.operator(); + switch (operator) + { + case EQ: + case NEQ: + case LT: + case LTE: + case GT: + case GTE: + indexTargetType = IndexTarget.Type.KEYS_AND_VALUES; + break; + case CONTAINS: + case NOT_CONTAINS: + indexTargetType = IndexTarget.Type.VALUES; + break; + case CONTAINS_KEY: + case NOT_CONTAINS_KEY: + indexTargetType = IndexTarget.Type.KEYS; + break; + default: + throw new InvalidRequestException("Invalid operator " + operator + " for map type"); + } + } + } + return indexTargetType; } - public UnfilteredRowIterator queryStorage(List keys, ReadExecutionController executionController) + /** + * Get an iterator over the rows for this partition key. Builds a search view that includes all memtables and all + * {@link SSTableSet#LIVE} sstables. + * @param keys + * @param executionController + * @return + */ + public UnfilteredRowIterator getPartition(List keys, ReadExecutionController executionController) { + if (keys == null) + throw new IllegalArgumentException("non-null keys required"); if (keys.isEmpty()) throw new IllegalArgumentException("At least one primary key is required!"); SinglePartitionReadCommand partition = SinglePartitionReadCommand.create(cfs.metadata(), command.nowInSec(), command.columnFilter(), - RowFilter.none(), + RowFilter.NONE, DataLimits.NONE, keys.get(0).partitionKey(), makeFilter(keys)); - return partition.queryMemtableAndDisk(cfs, executionController); } /** - * Get an iterator over the row(s) for this primary key. Restrict the search to the specified view. Apply the - * {@link #SOURCE_TABLE_ROW_TRANSFORMER} so that resulting cells have the source memtable/sstable. Expect one row - * for a fully qualified primary key or all rows within a partition for a static primary key. - * - * @param key primary key to fetch from storage. - * @param executionController the executionController to use when querying storage - * @return an iterator of rows matching the query + * Get an iterator over the rows for this partition key. Restrict the search to the specified view. + * @param key + * @param executionController + * @return */ - public UnfilteredRowIterator queryStorage(PrimaryKey key, ColumnFamilyStore.ViewFragment view, ReadExecutionController executionController) + public UnfilteredRowIterator getPartition(PrimaryKey key, ColumnFamilyStore.ViewFragment view, ReadExecutionController executionController) { if (key == null) throw new IllegalArgumentException("non-null key required"); - SinglePartitionReadCommand partition = SinglePartitionReadCommand.create(cfs.metadata(), - command.nowInSec(), - command.columnFilter(), - RowFilter.none(), - DataLimits.NONE, - key.partitionKey(), - makeFilter(List.of(key))); + SinglePartitionReadCommand partition = getPartitionReadCommand(key, executionController); + + // Class to transform the row to include its source table. + Function>> rowTransformer = (Object sourceTable) -> new Transformation<>() + { + @Override + protected Row applyToStatic(Row row) + { + return new RowWithSourceTable(row, sourceTable); + } + + @Override + protected Row applyToRow(Row row) + { + return new RowWithSourceTable(row, sourceTable); + } + }; - return partition.queryMemtableAndDisk(cfs, view, SOURCE_TABLE_ROW_TRANSFORMER, executionController); + return partition.queryMemtableAndDisk(cfs, view, rowTransformer, executionController); } - /** - * Build a {@link KeyRangeIterator.Builder} from the given list of {@link Expression}s. - *

    - * This is achieved by creating an on-disk view of the query that maps the expressions to - * the {@link SSTableIndex}s that will satisfy the expression. - *

    - * Each {@link QueryViewBuilder.QueryExpressionView} is then passed to - * {@link IndexSearchResultIterator#build(QueryViewBuilder.QueryExpressionView, AbstractBounds, QueryContext, boolean, Runnable)} - * to search the in-memory indexes associated with the expression and the SSTable indexes, the results of - * which are unioned and returned. - *

    - * The results from each call to {@link IndexSearchResultIterator#build(QueryViewBuilder.QueryExpressionView, AbstractBounds, QueryContext, boolean, Runnable)} - * are added to a {@link KeyRangeIntersectionIterator} and returned if strict filtering is allowed. - *

    - * If strict filtering is not allowed, indexes are split into two groups according to the repaired status of their - * backing SSTables. Results from searches over the repaired group are added to a - * {@link KeyRangeIntersectionIterator}, which is then added, along with results from searches on the unrepaired - * set, to a top-level {@link KeyRangeUnionIterator}, and returned. This is done to ensure that AND queries do not - * prematurely filter out matches on un-repaired partial updates. Post-filtering must also take this into - * account. (see {@link FilterTree#isSatisfiedBy(DecoratedKey, Row, Row)}) Note that Memtable-attached - * indexes are treated as part of the unrepaired set. - */ - public KeyRangeIterator.Builder getIndexQueryResults(Collection expressions) + public SinglePartitionReadCommand getPartitionReadCommand(PrimaryKey key, ReadExecutionController executionController) + { + if (key == null) + throw new IllegalArgumentException("non-null key required"); + + return SinglePartitionReadCommand.create(cfs.metadata(), + command.nowInSec(), + command.columnFilter(), + RowFilter.none(), + DataLimits.NONE, + key.partitionKey(), + makeFilter(key)); + } + + private void updateIndexMetricsQueriesCount(Plan plan) { - // VSTODO move ANN out of expressions and into its own abstraction? That will help get generic ORDER BY support - expressions = expressions.stream().filter(e -> e.getIndexOperator() != Expression.IndexOperator.ANN).collect(Collectors.toList()); + HashSet queriedIndexesContexts = new HashSet<>(); + plan.visitIndexesRecursive(queriedIndexesContexts::add); + queriedIndexesContexts.forEach(indexContext -> + indexContext.getIndexMetrics() + .ifPresent(m -> m.queriesCount.inc())); + } - QueryViewBuilder.QueryView queryView = new QueryViewBuilder(expressions, mergeRange).build(); - KeyRangeIterator.Builder builder = command.rowFilter().isStrict() - ? KeyRangeIntersectionIterator.builder(expressions.size(), queryView::close) - : KeyRangeUnionIterator.builder(expressions.size(), queryView::close); + Plan buildPlan() + { + Plan.KeysIteration keysIterationPlan = buildKeysIterationPlan(); + Plan.RowsIteration rowsIteration = planFactory.fetch(keysIterationPlan); - try + rowsIteration = planFactory.recheckFilter(command.rowFilter().withoutOrderingExpressions(), rowsIteration); + rowsIteration = planFactory.limit(rowsIteration, command.limits().rows()); + + // Limit the number of intersected clauses before optimizing so we reduce the size of the + // plan given to the optimizer and hence reduce the plan search space and speed up optimization. + // It is possible that some index operators like ':' expand to a huge number of MATCH predicates + // (see CNDB-10085) and could overload the optimizer. + // The intersected subplans are ordered by selectivity in the way the best ones are at the beginning + // of the list, therefore this limit is unlikely to remove good branches of the tree. + // The limit here is higher than the final limit, so that the optimizer has a bit more freedom + // in which predicates it leaves in the plan and the probability of accidentally removing a good branch + // here is even lower. + int intersectionClauseLimit = CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.getInt(); + Plan.RowsIteration origPlan = rowsIteration.limitIntersectedClauses(intersectionClauseLimit * 3); + Plan.RowsIteration plan = origPlan; + + if (QUERY_OPT_LEVEL > 0) + plan = origPlan.optimize(); + + plan = plan.limitIntersectedClauses(intersectionClauseLimit); + queryContext.recordQueryPlan(origPlan, plan); + updateIndexMetricsQueriesCount(plan); + + if (logger.isTraceEnabled()) + logger.trace("Query execution plan:\n" + plan.toStringRecursive(Redaction.REDACT)); + + if (Tracing.isTracing()) { - maybeTriggerGuardrails(queryView); + Tracing.trace("Query execution plan:\n" + plan.toStringRecursive(Redaction.NONE)); + List origIndexScans = keysIterationPlan.nodesOfType(Plan.IndexScan.class); + List selectedIndexScans = plan.nodesOfType(Plan.IndexScan.class); + Tracing.trace("Selecting {} {} of {} out of {} indexes", + selectedIndexScans.size(), + selectedIndexScans.size() > 1 ? "indexes with cardinalities" : "index with cardinality", + selectedIndexScans.stream().map(s -> "" + ((long) s.expectedKeys())).collect(Collectors.joining(", ")), + origIndexScans.size()); + } + return plan; + } - if (command.rowFilter().isStrict()) - { - // If strict filtering is enabled, evaluate indexes for both repaired and un-repaired SSTables together. - // This usually means we are making this local index query in the context of a user query that reads - // from a single replica and thus can safely perform local intersections. - for (QueryViewBuilder.QueryExpressionView queryExpressionView : queryView.view) - builder.add(IndexSearchResultIterator.build(queryExpressionView, mergeRange, queryContext, true, () -> {})); - } - else - { - KeyRangeIterator.Builder repairedBuilder = KeyRangeIntersectionIterator.builder(expressions.size(), () -> {}); + private Plan.KeysIteration buildKeysIterationPlan() + { + // Remove the ORDER BY filter expression from the filter tree, as it is added below. + var filterElement = filterOperation().filter(e -> !Orderer.isFilterExpressionOrderer(e)); + Plan.KeysIteration keysIterationPlan = Operation.Node.buildTree(this, filterElement) + .analyzeTree(this) + .plan(this); - for (QueryViewBuilder.QueryExpressionView queryExpressionView : queryView.view) - { - Expression expression = queryExpressionView.expression; - // The initial sizes here reflect little more than an effort to avoid resizing for - // partition-restricted searches w/ LCS: - List repaired = new ArrayList<>(5); - List unrepaired = new ArrayList<>(5); - - // Split SSTable indexes into repaired and un-reparired: - for (SSTableIndex index : queryExpressionView.sstableIndexes) - if (index.getSSTable().isRepaired()) - repaired.add(index); - else - unrepaired.add(index); - - // Always build an iterator for the un-repaired set, given this must include Memtable indexes... - IndexSearchResultIterator unrepairedIterator = - IndexSearchResultIterator.build(expression, queryExpressionView.memtableIndexes, unrepaired, mergeRange, queryContext, true, () -> {}); - - // ...but ignore it if our combined results are empty. - if (unrepairedIterator.getMaxKeys() > 0) - { - builder.add(unrepairedIterator); - queryContext.hasUnrepairedMatches = true; - } - else - { - // We're not going to use this, so release the resources it holds. - unrepairedIterator.close(); - } - - // ...then only add an iterator to the repaired intersection if repaired SSTable indexes exist. - if (!repaired.isEmpty()) - repairedBuilder.add(IndexSearchResultIterator.build(expression, Collections.emptyList(), repaired, mergeRange, queryContext, false, () -> {})); - } + // Because the orderer has a specific queue view + if (orderer != null) + keysIterationPlan = planFactory.sort(keysIterationPlan, orderer); - if (repairedBuilder.rangeCount() > 0) - builder.add(repairedBuilder.build()); - } + // This would mean we have no WHERE nor ANN clauses at all; this can happen in case an index was dropped after the + // query was initiated + if (keysIterationPlan == planFactory.everything) + throw invalidRequest(INDEX_MAY_HAVE_BEEN_DROPPED); + + return keysIterationPlan; + } + + public Iterator buildIterator(Plan plan) + { + try + { + Plan.KeysIteration keysIteration = plan.firstNodeOfType(Plan.KeysIteration.class); + assert keysIteration != null : "No index scan found"; + return keysIteration.execute(this); } - catch (Throwable t) + finally { - // all sstable indexes in view have been referenced, need to clean up when exception is thrown - builder.cleanup(); - throw t; + // Because we optimize the plan, it is possible that there exist iterators that we + // constructed but which weren't used by the final plan. + // Let's close them here, so they don't hold the resources. + closeUnusedIterators(); } - return builder; } - void maybeTriggerGuardrails(QueryViewBuilder.QueryView queryView) + /** + * Creates an iterator over keys of rows that match the given WHERE predicate. + * Does not cache the iterator! + */ + private KeyRangeIterator buildIterator(Expression predicate) + { + QueryView view = getQueryView(predicate.context); + return KeyRangeTermIterator.build(predicate, view, mergeRange, queryContext, false); + } + + /** + * Creates a consistent view of indexes. + * Invocations are memorized - multiple calls for the same context return the same view. + * The views are kept for the lifetime of this {@code QueryController}. + */ + QueryView getQueryView(IndexContext context) throws QueryView.Builder.MissingIndexException { - int referencedIndexes = 0; + return queryViews.computeIfAbsent(context, + c -> new QueryView.Builder(c, mergeRange).build()); + } - // We want to make sure that no individual column expression touches too many SSTable-attached indexes: - for (QueryViewBuilder.QueryExpressionView expressionSSTables : queryView.view) - referencedIndexes = Math.max(referencedIndexes, expressionSSTables.sstableIndexes.size()); + private float avgCellsPerRow() + { + long cells = 0; + long rows = 0; + for (SSTableReader sstable : cfs.getLiveSSTables()) + { + rows += sstable.getTotalRows(); + cells += sstable.getEstimatedCellPerPartitionCount().mean() * sstable.getEstimatedCellPerPartitionCount().count(); + } + return rows == 0 ? 0.0f : ((float) cells) / rows; + } - if (Guardrails.saiSSTableIndexesPerQuery.failsOn(referencedIndexes, null)) + private float avgRowSizeInBytes() + { + long totalLength = 0; + long rows = 0; + for (SSTableReader sstable : cfs.getLiveSSTables()) { - String msg = String.format("Query %s attempted to read from too many indexes (%s) but max allowed is %s; " + - "query aborted (see sai_sstable_indexes_per_query_fail_threshold)", - command.toCQLString(), - referencedIndexes, - Guardrails.CONFIG_PROVIDER.getOrCreate(null).getSaiSSTableIndexesPerQueryFailThreshold()); - Tracing.trace(msg); - MessageParams.add(ParamType.TOO_MANY_REFERENCED_INDEXES_FAIL, referencedIndexes); - throw new QueryReferencingTooManyIndexesException(msg); + rows += sstable.getTotalRows(); + totalLength += sstable.uncompressedLength(); } - else if (Guardrails.saiSSTableIndexesPerQuery.warnsOn(referencedIndexes, null)) + return rows == 0 ? 0.0f : ((float) totalLength) / rows; + } + + public FilterTree buildFilter() + { + return Operation.Node.buildTree(this, filterOperation()).analyzeTree(this).filterTree(); + } + + private Plan.KeysIteration buildHalfRangeFromInequality(Expression originPredicate, Operator op) + { + assert originPredicate.getOp() == Expression.Op.NOT_EQ : "assumes inequality"; + assert originPredicate.lower.value == originPredicate.upper.value : "assumes lower and upper are the same in inequality"; + + Expression halfRange = new Expression(originPredicate.context); + halfRange.add(op, originPredicate.lower.value.raw); + long matchingRowCount = Math.min(estimateMatchingRowCount(halfRange), planFactory.tableMetrics.rows); + return planFactory.indexScan(halfRange, matchingRowCount); + } + + /** + * Builds a plan for a restriction with inequality. It's implemented as + * union of two ranges, before the value and after the value. + * If the column type is truncatable, e.g., BigInteger or BigDecimal, + * then it returns a full index scan, since the ranges might result + * in false negatives when a truncated value is equivalent to + * the value to exclude. + * @param predicate Inequality expression with indexContext + * @return A plan on the index, which can also result false positives. + */ + private Plan.KeysIteration buildInequalityPlan(Expression predicate) + { + assert predicate.getOp()== Expression.Op.NOT_EQ : "Only inequality predicate is expected"; + + if (TypeUtil.supportsRounding(predicate.validator)) + return planFactory.fullIndexScan(predicate.context); + else { - MessageParams.add(ParamType.TOO_MANY_REFERENCED_INDEXES_WARN, referencedIndexes); + Plan.KeysIteration left = buildHalfRangeFromInequality(predicate, Operator.LT); + Plan.KeysIteration right = buildHalfRangeFromInequality(predicate, Operator.GT); + return planFactory.union(new ArrayList<>(Arrays.asList(left, right)), true); } } /** - * Returns whether this query is not selecting the {@link PrimaryKey}. - * The query does not select the key if both of the following statements are false: - * 1. The table associated with the query is not using clustering keys - * 2. The clustering index filter for the command wants the row. - *

    - * Item 2 is important in paged queries where the {@link org.apache.cassandra.db.filter.ClusteringIndexSliceFilter} for - * subsequent paged queries may not select rows that are returned by the index - * search because that is initially partition based. + * Build a {@link Plan} from the given list of expressions by applying given operation (OR/AND). + * Building of such builder involves index search, results of which are persisted in the internal resources list * - * @param key The {@link PrimaryKey} to be tested - * @return true if the key is not selected by the query + * @param builder The plan node builder which receives the built index scans + * @param expressions The expressions to build the plan from + */ + void buildPlanForExpressions(Plan.Builder builder, Collection expressions) + { + Operation.OperationType op = builder.type; + assert !expressions.isEmpty() : "expressions should not be empty for " + op + " in " + command.rowFilter().root; + + assert !expressions.stream().anyMatch(e -> e.operation == Expression.Op.ORDER_BY); + + // we cannot use indexes with OR if we have a mix of indexed and non-indexed columns (see CNDB-10142) + if (op == Operation.OperationType.OR && !expressions.stream().allMatch(e -> e.context.isIndexed())) + { + builder.add(planFactory.everything); + return; + } + + IndexHints hints = command.rowFilter().indexHints; + + for (Expression expression : expressions) + { + if (expression.context.isIndexed()) + { + // Skip the expressions using indexes that are excluded by the user-provided hints + if (hints.excludes(expression.context.getIndexName())) + continue; + + if (expression.getOp() == Expression.Op.NOT_EQ) + builder.add(buildInequalityPlan(expression)); + else + { + long expectedMatchingRowCount = Math.min(estimateMatchingRowCount(expression), planFactory.tableMetrics.rows); + builder.add(planFactory.indexScan(expression, expectedMatchingRowCount)); + } + } + } + } + + @Override + public Iterator getKeysFromIndex(Expression predicate) + { + Collection rangeIterators = keyIterators.get(predicate); + // This will be non-empty only if we created the iterator as part of the query planning process. + if (!rangeIterators.isEmpty()) + { + KeyRangeIterator iterator = rangeIterators.iterator().next(); + keyIterators.remove(predicate, iterator); // remove so we never accidentally reuse the same iterator + return iterator; + } + + return buildIterator(predicate); + } + + /** + * Use the configured {@link Orderer} to create an iterator that sorts the whole table by a specific column. */ - public boolean doesNotSelect(PrimaryKey key) + @Override + public CloseableIterator getTopKRows(Expression predicate, int softLimit) { - return key.kind() == PrimaryKey.Kind.WIDE && !command.clusteringIndexFilter(key.partitionKey()).selects(key.clustering()); + // Only the disk format limits the features of the index, but we also fail for in memory indexes because they + // will fail when flushed. + if (orderer.isBM25() && !orderer.context.version().onOrAfter(Version.BM25_EARLIEST)) + { + throw new FeatureNeedsIndexRebuildException(String.format(INDEX_VERSION_DOES_NOT_SUPPORT_BM25, + orderer.context.getIndexName())); + } + + MemtableSearcher memtableSearcher = index -> index.orderBy(queryContext, orderer, predicate, mergeRange, softLimit); + SSTableSearcher ssTableSearcher = (index, totalRows) -> index.orderBy(orderer, predicate, mergeRange, queryContext, softLimit, totalRows); + return searchTopKRows(memtableSearcher, ssTableSearcher); } - // This is an ANN only query - public CloseableIterator getTopKRows(QueryViewBuilder.QueryExpressionView queryExpressionView) + /** + * Use the configured {@link Orderer} to sort the rows from the given source iterator. + */ + public CloseableIterator getTopKRows(KeyRangeIterator source, int softLimit) { - assert queryExpressionView.expression.operator == Expression.IndexOperator.ANN; - List> intermediateResults = new ArrayList<>(); try { - for (MemtableIndex memtableIndex : queryExpressionView.memtableIndexes) - intermediateResults.add(memtableIndex.orderBy(queryContext, queryExpressionView.expression, mergeRange)); - for (SSTableIndex sstableIndex : queryExpressionView.sstableIndexes) - intermediateResults.addAll(sstableIndex.orderBy(queryExpressionView.expression, mergeRange, queryContext)); - return intermediateResults.isEmpty() ? CloseableIterator.empty() - : new MergePrimaryKeyWithScoreIterator(intermediateResults); + var primaryKeys = materializeKeys(source); + if (primaryKeys.isEmpty()) + { + FileUtils.closeQuietly(source); + return CloseableIterator.emptyIterator(); + } + var result = getTopKRows(primaryKeys, softLimit); + // We cannot close the source iterator eagerly because it produces partially loaded PrimaryKeys + // that might not be needed until a deeper search into the ordering index, which happens after + // we exit this block. + return CloseableIterator.withOnClose(result, source); } catch (Throwable t) { - // all sstable indexes in view have been referenced, need to clean up when exception is thrown - queryExpressionView.sstableIndexes.forEach(SSTableIndex::releaseQuietly); - intermediateResults.forEach(FileUtils::closeQuietly); - throw Throwables.cleaned(t); + FileUtils.closeQuietly(source); + throw t; + } + } + + /** + * Materialize the keys from the given source iterator. If there is a meaningful {@link #mergeRange}, the keys + * are filtered to only include those within the range. Note: does not close the source iterator. + * @param source The source iterator to materialize keys from. + * @return The list of materialized keys within the {@link #mergeRange}. + */ + private List materializeKeys(KeyRangeIterator source) + { + // Skip to the first key (which is really just a token) in the range if it is not the minimum token + if (!mergeRange.left.isMinimum()) + source.skipTo(firstPrimaryKey); + + if (!source.hasNext()) + return List.of(); + + var maxToken = primaryKeyFactory().createTokenOnly(mergeRange.right.getToken()); + var hasLimitingMaxToken = !maxToken.token().isMinimum() && maxToken.compareTo(source.getMaximum()) < 0; + List primaryKeys = new ArrayList<>(); + while (source.hasNext()) + { + var next = source.next(); + if (hasLimitingMaxToken && next.compareTo(maxToken) > 0) + break; + primaryKeys.add(next); } + return primaryKeys; } - // This is a hybrid query. We apply all other predicates before ordering and limiting. - public CloseableIterator getTopKRows(KeyRangeIterator source, QueryViewBuilder.QueryExpressionView queryExpressionView) + private CloseableIterator getTopKRows(List sourceKeys, int softLimit) { - List primaryKeys = materializeKeysAndCloseSource(source); - if (primaryKeys == null) - return getTopKRows(queryExpressionView); - if (primaryKeys.isEmpty()) - return CloseableIterator.empty(); - return getTopKRows(primaryKeys, queryExpressionView); + Tracing.logAndTrace(logger, "SAI predicates produced {} keys", sourceKeys.size()); + + MemtableSearcher memtableSearcher = index -> List.of(index.orderResultsBy(queryContext, + sourceKeys, + orderer, + softLimit)); + SSTableSearcher ssTableSearcher = (index, totalRows) -> index.orderResultsBy(queryContext, + sourceKeys, + orderer, + softLimit, + totalRows); + return searchTopKRows(memtableSearcher, ssTableSearcher); } - private CloseableIterator getTopKRows(List sourceKeys, QueryViewBuilder.QueryExpressionView queryExpressionView) + private CloseableIterator searchTopKRows(MemtableSearcher memtableSearcher, SSTableSearcher ssTableSearcher) { - List> intermediateResults = new ArrayList<>(); + List> memtableResults = new ArrayList<>(); try { - for (MemtableIndex memtableIndex : queryExpressionView.memtableIndexes) - intermediateResults.add(memtableIndex.orderResultsBy(queryContext, sourceKeys, queryExpressionView.expression)); - for (SSTableIndex sstableIndex : queryExpressionView.sstableIndexes) - intermediateResults.addAll(sstableIndex.orderResultsBy(queryContext, sourceKeys, queryExpressionView.expression)); - return intermediateResults.isEmpty() ? CloseableIterator.empty() - : new MergePrimaryKeyWithScoreIterator(intermediateResults); + QueryView view = getQueryView(orderer.context); + if (orderer.isBM25()) + { + // Pre-calculate term expressions + List> termAndExpressions = new ArrayList<>(); + for (ByteBuffer term : orderer.getQueryTerms()) + { + Expression termExpression = new Expression(orderer.context) + .add(Operator.ANALYZER_MATCHES, term); + termAndExpressions.add(Pair.create(term, termExpression)); + } + + for (MemtableIndex index : view.memtableIndexes) + orderer.bm25stats.add(index.getRowCount(), + index.getApproximateTermCount(), + termAndExpressions, + termExpression -> index.estimateMatchingRowsCount(termExpression)); + for (SSTableIndex index : view.sstableIndexes) + orderer.bm25stats.add(index.getRowCount(), + index.getApproximateTermCount(), + termAndExpressions, + termExpression -> index.getMatchingRowsCount(termExpression, mergeRange, queryContext)); + // No documents indexed, the iterator will be empty + if (orderer.bm25stats.getDocCount() == 0) + return CloseableIterator.emptyIterator(); + } + + for (MemtableIndex index : view.memtableIndexes) + memtableResults.addAll(memtableSearcher.search(index)); + List> sstableScoredPrimaryKeyIterators = searchSSTables(view, ssTableSearcher); + sstableScoredPrimaryKeyIterators.addAll(memtableResults); + return MergeIterator.getNonReducingCloseable(sstableScoredPrimaryKeyIterators, orderer.getComparator()); + } + catch (QueryView.Builder.MissingIndexException e) + { + if (orderer.context.isDropped()) + throw invalidRequest(TopKProcessor.INDEX_MAY_HAVE_BEEN_DROPPED); + else + throw new IllegalStateException("Index not found but hasn't been dropped", e); } catch (Throwable t) { - // all sstable indexes in view have been referenced, need to clean up when exception is thrown - queryExpressionView.sstableIndexes.forEach(SSTableIndex::releaseQuietly); - intermediateResults.forEach(FileUtils::closeQuietly); - throw Throwables.cleaned(t); + if (!memtableResults.isEmpty()) + FileUtils.closeQuietly(memtableResults); + throw t; } } + @FunctionalInterface + interface SSTableSearcher + { + List> search(SSTableIndex index, long totalRows) throws Exception; + } + + @FunctionalInterface + interface MemtableSearcher + { + List> search(MemtableIndex index); + } /** - * Materialize the keys from the given source iterator. If there is a meaningful {@link #mergeRange}, the keys - * are filtered to only include those within the range. Note: closes the source iterator. - * @param source The source iterator to fully consume by materializing its keys - * @return The list of materialized keys within the {@link #mergeRange}, or return null if source exceeded the - * materialized keys limit. + * Create the list of iterators over {@link PrimaryKeyWithSortKey} from the given {@link QueryView}. + * @param queryView The view to use to create the iterators. + * @return The list of iterators over {@link PrimaryKeyWithSortKey}. */ - private List materializeKeysAndCloseSource(KeyRangeIterator source) + private List> searchSSTables(QueryView queryView, SSTableSearcher searcher) { - try (source) + List> results = new ArrayList<>(); + long totalRows = queryView.getTotalSStableRows(); + for (var index : queryView.sstableIndexes) { - // Skip to the first key (which is really just a token) in the range if it is not the minimum token - if (!mergeRange.left.isMinimum()) - source.skipTo(firstPrimaryKey); - - if (!source.hasNext()) - return List.of(); - - PrimaryKey maxToken = keyFactory.create(mergeRange.right.getToken()); - boolean hasLimitingMaxToken = !maxToken.token().isMinimum() && maxToken.compareTo(source.getMaximum()) < 0; - List primaryKeys = new ArrayList<>(); - int count = 0; - while (source.hasNext()) + try { - PrimaryKey next = source.next(); - if (hasLimitingMaxToken && next.compareTo(maxToken) > 0) - break; - primaryKeys.add(next); - if (MAX_MATERIALIZED_KEYS < ++count) + var iterators = searcher.search(index, totalRows); + results.addAll(iterators); + } + catch (Throwable ex) + { + // Close any iterators that were successfully opened before the exception + FileUtils.closeQuietly(results); + if (logger.isDebugEnabled() && !(ex instanceof AbortedOperationException)) { - Tracing.trace("WHERE clause generated more than {} rows. Switching to ORDER BY then post filter.", MAX_MATERIALIZED_KEYS); - return null; + var msg = String.format("Failed search on index %s, aborting query.", index.getSSTable()); + logger.debug(index.getIndexContext().logMessage(msg), ex); } + throw Throwables.cleaned(ex); } - return primaryKeys; } + return results; + } + + public IndexFeatureSet indexFeatureSet() + { + return indexFeatureSet; + } + + public Orderer getOrderer() + { + return orderer; + } + + /** + * Returns whether this query is selecting the {@link PrimaryKey}. + * The query selects the key if the clustering index filter for the command wants the row. + * If the key has no clustering information, it is always selected. + *

    + * Checking the clustering index filter is important in paged queries where the {@link ClusteringIndexSliceFilter} + * for subsequent paged queries may not select rows that are returned by the index search because that is + * initially partition based. + * + * @param key The {@link PrimaryKey} to be tested + * @return true if the key is selected by the query + */ + public boolean selects(PrimaryKey key) + { + return !key.hasClustering() || + command.clusteringIndexFilter(key.partitionKey()).selects(key.clustering()); + } + + @Nullable + private StorageAttachedIndex getBestIndexFor(RowFilter.Expression expression) + { + return cfs.indexManager.getBestIndexFor(expression, command.rowFilter().indexHints, StorageAttachedIndex.class) + .orElse(null); } // Note: This method assumes that the selects method has already been called for the // key to avoid having to (potentially) call selects twice + private ClusteringIndexFilter makeFilter(PrimaryKey key) + { + ClusteringIndexFilter clusteringIndexFilter = command.clusteringIndexFilter(key.partitionKey()); + + if (!indexFeatureSet.isRowAware() || !key.hasClustering()) + return clusteringIndexFilter; + else + return new ClusteringIndexNamesFilter(FBUtilities.singleton(key.clustering(), cfs.metadata().comparator), + clusteringIndexFilter.isReversed()); + } + private ClusteringIndexFilter makeFilter(List keys) { PrimaryKey firstKey = keys.get(0); - assert cfs.metadata().comparator.size() == 0 && !firstKey.kind().hasClustering || - cfs.metadata().comparator.size() > 0 && firstKey.kind().hasClustering : - "PrimaryKey " + firstKey + " clustering does not match table. There should be a clustering of size " + cfs.metadata().comparator.size(); + assert !indexFeatureSet.isRowAware() || + cfs.metadata().comparator.size() == 0 && !firstKey.hasClustering() || + cfs.metadata().comparator.size() > 0 && (firstKey.hasClustering() || cfs.metadata().hasStaticColumns()) : + "PrimaryKey " + firstKey + " clustering does not match table. There should be a clustering of size " + cfs.metadata().comparator.size(); ClusteringIndexFilter clusteringIndexFilter = command.clusteringIndexFilter(firstKey.partitionKey()); - - // If we have skinny partitions or the key is for a static row then we need to get the partition as - // requested by the original query. - if (cfs.metadata().comparator.size() == 0 || firstKey.kind() == PrimaryKey.Kind.STATIC) + if (cfs.metadata().comparator.size() == 0 || !firstKey.hasClustering()) { return clusteringIndexFilter; } @@ -483,6 +868,53 @@ private ClusteringIndexFilter makeFilter(List keys) } } + /** + * Used to release all resources and record metrics when query finishes. + */ + public void finish() + { + closeUnusedIterators(); + closeQueryViews(); + if (tableQueryMetrics != null) + tableQueryMetrics.record(queryContext, command); + } + + /** + * Releases all resources and does not record the metrics. + */ + public void abort() + { + closeUnusedIterators(); + closeQueryViews(); + } + + private void closeUnusedIterators() + { + Iterator> entries = keyIterators.entries().iterator(); + while (entries.hasNext()) + { + FileUtils.closeQuietly(entries.next().getValue()); + entries.remove(); + } + } + + /** + * Try to reference all SSTableIndexes before querying on disk indexes. + * + * If we attempt to proceed into {@link KeyRangeTermIterator#build(Expression, Set, AbstractBounds, QueryContext, boolean, int)} + * without first referencing all indexes, a concurrent compaction may decrement one or more of their backing + * SSTable {@link Ref} instances. This will allow the {@link SSTableIndex} itself to be released and will fail the query. + */ + private void closeQueryViews() + { + Iterator> entries = queryViews.entrySet().iterator(); + while (entries.hasNext()) + { + entries.next().getValue().close(); + entries.remove(); + } + } + /** * Returns the {@link DataRange} list covered by the specified {@link ReadCommand}. * @@ -493,15 +925,143 @@ private static List dataRanges(ReadCommand command) { if (command instanceof SinglePartitionReadCommand) { - return Lists.newArrayList(command.dataRange()); + SinglePartitionReadCommand cmd = (SinglePartitionReadCommand) command; + DecoratedKey key = cmd.partitionKey(); + return Lists.newArrayList(new DataRange(new Bounds<>(key, key), cmd.clusteringIndexFilter())); } else if (command instanceof PartitionRangeReadCommand) { return Lists.newArrayList(command.dataRange()); } + else if (command instanceof MultiRangeReadCommand) + { + MultiRangeReadCommand cmd = (MultiRangeReadCommand) command; + return cmd.ranges(); + } else { throw new AssertionError("Unsupported read command type: " + command.getClass().getName()); } } + + /** + * Returns the total count of rows in the sstables which overlap with any of the given ranges + * and the rows in the memtables restricted to the queried token ranges. Token ranges are + * approximated to full shards, according to the way how TrieMemtableIndex shards the indexes. + */ + private long estimateTotalAvailableRows(List ranges) + { + long rows = 0; + for (Memtable memtable : cfs.getAllMemtables()) + rows += Memtable.estimateRowCount(memtable); + + for (SSTableReader sstable : cfs.getLiveSSTables()) + for (DataRange range : ranges) + if (RangeUtil.intersects(sstable, range.keyRange())) + rows += sstable.getTotalRows(); + + return rows; + } + + private static AbstractBounds merge(List ranges) + { + DataRange first = ranges.get(0); + DataRange last = ranges.get(ranges.size() - 1); + return ranges.size() == 1 ? first.keyRange() : first.keyRange().withNewRight(last.keyRange().right); + } + + /** + * Estimates how many rows match the predicate. + * There are no guarantees. The returned value may come with a significant estimation error. + * You must not rely on this except for query optimization purposes. + */ + private long estimateMatchingRowCount(Expression predicate) + { + switch (predicate.getOp()) + { + case EQ: + case MATCH: + case CONTAINS_KEY: + case CONTAINS_VALUE: + case NOT_EQ: + case NOT_CONTAINS_KEY: + case NOT_CONTAINS_VALUE: + case RANGE: + return (indexFeatureSet.hasTermsHistogram() && QUERY_OPT_USE_TERM_STATS) + ? estimateMatchingRowCountUsingHistograms(predicate) + : estimateMatchingRowCountUsingIndex(predicate); + default: + return estimateMatchingRowCountUsingIndex(predicate); + } + } + + /** + * Estimates the number of matching rows by consulting the terms histograms on the indexes. + * This is faster but the histograms are not available on indexes before V6. + */ + private long estimateMatchingRowCountUsingHistograms(Expression predicate) + { + assert indexFeatureSet.hasTermsHistogram(); + var queryView = getQueryView(predicate.context); + + long rowCount = 0; + for (MemtableIndex index : queryView.memtableIndexes) + rowCount += index.estimateMatchingRowsCount(predicate); + + for (SSTableIndex index : queryView.sstableIndexes) + rowCount += index.estimateMatchingRowsCount(predicate); + + return rowCount; + } + + /** + * Legacy way of estimating predicate selectivity. + * Runs the search on the index and returns the size of the iterator. + * Caches the iterator for future use to avoid doing search twice. + */ + private long estimateMatchingRowCountUsingIndex(Expression predicate) + { + // For older indexes we don't have histograms, so we need to construct the iterator + // and ask for the posting list size. + KeyRangeIterator iterator = buildIterator(predicate); + + // We're not going to consume the iterator here, so memorize it for future uses. + // It can be used when executing the plan. + keyIterators.put(predicate, iterator); + return iterator.getMaxKeys(); + } + + @Override + public double estimateAnnSearchCost(Orderer orderer, int limit, long candidates) + { + Preconditions.checkArgument(limit > 0, "limit must be > 0"); + + QueryView queryView = getQueryView(orderer.context); + + int memoryRerankK = orderer.rerankKFor(limit, VectorCompression.NO_COMPRESSION); + double cost = 0; + for (MemtableIndex index : queryView.memtableIndexes) + { + // FIXME convert nodes visited to search cost + int memtableCandidates = (int) Math.min(Integer.MAX_VALUE, candidates); + cost += ((VectorMemtableIndex) index).estimateAnnNodesVisited(memoryRerankK, memtableCandidates); + } + + long totalRows = 0; + for (SSTableIndex index : queryView.sstableIndexes) + totalRows += index.getSSTable().getTotalRows(); + + for (SSTableIndex index : queryView.sstableIndexes) + { + for (Segment segment : index.getSegments()) + { + if (!segment.intersects(mergeRange)) + continue; + int segmentLimit = segment.proportionalAnnLimit(limit, totalRows); + int segmentCandidates = max(1, (int) (candidates * (double) segment.metadata.numRows / totalRows)); + cost += segment.estimateAnnSearchCost(orderer, segmentLimit, segmentCandidates); + } + } + return cost; + } } diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryMonitorableExecutionInfo.java b/src/java/org/apache/cassandra/index/sai/plan/QueryMonitorableExecutionInfo.java new file mode 100644 index 000000000000..b7a52d8ea577 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryMonitorableExecutionInfo.java @@ -0,0 +1,105 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.plan; + +import java.util.function.Supplier; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.marshal.Redaction; +import org.apache.cassandra.db.monitoring.Monitorable; +import org.apache.cassandra.index.sai.QueryContext; + +/** + * {@link Monitorable.ExecutionInfo} implementation for SAI queries. + * It holds and prints the metrics from the {@link QueryContext} of the monitorized queries, and its {@link Plan}. + */ +public class QueryMonitorableExecutionInfo implements Monitorable.ExecutionInfo +{ + private final QueryContext.Snapshot metrics; + private final String plan; + + /** + * Builds a new execution info object for a query. + * + * @param metrics a snapshot of the query context metrics + * @param plan the query plan + */ + private QueryMonitorableExecutionInfo(QueryContext.Snapshot metrics, String plan) + { + this.metrics = metrics; + this.plan = plan; + } + + /** + * Returns a supplier of {@link Monitorable.ExecutionInfo} for a query, to be used when logging slow queries. + * + * @param context the query context + * @param plan the query plan + * @return a supplier of {@link Monitorable.ExecutionInfo} for a query + */ + public static Supplier supplier(QueryContext context, Plan plan) + { + if (!CassandraRelevantProperties.SAI_MONITORING_EXECUTION_INFO_ENABLED.getBoolean()) + return Monitorable.ExecutionInfo.EMPTY_SUPPLIER; + + String planAsString = toLogString(plan); + return () -> new QueryMonitorableExecutionInfo(context.snapshot(), planAsString); + } + + @Override + public String toLogString(boolean unique) + { + StringBuilder sb = new StringBuilder("\n"); + String sectionNamePrefix = INDENT + (unique ? "SAI slow query " : "SAI slowest query "); + + // append the index context metrics + sb.append(sectionNamePrefix).append("metrics:\n"); + appendMetric(sb, "sstablesHit", metrics.sstablesHit); + appendMetric(sb, "segmentsHit", metrics.segmentsHit); + appendMetric(sb, "keysFetched", metrics.keysFetched); + appendMetric(sb, "partitionsFetched", metrics.partitionsFetched); + appendMetric(sb, "partitionsReturned", metrics.partitionsReturned); + appendMetric(sb, "partitionTombstonesFetched", metrics.partitionTombstonesFetched); + appendMetric(sb, "rowsFetched", metrics.rowsFetched); + appendMetric(sb, "rowsReturned", metrics.rowsReturned); + appendMetric(sb, "rowTombstonesFetched", metrics.rowTombstonesFetched); + appendMetric(sb, "trieSegmentsHit", metrics.trieSegmentsHit); + appendMetric(sb, "triePostingsSkips", metrics.triePostingsSkips); + appendMetric(sb, "triePostingsDecodes", metrics.triePostingsDecodes); + appendMetric(sb, "bkdSegmentsHit", metrics.bkdSegmentsHit); + appendMetric(sb, "bkdPostingListsHit", metrics.bkdPostingListsHit); + appendMetric(sb, "bkdPostingsSkips", metrics.bkdPostingsSkips); + appendMetric(sb, "bkdPostingsDecodes", metrics.bkdPostingsDecodes); + appendMetric(sb, "annGraphSearchLatencyNanos", metrics.annGraphSearchLatency); + + // append the plan + sb.append(sectionNamePrefix).append("plan:\n").append(plan); + + return sb.toString(); + } + + private static String toLogString(Plan plan) + { + String s = plan.toStringRecursive(Redaction.REDACT, DOUBLE_INDENT); + return s.endsWith("\n") ? s.substring(0, s.length() - 1) : s; + } + + private static void appendMetric(StringBuilder sb, String name, Object value) + { + sb.append(DOUBLE_INDENT).append(name).append(": ").append(value).append('\n'); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryView.java b/src/java/org/apache/cassandra/index/sai/plan/QueryView.java new file mode 100644 index 000000000000..17e6a334f95b --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryView.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.plan; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.memory.MemtableIndex; +import org.apache.cassandra.index.sai.view.View; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter; +import org.apache.cassandra.tracing.Tracing; + + +public class QueryView implements AutoCloseable +{ + final View saiView; + final ColumnFamilyStore.ViewFragment viewFragment; + public final Set sstableIndexes; + public final Set memtableIndexes; + + public QueryView(View saiView, + ColumnFamilyStore.ViewFragment viewFragment, + Set sstableIndexes, + Set memtableIndexes) + { + this.saiView = saiView; + this.viewFragment = viewFragment; + this.sstableIndexes = sstableIndexes; + this.memtableIndexes = memtableIndexes; + } + + @Override + public void close() + { + saiView.release(); + } + + /** + * Returns the total count of rows in all sstables in this view + */ + public long getTotalSStableRows() + { + return viewFragment.sstables.stream().mapToLong(SSTableReader::getTotalRows).sum(); + } + + /** + * Build a query specific view of the memtables, sstables, and indexes for a query. + * For use with SAI ordered queries to ensure that the view is consistent over the lifetime of the query, + * which is particularly important for validation of a cell's source memtable/sstable. + */ + static class Builder + { + private static final Logger logger = LoggerFactory.getLogger(Builder.class); + + private final IndexContext indexContext; + private final AbstractBounds range; + + Builder(IndexContext indexContext, AbstractBounds range) + { + this.indexContext = indexContext; + this.range = range; + } + + /** + * Denotes a situation when there exist no index for an active memtable or sstable. + * This can happen e.g. when the index gets dropped while running the query. + */ + static class MissingIndexException extends RuntimeException + { + final boolean isDropped; + final String indexName; + + private MissingIndexException(IndexContext context) + { + super(); + this.isDropped = context.isDropped(); + this.indexName = context.getIndexName(); + } + + @Override + public String getMessage() + { + return isDropped ? "Index " + indexName + " was dropped." + : "Unable to acquire lock on index view: " + indexName + '.'; + } + } + + /** + * Acquire references to all the memtables, memtable indexes, sstables, and sstable indexes required for the + * given expression. + */ + protected QueryView build() throws MissingIndexException + { + var sstableIndexes = new HashSet(); + View saiView = null; + try + { + // Get memtables first in case we are in the middle of flushing one. + // Note that we get the memtables from the index context, which is updated via notifications after + // the index context's view is updated, which guarantees a complete and correct view of the table in + // favor of possibly duplicated search on a recently flushed memtable and its corresponding sstable. + var memtableIndexes = new HashSet<>(indexContext.getLiveMemtables().values()); + // This is an atomic operation to get an already referenced view of all current local indexes for the table + saiView = indexContext.getReferencedView(TimeUnit.SECONDS.toNanos(5)); + if (saiView == null) + throw new MissingIndexException(indexContext); + + // Now that we referenced a view, need to confirm that the view we referenced isn't somehow invalid. + if (!indexContext.isIndexed()) + throw new MissingIndexException(indexContext); + + var sstableReaders = new ArrayList(saiView.size()); + // These are already referenced because they are referenced by the same view we just referenced. + for (var index : saiView.getIndexes()) + { + if (!indexInRange(index)) + continue; + sstableIndexes.add(index); + sstableReaders.add(index.getSSTable()); + } + + var memtables = new ArrayList(memtableIndexes.size()); + for (var index : memtableIndexes) + { + var memtable = index.getMemtable(); + memtables.add(memtable); + } + + var viewFragment = new ColumnFamilyStore.ViewFragment(sstableReaders, memtables); + return new QueryView(saiView, viewFragment, sstableIndexes, memtableIndexes); + } + catch (Exception e) + { + if (saiView != null) + saiView.release(); + throw e; + } + finally + { + if (Tracing.isTracing()) + { + var groupedIndexes = sstableIndexes.stream().collect( + Collectors.groupingBy(i -> i.getIndexContext().getIndexName(), Collectors.counting())); + var summary = groupedIndexes.entrySet().stream() + .map(e -> String.format("%s (%s sstables)", e.getKey(), e.getValue())) + .collect(Collectors.joining(", ")); + Tracing.trace("Querying storage-attached indexes {}", summary); + } + } + } + + // I've removed the concept of "most selective index" since we don't actually have per-sstable + // statistics on that; it looks like it was only used to check bounds overlap, so computing + // an actual global bounds should be an improvement. But computing global bounds as an intersection + // of individual bounds is messy because you can end up with more than one range. + private boolean indexInRange(SSTableIndex index) + { + SSTableReader sstable = index.getSSTable(); + if (range instanceof Bounds && range.left.equals(range.right) && (!range.left.isMinimum()) && range.left instanceof DecoratedKey) + { + if (!((SSTableReaderWithFilter) sstable).getFilter().isPresent((DecoratedKey)range.left)) + return false; + } + return range.left.compareTo(sstable.last) <= 0 && (range.right.isMinimum() || sstable.first.compareTo(range.right) <= 0); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryViewBuilder.java b/src/java/org/apache/cassandra/index/sai/plan/QueryViewBuilder.java index 0d2aeb3838fb..e5f58f49aa0f 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/QueryViewBuilder.java +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryViewBuilder.java @@ -18,138 +18,209 @@ package org.apache.cassandra.index.sai.plan; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashSet; -import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.googlecode.concurrenttrees.common.Iterables; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.db.lifecycle.View; import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.SSTableIndex; import org.apache.cassandra.index.sai.memory.MemtableIndex; -import org.apache.cassandra.index.sai.view.View; +import org.apache.cassandra.index.sai.utils.RangeUtil; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.NoSpamLogger; /** - * Build a query specific view of the on-disk indexes for a query. This will return a - * {@link Collection} of {@link Expression} and {@link SSTableIndex}s that represent - * the on-disk data for a query. - *

    - * The query view will include all the indexed expressions even if they don't have any - * on-disk data. This in necessary because the query view is used to query in-memory - * data as well as the attached on-disk indexes. + * Build a query specific view of the memtables, sstables, and indexes for a query. + * For use with SAI ordered queries to ensure that the view is consistent over the lifetime of the query, + * which is particularly important for validation of a cell's source memtable/sstable. */ public class QueryViewBuilder { - private final Collection expressions; + private static final Logger logger = LoggerFactory.getLogger(QueryViewBuilder.class); + + private final ColumnFamilyStore cfs; + private final Orderer orderer; private final AbstractBounds range; + private final QueryContext queryContext; - QueryViewBuilder(Collection expressions, AbstractBounds range) + QueryViewBuilder(ColumnFamilyStore cfs, Orderer orderer, AbstractBounds range, QueryContext queryContext) { - this.expressions = expressions; + this.cfs = cfs; + this.orderer = orderer; this.range = range; - } - - public static class QueryExpressionView - { - public final Expression expression; - public final Collection memtableIndexes; - public final Collection sstableIndexes; - - public QueryExpressionView(Expression expression, Collection memtableIndexes, Collection sstableIndexes) - { - this.expression = expression; - this.memtableIndexes = memtableIndexes; - this.sstableIndexes = sstableIndexes; - } - - public ColumnFamilyStore.ViewFragment computeViewFragment() - { - // Because the SSTableIndex holds a reference to the SSTableReader, we know the sstable is still accessible - // so it is safe to build a view fragment. - List memtables = memtableIndexes.stream().map(MemtableIndex::getMemtable).collect(Collectors.toList()); - List sstableReaders = sstableIndexes.stream().map(SSTableIndex::getSSTable).collect(Collectors.toList()); - return new ColumnFamilyStore.ViewFragment(sstableReaders, memtables); - } + this.queryContext = queryContext; } public static class QueryView implements AutoCloseable { - public final Collection view; - public final Set referencedIndexes; - - public QueryView(Collection view, Set referencedIndexes) + final ColumnFamilyStore.RefViewFragment view; + final Set referencedIndexes; + final Set memtableIndexes; + final Orderer orderer; + + public QueryView(ColumnFamilyStore.RefViewFragment view, + Set referencedIndexes, + Set memtableIndexes, + Orderer orderer) { this.view = view; this.referencedIndexes = referencedIndexes; + this.memtableIndexes = memtableIndexes; + this.orderer = orderer; } @Override public void close() { - referencedIndexes.forEach(SSTableIndex::releaseQuietly); + view.release(); + referencedIndexes.forEach(SSTableIndex::release); + } + + /** + * Returns the total count of rows in all sstables in this view + */ + public long getTotalSStableRows() + { + return view.sstables.stream().mapToLong(SSTableReader::getTotalRows).sum(); } } + /** + * Acquire references to all the memtables, memtable indexes, sstables, and sstable indexes required for the + * given expression. + *

    + * Will retry if the active sstables change concurrently. + */ protected QueryView build() { - Set referencedIndexes = new HashSet<>(); - while (true) + var referencedIndexes = new HashSet(); + long failingSince = -1L; + try { - referencedIndexes.clear(); - boolean failed = false; - - Collection view = getQueryView(expressions); - for (SSTableIndex index : view.stream().map(v -> v.sstableIndexes).flatMap(Collection::stream).collect(Collectors.toList())) + outer: + while (true) { - if (index.reference()) - referencedIndexes.add(index); - else - failed = true; + // Prevent an infinite loop + queryContext.checkpoint(); + + // Acquire live memtable index and memtable references first to avoid missing an sstable due to flush. + // Copy the memtable indexes to avoid concurrent modification. + var memtableIndexes = new HashSet<>(orderer.context.getLiveMemtables().values()); + + // We must use the canonical view in order for the equality check for source sstable/memtable + // to work correctly. + var filter = RangeUtil.coversFullRing(range) + ? View.selectFunction(SSTableSet.CANONICAL) + : View.select(SSTableSet.CANONICAL, s -> RangeUtil.intersects(s, range)); + var refViewFragment = cfs.selectAndReference(filter); + var memtables = Iterables.toSet(refViewFragment.memtables); + // Confirm that all the memtables associated with the memtable indexes we already have are still live. + // There might be additional memtables that are not associated with the expression because tombstones + // are not indexed. + for (MemtableIndex memtableIndex : memtableIndexes) + { + if (!memtables.contains(memtableIndex.getMemtable())) + { + refViewFragment.release(); + continue outer; + } + } + + Set indexes = getIndexesForExpression(orderer); + // Attempt to reference each of the indexes, and thn confirm that the sstable associated with the index + // is in the refViewFragment. If it isn't in the refViewFragment, we will get incorrect results, so + // we release the indexes and refViewFragment and try again. + for (SSTableIndex index : indexes) + { + var success = index.reference(); + if (success) + referencedIndexes.add(index); + + if (!success || !refViewFragment.sstables.contains(index.getSSTable())) + { + referencedIndexes.forEach(SSTableIndex::release); + referencedIndexes.clear(); + refViewFragment.release(); + + // Log about the failures + if (failingSince <= 0) + { + failingSince = Clock.Global.nanoTime(); + } + else if (Clock.Global.nanoTime() - failingSince > TimeUnit.MILLISECONDS.toNanos(100)) + { + failingSince = Clock.Global.nanoTime(); + if (success) + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, + "Spinning trying to capture index reader for {}, but it was released.", index); + else + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, + "Spinning trying to capture readers for {}, but : {}, ", refViewFragment.sstables, index.getSSTable()); + } + continue outer; + } + } + return new QueryView(refViewFragment, referencedIndexes, memtableIndexes, orderer); } - - if (failed) - referencedIndexes.forEach(SSTableIndex::release); - else - return new QueryView(view, referencedIndexes); } - } - - private Collection getQueryView(Collection expressions) - { - List queryView = new ArrayList<>(); - - for (Expression expression : expressions) + finally { - // Non-index column query should only act as FILTER BY for satisfiedBy(Row) method - // because otherwise it likely to go through the whole index. - if (expression.isNotIndexed()) - continue; - - // Fetch the memtables first to ensure we don't miss any newly flushed memtable index - Collection memtableIndexes = expression.getIndex().memtableIndexManager().getLiveMemtableIndexesSnapshot(); - // Select all the sstable indexes that have a term range that is satisfied by this expression and - // overlap with the key range being queried. - View view = expression.getIndex().view(); - Collection sstableIndexes = selectIndexesInRange(view.match(expression)); - queryView.add(new QueryExpressionView(expression, memtableIndexes, sstableIndexes)); + if (Tracing.isTracing()) + { + Map groupedIndexes = referencedIndexes.stream().collect( + Collectors.groupingBy(i -> i.getIndexContext().getIndexName(), Collectors.counting())); + String summary = groupedIndexes.entrySet().stream() + .map(e -> String.format("%s (%s sstables)", e.getKey(), e.getValue())) + .collect(Collectors.joining(", ")); + Tracing.trace("Querying storage-attached indexes {}", summary); + } } - - return queryView; } - private List selectIndexesInRange(Collection indexes) + /** + * Get the index + */ + private Set getIndexesForExpression(Orderer orderer) { - return indexes.stream().filter(this::indexInRange).sorted(SSTableIndex.COMPARATOR).collect(Collectors.toList()); + if (!orderer.context.isIndexed()) + throw new IllegalArgumentException("Expression is not indexed"); + + // Get all the indexes in the range. + return orderer.context.getView().getIndexes().stream().filter(this::indexInRange).collect(Collectors.toSet()); } + // I've removed the concept of "most selective index" since we don't actually have per-sstable + // statistics on that; it looks like it was only used to check bounds overlap, so computing + // an actual global bounds should be an improvement. But computing global bounds as an intersection + // of individual bounds is messy because you can end up with more than one range. private boolean indexInRange(SSTableIndex index) { SSTableReader sstable = index.getSSTable(); - return range.left.compareTo(sstable.getLast()) <= 0 && (range.right.isMinimum() || sstable.getFirst().compareTo(range.right) <= 0); + if (range instanceof Bounds && range.left.equals(range.right) && (!range.left.isMinimum()) && range.left instanceof DecoratedKey) + { + if (sstable instanceof SSTableReaderWithFilter) + { + SSTableReaderWithFilter sstableWithFilter = (SSTableReaderWithFilter) sstable; + if (!sstableWithFilter.getFilter().isPresent((DecoratedKey) range.left)) + return false; + } + } + return range.left.compareTo(sstable.last) <= 0 && (range.right.isMinimum() || sstable.first.compareTo(range.right) <= 0); } } diff --git a/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexQueryPlan.java b/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexQueryPlan.java index 3cc18bfa2edc..d101d78754ef 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexQueryPlan.java +++ b/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexQueryPlan.java @@ -17,7 +17,8 @@ */ package org.apache.cassandra.index.sai.plan; -import java.util.Map; +import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -27,89 +28,168 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.partitions.PartitionIterator; -import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.metrics.TableQueryMetrics; -import org.apache.cassandra.schema.TableMetadata; public class StorageAttachedIndexQueryPlan implements Index.QueryPlan { public static final String UNSUPPORTED_NON_STRICT_OPERATOR = - "Operator %s is only supported in intersections for reads that do not require replica reconciliation."; + "Operator %s is only supported in intersections for reads that do not require replica reconciliation."; private final ColumnFamilyStore cfs; private final TableQueryMetrics queryMetrics; + + /** + * postIndexFilter comprised by those expressions in the read command row filter that can't be handled by + * {@link FilterTree#isSatisfiedBy(DecoratedKey, Unfiltered, Row)}. That includes expressions targeted + * at {@link RowFilter.UserExpression}s like those used by RLAC. + */ private final RowFilter postIndexFilter; - private final RowFilter indexFilter; private final Set indexes; - private final boolean isTopK; + private final IndexFeatureSet indexFeatureSet; + private final Orderer orderer; + private final boolean usesIndexFiltering; private StorageAttachedIndexQueryPlan(ColumnFamilyStore cfs, TableQueryMetrics queryMetrics, - RowFilter postIndexFilter, - RowFilter indexFilter, - ImmutableSet indexes) + RowFilter filter, + ImmutableSet indexes, + IndexFeatureSet indexFeatureSet) { this.cfs = cfs; this.queryMetrics = queryMetrics; - this.postIndexFilter = postIndexFilter; - this.indexFilter = indexFilter; + this.postIndexFilter = filter.restrict(RowFilter.Expression::isUserDefined); this.indexes = indexes; - this.isTopK = indexes.stream().anyMatch(i -> i instanceof StorageAttachedIndex && ((StorageAttachedIndex) i).termType().isVector()); + this.indexFeatureSet = indexFeatureSet; + this.orderer = Orderer.from(cfs.getIndexManager(), filter); + this.usesIndexFiltering = hasIndexFilters(filter, indexes); } @Nullable public static StorageAttachedIndexQueryPlan create(ColumnFamilyStore cfs, TableQueryMetrics queryMetrics, - Set indexes, - RowFilter filter) + Set allIndexes, + RowFilter rowFilter) { - ImmutableSet.Builder selectedIndexesBuilder = ImmutableSet.builder(); + // collect the indexes that can be used with the provided row filter + Set selectedIndexes = new HashSet<>(); + if (!selectedIndexes(rowFilter.root, allIndexes, selectedIndexes, rowFilter.indexHints)) + return null; + + // collect the features of the selected indexes + Version version = Version.current(cfs.keyspace.getName()); + IndexFeatureSet.Accumulator accumulator = new IndexFeatureSet.Accumulator(version); + for (StorageAttachedIndex index : selectedIndexes) + accumulator.accumulate(index.getIndexContext().indexFeatureSet()); - RowFilter preIndexFilter = filter; - RowFilter postIndexFilter = filter; + return new StorageAttachedIndexQueryPlan(cfs, + queryMetrics, + rowFilter, + ImmutableSet.copyOf(selectedIndexes), + accumulator.complete()); + } - for (RowFilter.Expression expression : filter) + /** + * Collects the indexes that can be used with the specified filtering tree without doing a full index scan. + *

    + * The selected indexes are those that can satisfy at least one of the expressions of the filter, and that + * aren't part of an OR operation that contains not indexed expressions, unless that OR operation is nested inside + * an AND operation that has at least one indexed operation. + *

    + * For example, for {@code x AND y} we can use any index in {@code x}, {@code y}, or both. + *

    + * For {@code x OR y} we can't use a single index on {@code x} or {@code y} because we would need to do a full index + * scan because of the unidexed expression. However, if both columns were indexed, we could use those two indexes. + *

    + * For {@code (x OR y) AND z}, where {@code x} and {@code z} are indexed, we can use the index on {@code z}, even + * though we will ignore the index on {@code x}. + * + * @param element a row filter tree node + * @param allIndexes all the indexes in the index group + * @param selectedIndexes the set of indexes where we'll add those indexes can be used with the specified expression + * @param hints the user-provided index hints for the query, used to exclude indexes + * @return {@code true} if this has collected any indexes, {@code false} otherwise + */ + private static boolean selectedIndexes(RowFilter.FilterElement element, + Set allIndexes, + Set selectedIndexes, + IndexHints hints) + { + if (element.isDisjunction()) // OR, all restrictions should have an index { - // We ignore any expressions here (currently IN and user-defined expressions) where we don't have a way to - // translate their #isSatifiedBy method, they will be included in the filter returned by - // QueryPlan#postIndexQueryFilter(). If strict filtering is not allowed, we must reject the query until the - // expression(s) in question are compatible with #isSatifiedBy. - // - // Note: For both the pre- and post-filters we need to check that the expression exists before removing it - // because the without method assert if the expression doesn't exist. This can be the case if we are given - // a duplicate expression - a = 1 and a = 1. The without method removes all instances of the expression. - if (expression.operator().isIN() || expression.isUserDefined()) + Set orIndexes = new HashSet<>(); + for (RowFilter.Expression expression : element.expressions()) { - if (!filter.isStrict()) - throw new InvalidRequestException(String.format(UNSUPPORTED_NON_STRICT_OPERATOR, expression.operator())); - - if (preIndexFilter.getExpressions().contains(expression)) - preIndexFilter = preIndexFilter.without(expression); - continue; + if (!selectedIndexes(expression, allIndexes, orIndexes, hints)) + return false; + } + for (RowFilter.FilterElement child : element.children()) + { + if (!selectedIndexes(child, allIndexes, orIndexes, hints)) + return false; } + selectedIndexes.addAll(orIndexes); + return !orIndexes.isEmpty(); + } + else // AND, only one restriction needs to have an index + { + boolean hasIndex = false; + for (RowFilter.Expression expression : element.expressions()) + { + hasIndex |= selectedIndexes(expression, allIndexes, selectedIndexes, hints); + } + for (RowFilter.FilterElement child : element.children()) + { + hasIndex |= selectedIndexes(child, allIndexes, selectedIndexes, hints); + } + return hasIndex; + } + } - if (postIndexFilter.getExpressions().contains(expression)) - postIndexFilter = postIndexFilter.without(expression); + /** + * Collects the indexes that can be used with the specified expression. + * + * @param expression a row filter expression + * @param allIndexes all the indexes in the index group + * @param selectedIndexes the set of indexes where we'll add those indexes can be used with the specified expression + * @param hints the user-provided index hints for the query, used to exclude indexes + * @return {@code true} if this has collected any indexes, {@code false} otherwise + */ + private static boolean selectedIndexes(RowFilter.Expression expression, + Set allIndexes, + Set selectedIndexes, + IndexHints hints) + { + // we ignore user-defined expressions here because we don't have a way to translate their #isSatifiedBy + // method, they will be included in the filter returned by QueryPlan#postIndexQueryFilter() + if (expression.isUserDefined()) + return false; - for (StorageAttachedIndex index : indexes) + // collect the indexes that support the specified expression + Set candidates = new HashSet<>(); + for (StorageAttachedIndex index : allIndexes) + { + if (index.supportsExpression(expression)) { - if (index.supportsExpression(expression.column(), expression.operator())) - { - selectedIndexesBuilder.add(index); - } + candidates.add(index); } } - ImmutableSet selectedIndexes = selectedIndexesBuilder.build(); - if (selectedIndexes.isEmpty()) - return null; + // let the hints choose the best index from those supporting the expression + Optional preferred = hints.getBestIndexFor(candidates, p -> true, expression.operator().isAnyContains()); + preferred.ifPresent(selectedIndexes::add); - return new StorageAttachedIndexQueryPlan(cfs, queryMetrics, postIndexFilter, preIndexFilter, selectedIndexes); + return preferred.isPresent(); } @Override @@ -131,12 +211,13 @@ public boolean shouldEstimateInitialConcurrency() } @Override - public Index.Searcher searcherFor(ReadCommand command) + public StorageAttachedIndexSearcher searcherFor(ReadCommand command) { return new StorageAttachedIndexSearcher(cfs, queryMetrics, command, - indexFilter, + orderer, + indexFeatureSet, DatabaseDescriptor.getRangeRpcTimeout(TimeUnit.MILLISECONDS)); } @@ -150,13 +231,11 @@ public Function postProcessor(ReadCommand return partitions -> partitions; // in case of top-k query, filter out rows that are not actually global top-K - return partitions -> (PartitionIterator) new VectorTopKProcessor(command).consumeSortByScoreAndTakeTopK(partitions); + return partitions -> new TopKProcessor(command).reorder(partitions); } /** - * @return a filter with all the expressions that are user-defined or for a non-indexed partition key column - *

    - * (currently index on partition columns is not supported, see {@link StorageAttachedIndex#validateOptions(Map, TableMetadata)}) + * @return a filter with all the expressions that are user-defined */ @Override public RowFilter postIndexQueryFilter() @@ -164,9 +243,40 @@ public RowFilter postIndexQueryFilter() return postIndexFilter; } + @Override + public boolean supportsMultiRangeReadCommand() + { + return true; + } + @Override public boolean isTopK() { - return isTopK; + return orderer != null; + } + + @Override + public boolean isBM25() + { + return orderer != null && orderer.isBM25(); + } + + @Override + public boolean usesIndexFiltering() + { + return usesIndexFiltering; + } + + public static boolean hasIndexFilters(RowFilter filter, Set indexes) + { + for (RowFilter.Expression e : filter.expressions()) + { + for (Index index : indexes) + { + if (index.supportsExpression(e) && !Orderer.isFilterExpressionOrderer(e)) + return true; + } + } + return false; } } diff --git a/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexSearcher.java index 7d16f3399058..c2278ac69ff1 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexSearcher.java +++ b/src/java/org/apache/cassandra/index/sai/plan/StorageAttachedIndexSearcher.java @@ -18,7 +18,7 @@ package org.apache.cassandra.index.sai.plan; -import java.nio.ByteBuffer; +import java.io.IOError; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -27,66 +27,74 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; +import java.util.Objects; import java.util.PriorityQueue; import java.util.Queue; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.netty.util.concurrent.FastThreadLocal; -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.db.Clustering; -import org.apache.cassandra.db.ClusteringBound; -import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.MessageParams; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadExecutionController; -import org.apache.cassandra.db.RegularAndStaticColumns; -import org.apache.cassandra.db.Slices; -import org.apache.cassandra.db.filter.ClusteringIndexFilter; -import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; -import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.monitoring.Monitorable; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.rows.AbstractUnfilteredRowIterator; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.BufferCell; +import org.apache.cassandra.db.rows.ColumnData; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.RequestTimeoutException; import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.QueryContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.index.sai.metrics.TableQueryMetrics; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.metrics.TableQueryMetrics; import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithScore; +import org.apache.cassandra.index.sai.utils.PrimaryKeyWithSortKey; import org.apache.cassandra.index.sai.utils.RangeUtil; import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.net.ParamType; +import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.CloseableIterator; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.btree.BTree; public class StorageAttachedIndexSearcher implements Index.Searcher { - private static final int PARTITION_ROW_BATCH_SIZE = 100; + protected static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndexSearcher.class); + + private static final int PARTITION_ROW_BATCH_SIZE = CassandraRelevantProperties.SAI_PARTITION_ROW_BATCH_SIZE.getInt(); private final ReadCommand command; - private final QueryController queryController; + private final QueryController controller; private final QueryContext queryContext; private final TableQueryMetrics tableQueryMetrics; + private Supplier executionInfoSupplier; private static final FastThreadLocal> nextKeys = new FastThreadLocal<>() { @@ -100,12 +108,13 @@ protected List initialValue() public StorageAttachedIndexSearcher(ColumnFamilyStore cfs, TableQueryMetrics tableQueryMetrics, ReadCommand command, - RowFilter indexFilter, + Orderer orderer, + IndexFeatureSet indexFeatureSet, long executionQuotaMs) { this.command = command; - this.queryContext = new QueryContext(command, executionQuotaMs); - this.queryController = new QueryController(cfs, command, indexFilter, queryContext); + this.queryContext = new QueryContext(executionQuotaMs); + this.controller = new QueryController(cfs, command, orderer, indexFeatureSet, queryContext, tableQueryMetrics); this.tableQueryMetrics = tableQueryMetrics; } @@ -115,165 +124,188 @@ public ReadCommand command() return command; } - @Override - public PartitionIterator filterReplicaFilteringProtection(PartitionIterator fullResponse) + @VisibleForTesting + public QueryContext queryContext() { - for (RowFilter.Expression expression : queryController.indexFilter()) - { - if (queryController.hasAnalyzer(expression)) - return applyIndexFilter(fullResponse, Operation.buildFilter(queryController, true), queryContext); - } + return queryContext; + } - // if no analyzer does transformation - return Index.Searcher.super.filterReplicaFilteringProtection(fullResponse); + /** + * Builds a Plan and stops. Leaves the controller in aborted state, and the Plan cannot be used to + * execute the query as all its iterators will be closed. This is only useful for testing purposes. + */ + @VisibleForTesting + public Plan.RowsIteration buildPlan() + { + return (Plan.RowsIteration) controller.buildPlan(); + } + + @VisibleForTesting + public void abort() + { + controller.abort(); } @Override + @SuppressWarnings("unchecked") public UnfilteredPartitionIterator search(ReadExecutionController executionController) throws RequestTimeoutException { - if (!command.isTopK()) + int retries = 0; + while (true) { - return new ResultRetriever(executionController); - } - else - { - // Need a consistent view of the memtables/sstables and their associated index, so we get the view now - // and propagate it as needed. - try (QueryViewBuilder.QueryView queryView = buildAnnQueryView()) + try { - queryController.maybeTriggerGuardrails(queryView); - ScoreOrderedResultRetriever result = new ScoreOrderedResultRetriever(executionController, queryView); - // takeTopKThenSortByPrimaryKey eagerly consumes up to k rows from the result because search must - // produce an iterator in PrimaryKey order. - return (UnfilteredPartitionIterator) new VectorTopKProcessor(command).takeTopKThenSortByPrimaryKey(result); + FilterTree filterTree = analyzeFilter(); + maybeTriggerReferencedIndexesGuardrail(filterTree); + + Plan plan = controller.buildPlan(); + executionInfoSupplier = QueryMonitorableExecutionInfo.supplier(queryContext, plan); + + Iterator keysIterator = controller.buildIterator(plan); + + // Can't check for `command.isTopK()` because the planner could optimize sorting out + UnfilteredPartitionIterator result; + Orderer ordering = plan.ordering(); + if (ordering == null) + { + assert keysIterator instanceof KeyRangeIterator; + result = new ResultRetriever((KeyRangeIterator) keysIterator, filterTree, executionController); + } + else + { + assert !(keysIterator instanceof KeyRangeIterator); + var scoredKeysIterator = (CloseableIterator) keysIterator; + var retriever = new ScoreOrderedResultRetriever(scoredKeysIterator, filterTree, controller, + executionController, queryContext, + command.nowInSec(), + command.limits().count(), + ordering.context.getDefinition()); + result = new TopKProcessor(command).filter(retriever); + } + return CountReturnedTransformation.apply(result, queryContext, controller::finish); } - } - } + catch (QueryView.Builder.MissingIndexException e) + { + // If an index was dropped while we were preparing the plan or between preparing the plan + // and creating the result retriever, we can retry without that index, + // because there may be other indexes that could be used to run the query. + // And if there are no good indexes left, we'd get a good contextual request error message. + if (e.isDropped && retries < 8) + { + logger.debug("Index " + e.indexName + " dropped while preparing the query plan. Retrying."); + retries++; + continue; + } - private QueryViewBuilder.QueryView buildAnnQueryView() - { - RowFilter.Expression annExpression = null; - for (RowFilter.Expression expression : queryController.indexFilter().getExpressions()) - { - if (expression.operator() == Operator.ANN) + // If we end up here, this is either a bug or a problem with an index (corrupted / missing components?). + controller.abort(); + // Throwing IOError here because we want the coordinator to handle it as any other serious storage error + // and report it up to the user as failed query. It is better to fail than to return an incomplete + // result set. + throw new IOError(e); + } + catch (Throwable t) { - if (annExpression != null) - throw new IllegalStateException("Multiple ANN expressions in a single query are not supported"); - annExpression = expression; + controller.abort(); + throw t; } } - if (annExpression == null) - throw new IllegalStateException("No ANN expression found in query"); - - StorageAttachedIndex index = queryController.indexFor(annExpression); - Expression planExpression = Expression.create(index).add(Operator.ANN, annExpression.getIndexValue().duplicate()); - return new QueryViewBuilder(Collections.singleton(planExpression), queryController.mergeRange()).build(); } - private abstract class AbstractRetreiver extends AbstractIterator implements UnfilteredPartitionIterator + private void maybeTriggerReferencedIndexesGuardrail(FilterTree filterTree) { - final FilterTree filterTree; - final ReadExecutionController executionController; + if (!Guardrails.saiSSTableIndexesPerQuery.enabled()) + return; + + int numReferencedIndexes = filterTree.numSSTableIndexes(); - AbstractRetreiver(ReadExecutionController executionController) + if (Guardrails.saiSSTableIndexesPerQuery.failsOn(numReferencedIndexes, null)) { - this.executionController = executionController; - this.filterTree = Operation.buildFilter(queryController, queryController.usesStrictFiltering()); + String msg = String.format("Query %s attempted to read from too many indexes (%s) but max allowed is %s; " + + "query aborted (see sai_sstable_indexes_per_query_fail_threshold)", + command.toRedactedCQLString(), + numReferencedIndexes, + Guardrails.CONFIG_PROVIDER.getOrCreate(null).getSaiSSTableIndexesPerQueryFailThreshold()); + Tracing.trace(msg); + MessageParams.add(ParamType.TOO_MANY_REFERENCED_INDEXES_FAIL, numReferencedIndexes); + throw new QueryReferencingTooManyIndexesException(msg); } - - @Override - public TableMetadata metadata() + else if (Guardrails.saiSSTableIndexesPerQuery.warnsOn(numReferencedIndexes, null)) { - return queryController.metadata(); + MessageParams.add(ParamType.TOO_MANY_REFERENCED_INDEXES_WARN, numReferencedIndexes); } + } + + @Override + public Supplier monitorableExecutionInfo() + { + return executionInfoSupplier; + } + /** + * Converts expressions into filter tree (which is currently just a single AND). + *

    + * Filter tree allows us to do a couple of important optimizations + * namely, group flattening for AND operations (query rewrite), expression bounds checks, + * "satisfies by" checks for resulting rows with an early exit. + * + * @return root of the filter tree. + */ + private FilterTree analyzeFilter() + { + return controller.buildFilter(); } - private class ResultRetriever extends AbstractRetreiver + private class ResultRetriever extends AbstractIterator implements UnfilteredPartitionIterator { private final PrimaryKey firstPrimaryKey; - private final PrimaryKey lastPrimaryKey; private final Iterator keyRanges; - private final DataRange firstDataRange; private AbstractBounds currentKeyRange; - private final KeyRangeIterator resultKeyIterator; + private final KeyRangeIterator operation; + private final FilterTree filterTree; + private final ReadExecutionController executionController; private final PrimaryKey.Factory keyFactory; private final int partitionRowBatchSize; + private final CountFetchedTransformation fetchedRowsCounter; private PrimaryKey lastKey; - private ResultRetriever(ReadExecutionController executionController) + private ResultRetriever(KeyRangeIterator operation, + FilterTree filterTree, + ReadExecutionController executionController) { - super(executionController); - this.keyRanges = queryController.dataRanges().iterator(); - this.firstDataRange = keyRanges.next(); - this.currentKeyRange = firstDataRange.keyRange(); - this.resultKeyIterator = Operation.buildIterator(queryController); - this.keyFactory = queryController.primaryKeyFactory(); - this.firstPrimaryKey = queryController.firstPrimaryKeyInRange(); - this.lastPrimaryKey = queryController.lastPrimaryKeyInRange(); + this.keyRanges = controller.dataRanges().iterator(); + this.currentKeyRange = keyRanges.next().keyRange(); - // Ensure we don't fetch larger batches than the provided LIMIT to avoid fetching keys we won't use: + this.operation = operation; + this.filterTree = filterTree; + this.executionController = executionController; + this.keyFactory = controller.primaryKeyFactory(); + this.fetchedRowsCounter = new CountFetchedTransformation(queryContext, command.nowInSec()); + + this.firstPrimaryKey = controller.firstPrimaryKey(); + + // Ensure we don't fetch larger batches than the provided LIMIT to avoid fetching keys we won't use: this.partitionRowBatchSize = Math.min(PARTITION_ROW_BATCH_SIZE, command.limits().count()); } @Override public UnfilteredRowIterator computeNext() { - if (resultKeyIterator == null) + // IMPORTANT: The correctness of the entire query pipeline relies on the fact that we consume a token + // and materialize its keys before moving on to the next token in the flow. This sequence must not be broken + // with toList() or similar. (Both the union and intersection flow constructs, to avoid excessive object + // allocation, reuse their token mergers as they process individual positions on the ring.) + + if (operation == null) return endOfData(); // If being called for the first time, skip to the beginning of the range. // We can't put this code in the constructor because it may throw and the caller // may not be prepared for that. if (lastKey == null) - { - PrimaryKey skipTarget = firstPrimaryKey; - ClusteringComparator comparator = command.metadata().comparator; - - // If there are no clusterings, the first data range selects an entire partitions, or we have static - // expressions, don't bother trying to skip forward within the partition. - if (comparator.size() > 0 && !firstDataRange.selectsAllPartition() && !command.rowFilter().hasStaticExpression()) - { - // Only attempt to skip if the first data range covers a single partition. - if (currentKeyRange.left.equals(currentKeyRange.right) && currentKeyRange.left instanceof DecoratedKey) - { - DecoratedKey decoratedKey = (DecoratedKey) currentKeyRange.left; - ClusteringIndexFilter filter = firstDataRange.clusteringIndexFilter(decoratedKey); - - if (filter instanceof ClusteringIndexSliceFilter) - { - Slices slices = ((ClusteringIndexSliceFilter) filter).requestedSlices(); - - if (!slices.isEmpty()) - { - ClusteringBound startBound = slices.get(0).start(); - - if (!startBound.isEmpty()) - { - ByteBuffer[] rawValues = startBound.getBufferArray(); - - if (rawValues.length == comparator.size()) - skipTarget = keyFactory.create(decoratedKey, Clustering.make(rawValues)); - } - } - } - else if (filter instanceof ClusteringIndexNamesFilter) - { - ClusteringIndexNamesFilter namesFilter = (ClusteringIndexNamesFilter) filter; - - if (!namesFilter.requestedRows().isEmpty()) - { - Clustering skipClustering = namesFilter.requestedRows().iterator().next(); - skipTarget = keyFactory.create(decoratedKey, skipClustering); - } - } - } - } - - resultKeyIterator.skipTo(skipTarget); - } + operation.skipTo(firstPrimaryKey); // Theoretically we wouldn't need this if the caller of computeNext always ran the // returned iterators to the completion. Unfortunately, we have no control over the caller behavior here. @@ -282,74 +314,32 @@ else if (filter instanceof ClusteringIndexNamesFilter) skipToNextPartition(); UnfilteredRowIterator iterator = nextRowIterator(this::nextSelectedKeysInRange); - return iterator != null ? iteratePartition(iterator) : endOfData(); + return iterator != null + ? iteratePartition(iterator) + : endOfData(); } /** - * Tries to obtain a row iterator for the supplied keys by repeatedly calling - * {@link ResultRetriever#queryStorageAndFilter} until it gives a non-null result. - * The keysSupplier should return the next batch of keys with every call to get() - * and null when there are no more keys to try. + * Tries to obtain a row iterator for one of the supplied keys by repeatedly calling + * {@link ResultRetriever#apply} until it gives a non-null result. + * The keySupplier should return the next key with every call to get() and + * null when there are no more keys to try. * * @return an iterator or null if all keys were tried with no success */ - private @Nullable UnfilteredRowIterator nextRowIterator(@Nonnull Supplier> keysSupplier) + private @Nullable UnfilteredRowIterator nextRowIterator(@Nonnull Supplier> keySupplier) { UnfilteredRowIterator iterator = null; while (iterator == null) { - List keys = keysSupplier.get(); + List keys = keySupplier.get(); if (keys.isEmpty()) return null; - iterator = queryStorageAndFilter(keys); + iterator = apply(keys); } return iterator; } - /** - * Retrieves the next batch of primary keys (i.e. up to {@link #partitionRowBatchSize} of them) that are - * contained by one of the query key ranges and selected by the {@link QueryController}. If the next key falls - * out of the current key range, it skips to the next key range, and so on. If no more keys accepted by - * the controller are available, and empty list is returned. - * - * @return a list of up to {@link #partitionRowBatchSize} primary keys - */ - private List nextSelectedKeysInRange() - { - List threadLocalNextKeys = nextKeys.get(); - threadLocalNextKeys.clear(); - PrimaryKey firstKey; - - do - { - firstKey = nextKeyInRange(); - - if (firstKey == null) - return Collections.emptyList(); - } - while (queryController.doesNotSelect(firstKey) || firstKey.equals(lastKey, false)); - - lastKey = firstKey; - threadLocalNextKeys.add(firstKey); - fillNextSelectedKeysInPartition(firstKey.partitionKey(), threadLocalNextKeys); - return threadLocalNextKeys; - } - - /** - * Retrieves the next batch of primary keys (i.e. up to {@link #partitionRowBatchSize} of them) that belong to - * the given partition and are selected by the query controller, advancing the underlying iterator only while - * the next key belongs to that partition. - * - * @return a list of up to {@link #partitionRowBatchSize} primary keys within the given partition - */ - private List nextSelectedKeysInPartition(DecoratedKey partitionKey) - { - List threadLocalNextKeys = nextKeys.get(); - threadLocalNextKeys.clear(); - fillNextSelectedKeysInPartition(partitionKey, threadLocalNextKeys); - return threadLocalNextKeys; - } - /** * Returns the next available key contained by one of the keyRanges. * If the next key falls out of the current key range, it skips to the next key range, and so on. @@ -370,18 +360,36 @@ private List nextSelectedKeysInPartition(DecoratedKey partitionKey) } else { - // key either before the current range, so let's move the key forward - skipTo(currentKeyRange.left.getToken()); + // the following condition may be false if currentKeyRange.left is not inclusive, + // and key == currentKeyRange.left; in this case we should not try to skipTo the beginning + // of the range because that would be requesting the key to go backwards + // (in some implementations, skipTo can go backwards, and we don't want that) + if (currentKeyRange.left.getToken().compareTo(key.token()) > 0) + { + // key before the current range, so let's move the key forward + skipTo(currentKeyRange.left.getToken()); + } key = nextKey(); } } return key; } + private boolean isEqualToLastKey(PrimaryKey key) + { + // We don't want key.equals(lastKey) because some PrimaryKey implementations consider more than just + // partition key and clustering for equality. This can break lastKey skipping, which is necessary for + // correctness when PrimaryKey doesn't have a clustering (as otherwise, the same partition may get + // filtered and considered as a result multiple times). + return lastKey != null && + Objects.equals(lastKey.partitionKey(), key.partitionKey()) && + (!lastKey.hasClustering() || !key.hasClustering() || Objects.equals(lastKey.clustering(), key.clustering())); + } + private void fillNextSelectedKeysInPartition(DecoratedKey partitionKey, List nextPrimaryKeys) { - while (resultKeyIterator.hasNext() - && resultKeyIterator.peek().partitionKey().equals(partitionKey) + while (operation.hasNext() + && operation.peek().partitionKey().equals(partitionKey) && nextPrimaryKeys.size() < partitionRowBatchSize) { PrimaryKey key = nextKey(); @@ -389,7 +397,7 @@ private void fillNextSelectedKeysInPartition(DecoratedKey partitionKey, List nextSelectedKeysInRange() { - if (!resultKeyIterator.hasNext()) - return null; - PrimaryKey key = resultKeyIterator.next(); - return isWithinUpperBound(key) ? key : null; + List threadLocalNextKeys = nextKeys.get(); + threadLocalNextKeys.clear(); + PrimaryKey firstKey; + + do + { + firstKey = nextKeyInRange(); + + if (firstKey == null) + return Collections.emptyList(); + } + while (!controller.selects(firstKey) || isEqualToLastKey(firstKey)); + + lastKey = firstKey; + threadLocalNextKeys.add(firstKey); + fillNextSelectedKeysInPartition(firstKey.partitionKey(), threadLocalNextKeys); + return threadLocalNextKeys; + } + + /** + * Retrieves the next batch of primary keys (i.e. up to {@link #partitionRowBatchSize} of them) that belong to + * the given partition and are selected by the query controller, advancing the underlying iterator only while + * the next key belongs to that partition. + * + * @return a list of up to {@link #partitionRowBatchSize} primary keys within the given partition + */ + private List nextSelectedKeysInPartition(DecoratedKey partitionKey) + { + List threadLocalNextKeys = nextKeys.get(); + threadLocalNextKeys.clear(); + fillNextSelectedKeysInPartition(partitionKey, threadLocalNextKeys); + return threadLocalNextKeys; } /** - * Returns true if the key is not greater than lastPrimaryKey + * Gets the next key from the underlying operation. + * Returns null if there are no more keys <= lastPrimaryKey. */ - private boolean isWithinUpperBound(PrimaryKey key) + private @Nullable PrimaryKey nextKey() { - return lastPrimaryKey.token().isMinimum() || lastPrimaryKey.compareTo(key, false) >= 0; + return operation.hasNext() ? operation.next() : null; } /** @@ -430,7 +471,7 @@ private boolean isWithinUpperBound(PrimaryKey key) */ private void skipTo(@Nonnull Token token) { - resultKeyIterator.skipTo(keyFactory.create(token)); + operation.skipTo(keyFactory.createTokenOnly(token)); } /** @@ -441,17 +482,17 @@ private void skipToNextPartition() if (lastKey == null) return; DecoratedKey lastPartitionKey = lastKey.partitionKey(); - while (resultKeyIterator.hasNext() && resultKeyIterator.peek().partitionKey().equals(lastPartitionKey)) - resultKeyIterator.next(); + while (operation.hasNext() && operation.peek().partitionKey().equals(lastPartitionKey)) + operation.next(); } /** * Returns an iterator over the rows in the partition associated with the given iterator. * Initially, it retrieves the rows from the given iterator until it runs out of data. - * Then it iterates the remaining primary keys obtained from the index in batches until the end of the - * partition, lazily constructing an itertor for each batch. Only one row iterator is open at a time. - *

    + * Then it iterates the primary keys obtained from the index until the end of the partition + * and lazily constructs new row itertors for each of the key. At a given time, only one row iterator is open. + *

    * The rows are retrieved in the order of primary keys provided by the underlying index. * The iterator is complete when the next key to be fetched belongs to different partition * (but the iterator does not consume that key). @@ -460,13 +501,14 @@ private void skipToNextPartition() */ private @Nonnull UnfilteredRowIterator iteratePartition(@Nonnull UnfilteredRowIterator startIter) { - return new AbstractUnfilteredRowIterator(startIter.metadata(), - startIter.partitionKey(), - startIter.partitionLevelDeletion(), - startIter.columns(), - startIter.staticRow(), - startIter.isReverseOrder(), - startIter.stats()) + return new AbstractUnfilteredRowIterator( + startIter.metadata(), + startIter.partitionKey(), + startIter.partitionLevelDeletion(), + startIter.columns(), + startIter.staticRow(), + startIter.isReverseOrder(), + startIter.stats()) { private UnfilteredRowIterator currentIter = startIter; private final DecoratedKey partitionKey = startIter.partitionKey(); @@ -493,102 +535,31 @@ public void close() }; } - private UnfilteredRowIterator queryStorageAndFilter(List keys) + public UnfilteredRowIterator apply(List keys) { long startTimeNanos = Clock.Global.nanoTime(); + UnfilteredRowIterator partition = controller.getPartition(keys, executionController); + UnfilteredRowIterator counted = fetchedRowsCounter.apply(partition); + queryContext.checkpoint(); - try (UnfilteredRowIterator partition = queryController.queryStorage(keys, executionController)) - { - queryContext.partitionsRead++; - queryContext.checkpoint(); - - List filtered = filterPartition(partition, filterTree, queryContext); - - // Note that we record the duration of the read after post-filtering, which actually - // materializes the rows from disk. - tableQueryMetrics.postFilteringReadLatency.update(Clock.Global.nanoTime() - startTimeNanos, TimeUnit.NANOSECONDS); + // Note that we record the duration of the read after post-filtering, which actually + // materializes the rows from disk. + queryContext.addPostFilteringReadLatency(Clock.Global.nanoTime() - startTimeNanos); - return filtered != null - ? new SinglePartitionIterator(partition, partition.staticRow(), filtered.iterator()) - : null; - } + queryContext.addKeysFetched(keys.size()); + return applyIndexFilter(counted, filterTree); } @Override - public void close() - { - FileUtils.closeQuietly(resultKeyIterator); - if (tableQueryMetrics != null) tableQueryMetrics.record(queryContext); - } - } - - private static List filterPartition(UnfilteredRowIterator partition, FilterTree tree, QueryContext context) - { - Row staticRow = partition.staticRow(); - DecoratedKey partitionKey = partition.partitionKey(); - List matches = new ArrayList<>(); - boolean hasMatch = false; - - while (partition.hasNext()) - { - Unfiltered unfiltered = partition.next(); - - if (unfiltered.isRow()) - { - context.rowsFiltered++; - - if (tree.isSatisfiedBy(partitionKey, (Row) unfiltered, staticRow)) - { - matches.add((Row) unfiltered); - hasMatch = true; - } - } - } - - // We may not have any non-static row data to filter... - if (!hasMatch) - { - context.rowsFiltered++; - - if (tree.isSatisfiedBy(partitionKey, staticRow, staticRow)) - { - hasMatch = true; - } - } - - if (!hasMatch) - { - // If there are no matches, return an empty partition. If reconciliation is required at the - // coordinator, replica filtering protection may make a second round trip to complete its view - // of the partition. - return null; - } - - // Return all matches found - return matches; - } - - private static class SinglePartitionIterator extends AbstractUnfilteredRowIterator - { - private final Iterator rows; - - public SinglePartitionIterator(UnfilteredRowIterator partition, Row staticRow, Iterator rows) + public TableMetadata metadata() { - super(partition.metadata(), - partition.partitionKey(), - partition.partitionLevelDeletion(), - partition.columns(), - staticRow, - partition.isReverseOrder(), - partition.stats()); - - this.rows = rows; + return controller.metadata(); } @Override - protected Unfiltered computeNext() + public void close() { - return rows.hasNext() ? rows.next() : endOfData(); + FileUtils.closeQuietly(operation); } } @@ -602,17 +573,25 @@ protected Unfiltered computeNext() * The resulting {@link UnfilteredRowIterator} objects are not guaranteed to be in any particular order. It is * the responsibility of the caller to sort the results if necessary. */ - public class ScoreOrderedResultRetriever extends AbstractRetreiver + public class ScoreOrderedResultRetriever extends AbstractIterator implements UnfilteredPartitionIterator { private final ColumnFamilyStore.ViewFragment view; private final List> keyRanges; private final boolean coversFullRing; - private final CloseableIterator scoredPrimaryKeyIterator; + private final CloseableIterator scoredPrimaryKeyIterator; + private final FilterTree filterTree; + private final ReadExecutionController executionController; + private final QueryContext queryContext; + private final long nowInSec; + private final CountFetchedTransformation fetchedRowsCounter; - private final boolean isVectorColumnStatic; private final HashSet processedKeys; private final Queue pendingRows; + // Null indicates we are not sending the synthetic score column to the coordinator + @Nullable + private final ColumnMetadata syntheticScoreColumn; + // The limit requested by the query. We cannot load more than softLimit rows in bulk because we only want // to fetch the topk rows where k is the limit. However, we allow the iterator to fetch more rows than the // soft limit to avoid confusing behavior. When the softLimit is reached, the iterator will fetch one row @@ -620,22 +599,37 @@ public class ScoreOrderedResultRetriever extends AbstractRetreiver private final int softLimit; private int returnedRowCount = 0; - private ScoreOrderedResultRetriever(ReadExecutionController executionController, - QueryViewBuilder.QueryView queryView) - { - super(executionController); - assert queryView.view.size() == 1; - QueryViewBuilder.QueryExpressionView queryExpressionView = queryView.view.stream().findFirst().get(); - this.view = queryExpressionView.computeViewFragment(); - this.keyRanges = queryController.dataRanges().stream().map(DataRange::keyRange).collect(Collectors.toList()); + private ScoreOrderedResultRetriever(CloseableIterator scoredPrimaryKeyIterator, + FilterTree filterTree, + QueryController controller, + ReadExecutionController executionController, + QueryContext queryContext, + long nowInSec, + int limit, + ColumnMetadata orderedColumn) + { + IndexContext context = controller.getOrderer().context; + this.view = controller.getQueryView(context).viewFragment; + this.keyRanges = controller.dataRanges().stream().map(DataRange::keyRange).collect(Collectors.toList()); this.coversFullRing = keyRanges.size() == 1 && RangeUtil.coversFullRing(keyRanges.get(0)); - this.scoredPrimaryKeyIterator = Operation.buildIteratorForOrder(queryController, queryExpressionView); + this.scoredPrimaryKeyIterator = scoredPrimaryKeyIterator; + this.filterTree = filterTree; + this.executionController = executionController; + this.queryContext = queryContext; + this.nowInSec = nowInSec; + this.fetchedRowsCounter = new CountFetchedTransformation(queryContext, nowInSec); + + this.processedKeys = new HashSet<>(limit); + this.pendingRows = new ArrayDeque<>(limit); + this.softLimit = limit; - this.isVectorColumnStatic = queryExpressionView.expression.getIndexTermType().columnMetadata().isStatic(); - this.softLimit = command.limits().count(); - this.processedKeys = new HashSet<>(softLimit); - this.pendingRows = new ArrayDeque<>(softLimit); + // When +score is added on the coordinator side, it's represented as a PrecomputedColumnFilter + // even in a 'SELECT *' because WCF is not capable of representing synthetic columns. + // This can be simplified when we remove ANN_USE_SYNTHETIC_SCORE + var tempColumn = ColumnMetadata.syntheticScoreColumn(orderedColumn, FloatType.instance); + var isScoreFetched = controller.command().columnFilter().fetchesExplicitly(tempColumn); + this.syntheticScoreColumn = isScoreFetched ? tempColumn : null; } @Override @@ -655,22 +649,21 @@ public UnfilteredRowIterator computeNext() private void fillPendingRows() { // Group PKs by source sstable/memtable - Map> groupedKeys = new HashMap<>(); - // We always want to get at least 1. When the vector column is static, we cannot batch because we need to - // retain the score ordering a bit longer. - int rowsToRetrieve = isVectorColumnStatic ? 1 : Math.max(1, softLimit - returnedRowCount); + var groupedKeys = new HashMap>(); + // We always want to get at least 1. + int rowsToRetrieve = Math.max(1, softLimit - returnedRowCount); // We want to get the first unique `rowsToRetrieve` keys to materialize // Don't pass the priority queue here because it is more efficient to add keys in bulk fillKeys(groupedKeys, rowsToRetrieve, null); // Sort the primary keys by PrK order, just in case that helps with cache and disk efficiency - PriorityQueue primaryKeyPriorityQueue = new PriorityQueue<>(groupedKeys.keySet()); + var primaryKeyPriorityQueue = new PriorityQueue<>(groupedKeys.keySet()); // drain groupedKeys into pendingRows while (!groupedKeys.isEmpty()) { - PrimaryKey pk = primaryKeyPriorityQueue.poll(); - List sourceKeys = groupedKeys.remove(pk); - UnfilteredRowIterator partitionIterator = readAndValidatePartition(pk, sourceKeys); + var pk = primaryKeyPriorityQueue.poll(); + var sourceKeys = groupedKeys.remove(pk); + var partitionIterator = readAndValidatePartition(pk, sourceKeys); if (partitionIterator != null) pendingRows.add(partitionIterator); else @@ -682,33 +675,34 @@ private void fillPendingRows() /** * Fills the `groupedKeys` Map with the next `count` unique primary keys that are in the keys produced by calling - * {@link #nextSelectedKeyInRange()}. We map PrimaryKey to a list of PrimaryKeyWithScore because the same + * {@link #nextSelectedKeyInRange()}. We map PrimaryKey to {@literal List} because the same * primary key can be in the result set multiple times, but with different source tables. * @param groupedKeys the map to fill * @param count the number of unique PrimaryKeys to consume from the iterator * @param primaryKeyPriorityQueue the priority queue to add new keys to. If the queue is null, we do not add * keys to the queue. */ - private void fillKeys(Map> groupedKeys, int count, PriorityQueue primaryKeyPriorityQueue) + private void fillKeys(Map> groupedKeys, int count, PriorityQueue primaryKeyPriorityQueue) { int initialSize = groupedKeys.size(); while (groupedKeys.size() - initialSize < count) { - PrimaryKeyWithScore primaryKeyWithScore = nextSelectedKeyInRange(); - if (primaryKeyWithScore == null) + var primaryKeyWithSortKey = nextSelectedKeyInRange(); + if (primaryKeyWithSortKey == null) return; - PrimaryKey nextPrimaryKey = primaryKeyWithScore.primaryKey(); - List accumulator = groupedKeys.computeIfAbsent(nextPrimaryKey, k -> new ArrayList<>()); + var nextPrimaryKey = primaryKeyWithSortKey.primaryKey(); + var accumulator = groupedKeys.computeIfAbsent(nextPrimaryKey, k -> new ArrayList<>()); if (primaryKeyPriorityQueue != null && accumulator.isEmpty()) primaryKeyPriorityQueue.add(nextPrimaryKey); - accumulator.add(primaryKeyWithScore); + accumulator.add(primaryKeyWithSortKey); } } /** * Determine if the key is in one of the queried key ranges. We do not iterate through results in * {@link PrimaryKey} order, so we have to check each range. - * @param key the key to test + * + * @param key a partition key * @return true if the key is in one of the queried key ranges */ private boolean isInRange(DecoratedKey key) @@ -727,12 +721,12 @@ private boolean isInRange(DecoratedKey key) * If the next key falls out of the current key range, it skips to the next key range, and so on. * If no more keys acceptd by the controller are available, returns null. */ - private @Nullable PrimaryKeyWithScore nextSelectedKeyInRange() + private @Nullable PrimaryKeyWithSortKey nextSelectedKeyInRange() { while (scoredPrimaryKeyIterator.hasNext()) { - PrimaryKeyWithScore key = scoredPrimaryKeyIterator.next(); - if (isInRange(key.primaryKey().partitionKey()) && !queryController.doesNotSelect(key.primaryKey())) + var key = scoredPrimaryKeyIterator.next(); + if (isInRange(key.partitionKey()) && controller.selects(key)) return key; } return null; @@ -742,7 +736,7 @@ private boolean isInRange(DecoratedKey key) * Reads and validates a partition for a given primary key against its sources. *

    * @param pk The primary key of the partition to read and validate - * @param sourceKeys A list of PrimaryKeyWithScore objects associated with the primary key. + * @param sourceKeys A list of PrimaryKeyWithSortKey objects associated with the primary key. * Multiple sort keys can exist for the same primary key when data comes from different * sstables or memtables. * @@ -752,7 +746,7 @@ private boolean isInRange(DecoratedKey key) * - The partition contains no valid rows * - The row data does not match the index metadata for any of the provided primary keys */ - public UnfilteredRowIterator readAndValidatePartition(PrimaryKey pk, List sourceKeys) + public UnfilteredRowIterator readAndValidatePartition(PrimaryKey pk, List sourceKeys) { // If we've already processed the key, we can skip it. Because the score ordered iterator does not // deduplicate rows, we could see dupes if a row is in the ordering index multiple times. This happens @@ -760,177 +754,201 @@ public UnfilteredRowIterator readAndValidatePartition(PrimaryKey pk, List clusters = filterPartition(partition, filterTree, queryContext); + UnfilteredRowIterator counted = fetchedRowsCounter.apply(partition); + UnfilteredRowIterator clusters = applyIndexFilter(counted, filterTree); if (clusters == null) - { - // Key counts as processed because the materialized row didn't satisfy the filter logic - processedKeys.add(pk); return null; - } - Row staticRow = partition.staticRow(); - long now = FBUtilities.nowInSeconds(); + var staticRow = partition.staticRow(); + boolean isStaticValid = false; - // If the pk is static, then we must check that the static row satisfies the source key's validity check. - // Otherwise, we need to make sure that we have one row in the cluster result and then we use that - // for checking validity. - Row representativeRow; - if (pk.kind() == PrimaryKey.Kind.STATIC) - { - representativeRow = staticRow; - } - else + // Each of the primary keys are equal, but they have different source tables. + // Therefore, we check to see if the static row is valid for any of them. + for (PrimaryKeyWithSortKey sourceKey : sourceKeys) { - if (clusters.isEmpty()) + if (sourceKey.isIndexDataValid(staticRow, nowInSec)) { - // Key counts as processed because the materialized row didn't satisfy the filter logic - processedKeys.add(pk); - return null; + // If there are no regular rows, return the static row only + if (!clusters.hasNext()) + return new PrimaryKeyIterator(partition, staticRow, null, sourceKey, syntheticScoreColumn, controller.getOrderer(), nowInSec); + + isStaticValid = true; + break; } - representativeRow = clusters.get(0); - assert clusters.size() == 1 : "Expect 1 result row, but got: " + clusters.size(); } - // Each of sourceKeys are equal with respect to primary key equality, but they have different source tables. - // As long as one is valid, we consider the row valid. - for (PrimaryKeyWithScore sourceKey : sourceKeys) + // If the static row isn't valid, we can skip the partition. + if (!isStaticValid) + return null; + + var row = clusters.next(); + if (!row.isRangeTombstoneMarker()) { - assert sourceKey.primaryKey().kind() == pk.kind(); - if (sourceKey.isIndexDataValid(representativeRow, now)) + for (PrimaryKeyWithSortKey sourceKey : sourceKeys) { - processedKeys.add(pk); - return new SinglePartitionIterator(partition, staticRow, clusters.iterator()); + // Each of these primary keys are equal, but they have different source tables. + // Only one can be valid. + if (sourceKey.isIndexDataValid((Row) row, nowInSec)) + { + // We can only count the pk as processed once we know it was valid for one of the + // scored keys. + processedKeys.add(pk); + return new PrimaryKeyIterator(partition, staticRow, row, sourceKey, syntheticScoreColumn, controller.getOrderer(), nowInSec); + } } } - // Key does not count as processed because the only thing that "failed" is the validity check on the - // grouped source keys, and it is possible that the score ordered iterator has the same key in the - // iterator lower. We only get here when a vector's value is updated to a more distant vector, so - // the old value ranks high in the iterator, but isn't the current value for the materialized row. return null; } } + @Override + public TableMetadata metadata() + { + return controller.metadata(); + } + + @Override public void close() { FileUtils.closeQuietly(scoredPrimaryKeyIterator); } - } - /** - * Used by {@link StorageAttachedIndexSearcher#filterReplicaFilteringProtection} to filter rows for columns that - * have transformations so won't get handled correctly by the row filter. - */ - private static PartitionIterator applyIndexFilter(PartitionIterator response, FilterTree tree, QueryContext context) - { - return new PartitionIterator() + public class PrimaryKeyIterator extends AbstractUnfilteredRowIterator { - @Override - public void close() - { - response.close(); - } + private boolean consumed = false; - @Override - public boolean hasNext() - { - return response.hasNext(); - } + @Nullable + private final Unfiltered row; - @Override - public RowIterator next() + public PrimaryKeyIterator(UnfilteredRowIterator partition, + Row staticRow, + @Nullable Unfiltered content, + PrimaryKeyWithSortKey primaryKeyWithSortKey, + ColumnMetadata syntheticScoreColumn, + Orderer orderer, + long nowInSec) { - RowIterator delegate = response.next(); - Row staticRow = delegate.staticRow(); - - // If we only restrict static columns, and we pass the filter, simply pass through the delegate, as all - // non-static rows are matches. If we fail on the filter, no rows are matches, so return nothing. - if (!tree.restrictsNonStaticRow()) - return tree.isSatisfiedBy(delegate.partitionKey(), staticRow, staticRow) ? delegate : null; - - return new RowIterator() + super(partition.metadata(), + partition.partitionKey(), + partition.partitionLevelDeletion(), + partition.columns(), + staticRow, + partition.isReverseOrder(), + partition.stats()); + + if (content == null || !content.isRow() || !(primaryKeyWithSortKey instanceof PrimaryKeyWithScore)) { - Row next; + this.row = content; + return; + } - @Override - public TableMetadata metadata() - { - return delegate.metadata(); - } + if (syntheticScoreColumn == null) + { + this.row = content; + return; + } - @Override - public boolean isReverseOrder() - { - return delegate.isReverseOrder(); - } + // Clone the original Row + Row originalRow = (Row) content; + ArrayList columnData = new ArrayList<>(originalRow.columnCount() + 1); + columnData.addAll(originalRow.columnData()); + + // inject +score as a new column + float score = ((PrimaryKeyWithScore) primaryKeyWithSortKey).getExactScore(orderer, originalRow); + columnData.add(BufferCell.live(syntheticScoreColumn, + nowInSec, + FloatType.instance.decompose(score))); + + this.row = BTreeRow.create(originalRow.clustering(), + originalRow.primaryKeyLivenessInfo(), + originalRow.deletion(), + BTree.builder(ColumnData.comparator) + .auto(true) + .addAll(columnData) + .build()); + } - @Override - public RegularAndStaticColumns columns() - { - return delegate.columns(); - } + @Override + protected Unfiltered computeNext() + { + if (consumed || row == null) + return endOfData(); + consumed = true; + return row; + } + } + } - @Override - public DecoratedKey partitionKey() - { - return delegate.partitionKey(); - } + private static UnfilteredRowIterator applyIndexFilter(UnfilteredRowIterator partition, FilterTree tree) + { + FilteringPartitionIterator filtered = new FilteringPartitionIterator(partition, tree); + if (!filtered.hasNext() && !filtered.matchesStaticRow()) + { + filtered.close(); + return null; + } + return filtered; + } - @Override - public Row staticRow() - { - return staticRow; - } + /** + * Filters the rows in the partition so that only non-static rows that match given filter are returned. + */ + private static class FilteringPartitionIterator extends AbstractUnfilteredRowIterator + { + private final FilterTree filter; + private final UnfilteredRowIterator rows; - @Override - public void close() - { - delegate.close(); - } + private final DecoratedKey key; + private final Row staticRow; - private Row computeNext() - { - while (delegate.hasNext()) - { - Row row = delegate.next(); - context.rowsFiltered++; - if (tree.isSatisfiedBy(delegate.partitionKey(), row, staticRow)) - return row; - } - return null; - } + public FilteringPartitionIterator(UnfilteredRowIterator partition, FilterTree filter) + { + super(partition.metadata(), + partition.partitionKey(), + partition.partitionLevelDeletion(), + partition.columns(), + partition.staticRow(), + partition.isReverseOrder(), + partition.stats()); - private Row loadNext() - { - if (next == null) - next = computeNext(); - return next; - } + this.rows = partition; + this.filter = filter; + this.key = partition.partitionKey(); + this.staticRow = partition.staticRow(); + } - @Override - public boolean hasNext() - { - return loadNext() != null; - } + public boolean matchesStaticRow() + { + return filter.isSatisfiedBy(key, staticRow, staticRow); + } - @Override - public Row next() - { - Row result = loadNext(); - next = null; + @Override + protected Unfiltered computeNext() + { + while (rows.hasNext()) + { + Unfiltered row = rows.next(); - if (result == null) - throw new NoSuchElementException(); + if (!row.isRow() || ((Row)row).isStatic()) + continue; - return result; - } - }; + if (filter.isSatisfiedBy(key, row, staticRow)) + return row; } - }; + return endOfData(); + } + + @Override + public void close() + { + super.close(); + rows.close(); + } } } diff --git a/src/java/org/apache/cassandra/index/sai/plan/TopKProcessor.java b/src/java/org/apache/cassandra/index/sai/plan/TopKProcessor.java new file mode 100644 index 000000000000..6ebab9963056 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/plan/TopKProcessor.java @@ -0,0 +1,364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.plan; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import javax.annotation.Nullable; + +import org.apache.commons.lang3.tuple.Triple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.jbellis.jvector.vector.VectorizationProvider; +import io.github.jbellis.jvector.vector.types.VectorFloat; +import io.github.jbellis.jvector.vector.types.VectorTypeSupport; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.filter.IndexHints; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.BaseRowIterator; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.index.SecondaryIndexManager; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher.ScoreOrderedResultRetriever; +import org.apache.cassandra.index.sai.utils.InMemoryPartitionIterator; +import org.apache.cassandra.index.sai.utils.InMemoryUnfilteredPartitionIterator; +import org.apache.cassandra.index.sai.utils.PartitionInfo; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.TopKSelector; + +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +/** + * Processor applied to SAI based ORDER BY queries. + *

    + * On a replica: + *
      + *
    • filter(ScoreOrderedResultRetriever) is used to collect up to the top-K rows.
    • + *
    • We store any tombstones as well, to avoid losing them during coordinator reconciliation.
    • + *
    • The result is returned in PK order so that coordinator can merge from multiple replicas.
    • + *
    + * On a coordinator: + *
      + *
    • reorder(PartitionIterator) is used to consume all rows from the provided partitions, + * compute the order based on either a column ordering or a similarity score, and keep top-K.
    • + *
    • The result is returned in score/sortkey order.
    • + *
    + */ +public class TopKProcessor +{ + public static final String INDEX_MAY_HAVE_BEEN_DROPPED = "An index may have been dropped. Ordering on non-clustering " + + "column requires the column to be indexed"; + protected static final Logger logger = LoggerFactory.getLogger(TopKProcessor.class); + private static final VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport(); + + private final ReadCommand command; + private final IndexContext indexContext; + private final RowFilter.Expression expression; + private final ColumnMetadata scoreColumn; + // Lazily compute this value, if needed. + private VectorFloat queryVector = null; + + private final int limit; + + public TopKProcessor(ReadCommand command) + { + this.command = command; + + Pair indexAndExpression = findTopKIndexContext(); + // this can happen in case an index was dropped after the query was initiated + if (indexAndExpression == null) + throw invalidRequest(INDEX_MAY_HAVE_BEEN_DROPPED); + + this.indexContext = indexAndExpression.left; + this.expression = indexAndExpression.right; + this.limit = command.limits().count(); + this.scoreColumn = ColumnMetadata.syntheticScoreColumn(expression.column(), FloatType.instance); + } + + /** + * Sort the specified filtered rows according to the {@code ORDER BY} clause and keep the first {@link #limit} rows. + * Called on the coordinator side. + * + * @param partitions the partitions collected by the coordinator. It will be closed as a side-effect. + * @return the provided rows, sorted and trimmed to {@link #limit} rows + */ + public PartitionIterator reorder(PartitionIterator partitions) + { + // We consume the partitions iterator and create a new one. Use a try-with-resources block to ensure the + // original iterator is closed. We do not expect exceptions here, but if they happen, we want to make sure the + // original iterator is closed to prevent leaking resources, which could compound the effect of an exception. + try (partitions) + { + Comparator> comparator = comparator() + .thenComparing(Triple::getLeft, Comparator.comparing(pi -> pi.key)) + .thenComparing(Triple::getMiddle, command.metadata().comparator); + + TopKSelector> topK = new TopKSelector<>(comparator, limit); + while (partitions.hasNext()) + { + try (BaseRowIterator partitionRowIterator = partitions.next()) + { + if (expression.operator() == Operator.ANN || expression.operator() == Operator.BM25) + { + PartitionResults pr = processScoredPartition(partitionRowIterator); + topK.addAll(pr.rows); + } + else + { + while (partitionRowIterator.hasNext()) + { + Row row = (Row) partitionRowIterator.next(); + ByteBuffer value = row.getCell(expression.column()).buffer(); + topK.add(Triple.of(PartitionInfo.create(partitionRowIterator), row, value)); + } + } + } + } + + // Convert the topK results to a PartitionIterator + List> sortedRows = new ArrayList<>(topK.size()); + for (Triple triple : topK.getShared()) + sortedRows.add(Pair.create(triple.getLeft(), triple.getMiddle())); + return InMemoryPartitionIterator.create(command, sortedRows); + } + } + + /** + * Sort the specified unfiltered rows according to the {@code ORDER BY} clause, keep the first {@link #limit} rows, + * and then order them again by primary key. + *

    + * This is meant to be used on the replica-side, before reconciliation. We need to order the rows by primary key + * after the top-k selection to avoid confusing reconciliation later, on the coordinator. Note that due to sstable + * overlap and how the full data set of each node is queried for top-k queries we can have multiple versions of the + * same row in the coordinator even with CL=ONE. Reconciliation should remove those duplicates, but it needs the + * rows to be ordered by primary key to do so. See CNDB-12308 for details. + *

    + * All tombstones will be kept. Caller must close the supplied iterator. + * + * @param partitions the partitions collected in the replica side of a query. It will be closed as a side-effect. + * @return the provided rows, sorted by the requested {@code ORDER BY} chriteria, trimmed to {@link #limit} rows, + * and the sorted again by primary key. + */ + public UnfilteredPartitionIterator filter(ScoreOrderedResultRetriever partitions) + { + try (partitions) + { + TreeMap> unfilteredByPartition = new TreeMap<>(Comparator.comparing(pi -> pi.key)); + + int rowsMatched = 0; + // Because each “partition” from ScoreOrderedResultRetriever is actually a single row + // or tombstone, we can simply read them until we have enough. + while (rowsMatched < limit && partitions.hasNext()) + { + try (BaseRowIterator partitionRowIterator = partitions.next()) + { + rowsMatched += processSingleRowPartition(unfilteredByPartition, partitionRowIterator); + } + } + + return new InMemoryUnfilteredPartitionIterator(command, unfilteredByPartition); + } + } + + /** + * Constructs a comparator for triple (PartitionInfo, Row, X) used for top-K ranking. + * For ANN/BM25 we compare descending by X (float score). For ORDER_BY_ASC or DESC, + * we compare ascending/descending by the row’s relevant ByteBuffer data. + */ + private Comparator> comparator() + { + if (expression.operator() == Operator.ANN || expression.operator() == Operator.BM25) + { + // For similarity, higher is better, so reversed + return Comparator.comparing((Triple t) -> (Float) t.getRight()).reversed(); + } + + Comparator> comparator = Comparator.comparing(t -> (ByteBuffer) t.getRight(), indexContext.getValidator()); + if (expression.operator() == Operator.ORDER_BY_DESC) + comparator = comparator.reversed(); + return comparator; + } + + /** + * Simple holder for partial results of a single partition (score-based path). + */ + private class PartitionResults + { + final PartitionInfo partitionInfo; + final SortedSet tombstones = new TreeSet<>(command.metadata().comparator); + final List> rows = new ArrayList<>(); + + PartitionResults(PartitionInfo partitionInfo) + { + this.partitionInfo = partitionInfo; + } + + void addTombstone(Unfiltered uf) + { + tombstones.add(uf); + } + + void addRow(Triple triple) + { + rows.add(triple); + } + } + + /** + * Processes all rows in a single partition to compute scores (for ANN or BM25) + */ + private PartitionResults processScoredPartition(BaseRowIterator partitionRowIterator) + { + // Compute key and static row score once per partition + DecoratedKey key = partitionRowIterator.partitionKey(); + Row staticRow = partitionRowIterator.staticRow(); + PartitionInfo partitionInfo = PartitionInfo.create(partitionRowIterator); + float keyAndStaticScore = getScoreForRow(key, staticRow); + PartitionResults pr = new PartitionResults(partitionInfo); + + if (!partitionRowIterator.hasNext()) + pr.addRow(Triple.of(partitionInfo, BTreeRow.emptyRow(Clustering.EMPTY), keyAndStaticScore)); + + while (partitionRowIterator.hasNext()) + { + Unfiltered unfiltered = partitionRowIterator.next(); + // Always include tombstones for coordinator. It relies on ReadCommand#withMetricsRecording to throw + // TombstoneOverwhelmingException to prevent OOM. + if (unfiltered.isRangeTombstoneMarker()) + { + pr.addTombstone(unfiltered); + continue; + } + + Row row = (Row) unfiltered; + float rowScore = getScoreForRow(null, row); + pr.addRow(Triple.of(partitionInfo, row, keyAndStaticScore + rowScore)); + } + + return pr; + } + + /** + * Processes a single partition, without scoring it. + */ + private int processSingleRowPartition(TreeMap> unfilteredByPartition, + BaseRowIterator partitionRowIterator) + { + Unfiltered unfiltered = partitionRowIterator.hasNext() ? partitionRowIterator.next() : null; + assert !partitionRowIterator.hasNext() : "Only one row should be returned"; + // Always include tombstones for coordinator. It relies on ReadCommand#withMetricsRecording to throw + // TombstoneOverwhelmingException to prevent OOM. + PartitionInfo partitionInfo = PartitionInfo.create(partitionRowIterator); + addUnfiltered(unfilteredByPartition, partitionInfo, unfiltered); + return unfiltered != null && unfiltered.isRangeTombstoneMarker() ? 0 : 1; + } + + private void addUnfiltered(SortedMap> unfilteredByPartition, + PartitionInfo partitionInfo, + Unfiltered unfiltered) + { + var map = unfilteredByPartition.computeIfAbsent(partitionInfo, k -> new TreeSet<>(command.metadata().comparator)); + if (unfiltered != null) + map.add(unfiltered); + } + + private float getScoreForRow(DecoratedKey key, Row row) + { + ColumnMetadata column = indexContext.getDefinition(); + + if (column.isPartitionKey() && key == null) + return 0; + + if (column.isStatic() && !row.isStatic()) + return 0; + + if ((column.isClusteringColumn() || column.isRegular()) && row.isStatic()) + return 0; + + // If we have a synthetic score column, use it + var scoreData = row.getColumnData(scoreColumn); + if (scoreData != null) + { + var cell = (Cell) scoreData; + return FloatType.instance.compose(cell.buffer()); + } + + // TODO remove this once we enable Ordering.Ann.USE_SYNTHETIC_SCORE + ByteBuffer value = indexContext.getValueOf(key, row, FBUtilities.nowInSeconds()); + if (value != null) + { + if (queryVector == null) + queryVector = vts.createFloatVector(TypeUtil.decomposeVector(indexContext, expression.getIndexValue().duplicate())); + var vector = vts.createFloatVector(TypeUtil.decomposeVector(indexContext, value)); + return indexContext.getIndexWriterConfig().getSimilarityFunction().compare(vector, queryVector); + } + return 0; + } + + private Pair findTopKIndexContext() + { + ColumnFamilyStore cfs = Keyspace.openAndGetStore(command.metadata()); + + for (RowFilter.Expression expression : command.rowFilter().expressions()) + { + StorageAttachedIndex sai = findOrderingIndexFor(cfs.indexManager, expression); + if (sai != null) + return Pair.create(sai.getIndexContext(), expression); + } + + return null; + } + + @Nullable + private StorageAttachedIndex findOrderingIndexFor(SecondaryIndexManager sim, RowFilter.Expression e) + { + if (e.operator() != Operator.ANN + && e.operator() != Operator.BM25 + && e.operator() != Operator.ORDER_BY_ASC + && e.operator() != Operator.ORDER_BY_DESC) + { + return null; + } + + IndexHints hints = command.rowFilter().indexHints; + return sim.getBestIndexFor(e, hints, StorageAttachedIndex.class).orElse(null); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/VectorTopKProcessor.java b/src/java/org/apache/cassandra/index/sai/plan/VectorTopKProcessor.java deleted file mode 100644 index d9ed16aeb940..000000000000 --- a/src/java/org/apache/cassandra/index/sai/plan/VectorTopKProcessor.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.plan; - -import java.nio.ByteBuffer; -import java.util.Comparator; -import java.util.PriorityQueue; -import java.util.TreeMap; -import java.util.TreeSet; -import javax.annotation.Nullable; - -import com.google.common.base.Preconditions; -import org.apache.commons.lang3.tuple.Triple; - -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.ReadCommand; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.partitions.BasePartitionIterator; -import org.apache.cassandra.db.partitions.PartitionIterator; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.rows.BaseRowIterator; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.rows.Unfiltered; -import org.apache.cassandra.index.SecondaryIndexManager; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.utils.InMemoryPartitionIterator; -import org.apache.cassandra.index.sai.utils.InMemoryUnfilteredPartitionIterator; -import org.apache.cassandra.index.sai.utils.IndexTermType; -import org.apache.cassandra.index.sai.utils.PartitionInfo; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; - -/** - * Processor that scans all rows from given partitions and selects rows with top-k scores based on vector indexes. - *

    - * This processor performs the following steps: - * - collect rows with score into {@link PriorityQueue} that sorts rows based on score. If there are multiple vector indexes, - * the final score is the sum of all vector index scores. - * - remove rows with the lowest scores from PQ if PQ size exceeds limit - * - return rows from PQ in primary key order to client - */ -public class VectorTopKProcessor -{ - private final ReadCommand command; - private final StorageAttachedIndex index; - private final IndexTermType indexTermType; - private final float[] queryVector; - - private final int limit; - - public VectorTopKProcessor(ReadCommand command) - { - this.command = command; - - Pair annIndexAndExpression = findTopKIndex(); - Preconditions.checkNotNull(annIndexAndExpression); - - this.index = annIndexAndExpression.left; - this.indexTermType = annIndexAndExpression.left().termType(); - this.queryVector = annIndexAndExpression.right; - this.limit = command.limits().count(); - } - - /** - * Filter given partitions and keep the rows with the highest scores. In case of {@link UnfilteredPartitionIterator}, - * all tombstones will be kept. - */ - public , P extends BasePartitionIterator> BasePartitionIterator consumeSortByScoreAndTakeTopK(P partitions) - { - // priority queue ordered by score in ascending order - PriorityQueue> topK = new PriorityQueue<>(limit + 1, Comparator.comparing(Triple::getRight)); - // to store top-k results in primary key order - TreeMap> unfilteredByPartition = new TreeMap<>(Comparator.comparing(p -> p.key)); - - while (partitions.hasNext()) - { - try (R partition = partitions.next()) - { - DecoratedKey key = partition.partitionKey(); - Row staticRow = partition.staticRow(); - PartitionInfo partitionInfo = PartitionInfo.create(partition); - // compute key and static row score once per partition - float keyAndStaticScore = getScoreForRow(key, staticRow); - - while (partition.hasNext()) - { - Unfiltered unfiltered = partition.next(); - // Always include tombstones for coordinator. It relies on ReadCommand#withMetricsRecording to throw - // TombstoneOverwhelmingException to prevent OOM. - if (!unfiltered.isRow()) - { - unfilteredByPartition.computeIfAbsent(partitionInfo, k -> new TreeSet<>(command.metadata().comparator)) - .add(unfiltered); - continue; - } - - Row row = (Row) unfiltered; - float rowScore = getScoreForRow(null, row); - topK.add(Triple.of(partitionInfo, row, keyAndStaticScore + rowScore)); - - // when exceeding limit, remove row with low score - while (topK.size() > limit) - topK.poll(); - } - } - } - partitions.close(); - - // reorder rows in partition/clustering order - for (Triple triple : topK) - unfilteredByPartition.computeIfAbsent(triple.getLeft(), k -> new TreeSet<>(command.metadata().comparator)) - .add(triple.getMiddle()); - - if (partitions instanceof PartitionIterator) - return new InMemoryPartitionIterator(command, unfilteredByPartition); - return new InMemoryUnfilteredPartitionIterator(command, unfilteredByPartition); - } - - /** - * Sum the scores from different vector indexes for the row - */ - private float getScoreForRow(DecoratedKey key, Row row) - { - ColumnMetadata column = indexTermType.columnMetadata(); - - if (column.isPrimaryKeyColumn() && key == null) - return 0; - - if (column.isStatic() && !row.isStatic()) - return 0; - - if ((column.isClusteringColumn() || column.isRegular()) && row.isStatic()) - return 0; - - ByteBuffer value = indexTermType.valueOf(key, row, FBUtilities.nowInSeconds()); - if (value != null) - { - float[] vector = indexTermType.decomposeVector(value); - return index.indexWriterConfig().getSimilarityFunction().compare(vector, queryVector); - } - return 0; - } - - /** - * Filter given partitions and keep the rows with the highest scores. In case of {@link UnfilteredPartitionIterator}, - * all tombstones will be kept. - */ - public , P extends BasePartitionIterator> BasePartitionIterator takeTopKThenSortByPrimaryKey(P partitions) - { - try (partitions) - { - TreeMap> unfilteredByPartition = new TreeMap<>(Comparator.comparing(pi -> pi.key)); - - int rowsMatched = 0; - while (rowsMatched < limit && partitions.hasNext()) - { - try (BaseRowIterator partitionRowIterator = partitions.next()) - { - rowsMatched += processSingleRowPartition(unfilteredByPartition, partitionRowIterator, limit - rowsMatched); - } - } - - return new InMemoryUnfilteredPartitionIterator(command, unfilteredByPartition); - } - } - - /** - * Processes a single partition, without scoring it. - */ - private int processSingleRowPartition(TreeMap> unfilteredByPartition, - BaseRowIterator partitionRowIterator, - int reamining) - { - if (!partitionRowIterator.hasNext()) - return 0; - - // Always include tombstones for coordinator. It relies on ReadCommand#withMetricsRecording to throw - // TombstoneOverwhelmingException to prevent OOM. - PartitionInfo partitionInfo = PartitionInfo.create(partitionRowIterator); - TreeSet map = unfilteredByPartition.computeIfAbsent(partitionInfo, k -> new TreeSet<>(command.metadata().comparator)); - int added = 0; - while (partitionRowIterator.hasNext() && added < reamining) - { - Unfiltered unfiltered = partitionRowIterator.next(); - map.add(unfiltered); - if (unfiltered.isRow()) - added++; - } - return added; - } - - private Pair findTopKIndex() - { - ColumnFamilyStore cfs = Keyspace.openAndGetStore(command.metadata()); - - for (RowFilter.Expression expression : command.rowFilter().getExpressions()) - { - StorageAttachedIndex sai = findVectorIndexFor(cfs.indexManager, expression); - if (sai != null) - { - float[] qv = sai.termType().decomposeVector(expression.getIndexValue().duplicate()); - return Pair.create(sai, qv); - } - } - - return null; - } - - @Nullable - private StorageAttachedIndex findVectorIndexFor(SecondaryIndexManager sim, RowFilter.Expression e) - { - if (e.operator() != Operator.ANN) - return null; - - return sim.getBestIndexFor(e, StorageAttachedIndex.class).orElse(null); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/postings/IntArrayPostingList.java b/src/java/org/apache/cassandra/index/sai/postings/IntArrayPostingList.java index 24ef9fa737a1..7dd20c502c55 100644 --- a/src/java/org/apache/cassandra/index/sai/postings/IntArrayPostingList.java +++ b/src/java/org/apache/cassandra/index/sai/postings/IntArrayPostingList.java @@ -17,8 +17,12 @@ */ package org.apache.cassandra.index.sai.postings; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import org.apache.cassandra.index.sai.disk.PostingList; +import org.apache.cassandra.index.sai.disk.v1.postings.OrdinalPostingList; + public class IntArrayPostingList implements OrdinalPostingList { private final int[] postings; @@ -30,13 +34,13 @@ public IntArrayPostingList(int[] postings) } @Override - public long getOrdinal() + public int getOrdinal() { return idx; } @Override - public long nextPosting() + public int nextPosting() { if (idx >= postings.length) { @@ -46,13 +50,13 @@ public long nextPosting() } @Override - public long size() + public int size() { return postings.length; } @Override - public long advance(long targetRowID) + public int advance(int targetRowID) { for (int i = idx; i < postings.length; ++i) { @@ -77,6 +81,12 @@ public String toString() .toString(); } + @VisibleForTesting + public void reset() + { + idx = 0; + } + public int getPostingAt(int i) { return postings[i]; diff --git a/src/java/org/apache/cassandra/index/sai/postings/PeekablePostingList.java b/src/java/org/apache/cassandra/index/sai/postings/PeekablePostingList.java deleted file mode 100644 index 02a3ae49cb43..000000000000 --- a/src/java/org/apache/cassandra/index/sai/postings/PeekablePostingList.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.postings; - -import java.io.IOException; - -import javax.annotation.concurrent.NotThreadSafe; - -import org.apache.cassandra.utils.Throwables; - -/** - * A peekable wrapper around a {@link PostingList} that allows the next value to be - * looked at without advancing the state of the {@link PostingList} - */ -@NotThreadSafe -public class PeekablePostingList implements PostingList -{ - private final PostingList wrapped; - - private boolean peeked = false; - private long next; - - public static PeekablePostingList makePeekable(PostingList postingList) - { - return postingList instanceof PeekablePostingList ? (PeekablePostingList) postingList - : new PeekablePostingList(postingList); - } - - private PeekablePostingList(PostingList wrapped) - { - this.wrapped = wrapped; - } - - public long peek() - { - if (peeked) - return next; - - try - { - peeked = true; - return next = wrapped.nextPosting(); - } - catch (IOException e) - { - throw Throwables.cleaned(e); - } - } - - public void advanceWithoutConsuming(long targetRowID) throws IOException - { - if (peek() == END_OF_STREAM) - return; - - if (peek() >= targetRowID) - { - peek(); - return; - } - - peeked = true; - next = wrapped.advance(targetRowID); - } - - @Override - public long minimum() - { - return wrapped.maximum(); - } - - @Override - public long maximum() - { - return wrapped.maximum(); - } - - @Override - public long nextPosting() throws IOException - { - if (peeked) - { - peeked = false; - return next; - } - return wrapped.nextPosting(); - } - - @Override - public long size() - { - return wrapped.size(); - } - - @Override - public long advance(long targetRowID) throws IOException - { - if (peeked && next >= targetRowID) - { - peeked = false; - return next; - } - - peeked = false; - return wrapped.advance(targetRowID); - } - - @Override - public void close() - { - wrapped.close(); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/postings/PostingList.java b/src/java/org/apache/cassandra/index/sai/postings/PostingList.java deleted file mode 100644 index 9c6485db903a..000000000000 --- a/src/java/org/apache/cassandra/index/sai/postings/PostingList.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.postings; - -import java.io.Closeable; -import java.io.IOException; - -/** - * Interface for advancing on and consuming a posting list. - */ -public interface PostingList extends Closeable -{ - PostingList EMPTY = new EmptyPostingList(); - - long OFFSET_NOT_FOUND = -1; - long END_OF_STREAM = Long.MAX_VALUE; - - @Override - default void close() {} - - default long minimum() - { - return Long.MIN_VALUE; - } - - default long maximum() - { - return Long.MAX_VALUE; - } - - /** - * Retrieves the next segment row ID, not including row IDs that have been returned by {@link #advance(long)}. - * - * @return next segment row ID - */ - long nextPosting() throws IOException; - - /** - * Returns the upper bound of postings in the list. During a merge individual postings may be - * de-duplicated, so we can't return the exact size only the upper bound of the size. - */ - long size(); - - /** - * Advances to the first row ID beyond the current that is greater than or equal to the - * target, and returns that row ID. Exhausts the iterator and returns {@link #END_OF_STREAM} if - * the target is greater than the highest row ID. - *

    - * Note: Callers must use the return value of this method before calling {@link #nextPosting()}, as calling - * that method will return the next posting, not the one to which we have just advanced. - * - * @param targetRowID target row ID to advance to - * - * @return first segment row ID which is >= the target row ID or {@link PostingList#END_OF_STREAM} if one does not exist - */ - long advance(long targetRowID) throws IOException; - - class EmptyPostingList implements PostingList - { - @Override - public long nextPosting() throws IOException - { - return END_OF_STREAM; - } - - @Override - public long size() - { - return 0; - } - - @Override - public long advance(long targetRowID) throws IOException - { - return END_OF_STREAM; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sai/utils/AbortedOperationException.java b/src/java/org/apache/cassandra/index/sai/utils/AbortedOperationException.java new file mode 100644 index 000000000000..072110808721 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/AbortedOperationException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + + +/** + * This exception indicates that a request was aborted, normally because it was taking too much time. + * + * It is handled in a special way by the verb handlers and the request execute method: it is simply + * passed to the onAborted callback without logging any message. Therefore if any logging is required, + * it is up to the code raising this exception to log anything. + */ +// TODO OSS doesn't support onAbort and timeout response +public class AbortedOperationException extends RuntimeException +{ + public AbortedOperationException() + { + super(); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/utils/AtomicRatio.java b/src/java/org/apache/cassandra/index/sai/utils/AtomicRatio.java index 76cab3d302ae..6e91c88ddcba 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/AtomicRatio.java +++ b/src/java/org/apache/cassandra/index/sai/utils/AtomicRatio.java @@ -21,45 +21,33 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -/** - * AtomicRatio provides thread safe operations to maintain a {@link Ratio} of a numerator and denominator. - * The ratio can be updated atomically by multiple threads calling the {@link #update(long, long)} method. - * The current ratio value can be retrieved via the {@link #get} method. - *

    - * The class also provides a thread safe {@link #updateCount} that maintains the number of times the {@link Ratio} - * has been updated. This can be used to determine whether the {@link Ratio} is useful based on the number of updates. - */ public class AtomicRatio { private final AtomicReference ratio = new AtomicReference<>(new Ratio(0, 0)); private final AtomicInteger updateCount = new AtomicInteger(); - private static class Ratio - { + private static class Ratio { public final long numerator; public final long denominator; - public Ratio(long numerator, long denominator) - { + public Ratio(long numerator, long denominator) { this.numerator = numerator; this.denominator = denominator; } } - public void update(long numerator, long denominator) - { - ratio.updateAndGet((current) -> new Ratio(current.numerator + numerator, current.denominator + denominator)); + public double updateAndGet(long numerator, long denominator) { + Ratio updated = ratio.updateAndGet((current) -> new Ratio(current.numerator + numerator, current.denominator + denominator)); updateCount.incrementAndGet(); + return (double) updated.numerator / updated.denominator; } - public double get() - { + public double get() { Ratio current = ratio.get(); return (double) current.numerator / current.denominator; } - public int getUpdateCount() - { + public int getUpdateCount() { return updateCount.get(); } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/BM25Utils.java b/src/java/org/apache/cassandra/index/sai/utils/BM25Utils.java new file mode 100644 index 000000000000..14391a7d2706 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/BM25Utils.java @@ -0,0 +1,249 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.utils; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import io.github.jbellis.jvector.graph.NodeQueue; +import io.github.jbellis.jvector.util.BoundedLongHeap; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.CloseableIterator; + +public class BM25Utils +{ + private static final float K1 = 1.2f; // BM25 term frequency saturation parameter + private static final float B = 0.75f; // BM25 length normalization parameter + + /** + * Term frequencies within a single document. All instances of a term are counted. Allows us to optimize for + * the sstable use case, which is able to skip some reads from disk as well as some memory allocations. + */ + public interface DocTF + { + int getTermFrequency(ByteBuffer term); + int termCount(); + PrimaryKeyWithSortKey primaryKey(IndexContext context, Memtable source, float score); + PrimaryKeyWithSortKey primaryKey(IndexContext context, SSTableId source, float score); + } + + /** + * Term frequencies within a single document. All instances of a term are counted. It is eager in that the + * PrimaryKey is already created. + */ + public static class EagerDocTF implements DocTF + { + private final PrimaryKey pk; + private final Map frequencies; + private final int termCount; + + public EagerDocTF(PrimaryKey pk, int termCount, Map frequencies) + { + this.pk = pk; + this.frequencies = frequencies; + this.termCount = termCount; + } + + public int getTermFrequency(ByteBuffer term) + { + return frequencies.getOrDefault(term, 0); + } + + public int termCount() + { + return termCount; + } + + public PrimaryKeyWithSortKey primaryKey(IndexContext context, Memtable source, float score) + { + return new PrimaryKeyWithScore(context, source, pk, score); + } + + public PrimaryKeyWithSortKey primaryKey(IndexContext context, SSTableId source, float score) + { + // BM25 scores are exact. + return new PrimaryKeyWithScore(context, source, pk, score, false); + } + + @Nullable + public static DocTF createFromDocument(PrimaryKey pk, + Cell cell, + AbstractAnalyzer docAnalyzer, + Collection queryTerms) + { + if (cell == null) + return null; + + int count = 0; + Map frequencies = new HashMap<>(); + docAnalyzer.reset(cell.buffer()); + try + { + while (docAnalyzer.hasNext()) + { + ByteBuffer term = docAnalyzer.next(); + count++; + if (queryTerms.contains(term)) + frequencies.merge(term, 1, Integer::sum); + } + } + finally + { + docAnalyzer.end(); + } + + // Every query term must be present in the document + if (queryTerms.size() > frequencies.size()) + return null; + + return new EagerDocTF(pk, count, frequencies); + } + } + + public static CloseableIterator computeScores(CloseableIterator docIterator, + List queryTerms, + DocBm25Stats docStats, + IndexContext indexContext, + Object source, + boolean isOldFormat) + { + assert source instanceof Memtable || source instanceof SSTableId : "Invalid source " + source.getClass(); + + // data structures for document stats and frequencies + ArrayList documents = new ArrayList<>(); + double totalTermCount = 0; + + // Compute TF within each document + while (docIterator.hasNext()) + { + var tf = docIterator.next(); + documents.add(tf); + if (isOldFormat) + totalTermCount += tf.termCount(); + } + + // An index format before {@link Version#ED} doesn't store the total term count + // on the disk to read it back. Thus, for the old format version it is calculated in the old way. + double avgDocLength = (isOldFormat && !documents.isEmpty()) + ? totalTermCount / documents.size() + : docStats.getAvgDocLength(); + + if (documents.isEmpty()) + return CloseableIterator.emptyIterator(); + + // Calculate BM25 scores. + // Uses a NodeQueue that avoids allocating an object for each document. + var nodeQueue = new NodeQueue(new BoundedLongHeap(documents.size()), NodeQueue.Order.MAX_HEAP); + // Create an anonymous NodeScoreIterator that holds the logic for computing BM25 + var iter = new NodeQueue.NodeScoreIterator() { + int current = 0; + + @Override + public boolean hasNext() { + return current < documents.size(); + } + + @Override + public int pop() { + return current++; + } + + @Override + public float topScore() { + // Compute BM25 for the current document + return scoreDoc(documents.get(current), + docStats.getFrequencies(), docStats.getDocCount(), avgDocLength, + queryTerms); + } + }; + // pushMany is an O(n) operation where n is the final size of the queue. Iterative calls to push is O(n log n). + nodeQueue.pushMany(iter, documents.size()); + + return new NodeQueueDocTFIterator(nodeQueue, documents, indexContext, source, docIterator); + } + + private static float scoreDoc(DocTF doc, Map frequencies, long docCount, double avgDocLength, List queryTerms) + { + double score = 0.0; + for (var queryTerm : queryTerms) + { + int tf = doc.getTermFrequency(queryTerm); + Long df = frequencies.get(queryTerm); + // we shouldn't have more hits for a term than we counted total documents + assert df <= docCount : String.format("df=%d, totalDocs=%d", df, docCount); + + double normalizedTf = tf / (tf + K1 * (1 - B + B * doc.termCount() / avgDocLength)); + double idf = Math.log(1 + (docCount - df + 0.5) / (df + 0.5)); + double deltaScore = normalizedTf * idf; + assert deltaScore >= 0 : String.format("BM25 score for tf=%d, df=%d, tc=%d, totalDocs=%d is %f", + tf, df, doc.termCount(), docCount, deltaScore); + score += deltaScore; + } + return (float) score; + } + + private static class NodeQueueDocTFIterator extends AbstractIterator + { + private final NodeQueue nodeQueue; + private final List documents; + private final IndexContext indexContext; + private final Object source; + private final CloseableIterator docIterator; + + NodeQueueDocTFIterator(NodeQueue nodeQueue, List documents, IndexContext indexContext, Object source, CloseableIterator docIterator) + { + this.nodeQueue = nodeQueue; + this.documents = documents; + this.indexContext = indexContext; + this.source = source; + this.docIterator = docIterator; + } + + @Override + protected PrimaryKeyWithSortKey computeNext() + { + if (nodeQueue.size() == 0) + return endOfData(); + + var score = nodeQueue.topScore(); + var node = nodeQueue.pop(); + var doc = documents.get(node); + if (source instanceof Memtable) + return doc.primaryKey(indexContext, (Memtable) source, score); + else + return doc.primaryKey(indexContext, (SSTableId) source, score); + } + + @Override + public void close() + { + FileUtils.closeQuietly(docIterator); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java b/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java index 858e541e22c3..891058c24c94 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java +++ b/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java @@ -221,7 +221,7 @@ public Cell purgeDataOlderThan(long timestamp) } @Override - protected int localDeletionTimeAsUnsignedInt() + public int localDeletionTimeAsUnsignedInt() { // Cannot call cell's localDeletionTimeAsUnsignedInt() because it's protected. throw new UnsupportedOperationException(); @@ -233,6 +233,18 @@ public long maxTimestamp() return cell.maxTimestamp(); } + @Override + public long minTimestamp() + { + return cell.minTimestamp(); + } + + @Override + public int liveDataSize(long nowInSec) + { + return cell.liveDataSize(nowInSec); + } + private Cell wrapIfNew(Cell maybeNewCell) { if (maybeNewCell == null) diff --git a/src/java/org/apache/cassandra/index/sai/utils/CellWithSourceTable.java b/src/java/org/apache/cassandra/index/sai/utils/CellWithSourceTable.java new file mode 100644 index 000000000000..041abded0496 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/CellWithSourceTable.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.nio.ByteBuffer; + +import org.apache.cassandra.db.DeletionPurger; +import org.apache.cassandra.db.Digest; +import org.apache.cassandra.db.marshal.ValueAccessor; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.memory.ByteBufferCloner; + +/** + * A wrapped {@link Cell} that includes a reference to the cell's source table. + * @param the type of the cell's value + */ +public class CellWithSourceTable extends Cell +{ + private final Cell cell; + private final Object sourceTable; + + public CellWithSourceTable(Cell cell, Object sourceTable) + { + super(cell.column()); + this.cell = cell; + this.sourceTable = sourceTable; + } + + public Object sourceTable() + { + return sourceTable; + } + + @Override + public boolean isCounterCell() + { + return cell.isCounterCell(); + } + + @Override + public T value() + { + return cell.value(); + } + + @Override + public ValueAccessor accessor() + { + return cell.accessor(); + } + + @Override + public long timestamp() + { + return cell.timestamp(); + } + + @Override + public int ttl() + { + return cell.ttl(); + } + + @Override + public long localDeletionTime() + { + return cell.localDeletionTime(); + } + + @Override + public boolean isTombstone() + { + return cell.isTombstone(); + } + + @Override + public boolean isExpiring() + { + return cell.isExpiring(); + } + + @Override + public boolean isLive(long nowInSec) + { + return cell.isLive(nowInSec); + } + + @Override + public CellPath path() + { + return cell.path(); + } + + @Override + public Cell withUpdatedColumn(ColumnMetadata newColumn) + { + return wrapIfNew(cell.withUpdatedColumn(newColumn)); + } + + @Override + public Cell withUpdatedValue(ByteBuffer newValue) + { + return wrapIfNew(cell.withUpdatedValue(newValue)); + } + + @Override + public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, long newLocalDeletionTime) + { + return wrapIfNew(cell.withUpdatedTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime)); + } + + @Override + public Cell withSkippedValue() + { + return wrapIfNew(cell.withSkippedValue()); + } + + @Override + public Cell clone(ByteBufferCloner cloner) + { + return wrapIfNew(cell.clone(cloner)); + } + + @Override + public int dataSize() + { + return cell.dataSize(); + } + + @Override + public int liveDataSize(long nowInSec) + { + return cell.liveDataSize(nowInSec); + } + + @Override + public long unsharedHeapSizeExcludingData() + { + return cell.unsharedHeapSizeExcludingData(); + } + + @Override + public long unsharedHeapSize() + { + return cell.unsharedHeapSize(); + } + + @Override + public void validate() + { + cell.validate(); + } + + @Override + public boolean hasInvalidDeletions() + { + return cell.hasInvalidDeletions(); + } + + @Override + public void digest(Digest digest) + { + cell.digest(digest); + } + + @Override + public ColumnData updateAllTimestamp(long newTimestamp) + { + var maybeNewCell = cell.updateAllTimestamp(newTimestamp); + if (maybeNewCell instanceof Cell) + return wrapIfNew((Cell) maybeNewCell); + if (maybeNewCell instanceof ComplexColumnData) + return ((ComplexColumnData) maybeNewCell).transform(this::wrapIfNew); + // It's not clear when we would hit this code path, but it seems we should not + // hit this from SAI. + throw new IllegalStateException("Expected a Cell instance, but got " + maybeNewCell); + } + + @Override + public Cell markCounterLocalToBeCleared() + { + return wrapIfNew(cell.markCounterLocalToBeCleared()); + } + + @Override + public Cell purge(DeletionPurger purger, long nowInSec) + { + return wrapIfNew(cell.purge(purger, nowInSec)); + } + + @Override + public Cell purgeDataOlderThan(long timestamp) + { + return wrapIfNew(cell.purgeDataOlderThan(timestamp)); + } + + @Override + public int localDeletionTimeAsUnsignedInt() + { + return cell.localDeletionTimeAsUnsignedInt(); + } + + @Override + public long maxTimestamp() + { + return cell.maxTimestamp(); + } + + @Override + public long minTimestamp() + { + return cell.minTimestamp(); + } + + private Cell wrapIfNew(Cell maybeNewCell) + { + if (maybeNewCell == null) + return null; + // If the cell's method returned a reference to the same cell, then + // we can skip creating a new wrapper. + if (maybeNewCell == this.cell) + return this; + return new CellWithSourceTable<>(maybeNewCell, sourceTable); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/DocBm25Stats.java b/src/java/org/apache/cassandra/index/sai/utils/DocBm25Stats.java new file mode 100644 index 000000000000..e63e363bacdf --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/DocBm25Stats.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.utils; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.utils.Pair; + +/** + * Term frequencies across all documents. Each document is only counted once. + */ +public class DocBm25Stats +{ + public Map getFrequencies() + { + return frequencies; + } + + private final Map frequencies = new HashMap<>(); + private long docCount; + private long totalTermCount; + private double avgDocLength; + + public long getDocCount() + { + return docCount; + } + + public double getAvgDocLength() + { + return avgDocLength; + } + + public void add(long docCount, long totalTermCount, List> termAndExpressions, DocumentFrequencyEstimator estimator) + { + this.docCount += docCount; + this.totalTermCount += totalTermCount; + if (this.docCount > 0 && this.totalTermCount > 0) + this.avgDocLength = (double) this.totalTermCount / this.docCount; + for (Pair pair : termAndExpressions) + frequencies.merge(pair.left, + Math.min(estimator.estimate(pair.right), docCount), + Long::sum); + } + + @FunctionalInterface + public interface DocumentFrequencyEstimator + { + long estimate(Expression predicate); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/GeoUtil.java b/src/java/org/apache/cassandra/index/sai/utils/GeoUtil.java new file mode 100644 index 000000000000..2510df17f3c1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/GeoUtil.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +public class GeoUtil +{ + // The distance in meters between two lines of longitude at the equator. We round down slightly to be more conservative + // and therefore include more results. + private static final double DISTANCE_PER_DEGREE_LONGITUDE_AT_EQUATOR = 110_000; + + /** + * Determines the worst ratio for meters to degrees for a given latitude. The worst ratio will be the distance in + * meters of 1 degree longitude. + * @param lat the search latitude + * @return + */ + private static double metersToDegreesRatioForLatitude(float lat) + { + // Got this formula from https://sciencing.com/what-parallels-maps-4689046.html. It seems + // to produce accurate results, but it'd be good to find additional support for its correctness. + return Math.cos(Math.toRadians(lat)) * DISTANCE_PER_DEGREE_LONGITUDE_AT_EQUATOR; + } + + /** + * Calculate the maximum bound for a squared distance between lat/long points on the earth. The result is + * increased proportionally to the latitude of the search vector because the distance between two lines of + * longitude decreases as you move away from the equator. + * @param vector search vector + * @param distanceInMeters the search radius + * @return the threshold to use for the given geo point and distance + */ + public static float amplifiedEuclideanSimilarityThreshold(float[] vector, float distanceInMeters) + { + // Get the conversion ratio for meters to degrees at the given latitude. + double distanceBetweenDegreeLatitude = metersToDegreesRatioForLatitude(vector[0]); + + // Calculate the number of degrees that the search radius represents because we're finding the distance between + // two points that are also using degrees as their units. + double degrees = distanceInMeters / distanceBetweenDegreeLatitude; + + return (float) (1.0 / (1 + Math.pow((float) degrees, 2))); + } + + /** + * Determine if the lat/lon intersects with the antimeridian for the given distance. + * @param lat the latitude + * @param lon the longitude + * @param distanceInMeters the search radius + * @return true if the search radius crosses the antimeridian + */ + public static boolean crossesAntimeridian(float lat, float lon, float distanceInMeters) + { + // Get the conversion ratio for meters to degrees at the given latitude. + // Result is always non-negative. + double distanceBetweenDegreeLatitude = metersToDegreesRatioForLatitude(lat); + + // Calculate the number of degrees that the search radius represents because we're finding the distance between + // two points that are also using degrees as their units. + double degrees = distanceInMeters / distanceBetweenDegreeLatitude; + + return Math.abs(lon) + degrees > 180; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/InMemoryPartitionIterator.java b/src/java/org/apache/cassandra/index/sai/utils/InMemoryPartitionIterator.java index 651959dbc177..bb6e464900a0 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/InMemoryPartitionIterator.java +++ b/src/java/org/apache/cassandra/index/sai/utils/InMemoryPartitionIterator.java @@ -18,10 +18,10 @@ package org.apache.cassandra.index.sai.utils; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; -import java.util.Map; -import java.util.TreeMap; -import java.util.TreeSet; +import java.util.List; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.ReadCommand; @@ -29,18 +29,48 @@ import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.RowIterator; -import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.Pair; public class InMemoryPartitionIterator implements PartitionIterator { - private final ReadCommand command; - private final Iterator>> partitions; + private final Iterator partitions; - public InMemoryPartitionIterator(ReadCommand command, TreeMap> rowsByPartitions) + private InMemoryPartitionIterator(List partitions) { - this.command = command; - this.partitions = rowsByPartitions.entrySet().iterator(); + this.partitions = partitions.iterator(); + } + + public static InMemoryPartitionIterator create(ReadCommand command, List> sortedRows) + { + if (sortedRows.isEmpty()) + return new InMemoryPartitionIterator(Collections.emptyList()); + + List partitions = new ArrayList<>(); + + PartitionInfo currentPartitionInfo = null; + List currentRows = null; + + for (Pair pair : sortedRows) + { + PartitionInfo partitionInfo = pair.left; + Row row = pair.right; + + if (currentPartitionInfo == null || !currentPartitionInfo.key.equals(partitionInfo.key)) + { + if (currentPartitionInfo != null) + partitions.add(new InMemoryRowIterator(command, currentPartitionInfo, currentRows)); + + currentPartitionInfo = partitionInfo; + currentRows = new ArrayList<>(1); + } + + currentRows.add(row); + } + + partitions.add(new InMemoryRowIterator(command, currentPartitionInfo, currentRows)); + + return new InMemoryPartitionIterator(partitions); } @Override @@ -57,19 +87,21 @@ public boolean hasNext() @Override public RowIterator next() { - return new InMemoryRowIterator(partitions.next()); + return partitions.next(); } - private class InMemoryRowIterator implements RowIterator + private static class InMemoryRowIterator implements RowIterator { + private final ReadCommand command; private final PartitionInfo partitionInfo; - private final Iterator rows; + private final Iterator rows; - public InMemoryRowIterator(Map.Entry> rows) + public InMemoryRowIterator(ReadCommand command, PartitionInfo partitionInfo, List rows) { - this.partitionInfo = rows.getKey(); - this.rows = rows.getValue().iterator(); + this.command = command; + this.partitionInfo = partitionInfo; + this.rows = rows.iterator(); } @Override @@ -86,7 +118,7 @@ public boolean hasNext() @Override public Row next() { - return (Row) rows.next(); + return rows.next(); } @Override diff --git a/src/java/org/apache/cassandra/index/sai/utils/InMemoryUnfilteredPartitionIterator.java b/src/java/org/apache/cassandra/index/sai/utils/InMemoryUnfilteredPartitionIterator.java index 6aab722e2a85..2fd9953c5fcc 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/InMemoryUnfilteredPartitionIterator.java +++ b/src/java/org/apache/cassandra/index/sai/utils/InMemoryUnfilteredPartitionIterator.java @@ -20,7 +20,7 @@ import java.util.Iterator; import java.util.Map; -import java.util.TreeMap; +import java.util.SortedMap; import java.util.TreeSet; import org.apache.cassandra.db.DecoratedKey; @@ -39,7 +39,7 @@ public class InMemoryUnfilteredPartitionIterator implements UnfilteredPartitionI private final ReadCommand command; private final Iterator>> partitions; - public InMemoryUnfilteredPartitionIterator(ReadCommand command, TreeMap> rowsByPartitions) + public InMemoryUnfilteredPartitionIterator(ReadCommand command, SortedMap> rowsByPartitions) { this.command = command; this.partitions = rowsByPartitions.entrySet().iterator(); @@ -112,7 +112,7 @@ public boolean isReverseOrder() @Override public RegularAndStaticColumns columns() { - return command.metadata().regularAndStaticColumns(); + return partitionInfo.columns; } @Override diff --git a/src/java/org/apache/cassandra/index/sai/utils/IndexEntry.java b/src/java/org/apache/cassandra/index/sai/utils/IndexEntry.java deleted file mode 100644 index a47d7a8ef43d..000000000000 --- a/src/java/org/apache/cassandra/index/sai/utils/IndexEntry.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.utils; - -import org.apache.cassandra.index.sai.postings.PostingList; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; - -public class IndexEntry -{ - public final ByteComparable term; - public final PostingList postingList; - - private IndexEntry(ByteComparable term, PostingList postingList) - { - this.term = term; - this.postingList = postingList; - } - - public static IndexEntry create(ByteComparable term, PostingList postingList) - { - return new IndexEntry(term, postingList); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/utils/IndexIdentifier.java b/src/java/org/apache/cassandra/index/sai/utils/IndexIdentifier.java deleted file mode 100644 index f324207d6b58..000000000000 --- a/src/java/org/apache/cassandra/index/sai/utils/IndexIdentifier.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.utils; - -import com.google.common.base.Objects; - -/** - * This is a simple wrapper around the index identity. Its primary purpose is to isolate classes that only need - * access to the identity from the main index classes. This is useful in testing but also makes it easier to pass - * the log message wrapper {@link #logMessage(String)} to classes that don't need any other information about the index. - */ -public class IndexIdentifier -{ - public final String keyspaceName; - public final String tableName; - public final String indexName; - - public IndexIdentifier(String keyspaceName, String tableName, String indexName) - { - this.keyspaceName = keyspaceName; - this.tableName = tableName; - this.indexName = indexName; - } - - /** - * A helper method for constructing consistent log messages for specific column indexes. - *

    - * Example: For the index "idx" in keyspace "ks" on table "tb", calling this method with the raw message - * "Flushing new index segment..." will produce... - *

    - * "[ks.tb.idx] Flushing new index segment..." - * - * @param message The raw content of a logging message, without information identifying it with an index. - * - * @return A log message with the proper keyspace, table and index name prepended to it. - */ - public String logMessage(String message) - { - // Index names are unique only within a keyspace. - return String.format("[%s.%s.%s] %s", keyspaceName, tableName, indexName, message); - } - - @Override - public String toString() - { - return String.format("%s.%s", keyspaceName, indexName); - } - - @Override - public int hashCode() - { - return Objects.hashCode(keyspaceName, tableName, indexName); - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - IndexIdentifier other = (IndexIdentifier) obj; - return Objects.equal(keyspaceName, other.keyspaceName) && - Objects.equal(tableName, other.tableName) && - Objects.equal(indexName, other.indexName); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java b/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java deleted file mode 100644 index f3c7e2c05f96..000000000000 --- a/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java +++ /dev/null @@ -1,914 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.utils; - -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import com.google.common.base.MoreObjects; -import com.google.common.collect.ImmutableSet; - -import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; -import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.cql3.statements.schema.IndexTarget; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.BooleanType; -import org.apache.cassandra.db.marshal.ByteBufferAccessor; -import org.apache.cassandra.db.marshal.CollectionType; -import org.apache.cassandra.db.marshal.CompositeType; -import org.apache.cassandra.db.marshal.DecimalType; -import org.apache.cassandra.db.marshal.InetAddressType; -import org.apache.cassandra.db.marshal.IntegerType; -import org.apache.cassandra.db.marshal.LongType; -import org.apache.cassandra.db.marshal.StringType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.db.marshal.UUIDType; -import org.apache.cassandra.db.marshal.VectorType; -import org.apache.cassandra.db.rows.Cell; -import org.apache.cassandra.db.rows.ComplexColumnData; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.serializers.MarshalException; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.FastByteOperations; -import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.cassandra.utils.bytecomparable.ByteSource; -import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; - -/** - * This class is a representation of an {@link AbstractType} as an indexable type. It is responsible for determining the - * capabilities of the type and provides helper methods for handling term values associated with the type. - */ -public class IndexTermType -{ - private static final Set> EQ_ONLY_TYPES = ImmutableSet.of(UTF8Type.instance, - AsciiType.instance, - BooleanType.instance, - UUIDType.instance); - - private static final byte[] IPV4_PREFIX = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1 }; - - /** - * DecimalType / BigDecimal values are indexed by truncating their asComparableBytes representation to this size, - * padding on the right with zero-value-bytes until this size is reached (if necessary). This causes - * false-positives that must be filtered in a separate step after hitting the index and reading the associated - * (full) values. - */ - private static final int DECIMAL_APPROXIMATION_BYTES = 24; - private static final int BIG_INTEGER_APPROXIMATION_BYTES = 20; - private static final int INET_ADDRESS_SIZE = 16; - private static final int DEFAULT_FIXED_LENGTH = 16; - - private enum Capability - { - STRING, - VECTOR, - INET_ADDRESS, - BIG_INTEGER, - BIG_DECIMAL, - LONG, - BOOLEAN, - LITERAL, - REVERSED, - FROZEN, - COLLECTION, - NON_FROZEN_COLLECTION, - COMPOSITE, - COMPOSITE_PARTITION - } - - private final ColumnMetadata columnMetadata; - private final IndexTarget.Type indexTargetType; - private final AbstractType indexType; - private final List subTypes; - private final AbstractType vectorElementType; - private final int vectorDimension; - private final EnumSet capabilities; - - /** - * Create an {@link IndexTermType} from a {@link ColumnMetadata} and {@link IndexTarget.Type}. - * - * @param columnMetadata the {@link ColumnMetadata} for the column being indexed - * @param partitionColumns the partition columns for the table this column belongs to. This is used for identifying - * if the {@code columnMetadata} is a partition column and if it belongs to a composite - * partition - * @param indexTargetType the {@link IndexTarget.Type} for the index - * - * @return the {@link IndexTermType} - */ - public static IndexTermType create(ColumnMetadata columnMetadata, List partitionColumns, IndexTarget.Type indexTargetType) - { - return new IndexTermType(columnMetadata, partitionColumns, indexTargetType); - } - - private IndexTermType(ColumnMetadata columnMetadata, List partitionColumns, IndexTarget.Type indexTargetType) - { - this.columnMetadata = columnMetadata; - this.indexTargetType = indexTargetType; - this.capabilities = calculateCapabilities(columnMetadata, partitionColumns, indexTargetType); - this.indexType = calculateIndexType(columnMetadata.type, capabilities, indexTargetType); - - AbstractType baseType = indexType.unwrap(); - - // We only need to inspect subtypes when it is possible for them to be queried individually. - if (baseType.subTypes().isEmpty() || indexTargetType == IndexTarget.Type.SIMPLE || indexTargetType == IndexTarget.Type.FULL) - { - this.subTypes = Collections.emptyList(); - } - else - { - List subTypes = new ArrayList<>(baseType.subTypes().size()); - for (AbstractType subType : baseType.subTypes()) - subTypes.add(new IndexTermType(columnMetadata.withNewType(subType), partitionColumns, indexTargetType)); - this.subTypes = Collections.unmodifiableList(subTypes); - } - - if (isVector()) - { - VectorType vectorType = (VectorType) baseType; - vectorElementType = vectorType.elementType; - vectorDimension = vectorType.dimension; - } - else - { - vectorElementType = null; - vectorDimension = -1; - } - } - - /** - * Returns {@code true} if the index type is a literal type and will use a literal index. This applies to - * string types, frozen types, composite types and boolean type. - */ - public boolean isLiteral() - { - return capabilities.contains(Capability.LITERAL); - } - - /** - * Returns {@code true} if the index type is a string type. This is used to determine if the type supports - * analysis. - */ - public boolean isString() - { - return capabilities.contains(Capability.STRING); - } - - /** - * Returns {@code true} if the index type is a vector type. Note: being a vector type does not mean that the type - * is valid for indexing in that we don't check the element type and dimension constraints here. - */ - public boolean isVector() - { - return capabilities.contains(Capability.VECTOR); - } - - /** - * Returns {@code true} if the index type is reversed. This is only the case (currently) for clustering keys with - * descending ordering. - */ - public boolean isReversed() - { - return capabilities.contains(Capability.REVERSED); - } - - /** - * Returns {@code true} if the index type is frozen, e.g. the type is wrapped with {@code frozen}. - */ - public boolean isFrozen() - { - return capabilities.contains(Capability.FROZEN); - } - - /** - * Returns {@code true} if the index type is a non-frozen collection - */ - public boolean isNonFrozenCollection() - { - return capabilities.contains(Capability.NON_FROZEN_COLLECTION); - } - - /** - * Returns {@code true} if the index type is a frozen collection. This is the inverse of a non-frozen collection - * but this method is here for clarity. - */ - public boolean isFrozenCollection() - { - return capabilities.contains(Capability.COLLECTION) && capabilities.contains(Capability.FROZEN); - } - - /** - * Returns {@code true} if the index type is a composite type, e.g. it has the form {@code Composite} - */ - public boolean isComposite() - { - return capabilities.contains(Capability.COMPOSITE); - } - - /** - * Returns {@code true} if the {@link RowFilter.Expression} passed is backed by a non-frozen collection and the - * {@code Operator} is one that cannot be merged together. - */ - public boolean isMultiExpression(RowFilter.Expression expression) - { - boolean multiExpression = false; - switch (expression.operator()) - { - case EQ: - multiExpression = isNonFrozenCollection(); - break; - case CONTAINS: - case CONTAINS_KEY: - multiExpression = true; - break; - } - return multiExpression; - } - - /** - * Returns true if given buffer would pass the {@link AbstractType#validate(ByteBuffer)} - * check. False otherwise. - */ - public boolean isValid(ByteBuffer term) - { - try - { - indexType.validate(term); - return true; - } - catch (MarshalException e) - { - return false; - } - } - - /** - * @return {@code true} if the empty values of the given type should be excluded from indexing - */ - public boolean skipsEmptyValue() - { - return !indexType.allowsEmpty() || !isLiteral(); - } - - public AbstractType indexType() - { - return indexType; - } - - public Collection subTypes() - { - return subTypes; - } - - public CQL3Type asCQL3Type() - { - return indexType.asCQL3Type(); - } - - public ColumnMetadata columnMetadata() - { - return columnMetadata; - } - - public String columnName() - { - return columnMetadata.name.toString(); - } - - public AbstractType vectorElementType() - { - assert isVector(); - - return vectorElementType; - } - - public int vectorDimension() - { - assert isVector(); - - return vectorDimension; - } - - public boolean dependsOn(ColumnMetadata columnMetadata) - { - return this.columnMetadata.compareTo(columnMetadata) == 0; - } - - public static boolean isEqOnlyType(AbstractType type) - { - return EQ_ONLY_TYPES.contains(type); - } - - /** - * Indicates if the type encoding supports rounding of the raw value. - *

    - * This is significant in range searches where we have to make all range - * queries inclusive when searching the indexes in order to avoid excluding - * rounded values. Excluded values are removed by post-filtering. - */ - public boolean supportsRounding() - { - return isBigInteger() || isBigDecimal(); - } - - /** - * Returns the value length for the given {@link AbstractType}, selecting 16 for types - * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length. - */ - public int fixedSizeOf() - { - if (indexType.isValueLengthFixed()) - return indexType.valueLengthIfFixed(); - else if (isInetAddress()) - return INET_ADDRESS_SIZE; - else if (isBigInteger()) - return BIG_INTEGER_APPROXIMATION_BYTES; - else if (isBigDecimal()) - return DECIMAL_APPROXIMATION_BYTES; - return DEFAULT_FIXED_LENGTH; - } - - /** - * Allows overriding the default getString method for {@link CompositeType}. It is - * a requirement of the {@link ConcurrentRadixTree} that the keys are strings but - * the getString method of {@link CompositeType} does not return a string that compares - * in the same order as the underlying {@link ByteBuffer}. To get round this we convert - * the {@link CompositeType} bytes to a hex string. - */ - public String asString(ByteBuffer value) - { - if (isComposite()) - return ByteBufferUtil.bytesToHex(value); - return indexType.getString(value); - } - - /** - * The inverse of the above method. Overrides the fromString method on {@link CompositeType} - * in order to convert the hex string to bytes. - */ - public ByteBuffer fromString(String value) - { - if (isComposite()) - return ByteBufferUtil.hexToBytes(value); - return indexType.fromString(value); - } - - /** - * Returns the cell value from the {@link DecoratedKey} or {@link Row} for the {@link IndexTermType} based on the - * kind of column this {@link IndexTermType} is based on. - * - * @param key the {@link DecoratedKey} of the row - * @param row the {@link Row} containing the non-partition column data - * @param nowInSecs the time that the index write operation started - * - * @return a {@link ByteBuffer} containing the cell value - */ - public ByteBuffer valueOf(DecoratedKey key, Row row, long nowInSecs) - { - if (row == null) - return null; - - switch (columnMetadata.kind) - { - case PARTITION_KEY: - return isCompositePartition() ? CompositeType.extractComponent(key.getKey(), columnMetadata.position()) - : key.getKey(); - case CLUSTERING: - // skip indexing of static clustering when regular column is indexed - return row.isStatic() ? null : row.clustering().bufferAt(columnMetadata.position()); - - // treat static cell retrieval the same was as regular - // only if row kind is STATIC otherwise return null - case STATIC: - if (!row.isStatic()) - return null; - case REGULAR: - Cell cell = row.getCell(columnMetadata); - return cell == null || !cell.isLive(nowInSecs) ? null : cell.buffer(); - - default: - return null; - } - } - - /** - * Returns a value iterator for collection type {@link IndexTermType}s. - * - * @param row the {@link Row} containing the column data - * @param nowInSecs the time that the index write operation started - * - * @return an {@link Iterator} of the collection values - */ - public Iterator valuesOf(Row row, long nowInSecs) - { - if (row == null) - return null; - - switch (columnMetadata.kind) - { - // treat static cell retrieval the same was as regular - // only if row kind is STATIC otherwise return null - case STATIC: - if (!row.isStatic()) - return null; - case REGULAR: - return collectionIterator(row.getComplexColumnData(columnMetadata), nowInSecs); - - default: - return null; - } - } - - public Comparator comparator() - { - // Override the comparator for BigInteger, frozen collections and composite types - if (isBigInteger() || isBigDecimal() || isComposite() || isFrozen()) - return FastByteOperations::compareUnsigned; - - return indexType; - } - - /** - * Compare two terms based on their type. This is used in place of {@link AbstractType#compare(ByteBuffer, ByteBuffer)} - * so that the default comparison can be overridden for specific types. - *

    - * Note: This should be used for all term comparison - */ - public int compare(ByteBuffer b1, ByteBuffer b2) - { - if (isInetAddress()) - return compareInet(b1, b2); - else if (isLong()) - return indexType.unwrap().compare(b1, b2); - // BigInteger values, frozen types and composite types (map entries) use compareUnsigned to maintain - // a consistent order between the in-memory index and the on-disk index. - else if (isBigInteger() || isBigDecimal() || isComposite() || isFrozen()) - return FastByteOperations.compareUnsigned(b1, b2); - - return indexType.compare(b1, b2); - } - - /** - * Returns the smaller of two {@code ByteBuffer} values, based on the result of {@link - * #compare(ByteBuffer, ByteBuffer)} comparision. - */ - public ByteBuffer min(ByteBuffer a, ByteBuffer b) - { - return a == null ? b : (b == null || compare(b, a) > 0) ? a : b; - } - - /** - * Returns the greater of two {@code ByteBuffer} values, based on the result of {@link - * #compare(ByteBuffer, ByteBuffer)} comparision. - */ - public ByteBuffer max(ByteBuffer a, ByteBuffer b) - { - return a == null ? b : (b == null || compare(b, a) < 0) ? a : b; - } - - /** - * This is used for value comparison in post-filtering - {@link Expression#isSatisfiedBy(ByteBuffer)}. - *

    - * This allows types to decide whether they should be compared based on their encoded value or their - * raw value. At present only {@link InetAddressType} values are compared by their encoded values to - * allow for ipv4 -> ipv6 equivalency in searches. - */ - public int comparePostFilter(Expression.Value requestedValue, Expression.Value columnValue) - { - if (isInetAddress()) - return compareInet(requestedValue.encoded, columnValue.encoded); - // bigint, decimal, and varint are not indexed in reversed byte-comparable form or treated as reversed types by - // Expression, so it is correct to compare with the base/unwrapped type - else if (isLong() || isBigDecimal() || isBigInteger()) - return indexType.unwrap().compare(requestedValue.raw, columnValue.raw); - // Override comparisons for frozen collections and composite types (map entries) - else if (isComposite() || isFrozen()) - return FastByteOperations.compareUnsigned(requestedValue.raw, columnValue.raw); - - // Reversed types are treated as such by Expression here, so we cannot blindly compare with the unwrapped type. - // In the future, we might consider simplifying things to have SAI ignore reversed types altogether, but this - // will require a change to the on-disk formats. - return indexType.compare(requestedValue.raw, columnValue.raw); - } - - /** - * Fills a byte array with the comparable bytes for a type. - *

    - * This method expects a {@code value} parameter generated by calling {@link #asIndexBytes(ByteBuffer)}. - * It is not generally safe to pass the output of other serialization methods to this method. For instance, it is - * not generally safe to pass the output of {@link AbstractType#decompose(Object)} as the {@code value} parameter - * (there are certain types for which this is technically OK, but that doesn't hold for all types). - * - * @param value a value buffer returned by {@link #asIndexBytes(ByteBuffer)} - * @param bytes this method's output - */ - public void toComparableBytes(ByteBuffer value, byte[] bytes) - { - if (isInetAddress()) - ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, INET_ADDRESS_SIZE); - else if (isBigInteger()) - ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, BIG_INTEGER_APPROXIMATION_BYTES); - else if (isBigDecimal()) - ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, DECIMAL_APPROXIMATION_BYTES); - else - ByteSourceInverse.copyBytes(asComparableBytes(value, ByteComparable.Version.OSS50), bytes); - } - - public ByteSource asComparableBytes(ByteBuffer value, ByteComparable.Version version) - { - if (value.remaining() == 0) - return ByteSource.EMPTY; - - if (isInetAddress() || isBigInteger() || isBigDecimal()) - return ByteSource.optionalFixedLength(ByteBufferAccessor.instance, value); - else if (isLong()) - // The LongType.asComparableBytes uses variableLengthInteger which doesn't play well with - // the balanced tree because it is expecting fixed length data. So for SAI we use a optionalSignedFixedLengthNumber - // to keep all comparable values the same length - return ByteSource.optionalSignedFixedLengthNumber(ByteBufferAccessor.instance, value); - else if (isFrozen()) - // We need to override the default frozen implementation here because it will defer to the underlying - // type's implementation which will be incorrect, for us, for the case of multi-cell types. - return ByteSource.of(value, version); - return indexType.asComparableBytes(value, version); - } - - /** - * Translates the external value of specific types into a format used by the index. - */ - public ByteBuffer asIndexBytes(ByteBuffer value) - { - if (value == null || value.remaining() == 0) - return value; - - if (isInetAddress()) - return encodeInetAddress(value); - else if (isBigInteger()) - return encodeBigInteger(value); - else if (isBigDecimal()) - return encodeDecimal(value); - return value; - } - - public float[] decomposeVector(ByteBuffer byteBuffer) - { - assert isVector(); - return ((VectorType) indexType).composeAsFloat(byteBuffer); - } - - public boolean supports(Operator operator) - { - if (operator == Operator.LIKE || - operator == Operator.LIKE_CONTAINS || - operator == Operator.LIKE_PREFIX || - operator == Operator.LIKE_MATCHES || - operator == Operator.LIKE_SUFFIX) return false; - - // ANN is only supported against vectors, and vector indexes only support ANN - if (operator == Operator.ANN) - return isVector(); - - Expression.IndexOperator indexOperator = Expression.IndexOperator.valueOf(operator); - - if (isNonFrozenCollection()) - { - if (indexTargetType == IndexTarget.Type.KEYS) return indexOperator == Expression.IndexOperator.CONTAINS_KEY; - if (indexTargetType == IndexTarget.Type.VALUES) return indexOperator == Expression.IndexOperator.CONTAINS_VALUE; - return indexTargetType == IndexTarget.Type.KEYS_AND_VALUES && indexOperator == Expression.IndexOperator.EQ; - } - - if (indexTargetType == IndexTarget.Type.FULL) - return indexOperator == Expression.IndexOperator.EQ; - - if (indexOperator != Expression.IndexOperator.EQ && EQ_ONLY_TYPES.contains(indexType)) return false; - - // RANGE only applicable to non-literal indexes - return (indexOperator != null) && !(isLiteral() && indexOperator == Expression.IndexOperator.RANGE); - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this) - .add("column", columnMetadata) - .add("type", indexType) - .add("indexType", indexTargetType) - .toString(); - } - - @Override - public boolean equals(Object obj) - { - if (obj == this) - return true; - - if (!(obj instanceof IndexTermType)) - return false; - - IndexTermType other = (IndexTermType) obj; - - return Objects.equals(columnMetadata, other.columnMetadata) && (indexTargetType == other.indexTargetType); - } - - @Override - public int hashCode() - { - return Objects.hash(columnMetadata, indexTargetType); - } - - private EnumSet calculateCapabilities(ColumnMetadata columnMetadata, List partitionKeyColumns, IndexTarget.Type indexTargetType) - { - EnumSet capabilities = EnumSet.noneOf(Capability.class); - - if (partitionKeyColumns.contains(columnMetadata) && partitionKeyColumns.size() > 1) - capabilities.add(Capability.COMPOSITE_PARTITION); - - AbstractType type = columnMetadata.type; - boolean reversed = type.isReversed(); - AbstractType baseType = type.unwrap(); - - if (baseType.isCollection()) - capabilities.add(Capability.COLLECTION); - - if (baseType.isCollection() && baseType.isMultiCell()) - capabilities.add(Capability.NON_FROZEN_COLLECTION); - - if (!baseType.subTypes().isEmpty() && !baseType.isMultiCell()) - capabilities.add(Capability.FROZEN); - - AbstractType indexType = calculateIndexType(baseType, capabilities, indexTargetType); - - if (indexType instanceof CompositeType) - capabilities.add(Capability.COMPOSITE); - else if (!indexType.subTypes().isEmpty() && !indexType.isMultiCell()) - capabilities.add(Capability.FROZEN); - - if (indexType instanceof StringType) - capabilities.add(Capability.STRING); - - if (indexType instanceof BooleanType) - capabilities.add(Capability.BOOLEAN); - - if (capabilities.contains(Capability.STRING) || - capabilities.contains(Capability.BOOLEAN) || - capabilities.contains(Capability.FROZEN) || - capabilities.contains(Capability.COMPOSITE)) - capabilities.add(Capability.LITERAL); - - if (indexType instanceof VectorType) - capabilities.add(Capability.VECTOR); - - if (indexType instanceof InetAddressType) - { - capabilities.add(Capability.INET_ADDRESS); - reversed = false; - } - - if (indexType instanceof IntegerType) - { - capabilities.add(Capability.BIG_INTEGER); - reversed = false; - } - - if (indexType instanceof DecimalType) - { - capabilities.add(Capability.BIG_DECIMAL); - reversed = false; - } - - if (indexType instanceof LongType) - { - capabilities.add(Capability.LONG); - reversed = false; - } - - if (reversed) - capabilities.add(Capability.REVERSED); - - return capabilities; - } - - private AbstractType calculateIndexType(AbstractType baseType, EnumSet capabilities, IndexTarget.Type indexTargetType) - { - return capabilities.contains(Capability.NON_FROZEN_COLLECTION) ? collectionCellValueType(baseType, indexTargetType) : baseType; - } - - private Iterator collectionIterator(ComplexColumnData cellData, long nowInSecs) - { - if (cellData == null) - return null; - - Stream stream = StreamSupport.stream(cellData.spliterator(), false) - .filter(cell -> cell != null && cell.isLive(nowInSecs)) - .map(this::cellValue); - - if (isInetAddress()) - stream = stream.sorted((c1, c2) -> compareInet(encodeInetAddress(c1), encodeInetAddress(c2))); - - return stream.iterator(); - } - - private ByteBuffer cellValue(Cell cell) - { - if (isNonFrozenCollection()) - { - switch (((CollectionType) columnMetadata.type).kind) - { - case LIST: - return cell.buffer(); - case SET: - return cell.path().get(0); - case MAP: - switch (indexTargetType) - { - case KEYS: - return cell.path().get(0); - case VALUES: - return cell.buffer(); - case KEYS_AND_VALUES: - return CompositeType.build(ByteBufferAccessor.instance, cell.path().get(0), cell.buffer()); - } - } - } - return cell.buffer(); - } - - private AbstractType collectionCellValueType(AbstractType type, IndexTarget.Type indexType) - { - CollectionType collection = ((CollectionType) type); - switch (collection.kind) - { - case LIST: - return collection.valueComparator(); - case SET: - return collection.nameComparator(); - case MAP: - switch (indexType) - { - case KEYS: - return collection.nameComparator(); - case VALUES: - return collection.valueComparator(); - case KEYS_AND_VALUES: - return CompositeType.getInstance(collection.nameComparator(), collection.valueComparator()); - } - default: - throw new IllegalArgumentException("Unsupported collection type: " + collection.kind); - } - } - - private boolean isCompositePartition() - { - return capabilities.contains(Capability.COMPOSITE_PARTITION); - } - - /** - * Returns true if given {@link AbstractType} is {@link InetAddressType} - */ - private boolean isInetAddress() - { - return capabilities.contains(Capability.INET_ADDRESS); - } - - /** - * Returns true if given {@link AbstractType} is {@link IntegerType} - */ - private boolean isBigInteger() - { - return capabilities.contains(Capability.BIG_INTEGER); - } - - /** - * Returns true if given {@link AbstractType} is {@link DecimalType} - */ - private boolean isBigDecimal() - { - return capabilities.contains(Capability.BIG_DECIMAL); - } - - private boolean isLong() - { - return capabilities.contains(Capability.LONG); - } - - /** - * Compares 2 InetAddress terms by ensuring that both addresses are represented as - * ipv6 addresses. - */ - private static int compareInet(ByteBuffer b1, ByteBuffer b2) - { - assert isIPv6(b1) && isIPv6(b2); - - return FastByteOperations.compareUnsigned(b1, b2); - } - - private static boolean isIPv6(ByteBuffer address) - { - return address.remaining() == INET_ADDRESS_SIZE; - } - - /** - * Encode a {@link InetAddress} into a fixed width 16 byte encoded value. - *

    - * The encoded value is byte comparable and prefix compressible. - *

    - * The encoding is done by converting ipv4 addresses to their ipv6 equivalent. - */ - private static ByteBuffer encodeInetAddress(ByteBuffer value) - { - if (value.remaining() == 4) - { - int position = value.hasArray() ? value.arrayOffset() + value.position() : value.position(); - ByteBuffer mapped = ByteBuffer.allocate(INET_ADDRESS_SIZE); - System.arraycopy(IPV4_PREFIX, 0, mapped.array(), 0, IPV4_PREFIX.length); - ByteBufferUtil.copyBytes(value, position, mapped, IPV4_PREFIX.length, value.remaining()); - return mapped; - } - return value; - } - - /** - * Encode a {@link BigInteger} into a fixed width 20 byte encoded value. The encoded value is byte comparable - * and prefix compressible. - *

    - * The format of the encoding is: - *

    - * The first 4 bytes contain the integer length of the {@link BigInteger} byte array - * with the top bit flipped for positive values. - *

    - * The remaining 16 bytes contain the 16 most significant bytes of the - * {@link BigInteger} byte array. - *

    - * For {@link BigInteger} values whose underlying byte array is less than - * 16 bytes, the encoded value is sign extended. - */ - public static ByteBuffer encodeBigInteger(ByteBuffer value) - { - int size = value.remaining(); - int position = value.hasArray() ? value.arrayOffset() + value.position() : value.position(); - byte[] bytes = new byte[BIG_INTEGER_APPROXIMATION_BYTES]; - if (size < BIG_INTEGER_APPROXIMATION_BYTES - Integer.BYTES) - { - ByteBufferUtil.copyBytes(value, position, bytes, bytes.length - size, size); - if ((bytes[bytes.length - size] & 0x80) != 0) - Arrays.fill(bytes, Integer.BYTES, bytes.length - size, (byte)0xff); - else - Arrays.fill(bytes, Integer.BYTES, bytes.length - size, (byte)0x00); - } - else - { - ByteBufferUtil.copyBytes(value, position, bytes, Integer.BYTES, BIG_INTEGER_APPROXIMATION_BYTES - Integer.BYTES); - } - if ((bytes[4] & 0x80) != 0) - { - size = -size; - } - bytes[0] = (byte)(size >> 24 & 0xff); - bytes[1] = (byte)(size >> 16 & 0xff); - bytes[2] = (byte)(size >> 8 & 0xff); - bytes[3] = (byte)(size & 0xff); - bytes[0] ^= 0x80; - return ByteBuffer.wrap(bytes); - } - - public static ByteBuffer encodeDecimal(ByteBuffer value) - { - ByteSource bs = DecimalType.instance.asComparableBytes(value, ByteComparable.Version.OSS50); - bs = ByteSource.cutOrRightPad(bs, DECIMAL_APPROXIMATION_BYTES, 0); - return ByteBuffer.wrap(ByteSourceInverse.readBytes(bs, DECIMAL_APPROXIMATION_BYTES)); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/utils/LowPriorityThreadFactory.java b/src/java/org/apache/cassandra/index/sai/utils/LowPriorityThreadFactory.java new file mode 100644 index 000000000000..41cfdd41a6a2 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/LowPriorityThreadFactory.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; + +public class LowPriorityThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory +{ + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + worker.setPriority(Thread.MIN_PRIORITY); + return worker; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/MemtableOrdering.java b/src/java/org/apache/cassandra/index/sai/utils/MemtableOrdering.java new file mode 100644 index 000000000000..3989192e337b --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/MemtableOrdering.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.List; + +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.utils.CloseableIterator; + +/*** + * Analogue of SegmentOrdering, but for memtables. + */ +public interface MemtableOrdering +{ + + /** + * Order the index based on the given expression. + * + * @param queryContext - the query context + * @param orderer - the expression to order by + * @param slice - the expression to restrict index search by + * @param keyRange - the key range to search + * @param limit - can be used to inform the search, but should not be used to prematurely limit the iterator + * @return an iterator over the results in score order. + */ + List> orderBy(QueryContext queryContext, + Orderer orderer, + Expression slice, + AbstractBounds keyRange, + int limit); + + /** + * Order the given list of {@link PrimaryKey} results corresponding to the given expression. + * Returns an iterator over the results in score order. + * + * Assumes that the given spans the same rows as the implementing index's segment. + */ + CloseableIterator orderResultsBy(QueryContext context, List keys, Orderer orderer, int limit); +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/MergePrimaryKeyWithScoreIterator.java b/src/java/org/apache/cassandra/index/sai/utils/MergePrimaryKeyWithScoreIterator.java deleted file mode 100644 index 5930d44994aa..000000000000 --- a/src/java/org/apache/cassandra/index/sai/utils/MergePrimaryKeyWithScoreIterator.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sai.utils; - -import java.util.Collection; -import java.util.PriorityQueue; - -import com.google.common.collect.Iterators; -import com.google.common.collect.PeekingIterator; - -import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.CloseableIterator; - -// TODO: this implementation is sub-optimal due to the combination of PriorityQueue poll/add. A non reducing version of -// the MergeIterator would be better -public class MergePrimaryKeyWithScoreIterator extends AbstractIterator -{ - private final PriorityQueue> queue; - private final Collection> iteratorsToClose; - - public MergePrimaryKeyWithScoreIterator(Collection> iterators) - { - assert !iterators.isEmpty(); - iteratorsToClose = iterators; - queue = new PriorityQueue<>(iterators.size(), (a, b) -> a.peek().compareTo(b.peek())); - for (CloseableIterator iterator : iterators) - { - if (iterator.hasNext()) - queue.add(Iterators.peekingIterator(iterator)); - } - } - - @Override - protected PrimaryKeyWithScore computeNext() - { - if (queue.isEmpty()) - return endOfData(); - - PeekingIterator iterator = queue.poll(); - PrimaryKeyWithScore next = iterator.next(); - if (iterator.hasNext()) - queue.add(iterator); - return next; - } - - @Override - public void close() - { - for (CloseableIterator iterator : iteratorsToClose) - FileUtils.closeQuietly(iterator); - } -} diff --git a/src/java/org/apache/cassandra/index/sai/utils/NamedMemoryLimiter.java b/src/java/org/apache/cassandra/index/sai/utils/NamedMemoryLimiter.java index 0a5fdf69c2b9..988fb1be44ca 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/NamedMemoryLimiter.java +++ b/src/java/org/apache/cassandra/index/sai/utils/NamedMemoryLimiter.java @@ -20,7 +20,6 @@ import java.util.concurrent.atomic.AtomicLong; import javax.annotation.concurrent.ThreadSafe; -import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,17 +33,16 @@ public final class NamedMemoryLimiter { private static final Logger logger = LoggerFactory.getLogger(NamedMemoryLimiter.class); + private final long limitBytes; private final AtomicLong bytesUsed = new AtomicLong(0); private final String scope; - private long limitBytes; - public NamedMemoryLimiter(long limitBytes, String scope) { this.limitBytes = limitBytes; this.scope = scope; - logger.info("[{}]: Memory limiter using limit of {}...", scope, FBUtilities.prettyPrintMemory(limitBytes)); + logger.debug("[{}]: Memory limiter using limit of {}...", scope, FBUtilities.prettyPrintMemory(limitBytes)); } /** @@ -59,29 +57,23 @@ public long increment(long bytes) { if (logger.isTraceEnabled()) logger.trace("[{}]: Incrementing tracked memory usage by {} bytes from current usage of {}...", scope, bytes, currentBytesUsed()); - return bytesUsed.addAndGet(bytes); + return this.bytesUsed.addAndGet(bytes); } public long decrement(long bytes) { if (logger.isTraceEnabled()) logger.trace("[{}]: Decrementing tracked memory usage by {} bytes from current usage of {}...", scope, bytes, currentBytesUsed()); - return bytesUsed.addAndGet(-bytes); + return this.bytesUsed.addAndGet(-bytes); } public long currentBytesUsed() { - return bytesUsed.get(); + return this.bytesUsed.get(); } public long limitBytes() { - return limitBytes; - } - - @VisibleForTesting - public void setLimitBytes(long bytes) - { - limitBytes = bytes; + return this.limitBytes; } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/OrderingFilterRangeIterator.java b/src/java/org/apache/cassandra/index/sai/utils/OrderingFilterRangeIterator.java new file mode 100644 index 000000000000..5426339c1c72 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/OrderingFilterRangeIterator.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.io.util.FileUtils; + +/** + * An iterator that consumes a chunk of {@link PrimaryKey}s from the {@link KeyRangeIterator}, passes them to the + * {@link Function} to filter the chunk of {@link PrimaryKey}s and then pass the results to next consumer. + * The PKs are currently returned in score order. + */ +@NotThreadSafe +public class OrderingFilterRangeIterator implements Iterator, AutoCloseable +{ + private final KeyRangeIterator input; + private final QueryContext context; + private final int chunkSize; + private final Function, T> nextRangeFunction; + + public OrderingFilterRangeIterator(KeyRangeIterator input, + int chunkSize, + QueryContext context, + Function, T> nextRangeFunction) + { + this.input = input; + this.chunkSize = chunkSize; + this.context = context; + this.nextRangeFunction = nextRangeFunction; + } + + @Override + public boolean hasNext() + { + return input.hasNext(); + } + + @Override + public T next() + { + List nextKeys = new ArrayList<>(chunkSize); + do + { + nextKeys.add(input.next()); + } + while (nextKeys.size() < chunkSize && input.hasNext()); + context.addPartitionsFetched(nextKeys.size()); + return nextRangeFunction.apply(nextKeys); + } + + public void close() { + FileUtils.closeQuietly(input); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/PartitionInfo.java b/src/java/org/apache/cassandra/index/sai/utils/PartitionInfo.java index c8e1c62b6543..496bc42a36c6 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/PartitionInfo.java +++ b/src/java/org/apache/cassandra/index/sai/utils/PartitionInfo.java @@ -18,11 +18,11 @@ package org.apache.cassandra.index.sai.utils; -import java.util.Objects; import javax.annotation.Nullable; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.rows.BaseRowIterator; import org.apache.cassandra.db.rows.EncodingStats; import org.apache.cassandra.db.rows.Row; @@ -33,6 +33,7 @@ public class PartitionInfo { public final DecoratedKey key; public final Row staticRow; + public final RegularAndStaticColumns columns; // present if it's unfiltered partition iterator @Nullable @@ -42,45 +43,35 @@ public class PartitionInfo @Nullable public final EncodingStats encodingStats; - public PartitionInfo(DecoratedKey key, Row staticRow) + private PartitionInfo(DecoratedKey key, + Row staticRow, + RegularAndStaticColumns columns, + @Nullable DeletionTime partitionDeletion, + @Nullable EncodingStats encodingStats) { this.key = key; this.staticRow = staticRow; - this.partitionDeletion = null; - this.encodingStats = null; - } - - public PartitionInfo(DecoratedKey key, Row staticRow, DeletionTime partitionDeletion, EncodingStats encodingStats) - { - this.key = key; - this.staticRow = staticRow; - + this.columns = columns; this.partitionDeletion = partitionDeletion; this.encodingStats = encodingStats; } public static > PartitionInfo create(R baseRowIterator) { - return baseRowIterator instanceof UnfilteredRowIterator - ? new PartitionInfo(baseRowIterator.partitionKey(), baseRowIterator.staticRow(), - ((UnfilteredRowIterator) baseRowIterator).partitionLevelDeletion(), - ((UnfilteredRowIterator) baseRowIterator).stats()) - : new PartitionInfo(baseRowIterator.partitionKey(), baseRowIterator.staticRow()); - } + // only unfiltered row iterators have a partition deletion time and encoding stats + DeletionTime partitionDeletion = null; + EncodingStats encodingStats = null; + if (baseRowIterator instanceof UnfilteredRowIterator) + { + UnfilteredRowIterator unfilteredRowIterator = (UnfilteredRowIterator) baseRowIterator; + partitionDeletion = unfilteredRowIterator.partitionLevelDeletion(); + encodingStats = unfilteredRowIterator.stats(); + } - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PartitionInfo that = (PartitionInfo) o; - return Objects.equals(key, that.key) && Objects.equals(staticRow, that.staticRow) - && Objects.equals(partitionDeletion, that.partitionDeletion) && Objects.equals(encodingStats, that.encodingStats); - } - - @Override - public int hashCode() - { - return Objects.hash(key, staticRow, partitionDeletion, encodingStats); + return new PartitionInfo(baseRowIterator.partitionKey(), + baseRowIterator.staticRow(), + baseRowIterator.columns(), + partitionDeletion, + encodingStats); } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java index 6de7a6c88462..5aaa81dc4faa 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java @@ -17,493 +17,180 @@ */ package org.apache.cassandra.index.sai.utils; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Objects; -import java.util.stream.Collectors; +import java.util.function.Supplier; -import org.apache.cassandra.db.BufferDecoratedKey; +import io.github.jbellis.jvector.util.Accountable; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.marshal.ByteBufferAccessor; -import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; +import org.apache.cassandra.index.sai.disk.v1.PartitionAwarePrimaryKeyFactory; +import org.apache.cassandra.index.sai.disk.v2.RowAwarePrimaryKeyFactory; +import org.apache.cassandra.index.sai.disk.v2.TokenOnlyPrimaryKey; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; -import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; /** * Representation of the primary key for a row consisting of the {@link DecoratedKey} and * {@link Clustering} associated with a {@link org.apache.cassandra.db.rows.Row}. - * The {@link Factory.TokenOnlyPrimaryKey} is used by the {@link org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher} to - * position the search within the query range. + * + * For legacy V1 support only the {@link DecoratedKey} will ever be supported for a row. + * + * For the V2 on-disk format the {@link DecoratedKey} and {@link Clustering} are supported. + * */ -public interface PrimaryKey extends Comparable, ByteComparable +public interface PrimaryKey extends Comparable, Accountable { /** - * See the javadoc for {@link #kind()} for how this enum is used. - */ - enum Kind - { - TOKEN(false), - SKINNY(false), - WIDE(true), - STATIC(true); - - public final boolean hasClustering; - - Kind(boolean hasClustering) - { - this.hasClustering = hasClustering; - } - - public boolean isIntersectable(Kind other) - { - if (this == TOKEN) - return other == TOKEN; - else if (this == SKINNY) - return other == SKINNY; - else if (this == WIDE || this == STATIC) - return other == WIDE || other == STATIC; - - throw new AssertionError("Unknown Kind: " + other); - } - } - - class Factory + * A factory for creating {@link PrimaryKey} instances + */ + interface Factory { - private final IPartitioner partitioner; - private final ClusteringComparator clusteringComparator; - - public Factory(IPartitioner partitioner, ClusteringComparator clusteringComparator) - { - this.partitioner = partitioner; - this.clusteringComparator = clusteringComparator; - } - /** * Creates a {@link PrimaryKey} that is represented by a {@link Token}. - *

    + * * {@link Token} only primary keys are used for defining the partition range * of a query. + * + * @param token the {@link Token} + * @return a {@link PrimaryKey} represented by a token only */ - public PrimaryKey create(Token token) + default PrimaryKey createTokenOnly(Token token) { - assert token != null : "Cannot create a primary key with a null token"; - + assert token != null; return new TokenOnlyPrimaryKey(token); } /** - * Create a {@link PrimaryKey} for tables without clustering columns - */ - public PrimaryKey create(DecoratedKey partitionKey) - { - assert clusteringComparator.size() == 0 : "Cannot create a skinny primary key for a table with clustering columns"; - assert partitionKey != null : "Cannot create a primary key with a null partition key"; - - return new SkinnyPrimaryKey(partitionKey); - } - - /** - * Creates a {@link PrimaryKey} that is fully represented by partition key - * and clustering. + * Creates a {@link PrimaryKey} that is represented by a {@link DecoratedKey}. + * + * {@link DecoratedKey} only primary keys are used to define the minumum and + * maximum coverage of an index. + * + * @param partitionKey the {@link DecoratedKey} + * @return a {@link PrimaryKey} represented by a partition key only */ - public PrimaryKey create(DecoratedKey partitionKey, Clustering clustering) - { - assert clusteringComparator.size() > 0 : "Cannot create a wide primary key for a table without clustering columns"; - assert partitionKey != null : "Cannot create a primary key with a null partition key"; - assert clustering != null : "Cannot create a primary key with a null clustering"; - - return clustering == Clustering.STATIC_CLUSTERING ? new StaticPrimaryKey(partitionKey) : new WidePrimaryKey(partitionKey, clustering); - } - - public boolean hasClusteringColumns() + default PrimaryKey createPartitionKeyOnly(DecoratedKey partitionKey) { - return clusteringComparator != null && clusteringComparator.size() > 0; + return create(partitionKey, Clustering.EMPTY); } /** - * Create a {@link PrimaryKey} from a {@link ByteSource}. This should only be used with {@link ByteSource} instances - * created by calls to {@link PrimaryKey#asComparableBytes(Version)}. + * Creates a {@link PrimaryKey} with deferred loading. Deferred loading means + * that the key will only be fully loaded when the full representation of the + * key is needed for comparison. Before the key is loaded it will be represented + * by a token only, so it will only need loading if the token is matched in a + * comparison or the byte comparable representation of the key is required. + * + * @param token the {@link Token} + * @param primaryKeySupplier the supplier of the full key + * @return a {@link PrimaryKey} the token and a primary key supplier */ - public PrimaryKey fromComparableBytes(ByteSource byteSource) - { - if (clusteringComparator.size() > 0) - { - ByteSource.Peekable peekable = ByteSource.peekable(byteSource); - DecoratedKey partitionKey = partitionKeyFromComparableBytes(ByteSourceInverse.nextComponentSource(peekable)); - Clustering clustering = clusteringFromByteComparable(ByteSourceInverse.nextComponentSource(peekable)); - return create(partitionKey, clustering); - } - else - { - return create(partitionKeyFromComparableBytes(byteSource)); - } - } + PrimaryKey createDeferred(Token token, Supplier primaryKeySupplier); /** - * Create a {@link DecoratedKey} from a {@link ByteSource}. This is a separate method because of it's use by - * the {@link org.apache.cassandra.index.sai.disk.PrimaryKeyMap} implementations to create partition keys. - */ - public DecoratedKey partitionKeyFromComparableBytes(ByteSource byteSource) - { - ByteBuffer decoratedKey = ByteBuffer.wrap(ByteSourceInverse.getUnescapedBytes(ByteSource.peekable(byteSource))); - return new BufferDecoratedKey(partitioner.getToken(decoratedKey), decoratedKey); - } - - /** - * Create a {@link Clustering} from a {@link ByteSource}. This is a separate method because of its use by - * the {@link org.apache.cassandra.index.sai.disk.v1.WidePrimaryKeyMap} to create its clustering keys. + * Creates a {@link PrimaryKey} that is fully represented by partition key + * and clustering. + * + * @param partitionKey the {@link DecoratedKey} + * @param clustering the {@link Clustering} + * @return a {@link PrimaryKey} contain the partition key and clustering */ - public Clustering clusteringFromByteComparable(ByteSource byteSource) - { - Clustering clustering = clusteringComparator.clusteringFromByteComparable(ByteBufferAccessor.instance, v -> byteSource); - - // Clustering is null for static rows - return (clustering == null) ? Clustering.STATIC_CLUSTERING : clustering; - } - - class TokenOnlyPrimaryKey implements PrimaryKey - { - protected final Token token; - - TokenOnlyPrimaryKey(Token token) - { - this.token = token; - } - - @Override - public Kind kind() - { - return Kind.TOKEN; - } - - @Override - public Token token() - { - return token; - } - - @Override - public DecoratedKey partitionKey() - { - throw new UnsupportedOperationException(); - } - - @Override - public Clustering clustering() - { - throw new UnsupportedOperationException(); - } - - @Override - public ByteSource asComparableBytes(Version version) - { - throw new UnsupportedOperationException(); - } - - @Override - public int compareTo(PrimaryKey o) - { - return token().compareTo(o.token()); - } - - @Override - public int hashCode() - { - return Objects.hash(token(), clusteringComparator); - } - - @Override - public boolean equals(Object o) - { - if (o instanceof PrimaryKey) - return compareTo((PrimaryKey) o) == 0; - return false; - } - - @Override - public boolean equals(Object o, boolean strict) - { - if (o == null) - return false; - if (o instanceof PrimaryKey) - return compareTo((PrimaryKey) o, strict) == 0; - return false; - } - - @Override - public String toString() - { - return String.format("PrimaryKey: { token: %s }", token()); - } - } - - class SkinnyPrimaryKey extends TokenOnlyPrimaryKey - { - protected final DecoratedKey partitionKey; - - SkinnyPrimaryKey(DecoratedKey partitionKey) - { - super(partitionKey.getToken()); - this.partitionKey = partitionKey; - } - - @Override - public Kind kind() - { - return Kind.SKINNY; - } - - @Override - public DecoratedKey partitionKey() - { - return partitionKey; - } - - @Override - public ByteSource asComparableBytes(Version version) - { - return ByteSource.of(partitionKey().getKey(), version); - } - - @Override - public int compareTo(PrimaryKey o) - { - int cmp = super.compareTo(o); - - // If the tokens don't match then we don't need to compare any more of the key. - // Otherwise, if the other key is token only we can only compare tokens - // This is used by the ResultRetriever to skip to the current key range start position - // during result retrieval. - if (cmp != 0 || o.kind() == Kind.TOKEN) - return cmp; - - return partitionKey().compareTo(o.partitionKey()); - } - - @Override - public int hashCode() - { - return Objects.hash(token(), partitionKey(), Clustering.EMPTY, clusteringComparator); - } - - @Override - public String toString() - { - return String.format("PrimaryKey: { token: %s, partition: %s }", token(), partitionKey()); - } - } - - class StaticPrimaryKey extends SkinnyPrimaryKey - { - StaticPrimaryKey(DecoratedKey partitionKey) - { - super(partitionKey); - } - - @Override - public Kind kind() - { - return Kind.STATIC; - } - - @Override - public Clustering clustering() - { - return Clustering.STATIC_CLUSTERING; - } - - @Override - public ByteSource asComparableBytes(ByteComparable.Version version) - { - ByteSource keyComparable = ByteSource.of(partitionKey().getKey(), version); - // Static clustering cannot be serialized or made to a byte comparable, so we use null as the component. - return ByteSource.withTerminator(version == ByteComparable.Version.LEGACY ? ByteSource.END_OF_STREAM - : ByteSource.TERMINATOR, - keyComparable, - null); - } - - @Override - public int compareTo(PrimaryKey o, boolean strict) - { - int cmp = super.compareTo(o); - if (cmp != 0 || o.kind() == Kind.TOKEN || o.kind() == Kind.SKINNY) - return cmp; - - // If we're comparing strictly, order this STATIC key before a WIDE key, as this corresponds to the - // order of the corresponding row IDs in an on-disk postings list. If we're not being strict, treat - // the keys as being equal, given they are in the same partition. - if (strict && o.kind() == Kind.WIDE) - return -1; - - return 0; - } - - @Override - public int compareTo(PrimaryKey o) - { - return compareTo(o, true); - } - - @Override - public int hashCode() - { - return Objects.hash(token(), partitionKey(), Clustering.STATIC_CLUSTERING, clusteringComparator); - } - - @Override - public String toString() - { - return String.format("PrimaryKey: { token: %s, partition: %s, clustering: STATIC } ", token(), partitionKey()); - } - - @Override - public PrimaryKey toStatic() - { - return this; - } - } - - class WidePrimaryKey extends SkinnyPrimaryKey - { - private final Clustering clustering; - - WidePrimaryKey(DecoratedKey partitionKey, Clustering clustering) - { - super(partitionKey); - this.clustering = clustering; - } - - @Override - public Kind kind() - { - return Kind.WIDE; - } - - @Override - public Clustering clustering() - { - return clustering; - } - - @Override - public ByteSource asComparableBytes(ByteComparable.Version version) - { - ByteSource keyComparable = ByteSource.of(partitionKey().getKey(), version); - // It is important that the ClusteringComparator.asBytesComparable method is used - // to maintain the correct clustering sort order. - ByteSource clusteringComparable = clusteringComparator.asByteComparable(clustering()).asComparableBytes(version); - return ByteSource.withTerminator(version == ByteComparable.Version.LEGACY ? ByteSource.END_OF_STREAM - : ByteSource.TERMINATOR, - keyComparable, - clusteringComparable); - } - - @Override - public int compareTo(PrimaryKey o, boolean strict) - { - int cmp = super.compareTo(o); - if (cmp != 0 || o.kind() == Kind.TOKEN || o.kind() == Kind.SKINNY) - return cmp; - - if (o.kind() == Kind.STATIC) - // If we're comparing strictly, order this WIDE key after the STATIC key, as this corresponds to the - // order of the corresponding row IDs in an on-disk postings list. If we're not being strict, treat - // the keys as being equal, given they are in the same partition. - return strict ? 1 : 0; - - return clusteringComparator.compare(clustering(), o.clustering()); - } - - @Override - public int compareTo(PrimaryKey o) - { - return compareTo(o, true); - } - - @Override - public int hashCode() - { - return Objects.hash(token(), partitionKey(), clustering(), clusteringComparator); - } - - @Override - public String toString() - { - return String.format("PrimaryKey: { token: %s, partition: %s, clustering: %s:%s } ", - token(), - partitionKey(), - clustering().kind(), - Arrays.stream(clustering().getBufferArray()) - .map(ByteBufferUtil::bytesToHex) - .collect(Collectors.joining(", "))); - } + PrimaryKey create(DecoratedKey partitionKey, Clustering clustering); + } - @Override - public PrimaryKey toStatic() - { - return new StaticPrimaryKey(partitionKey); - } - } + /** + * Returns a {@link Factory} for creating {@link PrimaryKey} instances. The factory + * returned is based on the capabilities of the {@link IndexFeatureSet}. + * + * @param clusteringComparator the {@link ClusteringComparator} used by the + * {@link RowAwarePrimaryKeyFactory} for clustering comparisons + * @param indexFeatureSet the {@link IndexFeatureSet} used to decide the type of + * factory to use + * @return a {@link Factory} for {@link PrimaryKey} creation + */ + static Factory factory(ClusteringComparator clusteringComparator, IndexFeatureSet indexFeatureSet) + { + return indexFeatureSet.isRowAware() ? new RowAwarePrimaryKeyFactory(clusteringComparator) + : new PartitionAwarePrimaryKeyFactory(); } /** - * Returns the {@link Kind} of the {@link PrimaryKey}. The {@link Kind} is used locally in the {@link #compareTo(Object)} - * methods to determine how far the comparision needs to go between keys. - *

    - * The {@link Kind} values have a categorization of {@code isClustering}. This indicates whether the key belongs to - * a table with clustering tables or not. + * Returns a {@link PrimaryKey} to fetch the static row of the partition associated with this primary key. + * + * @return a {@link PrimaryKey} for the static row */ - Kind kind(); + PrimaryKey forStaticRow(); + + default boolean isTokenOnly() + { + return false; + } /** - * Returns the {@link Token} component of the {@link PrimaryKey} + * Returns the {@link Token} associated with this primary key. + * + * @return the {@link Token} */ Token token(); /** - * Returns the {@link DecoratedKey} representing the partition key of the {@link PrimaryKey}. - *

    - * Note: This cannot be null but some {@link PrimaryKey} implementations can throw {@link UnsupportedOperationException} - * if they do not support partition keys. + * Returns the {@link DecoratedKey} associated with this primary key. + * + * @return the {@link DecoratedKey} */ DecoratedKey partitionKey(); /** - * Returns the {@link Clustering} representing the clustering component of the {@link PrimaryKey}. - *

    - * Note: This cannot be null but some {@link PrimaryKey} implementations can throw {@link UnsupportedOperationException} - * if they do not support clustering columns. + * Returns the {@link Clustering} associated with this primary key + * + * @return the {@link Clustering} */ Clustering clustering(); + /** + * Return whether the primary key has a clustering, i.e., has non-static clustering column(s). + * This operation might require loading the primary key. + */ + default boolean hasClustering() + { + return clustering() != null && !clustering().isEmpty(); + } + + /** + * Load the primary key from the {@link Supplier (PrimaryKey)} (if one + * is available) and fully populate the primary key. + * + * @return the fully populated {@link PrimaryKey} + */ + PrimaryKey loadDeferred(); + /** * Returns the {@link PrimaryKey} as a {@link ByteSource} byte comparable representation. - *

    + * * It is important that these representations are only ever used with byte comparables using * the same elements. This means that {@code asComparableBytes} responses can only be used * together from the same {@link PrimaryKey} implementation. * * @param version the {@link ByteComparable.Version} to use for the implementation * @return the {@code ByteSource} byte comparable. - * @throws UnsupportedOperationException for {@link PrimaryKey} implementations that are not byte-comparable */ ByteSource asComparableBytes(ByteComparable.Version version); - default PrimaryKey toStatic() - { - throw new UnsupportedOperationException("Only STATIC and WIDE keys can be converted to STATIC"); - } - - default int compareTo(PrimaryKey key, boolean strict) - { - return compareTo(key); - } + /** + * Returns the {@link PrimaryKey} as a {@link ByteSource} min prefix byte comparable representation. + * + * @param version the {@link ByteComparable.Version} to use for the implementation + * @return the {@code ByteSource} min prefix byte comparable. + */ + ByteSource asComparableBytesMinPrefix(ByteComparable.Version version); - boolean equals(Object obj, boolean strict); + /** + * Returns the {@link PrimaryKey} as a {@link ByteSource} max prefix byte comparable representation. + * + * @param version the {@link ByteComparable.Version} to use for the implementation + * @return the {@code ByteSource} max prefix byte comparable. + */ + ByteSource asComparableBytesMaxPrefix(ByteComparable.Version version); } diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java new file mode 100644 index 000000000000..21032ebfd7a0 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.Collections; +import java.util.List; + +public class PrimaryKeyListUtil +{ + + /** Create a sublist of the keys within the provided bounds (inclusive) */ + public static List getKeysInRange(List keys, PrimaryKey minKey, PrimaryKey maxKey) + { + int minIndex = PrimaryKeyListUtil.findBoundaryIndex(keys, minKey, false); + int maxIndex = PrimaryKeyListUtil.findBoundaryIndex(keys, maxKey, true); + return keys.subList(minIndex, maxIndex); + } + + private static int findBoundaryIndex(List keys, PrimaryKey key, boolean findMax) + { + int index = Collections.binarySearch(keys, key); + + if (index < 0) + return -index - 1; + + // When findMax is true, we are finding an exclusive upper bound, but binary search is inclusive, so we + // increment by 1 to get the exclusive upper bound. + return findMax ? index + 1 : index; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithByteComparable.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithByteComparable.java new file mode 100644 index 000000000000..cd64c158566e --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithByteComparable.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +/** + * A {@link PrimaryKey} that includes a {@link ByteComparable} value from a source index. + * Note: this class has a natural ordering that is inconsistent with equals. + */ +public class PrimaryKeyWithByteComparable extends PrimaryKeyWithSortKey +{ + private final ByteComparable byteComparable; + + public PrimaryKeyWithByteComparable(IndexContext context, Memtable sourceTable, PrimaryKey primaryKey, ByteComparable byteComparable) + { + this(context, (Object) sourceTable, primaryKey, byteComparable); + } + + public PrimaryKeyWithByteComparable(IndexContext context, SSTableId sourceTable, PrimaryKey primaryKey, ByteComparable byteComparable) + { + this(context, (Object) sourceTable, primaryKey, byteComparable); + } + + private PrimaryKeyWithByteComparable(IndexContext context, Object sourceTable, PrimaryKey primaryKey, ByteComparable byteComparable) + { + super(context, sourceTable, primaryKey); + this.byteComparable = byteComparable; + } + + @Override + public PrimaryKeyWithByteComparable forStaticRow() + { + return new PrimaryKeyWithByteComparable(context, sourceTable, primaryKey.forStaticRow(), byteComparable); + } + + @Override + protected boolean isIndexDataEqualToLiveData(ByteBuffer value) + { + if (context.isLiteral()) + { + ByteSource byteSource = byteComparable.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION); + byte[] indexedValue = ByteSourceInverse.readBytes(byteSource); + byte[] liveValue = ByteBufferUtil.getArray(value); + return Arrays.compare(indexedValue, liveValue) == 0; + } + else + { + var peekableBytes = ByteSource.peekable(byteComparable.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + var bytes = context.getValidator().fromComparableBytes(peekableBytes, TypeUtil.BYTE_COMPARABLE_VERSION); + return value.compareTo(bytes) == 0; + } + } + + @Override + public int compareTo(PrimaryKey o) + { + if (!(o instanceof PrimaryKeyWithByteComparable)) + throw new IllegalArgumentException("Cannot compare PrimaryKeyWithByteComparable with " + o.getClass().getSimpleName()); + + return ByteComparable.compare(byteComparable, ((PrimaryKeyWithByteComparable) o).byteComparable, TypeUtil.BYTE_COMPARABLE_VERSION); + } + + @Override + public long ramBytesUsed() + { + return super.ramBytesUsed() + RamUsageEstimator.NUM_BYTES_OBJECT_REF; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithScore.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithScore.java new file mode 100644 index 000000000000..d0d3800e13ac --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithScore.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.nio.ByteBuffer; + +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.io.sstable.SSTableId; + +/** + * A {@link PrimaryKey} that includes a score from a source index. + * Note: this class has a natural ordering that is inconsistent with equals. + */ +public class PrimaryKeyWithScore extends PrimaryKeyWithSortKey +{ + public final float indexScore; + public final boolean isScoreApproximate; + + /** + * Constructs a new {@link PrimaryKeyWithScore} for a memtable source. Memtables always have exact scores, so + * we do not parameterize the isScoreApproximate flag. + */ + public PrimaryKeyWithScore(IndexContext context, Memtable source, PrimaryKey primaryKey, float indexScore) + { + this(context, (Object) source, primaryKey, indexScore, false); + } + + /** + * Constructs a new {@link PrimaryKeyWithScore} for an sstable source. SStables may have approximate scores, so + * we parameterize the isScoreApproximate flag. Setting the isScoreApproximate flag to true will cause the score to be + * recalculated from the live data when the row is read. + */ + public PrimaryKeyWithScore(IndexContext context, SSTableId source, PrimaryKey primaryKey, float indexScore, boolean isScoreApproximate) + { + this(context, (Object) source, primaryKey, indexScore, isScoreApproximate); + } + + private PrimaryKeyWithScore(IndexContext context, Object source, PrimaryKey primaryKey, float indexScore, boolean isScoreApproximate) + { + super(context, source, primaryKey); + this.indexScore = indexScore; + this.isScoreApproximate = isScoreApproximate; + } + + @Override + public PrimaryKeyWithScore forStaticRow() + { + return new PrimaryKeyWithScore(context, sourceTable, primaryKey.forStaticRow(), indexScore, isScoreApproximate); + } + + @Override + protected boolean isIndexDataEqualToLiveData(ByteBuffer value) + { + // Vector indexes handle updated rows properly and not allow a row to have more than one value in the same + // index segment. Therefore, there is no need to validate the index data against the live data. + return true; + } + + public float getExactScore(Orderer orderer, Row row) + { + if (!isScoreApproximate) + return indexScore; + return orderer.score(row.getCell(context.getDefinition()).buffer()); + } + + @Override + public int compareTo(PrimaryKey o) + { + if (!(o instanceof PrimaryKeyWithScore)) + throw new IllegalArgumentException("Cannot compare PrimaryKeyWithScore with " + o.getClass().getSimpleName()); + + // Descending order + return Float.compare(((PrimaryKeyWithScore) o).indexScore, indexScore); + } + + @Override + public long ramBytesUsed() + { + // Include super class fields plus float value + return super.ramBytesUsed() + Float.BYTES; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithSortKey.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithSortKey.java new file mode 100644 index 000000000000..73de00c309c7 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyWithSortKey.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.nio.ByteBuffer; + +import io.github.jbellis.jvector.util.RamUsageEstimator; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * A PrimaryKey with one piece of metadata. Subclasses define the metadata, and to prevent unnecessary boxing, the + * metadata is not referenced in this calss. The metadata is not used to determine equality or hash code, but it is used + * to compare the PrimaryKey objects. + * Note: this class has a natural ordering that is inconsistent with equals. + */ +public abstract class PrimaryKeyWithSortKey implements PrimaryKey +{ + protected final IndexContext context; + protected final PrimaryKey primaryKey; + // Either a Memtable reference or an SSTableId reference + protected final Object sourceTable; + + protected PrimaryKeyWithSortKey(IndexContext context, Object sourceTable, PrimaryKey primaryKey) + { + assert sourceTable instanceof Memtable || sourceTable instanceof SSTableId; + this.context = context; + this.sourceTable = sourceTable; + this.primaryKey = primaryKey; + } + + public PrimaryKey primaryKey() + { + return primaryKey; + } + + public boolean isIndexDataValid(Row row, long nowInSecs) + { + ColumnMetadata column = context.getDefinition(); + + // If the indexed column is part of the primary key, we don't need this type of validation because we would have + // fetched the row using the indexed primary key, so they have to match. + if (column.isPrimaryKeyColumn()) + return true; + + // If the row is static and the column is not static, or vice versa, the indexed value won't be present so we + // don't need to check if live data matches indexed data. + if (row.isStatic() != column.isStatic()) + return true; + + var cell = row.getCell(column); + if (!cell.isLive(nowInSecs)) + return false; + + // Check if the row is wrapped and if not, skip the source table check + if (!(cell instanceof CellWithSourceTable)) + { + // If the cell is not wrapped, we can't validate the source table, + // so we just check if the index data matches the live data + return isIndexDataEqualToLiveData(cell.buffer()); + } + return sourceTable.equals(((CellWithSourceTable) cell).sourceTable()) + && isIndexDataEqualToLiveData(cell.buffer()); + } + + /** + * Compares the index data to the live data to ensure that the index data is still valid. This is only + * necessary when an index allows one row to have multiple values associated with it. + */ + abstract protected boolean isIndexDataEqualToLiveData(ByteBuffer value); + + @Override + public final int hashCode() + { + // The sort key must not affect the hash code because + // the same Primary Key could have different scores depending + // on the source sstable/index, and we store this object + // in a HashMap to prevent loading the same row multiple times. + return primaryKey.hashCode(); + } + + @Override + public final boolean equals(Object obj) + { + if (!(obj instanceof PrimaryKeyWithSortKey)) + return false; + + // The sort key must not affect the equality because + // the same Primary Key could have different scores depending + // on the source sstable/index, and we store this object + // in a HashMap to prevent loading the same row multiple times. + return primaryKey.equals(((PrimaryKeyWithSortKey) obj).primaryKey()); + } + + // Generic primary key wrapper methods: + @Override + public Token token() + { + return primaryKey.token(); + } + + @Override + public DecoratedKey partitionKey() + { + return primaryKey.partitionKey(); + } + + @Override + public Clustering clustering() + { + return primaryKey.clustering(); + } + + @Override + public PrimaryKey loadDeferred() + { + return primaryKey.loadDeferred(); + } + + @Override + public ByteSource asComparableBytes(ByteComparable.Version version) + { + return primaryKey.asComparableBytes(version); + } + + @Override + public ByteSource asComparableBytesMinPrefix(ByteComparable.Version version) + { + return primaryKey.asComparableBytesMinPrefix(version); + } + + @Override + public ByteSource asComparableBytesMaxPrefix(ByteComparable.Version version) + { + return primaryKey.asComparableBytesMaxPrefix(version); + } + + @Override + public long ramBytesUsed() + { + // Object header + 3 references (context, primaryKey, sourceTable) + return RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + + 3L * RamUsageEstimator.NUM_BYTES_OBJECT_REF + + primaryKey.ramBytesUsed(); + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeys.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeys.java index 3ba7c08af07c..15431b7d3a14 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeys.java +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeys.java @@ -19,10 +19,11 @@ import java.util.Iterator; import java.util.SortedSet; -import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ConcurrentSkipListMap; -import javax.annotation.concurrent.ThreadSafe; +import com.google.common.collect.Iterators; +import org.apache.cassandra.index.sai.memory.MemoryIndex; import org.apache.cassandra.utils.ObjectSizes; /** @@ -30,26 +31,52 @@ * * The primary keys are sorted first by token, then by partition key value, and then by clustering. */ -@ThreadSafe -public class PrimaryKeys implements Iterable +public class PrimaryKeys implements Iterable { private static final long EMPTY_SIZE = ObjectSizes.measure(new PrimaryKeys()); + // from https://github.com/gaul/java-collection-overhead - private static final long SET_ENTRY_OVERHEAD = 36; + private static final long MAP_ENTRY_OVERHEAD = 40 + Integer.BYTES; + + private final ConcurrentSkipListMap keys = new ConcurrentSkipListMap<>(); - private final ConcurrentSkipListSet keys = new ConcurrentSkipListSet<>(); + /** + * Adds the specified {@link PrimaryKey} incrementing its frequency. + * + * @param key a primary key + * @return the bytes allocated for the key (0 if it already existed in the set) + */ + public long addAndIncrementFrequency(PrimaryKey key) + { + return keys.compute(key, (k, v) -> v == null ? 1 : v + 1) == 1 ? MAP_ENTRY_OVERHEAD : 0; + } + + /** + * Adds the specified {@link PrimaryKey} resetting its frequency to 1. + * + * @param key a primary key + * @return the bytes allocated for the key (0 if it already existed in the set) + */ + public long addAndResetFrequency(PrimaryKey key) + { + Object prev = keys.put(key, 1); + return prev == null ? MAP_ENTRY_OVERHEAD : 0; + } /** - * Adds a {@link PrimaryKey} and returns the on-heap memory used if the key was added + * Removes the specified {@link PrimaryKey}. + * + * @param key the key to remove + * @return */ - public long add(PrimaryKey key) + public long remove(PrimaryKey key) { - return keys.add(key) ? SET_ENTRY_OVERHEAD : 0; + return keys.remove(key) != null ? -MAP_ENTRY_OVERHEAD : 0; } public SortedSet keys() { - return keys; + return keys.keySet(); } public int size() @@ -62,14 +89,15 @@ public boolean isEmpty() return keys.isEmpty(); } - public long unsharedHeapSize() + public static long unsharedHeapSize() { return EMPTY_SIZE; } @Override - public Iterator iterator() + public Iterator iterator() { - return keys.iterator(); + return Iterators.transform(keys.entrySet().iterator(), + entry -> new MemoryIndex.PkWithFrequency(entry.getKey(), entry.getValue())); } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/RangeUtil.java b/src/java/org/apache/cassandra/index/sai/utils/RangeUtil.java index 8a46a2d42935..6d828eb35dfe 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/RangeUtil.java +++ b/src/java/org/apache/cassandra/index/sai/utils/RangeUtil.java @@ -18,10 +18,15 @@ package org.apache.cassandra.index.sai.utils; +import java.util.List; + import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.format.SSTableReader; public class RangeUtil { @@ -31,4 +36,60 @@ public static boolean coversFullRing(AbstractBounds keyRange) { return keyRange.left.equals(MIN_KEY_BOUND) && keyRange.right.equals(MIN_KEY_BOUND); } + + /** + * Check if the provided {@link SSTableReader} intersects with the provided key range. + * @param reader SSTableReader + * @param keyRange key range + * @return true the key range intersects with the min/max key bounds + */ + public static boolean intersects(SSTableReader reader, AbstractBounds keyRange) + { + return intersects(reader.first.getToken().minKeyBound(), reader.last.getToken().maxKeyBound(), keyRange); + } + + /** + * Check if the min/max key bounds intersects with the keyRange + * @param minKeyBound min key bound + * @param maxKeyBound max key bound + * @param keyRange key range + * @return true the key range intersects with the min/max key bounds + */ + public static boolean intersects(Token.KeyBound minKeyBound, Token.KeyBound maxKeyBound, AbstractBounds keyRange) + { + if (keyRange instanceof Range && ((Range)keyRange).isWrapAround()) + return keyRange.contains(minKeyBound) || keyRange.contains(maxKeyBound); + + int cmp = keyRange.right.compareTo(minKeyBound); + // if right is minimum, it means right is the max token and bigger than maxKey. + // if right bound is less than minKeyBound, no intersection + if (!keyRange.right.isMinimum() && (!keyRange.inclusiveRight() && cmp == 0 || cmp < 0)) + return false; + + cmp = keyRange.left.compareTo(maxKeyBound); + // if left bound is bigger than maxKeyBound, no intersection + return (keyRange.isStartInclusive() || cmp != 0) && cmp <= 0; + } + + + public static double getRingFraction(AbstractBounds keyRange) + { + return keyRange.left.getToken().size(keyRange.right.getToken().nextValidToken()); + } + + public static double getRingFraction(DataRange dataRange) + { + return getRingFraction(dataRange.keyRange()); + } + + public static double getRingFraction(List dataRanges) + { + double fraction = 0.0; + for (DataRange range : dataRanges) + { + fraction += getRingFraction(range); + } + fraction = Math.min(1.0, fraction); // rounding errors could cause exceeding 1.0 + return fraction; + } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/RowIdWithByteComparable.java b/src/java/org/apache/cassandra/index/sai/utils/RowIdWithByteComparable.java new file mode 100644 index 000000000000..d14611ce7de1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/RowIdWithByteComparable.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + +public class RowIdWithByteComparable extends RowIdWithMeta +{ + private final ByteComparable byteComparable; + + public RowIdWithByteComparable(int segmentRowId, ByteComparable byteComparable) + { + super(segmentRowId); + this.byteComparable = byteComparable; + } + + @Override + protected PrimaryKeyWithSortKey wrapPrimaryKey(IndexContext context, SSTableId sstableId, PrimaryKey primaryKey) + { + return new PrimaryKeyWithByteComparable(context, sstableId, primaryKey, byteComparable); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/index/sai/utils/RowIdWithMeta.java b/src/java/org/apache/cassandra/index/sai/utils/RowIdWithMeta.java new file mode 100644 index 000000000000..88f0d3bb26b1 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/RowIdWithMeta.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.io.sstable.SSTableId; + +/** + * Represents a row id with additional metadata. The metadata is not a type parameter to prevent unnecessary boxing. + */ +public abstract class RowIdWithMeta +{ + private final int segmentRowId; + + protected RowIdWithMeta(int segmentRowId) + { + this.segmentRowId = segmentRowId; + } + + public final int getSegmentRowId() + { + return segmentRowId; + } + + public PrimaryKeyWithSortKey buildPrimaryKeyWithSortKey(IndexContext indexContext, + SSTableId sstableId, + PrimaryKeyMap primaryKeyMap, + long segmentRowIdOffset) + { + var pk = primaryKeyMap.primaryKeyFromRowId(segmentRowIdOffset + segmentRowId); + return wrapPrimaryKey(indexContext, sstableId, pk); + } + + /** + * Wrap the provided primary key with the stored metadata. + * @param indexContext the index context + * @param sstableId the sstable id + * @param primaryKey the primary key + * @return the wrapped primary key with its associated metadata + */ + protected abstract PrimaryKeyWithSortKey wrapPrimaryKey(IndexContext indexContext, SSTableId sstableId, PrimaryKey primaryKey); +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/RowIdWithScore.java b/src/java/org/apache/cassandra/index/sai/utils/RowIdWithScore.java new file mode 100644 index 000000000000..f14a348dd8d0 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/RowIdWithScore.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.io.sstable.SSTableId; + +/** + * Represents a row id with a score. + */ +public class RowIdWithScore extends RowIdWithMeta +{ + public final float score; + public final boolean isScoreApproximate; + + /** + * @param segmentRowId the row id + * @param score the score + * @param isScoreApproximate whether the score is approximate. If it is, the score will be recalculated from the live + * data when the row is read. + */ + public RowIdWithScore(int segmentRowId, float score, boolean isScoreApproximate) + { + super(segmentRowId); + this.score = score; + this.isScoreApproximate = isScoreApproximate; + } + + public static int compare(RowIdWithScore l, RowIdWithScore r) + { + // Inverted comparison to sort in descending order. + // Note that we are fine comparing approximate and exact scores and accept this as part of the "appoximate" + // of the ANN search logic. + return Float.compare(r.score, l.score); + } + + @Override + protected PrimaryKeyWithSortKey wrapPrimaryKey(IndexContext indexContext, SSTableId sstableId, PrimaryKey primaryKey) + { + return new PrimaryKeyWithScore(indexContext, sstableId, primaryKey, score, isScoreApproximate); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java b/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java index a883abb99b76..4b796e712e91 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java +++ b/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java @@ -290,6 +290,12 @@ public int dataSize() return row.dataSize(); } + @Override + public int liveDataSize(long nowInSec) + { + return row.liveDataSize(nowInSec); + } + @Override public long unsharedHeapSize() { @@ -389,4 +395,16 @@ public String toString() ", source=" + source + '}'; } + + @Override + public long minTimestamp() + { + return row.minTimestamp(); + } + + @Override + public long maxTimestamp() + { + return row.maxTimestamp(); + } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/RowWithSourceTable.java b/src/java/org/apache/cassandra/index/sai/utils/RowWithSourceTable.java new file mode 100644 index 000000000000..cacebfb280d4 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/RowWithSourceTable.java @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DeletionPurger; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Digest; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.BiLongAccumulator; +import org.apache.cassandra.utils.LongAccumulator; +import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.SearchIterator; +import org.apache.cassandra.utils.memory.Cloner; + +/** + * A Row wrapper that has a source object that gets added to cell as part of the getCell call. This can only be used + * validly when all the cells share a common source object. + */ +public class RowWithSourceTable implements Row +{ + private static final long EMPTY_SIZE = ObjectSizes.measure(new RowWithSourceTable(null, null)); + + private final Row row; + private final Object source; + + public RowWithSourceTable(Row row, Object source) + { + this.row = row; + this.source = source; + } + + @Override + public Kind kind() + { + return row.kind(); + } + + @Override + public Clustering clustering() + { + return row.clustering(); + } + + @Override + public void digest(Digest digest) + { + row.digest(digest); + } + + @Override + public void validateData(TableMetadata metadata) + { + row.validateData(metadata); + } + + @Override + public boolean hasInvalidDeletions() + { + return row.hasInvalidDeletions(); + } + + @Override + public Collection columns() + { + return row.columns(); + } + + @Override + public int columnCount() + { + return row.columnCount(); + } + + @Override + public Deletion deletion() + { + return row.deletion(); + } + + @Override + public LivenessInfo primaryKeyLivenessInfo() + { + return row.primaryKeyLivenessInfo(); + } + + @Override + public boolean isStatic() + { + return row.isStatic(); + } + + @Override + public boolean isEmpty() + { + return row.isEmpty(); + } + + @Override + public String toString(TableMetadata metadata) + { + return row.toString(metadata); + } + + @Override + public boolean hasLiveData(long nowInSec, boolean enforceStrictLiveness) + { + return row.hasLiveData(nowInSec, enforceStrictLiveness); + } + + @Override + public Cell getCell(ColumnMetadata c) + { + var cell = row.getCell(c); + if (cell == null) + return null; + return new CellWithSourceTable<>(cell, source); + } + + @Override + public Cell getCell(ColumnMetadata c, CellPath path) + { + return wrapCell(row.getCell(c, path)); + } + + @Override + public ComplexColumnData getComplexColumnData(ColumnMetadata c) + { + return (ComplexColumnData) wrapColumnData(row.getComplexColumnData(c)); + } + + @Override + public ColumnData getColumnData(ColumnMetadata c) + { + return wrapColumnData(row.getColumnData(c)); + } + + @Override + public Iterable> cells() + { + return Iterables.transform(row.cells(), this::wrapCell); + } + + @Override + public Collection columnData() + { + return Collections2.transform(row.columnData(), this::wrapColumnData); + } + + @Override + public Iterable> cellsInLegacyOrder(TableMetadata metadata, boolean reversed) + { + return Iterables.transform(row.cellsInLegacyOrder(metadata, reversed), this::wrapCell); + } + + @Override + public boolean hasComplexDeletion() + { + return row.hasComplexDeletion(); + } + + @Override + public boolean hasComplex() + { + return row.hasComplex(); + } + + @Override + public boolean hasDeletion(long nowInSec) + { + return row.hasDeletion(nowInSec); + } + + @Override + public SearchIterator searchIterator() + { + var iterator = row.searchIterator(); + return key -> wrapColumnData(iterator.next(key)); + } + + @Override + public Row filter(ColumnFilter filter, TableMetadata metadata) + { + return maybeWrapRow(row.filter(filter, metadata)); + } + + @Override + public Row filter(ColumnFilter filter, DeletionTime activeDeletion, boolean setActiveDeletionToRow, TableMetadata metadata) + { + return maybeWrapRow(row.filter(filter, activeDeletion, setActiveDeletionToRow, metadata)); + } + + @Override + public Row transformAndFilter(LivenessInfo info, Deletion deletion, Function function) + { + return maybeWrapRow(row.transformAndFilter(info, deletion, function)); + } + + @Override + public Row transformAndFilter(Function function) + { + return maybeWrapRow(row.transformAndFilter(function)); + } + + @Override + public Row clone(Cloner cloner) + { + return maybeWrapRow(row.clone(cloner)); + } + + @Override + public Row purge(DeletionPurger purger, long nowInSec, boolean enforceStrictLiveness) + { + return maybeWrapRow(row.purge(purger, nowInSec, enforceStrictLiveness)); + } + + @Override + public Row withOnlyQueriedData(ColumnFilter filter) + { + return maybeWrapRow(row.withOnlyQueriedData(filter)); + } + + @Override + public Row purgeDataOlderThan(long timestamp, boolean enforceStrictLiveness) + { + return maybeWrapRow(row.purgeDataOlderThan(timestamp, enforceStrictLiveness)); + } + + @Override + public Row markCounterLocalToBeCleared() + { + return maybeWrapRow(row.markCounterLocalToBeCleared()); + } + + @Override + public Row updateAllTimestamp(long newTimestamp) + { + return maybeWrapRow(row.updateAllTimestamp(newTimestamp)); + } + + @Override + public Row withRowDeletion(DeletionTime deletion) + { + return maybeWrapRow(row.withRowDeletion(deletion)); + } + + @Override + public int dataSize() + { + return row.dataSize(); + } + + @Override + public int liveDataSize(long nowInSec) + { + return row.liveDataSize(nowInSec); + } + + @Override + public long unsharedHeapSizeExcludingData() + { + return row.unsharedHeapSizeExcludingData() + EMPTY_SIZE; + } + + @Override + public String toString(TableMetadata metadata, boolean fullDetails) + { + return row.toString(metadata, fullDetails); + } + + @Override + public long unsharedHeapSize() + { + return row.unsharedHeapSize(); + } + + @Override + public String toString(TableMetadata metadata, boolean includeClusterKeys, boolean fullDetails) + { + return row.toString(metadata, includeClusterKeys, fullDetails); + } + + @Override + public long minTimestamp() + { + return row.minTimestamp(); + } + + @Override + public long maxTimestamp() + { + return row.maxTimestamp(); + } + + @Override + public void apply(Consumer function) + { + row.apply(function); + } + + @Override + public void apply(BiConsumer function, A arg) + { + row.apply(function, arg); + } + + @Override + public long accumulate(LongAccumulator accumulator, long initialValue) + { + return row.accumulate(accumulator, initialValue); + } + + @Override + public long accumulate(LongAccumulator accumulator, Comparator comparator, ColumnData from, long initialValue) + { + return row.accumulate(accumulator, comparator, from, initialValue); + } + + @Override + public long accumulate(BiLongAccumulator accumulator, A arg, long initialValue) + { + return row.accumulate(accumulator, arg, initialValue); + } + + @Override + public long accumulate(BiLongAccumulator accumulator, A arg, Comparator comparator, ColumnData from, long initialValue) + { + return row.accumulate(accumulator, arg, comparator, from, initialValue); + } + + @Override + public Iterator iterator() + { + return Iterators.transform(row.iterator(), this::wrapColumnData); + } + + private ColumnData wrapColumnData(ColumnData c) + { + if (c == null) + return null; + if (c instanceof Cell) + return new CellWithSourceTable<>((Cell) c, source); + if (c instanceof ComplexColumnData) + return ((ComplexColumnData) c).transform(c1 -> new CellWithSourceTable<>(c1, source)); + throw new IllegalStateException("Unexpected ColumnData type: " + c.getClass().getName()); + } + + private Cell wrapCell(Cell c) + { + return c != null ? new CellWithSourceTable<>(c, source) : null; + } + + private Row maybeWrapRow(Row r) + { + if (r == null) + return null; + if (r == this.row) + return this; + return new RowWithSourceTable(r, source); + } + + @Override + public String toString() + { + return "RowWithSourceTable{" + + row + + ", source=" + source + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/SAICodecUtils.java b/src/java/org/apache/cassandra/index/sai/utils/SAICodecUtils.java new file mode 100644 index 000000000000..a1eadf4dd105 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/SAICodecUtils.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.io.IOException; +import java.io.OutputStream; + +import io.github.jbellis.jvector.disk.RandomAccessWriter; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; +import org.apache.cassandra.io.compress.CorruptBlockException; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.store.ChecksumIndexInput; +import org.apache.lucene.store.DataInput; +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.store.OutputStreamDataOutput; + +import static org.apache.lucene.codecs.CodecUtil.CODEC_MAGIC; +import static org.apache.lucene.codecs.CodecUtil.FOOTER_MAGIC; +import static org.apache.lucene.codecs.CodecUtil.footerLength; +import static org.apache.lucene.codecs.CodecUtil.readBEInt; +import static org.apache.lucene.codecs.CodecUtil.readBELong; +import static org.apache.lucene.codecs.CodecUtil.writeBEInt; +import static org.apache.lucene.codecs.CodecUtil.writeBELong; + +public class SAICodecUtils +{ + public static final String FOOTER_POINTER = "footerPointer"; + + public static DataOutput toLuceneOutput(java.io.DataOutput out) { + var os = new OutputStream() + { + @Override + public void write(int b) throws IOException + { + out.write(b); + } + }; + return new OutputStreamDataOutput(os); + } + + public static void writeHeader(org.apache.cassandra.index.sai.disk.io.IndexOutput out) throws IOException + { + writeHeader(out, out.version()); + } + + /** + * Backward-compatible overload for code compiled against older versions of this class + * that had writeHeader(DataOutput) with a single parameter. + * + * @deprecated Use {@link #writeHeader(org.apache.cassandra.index.sai.disk.io.IndexOutput)} or + * {@link #writeHeader(DataOutput, Version)} instead. + */ + @Deprecated(since = "CC5") + public static void writeHeader(DataOutput out) throws IOException + { + if (out instanceof org.apache.cassandra.index.sai.disk.io.IndexOutput) + { + // If it's an IndexOutput, use the version-aware method + writeHeader((org.apache.cassandra.index.sai.disk.io.IndexOutput) out); + } + else + { + // For other DataOutput types, use LATEST version for backward compatibility + writeHeader(out, Version.LATEST); + } + } + + public static void writeHeader(DataOutput out, Version version) throws IOException + { + writeBEInt(out, CODEC_MAGIC); + out.writeString(version.toString()); + } + + public static int headerSize() { + // Lucene's string-writing code is complex, but this is what it works out to + // until version length exceeds 127 characters or we add non-ascii characters + return 7; + } + + public static void writeFooter(IndexOutput out) throws IOException + { + writeBEInt(out, FOOTER_MAGIC); + writeBEInt(out, 0); + writeChecksum(out); + } + + // Warning: this method produces an incomplete checksum when using other Lucene tooling because it computes + // the checksum without including the FOOTER_MAGIC and 0. See https://github.com/riptano/cndb/issues/14501. + public static void writeFooter(RandomAccessWriter braw, long checksum) throws IOException + { + var out = toLuceneOutput(braw); + writeBEInt(out, FOOTER_MAGIC); + writeBEInt(out, 0); + writeBELong(out, checksum); + } + + public static Version checkHeader(DataInput in) throws IOException + { + return checkHeader(in, Version.EARLIEST); + } + + public static Version checkHeader(DataInput in, Version earliest) throws IOException + { + try + { + final int actualMagic = readBEInt(in); + if (actualMagic != CODEC_MAGIC) + { + throw new CorruptIndexException("codec header mismatch: actual header=" + actualMagic + " vs expected header=" + CODEC_MAGIC, in); + } + final Version actualVersion = Version.parse(in.readString()); + if (!actualVersion.onOrAfter(earliest)) + { + throw new IOException("Unsupported version: " + actualVersion); + } + return actualVersion; + } + catch (Throwable th) + { + if (th.getCause() instanceof CorruptBlockException) + { + throw new CorruptIndexException("corrupted", in, th.getCause()); + } + else + { + throw th; + } + } + } + + public static long checkFooter(ChecksumIndexInput in) throws IOException + { + validateFooter(in, false); + long actualChecksum = in.getChecksum(); + long expectedChecksum = readChecksum(in); + if (expectedChecksum != actualChecksum) + { + throw new CorruptIndexException("checksum failed (hardware problem?) : expected=" + Long.toHexString(expectedChecksum) + + " actual=" + Long.toHexString(actualChecksum), in); + } + return actualChecksum; + } + + public static void validate(IndexInput input) throws IOException + { + validate(input, Version.EARLIEST); + } + + public static void validate(IndexInput input, Version earliest) throws IOException + { + checkHeader(input, earliest); + validateFooterAndResetPosition(input); + } + + public static void validate(IndexInput input, long footerPointer) throws IOException + { + checkHeader(input); + + long current = input.getFilePointer(); + input.seek(footerPointer); + validateFooter(input, true); + + input.seek(current); + } + + public static void validateFooterAndResetPosition(IndexInput in) throws IOException + { + long position = in.getFilePointer(); + long fileLength = in.length(); + long footerLength = footerLength(); + long footerPosition = fileLength - footerLength; + + if (footerPosition < 0) + { + throw new CorruptIndexException("invalid codec footer (file truncated?): file length=" + fileLength + ", footer length=" + footerLength, in); + } + + in.seek(footerPosition); + validateFooter(in, false); + in.seek(position); + } + + /** + * See {@link org.apache.lucene.codecs.CodecUtil#checksumEntireFile(org.apache.lucene.store.IndexInput)}. + * + * @param input IndexInput to validate. + * @param version Index version + * @throws IOException if a corruption is detected. + */ + public static void validateChecksum(IndexInput input, Version version) throws IOException + { + IndexInput clone = input.clone(); + clone.seek(0L); + ChecksumIndexInput in = IndexFileUtils.getBufferedChecksumIndexInput(clone, version); + + assert in.getFilePointer() == 0L : in.getFilePointer() + " bytes already read from this input!"; + + if (in.length() < (long) footerLength()) + throw new CorruptIndexException("misplaced codec footer (file truncated?): length=" + in.length() + " but footerLength==" + footerLength(), input); + else + { + in.seek(in.length() - (long) footerLength()); + checkFooter(in); + } + } + + /** + * Copied from org.apache.lucene.codecs.CodecUtil.validateFooter(IndexInput) + */ + public static void validateFooter(IndexInput in, boolean padded) throws IOException + { + long remaining = in.length() - in.getFilePointer(); + long expected = footerLength(); + + if (remaining >= 4) + { + final int magic = readBEInt(in); + + if (magic != FOOTER_MAGIC) + { + String additionalDetails = ""; + if (remaining != expected) + additionalDetails = " (and invalid number of bytes: remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer() + ')'; + throw new CorruptIndexException("codec footer mismatch (file truncated?): actual footer=" + magic + " vs expected footer=" + FOOTER_MAGIC + additionalDetails, in); + } + } + + if (!padded) + { + if (remaining < expected) + { + throw new CorruptIndexException("misplaced codec footer (file truncated?): remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer(), in); + } + else if (remaining > expected) + { + throw new CorruptIndexException("misplaced codec footer (file extended?): remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer(), in); + } + } + + + final int algorithmID = readBEInt(in); + + if (algorithmID != 0) + { + throw new CorruptIndexException("codec footer mismatch: unknown algorithmID: " + algorithmID, in); + } + } + + + // Copied from Lucene CodecUtil as they are not public + + /** + * Writes checksum value as a 64-bit long to the output. + * @throws IllegalStateException if CRC is formatted incorrectly (wrong bits set) + * @throws IOException if an i/o error occurs + */ + static void writeChecksum(IndexOutput output) throws IOException { + long value = output.getChecksum(); + if ((value & 0xFFFFFFFF00000000L) != 0) { + throw new IllegalStateException("Illegal checksum: " + value + " (resource=" + output + ")"); + } + writeBELong(output, value); + } + + /** + * Reads checksum value as a 64-bit long from the input. + * @throws CorruptIndexException if CRC is formatted incorrectly (wrong bits set) + * @throws IOException if an i/o error occurs + */ + static long readChecksum(IndexInput input) throws IOException { + long value = readBELong(input); + if ((value & 0xFFFFFFFF00000000L) != 0) { + throw new CorruptIndexException("Illegal checksum: " + value, input); + } + return value; + } + + // Copied from Lucene PackedInts as they are not public + + public static int checkBlockSize(int blockSize, int minBlockSize, int maxBlockSize) { + if (blockSize >= minBlockSize && blockSize <= maxBlockSize) { + if ((blockSize & blockSize - 1) != 0) { + throw new IllegalArgumentException("blockSize must be a power of two, got " + blockSize); + } else { + return Integer.numberOfTrailingZeros(blockSize); + } + } else { + throw new IllegalArgumentException("blockSize must be >= " + minBlockSize + " and <= " + maxBlockSize + ", got " + blockSize); + } + } + + public static int numBlocks(long size, int blockSize) { + int numBlocks = (int)(size / (long)blockSize) + (size % (long)blockSize == 0L ? 0 : 1); + if ((long)numBlocks * (long)blockSize < size) { + throw new IllegalArgumentException("size is too large for this block size"); + } else { + return numBlocks; + } + } + + // Copied from Lucene BlockPackedReaderIterator as they are not public + + /** + * Same as DataInput.readVLong but supports negative values + */ + public static long readVLong(DataInput in) throws IOException + { + byte b = in.readByte(); + if (b >= 0) return b; + long i = b & 0x7FL; + b = in.readByte(); + i |= (b & 0x7FL) << 7; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0x7FL) << 14; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0x7FL) << 21; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0x7FL) << 28; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0x7FL) << 35; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0x7FL) << 42; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0x7FL) << 49; + if (b >= 0) return i; + b = in.readByte(); + i |= (b & 0xFFL) << 56; + return i; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/SeekingRandomAccessInput.java b/src/java/org/apache/cassandra/index/sai/utils/SeekingRandomAccessInput.java new file mode 100644 index 000000000000..988a6909ce7a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/SeekingRandomAccessInput.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.utils; + +import java.io.IOException; +import java.nio.ByteOrder; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.RandomAccessInput; + +/** + * {@link IndexInput} adapter that exposes it as a {@link RandomAccessInput} type. + */ +public class SeekingRandomAccessInput implements RandomAccessInput +{ + private final IndexInput in; + private final ByteOrder order; + + public SeekingRandomAccessInput(org.apache.cassandra.index.sai.disk.io.IndexInput in) + { + this.in = in; + this.order = in.order(); + } + + @VisibleForTesting + public SeekingRandomAccessInput(IndexInput in, ByteOrder order) + { + this.in = in; + this.order = order; + } + + public ByteOrder order() + { + return order; + } + + @Override + public byte readByte(long pos) throws IOException + { + in.seek(pos); + return in.readByte(); + } + + @Override + public short readShort(long pos) throws IOException + { + in.seek(pos); + return in.readShort(); + } + + @Override + public int readInt(long pos) throws IOException + { + in.seek(pos); + return in.readInt(); + } + + @Override + public long readLong(long pos) throws IOException + { + in.seek(pos); + return in.readLong(); + } + + @Override + public String toString() + { + return "SeekingRandomAccessInput(" + in + ")"; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/SegmentOrdering.java b/src/java/org/apache/cassandra/index/sai/utils/SegmentOrdering.java new file mode 100644 index 000000000000..3fe522d679c3 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/SegmentOrdering.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.io.IOException; +import java.util.List; + +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.index.sai.QueryContext; +import org.apache.cassandra.index.sai.disk.v1.IndexSearcher; +import org.apache.cassandra.index.sai.iterators.KeyRangeIterator; +import org.apache.cassandra.index.sai.plan.Orderer; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.CloseableIterator; + +/** + * A {@link SegmentOrdering} orders an index and produces a stream of {@link PrimaryKeyWithSortKey}s. + * + * The limit can be used to lazily order the {@link PrimaryKey}s. Due to the possiblity for + * shadowed or updated keys, a {@link SegmentOrdering} should be able to order the whole index + * until exhausted. + * + * When using {@link SegmentOrdering} there are several steps to + * build the list of Primary Keys to be ordered: + * + * 1. Find all primary keys that match each non-ordering query predicate. + * 2. Union and intersect the results of step 1 to build a single {@link KeyRangeIterator} + * ordered by {@link PrimaryKey}. + * 3. Fan the primary keys from step 2 out to each sstable segment to order the list of primary keys. + * + * SegmentOrdering handles the third step. + * + * Note: a segment ordering is only used when a query has both ordering and non-ordering predicates. + * Where a query has only ordering predicates, the ordering is handled by the + * {@link IndexSearcher#orderBy(Orderer, org.apache.cassandra.index.sai.plan.Expression, AbstractBounds, QueryContext, int)}. + */ +public interface SegmentOrdering +{ + /** + * Order a list of primary keys to the top results. The limit is a hint indicating the minimum number of + * results the query requested. The keys passed to the method will already be limited to keys in the segment's + * Primary Key range. + */ + CloseableIterator orderResultsBy(SSTableReader reader, QueryContext context, List keys, Orderer orderer, int limit) throws IOException; +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/SegmentRowIdOrdinalPairs.java b/src/java/org/apache/cassandra/index/sai/utils/SegmentRowIdOrdinalPairs.java new file mode 100644 index 000000000000..0c47de40e181 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/SegmentRowIdOrdinalPairs.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.function.IntConsumer; + +import io.github.jbellis.jvector.graph.NodeQueue; +import io.github.jbellis.jvector.graph.similarity.ScoreFunction; +import org.agrona.collections.IntIntConsumer; + +/** + * A specialized data structure that stores segment row id to ordinal pairs efficiently. Implemented as an array of int + * pairs that avoids boxing. + */ +public class SegmentRowIdOrdinalPairs +{ + private final int capacity; + private int size; + private final int[] array; + + /** + * Create a new SegmentRowIdOrdinalPairs with the given capacity. + * @param capacity the capacity + */ + public SegmentRowIdOrdinalPairs(int capacity) + { + assert capacity < Integer.MAX_VALUE / 2 : "capacity is too large " + capacity; + this.capacity = capacity; + this.size = 0; + this.array = new int[capacity * 2]; + } + + /** + * Add a pair to the array. + * @param segmentRowId the first value + * @param ordinal the second value + */ + public void add(int segmentRowId, int ordinal) + { + if (size == capacity) + throw new ArrayIndexOutOfBoundsException(size); + array[size * 2] = segmentRowId; + array[size * 2 + 1] = ordinal; + size++; + } + + /** + * Get the row id at the given index. + * @param index the index + * @return the row id + */ + public int getSegmentRowId(int index) + { + if ( index < 0 || index >= size) + throw new ArrayIndexOutOfBoundsException(index); + return array[index * 2]; + } + + /** + * Get the ordinal at the given index. + * @param index the index + * @return the ordinal + */ + public int getOrdinal(int index) + { + if ( index < 0 || index >= size) + throw new ArrayIndexOutOfBoundsException(index); + return array[index * 2 + 1]; + } + + /** + * The number of pairs in the array. + * @return the number of pairs in the array + */ + public int size() + { + return size; + } + + /** + * Iterate over the pairs in the array, calling the consumer for each pair passing (index, x, y). + * @param consumer the consumer to call for each pair + */ + public void forEachSegmentRowIdOrdinalPair(IntIntConsumer consumer) + { + for (int i = 0; i < size; i++) + consumer.accept(array[i * 2], array[i * 2 + 1]); + } + + /** + * Create an iterator over the segment row id and scored ordinal pairs in the array. + * @param scoreFunction the score function to use to compute the next score based on the ordinal + */ + public NodeQueue.NodeScoreIterator mapToSegmentRowIdScoreIterator(ScoreFunction scoreFunction) + { + return mapToScoreIterator(scoreFunction, false); + } + + /** + * Create an iterator over the index and scored ordinal pairs in the array. + * @param scoreFunction the score function to use to compute the next score based on the ordinal + */ + public NodeQueue.NodeScoreIterator mapToIndexScoreIterator(ScoreFunction scoreFunction) + { + return mapToScoreIterator(scoreFunction, true); + } + + /** + * Create an iterator over the index or the segment row id and the score for the ordinal. + * @param scoreFunction the score function to use to compute the next score based on the ordinal + * @param mapToIndex whether to map to the index or the segment row id + */ + private NodeQueue.NodeScoreIterator mapToScoreIterator(ScoreFunction scoreFunction, boolean mapToIndex) + { + return new NodeQueue.NodeScoreIterator() + { + int i = 0; + + @Override + public boolean hasNext() + { + return i < size; + } + + @Override + public int pop() + { + return mapToIndex ? i++ : array[i++ * 2]; + } + + @Override + public float topScore() + { + return scoreFunction.similarityTo(array[i * 2 + 1]); + } + }; + } + + /** + * Calls the consumer for each right value in each pair of the array. + * @param consumer the consumer to call for each right value + */ + public void forEachOrdinal(IntConsumer consumer) + { + for (int i = 0; i < size; i++) + consumer.accept(array[i * 2 + 1]); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/SingletonIntIterator.java b/src/java/org/apache/cassandra/index/sai/utils/SingletonIntIterator.java new file mode 100644 index 000000000000..45418949dd3c --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/SingletonIntIterator.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import java.util.NoSuchElementException; +import java.util.PrimitiveIterator; + +/** + * Singleton int iterator used to prevent unnecessary object creation + */ +public class SingletonIntIterator implements PrimitiveIterator.OfInt +{ + private final int value; + private boolean hasNext = true; + + public SingletonIntIterator(int value) + { + this.value = value; + } + + @Override + public boolean hasNext() + { + return hasNext; + } + + @Override + public int nextInt() + { + if (!hasNext) + throw new NoSuchElementException(); + hasNext = false; + return value; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/SoftLimitUtil.java b/src/java/org/apache/cassandra/index/sai/utils/SoftLimitUtil.java new file mode 100644 index 000000000000..081ef8365fb8 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/SoftLimitUtil.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import org.apache.commons.math3.distribution.PascalDistribution; + +public class SoftLimitUtil +{ + /** + * Computes the number of items (e.g. keys, rows) that should be requested from a lower-layer of the system + * (e.g. storage) so that we obtain at least targetLimit number of items with given probability. + * It assumes that each item may randomly fail, in which case it is not delivered, thus the number of items + * delivered may be smaller than the number of items requested. Items are assumed to fail independently. + *

    + * For example, if we want to deliver 100 rows to the user, but we know 20% of rows are tombstoned and would + * be rejected, then we should request `softLimit(100, 0.95, 0.8)` rows from the storage, and that would deliver + * in 95% of cases a sufficient number of rows, without having to query again for more. + * + * @param targetLimit the number of items that should be delivered to the user or upper layer in the system + * @param confidenceLevel the desired probability we obtain enough items in range, given in range [0.0, 1.0), + * typically you want to set it close to 1.0. + * @param perItemSuccessRate the probability of obtaining an item, given in range [0.0, 1.0] + * @return the number of items that should be requested from the lower layer of the system; >= targetLimit; + * if the true result is greater than Integer.MAX_VALUE it is clamped to Integer.MAX_VALUE + */ + public static int softLimit(int targetLimit, double confidenceLevel, double perItemSuccessRate) + { + if (Double.isNaN(confidenceLevel)) + throw new IllegalArgumentException("confidenceLevel must not be NaN"); + if (confidenceLevel < 0.0 || confidenceLevel >= 1.0) + throw new IllegalArgumentException("confidenceLevel out of range [0.0, 1.0): " + confidenceLevel); + if (Double.isNaN(perItemSuccessRate)) + throw new IllegalArgumentException("perItemSuccessRate must not be NaN"); + if (perItemSuccessRate < 0.0 || perItemSuccessRate > 1.0) + throw new IllegalArgumentException("perItemSuccessRate out of range [0.0, 1.0]: " + perItemSuccessRate); + if (targetLimit < 0) + throw new IllegalArgumentException("targetLimit must not be < 0: " + targetLimit); + + // PascalDistribution (see further) cannot handle this case properly + if (targetLimit == 0) + return 0; + + // Consider we perform attempts until we get R successes (=targetLimit), where the probability of success is + // P (=perItemSuccessRate). In this case the number of failures is described by a negative binomial + // distribution NB(R, P). We use PascalDistribution, which is an optimized special case + // of NB for dealing with integers. + final var failureDistrib = new PascalDistribution(targetLimit, perItemSuccessRate); + long maxExpectedFailures = failureDistrib.inverseCumulativeProbability(confidenceLevel); + + long softLimit = (long) targetLimit + maxExpectedFailures; + return (int) Math.min(softLimit, Integer.MAX_VALUE); + } + +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/TermsIterator.java b/src/java/org/apache/cassandra/index/sai/utils/TermsIterator.java deleted file mode 100644 index 25649c71b518..000000000000 --- a/src/java/org/apache/cassandra/index/sai/utils/TermsIterator.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sai.utils; - -import java.io.Closeable; -import java.nio.ByteBuffer; -import java.util.Iterator; -import javax.annotation.concurrent.NotThreadSafe; - -/** - * An iterator over the contents of an index that extends {@link Iterator}<{@link IndexEntry}> that provides the min and max - * terms in the index. Each {@link IndexEntry} contains a term and the postings associated with that term. - */ -@NotThreadSafe -public interface TermsIterator extends Iterator, Closeable -{ - ByteBuffer getMinTerm(); - - ByteBuffer getMaxTerm(); -} diff --git a/src/java/org/apache/cassandra/index/sai/utils/TreeFormatter.java b/src/java/org/apache/cassandra/index/sai/utils/TreeFormatter.java new file mode 100644 index 000000000000..237827193044 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/TreeFormatter.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.utils; + +import javax.annotation.Nullable; +import java.util.function.Function; + +/** + * Pretty prints heterogenous tree structures like this: + *

    + * root
    + *   ├─ child 1
    + *   │   ├─ child 1a
    + *   │   └─ child 1b
    + *   └─ child 2
    + *       ├─ child 2a
    + *       └─ child 2b
    + * 
    + * @param type of the node of the tree + */ +public class TreeFormatter +{ + private final Function> children; + private final Function toString; + private final String indent; + + /** + * Constructs a formatter that knows how to format trees of given type. + * + * @param toString a function that returns the text describing each tree node + * @param children a function that returns a list of children nodes + */ + public TreeFormatter(Function toString, Function> children) + { + this.children = children; + this.toString = toString; + this.indent = ""; + } + + /** + * Constructs a formatter that knows how to format trees of given type. + * + * @param toString a function that returns the text describing each tree node + * @param children a function that returns a list of children nodes + * @param indent a string used for indentation + */ + public TreeFormatter(Function toString, Function> children, @Nullable String indent) + { + this.children = children; + this.toString = toString; + this.indent = indent == null ? "" : indent; + } + + /** + * Returns a multiline String with a formatted tree + * @param root root node of the tree + */ + public String format(T root) + { + StringBuilder sb = new StringBuilder(indent); + append(root, sb, new StringBuilder(), true, false); + return sb.toString(); + } + + /** + * Traverses the tree depth first and prints the tree. + * Called once per each node. + */ + private void append(T node, StringBuilder sb, StringBuilder padding, boolean isRoot, boolean hasRightSibling) + { + int origPaddingLength = padding.length(); + if (!isRoot) + { + sb.append(indent); + sb.append(padding); + sb.append(hasRightSibling ? " ├─ " : " └─ "); + padding.append(hasRightSibling ? " │ " : " "); + } + + String[] nodeStr = toString.apply(node).split("\n"); + sb.append(nodeStr[0]); + sb.append('\n'); + for (int i = 1; i < nodeStr.length; i++) + { + sb.append(indent); + sb.append(padding); + sb.append(nodeStr[i]); + sb.append('\n'); + } + + var iter = children.apply(node).iterator(); + while (iter.hasNext()) + { + T child = iter.next(); + append(child, sb, padding, false, iter.hasNext()); + } + padding.setLength(origPaddingLength); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java b/src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java new file mode 100644 index 000000000000..daf62e075d1c --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java @@ -0,0 +1,670 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.utils; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.VectorType; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ComplexColumnData; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.plan.Expression; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FastByteOperations; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +public class TypeUtil +{ + private static final byte[] IPV4_PREFIX = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1 }; + + /** + * DecimalType / BigDecimal values are indexed by truncating their asComparableBytes representation to this size, + * padding on the right with zero-value-bytes until this size is reached (if necessary). This causes + * false-positives that must be filtered in a separate step after hitting the index and reading the associated + * (full) values. + */ + public static final int DECIMAL_APPROXIMATION_BYTES = 24; + + public static final int BIG_INTEGER_APPROXIMATION_BYTES = 20; + + public static final int INET_ADDRESS_SIZE = 16; + + public static final int DEFAULT_FIXED_LENGTH = 16; + /** + * Byte comparable version currently used for all SAI files and structures, with the exception of terms data in + * the early AA on-disk format. + */ + public static final ByteComparable.Version BYTE_COMPARABLE_VERSION = ByteComparable.Version.OSS41; + + private TypeUtil() {} + + /** + * Returns true if given buffer would pass the {@link AbstractType#validate(ByteBuffer)} + * check. False otherwise. + */ + public static boolean isValid(ByteBuffer term, AbstractType validator) + { + try + { + validator.validate(term); + return true; + } + catch (MarshalException e) + { + return false; + } + } + + /** + * Indicates if the type encoding supports rounding of the raw value. + * + * This is significant in range searches where we have to make all range + * queries inclusive when searching the indexes in order to avoid excluding + * rounded values. Excluded values are removed by post-filtering. + */ + public static boolean supportsRounding(AbstractType type) + { + return isBigInteger(type) || isBigDecimal(type); + } + + /** + * Returns the smaller of two {@code ByteBuffer} values, based on the result of {@link + * #compare(ByteBuffer, ByteBuffer, AbstractType, Version)} comparision. + */ + public static ByteBuffer min(ByteBuffer a, ByteBuffer b, AbstractType type, Version version) + { + return a == null ? b : (b == null || compare(b, a, type, version) > 0) ? a : b; + } + + /** + * Returns the greater of two {@code ByteBuffer} values, based on the result of {@link + * #compare(ByteBuffer, ByteBuffer, AbstractType, Version)} comparision. + */ + public static ByteBuffer max(ByteBuffer a, ByteBuffer b, AbstractType type, Version version) + { + return a == null ? b : (b == null || compare(b, a, type, version) < 0) ? a : b; + } + + /** + * Returns the value length for the given {@link AbstractType}, selecting 16 for types + * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length. + */ + public static int fixedSizeOf(AbstractType type) + { + if (type.isValueLengthFixed()) + return type.valueLengthIfFixed(); + else if (isInetAddress(type)) + return INET_ADDRESS_SIZE; + else if (isBigInteger(type)) + return BIG_INTEGER_APPROXIMATION_BYTES; + else if (isBigDecimal(type)) + return DECIMAL_APPROXIMATION_BYTES; + return DEFAULT_FIXED_LENGTH; + } + + public static AbstractType cellValueType(ColumnMetadata columnMetadata, IndexTarget.Type indexType) + { + AbstractType type = columnMetadata.type; + if (isNonFrozenCollection(type)) + { + CollectionType collection = ((CollectionType) type); + switch (collection.kind) + { + case LIST: + return collection.valueComparator(); + case SET: + return collection.nameComparator(); + case MAP: + switch (indexType) + { + case KEYS: + return collection.nameComparator(); + case VALUES: + return collection.valueComparator(); + case KEYS_AND_VALUES: + return CompositeType.getInstance(collection.nameComparator(), collection.valueComparator()); + } + } + } + return type; + } + + /** + * Allows overriding the default getString method for {@link CompositeType}. It is + * a requirement of the {@link ConcurrentRadixTree} that the keys are strings but + * the getString method of {@link CompositeType} does not return a string that compares + * in the same order as the underlying {@link ByteBuffer}. To get round this we convert + * the {@link CompositeType} bytes to a hex string. + */ + public static String getString(ByteBuffer value, AbstractType type) + { + if (isComposite(type)) + return ByteBufferUtil.bytesToHex(value); + return type.getString(value); + } + + /** + * The inverse of the above method. Overrides the fromString method on {@link CompositeType} + * in order to convert the hex string to bytes. + */ + public static ByteBuffer fromString(String value, AbstractType type) + { + if (isComposite(type)) + return ByteBufferUtil.hexToBytes(value); + return type.fromString(value); + } + + public static ByteBuffer fromComparableBytes(ByteComparable value, AbstractType type, ByteComparable.Version version) + { + if (type instanceof InetAddressType || type instanceof IntegerType || type instanceof DecimalType) + return ByteBuffer.wrap(ByteSourceInverse.readBytes(value.asComparableBytes(version))); + + return type.fromComparableBytes(ByteSource.peekable(value.asComparableBytes(version)), version); + } + + public static ByteComparable asComparableBytes(ByteBuffer value, AbstractType type) + { + return version -> asComparableBytes(value, type, version); + } + + public static ByteSource asComparableBytes(ByteBuffer value, AbstractType type, ByteComparable.Version version) + { + if (type instanceof InetAddressType || type instanceof IntegerType || type instanceof DecimalType) + return ByteSource.optionalFixedLength(ByteBufferAccessor.instance, value); + // The LongType.asComparableBytes uses variableLengthInteger which doesn't play well with + // the balanced tree because it is expecting fixed length data. So for SAI we use a optionalSignedFixedLengthNumber + // to keep all comparable values the same length + else if (type instanceof LongType) + return ByteSource.optionalSignedFixedLengthNumber(ByteBufferAccessor.instance, value); + return type.asComparableBytes(value, version); + } + + /** + * Convenience method to create a {@link ByteComparable} from a {@link ByteBuffer} value for a given {@link CompositeType} + * with a terminator. This method is in this class to keep references to the {@link ByteBufferAccessor#instance} here. + */ + public static ByteComparable asComparableBytes(ByteBuffer value, int terminator, CompositeType type) + { + return v -> type.asComparableBytes(ByteBufferAccessor.instance, value, v, terminator); + } + + + /** + * Fills a byte array with the comparable bytes for a type. + *

    + * This method expects a {@code value} parameter generated by calling {@link #asIndexBytes(ByteBuffer, AbstractType)}. + * It is not generally safe to pass the output of other serialization methods to this method. For instance, it is + * not generally safe to pass the output of {@link AbstractType#decompose(Object)} as the {@code value} parameter + * (there are certain types for which this is technically OK, but that doesn't hold for all types). + * + * @param value a value buffer returned by {@link #asIndexBytes(ByteBuffer, AbstractType)} + * @param type the type associated with the encoded {@code value} parameter + * @param bytes this method's output + */ + public static void toComparableBytes(ByteBuffer value, AbstractType type, byte[] bytes) + { + if (isInetAddress(type)) + ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, INET_ADDRESS_SIZE); + else if (isBigInteger(type)) + ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, BIG_INTEGER_APPROXIMATION_BYTES); + else if (isBigDecimal(type)) + ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, DECIMAL_APPROXIMATION_BYTES); + else + ByteSourceInverse.readBytesMustFit(type.asComparableBytes(value, BYTE_COMPARABLE_VERSION), bytes); + } + + /** + * Translates the external value of specific types into a format used by the index. + */ + public static ByteBuffer asIndexBytes(ByteBuffer value, AbstractType type) + { + if (value == null || value.remaining() == 0) + return value; + + if (isInetAddress(type)) + return encodeInetAddress(value); + else if (isBigInteger(type)) + return encodeBigInteger(value); + else if (type instanceof DecimalType) + return encodeDecimal(value); + return value; + } + + /** + * Tries its best to return the inverse of {@link #encode}. + * For most of the types it returns the exact inverse. + * For big integers and decimals, which could be truncated by encode, some precision loss is possible. + */ + public static ByteBuffer decode(ByteBuffer value, AbstractType type) + { + if (value == null) + return null; + + if (isInetAddress(type)) + return decodeInetAddress(value); + else if (isBigInteger(type)) + return decodeBigInteger(value); + else if (type instanceof DecimalType) + return decodeDecimal(value); + return value; + } + + /** + * Compare two terms based on their type. This is used in place of {@link AbstractType#compare(ByteBuffer, ByteBuffer)} + * so that the default comparison can be overridden for specific types. + * + * Note: This should be used for all term comparison + */ + public static int compare(ByteBuffer b1, ByteBuffer b2, AbstractType type, Version version) + { + if (isInetAddress(type)) + return compareInet(b1, b2); + else if (useFastByteOperations(type, version)) + return FastByteOperations.compareUnsigned(b1, b2); + + return type.compare(b1, b2); + } + + /** + * This is used for value comparison in post-filtering - {@link Expression#isSatisfiedBy(ByteBuffer)}. + * + * This allows types to decide whether they should be compared based on their encoded value or their + * raw value. At present only {@link InetAddressType} values are compared by their encoded values to + * allow for ipv4 -> ipv6 equivalency in searches. + */ + public static int comparePostFilter(Expression.Value requestedValue, Expression.Value columnValue, AbstractType type) + { + if (isInetAddress(type)) + return compareInet(requestedValue.encoded, columnValue.encoded); + // Override comparisons for frozen collections + else if (isFrozen(type)) + return FastByteOperations.compareUnsigned(requestedValue.raw, columnValue.raw); + + return type.compare(requestedValue.raw, columnValue.raw); + } + + public static Iterator collectionIterator(AbstractType validator, + ComplexColumnData cellData, + ColumnMetadata columnMetadata, + IndexTarget.Type indexType, + long nowInSecs) + { + if (cellData == null) + return null; + + Stream stream = StreamSupport.stream(cellData.spliterator(), false).filter(cell -> cell != null && cell.isLive(nowInSecs)) + .map(cell -> cellValue(columnMetadata, indexType, cell)); + + if (isInetAddress(validator)) + stream = stream.sorted((c1, c2) -> compareInet(encodeInetAddress(c1), encodeInetAddress(c2))); + + return stream.iterator(); + } + + public static Comparator comparator(AbstractType type, Version version) + { + // Override the comparator for BigInteger, frozen collections (not including composite types) and + // composite types before DB version to maintain a consistent order between the in-memory index and the on-disk index. + if (useFastByteOperations(type, version)) + return FastByteOperations::compareUnsigned; + + return type; + } + + private static boolean useFastByteOperations(AbstractType type, Version version) + { + // BigInteger types, BigDecimal types, frozen types and composite types (map entries) use compareUnsigned to + // maintain a consistent order between the in-memory index and the on-disk index. Starting with Version.DB, + // composite types are compared using their AbstractType. + return isBigInteger(type) + || isBigDecimal(type) + || (!isComposite(type) && isFrozen(type)) + || (isComposite(type) && !version.onOrAfter(Version.DB)); + } + + public static float[] decomposeVector(AbstractType type, ByteBuffer byteBuffer) + { + return ((VectorType.VectorSerializer)type.getSerializer()).deserializeFloatArray(byteBuffer); + } + + public static float[] decomposeVector(IndexContext indexContext, ByteBuffer byteBuffer) + { + return decomposeVector(indexContext.getValidator(), byteBuffer); + } + + private static ByteBuffer cellValue(ColumnMetadata columnMetadata, IndexTarget.Type indexType, Cell cell) + { + if (columnMetadata.type.isCollection() && columnMetadata.type.isMultiCell()) + { + switch (((CollectionType) columnMetadata.type).kind) + { + case LIST: + //TODO Is there any optimisation can be done here with cell values? + return cell.buffer(); + case SET: + return cell.path().get(0); + case MAP: + switch (indexType) + { + case KEYS: + return cell.path().get(0); + case VALUES: + return cell.buffer(); + case KEYS_AND_VALUES: + return CompositeType.build(ByteBufferAccessor.instance, cell.path().get(0), cell.buffer()); + } + } + } + return cell.buffer(); + } + + /** + * Compares 2 InetAddress terms by ensuring that both addresses are represented as + * ipv6 addresses. + */ + private static int compareInet(ByteBuffer b1, ByteBuffer b2) + { + assert isIPv6(b1) && isIPv6(b2); + + return FastByteOperations.compareUnsigned(b1, b2); + } + + private static boolean isIPv6(ByteBuffer address) + { + return address.remaining() == INET_ADDRESS_SIZE; + } + + /** + * Encode a {@link InetAddress} into a fixed width 16 byte encoded value. + * + * The encoded value is byte comparable and prefix compressible. + * + * The encoding is done by converting ipv4 addresses to their ipv6 equivalent. + */ + private static ByteBuffer encodeInetAddress(ByteBuffer value) + { + if (value.remaining() == 4) + { + int position = value.hasArray() ? value.arrayOffset() + value.position() : value.position(); + ByteBuffer mapped = ByteBuffer.allocate(INET_ADDRESS_SIZE); + System.arraycopy(IPV4_PREFIX, 0, mapped.array(), 0, IPV4_PREFIX.length); + ByteBufferUtil.copyBytes(value, position, mapped, IPV4_PREFIX.length, value.remaining()); + return mapped; + } + return value; + } + + private static ByteBuffer decodeInetAddress(ByteBuffer value) + { + throw new UnsupportedOperationException("Decoding InetAddress not implemented yet"); + } + + + /** + * Encode a {@link BigInteger} into a fixed width 20 byte encoded value. + * + * The encoded value is byte comparable and prefix compressible. + * + * The format of the encoding is: + * + * The first 4 bytes contain the integer length of the {@link BigInteger} byte array + * with the top bit flipped for positive values. + * + * The remaining 16 bytes contain the 16 most significant bytes of the + * {@link BigInteger} byte array. + * + * For {@link BigInteger} values whose underlying byte array is less than + * 16 bytes, the encoded value is sign extended. + */ + public static ByteBuffer encodeBigInteger(ByteBuffer value) + { + int size = value.remaining(); + int position = value.hasArray() ? value.arrayOffset() + value.position() : value.position(); + byte[] bytes = new byte[BIG_INTEGER_APPROXIMATION_BYTES]; + if (size < BIG_INTEGER_APPROXIMATION_BYTES - Integer.BYTES) + { + ByteBufferUtil.copyBytes(value, position, bytes, bytes.length - size, size); + if ((bytes[bytes.length - size] & 0x80) != 0) + Arrays.fill(bytes, Integer.BYTES, bytes.length - size, (byte)0xff); + else + Arrays.fill(bytes, Integer.BYTES, bytes.length - size, (byte)0x00); + } + else + { + ByteBufferUtil.copyBytes(value, position, bytes, Integer.BYTES, BIG_INTEGER_APPROXIMATION_BYTES - Integer.BYTES); + } + if ((bytes[4] & 0x80) != 0) + { + size = -size; + } + bytes[0] = (byte)(size >> 24 & 0xff); + bytes[1] = (byte)(size >> 16 & 0xff); + bytes[2] = (byte)(size >> 8 & 0xff); + bytes[3] = (byte)(size & 0xff); + bytes[0] ^= 0x80; + return ByteBuffer.wrap(bytes); + } + + + public static ByteBuffer decodeBigInteger(ByteBuffer encoded) + { + byte[] bytes = new byte[20]; + encoded.get(bytes); + encoded.rewind(); + + // Undo the XOR operation on the first byte + bytes[0] ^= 0x80; + + // Extract the size (the first 4 bytes) + int size = ((bytes[0] & 0xff) << 24) | ((bytes[1] & 0xff) << 16) | ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff); + + boolean isNegative = size < 0; + if (isNegative) + size = -size; + + ByteBuffer result; + if (size < 16) + { + int offset = 20 - size; + result = ByteBuffer.wrap(Arrays.copyOfRange(bytes, offset, 20)); + } + else + { + // Size >= 16 means we extract 16 bytes starting from index 4 + var resultBytes = new byte[size]; + System.arraycopy(bytes, 4, resultBytes, 0, 16); + result = ByteBuffer.wrap(resultBytes); + } + + return result; + } + + + /* Type comparison to get rid of ReversedType */ + + /** + * Returns true if values of the given {@link AbstractType} should be indexed as literals. + */ + public static boolean isLiteral(AbstractType type) + { + return isUTF8OrAscii(type) || isCompositeOrFrozen(type) || baseType(type) instanceof BooleanType; + } + + /** + * Returns true if given {@link AbstractType} is UTF8 or Ascii + */ + public static boolean isUTF8OrAscii(AbstractType type) + { + type = baseType(type); + return type instanceof UTF8Type || type instanceof AsciiType; + } + +// /** +// * Returns true if given {@link AbstractType} is based on a string, e.g. UTF8 or Ascii +// */ +// public static boolean isString(AbstractType type) +// { +// type = baseType(type); +// return type instanceof StringType; +// } +// + /** + * Returns true if given {@link AbstractType} is a Composite(map entry) or frozen. + */ + public static boolean isCompositeOrFrozen(AbstractType type) + { + type = baseType(type); + return type instanceof CompositeType || isFrozen(type); + } + + /** + * Returns true if given {@link AbstractType} is frozen. + */ + public static boolean isFrozen(AbstractType type) + { + type = baseType(type); + return !type.subTypes().isEmpty() && !type.isMultiCell(); + } + + /** + * Returns true if given {@link AbstractType} is a frozen collection. + */ + public static boolean isFrozenCollection(AbstractType type) + { + type = baseType(type); + return type.isCollection() && !type.isMultiCell(); + } + + /** + * Returns true if given {@link AbstractType} is a non-frozen collection. + */ + public static boolean isNonFrozenCollection(AbstractType type) + { + type = baseType(type); + return type.isCollection() && type.isMultiCell(); + } + + /** + * Returns true if given {@link AbstractType} is included in the types. + */ + public static boolean isIn(AbstractType type, Set> types) + { + type = baseType(type); + return types.contains(type); + } + + /** + * Returns true if given {@link AbstractType} is {@link InetAddressType} + */ + private static boolean isInetAddress(AbstractType type) + { + type = baseType(type); + return type instanceof InetAddressType; + } + + /** + * Returns true if given {@link AbstractType} is {@link IntegerType} + */ + private static boolean isBigInteger(AbstractType type) + { + type = baseType(type); + return type instanceof IntegerType; + } + + /** + * Returns true if given {@link AbstractType} is {@link DecimalType} + */ + private static boolean isBigDecimal(AbstractType type) + { + type = baseType(type); + return type instanceof DecimalType; + } + + /** + * Returns true if given {@link AbstractType} is {@link CompositeType} + */ + public static boolean isComposite(AbstractType type) + { + type = baseType(type); + return type instanceof CompositeType; + } + + /** + * @return {@code true} if the empty values of the given type should be excluded from indexing, {@code false} otherwise. + */ + public static boolean skipsEmptyValue(AbstractType type) + { + return !type.allowsEmpty() || !isLiteral(type); + } + + /** + * @return base type if given type is reversed, otherwise return itself + */ + private static AbstractType baseType(AbstractType type) + { + return type.unwrap(); + } + + public static ByteBuffer encodeDecimal(ByteBuffer value) + { + ByteSource bs = DecimalType.instance.asComparableBytes(value, BYTE_COMPARABLE_VERSION); + byte[] data = new byte[DECIMAL_APPROXIMATION_BYTES]; // initialized with 0s + bs.nextBytes(data); // reads up to the number of bytes in the array, leaving 0s in the remaining bytes + return ByteBuffer.wrap(data); + } + + public static ByteBuffer decodeDecimal(ByteBuffer value) + { + var peekableValue = ByteSource.peekable(ByteSource.preencoded(value)); + return DecimalType.instance.fromComparableBytes(peekableValue, BYTE_COMPARABLE_VERSION); + } + + public static ByteComparable.Version byteComparableVersionForTermsData(Version version) + { + return version.byteComparableVersionFor(IndexComponentType.TERMS_DATA, DatabaseDescriptor.getSelectedSSTableFormat().getLatestVersion()); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/view/IndexViewManager.java b/src/java/org/apache/cassandra/index/sai/view/IndexViewManager.java index 3a2baaceef6d..02a037f0d392 100644 --- a/src/java/org/apache/cassandra/index/sai/view/IndexViewManager.java +++ b/src/java/org/apache/cassandra/index/sai/view/IndexViewManager.java @@ -21,24 +21,27 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.index.sai.IndexValidation; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.SSTableContext; -import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.index.sai.SSTableIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.utils.Pair; /** - * Maintain an atomic view for read requests, so that requests can read all data during concurrent compactions. - *

    + * Maintain a atomic view for read requests, so that requests can read all data during concurrent compactions. + * * All per-column {@link SSTableIndex} updates should be proxied by {@link StorageAttachedIndexGroup} to make * sure per-sstable {@link SSTableContext} are in-sync. */ @@ -46,18 +49,24 @@ public class IndexViewManager { private static final Logger logger = LoggerFactory.getLogger(IndexViewManager.class); - private final StorageAttachedIndex index; - private final AtomicReference view = new AtomicReference<>(); + private final IndexContext context; + private final AtomicReference viewRef = new AtomicReference<>(); + + public IndexViewManager(IndexContext context) + { + this(context, Collections.emptySet()); + } - public IndexViewManager(StorageAttachedIndex index) + @VisibleForTesting + IndexViewManager(IndexContext context, Collection indices) { - this.index = index; - this.view.set(new View(index.termType(), Collections.emptySet())); + this.context = context; + this.viewRef.set(new View(context, indices)); } - public View view() + public View getView() { - return view.get(); + return viewRef.get(); } /** @@ -65,149 +74,139 @@ public View view() * * @param oldSSTables A set of SSTables to remove. * @param newSSTableContexts A set of SSTableContexts to add to tracker. - * @param validation Controls how indexes should be validated + * @param validate if true, per-column index files' header and footer will be validated. * * @return A set of SSTables which have attached to them invalid index components. */ - public Collection update(Collection oldSSTables, Collection newSSTableContexts, IndexValidation validation) + public Set update(Collection oldSSTables, + Collection newSSTableContexts, + boolean validate) { // Valid indexes on the left and invalid SSTable contexts on the right... - Pair, Collection> indexes = getBuiltIndexes(newSSTableContexts, validation); + // The valid indexes are referenced as a part of object initialization. + Pair, Set> indexes = context.getBuiltIndexes(newSSTableContexts, validate); - View currentView, newView; - Collection newViewIndexes = new HashSet<>(); - Collection releasableIndexes = new ArrayList<>(); + View currentView, newView = null; + Map newViewIndexes = new HashMap<>(); + Collection referencedSSTableIndexes = new ArrayList<>(); + Collection toRemove = new HashSet<>(oldSSTables); + int iterations = 0; + outer: do { - currentView = view.get(); + currentView = viewRef.get(); + referencedSSTableIndexes.forEach(SSTableIndex::release); + referencedSSTableIndexes.clear(); newViewIndexes.clear(); - releasableIndexes.clear(); + + // Throw after releasing already referenced indexes + if (iterations++ > 1000) + throw new IllegalStateException("Failed to update index view after 1000 iterations"); for (SSTableIndex sstableIndex : currentView) { // When aborting early open transaction, toRemove may have the same sstable files as newSSTableContexts, // but different SSTableReader java objects with different start positions. So we need to release them - // from existing view. + // from existing view. see DSP-19677 SSTableReader sstable = sstableIndex.getSSTable(); - if (oldSSTables.contains(sstable) || newViewIndexes.contains(sstableIndex)) - releasableIndexes.add(sstableIndex); - else - newViewIndexes.add(sstableIndex); + if (!toRemove.contains(sstable)) + addOrUpdateSSTableIndex(sstableIndex, newViewIndexes); } for (SSTableIndex sstableIndex : indexes.left) + addOrUpdateSSTableIndex(sstableIndex, newViewIndexes); + + // Reference all the new indexes before publishing the new view. Becuase addOrUpdateSSTableIndex + // can overwrite entries, it is simpler to just reference all the ones we know we need here instead of + // tracking state across multiple iterations. By doing it the naive way, we reduce the complexity of this + // method quite a bit. + for (var sstableIndex : newViewIndexes.values()) { - if (newViewIndexes.contains(sstableIndex)) - releasableIndexes.add(sstableIndex); - else - newViewIndexes.add(sstableIndex); + if (!sstableIndex.reference()) + continue outer; + referencedSSTableIndexes.add(sstableIndex); } - newView = new View(index.termType(), newViewIndexes); + newView = new View(context, referencedSSTableIndexes); } - while (!view.compareAndSet(currentView, newView)); + while (newView == null || !viewRef.compareAndSet(currentView, newView)); - releasableIndexes.forEach(SSTableIndex::release); + // These were referenced when created and then the ones we are keeping were re-referenced if they made it into + // the newViewIndexes. + indexes.left.forEach(SSTableIndex::release); + + // Release the old view now that the new view is in place and we have successfully renewed the indexes + // that were transferred from the old view to the new view. + currentView.release(); if (logger.isTraceEnabled()) - logger.trace(index.identifier().logMessage("There are now {} active SSTable indexes."), view.get().getIndexes().size()); + logger.trace(context.logMessage("There are now {} active SSTable indexes."), viewRef.get().getIndexes().size()); return indexes.right; } - public void drop(Collection sstablesToRebuild) - { - View currentView = view.get(); - - Set toRemove = new HashSet<>(sstablesToRebuild); - for (SSTableIndex index : currentView) - { - SSTableReader sstable = index.getSSTable(); - if (!toRemove.contains(sstable)) - continue; - - index.markObsolete(); - } - - update(toRemove, Collections.emptyList(), IndexValidation.NONE); - } - - /** - * Called when index is dropped. Mark all {@link SSTableIndex} as released and per-column index files - * will be removed when in-flight queries are completed. - */ - public void invalidate() + private static void addOrUpdateSSTableIndex(SSTableIndex ssTableIndex, Map addTo) { - View previousView = view.getAndSet(new View(index.termType(), Collections.emptyList())); - - for (SSTableIndex index : previousView) + var descriptor = ssTableIndex.getSSTable().descriptor; + SSTableIndex previous = addTo.get(descriptor); + if (previous != null) { - index.markObsolete(); + // If the new index use the same files that the exiting one (and the previous one is still complete, meaning + // that the files weren't corrupted), then keep the old one (no point in changing for the same thing). + if (previous.usedPerIndexComponents().isComplete() && ssTableIndex.usedPerIndexComponents().buildId().equals(previous.usedPerIndexComponents().buildId())) + return; } + addTo.put(descriptor, ssTableIndex); } - /** - * @return the indexes that are built on the given SSTables on the left and corrupted indexes' - * corresponding contexts on the right - */ - private Pair, Collection> getBuiltIndexes(Collection sstableContexts, IndexValidation validation) + public void prepareSSTablesForRebuild(Collection sstablesToRebuild) { - Set valid = new HashSet<>(sstableContexts.size()); - Set invalid = new HashSet<>(); + Set toRemove = new HashSet<>(sstablesToRebuild); + View oldView, newView = null; + Collection newIndexes = new ArrayList<>(); - for (SSTableContext sstableContext : sstableContexts) + int iterations = 0; + outer: + do { - if (sstableContext.sstable.isMarkedCompacted()) - continue; + oldView = viewRef.get(); + newIndexes.forEach(SSTableIndex::release); + newIndexes.clear(); - if (!sstableContext.indexDescriptor.isPerColumnIndexBuildComplete(index.identifier())) - { - logger.debug(index.identifier().logMessage("An on-disk index build for SSTable {} has not completed."), sstableContext.descriptor()); - continue; - } + if (iterations++ > 1000) + throw new IllegalStateException("Failed to prepare index view after 1000 iterations"); - try + for (var index : oldView.getIndexes()) { - if (validation != IndexValidation.NONE) + if (!toRemove.contains(index.getSSTable())) { - if (!sstableContext.indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), validation, true, false)) - { - invalid.add(sstableContext); - continue; - } + if (!index.reference()) + continue outer; + newIndexes.add(index); } - - SSTableIndex ssTableIndex = sstableContext.newSSTableIndex(index); - // We used to skip these empty indexes. However, that leads to logically incomplete views of the table, - // so we keep them in the view now. For example, vector indexes use the view to materialize rows, and - // without a complete view, an sstable with no indexable vectors might still have valid data or - // tombstones necessary to ensure proper row materialization. - if (ssTableIndex.getRowCount() == 0) - { - logger.debug(index.identifier().logMessage("No on-disk index was built for SSTable {} because the SSTable " + - "had no indexable rows for the index."), sstableContext.descriptor()); - } - else - { - logger.debug(index.identifier().logMessage("Successfully created index for SSTable {}."), sstableContext.descriptor()); - } - - // Try to add new index to the set, if set already has such index, we'll simply release and move on. - // This covers situation when SSTable collection has the same SSTable multiple - // times because we don't know what kind of collection it actually is. - if (!valid.add(ssTableIndex)) - { - ssTableIndex.release(); - } - } - catch (Throwable e) - { - logger.warn(index.identifier().logMessage("Failed to update per-column components for SSTable {}"), sstableContext.descriptor(), e); - invalid.add(sstableContext); } + + newView = new View(context, newIndexes); } + while (newView == null || !viewRef.compareAndSet(oldView, newView)); + oldView.release(); + } - return Pair.create(valid, invalid); + /** + * Called when index is dropped. Mark all {@link SSTableIndex} as released and per-column index files + * will be removed when in-flight queries completed and {@code obsolete} is true. + * + * @param indexWasDropped true if the index is invalidated because it was dropped; false if the index is simply + * being unloaded. + */ + public void invalidate(boolean indexWasDropped) + { + // No need to loop here because we don't use the old view when building the new view. + var oldView = viewRef.getAndSet(new View(context, Collections.emptySet())); + if (indexWasDropped) + oldView.markIndexWasDropped(); + else + oldView.release(); } } diff --git a/src/java/org/apache/cassandra/index/sai/view/RangeTermTree.java b/src/java/org/apache/cassandra/index/sai/view/RangeTermTree.java index 2da7acfb1906..52629ff7e395 100644 --- a/src/java/org/apache/cassandra/index/sai/view/RangeTermTree.java +++ b/src/java/org/apache/cassandra/index/sai/view/RangeTermTree.java @@ -18,83 +18,96 @@ package org.apache.cassandra.index.sai.view; +import java.lang.invoke.MethodHandles; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; -import com.google.common.base.MoreObjects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.IndexTermType; +import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.utils.Interval; import org.apache.cassandra.utils.IntervalTree; -public class RangeTermTree +public class RangeTermTree implements TermTree { - private static final Logger logger = LoggerFactory.getLogger(RangeTermTree.class); + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - protected final ByteBuffer min, max; - protected final IndexTermType indexTermType; - - private final IntervalTree> rangeTree; + protected final AbstractType comparator; + // Because each version can have different encodings, we group indexes by version. + private final Map>> rangeTrees; - private RangeTermTree(ByteBuffer min, ByteBuffer max, IntervalTree> rangeTree, IndexTermType indexTermType) + private RangeTermTree(Map>> rangeTrees, AbstractType comparator) { - this.min = min; - this.max = max; - this.rangeTree = rangeTree; - this.indexTermType = indexTermType; + this.rangeTrees = rangeTrees; + this.comparator = comparator; } - public List search(Expression e) + public Set search(Expression e) { - ByteBuffer minTerm = e.lower() == null ? min : e.lower().value.encoded; - ByteBuffer maxTerm = e.upper() == null ? max : e.upper().value.encoded; - - return rangeTree.search(Interval.create(new Term(minTerm, indexTermType), - new Term(maxTerm, indexTermType), - null)); + Set result = new HashSet<>(); + rangeTrees.forEach((version, rangeTree) -> { + // Get the bounds given the version. Notice that we use the partially-encoded representation for bounds + // because that is how we store them in the range tree. The comparator is used to compare the bounds to + // each tree's min/max to see if the sstable index is in the query range. + Term minTerm = e.lower == null ? rangeTree.min() : new Term(e.getPartiallyEncodedLowerBound(version), comparator, version); + Term maxTerm = e.upper == null ? rangeTree.max() : new Term(e.getPartiallyEncodedUpperBound(version), comparator, version); + result.addAll(rangeTree.search(Interval.create(minTerm, maxTerm, null))); + }); + return result; } - static class Builder + static class Builder extends TermTree.Builder { - private final IndexTermType indexTermType; - private ByteBuffer min, max; - - final List> intervals = new ArrayList<>(); + // Because different indexes can have different encodings, we must track the versions of the indexes + final Map>> intervalsByVersion = new HashMap<>(); - protected Builder(IndexTermType indexTermType) + protected Builder(AbstractType comparator) { - this.indexTermType = indexTermType; + super(comparator); } - public final void add(SSTableIndex index) + public void addIndex(SSTableIndex index) { - assert !indexTermType.isVector(); - Interval interval = - Interval.create(new Term(index.minTerm(), indexTermType), new Term(index.maxTerm(), indexTermType), index); + Interval.create(new Term(index.minTerm(), comparator, index.getVersion()), + new Term(index.maxTerm(), comparator, index.getVersion()), + index); if (logger.isTraceEnabled()) { - logger.trace(index.getIndexIdentifier().logMessage("Adding index for SSTable {} with minTerm={} and maxTerm={}..."), - index.getSSTable().descriptor, - index.minTerm() != null ? indexTermType.indexType().compose(index.minTerm()) : null, - index.maxTerm() != null ? indexTermType.indexType().compose(index.maxTerm()) : null); + IndexContext context = index.getIndexContext(); + logger.trace(context.logMessage("Adding index for SSTable {} with minTerm={} and maxTerm={} and version={}..."), + index.getSSTable().descriptor, + index.minTerm() != null ? comparator.compose(index.minTerm()) : null, + index.maxTerm() != null ? comparator.compose(index.maxTerm()) : null, + index.getVersion()); } - intervals.add(interval); - - min = min == null || index.getIndexTermType().compare(min, index.minTerm()) > 0 ? index.minTerm() : min; - max = max == null || index.getIndexTermType().compare(max, index.maxTerm()) < 0 ? index.maxTerm() : max; + intervalsByVersion.compute(index.getVersion(), (__, list) -> + { + if (list == null) + list = new ArrayList<>(); + list.add(interval); + return list; + }); } - public RangeTermTree build() + public TermTree build() { - return new RangeTermTree(min, max, IntervalTree.build(intervals), indexTermType); + Map>> trees = new HashMap<>(); + intervalsByVersion.forEach((version, intervals) -> trees.put(version, IntervalTree.build(intervals))); + return new RangeTermTree(trees, comparator); } } @@ -105,24 +118,26 @@ public RangeTermTree build() protected static class Term implements Comparable { private final ByteBuffer term; - private final IndexTermType indexTermType; + private final AbstractType comparator; + private final Version version; - Term(ByteBuffer term, IndexTermType indexTermType) + Term(ByteBuffer term, AbstractType comparator, Version version) { this.term = term; - this.indexTermType = indexTermType; + this.comparator = comparator; + this.version = version; } - @Override public int compareTo(Term o) { - return indexTermType.compare(term, o.term); - } - - @Override - public String toString() - { - return MoreObjects.toStringHelper(this).add("term", indexTermType.asString(term)).toString(); + assert version == o.version : "Cannot compare terms from different versions, but found " + version + " and " + o.version; + if (term == null && o.term == null) + return 0; + if (term == null) + return -1; + if (o.term == null) + return 1; + return TypeUtil.compare(term, o.term, comparator, version); } } } diff --git a/src/java/org/apache/cassandra/index/sai/view/TermTree.java b/src/java/org/apache/cassandra/index/sai/view/TermTree.java new file mode 100644 index 000000000000..72eece0c2e1b --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/view/TermTree.java @@ -0,0 +1,55 @@ +/* + * All changes to the original code are Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.index.sai.view; + +import java.util.Set; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.plan.Expression; + +public interface TermTree +{ + Set search(Expression e); + + abstract class Builder + { + protected final AbstractType comparator; + + protected Builder(AbstractType comparator) + { + this.comparator = comparator; + } + + public final void add(SSTableIndex index) + { + addIndex(index); + } + + protected abstract void addIndex(SSTableIndex index); + + public abstract TermTree build(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/view/View.java b/src/java/org/apache/cassandra/index/sai/view/View.java index da88059ba1c9..2b620135909a 100644 --- a/src/java/org/apache/cassandra/index/sai/view/View.java +++ b/src/java/org/apache/cassandra/index/sai/view/View.java @@ -18,62 +18,86 @@ package org.apache.cassandra.index.sai.view; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; -import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; import org.apache.cassandra.index.sai.plan.Expression; -import org.apache.cassandra.index.sai.utils.IndexTermType; import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.io.sstable.format.SSTableReader; - -/** - * The View is an immutable, point in time, view of the avalailable {@link SSTableIndex}es for an index. - *

    - * The view maintains a {@link RangeTermTree} for querying the view by value range. This is used by the - * {@link org.apache.cassandra.index.sai.plan.QueryViewBuilder} to select the set of {@link SSTableIndex}es - * to perform a query without needing to query indexes that are known not to contain to the requested - * expression value range. - */ +import org.apache.cassandra.utils.Interval; +import org.apache.cassandra.utils.IntervalTree; + public class View implements Iterable { private final Map view; + private final AtomicInteger references = new AtomicInteger(1); + private volatile boolean indexWasDropped; - private final RangeTermTree rangeTermTree; + private final TermTree termTree; + private final AbstractType keyValidator; + private final IntervalTree> keyIntervalTree; - public View(IndexTermType indexTermType, Collection indexes) + /** + * Construct a threadsafe view. + * @param context the index context + * @param indexes the indexes. Note that the referencing logic for these indexes is handled + * outside of this constructor and all indexes are assumed to have been referenced already. + * The view will release the indexes when it is finally released. + */ + public View(IndexContext context, Collection indexes) { this.view = new HashMap<>(); + this.keyValidator = context.keyValidator(); + + AbstractType validator = context.getValidator(); - RangeTermTree.Builder rangeTermTreeBuilder = new RangeTermTree.Builder(indexTermType); + TermTree.Builder termTreeBuilder = new RangeTermTree.Builder(validator); + List> keyIntervals = new ArrayList<>(); for (SSTableIndex sstableIndex : indexes) { this.view.put(sstableIndex.getSSTable().descriptor, sstableIndex); - // Skip vector indexes, since they are scatter gather for all terms. Skip empty indexes since they - // cannot be inserted into the tree due to the lack of min and max terms. - if (!indexTermType.isVector() && sstableIndex.getRowCount() > 0) - rangeTermTreeBuilder.add(sstableIndex); + if (!sstableIndex.getIndexContext().isVector()) + termTreeBuilder.add(sstableIndex); + + keyIntervals.add(Interval.create(new Key(sstableIndex.minKey()), + new Key(sstableIndex.maxKey()), + sstableIndex)); } - this.rangeTermTree = rangeTermTreeBuilder.build(); + this.termTree = termTreeBuilder.build(); + this.keyIntervalTree = IntervalTree.build(keyIntervals); } /** * Search for a list of {@link SSTableIndex}es that contain values within - * the value range requested in the {@link Expression} + * the value range requested in the {@link Expression}. Expressions associated with ORDER BY are not + * expected, and will throw an exception. */ - public Collection match(Expression expression) + public Set match(Expression expression) { - if (expression.getIndexOperator() == Expression.IndexOperator.ANN) - return getIndexes(); + if (expression.getOp() == Expression.Op.ORDER_BY) + throw new IllegalArgumentException("ORDER BY expression is not supported"); + if (expression.getOp() == Expression.Op.BOUNDED_ANN || expression.getOp().isNonEquality()) + return new HashSet<>(getIndexes()); + return termTree.search(expression); + } - return rangeTermTree.search(expression); + public List match(DecoratedKey minKey, DecoratedKey maxKey) + { + return keyIntervalTree.search(Interval.create(new Key(minKey), new Key(maxKey), null)); } - @Override public Iterator iterator() { return view.values().iterator(); @@ -84,9 +108,35 @@ public Collection getIndexes() return view.values(); } - public boolean containsSSTable(SSTableReader sstable) + public boolean reference() + { + while (true) + { + int n = references.get(); + if (n <= 0) + return false; + if (references.compareAndSet(n, n + 1)) + { + return true; + } + } + } + + public void release() + { + int n = references.decrementAndGet(); + if (n == 0) + if (indexWasDropped) + view.values().forEach(SSTableIndex::markIndexDropped); + else + view.values().forEach(SSTableIndex::release); + } + + public void markIndexWasDropped() { - return view.containsKey(sstable.descriptor); + // This ordering allows us to guarantee that in flight queries will not be interrupted in problematic ways. + indexWasDropped = true; + release(); } public int size() @@ -94,9 +144,63 @@ public int size() return view.size(); } + /** + * Tells if an index for the given sstable exists. + * It's equivalent to {@code getSSTableIndex(descriptor) != null }. + * @param descriptor identifies the sstable + */ + public boolean containsSSTableIndex(Descriptor descriptor) + { + return view.containsKey(descriptor); + } + + /** + * Tells if the view is aware of the given sstable. + * @param descriptor identifies the sstable + */ + public boolean isAwareOfSSTable(Descriptor descriptor) + { + return view.containsKey(descriptor); + } + + /** + * Get the SSTableIndex for the given sstable descriptor + * @param descriptor identifies the sstable + * @return the SSTableIndex or null if not found + */ + public SSTableIndex getSSTableIndex(Descriptor descriptor) + { + return view.get(descriptor); + } + + /** + * This is required since IntervalTree doesn't support custom Comparator + * implementations and relied on items to be comparable which "raw" keys are not. + */ + private static class Key implements Comparable + { + private final DecoratedKey key; + + public Key(DecoratedKey key) + { + this.key = key; + } + + public int compareTo(Key o) + { + if (key == null && o.key == null) + return 0; + if (key == null) + return -1; + if (o.key == null) + return 1; + return key.compareTo(o.key); + } + } + @Override public String toString() { - return String.format("View{view=%s}", view); + return String.format("View{view=%s, keyValidator=%s, keyIntervalTree=%s}", view, keyValidator, keyIntervalTree); } } diff --git a/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java index 2d2e5e2634c4..7430ce341280 100644 --- a/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java +++ b/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java @@ -27,7 +27,9 @@ import org.apache.cassandra.db.virtual.VirtualTable; import org.apache.cassandra.dht.LocalPartitioner; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.SecondaryIndexManager; +import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; import org.apache.cassandra.schema.Schema; @@ -90,18 +92,19 @@ public DataSet data() if (group != null) { - group.getIndexes().forEach(i -> { - StorageAttachedIndex index = (StorageAttachedIndex) i; - String indexName = index.identifier().indexName; + for (Index index : group.getIndexes()) + { + IndexContext context = ((StorageAttachedIndex) index).getIndexContext(); + String indexName = context.getIndexName(); dataset.row(ks, indexName) .column(TABLE_NAME, cfs.name) - .column(COLUMN_NAME, index.termType().columnName()) + .column(COLUMN_NAME, context.getColumnName()) .column(IS_QUERYABLE, manager.isIndexQueryable(index)) .column(IS_BUILDING, manager.isIndexBuilding(indexName)) - .column(IS_STRING, index.termType().isLiteral()) - .column(ANALYZER, index.hasAnalyzer() ? index.analyzer().toString() : "NoOpAnalyzer"); - }); + .column(IS_STRING, context.isLiteral()) + .column(ANALYZER, context.getAnalyzerFactory().toString()); + } } } } diff --git a/src/java/org/apache/cassandra/index/sai/virtual/IndexesSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/IndexesSystemView.java new file mode 100644 index 000000000000..79fc350839cd --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/virtual/IndexesSystemView.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.virtual; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.virtual.AbstractVirtualTable; +import org.apache.cassandra.db.virtual.SimpleDataSet; +import org.apache.cassandra.db.virtual.VirtualTable; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.SecondaryIndexManager; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.index.sai.view.View; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; + +/** + * A {@link VirtualTable} providing a system view of per-column storage-attached index metadata. + */ +public class IndexesSystemView extends AbstractVirtualTable +{ + static final String NAME = "indexes"; + + static final String KEYSPACE_NAME = "keyspace_name"; + static final String INDEX_NAME = "index_name"; + static final String TABLE_NAME = "table_name"; + static final String COLUMN_NAME = "column_name"; + static final String IS_QUERYABLE = "is_queryable"; + static final String IS_BUILDING = "is_building"; + static final String IS_STRING = "is_string"; + static final String ANALYZER = "analyzer"; + static final String INDEXED_SSTABLE_COUNT = "indexed_sstable_count"; + static final String SSTABLE_COUNT = "sstable_count"; + static final String CELL_COUNT = "cell_count"; + static final String PER_TABLE_DISK_SIZE = "per_table_disk_size"; + static final String PER_COLUMN_DISK_SIZE = "per_column_disk_size"; + + public IndexesSystemView(String keyspace) + { + super(TableMetadata.builder(keyspace, NAME) + .partitioner(new LocalPartitioner(UTF8Type.instance)) + .comment("Storage-attached column index metadata") + .kind(TableMetadata.Kind.VIRTUAL) + .addPartitionKeyColumn(KEYSPACE_NAME, UTF8Type.instance) + .addClusteringColumn(INDEX_NAME, UTF8Type.instance) + .addRegularColumn(TABLE_NAME, UTF8Type.instance) + .addRegularColumn(COLUMN_NAME, UTF8Type.instance) + .addRegularColumn(IS_QUERYABLE, BooleanType.instance) + .addRegularColumn(IS_BUILDING, BooleanType.instance) + .addRegularColumn(IS_STRING, BooleanType.instance) + .addRegularColumn(ANALYZER, UTF8Type.instance) + .addRegularColumn(INDEXED_SSTABLE_COUNT, Int32Type.instance) + .addRegularColumn(SSTABLE_COUNT, Int32Type.instance) + .addRegularColumn(CELL_COUNT, LongType.instance) + .addRegularColumn(PER_TABLE_DISK_SIZE, LongType.instance) + .addRegularColumn(PER_COLUMN_DISK_SIZE, LongType.instance) + .build()); + } + + @Override + public void apply(PartitionUpdate update) + { + // TODO port DataSet. Now we can't change index queryability via system view + throw new InvalidRequestException("Modification is not supported by table " + metadata); + } + + @Override + public DataSet data() + { + SimpleDataSet dataset = new SimpleDataSet(metadata()); + + for (String ks : Schema.instance.getUserKeyspaces()) + { + Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks); + if (keyspace == null) + throw new IllegalArgumentException("Unknown keyspace " + ks); + + for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) + { + SecondaryIndexManager manager = cfs.indexManager; + StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs); + + int sstables = cfs.getLiveSSTables().size(); + if (group != null) + { + for (Index index : group.getIndexes()) + { + IndexContext context = ((StorageAttachedIndex)index).getIndexContext(); + String indexName = context.getIndexName(); + View view = context.getView(); + + dataset.row(ks, indexName) + .column(TABLE_NAME, cfs.name) + .column(COLUMN_NAME, context.getColumnName()) + .column(IS_QUERYABLE, manager.isIndexQueryable(index)) + .column(IS_BUILDING, manager.isIndexBuilding(indexName)) + .column(IS_STRING, context.isLiteral()) + .column(ANALYZER, context.getAnalyzerFactory().toString()) + .column(INDEXED_SSTABLE_COUNT, view.size()) + .column(SSTABLE_COUNT, sstables) + .column(CELL_COUNT, context.getCellCount()) + .column(PER_TABLE_DISK_SIZE, group.diskUsage()) + .column(PER_COLUMN_DISK_SIZE, context.diskUsage()); + } + } + } + } + + return dataset; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java index 40c0c7bc12a5..12c42b1f2e67 100644 --- a/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java +++ b/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java @@ -27,9 +27,11 @@ import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.LocalPartitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; -import org.apache.cassandra.index.sai.disk.SSTableIndex; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.Schema; @@ -97,10 +99,11 @@ public DataSet data() { Token.TokenFactory tokenFactory = cfs.metadata().partitioner.getTokenFactory(); - group.getIndexes().forEach(i -> { - StorageAttachedIndex index = (StorageAttachedIndex)i; + for (Index index : group.getIndexes()) + { + IndexContext indexContext = ((StorageAttachedIndex)index).getIndexContext(); - for (SSTableIndex sstableIndex : index.view()) + for (SSTableIndex sstableIndex : indexContext.getView()) { // Empty indexes are tracked internally for the sake of having complete views. However, // these indexes have not historically been exposed in this virtual table, so we skip @@ -112,19 +115,19 @@ public DataSet data() Descriptor descriptor = sstable.descriptor; AbstractBounds bounds = sstable.getBounds(); - dataset.row(ks, index.identifier().indexName, sstable.getFilename()) + dataset.row(ks, indexContext.getIndexName(), sstable.getFilename()) .column(TABLE_NAME, descriptor.cfname) - .column(COLUMN_NAME, index.termType().columnName()) + .column(COLUMN_NAME, indexContext.getColumnName()) .column(FORMAT_VERSION, sstableIndex.getVersion().toString()) .column(CELL_COUNT, sstableIndex.getRowCount()) .column(MIN_ROW_ID, sstableIndex.minSSTableRowId()) .column(MAX_ROW_ID, sstableIndex.maxSSTableRowId()) .column(START_TOKEN, tokenFactory.toString(bounds.left)) .column(END_TOKEN, tokenFactory.toString(bounds.right)) - .column(PER_TABLE_DISK_SIZE, sstableIndex.getSSTableContext().diskUsage()) + .column(PER_TABLE_DISK_SIZE, sstableIndex.sizeOfPerSSTableComponents()) .column(PER_COLUMN_DISK_SIZE, sstableIndex.sizeOfPerColumnComponents()); } - }); + } } } } diff --git a/src/java/org/apache/cassandra/index/sai/virtual/SSTablesSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/SSTablesSystemView.java new file mode 100644 index 000000000000..68a258520072 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/virtual/SSTablesSystemView.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.index.sai.virtual; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.virtual.AbstractVirtualTable; +import org.apache.cassandra.db.virtual.SimpleDataSet; +import org.apache.cassandra.db.virtual.VirtualTable; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; +import org.apache.cassandra.index.sai.StorageAttachedIndex; +import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; + +/** + * A {@link VirtualTable} providing a system view of SSTable index metadata. + */ +public class SSTablesSystemView extends AbstractVirtualTable +{ + static final String NAME = "sstable_indexes"; + + static final String KEYSPACE_NAME = "keyspace_name"; + static final String INDEX_NAME = "index_name"; + static final String SSTABLE_NAME = "sstable_name"; + static final String TABLE_NAME = "table_name"; + static final String COLUMN_NAME = "column_name"; + static final String FORMAT_VERSION = "format_version"; + static final String CELL_COUNT = "cell_count"; + static final String MIN_ROW_ID = "min_row_id"; + static final String MAX_ROW_ID = "max_row_id"; + static final String START_TOKEN = "start_token"; + static final String END_TOKEN = "end_token"; + static final String PER_TABLE_DISK_SIZE = "per_table_disk_size"; + static final String PER_COLUMN_DISK_SIZE = "per_column_disk_size"; + + public SSTablesSystemView(String keyspace) + { + super(TableMetadata.builder(keyspace, NAME) + .partitioner(new LocalPartitioner(UTF8Type.instance)) + .comment("SSTable index metadata") + .kind(TableMetadata.Kind.VIRTUAL) + .addPartitionKeyColumn(KEYSPACE_NAME, UTF8Type.instance) + .addClusteringColumn(INDEX_NAME, UTF8Type.instance) + .addClusteringColumn(SSTABLE_NAME, UTF8Type.instance) + .addRegularColumn(TABLE_NAME, UTF8Type.instance) + .addRegularColumn(COLUMN_NAME, UTF8Type.instance) + .addRegularColumn(FORMAT_VERSION, UTF8Type.instance) + .addRegularColumn(CELL_COUNT, LongType.instance) + .addRegularColumn(MIN_ROW_ID, LongType.instance) + .addRegularColumn(MAX_ROW_ID, LongType.instance) + .addRegularColumn(START_TOKEN, UTF8Type.instance) + .addRegularColumn(END_TOKEN, UTF8Type.instance) + .addRegularColumn(PER_TABLE_DISK_SIZE, LongType.instance) + .addRegularColumn(PER_COLUMN_DISK_SIZE, LongType.instance) + .build()); + } + + @Override + public DataSet data() + { + SimpleDataSet dataset = new SimpleDataSet(metadata()); + + for (String ks : Schema.instance.getUserKeyspaces()) + { + Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks); + if (keyspace == null) + throw new IllegalArgumentException("Unknown keyspace " + ks); + + for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) + { + StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs); + + if (group != null) + { + Token.TokenFactory tokenFactory = cfs.metadata().partitioner.getTokenFactory(); + + for (Index index : group.getIndexes()) + { + IndexContext indexContext = ((StorageAttachedIndex)index).getIndexContext(); + + for (SSTableIndex sstableIndex : indexContext.getView()) + { + // Empty indexes were introduced to make negative searches + // (NOT_EQ, NOT_CONTAINS, NOT_CONTAINS_KEY) work, but they don't have any representation + // on disk, so for backwards compatibility we're not reporting them. + if (sstableIndex.isEmpty()) + continue; + + SSTableReader sstable = sstableIndex.getSSTable(); + Descriptor descriptor = sstable.descriptor; + AbstractBounds bounds = sstable.getBounds(); + + dataset.row(ks, indexContext.getIndexName(), sstable.getFilename()) + .column(TABLE_NAME, descriptor.cfname) + .column(COLUMN_NAME, indexContext.getColumnName()) + .column(FORMAT_VERSION, sstableIndex.getVersion().toString()) + .column(CELL_COUNT, sstableIndex.getRowCount()) + .column(MIN_ROW_ID, sstableIndex.minSSTableRowId()) + .column(MAX_ROW_ID, sstableIndex.maxSSTableRowId()) + .column(START_TOKEN, tokenFactory.toString(bounds.left)) + .column(END_TOKEN, tokenFactory.toString(bounds.right)) + .column(PER_TABLE_DISK_SIZE, sstableIndex.sizeOfPerSSTableComponents()) + .column(PER_COLUMN_DISK_SIZE, sstableIndex.sizeOfPerColumnComponents()); + } + } + } + } + } + + return dataset; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java index d3206f1766d8..e4f4dd1d1978 100644 --- a/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java +++ b/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java @@ -28,9 +28,11 @@ import org.apache.cassandra.db.virtual.SimpleDataSet; import org.apache.cassandra.db.virtual.VirtualTable; import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.SSTableIndex; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; -import org.apache.cassandra.index.sai.disk.SSTableIndex; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; @@ -39,7 +41,7 @@ */ public class SegmentsSystemView extends AbstractVirtualTable { - public static final String NAME = "sai_sstable_index_segments"; + public static final String NAME = "sstable_index_segments"; public static final String KEYSPACE_NAME = "keyspace_name"; public static final String INDEX_NAME = "index_name"; @@ -87,8 +89,8 @@ public DataSet data() { SimpleDataSet dataset = new SimpleDataSet(metadata()); - forEachIndex(index -> { - for (SSTableIndex sstableIndex : index.view()) + forEachIndex(indexContext -> { + for (SSTableIndex sstableIndex : indexContext.getView()) { sstableIndex.populateSegmentView(dataset); } @@ -97,20 +99,25 @@ public DataSet data() return dataset; } - private void forEachIndex(Consumer process) + private void forEachIndex(Consumer process) { for (String ks : Schema.instance.getUserKeyspaces()) { Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks); if (keyspace == null) - throw new IllegalStateException("Unknown keyspace " + ks + ". This can occur if the keyspace is being dropped."); + throw new IllegalArgumentException("Unknown keyspace " + ks); for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) { StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs); if (group != null) - group.getIndexes().stream().map(index -> (StorageAttachedIndex) index).forEach(process); + { + for (Index index : group.getIndexes()) + { + process.accept(((StorageAttachedIndex)index).getIndexContext()); + } + } } } } diff --git a/src/java/org/apache/cassandra/index/sasi/SASIIndex.java b/src/java/org/apache/cassandra/index/sasi/SASIIndex.java deleted file mode 100644 index 3440cd8cbffd..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/SASIIndex.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Optional; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.concurrent.Callable; - -import com.googlecode.concurrenttrees.common.Iterables; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.cql3.statements.schema.IndexTarget; -import org.apache.cassandra.db.CassandraWriteContext; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.DeletionTime; -import org.apache.cassandra.db.RangeTombstone; -import org.apache.cassandra.db.ReadCommand; -import org.apache.cassandra.db.RegularAndStaticColumns; -import org.apache.cassandra.db.WriteContext; -import org.apache.cassandra.db.compaction.CompactionManager; -import org.apache.cassandra.db.compaction.OperationType; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; -import org.apache.cassandra.db.lifecycle.Tracker; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.partitions.PartitionUpdate; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.index.Index; -import org.apache.cassandra.index.IndexRegistry; -import org.apache.cassandra.index.SecondaryIndexBuilder; -import org.apache.cassandra.index.TargetParser; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.conf.IndexMode; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode; -import org.apache.cassandra.index.sasi.disk.PerSSTableIndexWriter; -import org.apache.cassandra.index.sasi.plan.SASIIndexSearcher; -import org.apache.cassandra.index.transactions.IndexTransaction; -import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.io.sstable.SSTableFlushObserver; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.notifications.INotification; -import org.apache.cassandra.notifications.INotificationConsumer; -import org.apache.cassandra.notifications.MemtableDiscardedNotification; -import org.apache.cassandra.notifications.MemtableRenewedNotification; -import org.apache.cassandra.notifications.MemtableSwitchedNotification; -import org.apache.cassandra.notifications.SSTableAddedNotification; -import org.apache.cassandra.notifications.SSTableListChangedNotification; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.IndexMetadata; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.concurrent.OpOrder; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -public class SASIIndex implements Index, INotificationConsumer -{ - public final static String USAGE_WARNING = "SASI indexes are experimental and are not recommended for production use."; - - private static class SASIIndexBuildingSupport implements IndexBuildingSupport - { - public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, - Set indexes, - Collection sstablesToRebuild, - boolean isFullRebuild) - { - NavigableMap> sstables = new TreeMap<>(SSTableReader.idComparator); - - indexes.stream() - .filter((i) -> i instanceof SASIIndex) - .forEach((i) -> { - SASIIndex sasi = (SASIIndex) i; - sasi.index.dropData(sstablesToRebuild); - sstablesToRebuild.stream() - .filter((sstable) -> !sasi.index.hasSSTable(sstable)) - .forEach((sstable) -> { - Map toBuild = sstables.get(sstable); - if (toBuild == null) - sstables.put(sstable, (toBuild = new HashMap<>())); - - toBuild.put(sasi.index.getDefinition(), sasi.index); - }); - }); - - return new SASIIndexBuilder(cfs, sstables); - } - } - - private static final SASIIndexBuildingSupport INDEX_BUILDER_SUPPORT = new SASIIndexBuildingSupport(); - - private final ColumnFamilyStore baseCfs; - private final IndexMetadata config; - private final ColumnIndex index; - - public SASIIndex(ColumnFamilyStore baseCfs, IndexMetadata config) - { - this.baseCfs = baseCfs; - this.config = config; - - ColumnMetadata column = TargetParser.parse(baseCfs.metadata(), config).left; - this.index = new ColumnIndex(baseCfs.metadata().partitionKeyType, column, config); - - Tracker tracker = baseCfs.getTracker(); - tracker.subscribe(this); - - SortedMap> toRebuild = new TreeMap<>(SSTableReader.idComparator); - - for (SSTableReader sstable : index.init(tracker.getView().liveSSTables())) - { - Map perSSTable = toRebuild.get(sstable); - if (perSSTable == null) - toRebuild.put(sstable, (perSSTable = new HashMap<>())); - - perSSTable.put(index.getDefinition(), index); - } - - CompactionManager.instance.submitIndexBuild(new SASIIndexBuilder(baseCfs, toRebuild)); - } - - /** - * Called via reflection at {@link IndexMetadata#validateCustomIndexOptions} - */ - public static Map validateOptions(Map options, TableMetadata metadata) - { - if (!(metadata.partitioner instanceof Murmur3Partitioner)) - throw new ConfigurationException("SASI only supports Murmur3Partitioner."); - - String targetColumn = options.get("target"); - if (targetColumn == null) - throw new ConfigurationException("unknown target column"); - - Pair target = TargetParser.parse(metadata, targetColumn); - if (target == null) - throw new ConfigurationException("failed to retrieve target column for: " + targetColumn); - - if (target.left.isComplex()) - throw new ConfigurationException("complex columns are not yet supported by SASI"); - - if (target.left.isPartitionKey()) - throw new ConfigurationException("partition key columns are not yet supported by SASI"); - - IndexMode.validateAnalyzer(options, target.left); - - IndexMode mode = IndexMode.getMode(target.left, options); - if (mode.mode == Mode.SPARSE) - { - if (mode.isLiteral) - throw new ConfigurationException("SPARSE mode is only supported on non-literal columns."); - - if (mode.isAnalyzed) - throw new ConfigurationException("SPARSE mode doesn't support analyzers."); - } - - return Collections.emptyMap(); - } - - @Override - public void register(IndexRegistry registry) - { - registry.registerIndex(this, new Group.Key(this), () -> new SASIIndexGroup(this)); - } - - public IndexMetadata getIndexMetadata() - { - return config; - } - - public Callable getInitializationTask() - { - return null; - } - - public Callable getMetadataReloadTask(IndexMetadata indexMetadata) - { - return null; - } - - public Callable getBlockingFlushTask() - { - return null; // SASI indexes are flushed along side memtable - } - - public Callable getInvalidateTask() - { - return getTruncateTask(FBUtilities.timestampMicros()); - } - - public Callable getTruncateTask(long truncatedAt) - { - return () -> { - index.dropData(truncatedAt); - return null; - }; - } - - @Override - public boolean shouldBuildBlocking() - { - return true; - } - - public Optional getBackingTable() - { - return Optional.empty(); - } - - public boolean indexes(RegularAndStaticColumns columns) - { - return columns.contains(index.getDefinition()); - } - - public boolean dependsOn(ColumnMetadata column) - { - return index.getDefinition().compareTo(column) == 0; - } - - public boolean supportsExpression(ColumnMetadata column, Operator operator) - { - return dependsOn(column) && index.supports(operator); - } - - public AbstractType customExpressionValueType() - { - return null; - } - - public RowFilter getPostIndexQueryFilter(RowFilter filter) - { - return filter.withoutExpressions(); - } - - public long getEstimatedResultRows() - { - // this is temporary (until proper QueryPlan is integrated into Cassandra) - // and allows us to priority SASI indexes if any in the query since they - // are going to be more efficient, to query and intersect, than built-in indexes. - return Long.MIN_VALUE; - } - - @Override - public void validate(PartitionUpdate update, ClientState state) throws InvalidRequestException - {} - - @Override - public boolean notifyIndexerAboutRowsInFullyExpiredSSTables() - { - return false; - } - - @Override - public Indexer indexerFor(DecoratedKey key, RegularAndStaticColumns columns, long nowInSec, WriteContext context, IndexTransaction.Type transactionType, Memtable memtable) - { - return new Indexer() - { - public void begin() - {} - - public void partitionDelete(DeletionTime deletionTime) - {} - - public void rangeTombstone(RangeTombstone tombstone) - {} - - public void insertRow(Row row) - { - if (isNewData()) - adjustMemtableSize(index.index(key, row), CassandraWriteContext.fromContext(context).getGroup()); - } - - public void updateRow(Row oldRow, Row newRow) - { - insertRow(newRow); - } - - public void removeRow(Row row) - {} - - public void finish() - {} - - // we are only interested in the data from Memtable - // everything else is going to be handled by SSTableWriter observers - private boolean isNewData() - { - return transactionType == IndexTransaction.Type.UPDATE; - } - - public void adjustMemtableSize(long additionalSpace, OpOrder.Group opGroup) - { - baseCfs.getTracker().getView().getCurrentMemtable().markExtraOnHeapUsed(additionalSpace, opGroup); - } - }; - } - - public Searcher searcherFor(ReadCommand command) throws InvalidRequestException - { - TableMetadata config = command.metadata(); - ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(config.id); - return new SASIIndexSearcher(cfs, command, DatabaseDescriptor.getRangeRpcTimeout(MILLISECONDS)); - } - - public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker) - { - return newWriter(baseCfs.metadata().partitionKeyType, descriptor, Collections.singletonMap(index.getDefinition(), index), tracker.opType()); - } - - public IndexBuildingSupport getBuildTaskSupport() - { - return INDEX_BUILDER_SUPPORT; - } - - public void handleNotification(INotification notification, Object sender) - { - // unfortunately, we can only check the type of notification via instanceof :( - if (notification instanceof SSTableAddedNotification) - { - SSTableAddedNotification notice = (SSTableAddedNotification) notification; - index.update(Collections.emptyList(), Iterables.toList(notice.added)); - } - else if (notification instanceof SSTableListChangedNotification) - { - SSTableListChangedNotification notice = (SSTableListChangedNotification) notification; - index.update(notice.removed, notice.added); - } - else if (notification instanceof MemtableRenewedNotification) - { - index.switchMemtable(); - } - else if (notification instanceof MemtableSwitchedNotification) - { - index.switchMemtable(((MemtableSwitchedNotification) notification).previous); - } - else if (notification instanceof MemtableDiscardedNotification) - { - index.discardMemtable(((MemtableDiscardedNotification) notification).memtable); - } - } - - public ColumnIndex getIndex() - { - return index; - } - - protected static PerSSTableIndexWriter newWriter(AbstractType keyValidator, - Descriptor descriptor, - Map indexes, - OperationType opType) - { - return new PerSSTableIndexWriter(keyValidator, descriptor, opType, indexes); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java b/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java deleted file mode 100644 index 555bce1b9add..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - */ -package org.apache.cassandra.index.sasi; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.SortedMap; - -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.compaction.CompactionInfo; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; -import org.apache.cassandra.db.compaction.OperationType; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.index.SecondaryIndexBuilder; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.PerSSTableIndexWriter; -import org.apache.cassandra.io.FSReadError; -import org.apache.cassandra.io.sstable.KeyReader; -import org.apache.cassandra.io.sstable.SSTableIdentityIterator; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.RandomAccessReader; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.TimeUUID; - -import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; - -class SASIIndexBuilder extends SecondaryIndexBuilder -{ - private final ColumnFamilyStore cfs; - private final TimeUUID compactionId = nextTimeUUID(); - - // Keep targetDirectory for compactions, needed for `nodetool compactionstats` - private String targetDirectory; - - private final SortedMap> sstables; - - private long bytesProcessed = 0; - private final long totalBytesToProcess; - - public SASIIndexBuilder(ColumnFamilyStore cfs, SortedMap> sstables) - { - long totalBytesToProcess = 0; - for (SSTableReader sstable : sstables.keySet()) - totalBytesToProcess += sstable.uncompressedLength(); - - this.cfs = cfs; - this.sstables = sstables; - this.totalBytesToProcess = totalBytesToProcess; - } - - public void build() - { - AbstractType keyValidator = cfs.metadata().partitionKeyType; - long processedBytesInFinishedSSTables = 0; - for (Map.Entry> e : sstables.entrySet()) - { - SSTableReader sstable = e.getKey(); - Map indexes = e.getValue(); - - try (RandomAccessReader dataFile = sstable.openDataReader()) - { - PerSSTableIndexWriter indexWriter = SASIIndex.newWriter(keyValidator, sstable.descriptor, indexes, OperationType.COMPACTION); - targetDirectory = indexWriter.getDescriptor().directory.path(); - - try (KeyReader keys = sstable.keyReader()) - { - while (!keys.isExhausted()) - { - if (isStopRequested()) - throw new CompactionInterruptedException(getCompactionInfo()); - - final DecoratedKey key = sstable.decorateKey(keys.key()); - final long keyPosition = keys.keyPositionForSecondaryIndex(); - - indexWriter.startPartition(key, keys.dataPosition(), keyPosition); - - dataFile.seek(keys.dataPosition()); - ByteBufferUtil.readWithShortLength(dataFile); // key - - try (SSTableIdentityIterator partition = SSTableIdentityIterator.create(sstable, dataFile, key)) - { - // if the row has statics attached, it has to be indexed separately - if (cfs.metadata().hasStaticColumns()) - { - indexWriter.nextUnfilteredCluster(partition.staticRow()); - } - - while (partition.hasNext()) - indexWriter.nextUnfilteredCluster(partition.next()); - } - - keys.advance(); - long dataPosition = keys.isExhausted() ? sstable.uncompressedLength() : keys.dataPosition(); - bytesProcessed = processedBytesInFinishedSSTables + dataPosition; - } - - completeSSTable(indexWriter, sstable, indexes.values()); - } - catch (IOException ex) - { - throw new FSReadError(ex, sstable.getFilename()); - } - processedBytesInFinishedSSTables += sstable.uncompressedLength(); - } - } - } - - public CompactionInfo getCompactionInfo() - { - return new CompactionInfo(cfs.metadata(), - OperationType.INDEX_BUILD, - bytesProcessed, - totalBytesToProcess, - compactionId, - sstables.keySet(), - targetDirectory); - } - - private void completeSSTable(PerSSTableIndexWriter indexWriter, SSTableReader sstable, Collection indexes) - { - indexWriter.complete(); - - for (ColumnIndex index : indexes) - { - File tmpIndex = sstable.descriptor.fileFor(index.getComponent()); - if (!tmpIndex.exists()) // no data was inserted into the index for given sstable - continue; - - index.update(Collections.emptyList(), Collections.singletonList(sstable)); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/SASIIndexGroup.java b/src/java/org/apache/cassandra/index/sasi/SASIIndexGroup.java deleted file mode 100644 index 8b4c6fa0cbcf..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/SASIIndexGroup.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi; - -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.index.Index; -import org.apache.cassandra.index.SingletonIndexGroup; -import org.apache.cassandra.index.sasi.plan.SASIIndexQueryPlan; - -public class SASIIndexGroup extends SingletonIndexGroup -{ - public SASIIndexGroup(SASIIndex index) - { - super(index); - } - - @Override - public Index.QueryPlan queryPlanFor(RowFilter rowFilter) - { - return SASIIndexQueryPlan.create((SASIIndex) getIndex(), rowFilter); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/SSTableIndex.java b/src/java/org/apache/cassandra/index/sasi/SSTableIndex.java deleted file mode 100644 index b5e790564013..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/SSTableIndex.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import com.google.common.base.Function; -import org.apache.commons.lang3.builder.HashCodeBuilder; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.io.FSReadError; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.concurrent.Ref; - -public class SSTableIndex implements Comparable -{ - private final ColumnIndex columnIndex; - private final Ref sstableRef; - private final SSTableReader sstable; - private final OnDiskIndex index; - private final AtomicInteger references = new AtomicInteger(1); - private final AtomicBoolean obsolete = new AtomicBoolean(false); - - public SSTableIndex(ColumnIndex index, File indexFile, SSTableReader referent) - { - this.columnIndex = index; - this.sstableRef = referent.tryRef(); - this.sstable = sstableRef.get(); - - if (sstable == null) - throw new IllegalStateException("Couldn't acquire reference to the sstable: " + referent); - - AbstractType validator = columnIndex.getValidator(); - - assert validator != null; - assert indexFile.exists() : String.format("SSTable %s should have index %s.", - sstable.getFilename(), - columnIndex.getIndexName()); - - this.index = new OnDiskIndex(indexFile, validator, new DecoratedKeyFetcher(sstable)); - } - - public OnDiskIndexBuilder.Mode mode() - { - return index.mode(); - } - - public boolean hasMarkedPartials() - { - return index.hasMarkedPartials(); - } - - public ByteBuffer minTerm() - { - return index.minTerm(); - } - - public ByteBuffer maxTerm() - { - return index.maxTerm(); - } - - public ByteBuffer minKey() - { - return index.minKey(); - } - - public ByteBuffer maxKey() - { - return index.maxKey(); - } - - public RangeIterator search(Expression expression) - { - return index.search(expression); - } - - public SSTableReader getSSTable() - { - return sstable; - } - - public String getPath() - { - return index.getIndexPath(); - } - - public boolean reference() - { - while (true) - { - int n = references.get(); - if (n <= 0) - return false; - if (references.compareAndSet(n, n + 1)) - return true; - } - } - - public void release() - { - int n = references.decrementAndGet(); - if (n == 0) - { - FileUtils.closeQuietly(index); - sstableRef.release(); - if (obsolete.get() || sstableRef.globalCount() == 0) - FileUtils.delete(index.getIndexPath()); - } - } - - public void markObsolete() - { - obsolete.getAndSet(true); - release(); - } - - public boolean isObsolete() - { - return obsolete.get(); - } - - public boolean equals(Object o) - { - return o instanceof SSTableIndex && index.getIndexPath().equals(((SSTableIndex) o).index.getIndexPath()); - } - - public int hashCode() - { - return new HashCodeBuilder().append(index.getIndexPath()).build(); - } - - public String toString() - { - return String.format("SSTableIndex(column: %s, SSTable: %s)", columnIndex.getColumnName(), sstable.descriptor); - } - - @Override - public int compareTo(SSTableIndex o) - { - // Relied on in IntervalTree to be unique - return sstable.compareTo(o.sstable); - } - - private static class DecoratedKeyFetcher implements Function - { - private final SSTableReader sstable; - - DecoratedKeyFetcher(SSTableReader reader) - { - sstable = reader; - } - - public DecoratedKey apply(Long offset) - { - try - { - return sstable.keyAtPositionFromSecondaryIndex(offset); - } - catch (IOException e) - { - throw new FSReadError(new IOException("Failed to read key from " + sstable.descriptor, e), sstable.getFilename()); - } - } - - public int hashCode() - { - return sstable.descriptor.hashCode(); - } - - public boolean equals(Object other) - { - return other instanceof DecoratedKeyFetcher - && sstable.descriptor.equals(((DecoratedKeyFetcher) other).sstable.descriptor); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/Term.java b/src/java/org/apache/cassandra/index/sasi/Term.java deleted file mode 100644 index 8f42d5874a8a..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/Term.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.TermSize; -import org.apache.cassandra.index.sasi.utils.MappedBuffer; -import org.apache.cassandra.db.marshal.AbstractType; - -import static org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.IS_PARTIAL_BIT; - -public class Term -{ - protected final MappedBuffer content; - protected final TermSize termSize; - - private final boolean hasMarkedPartials; - - public Term(MappedBuffer content, TermSize size, boolean hasMarkedPartials) - { - this.content = content; - this.termSize = size; - this.hasMarkedPartials = hasMarkedPartials; - } - - public ByteBuffer getTerm() - { - long offset = termSize.isConstant() ? content.position() : content.position() + 2; - int length = termSize.isConstant() ? termSize.size : readLength(content.position()); - - return content.getPageRegion(offset, length); - } - - public boolean isPartial() - { - return !termSize.isConstant() - && hasMarkedPartials - && (content.getShort(content.position()) & (1 << IS_PARTIAL_BIT)) != 0; - } - - public long getDataOffset() - { - long position = content.position(); - return position + (termSize.isConstant() ? termSize.size : 2 + readLength(position)); - } - - public int compareTo(AbstractType comparator, ByteBuffer query) - { - return compareTo(comparator, query, true); - } - - public int compareTo(AbstractType comparator, ByteBuffer query, boolean checkFully) - { - long position = content.position(); - int padding = termSize.isConstant() ? 0 : 2; - int len = termSize.isConstant() ? termSize.size : readLength(position); - - return content.comparePageTo(position + padding, checkFully ? len : Math.min(len, query.remaining()), comparator, query); - } - - private short readLength(long position) - { - return (short) (content.getShort(position) & ~(1 << IS_PARTIAL_BIT)); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/TermIterator.java b/src/java/org/apache/cassandra/index/sasi/TermIterator.java deleted file mode 100644 index a180ac3478af..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/TermIterator.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; - -import io.netty.util.concurrent.FastThreadLocal; -import org.apache.cassandra.concurrent.ImmediateExecutor; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.io.util.FileUtils; - -import org.apache.cassandra.utils.concurrent.CountDownLatch; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static java.lang.String.format; -import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; -import static org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode.CONTAINS; -import static org.apache.cassandra.index.sasi.plan.Expression.Op.PREFIX; -import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch; - -public class TermIterator extends RangeIterator -{ - private static final Logger logger = LoggerFactory.getLogger(TermIterator.class); - - private static final FastThreadLocal SEARCH_EXECUTOR = new FastThreadLocal() - { - public ExecutorService initialValue() - { - final String currentThread = Thread.currentThread().getName(); - final int concurrencyFactor = DatabaseDescriptor.searchConcurrencyFactor(); - - logger.info("Search Concurrency Factor is set to {} for {}", concurrencyFactor, currentThread); - - return (concurrencyFactor <= 1) - ? ImmediateExecutor.INSTANCE - : executorFactory().pooled(currentThread + "-SEARCH-", concurrencyFactor); - } - }; - - private final Expression expression; - - private final RangeIterator union; - private final Set referencedIndexes; - - private TermIterator(Expression e, - RangeIterator union, - Set referencedIndexes) - { - super(union.getMinimum(), union.getMaximum(), union.getCount()); - - this.expression = e; - this.union = union; - this.referencedIndexes = referencedIndexes; - } - - public static TermIterator build(final Expression e, Set perSSTableIndexes) - { - final List> tokens = new CopyOnWriteArrayList<>(); - final AtomicLong tokenCount = new AtomicLong(0); - - RangeIterator memtableIterator = e.index.searchMemtable(e); - if (memtableIterator != null) - { - tokens.add(memtableIterator); - tokenCount.addAndGet(memtableIterator.getCount()); - } - - final Set referencedIndexes = new CopyOnWriteArraySet<>(); - - try - { - final CountDownLatch latch = newCountDownLatch(perSSTableIndexes.size()); - final ExecutorService searchExecutor = SEARCH_EXECUTOR.get(); - - for (final SSTableIndex index : perSSTableIndexes) - { - if (e.getOp() == PREFIX && - index.mode() == CONTAINS && !index.hasMarkedPartials()) - throw new UnsupportedOperationException(format("The index %s has not yet been upgraded " + - "to support prefix queries in CONTAINS mode. " + - "Wait for compaction or rebuild the index.", - index.getPath())); - - - if (!index.reference()) - { - latch.decrement(); - continue; - } - - // add to referenced right after the reference was acquired, - // that helps to release index if something goes bad inside of the search - referencedIndexes.add(index); - - searchExecutor.submit((Runnable) () -> { - try - { - e.checkpoint(); - - RangeIterator keyIterator = index.search(e); - if (keyIterator == null) - { - releaseIndex(referencedIndexes, index); - return; - } - - tokens.add(keyIterator); - tokenCount.getAndAdd(keyIterator.getCount()); - } - catch (Throwable e1) - { - releaseIndex(referencedIndexes, index); - - if (logger.isDebugEnabled()) - logger.debug(format("Failed search an index %s, skipping.", index.getPath()), e1); - } - finally - { - latch.decrement(); - } - }); - } - - latch.awaitUninterruptibly(); - - // checkpoint right away after all indexes complete search because we might have crossed the quota - e.checkpoint(); - - RangeIterator ranges = RangeUnionIterator.build(tokens); - return new TermIterator(e, ranges, referencedIndexes); - } - catch (Throwable ex) - { - // if execution quota was exceeded while opening indexes or something else happened - // local (yet to be tracked) indexes should be released first before re-throwing exception - referencedIndexes.forEach(TermIterator::releaseQuietly); - - throw ex; - } - } - - protected Token computeNext() - { - try - { - return union.hasNext() ? union.next() : endOfData(); - } - finally - { - expression.checkpoint(); - } - } - - protected void performSkipTo(Long nextToken) - { - try - { - union.skipTo(nextToken); - } - finally - { - expression.checkpoint(); - } - } - - public void close() - { - FileUtils.closeQuietly(union); - referencedIndexes.forEach(TermIterator::releaseQuietly); - referencedIndexes.clear(); - } - - private static void releaseIndex(Set indexes, SSTableIndex index) - { - indexes.remove(index); - releaseQuietly(index); - } - - private static void releaseQuietly(SSTableIndex index) - { - try - { - index.release(); - } - catch (Throwable e) - { - logger.error(String.format("Failed to release index %s", index.getPath()), e); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java deleted file mode 100644 index f9d63885bc18..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.nio.ByteBuffer; -import java.text.Normalizer; -import java.util.Iterator; -import java.util.Map; - -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.schema.ColumnMetadata; - -public abstract class AbstractAnalyzer implements Iterator -{ - protected ByteBuffer next = null; - - public ByteBuffer next() - { - return next; - } - - public void remove() - { - throw new UnsupportedOperationException(); - } - - public void validate(Map options, ColumnMetadata cm) throws ConfigurationException - { - if (!isCompatibleWith(cm.type)) - throw new ConfigurationException(String.format("%s does not support type %s", - this.getClass().getSimpleName(), - cm.type.asCQL3Type())); - } - - public abstract void init(Map options, AbstractType validator); - - public abstract void reset(ByteBuffer input); - - /** - * Test whether the given validator is compatible with the underlying analyzer. - * - * @param validator the validator to test the compatibility with - * @return true if the give validator is compatible, false otherwise - */ - protected abstract boolean isCompatibleWith(AbstractType validator); - - /** - * @return true if current analyzer provides text tokenization, false otherwise. - */ - public boolean isTokenizing() - { - return false; - } - - public static String normalize(String original) - { - return Normalizer.isNormalized(original, Normalizer.Form.NFC) - ? original - : Normalizer.normalize(original, Normalizer.Form.NFC); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzer.java deleted file mode 100644 index b7f297b774df..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzer.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.nio.CharBuffer; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import com.google.common.annotations.Beta; -import com.google.common.base.Preconditions; - -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.utils.AbstractIterator; - -@Beta -public class DelimiterAnalyzer extends AbstractAnalyzer -{ - - private static final Map, Charset> VALID_ANALYZABLE_TYPES = new HashMap, Charset>() - {{ - put(UTF8Type.instance, StandardCharsets.UTF_8); - put(AsciiType.instance, StandardCharsets.US_ASCII); - }}; - - private char delimiter; - private Charset charset; - private Iterator iter; - - public DelimiterAnalyzer() - { - } - - @Override - public ByteBuffer next() - { - return iter.next(); - } - - public void init(Map options, AbstractType validator) - { - DelimiterTokenizingOptions tokenizingOptions = DelimiterTokenizingOptions.buildFromMap(options); - delimiter = tokenizingOptions.getDelimiter(); - charset = VALID_ANALYZABLE_TYPES.get(validator); - } - - public boolean hasNext() - { - return iter.hasNext(); - } - - public void reset(ByteBuffer input) - { - Preconditions.checkNotNull(input); - final CharBuffer cb = charset.decode(input); - - this.iter = new AbstractIterator() { - protected ByteBuffer computeNext() { - - if (!cb.hasRemaining()) - return endOfData(); - - CharBuffer readahead = cb.duplicate(); - // loop until we see the next delimiter character, or reach end of data - boolean readaheadRemaining; - while ((readaheadRemaining = readahead.hasRemaining()) && readahead.get() != delimiter); - - char[] chars = new char[readahead.position() - cb.position() - (readaheadRemaining ? 1 : 0)]; - cb.get(chars); - Preconditions.checkState(!cb.hasRemaining() || cb.get() == delimiter); - - return 0 < chars.length - ? charset.encode(CharBuffer.wrap(chars)) - // blank partition keys not permitted, ref ConcurrentRadixTree.putIfAbsent(..) - : computeNext(); - } - }; - } - - @Override - public boolean isTokenizing() - { - return true; - } - - @Override - public boolean isCompatibleWith(AbstractType validator) - { - return VALID_ANALYZABLE_TYPES.containsKey(validator); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterTokenizingOptions.java b/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterTokenizingOptions.java deleted file mode 100644 index c2c8ef7d53a6..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterTokenizingOptions.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.util.Map; - -/** Simple tokenizer based on a specified delimiter (rather than whitespace). - */ -public class DelimiterTokenizingOptions -{ - public static final String DELIMITER = "delimiter"; - - private final char delimiter; - - private DelimiterTokenizingOptions(char delimiter) - { - this.delimiter = delimiter; - } - - char getDelimiter() - { - return delimiter; - } - - private static class OptionsBuilder - { - private char delimiter = ','; - - public DelimiterTokenizingOptions build() - { - return new DelimiterTokenizingOptions(delimiter); - } - } - - static DelimiterTokenizingOptions buildFromMap(Map optionsMap) - { - OptionsBuilder optionsBuilder = new OptionsBuilder(); - - for (Map.Entry entry : optionsMap.entrySet()) - { - switch (entry.getKey()) - { - case DELIMITER: - { - String value = entry.getValue(); - if (1 != value.length()) - throw new IllegalArgumentException(String.format("Only single character delimiters supported, was %s", value)); - - optionsBuilder.delimiter = entry.getValue().charAt(0); - break; - } - } - } - return optionsBuilder.build(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzer.java deleted file mode 100644 index 195ad2a7a284..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzer.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.nio.ByteBuffer; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.index.sasi.analyzer.filter.BasicResultFilters; -import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineBuilder; -import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineExecutor; -import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineTask; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.serializers.MarshalException; -import org.apache.cassandra.utils.ByteBufferUtil; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Analyzer that does *not* tokenize the input. Optionally will - * apply filters for the input output as defined in analyzers options - */ -public class NonTokenizingAnalyzer extends AbstractAnalyzer -{ - private static final Logger logger = LoggerFactory.getLogger(NonTokenizingAnalyzer.class); - - private static final Set> VALID_ANALYZABLE_TYPES = new HashSet>() - {{ - add(UTF8Type.instance); - add(AsciiType.instance); - }}; - - private AbstractType validator; - private NonTokenizingOptions options; - private FilterPipelineTask filterPipeline; - - private ByteBuffer input; - private boolean hasNext = false; - - @Override - public void validate(Map options, ColumnMetadata cm) throws ConfigurationException - { - super.validate(options, cm); - if (options.containsKey(NonTokenizingOptions.CASE_SENSITIVE) && - (options.containsKey(NonTokenizingOptions.NORMALIZE_LOWERCASE) - || options.containsKey(NonTokenizingOptions.NORMALIZE_UPPERCASE))) - throw new ConfigurationException("case_sensitive option cannot be specified together " + - "with either normalize_lowercase or normalize_uppercase"); - } - - public void init(Map options, AbstractType validator) - { - init(NonTokenizingOptions.buildFromMap(options), validator); - } - - public void init(NonTokenizingOptions tokenizerOptions, AbstractType validator) - { - this.validator = validator; - this.options = tokenizerOptions; - this.filterPipeline = getFilterPipeline(); - } - - public boolean hasNext() - { - // check that we know how to handle the input, otherwise bail - if (!VALID_ANALYZABLE_TYPES.contains(validator)) - return false; - - if (hasNext) - { - String inputStr; - - try - { - inputStr = validator.getString(input); - if (inputStr == null) - throw new MarshalException(String.format("'null' deserialized value for %s with %s", ByteBufferUtil.bytesToHex(input), validator)); - - Object pipelineRes = FilterPipelineExecutor.execute(filterPipeline, inputStr); - if (pipelineRes == null) - return false; - - next = validator.fromString(normalize((String) pipelineRes)); - return true; - } - catch (MarshalException e) - { - logger.error("Failed to deserialize value with " + validator, e); - return false; - } - finally - { - hasNext = false; - } - } - - return false; - } - - public void reset(ByteBuffer input) - { - this.next = null; - this.input = input; - this.hasNext = true; - } - - private FilterPipelineTask getFilterPipeline() - { - FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation()); - if (options.isCaseSensitive() && options.shouldLowerCaseOutput()) - builder = builder.add("to_lower", new BasicResultFilters.LowerCase()); - if (options.isCaseSensitive() && options.shouldUpperCaseOutput()) - builder = builder.add("to_upper", new BasicResultFilters.UpperCase()); - if (!options.isCaseSensitive()) - builder = builder.add("to_lower", new BasicResultFilters.LowerCase()); - return builder.build(); - } - - @Override - public boolean isCompatibleWith(AbstractType validator) - { - return VALID_ANALYZABLE_TYPES.contains(validator); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingOptions.java b/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingOptions.java deleted file mode 100644 index d7830287e438..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingOptions.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.util.Map; - -public class NonTokenizingOptions -{ - public static final String NORMALIZE_LOWERCASE = "normalize_lowercase"; - public static final String NORMALIZE_UPPERCASE = "normalize_uppercase"; - public static final String CASE_SENSITIVE = "case_sensitive"; - - private boolean caseSensitive; - private boolean upperCaseOutput; - private boolean lowerCaseOutput; - - public boolean isCaseSensitive() - { - return caseSensitive; - } - - public void setCaseSensitive(boolean caseSensitive) - { - this.caseSensitive = caseSensitive; - } - - public boolean shouldUpperCaseOutput() - { - return upperCaseOutput; - } - - public void setUpperCaseOutput(boolean upperCaseOutput) - { - this.upperCaseOutput = upperCaseOutput; - } - - public boolean shouldLowerCaseOutput() - { - return lowerCaseOutput; - } - - public void setLowerCaseOutput(boolean lowerCaseOutput) - { - this.lowerCaseOutput = lowerCaseOutput; - } - - public static class OptionsBuilder - { - private boolean caseSensitive = true; - private boolean upperCaseOutput = false; - private boolean lowerCaseOutput = false; - - public OptionsBuilder() - { - } - - public OptionsBuilder caseSensitive(boolean caseSensitive) - { - this.caseSensitive = caseSensitive; - return this; - } - - public OptionsBuilder upperCaseOutput(boolean upperCaseOutput) - { - this.upperCaseOutput = upperCaseOutput; - return this; - } - - public OptionsBuilder lowerCaseOutput(boolean lowerCaseOutput) - { - this.lowerCaseOutput = lowerCaseOutput; - return this; - } - - public NonTokenizingOptions build() - { - if (lowerCaseOutput && upperCaseOutput) - throw new IllegalArgumentException("Options to normalize terms cannot be " + - "both uppercase and lowercase at the same time"); - - NonTokenizingOptions options = new NonTokenizingOptions(); - options.setCaseSensitive(caseSensitive); - options.setUpperCaseOutput(upperCaseOutput); - options.setLowerCaseOutput(lowerCaseOutput); - return options; - } - } - - public static NonTokenizingOptions buildFromMap(Map optionsMap) - { - OptionsBuilder optionsBuilder = new OptionsBuilder(); - - for (Map.Entry entry : optionsMap.entrySet()) - { - switch (entry.getKey()) - { - case NORMALIZE_LOWERCASE: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.lowerCaseOutput(bool); - break; - } - case NORMALIZE_UPPERCASE: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.upperCaseOutput(bool); - break; - } - case CASE_SENSITIVE: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.caseSensitive(bool); - break; - } - } - } - return optionsBuilder.build(); - } - - public static NonTokenizingOptions getDefaultOptions() - { - return new OptionsBuilder() - .caseSensitive(true).lowerCaseOutput(false) - .upperCaseOutput(false) - .build(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/SUPPLEMENTARY.jflex-macro b/src/java/org/apache/cassandra/index/sasi/analyzer/SUPPLEMENTARY.jflex-macro deleted file mode 100644 index f5bf68e254b3..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/SUPPLEMENTARY.jflex-macro +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// Generated using ICU4J 52.1.0.0 -// by org.apache.lucene.analysis.icu.GenerateJFlexSupplementaryMacros - - -ALetterSupp = ( - ([\ud83b][\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]) - | ([\ud81a][\uDC00-\uDE38]) - | ([\ud81b][\uDF00-\uDF44\uDF50\uDF93-\uDF9F]) - | ([\ud835][\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]) - | ([\ud80d][\uDC00-\uDC2E]) - | ([\ud80c][\uDC00-\uDFFF]) - | ([\ud809][\uDC00-\uDC62]) - | ([\ud808][\uDC00-\uDF6E]) - | ([\ud805][\uDE80-\uDEAA]) - | ([\ud804][\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD83-\uDDB2\uDDC1-\uDDC4]) - | ([\ud801][\uDC00-\uDC9D]) - | ([\ud800][\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1E\uDF30-\uDF4A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]) - | ([\ud803][\uDC00-\uDC48]) - | ([\ud802][\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72]) -) -FormatSupp = ( - ([\ud804][\uDCBD]) - | ([\ud834][\uDD73-\uDD7A]) - | ([\udb40][\uDC01\uDC20-\uDC7F]) -) -NumericSupp = ( - ([\ud805][\uDEC0-\uDEC9]) - | ([\ud804][\uDC66-\uDC6F\uDCF0-\uDCF9\uDD36-\uDD3F\uDDD0-\uDDD9]) - | ([\ud835][\uDFCE-\uDFFF]) - | ([\ud801][\uDCA0-\uDCA9]) -) -ExtendSupp = ( - ([\ud81b][\uDF51-\uDF7E\uDF8F-\uDF92]) - | ([\ud805][\uDEAB-\uDEB7]) - | ([\ud804][\uDC00-\uDC02\uDC38-\uDC46\uDC80-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD80-\uDD82\uDDB3-\uDDC0]) - | ([\ud834][\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]) - | ([\ud800][\uDDFD]) - | ([\udb40][\uDD00-\uDDEF]) - | ([\ud802][\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F]) -) -KatakanaSupp = ( - ([\ud82c][\uDC00]) -) -MidLetterSupp = ( - [] -) -MidNumSupp = ( - [] -) -MidNumLetSupp = ( - [] -) -ExtendNumLetSupp = ( - [] -) -ExtendNumLetSupp = ( - [] -) -ComplexContextSupp = ( - [] -) -HanSupp = ( - ([\ud87e][\uDC00-\uDE1D]) - | ([\ud86b][\uDC00-\uDFFF]) - | ([\ud86a][\uDC00-\uDFFF]) - | ([\ud869][\uDC00-\uDED6\uDF00-\uDFFF]) - | ([\ud868][\uDC00-\uDFFF]) - | ([\ud86e][\uDC00-\uDC1D]) - | ([\ud86d][\uDC00-\uDF34\uDF40-\uDFFF]) - | ([\ud86c][\uDC00-\uDFFF]) - | ([\ud863][\uDC00-\uDFFF]) - | ([\ud862][\uDC00-\uDFFF]) - | ([\ud861][\uDC00-\uDFFF]) - | ([\ud860][\uDC00-\uDFFF]) - | ([\ud867][\uDC00-\uDFFF]) - | ([\ud866][\uDC00-\uDFFF]) - | ([\ud865][\uDC00-\uDFFF]) - | ([\ud864][\uDC00-\uDFFF]) - | ([\ud858][\uDC00-\uDFFF]) - | ([\ud859][\uDC00-\uDFFF]) - | ([\ud85a][\uDC00-\uDFFF]) - | ([\ud85b][\uDC00-\uDFFF]) - | ([\ud85c][\uDC00-\uDFFF]) - | ([\ud85d][\uDC00-\uDFFF]) - | ([\ud85e][\uDC00-\uDFFF]) - | ([\ud85f][\uDC00-\uDFFF]) - | ([\ud850][\uDC00-\uDFFF]) - | ([\ud851][\uDC00-\uDFFF]) - | ([\ud852][\uDC00-\uDFFF]) - | ([\ud853][\uDC00-\uDFFF]) - | ([\ud854][\uDC00-\uDFFF]) - | ([\ud855][\uDC00-\uDFFF]) - | ([\ud856][\uDC00-\uDFFF]) - | ([\ud857][\uDC00-\uDFFF]) - | ([\ud849][\uDC00-\uDFFF]) - | ([\ud848][\uDC00-\uDFFF]) - | ([\ud84b][\uDC00-\uDFFF]) - | ([\ud84a][\uDC00-\uDFFF]) - | ([\ud84d][\uDC00-\uDFFF]) - | ([\ud84c][\uDC00-\uDFFF]) - | ([\ud84f][\uDC00-\uDFFF]) - | ([\ud84e][\uDC00-\uDFFF]) - | ([\ud841][\uDC00-\uDFFF]) - | ([\ud840][\uDC00-\uDFFF]) - | ([\ud843][\uDC00-\uDFFF]) - | ([\ud842][\uDC00-\uDFFF]) - | ([\ud845][\uDC00-\uDFFF]) - | ([\ud844][\uDC00-\uDFFF]) - | ([\ud847][\uDC00-\uDFFF]) - | ([\ud846][\uDC00-\uDFFF]) -) -HiraganaSupp = ( - ([\ud83c][\uDE00]) - | ([\ud82c][\uDC01]) -) -SingleQuoteSupp = ( - [] -) -DoubleQuoteSupp = ( - [] -) -HebrewLetterSupp = ( - [] -) -RegionalIndicatorSupp = ( - ([\ud83c][\uDDE6-\uDDFF]) -) diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java deleted file mode 100644 index 1af070c543f6..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.cassandra.index.sasi.analyzer.filter.*; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.io.util.DataInputBuffer; -import org.apache.cassandra.utils.ByteBufferUtil; - -import com.google.common.annotations.VisibleForTesting; - -import com.carrotsearch.hppc.IntObjectMap; -import com.carrotsearch.hppc.IntObjectHashMap; - -public class StandardAnalyzer extends AbstractAnalyzer -{ - - private static final Set> VALID_ANALYZABLE_TYPES = new HashSet>() - { - { - add(UTF8Type.instance); - add(AsciiType.instance); - } - }; - - public enum TokenType - { - EOF(-1), - ALPHANUM(0), - NUM(6), - SOUTHEAST_ASIAN(9), - IDEOGRAPHIC(10), - HIRAGANA(11), - KATAKANA(12), - HANGUL(13); - - private static final IntObjectMap TOKENS = new IntObjectHashMap<>(); - - static - { - for (TokenType type : TokenType.values()) - TOKENS.put(type.value, type); - } - - public final int value; - - TokenType(int value) - { - this.value = value; - } - - public int getValue() - { - return value; - } - - public static TokenType fromValue(int val) - { - return TOKENS.get(val); - } - } - - private AbstractType validator; - - private StandardTokenizerInterface scanner; - private StandardTokenizerOptions options; - private FilterPipelineTask filterPipeline; - - protected Reader inputReader = null; - - public String getToken() - { - return scanner.getText(); - } - - public final boolean incrementToken() throws IOException - { - while(true) - { - TokenType currentTokenType = TokenType.fromValue(scanner.getNextToken()); - if (currentTokenType == TokenType.EOF) - return false; - if (scanner.yylength() <= options.getMaxTokenLength() - && scanner.yylength() >= options.getMinTokenLength()) - return true; - } - } - - protected String getFilteredCurrentToken() throws IOException - { - String token = getToken(); - Object pipelineRes; - - while (true) - { - pipelineRes = FilterPipelineExecutor.execute(filterPipeline, token); - if (pipelineRes != null) - break; - - boolean reachedEOF = incrementToken(); - if (!reachedEOF) - break; - - token = getToken(); - } - - return (String) pipelineRes; - } - - private FilterPipelineTask getFilterPipeline() - { - FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation()); - if (!options.isCaseSensitive() && options.shouldLowerCaseTerms()) - builder = builder.add("to_lower", new BasicResultFilters.LowerCase()); - if (!options.isCaseSensitive() && options.shouldUpperCaseTerms()) - builder = builder.add("to_upper", new BasicResultFilters.UpperCase()); - if (options.shouldIgnoreStopTerms()) - builder = builder.add("skip_stop_words", new StopWordFilters.DefaultStopWordFilter(options.getLocale())); - if (options.shouldStemTerms()) - builder = builder.add("term_stemming", new StemmingFilters.DefaultStemmingFilter(options.getLocale())); - return builder.build(); - } - - public void init(Map options, AbstractType validator) - { - init(StandardTokenizerOptions.buildFromMap(options), validator); - } - - @VisibleForTesting - protected void init(StandardTokenizerOptions options) - { - init(options, UTF8Type.instance); - } - - public void init(StandardTokenizerOptions tokenizerOptions, AbstractType validator) - { - this.validator = validator; - this.options = tokenizerOptions; - this.filterPipeline = getFilterPipeline(); - - Reader reader = new InputStreamReader(new DataInputBuffer(ByteBufferUtil.EMPTY_BYTE_BUFFER, false), StandardCharsets.UTF_8); - this.scanner = new StandardTokenizerImpl(reader); - this.inputReader = reader; - } - - public boolean hasNext() - { - try - { - if (incrementToken()) - { - if (getFilteredCurrentToken() != null) - { - this.next = validator.fromString(normalize(getFilteredCurrentToken())); - return true; - } - } - } - catch (IOException e) - {} - - return false; - } - - public void reset(ByteBuffer input) - { - this.next = null; - Reader reader = new InputStreamReader(new DataInputBuffer(input, false), StandardCharsets.UTF_8); - scanner.yyreset(reader); - this.inputReader = reader; - } - - @VisibleForTesting - public void reset(InputStream input) - { - this.next = null; - Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8); - scanner.yyreset(reader); - this.inputReader = reader; - } - - @Override - public boolean isTokenizing() - { - return true; - } - - @Override - public boolean isCompatibleWith(AbstractType validator) - { - return VALID_ANALYZABLE_TYPES.contains(validator); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex deleted file mode 100644 index 86c645101dd9..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex +++ /dev/null @@ -1,220 +0,0 @@ -package org.apache.cassandra.index.sasi.analyzer; - -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.util.Arrays; - -/** - * This class implements Word Break rules from the Unicode Text Segmentation - * algorithm, as specified in - * Unicode Standard Annex #29. - *

    - * Tokens produced are of the following types: - *

      - *
    • <ALPHANUM>: A sequence of alphabetic and numeric characters
    • - *
    • <NUM>: A number
    • - *
    • <SOUTHEAST_ASIAN>: A sequence of characters from South and Southeast - * Asian languages, including Thai, Lao, Myanmar, and Khmer
    • - *
    • <IDEOGRAPHIC>: A single CJKV ideographic character
    • - *
    • <HIRAGANA>: A single hiragana character
    • - *
    • <KATAKANA>: A sequence of katakana characters
    • - *
    • <HANGUL>: A sequence of Hangul characters
    • - *
    - */ -%% - -%unicode 6.3 -%integer -%final -%public -%class StandardTokenizerImpl -%implements StandardTokenizerInterface -%function getNextToken -%char -%buffer 4096 - -%include SUPPLEMENTARY.jflex-macro -ALetter = (\p{WB:ALetter} | {ALetterSupp}) -Format = (\p{WB:Format} | {FormatSupp}) -Numeric = ([\p{WB:Numeric}[\p{Blk:HalfAndFullForms}&&\p{Nd}]] | {NumericSupp}) -Extend = (\p{WB:Extend} | {ExtendSupp}) -Katakana = (\p{WB:Katakana} | {KatakanaSupp}) -MidLetter = (\p{WB:MidLetter} | {MidLetterSupp}) -MidNum = (\p{WB:MidNum} | {MidNumSupp}) -MidNumLet = (\p{WB:MidNumLet} | {MidNumLetSupp}) -ExtendNumLet = (\p{WB:ExtendNumLet} | {ExtendNumLetSupp}) -ComplexContext = (\p{LB:Complex_Context} | {ComplexContextSupp}) -Han = (\p{Script:Han} | {HanSupp}) -Hiragana = (\p{Script:Hiragana} | {HiraganaSupp}) -SingleQuote = (\p{WB:Single_Quote} | {SingleQuoteSupp}) -DoubleQuote = (\p{WB:Double_Quote} | {DoubleQuoteSupp}) -HebrewLetter = (\p{WB:Hebrew_Letter} | {HebrewLetterSupp}) -RegionalIndicator = (\p{WB:Regional_Indicator} | {RegionalIndicatorSupp}) -HebrewOrALetter = ({HebrewLetter} | {ALetter}) - -// UAX#29 WB4. X (Extend | Format)* --> X -// -HangulEx = [\p{Script:Hangul}&&[\p{WB:ALetter}\p{WB:Hebrew_Letter}]] ({Format} | {Extend})* -HebrewOrALetterEx = {HebrewOrALetter} ({Format} | {Extend})* -NumericEx = {Numeric} ({Format} | {Extend})* -KatakanaEx = {Katakana} ({Format} | {Extend})* -MidLetterEx = ({MidLetter} | {MidNumLet} | {SingleQuote}) ({Format} | {Extend})* -MidNumericEx = ({MidNum} | {MidNumLet} | {SingleQuote}) ({Format} | {Extend})* -ExtendNumLetEx = {ExtendNumLet} ({Format} | {Extend})* -HanEx = {Han} ({Format} | {Extend})* -HiraganaEx = {Hiragana} ({Format} | {Extend})* -SingleQuoteEx = {SingleQuote} ({Format} | {Extend})* -DoubleQuoteEx = {DoubleQuote} ({Format} | {Extend})* -HebrewLetterEx = {HebrewLetter} ({Format} | {Extend})* -RegionalIndicatorEx = {RegionalIndicator} ({Format} | {Extend})* - - -%{ - /** Alphanumeric sequences */ - public static final int WORD_TYPE = StandardAnalyzer.TokenType.ALPHANUM.value; - - /** Numbers */ - public static final int NUMERIC_TYPE = StandardAnalyzer.TokenType.NUM.value; - - /** - * Chars in class \p{Line_Break = Complex_Context} are from South East Asian - * scripts (Thai, Lao, Myanmar, Khmer, etc.). Sequences of these are kept - * together as as a single token rather than broken up, because the logic - * required to break them at word boundaries is too complex for UAX#29. - *

    - * See Unicode Line Breaking Algorithm: http://www.unicode.org/reports/tr14/#SA - */ - public static final int SOUTH_EAST_ASIAN_TYPE = StandardAnalyzer.TokenType.SOUTHEAST_ASIAN.value; - - public static final int IDEOGRAPHIC_TYPE = StandardAnalyzer.TokenType.IDEOGRAPHIC.value; - - public static final int HIRAGANA_TYPE = StandardAnalyzer.TokenType.HIRAGANA.value; - - public static final int KATAKANA_TYPE = StandardAnalyzer.TokenType.KATAKANA.value; - - public static final int HANGUL_TYPE = StandardAnalyzer.TokenType.HANGUL.value; - - public final long yychar() - { - return yychar; - } - - public String getText() - { - return String.valueOf(zzBuffer, zzStartRead, zzMarkedPos-zzStartRead); - } - - public char[] getArray() - { - return Arrays.copyOfRange(zzBuffer, zzStartRead, zzMarkedPos); - } - - public byte[] getBytes() - { - return getText().getBytes(); - } - -%} - -%% - -// UAX#29 WB1. sot ÷ -// WB2. ÷ eot -// -<> { return StandardAnalyzer.TokenType.EOF.value; } - -// UAX#29 WB8. Numeric × Numeric -// WB11. Numeric (MidNum | MidNumLet | Single_Quote) × Numeric -// WB12. Numeric × (MidNum | MidNumLet | Single_Quote) Numeric -// WB13a. (ALetter | Hebrew_Letter | Numeric | Katakana | ExtendNumLet) × ExtendNumLet -// WB13b. ExtendNumLet × (ALetter | Hebrew_Letter | Numeric | Katakana) -// -{ExtendNumLetEx}* {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )* {ExtendNumLetEx}* - { return NUMERIC_TYPE; } - -// subset of the below for typing purposes only! -{HangulEx}+ - { return HANGUL_TYPE; } - -{KatakanaEx}+ - { return KATAKANA_TYPE; } - -// UAX#29 WB5. (ALetter | Hebrew_Letter) × (ALetter | Hebrew_Letter) -// WB6. (ALetter | Hebrew_Letter) × (MidLetter | MidNumLet | Single_Quote) (ALetter | Hebrew_Letter) -// WB7. (ALetter | Hebrew_Letter) (MidLetter | MidNumLet | Single_Quote) × (ALetter | Hebrew_Letter) -// WB7a. Hebrew_Letter × Single_Quote -// WB7b. Hebrew_Letter × Double_Quote Hebrew_Letter -// WB7c. Hebrew_Letter Double_Quote × Hebrew_Letter -// WB9. (ALetter | Hebrew_Letter) × Numeric -// WB10. Numeric × (ALetter | Hebrew_Letter) -// WB13. Katakana × Katakana -// WB13a. (ALetter | Hebrew_Letter | Numeric | Katakana | ExtendNumLet) × ExtendNumLet -// WB13b. ExtendNumLet × (ALetter | Hebrew_Letter | Numeric | Katakana) -// -{ExtendNumLetEx}* ( {KatakanaEx} ( {ExtendNumLetEx}* {KatakanaEx} )* - | ( {HebrewLetterEx} ( {SingleQuoteEx} | {DoubleQuoteEx} {HebrewLetterEx} ) - | {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )* - | {HebrewOrALetterEx} ( ( {ExtendNumLetEx}* | {MidLetterEx} ) {HebrewOrALetterEx} )* - )+ - ) -({ExtendNumLetEx}+ ( {KatakanaEx} ( {ExtendNumLetEx}* {KatakanaEx} )* - | ( {HebrewLetterEx} ( {SingleQuoteEx} | {DoubleQuoteEx} {HebrewLetterEx} ) - | {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )* - | {HebrewOrALetterEx} ( ( {ExtendNumLetEx}* | {MidLetterEx} ) {HebrewOrALetterEx} )* - )+ - ) -)* -{ExtendNumLetEx}* - { return WORD_TYPE; } - - -// From UAX #29: -// -// [C]haracters with the Line_Break property values of Contingent_Break (CB), -// Complex_Context (SA/South East Asian), and XX (Unknown) are assigned word -// boundary property values based on criteria outside of the scope of this -// annex. That means that satisfactory treatment of languages like Chinese -// or Thai requires special handling. -// -// In Unicode 6.3, only one character has the \p{Line_Break = Contingent_Break} -// property: U+FFFC (  ) OBJECT REPLACEMENT CHARACTER. -// -// In the ICU implementation of UAX#29, \p{Line_Break = Complex_Context} -// character sequences (from South East Asian scripts like Thai, Myanmar, Khmer, -// Lao, etc.) are kept together. This grammar does the same below. -// -// See also the Unicode Line Breaking Algorithm: -// -// http://www.unicode.org/reports/tr14/#SA -// -{ComplexContext}+ { return SOUTH_EAST_ASIAN_TYPE; } - -// UAX#29 WB14. Any ÷ Any -// -{HanEx} { return IDEOGRAPHIC_TYPE; } -{HiraganaEx} { return HIRAGANA_TYPE; } - - -// UAX#29 WB3. CR × LF -// WB3a. (Newline | CR | LF) ÷ -// WB3b. ÷ (Newline | CR | LF) -// WB13c. Regional_Indicator × Regional_Indicator -// WB14. Any ÷ Any -// -{RegionalIndicatorEx} {RegionalIndicatorEx}+ | [^] - { /* Break so we don't hit fall-through warning: */ break; /* Not numeric, word, ideographic, hiragana, or SE Asian -- ignore it. */ } diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerInterface.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerInterface.java deleted file mode 100644 index c12d7a117ce8..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerInterface.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.io.IOException; -import java.io.Reader; - -/** - * Internal interface for supporting versioned grammars. - */ -public interface StandardTokenizerInterface -{ - - String getText(); - - char[] getArray(); - - byte[] getBytes(); - - /** - * Returns the current position. - */ - long yychar(); - - /** - * Returns the length of the matched text region. - */ - int yylength(); - - /** - * Resumes scanning until the next regular expression is matched, - * the end of input is encountered or an I/O-Error occurs. - * - * @return the next token, {@link StandardTokenizerImpl#YYEOF} on end of stream - * @exception java.io.IOException if any I/O-Error occurs - */ - int getNextToken() throws IOException; - - /** - * Resets the scanner to read from a new input stream. - * Does not close the old reader. - * - * All internal variables are reset, the old input stream - * cannot be reused (internal buffer is discarded and lost). - * Lexical state is set to ZZ_INITIAL. - * - * @param reader the new input stream - */ - void yyreset(Reader reader); -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerOptions.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerOptions.java deleted file mode 100644 index da44f0ad7bed..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerOptions.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer; - -import java.util.Locale; -import java.util.Map; - -/** - * Various options for controlling tokenization and enabling - * or disabling features - */ -public class StandardTokenizerOptions -{ - public static final String TOKENIZATION_ENABLE_STEMMING = "tokenization_enable_stemming"; - public static final String TOKENIZATION_SKIP_STOP_WORDS = "tokenization_skip_stop_words"; - public static final String TOKENIZATION_LOCALE = "tokenization_locale"; - public static final String TOKENIZATION_NORMALIZE_LOWERCASE = "tokenization_normalize_lowercase"; - public static final String TOKENIZATION_NORMALIZE_UPPERCASE = "tokenization_normalize_uppercase"; - - public static final int DEFAULT_MAX_TOKEN_LENGTH = 255; - public static final int DEFAULT_MIN_TOKEN_LENGTH = 0; - - private boolean stemTerms; - private boolean ignoreStopTerms; - private Locale locale; - private boolean caseSensitive; - private boolean allTermsToUpperCase; - private boolean allTermsToLowerCase; - private int minTokenLength; - private int maxTokenLength; - - public boolean shouldStemTerms() - { - return stemTerms; - } - - public void setStemTerms(boolean stemTerms) - { - this.stemTerms = stemTerms; - } - - public boolean shouldIgnoreStopTerms() - { - return ignoreStopTerms; - } - - public void setIgnoreStopTerms(boolean ignoreStopTerms) - { - this.ignoreStopTerms = ignoreStopTerms; - } - - public Locale getLocale() - { - return locale; - } - - public void setLocale(Locale locale) - { - this.locale = locale; - } - - public boolean isCaseSensitive() - { - return caseSensitive; - } - - public void setCaseSensitive(boolean caseSensitive) - { - this.caseSensitive = caseSensitive; - } - - public boolean shouldUpperCaseTerms() - { - return allTermsToUpperCase; - } - - public void setAllTermsToUpperCase(boolean allTermsToUpperCase) - { - this.allTermsToUpperCase = allTermsToUpperCase; - } - - public boolean shouldLowerCaseTerms() - { - return allTermsToLowerCase; - } - - public void setAllTermsToLowerCase(boolean allTermsToLowerCase) - { - this.allTermsToLowerCase = allTermsToLowerCase; - } - - public int getMinTokenLength() - { - return minTokenLength; - } - - public void setMinTokenLength(int minTokenLength) - { - this.minTokenLength = minTokenLength; - } - - public int getMaxTokenLength() - { - return maxTokenLength; - } - - public void setMaxTokenLength(int maxTokenLength) - { - this.maxTokenLength = maxTokenLength; - } - - public static class OptionsBuilder - { - private boolean stemTerms; - private boolean ignoreStopTerms; - private Locale locale; - private boolean caseSensitive; - private boolean allTermsToUpperCase; - private boolean allTermsToLowerCase; - private int minTokenLength = DEFAULT_MIN_TOKEN_LENGTH; - private int maxTokenLength = DEFAULT_MAX_TOKEN_LENGTH; - - public OptionsBuilder() - { - } - - public OptionsBuilder stemTerms(boolean stemTerms) - { - this.stemTerms = stemTerms; - return this; - } - - public OptionsBuilder ignoreStopTerms(boolean ignoreStopTerms) - { - this.ignoreStopTerms = ignoreStopTerms; - return this; - } - - public OptionsBuilder useLocale(Locale locale) - { - this.locale = locale; - return this; - } - - public OptionsBuilder caseSensitive(boolean caseSensitive) - { - this.caseSensitive = caseSensitive; - return this; - } - - public OptionsBuilder alwaysUpperCaseTerms(boolean allTermsToUpperCase) - { - this.allTermsToUpperCase = allTermsToUpperCase; - return this; - } - - public OptionsBuilder alwaysLowerCaseTerms(boolean allTermsToLowerCase) - { - this.allTermsToLowerCase = allTermsToLowerCase; - return this; - } - - /** - * Set the min allowed token length. Any token shorter - * than this is skipped. - */ - public OptionsBuilder minTokenLength(int minTokenLength) - { - if (minTokenLength < 1) - throw new IllegalArgumentException("minTokenLength must be greater than zero"); - this.minTokenLength = minTokenLength; - return this; - } - - /** - * Set the max allowed token length. Any token longer - * than this is skipped. - */ - public OptionsBuilder maxTokenLength(int maxTokenLength) - { - if (maxTokenLength < 1) - throw new IllegalArgumentException("maxTokenLength must be greater than zero"); - this.maxTokenLength = maxTokenLength; - return this; - } - - public StandardTokenizerOptions build() - { - if(allTermsToLowerCase && allTermsToUpperCase) - throw new IllegalArgumentException("Options to normalize terms cannot be " + - "both uppercase and lowercase at the same time"); - - StandardTokenizerOptions options = new StandardTokenizerOptions(); - options.setIgnoreStopTerms(ignoreStopTerms); - options.setStemTerms(stemTerms); - options.setLocale(locale); - options.setCaseSensitive(caseSensitive); - options.setAllTermsToLowerCase(allTermsToLowerCase); - options.setAllTermsToUpperCase(allTermsToUpperCase); - options.setMinTokenLength(minTokenLength); - options.setMaxTokenLength(maxTokenLength); - return options; - } - } - - public static StandardTokenizerOptions buildFromMap(Map optionsMap) - { - OptionsBuilder optionsBuilder = new OptionsBuilder(); - - for (Map.Entry entry : optionsMap.entrySet()) - { - switch(entry.getKey()) - { - case TOKENIZATION_ENABLE_STEMMING: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.stemTerms(bool); - break; - } - case TOKENIZATION_SKIP_STOP_WORDS: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.ignoreStopTerms(bool); - break; - } - case TOKENIZATION_LOCALE: - { - Locale locale = new Locale(entry.getValue()); - optionsBuilder = optionsBuilder.useLocale(locale); - break; - } - case TOKENIZATION_NORMALIZE_UPPERCASE: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.alwaysUpperCaseTerms(bool); - break; - } - case TOKENIZATION_NORMALIZE_LOWERCASE: - { - boolean bool = Boolean.parseBoolean(entry.getValue()); - optionsBuilder = optionsBuilder.alwaysLowerCaseTerms(bool); - break; - } - default: - { - } - } - } - return optionsBuilder.build(); - } - - public static StandardTokenizerOptions getDefaultOptions() - { - return new OptionsBuilder() - .ignoreStopTerms(true).alwaysLowerCaseTerms(true) - .stemTerms(false).useLocale(Locale.ENGLISH).build(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/BasicResultFilters.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/BasicResultFilters.java deleted file mode 100644 index 2b949b898b25..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/BasicResultFilters.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer.filter; - -import java.util.Locale; - -/** - * Basic/General Token Filters - */ -public class BasicResultFilters -{ - private static final Locale DEFAULT_LOCALE = Locale.getDefault(); - - public static class LowerCase extends FilterPipelineTask - { - private Locale locale; - - public LowerCase(Locale locale) - { - this.locale = locale; - } - - public LowerCase() - { - this.locale = DEFAULT_LOCALE; - } - - public String process(String input) throws Exception - { - return input.toLowerCase(locale); - } - } - - public static class UpperCase extends FilterPipelineTask - { - private Locale locale; - - public UpperCase(Locale locale) - { - this.locale = locale; - } - - public UpperCase() - { - this.locale = DEFAULT_LOCALE; - } - - public String process(String input) throws Exception - { - return input.toUpperCase(locale); - } - } - - public static class NoOperation extends FilterPipelineTask - { - public Object process(Object input) throws Exception - { - return input; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineExecutor.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineExecutor.java deleted file mode 100644 index 68c055e6c0cb..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineExecutor.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer.filter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Executes all linked Pipeline Tasks serially and returns - * output (if exists) from the executed logic - */ -public class FilterPipelineExecutor -{ - private static final Logger logger = LoggerFactory.getLogger(FilterPipelineExecutor.class); - - public static T execute(FilterPipelineTask task, T initialInput) - { - FilterPipelineTask taskPtr = task; - T result = initialInput; - try - { - while (true) - { - FilterPipelineTask taskGeneric = (FilterPipelineTask) taskPtr; - result = taskGeneric.process((F) result); - taskPtr = taskPtr.next; - if(taskPtr == null) - return result; - } - } - catch (Exception e) - { - logger.info("An unhandled exception to occurred while processing " + - "pipeline [{}]", task.getName(), e); - } - return null; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmerFactory.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmerFactory.java deleted file mode 100644 index 9786a86ae28f..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmerFactory.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer.filter; - -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.github.benmanes.caffeine.cache.CacheLoader; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.LoadingCache; -import org.apache.cassandra.concurrent.ImmediateExecutor; -import org.tartarus.snowball.SnowballStemmer; -import org.tartarus.snowball.ext.DanishStemmer; -import org.tartarus.snowball.ext.DutchStemmer; -import org.tartarus.snowball.ext.EnglishStemmer; -import org.tartarus.snowball.ext.FinnishStemmer; -import org.tartarus.snowball.ext.FrenchStemmer; -import org.tartarus.snowball.ext.GermanStemmer; -import org.tartarus.snowball.ext.HungarianStemmer; -import org.tartarus.snowball.ext.ItalianStemmer; -import org.tartarus.snowball.ext.NorwegianStemmer; -import org.tartarus.snowball.ext.PortugueseStemmer; -import org.tartarus.snowball.ext.RomanianStemmer; -import org.tartarus.snowball.ext.RussianStemmer; -import org.tartarus.snowball.ext.SpanishStemmer; -import org.tartarus.snowball.ext.SwedishStemmer; -import org.tartarus.snowball.ext.TurkishStemmer; - -/** - * Returns a SnowballStemmer instance appropriate for - * a given language - */ -public class StemmerFactory -{ - private static final Logger logger = LoggerFactory.getLogger(StemmerFactory.class); - private static final LoadingCache> STEMMER_CONSTRUCTOR_CACHE = Caffeine.newBuilder() - .executor(ImmediateExecutor.INSTANCE) - .build(new CacheLoader>() - { - public Constructor load(Class aClass) throws Exception - { - try - { - return aClass.getConstructor(); - } - catch (Exception e) - { - logger.error("Failed to get stemmer constructor", e); - } - return null; - } - }); - - private static final Map SUPPORTED_LANGUAGES; - - static - { - SUPPORTED_LANGUAGES = new HashMap<>(); - SUPPORTED_LANGUAGES.put("de", GermanStemmer.class); - SUPPORTED_LANGUAGES.put("da", DanishStemmer.class); - SUPPORTED_LANGUAGES.put("es", SpanishStemmer.class); - SUPPORTED_LANGUAGES.put("en", EnglishStemmer.class); - SUPPORTED_LANGUAGES.put("fl", FinnishStemmer.class); - SUPPORTED_LANGUAGES.put("fr", FrenchStemmer.class); - SUPPORTED_LANGUAGES.put("hu", HungarianStemmer.class); - SUPPORTED_LANGUAGES.put("it", ItalianStemmer.class); - SUPPORTED_LANGUAGES.put("nl", DutchStemmer.class); - SUPPORTED_LANGUAGES.put("no", NorwegianStemmer.class); - SUPPORTED_LANGUAGES.put("pt", PortugueseStemmer.class); - SUPPORTED_LANGUAGES.put("ro", RomanianStemmer.class); - SUPPORTED_LANGUAGES.put("ru", RussianStemmer.class); - SUPPORTED_LANGUAGES.put("sv", SwedishStemmer.class); - SUPPORTED_LANGUAGES.put("tr", TurkishStemmer.class); - } - - public static SnowballStemmer getStemmer(Locale locale) - { - if (locale == null) - return null; - - String rootLang = locale.getLanguage().substring(0, 2); - try - { - Class clazz = SUPPORTED_LANGUAGES.get(rootLang); - if(clazz == null) - return null; - Constructor ctor = STEMMER_CONSTRUCTOR_CACHE.get(clazz); - return (SnowballStemmer) ctor.newInstance(); - } - catch (Exception e) - { - logger.debug("Failed to create new SnowballStemmer instance " + - "for language [{}]", locale.getLanguage(), e); - } - return null; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmingFilters.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmingFilters.java deleted file mode 100644 index cb840a87058a..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmingFilters.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer.filter; - -import java.util.Locale; - -import org.tartarus.snowball.SnowballStemmer; - -/** - * Filters for performing Stemming on tokens - */ -public class StemmingFilters -{ - public static class DefaultStemmingFilter extends FilterPipelineTask - { - private SnowballStemmer stemmer; - - public DefaultStemmingFilter(Locale locale) - { - stemmer = StemmerFactory.getStemmer(locale); - } - - public String process(String input) throws Exception - { - if (input == null || stemmer == null) - return input; - stemmer.setCurrent(input); - return (stemmer.stem()) ? stemmer.getCurrent() : input; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFactory.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFactory.java deleted file mode 100644 index b85a36fe9390..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFactory.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer.filter; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.CompletionException; - -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.LoadingCache; - -import org.apache.cassandra.concurrent.ImmediateExecutor; -import org.apache.cassandra.io.util.File; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provides a list of Stop Words for a given language - */ -public class StopWordFactory -{ - private static final Logger logger = LoggerFactory.getLogger(StopWordFactory.class); - - private static final String DEFAULT_RESOURCE_EXT = "_ST.txt"; - private static final String DEFAULT_RESOURCE_PREFIX = StopWordFactory.class.getPackage() - .getName().replace(".", File.pathSeparator()); - private static final Set SUPPORTED_LANGUAGES = new HashSet<>( - Arrays.asList("ar","bg","cs","de","en","es","fi","fr","hi","hu","it", - "pl","pt","ro","ru","sv")); - - private static final LoadingCache> STOP_WORDS_CACHE = Caffeine.newBuilder() - .executor(ImmediateExecutor.INSTANCE) - .build(StopWordFactory::getStopWordsFromResource); - - public static Set getStopWordsForLanguage(Locale locale) - { - if (locale == null) - return null; - - String rootLang = locale.getLanguage().substring(0, 2); - try - { - return (!SUPPORTED_LANGUAGES.contains(rootLang)) ? null : STOP_WORDS_CACHE.get(rootLang); - } - catch (CompletionException e) - { - logger.error("Failed to populate Stop Words Cache for language [{}]", locale.getLanguage(), e); - return null; - } - } - - private static Set getStopWordsFromResource(String language) - { - Set stopWords = new HashSet<>(); - String resourceName = DEFAULT_RESOURCE_PREFIX + File.pathSeparator() + language + DEFAULT_RESOURCE_EXT; - try (InputStream is = StopWordFactory.class.getClassLoader().getResourceAsStream(resourceName); - BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) - { - String line; - while ((line = r.readLine()) != null) - { - //skip comments (lines starting with # char) - if(line.charAt(0) == '#') - continue; - stopWords.add(line.trim()); - } - } - catch (Exception e) - { - logger.error("Failed to retrieve Stop Terms resource for language [{}]", language, e); - } - return stopWords; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFilters.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFilters.java deleted file mode 100644 index 4ae849c1f41d..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFilters.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.analyzer.filter; - -import java.util.Locale; -import java.util.Set; - -/** - * Filter implementations for input matching Stop Words - */ -public class StopWordFilters -{ - public static class DefaultStopWordFilter extends FilterPipelineTask - { - private Set stopWords = null; - - public DefaultStopWordFilter(Locale locale) - { - this.stopWords = StopWordFactory.getStopWordsForLanguage(locale); - } - - public String process(String input) throws Exception - { - return (stopWords != null && stopWords.contains(input)) ? null : input; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java b/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java deleted file mode 100644 index afbeaaaf88d3..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf; - -import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicReference; - -import com.google.common.annotations.VisibleForTesting; - -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.rows.Cell; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sasi.conf.view.View; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.memory.IndexMemtable; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.plan.Expression.Op; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; -import org.apache.cassandra.io.sstable.Component; -import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.IndexMetadata; -import org.apache.cassandra.utils.FBUtilities; - -public class ColumnIndex -{ - private static final String FILE_NAME_FORMAT = "SI_%s.db"; - - private final AbstractType keyValidator; - - private final ColumnMetadata column; - private final Optional config; - - private final AtomicReference memtable; - private final ConcurrentMap pendingFlush = new ConcurrentHashMap<>(); - - private final IndexMode mode; - - private final Component component; - private final DataTracker tracker; - - private final boolean isTokenized; - - public ColumnIndex(AbstractType keyValidator, ColumnMetadata column, IndexMetadata metadata) - { - this.keyValidator = keyValidator; - this.column = column; - this.config = metadata == null ? Optional.empty() : Optional.of(metadata); - this.mode = IndexMode.getMode(column, config); - this.memtable = new AtomicReference<>(new IndexMemtable(this)); - this.tracker = new DataTracker(keyValidator, this); - this.component = Components.Types.SECONDARY_INDEX.createComponent(String.format(FILE_NAME_FORMAT, getIndexName())); - this.isTokenized = getAnalyzer().isTokenizing(); - } - - /** - * Initialize this column index with specific set of SSTables. - * - * @param sstables The sstables to be used by index initially. - * - * @return A collection of sstables which don't have this specific index attached to them. - */ - public Iterable init(Set sstables) - { - return tracker.update(Collections.emptySet(), sstables); - } - - public AbstractType keyValidator() - { - return keyValidator; - } - - public long index(DecoratedKey key, Row row) - { - return getCurrentMemtable().index(key, getValueOf(column, row, FBUtilities.nowInSeconds())); - } - - public void switchMemtable() - { - // discard current memtable with all of it's data, useful on truncate - memtable.set(new IndexMemtable(this)); - } - - public void switchMemtable(Memtable parent) - { - pendingFlush.putIfAbsent(parent, memtable.getAndSet(new IndexMemtable(this))); - } - - public void discardMemtable(Memtable parent) - { - pendingFlush.remove(parent); - } - - @VisibleForTesting - public IndexMemtable getCurrentMemtable() - { - return memtable.get(); - } - - @VisibleForTesting - public Collection getPendingMemtables() - { - return pendingFlush.values(); - } - - public RangeIterator searchMemtable(Expression e) - { - RangeIterator.Builder builder = new RangeUnionIterator.Builder<>(); - builder.add(getCurrentMemtable().search(e)); - for (IndexMemtable memtable : getPendingMemtables()) - builder.add(memtable.search(e)); - - return builder.build(); - } - - public void update(Collection oldSSTables, Collection newSSTables) - { - tracker.update(oldSSTables, newSSTables); - } - - public ColumnMetadata getDefinition() - { - return column; - } - - public AbstractType getValidator() - { - return column.cellValueType(); - } - - public Component getComponent() - { - return component; - } - - public IndexMode getMode() - { - return mode; - } - - public String getColumnName() - { - return column.name.toString(); - } - - public String getIndexName() - { - return config.isPresent() ? config.get().name : "undefined"; - } - - public AbstractAnalyzer getAnalyzer() - { - AbstractAnalyzer analyzer = mode.getAnalyzer(getValidator()); - analyzer.init(config.isPresent() ? config.get().options : Collections.emptyMap(), column.cellValueType()); - return analyzer; - } - - public View getView() - { - return tracker.getView(); - } - - public boolean hasSSTable(SSTableReader sstable) - { - return tracker.hasSSTable(sstable); - } - - public void dropData(Collection sstablesToRebuild) - { - tracker.dropData(sstablesToRebuild); - } - - public void dropData(long truncateUntil) - { - switchMemtable(); - tracker.dropData(truncateUntil); - } - - public boolean isIndexed() - { - return mode != IndexMode.NOT_INDEXED; - } - - public boolean isLiteral() - { - AbstractType validator = getValidator(); - return isIndexed() ? mode.isLiteral : (validator instanceof UTF8Type || validator instanceof AsciiType); - } - - public boolean supports(Operator op) - { - if (op == Operator.LIKE) - return isLiteral(); - - Op operator = Op.valueOf(op); - return !(isTokenized && operator == Op.EQ) // EQ is only applicable to non-tokenized indexes - && operator != Op.IN // IN operator is not supported - && !(isTokenized && mode.mode == OnDiskIndexBuilder.Mode.CONTAINS && operator == Op.PREFIX) // PREFIX not supported on tokenized CONTAINS mode indexes - && !(isLiteral() && operator == Op.RANGE) // RANGE only applicable to indexes non-literal indexes - && mode.supports(operator); // for all other cases let's refer to index itself - } - - public static ByteBuffer getValueOf(ColumnMetadata column, Row row, long nowInSecs) - { - if (row == null) - return null; - - switch (column.kind) - { - case CLUSTERING: - // skip indexing of static clustering when regular column is indexed - if (row.isStatic()) - return null; - - return row.clustering().bufferAt(column.position()); - - // treat static cell retrieval the same was as regular - // only if row kind is STATIC otherwise return null - case STATIC: - if (!row.isStatic()) - return null; - case REGULAR: - Cell cell = row.getCell(column); - return cell == null || !cell.isLive(nowInSecs) ? null : cell.buffer(); - - default: - return null; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java b/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java deleted file mode 100644 index 9b3e9ab8ff84..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.index.sasi.SSTableIndex; -import org.apache.cassandra.index.sasi.conf.view.View; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.utils.Pair; - -/** a pared-down version of DataTracker and DT.View. need one for each index of each column family */ -public class DataTracker -{ - private static final Logger logger = LoggerFactory.getLogger(DataTracker.class); - - private final AbstractType keyValidator; - private final ColumnIndex columnIndex; - private final AtomicReference view = new AtomicReference<>(); - - public DataTracker(AbstractType keyValidator, ColumnIndex index) - { - this.keyValidator = keyValidator; - this.columnIndex = index; - this.view.set(new View(index, Collections.emptySet())); - } - - public View getView() - { - return view.get(); - } - - /** - * Replaces old SSTables with new by creating new immutable tracker. - * - * @param oldSSTables A set of SSTables to remove. - * @param newSSTables A set of SSTables to add to tracker. - * - * @return A collection of SSTables which don't have component attached for current index. - */ - public Iterable update(Collection oldSSTables, Collection newSSTables) - { - final Pair, Set> built = getBuiltIndexes(newSSTables); - final Set newIndexes = built.left; - final Set indexedSSTables = built.right; - - View currentView, newView; - do - { - currentView = view.get(); - newView = new View(columnIndex, currentView.getIndexes(), oldSSTables, newIndexes); - } - while (!view.compareAndSet(currentView, newView)); - - for (SSTableReader sstable : indexedSSTables) - { - sstable.addComponents(Collections.singleton(columnIndex.getComponent())); - } - - return newSSTables.stream().filter(sstable -> !indexedSSTables.contains(sstable)).collect(Collectors.toList()); - } - - public boolean hasSSTable(SSTableReader sstable) - { - View currentView = view.get(); - for (SSTableIndex index : currentView) - { - if (index.getSSTable().equals(sstable)) - return true; - } - - return false; - } - - public void dropData(Collection sstablesToRebuild) - { - View currentView = view.get(); - if (currentView == null) - return; - - Set toRemove = new HashSet<>(sstablesToRebuild); - for (SSTableIndex index : currentView) - { - SSTableReader sstable = index.getSSTable(); - if (!sstablesToRebuild.contains(sstable)) - continue; - - index.markObsolete(); - } - - update(toRemove, Collections.emptyList()); - } - - public void dropData(long truncateUntil) - { - View currentView = view.get(); - if (currentView == null) - return; - - Set toRemove = new HashSet<>(); - for (SSTableIndex index : currentView) - { - SSTableReader sstable = index.getSSTable(); - if (sstable.getMaxTimestamp() > truncateUntil) - continue; - - index.markObsolete(); - toRemove.add(sstable); - } - - update(toRemove, Collections.emptyList()); - } - - private Pair, Set> getBuiltIndexes(Collection sstables) - { - Set indexes = new HashSet<>(sstables.size()); - Set builtSSTables = new HashSet<>(sstables.size()); - for (SSTableReader sstable : sstables) - { - if (sstable.isMarkedCompacted()) - continue; - - File indexFile = sstable.descriptor.fileFor(columnIndex.getComponent()); - if (!indexFile.exists()) - continue; - - // if the index file is empty, we have to ignore it to avoid re-building, but it doesn't take - // a part in query process - if (indexFile.length() == 0) - { - builtSSTables.add(sstable); - continue; - } - - SSTableIndex index = null; - - try - { - index = new SSTableIndex(columnIndex, indexFile, sstable); - - logger.info("SSTableIndex.open(column: {}, minTerm: {}, maxTerm: {}, minKey: {}, maxKey: {}, sstable: {})", - columnIndex.getColumnName(), - columnIndex.getValidator().getString(index.minTerm()), - columnIndex.getValidator().getString(index.maxTerm()), - keyValidator.getString(index.minKey()), - keyValidator.getString(index.maxKey()), - index.getSSTable()); - - // Try to add new index to the set, if set already has such index, we'll simply release and move on. - // This covers situation when sstable collection has the same sstable multiple - // times because we don't know what kind of collection it actually is. - if (indexes.add(index)) - builtSSTables.add(sstable); - else - index.release(); - } - catch (Throwable t) - { - logger.error("Can't open index file at " + indexFile.absolutePath() + ", skipping.", t); - if (index != null) - index.release(); - } - } - - return Pair.create(indexes, builtSSTables); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java deleted file mode 100644 index 7d4f5c9467d1..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf; - -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sasi.analyzer.NoOpAnalyzer; -import org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer; -import org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.index.sasi.plan.Expression.Op; -import org.apache.cassandra.schema.IndexMetadata; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IndexMode -{ - private static final Logger logger = LoggerFactory.getLogger(IndexMode.class); - - public static final IndexMode NOT_INDEXED = new IndexMode(Mode.PREFIX, true, false, NonTokenizingAnalyzer.class, 0); - - private static final Set> TOKENIZABLE_TYPES = new HashSet>() - {{ - add(UTF8Type.instance); - add(AsciiType.instance); - }}; - - private static final String INDEX_MODE_OPTION = "mode"; - private static final String INDEX_ANALYZED_OPTION = "analyzed"; - private static final String INDEX_ANALYZER_CLASS_OPTION = "analyzer_class"; - private static final String INDEX_IS_LITERAL_OPTION = "is_literal"; - private static final String INDEX_MAX_FLUSH_MEMORY_OPTION = "max_compaction_flush_memory_in_mb"; - private static final double INDEX_MAX_FLUSH_DEFAULT_MULTIPLIER = 0.15; - private static final long DEFAULT_MAX_MEM_BYTES = (long) (1073741824 * INDEX_MAX_FLUSH_DEFAULT_MULTIPLIER); // 1G default for memtable - - public final Mode mode; - public final boolean isAnalyzed, isLiteral; - public final Class analyzerClass; - public final long maxCompactionFlushMemoryInBytes; - - private IndexMode(Mode mode, boolean isLiteral, boolean isAnalyzed, Class analyzerClass, long maxMemBytes) - { - this.mode = mode; - this.isLiteral = isLiteral; - this.isAnalyzed = isAnalyzed; - this.analyzerClass = analyzerClass; - this.maxCompactionFlushMemoryInBytes = maxMemBytes; - } - - public AbstractAnalyzer getAnalyzer(AbstractType validator) - { - AbstractAnalyzer analyzer = new NoOpAnalyzer(); - - try - { - if (isAnalyzed) - { - if (analyzerClass != null) - analyzer = (AbstractAnalyzer) analyzerClass.newInstance(); - else if (TOKENIZABLE_TYPES.contains(validator)) - analyzer = new StandardAnalyzer(); - } - } - catch (InstantiationException | IllegalAccessException e) - { - logger.error("Failed to create new instance of analyzer with class [{}]", analyzerClass.getName(), e); - } - - return analyzer; - } - - public static void validateAnalyzer(Map indexOptions, ColumnMetadata cd) throws ConfigurationException - { - // 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))); - } - - AbstractAnalyzer analyzer; - try - { - analyzer = (AbstractAnalyzer) analyzerClass.newInstance(); - analyzer.validate(indexOptions, cd); - } - catch (InstantiationException | IllegalAccessException e) - { - throw new ConfigurationException(String.format("Unable to initialize analyzer class option specified [%s]", - analyzerClass.getSimpleName())); - } - } - } - - public static IndexMode getMode(ColumnMetadata column, Optional config) throws ConfigurationException - { - return getMode(column, config.isPresent() ? config.get().options : null); - } - - public static IndexMode getMode(ColumnMetadata column, Map indexOptions) throws ConfigurationException - { - if (indexOptions == null || indexOptions.isEmpty()) - return IndexMode.NOT_INDEXED; - - Mode mode; - - try - { - mode = indexOptions.get(INDEX_MODE_OPTION) == null - ? Mode.PREFIX - : Mode.mode(indexOptions.get(INDEX_MODE_OPTION)); - } - catch (IllegalArgumentException e) - { - throw new ConfigurationException("Incorrect index mode: " + indexOptions.get(INDEX_MODE_OPTION)); - } - - boolean isAnalyzed = false; - Class analyzerClass = null; - try - { - if (indexOptions.get(INDEX_ANALYZER_CLASS_OPTION) != null) - { - analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)); - isAnalyzed = indexOptions.get(INDEX_ANALYZED_OPTION) == null - ? true : Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_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 - { - String literalOption = indexOptions.get(INDEX_IS_LITERAL_OPTION); - AbstractType validator = column.cellValueType(); - - isLiteral = literalOption == null - ? (validator instanceof UTF8Type || validator instanceof AsciiType) - : Boolean.parseBoolean(literalOption); - } - catch (Exception e) - { - logger.error("failed to parse {} option, defaulting to 'false'.", INDEX_IS_LITERAL_OPTION); - } - - long maxMemBytes = indexOptions.get(INDEX_MAX_FLUSH_MEMORY_OPTION) == null - ? DEFAULT_MAX_MEM_BYTES - : 1048576L * Long.parseLong(indexOptions.get(INDEX_MAX_FLUSH_MEMORY_OPTION)); - - if (maxMemBytes > 100L * 1073741824) - { - logger.error("{} configured as {} is above 100GiB, reverting to default 1GB", INDEX_MAX_FLUSH_MEMORY_OPTION, maxMemBytes); - maxMemBytes = DEFAULT_MAX_MEM_BYTES; - } - return new IndexMode(mode, isLiteral, isAnalyzed, analyzerClass, maxMemBytes); - } - - public boolean supports(Op operator) - { - return mode.supports(operator); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/PrefixTermTree.java b/src/java/org/apache/cassandra/index/sasi/conf/view/PrefixTermTree.java deleted file mode 100644 index f7cd942d5c6c..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/view/PrefixTermTree.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf.view; - -import java.nio.ByteBuffer; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.cassandra.index.sasi.SSTableIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.utils.trie.KeyAnalyzer; -import org.apache.cassandra.index.sasi.utils.trie.PatriciaTrie; -import org.apache.cassandra.index.sasi.utils.trie.Trie; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.utils.Interval; -import org.apache.cassandra.utils.IntervalTree; - -import com.google.common.collect.Sets; - -/** - * This class is an extension over RangeTermTree for string terms, - * it is required because interval tree can't handle matching if search is on the - * prefix of min/max of the range, so for ascii/utf8 fields we build an additional - * prefix trie (including both min/max terms of the index) and do union of the results - * of the prefix tree search and results from the interval tree lookup. - */ -public class PrefixTermTree extends RangeTermTree -{ - private final OnDiskIndexBuilder.Mode mode; - private final Trie> trie; - - public PrefixTermTree(ByteBuffer min, ByteBuffer max, - Trie> trie, - IntervalTree> ranges, - OnDiskIndexBuilder.Mode mode, - AbstractType comparator) - { - super(min, max, ranges, comparator); - - this.mode = mode; - this.trie = trie; - } - - public Set search(Expression e) - { - Map> indexes = (e == null || e.lower == null || mode == OnDiskIndexBuilder.Mode.CONTAINS) - ? trie : trie.prefixMap(e.lower.value); - - Set view = new HashSet<>(indexes.size()); - indexes.values().forEach(view::addAll); - return Sets.union(view, super.search(e)); - } - - public static class Builder extends RangeTermTree.Builder - { - private final PatriciaTrie> trie; - - protected Builder(OnDiskIndexBuilder.Mode mode, final AbstractType comparator) - { - super(mode, comparator); - trie = new PatriciaTrie<>(new ByteBufferKeyAnalyzer(comparator)); - - } - - public void addIndex(SSTableIndex index) - { - super.addIndex(index); - addTerm(index.minTerm(), index); - addTerm(index.maxTerm(), index); - } - - public TermTree build() - { - return new PrefixTermTree(min, max, trie, IntervalTree.build(intervals), mode, comparator); - } - - private void addTerm(ByteBuffer term, SSTableIndex index) - { - Set indexes = trie.get(term); - if (indexes == null) - trie.put(term, (indexes = new HashSet<>())); - - indexes.add(index); - } - } - - private static class ByteBufferKeyAnalyzer implements KeyAnalyzer - { - private final AbstractType comparator; - - public ByteBufferKeyAnalyzer(AbstractType comparator) - { - this.comparator = comparator; - } - - /** - * A bit mask where the first bit is 1 and the others are zero - */ - private static final int MSB = 1 << Byte.SIZE-1; - - public int compare(ByteBuffer a, ByteBuffer b) - { - return comparator.compare(a, b); - } - - public int lengthInBits(ByteBuffer o) - { - return o.remaining() * Byte.SIZE; - } - - public boolean isBitSet(ByteBuffer key, int bitIndex) - { - if (bitIndex >= lengthInBits(key)) - return false; - - int index = bitIndex / Byte.SIZE; - int bit = bitIndex % Byte.SIZE; - return (key.get(index) & mask(bit)) != 0; - } - - public int bitIndex(ByteBuffer key, ByteBuffer otherKey) - { - int length = Math.max(key.remaining(), otherKey.remaining()); - - boolean allNull = true; - for (int i = 0; i < length; i++) - { - byte b1 = valueAt(key, i); - byte b2 = valueAt(otherKey, i); - - if (b1 != b2) - { - int xor = b1 ^ b2; - for (int j = 0; j < Byte.SIZE; j++) - { - if ((xor & mask(j)) != 0) - return (i * Byte.SIZE) + j; - } - } - - if (b1 != 0) - allNull = false; - } - - return allNull ? KeyAnalyzer.NULL_BIT_KEY : KeyAnalyzer.EQUAL_BIT_KEY; - } - - public boolean isPrefix(ByteBuffer key, ByteBuffer prefix) - { - if (key.remaining() < prefix.remaining()) - return false; - - for (int i = 0; i < prefix.remaining(); i++) - { - if (key.get(i) != prefix.get(i)) - return false; - } - - return true; - } - - /** - * Returns the {@code byte} value at the given index. - */ - private byte valueAt(ByteBuffer value, int index) - { - return index >= 0 && index < value.remaining() ? value.get(index) : 0; - } - - /** - * Returns a bit mask where the given bit is set - */ - private int mask(int bit) - { - return MSB >>> bit; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/RangeTermTree.java b/src/java/org/apache/cassandra/index/sasi/conf/view/RangeTermTree.java deleted file mode 100644 index d6b4551ea7bf..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/view/RangeTermTree.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf.view; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.apache.cassandra.index.sasi.SSTableIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.utils.Interval; -import org.apache.cassandra.utils.IntervalTree; - -public class RangeTermTree implements TermTree -{ - protected final ByteBuffer min, max; - protected final IntervalTree> rangeTree; - protected final AbstractType comparator; - - public RangeTermTree(ByteBuffer min, ByteBuffer max, IntervalTree> rangeTree, AbstractType comparator) - { - this.min = min; - this.max = max; - this.rangeTree = rangeTree; - this.comparator = comparator; - } - - public Set search(Expression e) - { - ByteBuffer minTerm = e.lower == null ? min : e.lower.value; - ByteBuffer maxTerm = e.upper == null ? max : e.upper.value; - - return new HashSet<>(rangeTree.search(Interval.create(new Term(minTerm, comparator), - new Term(maxTerm, comparator), - (SSTableIndex) null))); - } - - public int intervalCount() - { - return rangeTree.intervalCount(); - } - - static class Builder extends TermTree.Builder - { - protected final List> intervals = new ArrayList<>(); - - protected Builder(OnDiskIndexBuilder.Mode mode, AbstractType comparator) - { - super(mode, comparator); - } - - public void addIndex(SSTableIndex index) - { - intervals.add(Interval.create(new Term(index.minTerm(), comparator), - new Term(index.maxTerm(), comparator), index)); - } - - - public TermTree build() - { - return new RangeTermTree(min, max, IntervalTree.build(intervals), comparator); - } - } - - - /** - * This is required since IntervalTree doesn't support custom Comparator - * implementations and relied on items to be comparable which "raw" terms are not. - */ - protected static class Term implements Comparable - { - private final ByteBuffer term; - private final AbstractType comparator; - - public Term(ByteBuffer term, AbstractType comparator) - { - this.term = term; - this.comparator = comparator; - } - - public int compareTo(Term o) - { - return comparator.compare(term, o.term); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/TermTree.java b/src/java/org/apache/cassandra/index/sasi/conf/view/TermTree.java deleted file mode 100644 index a175e225682a..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/view/TermTree.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf.view; - -import java.nio.ByteBuffer; -import java.util.Set; - -import org.apache.cassandra.index.sasi.SSTableIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.db.marshal.AbstractType; - -public interface TermTree -{ - Set search(Expression e); - - int intervalCount(); - - abstract class Builder - { - protected final OnDiskIndexBuilder.Mode mode; - protected final AbstractType comparator; - protected ByteBuffer min, max; - - protected Builder(OnDiskIndexBuilder.Mode mode, AbstractType comparator) - { - this.mode = mode; - this.comparator = comparator; - } - - public final void add(SSTableIndex index) - { - addIndex(index); - - min = min == null || comparator.compare(min, index.minTerm()) > 0 ? index.minTerm() : min; - max = max == null || comparator.compare(max, index.maxTerm()) < 0 ? index.maxTerm() : max; - } - - protected abstract void addIndex(SSTableIndex index); - - public abstract TermTree build(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/View.java b/src/java/org/apache/cassandra/index/sasi/conf/view/View.java deleted file mode 100644 index b0afc5b5647a..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/conf/view/View.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.conf.view; - -import java.nio.ByteBuffer; -import java.util.*; -import java.util.stream.Collectors; - -import org.apache.cassandra.index.sasi.SSTableIndex; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.utils.Interval; -import org.apache.cassandra.utils.IntervalTree; - -import com.google.common.collect.Iterables; - -public class View implements Iterable -{ - private final Map view; - - private final TermTree termTree; - private final AbstractType keyValidator; - private final IntervalTree> keyIntervalTree; - - public View(ColumnIndex index, Set indexes) - { - this(index, Collections.emptyList(), Collections.emptyList(), indexes); - } - - public View(ColumnIndex index, - Collection currentView, - Collection oldSSTables, - Set newIndexes) - { - Map newView = new HashMap<>(); - - AbstractType validator = index.getValidator(); - TermTree.Builder termTreeBuilder = (validator instanceof AsciiType || validator instanceof UTF8Type) - ? new PrefixTermTree.Builder(index.getMode().mode, validator) - : new RangeTermTree.Builder(index.getMode().mode, validator); - - List> keyIntervals = new ArrayList<>(); - // Ensure oldSSTables and newIndexes are disjoint (in index redistribution case the intersection can be non-empty). - // also favor newIndexes over currentView in case an SSTable has been re-opened (also occurs during redistribution) - // See CASSANDRA-14055 - Collection toRemove = new HashSet<>(oldSSTables); - toRemove.removeAll(newIndexes.stream().map(SSTableIndex::getSSTable).collect(Collectors.toSet())); - for (SSTableIndex sstableIndex : Iterables.concat(newIndexes, currentView)) - { - SSTableReader sstable = sstableIndex.getSSTable(); - if (toRemove.contains(sstable) || sstable.isMarkedCompacted() || newView.containsKey(sstable.descriptor)) - { - sstableIndex.release(); - continue; - } - - newView.put(sstable.descriptor, sstableIndex); - - termTreeBuilder.add(sstableIndex); - keyIntervals.add(Interval.create(new Key(sstableIndex.minKey(), index.keyValidator()), - new Key(sstableIndex.maxKey(), index.keyValidator()), - sstableIndex)); - } - - this.view = newView; - this.termTree = termTreeBuilder.build(); - this.keyValidator = index.keyValidator(); - this.keyIntervalTree = IntervalTree.build(keyIntervals); - - if (keyIntervalTree.intervalCount() != termTree.intervalCount()) - throw new IllegalStateException(String.format("mismatched sizes for intervals tree for keys vs terms: %d != %d", keyIntervalTree.intervalCount(), termTree.intervalCount())); - } - - public Set match(Expression expression) - { - return termTree.search(expression); - } - - public List match(ByteBuffer minKey, ByteBuffer maxKey) - { - return keyIntervalTree.search(Interval.create(new Key(minKey, keyValidator), new Key(maxKey, keyValidator), (SSTableIndex) null)); - } - - public Iterator iterator() - { - return view.values().iterator(); - } - - public Collection getIndexes() - { - return view.values(); - } - - /** - * This is required since IntervalTree doesn't support custom Comparator - * implementations and relied on items to be comparable which "raw" keys are not. - */ - private static class Key implements Comparable - { - private final ByteBuffer key; - private final AbstractType comparator; - - public Key(ByteBuffer key, AbstractType comparator) - { - this.key = key; - this.comparator = comparator; - } - - public int compareTo(Key o) - { - return comparator.compare(key, o.key); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/AbstractTokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/AbstractTokenTreeBuilder.java deleted file mode 100644 index ae1024fb5b16..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/AbstractTokenTreeBuilder.java +++ /dev/null @@ -1,681 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.disk; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; - -import com.carrotsearch.hppc.LongArrayList; -import com.carrotsearch.hppc.LongSet; -import com.carrotsearch.hppc.cursors.LongCursor; - -public abstract class AbstractTokenTreeBuilder implements TokenTreeBuilder -{ - protected int numBlocks; - protected Node root; - protected InteriorNode rightmostParent; - protected Leaf leftmostLeaf; - protected Leaf rightmostLeaf; - protected long tokenCount = 0; - protected long treeMinToken; - protected long treeMaxToken; - - public void add(TokenTreeBuilder other) - { - add(other.iterator()); - } - - public TokenTreeBuilder finish() - { - if (root == null) - constructTree(); - - return this; - } - - public long getTokenCount() - { - return tokenCount; - } - - public int serializedSize() - { - if (numBlocks == 1) - return BLOCK_HEADER_BYTES + - ((int) tokenCount * BLOCK_ENTRY_BYTES) + - (((Leaf) root).overflowCollisionCount() * OVERFLOW_ENTRY_BYTES); - else - return numBlocks * BLOCK_BYTES; - } - - public void write(DataOutputPlus out) throws IOException - { - ByteBuffer blockBuffer = ByteBuffer.allocate(BLOCK_BYTES); - Iterator levelIterator = root.levelIterator(); - long childBlockIndex = 1; - - while (levelIterator != null) - { - Node firstChild = null; - while (levelIterator.hasNext()) - { - Node block = levelIterator.next(); - - if (firstChild == null && !block.isLeaf()) - firstChild = ((InteriorNode) block).children.get(0); - - if (block.isSerializable()) - { - block.serialize(childBlockIndex, blockBuffer); - flushBuffer(blockBuffer, out, numBlocks != 1); - } - - childBlockIndex += block.childCount(); - } - - levelIterator = (firstChild == null) ? null : firstChild.levelIterator(); - } - } - - protected abstract void constructTree(); - - protected void flushBuffer(ByteBuffer buffer, DataOutputPlus o, boolean align) throws IOException - { - // seek to end of last block before flushing - if (align) - alignBuffer(buffer, BLOCK_BYTES); - - buffer.flip(); - o.write(buffer); - buffer.clear(); - } - - protected abstract class Node - { - protected InteriorNode parent; - protected Node next; - protected Long nodeMinToken, nodeMaxToken; - - public Node(Long minToken, Long maxToken) - { - nodeMinToken = minToken; - nodeMaxToken = maxToken; - } - - public abstract boolean isSerializable(); - public abstract void serialize(long childBlockIndex, ByteBuffer buf); - public abstract int childCount(); - public abstract int tokenCount(); - - public Long smallestToken() - { - return nodeMinToken; - } - - public Long largestToken() - { - return nodeMaxToken; - } - - public Iterator levelIterator() - { - return new LevelIterator(this); - } - - public boolean isLeaf() - { - return (this instanceof Leaf); - } - - protected boolean isLastLeaf() - { - return this == rightmostLeaf; - } - - protected boolean isRoot() - { - return this == root; - } - - protected void updateTokenRange(long token) - { - nodeMinToken = nodeMinToken == null ? token : Math.min(nodeMinToken, token); - nodeMaxToken = nodeMaxToken == null ? token : Math.max(nodeMaxToken, token); - } - - protected void serializeHeader(ByteBuffer buf) - { - Header header; - if (isRoot()) - header = new RootHeader(); - else if (!isLeaf()) - header = new InteriorNodeHeader(); - else - header = new LeafHeader(); - - header.serialize(buf); - alignBuffer(buf, BLOCK_HEADER_BYTES); - } - - private abstract class Header - { - public void serialize(ByteBuffer buf) - { - buf.put(infoByte()) - .putShort((short) (tokenCount())) - .putLong(nodeMinToken) - .putLong(nodeMaxToken); - } - - protected abstract byte infoByte(); - } - - private class RootHeader extends Header - { - public void serialize(ByteBuffer buf) - { - super.serialize(buf); - writeMagic(buf); - buf.putLong(tokenCount) - .putLong(treeMinToken) - .putLong(treeMaxToken); - } - - protected byte infoByte() - { - // if leaf, set leaf indicator and last leaf indicator (bits 0 & 1) - // if not leaf, clear both bits - return (byte) ((isLeaf()) ? 3 : 0); - } - - protected void writeMagic(ByteBuffer buf) - { - switch (Descriptor.CURRENT_VERSION) - { - case Descriptor.VERSION_AB: - buf.putShort(AB_MAGIC); - break; - - default: - break; - } - - } - } - - private class InteriorNodeHeader extends Header - { - // bit 0 (leaf indicator) & bit 1 (last leaf indicator) cleared - protected byte infoByte() - { - return 0; - } - } - - private class LeafHeader extends Header - { - // bit 0 set as leaf indicator - // bit 1 set if this is last leaf of data - protected byte infoByte() - { - byte infoByte = 1; - infoByte |= (isLastLeaf()) ? (1 << LAST_LEAF_SHIFT) : 0; - - return infoByte; - } - } - - } - - protected abstract class Leaf extends Node - { - protected LongArrayList overflowCollisions; - - public Leaf(Long minToken, Long maxToken) - { - super(minToken, maxToken); - } - - public int childCount() - { - return 0; - } - - public int overflowCollisionCount() { - return overflowCollisions == null ? 0 : overflowCollisions.size(); - } - - protected void serializeOverflowCollisions(ByteBuffer buf) - { - if (overflowCollisions != null) - for (LongCursor offset : overflowCollisions) - buf.putLong(offset.value); - } - - public void serialize(long childBlockIndex, ByteBuffer buf) - { - serializeHeader(buf); - serializeData(buf); - serializeOverflowCollisions(buf); - } - - protected abstract void serializeData(ByteBuffer buf); - - protected LeafEntry createEntry(final long tok, final LongSet offsets) - { - int offsetCount = offsets.size(); - switch (offsetCount) - { - case 0: - throw new AssertionError("no offsets for token " + tok); - case 1: - long offset = offsets.toArray()[0]; - if (offset > MAX_OFFSET) - throw new AssertionError("offset " + offset + " cannot be greater than " + MAX_OFFSET); - else if (offset <= Integer.MAX_VALUE) - return new SimpleLeafEntry(tok, offset); - else - return new FactoredOffsetLeafEntry(tok, offset); - case 2: - long[] rawOffsets = offsets.toArray(); - if (rawOffsets[0] <= Integer.MAX_VALUE && rawOffsets[1] <= Integer.MAX_VALUE && - (rawOffsets[0] <= Short.MAX_VALUE || rawOffsets[1] <= Short.MAX_VALUE)) - return new PackedCollisionLeafEntry(tok, rawOffsets); - else - return createOverflowEntry(tok, offsetCount, offsets); - default: - return createOverflowEntry(tok, offsetCount, offsets); - } - } - - private LeafEntry createOverflowEntry(final long tok, final int offsetCount, final LongSet offsets) - { - if (overflowCollisions == null) - overflowCollisions = new LongArrayList(); - - LeafEntry entry = new OverflowCollisionLeafEntry(tok, (short) overflowCollisions.size(), (short) offsetCount); - for (LongCursor o : offsets) - { - if (overflowCollisions.size() == OVERFLOW_TRAILER_CAPACITY) - throw new AssertionError("cannot have more than " + OVERFLOW_TRAILER_CAPACITY + " overflow collisions per leaf"); - else - overflowCollisions.add(o.value); - } - return entry; - } - - protected abstract class LeafEntry - { - protected final long token; - - abstract public EntryType type(); - abstract public int offsetData(); - abstract public short offsetExtra(); - - public LeafEntry(final long tok) - { - token = tok; - } - - public void serialize(ByteBuffer buf) - { - buf.putShort((short) type().ordinal()) - .putShort(offsetExtra()) - .putLong(token) - .putInt(offsetData()); - } - - } - - - // assumes there is a single offset and the offset is <= Integer.MAX_VALUE - protected class SimpleLeafEntry extends LeafEntry - { - private final long offset; - - public SimpleLeafEntry(final long tok, final long off) - { - super(tok); - offset = off; - } - - public EntryType type() - { - return EntryType.SIMPLE; - } - - public int offsetData() - { - return (int) offset; - } - - public short offsetExtra() - { - return 0; - } - } - - // assumes there is a single offset and Integer.MAX_VALUE < offset <= MAX_OFFSET - // take the middle 32 bits of offset (or the top 32 when considering offset is max 48 bits) - // and store where offset is normally stored. take bottom 16 bits of offset and store in entry header - private class FactoredOffsetLeafEntry extends LeafEntry - { - private final long offset; - - public FactoredOffsetLeafEntry(final long tok, final long off) - { - super(tok); - offset = off; - } - - public EntryType type() - { - return EntryType.FACTORED; - } - - public int offsetData() - { - return (int) (offset >>> Short.SIZE); - } - - public short offsetExtra() - { - // exta offset is supposed to be an unsigned 16-bit integer - return (short) offset; - } - } - - // holds an entry with two offsets that can be packed in an int & a short - // the int offset is stored where offset is normally stored. short offset is - // stored in entry header - private class PackedCollisionLeafEntry extends LeafEntry - { - private short smallerOffset; - private int largerOffset; - - public PackedCollisionLeafEntry(final long tok, final long[] offs) - { - super(tok); - - smallerOffset = (short) Math.min(offs[0], offs[1]); - largerOffset = (int) Math.max(offs[0], offs[1]); - } - - public EntryType type() - { - return EntryType.PACKED; - } - - public int offsetData() - { - return largerOffset; - } - - public short offsetExtra() - { - return smallerOffset; - } - } - - // holds an entry with three or more offsets, or two offsets that cannot - // be packed into an int & a short. the index into the overflow list - // is stored where the offset is normally stored. the number of overflowed offsets - // for the entry is stored in the entry header - private class OverflowCollisionLeafEntry extends LeafEntry - { - private final short startIndex; - private final short count; - - public OverflowCollisionLeafEntry(final long tok, final short collisionStartIndex, final short collisionCount) - { - super(tok); - startIndex = collisionStartIndex; - count = collisionCount; - } - - public EntryType type() - { - return EntryType.OVERFLOW; - } - - public int offsetData() - { - return startIndex; - } - - public short offsetExtra() - { - return count; - } - - } - - } - - protected class InteriorNode extends Node - { - protected List tokens = new ArrayList<>(TOKENS_PER_BLOCK); - protected List children = new ArrayList<>(TOKENS_PER_BLOCK + 1); - protected int position = 0; - - public InteriorNode() - { - super(null, null); - } - - public boolean isSerializable() - { - return true; - } - - public void serialize(long childBlockIndex, ByteBuffer buf) - { - serializeHeader(buf); - serializeTokens(buf); - serializeChildOffsets(childBlockIndex, buf); - } - - public int childCount() - { - return children.size(); - } - - public int tokenCount() - { - return tokens.size(); - } - - public Long smallestToken() - { - return tokens.get(0); - } - - protected void add(Long token, InteriorNode leftChild, InteriorNode rightChild) - { - int pos = tokens.size(); - if (pos == TOKENS_PER_BLOCK) - { - InteriorNode sibling = split(); - sibling.add(token, leftChild, rightChild); - - } - else - { - if (leftChild != null) - children.add(pos, leftChild); - - if (rightChild != null) - { - children.add(pos + 1, rightChild); - rightChild.parent = this; - } - - updateTokenRange(token); - tokens.add(pos, token); - } - } - - protected void add(Leaf node) - { - - if (position == (TOKENS_PER_BLOCK + 1)) - { - rightmostParent = split(); - rightmostParent.add(node); - } - else - { - - node.parent = this; - children.add(position, node); - position++; - - // the first child is referenced only during bulk load. we don't take a value - // to store into the tree, one is subtracted since position has already been incremented - // for the next node to be added - if (position - 1 == 0) - return; - - - // tokens are inserted one behind the current position, but 2 is subtracted because - // position has already been incremented for the next add - Long smallestToken = node.smallestToken(); - updateTokenRange(smallestToken); - tokens.add(position - 2, smallestToken); - } - - } - - protected InteriorNode split() - { - Pair splitResult = splitBlock(); - Long middleValue = splitResult.left; - InteriorNode sibling = splitResult.right; - InteriorNode leftChild = null; - - // create a new root if necessary - if (parent == null) - { - parent = new InteriorNode(); - root = parent; - sibling.parent = parent; - leftChild = this; - numBlocks++; - } - - parent.add(middleValue, leftChild, sibling); - - return sibling; - } - - protected Pair splitBlock() - { - final int splitPosition = TOKENS_PER_BLOCK - 2; - InteriorNode sibling = new InteriorNode(); - sibling.parent = parent; - next = sibling; - - Long middleValue = tokens.get(splitPosition); - - for (int i = splitPosition; i < TOKENS_PER_BLOCK; i++) - { - if (i != TOKENS_PER_BLOCK && i != splitPosition) - { - long token = tokens.get(i); - sibling.updateTokenRange(token); - sibling.tokens.add(token); - } - - Node child = children.get(i + 1); - child.parent = sibling; - sibling.children.add(child); - sibling.position++; - } - - for (int i = TOKENS_PER_BLOCK; i >= splitPosition; i--) - { - if (i != TOKENS_PER_BLOCK) - tokens.remove(i); - - if (i != splitPosition) - children.remove(i); - } - - nodeMinToken = smallestToken(); - nodeMaxToken = tokens.get(tokens.size() - 1); - numBlocks++; - - return Pair.create(middleValue, sibling); - } - - protected boolean isFull() - { - return (position >= TOKENS_PER_BLOCK + 1); - } - - private void serializeTokens(ByteBuffer buf) - { - tokens.forEach(buf::putLong); - } - - private void serializeChildOffsets(long childBlockIndex, ByteBuffer buf) - { - for (int i = 0; i < children.size(); i++) - buf.putLong((childBlockIndex + i) * BLOCK_BYTES); - } - } - - public static class LevelIterator extends AbstractIterator - { - private Node currentNode; - - LevelIterator(Node first) - { - currentNode = first; - } - - public Node computeNext() - { - if (currentNode == null) - return endOfData(); - - Node returnNode = currentNode; - currentNode = returnNode.next; - - return returnNode; - } - } - - - protected static void alignBuffer(ByteBuffer buffer, int blockSize) - { - long curPos = buffer.position(); - if ((curPos & (blockSize - 1)) != 0) // align on the block boundary if needed - buffer.position((int) FBUtilities.align(curPos, blockSize)); - } - -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/Descriptor.java b/src/java/org/apache/cassandra/index/sasi/disk/Descriptor.java deleted file mode 100644 index 3aa6f14a4edd..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/Descriptor.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -/** - * Object descriptor for SASIIndex files. Similar to, and based upon, the sstable descriptor. - */ -public class Descriptor -{ - public static final String VERSION_AA = "aa"; - public static final String VERSION_AB = "ab"; - public static final String CURRENT_VERSION = VERSION_AB; - public static final Descriptor CURRENT = new Descriptor(CURRENT_VERSION); - - public static class Version - { - public final String version; - - public Version(String version) - { - this.version = version; - } - - public String toString() - { - return version; - } - } - - public final Version version; - - public Descriptor(String v) - { - this.version = new Version(v); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/DynamicTokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/DynamicTokenTreeBuilder.java deleted file mode 100644 index 0e906e20d02f..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/DynamicTokenTreeBuilder.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.nio.ByteBuffer; -import java.util.*; - -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.Pair; - -import com.carrotsearch.hppc.LongHashSet; -import com.carrotsearch.hppc.LongSet; -import com.carrotsearch.hppc.cursors.LongCursor; - -public class DynamicTokenTreeBuilder extends AbstractTokenTreeBuilder -{ - private final SortedMap tokens = new TreeMap<>(); - - - public DynamicTokenTreeBuilder() - {} - - public DynamicTokenTreeBuilder(TokenTreeBuilder data) - { - add(data); - } - - public DynamicTokenTreeBuilder(SortedMap data) - { - add(data); - } - - public void add(Long token, long keyPosition) - { - LongSet found = tokens.get(token); - if (found == null) - tokens.put(token, (found = new LongHashSet(2))); - - found.add(keyPosition); - } - - public void add(Iterator> data) - { - while (data.hasNext()) - { - Pair entry = data.next(); - for (LongCursor l : entry.right) - add(entry.left, l.value); - } - } - - public void add(SortedMap data) - { - for (Map.Entry newEntry : data.entrySet()) - { - LongSet found = tokens.get(newEntry.getKey()); - if (found == null) - tokens.put(newEntry.getKey(), (found = new LongHashSet(4))); - - for (LongCursor offset : newEntry.getValue()) - found.add(offset.value); - } - } - - public Iterator> iterator() - { - final Iterator> iterator = tokens.entrySet().iterator(); - return new AbstractIterator>() - { - protected Pair computeNext() - { - if (!iterator.hasNext()) - return endOfData(); - - Map.Entry entry = iterator.next(); - return Pair.create(entry.getKey(), entry.getValue()); - } - }; - } - - public boolean isEmpty() - { - return tokens.size() == 0; - } - - protected void constructTree() - { - tokenCount = tokens.size(); - treeMinToken = tokens.firstKey(); - treeMaxToken = tokens.lastKey(); - numBlocks = 1; - - // special case the tree that only has a single block in it (so we don't create a useless root) - if (tokenCount <= TOKENS_PER_BLOCK) - { - leftmostLeaf = new DynamicLeaf(tokens); - rightmostLeaf = leftmostLeaf; - root = leftmostLeaf; - } - else - { - root = new InteriorNode(); - rightmostParent = (InteriorNode) root; - - int i = 0; - Leaf lastLeaf = null; - Long firstToken = tokens.firstKey(); - Long finalToken = tokens.lastKey(); - Long lastToken; - for (Long token : tokens.keySet()) - { - if (i == 0 || (i % TOKENS_PER_BLOCK != 0 && i != (tokenCount - 1))) - { - i++; - continue; - } - - lastToken = token; - Leaf leaf = (i != (tokenCount - 1) || token.equals(finalToken)) ? - new DynamicLeaf(tokens.subMap(firstToken, lastToken)) : new DynamicLeaf(tokens.tailMap(firstToken)); - - if (i == TOKENS_PER_BLOCK) - leftmostLeaf = leaf; - else - lastLeaf.next = leaf; - - rightmostParent.add(leaf); - lastLeaf = leaf; - rightmostLeaf = leaf; - firstToken = lastToken; - i++; - numBlocks++; - - if (token.equals(finalToken)) - { - Leaf finalLeaf = new DynamicLeaf(tokens.tailMap(token)); - lastLeaf.next = finalLeaf; - rightmostParent.add(finalLeaf); - rightmostLeaf = finalLeaf; - numBlocks++; - } - } - - } - } - - private class DynamicLeaf extends Leaf - { - private final SortedMap tokens; - - DynamicLeaf(SortedMap data) - { - super(data.firstKey(), data.lastKey()); - tokens = data; - } - - public int tokenCount() - { - return tokens.size(); - } - - public boolean isSerializable() - { - return true; - } - - protected void serializeData(ByteBuffer buf) - { - for (Map.Entry entry : tokens.entrySet()) - createEntry(entry.getKey(), entry.getValue()).serialize(buf); - } - - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskBlock.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskBlock.java deleted file mode 100644 index 32cda53d7e4f..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskBlock.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sasi.Term; -import org.apache.cassandra.index.sasi.utils.MappedBuffer; -import org.apache.cassandra.db.marshal.AbstractType; - -public abstract class OnDiskBlock -{ - public enum BlockType - { - POINTER, DATA - } - - // this contains offsets of the terms and term data - protected final MappedBuffer blockIndex; - protected final int blockIndexSize; - - protected final boolean hasCombinedIndex; - protected final TokenTree combinedIndex; - - public OnDiskBlock(Descriptor descriptor, MappedBuffer block, BlockType blockType) - { - blockIndex = block; - - if (blockType == BlockType.POINTER) - { - hasCombinedIndex = false; - combinedIndex = null; - blockIndexSize = block.getInt() << 1; // num terms * sizeof(short) - return; - } - - long blockOffset = block.position(); - int combinedIndexOffset = block.getInt(blockOffset + OnDiskIndexBuilder.BLOCK_SIZE); - - hasCombinedIndex = (combinedIndexOffset >= 0); - long blockIndexOffset = blockOffset + OnDiskIndexBuilder.BLOCK_SIZE + 4 + combinedIndexOffset; - - combinedIndex = hasCombinedIndex ? new TokenTree(descriptor, blockIndex.duplicate().position(blockIndexOffset)) : null; - blockIndexSize = block.getInt() * 2; - } - - public SearchResult search(AbstractType comparator, ByteBuffer query) - { - int cmp = -1, start = 0, end = termCount() - 1, middle = 0; - - T element = null; - while (start <= end) - { - middle = start + ((end - start) >> 1); - element = getTerm(middle); - - cmp = element.compareTo(comparator, query); - if (cmp == 0) - return new SearchResult<>(element, cmp, middle); - else if (cmp < 0) - start = middle + 1; - else - end = middle - 1; - } - - return new SearchResult<>(element, cmp, middle); - } - - protected T getTerm(int index) - { - MappedBuffer dup = blockIndex.duplicate(); - long startsAt = getTermPosition(index); - if (termCount() - 1 == index) // last element - dup.position(startsAt); - else - dup.position(startsAt).limit(getTermPosition(index + 1)); - - return cast(dup); - } - - protected long getTermPosition(int idx) - { - return getTermPosition(blockIndex, idx, blockIndexSize); - } - - protected int termCount() - { - return blockIndexSize >> 1; - } - - protected abstract T cast(MappedBuffer data); - - static long getTermPosition(MappedBuffer data, int idx, int indexSize) - { - idx <<= 1; - assert idx < indexSize; - return data.position() + indexSize + data.getShort(data.position() + idx); - } - - public TokenTree getBlockIndex() - { - return combinedIndex; - } - - public int minOffset(OnDiskIndex.IteratorOrder order) - { - return order == OnDiskIndex.IteratorOrder.DESC ? 0 : termCount() - 1; - } - - public int maxOffset(OnDiskIndex.IteratorOrder order) - { - return minOffset(order) == 0 ? termCount() - 1 : 0; - } - - public static class SearchResult - { - public final T result; - public final int index, cmp; - - public SearchResult(T result, int cmp, int index) - { - this.result = result; - this.index = index; - this.cmp = cmp; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java deleted file mode 100644 index 0eab229556ed..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java +++ /dev/null @@ -1,816 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; -import java.util.stream.Collectors; - -import com.google.common.base.Function; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; -import com.google.common.collect.PeekingIterator; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.index.sasi.Term; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.plan.Expression.Op; -import org.apache.cassandra.index.sasi.utils.MappedBuffer; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.io.FSReadError; -import org.apache.cassandra.io.util.ChannelProxy; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileInputStreamPlus; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.utils.AbstractGuavaIterator; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.FBUtilities; - -import static org.apache.cassandra.index.sasi.disk.OnDiskBlock.SearchResult; - -public class OnDiskIndex implements Iterable, Closeable -{ - public enum IteratorOrder - { - DESC(1), ASC(-1); - - public final int step; - - IteratorOrder(int step) - { - this.step = step; - } - - public int startAt(OnDiskBlock block, Expression e) - { - switch (this) - { - case DESC: - return e.lower == null - ? 0 - : startAt(block.search(e.validator, e.lower.value), e.lower.inclusive); - - case ASC: - return e.upper == null - ? block.termCount() - 1 - : startAt(block.search(e.validator, e.upper.value), e.upper.inclusive); - - default: - throw new IllegalArgumentException("Unknown order: " + this); - } - } - - public int startAt(SearchResult found, boolean inclusive) - { - switch (this) - { - case DESC: - if (found.cmp < 0) - return found.index + 1; - - return inclusive || found.cmp != 0 ? found.index : found.index + 1; - - case ASC: - if (found.cmp < 0) // search term was bigger then whole data set - return found.index; - return inclusive && (found.cmp == 0 || found.cmp < 0) ? found.index : found.index - 1; - - default: - throw new IllegalArgumentException("Unknown order: " + this); - } - } - } - - public final Descriptor descriptor; - protected final OnDiskIndexBuilder.Mode mode; - protected final OnDiskIndexBuilder.TermSize termSize; - - protected final AbstractType comparator; - protected final MappedBuffer indexFile; - protected final long indexSize; - protected final boolean hasMarkedPartials; - - protected final Function keyFetcher; - - protected final String indexPath; - - protected final PointerLevel[] levels; - protected final DataLevel dataLevel; - - protected final ByteBuffer minTerm, maxTerm, minKey, maxKey; - - public OnDiskIndex(File index, AbstractType cmp, Function keyReader) - { - keyFetcher = keyReader; - - comparator = cmp; - indexPath = index.absolutePath(); - - - try (FileInputStreamPlus backingFile = new FileInputStreamPlus(index)) - { - descriptor = new Descriptor(backingFile.readUTF()); - - termSize = OnDiskIndexBuilder.TermSize.of(backingFile.readShort()); - - minTerm = ByteBufferUtil.readWithShortLength(backingFile); - maxTerm = ByteBufferUtil.readWithShortLength(backingFile); - - minKey = ByteBufferUtil.readWithShortLength(backingFile); - maxKey = ByteBufferUtil.readWithShortLength(backingFile); - - mode = OnDiskIndexBuilder.Mode.mode(backingFile.readUTF()); - hasMarkedPartials = backingFile.readBoolean(); - - FileChannel channel = index.newReadChannel(); - indexSize = channel.size(); - indexFile = new MappedBuffer(new ChannelProxy(index, channel)); - } - catch (IOException e) - { - throw new FSReadError(e, index); - } - - // start of the levels - indexFile.position(indexFile.getLong(indexSize - 8)); - - int numLevels = indexFile.getInt(); - levels = new PointerLevel[numLevels]; - for (int i = 0; i < levels.length; i++) - { - int blockCount = indexFile.getInt(); - levels[i] = new PointerLevel(indexFile.position(), blockCount); - indexFile.position(indexFile.position() + blockCount * 8); - } - - int blockCount = indexFile.getInt(); - dataLevel = new DataLevel(indexFile.position(), blockCount); - } - - public boolean hasMarkedPartials() - { - return hasMarkedPartials; - } - - public OnDiskIndexBuilder.Mode mode() - { - return mode; - } - - public ByteBuffer minTerm() - { - return minTerm; - } - - public ByteBuffer maxTerm() - { - return maxTerm; - } - - public ByteBuffer minKey() - { - return minKey; - } - - public ByteBuffer maxKey() - { - return maxKey; - } - - public DataTerm min() - { - return dataLevel.getBlock(0).getTerm(0); - } - - public DataTerm max() - { - DataBlock block = dataLevel.getBlock(dataLevel.blockCount - 1); - return block.getTerm(block.termCount() - 1); - } - - /** - * Search for rows which match all of the terms inside the given expression in the index file. - * - * @param exp The expression to use for the query. - * - * @return Iterator which contains rows for all of the terms from the given range. - */ - public RangeIterator search(Expression exp) - { - assert mode.supports(exp.getOp()); - - if (exp.getOp() == Expression.Op.PREFIX && mode == OnDiskIndexBuilder.Mode.CONTAINS && !hasMarkedPartials) - throw new UnsupportedOperationException("prefix queries in CONTAINS mode are not supported by this index"); - - // optimization in case single term is requested from index - // we don't really need to build additional union iterator - if (exp.getOp() == Op.EQ) - { - DataTerm term = getTerm(exp.lower.value); - return term == null ? null : term.getTokens(); - } - - // convert single NOT_EQ to range with exclusion - final Expression expression = (exp.getOp() != Op.NOT_EQ) - ? exp - : new Expression(exp).setOp(Op.RANGE) - .setLower(new Expression.Bound(minTerm, true)) - .setUpper(new Expression.Bound(maxTerm, true)) - .addExclusion(exp.lower.value); - - List exclusions = new ArrayList<>(expression.exclusions.size()); - - Iterables.addAll(exclusions, expression.exclusions.stream().filter(exclusion -> { - // accept only exclusions which are in the bounds of lower/upper - return !(expression.lower != null && comparator.compare(exclusion, expression.lower.value) < 0) - && !(expression.upper != null && comparator.compare(exclusion, expression.upper.value) > 0); - }).collect(Collectors.toList())); - - Collections.sort(exclusions, comparator); - - if (exclusions.size() == 0) - return searchRange(expression); - - List ranges = new ArrayList<>(exclusions.size()); - - // calculate range splits based on the sorted exclusions - Iterator exclusionsIterator = exclusions.iterator(); - - Expression.Bound min = expression.lower, max = null; - while (exclusionsIterator.hasNext()) - { - max = new Expression.Bound(exclusionsIterator.next(), false); - ranges.add(new Expression(expression).setOp(Op.RANGE).setLower(min).setUpper(max)); - min = max; - } - - assert max != null; - ranges.add(new Expression(expression).setOp(Op.RANGE).setLower(max).setUpper(expression.upper)); - - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - for (Expression e : ranges) - { - RangeIterator range = searchRange(e); - if (range != null) - builder.add(range); - } - - return builder.build(); - } - - private RangeIterator searchRange(Expression range) - { - Expression.Bound lower = range.lower; - Expression.Bound upper = range.upper; - - int lowerBlock = lower == null ? 0 : getDataBlock(lower.value); - int upperBlock = upper == null - ? dataLevel.blockCount - 1 - // optimization so we don't have to fetch upperBlock when query has lower == upper - : (lower != null && comparator.compare(lower.value, upper.value) == 0) ? lowerBlock : getDataBlock(upper.value); - - return (mode != OnDiskIndexBuilder.Mode.SPARSE || lowerBlock == upperBlock || upperBlock - lowerBlock <= 1) - ? searchPoint(lowerBlock, range) - : searchRange(lowerBlock, lower, upperBlock, upper); - } - - private RangeIterator searchRange(int lowerBlock, Expression.Bound lower, int upperBlock, Expression.Bound upper) - { - // if lower is at the beginning of the block that means we can just do a single iterator per block - SearchResult lowerPosition = (lower == null) ? null : searchIndex(lower.value, lowerBlock); - SearchResult upperPosition = (upper == null) ? null : searchIndex(upper.value, upperBlock); - - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - - // optimistically assume that first and last blocks are full block reads, saves at least 3 'else' conditions - int firstFullBlockIdx = lowerBlock, lastFullBlockIdx = upperBlock; - - // 'lower' doesn't cover the whole block so we need to do a partial iteration - // Two reasons why that can happen: - // - 'lower' is not the first element of the block - // - 'lower' is first element but it's not inclusive in the query - if (lowerPosition != null && (lowerPosition.index > 0 || !lower.inclusive)) - { - DataBlock block = dataLevel.getBlock(lowerBlock); - int start = (lower.inclusive || lowerPosition.cmp != 0) ? lowerPosition.index : lowerPosition.index + 1; - - builder.add(block.getRange(start, block.termCount())); - firstFullBlockIdx = lowerBlock + 1; - } - - if (upperPosition != null) - { - DataBlock block = dataLevel.getBlock(upperBlock); - int lastIndex = block.termCount() - 1; - - // The save as with 'lower' but here we need to check if the upper is the last element of the block, - // which means that we only have to get individual results if: - // - if it *is not* the last element, or - // - it *is* but shouldn't be included (dictated by upperInclusive) - if (upperPosition.index != lastIndex || !upper.inclusive) - { - int end = (upperPosition.cmp < 0 || (upperPosition.cmp == 0 && upper.inclusive)) - ? upperPosition.index + 1 : upperPosition.index; - - builder.add(block.getRange(0, end)); - lastFullBlockIdx = upperBlock - 1; - } - } - - int totalSuperBlocks = (lastFullBlockIdx - firstFullBlockIdx) / OnDiskIndexBuilder.SUPER_BLOCK_SIZE; - - // if there are no super-blocks, we can simply read all of the block iterators in sequence - if (totalSuperBlocks == 0) - { - for (int i = firstFullBlockIdx; i <= lastFullBlockIdx; i++) - builder.add(dataLevel.getBlock(i).getBlockIndex().iterator(keyFetcher)); - - return builder.build(); - } - - // first get all of the blocks which are aligned before the first super-block in the sequence, - // e.g. if the block range was (1, 9) and super-block-size = 4, we need to read 1, 2, 3, 4 - 7 is covered by - // super-block, 8, 9 is a remainder. - - int superBlockAlignedStart = firstFullBlockIdx == 0 ? 0 : (int) FBUtilities.align(firstFullBlockIdx, OnDiskIndexBuilder.SUPER_BLOCK_SIZE); - for (int blockIdx = firstFullBlockIdx; blockIdx < Math.min(superBlockAlignedStart, lastFullBlockIdx); blockIdx++) - builder.add(getBlockIterator(blockIdx)); - - // now read all of the super-blocks matched by the request, from the previous comment - // it's a block with index 1 (which covers everything from 4 to 7) - - int superBlockIdx = superBlockAlignedStart / OnDiskIndexBuilder.SUPER_BLOCK_SIZE; - for (int offset = 0; offset < totalSuperBlocks - 1; offset++) - builder.add(dataLevel.getSuperBlock(superBlockIdx++).iterator()); - - // now it's time for a remainder read, again from the previous example it's 8, 9 because - // we have over-shot previous block but didn't request enough to cover next super-block. - - int lastCoveredBlock = superBlockIdx * OnDiskIndexBuilder.SUPER_BLOCK_SIZE; - for (int offset = 0; offset <= (lastFullBlockIdx - lastCoveredBlock); offset++) - builder.add(getBlockIterator(lastCoveredBlock + offset)); - - return builder.build(); - } - - private RangeIterator searchPoint(int lowerBlock, Expression expression) - { - Iterator terms = new TermIterator(lowerBlock, expression, IteratorOrder.DESC); - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - - while (terms.hasNext()) - { - try - { - builder.add(terms.next().getTokens()); - } - finally - { - expression.checkpoint(); - } - } - - return builder.build(); - } - - private RangeIterator getBlockIterator(int blockIdx) - { - DataBlock block = dataLevel.getBlock(blockIdx); - return (block.hasCombinedIndex) - ? block.getBlockIndex().iterator(keyFetcher) - : block.getRange(0, block.termCount()); - } - - public Iterator iteratorAt(ByteBuffer query, IteratorOrder order, boolean inclusive) - { - Expression e = new Expression("", comparator); - Expression.Bound bound = new Expression.Bound(query, inclusive); - - switch (order) - { - case DESC: - e.setLower(bound); - break; - - case ASC: - e.setUpper(bound); - break; - - default: - throw new IllegalArgumentException("Unknown order: " + order); - } - - return new TermIterator(levels.length == 0 ? 0 : getBlockIdx(findPointer(query), query), e, order); - } - - private int getDataBlock(ByteBuffer query) - { - return levels.length == 0 ? 0 : getBlockIdx(findPointer(query), query); - } - - public Iterator iterator() - { - return new TermIterator(0, new Expression("", comparator), IteratorOrder.DESC); - } - - public void close() throws IOException - { - FileUtils.closeQuietly(indexFile); - } - - private PointerTerm findPointer(ByteBuffer query) - { - PointerTerm ptr = null; - for (PointerLevel level : levels) - { - if ((ptr = level.getPointer(ptr, query)) == null) - return null; - } - - return ptr; - } - - private DataTerm getTerm(ByteBuffer query) - { - SearchResult term = searchIndex(query, getDataBlock(query)); - return term.cmp == 0 ? term.result : null; - } - - private SearchResult searchIndex(ByteBuffer query, int blockIdx) - { - return dataLevel.getBlock(blockIdx).search(comparator, query); - } - - private int getBlockIdx(PointerTerm ptr, ByteBuffer query) - { - int blockIdx = 0; - if (ptr != null) - { - int cmp = ptr.compareTo(comparator, query); - blockIdx = (cmp == 0 || cmp > 0) ? ptr.getBlock() : ptr.getBlock() + 1; - } - - return blockIdx; - } - - protected class PointerLevel extends Level - { - public PointerLevel(long offset, int count) - { - super(offset, count); - } - - public PointerTerm getPointer(PointerTerm parent, ByteBuffer query) - { - return getBlock(getBlockIdx(parent, query)).search(comparator, query).result; - } - - protected PointerBlock cast(MappedBuffer block) - { - return new PointerBlock(block); - } - } - - protected class DataLevel extends Level - { - protected final int superBlockCnt; - protected final long superBlocksOffset; - - public DataLevel(long offset, int count) - { - super(offset, count); - long baseOffset = blockOffsets + blockCount * 8; - superBlockCnt = indexFile.getInt(baseOffset); - superBlocksOffset = baseOffset + 4; - } - - protected DataBlock cast(MappedBuffer block) - { - return new DataBlock(block); - } - - public OnDiskSuperBlock getSuperBlock(int idx) - { - assert idx < superBlockCnt : String.format("requested index %d is greater than super block count %d", idx, superBlockCnt); - long blockOffset = indexFile.getLong(superBlocksOffset + idx * 8); - return new OnDiskSuperBlock(indexFile.duplicate().position(blockOffset)); - } - } - - protected class OnDiskSuperBlock - { - private final TokenTree tokenTree; - - public OnDiskSuperBlock(MappedBuffer buffer) - { - tokenTree = new TokenTree(descriptor, buffer); - } - - public RangeIterator iterator() - { - return tokenTree.iterator(keyFetcher); - } - } - - protected abstract class Level - { - protected final long blockOffsets; - protected final int blockCount; - - public Level(long offsets, int count) - { - this.blockOffsets = offsets; - this.blockCount = count; - } - - public T getBlock(int idx) throws FSReadError - { - assert idx >= 0 && idx < blockCount; - - // calculate block offset and move there - // (long is intentional, we'll just need mmap implementation which supports long positions) - long blockOffset = indexFile.getLong(blockOffsets + idx * 8); - return cast(indexFile.duplicate().position(blockOffset)); - } - - protected abstract T cast(MappedBuffer block); - } - - protected class DataBlock extends OnDiskBlock - { - public DataBlock(MappedBuffer data) - { - super(descriptor, data, BlockType.DATA); - } - - protected DataTerm cast(MappedBuffer data) - { - return new DataTerm(data, termSize, getBlockIndex()); - } - - public RangeIterator getRange(int start, int end) - { - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - NavigableMap sparse = new TreeMap<>(); - - for (int i = start; i < end; i++) - { - DataTerm term = getTerm(i); - - if (term.isSparse()) - { - NavigableMap tokens = term.getSparseTokens(); - for (Map.Entry t : tokens.entrySet()) - { - Token token = sparse.get(t.getKey()); - if (token == null) - sparse.put(t.getKey(), t.getValue()); - else - token.merge(t.getValue()); - } - } - else - { - builder.add(term.getTokens()); - } - } - - PrefetchedTokensIterator prefetched = sparse.isEmpty() ? null : new PrefetchedTokensIterator(sparse); - - if (builder.rangeCount() == 0) - return prefetched; - - builder.add(prefetched); - return builder.build(); - } - } - - protected class PointerBlock extends OnDiskBlock - { - public PointerBlock(MappedBuffer block) - { - super(descriptor, block, BlockType.POINTER); - } - - protected PointerTerm cast(MappedBuffer data) - { - return new PointerTerm(data, termSize, hasMarkedPartials); - } - } - - public class DataTerm extends Term implements Comparable - { - private final TokenTree perBlockIndex; - - protected DataTerm(MappedBuffer content, OnDiskIndexBuilder.TermSize size, TokenTree perBlockIndex) - { - super(content, size, hasMarkedPartials); - this.perBlockIndex = perBlockIndex; - } - - public RangeIterator getTokens() - { - final long blockEnd = FBUtilities.align(content.position(), OnDiskIndexBuilder.BLOCK_SIZE); - - if (isSparse()) - return new PrefetchedTokensIterator(getSparseTokens()); - - long offset = blockEnd + 4 + content.getInt(getDataOffset() + 1); - return new TokenTree(descriptor, indexFile.duplicate().position(offset)).iterator(keyFetcher); - } - - public boolean isSparse() - { - return content.get(getDataOffset()) > 0; - } - - public NavigableMap getSparseTokens() - { - long ptrOffset = getDataOffset(); - - byte size = content.get(ptrOffset); - - assert size > 0; - - NavigableMap individualTokens = new TreeMap<>(); - for (int i = 0; i < size; i++) - { - Token token = perBlockIndex.get(content.getLong(ptrOffset + 1 + (8 * i)), keyFetcher); - - assert token != null; - individualTokens.put(token.get(), token); - } - - return individualTokens; - } - - public int compareTo(DataTerm other) - { - return other == null ? 1 : compareTo(comparator, other.getTerm()); - } - } - - protected static class PointerTerm extends Term - { - public PointerTerm(MappedBuffer content, OnDiskIndexBuilder.TermSize size, boolean hasMarkedPartials) - { - super(content, size, hasMarkedPartials); - } - - public int getBlock() - { - return content.getInt(getDataOffset()); - } - } - - private static class PrefetchedTokensIterator extends RangeIterator - { - private final NavigableMap tokens; - private PeekingIterator currentIterator; - - public PrefetchedTokensIterator(NavigableMap tokens) - { - super(tokens.firstKey(), tokens.lastKey(), tokens.size()); - this.tokens = tokens; - this.currentIterator = Iterators.peekingIterator(tokens.values().iterator()); - } - - protected Token computeNext() - { - return currentIterator != null && currentIterator.hasNext() - ? currentIterator.next() - : endOfData(); - } - - protected void performSkipTo(Long nextToken) - { - currentIterator = Iterators.peekingIterator(tokens.tailMap(nextToken, true).values().iterator()); - } - - public void close() throws IOException - { - endOfData(); - } - } - - public AbstractType getComparator() - { - return comparator; - } - - public String getIndexPath() - { - return indexPath; - } - - private class TermIterator extends AbstractGuavaIterator - { - private final Expression e; - private final IteratorOrder order; - - protected OnDiskBlock currentBlock; - protected int blockIndex, offset; - - private boolean checkLower = true, checkUpper = true; - - public TermIterator(int startBlock, Expression expression, IteratorOrder order) - { - this.e = expression; - this.order = order; - this.blockIndex = startBlock; - - nextBlock(); - } - - protected DataTerm computeNext() - { - for (;;) - { - if (currentBlock == null) - return endOfData(); - - if (offset >= 0 && offset < currentBlock.termCount()) - { - DataTerm currentTerm = currentBlock.getTerm(nextOffset()); - - // we need to step over all of the partial terms, in PREFIX mode, - // encountered by the query until upper-bound tells us to stop - if (e.getOp() == Op.PREFIX && currentTerm.isPartial()) - continue; - - // haven't reached the start of the query range yet, let's - // keep skip the current term until lower bound is satisfied - if (checkLower && !e.isLowerSatisfiedBy(currentTerm)) - continue; - - // flip the flag right on the first bounds match - // to avoid expensive comparisons - checkLower = false; - - if (checkUpper && !e.isUpperSatisfiedBy(currentTerm)) - return endOfData(); - - return currentTerm; - } - - nextBlock(); - } - } - - protected void nextBlock() - { - currentBlock = null; - - if (blockIndex < 0 || blockIndex >= dataLevel.blockCount) - return; - - currentBlock = dataLevel.getBlock(nextBlockIndex()); - offset = checkLower ? order.startAt(currentBlock, e) : currentBlock.minOffset(order); - - // let's check the last term of the new block right away - // if expression's upper bound is satisfied by it such means that we can avoid - // doing any expensive upper bound checks for that block. - checkUpper = e.hasUpper() && !e.isUpperSatisfiedBy(currentBlock.getTerm(currentBlock.maxOffset(order))); - } - - protected int nextBlockIndex() - { - int current = blockIndex; - blockIndex += order.step; - return current; - } - - protected int nextOffset() - { - int current = offset; - offset += order.step; - return current; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java deleted file mode 100644 index 9071e1088419..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java +++ /dev/null @@ -1,671 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.*; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sasi.plan.Expression.Op; -import org.apache.cassandra.index.sasi.sa.IndexedTerm; -import org.apache.cassandra.index.sasi.sa.IntegralSA; -import org.apache.cassandra.index.sasi.sa.SA; -import org.apache.cassandra.index.sasi.sa.TermIterator; -import org.apache.cassandra.index.sasi.sa.SuffixSA; -import org.apache.cassandra.db.marshal.*; -import org.apache.cassandra.io.FSWriteError; -import org.apache.cassandra.io.util.*; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; - -import com.carrotsearch.hppc.LongArrayList; -import com.carrotsearch.hppc.LongSet; -import com.carrotsearch.hppc.ShortArrayList; -import com.google.common.annotations.VisibleForTesting; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OnDiskIndexBuilder -{ - private static final Logger logger = LoggerFactory.getLogger(OnDiskIndexBuilder.class); - - public enum Mode - { - PREFIX(EnumSet.of(Op.EQ, Op.MATCH, Op.PREFIX, Op.NOT_EQ, Op.RANGE)), - CONTAINS(EnumSet.of(Op.EQ, Op.MATCH, Op.CONTAINS, Op.PREFIX, Op.SUFFIX, Op.NOT_EQ)), - SPARSE(EnumSet.of(Op.EQ, Op.NOT_EQ, Op.RANGE)); - - Set supportedOps; - - Mode(Set ops) - { - supportedOps = ops; - } - - public static Mode mode(String mode) - { - return Mode.valueOf(mode.toUpperCase()); - } - - public boolean supports(Op op) - { - return supportedOps.contains(op); - } - } - - public enum TermSize - { - INT(4), LONG(8), UUID(16), VARIABLE(-1); - - public final int size; - - TermSize(int size) - { - this.size = size; - } - - public boolean isConstant() - { - return this != VARIABLE; - } - - public static TermSize of(int size) - { - switch (size) - { - case -1: - return VARIABLE; - - case 4: - return INT; - - case 8: - return LONG; - - case 16: - return UUID; - - default: - throw new IllegalStateException("unknown state: " + size); - } - } - - public static TermSize sizeOf(AbstractType comparator) - { - if (comparator instanceof Int32Type || comparator instanceof FloatType) - return INT; - - if (comparator instanceof LongType || comparator instanceof DoubleType - || comparator instanceof TimestampType || comparator instanceof DateType) - return LONG; - - if (comparator instanceof TimeUUIDType || comparator instanceof UUIDType) - return UUID; - - return VARIABLE; - } - } - - public static final int BLOCK_SIZE = 4096; - public static final int MAX_TERM_SIZE = 1024; - public static final int SUPER_BLOCK_SIZE = 64; - public static final int IS_PARTIAL_BIT = 15; - - private static final SequentialWriterOption WRITER_OPTION = SequentialWriterOption.newBuilder() - .bufferSize(BLOCK_SIZE) - .build(); - - private final List> levels = new ArrayList<>(); - private MutableLevel dataLevel; - - private final TermSize termSize; - - private final AbstractType keyComparator, termComparator; - - private final Map terms; - private final Mode mode; - private final boolean marksPartials; - - private ByteBuffer minKey, maxKey; - private long estimatedBytes; - - public OnDiskIndexBuilder(AbstractType keyComparator, AbstractType comparator, Mode mode) - { - this(keyComparator, comparator, mode, true); - } - - public OnDiskIndexBuilder(AbstractType keyComparator, AbstractType comparator, Mode mode, boolean marksPartials) - { - this.keyComparator = keyComparator; - this.termComparator = comparator; - this.terms = new HashMap<>(); - this.termSize = TermSize.sizeOf(comparator); - this.mode = mode; - this.marksPartials = marksPartials; - } - - public OnDiskIndexBuilder add(ByteBuffer term, DecoratedKey key, long keyPosition) - { - if (term.remaining() >= MAX_TERM_SIZE) - { - logger.error("Rejecting value (value size {}, maximum size {}).", - FBUtilities.prettyPrintMemory(term.remaining()), - FBUtilities.prettyPrintMemory(Short.MAX_VALUE)); - return this; - } - - TokenTreeBuilder tokens = terms.get(term); - if (tokens == null) - { - terms.put(term, (tokens = new DynamicTokenTreeBuilder())); - - // on-heap size estimates from jol - // 64 bytes for TTB + 48 bytes for TreeMap in TTB + size bytes for the term (map key) - estimatedBytes += 64 + 48 + term.remaining(); - } - - tokens.add((Long) key.getToken().getTokenValue(), keyPosition); - - // calculate key range (based on actual key values) for current index - minKey = (minKey == null || keyComparator.compare(minKey, key.getKey()) > 0) ? key.getKey() : minKey; - maxKey = (maxKey == null || keyComparator.compare(maxKey, key.getKey()) < 0) ? key.getKey() : maxKey; - - // 60 ((boolean(1)*4) + (long(8)*4) + 24) bytes for the LongOpenHashSet created when the keyPosition was added - // + 40 bytes for the TreeMap.Entry + 8 bytes for the token (key). - // in the case of hash collision for the token we may overestimate but this is extremely rare - estimatedBytes += 60 + 40 + 8; - - return this; - } - - public long estimatedMemoryUse() - { - return estimatedBytes; - } - - private void addTerm(InMemoryDataTerm term, SequentialWriter out) throws IOException - { - InMemoryPointerTerm ptr = dataLevel.add(term); - if (ptr == null) - return; - - int levelIdx = 0; - for (;;) - { - MutableLevel level = getIndexLevel(levelIdx++, out); - if ((ptr = level.add(ptr)) == null) - break; - } - } - - public boolean isEmpty() - { - return terms.isEmpty(); - } - - public void finish(Pair range, File file, TermIterator terms) - { - finish(Descriptor.CURRENT, range, file, terms); - } - - /** - * Finishes up index building process by creating/populating index file. - * - * @param indexFile The file to write index contents to. - * - * @return true if index was written successfully, false otherwise (e.g. if index was empty). - * - * @throws FSWriteError on I/O error. - */ - public boolean finish(File indexFile) throws FSWriteError - { - return finish(Descriptor.CURRENT, indexFile); - } - - @VisibleForTesting - protected boolean finish(Descriptor descriptor, File file) throws FSWriteError - { - // no terms means there is nothing to build - if (terms.isEmpty()) - { - file.createFileIfNotExists(); - return false; - } - - // split terms into suffixes only if it's text, otherwise (even if CONTAINS is set) use terms in original form - SA sa = ((termComparator instanceof UTF8Type || termComparator instanceof AsciiType) && mode == Mode.CONTAINS) - ? new SuffixSA(termComparator, mode) : new IntegralSA(termComparator, mode); - - for (Map.Entry term : terms.entrySet()) - sa.add(term.getKey(), term.getValue()); - - finish(descriptor, Pair.create(minKey, maxKey), file, sa.finish()); - return true; - } - - protected void finish(Descriptor descriptor, Pair range, File file, TermIterator terms) - { - SequentialWriter out = null; - - try - { - out = new SequentialWriter(file, WRITER_OPTION); - - out.writeUTF(descriptor.version.toString()); - - out.writeShort(termSize.size); - - // min, max term (useful to find initial scan range from search expressions) - ByteBufferUtil.writeWithShortLength(terms.minTerm(), out); - ByteBufferUtil.writeWithShortLength(terms.maxTerm(), out); - - // min, max keys covered by index (useful when searching across multiple indexes) - ByteBufferUtil.writeWithShortLength(range.left, out); - ByteBufferUtil.writeWithShortLength(range.right, out); - - out.writeUTF(mode.toString()); - out.writeBoolean(marksPartials); - - out.skipBytes((int) (BLOCK_SIZE - out.position())); - - dataLevel = mode == Mode.SPARSE ? new DataBuilderLevel(out, new MutableDataBlock(termComparator, mode)) - : new MutableLevel<>(out, new MutableDataBlock(termComparator, mode)); - while (terms.hasNext()) - { - Pair term = terms.next(); - addTerm(new InMemoryDataTerm(term.left, term.right), out); - } - - dataLevel.finalFlush(); - for (MutableLevel l : levels) - l.flush(); // flush all of the buffers - - // and finally write levels index - final long levelIndexPosition = out.position(); - - out.writeInt(levels.size()); - for (int i = levels.size() - 1; i >= 0; i--) - levels.get(i).flushMetadata(); - - dataLevel.flushMetadata(); - - out.writeLong(levelIndexPosition); - - // sync contents of the output and disk, - // since it's not done implicitly on close - out.sync(); - } - catch (IOException e) - { - throw new FSWriteError(e, file); - } - finally - { - FileUtils.closeQuietly(out); - } - } - - private MutableLevel getIndexLevel(int idx, SequentialWriter out) - { - if (levels.size() == 0) - levels.add(new MutableLevel<>(out, new MutableBlock<>())); - - if (levels.size() - 1 < idx) - { - int toAdd = idx - (levels.size() - 1); - for (int i = 0; i < toAdd; i++) - levels.add(new MutableLevel<>(out, new MutableBlock<>())); - } - - return levels.get(idx); - } - - protected static void alignToBlock(SequentialWriter out) throws IOException - { - long endOfBlock = out.position(); - if ((endOfBlock & (BLOCK_SIZE - 1)) != 0) // align on the block boundary if needed - out.skipBytes((int) (FBUtilities.align(endOfBlock, BLOCK_SIZE) - endOfBlock)); - } - - private class InMemoryTerm - { - protected final IndexedTerm term; - - public InMemoryTerm(IndexedTerm term) - { - this.term = term; - } - - public int serializedSize() - { - return (termSize.isConstant() ? 0 : 2) + term.getBytes().remaining(); - } - - public void serialize(DataOutputPlus out) throws IOException - { - if (termSize.isConstant()) - { - out.write(term.getBytes()); - } - else - { - out.writeShort(term.getBytes().remaining() | ((marksPartials && term.isPartial() ? 1 : 0) << IS_PARTIAL_BIT)); - out.write(term.getBytes()); - } - - } - } - - private class InMemoryPointerTerm extends InMemoryTerm - { - protected final int blockCnt; - - public InMemoryPointerTerm(IndexedTerm term, int blockCnt) - { - super(term); - this.blockCnt = blockCnt; - } - - public int serializedSize() - { - return super.serializedSize() + 4; - } - - public void serialize(DataOutputPlus out) throws IOException - { - super.serialize(out); - out.writeInt(blockCnt); - } - } - - private class InMemoryDataTerm extends InMemoryTerm - { - private final TokenTreeBuilder keys; - - public InMemoryDataTerm(IndexedTerm term, TokenTreeBuilder keys) - { - super(term); - this.keys = keys; - } - } - - private class MutableLevel - { - private final LongArrayList blockOffsets = new LongArrayList(); - - protected final SequentialWriter out; - - private final MutableBlock inProcessBlock; - private InMemoryPointerTerm lastTerm; - - public MutableLevel(SequentialWriter out, MutableBlock block) - { - this.out = out; - this.inProcessBlock = block; - } - - /** - * @return If we flushed a block, return the last term of that block; else, null. - */ - public InMemoryPointerTerm add(T term) throws IOException - { - InMemoryPointerTerm toPromote = null; - - if (!inProcessBlock.hasSpaceFor(term)) - { - flush(); - toPromote = lastTerm; - } - - inProcessBlock.add(term); - - lastTerm = new InMemoryPointerTerm(term.term, blockOffsets.size()); - return toPromote; - } - - public void flush() throws IOException - { - blockOffsets.add(out.position()); - inProcessBlock.flushAndClear(out); - } - - public void finalFlush() throws IOException - { - flush(); - } - - public void flushMetadata() throws IOException - { - flushMetadata(blockOffsets); - } - - protected void flushMetadata(LongArrayList longArrayList) throws IOException - { - out.writeInt(longArrayList.size()); - for (int i = 0; i < longArrayList.size(); i++) - out.writeLong(longArrayList.get(i)); - } - } - - /** builds standard data blocks and super blocks, as well */ - private class DataBuilderLevel extends MutableLevel - { - private final LongArrayList superBlockOffsets = new LongArrayList(); - - /** count of regular data blocks written since current super block was init'd */ - private int dataBlocksCnt; - private TokenTreeBuilder superBlockTree; - - public DataBuilderLevel(SequentialWriter out, MutableBlock block) - { - super(out, block); - superBlockTree = new DynamicTokenTreeBuilder(); - } - - public InMemoryPointerTerm add(InMemoryDataTerm term) throws IOException - { - InMemoryPointerTerm ptr = super.add(term); - if (ptr != null) - { - dataBlocksCnt++; - flushSuperBlock(false); - } - superBlockTree.add(term.keys); - return ptr; - } - - public void flushSuperBlock(boolean force) throws IOException - { - if (dataBlocksCnt == SUPER_BLOCK_SIZE || (force && !superBlockTree.isEmpty())) - { - superBlockOffsets.add(out.position()); - superBlockTree.finish().write(out); - alignToBlock(out); - - dataBlocksCnt = 0; - superBlockTree = new DynamicTokenTreeBuilder(); - } - } - - public void finalFlush() throws IOException - { - super.flush(); - flushSuperBlock(true); - } - - public void flushMetadata() throws IOException - { - super.flushMetadata(); - flushMetadata(superBlockOffsets); - } - } - - private static class MutableBlock - { - protected final DataOutputBufferFixed buffer; - protected final ShortArrayList offsets; - - public MutableBlock() - { - buffer = new DataOutputBufferFixed(BLOCK_SIZE); - offsets = new ShortArrayList(); - } - - public final void add(T term) throws IOException - { - offsets.add((short) buffer.position()); - addInternal(term); - } - - protected void addInternal(T term) throws IOException - { - term.serialize(buffer); - } - - public boolean hasSpaceFor(T element) - { - return sizeAfter(element) < BLOCK_SIZE; - } - - protected int sizeAfter(T element) - { - return getWatermark() + 4 + element.serializedSize(); - } - - protected int getWatermark() - { - return 4 + offsets.size() * 2 + (int) buffer.position(); - } - - public void flushAndClear(SequentialWriter out) throws IOException - { - out.writeInt(offsets.size()); - for (int i = 0; i < offsets.size(); i++) - out.writeShort(offsets.get(i)); - - out.write(buffer.buffer()); - - alignToBlock(out); - - offsets.clear(); - buffer.clear(); - } - } - - private static class MutableDataBlock extends MutableBlock - { - private static final int MAX_KEYS_SPARSE = 5; - - private final AbstractType comparator; - private final Mode mode; - - private int offset = 0; - - private final List containers = new ArrayList<>(); - private TokenTreeBuilder combinedIndex; - - public MutableDataBlock(AbstractType comparator, Mode mode) - { - this.comparator = comparator; - this.mode = mode; - this.combinedIndex = initCombinedIndex(); - } - - protected void addInternal(InMemoryDataTerm term) throws IOException - { - TokenTreeBuilder keys = term.keys; - - if (mode == Mode.SPARSE) - { - if (keys.getTokenCount() > MAX_KEYS_SPARSE) - throw new IOException(String.format("Term - '%s' belongs to more than %d keys in %s mode, which is not allowed.", - comparator.getString(term.term.getBytes()), MAX_KEYS_SPARSE, mode.name())); - - writeTerm(term, keys); - } - else - { - writeTerm(term, offset); - - offset += keys.serializedSize(); - containers.add(keys); - } - - if (mode == Mode.SPARSE) - combinedIndex.add(keys); - } - - protected int sizeAfter(InMemoryDataTerm element) - { - return super.sizeAfter(element) + ptrLength(element); - } - - public void flushAndClear(SequentialWriter out) throws IOException - { - super.flushAndClear(out); - - out.writeInt(mode == Mode.SPARSE ? offset : -1); - - if (containers.size() > 0) - { - for (TokenTreeBuilder tokens : containers) - tokens.write(out); - } - - if (mode == Mode.SPARSE && combinedIndex != null) - combinedIndex.finish().write(out); - - alignToBlock(out); - - containers.clear(); - combinedIndex = initCombinedIndex(); - - offset = 0; - } - - private int ptrLength(InMemoryDataTerm term) - { - return (term.keys.getTokenCount() > 5) - ? 5 // 1 byte type + 4 byte offset to the tree - : 1 + (8 * (int) term.keys.getTokenCount()); // 1 byte size + n 8 byte tokens - } - - private void writeTerm(InMemoryTerm term, TokenTreeBuilder keys) throws IOException - { - term.serialize(buffer); - buffer.writeByte((byte) keys.getTokenCount()); - for (Pair key : keys) - buffer.writeLong(key.left); - } - - private void writeTerm(InMemoryTerm term, int offset) throws IOException - { - term.serialize(buffer); - buffer.writeByte(0x0); - buffer.writeInt(offset); - } - - private TokenTreeBuilder initCombinedIndex() - { - return mode == Mode.SPARSE ? new DynamicTokenTreeBuilder() : null; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java b/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java deleted file mode 100644 index 5b01cad12402..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.nio.ByteBuffer; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Maps; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ExecutorPlus; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.compaction.OperationType; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.rows.Unfiltered; -import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.utils.CombinedTermIterator; -import org.apache.cassandra.index.sasi.utils.TypeUtil; -import org.apache.cassandra.io.FSError; -import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.io.sstable.SSTableFlushObserver; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.concurrent.CountDownLatch; -import org.apache.cassandra.utils.concurrent.ImmediateFuture; - -import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; -import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch; - -public class PerSSTableIndexWriter implements SSTableFlushObserver -{ - private static final Logger logger = LoggerFactory.getLogger(PerSSTableIndexWriter.class); - - private static final int POOL_SIZE = 8; - private static final ExecutorPlus INDEX_FLUSHER_MEMTABLE; - private static final ExecutorPlus INDEX_FLUSHER_GENERAL; - - static - { - INDEX_FLUSHER_GENERAL = executorFactory().withJmxInternal() - .pooled("SASI-General", POOL_SIZE); - - INDEX_FLUSHER_MEMTABLE = executorFactory().withJmxInternal() - .pooled("SASI-Memtable", POOL_SIZE); - } - - private final long nowInSec = FBUtilities.nowInSeconds(); - - private final Descriptor descriptor; - private final OperationType source; - - private final AbstractType keyValidator; - - @VisibleForTesting - protected final Map indexes; - - private DecoratedKey currentKey; - private long currentKeyPosition; - private boolean isComplete; - - public PerSSTableIndexWriter(AbstractType keyValidator, - Descriptor descriptor, - OperationType source, - Map supportedIndexes) - { - this.keyValidator = keyValidator; - this.descriptor = descriptor; - this.source = source; - this.indexes = Maps.newHashMapWithExpectedSize(supportedIndexes.size()); - for (Map.Entry entry : supportedIndexes.entrySet()) - indexes.put(entry.getKey(), newIndex(entry.getValue())); - } - - @Override - public void begin() - {} - - @Override - public void startPartition(DecoratedKey key, long keyPosition, long KeyPositionForSASI) - { - currentKey = key; - currentKeyPosition = KeyPositionForSASI; - } - - @Override - public void staticRow(Row staticRow) - { - nextUnfilteredCluster(staticRow); - } - - @Override - public void nextUnfilteredCluster(Unfiltered unfiltered) - { - if (!unfiltered.isRow()) - return; - - Row row = (Row) unfiltered; - - indexes.forEach((column, index) -> { - ByteBuffer value = ColumnIndex.getValueOf(column, row, nowInSec); - if (value == null) - return; - - if (index == null) - throw new IllegalArgumentException("No index exists for column " + column.name.toString()); - - index.add(value.duplicate(), currentKey, currentKeyPosition); - }); - } - - @Override - public void complete() - { - if (isComplete) - return; - - currentKey = null; - - try - { - CountDownLatch latch = newCountDownLatch(indexes.size()); - for (Index index : indexes.values()) - index.complete(latch); - - latch.awaitUninterruptibly(); - } - finally - { - indexes.clear(); - isComplete = true; - } - } - - public Index getIndex(ColumnMetadata columnDef) - { - return indexes.get(columnDef); - } - - public Descriptor getDescriptor() - { - return descriptor; - } - - @VisibleForTesting - protected Index newIndex(ColumnIndex columnIndex) - { - return new Index(columnIndex); - } - - @VisibleForTesting - protected class Index - { - @VisibleForTesting - protected final File outputFile; - - private final ColumnIndex columnIndex; - private final AbstractAnalyzer analyzer; - private final long maxMemorySize; - - @VisibleForTesting - protected final Set> segments; - private int segmentNumber = 0; - - private OnDiskIndexBuilder currentBuilder; - - public Index(ColumnIndex columnIndex) - { - this.columnIndex = columnIndex; - this.outputFile = descriptor.fileFor(columnIndex.getComponent()); - this.analyzer = columnIndex.getAnalyzer(); - this.segments = new HashSet<>(); - this.maxMemorySize = maxMemorySize(columnIndex); - this.currentBuilder = newIndexBuilder(); - } - - public void add(ByteBuffer term, DecoratedKey key, long keyPosition) - { - if (term.remaining() == 0) - return; - - boolean isAdded = false; - - analyzer.reset(term); - while (analyzer.hasNext()) - { - ByteBuffer token = analyzer.next(); - int size = token.remaining(); - - if (token.remaining() >= OnDiskIndexBuilder.MAX_TERM_SIZE) - { - logger.info("Rejecting value (size {}, maximum {}) for column {} (analyzed {}) at {} SSTable.", - FBUtilities.prettyPrintMemory(term.remaining()), - FBUtilities.prettyPrintMemory(OnDiskIndexBuilder.MAX_TERM_SIZE), - columnIndex.getColumnName(), - columnIndex.getMode().isAnalyzed, - descriptor); - continue; - } - - if (!TypeUtil.isValid(token, columnIndex.getValidator())) - { - if ((token = TypeUtil.tryUpcast(token, columnIndex.getValidator())) == null) - { - logger.info("({}) Failed to add {} to index for key: {}, value size was {}, validator is {}.", - outputFile, - columnIndex.getColumnName(), - keyValidator.getString(key.getKey()), - FBUtilities.prettyPrintMemory(size), - columnIndex.getValidator()); - continue; - } - } - - currentBuilder.add(token, key, keyPosition); - isAdded = true; - } - - if (!isAdded || currentBuilder.estimatedMemoryUse() < maxMemorySize) - return; // non of the generated tokens were added to the index or memory size wasn't reached - - segments.add(getExecutor().submit(scheduleSegmentFlush(false))); - } - - @VisibleForTesting - protected Callable scheduleSegmentFlush(final boolean isFinal) - { - final OnDiskIndexBuilder builder = currentBuilder; - currentBuilder = newIndexBuilder(); - - final File segmentFile = file(isFinal); - - return () -> { - long start = nanoTime(); - - try - { - return builder.finish(segmentFile) ? new OnDiskIndex(segmentFile, columnIndex.getValidator(), null) : null; - } - catch (Exception | FSError e) - { - logger.error("Failed to build index segment {}", segmentFile, e); - return null; - } - finally - { - if (!isFinal) - logger.info("Flushed index segment {}, took {} ms.", segmentFile, TimeUnit.NANOSECONDS.toMillis(nanoTime() - start)); - } - }; - } - - public void complete(final CountDownLatch latch) - { - logger.info("Scheduling index flush to {}", outputFile); - - getExecutor().submit(() -> { - long start1 = nanoTime(); - - OnDiskIndex[] parts = new OnDiskIndex[segments.size() + 1]; - - try - { - // no parts present, build entire index from memory - if (segments.isEmpty()) - { - scheduleSegmentFlush(true).call(); - return; - } - - // parts are present but there is something still in memory, let's flush that inline - if (!currentBuilder.isEmpty()) - { - OnDiskIndex last = scheduleSegmentFlush(false).call(); - segments.add(ImmediateFuture.success(last)); - } - - int index = 0; - ByteBuffer combinedMin = null, combinedMax = null; - - for (Future f : segments) - { - OnDiskIndex part = f.get(); - if (part == null) - continue; - - parts[index++] = part; - combinedMin = (combinedMin == null || keyValidator.compare(combinedMin, part.minKey()) > 0) ? part.minKey() : combinedMin; - combinedMax = (combinedMax == null || keyValidator.compare(combinedMax, part.maxKey()) < 0) ? part.maxKey() : combinedMax; - } - - OnDiskIndexBuilder builder = newIndexBuilder(); - builder.finish(Pair.create(combinedMin, combinedMax), - outputFile, - new CombinedTermIterator(parts)); - } - catch (Exception | FSError e) - { - logger.error("Failed to flush index {}.", outputFile, e); - outputFile.tryDelete(); - } - finally - { - logger.info("Index flush to {} took {} ms.", outputFile, TimeUnit.NANOSECONDS.toMillis(nanoTime() - start1)); - - for (int segment = 0; segment < segmentNumber; segment++) - { - OnDiskIndex part = parts[segment]; - - if (part != null) - FileUtils.closeQuietly(part); - - outputFile.withSuffix("_" + segment).tryDelete(); - } - - latch.decrement(); - } - }); - } - - private ExecutorService getExecutor() - { - return source == OperationType.FLUSH ? INDEX_FLUSHER_MEMTABLE : INDEX_FLUSHER_GENERAL; - } - - private OnDiskIndexBuilder newIndexBuilder() - { - return new OnDiskIndexBuilder(keyValidator, columnIndex.getValidator(), columnIndex.getMode().mode); - } - - public File file(boolean isFinal) - { - return isFinal ? outputFile : outputFile.withSuffix("_" + segmentNumber++); - } - } - - protected long maxMemorySize(ColumnIndex columnIndex) - { - // 1G for memtable and configuration for compaction - return source == OperationType.FLUSH ? 1073741824L : columnIndex.getMode().maxCompactionFlushMemoryInBytes; - } - - public int hashCode() - { - return descriptor.hashCode(); - } - - public boolean equals(Object o) - { - return o instanceof PerSSTableIndexWriter && descriptor.equals(((PerSSTableIndexWriter) o).descriptor); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/StaticTokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/StaticTokenTreeBuilder.java deleted file mode 100644 index 7a41b38d7a80..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/StaticTokenTreeBuilder.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Iterator; -import java.util.SortedMap; - -import org.apache.cassandra.index.sasi.utils.CombinedTerm; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.Pair; - -import com.carrotsearch.hppc.LongSet; -import com.google.common.collect.Iterators; - -/** - * Intended usage of this class is to be used in place of {@link DynamicTokenTreeBuilder} - * when multiple index segments produced by {@link PerSSTableIndexWriter} are stitched together - * by {@link PerSSTableIndexWriter#complete()}. - * - * This class uses the RangeIterator, now provided by - * {@link CombinedTerm#getTokenIterator()}, to iterate the data twice. - * The first iteration builds the tree with leaves that contain only enough - * information to build the upper layers -- these leaves do not store more - * than their minimum and maximum tokens plus their total size, which makes them - * un-serializable. - * - * When the tree is written to disk the final layer is not - * written. Its at this point the data is iterated once again to write - * the leaves to disk. This (logarithmically) reduces copying of the - * token values while building and writing upper layers of the tree, - * removes the use of SortedMap when combining SAs, and relies on the - * memory mapped SAs otherwise, greatly improving performance and no - * longer causing OOMs when TokenTree sizes are big. - * - * See https://issues.apache.org/jira/browse/CASSANDRA-11383 for more details. - */ -public class StaticTokenTreeBuilder extends AbstractTokenTreeBuilder -{ - private final CombinedTerm combinedTerm; - - public StaticTokenTreeBuilder(CombinedTerm term) - { - combinedTerm = term; - } - - public void add(Long token, long keyPosition) - { - throw new UnsupportedOperationException(); - } - - public void add(SortedMap data) - { - throw new UnsupportedOperationException(); - } - - public void add(Iterator> data) - { - throw new UnsupportedOperationException(); - } - - public boolean isEmpty() - { - return tokenCount == 0; - } - - public Iterator> iterator() - { - Iterator iterator = combinedTerm.getTokenIterator(); - return new AbstractIterator>() - { - protected Pair computeNext() - { - if (!iterator.hasNext()) - return endOfData(); - - Token token = iterator.next(); - return Pair.create(token.get(), token.getOffsets()); - } - }; - } - - public long getTokenCount() - { - return tokenCount; - } - - @Override - public void write(DataOutputPlus out) throws IOException - { - // if the root is not a leaf then none of the leaves have been written (all are PartialLeaf) - // so write out the last layer of the tree by converting PartialLeaf to StaticLeaf and - // iterating the data once more - super.write(out); - if (root.isLeaf()) - return; - - RangeIterator tokens = combinedTerm.getTokenIterator(); - ByteBuffer blockBuffer = ByteBuffer.allocate(BLOCK_BYTES); - Iterator leafIterator = leftmostLeaf.levelIterator(); - while (leafIterator.hasNext()) - { - Leaf leaf = (Leaf) leafIterator.next(); - Leaf writeableLeaf = new StaticLeaf(Iterators.limit(tokens, leaf.tokenCount()), leaf); - writeableLeaf.serialize(-1, blockBuffer); - flushBuffer(blockBuffer, out, true); - } - - } - - protected void constructTree() - { - RangeIterator tokens = combinedTerm.getTokenIterator(); - - tokenCount = 0; - treeMinToken = tokens.getMinimum(); - treeMaxToken = tokens.getMaximum(); - numBlocks = 1; - - root = new InteriorNode(); - rightmostParent = (InteriorNode) root; - Leaf lastLeaf = null; - Long lastToken, firstToken = null; - int leafSize = 0; - while (tokens.hasNext()) - { - Long token = tokens.next().get(); - if (firstToken == null) - firstToken = token; - - tokenCount++; - leafSize++; - - // skip until the last token in the leaf - if (tokenCount % TOKENS_PER_BLOCK != 0 && token != treeMaxToken) - continue; - - lastToken = token; - Leaf leaf = new PartialLeaf(firstToken, lastToken, leafSize); - if (lastLeaf == null) // first leaf created - leftmostLeaf = leaf; - else - lastLeaf.next = leaf; - - - rightmostParent.add(leaf); - lastLeaf = rightmostLeaf = leaf; - firstToken = null; - numBlocks++; - leafSize = 0; - } - - // if the tree is really a single leaf the empty root interior - // node must be discarded - if (root.tokenCount() == 0) - { - numBlocks = 1; - root = new StaticLeaf(combinedTerm.getTokenIterator(), treeMinToken, treeMaxToken, tokenCount, true); - } - } - - // This denotes the leaf which only has min/max and token counts - // but doesn't have any associated data yet, so it can't be serialized. - private class PartialLeaf extends Leaf - { - private final int size; - public PartialLeaf(Long min, Long max, int count) - { - super(min, max); - size = count; - } - - public int tokenCount() - { - return size; - } - - public void serializeData(ByteBuffer buf) - { - throw new UnsupportedOperationException(); - } - - public boolean isSerializable() - { - return false; - } - } - - // This denotes the leaf which has been filled with data and is ready to be serialized - private class StaticLeaf extends Leaf - { - private final Iterator tokens; - private final int count; - private final boolean isLast; - - public StaticLeaf(Iterator tokens, Leaf leaf) - { - this(tokens, leaf.smallestToken(), leaf.largestToken(), leaf.tokenCount(), leaf.isLastLeaf()); - } - - public StaticLeaf(Iterator tokens, Long min, Long max, long count, boolean isLastLeaf) - { - super(min, max); - - this.count = (int) count; // downcast is safe since leaf size is always < Integer.MAX_VALUE - this.tokens = tokens; - this.isLast = isLastLeaf; - } - - public boolean isLastLeaf() - { - return isLast; - } - - public int tokenCount() - { - return count; - } - - public void serializeData(ByteBuffer buf) - { - while (tokens.hasNext()) - { - Token entry = tokens.next(); - createEntry(entry.get(), entry.getOffsets()).serialize(buf); - } - } - - public boolean isSerializable() - { - return true; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/Token.java b/src/java/org/apache/cassandra/index/sasi/disk/Token.java deleted file mode 100644 index 4cd1ea352a8f..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/Token.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import com.google.common.primitives.Longs; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sasi.utils.CombinedValue; - -import com.carrotsearch.hppc.LongSet; - -public abstract class Token implements CombinedValue, Iterable -{ - protected final long token; - - public Token(long token) - { - this.token = token; - } - - public Long get() - { - return token; - } - - public abstract LongSet getOffsets(); - - public int compareTo(CombinedValue o) - { - return Longs.compare(token, ((Token) o).token); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java b/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java deleted file mode 100644 index 7f8f3a0f36bd..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.io.IOException; -import java.util.*; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sasi.utils.CombinedValue; -import org.apache.cassandra.index.sasi.utils.MappedBuffer; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.utils.AbstractGuavaIterator; -import org.apache.cassandra.utils.MergeIterator; - -import com.carrotsearch.hppc.LongHashSet; -import com.carrotsearch.hppc.LongSet; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; -import com.google.common.collect.Iterators; -import org.apache.commons.lang3.builder.HashCodeBuilder; - -import static org.apache.cassandra.index.sasi.disk.TokenTreeBuilder.EntryType; - -// Note: all of the seek-able offsets contained in TokenTree should be sizeof(long) -// even if currently only lower int portion of them if used, because that makes -// it possible to switch to mmap implementation which supports long positions -// without any on-disk format changes and/or re-indexing if one day we'll have a need to. -public class TokenTree -{ - private static final int LONG_BYTES = Long.SIZE / 8; - private static final int SHORT_BYTES = Short.SIZE / 8; - - private final Descriptor descriptor; - private final MappedBuffer file; - private final long startPos; - private final long treeMinToken; - private final long treeMaxToken; - private final long tokenCount; - - @VisibleForTesting - protected TokenTree(MappedBuffer tokenTree) - { - this(Descriptor.CURRENT, tokenTree); - } - - public TokenTree(Descriptor d, MappedBuffer tokenTree) - { - descriptor = d; - file = tokenTree; - startPos = file.position(); - - file.position(startPos + TokenTreeBuilder.SHARED_HEADER_BYTES); - - if (!validateMagic()) - throw new IllegalArgumentException("invalid token tree"); - - tokenCount = file.getLong(); - treeMinToken = file.getLong(); - treeMaxToken = file.getLong(); - } - - public long getCount() - { - return tokenCount; - } - - public RangeIterator iterator(Function keyFetcher) - { - return new TokenTreeIterator(file.duplicate(), keyFetcher); - } - - public OnDiskToken get(final long searchToken, Function keyFetcher) - { - seekToLeaf(searchToken, file); - long leafStart = file.position(); - short leafSize = file.getShort(leafStart + 1); // skip the info byte - - file.position(leafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES); // skip to tokens - short tokenIndex = searchLeaf(searchToken, leafSize); - - file.position(leafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES); - - OnDiskToken token = OnDiskToken.getTokenAt(file, tokenIndex, leafSize, keyFetcher); - return token.get().equals(searchToken) ? token : null; - } - - private boolean validateMagic() - { - switch (descriptor.version.toString()) - { - case Descriptor.VERSION_AA: - return true; - case Descriptor.VERSION_AB: - return TokenTreeBuilder.AB_MAGIC == file.getShort(); - default: - return false; - } - } - - // finds leaf that *could* contain token - private void seekToLeaf(long token, MappedBuffer file) - { - // this loop always seeks forward except for the first iteration - // where it may seek back to the root - long blockStart = startPos; - while (true) - { - file.position(blockStart); - - byte info = file.get(); - boolean isLeaf = (info & 1) == 1; - - if (isLeaf) - { - file.position(blockStart); - break; - } - - short tokenCount = file.getShort(); - - long minToken = file.getLong(); - long maxToken = file.getLong(); - - long seekBase = blockStart + TokenTreeBuilder.BLOCK_HEADER_BYTES; - if (minToken > token) - { - // seek to beginning of child offsets to locate first child - file.position(seekBase + tokenCount * LONG_BYTES); - blockStart = (startPos + (int) file.getLong()); - } - else if (maxToken < token) - { - // seek to end of child offsets to locate last child - file.position(seekBase + (2 * tokenCount) * LONG_BYTES); - blockStart = (startPos + (int) file.getLong()); - } - else - { - // skip to end of block header/start of interior block tokens - file.position(seekBase); - - short offsetIndex = searchBlock(token, tokenCount, file); - - // file pointer is now at beginning of offsets - if (offsetIndex == tokenCount) - file.position(file.position() + (offsetIndex * LONG_BYTES)); - else - file.position(file.position() + ((tokenCount - offsetIndex - 1) + offsetIndex) * LONG_BYTES); - - blockStart = (startPos + (int) file.getLong()); - } - } - } - - private short searchBlock(long searchToken, short tokenCount, MappedBuffer file) - { - short offsetIndex = 0; - for (int i = 0; i < tokenCount; i++) - { - long readToken = file.getLong(); - if (searchToken < readToken) - break; - - offsetIndex++; - } - - return offsetIndex; - } - - private short searchLeaf(long searchToken, short tokenCount) - { - long base = file.position(); - - int start = 0; - int end = tokenCount; - int middle = 0; - - while (start <= end) - { - middle = start + ((end - start) >> 1); - - // each entry is 16 bytes wide, token is in bytes 4-11 - long token = file.getLong(base + (middle * (2 * LONG_BYTES) + 4)); - - if (token == searchToken) - break; - - if (token < searchToken) - start = middle + 1; - else - end = middle - 1; - } - - return (short) middle; - } - - public class TokenTreeIterator extends RangeIterator - { - private final Function keyFetcher; - private final MappedBuffer file; - - private long currentLeafStart; - private int currentTokenIndex; - - private long leafMinToken; - private long leafMaxToken; - private short leafSize; - - protected boolean firstIteration = true; - private boolean lastLeaf; - - TokenTreeIterator(MappedBuffer file, Function keyFetcher) - { - super(treeMinToken, treeMaxToken, tokenCount); - - this.file = file; - this.keyFetcher = keyFetcher; - } - - protected Token computeNext() - { - maybeFirstIteration(); - - if (currentTokenIndex >= leafSize && lastLeaf) - return endOfData(); - - if (currentTokenIndex < leafSize) // tokens remaining in this leaf - { - return getTokenAt(currentTokenIndex++); - } - else // no more tokens remaining in this leaf - { - assert !lastLeaf; - - seekToNextLeaf(); - setupBlock(); - return computeNext(); - } - } - - protected void performSkipTo(Long nextToken) - { - maybeFirstIteration(); - - if (nextToken <= leafMaxToken) // next is in this leaf block - { - searchLeaf(nextToken); - } - else // next is in a leaf block that needs to be found - { - seekToLeaf(nextToken, file); - setupBlock(); - findNearest(nextToken); - } - } - - private void setupBlock() - { - currentLeafStart = file.position(); - currentTokenIndex = 0; - - lastLeaf = (file.get() & (1 << TokenTreeBuilder.LAST_LEAF_SHIFT)) > 0; - leafSize = file.getShort(); - - leafMinToken = file.getLong(); - leafMaxToken = file.getLong(); - - // seek to end of leaf header/start of data - file.position(currentLeafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES); - } - - private void findNearest(Long next) - { - if (next > leafMaxToken && !lastLeaf) - { - seekToNextLeaf(); - setupBlock(); - findNearest(next); - } - else if (next > leafMinToken) - searchLeaf(next); - } - - private void searchLeaf(long next) - { - for (int i = currentTokenIndex; i < leafSize; i++) - { - if (compareTokenAt(currentTokenIndex, next) >= 0) - break; - - currentTokenIndex++; - } - } - - private int compareTokenAt(int idx, long toToken) - { - return Long.compare(file.getLong(getTokenPosition(idx)), toToken); - } - - private Token getTokenAt(int idx) - { - return OnDiskToken.getTokenAt(file, idx, leafSize, keyFetcher); - } - - private long getTokenPosition(int idx) - { - // skip 4 byte entry header to get position pointing directly at the entry's token - return OnDiskToken.getEntryPosition(idx, file) + (2 * SHORT_BYTES); - } - - private void seekToNextLeaf() - { - file.position(currentLeafStart + TokenTreeBuilder.BLOCK_BYTES); - } - - public void close() throws IOException - { - // nothing to do here - } - - private void maybeFirstIteration() - { - // seek to the first token only when requested for the first time, - // highly predictable branch and saves us a lot by not traversing the tree - // on creation time because it's not at all required. - if (!firstIteration) - return; - - seekToLeaf(treeMinToken, file); - setupBlock(); - firstIteration = false; - } - } - - public static class OnDiskToken extends Token - { - private final Set info = new HashSet<>(2); - private final Set loadedKeys = new TreeSet<>(DecoratedKey.comparator); - - public OnDiskToken(MappedBuffer buffer, long position, short leafSize, Function keyFetcher) - { - super(buffer.getLong(position + (2 * SHORT_BYTES))); - info.add(new TokenInfo(buffer, position, leafSize, keyFetcher)); - } - - public void merge(CombinedValue other) - { - if (!(other instanceof Token)) - return; - - Token o = (Token) other; - if (token != o.token) - throw new IllegalArgumentException(String.format("%s != %s", token, o.token)); - - if (o instanceof OnDiskToken) - { - info.addAll(((OnDiskToken) other).info); - } - else - { - Iterators.addAll(loadedKeys, o.iterator()); - } - } - - public Iterator iterator() - { - List> keys = new ArrayList<>(info.size()); - - for (TokenInfo i : info) - keys.add(i.iterator()); - - if (!loadedKeys.isEmpty()) - keys.add(loadedKeys.iterator()); - - return MergeIterator.get(keys, DecoratedKey.comparator, new MergeIterator.Reducer() - { - DecoratedKey reduced = null; - - public boolean trivialReduceIsTrivial() - { - return true; - } - - public void reduce(int idx, DecoratedKey current) - { - reduced = current; - } - - protected DecoratedKey getReduced() - { - return reduced; - } - }); - } - - public LongSet getOffsets() - { - LongSet offsets = new LongHashSet(4); - for (TokenInfo i : info) - { - for (long offset : i.fetchOffsets()) - offsets.add(offset); - } - - return offsets; - } - - public static OnDiskToken getTokenAt(MappedBuffer buffer, int idx, short leafSize, Function keyFetcher) - { - return new OnDiskToken(buffer, getEntryPosition(idx, buffer), leafSize, keyFetcher); - } - - private static long getEntryPosition(int idx, MappedBuffer file) - { - // info (4 bytes) + token (8 bytes) + offset (4 bytes) = 16 bytes - return file.position() + (idx * (2 * LONG_BYTES)); - } - } - - private static class TokenInfo - { - private final MappedBuffer buffer; - private final Function keyFetcher; - - private final long position; - private final short leafSize; - - public TokenInfo(MappedBuffer buffer, long position, short leafSize, Function keyFetcher) - { - this.keyFetcher = keyFetcher; - this.buffer = buffer; - this.position = position; - this.leafSize = leafSize; - } - - public Iterator iterator() - { - return new KeyIterator(keyFetcher, fetchOffsets()); - } - - public int hashCode() - { - return new HashCodeBuilder().append(keyFetcher).append(position).append(leafSize).build(); - } - - public boolean equals(Object other) - { - if (!(other instanceof TokenInfo)) - return false; - - TokenInfo o = (TokenInfo) other; - return keyFetcher == o.keyFetcher && position == o.position; - } - - private long[] fetchOffsets() - { - short info = buffer.getShort(position); - // offset extra is unsigned short (right-most 16 bits of 48 bits allowed for an offset) - int offsetExtra = buffer.getShort(position + SHORT_BYTES) & 0xFFFF; - // is the it left-most (32-bit) base of the actual offset in the index file - int offsetData = buffer.getInt(position + (2 * SHORT_BYTES) + LONG_BYTES); - - EntryType type = EntryType.of(info & TokenTreeBuilder.ENTRY_TYPE_MASK); - - switch (type) - { - case SIMPLE: - return new long[] { offsetData }; - - case OVERFLOW: - long[] offsets = new long[offsetExtra]; // offsetShort contains count of tokens - long offsetPos = (buffer.position() + (2 * (leafSize * LONG_BYTES)) + (offsetData * LONG_BYTES)); - - for (int i = 0; i < offsetExtra; i++) - offsets[i] = buffer.getLong(offsetPos + (i * LONG_BYTES)); - - return offsets; - - case FACTORED: - return new long[] { (((long) offsetData) << Short.SIZE) + offsetExtra }; - - case PACKED: - return new long[] { offsetExtra, offsetData }; - - default: - throw new IllegalStateException("Unknown entry type: " + type); - } - } - } - - private static class KeyIterator extends AbstractGuavaIterator - { - private final Function keyFetcher; - private final long[] offsets; - private int index = 0; - - public KeyIterator(Function keyFetcher, long[] offsets) - { - this.keyFetcher = keyFetcher; - this.offsets = offsets; - } - - public DecoratedKey computeNext() - { - return index < offsets.length ? keyFetcher.apply(offsets[index++]) : endOfData(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java deleted file mode 100644 index 01a536ccdab9..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.disk; - -import java.io.IOException; -import java.util.*; - -import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.utils.Pair; - -import com.carrotsearch.hppc.LongSet; - -public interface TokenTreeBuilder extends Iterable> -{ - int BLOCK_BYTES = 4096; - int BLOCK_HEADER_BYTES = 64; - int BLOCK_ENTRY_BYTES = 2 * Long.BYTES; - int OVERFLOW_TRAILER_BYTES = 64; - int OVERFLOW_ENTRY_BYTES = Long.BYTES; - int OVERFLOW_TRAILER_CAPACITY = OVERFLOW_TRAILER_BYTES / OVERFLOW_ENTRY_BYTES; - int TOKENS_PER_BLOCK = (BLOCK_BYTES - BLOCK_HEADER_BYTES - OVERFLOW_TRAILER_BYTES) / BLOCK_ENTRY_BYTES; - long MAX_OFFSET = (1L << 47) - 1; // 48 bits for (signed) offset - byte LAST_LEAF_SHIFT = 1; - byte SHARED_HEADER_BYTES = 19; - byte ENTRY_TYPE_MASK = 0x03; - short AB_MAGIC = 0x5A51; - - // note: ordinal positions are used here, do not change order - enum EntryType - { - SIMPLE, FACTORED, PACKED, OVERFLOW; - - public static EntryType of(int ordinal) - { - if (ordinal == SIMPLE.ordinal()) - return SIMPLE; - - if (ordinal == FACTORED.ordinal()) - return FACTORED; - - if (ordinal == PACKED.ordinal()) - return PACKED; - - if (ordinal == OVERFLOW.ordinal()) - return OVERFLOW; - - throw new IllegalArgumentException("Unknown ordinal: " + ordinal); - } - } - - void add(Long token, long keyPosition); - void add(SortedMap data); - void add(Iterator> data); - void add(TokenTreeBuilder ttb); - - boolean isEmpty(); - long getTokenCount(); - - TokenTreeBuilder finish(); - - int serializedSize(); - void write(DataOutputPlus out) throws IOException; -} diff --git a/src/java/org/apache/cassandra/index/sasi/exceptions/TimeQuotaExceededException.java b/src/java/org/apache/cassandra/index/sasi/exceptions/TimeQuotaExceededException.java deleted file mode 100644 index e237614bbbfd..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/exceptions/TimeQuotaExceededException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.exceptions; - -public class TimeQuotaExceededException extends RuntimeException -{ - public TimeQuotaExceededException(String message) { - super(message); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java b/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java deleted file mode 100644 index e55a806ab5ec..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.memory; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.index.sasi.utils.TypeUtil; -import org.apache.cassandra.utils.FBUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IndexMemtable -{ - private static final Logger logger = LoggerFactory.getLogger(IndexMemtable.class); - - private final MemIndex index; - - public IndexMemtable(ColumnIndex columnIndex) - { - this.index = MemIndex.forColumn(columnIndex.keyValidator(), columnIndex); - } - - public long index(DecoratedKey key, ByteBuffer value) - { - if (value == null || value.remaining() == 0) - return 0; - - AbstractType validator = index.columnIndex.getValidator(); - if (!TypeUtil.isValid(value, validator)) - { - int size = value.remaining(); - if ((value = TypeUtil.tryUpcast(value, validator)) == null) - { - logger.error("Can't add column {} to index for key: {}, value size {}, validator: {}.", - index.columnIndex.getColumnName(), - index.columnIndex.keyValidator().getString(key.getKey()), - FBUtilities.prettyPrintMemory(size), - validator); - return 0; - } - } - - return index.add(key, value); - } - - public RangeIterator search(Expression expression) - { - return index == null ? null : index.search(expression); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/memory/KeyRangeIterator.java b/src/java/org/apache/cassandra/index/sasi/memory/KeyRangeIterator.java deleted file mode 100644 index b4b1ccb9ebd1..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/memory/KeyRangeIterator.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.memory; - -import java.io.IOException; -import java.util.Iterator; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentSkipListSet; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.utils.CombinedValue; -import org.apache.cassandra.index.sasi.utils.RangeIterator; - -import com.carrotsearch.hppc.LongHashSet; -import com.carrotsearch.hppc.LongSet; -import org.apache.cassandra.utils.AbstractGuavaIterator; - -import com.google.common.collect.PeekingIterator; - -public class KeyRangeIterator extends RangeIterator -{ - private final DKIterator iterator; - - public KeyRangeIterator(ConcurrentSkipListSet keys, int size) - { - super((Long) keys.first().getToken().getTokenValue(), (Long) keys.last().getToken().getTokenValue(), size); - this.iterator = new DKIterator(keys.iterator()); - } - - protected Token computeNext() - { - return iterator.hasNext() ? new DKToken(iterator.next()) : endOfData(); - } - - protected void performSkipTo(Long nextToken) - { - while (iterator.hasNext()) - { - DecoratedKey key = iterator.peek(); - if (Long.compare((long) key.getToken().getTokenValue(), nextToken) >= 0) - break; - - // consume smaller key - iterator.next(); - } - } - - public void close() throws IOException - {} - - private static class DKIterator extends AbstractGuavaIterator implements PeekingIterator - { - private final Iterator keys; - - public DKIterator(Iterator keys) - { - this.keys = keys; - } - - protected DecoratedKey computeNext() - { - return keys.hasNext() ? keys.next() : endOfData(); - } - } - - private static class DKToken extends Token - { - private final SortedSet keys; - - public DKToken(final DecoratedKey key) - { - super((long) key.getToken().getTokenValue()); - - keys = new TreeSet(DecoratedKey.comparator) - {{ - add(key); - }}; - } - - public LongSet getOffsets() - { - LongSet offsets = new LongHashSet(4); - for (DecoratedKey key : keys) - offsets.add((long) key.getToken().getTokenValue()); - - return offsets; - } - - public void merge(CombinedValue other) - { - if (!(other instanceof Token)) - return; - - Token o = (Token) other; - assert o.get().equals(token); - - if (o instanceof DKToken) - { - keys.addAll(((DKToken) o).keys); - } - else - { - for (DecoratedKey key : o) - keys.add(key); - } - } - - public Iterator iterator() - { - return keys.iterator(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/memory/MemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/MemIndex.java deleted file mode 100644 index cc1eb3ff399e..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/memory/MemIndex.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.memory; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.db.marshal.AbstractType; - -public abstract class MemIndex -{ - protected final AbstractType keyValidator; - protected final ColumnIndex columnIndex; - - protected MemIndex(AbstractType keyValidator, ColumnIndex columnIndex) - { - this.keyValidator = keyValidator; - this.columnIndex = columnIndex; - } - - public abstract long add(DecoratedKey key, ByteBuffer value); - public abstract RangeIterator search(Expression expression); - - public static MemIndex forColumn(AbstractType keyValidator, ColumnIndex columnIndex) - { - return columnIndex.isLiteral() - ? new TrieMemIndex(keyValidator, columnIndex) - : new SkipListMemIndex(keyValidator, columnIndex); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java deleted file mode 100644 index 9e44344f123b..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.memory; - -import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.ConcurrentSkipListSet; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.db.marshal.AbstractType; - -public class SkipListMemIndex extends MemIndex -{ - public static final int CSLM_OVERHEAD = 128; // average overhead of CSLM - - private final ConcurrentSkipListMap> index; - - public SkipListMemIndex(AbstractType keyValidator, ColumnIndex columnIndex) - { - super(keyValidator, columnIndex); - index = new ConcurrentSkipListMap<>(columnIndex.getValidator()); - } - - public long add(DecoratedKey key, ByteBuffer value) - { - long overhead = CSLM_OVERHEAD; // DKs are shared - ConcurrentSkipListSet keys = index.get(value); - - if (keys == null) - { - ConcurrentSkipListSet newKeys = new ConcurrentSkipListSet<>(DecoratedKey.comparator); - keys = index.putIfAbsent(value, newKeys); - if (keys == null) - { - overhead += CSLM_OVERHEAD + value.remaining(); - keys = newKeys; - } - } - - keys.add(key); - - return overhead; - } - - public RangeIterator search(Expression expression) - { - ByteBuffer min = expression.lower == null ? null : expression.lower.value; - ByteBuffer max = expression.upper == null ? null : expression.upper.value; - - SortedMap> search; - - if (min == null && max == null) - { - throw new IllegalArgumentException(); - } - if (min != null && max != null) - { - search = index.subMap(min, expression.lower.inclusive, max, expression.upper.inclusive); - } - else if (min == null) - { - search = index.headMap(max, expression.upper.inclusive); - } - else - { - search = index.tailMap(min, expression.lower.inclusive); - } - - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - - for (ConcurrentSkipListSet keys : search.values()) { - int size; - if ((size = keys.size()) > 0) - builder.add(new KeyRangeIterator(keys, size)); - } - - return builder.build(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java deleted file mode 100644 index cebd68f08c44..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.memory; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ConcurrentSkipListSet; - -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression; -import org.apache.cassandra.index.sasi.plan.Expression.Op; -import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.db.marshal.AbstractType; - -import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; -import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree; -import com.googlecode.concurrenttrees.radix.node.concrete.SmartArrayBasedNodeFactory; -import com.googlecode.concurrenttrees.radix.node.Node; -import org.apache.cassandra.utils.FBUtilities; - - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.apache.cassandra.index.sasi.memory.SkipListMemIndex.CSLM_OVERHEAD; - -public class TrieMemIndex extends MemIndex -{ - private static final Logger logger = LoggerFactory.getLogger(TrieMemIndex.class); - - private final ConcurrentTrie index; - - public TrieMemIndex(AbstractType keyValidator, ColumnIndex columnIndex) - { - super(keyValidator, columnIndex); - - switch (columnIndex.getMode().mode) - { - case CONTAINS: - index = new ConcurrentSuffixTrie(columnIndex.getDefinition()); - break; - - case PREFIX: - index = new ConcurrentPrefixTrie(columnIndex.getDefinition()); - break; - - default: - throw new IllegalStateException("Unsupported mode: " + columnIndex.getMode().mode); - } - } - - public long add(DecoratedKey key, ByteBuffer value) - { - AbstractAnalyzer analyzer = columnIndex.getAnalyzer(); - analyzer.reset(value.duplicate()); - - long size = 0; - while (analyzer.hasNext()) - { - ByteBuffer term = analyzer.next(); - - if (term.remaining() >= OnDiskIndexBuilder.MAX_TERM_SIZE) - { - logger.info("Can't add term of column {} to index for key: {}, term size {}, max allowed size {}, use analyzed = true (if not yet set) for that column.", - columnIndex.getColumnName(), - keyValidator.getString(key.getKey()), - FBUtilities.prettyPrintMemory(term.remaining()), - FBUtilities.prettyPrintMemory(OnDiskIndexBuilder.MAX_TERM_SIZE)); - continue; - } - - size += index.add(columnIndex.getValidator().getString(term), key); - } - - return size; - } - - public RangeIterator search(Expression expression) - { - return index.search(expression); - } - - private static abstract class ConcurrentTrie - { - public static final SizeEstimatingNodeFactory NODE_FACTORY = new SizeEstimatingNodeFactory(); - - protected final ColumnMetadata definition; - - public ConcurrentTrie(ColumnMetadata column) - { - definition = column; - } - - public long add(String value, DecoratedKey key) - { - long overhead = CSLM_OVERHEAD; - ConcurrentSkipListSet keys = get(value); - if (keys == null) - { - ConcurrentSkipListSet newKeys = new ConcurrentSkipListSet<>(DecoratedKey.comparator); - keys = putIfAbsent(value, newKeys); - if (keys == null) - { - overhead += CSLM_OVERHEAD + value.length(); - keys = newKeys; - } - } - - keys.add(key); - - // get and reset new memory size allocated by current thread - overhead += NODE_FACTORY.currentUpdateSize(); - NODE_FACTORY.reset(); - - return overhead; - } - - public RangeIterator search(Expression expression) - { - ByteBuffer prefix = expression.lower == null ? null : expression.lower.value; - - Iterable> search = search(expression.getOp(), definition.cellValueType().getString(prefix)); - - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - for (ConcurrentSkipListSet keys : search) - { - int size; - if ((size = keys.size()) > 0) - builder.add(new KeyRangeIterator(keys, size)); - } - - return builder.build(); - } - - protected abstract ConcurrentSkipListSet get(String value); - protected abstract Iterable> search(Op operator, String value); - protected abstract ConcurrentSkipListSet putIfAbsent(String value, ConcurrentSkipListSet key); - } - - protected static class ConcurrentPrefixTrie extends ConcurrentTrie - { - private final ConcurrentRadixTree> trie; - - private ConcurrentPrefixTrie(ColumnMetadata column) - { - super(column); - trie = new ConcurrentRadixTree<>(NODE_FACTORY); - } - - public ConcurrentSkipListSet get(String value) - { - return trie.getValueForExactKey(value); - } - - public ConcurrentSkipListSet putIfAbsent(String value, ConcurrentSkipListSet newKeys) - { - return trie.putIfAbsent(value, newKeys); - } - - public Iterable> search(Op operator, String value) - { - switch (operator) - { - case EQ: - case MATCH: - ConcurrentSkipListSet keys = trie.getValueForExactKey(value); - return keys == null ? Collections.emptyList() : Collections.singletonList(keys); - - case PREFIX: - return trie.getValuesForKeysStartingWith(value); - - default: - throw new UnsupportedOperationException(String.format("operation %s is not supported.", operator)); - } - } - } - - protected static class ConcurrentSuffixTrie extends ConcurrentTrie - { - private final ConcurrentSuffixTree> trie; - - private ConcurrentSuffixTrie(ColumnMetadata column) - { - super(column); - trie = new ConcurrentSuffixTree<>(NODE_FACTORY); - } - - public ConcurrentSkipListSet get(String value) - { - return trie.getValueForExactKey(value); - } - - public ConcurrentSkipListSet putIfAbsent(String value, ConcurrentSkipListSet newKeys) - { - return trie.putIfAbsent(value, newKeys); - } - - public Iterable> search(Op operator, String value) - { - switch (operator) - { - case EQ: - case MATCH: - ConcurrentSkipListSet keys = trie.getValueForExactKey(value); - return keys == null ? Collections.emptyList() : Collections.singletonList(keys); - - case SUFFIX: - return trie.getValuesForKeysEndingWith(value); - - case PREFIX: - case CONTAINS: - return trie.getValuesForKeysContaining(value); - - default: - throw new UnsupportedOperationException(String.format("operation %s is not supported.", operator)); - } - } - } - - // This relies on the fact that all of the tree updates are done under exclusive write lock, - // method would overestimate in certain circumstances e.g. when nodes are replaced in place, - // but it's still better comparing to underestimate since it gives more breathing room for other memory users. - private static class SizeEstimatingNodeFactory extends SmartArrayBasedNodeFactory - { - private final ThreadLocal updateSize = ThreadLocal.withInitial(() -> 0L); - - public Node createNode(CharSequence edgeCharacters, Object value, List childNodes, boolean isRoot) - { - Node node = super.createNode(edgeCharacters, value, childNodes, isRoot); - updateSize.set(updateSize.get() + measure(node)); - return node; - } - - public long currentUpdateSize() - { - return updateSize.get(); - } - - public void reset() - { - updateSize.set(0L); - } - - private long measure(Node node) - { - // node with max overhead is CharArrayNodeLeafWithValue = 24B - long overhead = 24; - - // array of chars (2 bytes) + CharSequence overhead - overhead += 24 + node.getIncomingEdge().length() * 2; - - if (node.getOutgoingEdges() != null) - { - // 16 bytes for AtomicReferenceArray - overhead += 16; - overhead += 24 * node.getOutgoingEdges().size(); - } - - return overhead; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/plan/Expression.java b/src/java/org/apache/cassandra/index/sasi/plan/Expression.java deleted file mode 100644 index 6c3b9b88f30f..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/plan/Expression.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.plan; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndex; -import org.apache.cassandra.index.sasi.utils.TypeUtil; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.UTF8Type; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.FBUtilities; - -import org.apache.commons.lang3.builder.HashCodeBuilder; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Iterators; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Expression -{ - private static final Logger logger = LoggerFactory.getLogger(Expression.class); - - public enum Op - { - EQ, MATCH, PREFIX, SUFFIX, CONTAINS, NOT_EQ, RANGE, IN; - - public static Op valueOf(Operator operator) - { - switch (operator) - { - case EQ: - return EQ; - - case IN: - return IN; - - case NEQ: - return NOT_EQ; - - case LT: - case GT: - case LTE: - case GTE: - return RANGE; - - case LIKE_PREFIX: - return PREFIX; - - case LIKE_SUFFIX: - return SUFFIX; - - case LIKE_CONTAINS: - return CONTAINS; - - case LIKE_MATCHES: - return MATCH; - - default: - throw new IllegalArgumentException("unknown operator: " + operator); - } - } - } - - private final QueryController controller; - - public final AbstractAnalyzer analyzer; - - public final ColumnIndex index; - public final AbstractType validator; - public final boolean isLiteral; - - @VisibleForTesting - protected Op operation; - - public Bound lower, upper; - public List exclusions = new ArrayList<>(); - - public Expression(Expression other) - { - this(other.controller, other.index); - operation = other.operation; - } - - public Expression(QueryController controller, ColumnIndex columnIndex) - { - this.controller = controller; - this.index = columnIndex; - this.analyzer = columnIndex.getAnalyzer(); - this.validator = columnIndex.getValidator(); - this.isLiteral = columnIndex.isLiteral(); - } - - @VisibleForTesting - public Expression(String name, AbstractType validator) - { - this(null, new ColumnIndex(UTF8Type.instance, ColumnMetadata.regularColumn("sasi", "internal", name, validator), null)); - } - - public Expression setLower(Bound newLower) - { - lower = newLower == null ? null : new Bound(newLower.value, newLower.inclusive); - return this; - } - - public Expression setUpper(Bound newUpper) - { - upper = newUpper == null ? null : new Bound(newUpper.value, newUpper.inclusive); - return this; - } - - public Expression setOp(Op op) - { - this.operation = op; - return this; - } - - public Expression add(Operator op, ByteBuffer value) - { - boolean lowerInclusive = false, upperInclusive = false; - switch (op) - { - case LIKE_PREFIX: - case LIKE_SUFFIX: - case LIKE_CONTAINS: - case LIKE_MATCHES: - case EQ: - lower = new Bound(value, true); - upper = lower; - operation = Op.valueOf(op); - break; - - case NEQ: - // index expressions are priority sorted - // and NOT_EQ is the lowest priority, which means that operation type - // is always going to be set before reaching it in case of RANGE or EQ. - if (operation == null) - { - operation = Op.NOT_EQ; - lower = new Bound(value, true); - upper = lower; - } - else - exclusions.add(value); - break; - - case LTE: - if (index.getDefinition().isReversedType()) - lowerInclusive = true; - else - upperInclusive = true; - case LT: - operation = Op.RANGE; - if (index.getDefinition().isReversedType()) - lower = new Bound(value, lowerInclusive); - else - upper = new Bound(value, upperInclusive); - break; - - case GTE: - if (index.getDefinition().isReversedType()) - upperInclusive = true; - else - lowerInclusive = true; - case GT: - operation = Op.RANGE; - if (index.getDefinition().isReversedType()) - upper = new Bound(value, upperInclusive); - else - lower = new Bound(value, lowerInclusive); - - break; - } - - return this; - } - - public Expression addExclusion(ByteBuffer value) - { - exclusions.add(value); - return this; - } - - public boolean isSatisfiedBy(ByteBuffer value) - { - if (!TypeUtil.isValid(value, validator)) - { - int size = value.remaining(); - if ((value = TypeUtil.tryUpcast(value, validator)) == null) - { - logger.error("Can't cast value for {} to size accepted by {}, value size is {}.", - index.getColumnName(), - validator, - FBUtilities.prettyPrintMemory(size)); - return false; - } - } - - if (lower != null) - { - // suffix check - if (isLiteral) - { - if (!validateStringValue(value, lower.value)) - return false; - } - else - { - // range or (not-)equals - (mainly) for numeric values - int cmp = validator.compare(lower.value, value); - - // in case of (NOT_)EQ lower == upper - if (operation == Op.EQ || operation == Op.NOT_EQ) - return cmp == 0; - - if (cmp > 0 || (cmp == 0 && !lower.inclusive)) - return false; - } - } - - if (upper != null && lower != upper) - { - // string (prefix or suffix) check - if (isLiteral) - { - if (!validateStringValue(value, upper.value)) - return false; - } - else - { - // range - mainly for numeric values - int cmp = validator.compare(upper.value, value); - if (cmp < 0 || (cmp == 0 && !upper.inclusive)) - return false; - } - } - - // as a last step let's check exclusions for the given field, - // this covers EQ/RANGE with exclusions. - for (ByteBuffer term : exclusions) - { - if (isLiteral && validateStringValue(value, term)) - return false; - else if (validator.compare(term, value) == 0) - return false; - } - - return true; - } - - private boolean validateStringValue(ByteBuffer columnValue, ByteBuffer requestedValue) - { - analyzer.reset(columnValue.duplicate()); - while (analyzer.hasNext()) - { - ByteBuffer term = analyzer.next(); - - boolean isMatch = false; - switch (operation) - { - case EQ: - case MATCH: - // Operation.isSatisfiedBy handles conclusion on !=, - // here we just need to make sure that term matched it - case NOT_EQ: - isMatch = validator.compare(term, requestedValue) == 0; - break; - - case PREFIX: - isMatch = ByteBufferUtil.startsWith(term, requestedValue); - break; - - case SUFFIX: - isMatch = ByteBufferUtil.endsWith(term, requestedValue); - break; - - case CONTAINS: - isMatch = ByteBufferUtil.contains(term, requestedValue); - break; - } - - if (isMatch) - return true; - } - - return false; - } - - public Op getOp() - { - return operation; - } - - public void checkpoint() - { - if (controller == null) - return; - - controller.checkpoint(); - } - - public boolean hasLower() - { - return lower != null; - } - - public boolean hasUpper() - { - return upper != null; - } - - public boolean isLowerSatisfiedBy(OnDiskIndex.DataTerm term) - { - if (!hasLower()) - return true; - - int cmp = term.compareTo(validator, lower.value, operation == Op.RANGE && !isLiteral); - return cmp > 0 || cmp == 0 && lower.inclusive; - } - - public boolean isUpperSatisfiedBy(OnDiskIndex.DataTerm term) - { - if (!hasUpper()) - return true; - - int cmp = term.compareTo(validator, upper.value, operation == Op.RANGE && !isLiteral); - return cmp < 0 || cmp == 0 && upper.inclusive; - } - - public boolean isIndexed() - { - return index.isIndexed(); - } - - public String toString() - { - return String.format("Expression{name: %s, op: %s, lower: (%s, %s), upper: (%s, %s), exclusions: %s}", - index.getColumnName(), - operation, - lower == null ? "null" : validator.getString(lower.value), - lower != null && lower.inclusive, - upper == null ? "null" : validator.getString(upper.value), - upper != null && upper.inclusive, - Iterators.toString(Iterators.transform(exclusions.iterator(), validator::getString))); - } - - public int hashCode() - { - return new HashCodeBuilder().append(index.getColumnName()) - .append(operation) - .append(validator) - .append(lower).append(upper) - .append(exclusions).build(); - } - - public boolean equals(Object other) - { - if (!(other instanceof Expression)) - return false; - - if (this == other) - return true; - - Expression o = (Expression) other; - - return Objects.equals(index.getColumnName(), o.index.getColumnName()) - && validator.equals(o.validator) - && operation == o.operation - && Objects.equals(lower, o.lower) - && Objects.equals(upper, o.upper) - && exclusions.equals(o.exclusions); - } - - public static class Bound - { - public final ByteBuffer value; - public final boolean inclusive; - - public Bound(ByteBuffer value, boolean inclusive) - { - this.value = value; - this.inclusive = inclusive; - } - - public boolean equals(Object other) - { - if (!(other instanceof Bound)) - return false; - - Bound o = (Bound) other; - return value.equals(o.value) && inclusive == o.inclusive; - } - - public int hashCode() - { - HashCodeBuilder builder = new HashCodeBuilder(); - builder.append(value); - builder.append(inclusive); - return builder.toHashCode(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/plan/Operation.java b/src/java/org/apache/cassandra/index/sasi/plan/Operation.java deleted file mode 100644 index 5b7f43328202..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/plan/Operation.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.plan; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.*; - -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.ColumnMetadata.Kind; -import org.apache.cassandra.cql3.Operator; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.rows.Unfiltered; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Expression.Op; -import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.*; - -import org.apache.cassandra.utils.FBUtilities; - -public class Operation extends RangeIterator -{ - public enum OperationType - { - AND, OR; - - public boolean apply(boolean a, boolean b) - { - switch (this) - { - case OR: - return a | b; - - case AND: - return a & b; - - default: - throw new AssertionError(); - } - } - } - - private final QueryController controller; - - protected final OperationType op; - protected final ListMultimap expressions; - protected final RangeIterator range; - - protected Operation left, right; - - private Operation(OperationType operation, - QueryController controller, - ListMultimap expressions, - RangeIterator range, - Operation left, Operation right) - { - super(range); - - this.op = operation; - this.controller = controller; - this.expressions = expressions; - this.range = range; - - this.left = left; - this.right = right; - } - - /** - * Recursive "satisfies" checks based on operation - * and data from the lower level members using depth-first search - * and bubbling the results back to the top level caller. - * - * Most of the work here is done by {@link #localSatisfiedBy(Unfiltered, Row, boolean)} - * see it's comment for details, if there are no local expressions - * assigned to Operation it will call satisfiedBy(Row) on it's children. - * - * Query: first_name = X AND (last_name = Y OR address = XYZ AND street = IL AND city = C) OR (state = 'CA' AND country = 'US') - * Row: key1: (first_name: X, last_name: Z, address: XYZ, street: IL, city: C, state: NY, country:US) - * - * #1 OR - * / \ - * #2 (first_name) AND AND (state, country) - * \ - * #3 (last_name) OR - * \ - * #4 AND (address, street, city) - * - * - * Evaluation of the key1 is top-down depth-first search: - * - * --- going down --- - * Level #1 is evaluated, OR expression has to pull results from it's children which are at level #2 and OR them together, - * Level #2 AND (state, country) could be be evaluated right away, AND (first_name) refers to it's "right" child from level #3 - * Level #3 OR (last_name) requests results from level #4 - * Level #4 AND (address, street, city) does logical AND between it's 3 fields, returns result back to level #3. - * --- bubbling up --- - * Level #3 computes OR between AND (address, street, city) result and it's "last_name" expression - * Level #2 computes AND between "first_name" and result of level #3, AND (state, country) which is already computed - * Level #1 does OR between results of AND (first_name) and AND (state, country) and returns final result. - * - * @param currentCluster The row cluster to check. - * @param staticRow The static row associated with current cluster. - * @param allowMissingColumns allow columns value to be null. - * @return true if give Row satisfied all of the expressions in the tree, - * false otherwise. - */ - public boolean satisfiedBy(Unfiltered currentCluster, Row staticRow, boolean allowMissingColumns) - { - boolean sideL, sideR; - - if (expressions == null || expressions.isEmpty()) - { - sideL = left != null && left.satisfiedBy(currentCluster, staticRow, allowMissingColumns); - sideR = right != null && right.satisfiedBy(currentCluster, staticRow, allowMissingColumns); - - // one of the expressions was skipped - // because it had no indexes attached - if (left == null) - return sideR; - } - else - { - sideL = localSatisfiedBy(currentCluster, staticRow, allowMissingColumns); - - // if there is no right it means that this expression - // is last in the sequence, we can just return result from local expressions - if (right == null) - return sideL; - - sideR = right.satisfiedBy(currentCluster, staticRow, allowMissingColumns); - } - - - return op.apply(sideL, sideR); - } - - /** - * Check every expression in the analyzed list to figure out if the - * columns in the give row match all of the based on the operation - * set to the current operation node. - * - * The algorithm is as follows: for every given expression from analyzed - * list get corresponding column from the Row: - * - apply {@link Expression#isSatisfiedBy(ByteBuffer)} - * method to figure out if it's satisfied; - * - apply logical operation between boolean accumulator and current boolean result; - * - if result == false and node's operation is AND return right away; - * - * After all of the expressions have been evaluated return resulting accumulator variable. - * - * Example: - * - * Operation = (op: AND, columns: [first_name = p, 5 < age < 7, last_name: y]) - * Row = (first_name: pavel, last_name: y, age: 6, timestamp: 15) - * - * #1 get "first_name" = p (expressions) - * - row-get "first_name" => "pavel" - * - compare "pavel" against "p" => true (current) - * - set accumulator current => true (because this is expression #1) - * - * #2 get "last_name" = y (expressions) - * - row-get "last_name" => "y" - * - compare "y" against "y" => true (current) - * - set accumulator to accumulator & current => true - * - * #3 get 5 < "age" < 7 (expressions) - * - row-get "age" => "6" - * - compare 5 < 6 < 7 => true (current) - * - set accumulator to accumulator & current => true - * - * #4 return accumulator => true (row satisfied all of the conditions) - * - * @param currentCluster The row cluster to check. - * @param staticRow The static row associated with current cluster. - * @param allowMissingColumns allow columns value to be null. - * @return true if give Row satisfied all of the analyzed expressions, - * false otherwise. - */ - private boolean localSatisfiedBy(Unfiltered currentCluster, Row staticRow, boolean allowMissingColumns) - { - if (currentCluster == null || !currentCluster.isRow()) - return false; - - final long now = FBUtilities.nowInSeconds(); - boolean result = false; - int idx = 0; - - for (ColumnMetadata column : expressions.keySet()) - { - if (column.kind == Kind.PARTITION_KEY) - continue; - - ByteBuffer value = ColumnIndex.getValueOf(column, column.kind == Kind.STATIC ? staticRow : (Row) currentCluster, now); - boolean isMissingColumn = value == null; - - if (!allowMissingColumns && isMissingColumn) - throw new IllegalStateException("All indexed columns should be included into the column slice, missing: " + column); - - boolean isMatch = false; - // If there is a column with multiple expressions that effectively means an OR - // e.g. comment = 'x y z' could be split into 'comment' EQ 'x', 'comment' EQ 'y', 'comment' EQ 'z' - // by analyzer, in situation like that we only need to check if at least one of expressions matches, - // and there is no hit on the NOT_EQ (if any) which are always at the end of the filter list. - // Loop always starts from the end of the list, which makes it possible to break after the last - // NOT_EQ condition on first EQ/RANGE condition satisfied, instead of checking every - // single expression in the column filter list. - List filters = expressions.get(column); - for (int i = filters.size() - 1; i >= 0; i--) - { - Expression expression = filters.get(i); - isMatch = !isMissingColumn && expression.isSatisfiedBy(value); - if (expression.getOp() == Op.NOT_EQ) - { - // since this is NOT_EQ operation we have to - // inverse match flag (to check against other expressions), - // and break in case of negative inverse because that means - // that it's a positive hit on the not-eq clause. - isMatch = !isMatch; - if (!isMatch) - break; - } // if it was a match on EQ/RANGE or column is missing - else if (isMatch || isMissingColumn) - break; - } - - if (idx++ == 0) - { - result = isMatch; - continue; - } - - result = op.apply(result, isMatch); - - // exit early because we already got a single false - if (op == OperationType.AND && !result) - return false; - } - - return idx == 0 || result; - } - - @VisibleForTesting - protected static ListMultimap analyzeGroup(QueryController controller, - OperationType op, - List expressions) - { - ListMultimap analyzed = ArrayListMultimap.create(); - - // sort all of the expressions in the operation by name and priority of the logical operator - // this gives us an efficient way to handle inequality and combining into ranges without extra processing - // and converting expressions from one type to another. - Collections.sort(expressions, (a, b) -> { - int cmp = a.column().compareTo(b.column()); - return cmp == 0 ? -Integer.compare(getPriority(a.operator()), getPriority(b.operator())) : cmp; - }); - - for (final RowFilter.Expression e : expressions) - { - ColumnIndex columnIndex = controller.getIndex(e); - List perColumn = analyzed.get(e.column()); - - if (columnIndex == null) - columnIndex = new ColumnIndex(controller.getKeyValidator(), e.column(), null); - - AbstractAnalyzer analyzer = columnIndex.getAnalyzer(); - analyzer.reset(e.getIndexValue().duplicate()); - - // EQ/LIKE_*/NOT_EQ can have multiple expressions e.g. text = "Hello World", - // becomes text = "Hello" OR text = "World" because "space" is always interpreted as a split point (by analyzer), - // NOT_EQ is made an independent expression only in case of pre-existing multiple EQ expressions, or - // if there is no EQ operations and NOT_EQ is met or a single NOT_EQ expression present, - // in such case we know exactly that there would be no more EQ/RANGE expressions for given column - // since NOT_EQ has the lowest priority. - boolean isMultiExpression = false; - switch (e.operator()) - { - case EQ: - isMultiExpression = false; - break; - - case LIKE_PREFIX: - case LIKE_SUFFIX: - case LIKE_CONTAINS: - case LIKE_MATCHES: - isMultiExpression = true; - break; - - case NEQ: - isMultiExpression = (perColumn.size() == 0 || perColumn.size() > 1 - || (perColumn.size() == 1 && perColumn.get(0).getOp() == Op.NOT_EQ)); - break; - } - - if (isMultiExpression) - { - while (analyzer.hasNext()) - { - final ByteBuffer token = analyzer.next(); - perColumn.add(new Expression(controller, columnIndex).add(e.operator(), token)); - } - } - else - // "range" or not-equals operator, combines both bounds together into the single expression, - // iff operation of the group is AND, otherwise we are forced to create separate expressions, - // not-equals is combined with the range iff operator is AND. - { - Expression range; - if (perColumn.size() == 0 || op != OperationType.AND) - perColumn.add((range = new Expression(controller, columnIndex))); - else - range = Iterables.getLast(perColumn); - - while (analyzer.hasNext()) - range.add(e.operator(), analyzer.next()); - } - } - - return analyzed; - } - - private static int getPriority(Operator op) - { - switch (op) - { - case EQ: - return 5; - - case LIKE_PREFIX: - case LIKE_SUFFIX: - case LIKE_CONTAINS: - case LIKE_MATCHES: - return 4; - - case GTE: - case GT: - return 3; - - case LTE: - case LT: - return 2; - - case NEQ: - return 1; - - default: - return 0; - } - } - - protected Token computeNext() - { - return range != null && range.hasNext() ? range.next() : endOfData(); - } - - protected void performSkipTo(Long nextToken) - { - if (range != null) - range.skipTo(nextToken); - } - - public void close() throws IOException - { - controller.releaseIndexes(this); - } - - public static class Builder - { - private final QueryController controller; - - protected final OperationType op; - protected final List expressions; - - protected Builder left, right; - - public Builder(OperationType operation, QueryController controller, RowFilter.Expression... columns) - { - this.op = operation; - this.controller = controller; - this.expressions = new ArrayList<>(); - Collections.addAll(expressions, columns); - } - - public Builder setRight(Builder operation) - { - this.right = operation; - return this; - } - - public Builder setLeft(Builder operation) - { - this.left = operation; - return this; - } - - public void add(RowFilter.Expression e) - { - expressions.add(e); - } - - public void add(Collection newExpressions) - { - if (expressions != null) - expressions.addAll(newExpressions); - } - - public Operation complete() - { - if (!expressions.isEmpty()) - { - ListMultimap analyzedExpressions = analyzeGroup(controller, op, expressions); - RangeIterator.Builder range = controller.getIndexes(op, analyzedExpressions.values()); - - Operation rightOp = null; - if (right != null) - { - rightOp = right.complete(); - range.add(rightOp); - } - - return new Operation(op, controller, analyzedExpressions, range.build(), null, rightOp); - } - else - { - Operation leftOp = null, rightOp = null; - boolean leftIndexes = false, rightIndexes = false; - - if (left != null) - { - leftOp = left.complete(); - leftIndexes = leftOp != null && leftOp.range != null; - } - - if (right != null) - { - rightOp = right.complete(); - rightIndexes = rightOp != null && rightOp.range != null; - } - - RangeIterator join; - /** - * Operation should allow one of it's sub-trees to wrap no indexes, that is related to the fact that we - * have to accept defined-but-not-indexed columns as well as key range as IndexExpressions. - * - * Two cases are possible: - * - * only left child produced indexed iterators, that could happen when there are two columns - * or key range on the right: - * - * AND - * / \ - * OR \ - * / \ AND - * a b / \ - * key key - * - * only right child produced indexed iterators: - * - * AND - * / \ - * AND a - * / \ - * key key - */ - if (leftIndexes && !rightIndexes) - join = leftOp; - else if (!leftIndexes && rightIndexes) - join = rightOp; - else if (leftIndexes) - { - RangeIterator.Builder builder = op == OperationType.OR - ? RangeUnionIterator.builder() - : RangeIntersectionIterator.builder(); - - join = builder.add(leftOp).add(rightOp).build(); - } - else - throw new AssertionError("both sub-trees have 0 indexes."); - - return new Operation(op, controller, null, join, leftOp, rightOp); - } - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java b/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java deleted file mode 100644 index 432dd8067705..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.plan; - -import java.util.*; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - -import com.google.common.collect.Sets; - -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.DataRange; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.ReadCommand; -import org.apache.cassandra.db.ReadExecutionController; -import org.apache.cassandra.db.SinglePartitionReadCommand; -import org.apache.cassandra.db.filter.DataLimits; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.rows.UnfilteredRowIterator; -import org.apache.cassandra.index.sasi.SASIIndex; -import org.apache.cassandra.index.sasi.SSTableIndex; -import org.apache.cassandra.index.sasi.TermIterator; -import org.apache.cassandra.index.sasi.conf.ColumnIndex; -import org.apache.cassandra.index.sasi.conf.view.View; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.exceptions.TimeQuotaExceededException; -import org.apache.cassandra.index.sasi.plan.Operation.OperationType; -import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator; -import org.apache.cassandra.index.sasi.utils.RangeIterator; -import org.apache.cassandra.index.sasi.utils.RangeUnionIterator; -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.Pair; - -import static org.apache.cassandra.utils.Clock.Global.nanoTime; - -public class QueryController -{ - private final long executionQuota; - private final long executionStart; - - private final ColumnFamilyStore cfs; - private final ReadCommand command; - private final DataRange range; - private final Map, List>> resources = new HashMap<>(); - - public QueryController(ColumnFamilyStore cfs, ReadCommand command, long timeQuotaMs) - { - this.cfs = cfs; - this.command = command; - this.range = command.dataRange(); - this.executionQuota = TimeUnit.MILLISECONDS.toNanos(timeQuotaMs); - this.executionStart = nanoTime(); - } - - public TableMetadata metadata() - { - return command.metadata(); - } - - public Collection getExpressions() - { - return command.rowFilter().getExpressions(); - } - - public DataRange dataRange() - { - return command.dataRange(); - } - - public AbstractType getKeyValidator() - { - return cfs.metadata().partitionKeyType; - } - - @Nullable - public ColumnIndex getIndex(RowFilter.Expression expression) - { - return cfs.indexManager.getBestIndexFor(expression, SASIIndex.class).map(SASIIndex::getIndex).orElse(null); - } - - public UnfilteredRowIterator getPartition(DecoratedKey key, ReadExecutionController executionController) - { - if (key == null) - throw new NullPointerException(); - try - { - SinglePartitionReadCommand partition = SinglePartitionReadCommand.create(cfs.metadata(), - command.nowInSec(), - command.columnFilter(), - command.rowFilter().withoutExpressions(), - DataLimits.NONE, - key, - command.clusteringIndexFilter(key)); - - return partition.queryMemtableAndDisk(cfs, executionController); - } - finally - { - checkpoint(); - } - } - - /** - * Build a range iterator from the given list of expressions by applying given operation (OR/AND). - * Building of such iterator involves index search, results of which are persisted in the internal resources list - * and can be released later via {@link QueryController#releaseIndexes(Operation)}. - * - * @param op The operation type to coalesce expressions with. - * @param expressions The expressions to build range iterator from (expressions with not results are ignored). - * - * @return The range builder based on given expressions and operation type. - */ - public RangeIterator.Builder getIndexes(OperationType op, Collection expressions) - { - if (resources.containsKey(expressions)) - throw new IllegalArgumentException("Can't process the same expressions multiple times."); - - RangeIterator.Builder builder = op == OperationType.OR - ? RangeUnionIterator.builder() - : RangeIntersectionIterator.builder(); - - Set>> view = getView(op, expressions).entrySet(); - List> perIndexUnions = new ArrayList<>(view.size()); - - for (Map.Entry> e : view) - { - RangeIterator index = TermIterator.build(e.getKey(), e.getValue()); - - builder.add(index); - perIndexUnions.add(index); - } - - resources.put(expressions, perIndexUnions); - return builder; - } - - public void checkpoint() - { - long executionTime = (nanoTime() - executionStart); - - if (executionTime >= executionQuota) - throw new TimeQuotaExceededException( - "Command '" + command + "' took too long " + - "(" + TimeUnit.NANOSECONDS.toMillis(executionTime) + - " >= " + TimeUnit.NANOSECONDS.toMillis(executionQuota) + "ms)."); - } - - public void releaseIndexes(Operation operation) - { - if (operation.expressions != null) - releaseIndexes(resources.remove(operation.expressions.values())); - } - - private void releaseIndexes(List> indexes) - { - if (indexes == null) - return; - - indexes.forEach(FileUtils::closeQuietly); - } - - public void finish() - { - resources.values().forEach(this::releaseIndexes); - } - - private Map> getView(OperationType op, Collection expressions) - { - // first let's determine the primary expression if op is AND - Pair> primary = (op == OperationType.AND) ? calculatePrimary(expressions) : null; - - Map> indexes = new HashMap<>(); - for (Expression e : expressions) - { - // NO_EQ and non-index column query should only act as FILTER BY for satisfiedBy(Row) method - // because otherwise it likely to go through the whole index. - if (!e.isIndexed() || e.getOp() == Expression.Op.NOT_EQ) - continue; - - // primary expression, we'll have to add as is - if (primary != null && e.equals(primary.left)) - { - indexes.put(primary.left, primary.right); - continue; - } - - View view = e.index.getView(); - if (view == null) - continue; - - Set readers = new HashSet<>(); - if (primary != null && primary.right.size() > 0) - { - for (SSTableIndex index : primary.right) - readers.addAll(view.match(index.minKey(), index.maxKey())); - } - else - { - readers.addAll(applyScope(view.match(e))); - } - - indexes.put(e, readers); - } - - return indexes; - } - - private Pair> calculatePrimary(Collection expressions) - { - Expression expression = null; - Set primaryIndexes = Collections.emptySet(); - - for (Expression e : expressions) - { - if (!e.isIndexed()) - continue; - - View view = e.index.getView(); - if (view == null) - continue; - - Set indexes = applyScope(view.match(e)); - if (expression == null || primaryIndexes.size() > indexes.size()) - { - primaryIndexes = indexes; - expression = e; - } - } - - return expression == null ? null : Pair.create(expression, primaryIndexes); - } - - private Set applyScope(Set indexes) - { - return Sets.filter(indexes, index -> { - SSTableReader sstable = index.getSSTable(); - return range.startKey().compareTo(sstable.getLast()) <= 0 && (range.stopKey().isMinimum() || sstable.getFirst().compareTo(range.stopKey()) <= 0); - }); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/plan/SASIIndexQueryPlan.java b/src/java/org/apache/cassandra/index/sasi/plan/SASIIndexQueryPlan.java deleted file mode 100644 index 06664a18343b..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/plan/SASIIndexQueryPlan.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.plan; - -import javax.annotation.Nullable; - -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.index.SingletonIndexQueryPlan; -import org.apache.cassandra.index.sasi.SASIIndex; - -public class SASIIndexQueryPlan extends SingletonIndexQueryPlan -{ - private SASIIndexQueryPlan(SASIIndex index, RowFilter postIndexFilter) - { - super(index, postIndexFilter); - } - - @Nullable - public static SASIIndexQueryPlan create(SASIIndex index, RowFilter rowFilter) - { - for (RowFilter.Expression e : rowFilter.getExpressions()) - { - if (index.supportsExpression(e.column(), e.operator())) - return new SASIIndexQueryPlan(index, index.getPostIndexQueryFilter(rowFilter)); - } - - return null; - } - - @Override - public boolean supportsReplicaFilteringProtection(RowFilter rowFilter) - { - return false; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/plan/SASIIndexSearcher.java b/src/java/org/apache/cassandra/index/sasi/plan/SASIIndexSearcher.java deleted file mode 100644 index 6da4f8a9692e..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/plan/SASIIndexSearcher.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.plan; - -import java.util.*; - -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.rows.*; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.index.Index; -import org.apache.cassandra.index.sasi.disk.Token; -import org.apache.cassandra.index.sasi.plan.Operation.OperationType; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.AbstractIterator; - -public class SASIIndexSearcher implements Index.Searcher -{ - private final ReadCommand command; - private final QueryController controller; - - public SASIIndexSearcher(ColumnFamilyStore cfs, ReadCommand command, long executionQuotaMs) - { - this.command = command; - this.controller = new QueryController(cfs, command, executionQuotaMs); - } - - @Override - public ReadCommand command() - { - return command; - } - - /** - * Converts expressions into operation tree (which is currently just a single AND). - * - * Operation tree allows us to do a couple of important optimizations - * namely, group flattening for AND operations (query rewrite), expression bounds checks, - * "satisfies by" checks for resulting rows with an early exit. - * - * @return root of the operations tree. - */ - private Operation analyze() - { - try - { - Operation.Builder and = new Operation.Builder(OperationType.AND, controller); - controller.getExpressions().forEach(and::add); - return and.complete(); - } - catch (Exception | Error e) - { - controller.finish(); - throw e; - } - } - - @Override - public UnfilteredPartitionIterator search(ReadExecutionController executionController) - { - return new ResultIterator(analyze(), controller, executionController); - } - - private static class ResultIterator extends AbstractIterator implements UnfilteredPartitionIterator - { - private final AbstractBounds keyRange; - private final Operation operationTree; - private final QueryController controller; - private final ReadExecutionController executionController; - - private Iterator currentKeys = null; - - public ResultIterator(Operation operationTree, QueryController controller, ReadExecutionController executionController) - { - this.keyRange = controller.dataRange().keyRange(); - this.operationTree = operationTree; - this.controller = controller; - this.executionController = executionController; - if (operationTree != null) - operationTree.skipTo((Long) keyRange.left.getToken().getTokenValue()); - } - - protected UnfilteredRowIterator computeNext() - { - if (operationTree == null) - return endOfData(); - - for (;;) - { - if (currentKeys == null || !currentKeys.hasNext()) - { - if (!operationTree.hasNext()) - return endOfData(); - - Token token = operationTree.next(); - currentKeys = token.iterator(); - } - - while (currentKeys.hasNext()) - { - DecoratedKey key = currentKeys.next(); - - if (!keyRange.right.isMinimum() && keyRange.right.compareTo(key) < 0) - return endOfData(); - - if (!keyRange.inclusiveLeft() && key.compareTo(keyRange.left) == 0) - continue; - - try (UnfilteredRowIterator partition = controller.getPartition(key, executionController)) - { - Row staticRow = partition.staticRow(); - List clusters = new ArrayList<>(); - - while (partition.hasNext()) - { - Unfiltered row = partition.next(); - if (operationTree.satisfiedBy(row, staticRow, true)) - clusters.add(row); - } - - if (!clusters.isEmpty()) - return new PartitionIterator(partition, clusters); - } - } - } - } - - private static class PartitionIterator extends AbstractUnfilteredRowIterator - { - private final Iterator rows; - - public PartitionIterator(UnfilteredRowIterator partition, Collection content) - { - super(partition.metadata(), - partition.partitionKey(), - partition.partitionLevelDeletion(), - partition.columns(), - partition.staticRow(), - partition.isReverseOrder(), - partition.stats()); - - rows = content.iterator(); - } - - @Override - protected Unfiltered computeNext() - { - return rows.hasNext() ? rows.next() : endOfData(); - } - } - - public TableMetadata metadata() - { - return controller.metadata(); - } - - public void close() - { - FileUtils.closeQuietly(operationTree); - controller.finish(); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/ByteTerm.java b/src/java/org/apache/cassandra/index/sasi/sa/ByteTerm.java deleted file mode 100644 index c7bbab781c0a..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/ByteTerm.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.db.marshal.AbstractType; - -public class ByteTerm extends Term -{ - public ByteTerm(int position, ByteBuffer value, TokenTreeBuilder tokens) - { - super(position, value, tokens); - } - - public ByteBuffer getTerm() - { - return value.duplicate(); - } - - public ByteBuffer getSuffix(int start) - { - return (ByteBuffer) value.duplicate().position(value.position() + start); - } - - public int compareTo(AbstractType comparator, Term other) - { - return comparator.compare(value, (ByteBuffer) other.value); - } - - public int length() - { - return value.remaining(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/CharTerm.java b/src/java/org/apache/cassandra/index/sasi/sa/CharTerm.java deleted file mode 100644 index 533b566441f3..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/CharTerm.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; - -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.db.marshal.AbstractType; - -import com.google.common.base.Charsets; - -public class CharTerm extends Term -{ - public CharTerm(int position, CharBuffer value, TokenTreeBuilder tokens) - { - super(position, value, tokens); - } - - public ByteBuffer getTerm() - { - return Charsets.UTF_8.encode(value.duplicate()); - } - - public ByteBuffer getSuffix(int start) - { - return Charsets.UTF_8.encode(value.subSequence(value.position() + start, value.remaining())); - } - - public int compareTo(AbstractType comparator, Term other) - { - return value.compareTo((CharBuffer) other.value); - } - - public int length() - { - return value.length(); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/IndexedTerm.java b/src/java/org/apache/cassandra/index/sasi/sa/IndexedTerm.java deleted file mode 100644 index 8e27134349ef..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/IndexedTerm.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.sa; - -import java.nio.ByteBuffer; - -public class IndexedTerm -{ - private final ByteBuffer term; - private final boolean isPartial; - - public IndexedTerm(ByteBuffer term, boolean isPartial) - { - this.term = term; - this.isPartial = isPartial; - } - - public ByteBuffer getBytes() - { - return term; - } - - public boolean isPartial() - { - return isPartial; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/IntegralSA.java b/src/java/org/apache/cassandra/index/sasi/sa/IntegralSA.java deleted file mode 100644 index 5f04876ed9fd..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/IntegralSA.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; - -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.utils.Pair; - -public class IntegralSA extends SA -{ - public IntegralSA(AbstractType comparator, OnDiskIndexBuilder.Mode mode) - { - super(comparator, mode); - } - - public Term getTerm(ByteBuffer termValue, TokenTreeBuilder tokens) - { - return new ByteTerm(charCount, termValue, tokens); - } - - public TermIterator finish() - { - return new IntegralSuffixIterator(); - } - - - private class IntegralSuffixIterator extends TermIterator - { - private final Iterator> termIterator; - - public IntegralSuffixIterator() - { - Collections.sort(terms, new Comparator>() - { - public int compare(Term a, Term b) - { - return a.compareTo(comparator, b); - } - }); - - termIterator = terms.iterator(); - } - - public ByteBuffer minTerm() - { - return terms.get(0).getTerm(); - } - - public ByteBuffer maxTerm() - { - return terms.get(terms.size() - 1).getTerm(); - } - - protected Pair computeNext() - { - if (!termIterator.hasNext()) - return endOfData(); - - Term term = termIterator.next(); - return Pair.create(new IndexedTerm(term.getTerm(), false), term.getTokens().finish()); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/SA.java b/src/java/org/apache/cassandra/index/sasi/sa/SA.java deleted file mode 100644 index 75f9f92e8ec7..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/SA.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode; -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.db.marshal.AbstractType; - -public abstract class SA -{ - protected final AbstractType comparator; - protected final Mode mode; - - protected final List> terms = new ArrayList<>(); - protected int charCount = 0; - - public SA(AbstractType comparator, Mode mode) - { - this.comparator = comparator; - this.mode = mode; - } - - public Mode getMode() - { - return mode; - } - - public void add(ByteBuffer termValue, TokenTreeBuilder tokens) - { - Term term = getTerm(termValue, tokens); - terms.add(term); - charCount += term.length(); - } - - public abstract TermIterator finish(); - - protected abstract Term getTerm(ByteBuffer termValue, TokenTreeBuilder tokens); -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/SuffixSA.java b/src/java/org/apache/cassandra/index/sasi/sa/SuffixSA.java deleted file mode 100644 index 9e1c76a85003..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/SuffixSA.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; - -import org.apache.cassandra.index.sasi.disk.DynamicTokenTreeBuilder; -import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder; -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.utils.LongTimSort; -import org.apache.cassandra.utils.Pair; - -import com.google.common.base.Charsets; - -public class SuffixSA extends SA -{ - public SuffixSA(AbstractType comparator, OnDiskIndexBuilder.Mode mode) - { - super(comparator, mode); - } - - protected Term getTerm(ByteBuffer termValue, TokenTreeBuilder tokens) - { - return new CharTerm(charCount, Charsets.UTF_8.decode(termValue.duplicate()), tokens); - } - - public TermIterator finish() - { - return new SASuffixIterator(); - } - - private class SASuffixIterator extends TermIterator - { - - private static final int COMPLETE_BIT = 31; - - private final long[] suffixes; - - private int current = 0; - private IndexedTerm lastProcessedSuffix; - private TokenTreeBuilder container; - - public SASuffixIterator() - { - // each element has term index and char position encoded as two 32-bit integers - // to avoid binary search per suffix while sorting suffix array. - suffixes = new long[charCount]; - - long termIndex = -1, currentTermLength = -1; - boolean isComplete = false; - for (int i = 0; i < charCount; i++) - { - if (i >= currentTermLength || currentTermLength == -1) - { - Term currentTerm = terms.get((int) ++termIndex); - currentTermLength = currentTerm.getPosition() + currentTerm.length(); - isComplete = true; - } - - suffixes[i] = (termIndex << 32) | i; - if (isComplete) - suffixes[i] |= (1L << COMPLETE_BIT); - - isComplete = false; - } - - LongTimSort.sort(suffixes, (a, b) -> { - Term aTerm = terms.get((int) (a >>> 32)); - Term bTerm = terms.get((int) (b >>> 32)); - return comparator.compare(aTerm.getSuffix(clearCompleteBit(a) - aTerm.getPosition()), - bTerm.getSuffix(clearCompleteBit(b) - bTerm.getPosition())); - }); - } - - private int clearCompleteBit(long value) - { - return (int) (value & ~(1L << COMPLETE_BIT)); - } - - private Pair suffixAt(int position) - { - long index = suffixes[position]; - Term term = terms.get((int) (index >>> 32)); - boolean isPartitial = (index & ((long) 1 << 31)) == 0; - return Pair.create(new IndexedTerm(term.getSuffix(clearCompleteBit(index) - term.getPosition()), isPartitial), term.getTokens()); - } - - public ByteBuffer minTerm() - { - return suffixAt(0).left.getBytes(); - } - - public ByteBuffer maxTerm() - { - return suffixAt(suffixes.length - 1).left.getBytes(); - } - - protected Pair computeNext() - { - while (true) - { - if (current >= suffixes.length) - { - if (lastProcessedSuffix == null) - return endOfData(); - - Pair result = finishSuffix(); - - lastProcessedSuffix = null; - return result; - } - - Pair suffix = suffixAt(current++); - - if (lastProcessedSuffix == null) - { - lastProcessedSuffix = suffix.left; - container = new DynamicTokenTreeBuilder(suffix.right); - } - else if (comparator.compare(lastProcessedSuffix.getBytes(), suffix.left.getBytes()) == 0) - { - lastProcessedSuffix = suffix.left; - container.add(suffix.right); - } - else - { - Pair result = finishSuffix(); - - lastProcessedSuffix = suffix.left; - container = new DynamicTokenTreeBuilder(suffix.right); - - return result; - } - } - } - - private Pair finishSuffix() - { - return Pair.create(lastProcessedSuffix, container.finish()); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/sa/Term.java b/src/java/org/apache/cassandra/index/sasi/sa/Term.java deleted file mode 100644 index fe6eca8c3571..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/Term.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.Buffer; -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.db.marshal.AbstractType; - -public abstract class Term -{ - protected final int position; - protected final T value; - protected TokenTreeBuilder tokens; - - - public Term(int position, T value, TokenTreeBuilder tokens) - { - this.position = position; - this.value = value; - this.tokens = tokens; - } - - public int getPosition() - { - return position; - } - - public abstract ByteBuffer getTerm(); - public abstract ByteBuffer getSuffix(int start); - - public TokenTreeBuilder getTokens() - { - return tokens; - } - - public abstract int compareTo(AbstractType comparator, Term other); - - public abstract int length(); - -} - diff --git a/src/java/org/apache/cassandra/index/sasi/sa/TermIterator.java b/src/java/org/apache/cassandra/index/sasi/sa/TermIterator.java deleted file mode 100644 index c8572a9d43bd..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/sa/TermIterator.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.sa; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.utils.Pair; - -import com.google.common.collect.AbstractIterator; - -public abstract class TermIterator extends AbstractIterator> -{ - public abstract ByteBuffer minTerm(); - public abstract ByteBuffer maxTerm(); -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/CombinedTerm.java b/src/java/org/apache/cassandra/index/sasi/utils/CombinedTerm.java deleted file mode 100644 index cc327bcfdd58..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/CombinedTerm.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.nio.ByteBuffer; -import java.util.*; - -import org.apache.cassandra.index.sasi.disk.*; -import org.apache.cassandra.index.sasi.disk.OnDiskIndex.DataTerm; -import org.apache.cassandra.db.marshal.AbstractType; - -public class CombinedTerm implements CombinedValue -{ - private final AbstractType comparator; - private final DataTerm term; - private final List mergedTerms = new ArrayList<>(); - - public CombinedTerm(AbstractType comparator, DataTerm term) - { - this.comparator = comparator; - this.term = term; - } - - public ByteBuffer getTerm() - { - return term.getTerm(); - } - - public boolean isPartial() - { - return term.isPartial(); - } - - public RangeIterator getTokenIterator() - { - RangeIterator.Builder union = RangeUnionIterator.builder(); - union.add(term.getTokens()); - mergedTerms.stream().map(OnDiskIndex.DataTerm::getTokens).forEach(union::add); - - return union.build(); - } - - public TokenTreeBuilder getTokenTreeBuilder() - { - return new StaticTokenTreeBuilder(this).finish(); - } - - public void merge(CombinedValue other) - { - if (!(other instanceof CombinedTerm)) - return; - - CombinedTerm o = (CombinedTerm) other; - - assert comparator == o.comparator; - - mergedTerms.add(o.term); - } - - public DataTerm get() - { - return term; - } - - public int compareTo(CombinedValue o) - { - return term.compareTo(comparator, o.get().getTerm()); - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/CombinedTermIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/CombinedTermIterator.java deleted file mode 100644 index 683ca7adece5..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/CombinedTermIterator.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.index.sasi.disk.Descriptor; -import org.apache.cassandra.index.sasi.disk.OnDiskIndex; -import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder; -import org.apache.cassandra.index.sasi.sa.IndexedTerm; -import org.apache.cassandra.index.sasi.sa.TermIterator; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.utils.Pair; - -public class CombinedTermIterator extends TermIterator -{ - final Descriptor descriptor; - final RangeIterator union; - final ByteBuffer min; - final ByteBuffer max; - - public CombinedTermIterator(OnDiskIndex... sas) - { - this(Descriptor.CURRENT, sas); - } - - public CombinedTermIterator(Descriptor d, OnDiskIndex... parts) - { - descriptor = d; - union = OnDiskIndexIterator.union(parts); - - AbstractType comparator = parts[0].getComparator(); // assumes all SAs have same comparator - ByteBuffer minimum = parts[0].minTerm(); - ByteBuffer maximum = parts[0].maxTerm(); - - for (int i = 1; i < parts.length; i++) - { - OnDiskIndex part = parts[i]; - if (part == null) - continue; - - minimum = comparator.compare(minimum, part.minTerm()) > 0 ? part.minTerm() : minimum; - maximum = comparator.compare(maximum, part.maxTerm()) < 0 ? part.maxTerm() : maximum; - } - - min = minimum; - max = maximum; - } - - public ByteBuffer minTerm() - { - return min; - } - - public ByteBuffer maxTerm() - { - return max; - } - - protected Pair computeNext() - { - if (!union.hasNext()) - { - return endOfData(); - } - else - { - CombinedTerm term = union.next(); - return Pair.create(new IndexedTerm(term.getTerm(), term.isPartial()), term.getTokenTreeBuilder()); - } - - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/CombinedValue.java b/src/java/org/apache/cassandra/index/sasi/utils/CombinedValue.java deleted file mode 100644 index ca5f9beb7812..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/CombinedValue.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -public interface CombinedValue extends Comparable> -{ - void merge(CombinedValue other); - - V get(); -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/MappedBuffer.java b/src/java/org/apache/cassandra/index/sasi/utils/MappedBuffer.java deleted file mode 100644 index 0899be6b2bc1..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/MappedBuffer.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.io.Closeable; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel.MapMode; - -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.io.util.ChannelProxy; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.io.util.RandomAccessReader; - -import com.google.common.annotations.VisibleForTesting; - -public class MappedBuffer implements Closeable -{ - private final MappedByteBuffer[] pages; - - private long position, limit; - private final long capacity; - private final int pageSize, sizeBits; - - private MappedBuffer(MappedBuffer other) - { - this.sizeBits = other.sizeBits; - this.pageSize = other.pageSize; - this.position = other.position; - this.limit = other.limit; - this.capacity = other.capacity; - this.pages = other.pages; - } - - public MappedBuffer(RandomAccessReader file) - { - this(file.getChannel(), 30); - } - - public MappedBuffer(ChannelProxy file) - { - this(file, 30); - } - - @VisibleForTesting - protected MappedBuffer(ChannelProxy file, int numPageBits) - { - if (numPageBits > Integer.SIZE - 1) - throw new IllegalArgumentException("page size can't be bigger than 1G"); - - sizeBits = numPageBits; - pageSize = 1 << sizeBits; - position = 0; - limit = capacity = file.size(); - pages = new MappedByteBuffer[(int) (file.size() / pageSize) + 1]; - - try - { - long offset = 0; - for (int i = 0; i < pages.length; i++) - { - long pageSize = Math.min(this.pageSize, (capacity - offset)); - pages[i] = file.map(MapMode.READ_ONLY, offset, pageSize); - offset += pageSize; - } - } - finally - { - file.close(); - } - } - - public int comparePageTo(long offset, int length, AbstractType comparator, ByteBuffer other) - { - return comparator.compare(getPageRegion(offset, length), other); - } - - public long capacity() - { - return capacity; - } - - public long position() - { - return position; - } - - public MappedBuffer position(long newPosition) - { - if (newPosition < 0 || newPosition > limit) - throw new IllegalArgumentException("position: " + newPosition + ", limit: " + limit); - - position = newPosition; - return this; - } - - public long limit() - { - return limit; - } - - public MappedBuffer limit(long newLimit) - { - if (newLimit < position || newLimit > capacity) - throw new IllegalArgumentException(); - - limit = newLimit; - return this; - } - - public long remaining() - { - return limit - position; - } - - public boolean hasRemaining() - { - return remaining() > 0; - } - - public byte get() - { - return get(position++); - } - - public byte get(long pos) - { - return pages[getPage(pos)].get(getPageOffset(pos)); - } - - public short getShort() - { - short value = getShort(position); - position += 2; - return value; - } - - public short getShort(long pos) - { - if (isPageAligned(pos, 2)) - return pages[getPage(pos)].getShort(getPageOffset(pos)); - - int ch1 = get(pos) & 0xff; - int ch2 = get(pos + 1) & 0xff; - return (short) ((ch1 << 8) + ch2); - } - - public int getInt() - { - int value = getInt(position); - position += 4; - return value; - } - - public int getInt(long pos) - { - if (isPageAligned(pos, 4)) - return pages[getPage(pos)].getInt(getPageOffset(pos)); - - int ch1 = get(pos) & 0xff; - int ch2 = get(pos + 1) & 0xff; - int ch3 = get(pos + 2) & 0xff; - int ch4 = get(pos + 3) & 0xff; - - return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4); - } - - public long getLong() - { - long value = getLong(position); - position += 8; - return value; - } - - - public long getLong(long pos) - { - // fast path if the long could be retrieved from a single page - // that would avoid multiple expensive look-ups into page array. - return (isPageAligned(pos, 8)) - ? pages[getPage(pos)].getLong(getPageOffset(pos)) - : ((long) (getInt(pos)) << 32) + (getInt(pos + 4) & 0xFFFFFFFFL); - } - - public ByteBuffer getPageRegion(long position, int length) - { - if (!isPageAligned(position, length)) - throw new IllegalArgumentException(String.format("range: %s-%s wraps more than one page", position, length)); - - ByteBuffer slice = pages[getPage(position)].duplicate(); - - int pageOffset = getPageOffset(position); - slice.position(pageOffset).limit(pageOffset + length); - - return slice; - } - - public MappedBuffer duplicate() - { - return new MappedBuffer(this); - } - - public void close() - { - /* - * Try forcing the unmapping of pages using undocumented unsafe sun APIs. - * If this fails (non Sun JVM), we'll have to wait for the GC to finalize the mapping. - * If this works and a thread tries to access any page, hell will unleash on earth. - */ - try - { - for (MappedByteBuffer segment : pages) - FileUtils.clean(segment); - } - catch (Exception e) - { - // This is not supposed to happen - } - } - - private int getPage(long position) - { - return (int) (position >> sizeBits); - } - - private int getPageOffset(long position) - { - return (int) (position & pageSize - 1); - } - - private boolean isPageAligned(long position, int length) - { - return pageSize - (getPageOffset(position) + length) > 0; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/OnDiskIndexIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/OnDiskIndexIterator.java deleted file mode 100644 index ae97cabfc2f8..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/OnDiskIndexIterator.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.io.IOException; -import java.util.Iterator; - -import org.apache.cassandra.index.sasi.disk.OnDiskIndex; -import org.apache.cassandra.index.sasi.disk.OnDiskIndex.DataTerm; -import org.apache.cassandra.db.marshal.AbstractType; - -public class OnDiskIndexIterator extends RangeIterator -{ - private final AbstractType comparator; - private final Iterator terms; - - public OnDiskIndexIterator(OnDiskIndex index) - { - super(index.min(), index.max(), Long.MAX_VALUE); - - this.comparator = index.getComparator(); - this.terms = index.iterator(); - } - - public static RangeIterator union(OnDiskIndex... union) - { - RangeUnionIterator.Builder builder = RangeUnionIterator.builder(); - for (OnDiskIndex e : union) - { - if (e != null) - builder.add(new OnDiskIndexIterator(e)); - } - - return builder.build(); - } - - protected CombinedTerm computeNext() - { - return terms.hasNext() ? new CombinedTerm(comparator, terms.next()) : endOfData(); - } - - protected void performSkipTo(DataTerm nextToken) - { - throw new UnsupportedOperationException(); - } - - public void close() throws IOException - {} -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java deleted file mode 100644 index 331f4edff349..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.PriorityQueue; - -import com.google.common.collect.Iterators; -import org.apache.cassandra.io.util.FileUtils; - -import com.google.common.annotations.VisibleForTesting; - -public class RangeIntersectionIterator -{ - protected enum Strategy - { - BOUNCE, LOOKUP, ADAPTIVE - } - - public static , D extends CombinedValue> Builder builder() - { - return builder(Strategy.ADAPTIVE); - } - - @VisibleForTesting - protected static , D extends CombinedValue> Builder builder(Strategy strategy) - { - return new Builder<>(strategy); - } - - public static class Builder, D extends CombinedValue> extends RangeIterator.Builder - { - private final Strategy strategy; - - public Builder(Strategy strategy) - { - super(IteratorType.INTERSECTION); - this.strategy = strategy; - } - - protected RangeIterator buildIterator() - { - // if the range is disjoint or we have an intersection with an empty set, - // we can simply return an empty iterator, because it's not going to produce any results. - if (statistics.isDisjoint()) - return new EmptyRangeIterator<>(); - - if (rangeCount() == 1) - return ranges.poll(); - - switch (strategy) - { - case LOOKUP: - return new LookupIntersectionIterator<>(statistics, ranges); - - case BOUNCE: - return new BounceIntersectionIterator<>(statistics, ranges); - - case ADAPTIVE: - return statistics.sizeRatio() <= 0.01d - ? new LookupIntersectionIterator<>(statistics, ranges) - : new BounceIntersectionIterator<>(statistics, ranges); - - default: - throw new IllegalStateException("Unknown strategy: " + strategy); - } - } - } - - private static abstract class AbstractIntersectionIterator, D extends CombinedValue> extends RangeIterator - { - protected final PriorityQueue> ranges; - - private AbstractIntersectionIterator(Builder.Statistics statistics, PriorityQueue> ranges) - { - super(statistics); - this.ranges = ranges; - } - - public void close() throws IOException - { - for (RangeIterator range : ranges) - FileUtils.closeQuietly(range); - } - } - - /** - * Iterator which performs intersection of multiple ranges by using bouncing (merge-join) technique to identify - * common elements in the given ranges. Aforementioned "bounce" works as follows: range queue is poll'ed for the - * range with the smallest current token (main loop), that token is used to {@link RangeIterator#skipTo(Comparable)} - * other ranges, if token produced by {@link RangeIterator#skipTo(Comparable)} is equal to current "candidate" token, - * both get merged together and the same operation is repeated for next range from the queue, if returned token - * is not equal than candidate, candidate's range gets put back into the queue and the main loop gets repeated until - * next intersection token is found or at least one iterator runs out of tokens. - * - * This technique is every efficient to jump over gaps in the ranges. - * - * @param The type used to sort ranges. - * @param The container type which is going to be returned by {@link Iterator#next()}. - */ - @VisibleForTesting - protected static class BounceIntersectionIterator, D extends CombinedValue> extends AbstractIntersectionIterator - { - private BounceIntersectionIterator(Builder.Statistics statistics, PriorityQueue> ranges) - { - super(statistics, ranges); - } - - protected D computeNext() - { - List> processed = null; - - while (!ranges.isEmpty()) - { - RangeIterator head = ranges.poll(); - - // jump right to the beginning of the intersection or return next element - if (head.getCurrent().compareTo(getMinimum()) < 0) - head.skipTo(getMinimum()); - - D candidate = head.hasNext() ? head.next() : null; - if (candidate == null || candidate.get().compareTo(getMaximum()) > 0) - { - ranges.add(head); - return endOfData(); - } - - if (processed == null) - processed = new ArrayList<>(); - - boolean intersectsAll = true, exhausted = false; - while (!ranges.isEmpty()) - { - RangeIterator range = ranges.poll(); - - // found a range which doesn't overlap with one (or possibly more) other range(s) - if (!isOverlapping(head, range)) - { - exhausted = true; - intersectsAll = false; - break; - } - - D point = range.skipTo(candidate.get()); - - if (point == null) // other range is exhausted - { - exhausted = true; - intersectsAll = false; - break; - } - - processed.add(range); - - if (candidate.get().equals(point.get())) - { - candidate.merge(point); - // advance skipped range to the next element if any - Iterators.getNext(range, null); - } - else - { - intersectsAll = false; - break; - } - } - - ranges.add(head); - - ranges.addAll(processed); - processed.clear(); - - if (exhausted) - return endOfData(); - - if (intersectsAll) - return candidate; - } - - return endOfData(); - } - - protected void performSkipTo(K nextToken) - { - List> skipped = new ArrayList<>(); - - while (!ranges.isEmpty()) - { - RangeIterator range = ranges.poll(); - range.skipTo(nextToken); - skipped.add(range); - } - - for (RangeIterator range : skipped) - ranges.add(range); - } - } - - /** - * Iterator which performs a linear scan over a primary range (the smallest of the ranges) - * and O(log(n)) lookup into secondary ranges using values from the primary iterator. - * This technique is efficient when one of the intersection ranges is smaller than others - * e.g. ratio 0.01d (default), in such situation scan + lookup is more efficient comparing - * to "bounce" merge because "bounce" distance is never going to be big. - * - * @param The type used to sort ranges. - * @param The container type which is going to be returned by {@link Iterator#next()}. - */ - @VisibleForTesting - protected static class LookupIntersectionIterator, D extends CombinedValue> extends AbstractIntersectionIterator - { - private final RangeIterator smallestIterator; - - private LookupIntersectionIterator(Builder.Statistics statistics, PriorityQueue> ranges) - { - super(statistics, ranges); - - smallestIterator = statistics.minRange; - - if (smallestIterator.getCurrent().compareTo(getMinimum()) < 0) - smallestIterator.skipTo(getMinimum()); - } - - protected D computeNext() - { - while (smallestIterator.hasNext()) - { - D candidate = smallestIterator.next(); - K token = candidate.get(); - - boolean intersectsAll = true; - for (RangeIterator range : ranges) - { - // avoid checking against self, much cheaper than changing queue comparator - // to compare based on the size and re-populating such queue. - if (range.equals(smallestIterator)) - continue; - - // found a range which doesn't overlap with one (or possibly more) other range(s) - if (!isOverlapping(smallestIterator, range)) - return endOfData(); - - D point = range.skipTo(token); - - if (point == null) // one of the iterators is exhausted - return endOfData(); - - if (!point.get().equals(token)) - { - intersectsAll = false; - break; - } - - candidate.merge(point); - } - - if (intersectsAll) - return candidate; - } - - return endOfData(); - } - - protected void performSkipTo(K nextToken) - { - smallestIterator.skipTo(nextToken); - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/RangeIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/RangeIterator.java deleted file mode 100644 index c7db7737052a..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/RangeIterator.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.io.Closeable; -import java.util.Comparator; -import java.util.List; -import java.util.PriorityQueue; - -import com.google.common.annotations.VisibleForTesting; - -import org.apache.cassandra.utils.AbstractGuavaIterator; - -public abstract class RangeIterator, T extends CombinedValue> extends AbstractGuavaIterator implements Closeable -{ - private final K min, max; - private final long count; - private K current; - - protected RangeIterator(Builder.Statistics statistics) - { - this(statistics.min, statistics.max, statistics.tokenCount); - } - - public RangeIterator(RangeIterator range) - { - this(range == null ? null : range.min, range == null ? null : range.max, range == null ? -1 : range.count); - } - - public RangeIterator(K min, K max, long count) - { - if (min == null || max == null || count == 0) - assert min == null && max == null && (count == 0 || count == -1); - - this.min = min; - this.current = min; - this.max = max; - this.count = count; - } - - public final K getMinimum() - { - return min; - } - - public final K getCurrent() - { - return current; - } - - public final K getMaximum() - { - return max; - } - - public final long getCount() - { - return count; - } - - /** - * When called, this iterators current position should - * be skipped forwards until finding either: - * 1) an element equal to or bigger than next - * 2) the end of the iterator - * - * @param nextToken value to skip the iterator forward until matching - * - * @return The next current token after the skip was performed - */ - public final T skipTo(K nextToken) - { - if (min == null || max == null) - return endOfData(); - - if (current.compareTo(nextToken) >= 0) - return next == null ? recomputeNext() : next; - - if (max.compareTo(nextToken) < 0) - return endOfData(); - - performSkipTo(nextToken); - return recomputeNext(); - } - - protected abstract void performSkipTo(K nextToken); - - protected T recomputeNext() - { - return tryToComputeNext() ? peek() : endOfData(); - } - - protected boolean tryToComputeNext() - { - boolean hasNext = super.tryToComputeNext(); - current = hasNext ? next.get() : getMaximum(); - return hasNext; - } - - public static abstract class Builder, D extends CombinedValue> - { - public enum IteratorType - { - UNION, INTERSECTION - } - - @VisibleForTesting - protected final Statistics statistics; - - @VisibleForTesting - protected final PriorityQueue> ranges; - - public Builder(IteratorType type) - { - statistics = new Statistics<>(type); - ranges = new PriorityQueue<>(16, (Comparator>) (a, b) -> a.getCurrent().compareTo(b.getCurrent())); - } - - public K getMinimum() - { - return statistics.min; - } - - public K getMaximum() - { - return statistics.max; - } - - public long getTokenCount() - { - return statistics.tokenCount; - } - - public int rangeCount() - { - return ranges.size(); - } - - public Builder add(RangeIterator range) - { - if (range == null) - return this; - - if (range.getCount() > 0) - ranges.add(range); - statistics.update(range); - - return this; - } - - public Builder add(List> ranges) - { - if (ranges == null || ranges.isEmpty()) - return this; - - ranges.forEach(this::add); - return this; - } - - public final RangeIterator build() - { - if (rangeCount() == 0) - return new EmptyRangeIterator<>(); - else - return buildIterator(); - } - - public static class EmptyRangeIterator, D extends CombinedValue> extends RangeIterator - { - EmptyRangeIterator() { super(null, null, 0); } - public D computeNext() { return endOfData(); } - protected void performSkipTo(K nextToken) { } - public void close() { } - } - - protected abstract RangeIterator buildIterator(); - - public static class Statistics, D extends CombinedValue> - { - protected final IteratorType iteratorType; - - protected K min, max; - protected long tokenCount; - - // iterator with the least number of items - protected RangeIterator minRange; - // iterator with the most number of items - protected RangeIterator maxRange; - - // tracks if all of the added ranges overlap, which is useful in case of intersection, - // as it gives direct answer as to such iterator is going to produce any results. - private boolean isOverlapping = true; - - public Statistics(IteratorType iteratorType) - { - this.iteratorType = iteratorType; - } - - /** - * Update statistics information with the given range. - * - * Updates min/max of the combined range, token count and - * tracks range with the least/most number of tokens. - * - * @param range The range to update statistics with. - */ - public void update(RangeIterator range) - { - switch (iteratorType) - { - case UNION: - min = nullSafeMin(min, range.getMinimum()); - max = nullSafeMax(max, range.getMaximum()); - break; - - case INTERSECTION: - // minimum of the intersection is the biggest minimum of individual iterators - min = nullSafeMax(min, range.getMinimum()); - // maximum of the intersection is the smallest maximum of individual iterators - max = nullSafeMin(max, range.getMaximum()); - break; - - default: - throw new IllegalStateException("Unknown iterator type: " + iteratorType); - } - - // check if new range is disjoint with already added ranges, which means that this intersection - // is not going to produce any results, so we can cleanup range storage and never added anything to it. - isOverlapping &= isOverlapping(min, max, range); - - minRange = minRange == null ? range : min(minRange, range); - maxRange = maxRange == null ? range : max(maxRange, range); - - tokenCount += range.getCount(); - } - - private RangeIterator min(RangeIterator a, RangeIterator b) - { - return a.getCount() > b.getCount() ? b : a; - } - - private RangeIterator max(RangeIterator a, RangeIterator b) - { - return a.getCount() > b.getCount() ? a : b; - } - - public boolean isDisjoint() - { - return !isOverlapping; - } - - public double sizeRatio() - { - return minRange.getCount() * 1d / maxRange.getCount(); - } - } - } - - @VisibleForTesting - protected static , D extends CombinedValue> boolean isOverlapping(RangeIterator a, RangeIterator b) - { - return isOverlapping(a.getCurrent(), a.getMaximum(), b); - } - - /** - * Ranges are overlapping the following cases: - * - * * When they have a common subrange: - * - * min b.current max b.max - * +---------|--------------+------------| - * - * b.current min max b.max - * |--------------+---------+------------| - * - * min b.current b.max max - * +----------|-------------|------------+ - * - * - * If either range is empty, they're disjoint. - */ - @VisibleForTesting - protected static , D extends CombinedValue> boolean isOverlapping(K min, K max, RangeIterator b) - { - return (min != null && max != null) && - b.getCount() != 0 && - (min.compareTo(b.getMaximum()) <= 0 && b.getCurrent().compareTo(max) <= 0); - } - - @SuppressWarnings("unchecked") - private static T nullSafeMin(T a, T b) - { - if (a == null) return b; - if (b == null) return a; - - return a.compareTo(b) > 0 ? b : a; - } - - @SuppressWarnings("unchecked") - private static T nullSafeMax(T a, T b) - { - if (a == null) return b; - if (b == null) return a; - - return a.compareTo(b) > 0 ? a : b; - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java deleted file mode 100644 index f2295699704f..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; - -import org.apache.cassandra.io.util.FileUtils; - -/** - * Range Union Iterator is used to return sorted stream of elements from multiple RangeIterator instances. - * - * PriorityQueue is used as a sorting mechanism for the ranges, where each computeNext() operation would poll - * from the queue (and push when done), which returns range that contains the smallest element, because - * sorting is done on the moving window of range iteration {@link RangeIterator#getCurrent()}. Once retrieved - * the smallest element (return candidate) is attempted to be merged with other ranges, because there could - * be equal elements in adjacent ranges, such ranges are poll'ed only if their {@link RangeIterator#getCurrent()} - * equals to the return candidate. - * - * @param The type used to sort ranges. - * @param The container type which is going to be returned by {@link Iterator#next()}. - */ -public class RangeUnionIterator, D extends CombinedValue> extends RangeIterator -{ - private final PriorityQueue> ranges; - - private RangeUnionIterator(Builder.Statistics statistics, PriorityQueue> ranges) - { - super(statistics); - this.ranges = ranges; - } - - public D computeNext() - { - RangeIterator head = null; - - while (!ranges.isEmpty()) - { - head = ranges.poll(); - if (head.hasNext()) - break; - - FileUtils.closeQuietly(head); - } - - if (head == null || !head.hasNext()) - return endOfData(); - - D candidate = head.next(); - - List> processedRanges = new ArrayList<>(); - - if (head.hasNext()) - processedRanges.add(head); - else - FileUtils.closeQuietly(head); - - while (!ranges.isEmpty()) - { - // peek here instead of poll is an optimization - // so we can re-insert less ranges back if candidate - // is less than head of the current range. - RangeIterator range = ranges.peek(); - - int cmp = candidate.get().compareTo(range.getCurrent()); - - assert cmp <= 0; - - if (cmp < 0) - { - break; // candidate is smaller than next token, return immediately - } - else if (cmp == 0) - { - candidate.merge(range.next()); // consume and merge - - range = ranges.poll(); - // re-prioritize changed range - - if (range.hasNext()) - processedRanges.add(range); - else - FileUtils.closeQuietly(range); - } - } - - ranges.addAll(processedRanges); - return candidate; - } - - protected void performSkipTo(K nextToken) - { - List> changedRanges = new ArrayList<>(); - - while (!ranges.isEmpty()) - { - if (ranges.peek().getCurrent().compareTo(nextToken) >= 0) - break; - - RangeIterator head = ranges.poll(); - - if (head.getMaximum().compareTo(nextToken) >= 0) - { - head.skipTo(nextToken); - changedRanges.add(head); - continue; - } - - FileUtils.closeQuietly(head); - } - - ranges.addAll(changedRanges.stream().collect(Collectors.toList())); - } - - public void close() throws IOException - { - ranges.forEach(FileUtils::closeQuietly); - } - - public static , D extends CombinedValue> Builder builder() - { - return new Builder<>(); - } - - public static , D extends CombinedValue> RangeIterator build(List> tokens) - { - return new Builder().add(tokens).build(); - } - - public static class Builder, D extends CombinedValue> extends RangeIterator.Builder - { - public Builder() - { - super(IteratorType.UNION); - } - - protected RangeIterator buildIterator() - { - switch (rangeCount()) - { - case 1: - return ranges.poll(); - - default: - return new RangeUnionIterator<>(statistics, ranges); - } - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/TypeUtil.java b/src/java/org/apache/cassandra/index/sasi/utils/TypeUtil.java deleted file mode 100644 index 9815233cf362..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/TypeUtil.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.index.sasi.utils; - -import java.nio.ByteBuffer; - -import org.apache.cassandra.db.marshal.*; -import org.apache.cassandra.serializers.MarshalException; - -public class TypeUtil -{ - public static boolean isValid(ByteBuffer term, AbstractType validator) - { - try - { - validator.validate(term); - return true; - } - catch (MarshalException e) - { - return false; - } - } - - public static ByteBuffer tryUpcast(ByteBuffer term, AbstractType validator) - { - if (term.remaining() == 0) - return null; - - try - { - if (validator instanceof Int32Type && term.remaining() == 2) - { - return Int32Type.instance.decompose((int) term.getShort(term.position())); - } - else if (validator instanceof LongType) - { - long upcastToken; - - switch (term.remaining()) - { - case 2: - upcastToken = (long) term.getShort(term.position()); - break; - - case 4: - upcastToken = (long) Int32Type.instance.compose(term); - break; - - default: - upcastToken = Long.parseLong(UTF8Type.instance.getString(term)); - } - - return LongType.instance.decompose(upcastToken); - } - else if (validator instanceof DoubleType && term.remaining() == 4) - { - return DoubleType.instance.decompose((double) FloatType.instance.compose(term)); - } - - // maybe it was a string after all - return validator.fromString(UTF8Type.instance.getString(term)); - } - catch (Exception e) - { - return null; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractPatriciaTrie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractPatriciaTrie.java deleted file mode 100644 index 8067ccc30cdc..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractPatriciaTrie.java +++ /dev/null @@ -1,1152 +0,0 @@ -/* - * Copyright 2005-2010 Roger Kapsi, Sam Berlin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -import java.util.AbstractCollection; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.ConcurrentModificationException; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; - -import org.apache.cassandra.index.sasi.utils.trie.Cursor.Decision; - -/** - * This class implements the base PATRICIA algorithm and everything that - * is related to the {@link Map} interface. - */ -abstract class AbstractPatriciaTrie extends AbstractTrie -{ - private static final long serialVersionUID = -2303909182832019043L; - - /** - * The root node of the {@link Trie}. - */ - final TrieEntry root = new TrieEntry<>(null, null, -1); - - /** - * Each of these fields are initialized to contain an instance of the - * appropriate view the first time this view is requested. The views are - * stateless, so there's no reason to create more than one of each. - */ - private transient volatile Set keySet; - private transient volatile Collection values; - private transient volatile Set> entrySet; - - /** - * The current size of the {@link Trie} - */ - private int size = 0; - - /** - * The number of times this {@link Trie} has been modified. - * It's used to detect concurrent modifications and fail-fast - * the {@link Iterator}s. - */ - transient int modCount = 0; - - public AbstractPatriciaTrie(KeyAnalyzer keyAnalyzer) - { - super(keyAnalyzer); - } - - public AbstractPatriciaTrie(KeyAnalyzer keyAnalyzer, Map m) - { - super(keyAnalyzer); - putAll(m); - } - - @Override - public void clear() - { - root.key = null; - root.bitIndex = -1; - root.value = null; - - root.parent = null; - root.left = root; - root.right = null; - root.predecessor = root; - - size = 0; - incrementModCount(); - } - - @Override - public int size() - { - return size; - } - - /** - * A helper method to increment the {@link Trie} size - * and the modification counter. - */ - void incrementSize() - { - size++; - incrementModCount(); - } - - /** - * A helper method to decrement the {@link Trie} size - * and increment the modification counter. - */ - void decrementSize() - { - size--; - incrementModCount(); - } - - /** - * A helper method to increment the modification counter. - */ - private void incrementModCount() - { - ++modCount; - } - - @Override - public V put(K key, V value) - { - if (key == null) - throw new NullPointerException("Key cannot be null"); - - int lengthInBits = lengthInBits(key); - - // The only place to store a key with a length - // of zero bits is the root node - if (lengthInBits == 0) - { - if (root.isEmpty()) - incrementSize(); - else - incrementModCount(); - - return root.setKeyValue(key, value); - } - - TrieEntry found = getNearestEntryForKey(key); - if (compareKeys(key, found.key)) - { - if (found.isEmpty()) // <- must be the root - incrementSize(); - else - incrementModCount(); - - return found.setKeyValue(key, value); - } - - int bitIndex = bitIndex(key, found.key); - if (!Tries.isOutOfBoundsIndex(bitIndex)) - { - if (Tries.isValidBitIndex(bitIndex)) // in 99.999...9% the case - { - /* NEW KEY+VALUE TUPLE */ - TrieEntry t = new TrieEntry<>(key, value, bitIndex); - addEntry(t); - incrementSize(); - return null; - } - else if (Tries.isNullBitKey(bitIndex)) - { - // A bits of the Key are zero. The only place to - // store such a Key is the root Node! - - /* NULL BIT KEY */ - if (root.isEmpty()) - incrementSize(); - else - incrementModCount(); - - return root.setKeyValue(key, value); - - } - else if (Tries.isEqualBitKey(bitIndex)) - { - // This is a very special and rare case. - - /* REPLACE OLD KEY+VALUE */ - if (found != root) - { - incrementModCount(); - return found.setKeyValue(key, value); - } - } - } - - throw new IndexOutOfBoundsException("Failed to put: " - + key + " -> " + value + ", " + bitIndex); - } - - /** - * Adds the given {@link TrieEntry} to the {@link Trie} - */ - TrieEntry addEntry(TrieEntry entry) - { - TrieEntry current = root.left; - TrieEntry path = root; - - while(true) - { - if (current.bitIndex >= entry.bitIndex || current.bitIndex <= path.bitIndex) - { - entry.predecessor = entry; - - if (!isBitSet(entry.key, entry.bitIndex)) - { - entry.left = entry; - entry.right = current; - } - else - { - entry.left = current; - entry.right = entry; - } - - entry.parent = path; - if (current.bitIndex >= entry.bitIndex) - current.parent = entry; - - // if we inserted an uplink, set the predecessor on it - if (current.bitIndex <= path.bitIndex) - current.predecessor = entry; - - if (path == root || !isBitSet(entry.key, path.bitIndex)) - path.left = entry; - else - path.right = entry; - - return entry; - } - - path = current; - - current = !isBitSet(entry.key, current.bitIndex) - ? current.left : current.right; - } - } - - @Override - public V get(Object k) - { - TrieEntry entry = getEntry(k); - return entry != null ? entry.getValue() : null; - } - - /** - * Returns the entry associated with the specified key in the - * AbstractPatriciaTrie. Returns null if the map contains no mapping - * for this key. - * - * This may throw ClassCastException if the object is not of type K. - */ - TrieEntry getEntry(Object k) - { - K key = Tries.cast(k); - if (key == null) - return null; - - TrieEntry entry = getNearestEntryForKey(key); - return !entry.isEmpty() && compareKeys(key, entry.key) ? entry : null; - } - - @Override - public Map.Entry select(K key) - { - Reference> reference = new Reference<>(); - return !selectR(root.left, -1, key, reference) ? reference.get() : null; - } - - @Override - public Map.Entry select(K key, Cursor cursor) - { - Reference> reference = new Reference<>(); - selectR(root.left, -1, key, cursor, reference); - return reference.get(); - } - - /** - * This is equivalent to the other {@link #selectR(TrieEntry, int, - * K, Cursor, Reference)} method but without its overhead - * because we're selecting only one best matching Entry from the - * {@link Trie}. - */ - private boolean selectR(TrieEntry h, int bitIndex, final K key, final Reference> reference) - { - if (h.bitIndex <= bitIndex) - { - // If we hit the root Node and it is empty - // we have to look for an alternative best - // matching node. - if (!h.isEmpty()) - { - reference.set(h); - return false; - } - return true; - } - - if (!isBitSet(key, h.bitIndex)) - { - if (selectR(h.left, h.bitIndex, key, reference)) - { - return selectR(h.right, h.bitIndex, key, reference); - } - } - else - { - if (selectR(h.right, h.bitIndex, key, reference)) - { - return selectR(h.left, h.bitIndex, key, reference); - } - } - - return false; - } - - /** - * - */ - private boolean selectR(TrieEntry h, int bitIndex, - final K key, final Cursor cursor, - final Reference> reference) - { - if (h.bitIndex <= bitIndex) - { - if (!h.isEmpty()) - { - Decision decision = cursor.select(h); - switch(decision) - { - case REMOVE: - throw new UnsupportedOperationException("Cannot remove during select"); - - case EXIT: - reference.set(h); - return false; // exit - - case REMOVE_AND_EXIT: - TrieEntry entry = new TrieEntry<>(h.getKey(), h.getValue(), -1); - reference.set(entry); - removeEntry(h); - return false; - - case CONTINUE: - // fall through. - } - } - - return true; // continue - } - - if (!isBitSet(key, h.bitIndex)) - { - if (selectR(h.left, h.bitIndex, key, cursor, reference)) - { - return selectR(h.right, h.bitIndex, key, cursor, reference); - } - } - else - { - if (selectR(h.right, h.bitIndex, key, cursor, reference)) - { - return selectR(h.left, h.bitIndex, key, cursor, reference); - } - } - - return false; - } - - @Override - public Map.Entry traverse(Cursor cursor) - { - TrieEntry entry = nextEntry(null); - while (entry != null) - { - TrieEntry current = entry; - - Decision decision = cursor.select(current); - entry = nextEntry(current); - - switch(decision) - { - case EXIT: - return current; - - case REMOVE: - removeEntry(current); - break; // out of switch, stay in while loop - - case REMOVE_AND_EXIT: - Map.Entry value = new TrieEntry<>(current.getKey(), current.getValue(), -1); - removeEntry(current); - return value; - - case CONTINUE: // do nothing. - } - } - - return null; - } - - @Override - public boolean containsKey(Object k) - { - if (k == null) - return false; - - K key = Tries.cast(k); - TrieEntry entry = getNearestEntryForKey(key); - return !entry.isEmpty() && compareKeys(key, entry.key); - } - - @Override - public Set> entrySet() - { - if (entrySet == null) - entrySet = new EntrySet(); - - return entrySet; - } - - @Override - public Set keySet() - { - if (keySet == null) - keySet = new KeySet(); - return keySet; - } - - @Override - public Collection values() - { - if (values == null) - values = new Values(); - return values; - } - - /** - * {@inheritDoc} - * - * @throws ClassCastException if provided key is of an incompatible type - */ - @Override - public V remove(Object k) - { - if (k == null) - return null; - - K key = Tries.cast(k); - TrieEntry current = root.left; - TrieEntry path = root; - while (true) - { - if (current.bitIndex <= path.bitIndex) - { - if (!current.isEmpty() && compareKeys(key, current.key)) - { - return removeEntry(current); - } - else - { - return null; - } - } - - path = current; - current = !isBitSet(key, current.bitIndex) ? current.left : current.right; - } - } - - /** - * Returns the nearest entry for a given key. This is useful - * for finding knowing if a given key exists (and finding the value - * for it), or for inserting the key. - * - * The actual get implementation. This is very similar to - * selectR but with the exception that it might return the - * root Entry even if it's empty. - */ - TrieEntry getNearestEntryForKey(K key) - { - TrieEntry current = root.left; - TrieEntry path = root; - - while(true) - { - if (current.bitIndex <= path.bitIndex) - return current; - - path = current; - current = !isBitSet(key, current.bitIndex) ? current.left : current.right; - } - } - - /** - * Removes a single entry from the {@link Trie}. - * - * If we found a Key (Entry h) then figure out if it's - * an internal (hard to remove) or external Entry (easy - * to remove) - */ - V removeEntry(TrieEntry h) - { - if (h != root) - { - if (h.isInternalNode()) - { - removeInternalEntry(h); - } - else - { - removeExternalEntry(h); - } - } - - decrementSize(); - return h.setKeyValue(null, null); - } - - /** - * Removes an external entry from the {@link Trie}. - * - * If it's an external Entry then just remove it. - * This is very easy and straight forward. - */ - private void removeExternalEntry(TrieEntry h) - { - if (h == root) - { - throw new IllegalArgumentException("Cannot delete root Entry!"); - } - else if (!h.isExternalNode()) - { - throw new IllegalArgumentException(h + " is not an external Entry!"); - } - - TrieEntry parent = h.parent; - TrieEntry child = (h.left == h) ? h.right : h.left; - - if (parent.left == h) - { - parent.left = child; - } - else - { - parent.right = child; - } - - // either the parent is changing, or the predecessor is changing. - if (child.bitIndex > parent.bitIndex) - { - child.parent = parent; - } - else - { - child.predecessor = parent; - } - - } - - /** - * Removes an internal entry from the {@link Trie}. - * - * If it's an internal Entry then "good luck" with understanding - * this code. The Idea is essentially that Entry p takes Entry h's - * place in the trie which requires some re-wiring. - */ - private void removeInternalEntry(TrieEntry h) - { - if (h == root) - { - throw new IllegalArgumentException("Cannot delete root Entry!"); - } - else if (!h.isInternalNode()) - { - throw new IllegalArgumentException(h + " is not an internal Entry!"); - } - - TrieEntry p = h.predecessor; - - // Set P's bitIndex - p.bitIndex = h.bitIndex; - - // Fix P's parent, predecessor and child Nodes - { - TrieEntry parent = p.parent; - TrieEntry child = (p.left == h) ? p.right : p.left; - - // if it was looping to itself previously, - // it will now be pointed from it's parent - // (if we aren't removing it's parent -- - // in that case, it remains looping to itself). - // otherwise, it will continue to have the same - // predecessor. - if (p.predecessor == p && p.parent != h) - p.predecessor = p.parent; - - if (parent.left == p) - { - parent.left = child; - } - else - { - parent.right = child; - } - - if (child.bitIndex > parent.bitIndex) - { - child.parent = parent; - } - } - - // Fix H's parent and child Nodes - { - // If H is a parent of its left and right child - // then change them to P - if (h.left.parent == h) - h.left.parent = p; - - if (h.right.parent == h) - h.right.parent = p; - - // Change H's parent - if (h.parent.left == h) - { - h.parent.left = p; - } - else - { - h.parent.right = p; - } - } - - // Copy the remaining fields from H to P - //p.bitIndex = h.bitIndex; - p.parent = h.parent; - p.left = h.left; - p.right = h.right; - - // Make sure that if h was pointing to any uplinks, - // p now points to them. - if (isValidUplink(p.left, p)) - p.left.predecessor = p; - - if (isValidUplink(p.right, p)) - p.right.predecessor = p; - } - - /** - * Returns the entry lexicographically after the given entry. - * If the given entry is null, returns the first node. - */ - TrieEntry nextEntry(TrieEntry node) - { - return (node == null) ? firstEntry() : nextEntryImpl(node.predecessor, node, null); - } - - /** - * Scans for the next node, starting at the specified point, and using 'previous' - * as a hint that the last node we returned was 'previous' (so we know not to return - * it again). If 'tree' is non-null, this will limit the search to the given tree. - * - * The basic premise is that each iteration can follow the following steps: - * - * 1) Scan all the way to the left. - * a) If we already started from this node last time, proceed to Step 2. - * b) If a valid uplink is found, use it. - * c) If the result is an empty node (root not set), break the scan. - * d) If we already returned the left node, break the scan. - * - * 2) Check the right. - * a) If we already returned the right node, proceed to Step 3. - * b) If it is a valid uplink, use it. - * c) Do Step 1 from the right node. - * - * 3) Back up through the parents until we encounter find a parent - * that we're not the right child of. - * - * 4) If there's no right child of that parent, the iteration is finished. - * Otherwise continue to Step 5. - * - * 5) Check to see if the right child is a valid uplink. - * a) If we already returned that child, proceed to Step 6. - * Otherwise, use it. - * - * 6) If the right child of the parent is the parent itself, we've - * already found & returned the end of the Trie, so exit. - * - * 7) Do Step 1 on the parent's right child. - */ - TrieEntry nextEntryImpl(TrieEntry start, TrieEntry previous, TrieEntry tree) - { - TrieEntry current = start; - - // Only look at the left if this was a recursive or - // the first check, otherwise we know we've already looked - // at the left. - if (previous == null || start != previous.predecessor) - { - while (!current.left.isEmpty()) - { - // stop traversing if we've already - // returned the left of this node. - if (previous == current.left) - break; - - if (isValidUplink(current.left, current)) - return current.left; - - current = current.left; - } - } - - // If there's no data at all, exit. - if (current.isEmpty()) - return null; - - // If we've already returned the left, - // and the immediate right is null, - // there's only one entry in the Trie - // which is stored at the root. - // - // / ("") <-- root - // \_/ \ - // null <-- 'current' - // - if (current.right == null) - return null; - - // If nothing valid on the left, try the right. - if (previous != current.right) - { - // See if it immediately is valid. - if (isValidUplink(current.right, current)) - return current.right; - - // Must search on the right's side if it wasn't initially valid. - return nextEntryImpl(current.right, previous, tree); - } - - // Neither left nor right are valid, find the first parent - // whose child did not come from the right & traverse it. - while (current == current.parent.right) - { - // If we're going to traverse to above the subtree, stop. - if (current == tree) - return null; - - current = current.parent; - } - - // If we're on the top of the subtree, we can't go any higher. - if (current == tree) - return null; - - // If there's no right, the parent must be root, so we're done. - if (current.parent.right == null) - return null; - - // If the parent's right points to itself, we've found one. - if (previous != current.parent.right && isValidUplink(current.parent.right, current.parent)) - return current.parent.right; - - // If the parent's right is itself, there can't be any more nodes. - if (current.parent.right == current.parent) - return null; - - // We need to traverse down the parent's right's path. - return nextEntryImpl(current.parent.right, previous, tree); - } - - /** - * Returns the first entry the {@link Trie} is storing. - * - * This is implemented by going always to the left until - * we encounter a valid uplink. That uplink is the first key. - */ - TrieEntry firstEntry() - { - // if Trie is empty, no first node. - return isEmpty() ? null : followLeft(root); - } - - /** - * Goes left through the tree until it finds a valid node. - */ - TrieEntry followLeft(TrieEntry node) - { - while(true) - { - TrieEntry child = node.left; - // if we hit root and it didn't have a node, go right instead. - if (child.isEmpty()) - child = node.right; - - if (child.bitIndex <= node.bitIndex) - return child; - - node = child; - } - } - - /** - * Returns true if 'next' is a valid uplink coming from 'from'. - */ - static boolean isValidUplink(TrieEntry next, TrieEntry from) - { - return next != null && next.bitIndex <= from.bitIndex && !next.isEmpty(); - } - - /** - * A {@link Reference} allows us to return something through a Method's - * argument list. An alternative would be to an Array with a length of - * one (1) but that leads to compiler warnings. Computationally and memory - * wise there's no difference (except for the need to load the - * {@link Reference} Class but that happens only once). - */ - private static class Reference - { - - private E item; - - public void set(E item) - { - this.item = item; - } - - public E get() - { - return item; - } - } - - /** - * A {@link Trie} is a set of {@link TrieEntry} nodes - */ - static class TrieEntry extends BasicEntry - { - - private static final long serialVersionUID = 4596023148184140013L; - - /** The index this entry is comparing. */ - protected int bitIndex; - - /** The parent of this entry. */ - protected TrieEntry parent; - - /** The left child of this entry. */ - protected TrieEntry left; - - /** The right child of this entry. */ - protected TrieEntry right; - - /** The entry who uplinks to this entry. */ - protected TrieEntry predecessor; - - public TrieEntry(K key, V value, int bitIndex) - { - super(key, value); - - this.bitIndex = bitIndex; - - this.parent = null; - this.left = this; - this.right = null; - this.predecessor = this; - } - - /** - * Whether or not the entry is storing a key. - * Only the root can potentially be empty, all other - * nodes must have a key. - */ - public boolean isEmpty() - { - return key == null; - } - - /** - * Neither the left nor right child is a loopback - */ - public boolean isInternalNode() - { - return left != this && right != this; - } - - /** - * Either the left or right child is a loopback - */ - public boolean isExternalNode() - { - return !isInternalNode(); - } - } - - - /** - * This is a entry set view of the {@link Trie} as returned - * by {@link Map#entrySet()} - */ - private class EntrySet extends AbstractSet> - { - @Override - public Iterator> iterator() - { - return new EntryIterator(); - } - - @Override - public boolean contains(Object o) - { - if (!(o instanceof Map.Entry)) - return false; - - TrieEntry candidate = getEntry(((Map.Entry)o).getKey()); - return candidate != null && candidate.equals(o); - } - - @Override - public boolean remove(Object o) - { - int size = size(); - AbstractPatriciaTrie.this.remove(o); - return size != size(); - } - - @Override - public int size() - { - return AbstractPatriciaTrie.this.size(); - } - - @Override - public void clear() - { - AbstractPatriciaTrie.this.clear(); - } - - /** - * An {@link Iterator} that returns {@link Entry} Objects - */ - private class EntryIterator extends TrieIterator> - { - @Override - public Map.Entry next() - { - return nextEntry(); - } - } - } - - /** - * This is a key set view of the {@link Trie} as returned - * by {@link Map#keySet()} - */ - private class KeySet extends AbstractSet - { - @Override - public Iterator iterator() - { - return new KeyIterator(); - } - - @Override - public int size() - { - return AbstractPatriciaTrie.this.size(); - } - - @Override - public boolean contains(Object o) - { - return containsKey(o); - } - - @Override - public boolean remove(Object o) - { - int size = size(); - AbstractPatriciaTrie.this.remove(o); - return size != size(); - } - - @Override - public void clear() - { - AbstractPatriciaTrie.this.clear(); - } - - /** - * An {@link Iterator} that returns Key Objects - */ - private class KeyIterator extends TrieIterator - { - @Override - public K next() - { - return nextEntry().getKey(); - } - } - } - - /** - * This is a value view of the {@link Trie} as returned - * by {@link Map#values()} - */ - private class Values extends AbstractCollection - { - @Override - public Iterator iterator() - { - return new ValueIterator(); - } - - @Override - public int size() - { - return AbstractPatriciaTrie.this.size(); - } - - @Override - public boolean contains(Object o) - { - return containsValue(o); - } - - @Override - public void clear() - { - AbstractPatriciaTrie.this.clear(); - } - - @Override - public boolean remove(Object o) - { - for (Iterator it = iterator(); it.hasNext(); ) - { - V value = it.next(); - if (Tries.areEqual(value, o)) - { - it.remove(); - return true; - } - } - return false; - } - - /** - * An {@link Iterator} that returns Value Objects - */ - private class ValueIterator extends TrieIterator - { - @Override - public V next() - { - return nextEntry().getValue(); - } - } - } - - /** - * An iterator for the entries. - */ - abstract class TrieIterator implements Iterator - { - /** - * For fast-fail - */ - protected int expectedModCount = AbstractPatriciaTrie.this.modCount; - - protected TrieEntry next; // the next node to return - protected TrieEntry current; // the current entry we're on - - /** - * Starts iteration from the root - */ - protected TrieIterator() - { - next = AbstractPatriciaTrie.this.nextEntry(null); - } - - /** - * Starts iteration at the given entry - */ - protected TrieIterator(TrieEntry firstEntry) - { - next = firstEntry; - } - - /** - * Returns the next {@link TrieEntry} - */ - protected TrieEntry nextEntry() - { - if (expectedModCount != AbstractPatriciaTrie.this.modCount) - throw new ConcurrentModificationException(); - - TrieEntry e = next; - if (e == null) - throw new NoSuchElementException(); - - next = findNext(e); - current = e; - return e; - } - - /** - * @see PatriciaTrie#nextEntry(TrieEntry) - */ - protected TrieEntry findNext(TrieEntry prior) - { - return AbstractPatriciaTrie.this.nextEntry(prior); - } - - @Override - public boolean hasNext() - { - return next != null; - } - - @Override - public void remove() - { - if (current == null) - throw new IllegalStateException(); - - if (expectedModCount != AbstractPatriciaTrie.this.modCount) - throw new ConcurrentModificationException(); - - TrieEntry node = current; - current = null; - AbstractPatriciaTrie.this.removeEntry(node); - - expectedModCount = AbstractPatriciaTrie.this.modCount; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractTrie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractTrie.java deleted file mode 100644 index f24ec141d580..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractTrie.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2005-2010 Roger Kapsi, Sam Berlin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -import java.io.Serializable; -import java.util.AbstractMap; -import java.util.Map; - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -/** - * This class provides some basic {@link Trie} functionality and - * utility methods for actual {@link Trie} implementations. - */ -abstract class AbstractTrie extends AbstractMap implements Serializable, Trie -{ - private static final long serialVersionUID = -6358111100045408883L; - - /** - * The {@link KeyAnalyzer} that's being used to build the - * PATRICIA {@link Trie} - */ - protected final KeyAnalyzer keyAnalyzer; - - /** - * Constructs a new {@link Trie} using the given {@link KeyAnalyzer} - */ - public AbstractTrie(KeyAnalyzer keyAnalyzer) - { - this.keyAnalyzer = Tries.notNull(keyAnalyzer, "keyAnalyzer"); - } - - @Override - public K selectKey(K key) - { - Map.Entry entry = select(key); - return entry != null ? entry.getKey() : null; - } - - @Override - public V selectValue(K key) - { - Map.Entry entry = select(key); - return entry != null ? entry.getValue() : null; - } - - @Override - public String toString() - { - StringBuilder buffer = new StringBuilder(); - buffer.append("Trie[").append(size()).append("]={\n"); - for (Map.Entry entry : entrySet()) - { - buffer.append(" ").append(entry).append("\n"); - } - buffer.append("}\n"); - return buffer.toString(); - } - - /** - * Returns the length of the given key in bits - * - * @see KeyAnalyzer#lengthInBits(Object) - */ - final int lengthInBits(K key) - { - return key == null ? 0 : keyAnalyzer.lengthInBits(key); - } - - /** - * Returns whether or not the given bit on the - * key is set or false if the key is null. - * - * @see KeyAnalyzer#isBitSet(Object, int) - */ - final boolean isBitSet(K key, int bitIndex) - { - return key != null && keyAnalyzer.isBitSet(key, bitIndex); - } - - /** - * Utility method for calling {@link KeyAnalyzer#bitIndex(Object, Object)} - */ - final int bitIndex(K key, K otherKey) - { - if (key != null && otherKey != null) - { - return keyAnalyzer.bitIndex(key, otherKey); - } - else if (key != null) - { - return bitIndex(key); - } - else if (otherKey != null) - { - return bitIndex(otherKey); - } - - return KeyAnalyzer.NULL_BIT_KEY; - } - - private int bitIndex(K key) - { - int lengthInBits = lengthInBits(key); - for (int i = 0; i < lengthInBits; i++) - { - if (isBitSet(key, i)) - return i; - } - - return KeyAnalyzer.NULL_BIT_KEY; - } - - /** - * An utility method for calling {@link KeyAnalyzer#compare(Object, Object)} - */ - final boolean compareKeys(K key, K other) - { - if (key == null) - { - return (other == null); - } - else if (other == null) - { - return false; - } - - return keyAnalyzer.compare(key, other) == 0; - } - - /** - * A basic implementation of {@link Entry} - */ - abstract static class BasicEntry implements Map.Entry, Serializable - { - private static final long serialVersionUID = -944364551314110330L; - - protected K key; - - protected V value; - - private transient int hashCode = 0; - - public BasicEntry(K key, V value) - { - this.key = key; - this.value = value; - } - - /** - * Replaces the current key and value with the provided - * key & value - */ - public V setKeyValue(K key, V value) - { - this.key = key; - this.hashCode = 0; - return setValue(value); - } - - @Override - public K getKey() - { - return key; - } - - @Override - public V getValue() - { - return value; - } - - @Override - public V setValue(V value) - { - V previous = this.value; - this.value = value; - return previous; - } - - @Override - public int hashCode() - { - if (hashCode == 0) - hashCode = (key != null ? key.hashCode() : 0); - return hashCode; - } - - @Override - public boolean equals(Object o) - { - if (o == this) - { - return true; - } - else if (!(o instanceof Map.Entry)) - { - return false; - } - - Map.Entry other = (Map.Entry)o; - return Tries.areEqual(key, other.getKey()) && Tries.areEqual(value, other.getValue()); - } - - @Override - public String toString() - { - return key + "=" + value; - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/Cursor.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/Cursor.java deleted file mode 100644 index 7c86b9709ef5..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/Cursor.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2005-2010 Roger Kapsi, Sam Berlin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -import java.util.Map; -import java.util.Map.Entry; - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -/** - * A {@link Cursor} can be used to traverse a {@link Trie}, visit each node - * step by step and make {@link Decision}s on each step how to continue with - * traversing the {@link Trie}. - */ -public interface Cursor -{ - - /** - * The {@link Decision} tells the {@link Cursor} what to do on each step - * while traversing the {@link Trie}. - * - * NOTE: Not all operations that work with a {@link Cursor} support all - * {@link Decision} types - */ - enum Decision - { - - /** - * Exit the traverse operation - */ - EXIT, - - /** - * Continue with the traverse operation - */ - CONTINUE, - - /** - * Remove the previously returned element - * from the {@link Trie} and continue - */ - REMOVE, - - /** - * Remove the previously returned element - * from the {@link Trie} and exit from the - * traverse operation - */ - REMOVE_AND_EXIT - } - - /** - * Called for each {@link Entry} in the {@link Trie}. Return - * {@link Decision#EXIT} to finish the {@link Trie} operation, - * {@link Decision#CONTINUE} to go to the next {@link Entry}, - * {@link Decision#REMOVE} to remove the {@link Entry} and - * continue iterating or {@link Decision#REMOVE_AND_EXIT} to - * remove the {@link Entry} and stop iterating. - * - * Note: Not all operations support {@link Decision#REMOVE}. - */ - Decision select(Map.Entry entry); -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/KeyAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/KeyAnalyzer.java deleted file mode 100644 index c6ef66523907..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/KeyAnalyzer.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2010 Roger Kapsi - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -import java.util.Comparator; - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -/** - * The {@link KeyAnalyzer} provides bit-level access to keys - * for the {@link PatriciaTrie}. - */ -public interface KeyAnalyzer extends Comparator -{ - /** - * Returned by {@link #bitIndex(Object, Object)} if a key's - * bits were all zero (0). - */ - int NULL_BIT_KEY = -1; - - /** - * Returned by {@link #bitIndex(Object, Object)} if a the - * bits of two keys were all equal. - */ - int EQUAL_BIT_KEY = -2; - - /** - * Returned by {@link #bitIndex(Object, Object)} if a keys - * indices are out of bounds. - */ - int OUT_OF_BOUNDS_BIT_KEY = -3; - - /** - * Returns the key's length in bits. - */ - int lengthInBits(K key); - - /** - * Returns {@code true} if a key's bit it set at the given index. - */ - boolean isBitSet(K key, int bitIndex); - - /** - * Returns the index of the first bit that is different in the two keys. - */ - int bitIndex(K key, K otherKey); - - /** - * Returns {@code true} if the second argument is a - * prefix of the first argument. - */ - boolean isPrefix(K key, K prefix); -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/PatriciaTrie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/PatriciaTrie.java deleted file mode 100644 index a36af9828c8b..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/PatriciaTrie.java +++ /dev/null @@ -1,1261 +0,0 @@ -/* - * Copyright 2005-2010 Roger Kapsi, Sam Berlin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -import java.io.Serializable; -import java.util.*; - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -/** - *

    PATRICIA {@link Trie}

    - * - * Practical Algorithm to Retrieve Information Coded in Alphanumeric - * - *

    A PATRICIA {@link Trie} is a compressed {@link Trie}. Instead of storing - * all data at the edges of the {@link Trie} (and having empty internal nodes), - * PATRICIA stores data in every node. This allows for very efficient traversal, - * insert, delete, predecessor, successor, prefix, range, and {@link #select(Object)} - * operations. All operations are performed at worst in O(K) time, where K - * is the number of bits in the largest item in the tree. In practice, - * operations actually take O(A(K)) time, where A(K) is the average number of - * bits of all items in the tree. - * - *

    Most importantly, PATRICIA requires very few comparisons to keys while - * doing any operation. While performing a lookup, each comparison (at most - * K of them, described above) will perform a single bit comparison against - * the given key, instead of comparing the entire key to another key. - * - *

    The {@link Trie} can return operations in lexicographical order using the - * {@link #traverse(Cursor)}, 'prefix', 'submap', or 'iterator' methods. The - * {@link Trie} can also scan for items that are 'bitwise' (using an XOR - * metric) by the 'select' method. Bitwise closeness is determined by the - * {@link KeyAnalyzer} returning true or false for a bit being set or not in - * a given key. - * - *

    Any methods here that take an {@link Object} argument may throw a - * {@link ClassCastException} if the method is expecting an instance of K - * and it isn't K. - * - * @see Radix Tree - * @see PATRICIA - * @see Crit-Bit Tree - * - * @author Roger Kapsi - * @author Sam Berlin - */ -public class PatriciaTrie extends AbstractPatriciaTrie implements Serializable -{ - private static final long serialVersionUID = -2246014692353432660L; - - public PatriciaTrie(KeyAnalyzer keyAnalyzer) - { - super(keyAnalyzer); - } - - public PatriciaTrie(KeyAnalyzer keyAnalyzer, Map m) - { - super(keyAnalyzer, m); - } - - @Override - public Comparator comparator() - { - return keyAnalyzer; - } - - @Override - public SortedMap prefixMap(K prefix) - { - return lengthInBits(prefix) == 0 ? this : new PrefixRangeMap(prefix); - } - - @Override - public K firstKey() - { - return firstEntry().getKey(); - } - - @Override - public K lastKey() - { - TrieEntry entry = lastEntry(); - return entry != null ? entry.getKey() : null; - } - - @Override - public SortedMap headMap(K toKey) - { - return new RangeEntryMap(null, toKey); - } - - @Override - public SortedMap subMap(K fromKey, K toKey) - { - return new RangeEntryMap(fromKey, toKey); - } - - @Override - public SortedMap tailMap(K fromKey) - { - return new RangeEntryMap(fromKey, null); - } - - /** - * Returns an entry strictly higher than the given key, - * or null if no such entry exists. - */ - private TrieEntry higherEntry(K key) - { - // TODO: Cleanup so that we don't actually have to add/remove from the - // tree. (We do it here because there are other well-defined - // functions to perform the search.) - int lengthInBits = lengthInBits(key); - - if (lengthInBits == 0) - { - if (!root.isEmpty()) - { - // If data in root, and more after -- return it. - return size() > 1 ? nextEntry(root) : null; - } - else - { - // Root is empty & we want something after empty, return first. - return firstEntry(); - } - } - - TrieEntry found = getNearestEntryForKey(key); - if (compareKeys(key, found.key)) - return nextEntry(found); - - int bitIndex = bitIndex(key, found.key); - if (Tries.isValidBitIndex(bitIndex)) - { - return replaceCeil(key, bitIndex); - } - else if (Tries.isNullBitKey(bitIndex)) - { - if (!root.isEmpty()) - { - return firstEntry(); - } - else if (size() > 1) - { - return nextEntry(firstEntry()); - } - else - { - return null; - } - } - else if (Tries.isEqualBitKey(bitIndex)) - { - return nextEntry(found); - } - - // we should have exited above. - throw new IllegalStateException("invalid lookup: " + key); - } - - /** - * Returns a key-value mapping associated with the least key greater - * than or equal to the given key, or null if there is no such key. - */ - TrieEntry ceilingEntry(K key) - { - // Basically: - // Follow the steps of adding an entry, but instead... - // - // - If we ever encounter a situation where we found an equal - // key, we return it immediately. - // - // - If we hit an empty root, return the first iterable item. - // - // - If we have to add a new item, we temporarily add it, - // find the successor to it, then remove the added item. - // - // These steps ensure that the returned value is either the - // entry for the key itself, or the first entry directly after - // the key. - - // TODO: Cleanup so that we don't actually have to add/remove from the - // tree. (We do it here because there are other well-defined - // functions to perform the search.) - int lengthInBits = lengthInBits(key); - - if (lengthInBits == 0) - { - if (!root.isEmpty()) - { - return root; - } - else - { - return firstEntry(); - } - } - - TrieEntry found = getNearestEntryForKey(key); - if (compareKeys(key, found.key)) - return found; - - int bitIndex = bitIndex(key, found.key); - if (Tries.isValidBitIndex(bitIndex)) - { - return replaceCeil(key, bitIndex); - } - else if (Tries.isNullBitKey(bitIndex)) - { - if (!root.isEmpty()) - { - return root; - } - else - { - return firstEntry(); - } - } - else if (Tries.isEqualBitKey(bitIndex)) - { - return found; - } - - // we should have exited above. - throw new IllegalStateException("invalid lookup: " + key); - } - - private TrieEntry replaceCeil(K key, int bitIndex) - { - TrieEntry added = new TrieEntry<>(key, null, bitIndex); - addEntry(added); - incrementSize(); // must increment because remove will decrement - TrieEntry ceil = nextEntry(added); - removeEntry(added); - modCount -= 2; // we didn't really modify it. - return ceil; - } - - private TrieEntry replaceLower(K key, int bitIndex) - { - TrieEntry added = new TrieEntry<>(key, null, bitIndex); - addEntry(added); - incrementSize(); // must increment because remove will decrement - TrieEntry prior = previousEntry(added); - removeEntry(added); - modCount -= 2; // we didn't really modify it. - return prior; - } - - /** - * Returns a key-value mapping associated with the greatest key - * strictly less than the given key, or null if there is no such key. - */ - TrieEntry lowerEntry(K key) - { - // Basically: - // Follow the steps of adding an entry, but instead... - // - // - If we ever encounter a situation where we found an equal - // key, we return it's previousEntry immediately. - // - // - If we hit root (empty or not), return null. - // - // - If we have to add a new item, we temporarily add it, - // find the previousEntry to it, then remove the added item. - // - // These steps ensure that the returned value is always just before - // the key or null (if there was nothing before it). - - // TODO: Cleanup so that we don't actually have to add/remove from the - // tree. (We do it here because there are other well-defined - // functions to perform the search.) - int lengthInBits = lengthInBits(key); - - if (lengthInBits == 0) - return null; // there can never be anything before root. - - TrieEntry found = getNearestEntryForKey(key); - if (compareKeys(key, found.key)) - return previousEntry(found); - - int bitIndex = bitIndex(key, found.key); - if (Tries.isValidBitIndex(bitIndex)) - { - return replaceLower(key, bitIndex); - } - else if (Tries.isNullBitKey(bitIndex)) - { - return null; - } - else if (Tries.isEqualBitKey(bitIndex)) - { - return previousEntry(found); - } - - // we should have exited above. - throw new IllegalStateException("invalid lookup: " + key); - } - - /** - * Returns a key-value mapping associated with the greatest key - * less than or equal to the given key, or null if there is no such key. - */ - TrieEntry floorEntry(K key) { - // TODO: Cleanup so that we don't actually have to add/remove from the - // tree. (We do it here because there are other well-defined - // functions to perform the search.) - int lengthInBits = lengthInBits(key); - - if (lengthInBits == 0) - { - return !root.isEmpty() ? root : null; - } - - TrieEntry found = getNearestEntryForKey(key); - if (compareKeys(key, found.key)) - return found; - - int bitIndex = bitIndex(key, found.key); - if (Tries.isValidBitIndex(bitIndex)) - { - return replaceLower(key, bitIndex); - } - else if (Tries.isNullBitKey(bitIndex)) - { - if (!root.isEmpty()) - { - return root; - } - else - { - return null; - } - } - else if (Tries.isEqualBitKey(bitIndex)) - { - return found; - } - - // we should have exited above. - throw new IllegalStateException("invalid lookup: " + key); - } - - /** - * Finds the subtree that contains the prefix. - * - * This is very similar to getR but with the difference that - * we stop the lookup if h.bitIndex > lengthInBits. - */ - private TrieEntry subtree(K prefix) - { - int lengthInBits = lengthInBits(prefix); - - TrieEntry current = root.left; - TrieEntry path = root; - while(true) - { - if (current.bitIndex <= path.bitIndex || lengthInBits < current.bitIndex) - break; - - path = current; - current = !isBitSet(prefix, current.bitIndex) - ? current.left : current.right; - } - - // Make sure the entry is valid for a subtree. - TrieEntry entry = current.isEmpty() ? path : current; - - // If entry is root, it can't be empty. - if (entry.isEmpty()) - return null; - - // if root && length of root is less than length of lookup, - // there's nothing. - // (this prevents returning the whole subtree if root has an empty - // string and we want to lookup things with "\0") - if (entry == root && lengthInBits(entry.getKey()) < lengthInBits) - return null; - - // Found key's length-th bit differs from our key - // which means it cannot be the prefix... - if (isBitSet(prefix, lengthInBits) != isBitSet(entry.key, lengthInBits)) - return null; - - // ... or there are less than 'length' equal bits - int bitIndex = bitIndex(prefix, entry.key); - return (bitIndex >= 0 && bitIndex < lengthInBits) ? null : entry; - } - - /** - * Returns the last entry the {@link Trie} is storing. - * - *

    This is implemented by going always to the right until - * we encounter a valid uplink. That uplink is the last key. - */ - private TrieEntry lastEntry() - { - return followRight(root.left); - } - - /** - * Traverses down the right path until it finds an uplink. - */ - private TrieEntry followRight(TrieEntry node) - { - // if Trie is empty, no last entry. - if (node.right == null) - return null; - - // Go as far right as possible, until we encounter an uplink. - while (node.right.bitIndex > node.bitIndex) - { - node = node.right; - } - - return node.right; - } - - /** - * Returns the node lexicographically before the given node (or null if none). - * - * This follows four simple branches: - * - If the uplink that returned us was a right uplink: - * - If predecessor's left is a valid uplink from predecessor, return it. - * - Else, follow the right path from the predecessor's left. - * - If the uplink that returned us was a left uplink: - * - Loop back through parents until we encounter a node where - * node != node.parent.left. - * - If node.parent.left is uplink from node.parent: - * - If node.parent.left is not root, return it. - * - If it is root & root isEmpty, return null. - * - If it is root & root !isEmpty, return root. - * - If node.parent.left is not uplink from node.parent: - * - Follow right path for first right child from node.parent.left - * - * @param start the start entry - */ - private TrieEntry previousEntry(TrieEntry start) - { - if (start.predecessor == null) - throw new IllegalArgumentException("must have come from somewhere!"); - - if (start.predecessor.right == start) - { - return isValidUplink(start.predecessor.left, start.predecessor) - ? start.predecessor.left - : followRight(start.predecessor.left); - } - - TrieEntry node = start.predecessor; - while (node.parent != null && node == node.parent.left) - { - node = node.parent; - } - - if (node.parent == null) // can be null if we're looking up root. - return null; - - if (isValidUplink(node.parent.left, node.parent)) - { - if (node.parent.left == root) - { - return root.isEmpty() ? null : root; - } - else - { - return node.parent.left; - } - } - else - { - return followRight(node.parent.left); - } - } - - /** - * Returns the entry lexicographically after the given entry. - * If the given entry is null, returns the first node. - * - * This will traverse only within the subtree. If the given node - * is not within the subtree, this will have undefined results. - */ - private TrieEntry nextEntryInSubtree(TrieEntry node, TrieEntry parentOfSubtree) - { - return (node == null) ? firstEntry() : nextEntryImpl(node.predecessor, node, parentOfSubtree); - } - - private boolean isPrefix(K key, K prefix) - { - return keyAnalyzer.isPrefix(key, prefix); - } - - /** - * A range view of the {@link Trie} - */ - private abstract class RangeMap extends AbstractMap implements SortedMap - { - /** - * The {@link #entrySet()} view - */ - private transient volatile Set> entrySet; - - /** - * Creates and returns an {@link #entrySet()} - * view of the {@link RangeMap} - */ - protected abstract Set> createEntrySet(); - - /** - * Returns the FROM Key - */ - protected abstract K getFromKey(); - - /** - * Whether or not the {@link #getFromKey()} is in the range - */ - protected abstract boolean isFromInclusive(); - - /** - * Returns the TO Key - */ - protected abstract K getToKey(); - - /** - * Whether or not the {@link #getToKey()} is in the range - */ - protected abstract boolean isToInclusive(); - - - @Override - public Comparator comparator() - { - return PatriciaTrie.this.comparator(); - } - - @Override - public boolean containsKey(Object key) - { - return inRange(Tries.cast(key)) && PatriciaTrie.this.containsKey(key); - } - - @Override - public V remove(Object key) - { - return (!inRange(Tries.cast(key))) ? null : PatriciaTrie.this.remove(key); - } - - @Override - public V get(Object key) - { - return (!inRange(Tries.cast(key))) ? null : PatriciaTrie.this.get(key); - } - - @Override - public V put(K key, V value) - { - if (!inRange(key)) - throw new IllegalArgumentException("Key is out of range: " + key); - - return PatriciaTrie.this.put(key, value); - } - - @Override - public Set> entrySet() - { - if (entrySet == null) - entrySet = createEntrySet(); - return entrySet; - } - - @Override - public SortedMap subMap(K fromKey, K toKey) - { - if (!inRange2(fromKey)) - throw new IllegalArgumentException("FromKey is out of range: " + fromKey); - - if (!inRange2(toKey)) - throw new IllegalArgumentException("ToKey is out of range: " + toKey); - - return createRangeMap(fromKey, isFromInclusive(), toKey, isToInclusive()); - } - - @Override - public SortedMap headMap(K toKey) - { - if (!inRange2(toKey)) - throw new IllegalArgumentException("ToKey is out of range: " + toKey); - - return createRangeMap(getFromKey(), isFromInclusive(), toKey, isToInclusive()); - } - - @Override - public SortedMap tailMap(K fromKey) - { - if (!inRange2(fromKey)) - throw new IllegalArgumentException("FromKey is out of range: " + fromKey); - - return createRangeMap(fromKey, isFromInclusive(), getToKey(), isToInclusive()); - } - - /** - * Returns true if the provided key is greater than TO and - * less than FROM - */ - protected boolean inRange(K key) - { - K fromKey = getFromKey(); - K toKey = getToKey(); - - return (fromKey == null || inFromRange(key, false)) - && (toKey == null || inToRange(key, false)); - } - - /** - * This form allows the high endpoint (as well as all legit keys) - */ - protected boolean inRange2(K key) - { - K fromKey = getFromKey(); - K toKey = getToKey(); - - return (fromKey == null || inFromRange(key, false)) - && (toKey == null || inToRange(key, true)); - } - - /** - * Returns true if the provided key is in the FROM range - * of the {@link RangeMap} - */ - protected boolean inFromRange(K key, boolean forceInclusive) - { - K fromKey = getFromKey(); - boolean fromInclusive = isFromInclusive(); - - int ret = keyAnalyzer.compare(key, fromKey); - return (fromInclusive || forceInclusive) ? ret >= 0 : ret > 0; - } - - /** - * Returns true if the provided key is in the TO range - * of the {@link RangeMap} - */ - protected boolean inToRange(K key, boolean forceInclusive) - { - K toKey = getToKey(); - boolean toInclusive = isToInclusive(); - - int ret = keyAnalyzer.compare(key, toKey); - return (toInclusive || forceInclusive) ? ret <= 0 : ret < 0; - } - - /** - * Creates and returns a sub-range view of the current {@link RangeMap} - */ - protected abstract SortedMap createRangeMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive); - } - - /** - * A {@link RangeMap} that deals with {@link Entry}s - */ - private class RangeEntryMap extends RangeMap - { - /** - * The key to start from, null if the beginning. - */ - protected final K fromKey; - - /** - * The key to end at, null if till the end. - */ - protected final K toKey; - - /** - * Whether or not the 'from' is inclusive. - */ - protected final boolean fromInclusive; - - /** - * Whether or not the 'to' is inclusive. - */ - protected final boolean toInclusive; - - /** - * Creates a {@link RangeEntryMap} with the fromKey included and - * the toKey excluded from the range - */ - protected RangeEntryMap(K fromKey, K toKey) - { - this(fromKey, true, toKey, false); - } - - /** - * Creates a {@link RangeEntryMap} - */ - protected RangeEntryMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) - { - if (fromKey == null && toKey == null) - throw new IllegalArgumentException("must have a from or to!"); - - if (fromKey != null && toKey != null && keyAnalyzer.compare(fromKey, toKey) > 0) - throw new IllegalArgumentException("fromKey > toKey"); - - this.fromKey = fromKey; - this.fromInclusive = fromInclusive; - this.toKey = toKey; - this.toInclusive = toInclusive; - } - - - @Override - public K firstKey() - { - Map.Entry e = fromKey == null - ? firstEntry() - : fromInclusive ? ceilingEntry(fromKey) : higherEntry(fromKey); - - K first = e != null ? e.getKey() : null; - if (e == null || toKey != null && !inToRange(first, false)) - throw new NoSuchElementException(); - - return first; - } - - - @Override - public K lastKey() - { - Map.Entry e = toKey == null - ? lastEntry() - : toInclusive ? floorEntry(toKey) : lowerEntry(toKey); - - K last = e != null ? e.getKey() : null; - if (e == null || fromKey != null && !inFromRange(last, false)) - throw new NoSuchElementException(); - - return last; - } - - @Override - protected Set> createEntrySet() - { - return new RangeEntrySet(this); - } - - @Override - public K getFromKey() - { - return fromKey; - } - - @Override - public K getToKey() - { - return toKey; - } - - @Override - public boolean isFromInclusive() - { - return fromInclusive; - } - - @Override - public boolean isToInclusive() - { - return toInclusive; - } - - @Override - protected SortedMap createRangeMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) - { - return new RangeEntryMap(fromKey, fromInclusive, toKey, toInclusive); - } - } - - /** - * A {@link Set} view of a {@link RangeMap} - */ - private class RangeEntrySet extends AbstractSet> - { - - private final RangeMap delegate; - - private int size = -1; - - private int expectedModCount = -1; - - /** - * Creates a {@link RangeEntrySet} - */ - public RangeEntrySet(RangeMap delegate) - { - if (delegate == null) - throw new NullPointerException("delegate"); - - this.delegate = delegate; - } - - @Override - public Iterator> iterator() - { - K fromKey = delegate.getFromKey(); - K toKey = delegate.getToKey(); - - TrieEntry first = fromKey == null ? firstEntry() : ceilingEntry(fromKey); - TrieEntry last = null; - if (toKey != null) - last = ceilingEntry(toKey); - - return new EntryIterator(first, last); - } - - @Override - public int size() - { - if (size == -1 || expectedModCount != PatriciaTrie.this.modCount) - { - size = 0; - - for (Iterator it = iterator(); it.hasNext(); it.next()) - { - ++size; - } - - expectedModCount = PatriciaTrie.this.modCount; - } - - return size; - } - - @Override - public boolean isEmpty() - { - return !iterator().hasNext(); - } - - @Override - public boolean contains(Object o) - { - if (!(o instanceof Map.Entry)) - return false; - - @SuppressWarnings("unchecked") - Map.Entry entry = (Map.Entry) o; - K key = entry.getKey(); - if (!delegate.inRange(key)) - return false; - - TrieEntry node = getEntry(key); - return node != null && Tries.areEqual(node.getValue(), entry.getValue()); - } - - @Override - public boolean remove(Object o) - { - if (!(o instanceof Map.Entry)) - return false; - - @SuppressWarnings("unchecked") - Map.Entry entry = (Map.Entry) o; - K key = entry.getKey(); - if (!delegate.inRange(key)) - return false; - - TrieEntry node = getEntry(key); - if (node != null && Tries.areEqual(node.getValue(), entry.getValue())) - { - removeEntry(node); - return true; - } - - return false; - } - - /** - * An {@link Iterator} for {@link RangeEntrySet}s. - */ - private final class EntryIterator extends TrieIterator> - { - private final K excludedKey; - - /** - * Creates a {@link EntryIterator} - */ - private EntryIterator(TrieEntry first, TrieEntry last) - { - super(first); - this.excludedKey = (last != null ? last.getKey() : null); - } - - @Override - public boolean hasNext() - { - return next != null && !Tries.areEqual(next.key, excludedKey); - } - - @Override - public Map.Entry next() - { - if (next == null || Tries.areEqual(next.key, excludedKey)) - throw new NoSuchElementException(); - - return nextEntry(); - } - } - } - - /** - * A submap used for prefix views over the {@link Trie}. - */ - private class PrefixRangeMap extends RangeMap - { - - private final K prefix; - - private K fromKey = null; - - private K toKey = null; - - private int expectedModCount = -1; - - private int size = -1; - - /** - * Creates a {@link PrefixRangeMap} - */ - private PrefixRangeMap(K prefix) - { - this.prefix = prefix; - } - - /** - * This method does two things. It determinates the FROM - * and TO range of the {@link PrefixRangeMap} and the number - * of elements in the range. This method must be called every - * time the {@link Trie} has changed. - */ - private int fixup() - { - // The trie has changed since we last - // found our toKey / fromKey - if (size == - 1 || PatriciaTrie.this.modCount != expectedModCount) - { - Iterator> it = entrySet().iterator(); - size = 0; - - Map.Entry entry = null; - if (it.hasNext()) - { - entry = it.next(); - size = 1; - } - - fromKey = entry == null ? null : entry.getKey(); - if (fromKey != null) - { - TrieEntry prior = previousEntry((TrieEntry)entry); - fromKey = prior == null ? null : prior.getKey(); - } - - toKey = fromKey; - - while (it.hasNext()) - { - ++size; - entry = it.next(); - } - - toKey = entry == null ? null : entry.getKey(); - - if (toKey != null) - { - entry = nextEntry((TrieEntry)entry); - toKey = entry == null ? null : entry.getKey(); - } - - expectedModCount = PatriciaTrie.this.modCount; - } - - return size; - } - - @Override - public K firstKey() - { - fixup(); - - Map.Entry e = fromKey == null ? firstEntry() : higherEntry(fromKey); - K first = e != null ? e.getKey() : null; - if (e == null || !isPrefix(first, prefix)) - throw new NoSuchElementException(); - - return first; - } - - @Override - public K lastKey() - { - fixup(); - - Map.Entry e = toKey == null ? lastEntry() : lowerEntry(toKey); - K last = e != null ? e.getKey() : null; - if (e == null || !isPrefix(last, prefix)) - throw new NoSuchElementException(); - - return last; - } - - /** - * Returns true if this {@link PrefixRangeMap}'s key is a prefix - * of the provided key. - */ - @Override - protected boolean inRange(K key) - { - return isPrefix(key, prefix); - } - - /** - * Same as {@link #inRange(Object)} - */ - @Override - protected boolean inRange2(K key) - { - return inRange(key); - } - - /** - * Returns true if the provided Key is in the FROM range - * of the {@link PrefixRangeMap} - */ - @Override - protected boolean inFromRange(K key, boolean forceInclusive) - { - return isPrefix(key, prefix); - } - - /** - * Returns true if the provided Key is in the TO range - * of the {@link PrefixRangeMap} - */ - @Override - protected boolean inToRange(K key, boolean forceInclusive) - { - return isPrefix(key, prefix); - } - - @Override - protected Set> createEntrySet() - { - return new PrefixRangeEntrySet(this); - } - - @Override - public K getFromKey() - { - return fromKey; - } - - @Override - public K getToKey() - { - return toKey; - } - - @Override - public boolean isFromInclusive() - { - return false; - } - - @Override - public boolean isToInclusive() - { - return false; - } - - @Override - protected SortedMap createRangeMap(K fromKey, boolean fromInclusive, - K toKey, boolean toInclusive) - { - return new RangeEntryMap(fromKey, fromInclusive, toKey, toInclusive); - } - } - - /** - * A prefix {@link RangeEntrySet} view of the {@link Trie} - */ - private final class PrefixRangeEntrySet extends RangeEntrySet - { - private final PrefixRangeMap delegate; - - private TrieEntry prefixStart; - - private int expectedModCount = -1; - - /** - * Creates a {@link PrefixRangeEntrySet} - */ - public PrefixRangeEntrySet(PrefixRangeMap delegate) - { - super(delegate); - this.delegate = delegate; - } - - @Override - public int size() - { - return delegate.fixup(); - } - - @Override - public Iterator> iterator() - { - if (PatriciaTrie.this.modCount != expectedModCount) - { - prefixStart = subtree(delegate.prefix); - expectedModCount = PatriciaTrie.this.modCount; - } - - if (prefixStart == null) - { - Set> empty = Collections.emptySet(); - return empty.iterator(); - } - else if (lengthInBits(delegate.prefix) >= prefixStart.bitIndex) - { - return new SingletonIterator(prefixStart); - } - else - { - return new EntryIterator(prefixStart, delegate.prefix); - } - } - - /** - * An {@link Iterator} that holds a single {@link TrieEntry}. - */ - private final class SingletonIterator implements Iterator> - { - private final TrieEntry entry; - - private int hit = 0; - - public SingletonIterator(TrieEntry entry) - { - this.entry = entry; - } - - @Override - public boolean hasNext() - { - return hit == 0; - } - - @Override - public Map.Entry next() - { - if (hit != 0) - throw new NoSuchElementException(); - - ++hit; - return entry; - } - - - @Override - public void remove() - { - if (hit != 1) - throw new IllegalStateException(); - - ++hit; - PatriciaTrie.this.removeEntry(entry); - } - } - - /** - * An {@link Iterator} for iterating over a prefix search. - */ - private final class EntryIterator extends TrieIterator> - { - // values to reset the subtree if we remove it. - protected final K prefix; - protected boolean lastOne; - - protected TrieEntry subtree; // the subtree to search within - - /** - * Starts iteration at the given entry & search only - * within the given subtree. - */ - EntryIterator(TrieEntry startScan, K prefix) - { - subtree = startScan; - next = PatriciaTrie.this.followLeft(startScan); - this.prefix = prefix; - } - - @Override - public Map.Entry next() - { - Map.Entry entry = nextEntry(); - if (lastOne) - next = null; - return entry; - } - - @Override - protected TrieEntry findNext(TrieEntry prior) - { - return PatriciaTrie.this.nextEntryInSubtree(prior, subtree); - } - - @Override - public void remove() - { - // If the current entry we're removing is the subtree - // then we need to find a new subtree parent. - boolean needsFixing = false; - int bitIdx = subtree.bitIndex; - if (current == subtree) - needsFixing = true; - - super.remove(); - - // If the subtree changed its bitIndex or we - // removed the old subtree, get a new one. - if (bitIdx != subtree.bitIndex || needsFixing) - subtree = subtree(prefix); - - // If the subtree's bitIndex is less than the - // length of our prefix, it's the last item - // in the prefix tree. - if (lengthInBits(prefix) >= subtree.bitIndex) - lastOne = true; - } - } - } -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/Trie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/Trie.java deleted file mode 100644 index 1866fec0c75b..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/Trie.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2005-2010 Roger Kapsi, Sam Berlin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -import java.util.Map; -import java.util.SortedMap; - -import org.apache.cassandra.index.sasi.utils.trie.Cursor.Decision; - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -/** - * Defines the interface for a prefix tree, an ordered tree data structure. For - * more information, see Tries. - * - * @author Roger Kapsi - * @author Sam Berlin - */ -public interface Trie extends SortedMap -{ - /** - * Returns the {@link Map.Entry} whose key is closest in a bitwise XOR - * metric to the given key. This is NOT lexicographic closeness. - * For example, given the keys: - * - *

      - *
    1. D = 1000100 - *
    2. H = 1001000 - *
    3. L = 1001100 - *
    - * - * If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would - * return 'L', because the XOR distance between D & L is smaller - * than the XOR distance between D & H. - * - * @return The {@link Map.Entry} whose key is closest in a bitwise XOR metric - * to the provided key. - */ - Map.Entry select(K key); - - /** - * Returns the key that is closest in a bitwise XOR metric to the - * provided key. This is NOT lexicographic closeness! - * - * For example, given the keys: - * - *
      - *
    1. D = 1000100 - *
    2. H = 1001000 - *
    3. L = 1001100 - *
    - * - * If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would - * return 'L', because the XOR distance between D & L is smaller - * than the XOR distance between D & H. - * - * @return The key that is closest in a bitwise XOR metric to the provided key. - */ - @SuppressWarnings("unused") - K selectKey(K key); - - /** - * Returns the value whose key is closest in a bitwise XOR metric to - * the provided key. This is NOT lexicographic closeness! - * - * For example, given the keys: - * - *
      - *
    1. D = 1000100 - *
    2. H = 1001000 - *
    3. L = 1001100 - *
    - * - * If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would - * return 'L', because the XOR distance between D & L is smaller - * than the XOR distance between D & H. - * - * @return The value whose key is closest in a bitwise XOR metric - * to the provided key. - */ - @SuppressWarnings("unused") - V selectValue(K key); - - /** - * Iterates through the {@link Trie}, starting with the entry whose bitwise - * value is closest in an XOR metric to the given key. After the closest - * entry is found, the {@link Trie} will call select on that entry and continue - * calling select for each entry (traversing in order of XOR closeness, - * NOT lexicographically) until the cursor returns {@link Decision#EXIT}. - * - *

    The cursor can return {@link Decision#CONTINUE} to continue traversing. - * - *

    {@link Decision#REMOVE_AND_EXIT} is used to remove the current element - * and stop traversing. - * - *

    Note: The {@link Decision#REMOVE} operation is not supported. - * - * @return The entry the cursor returned {@link Decision#EXIT} on, or null - * if it continued till the end. - */ - Map.Entry select(K key, Cursor cursor); - - /** - * Traverses the {@link Trie} in lexicographical order. - * {@link Cursor#select(java.util.Map.Entry)} will be called on each entry. - * - *

    The traversal will stop when the cursor returns {@link Decision#EXIT}, - * {@link Decision#CONTINUE} is used to continue traversing and - * {@link Decision#REMOVE} is used to remove the element that was selected - * and continue traversing. - * - *

    {@link Decision#REMOVE_AND_EXIT} is used to remove the current element - * and stop traversing. - * - * @return The entry the cursor returned {@link Decision#EXIT} on, or null - * if it continued till the end. - */ - Map.Entry traverse(Cursor cursor); - - /** - * Returns a view of this {@link Trie} of all elements that are prefixed - * by the given key. - * - *

    In a {@link Trie} with fixed size keys, this is essentially a - * {@link #get(Object)} operation. - * - *

    For example, if the {@link Trie} contains 'Anna', 'Anael', - * 'Analu', 'Andreas', 'Andrea', 'Andres', and 'Anatole', then - * a lookup of 'And' would return 'Andreas', 'Andrea', and 'Andres'. - */ - SortedMap prefixMap(K prefix); -} diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/Tries.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/Tries.java deleted file mode 100644 index 84080c2b4d2c..000000000000 --- a/src/java/org/apache/cassandra/index/sasi/utils/trie/Tries.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2005-2010 Roger Kapsi - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified - * to correspond to Cassandra code style, as the only Patricia Trie implementation, - * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based - * on rkapsi/patricia-trie project) only supports String keys) - * but unfortunately is not deployed to the maven central as a downloadable artifact. - */ - -package org.apache.cassandra.index.sasi.utils.trie; - -/** - * A collection of {@link Trie} utilities - */ -public class Tries -{ - /** - * Returns true if bitIndex is a {@link KeyAnalyzer#OUT_OF_BOUNDS_BIT_KEY} - */ - static boolean isOutOfBoundsIndex(int bitIndex) - { - return bitIndex == KeyAnalyzer.OUT_OF_BOUNDS_BIT_KEY; - } - - /** - * Returns true if bitIndex is a {@link KeyAnalyzer#EQUAL_BIT_KEY} - */ - static boolean isEqualBitKey(int bitIndex) - { - return bitIndex == KeyAnalyzer.EQUAL_BIT_KEY; - } - - /** - * Returns true if bitIndex is a {@link KeyAnalyzer#NULL_BIT_KEY} - */ - static boolean isNullBitKey(int bitIndex) - { - return bitIndex == KeyAnalyzer.NULL_BIT_KEY; - } - - /** - * Returns true if the given bitIndex is valid. Indices - * are considered valid if they're between 0 and - * {@link Integer#MAX_VALUE} - */ - static boolean isValidBitIndex(int bitIndex) - { - return 0 <= bitIndex; - } - - /** - * Returns true if both values are either null or equal - */ - static boolean areEqual(Object a, Object b) - { - return (a == null ? b == null : a.equals(b)); - } - - /** - * Throws a {@link NullPointerException} with the given message if - * the argument is null. - */ - static T notNull(T o, String message) - { - if (o == null) - throw new NullPointerException(message); - - return o; - } - - /** - * A utility method to cast keys. It actually doesn't - * cast anything. It's just fooling the compiler! - */ - @SuppressWarnings("unchecked") - static K cast(Object key) - { - return (K)key; - } -} diff --git a/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java b/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java index 51533c22a9a7..b97de9c86598 100644 --- a/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java +++ b/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java @@ -63,6 +63,11 @@ public interface UpdateTransaction extends IndexTransaction void onPartitionDeletion(DeletionTime deletionTime); void onRangeTombstone(RangeTombstone rangeTombstone); void onInserted(Row row); + + /** + * @param existing the existing row from the Memtable + * @param updated the updated version, which includes the update merged with the existing version + */ void onUpdated(Row existing, Row updated); UpdateTransaction NO_OP = new UpdateTransaction() diff --git a/src/java/org/apache/cassandra/io/FSDiskFullWriteError.java b/src/java/org/apache/cassandra/io/FSDiskFullWriteError.java index abf0b6ca6947..cf3dd7687e1d 100644 --- a/src/java/org/apache/cassandra/io/FSDiskFullWriteError.java +++ b/src/java/org/apache/cassandra/io/FSDiskFullWriteError.java @@ -22,10 +22,19 @@ public class FSDiskFullWriteError extends FSWriteError { + private final String keyspace; + public FSDiskFullWriteError(String keyspace, long mutationSize) { super(new IOException(String.format("Insufficient disk space to write %d bytes into the %s keyspace", mutationSize, keyspace))); + + this.keyspace = keyspace; + } + + public String getKeyspace() + { + return keyspace; } } diff --git a/src/java/org/apache/cassandra/io/FSError.java b/src/java/org/apache/cassandra/io/FSError.java index 4c06d9c61f1a..afe1fc635dce 100644 --- a/src/java/org/apache/cassandra/io/FSError.java +++ b/src/java/org/apache/cassandra/io/FSError.java @@ -25,11 +25,11 @@ public abstract class FSError extends IOError { final String message; - public final String path; + public final File file; - public FSError(Throwable cause, File path) + public FSError(Throwable cause, File file) { - this(null, cause, path); + this(null, cause, file); } public FSError(Throwable cause, Path path) @@ -37,18 +37,24 @@ public FSError(Throwable cause, Path path) this(null, cause, path); } - public FSError(String message, Throwable cause, File path) + public FSError(String message, Throwable cause, File file) { super(cause); this.message = message; - this.path = path.toString(); + this.file = file; } public FSError(String message, Throwable cause, Path path) { super(cause); this.message = message; - this.path = path.toString(); + this.file = new File(path); + } + + @Override + public String getMessage() + { + return super.getMessage() + " on file " + String.valueOf(file); } /** @@ -70,6 +76,6 @@ public static FSError findNested(Throwable top) @Override public String toString() { - return getClass().getSimpleName() + (message != null ? ' ' + message : "") + (path != null ? " in " + path : ""); + return getClass().getSimpleName() + (message != null ? ' ' + message : "") + (file != null ? " in " + file : ""); } } diff --git a/src/java/org/apache/cassandra/io/FSNoDiskAvailableForWriteError.java b/src/java/org/apache/cassandra/io/FSNoDiskAvailableForWriteError.java index 415f204b113d..15b130a95c63 100644 --- a/src/java/org/apache/cassandra/io/FSNoDiskAvailableForWriteError.java +++ b/src/java/org/apache/cassandra/io/FSNoDiskAvailableForWriteError.java @@ -25,9 +25,18 @@ */ public class FSNoDiskAvailableForWriteError extends FSWriteError { + private final String keyspace; + public FSNoDiskAvailableForWriteError(String keyspace) { super(new IOException(String.format("The data directories for the %s keyspace have been marked as unwritable", keyspace))); + + this.keyspace = keyspace; + } + + public String getKeyspace() + { + return keyspace; } } diff --git a/src/java/org/apache/cassandra/io/FSReadError.java b/src/java/org/apache/cassandra/io/FSReadError.java index ac1553477032..b785a28efa7c 100644 --- a/src/java/org/apache/cassandra/io/FSReadError.java +++ b/src/java/org/apache/cassandra/io/FSReadError.java @@ -29,9 +29,9 @@ public FSReadError(Throwable cause, Path path) super(cause, path); } - public FSReadError(Throwable cause, File path) + public FSReadError(Throwable cause, File file) { - super(cause, path); + super(cause, file); } public FSReadError(Throwable cause, String path) diff --git a/src/java/org/apache/cassandra/io/compress/AdaptiveCompressor.java b/src/java/org/apache/cassandra/io/compress/AdaptiveCompressor.java new file mode 100644 index 000000000000..e8b5e1e1206c --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/AdaptiveCompressor.java @@ -0,0 +1,524 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdCompressCtx; +import com.github.luben.zstd.ZstdDecompressCtx; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.memtable.AbstractAllocatorMemtable; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.metrics.CassandraMetricsRegistry; +import org.apache.cassandra.metrics.DefaultNameFactory; +import org.apache.cassandra.metrics.MetricNameFactory; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.ExpMovingAverage; + +/** + * A compressor that dynamically adapts the compression level to the load. + * If the system is not heavily loaded by writes, data are compressed using high compression level. + * If the number of compactions or flushes queue up, it decreases the compression level to speed up + * flushing / compacting. + *

    + * Underneath, the ZStandard compressor is used. The compression level can be changed between the frames, + * even when compressing the same sstable file. ZStandard was chosen because at the fast end it + * can reach compression speed of LZ4, but at moderate compression levels it usually offers much better + * compression ratio (typically files are smaller by 20-40%) than LZ4 without compromising compression speed by too much. + *

    + * This compressor can be used for either of Uses: FAST_COMPRESSION and GENERAL. + * Each use can have different minimum and maximum compression level limits. + * For FAST_COMPRESSION, the number of pending flushes is used as the indicator of write load. + * For GENERAL compression, the number of pending compactions is used as the indicator of write load. + *

    + * Valid compression levels are in range 0..15 (inclusive), where 0 means fastest compression and 15 means slowest/best. + * Usually levels around 7-11 strike the best balance between performance and compresion ratio. + * Going above level 12 usually only results in slower compression but not much compression ratio improvement. + *

    + * Caution: This compressor decompresses about 2x-4x slower than LZ4Compressor, regardless of the compression level. + * Therefore, it may negatively affect read speed from very read-heavy tables, especially when the chunk-cache + * hit ratio is low. In synthetic tests with chunk cache disabled, read throughput turned out to be up to 10% + * lower than when using LZ4 on some workloads. + */ +public class AdaptiveCompressor implements ICompressor +{ + @VisibleForTesting + static final Map metrics = new EnumMap<>(Map.of( + Uses.FAST_COMPRESSION, new Metrics(Uses.FAST_COMPRESSION), + Uses.GENERAL, new Metrics(Uses.GENERAL) + )); + + protected static final String MIN_COMPRESSION_LEVEL_OPTION_NAME = "min_compression_level"; + protected static final String MAX_COMPRESSION_LEVEL_OPTION_NAME = "max_compression_level"; + protected static final String MAX_COMPACTION_QUEUE_LENGTH_OPTION_NAME = "max_compaction_queue_length"; + + + /** + * Maps AdaptiveCompressor compression level to underlying ZStandard compression levels. + * This mapping is needed because ZStandard levels are not continuous, zstd level 0 is special and means level 3. + * Hence, we just use our own continuous scale starting at 0. + */ + private static final int[] zstdCompressionLevels = { + -7, // 0 (very fast but compresses poorly) + -6, // 1 + -5, // 2 (LZ4 level is somewhere here) + -4, // 3 + -3, // 4 + -2, // 5 + -1, // 6 + 1, // 7 (sweet spot area usually here) + 2, // 8 (sweet spot area usually here) + 3, // 9 (sweet spot area usually here, ~50% slower than LZ4) + 4, // 10 (sweet spot area usually here) + 5, // 11 (sweet spot area usually here) + 6, // 12 + 7, // 13 + 8, // 14 + 9, // 15 (very slow, usually over 10x slower than LZ4) + }; + + public static final int MIN_COMPRESSION_LEVEL = 0; + public static final int MAX_COMPRESSION_LEVEL = 15; + + public static final int DEFAULT_MIN_FAST_COMPRESSION_LEVEL = 2; // zstd level -5 + public static final int DEFAULT_MAX_FAST_COMPRESSION_LEVEL = 9; // zstd level 5 + public static final int DEFAULT_MIN_GENERAL_COMPRESSION_LEVEL = 7; // zstd level 1 + public static final int DEFAULT_MAX_GENERAL_COMPRESSION_LEVEL = 12; // zstd level 6 + public static final int DEFAULT_MAX_COMPACTION_QUEUE_LENGTH = 16; + + private static final ConcurrentHashMap instances = new ConcurrentHashMap<>(); + + public static AdaptiveCompressor create(Map options) + { + int minCompressionLevel = getMinCompressionLevel(Uses.GENERAL, options); + int maxCompressionLevel = getMaxCompressionLevel(Uses.GENERAL, options); + int maxCompactionQueueLength = getMaxCompactionQueueLength(options); + return createForCompaction(minCompressionLevel, maxCompressionLevel, maxCompactionQueueLength); + } + + private static AdaptiveCompressor createForCompaction(int minCompressionLevel, int maxCompressionLevel, int maxCompactionQueueLength) + { + Params params = new Params(Uses.GENERAL, minCompressionLevel, maxCompressionLevel, maxCompactionQueueLength); + Supplier compactionPressureSupplier = () -> getCompactionPressure(maxCompactionQueueLength); + return instances.computeIfAbsent(params, p -> new AdaptiveCompressor(p, compactionPressureSupplier)); + } + + /** + * Creates a compressor that doesn't refer to any other C* components like compaction manager or memory pools. + */ + @VisibleForTesting + public static ICompressor createForUnitTesting() + { + Params params = new Params(Uses.GENERAL, 9, 9, 0); + return new AdaptiveCompressor(params, () -> 0.0); + } + + public static AdaptiveCompressor createForFlush(Map options) + { + int minCompressionLevel = getMinCompressionLevel(Uses.FAST_COMPRESSION, options); + int maxCompressionLevel = getMaxCompressionLevel(Uses.FAST_COMPRESSION, options); + return createForFlush(minCompressionLevel, maxCompressionLevel); + } + + private static AdaptiveCompressor createForFlush(int minCompressionLevel, int maxCompressionLevel) + { + Params params = new Params(Uses.FAST_COMPRESSION, minCompressionLevel, maxCompressionLevel, 0); + return instances.computeIfAbsent(params, p -> new AdaptiveCompressor(p, AdaptiveCompressor::getFlushPressure)); + } + + final Params params; + private final ThreadLocal state; + private final Supplier writePressureSupplier; + + static class Params + { + final Uses use; + final int minCompressionLevel; + final int maxCompressionLevel; + final int maxCompactionQueueLength; + + Params(Uses use, int minCompressionLevel, int maxCompressionLevel, int maxCompactionQueueLength) + { + if (minCompressionLevel < MIN_COMPRESSION_LEVEL || minCompressionLevel > MAX_COMPRESSION_LEVEL) + throw new IllegalArgumentException("Min compression level " + minCompressionLevel + "out of range" + + " [" + MIN_COMPRESSION_LEVEL + ", " + MAX_COMPRESSION_LEVEL + ']'); + if (maxCompressionLevel < MIN_COMPRESSION_LEVEL || maxCompressionLevel > MAX_COMPRESSION_LEVEL) + throw new IllegalArgumentException("Max compression level " + maxCompressionLevel + "out of range" + + " [" + MIN_COMPRESSION_LEVEL + ", " + MAX_COMPRESSION_LEVEL + ']'); + if (maxCompactionQueueLength < 0) + throw new IllegalArgumentException("Negative max compaction queue length: " + maxCompactionQueueLength); + + this.use = use; + this.minCompressionLevel = minCompressionLevel; + this.maxCompressionLevel = maxCompressionLevel; + this.maxCompactionQueueLength = maxCompactionQueueLength; + } + + @Override + public boolean equals(Object o) + { + if (o == null || getClass() != o.getClass()) + return false; + Params params = (Params) o; + return minCompressionLevel == params.minCompressionLevel + && maxCompressionLevel == params.maxCompressionLevel + && maxCompactionQueueLength == params.maxCompactionQueueLength + && use == params.use; + } + + @Override + public int hashCode() + { + return Objects.hash(use, minCompressionLevel, maxCompressionLevel, maxCompactionQueueLength); + } + } + + /** + * Keeps thread local state. + * We need this because we want to not only monitor pending flushes/compactions but also how much + * time we spend in compression relative to time spent by the thread in non-compression tasks like preparation + * of data or writing. Because ICompressor can be shared by multiple threads, we need to keep + * track of each thread separately. + */ + class State + { + final ZstdCompressCtx compressCtx = new ZstdCompressCtx().setChecksum(true); + final ZstdDecompressCtx decompressCtx = new ZstdDecompressCtx(); + + /** + * ZStandard compression level that was used when compressing the previous chunk. + * Can be adjusted up or down by at most 1 with every next block. + */ + int currentCompressionLevel; + + /** + * How much time is spent by the thread in compression relative to the time spent in non-compression code. + * Valid range is [0.0, 1.0]. + * 1.0 means we're doing only compression and nothing else. + * 0.0 means we're not spending any time doing compression. + * This indicator allows us to detect whether we're bottlenecked by something else than compression, + * e.g. by disk I/O or by preparation of data to compress (e.g. iterating the memtable trie). + * If this value is low, then there is not much gain in decreasing the compression + * level. + */ + ExpMovingAverage relativeTimeSpentCompressing = ExpMovingAverage.decayBy10(); + + long lastCompressionStartTime; + long lastCompressionDuration; + + /** + * Computes the new compression level to use for the next chunk, based on the load. + */ + public void adjustCompressionLevel(long currentTime) + { + // The more write "pressure", the faster we want to go, so the lower the desired compression level. + double pressure = getWritePressure(); + assert pressure >= 0.0 && pressure <= 1.0 : "pressure (" + pressure + ") out of valid range [0.0, 1.0]"; + + // Use minCompressionLevel when pressure = 1.0, maxCompressionLevel when pressure = 0.0 + int pressurePoints = (int) (pressure * (params.maxCompressionLevel - params.minCompressionLevel)); + int compressionLevelTarget = params.maxCompressionLevel - pressurePoints; + + // We use wall clock time and not CPU time, because we also want to include time spent by I/O. + // If we're bottlenecked by writing the data to disk, this indicator should be low. + double relativeTimeSpentCompressing = (double) (1 + lastCompressionDuration) / (1 + currentTime - lastCompressionStartTime); + + // Some smoothing is needed to avoid changing level too fast due to performance hiccups + this.relativeTimeSpentCompressing.update(relativeTimeSpentCompressing); + + // If we're under pressure to write data fast, we need to decrease compression level. + // But we do that only if we're really spending significant amount of time doing compression. + if (compressionLevelTarget < currentCompressionLevel && this.relativeTimeSpentCompressing.get() > 0.1) + currentCompressionLevel--; + // If we're not under heavy write pressure, or we're spending very little time compressing data, + // we can increase the compression level and get some space savings at a low performance overhead: + else if (compressionLevelTarget > currentCompressionLevel || this.relativeTimeSpentCompressing.get() < 0.02) + currentCompressionLevel++; + + currentCompressionLevel = clampCompressionLevel(currentCompressionLevel); + compressCtx.setLevel(zstdCompressionLevels[currentCompressionLevel]); + } + + /** + * Must be called after compressing a chunk, + * so we can measure how much time we spend in compressing vs time spent not-compressing. + */ + public void recordCompressionDuration(long startTime, long endTime) + { + this.lastCompressionDuration = endTime - startTime; + this.lastCompressionStartTime = startTime; + } + + @VisibleForTesting + double getRelativeTimeSpentCompressing() + { + return this.relativeTimeSpentCompressing.get(); + } + } + + /** + * @param params user-provided configuration such as min/max compression level range + * @param writePressureSupplier returns a non-negative score determining the write load on the system which + * is used to control the desired compression level. Influences the compression + * level linearly: an inceease of pressure by 1 point causes the target + * compression level to be decreased by 1 point. Zero will select the + * maximum allowed compression level. + */ + @VisibleForTesting + AdaptiveCompressor(Params params, Supplier writePressureSupplier) + { + this.params = params; + this.state = new ThreadLocal<>(); + this.writePressureSupplier = writePressureSupplier; + } + + @Override + public int initialCompressedBufferLength(int chunkLength) + { + return (int) Zstd.compressBound(chunkLength); + } + + + @Override + public void compress(ByteBuffer input, ByteBuffer output) throws IOException + { + try + { + State state = getThreadLocalState(); + long startTime = Clock.Global.nanoTime(); + state.adjustCompressionLevel(startTime); + long inputSize = input.remaining(); + state.compressCtx.compress(output, input); + long endTime = Clock.Global.nanoTime(); + state.recordCompressionDuration(startTime, endTime); + + Metrics m = metrics.get(params.use); + m.updateFrom(state); + m.compressionRate.mark(inputSize); + } + catch (Exception e) + { + throw new IOException("Compression failed", e); + } + + } + + @Override + public int uncompress(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset) throws IOException + { + State state = getThreadLocalState(); + long dsz = state.decompressCtx.decompressByteArray(output, outputOffset, output.length - outputOffset, + input, inputOffset, inputLength); + + if (Zstd.isError(dsz)) + throw new IOException(String.format("Decompression failed due to %s", Zstd.getErrorName(dsz))); + + metrics.get(params.use).decompressionRate.mark(dsz); + return (int) dsz; + } + + @Override + public void uncompress(ByteBuffer input, ByteBuffer output) throws IOException + { + try + { + State state = getThreadLocalState(); + long dsz = state.decompressCtx.decompress(output, input); + metrics.get(params.use).decompressionRate.mark(dsz); + } catch (Exception e) + { + throw new IOException("Decompression failed", e); + } + } + + @Override + public BufferType preferredBufferType() + { + return BufferType.OFF_HEAP; + } + + @Override + public Set recommendedUses() + { + return params.minCompressionLevel <= DEFAULT_MIN_FAST_COMPRESSION_LEVEL + ? EnumSet.of(Uses.GENERAL, Uses.FAST_COMPRESSION) + : EnumSet.of(params.use); + } + + @Override + public ICompressor forUse(Uses use) + { + if (use == params.use) + return this; + + switch (use) + { + case GENERAL: + return createForCompaction(params.minCompressionLevel, params.maxCompressionLevel, params.maxCompactionQueueLength); + case FAST_COMPRESSION: + return createForFlush(params.minCompressionLevel, params.maxCompressionLevel); + } + + return null; + } + + @Override + public boolean supports(BufferType bufferType) + { + return bufferType == BufferType.OFF_HEAP; + } + + @Override + public Set supportedOptions() + { + return Set.of("max_compression_level", "min_compression_level"); + } + + private static int getMinCompressionLevel(Uses mode, Map options) + { + int defaultValue = mode == Uses.FAST_COMPRESSION ? DEFAULT_MIN_FAST_COMPRESSION_LEVEL : DEFAULT_MIN_GENERAL_COMPRESSION_LEVEL; + return getIntOption(options, MIN_COMPRESSION_LEVEL_OPTION_NAME, defaultValue); + } + + private static int getMaxCompressionLevel(Uses mode, Map options) + { + var defaultValue = mode == Uses.FAST_COMPRESSION ? DEFAULT_MAX_FAST_COMPRESSION_LEVEL : DEFAULT_MAX_GENERAL_COMPRESSION_LEVEL; + return getIntOption(options, MAX_COMPRESSION_LEVEL_OPTION_NAME, defaultValue); + } + + private static int getMaxCompactionQueueLength(Map options) + { + return getIntOption(options, MAX_COMPACTION_QUEUE_LENGTH_OPTION_NAME, DEFAULT_MAX_COMPACTION_QUEUE_LENGTH); + } + + private static int getIntOption(Map options, String key, int defaultValue) + { + if (options == null) + return defaultValue; + + String val = options.get(key); + if (val == null) + return defaultValue; + + return Integer.parseInt(val); + } + + private double getWritePressure() + { + return writePressureSupplier.get(); + } + + private static double getFlushPressure() + { + var memoryPool = AbstractAllocatorMemtable.MEMORY_POOL; + var usedRatio = Math.max(memoryPool.onHeap.usedRatio(), memoryPool.offHeap.usedRatio()); + var cleanupThreshold = DatabaseDescriptor.getMemtableCleanupThreshold(); + // we max out the pressure when we're halfway between the cleanupThreshold and max memory + // so we still have some memory left while compression already working at max speed; + // setting the compressor to maximum speed when we exhausted all memory would be too late + return Math.min(1.0, Math.max(0.0, 2 * (usedRatio - cleanupThreshold)) / (1.0 - cleanupThreshold)); + } + + private static double getCompactionPressure(int maxCompactionQueueLength) + { + CompactionManager compactionManager = CompactionManager.instance; + long rateLimit = DatabaseDescriptor.getCompactionThroughputMebibytesPerSecAsInt() * FileUtils.ONE_MIB; + if (rateLimit == 0) + rateLimit = Long.MAX_VALUE; + double actualRate = compactionManager.getMetrics().bytesCompactedThroughput.getOneMinuteRate(); + // We don't want to speed up compression if we can keep up with the configured compression rate limit + // 0.0 if actualRate >= rateLimit + // 1.0 if actualRate <= 0.8 * rateLimit; + double rateLimitFactor = Math.min(1.0, Math.max(0.0, (rateLimit - actualRate) / (0.2 * rateLimit))); + assert rateLimitFactor >= 0.0 : "rateLimitFactor (" + rateLimitFactor + " out of valid range [0.0, 1.0]"; + + long pendingCompactions = compactionManager.getPendingTasks(); + double compactionQueuePressure = Math.min(1.0, (double) pendingCompactions / (maxCompactionQueueLength * DatabaseDescriptor.getConcurrentCompactors())); + assert compactionQueuePressure >= 0.0 && compactionQueuePressure <= 1.0 + : "compactionQueuePressure (" + compactionQueuePressure + ") out of valid range [0.0, 1.0]"; + return compactionQueuePressure * rateLimitFactor; + } + + private int clampCompressionLevel(long compressionLevel) + { + return (int) Math.min(params.maxCompressionLevel, Math.max(params.minCompressionLevel, compressionLevel)); + } + + @VisibleForTesting + State getThreadLocalState() + { + State state = this.state.get(); + if (state == null) + { + state = new State(); + state.currentCompressionLevel = params.maxCompressionLevel; + state.lastCompressionDuration = 0; + this.state.set(state); + } + return state; + } + + static class Metrics + { + private final Counter[] compressionLevelHistogram; // separate counters for each compression level + private final Histogram relativeTimeSpentCompressing; // in % (i.e. multiplied by 100 becaue Histogram can only keep integers) + private final Meter compressionRate; + private final Meter decompressionRate; + + + Metrics(Uses use) + { + MetricNameFactory factory = new DefaultNameFactory("AdaptiveCompression"); + + // cannot use Metrics.histogram for compression levels, because histograms do not handle negative numbers; + // also this histogram is small enough that storing all buckets is not a problem, but it gives + // much more information + compressionLevelHistogram = new Counter[MAX_COMPRESSION_LEVEL + 1]; + for (int i = 0; i < compressionLevelHistogram.length; i++) + { + CassandraMetricsRegistry.MetricName metricName = factory.createMetricName(String.format("CompressionLevel_%s_%02d", use.name(), i)); + compressionLevelHistogram[i] = CassandraMetricsRegistry.Metrics.counter(metricName); + } + + relativeTimeSpentCompressing = CassandraMetricsRegistry.Metrics.histogram(factory.createMetricName("RelativeTimeSpentCompressing_" + use.name()), true); + + compressionRate = CassandraMetricsRegistry.Metrics.meter(factory.createMetricName("CompressionRate_" + use.name())); + decompressionRate = CassandraMetricsRegistry.Metrics.meter(factory.createMetricName("DecompressionRate_" + use.name())); + } + + void updateFrom(State state) + { + compressionLevelHistogram[state.currentCompressionLevel].inc(); + relativeTimeSpentCompressing.update((int)(state.getRelativeTimeSpentCompressing() * 100.0)); + } + } +} diff --git a/src/java/org/apache/cassandra/io/compress/BufferType.java b/src/java/org/apache/cassandra/io/compress/BufferType.java index 881780229f74..5f75958f9762 100644 --- a/src/java/org/apache/cassandra/io/compress/BufferType.java +++ b/src/java/org/apache/cassandra/io/compress/BufferType.java @@ -42,4 +42,9 @@ public static BufferType typeOf(ByteBuffer buffer) { return buffer.isDirect() ? OFF_HEAP : ON_HEAP; } + + public static BufferType preferredForCompression() + { + return OFF_HEAP; + } } diff --git a/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java b/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java index 4a7f8161cc0d..8bf883496d93 100644 --- a/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java +++ b/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java @@ -20,10 +20,16 @@ import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import java.nio.channels.FileChannel; import java.util.Optional; import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.FSWriteError; @@ -31,6 +37,7 @@ import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.util.ChecksumWriter; import org.apache.cassandra.io.util.DataPosition; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.SequentialWriter; @@ -42,6 +49,8 @@ public class CompressedSequentialWriter extends SequentialWriter { + protected static final Logger logger = LoggerFactory.getLogger(CompressedSequentialWriter.class); + private final ChecksumWriter crcMetadata; // holds offset in the file where current chunk should be written @@ -58,8 +67,6 @@ public class CompressedSequentialWriter extends SequentialWriter // holds a number of already written chunks private int chunkCount = 0; - private long uncompressedSize = 0, compressedSize = 0; - private final MetadataCollector sstableMetadataCollector; private final ByteBuffer crcCheckBuffer = ByteBuffer.allocate(4); @@ -67,6 +74,12 @@ public class CompressedSequentialWriter extends SequentialWriter private final int maxCompressedLength; + /** + * When corruption is found, the file writer is reset to previous data point but we can't reset the CRC checksum. + * So we have to recompute digest value. + */ + private boolean recomputeChecksum = false; + /** * Create CompressedSequentialWriter without digest file. * @@ -85,12 +98,12 @@ public CompressedSequentialWriter(File file, MetadataCollector sstableMetadataCollector) { super(file, SequentialWriterOption.newBuilder() - .bufferSize(option.bufferSize()) - .bufferType(option.bufferType()) - .bufferSize(parameters.chunkLength()) - .bufferType(parameters.getSstableCompressor().preferredBufferType()) - .finishOnClose(option.finishOnClose()) - .build()); + .bufferSize(option.bufferSize()) + .bufferType(option.bufferType()) + .bufferSize(parameters.chunkLength()) + .bufferType(parameters.getSstableCompressor().preferredBufferType()) + .finishOnClose(option.finishOnClose()) + .build()); this.compressor = parameters.getSstableCompressor(); this.digestFile = Optional.ofNullable(digestFile); @@ -115,7 +128,7 @@ public long getOnDiskFilePointer() } catch (IOException e) { - throw new FSReadError(e, getPath()); + throw new FSReadError(e, getFile()); } } @@ -141,6 +154,15 @@ protected void flushData() { seekToChunkStart(); // why is this necessary? seems like it should always be at chunk start in normal operation + if (buffer.limit() == 0) + { + // nothing to compress + if (runPostFlush != null) + runPostFlush.accept(0); + + return; + } + try { // compressing data with buffer re-use @@ -155,7 +177,6 @@ protected void flushData() int uncompressedLength = buffer.position(); int compressedLength = compressed.position(); - uncompressedSize += uncompressedLength; ByteBuffer toWrite = compressed; if (compressedLength >= maxCompressedLength) { @@ -175,7 +196,6 @@ protected void flushData() compressedLength = maxCompressedLength; } } - compressedSize += compressedLength; try { @@ -190,11 +210,11 @@ protected void flushData() // write corresponding checksum toWrite.rewind(); crcMetadata.appendDirect(toWrite, true); - lastFlushOffset = uncompressedSize; + lastFlushOffset += uncompressedLength; } catch (IOException e) { - throw new FSWriteError(e, getPath()); + throw new FSWriteError(e, getFile()); } if (toWrite == buffer) buffer.position(uncompressedLength); @@ -208,7 +228,7 @@ protected void flushData() public CompressionMetadata open(long overrideLength) { if (overrideLength <= 0) - overrideLength = uncompressedSize; + overrideLength = lastFlushOffset; return metadataWriter.open(overrideLength, chunkOffset); } @@ -222,6 +242,21 @@ public DataPosition mark() @Override public synchronized void resetAndTruncate(DataPosition mark) + { + try + { + doResetAndTruncate(mark); + } + catch (Throwable t) + { + CompressedFileWriterMark realMark = mark instanceof CompressedFileWriterMark ? (CompressedFileWriterMark) mark : null; + logger.error("Failed to reset and truncate {} at chunk offset {} because of {}", file.name(), + realMark == null ? -1 : realMark.chunkOffset, t.getMessage()); + throw t; + } + } + + private synchronized void doResetAndTruncate(DataPosition mark) { assert mark instanceof CompressedFileWriterMark; @@ -250,12 +285,12 @@ public synchronized void resetAndTruncate(DataPosition mark) compressed = compressor.preferredBufferType().allocate(chunkSize); } - try + try(FileChannel readChannel = StorageProvider.instance.writeTimeReadFileChannelFor(getFile())) { compressed.clear(); compressed.limit(chunkSize); - fchannel.position(chunkOffset); - fchannel.read(compressed); + readChannel.position(chunkOffset); + readChannel.read(compressed); try { @@ -269,7 +304,7 @@ public synchronized void resetAndTruncate(DataPosition mark) } catch (IOException e) { - throw new CorruptBlockException(getPath(), chunkOffset, chunkSize, e); + throw new CorruptBlockException(getFile(), chunkOffset, chunkSize, e); } CRC32 checksum = new CRC32(); @@ -277,22 +312,24 @@ public synchronized void resetAndTruncate(DataPosition mark) checksum.update(compressed); crcCheckBuffer.clear(); - fchannel.read(crcCheckBuffer); + readChannel.read(crcCheckBuffer); crcCheckBuffer.flip(); - if (crcCheckBuffer.getInt() != (int) checksum.getValue()) - throw new CorruptBlockException(getPath(), chunkOffset, chunkSize); + int storedChecksum = crcCheckBuffer.getInt(); + int computedChecksum = (int) checksum.getValue(); + if (storedChecksum != computedChecksum) + throw new CorruptBlockException(getFile(), chunkOffset, chunkSize, storedChecksum, computedChecksum); } catch (CorruptBlockException e) { - throw new CorruptSSTableException(e, getPath()); + throw new CorruptSSTableException(e, getFile()); } catch (EOFException e) { - throw new CorruptSSTableException(new CorruptBlockException(getPath(), chunkOffset, chunkSize), getPath()); + throw new CorruptSSTableException(new CorruptBlockException(getFile(), chunkOffset, chunkSize), getFile()); } catch (IOException e) { - throw new FSReadError(e, getPath()); + throw new FSReadError(e, getFile()); } // Mark as dirty so we can guarantee the newly buffered bytes won't be lost on a rebuffer @@ -301,9 +338,13 @@ public synchronized void resetAndTruncate(DataPosition mark) bufferOffset = truncateTarget - buffer.position(); chunkCount = realMark.nextChunkIndex - 1; - // truncate data and index file + // truncate data and index file. Unfortunately we can't reset and truncate CRC value, we have to recompute + // the CRC value otherwise it won't match the actual file checksum + recomputeChecksum = true; truncate(chunkOffset, bufferOffset); metadataWriter.resetAndTruncate(realMark.nextChunkIndex - 1); + + logger.info("reset and truncated {} to {}", file, chunkOffset); } private void truncate(long toFileSize, long toBufferOffset) @@ -315,7 +356,7 @@ private void truncate(long toFileSize, long toBufferOffset) } catch (IOException e) { - throw new FSWriteError(e, getPath()); + throw new FSWriteError(e, getFile()); } } @@ -332,7 +373,7 @@ private void seekToChunkStart() } catch (IOException e) { - throw new FSReadError(e, getPath()); + throw new FSReadError(e, getFile()); } } } @@ -390,8 +431,9 @@ protected Throwable doAbort(Throwable accumulate) protected void doPrepare() { syncInternal(); - digestFile.ifPresent(crcMetadata::writeFullChecksum); - sstableMetadataCollector.addCompressionRatio(compressedSize, uncompressedSize); + maybeWriteChecksum(); + + sstableMetadataCollector.addCompressionRatio(chunkOffset, lastFlushOffset); metadataWriter.finalizeLength(current(), chunkCount).prepareToCommit(); } @@ -410,6 +452,40 @@ protected Throwable doPreCleanup(Throwable accumulate) } } + private void maybeWriteChecksum() + { + if (digestFile.isEmpty()) + return; + + File digest = digestFile.get(); + if (recomputeChecksum) + { + logger.info("Rescanning data file to populate digest into {} because file writer has been reset and truncated", digest); + try (FileChannel fileChannel = StorageProvider.instance.writeTimeReadFileChannelFor(file); + InputStream stream = Channels.newInputStream(fileChannel)) + { + CRC32 checksum = new CRC32(); + try (CheckedInputStream checkedInputStream = new CheckedInputStream(stream, checksum)) + { + byte[] chunk = new byte[64 * 1024]; + while (checkedInputStream.read(chunk) >= 0) {} + + long digestValue = checkedInputStream.getChecksum().getValue(); + ChecksumWriter.writeFullChecksum(digest, digestValue); + } + } + catch (IOException e) + { + throw new FSWriteError(e, digest); + } + logger.info("Successfully recomputed checksum for {}", digest); + } + else + { + crcMetadata.writeFullChecksum(digest); + } + } + @Override protected SequentialWriter.TransactionalProxy txnProxy() { diff --git a/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java b/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java index d5f5f05655e9..750f2eb2b6da 100644 --- a/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java +++ b/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java @@ -27,8 +27,10 @@ import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicLong; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.primitives.Longs; import org.apache.cassandra.db.TypeSizes; @@ -45,7 +47,9 @@ import org.apache.cassandra.io.util.FileOutputStreamPlus; import org.apache.cassandra.io.util.Memory; import org.apache.cassandra.io.util.SafeMemory; +import org.apache.cassandra.io.util.SliceDescriptor; import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Ref; import org.apache.cassandra.utils.concurrent.Transactional; import org.apache.cassandra.utils.concurrent.WrappedSharedCloseable; @@ -56,22 +60,122 @@ */ public class CompressionMetadata extends WrappedSharedCloseable { - // dataLength can represent either the true length of the file - // or some shorter value, in the case we want to impose a shorter limit on readers - // (when early opening, we want to ensure readers cannot read past fully written sections) + /** + * This class extends Memory.LongArray in order to record the memory usage. + */ + public static class ChunkOffsetMemory extends Memory.LongArray + { + + public ChunkOffsetMemory(long size) + { + super(size); + } + + public ChunkOffsetMemory(SafeMemory memory, long cnt) + { + super(memory, cnt); + } + + @Override + public void close() + { + NATIVE_MEMORY_USAGE.addAndGet(-memoryUsed()); + super.close(); + } + } + + private static final AtomicLong NATIVE_MEMORY_USAGE = new AtomicLong(0); + /** + * DataLength can represent either the true length of the file + * or some shorter value, in the case we want to impose a shorter limit on readers + * (when early opening, we want to ensure readers cannot read past fully written sections). + * If zero copy metadata is present, this is the uncompressed length of the partial data file. + */ public final long dataLength; + + /** + * Length of the compressed file in bytes. This refers to the partial file length if zero copy metadata is present. + */ public final long compressedFileLength; - private final Memory chunkOffsets; - private final long chunkOffsetsSize; + + /** + * If true, the actual file size should be used instead of compressedFileLength when reading. + * This is used for encryption-only files where the final size isn't known at write time. + */ + public final boolean useActualFileSize; + + /** + * Offsets of consecutive chunks in the (compressed) data file. The length of this array is equal to the number of + * chunks. Each item is of Long type, thus 8 bytes long. Note that even if we deal with a partial data file (zero + * copy metadata is present), we store offsets of all chunks for the original (compressed) data file. + */ + private final ChunkOffsetMemory chunkOffsets; public final File chunksIndexFile; public final CompressionParams parameters; - @VisibleForTesting + /** + * The length of the chunk in bits. The chunk length must be a power of 2, so this is the number of trailing zeros + * in the chunk length. + */ + private final int chunkLengthBits; + + /** + * If we don't want to load the all offsets into memory, for example when we deal with a slice, this is the index of + * the first offset we loaded. + */ + private final int startChunkIndex; + public static CompressionMetadata open(File chunksIndexFile, long compressedLength, boolean hasMaxCompressedSize) { + return open(chunksIndexFile, compressedLength, hasMaxCompressedSize, SliceDescriptor.NONE); + } + + /** + * Create a CompressionMetadata for encryption-only (no compression) files. + * @param compressionParams The compression parameters containing the encryptor + * @return A CompressionMetadata instance suitable for encryption-only files + */ + public static CompressionMetadata encryptedOnly(CompressionParams compressionParams) + { + // For encryption-only files, we create a CompressionMetadata that doesn't limit the file size + // We'll create dummy chunk offsets that allow the reader to work with large files + int maxChunks = 1000; // Support files up to ~4MB + ChunkOffsetMemory offsets = new ChunkOffsetMemory(maxChunks + 1); + + // Set chunk offsets - each chunk is CHUNK_SIZE + 4 bytes apart + long offset = 0; + for (int i = 0; i <= maxChunks; i++) { + offsets.set(i, offset); + offset += EncryptedSequentialWriter.CHUNK_SIZE + 4; + } + + int chunkLength = EncryptedSequentialWriter.CHUNK_SIZE; + int chunkLengthBits = Integer.numberOfTrailingZeros(chunkLength); + + // Set large values for data and compressed lengths to avoid limiting the reader + long dataLength = (long) maxChunks * chunkLength; + // Use a reasonable default compressed length (will be ignored due to useActualFileSize flag) + long compressedLength = dataLength; + + return new CompressionMetadata(null, // chunksIndexFile + compressionParams, + offsets, + dataLength, + compressedLength, + chunkLengthBits, + 0, // startChunkIndex + true); // useActualFileSize = true for encryption-only files + } + + @VisibleForTesting + public static CompressionMetadata open(File chunksIndexFile, long compressedLength, boolean hasMaxCompressedSize, SliceDescriptor sliceDescriptor) + { + long uncompressedOffset = sliceDescriptor.exists() ? sliceDescriptor.sliceStart : 0; + long uncompressedLength = sliceDescriptor.exists() ? sliceDescriptor.dataEnd - sliceDescriptor.sliceStart : -1; + CompressionParams parameters; long dataLength; - Memory chunkOffsets; + ChunkOffsetMemory chunkOffsets; try (FileInputStreamPlus stream = chunksIndexFile.newInputStream()) { @@ -97,8 +201,25 @@ public static CompressionMetadata open(File chunksIndexFile, long compressedLeng throw new RuntimeException("Cannot create CompressionParams for stored parameters", e); } - dataLength = stream.readLong(); - chunkOffsets = readChunkOffsets(stream); + assert Integer.bitCount(chunkLength) == 1; + int chunkLengthBits = Integer.numberOfTrailingZeros(chunkLength); + long readDataLength = stream.readLong(); + dataLength = uncompressedLength >= 0 ? uncompressedLength : readDataLength; + + int startChunkIndex = Math.toIntExact(uncompressedOffset >> chunkLengthBits); + assert uncompressedOffset == (long) startChunkIndex << chunkLengthBits; + + int endChunkIndex = Math.toIntExact((uncompressedOffset + dataLength - 1) >> chunkLengthBits) + 1; + + Pair offsetsAndLimit = readChunkOffsets(stream, startChunkIndex, endChunkIndex, compressedLength); + chunkOffsets = offsetsAndLimit.left; + // We adjust the compressed file length to store the position after the last chunk just to be able to + // calculate the offset of the chunk next to the last one (in order to calculate the length of the last chunk). + // Obvously, we could use the compressed file length for that purpose but unfortunately, sometimes there is + // an empty chunk added to the end of the file thus we cannot rely on the file length. + long compressedFileLength = offsetsAndLimit.right; + + return new CompressionMetadata(chunksIndexFile, parameters, chunkOffsets, dataLength, compressedFileLength, chunkLengthBits, startChunkIndex); } catch (FileNotFoundException | NoSuchFileException e) { @@ -108,18 +229,30 @@ public static CompressionMetadata open(File chunksIndexFile, long compressedLeng { throw new CorruptSSTableException(e, chunksIndexFile); } - - return new CompressionMetadata(chunksIndexFile, parameters, chunkOffsets, chunkOffsets.size(), dataLength, compressedLength); } // do not call this constructor directly, unless used in testing @VisibleForTesting public CompressionMetadata(File chunksIndexFile, CompressionParams parameters, - Memory chunkOffsets, - long chunkOffsetsSize, + ChunkOffsetMemory chunkOffsets, long dataLength, - long compressedFileLength) + long compressedFileLength, + int chunkLengthBits, + int startChunkIndex) + { + this(chunksIndexFile, parameters, chunkOffsets, dataLength, compressedFileLength, chunkLengthBits, startChunkIndex, false); + } + + // Constructor with explicit useActualFileSize flag + private CompressionMetadata(File chunksIndexFile, + CompressionParams parameters, + ChunkOffsetMemory chunkOffsets, + long dataLength, + long compressedFileLength, + int chunkLengthBits, + int startChunkIndex, + boolean useActualFileSize) { super(chunkOffsets); this.chunksIndexFile = chunksIndexFile; @@ -127,7 +260,9 @@ public CompressionMetadata(File chunksIndexFile, this.dataLength = dataLength; this.compressedFileLength = compressedFileLength; this.chunkOffsets = chunkOffsets; - this.chunkOffsetsSize = chunkOffsetsSize; + this.chunkLengthBits = chunkLengthBits; + this.startChunkIndex = startChunkIndex; + this.useActualFileSize = useActualFileSize; } private CompressionMetadata(CompressionMetadata copy) @@ -138,7 +273,14 @@ private CompressionMetadata(CompressionMetadata copy) this.dataLength = copy.dataLength; this.compressedFileLength = copy.compressedFileLength; this.chunkOffsets = copy.chunkOffsets; - this.chunkOffsetsSize = copy.chunkOffsetsSize; + this.chunkLengthBits = copy.chunkLengthBits; + this.startChunkIndex = copy.startChunkIndex; + this.useActualFileSize = copy.useActualFileSize; + } + + public static long nativeMemoryAllocated() + { + return NATIVE_MEMORY_USAGE.get(); } public ICompressor compressor() @@ -162,14 +304,14 @@ public int maxCompressedLength() */ public long offHeapSize() { - return chunkOffsets.size(); + return chunkOffsets.memory.size(); } @Override public void addTo(Ref.IdentityCollection identities) { super.addTo(identities); - identities.add(chunkOffsets); + identities.add(chunkOffsets.memory); } @Override @@ -179,14 +321,19 @@ public CompressionMetadata sharedCopy() } /** - * Read offsets of the individual chunks from the given input. + * Reads offsets of the individual chunks from the given input, filtering out non-relevant offsets (outside the + * specified range). * - * @param input Source of the data. + * @param input Source of the data + * @param startIndex Index of the first chunk to read, inclusive + * @param endIndex Index of the last chunk to read, exclusive + * @param compressedFileLength compressed file length * - * @return collection of the chunk offsets. + * @return A pair of chunk offsets array and the offset next to the last read chunk */ - private static Memory readChunkOffsets(FileInputStreamPlus input) + private static Pair readChunkOffsets(FileInputStreamPlus input, int startIndex, int endIndex, long compressedFileLength) { + final ChunkOffsetMemory offsets; final int chunkCount; try { @@ -199,29 +346,41 @@ private static Memory readChunkOffsets(FileInputStreamPlus input) throw new FSReadError(e, input.file); } - Memory offsets = Memory.allocate(chunkCount * 8L); - int i = 0; + Preconditions.checkState(startIndex < chunkCount, "The start index %s has to be < chunk count %s", startIndex, chunkCount); + Preconditions.checkState(endIndex <= chunkCount, "The end index %s has to be <= chunk count %s", endIndex, chunkCount); + Preconditions.checkState(startIndex <= endIndex, "The start index %s has to be < end index %s", startIndex, endIndex); + + int chunksToRead = endIndex - startIndex; + + if (chunksToRead == 0) + return Pair.create(new ChunkOffsetMemory(0), 0L); + + offsets = new ChunkOffsetMemory(chunksToRead); + long i = 0; try { - - for (i = 0; i < chunkCount; i++) + input.skipBytes(startIndex * 8); + long lastOffset; + for (i = 0; i < chunksToRead; i++) { - offsets.setLong(i * 8L, input.readLong()); + lastOffset = input.readLong(); + offsets.set(i, lastOffset); } - return offsets; + lastOffset = endIndex < chunkCount ? input.readLong() - offsets.get(0) : compressedFileLength; + NATIVE_MEMORY_USAGE.addAndGet(offsets.memoryUsed()); + return Pair.create(offsets, lastOffset); + } + catch (EOFException e) + { + offsets.close(); + String msg = String.format("Corrupted Index File %s: read %d but expected at least %d chunks.", + input, i, chunksToRead); + throw new CorruptSSTableException(new IOException(msg, e), input.file); } catch (IOException e) { - if (offsets != null) - offsets.close(); - - if (e instanceof EOFException) - { - String msg = String.format("Corrupted Index File %s: read %d but expected %d chunks.", - input.file.path(), i, chunkCount); - throw new CorruptSSTableException(new IOException(msg, e), input.file); - } + offsets.close(); throw new FSReadError(e, input.file); } } @@ -229,46 +388,74 @@ private static Memory readChunkOffsets(FileInputStreamPlus input) /** * Get a chunk of compressed data (offset, length) corresponding to given position * - * @param position Position in the file. - * @return pair of chunk offset and length. + * @param uncompressedDataPosition Position in the uncompressed data. If we deal with a slice, this is the position + * in the original uncompressed data. + * @return A pair of chunk offset and length. If we deal with a slice, the chunk offset refers to the position in + * the compressed slice. */ - public Chunk chunkFor(long position) + public Chunk chunkFor(long uncompressedDataPosition) { - // position of the chunk - long idx = 8 * (position / parameters.chunkLength()); + int chunkIdx = chunkIndex(uncompressedDataPosition); + return chunk(chunkIdx); + } + + private Chunk chunk(long chunkOffset, long nextChunkOffset) + { + return new Chunk(chunkOffset, Math.toIntExact(nextChunkOffset - chunkOffset - 4)); // "4" bytes reserved for checksum + } + + private Chunk chunk(int chunkIdx) + { + long chunkOffset = chunkOffset(chunkIdx); + long nextChunkOffset = nextChunkOffset(chunkIdx); + return chunk(chunkOffset, nextChunkOffset); + } - if (idx >= chunkOffsetsSize) - throw new CorruptSSTableException(new EOFException(), chunksIndexFile); + private long nextChunkOffset(int chunkIdx) + { + if (chunkIdx == chunkOffsets.size() - 1) + return compressedFileLength + chunkOffsets.get(0); + return chunkOffset(chunkIdx + 1); + } - if (idx < 0) - throw new CorruptSSTableException(new IllegalArgumentException(String.format("Invalid negative chunk index %d with position %d", idx, position)), - chunksIndexFile); + private long chunkOffset(int chunkIdx) + { + if (chunkIdx >= chunkOffsets.size()) + throw new CorruptSSTableException(new EOFException(String.format("Chunk %d out of bounds: %d", chunkIdx, chunkOffsets.size())), chunksIndexFile); - long chunkOffset = chunkOffsets.getLong(idx); - long nextChunkOffset = (idx + 8 == chunkOffsetsSize) - ? compressedFileLength - : chunkOffsets.getLong(idx + 8); + return chunkOffsets.get(chunkIdx); + } - return new Chunk(chunkOffset, (int) (nextChunkOffset - chunkOffset - 4)); // "4" bytes reserved for checksum + private int chunkIndex(long uncompressedDataPosition) + { + return Math.toIntExact(uncompressedDataPosition >> chunkLengthBits) - startChunkIndex; } + /** + * Searches for the chunk with the given offset and returns the offset of uncompressed data for the found chunk. + * @param chunkOffset exact chunk offset to search for; if we deal with a slice this is a chunk offset + * in the original compressed file + * @return offset of uncompressed data for the found chunk; if we deal with a slice this is the offset + * in the original uncompressed data + * @throws IllegalArgumentException if no chunk with the given offset is found + */ public long getDataOffsetForChunkOffset(long chunkOffset) { long l = 0; - long h = (chunkOffsetsSize >> 3) - 1; + long h = chunkOffsets.size() - 1; long idx, offset; while (l <= h) { idx = (l + h) >>> 1; - offset = chunkOffsets.getLong(idx << 3); + offset = chunkOffsets.get(idx); if (offset < chunkOffset) l = idx + 1; else if (offset > chunkOffset) h = idx - 1; else - return idx * parameters.chunkLength(); + return (idx + startChunkIndex) << chunkLengthBits; } throw new IllegalArgumentException("No chunk with offset " + chunkOffset); @@ -281,35 +468,28 @@ else if (offset > chunkOffset) public long getTotalSizeForSections(Collection sections) { long size = 0; - long lastOffset = -1; + int lastIncludedChunkIdx = -1; for (SSTableReader.PartitionPositionBounds section : sections) { - int startIndex = (int) (section.lowerPosition / parameters.chunkLength()); - - int endIndex = (int) (section.upperPosition / parameters.chunkLength()); - if (section.upperPosition % parameters.chunkLength() == 0) - endIndex--; + int sectionStartIdx = Math.max(chunkIndex(section.lowerPosition), lastIncludedChunkIdx + 1); + int sectionEndIdx = chunkIndex(section.upperPosition - 1); // we need to include the last byte of the seciont but not the upper position (which is excludded) - for (int i = startIndex; i <= endIndex; i++) + for (int idx = sectionStartIdx; idx <= sectionEndIdx; idx++) { - long offset = i * 8L; - long chunkOffset = chunkOffsets.getLong(offset); - if (chunkOffset > lastOffset) - { - lastOffset = chunkOffset; - long nextChunkOffset = offset + 8 == chunkOffsetsSize - ? compressedFileLength - : chunkOffsets.getLong(offset + 8); - size += (nextChunkOffset - chunkOffset); - } + long chunkOffset = chunkOffset(idx); + long nextChunkOffset = nextChunkOffset(idx); + size += nextChunkOffset - chunkOffset; } + lastIncludedChunkIdx = sectionEndIdx; } return size; } /** - * @param sections Collection of sections in uncompressed file - * @return Array of chunks which corresponds to given sections of uncompressed file, sorted by chunk offset + * @param sections Collection of sections in uncompressed data. If we deal with a slice, the sections refer to the + * positions in the original uncompressed data. + * @return Array of chunks which corresponds to given sections of uncompressed file, sorted by chunk offset. + * Note that if we deal with a slice, the chunk offsets refer to the positions in the compressed slice. */ public Chunk[] getChunksForSections(Collection sections) { @@ -318,21 +498,11 @@ public Chunk[] getChunksForSections(Collection 0); + assert(length >= 0); this.offset = offset; this.length = length; diff --git a/src/java/org/apache/cassandra/io/compress/CompressionMetadataReaderType.java b/src/java/org/apache/cassandra/io/compress/CompressionMetadataReaderType.java new file mode 100644 index 000000000000..78f71ce27504 --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/CompressionMetadataReaderType.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +enum CompressionMetadataReaderType +{ + WRITE_TIME, + READ_TIME +} diff --git a/src/java/org/apache/cassandra/io/compress/CorruptBlockException.java b/src/java/org/apache/cassandra/io/compress/CorruptBlockException.java index bcce6b9ca060..9315fff66048 100644 --- a/src/java/org/apache/cassandra/io/compress/CorruptBlockException.java +++ b/src/java/org/apache/cassandra/io/compress/CorruptBlockException.java @@ -19,25 +19,47 @@ import java.io.IOException; +import org.apache.cassandra.io.util.File; + public class CorruptBlockException extends IOException { - public CorruptBlockException(String filePath, CompressionMetadata.Chunk chunk) + private final File file; + + public CorruptBlockException(File file, CompressionMetadata.Chunk chunk) + { + this(file, chunk, null); + } + + public CorruptBlockException(File file, CompressionMetadata.Chunk chunk, Throwable cause) + { + this(file, chunk.offset, chunk.length, cause); + } + + public CorruptBlockException(File file, long offset, int length) + { + this(file, offset, length, null); + } + + public CorruptBlockException(File file, long offset, int length, Throwable cause) { - this(filePath, chunk, null); + super(String.format("(%s): corruption detected, chunk at %d of length %d.", file.toString(), offset, length), cause); + this.file = file; } - public CorruptBlockException(String filePath, CompressionMetadata.Chunk chunk, Throwable cause) + public CorruptBlockException(File file, CompressionMetadata.Chunk chunk, int storedChecksum, int calculatedChecksum) { - this(filePath, chunk.offset, chunk.length, cause); + this(file, chunk.offset, chunk.length, storedChecksum, calculatedChecksum); } - public CorruptBlockException(String filePath, long offset, int length) + public CorruptBlockException(File file, long offset, int length, int storedChecksum, int calculatedChecksum) { - this(filePath, offset, length, null); + super(String.format("(%s): corruption detected, chunk at %d of length %d has mismatched checksums. Expected %d, but calculated %d", + file.toString(), offset, length, storedChecksum, calculatedChecksum)); + this.file = file; } - public CorruptBlockException(String filePath, long offset, int length, Throwable cause) + public File getFile() { - super(String.format("(%s): corruption detected, chunk at %d of length %d.", filePath, offset, length), cause); + return file; } } diff --git a/src/java/org/apache/cassandra/io/compress/EncryptedSequentialWriter.java b/src/java/org/apache/cassandra/io/compress/EncryptedSequentialWriter.java new file mode 100644 index 000000000000..0a5d6e33297b --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/EncryptedSequentialWriter.java @@ -0,0 +1,308 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.zip.CRC32; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.FSReadError; +import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.util.ChecksumWriter; +import org.apache.cassandra.io.util.DataPosition; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.SequentialWriter; +import org.apache.cassandra.io.util.SequentialWriterOption; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.io.util.PageAware; + +/** + * Encryption-only writer. This is not a normal file writer in the sense that it is not meant to just accept a sequence + * of writes. Instead, data is written to file in portions (chunks) of a given size (typically a disk page) but to write + * a chunk the writer must be given a smaller slice of data (up to maxBytesInPage()) and then told to pad + * (padToPageBoundary()) to the next chunk boundary. This triggers encryption and writing of the current chunk, and the + * file position moves to the start of the next chunk. + * + * This essentially reserves bytes at the end of the chunk for storing a CRC code plus any information necessary for + * decryption. The advantage of the scheme (over the compressed writer) is the fact that decrypted and encrypted file + * positions do not differ for any written data, and thus we can avoid having to consult a compressed offsets map + * to find where the encrypted data resides. + * + * Note that if asked to write a sequence of bytes that goes beyond the chunk boundary, the writer will accept it, and + * the reader will jump over the metadata when consuming the same bytes. This functionality is to be used sparingly as + * it may cause some surprises (e.g. the difference between two positions is not equal to the size of the data); it is + * currently used to write keys whose length can go over the page size. + * + * This writer does not provide precise data length; instead when constructed from a file on disk it will return the + * position after the last useable write. If any user depends on reading data located at the end of the file, they + * should make sure that data is positioned at the end of a chunk. See establishEndAddressablePosition below. + */ +public class EncryptedSequentialWriter extends SequentialWriter +{ + private static final Logger logger = LoggerFactory.getLogger(EncryptedSequentialWriter.class); + + public static final int FOOTER_LENGTH = 8; // CRC and encrypted length + public static final int CHUNK_SIZE = PageAware.PAGE_SIZE; + // Note: We could just as well permit other chunk sizes for encryption, but as is stands we can't specify them + // without changing CompressionParams (either its serialization or map entries), which is validated on streaming + // and could cause incompatibility. + // Having the size fixed to 4k also prevents fragmentation in the chunk cache, but may be wasteful if the + // encryptor needs to store a lot of information each frame. + + + private final ChecksumWriter crcMetadata; + + private final ICompressor encryptor; + + // used to store encrypted data + private final ByteBuffer encrypted; + + private final int maxBytesInChunk; + + /** Position of the last synced content. Used to avoid having to find the unencrypted end position of the file. */ + private long lastContent = 0; + + /** + * @param file File to write + * @param option Write option (buffer size and type will be set the same as compression params) + * @param encryptor Encryptor to use as an ICompressor + */ + public EncryptedSequentialWriter(File file, + SequentialWriterOption option, + ICompressor encryptor) + { + super(file, true, SequentialWriterOption.newBuilder() + .bufferSize(maxBytesInPage(encryptor)) + .bufferType(BufferType.preferredForCompression()) + .finishOnClose(option.finishOnClose()) + .build(), true); + assert Integer.bitCount(CHUNK_SIZE) == 1 + : "Chunk size of EncryptedSequentialWriter must be a power of two, was " + CHUNK_SIZE; + + this.encryptor = encryptor; + this.encrypted = BufferType.preferredForCompression().allocate(CHUNK_SIZE); + + maxBytesInChunk = buffer.capacity(); + crcMetadata = new ChecksumWriter(new DataOutputStream(Channels.newOutputStream(channel))); + } + + public static int maxBytesInPage(ICompressor encryptor) + { + return encryptor.findMaxBytesInChunk(CHUNK_SIZE - FOOTER_LENGTH); + } + + @Override + public long getOnDiskFilePointer() + { + return lastFlushOffset; + } + + @Override + public void flush() + { + throw new UnsupportedOperationException(); + } + + @Override + protected void flushData() + { + try + { + // compressing data with buffer re-use + buffer.flip(); + encrypted.clear(); + encryptor.compress(buffer, encrypted); + } + catch (IOException e) + { + throw new RuntimeException("Compression exception", e); // shouldn't happen + } + + try + { + int compressedLength = encrypted.position(); + + assert encrypted.remaining() >= FOOTER_LENGTH; + // pad _after_ encryption because known 0s in plaintext can make encryption easier to break + ByteBufferUtil.writeZeroes(encrypted, encrypted.remaining() - FOOTER_LENGTH); + + encrypted.putInt(compressedLength); + + encrypted.flip(); + // add the corresponding checksum + crcMetadata.appendToBuf(encrypted); + + // write everything out + channel.write(encrypted); + + assert encrypted.limit() == encrypted.capacity() : "encrypted.limit()=" + encrypted.limit() + " encrypted.capacity()=" + encrypted.capacity() + " compressedLength=" + compressedLength; + + lastFlushOffset += encrypted.capacity(); + lastContent = current(); + assert fchannel.position() == lastFlushOffset : "fchannel.position=" + fchannel.position() + " lastFlushOffset=" + lastFlushOffset; + + if (runPostFlush != null) + runPostFlush.accept(current()); + } + catch (IOException e) + { + throw new FSWriteError(e, getFile()); + } + } + + @Override + protected void resetBuffer() + { + // Our position now moves to the position at the start of next page. + bufferOffset = lastFlushOffset; + buffer.clear(); + } + + public void updateFileHandle(FileHandle.Builder fhBuilder, long dataLength) + { + // Set length to last content position to avoid having to read and decrypt the last chunk to find it. + fhBuilder.withLengthOverride(lastContent); + } + + @Override + public synchronized void resetAndTruncate(DataPosition mark) + { + assert mark instanceof BufferedFileWriterMark; + + long previous = current(); + long truncateTarget = ((BufferedFileWriterMark) mark).pointer; + + // If we're resetting to a point within our buffered data, just adjust our buffered position to drop bytes to + // the right of the desired mark. + if (previous - truncateTarget <= buffer.position()) + { + buffer.position(buffer.position() - ((int) (previous - truncateTarget))); + return; + } + + // synchronize current buffer with disk - we don't want any data loss + sync(); + + // find the aligned position of the truncation target; we should keep everything in the file before this point + // and restore the buffer contents written during the next sync. + long truncateChunk = truncateTarget & -CHUNK_SIZE; + + try + { + encrypted.clear(); + encrypted.limit(CHUNK_SIZE); + fchannel.position(truncateChunk); + fchannel.read(encrypted); + + CRC32 checksum = new CRC32(); + encrypted.flip(); + encrypted.limit(CHUNK_SIZE - 4); + checksum.update(encrypted); + encrypted.limit(CHUNK_SIZE); + + if (encrypted.getInt(CHUNK_SIZE - 4) != (int) checksum.getValue()) + throw new CorruptBlockException(getFile(), truncateChunk, CHUNK_SIZE); + + try + { + // Repopulate buffer from encrypted data + buffer.clear(); + int length = encrypted.getInt(CHUNK_SIZE - FOOTER_LENGTH); + encrypted.position(0).limit(length); + encryptor.uncompress(encrypted, buffer); + } + catch (IOException e) + { + throw new CorruptBlockException(getFile(), truncateChunk, CHUNK_SIZE, e); + } + } + catch (CorruptBlockException e) + { + throw new CorruptSSTableException(e, getFile()); + } + catch (EOFException e) + { + throw new CorruptSSTableException(new CorruptBlockException(getFile(), truncateChunk, CHUNK_SIZE), getFile()); + } + catch (IOException e) + { + throw new FSReadError(e, getFile()); + } + + + // truncate file to given position + truncate(truncateChunk); + + bufferOffset = truncateChunk; + buffer.position((int) truncateTarget & (CHUNK_SIZE - 1)); + lastContent = current(); + } + + // Page management using chunk boundaries + + @Override + public int maxBytesInPage() + { + return maxBytesInChunk; + } + + @Override + public void padToPageBoundary() + { + if (buffer.position() == 0) + return; + + doFlush(0); + } + + @Override + public int bytesLeftInPage() + { + return buffer.remaining(); + } + + @Override + public long paddedPosition() + { + return bufferOffset + (buffer.position() == 0 ? 0 : CHUNK_SIZE); + } + + public void establishEndAddressablePosition(int bytesNeeded) throws IOException + { + // Make sure the data does not span a page boundary (and the encryption data put there). + if (bytesLeftInPage() < bytesNeeded) + padToPageBoundary(); + + // Now pad to place the data at the end of the page. + int padding = bytesLeftInPage() - bytesNeeded; + assert padding >= 0 : "Requested " + bytesNeeded + " metadata bytes do not fit max page size " + bytesLeftInPage(); + + ByteBufferUtil.writeZeroes(buffer, padding); + + assert bytesLeftInPage() == bytesNeeded; + + // The padding above should not affect space used at all, but saves us from having to decode the last page to + // find the real end position in the file. + } +} diff --git a/src/java/org/apache/cassandra/io/compress/EncryptionConfig.java b/src/java/org/apache/cassandra/io/compress/EncryptionConfig.java new file mode 100644 index 000000000000..36819e981fe6 --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/EncryptionConfig.java @@ -0,0 +1,201 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +import org.apache.cassandra.config.OptionMap; +import org.apache.cassandra.crypto.IKeyProvider; +import org.apache.cassandra.crypto.IKeyProviderFactory; + +public class EncryptionConfig +{ + public static final String CIPHER_ALGORITHM = "cipher_algorithm"; + public static final String SECRET_KEY_STRENGTH = "secret_key_strength"; + public static final String SECRET_KEY_PROVIDER_FACTORY_CLASS = "secret_key_provider_factory_class"; + public static final String KEY_PROVIDER = "key_provider"; + public static final String IV_LENGTH = "iv_length"; + + private static final String DEFAULT_CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"; + private static final int DEFAULT_SECRET_KEY_STRENGTH = 128; + + private final IKeyProviderFactory keyProviderFactory; + private final IKeyProvider keyProvider; + private final String cipherName; + private final int keyStrength; + private final boolean ivEnabled; + private final int ivLength; + private final ImmutableMap encryptionOptions; + + public static Builder forClass(Class callerClass) + { + return new Builder(callerClass); + } + + private EncryptionConfig(IKeyProviderFactory keyProviderFactory, IKeyProvider keyProvider, + String cipherName, int keyStrength, boolean ivEnabled, int ivLength, + Map encryptionOptions) + { + this.keyProviderFactory = keyProviderFactory; + this.keyProvider = keyProvider; + this.cipherName = cipherName; + this.keyStrength = keyStrength; + this.ivEnabled = ivEnabled; + this.ivLength = ivLength; + this.encryptionOptions = ImmutableMap.copyOf(encryptionOptions); + } + + public IKeyProviderFactory getKeyProviderFactory() + { + return keyProviderFactory; + } + + public IKeyProvider getKeyProvider() + { + return keyProvider; + } + + public int getKeyStrength() + { + return keyStrength; + } + + public String getCipherName() + { + return cipherName; + } + + public boolean isIvEnabled() + { + return ivEnabled; + } + + public int getIvLength() + { + return ivLength; + } + + public Map asMap() + { + return encryptionOptions; + } + + public static class Builder + { + private final Class callerClass; + private final Map compressionOptions = new HashMap<>(); + + private Builder(Class callerClass) + { + this.callerClass = callerClass; + } + + public Builder fromCompressionOptions(Map compressionOptions) + { + this.compressionOptions.putAll(compressionOptions); + return this; + } + + public EncryptionConfig build() + { + OptionMap optionMap = new OptionMap(compressionOptions); + String cipherName = optionMap.get(CIPHER_ALGORITHM, DEFAULT_CIPHER_TRANSFORMATION); + int keyStrength = optionMap.get(SECRET_KEY_STRENGTH, DEFAULT_SECRET_KEY_STRENGTH); + int userIvLength = optionMap.get(IV_LENGTH, -1); + boolean ivEnabled = cipherName.matches(".*/(CBC|CFB|OFB|PCBC)/.*"); + int ivLength = !ivEnabled + ? 0 + : userIvLength > 0 + ? userIvLength + : getIvLength(cipherName.replaceAll("/.*", "")); + + try + { + Class keyProviderFactoryClass = getKeyFactory(compressionOptions); + IKeyProviderFactory keyProviderFactory = (IKeyProviderFactory) keyProviderFactoryClass.newInstance(); + IKeyProvider keyProvider = keyProviderFactory.getKeyProvider(compressionOptions); + return new EncryptionConfig(keyProviderFactory, keyProvider, cipherName, keyStrength, + ivEnabled, ivLength, compressionOptions); + } + catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IOException e) + { + throw new RuntimeException("Failed to initialize " + callerClass.getSimpleName() + ": " + e.getMessage(), e); + } + } + + private Class getKeyFactory(Map options) throws ClassNotFoundException + { + String className; + if (options.containsKey(KEY_PROVIDER)) + { + className = options.get(KEY_PROVIDER); + } + else if (options.containsKey(SECRET_KEY_PROVIDER_FACTORY_CLASS)) + { + // for backwards compatibility + className = options.get(SECRET_KEY_PROVIDER_FACTORY_CLASS); + } + else + { + className = "LocalFileSystemKeyProvider"; + } + + if (!className.contains(".")) + { + className = "org.apache.cassandra.crypto." + className; + } + + return Class.forName(className); + } + + private int getIvLength(String algorithm) + { + return algorithm.equals("AES") ? 16 : 8; + } + } + + @Override + public String toString() + { + return "EncryptionConfig{" + + "cipher name: " + cipherName + ", " + + "key strength: " + keyStrength + ", " + + "options" + encryptionOptions.toString() + "}"; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (!(obj instanceof EncryptionConfig)) + return false; + EncryptionConfig other = (EncryptionConfig) obj; + // All EncryptionConfig options are actually based on the passed compression/encryption options map. + return encryptionOptions.equals(other.encryptionOptions); + } + + @Override + public int hashCode() + { + // All EncryptionConfig options are actually based on the passed compression/encryption options map. + return encryptionOptions.hashCode(); + } +} diff --git a/src/java/org/apache/cassandra/io/compress/Encryptor.java b/src/java/org/apache/cassandra/io/compress/Encryptor.java new file mode 100644 index 000000000000..b64e4642d1fd --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/Encryptor.java @@ -0,0 +1,231 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.crypto.NoSuchPaddingException; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.crypto.KeyAccessException; +import org.apache.cassandra.crypto.KeyGenerationException; +import org.apache.cassandra.service.StorageService; + +/** + * Encrypts and decrypts data stored in Cassandra database. Plugs into C* custom compression API. + * Use compression_options of a column family to make this class process data in CF. + *

    + * The following compression_options properties are available for this class: + *

      + *
    • cipher_algorithm - name of the encryption algorithm provided in Java Cryptographic Extensions, default AES/CBC/PKCS5Padding
    • + *
    • secret_key_strength - bit-strength of the key, default 128
    • + *
    • secret_key_provider_factory_class - name of the factory for
    • + *
    + */ +public class Encryptor implements ICompressor +{ + private static final Logger logger = LoggerFactory.getLogger(Encryptor.class); + private static final SecureRandom random = new SecureRandom(); + + // Cache Encryptor instances by EncryptionConfig. + private static final LoadingCache encryptorsByConfig = + CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.DAYS) + .removalListener(notification -> + { + EncryptionConfig config = (EncryptionConfig) notification.getKey(); + logger.debug("Removing cached encryptor for {}", config); + }) + .build(new CacheLoader() + { + @Override + public Encryptor load(EncryptionConfig encryptionConfig) + { + return new Encryptor(encryptionConfig); + } + }); + + private final EncryptionConfig encryptionConfig; + + // No need to migrate these to InlinedThreadLocal and take a slot from the optimized local access fields. These would probably be + // used and accessed for a total period of time that's much shorter than their expected lifetime (as determined by the thread + // lifetime, but more importantly for long-living threads - by the enclosing Encryptor's lifetime), so most of the time they + // would just sit there, occupying a slot. + private final ThreadLocal encryptor = new ThreadLocal<>(); + private final ThreadLocal decryptor = new ThreadLocal<>(); + + public static Encryptor create(Map options) + { + EncryptionConfig encryptionConfig = EncryptionConfig.forClass(Encryptor.class).fromCompressionOptions(options).build(); + return encryptorsByConfig.getUnchecked(encryptionConfig); + } + + protected Encryptor(EncryptionConfig encryptionConfig) + { + this.encryptionConfig = encryptionConfig; + logger.debug("Creating new encryptor with {}", encryptionConfig); + try + { + // Try to get encryptor/decryptor objects to signal any errors now + if (StorageService.instance.isInitialized() && !StorageService.instance.isBootstrapMode()) + { + getEncryptor(); + getDecryptor(); + } + + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException("Failed to initialize " + Encryptor.class.getSimpleName() + ". " + + "Cipher algorithm not supported: " + encryptionConfig.getCipherName()); + } + catch (NoSuchPaddingException e) + { + throw new RuntimeException("Failed to initialize " + Encryptor.class.getSimpleName() + ". " + + "Cipher padding not supported: " + encryptionConfig.getCipherName()); + } + catch (Exception e) + { + throw new RuntimeException("Failed to initialize " + Encryptor.class.getSimpleName() + ": " + e.getClass().getName() + ": " + e.getMessage(), e); + } + } + + private StatefulEncryptor getEncryptor() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, KeyAccessException, KeyGenerationException + { + StatefulEncryptor encryptor = this.encryptor.get(); + if (encryptor == null) + { + encryptor = new StatefulEncryptor(encryptionConfig, random); + this.encryptor.set(encryptor); + } + + return encryptor; + } + + private StatefulDecryptor getDecryptor() + throws NoSuchAlgorithmException, NoSuchPaddingException + { + StatefulDecryptor decryptor = this.decryptor.get(); + if (decryptor == null) + { + decryptor = new StatefulDecryptor(encryptionConfig, random); + this.decryptor.set(decryptor); + } + return decryptor; + } + + /** Returns expected size of data after encryption */ + public int initialCompressedBufferLength(int chunkLength) + { + try + { + return getEncryptor().outputLength(chunkLength); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + /** Encrypts a block of data */ + public void compress(ByteBuffer input, ByteBuffer output) throws IOException + { + try + { + getEncryptor().encrypt(input, output); + } + catch (Exception e) + { + throw new IOException("Failed to encrypt data", e); + } + } + + /** Decrypts a block of data */ + public int uncompress(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset) throws IOException + { + try + { + return getDecryptor().decrypt(input, inputOffset, inputLength, output, outputOffset); + } + catch (Exception e) + { + throw new IOException("Failed to decrypt data inputOffset=" + inputOffset + " inputLength=" + inputLength + " outputOffset=" + outputOffset, e); + } + } + + public void uncompress(ByteBuffer input, ByteBuffer output) throws IOException + { + try + { + getDecryptor().decrypt(input, output); + } + catch (Exception e) + { + throw new IOException("Failed to decrypt data", e); + } + } + + @Override + public BufferType preferredBufferType() + { + // encrypt and decrypt operations happen on heap + return BufferType.ON_HEAP; + } + + @Override + public boolean supports(BufferType bufferType) + { + return BufferType.ON_HEAP == bufferType; + } + + public Set supportedOptions() + { + return Sets.union( + Sets.newHashSet(EncryptionConfig.CIPHER_ALGORITHM, + EncryptionConfig.SECRET_KEY_STRENGTH, + EncryptionConfig.IV_LENGTH, + EncryptionConfig.KEY_PROVIDER, + EncryptionConfig.SECRET_KEY_PROVIDER_FACTORY_CLASS), + encryptionConfig.getKeyProviderFactory().supportedOptions()); + } + + public ICompressor encryptionOnly() + { + return this; + } + + public boolean canDecompressInPlace() + { + // Encryptor stores metadata first, and then the encrypted data. In any case, the write position in the buffer + // is earlier, or at most equal, to the read position, which means decryptors (being copy-safe) can work + // correctly with the same buffer as both input and output. + return true; + } +} diff --git a/src/java/org/apache/cassandra/io/compress/ICompressor.java b/src/java/org/apache/cassandra/io/compress/ICompressor.java index fd6a104431b3..11d0ce3d183f 100644 --- a/src/java/org/apache/cassandra/io/compress/ICompressor.java +++ b/src/java/org/apache/cassandra/io/compress/ICompressor.java @@ -22,8 +22,12 @@ import java.util.EnumSet; import java.util.Set; +import javax.annotation.Nullable; + import com.google.common.collect.ImmutableSet; +import org.apache.cassandra.crypto.IKeyProviderFactory; + public interface ICompressor { /** @@ -83,4 +87,153 @@ default Set recommendedUses() { return ImmutableSet.copyOf(EnumSet.allOf(Uses.class)); } + + /** + * Returns the compressor configured for a particular use. + * Allows creating a compressor implementation that can handle multiple uses but requires different configurations + * adapted to a particular use. + *

    + * May return this object. + * May not modify this object. + * Should return null if the request cannot be satisfied. + */ + default @Nullable ICompressor forUse(Uses use) + { + return recommendedUses().contains(use) ? this : null; + } + + /** + * Indicates whether this compressor supports encryption metadata + * @return true if this compressor can handle encryption metadata + */ + default boolean supportsEncryption() + { + return false; + } + + /** + * Creates an Encryptor instance for this compressor. + * This should only be called if supportsEncryption() returns true. + * + * @param encryption The encryption configuration + * @return An Encryptor instance, or null if encryption is not supported + * @throws IOException if there's an error creating the encryptor + */ + default Encryptor encryptor(EncryptionConfig encryption) throws IOException + { + return null; + } + + /** + * For compressors that support encryption, returns the key provider factory + * @return The key provider factory, or null if encryption is not supported + */ + default IKeyProviderFactory keyProviderFactory() + { + return null; + } + + /** + * Get the number of extra bytes required for encryption metadata + * This should only be called if supportsEncryption() returns true. + * + * @param encryption The encryption configuration + * @return The number of extra bytes required, 0 if encryption is not supported + */ + default int extraBytesForEncryption(EncryptionConfig encryption) + { + return 0; + } + + /** + * Get the encryption metadata bytes for encrypted data. + * This should only be called if supportsEncryption() returns true. + * + * @param encryption The encryption configuration + * @param buffer The buffer containing the encrypted data + * @return The encryption metadata bytes, or null if encryption is not supported + */ + default byte[] getEncryptionMetadata(EncryptionConfig encryption, ByteBuffer buffer) + { + return null; + } + + /** + * Set encryption metadata in the buffer. + * This should only be called if supportsEncryption() returns true. + * + * @param encryption The encryption configuration + * @param buffer The buffer to write metadata to + * @param metadata The metadata bytes to write + */ + default void setEncryptionMetadata(EncryptionConfig encryption, ByteBuffer buffer, byte[] metadata) + { + // Default implementation does nothing + } + + /** + * Whether the compressor can decompress data in place (using the same buffer for input and output) + * @return true if in-place decompression is supported + */ + default boolean canDecompressInPlace() + { + return false; + } + + /** + * Find the maximum number of bytes that can fit in a chunk of the given size + * @param targetSize The target chunk size + * @return The maximum bytes that can fit + */ + default int findMaxBytesInChunk(int targetSize) + { + int rawSize = targetSize; + int encSize = 0; + int stepMax = targetSize; + + encSize = initialCompressedBufferLength(rawSize); + while (stepMax > 4) + { + if (encSize == targetSize) + break; + + if (encSize < targetSize) + rawSize += Math.min(stepMax, targetSize - encSize); + else + rawSize -= Math.min(stepMax, encSize - targetSize); + stepMax = stepMax * 3 / 4; // decrease the step for next round so that we don't jump between two values + // indefinitely + + encSize = initialCompressedBufferLength(rawSize); + } + + // decrease size by a byte while we need to + while (encSize > targetSize) + { + --rawSize; + encSize = initialCompressedBufferLength(rawSize); + } + + // and finally increase it while we can + while (true) + { + int nextSize = initialCompressedBufferLength(rawSize + 1); + if (nextSize > targetSize) + break; + ++rawSize; + encSize = nextSize; + } + + return rawSize; + } + + /** + * Get an encryption-only version of this compressor (no compression, only encryption) + * @return An ICompressor that only encrypts without compression + */ + default ICompressor encryptionOnly() + { + // Default implementation returns null, should be overridden by compressors that support encryption + return null; + } } diff --git a/src/java/org/apache/cassandra/io/compress/StatefulDecryptor.java b/src/java/org/apache/cassandra/io/compress/StatefulDecryptor.java new file mode 100644 index 000000000000..9c130266d678 --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/StatefulDecryptor.java @@ -0,0 +1,153 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; + +import org.apache.cassandra.crypto.IKeyProvider; +import org.apache.cassandra.crypto.IMultiKeyProvider; +import org.apache.cassandra.crypto.KeyAccessException; +import org.apache.cassandra.crypto.KeyGenerationException; + +/** + * Decrypts blocks of data. Reuses the same Cipher instance and avoids needless initialization. + * Not thread-safe! + */ +class StatefulDecryptor +{ + private final EncryptionConfig encryptionConfig; + private final SecureRandom secureRandom; + private final Cipher cipher; + + StatefulDecryptor(EncryptionConfig encryptionConfig, SecureRandom secureRandom) throws NoSuchPaddingException, NoSuchAlgorithmException + { + this.encryptionConfig = encryptionConfig; + this.secureRandom = secureRandom; + this.cipher = Cipher.getInstance(encryptionConfig.getCipherName()); + } + + void decrypt(ByteBuffer input, ByteBuffer output) + throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, ShortBufferException, IllegalBlockSizeException, KeyAccessException, KeyGenerationException + { + final IKeyProvider keyProvider = encryptionConfig.getKeyProvider(); + SecretKey key; + if (keyProvider instanceof IMultiKeyProvider) + { + key = ((IMultiKeyProvider) keyProvider) + .readHeader(encryptionConfig.getCipherName(), encryptionConfig.getKeyStrength(), input); + } + else + { + key = keyProvider.getSecretKey(encryptionConfig.getCipherName(), encryptionConfig.getKeyStrength()); + } + init(key, input); + + // The output size needed to decrypt is bigger than the resulting decrypted size + // so we need to check that we have a large enough buffer and create a + // temporary one if we haven't + int requiredOutputSize = cipher.getOutputSize(input.limit() - input.position()); + int outputSize = output.limit() - output.position(); + if (outputSize >= requiredOutputSize) + { + cipher.doFinal(input, output); + } + else + { + ByteBuffer tempBuffer = ByteBuffer.allocate(requiredOutputSize); + cipher.doFinal(input, tempBuffer); + tempBuffer.flip(); + int actualOutputSize = tempBuffer.remaining(); + // If the output buffer size is still too small then we have to + // throw + if (actualOutputSize > outputSize) + { + throw new ShortBufferException("Need at least " + actualOutputSize + " bytes of space in output buffer"); + } + output.put(tempBuffer); + } + } + + int decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset) + throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, ShortBufferException, IllegalBlockSizeException, KeyAccessException, KeyGenerationException + { + final IKeyProvider keyProvider = encryptionConfig.getKeyProvider(); + SecretKey key; + int headerSize = 0; + if (keyProvider instanceof IMultiKeyProvider) + { + ByteBuffer inputBuffer = ByteBuffer.wrap(input, inputOffset, inputLength - inputOffset); + key = ((IMultiKeyProvider) keyProvider) + .readHeader(encryptionConfig.getCipherName(), encryptionConfig.getKeyStrength(), inputBuffer); + + // only update the offset if the header decryption was successful, + // otherwise assume there is no header and default to the local key + headerSize = inputBuffer.position() - inputOffset; + inputLength -= headerSize; + inputOffset = inputBuffer.position(); + } + else + { + key = keyProvider.getSecretKey(encryptionConfig.getCipherName(), encryptionConfig.getKeyStrength()); + } + init(key, input, inputOffset); + int ivLength = encryptionConfig.getIvLength(); + return cipher.doFinal( + input, + inputOffset + ivLength, + inputLength - ivLength, + output, + outputOffset); + } + + private void init(SecretKey key, ByteBuffer input) throws InvalidKeyException, InvalidAlgorithmParameterException + { + if (encryptionConfig.isIvEnabled()) + { + byte[] iv = new byte[encryptionConfig.getIvLength()]; + input.get(iv); + IvParameterSpec ivParam = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, key, ivParam, secureRandom); + } + else + { + cipher.init(Cipher.DECRYPT_MODE, key, secureRandom); + } + } + + private void init(SecretKey key, byte[] input, int inputOffset) throws InvalidKeyException, InvalidAlgorithmParameterException + { + if (encryptionConfig.isIvEnabled()) + { + IvParameterSpec ivParam = new IvParameterSpec(input, inputOffset, encryptionConfig.getIvLength()); + cipher.init(Cipher.DECRYPT_MODE, key, ivParam, secureRandom); + } + else + { + cipher.init(Cipher.DECRYPT_MODE, key, secureRandom); + } + } +} diff --git a/src/java/org/apache/cassandra/io/compress/StatefulEncryptor.java b/src/java/org/apache/cassandra/io/compress/StatefulEncryptor.java new file mode 100644 index 000000000000..e61b4dba7dad --- /dev/null +++ b/src/java/org/apache/cassandra/io/compress/StatefulEncryptor.java @@ -0,0 +1,125 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.compress; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; + +import org.apache.commons.math3.random.ISAACRandom; + +import org.apache.cassandra.crypto.IKeyProvider; +import org.apache.cassandra.crypto.IMultiKeyProvider; +import org.apache.cassandra.crypto.KeyAccessException; +import org.apache.cassandra.crypto.KeyGenerationException; + +/** + * Encrypts blocks of data. Reuses the same Cipher instance and avoids needless initialization. + * Not thread-safe! + */ +class StatefulEncryptor +{ + private final SecureRandom random; + private final EncryptionConfig config; + private final byte[] iv; + private final Cipher cipher; + private final ISAACRandom fastRandom; + + private boolean initialized = false; + + StatefulEncryptor(EncryptionConfig config, SecureRandom random) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException, KeyAccessException, KeyGenerationException + { + this.random = random; + this.config = config; + this.iv = new byte[config.getIvLength()]; + cipher = Cipher.getInstance(config.getCipherName()); + int[] seed = new int[256]; + for (int i = 0; i < seed.length; i++) + seed[i] = random.nextInt(); + this.fastRandom = new ISAACRandom(seed); + maybeInit(config.getKeyProvider().getSecretKey(config.getCipherName(), config.getKeyStrength())); + } + + private void maybeInit(SecretKey key) throws InvalidAlgorithmParameterException, InvalidKeyException + { + if (!initialized) + init(key); + } + + private void init(SecretKey key) throws InvalidAlgorithmParameterException, InvalidKeyException + { + if (config.isIvEnabled()) + cipher.init(Cipher.ENCRYPT_MODE, key, createIV(), random); + else + cipher.init(Cipher.ENCRYPT_MODE, key, random); + initialized = true; + } + + private AlgorithmParameterSpec createIV() + { + for (int i = 0; i < config.getIvLength(); i += 4) + { + int value = fastRandom.nextInt(); + iv[i] = (byte)(value >>> 24); + iv[i + 1] = (byte)(value >>> 16); + iv[i + 2] = (byte)(value >>> 8); + iv[i + 3] = (byte) value; + } + return new IvParameterSpec(iv); + } + + void encrypt(ByteBuffer input, ByteBuffer output) + throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, ShortBufferException, IllegalBlockSizeException, KeyAccessException, KeyGenerationException + { + SecretKey key; + IKeyProvider keyProvider = config.getKeyProvider(); + if (keyProvider instanceof IMultiKeyProvider) + { + key = ((IMultiKeyProvider) keyProvider).writeHeader(config.getCipherName(), config.getKeyStrength(), output); + } + else + { + key = keyProvider.getSecretKey(config.getCipherName(), config.getKeyStrength()); + } + + maybeInit(key); + if (config.isIvEnabled()) + { + output.put(iv); + } + cipher.doFinal(input, output); + initialized = false; + } + + int outputLength(int inputSize) throws InvalidAlgorithmParameterException, InvalidKeyException, KeyAccessException, KeyGenerationException + { + IKeyProvider keyProvider = config.getKeyProvider(); + maybeInit(keyProvider.getSecretKey(config.getCipherName(), config.getKeyStrength())); + int headerSize = keyProvider instanceof IMultiKeyProvider ? ((IMultiKeyProvider) keyProvider).headerLength() : 0; + return config.getIvLength() + cipher.getOutputSize(inputSize) + headerSize; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/AbstractMetricsProviders.java b/src/java/org/apache/cassandra/io/sstable/AbstractMetricsProviders.java index 15b1161cf9ba..8897d4364394 100644 --- a/src/java/org/apache/cassandra/io/sstable/AbstractMetricsProviders.java +++ b/src/java/org/apache/cassandra/io/sstable/AbstractMetricsProviders.java @@ -21,7 +21,9 @@ import java.util.function.BiFunction; import java.util.function.Function; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.metrics.TableMetrics; public abstract class AbstractMetricsProviders implements MetricsProviders { @@ -40,5 +42,24 @@ protected final GaugeProvider newGaugeProvider(String name }); } + protected final GaugeProvider bloomFilterOffHeapMemoryUsedAwareCFSGaugeProvider(String name, Long neutralValue, Function extractor, BiFunction combiner) + { + return new SimpleGaugeProvider<>(this::map, name, readers -> { + Long total = neutralValue; + + // Optionally add in-flight bloom filter off-heap memory used + if (readers.iterator().hasNext()) { + R firstReader = readers.iterator().next(); + TableMetrics tableMetrics = ColumnFamilyStore.metricsForIfPresent(firstReader.metadata().id); + if (tableMetrics != null) + total = tableMetrics.inFlightBloomFilterOffHeapMemoryUsed.get(); + } + + for (R reader : readers) + total = combiner.apply(total, extractor.apply(reader)); + return total; + }); + } + protected abstract R map(SSTableReader r); } diff --git a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java index e6fbad882e12..623d2202baf8 100644 --- a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java @@ -44,6 +44,7 @@ import org.apache.cassandra.db.rows.UnfilteredSerializer; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.schema.TableMetadata; @@ -97,6 +98,7 @@ protected AbstractSSTableIterator(SSTableReader sstable, } else { + Reader reader = null; boolean shouldCloseFile = file == null; try { @@ -119,15 +121,18 @@ protected AbstractSSTableIterator(SSTableReader sstable, // Note that this needs to be called after file != null and after the partitionDeletion has been set, but before readStaticRow // (since it uses it) so we can't move that up (but we'll be able to simplify as soon as we drop support for the old file format). - this.reader = createReader(indexEntry, file, shouldCloseFile); + reader = createReader(indexEntry, file, shouldCloseFile); this.staticRow = readStaticRow(sstable, file, helper, columns.fetchedColumns().statics); } else { this.partitionLevelDeletion = indexEntry.deletionTime(); this.staticRow = Rows.EMPTY_STATIC_ROW; - this.reader = createReader(indexEntry, file, shouldCloseFile); + reader = createReader(indexEntry, file, shouldCloseFile); } + + this.reader = reader; + if (!partitionLevelDeletion.validate()) UnfilteredValidation.handleInvalid(metadata(), key, sstable, "partitionLevelDeletion="+partitionLevelDeletion.toString()); @@ -137,10 +142,26 @@ protected AbstractSSTableIterator(SSTableReader sstable, if (reader == null && file != null && shouldCloseFile) file.close(); } - catch (IOException e) + catch (CorruptSSTableException | IOException e) { sstable.markSuspect(); - String filePath = file.getPath(); + + if (reader != null) + { + try + { + reader.close(); + // reader will close the file internally, so there's no + // need to close it in the next block + shouldCloseFile = false; + } + catch (IOException suppressed) + { + e.addSuppressed(suppressed); + } + } + + File filePath = file.getFile(); if (shouldCloseFile) { try @@ -152,6 +173,8 @@ protected AbstractSSTableIterator(SSTableReader sstable, e.addSuppressed(suppressed); } } + if (e instanceof CorruptSSTableException) + throw (CorruptSSTableException)e; throw new CorruptSSTableException(e, filePath); } } @@ -174,10 +197,10 @@ private Slice nextSlice() */ protected abstract boolean hasMoreSlices(); - private static Row readStaticRow(SSTableReader sstable, - FileDataInput file, - DeserializationHelper helper, - Columns statics) throws IOException + public static Row readStaticRow(SSTableReader sstable, + FileDataInput file, + DeserializationHelper helper, + Columns statics) throws IOException { if (!sstable.header.hasStatic()) return Rows.EMPTY_STATIC_ROW; @@ -316,13 +339,13 @@ public interface Reader extends Iterator, Closeable { public abstract class AbstractReader implements Reader { - private final boolean shouldCloseFile; public FileDataInput file; public UnfilteredDeserializer deserializer; // Records the currently open range tombstone (if any) public DeletionTime openMarker; + protected final boolean shouldCloseFile; protected AbstractReader(FileDataInput file, boolean shouldCloseFile) { @@ -415,6 +438,7 @@ public Unfiltered next() public abstract void setForSlice(Slice slice) throws IOException; protected abstract boolean hasNextInternal() throws IOException; + protected abstract Unfiltered nextInternal() throws IOException; @Override diff --git a/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java index 64834792fc96..83e29e968285 100644 --- a/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java @@ -358,6 +358,11 @@ public UserType getUDType(String dataType) return (UserType) JavaDriverUtils.driverType(userType); } + public TableMetadataRef getMetadata() + { + return writer.metadata; + } + /** * Close this writer. *

    diff --git a/src/java/org/apache/cassandra/io/sstable/Component.java b/src/java/org/apache/cassandra/io/sstable/Component.java index 0d89cf0b927d..d9107cb781b1 100644 --- a/src/java/org/apache/cassandra/io/sstable/Component.java +++ b/src/java/org/apache/cassandra/io/sstable/Component.java @@ -30,6 +30,10 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components.Types; +import org.apache.cassandra.io.storage.StorageProvider; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.PathUtils; + /** * SSTables are made up of multiple components in separate files. Components are * identified by a type and an id, but required unique components (such as the Data @@ -37,7 +41,7 @@ */ public class Component { - public static final char separator = '-'; + public static final char SEPARATOR = '-'; /** * WARNING: Be careful while changing the names or string representation of the enum @@ -54,6 +58,7 @@ public final static class Type public final String repr; public final boolean streamable; private final Component singleton; + private final Pattern pattern; @SuppressWarnings("rawtypes") public final Class formatClass; @@ -92,6 +97,7 @@ private Type(String name, String repr, boolean isSingleton, boolean streamable, this.id = typesCollector.size(); this.formatClass = formatClass == null ? SSTableFormat.class : formatClass; this.singleton = isSingleton ? new Component(this) : null; + this.pattern = (repr != null) ? Pattern.compile(repr) : null; registerType(this); } @@ -120,9 +126,13 @@ private static void registerType(Type type) @VisibleForTesting public static Type fromRepresentation(String repr, SSTableFormat format) { + if (repr == null) + throw new IllegalArgumentException("Component representation cannot be null"); + for (Type type : Type.all) { - if (type.repr != null && Pattern.matches(type.repr, repr) && type.formatClass.isAssignableFrom(format.getClass())) + if (type.pattern != null && type.pattern.matcher(repr).matches() + && type.formatClass.isAssignableFrom((null != format ? format.getClass() : SSTableFormat.class))) return type; } return Types.CUSTOM; @@ -163,10 +173,10 @@ public Component getSingleton() return Objects.requireNonNull(singleton); } - public Component createComponent(String repr) + public Component createComponent(String componentFileName) { Preconditions.checkArgument(singleton == null); - return new Component(this, repr); + return new Component(this, componentFileName); } } @@ -179,7 +189,7 @@ private Component(Type type) this(type, type.repr); } - private Component(Type type, String name) + public Component(Type type, String name) { assert name != null : "Component name cannot be null"; @@ -223,10 +233,24 @@ public boolean isValidFor(Descriptor descriptor) return type.formatClass.isAssignableFrom(descriptor.version.format.getClass()); } + public File getFile(String absolutePath) + { + File ret; + if (absolutePath.lastIndexOf(SEPARATOR) != (absolutePath.length() - 1)) + ret = new File(PathUtils.getPath(absolutePath + SEPARATOR + name)); + else + ret = new File(PathUtils.getPath(absolutePath + name)); + + return StorageProvider.instance.withOpenOptions(ret, this); + } + @Override public String toString() { - return this.name(); + return "Component{" + + "name='" + name + '\'' + + ", type=" + type + + '}'; } @Override diff --git a/src/java/org/apache/cassandra/io/sstable/CorruptSSTableException.java b/src/java/org/apache/cassandra/io/sstable/CorruptSSTableException.java index 991a91d904f8..559e271fe585 100644 --- a/src/java/org/apache/cassandra/io/sstable/CorruptSSTableException.java +++ b/src/java/org/apache/cassandra/io/sstable/CorruptSSTableException.java @@ -17,20 +17,35 @@ */ package org.apache.cassandra.io.sstable; +import java.nio.file.Path; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.utils.DseLegacy; public class CorruptSSTableException extends RuntimeException { - public final File path; + public final File file; - public CorruptSSTableException(Throwable cause, File path) + public CorruptSSTableException(Throwable cause, File file) { - super("Corrupted: " + path, cause); - this.path = path; + super("Corrupted: " + file, cause); + this.file = file; } public CorruptSSTableException(Throwable cause, String path) + { + this(cause, new File(PathUtils.getPath(path))); + } + + protected CorruptSSTableException(String msg, Throwable cause, File file) + { + super(msg, cause); + this.file = file; + } + + @DseLegacy + public CorruptSSTableException(Throwable cause, Path path) { this(cause, new File(path)); } diff --git a/src/java/org/apache/cassandra/io/sstable/DefaultStorageHandler.java b/src/java/org/apache/cassandra/io/sstable/DefaultStorageHandler.java new file mode 100644 index 000000000000..71c446d800e5 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/DefaultStorageHandler.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import java.util.Collection; +import java.util.Collections; + +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.lifecycle.Tracker; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.service.StorageService; + +/** + * The default storage handler, used when sstables are stored on the local file system. + */ +public class DefaultStorageHandler extends StorageHandler +{ + public DefaultStorageHandler(SSTable.Owner owner, TableMetadataRef metadata, Directories directories, Tracker dataTracker) + { + super(owner, metadata, directories, dataTracker); + } + + @Override + public boolean isReady() + { + return !StorageService.instance.isBootstrapMode(); + } + + @Override + public Collection loadInitialSSTables() + { + Directories.SSTableLister sstableFiles = directories.sstableLister(Directories.OnTxnErr.IGNORE).skipTemporary(true); + Collection sstables = SSTableReader.openAll(owner, sstableFiles.list().entrySet(), metadata); + dataTracker.addInitialSSTablesWithoutUpdatingSize(sstables); + return sstables; + } + + @Override + public Collection reloadSSTables(ReloadReason reason) + { + // no op for local storage + return Collections.emptySet(); + } + + @Override + public void unload() + { + // no op for local storage + } + + @Override + public boolean enableAutoCompaction() + { + return true; + } + + @Override + public void runWithReloadingDisabled(Runnable runnable) + { + // by default no sstables are loaded, so we just need to execute the runnable + runnable.run(); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/Descriptor.java b/src/java/org/apache/cassandra/io/sstable/Descriptor.java index b9e149804278..50f8e69c8c09 100644 --- a/src/java/org/apache/cassandra/io/sstable/Descriptor.java +++ b/src/java/org/apache/cassandra/io/sstable/Descriptor.java @@ -17,10 +17,13 @@ */ package org.apache.cassandra.io.sstable; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; import com.google.common.annotations.VisibleForTesting; @@ -37,11 +40,13 @@ import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer; import org.apache.cassandra.io.sstable.metadata.MetadataSerializer; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.utils.DseLegacy; import org.apache.cassandra.utils.Pair; import static com.google.common.base.Preconditions.checkNotNull; -import static org.apache.cassandra.io.sstable.Component.separator; +import static org.apache.cassandra.io.sstable.Component.SEPARATOR; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; /** @@ -96,6 +101,8 @@ public class Descriptor private final int hashCode; private final String prefix; private final File baseFile; + private final String baseFileURI; + private final ConcurrentMap componentFileMap; /** * A descriptor that assumes CURRENT_VERSION. @@ -140,23 +147,26 @@ public Descriptor(Version version, File directory, String ksname, String cfname, // directory is unnecessary for hashCode, and for simulator consistency we do not include it hashCode = Objects.hashCode(version, id, ksname, cfname); - } - private String tmpFilenameFor(Component component) - { - return fileFor(component) + TMP_EXT; + String locationURI = directory.toUri().toString(); + if (!locationURI.endsWith(java.io.File.separator)) + locationURI = locationURI + java.io.File.separatorChar; + baseFileURI = locationURI + prefix; + + componentFileMap = new ConcurrentHashMap<>(); } public File tmpFileFor(Component component) { - return new File(directory.toPath().resolve(tmpFilenameFor(component))); + File file = StorageProvider.instance.getLocalPath(fileFor(component)); + return file.resolveSibling(file.name() + TMP_EXT); } private String tmpFilenameForStreaming(Component component) { // Use UUID to handle concurrent streamings on the same sstable. // TMP_EXT allows temp file to be removed by {@link ColumnFamilyStore#scrubDataDirectories} - return String.format("%s.%s%s", filenameFor(component), nextTimeUUID(), TMP_EXT); + return String.format("%s.%s%s", fileFor(component), nextTimeUUID(), TMP_EXT); } /** @@ -167,14 +177,33 @@ public File tmpFileForStreaming(Component component) return new File(directory.toPath().resolve(tmpFilenameForStreaming(component))); } - private String filenameFor(Component component) + public String filenameFor(Component component) { - return prefix + separator + component.name(); + return prefix + SEPARATOR + component.name(); } public File fileFor(Component component) { - return new File(directory.toPath().resolve(filenameFor(component))); + return componentFileMap.computeIfAbsent(component, c -> { + // STAR-1892 - CNDB depdends on using Component.getFile here to be able to create a RemotePath if the + // URI matches. However, tests that extend CQLTester.InMemory (using the jimfs file system) will fail + // with an UnsupportedOperationException. One such test is CQLVectorTest. + // + // Because Component.getFile uses the file URI to create a new Path object, it will not be wrapped by + // ListenablePath. That leads to an UnsupportedOperationException because ListenablePath implements certain + // methods that are not implemented by jimfs. + // + // Using the resolve method on the directory to create a new Path object keeps it wrapped by ListenablePath + // and will prevent an UnsupportedOperationException. + if (baseFileUri().startsWith("jimfs:")) + { + return new File(directory.toPath().resolve(filenameFor(component))); + } + else { + return component.getFile(baseFileUri()); + } + + }); } public File baseFile() @@ -184,9 +213,19 @@ public File baseFile() private void appendFileName(StringBuilder buff) { - buff.append(version).append(separator); + buff.append(version).append(SEPARATOR); buff.append(id.toString()); - buff.append(separator).append(version.format.name()); + buff.append(SEPARATOR).append(version.format.name()); + } + + public String baseFileUri() + { + return baseFileURI; + } + + public String filenamePart() + { + return prefix; } public String relativeFilenameFor(Component component) @@ -198,7 +237,7 @@ public String relativeFilenameFor(Component component) } appendFileName(buff); - buff.append(separator).append(component.name()); + buff.append(SEPARATOR).append(component.name()); return buff.toString(); } @@ -255,7 +294,7 @@ public static boolean isValidFile(File file) * @param file the {@code File} object for the filename to parse. * @return the descriptor for the parsed file. * - * @throws IllegalArgumentException if the provided {@code file} does point to a valid sstable filename. This could + * @throws IllegalArgumentException if the provided {@code file} does not point to a valid sstable filename. This could * mean either that the filename doesn't look like a sstable file, or that it is for an old and unsupported * versions. */ @@ -264,12 +303,43 @@ public static Descriptor fromFile(File file) return fromFileWithComponent(file).left; } + public static Descriptor fromFilename(String filename) + { + return fromFile(new File(filename)); + } + + public static Descriptor fromFilename(File file) + { + return fromFileWithComponent(file).left; + } + + public static Pair fromFilenameWithComponent(File file) + { + return fromFileWithComponent(file); + } + public static Component componentFromFile(File file) { - String name = file.name(); - List tokens = filenameTokens(name); + return validFilenameWithComponent(file.name()); + } + + public static Component validFilenameWithComponent(String name) + { + try + { + List tokens = filenameTokens(name); - return Component.parse(tokens.get(3), formatFromName(name, tokens)); + return Component.parse(tokens.get(3), formatFromName(name, tokens)); + } + catch (Exception e) + { + return null; + } + } + + public static boolean validFilename(String name) + { + return validFilenameWithComponent(name) != null; } private static SSTableFormat formatFromName(String fileName, List tokens) @@ -282,13 +352,13 @@ public static Component componentFromFile(File file) } /** - * Parse a sstable filename, extracting both the {@code Descriptor} and {@code Component} part. + * Parse a sstable file, extracting both the {@code Descriptor} and {@code Component} part. * The keyspace/table name will be extracted from the directory path. * * @param file the {@code File} object for the filename to parse. * @return a pair of the descriptor and component corresponding to the provided {@code file}. * - * @throws IllegalArgumentException if the provided {@code file} does point to a valid sstable filename. This could + * @throws IllegalArgumentException if the provided {@code file} does not point to a valid sstable filename. This could * mean either that the filename doesn't look like a sstable file, or that it is for an old and unsupported * versions. */ @@ -304,18 +374,51 @@ public static Pair fromFileWithComponent(File file, boole if (!file.isAbsolute()) file = file.toAbsolute(); - SSTableInfo info = validateAndExtractInfo(file); - String name = file.name(); + String filename = file.name(); + File tableDirectory = parentOf(filename, file); + + // Delegate to the new method that accepts directory and filename separately + return fromFileWithComponent(tableDirectory, filename, validateDirs); + } + + /** + * Parse a table directory and sstable file name, extracting both the {@code Descriptor} and {@code Component} part. + * + * @param tableDirectory the {@code File} object for the sstable directory + * @param name the name of the sstable file to parse + * @param validateDirs whether to validate that the directory structure matches expected patterns + * @return a pair of the descriptor and component corresponding to the provided directory and filename. + * + * @throws IllegalArgumentException if the provided {@code filename} does not point to a valid sstable filename. This could + * mean either that the filename doesn't look like a sstable file, or that it is for an old and unsupported + * versions. + */ + public static Pair fromFileWithComponent(File tableDirectory, String name, boolean validateDirs) + { + checkNotNull(tableDirectory); + checkNotNull(name); + + // We need to extract the keyspace and table names from the parent directories, so make sure we deal with the + // absolute path. + if (!tableDirectory.isAbsolute()) + tableDirectory = tableDirectory.toAbsolute(); + + SSTableInfo info = validateAndExtractInfo(name); String keyspaceName = ""; String tableName = ""; - Matcher sstableDirMatcher = SSTABLE_DIR_PATTERN.matcher(file.toString()); + String fullPath = tableDirectory.toString(); + if (!fullPath.endsWith(File.pathSeparator())) + fullPath = fullPath + File.pathSeparator(); + fullPath = fullPath + name; + + Matcher sstableDirMatcher = SSTABLE_DIR_PATTERN.matcher(fullPath); // Use pre-2.1 SSTable format if current one does not match it if (!sstableDirMatcher.find(0)) { - sstableDirMatcher = LEGACY_SSTABLE_DIR_PATTERN.matcher(file.toString()); + sstableDirMatcher = LEGACY_SSTABLE_DIR_PATTERN.matcher(fullPath); } if (sstableDirMatcher.find(0)) @@ -330,11 +433,12 @@ public static Pair fromFileWithComponent(File file, boole } else if (validateDirs) { - logger.debug("Could not extract keyspace/table info from sstable directory {}", file.toString()); - throw invalidSSTable(name, String.format("cannot extract keyspace and table name from %s; make sure the sstable is in the proper sub-directories", file)); + logger.debug("Could not extract keyspace/table info from sstable directory {}", fullPath); + throw invalidSSTable(name, String.format("cannot extract keyspace and table name from %s; make sure the sstable is in the proper sub-directories", fullPath)); } - return Pair.create(new Descriptor(info.version, parentOf(name, file), keyspaceName, tableName, info.id), info.component); + // Use tableDirectory directly, reusing the same File instance across multiple descriptors + return Pair.create(new Descriptor(info.version, tableDirectory, keyspaceName, tableName, info.id), info.component); } /** @@ -357,7 +461,7 @@ public static Pair fromFileWithComponent(File file, Strin return fromFileWithComponent(file); } - SSTableInfo info = validateAndExtractInfo(file); + SSTableInfo info = validateAndExtractInfo(file.name()); return Pair.create(new Descriptor(info.version, parentOf(file.name(), file), keyspace, table, info.id), info.component); } @@ -380,9 +484,8 @@ private static List filenameTokens(String name) return tokens; } - private static SSTableInfo validateAndExtractInfo(File file) + private static SSTableInfo validateAndExtractInfo(String name) { - String name = file.name(); List tokens = filenameTokens(name); String versionString = tokens.get(0); @@ -463,7 +566,7 @@ public Set discoverComponents() @Override public String toString() { - return baseFile().absolutePath(); + return baseFileUri(); } @Override @@ -488,4 +591,22 @@ public int hashCode() { return hashCode; } + + @DseLegacy + public Path getDirectory() + { + return directory.toPath(); + } + + @DseLegacy + public Path pathFor(Component component) + { + return fileFor(component).toPath(); + } + + @DseLegacy + public String baseFileURI() + { + return baseFileUri(); + } } diff --git a/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java index 8976ed413072..3fc5bb6e8e01 100644 --- a/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java @@ -56,6 +56,12 @@ public Set getBackingSSTables() return ImmutableSet.of(sstable); } + @Override + public int level() + { + return 0; + } + public long getCurrentPosition() { return 0; diff --git a/src/java/org/apache/cassandra/io/sstable/IKeyFetcher.java b/src/java/org/apache/cassandra/io/sstable/IKeyFetcher.java new file mode 100644 index 000000000000..3485d03cbf44 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/IKeyFetcher.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import java.util.function.LongFunction; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.utils.Closeable; + +public interface IKeyFetcher extends LongFunction, Closeable +{ + /** + * @param keyOffset the offset of the key + * @return the key at the given offset, or null if the key is not present or the offset is out of range + */ + DecoratedKey apply(long keyOffset); +} diff --git a/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java index 671bccb824b5..c24c2b23d7e4 100644 --- a/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java @@ -34,13 +34,14 @@ */ public interface ISSTableScanner extends UnfilteredPartitionIterator { - public long getLengthInBytes(); - public long getCompressedLengthInBytes(); - public long getCurrentPosition(); - public long getBytesScanned(); - public Set getBackingSSTables(); + long getLengthInBytes(); + long getCompressedLengthInBytes(); + long getCurrentPosition(); + long getBytesScanned(); + Set getBackingSSTables(); + int level(); - public static void closeAllAndPropagate(Collection scanners, Throwable throwable) + static Throwable closeAllAndPropagate(Collection scanners, Throwable throwable) { for (ISSTableScanner scanner: scanners) { @@ -67,6 +68,6 @@ public static void closeAllAndPropagate(Collection scanners, Th Throwables.throwIfUnchecked(throwable); throw new RuntimeException(throwable); } - + return null; } } diff --git a/src/java/org/apache/cassandra/io/sstable/IScrubber.java b/src/java/org/apache/cassandra/io/sstable/IScrubber.java index 50c6eb35fa16..e280ee3dac95 100644 --- a/src/java/org/apache/cassandra/io/sstable/IScrubber.java +++ b/src/java/org/apache/cassandra/io/sstable/IScrubber.java @@ -18,20 +18,22 @@ package org.apache.cassandra.io.sstable; +import java.util.List; import java.util.StringJoiner; import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.TableOperation; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.utils.Closeable; public interface IScrubber extends Closeable { - void scrub(); + List scrub(); void close(); - CompactionInfo.Holder getScrubInfo(); + TableOperation getScrubInfo(); @VisibleForTesting ScrubResult scrubWithResult(); @@ -46,12 +48,14 @@ final class ScrubResult public final int goodPartitions; public final int badPartitions; public final int emptyPartitions; + public final List scrubbed; - public ScrubResult(int goodPartitions, int badPartitions, int emptyPartitions) + public ScrubResult(int goodPartitions, int badPartitions, int emptyPartitions, List scrubbed) { this.goodPartitions = goodPartitions; this.badPartitions = badPartitions; this.emptyPartitions = emptyPartitions; + this.scrubbed = scrubbed; } } @@ -60,12 +64,14 @@ class Options public final boolean checkData; public final boolean reinsertOverflowedTTLRows; public final boolean skipCorrupted; + public final boolean overrideTxnIsOffline; - private Options(boolean checkData, boolean reinsertOverflowedTTLRows, boolean skipCorrupted) + private Options(boolean checkData, boolean reinsertOverflowedTTLRows, boolean skipCorrupted, boolean overrideTxnIsOffline) { this.checkData = checkData; this.reinsertOverflowedTTLRows = reinsertOverflowedTTLRows; this.skipCorrupted = skipCorrupted; + this.overrideTxnIsOffline = overrideTxnIsOffline; } @Override @@ -83,6 +89,7 @@ public static class Builder private boolean checkData = false; private boolean reinsertOverflowedTTLRows = false; private boolean skipCorrupted = false; + private boolean overrideTxnIsOffline = false; public Builder checkData() { @@ -120,9 +127,21 @@ public Builder skipCorrupted(boolean skipCorrupted) return this; } + public Builder overrideTxnIsOffline() + { + this.overrideTxnIsOffline = true; + return this; + } + + public Builder overrideTxnIsOffline(boolean overrideTxnIsOffline) + { + this.overrideTxnIsOffline = overrideTxnIsOffline; + return this; + } + public Options build() { - return new Options(checkData, reinsertOverflowedTTLRows, skipCorrupted); + return new Options(checkData, reinsertOverflowedTTLRows, skipCorrupted, overrideTxnIsOffline); } } } diff --git a/src/java/org/apache/cassandra/io/sstable/IVerifier.java b/src/java/org/apache/cassandra/io/sstable/IVerifier.java index 62ec0659af61..a23fe686a745 100644 --- a/src/java/org/apache/cassandra/io/sstable/IVerifier.java +++ b/src/java/org/apache/cassandra/io/sstable/IVerifier.java @@ -22,7 +22,7 @@ import java.util.Collection; import java.util.function.Function; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.service.StorageService; @@ -39,7 +39,7 @@ static Options.Builder options() @Override void close(); - CompactionInfo.Holder getVerifyInfo(); + AbstractTableOperation getVerifyInfo(); class Options { @@ -50,6 +50,7 @@ class Options * if there is no digest present. Setting it along with quick makes no sense. */ public final boolean extendedVerification; + public final boolean validateAllRows; public final boolean checkVersion; public final boolean mutateRepairStatus; @@ -64,6 +65,7 @@ class Options private Options(boolean invokeDiskFailurePolicy, boolean extendedVerification, + boolean validateAllRows, boolean checkVersion, boolean mutateRepairStatus, boolean checkOwnsTokens, @@ -72,11 +74,15 @@ private Options(boolean invokeDiskFailurePolicy, { this.invokeDiskFailurePolicy = invokeDiskFailurePolicy; this.extendedVerification = extendedVerification; + this.validateAllRows = validateAllRows; this.checkVersion = checkVersion; this.mutateRepairStatus = mutateRepairStatus; this.checkOwnsTokens = checkOwnsTokens; this.quick = quick; this.tokenLookup = tokenLookup; + + if (validateAllRows && !extendedVerification) + throw new IllegalArgumentException("validateAllRows must be enabled with extended verification"); } @Override @@ -85,6 +91,7 @@ public String toString() return "Options{" + "invokeDiskFailurePolicy=" + invokeDiskFailurePolicy + ", extendedVerification=" + extendedVerification + + ", validateAllRows=" + validateAllRows + ", checkVersion=" + checkVersion + ", mutateRepairStatus=" + mutateRepairStatus + ", checkOwnsTokens=" + checkOwnsTokens + @@ -96,6 +103,7 @@ public static class Builder { private boolean invokeDiskFailurePolicy = false; // invoking disk failure policy can stop the node if we find a corrupt stable private boolean extendedVerification = false; + private boolean validateAllRows = false; // whether to validate all rows in each partition in extended verification mode private boolean checkVersion = false; private boolean mutateRepairStatus = false; // mutating repair status can be dangerous private boolean checkOwnsTokens = false; @@ -114,6 +122,12 @@ public Builder extendedVerification(boolean param) return this; } + public Builder validateAllRows(boolean param) + { + this.validateAllRows = param; + return this; + } + public Builder checkVersion(boolean param) { this.checkVersion = param; @@ -146,7 +160,7 @@ public Builder tokenLookup(Function>> public Options build() { - return new Options(invokeDiskFailurePolicy, extendedVerification, checkVersion, mutateRepairStatus, checkOwnsTokens, quick, tokenLookup); + return new Options(invokeDiskFailurePolicy, extendedVerification, validateAllRows, checkVersion, mutateRepairStatus, checkOwnsTokens, quick, tokenLookup); } } } diff --git a/src/java/org/apache/cassandra/io/sstable/KeyIterator.java b/src/java/org/apache/cassandra/io/sstable/KeyIterator.java index dbe501f36e7e..b6dd9934861a 100644 --- a/src/java/org/apache/cassandra/io/sstable/KeyIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/KeyIterator.java @@ -25,6 +25,7 @@ import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.CloseableIterator; +// TODO STAR-247: Implement a unit test public class KeyIterator extends AbstractIterator implements CloseableIterator { private final IPartitioner partitioner; diff --git a/src/java/org/apache/cassandra/io/sstable/RangeAwareSSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/RangeAwareSSTableWriter.java index 422c6eaa6eb7..726ed56163c3 100644 --- a/src/java/org/apache/cassandra/io/sstable/RangeAwareSSTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/RangeAwareSSTableWriter.java @@ -26,21 +26,24 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.DiskBoundaries; -import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.commitlog.IntervalSet; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.TimeUUID; public class RangeAwareSSTableWriter implements SSTableMultiWriter { - private final List boundaries; + private final List boundaries; private final List directories; private final int sstableLevel; + private final IntervalSet commitLogIntervals; private final long estimatedKeys; private final long repairedAt; private final TimeUUID pendingRepair; @@ -54,7 +57,7 @@ public class RangeAwareSSTableWriter implements SSTableMultiWriter private final List finishedReaders = new ArrayList<>(); private SSTableMultiWriter currentWriter = null; - public RangeAwareSSTableWriter(ColumnFamilyStore cfs, long estimatedKeys, long repairedAt, TimeUUID pendingRepair, boolean isTransient, SSTableFormat format, int sstableLevel, long totalSize, LifecycleNewTracker lifecycleNewTracker, SerializationHeader header) throws IOException + public RangeAwareSSTableWriter(ColumnFamilyStore cfs, long estimatedKeys, long repairedAt, TimeUUID pendingRepair, boolean isTransient, SSTableFormat format, IntervalSet commitLogIntervals, int sstableLevel, long totalSize, LifecycleNewTracker lifecycleNewTracker, SerializationHeader header) throws IOException { DiskBoundaries db = cfs.getDiskBoundaries(); directories = db.directories; @@ -67,7 +70,8 @@ public RangeAwareSSTableWriter(ColumnFamilyStore cfs, long estimatedKeys, long r this.format = format; this.lifecycleNewTracker = lifecycleNewTracker; this.header = header; - boundaries = db.positions; + this.commitLogIntervals = commitLogIntervals; + boundaries = db.getPositions(); if (boundaries == null) { Directories.DataDirectory localDir = cfs.getDirectories().getWriteableLocation(totalSize); @@ -75,7 +79,7 @@ public RangeAwareSSTableWriter(ColumnFamilyStore cfs, long estimatedKeys, long r throw new IOException(String.format("Insufficient disk space to store %s", FBUtilities.prettyPrintMemory(totalSize))); Descriptor desc = cfs.newSSTableDescriptor(cfs.getDirectories().getLocationForDisk(localDir), format); - currentWriter = cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, pendingRepair, isTransient, null, sstableLevel, header, lifecycleNewTracker); + currentWriter = cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, pendingRepair, isTransient, commitLogIntervals, sstableLevel, header, lifecycleNewTracker); } } @@ -85,7 +89,7 @@ private void maybeSwitchWriter(DecoratedKey key) return; boolean switched = false; - while (currentIndex < 0 || key.compareTo(boundaries.get(currentIndex)) > 0) + while (currentIndex < 0 || key.getToken().compareTo(boundaries.get(currentIndex)) > 0) { switched = true; currentIndex++; @@ -97,7 +101,7 @@ private void maybeSwitchWriter(DecoratedKey key) finishedWriters.add(currentWriter); Descriptor desc = cfs.newSSTableDescriptor(cfs.getDirectories().getLocationForDisk(directories.get(currentIndex)), format); - currentWriter = cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, pendingRepair, isTransient, null, sstableLevel, header, lifecycleNewTracker); + currentWriter = cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, pendingRepair, isTransient, commitLogIntervals, sstableLevel, header, lifecycleNewTracker); } } @@ -108,7 +112,7 @@ public void append(UnfilteredRowIterator partition) } @Override - public Collection finish(boolean openResult) + public Collection finish(boolean openResult, StorageHandler storageHandler) { if (currentWriter != null) finishedWriters.add(currentWriter); @@ -116,7 +120,7 @@ public Collection finish(boolean openResult) for (SSTableMultiWriter writer : finishedWriters) { if (writer.getBytesWritten() > 0) - finishedReaders.addAll(writer.finish(openResult)); + finishedReaders.addAll(writer.finish(openResult, storageHandler)); else SSTableMultiWriter.abortOrDie(writer); } @@ -130,11 +134,10 @@ public Collection finished() } @Override - public SSTableMultiWriter setOpenResult(boolean openResult) + public void openResult(StorageHandler storageHandler) { - finishedWriters.forEach((w) -> w.setOpenResult(openResult)); - currentWriter.setOpenResult(openResult); - return this; + finishedWriters.forEach(w -> w.openResult(storageHandler)); + currentWriter.openResult(storageHandler); } public String getFilename() @@ -145,13 +148,25 @@ public String getFilename() @Override public long getBytesWritten() { - return currentWriter != null ? currentWriter.getBytesWritten() : 0L; + long bytesWritten = currentWriter != null ? currentWriter.getBytesWritten() : 0L; + for (SSTableMultiWriter writer : finishedWriters) + bytesWritten += writer.getBytesWritten(); + return bytesWritten; } @Override public long getOnDiskBytesWritten() { - return currentWriter != null ? currentWriter.getOnDiskBytesWritten() : 0L; + long bytesWritten = currentWriter != null ? currentWriter.getOnDiskBytesWritten() : 0L; + for (SSTableMultiWriter writer : finishedWriters) + bytesWritten += writer.getOnDiskBytesWritten(); + return bytesWritten; + } + + @Override + public int getSegmentCount() + { + return finishedWriters.size() + 1; } @Override diff --git a/src/java/org/apache/cassandra/io/sstable/ReducingKeyIterator.java b/src/java/org/apache/cassandra/io/sstable/ReducingKeyIterator.java index 1cd780e0769d..201d4384f7e7 100644 --- a/src/java/org/apache/cassandra/io/sstable/ReducingKeyIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/ReducingKeyIterator.java @@ -20,14 +20,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.utils.CloseableIterator; -import org.apache.cassandra.utils.IMergeIterator; import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.Reducer; import org.apache.cassandra.utils.Throwables; /** @@ -36,23 +35,28 @@ public class ReducingKeyIterator implements CloseableIterator { private final ArrayList iters; - private volatile IMergeIterator mi; + private volatile CloseableIterator mi; + private final long totalLength; public ReducingKeyIterator(Collection sstables) { iters = new ArrayList<>(sstables.size()); + long len = 0; for (SSTableReader sstable : sstables) { try { - iters.add(sstable.keyIterator()); + KeyIterator iter = sstable.keyIterator(); + iters.add(iter); + len += iter.getTotalBytes(); } - catch (IOException ex) + catch (IOException | RuntimeException ex) { iters.forEach(FileUtils::closeQuietly); - throw new RuntimeException("Failed to create a key iterator for sstable " + sstable.getFilename()); + throw new RuntimeException("Failed to create a key iterator for sstable " + sstable.getFilename(), ex); } } + this.totalLength = len; } private void maybeInit() @@ -64,12 +68,12 @@ private void maybeInit() { if (mi == null) { - mi = MergeIterator.get(iters, DecoratedKey.comparator, new MergeIterator.Reducer() + mi = MergeIterator.getCloseable(iters, DecoratedKey.comparator, new Reducer<>() { DecoratedKey reduced = null; @Override - public boolean trivialReduceIsTrivial() + public boolean singleSourceReduceIsTrivial() { return true; } @@ -79,7 +83,7 @@ public void reduce(int idx, DecoratedKey current) reduced = current; } - protected DecoratedKey getReduced() + public DecoratedKey getReduced() { return reduced; } @@ -106,14 +110,7 @@ public void close() public long getTotalBytes() { - maybeInit(); - - long m = 0; - for (Iterator iter : mi.iterators()) - { - m += ((KeyIterator) iter).getTotalBytes(); - } - return m; + return totalLength; } public long getBytesRead() @@ -121,9 +118,9 @@ public long getBytesRead() maybeInit(); long m = 0; - for (Iterator iter : mi.iterators()) + for (KeyIterator iter : iters) { - m += ((KeyIterator) iter).getBytesRead(); + m += iter.getBytesRead(); } return m; } diff --git a/src/java/org/apache/cassandra/io/sstable/SSTable.java b/src/java/org/apache/cassandra/io/sstable/SSTable.java index 8f77908184d1..22d563a9cca4 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTable.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTable.java @@ -17,35 +17,38 @@ */ package org.apache.cassandra.io.sstable; +import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; import javax.annotation.Nullable; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Predicates; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.cache.ChunkCache; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.filter.BloomFilterTracker; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.TOCComponent; +import org.apache.cassandra.io.sstable.metadata.CompactionMetadata; +import org.apache.cassandra.io.sstable.metadata.MetadataType; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.metrics.TableMetrics; @@ -66,15 +69,19 @@ */ public abstract class SSTable { + static final Logger logger = LoggerFactory.getLogger(SSTable.class); + public static final int TOMBSTONE_HISTOGRAM_BIN_SIZE = 100; public static final int TOMBSTONE_HISTOGRAM_SPOOL_SIZE = 100000; public static final int TOMBSTONE_HISTOGRAM_TTL_ROUND_SECONDS = CassandraRelevantProperties.STREAMING_HISTOGRAM_ROUND_SECONDS.getInt(); public final Descriptor descriptor; - protected final Set components; + private volatile ImmutableSet components; public final boolean compression; protected final TableMetadataRef metadata; + // This field is null if the compaction metadata is not loaded yet, it can be a empty optional if the compaction metadata is not available + protected Optional compactionMetadata; public final ChunkCache chunkCache; public final IOOptions ioOptions; @@ -90,7 +97,7 @@ public SSTable(Builder builder, Owner owner) this.descriptor = builder.descriptor; this.ioOptions = builder.getIOOptions(); - this.components = new CopyOnWriteArraySet<>(builder.getComponents()); + this.components = ImmutableSet.copyOf(builder.getComponents()); this.compression = components.contains(Components.COMPRESSION_INFO); this.metadata = builder.getTableMetadataRef(); this.chunkCache = builder.getChunkCache(); @@ -151,16 +158,26 @@ public static void hardlink(Descriptor tmpdesc, Descriptor newdesc, Set FileUtils.createHardLinkWithoutConfirm(tmpdesc.fileFor(c), newdesc.fileFor(c))); } - public abstract DecoratedKey getFirst(); + public abstract PartitionPosition getFirst(); - public abstract DecoratedKey getLast(); + public abstract PartitionPosition getLast(); public abstract AbstractBounds getBounds(); - @VisibleForTesting - public Set getComponents() + public ImmutableSet components() + { + return components; + } + + public Optional getCompactionMetadata() throws IOException { - return ImmutableSet.copyOf(components); + // if compaction metadata is not loaded yet, load it + if (compactionMetadata == null) + { + logger.debug("Loading compaction metadata for {}", descriptor); + compactionMetadata = Optional.ofNullable((CompactionMetadata) descriptor.getMetadataSerializer().deserialize(descriptor, MetadataType.COMPACTION)); + } + return compactionMetadata; } /** @@ -178,6 +195,11 @@ public TableMetadata metadata() return metadata.get(); } + public TableMetadataRef metadataRef() + { + return metadata; + } + public IPartitioner getPartitioner() { return metadata().partitioner; @@ -190,7 +212,12 @@ public DecoratedKey decorateKey(ByteBuffer key) public String getFilename() { - return descriptor.fileFor(Components.DATA).absolutePath(); + return getDataFile().path(); + } + + public File getDataFile() + { + return descriptor.fileFor(Components.DATA); } public String getColumnFamilyName() @@ -208,12 +235,9 @@ public SSTableId getId() return descriptor.id; } - public List getAllFilePaths() + public int getComponentSize() { - List ret = new ArrayList<>(components.size()); - for (Component component : components) - ret.add(descriptor.fileFor(component).absolutePath()); - return ret; + return components.size(); } /** @@ -324,9 +348,7 @@ public static void validateRepairedMetadata(long repairedAt, TimeUUID pendingRep */ public synchronized void addComponents(Collection newComponents) { - Collection componentsToAdd = Collections2.filter(newComponents, Predicates.not(Predicates.in(components))); - TOCComponent.updateTOC(descriptor, componentsToAdd); - components.addAll(componentsToAdd); + registerComponents(newComponents, null); } /** @@ -337,15 +359,13 @@ public synchronized void addComponents(Collection newComponents) public synchronized void registerComponents(Collection newComponents, Tracker tracker) { Collection componentsToAdd = new HashSet<>(Collections2.filter(newComponents, x -> !components.contains(x))); + if (componentsToAdd.isEmpty()) + return; + TOCComponent.updateTOC(descriptor, componentsToAdd); - components.addAll(componentsToAdd); + components = ImmutableSet.builder().addAll(components).addAll(componentsToAdd).build(); - for (Component component : componentsToAdd) - { - File file = descriptor.fileFor(component); - if (file.exists()) - tracker.updateLiveDiskSpaceUsed(file.length()); - } + updateComponentsTracking(componentsToAdd, tracker, 1); } /** @@ -355,22 +375,69 @@ public synchronized void registerComponents(Collection newComponents, */ public synchronized void unregisterComponents(Collection removeComponents, Tracker tracker) { - Collection componentsToRemove = new HashSet<>(Collections2.filter(removeComponents, components::contains)); - components.removeAll(componentsToRemove); + Set componentsToRemove = new HashSet<>(Collections2.filter(removeComponents, components::contains)); + components = Sets.difference(components, componentsToRemove).immutableCopy(); TOCComponent.rewriteTOC(descriptor, components); - for (Component component : componentsToRemove) + updateComponentsTracking(componentsToRemove, tracker, -1); + } + + private void updateComponentsTracking(Collection toUpdate, Tracker tracker, long multiplier) + { + if (tracker == null) + return; + + for (Component component : toUpdate) { File file = descriptor.fileFor(component); if (file.exists()) - tracker.updateLiveDiskSpaceUsed(-file.length()); + tracker.updateSizeTracking(multiplier * file.length()); } } + /** + * Reads components from the TOC file and update the `components` set of this object accordindly. + *

    + * Usually, components are added/removed through {@link #addComponents}, {@link #registerComponents} or + * {@link #unregisterComponents}, which both update this object component and update the TOC file accordingly, and + * this method should not be used. But some implementation of tiered storage may add components/rewrite the TOC + * "externally" (one reason can be offloading index rebuild) and need those change to be reflected to this object + * and this is where this method comes in. + *

    + * If the TOC file does not exist, cannot be read, or does not at least contains the minimal components that all + * sstables should have when this is called, this method is a no-op. + */ + public synchronized void reloadComponentsFromTOC(Tracker tracker) + { + try + { + Set tocComponents = TOCComponent.loadTOC(descriptor); + Set requiredComponents = descriptor.getFormat().requiredComponents(); + if (!tocComponents.containsAll(requiredComponents)) + { + logger.error("Cannot reload components from read TOC file for {}; the TOC does not contain all the required components for the sstable type and is like corrupted (components in TOC: {}, required by sstable format: {})", + descriptor, tocComponents, requiredComponents); + return; + } + + Set toAdd = Sets.difference(tocComponents, components); + Set toRemove = Sets.difference(components, tocComponents); + components = ImmutableSet.copyOf(tocComponents); + + updateComponentsTracking(toAdd, tracker, 1); + updateComponentsTracking(toRemove, tracker, -1); + + } + catch (IOException e) + { + logger.error("Failed to read TOC file for {}; ignoring component reload", descriptor, e); + } + } + public interface Owner { Double getCrcCheckChance(); - + BloomFilterTracker getBloomFilterTracker(); OpOrder.Barrier newReadOrderingBarrier(); TableMetrics getMetrics(); diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableFlushObserver.java b/src/java/org/apache/cassandra/io/sstable/SSTableFlushObserver.java index 0f28f62dab31..7646377d8bfb 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableFlushObserver.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableFlushObserver.java @@ -20,10 +20,9 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Unfiltered; -import org.apache.cassandra.io.sstable.format.SSTableReader; /** - * Observer for events in the lifecycle of writing out an sstable. + * Observer for events in the lifecycle of writing out an sstable -- for compaction as well as for flush! */ public interface SSTableFlushObserver { @@ -41,7 +40,7 @@ public interface SSTableFlushObserver * @param keyPositionForSASI SSTable format specific key position for storage attached indexes, it can be * in data file or in some index file. It is the same position as returned by * {@link KeyReader#keyPositionForSecondaryIndex()} for the same format, and the same - * position as expected by {@link SSTableReader#keyAtPositionFromSecondaryIndex(long)}. + * position as expected by {@link IKeyFetcher#apply(long)} when created for SASI. */ void startPartition(DecoratedKey key, long keyPosition, long keyPositionForSASI); @@ -66,7 +65,7 @@ public interface SSTableFlushObserver /** * Called when all data is written to the file and it's ready to be finished up. */ - void complete(); + void complete(SSTable ssTable); /** * Called when current sstable writer is switched during sharded compaction to free any in-memory resources associated diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableHeaderFix.java b/src/java/org/apache/cassandra/io/sstable/SSTableHeaderFix.java new file mode 100644 index 000000000000..c1fb31e86673 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/SSTableHeaderFix.java @@ -0,0 +1,960 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.sstable; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.marshal.AbstractCompositeType; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.DynamicCompositeType; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.MultiCellCapableType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.TupleType; +import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.metadata.MetadataComponent; +import org.apache.cassandra.io.sstable.metadata.MetadataType; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.DroppedColumn; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_SKIP_AUTOMATIC_UDT_FIX; + +/** + * Validates and fixes type issues in the serialization-header of sstables. + */ +public abstract class SSTableHeaderFix +{ + // C* 3.0 upgrade code + + private static final boolean SKIP_AUTOMATIC_FIX_ON_UPGRADE = CASSANDRA_SKIP_AUTOMATIC_UDT_FIX.getBoolean(); + + public static void fixNonFrozenUDTIfUpgradeFrom30() + { + String previousVersionString = FBUtilities.getPreviousReleaseVersionString(); + if (previousVersionString == null) + return; + CassandraVersion previousVersion = new CassandraVersion(previousVersionString); + if (previousVersion.major != 3 || previousVersion.minor > 0) + { + // Not an upgrade from 3.0, nothing to do here + return; + } + + boolean hasDroppedFrozenColumn = false; + for (String keyspace : Schema.instance.getUserKeyspaces()) + for (TableMetadata tableMetadata : Schema.instance.getTablesAndViews(keyspace)) + if (!tableMetadata.isIndex() && !tableMetadata.isView() && !tableMetadata.isVirtual()) + hasDroppedFrozenColumn |= hasDroppedFrozenColumn(tableMetadata.droppedColumns.values()); + + if (!hasDroppedFrozenColumn) + { + logger.info("No dropped frozen columns found - NOT fixing sstable metadata serialization-headers"); + return; + } + + if (SKIP_AUTOMATIC_FIX_ON_UPGRADE) + { + logger.warn("Detected upgrade from {} to {}, but -D{}=true, NOT fixing UDT type references in " + + "sstable metadata serialization-headers", + previousVersionString, + FBUtilities.getReleaseVersionString(), + CASSANDRA_SKIP_AUTOMATIC_UDT_FIX.getKey()); + return; + } + + logger.info("Detected upgrade from {} to {}, fixing UDT type references in sstable metadata serialization-headers", + previousVersionString, + FBUtilities.getReleaseVersionString()); + + SSTableHeaderFix instance = SSTableHeaderFix.builder() + .schemaCallback(() -> Schema.instance::getTableMetadata) + .build(); + instance.execute(); + } + + // "regular" SSTableHeaderFix code, also used by StandaloneScrubber. + + private static final Logger logger = LoggerFactory.getLogger(SSTableHeaderFix.class); + + protected final Consumer info; + protected final Consumer warn; + protected final Consumer error; + protected final boolean dryRun; + protected final Function schemaCallback; + + private final List descriptors; + + private final List>> updates = new ArrayList<>(); + private boolean hasErrors; + + SSTableHeaderFix(Builder builder) + { + this.info = builder.info; + this.warn = builder.warn; + this.error = builder.error; + this.dryRun = builder.dryRun; + this.schemaCallback = builder.schemaCallback.get(); + this.descriptors = new ArrayList<>(builder.descriptors); + Objects.requireNonNull(this.info, "info is null"); + Objects.requireNonNull(this.warn, "warn is null"); + Objects.requireNonNull(this.error, "error is null"); + Objects.requireNonNull(this.schemaCallback, "schemaCallback is null"); + } + + public static Builder builder() + { + return new Builder(); + } + + /** + * Builder to configure and construct an instance of {@link SSTableHeaderFix}. + * Default settings: + *

      + *
    • log via the slf4j logger of {@link SSTableHeaderFix}
    • + *
    • no dry-run (i.e. validate and fix, if no serious errors are detected)
    • + *
    • no schema callback
    • + *
    + * If neither {@link #withDescriptor(Descriptor)} nor {@link #withPath(Path)} are used, + * all "live" sstables in all data directories will be scanned. + */ + public static class Builder + { + private final List paths = new ArrayList<>(); + private final List descriptors = new ArrayList<>(); + private Consumer info = (ln) -> logger.info("{}", ln); + private Consumer warn = (ln) -> logger.warn("{}", ln); + private Consumer error = (ln) -> logger.error("{}", ln); + private boolean dryRun; + private Supplier> schemaCallback = () -> null; + + private Builder() + {} + + /** + * Only validate and prepare fix, but do not write updated (fixed) sstable serialization-headers. + */ + public Builder dryRun() + { + dryRun = true; + return this; + } + + public Builder info(Consumer output) + { + this.info = output; + return this; + } + + public Builder warn(Consumer warn) + { + this.warn = warn; + return this; + } + + public Builder error(Consumer error) + { + this.error = error; + return this; + } + + /** + * Manually provide an individual sstable or directory containing sstables. + * + * Implementation note: procesing "live" sstables in their data directories as well as sstables + * in snapshots and backups in the data directories works. + * + * But processing sstables that reside somewhere else (i.e. verifying sstables before import) + * requires the use of {@link #withDescriptor(Descriptor)}. + */ + public Builder withPath(Path path) + { + this.paths.add(path); + return this; + } + + public Builder withDescriptor(Descriptor descriptor) + { + this.descriptors.add(descriptor); + return this; + } + + /** + * Schema callback to retrieve the schema of a table. Production code always delegates to the + * live schema ({@code Schema.instance}). Unit tests use this method to feed a custom schema. + */ + public Builder schemaCallback(Supplier> schemaCallback) + { + this.schemaCallback = schemaCallback; + return this; + } + + public SSTableHeaderFix build() + { + if (paths.isEmpty() && descriptors.isEmpty()) + return new AutomaticHeaderFix(this); + + return new ManualHeaderFix(this); + } + + public Builder logToList(List output) + { + return info(ln -> output.add("INFO " + ln)) + .warn(ln -> output.add("WARN " + ln)) + .error(ln -> output.add("ERROR " + ln)); + } + } + + public final void execute() + { + prepare(); + + logger.debug("Processing {} sstables:{}", + descriptors.size(), + descriptors.stream().map(Descriptor::toString).collect(Collectors.joining("\n ", "\n ", ""))); + + descriptors.forEach(this::processSSTable); + + if (updates.isEmpty()) + return; + + if (hasErrors) + { + info.accept("Stopping due to previous errors. Either fix the errors or specify the ignore-errors option."); + return; + } + + if (dryRun) + { + info.accept("Not fixing identified and fixable serialization-header issues."); + return; + } + + info.accept("Writing new metadata files"); + updates.forEach(descAndMeta -> writeNewMetadata(descAndMeta.left, descAndMeta.right)); + info.accept("Finished writing new metadata files"); + } + + /** + * Whether {@link #execute()} encountered an error. + */ + public boolean hasError() + { + return hasErrors; + } + + /** + * Whether {@link #execute()} found mismatches. + */ + public boolean hasChanges() + { + return !updates.isEmpty(); + } + + abstract void prepare(); + + private void error(String format, Object... args) + { + hasErrors = true; + error.accept(String.format(format, args)); + } + + void processFileOrDirectory(Path path) + { + Stream.of(path) + .flatMap(SSTableHeaderFix::maybeExpandDirectory) + .filter(p -> { + try + { + return Descriptor.fromFileWithComponent(new File(p)).right.type == SSTableFormat.Components.DATA.type; + } + catch (IllegalArgumentException e) // ignore the '.keep' files + { + return false; + } + }) + .map(Path::toString) + .map((String file) -> Descriptor.fromFile(new File(file))) + .forEach(descriptors::add); + } + + private static Stream maybeExpandDirectory(Path path) + { + if (Files.isRegularFile(path)) + return Stream.of(path); + return LifecycleTransaction.getFiles(path, (file, fileType) -> fileType == Directories.FileType.FINAL, Directories.OnTxnErr.IGNORE) + .stream() + .map(File::toPath); + } + + private static boolean hasDroppedFrozenColumn(Collection droppedColumns) + { + boolean hasDroppedFrozenColumn = false; + for (DroppedColumn droppedColumn : droppedColumns) + hasDroppedFrozenColumn |= droppedColumn.column.type.equals(droppedColumn.column.type.freeze()) + && droppedColumn.column.type instanceof MultiCellCapableType; + + return hasDroppedFrozenColumn; + } + + private void processSSTable(Descriptor desc) + { + if (desc.cfname.indexOf('.') != -1) + { + // secondary index not checked + + // partition-key is the indexed column type + // clustering-key is org.apache.cassandra.db.marshal.PartitionerDefinedOrder + // no static columns, no regular columns + return; + } + + TableMetadata tableMetadata = schemaCallback.apply(desc); + if (tableMetadata == null) + { + error("Table %s.%s not found in the schema - NOT checking sstable %s", desc.ksname, desc.cfname, desc); + return; + } + + if (!hasDroppedFrozenColumn(tableMetadata.droppedColumns.values())) + { + info.accept(String.format("Table %s.%s has no dropped frozen columns - NOT checking sstable %s", desc.ksname, desc.cfname, desc)); + return; + } + + Set components = desc.discoverComponents(); + if (components.stream().noneMatch(c -> c.type == SSTableFormat.Components.STATS.type)) + { + error("sstable %s has no -Statistics.db component.", desc); + return; + } + + Map metadata = readSSTableMetadata(desc); + if (metadata == null) + return; + + MetadataComponent component = metadata.get(MetadataType.HEADER); + if (!(component instanceof SerializationHeader.Component)) + { + error("sstable %s: Expected %s, but got %s from metadata.get(MetadataType.HEADER)", + desc, + SerializationHeader.Component.class.getName(), + component != null ? component.getClass().getName() : "'null'"); + return; + } + SerializationHeader.Component header = (SerializationHeader.Component) component; + + // check partition key type + AbstractType keyType = validatePartitionKey(desc, tableMetadata, header); + + // check clustering columns + List> clusteringTypes = validateClusteringColumns(desc, tableMetadata, header); + + // check static and regular columns + LinkedHashMap> staticColumns = validateColumns(desc, tableMetadata, header.getStaticColumns(), ColumnMetadata.Kind.STATIC); + LinkedHashMap> regularColumns = validateColumns(desc, tableMetadata, header.getRegularColumns(), ColumnMetadata.Kind.REGULAR); + + SerializationHeader.Component newHeader = SerializationHeader.Component.buildComponentForTools(keyType, + clusteringTypes, + staticColumns, + regularColumns, + header.getEncodingStats()); + + // SerializationHeader.Component has no equals(), but a "good" toString() + if (header.toString().equals(newHeader.toString())) + return; + + Map newMetadata = new LinkedHashMap<>(metadata); + newMetadata.put(MetadataType.HEADER, newHeader); + + updates.add(Pair.create(desc, newMetadata)); + } + + private AbstractType validatePartitionKey(Descriptor desc, TableMetadata tableMetadata, SerializationHeader.Component header) + { + boolean keyMismatch = false; + AbstractType headerKeyType = header.getKeyType(); + AbstractType schemaKeyType = tableMetadata.partitionKeyType; + boolean headerKeyComposite = headerKeyType instanceof CompositeType; + boolean schemaKeyComposite = schemaKeyType instanceof CompositeType; + if (headerKeyComposite != schemaKeyComposite) + { + // one is a composite partition key, the other is not - very suspicious + keyMismatch = true; + } + else if (headerKeyComposite) // && schemaKeyComposite + { + // Note, the logic is similar as just calling 'fixType()' using the composite partition key, + // but the log messages should use the composite partition key column names. + List> headerKeyComponents = ((CompositeType) headerKeyType).subTypes(); + List> schemaKeyComponents = ((CompositeType) schemaKeyType).subTypes(); + if (headerKeyComponents.size() != schemaKeyComponents.size()) + { + // different number of components in composite partition keys - very suspicious + keyMismatch = true; + // Just use the original type from the header. Since the number of partition key components + // don't match, there's nothing to meaningfully validate against. + } + else + { + // fix components in composite partition key, if necessary + List> newComponents = new ArrayList<>(schemaKeyComponents.size()); + for (int i = 0; i < schemaKeyComponents.size(); i++) + { + AbstractType headerKeyComponent = headerKeyComponents.get(i); + AbstractType schemaKeyComponent = schemaKeyComponents.get(i); + AbstractType fixedType = fixType(desc, + tableMetadata.partitionKeyColumns().get(i).name.bytes, + headerKeyComponent, + schemaKeyComponent, + false); + if (fixedType == null) + keyMismatch = true; + else + headerKeyComponent = fixedType; + newComponents.add(fixType(desc, + tableMetadata.partitionKeyColumns().get(i).name.bytes, + headerKeyComponent, + schemaKeyComponent, + false)); + } + headerKeyType = CompositeType.getInstance(newComponents); + } + } + else + { + // fix non-composite partition key, if necessary + AbstractType fixedType = fixType(desc, tableMetadata.partitionKeyColumns().get(0).name.bytes, headerKeyType, schemaKeyType, false); + if (fixedType == null) + // non-composite partition key doesn't match and cannot be fixed + keyMismatch = true; + else + headerKeyType = fixedType; + } + if (keyMismatch) + error("sstable %s: Mismatch in partition key type between sstable serialization-header and schema (%s vs %s)", + desc, + headerKeyType.asCQL3Type(), + schemaKeyType.asCQL3Type()); + return headerKeyType; + } + + private List> validateClusteringColumns(Descriptor desc, TableMetadata tableMetadata, SerializationHeader.Component header) + { + List> headerClusteringTypes = header.getClusteringTypes(); + List> clusteringTypes = new ArrayList<>(); + boolean clusteringMismatch = false; + List schemaClustering = tableMetadata.clusteringColumns(); + if (schemaClustering.size() != headerClusteringTypes.size()) + { + clusteringMismatch = true; + // Just use the original types. Since the number of clustering columns don't match, there's nothing to + // meaningfully validate against. + clusteringTypes.addAll(headerClusteringTypes); + } + else + { + for (int i = 0; i < headerClusteringTypes.size(); i++) + { + AbstractType headerType = headerClusteringTypes.get(i); + ColumnMetadata column = schemaClustering.get(i); + AbstractType schemaType = column.type; + AbstractType fixedType = fixType(desc, column.name.bytes, headerType, schemaType, false); + if (fixedType == null) + clusteringMismatch = true; + else + headerType = fixedType; + clusteringTypes.add(headerType); + } + } + if (clusteringMismatch) + error("sstable %s: mismatch in clustering columns between sstable serialization-header and schema (%s vs %s)", + desc, + headerClusteringTypes.stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(Collectors.joining(",")), + schemaClustering.stream().map(cd -> cd.type.asCQL3Type().toString()).collect(Collectors.joining(","))); + return clusteringTypes; + } + + private LinkedHashMap> validateColumns(Descriptor desc, TableMetadata tableMetadata, Map> columns, ColumnMetadata.Kind kind) + { + LinkedHashMap> target = new LinkedHashMap<>(); + for (Map.Entry> nameAndType : columns.entrySet()) + { + ByteBuffer name = nameAndType.getKey(); + AbstractType type = nameAndType.getValue(); + + AbstractType fixedType = validateColumn(desc, tableMetadata, kind, name, type); + if (fixedType == null) + { + error("sstable %s: contains column '%s' of type '%s', which could not be validated", + desc, + type, + logColumnName(name)); + // don't use a "null" type instance + fixedType = type; + } + + target.put(name, fixedType); + } + return target; + } + + private AbstractType validateColumn(Descriptor desc, TableMetadata tableMetadata, ColumnMetadata.Kind kind, ByteBuffer name, AbstractType type) + { + ColumnMetadata cd = tableMetadata.getColumn(name); + if (cd == null) + { + // In case the column was dropped, there is not much that we can actually validate. + // The column could have been recreated using the same or a different kind or the same or + // a different type. Lottery... + + cd = tableMetadata.getDroppedColumn(name, kind == ColumnMetadata.Kind.STATIC); + if (cd == null) + { + for (IndexMetadata indexMetadata : tableMetadata.indexes) + { + String target = indexMetadata.options.get(IndexTarget.TARGET_OPTION_NAME); + if (target != null && ByteBufferUtil.bytes(target).equals(name)) + { + warn.accept(String.format("sstable %s: contains column '%s', which is not a column in the table '%s.%s', but a target for that table's index '%s'", + desc, + logColumnName(name), + tableMetadata.keyspace, + tableMetadata.name, + indexMetadata.name)); + return type; + } + } + + warn.accept(String.format("sstable %s: contains column '%s', which is not present in the schema", + desc, + logColumnName(name))); + } + else + { + // This is a best-effort approach to handle the case of a UDT column created *AND* dropped in + // C* 3.0. + if (type instanceof UserType && cd.type instanceof TupleType) + { + // At this point, we know that the type belongs to a dropped column, recorded with the + // dropped column type "TupleType" and using "UserType" in the sstable. So it is very + // likely, that this belongs to a dropped UDT. Fix that information to tuple-type. + return fixType(desc, name, type, cd.type, true); + } + } + + return type; + } + + // At this point, the column name is known to be a "non-dropped" column in the table. + if (cd.kind != kind) + error("sstable %s: contains column '%s' as a %s column, but is of kind %s in the schema", + desc, + logColumnName(name), + kind.name().toLowerCase(), + cd.kind.name().toLowerCase()); + else + type = fixType(desc, name, type, cd.type, false); + return type; + } + + private AbstractType fixType(Descriptor desc, ByteBuffer name, AbstractType typeInHeader, AbstractType typeInSchema, boolean droppedColumnMode) + { + AbstractType fixedType = fixTypeInner(typeInHeader, typeInSchema, droppedColumnMode); + if (fixedType != null) + { + if (fixedType != typeInHeader) + info.accept(String.format("sstable %s: Column '%s' needs to be updated from type '%s' to '%s'", + desc, + logColumnName(name), + typeInHeader.asCQL3Type().toSchemaString(), + fixedType.asCQL3Type().toSchemaString())); + return fixedType; + } + + error("sstable %s: contains column '%s' as type '%s', but schema mentions '%s'", + desc, + logColumnName(name), + typeInHeader.asCQL3Type(), + typeInSchema.asCQL3Type()); + + return typeInHeader; + } + + private AbstractType fixTypeInner(AbstractType typeInHeader, AbstractType typeInSchema, boolean droppedColumnMode) + { + if (typeEquals(typeInHeader, typeInSchema)) + return typeInHeader; + + if (typeInHeader instanceof CollectionType) + return fixTypeInnerCollection(typeInHeader, typeInSchema, droppedColumnMode); + + if (typeInHeader instanceof AbstractCompositeType) + return fixTypeInnerAbstractComposite(typeInHeader, typeInSchema, droppedColumnMode); + + if (typeInHeader instanceof TupleType) + return fixTypeInnerAbstractTuple(typeInHeader, typeInSchema, droppedColumnMode); + + // all types, beside CollectionType + AbstractCompositeType + TupleType, should be ok (no nested types) - just check for compatibility + if (typeInHeader.isCompatibleWith(typeInSchema)) + return typeInHeader; + + return null; + } + + private AbstractType fixTypeInnerAbstractTuple(AbstractType typeInHeader, AbstractType typeInSchema, boolean droppedColumnMode) + { + // This first 'if' handles the case when a UDT has been dropped, as a dropped UDT is recorded as a tuple + // in dropped_columns. If a UDT is to be replaced with a tuple, then also do that for the inner UDTs. + if (droppedColumnMode && typeInHeader.getClass() == UserType.class && typeInSchema instanceof TupleType) + return fixTypeInnerUserTypeDropped((UserType) typeInHeader, (TupleType) typeInSchema); + + if (typeInHeader.getClass() != typeInSchema.getClass()) + return null; + + if (typeInHeader.getClass() == UserType.class) + return fixTypeInnerUserType((UserType) typeInHeader, (UserType) typeInSchema); + + if (typeInHeader.getClass() == TupleType.class) + return fixTypeInnerTuple((TupleType) typeInHeader, (TupleType) typeInSchema, droppedColumnMode); + + throw new IllegalArgumentException("Unknown tuple type class " + typeInHeader.getClass().getName()); + } + + private AbstractType fixTypeInnerCollection(AbstractType typeInHeader, AbstractType typeInSchema, boolean droppedColumnMode) + { + if (typeInHeader.getClass() != typeInSchema.getClass()) + return null; + + if (typeInHeader.getClass() == ListType.class) + return fixTypeInnerList((ListType) typeInHeader, (ListType) typeInSchema, droppedColumnMode); + + if (typeInHeader.getClass() == SetType.class) + return fixTypeInnerSet((SetType) typeInHeader, (SetType) typeInSchema, droppedColumnMode); + + if (typeInHeader.getClass() == MapType.class) + return fixTypeInnerMap((MapType) typeInHeader, (MapType) typeInSchema, droppedColumnMode); + + throw new IllegalArgumentException("Unknown collection type class " + typeInHeader.getClass().getName()); + } + + private AbstractType fixTypeInnerAbstractComposite(AbstractType typeInHeader, AbstractType typeInSchema, boolean droppedColumnMode) + { + if (typeInHeader.getClass() != typeInSchema.getClass()) + return null; + + if (typeInHeader.getClass() == CompositeType.class) + return fixTypeInnerComposite((CompositeType) typeInHeader, (CompositeType) typeInSchema, droppedColumnMode); + + if (typeInHeader.getClass() == DynamicCompositeType.class) + { + // Not sure if we should care about UDTs in DynamicCompositeType at all... + if (!typeInHeader.isCompatibleWith(typeInSchema)) + return null; + + return typeInHeader; + } + + throw new IllegalArgumentException("Unknown composite type class " + typeInHeader.getClass().getName()); + } + + private AbstractType fixTypeInnerUserType(UserType cHeader, UserType cSchema) + { + if (!cHeader.keyspace.equals(cSchema.keyspace) || !cHeader.name.equals(cSchema.name)) + // different UDT - bummer... + return null; + + if (cHeader.isMultiCell() != cSchema.isMultiCell()) + { + if (cHeader.isMultiCell() && !cSchema.isMultiCell()) + { + // C* 3.0 writes broken SerializationHeader.Component instances - i.e. broken UDT type + // definitions into the sstable -Stats.db file, because 3.0 does not enclose frozen UDTs + // (and all UDTs in 3.0 were frozen) with an '' bracket. Since CASSANDRA-7423 (support + // for non-frozen UDTs, committed to C* 3.6), that frozen-bracket is quite important. + // Non-frozen (= multi-cell) UDTs are serialized in a fundamentally different way than + // frozen UDTs in sstables - most importantly, the order of serialized columns depends on + // the type: fixed-width types first, then variable length types (like frozen types), + // multi-cell types last. If C* >= 3.6 reads an sstable with a UDT that's written by + // C* < 3.6, a variety of CorruptSSTableExceptions get logged and clients will encounter + // read errors. + // At this point, we know that the type belongs to a "live" (non-dropped) column, so it + // is safe to correct the information from the header. + return cSchema; + } + + // In all other cases, there's not much we can do. + return null; + } + + return cHeader; + } + + private AbstractType fixTypeInnerUserTypeDropped(UserType cHeader, TupleType cSchema) + { + // Do not mess around with the UserType in the serialization header, if the column has been dropped. + // Only fix the multi-cell status when the header contains it as a multicell (non-frozen) UserType, + // but the schema says "frozen". + if (cHeader.isMultiCell() && !cSchema.isMultiCell()) + { + return new UserType(cHeader.keyspace, cHeader.name, cHeader.fieldNames(), cHeader.fieldTypes(), cSchema.isMultiCell()); + } + + return cHeader; + } + + private AbstractType fixTypeInnerTuple(TupleType cHeader, TupleType cSchema, boolean droppedColumnMode) + { + if (cHeader.size() != cSchema.size()) + // different number of components - bummer... + return null; + List> cHeaderFixed = new ArrayList<>(cHeader.size()); + boolean anyChanged = false; + for (int i = 0; i < cHeader.size(); i++) + { + AbstractType cHeaderComp = cHeader.type(i); + AbstractType cHeaderCompFixed = fixTypeInner(cHeaderComp, cSchema.type(i), droppedColumnMode); + if (cHeaderCompFixed == null) + // incompatible, bummer... + return null; + cHeaderFixed.add(cHeaderCompFixed); + anyChanged |= cHeaderComp != cHeaderCompFixed; + } + if (anyChanged || cSchema.isMultiCell() != cHeader.isMultiCell()) + // TODO this should create a non-frozen tuple type for the sake of handling a dropped, non-frozen UDT + return new TupleType(cHeaderFixed); + return cHeader; + } + + private AbstractType fixTypeInnerComposite(CompositeType cHeader, CompositeType cSchema, boolean droppedColumnMode) + { + if (cHeader.subTypes().size() != cSchema.subTypes().size()) + // different number of components - bummer... + return null; + List> cHeaderFixed = new ArrayList<>(cHeader.subTypes().size()); + boolean anyChanged = false; + for (int i = 0; i < cHeader.subTypes().size(); i++) + { + AbstractType cHeaderComp = cHeader.subTypes().get(i); + AbstractType cHeaderCompFixed = fixTypeInner(cHeaderComp, cSchema.subTypes().get(i), droppedColumnMode); + if (cHeaderCompFixed == null) + // incompatible, bummer... + return null; + cHeaderFixed.add(cHeaderCompFixed); + anyChanged |= cHeaderComp != cHeaderCompFixed; + } + if (anyChanged) + return CompositeType.getInstance(cHeaderFixed); + return cHeader; + } + + private AbstractType fixTypeInnerList(ListType cHeader, ListType cSchema, boolean droppedColumnMode) + { + AbstractType cHeaderElem = cHeader.getElementsType(); + AbstractType cHeaderElemFixed = fixTypeInner(cHeaderElem, cSchema.getElementsType(), droppedColumnMode); + if (cHeaderElemFixed == null) + // bummer... + return null; + if (cHeaderElem != cHeaderElemFixed) + // element type changed + return ListType.getInstance(cHeaderElemFixed, cHeader.isMultiCell()); + return cHeader; + } + + private AbstractType fixTypeInnerSet(SetType cHeader, SetType cSchema, boolean droppedColumnMode) + { + AbstractType cHeaderElem = cHeader.getElementsType(); + AbstractType cHeaderElemFixed = fixTypeInner(cHeaderElem, cSchema.getElementsType(), droppedColumnMode); + if (cHeaderElemFixed == null) + // bummer... + return null; + if (cHeaderElem != cHeaderElemFixed) + // element type changed + return SetType.getInstance(cHeaderElemFixed, cHeader.isMultiCell()); + return cHeader; + } + + private AbstractType fixTypeInnerMap(MapType cHeader, MapType cSchema, boolean droppedColumnMode) + { + AbstractType cHeaderKey = cHeader.getKeysType(); + AbstractType cHeaderVal = cHeader.getValuesType(); + AbstractType cHeaderKeyFixed = fixTypeInner(cHeaderKey, cSchema.getKeysType(), droppedColumnMode); + AbstractType cHeaderValFixed = fixTypeInner(cHeaderVal, cSchema.getValuesType(), droppedColumnMode); + if (cHeaderKeyFixed == null || cHeaderValFixed == null) + // bummer... + return null; + if (cHeaderKey != cHeaderKeyFixed || cHeaderVal != cHeaderValFixed) + // element type changed + return MapType.getInstance(cHeaderKeyFixed, cHeaderValFixed, cHeader.isMultiCell()); + return cHeader; + } + + private boolean typeEquals(AbstractType typeInHeader, AbstractType typeInSchema) + { + // Quite annoying, but the implementations of equals() on some implementation of AbstractType seems to be + // wrong, but toString() seems to work in such cases. + return typeInHeader.equals(typeInSchema) || typeInHeader.toString().equals(typeInSchema.toString()); + } + + private static String logColumnName(ByteBuffer columnName) + { + try + { + return ByteBufferUtil.string(columnName); + } + catch (CharacterCodingException e) + { + return "?? " + e; + } + } + + private Map readSSTableMetadata(Descriptor desc) + { + Map metadata; + try + { + metadata = desc.getMetadataSerializer().deserialize(desc, EnumSet.allOf(MetadataType.class)); + } + catch (IOException e) + { + error("Failed to deserialize metadata for sstable %s: %s", desc, e.toString()); + return null; + } + return metadata; + } + + private void writeNewMetadata(Descriptor desc, Map newMetadata) + { + File file = desc.fileFor(SSTableFormat.Components.STATS); + info.accept(String.format(" Writing new metadata file %s", file)); + try + { + desc.getMetadataSerializer().rewriteSSTableMetadata(desc, newMetadata); + } + catch (IOException e) + { + error("Failed to write metadata component for %s: %s", file, e.toString()); + throw new RuntimeException(e); + } + } + + /** + * Fix individually provided sstables or directories containing sstables. + */ + static class ManualHeaderFix extends SSTableHeaderFix + { + private final List paths; + + ManualHeaderFix(Builder builder) + { + super(builder); + this.paths = builder.paths; + } + + public void prepare() + { + paths.forEach(this::processFileOrDirectory); + } + } + + /** + * Fix all sstables in the configured data-directories. + */ + static class AutomaticHeaderFix extends SSTableHeaderFix + { + AutomaticHeaderFix(Builder builder) + { + super(builder); + } + + public void prepare() + { + info.accept("Scanning all data directories..."); + for (Directories.DataDirectory dataDirectory : Directories.dataDirectories) + scanDataDirectory(dataDirectory); + info.accept("Finished scanning all data directories..."); + } + + private void scanDataDirectory(Directories.DataDirectory dataDirectory) + { + info.accept(String.format("Scanning data directory %s", dataDirectory.location)); + File[] ksDirs = dataDirectory.location.tryList(); + if (ksDirs == null) + return; + for (File ksDir : ksDirs) + { + if (!ksDir.isDirectory() || !ksDir.isReadable()) + continue; + + String name = ksDir.name(); + + // silently ignore all system keyspaces + if (SchemaConstants.isLocalSystemKeyspace(name) || SchemaConstants.isReplicatedSystemKeyspace(name)) + continue; + + File[] tabDirs = ksDir.tryList(); + if (tabDirs == null) + continue; + for (File tabDir : tabDirs) + { + if (!tabDir.isDirectory() || !tabDir.isReadable()) + continue; + + processFileOrDirectory(tabDir.toPath()); + } + } + } + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableId.java b/src/java/org/apache/cassandra/io/sstable/SSTableId.java index 35c2dc986fe3..ddf255788255 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableId.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableId.java @@ -38,7 +38,7 @@ * - must be case-insensitive because the sstables can be stored on case-insensitive file system *

    */ -public interface SSTableId extends CellSourceIdentifier +public interface SSTableId extends Comparable, CellSourceIdentifier { /** * Creates a byte format of the identifier that can be parsed by diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableIdFactory.java b/src/java/org/apache/cassandra/io/sstable/SSTableIdFactory.java index d5b3276ed523..66a41fb030f2 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableIdFactory.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableIdFactory.java @@ -22,12 +22,34 @@ import java.util.Comparator; import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; + +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.utils.TimeUUID; public class SSTableIdFactory { public static final SSTableIdFactory instance = new SSTableIdFactory(); + private static boolean isULIDImpl() + { + String impl = CassandraRelevantProperties.SSTABLE_UUID_IMPL.getString().toLowerCase(); + if ("uuid".equals(impl)) + return false; + else if ("ulid".equals(impl)) + return true; + else + throw new IllegalArgumentException("Unsupported value for property " + CassandraRelevantProperties.SSTABLE_UUID_IMPL.getKey() + ": " + impl); + } + + private Stream> makeIdBuildersStream() + { + return isULIDImpl() + ? Stream.of(ULIDBasedSSTableId.Builder.instance, UUIDBasedSSTableId.Builder.instance, SequenceBasedSSTableId.Builder.instance) + : Stream.of(UUIDBasedSSTableId.Builder.instance, ULIDBasedSSTableId.Builder.instance, SequenceBasedSSTableId.Builder.instance); + } + /** * Constructs the instance of {@link SSTableId} from the given string representation. * It finds the right builder by verifying whether the given string is the representation of the related identifier @@ -37,18 +59,17 @@ public class SSTableIdFactory */ public SSTableId fromString(String str) throws IllegalArgumentException { - return Stream.of(UUIDBasedSSTableId.Builder.instance, SequenceBasedSSTableId.Builder.instance) - .filter(b -> b.isUniqueIdentifier(str)) - .findFirst() - .map(b -> b.fromString(str)) - .orElseThrow(() -> new IllegalArgumentException("String '" + str + "' does not match any SSTable identifier format")); + return makeIdBuildersStream().filter(b -> b.isUniqueIdentifier(str)) + .findFirst() + .map(b -> b.fromString(str)) + .orElseThrow(() -> new IllegalArgumentException("String '" + str + "' does not match any SSTable identifier format")); } /** * Constructs the instance of {@link SSTableId} from the given bytes. * It finds the right builder by verifying whether the given buffer is the representation of the related identifier * type using {@link SSTableId.Builder#isUniqueIdentifier(ByteBuffer)} method. - * + *

    * The method expects the identifier is encoded in all remaining bytes of the buffer. The method does not move the * pointer of the buffer. * @@ -56,42 +77,39 @@ public SSTableId fromString(String str) throws IllegalArgumentException */ public SSTableId fromBytes(ByteBuffer bytes) { - return Stream.of(UUIDBasedSSTableId.Builder.instance, SequenceBasedSSTableId.Builder.instance) - .filter(b -> b.isUniqueIdentifier(bytes)) - .findFirst() - .map(b -> b.fromBytes(bytes)) - .orElseThrow(() -> new IllegalArgumentException("Byte buffer of length " + bytes.remaining() + " does not match any SSTable identifier format")); + return makeIdBuildersStream().filter(b -> b.isUniqueIdentifier(bytes)) + .findFirst() + .map(b -> b.fromBytes(bytes)) + .orElseThrow(() -> new IllegalArgumentException("Byte buffer of length " + bytes.remaining() + " does not match any SSTable identifier format")); } /** * Returns default identifiers builder. */ - @SuppressWarnings("unchecked") - public SSTableId.Builder defaultBuilder() + public SSTableId.Builder defaultBuilder() { - SSTableId.Builder builder = DatabaseDescriptor.isUUIDSSTableIdentifiersEnabled() - ? UUIDBasedSSTableId.Builder.instance - : SequenceBasedSSTableId.Builder.instance; - return (SSTableId.Builder) builder; + if (DatabaseDescriptor.isUUIDSSTableIdentifiersEnabled()) + return isULIDImpl() + ? ULIDBasedSSTableId.Builder.instance + : UUIDBasedSSTableId.Builder.instance; + else + return SequenceBasedSSTableId.Builder.instance; } /** * Compare sstable identifiers so that UUID based identifier is always greater than sequence based identifier */ - public final static Comparator COMPARATOR = Comparator.nullsFirst((id1, id2) -> { - if (id1 instanceof UUIDBasedSSTableId) - { - UUIDBasedSSTableId uuidId1 = (UUIDBasedSSTableId) id1; - return (id2 instanceof UUIDBasedSSTableId) ? uuidId1.compareTo((UUIDBasedSSTableId) id2) : 1; - } - else if (id1 instanceof SequenceBasedSSTableId) - { - SequenceBasedSSTableId seqId1 = (SequenceBasedSSTableId) id1; - return (id2 instanceof SequenceBasedSSTableId) ? seqId1.compareTo((SequenceBasedSSTableId) id2) : -1; - } + public static final Comparator COMPARATOR = Comparator.nullsFirst(Comparator.comparing(SSTableIdFactory::asTimeUUID)); + + private static Pair asTimeUUID(SSTableId id) + { + if (id instanceof UUIDBasedSSTableId) + return Pair.of(((UUIDBasedSSTableId) id).uuid, null); + else if (id instanceof ULIDBasedSSTableId) + return Pair.of(((ULIDBasedSSTableId) id).approximateTimeUUID, null); + else if (id instanceof SequenceBasedSSTableId) + return Pair.of(null, ((SequenceBasedSSTableId) id).generation); else - { - throw new AssertionError("Unsupported comparison between " + id1.getClass().getName() + " and " + id2.getClass().getName()); - } - }); + throw new AssertionError("Unsupported sstable identifier type " + id.getClass().getName()); + } } diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java b/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java index d5a1ae8bccc5..7a5d2a9c23e3 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java @@ -30,6 +30,7 @@ import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.schema.TableMetadata; @@ -42,20 +43,20 @@ public class SSTableIdentityIterator implements Comparable finished = writer.finish(shouldOpenSSTables()); + Collection finished = writer.finish(shouldOpenSSTables(), null); notifySSTableProduced(finished); } catch (Throwable t) diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java index 3b43dcfdf4fb..767d8be9b583 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java @@ -24,6 +24,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.commitlog.IntervalSet; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.rows.UnfilteredRowIterator; @@ -31,6 +32,7 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Transactional; @@ -95,22 +97,23 @@ protected void doPrepare() @Override protected Throwable doPostCleanup(Throwable accumulate) { - txn.close(); - writer.close(); + accumulate = Throwables.close(accumulate, txn, writer); return super.doPostCleanup(accumulate); } - public Collection finish(boolean openResult) + public Collection finish(boolean openResult, StorageHandler storageHandler) { - writer.setOpenResult(openResult); - finish(); + prepareToCommit(); + if (openResult) + writer.openResult(storageHandler); + commit(); return writer.finished(); } @SuppressWarnings({"resource", "RedundantSuppression"}) // log and writer closed during doPostCleanup public static SSTableTxnWriter create(ColumnFamilyStore cfs, Descriptor descriptor, long keyCount, long repairedAt, TimeUUID pendingRepair, boolean isTransient, SerializationHeader header) { - LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE); + LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE, cfs.metadata); SSTableMultiWriter writer = cfs.createSSTableMultiWriter(descriptor, keyCount, repairedAt, pendingRepair, isTransient, header, txn); return new SSTableTxnWriter(txn, writer); } @@ -125,11 +128,11 @@ public static SSTableTxnWriter createRangeAware(TableMetadataRef metadata, { ColumnFamilyStore cfs = Keyspace.open(metadata.keyspace).getColumnFamilyStore(metadata.name); - LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE); + LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE, cfs.metadata); SSTableMultiWriter writer; try { - writer = new RangeAwareSSTableWriter(cfs, keyCount, repairedAt, pendingRepair, isTransient, type, 0, 0, txn, header); + writer = new RangeAwareSSTableWriter(cfs, keyCount, repairedAt, pendingRepair, isTransient, type, IntervalSet.empty(), 0, 0, txn, header); } catch (IOException e) { @@ -153,8 +156,8 @@ public static SSTableTxnWriter create(TableMetadataRef metadata, SSTable.Owner owner) { // if the column family store does not exist, we create a new default SSTableMultiWriter to use: - LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE); - SSTableMultiWriter writer = SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, pendingRepair, isTransient, metadata, null, 0, header, indexGroups, txn, owner); + LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE, metadata); + SSTableMultiWriter writer = SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, pendingRepair, isTransient, metadata, IntervalSet.empty(), 0, header, indexGroups, txn, owner); return new SSTableTxnWriter(txn, writer); } } diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableWatcher.java b/src/java/org/apache/cassandra/io/sstable/SSTableWatcher.java new file mode 100644 index 000000000000..41f58a49f828 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/SSTableWatcher.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import java.util.Set; + +import org.apache.cassandra.index.Index; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_SSTABLE_WATCHER; + +/** + * Watcher used when opening sstables to discover extra components, eg. archive component + */ +public interface SSTableWatcher +{ + SSTableWatcher instance = !CUSTOM_SSTABLE_WATCHER.isPresent() + ? new SSTableWatcher() {} + : FBUtilities.construct(CUSTOM_SSTABLE_WATCHER.getString(), "sstable watcher"); + + /** + * Discover extra components before reading TOC file + * + * @param descriptor sstable descriptor for current sstable + */ + default void discoverComponents(Descriptor descriptor) + { + } + + /** + * Discover extra components before opening sstable + * + * @param descriptor sstable descriptor for current sstable + * @param existing existing sstable components + * @return all discovered sstable components + */ + default Set discoverComponents(Descriptor descriptor, Set existing) + { + return existing; + } + + /** + * Called before executing index build on existing sstable + */ + default void onIndexBuild(SSTableReader sstable, Set indexes) + { + } + + /** + * Called when an index is dropped on index components affected by that drop. + *

    + * By default, this method simply deletes the components locally, but it can overriden if different/additional + * behavior is needed. + * + * @param metadata table metadata of the table the index was dropped from. + * @param components index components that are no longer in used due to an index drop. Note that this can + * be either per-index components (for the components of the exact index being dropped), + * or per-sstable components if the index dropped was the only index for the table and the + * per-sstable components are no longer needed. More precisely, if the last index of a table + * is dropped, then this method will usually be called twice per sstable, once for the index + * components, and once for the per-sstable components. + */ + default void onIndexDropped(TableMetadata metadata, IndexComponents.ForWrite components) + { + components.forceDeleteAllComponents(); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableZeroCopyWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableZeroCopyWriter.java index 46a490974e3e..75456e3e242f 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableZeroCopyWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableZeroCopyWriter.java @@ -37,6 +37,7 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.TOCComponent; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.SequentialWriter; @@ -54,23 +55,25 @@ public class SSTableZeroCopyWriter extends SSTable implements SSTableMultiWriter private volatile SSTableReader finalReader; private final Map componentWriters; // indexed by component name + private final LifecycleNewTracker lifecycleNewTracker; public SSTableZeroCopyWriter(Builder builder, LifecycleNewTracker lifecycleNewTracker, SSTable.Owner owner) { super(builder, owner); + this.lifecycleNewTracker = lifecycleNewTracker; lifecycleNewTracker.trackNew(this); this.componentWriters = new HashMap<>(); - Set unsupported = components.stream() + Set unsupported = components().stream() .filter(c -> !c.type.streamable) .collect(Collectors.toSet()); if (!unsupported.isEmpty()) throw new AssertionError(format("Unsupported streaming components detected: %s", unsupported)); - for (Component c : components) + for (Component c : components()) componentWriters.put(c.name, makeWriter(descriptor, c)); } @@ -127,13 +130,13 @@ public void append(UnfilteredRowIterator partition) } @Override - public Collection finish(boolean openResult) + public Collection finish(boolean openResult, StorageHandler storageHandler) { - setOpenResult(openResult); - for (ZeroCopySequentialWriter writer : componentWriters.values()) writer.finish(); + TOCComponent.updateTOC(descriptor, components()); + lifecycleNewTracker.trackNewWritten(this); return finished(); } @@ -141,20 +144,20 @@ public Collection finish(boolean openResult) public Collection finished() { if (finalReader == null) - finalReader = SSTableReader.open(owner().orElse(null), descriptor, components, metadata); + finalReader = SSTableReader.open(owner().orElse(null), descriptor, components(), metadata); return ImmutableList.of(finalReader); } @Override - public SSTableMultiWriter setOpenResult(boolean openResult) + public void openResult(StorageHandler storageHandler) { - return null; } @Override public long getBytesWritten() { + // TODO: these two may need fixing. return 0; } @@ -164,6 +167,12 @@ public long getOnDiskBytesWritten() return 0; } + @Override + public int getSegmentCount() + { + return 1; + } + @Override public TableId getTableId() { @@ -191,6 +200,8 @@ public void prepareToCommit() { for (ZeroCopySequentialWriter writer : componentWriters.values()) writer.prepareToCommit(); + + lifecycleNewTracker.trackNewWritten(this); } @Override @@ -203,7 +214,7 @@ public void close() public void writeComponent(Component component, DataInputPlus in, long size) throws ClosedChannelException { ZeroCopySequentialWriter writer = componentWriters.get(component.name); - logger.info("Writing component {} to {} length {}", component, writer.getPath(), prettyPrintMemory(size)); + logger.info("Writing component {} to {} length {}", component, writer.getFile(), prettyPrintMemory(size)); if (in instanceof AsyncStreamingInputPlus) write((AsyncStreamingInputPlus) in, size, writer); @@ -214,7 +225,7 @@ public void writeComponent(Component component, DataInputPlus in, long size) thr private void write(AsyncStreamingInputPlus in, long size, ZeroCopySequentialWriter writer) throws ClosedChannelException { - logger.info("Block Writing component to {} length {}", writer.getPath(), prettyPrintMemory(size)); + logger.info("Block Writing component to {} length {}", writer.getFile(), prettyPrintMemory(size)); try { @@ -234,7 +245,7 @@ private void write(AsyncStreamingInputPlus in, long size, ZeroCopySequentialWrit } catch (IOException e) { - throw new FSWriteError(e, writer.getPath()); + throw new FSWriteError(e, writer.getFile()); } } diff --git a/src/java/org/apache/cassandra/io/sstable/ScannerList.java b/src/java/org/apache/cassandra/io/sstable/ScannerList.java new file mode 100644 index 000000000000..00683382e917 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/ScannerList.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.google.common.base.Throwables; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; + +public class ScannerList implements AutoCloseable +{ + public final List scanners; + public ScannerList(List scanners) + { + this.scanners = scanners; + } + + public long getTotalBytesScanned() + { + long bytesScanned = 0L; + for (ISSTableScanner scanner : scanners) + bytesScanned += scanner.getBytesScanned(); + + return bytesScanned; + } + + public long getTotalCompressedSize() + { + long compressedSize = 0; + for (int i=0, isize=scanners.size(); i sstables, Collection> ranges) + { + ArrayList scanners = new ArrayList<>(); + try + { + for (SSTableReader sstable : sstables) + scanners.add(sstable.getScanner(ranges)); + return new ScannerList(scanners); + } + catch (Throwable t) + { + throw Throwables.propagate(ISSTableScanner.closeAllAndPropagate(scanners, t)); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/io/sstable/SequenceBasedSSTableId.java b/src/java/org/apache/cassandra/io/sstable/SequenceBasedSSTableId.java index acb91f8d7546..6007f62707f9 100644 --- a/src/java/org/apache/cassandra/io/sstable/SequenceBasedSSTableId.java +++ b/src/java/org/apache/cassandra/io/sstable/SequenceBasedSSTableId.java @@ -31,7 +31,7 @@ * Generation identifier based on sequence of integers. * This has been the standard implementation in C* since inception. */ -public class SequenceBasedSSTableId implements SSTableId, Comparable +public class SequenceBasedSSTableId implements SSTableId { public final int generation; diff --git a/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java b/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java index 99406dba31cf..7847de6430c7 100644 --- a/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java @@ -20,9 +20,9 @@ import java.util.Collection; import java.util.Collections; -import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.commitlog.IntervalSet; +import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.index.Index; @@ -49,9 +49,9 @@ public void append(UnfilteredRowIterator partition) writer.append(partition); } - public Collection finish(boolean openResult) + public Collection finish(boolean openResult, StorageHandler storageHandler) { - return Collections.singleton(writer.finish(openResult)); + return Collections.singleton(writer.finish(openResult, storageHandler)); } public Collection finished() @@ -59,10 +59,9 @@ public Collection finished() return Collections.singleton(writer.finished()); } - public SSTableMultiWriter setOpenResult(boolean openResult) + public void openResult(StorageHandler storageHandler) { - writer.setOpenResult(openResult); - return this; + writer.openResult(storageHandler); } public String getFilename() @@ -80,6 +79,11 @@ public long getOnDiskBytesWritten() return writer.getEstimatedOnDiskBytesWritten(); } + public int getSegmentCount() + { + return 1; + } + public TableId getTableId() { return writer.metadata().id; diff --git a/src/java/org/apache/cassandra/io/sstable/StorageHandler.java b/src/java/org/apache/cassandra/io/sstable/StorageHandler.java new file mode 100644 index 000000000000..dc8531ef4000 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/StorageHandler.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Set; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.lifecycle.Tracker; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.metadata.StatsMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Throwables; + +import static org.apache.cassandra.config.CassandraRelevantProperties.REMOTE_STORAGE_HANDLER_FACTORY; + +/** + * The handler of the storage of sstables, and possibly other files such as txn logs. + *

    + * If sstables are stored on the local disk, then this handler is a thin wrapper of {@link Directories.SSTableLister}, + * but for sstables stored remotely, for example on S3, then the handler may need to perform more + * work, such as selecting only part of the remote sstables available, or adding new ones when offline compaction + * has run. This behaviour can be implemented in a sub-class created from factory that can be set with {@link #remoteStorageHandlerFactory}. + *

    + */ +public abstract class StorageHandler +{ + private final static String remoteStorageHandlerFactory = REMOTE_STORAGE_HANDLER_FACTORY.getString(); + + private static class InstanceHolder + { + private static final StorageHandlerFactory FACTORY = maybeInitializeFactory(remoteStorageHandlerFactory); + } + + public enum ReloadReason + { + /** New nodes joined or left */ + TOPOLOGY_CHANGED(true), + /** Data was truncated */ + TRUNCATION(false), + /** SSTables might have been added or removed, regardless of a specific reason + * e.g. it could be compaction or flushing or regions being updated which caused + * new sstables to arrive */ + SSTABLES_CHANGED(false), + /** Data was replayed either from the commit log or a batch log */ + DATA_REPLAYED(true), + /** When repair task started */ + REPAIR(true), + /** A request over forced by users to reload. */ + USER_REQUESTED(true), + /** When region status changed */ + REGION_CHANGED(false), + /** When index is built */ + INDEX_BUILT(false), + /** New node restarted with existing on disk data */ + REPLACE(true), + /** Retry in case of failure, i.e. if a timeout occurred **/ + RETRY(false); + + /** When this is true, a reload operation will reload all sstables even those that could + * have been flushed by other nodes. */ + public final boolean loadFlushedSSTables; + + ReloadReason(boolean loadFlushedSSTables) + { + this.loadFlushedSSTables = loadFlushedSSTables; + } + } + + protected final SSTable.Owner owner; + protected final TableMetadataRef metadata; + protected final Directories directories; + protected final Tracker dataTracker; + + public StorageHandler(SSTable.Owner owner, TableMetadataRef metadata, Directories directories, Tracker dataTracker) + { + Preconditions.checkNotNull(directories, "Directories should not be null"); + + this.owner = owner; + this.metadata = metadata; + this.directories = directories; + this.dataTracker = dataTracker; + } + + /** + * @return true if the node is ready to serve data for this table. This means that the + * node is not bootstrapping and that no data may be missing, e.g. if sstables are + * being downloaded from remote storage or streamed from other nodes then isReady() + * would return false. Generally, user read queries should not succeed if this method + * returns false. + */ + public abstract boolean isReady(); + + /** + * Load the initial sstables into the tracker that was passed in to the constructor. + * + * @return the sstables that were loaded + */ + public abstract Collection loadInitialSSTables(); + + /** + * Reload any sstables that may have been created and not yet loaded. This is normally + * a no-op for the default local storage, but for remote storage implementations it + * signals that sstables need to be refreshed. + * + * @return the sstables that were loaded + */ + public abstract Collection reloadSSTables(ReloadReason reason); + + /** + * This method determines if the backing storage handler allows auto compaction + *

    + * @return true if auto compaction should be enabled + */ + public abstract boolean enableAutoCompaction(); + + /** + * This method will run the operation specified by the {@link Runnable} passed it + * whilst guaranteeing the guarantees that no sstable will be loaded or unloaded + * whilst this operation is running, by waiting for in-progress operation to complete. + * In other words, the storage handler must not change the status of the tracker, + * or try to load any sstable as long as this operation is executing. + * + * @param runnable the operation to execute. + */ + public abstract void runWithReloadingDisabled(Runnable runnable); + + /** + * Called when the CFS is unloaded, this needs to perform any cleanup. + */ + public abstract void unload(); + + /** + * Called during flush when we try to open a {@link SSTableReader} on the written sstable but reading it fails. + *

    + * The default implementation simply propagates the exception that failed the opening, but it can be overriden to + * try to recover and return a proper reader. This method provides as much information on the written sstable + * as to allow recovering. In the case of tiered storage, where some tier may be temporarily unresponsive, it + * can be used to provide a "shim" reader to avoid failing the flush and until sstable can be successfully reloaded. + * + * @param reason the {@link SSTableReader.OpenReason} for the opening that failed. + * @param descriptor the sstable descriptor. + * @param components the components that have been written. + * @param compressedSize the size on disk of the file that was writen. + * @param uncompressedSize the size of the uncompressed data that was written. If the sstable is not compressed, it + * will be the same as {@code compressedSize}. + * @param stats the metadata/statistics on the written sstable. + * @param firstKey the first key of the sstable. + * @param lastKey the last key of the sstable. + * @param estimatedKeys the number of keys written in the sstable (this is allowed to be an estimation, to mimick + * what {@link SSTableReader#estimatedKeys()} would return, but in practice it will be exact + * since we know how many keys we just wrote). + * @param throwable the exeption that failed the sstable opening. + * @return a reader for the sstable, if one can be created despite the initial error. If not, this message should + * simply rethrow {@code throwable}. + */ + public SSTableReader onOpeningWrittenSSTableFailure(SSTableReader.OpenReason reason, + Descriptor descriptor, + Set components, + long compressedSize, + long uncompressedSize, + StatsMetadata stats, + DecoratedKey firstKey, + DecoratedKey lastKey, + long estimatedKeys, + Throwable throwable) + { + // By default, just propagate the exception (not much we can do with local storage in particular). + throw Throwables.unchecked(throwable); + } + + public static StorageHandler create(SSTable.Owner owner, TableMetadataRef metadata, Directories directories, Tracker dataTracker) + { + return InstanceHolder.FACTORY.create(owner, metadata, directories, dataTracker); + } + + private static StorageHandlerFactory maybeInitializeFactory(String factory) + { + if (factory == null) + return StorageHandlerFactory.DEFAULT; + + Class factoryClass = FBUtilities.classForName(factory, "Remote storage handler factory"); + + try + { + return factoryClass.getConstructor().newInstance(); + } + catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) + { + throw new ConfigurationException("Unable to find correct constructor for " + factory, e); + } + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/StorageHandlerFactory.java b/src/java/org/apache/cassandra/io/sstable/StorageHandlerFactory.java new file mode 100644 index 000000000000..02e9c4ae4630 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/StorageHandlerFactory.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.lifecycle.Tracker; +import org.apache.cassandra.schema.TableMetadataRef; + +public interface StorageHandlerFactory +{ + StorageHandlerFactory DEFAULT = new StorageHandlerFactory() {}; + + default StorageHandler create(SSTable.Owner owner, TableMetadataRef metadata, Directories directories, Tracker dataTracker) + { + return new DefaultStorageHandler(owner, metadata, directories, dataTracker); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/ULIDBasedSSTableId.java b/src/java/org/apache/cassandra/io/sstable/ULIDBasedSSTableId.java new file mode 100644 index 000000000000..69aedc506d9c --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/ULIDBasedSSTableId.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import javax.annotation.Nonnull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import de.huxhorn.sulky.ulid.ULID; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.TimeUUID; + +/** + * SSTable generation identifiers that can be stored across nodes in one directory/bucket + * Uses the ULID based identifiers + */ +public final class ULIDBasedSSTableId implements SSTableId +{ + public static final int STRING_LEN = 26; + public static final int BYTES_LEN = 16; + + final ULID.Value ulid; + final TimeUUID approximateTimeUUID; + + public ULIDBasedSSTableId(ULID.Value ulid) + { + this.ulid = ulid; + this.approximateTimeUUID = approximateFromULID(ulid); + } + + public static TimeUUID approximateFromULID(ULID.Value ulid) + { + long rawTimestamp = TimeUUID.unixMillisToRawTimestamp(ulid.timestamp(), (10_000L * (ulid.getMostSignificantBits() & 0xFFFF)) >> 16); + return new TimeUUID(rawTimestamp, ulid.getLeastSignificantBits()); + } + + /** + * @return approximated {@link TimeUUID} based on ulid + */ + public TimeUUID getApproximateTimeUUID() + { + return approximateTimeUUID; + } + + @Override + public ByteBuffer asBytes() + { + return ByteBuffer.wrap(ulid.toBytes()); + } + + @Override + public String toString() + { + return ulid.toString(); + } + + @Override + public int compareTo(ULIDBasedSSTableId o) + { + if (o == null) + return 1; + else if (o == this) + return 0; + + return ulid.compareTo(o.ulid); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) + return false; + ULIDBasedSSTableId that = (ULIDBasedSSTableId) o; + return ulid.equals(that.ulid); + } + + @Override + public int hashCode() + { + return Objects.hash(ulid); + } + + public static class Builder implements SSTableId.Builder + { + private static final Pattern PATTERN = Pattern.compile("[0-9a-z]{26}", Pattern.CASE_INSENSITIVE); + + public static final Builder instance = new Builder(new ULID()); + + private final ULID ulid; + private static final AtomicReference prevRef = new AtomicReference<>(); + + @VisibleForTesting + Builder(ULID ulid) + { + this.ulid = ulid; + } + + /** + * Creates a new ULID based identifiers generator. + * + * @param existingIdentifiers not used by UUID based generator + */ + @Override + public Supplier generator(Stream existingIdentifiers) + { + return () -> { + ULID.Value prevVal; + ULID.Value newVal = null; + do + { + prevVal = prevRef.get(); + if (prevVal != null) + { + Optional newValOpt = ulid.nextStrictlyMonotonicValue(prevVal); + if (!newValOpt.isPresent()) + continue; + newVal = newValOpt.get(); + } + else + { + newVal = ulid.nextValue(); + } + } while (newVal == null || !prevRef.compareAndSet(prevVal, newVal)); + return new ULIDBasedSSTableId(newVal); + }; + } + + @Override + public boolean isUniqueIdentifier(String str) + { + return str != null && str.length() == STRING_LEN && PATTERN.matcher(str).matches(); + } + + @Override + public boolean isUniqueIdentifier(ByteBuffer bytes) + { + return bytes != null && bytes.remaining() == BYTES_LEN; + } + + @Override + public ULIDBasedSSTableId fromString(@Nonnull String s) throws IllegalArgumentException + { + Matcher m = PATTERN.matcher(s); + if (!m.matches()) + throw new IllegalArgumentException("String '" + s + "' is not a valid ULID based sstable identifier"); + + return new ULIDBasedSSTableId(ULID.parseULID(s)); + } + + @Override + public ULIDBasedSSTableId fromBytes(@Nonnull ByteBuffer bytes) throws IllegalArgumentException + { + Preconditions.checkArgument(bytes.remaining() == ULIDBasedSSTableId.BYTES_LEN, + "Buffer does not have a valid number of bytes remaining. Expecting: %s but was: %s", + ULIDBasedSSTableId.BYTES_LEN, bytes.remaining()); + + return new ULIDBasedSSTableId(ULID.fromBytes(ByteBufferUtil.getArray(bytes))); + } + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/UUIDBasedSSTableId.java b/src/java/org/apache/cassandra/io/sstable/UUIDBasedSSTableId.java index 9cec5879f62d..bab5b7868cce 100644 --- a/src/java/org/apache/cassandra/io/sstable/UUIDBasedSSTableId.java +++ b/src/java/org/apache/cassandra/io/sstable/UUIDBasedSSTableId.java @@ -36,12 +36,12 @@ *

    * Uses the UUID v1 identifiers */ -public final class UUIDBasedSSTableId implements SSTableId, Comparable +public final class UUIDBasedSSTableId implements SSTableId { public final static int STRING_LEN = 28; public final static int BYTES_LEN = 16; - private final TimeUUID uuid; + final TimeUUID uuid; private final String repr; public UUIDBasedSSTableId(TimeUUID uuid) diff --git a/src/java/org/apache/cassandra/io/sstable/UnsupportedSSTableException.java b/src/java/org/apache/cassandra/io/sstable/UnsupportedSSTableException.java new file mode 100644 index 000000000000..f4e7f58d683e --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/UnsupportedSSTableException.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable; + +import org.apache.cassandra.io.util.File; + +public class UnsupportedSSTableException extends CorruptSSTableException +{ + public UnsupportedSSTableException(String msg, Throwable cause, File path) + { + super(msg, cause, path); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/IteratorFromCursor.java b/src/java/org/apache/cassandra/io/sstable/compaction/IteratorFromCursor.java new file mode 100644 index 000000000000..c86aac660fb8 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/IteratorFromCursor.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.compaction; + +import java.util.NoSuchElementException; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringBoundary; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.RangeTombstoneBoundMarker; +import org.apache.cassandra.db.rows.RangeTombstoneBoundaryMarker; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.schema.TableMetadata; + +/** + * Wrapper that converts a cursor into an UnfilteredPartitionIterator for testing. + */ +public class IteratorFromCursor implements UnfilteredPartitionIterator +{ + final TableMetadata metadata; + final SSTableCursor cursor; + final Row.Builder rowBuilder; + + public IteratorFromCursor(TableMetadata metadata, SSTableCursor cursor) + { + this.metadata = metadata; + this.cursor = cursor; + this.rowBuilder = BTreeRow.sortedBuilder(); + } + + public TableMetadata metadata() + { + return metadata; + } + + public void close() + { + cursor.close(); + } + + public boolean hasNext() + { + return (advanceToNextPartition() == SSTableCursor.Type.PARTITION); + } + + private SSTableCursor.Type advanceToNextPartition() + { + SSTableCursor.Type type = cursor.type(); + while (true) + { + switch (type) + { + case PARTITION: + case EXHAUSTED: + return type; + default: + type = cursor.advance(); + } + } + } + + public UnfilteredRowIterator next() + { + SSTableCursor.Type type = advanceToNextPartition(); + if (type == SSTableCursor.Type.EXHAUSTED) + throw new NoSuchElementException(); + assert type == SSTableCursor.Type.PARTITION; + switch (cursor.advance()) + { + case PARTITION: + case EXHAUSTED: + return EmptyIterators.unfilteredRow(metadata, + cursor.partitionKey(), + false, + Rows.EMPTY_STATIC_ROW, + cursor.partitionLevelDeletion()); + case ROW: + case RANGE_TOMBSTONE: + return new RowIterator(); + default: + throw new AssertionError(); + } + } + + class RowIterator implements UnfilteredRowIterator + { + final DecoratedKey partitionKey; + final Row staticRow; + final DeletionTime partitionLevelDeletion; + + protected RowIterator() + { + this.partitionKey = cursor.partitionKey(); + this.partitionLevelDeletion = cursor.partitionLevelDeletion(); + if (Clustering.STATIC_CLUSTERING.equals(cursor.clusteringKey())) + { + staticRow = collectRow(cursor, rowBuilder); + } + else + { + staticRow = Rows.EMPTY_STATIC_ROW; + } + } + + public boolean hasNext() + { + return cursor.type().level == SSTableCursor.Type.ROW.level; + } + + public Unfiltered next() + { + switch (cursor.type()) + { + case ROW: + return collectRow(cursor, rowBuilder); + case RANGE_TOMBSTONE: + return collectRangeTombstoneMarker(cursor); + default: + throw new AssertionError(); + } + } + + public TableMetadata metadata() + { + return metadata; + } + + public boolean isReverseOrder() + { + return false; + } + + public RegularAndStaticColumns columns() + { + return metadata.regularAndStaticColumns(); + } + + public DecoratedKey partitionKey() + { + return partitionKey; + } + + public Row staticRow() + { + return staticRow; + } + + public DeletionTime partitionLevelDeletion() + { + return partitionLevelDeletion; + } + + public EncodingStats stats() + { + return EncodingStats.NO_STATS; + } + + public void close() + { + // Nothing to do on row close + } + } + + public static RangeTombstoneMarker collectRangeTombstoneMarker(SSTableCursor cursor) + { + ClusteringPrefix key = cursor.clusteringKey(); + DeletionTime previous = cursor.activeRangeDeletion(); + DeletionTime next = cursor.rowLevelDeletion(); + cursor.advance(); + switch (key.kind()) + { + case INCL_START_BOUND: + case EXCL_START_BOUND: + return new RangeTombstoneBoundMarker((ClusteringBound) key, next); + case INCL_END_BOUND: + case EXCL_END_BOUND: + return new RangeTombstoneBoundMarker((ClusteringBound) key, previous); + case EXCL_END_INCL_START_BOUNDARY: + case INCL_END_EXCL_START_BOUNDARY: + return new RangeTombstoneBoundaryMarker((ClusteringBoundary) key, previous, next); + default: + throw new AssertionError(); + } + } + + public static Row collectRow(SSTableCursor cursor, Row.Builder builder) + { + builder.newRow((Clustering) cursor.clusteringKey()); + builder.addPrimaryKeyLivenessInfo(cursor.clusteringKeyLivenessInfo()); + builder.addRowDeletion(Row.Deletion.regular(cursor.rowLevelDeletion())); + while (true) + { + switch (cursor.advance()) + { + case COMPLEX_COLUMN_CELL: + case SIMPLE_COLUMN: + builder.addCell(cursor.cell()); + break; + case COMPLEX_COLUMN: + // Note: we want to create complex deletion cell even if there is no deletion because this passes + // the correct version of the column metadata to the builder. + builder.addComplexDeletion(cursor.column(), cursor.complexColumnDeletion()); + break; + default: + return builder.build(); + } + } + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/PurgeCursor.java b/src/java/org/apache/cassandra/io/sstable/compaction/PurgeCursor.java new file mode 100644 index 000000000000..e50f492bd097 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/PurgeCursor.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.compaction; + +import java.util.function.LongPredicate; + +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionPurger; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.compaction.CompactionController; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.schema.ColumnMetadata; + +/** + * A wrapping cursor that applies tombstone purging, counterpart to + * {@link org.apache.cassandra.db.compaction.CompactionIterator.Purger}. Purging is the process of removing tombstones + * that do not need to be preserved, defined at minimum as: + * - there is no data in sstables not taking part in this compaction that may be covered by the tombstone, and + * - the gc_grace period, during which we protect tombstones to ensure that they are propagated to other replicas, has + * expired, and + * - we are compacting repaired sstables or the CFS does not request that only repaired tombstones are purged. + * Additionally, the purger converts expiring cells that have gone beyond their time-to-live to tombstones (deleting + * their data), and collects said tombstones if they are purgeable. + * + * Note that this may end up creating empty rows (i.e. headers with no deletion/timestamp and no cells) -- typically + * a SkipEmptyDataCursor would be required to apply on top of the result. + */ +public class PurgeCursor implements SSTableCursor, DeletionPurger +{ + private final SSTableCursor wrapped; + private final CompactionController controller; + private final long nowInSec; + private final long gcBefore; + private final boolean purgeTombstones; + private LongPredicate purgeEvaluator; + + private DeletionTime partitionLevelDeletion; + private DeletionTime activeRangeDeletion = DeletionTime.LIVE; + private DeletionTime rowLevelDeletion; + private DeletionTime complexColumnDeletion; + private LivenessInfo clusteringKeyLivenessInfo; + private ClusteringPrefix clusteringKey; + private Cell cell; + + public PurgeCursor(SSTableCursor wrapped, CompactionController controller, long nowInSec) + { + this.gcBefore = controller.gcBefore; + this.purgeTombstones = controller.compactingRepaired(); // this is also true if !cfs.onlyPurgeRepairedTombstones + this.wrapped = wrapped; + this.controller = controller; + this.nowInSec = nowInSec; + } + + @Override + public boolean shouldPurge(long timestamp, long localDeletionTime) + { + return purgeTombstones + && (localDeletionTime < gcBefore || controller.realm.shouldIgnoreGcGraceForKey(partitionKey())) + && getPurgeEvaluator().test(timestamp); + } + + /* + * Evaluates whether a tombstone with the given deletion timestamp can be purged. This is the minimum + * timestamp for any sstable containing `currentKey` outside of the set of sstables involved in this compaction. + * This is computed lazily on demand as we only need this if there is tombstones and this a bit expensive + * (see #8914). + */ + protected LongPredicate getPurgeEvaluator() + { + if (purgeEvaluator == null) + purgeEvaluator = controller.getPurgeEvaluator(partitionKey()); + + return purgeEvaluator; + } + + public Type advance() + { + if (wrapped.type() == Type.RANGE_TOMBSTONE) + activeRangeDeletion = rowLevelDeletion; + + while (true) + { + Type type = wrapped.advance(); + switch (type) + { + case EXHAUSTED: + return type; + case PARTITION: + purgeEvaluator = null; + partitionLevelDeletion = maybePurge(wrapped.partitionLevelDeletion()); + assert activeRangeDeletion == DeletionTime.LIVE; + return type; + case RANGE_TOMBSTONE: + rowLevelDeletion = maybePurge(wrapped.rowLevelDeletion()); + clusteringKey = maybePurge(wrapped.clusteringKey(), activeRangeDeletion, rowLevelDeletion); + if (clusteringKey != null) + return type; + else + break; // no bound remained, move on to next item + case ROW: + clusteringKey = wrapped.clusteringKey(); + rowLevelDeletion = maybePurge(wrapped.rowLevelDeletion()); + clusteringKeyLivenessInfo = maybePurge(wrapped.clusteringKeyLivenessInfo(), nowInSec); + return type; + case COMPLEX_COLUMN: + this.complexColumnDeletion = maybePurge(wrapped.complexColumnDeletion()); + return type; + case SIMPLE_COLUMN: + case COMPLEX_COLUMN_CELL: + // This also applies cells' time-to-live, converting expired cells to tombstones. + cell = wrapped.cell().purge(this, nowInSec); + if (cell != null) + return type; + break; // otherwise, skip this cell + default: + throw new AssertionError(); + } + } + } + + private DeletionTime maybePurge(DeletionTime deletionTime) + { + return shouldPurge(deletionTime) ? DeletionTime.LIVE : deletionTime; + } + + private LivenessInfo maybePurge(LivenessInfo liveness, long nowInSec) + { + return shouldPurge(liveness, nowInSec) ? LivenessInfo.EMPTY : liveness; + } + + private ClusteringPrefix maybePurge(ClusteringPrefix clusteringKey, DeletionTime deletionBefore, DeletionTime deletionAfter) + { + // We pass only the current deletion to the purger. This may mean close bounds' deletion time is + // already purged. + if (deletionBefore.isLive() && clusteringKey.kind().isEnd()) + { + // we need to strip the closing part of the tombstone + // if only a close bound, or the new deletion is also purged, do not return + if (clusteringKey.kind().isBound() || deletionAfter.isLive()) + return null; + + return ClusteringBound.create(clusteringKey.kind().openBoundOfBoundary(false), clusteringKey); + } + else if (clusteringKey.kind().isStart() && deletionAfter.isLive()) + { + // we need to strip the opening part of the tombstone + if (clusteringKey.kind().isBound()) + return null; // only an open bound whose time is now purged. Do not return. + assert !deletionBefore.isLive(); // If ending was also deleted, we would have gone through the path above. + return ClusteringBound.create(clusteringKey.kind().closeBoundOfBoundary(false), clusteringKey); + } + else // Nothing is dropped + return clusteringKey; + } + + public Type type() + { + return wrapped.type(); + } + + public DecoratedKey partitionKey() + { + return wrapped.partitionKey(); + } + + public DeletionTime partitionLevelDeletion() + { + return partitionLevelDeletion; + } + + public ClusteringPrefix clusteringKey() + { + return clusteringKey; + } + + public LivenessInfo clusteringKeyLivenessInfo() + { + return clusteringKeyLivenessInfo; + } + + public DeletionTime rowLevelDeletion() + { + return rowLevelDeletion; + } + + public DeletionTime activeRangeDeletion() + { + return activeRangeDeletion; + } + + public DeletionTime complexColumnDeletion() + { + return complexColumnDeletion; + } + + public ColumnMetadata column() + { + return wrapped.column(); + } + + public Cell cell() + { + return cell; + } + + public long bytesProcessed() + { + return wrapped.bytesProcessed(); + } + + public long bytesTotal() + { + return wrapped.bytesTotal(); + } + + public void close() + { + wrapped.close(); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/SSTableCursor.java b/src/java/org/apache/cassandra/io/sstable/compaction/SSTableCursor.java new file mode 100644 index 000000000000..1785e4ec542e --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/SSTableCursor.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.compaction; + +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.schema.ColumnMetadata; + +/** + * An sstable cursor is an iterator-like object that is used to enumerate and merge the content of sstables. + * It produces a stream of cells, broken up with row and partition boundaries -- doing this allows the merging to be + * done using a single container and merger instead of the hierarchy used in UnfilteredPartitionIterator- + * UnfilteredRowIterator-Row-ComplexColumn-Cell. + * + * There are two other important differences done to improve merging performance: + * - static rows are not special and are specified as normal rows with STATIC_CLUSTERING + * - cursors track the currently active range deletion + * + * More details about the design, functionality and performance of cursors can be found in the included cursors.md file. + */ +public interface SSTableCursor extends AutoCloseable +{ + /** + * Enumeration of the type of item at the current position: either a cell, a range tombstone marker, a header of + * some upper level of the hierarchy, or an end-of-stream. + * This combines information about the object seen with information about its level in the logical hierarchy + * (specified in the "level" field). The latter is key for comparing the position of two cursors which are iterated + * together in a merge: if a cursor is positioned on a lower level in the hierarchy than another, it is listing + * content in a group that is either exhausted or not opened in the other cursor, and in both cases the other + * cursor's position must have a bigger higher-level key. That is, a cursor with a smaller level is always before + * a cursor with a higher one (see {@link SSTableCursorMerger#mergeComparator}). + */ + enum Type + { + COMPLEX_COLUMN_CELL(0), + COMPLEX_COLUMN(1), + SIMPLE_COLUMN(1), + ROW(2), + RANGE_TOMBSTONE(2), + PARTITION(3), + EXHAUSTED(4), + UNINITIALIZED(-1); + + /** The actual level, used for comparisons (some types e.g. ROW and RANGE_TOMBSTONE share a level). */ + final int level; + + Type(int level) + { + this.level = level; + } + } + + /** + * Advance the cursor and return the level of the next element. The returned level is the same as what level() + * returns next. + * Any errors during read should be converted to CorruptSSTableException. + */ + Type advance(); + + /** + * The current level. UNINITIALIZED if iteration has not started, otherwise what the last advance() returned. + */ + Type type(); + + /** + * Current partition key. Only valid if a partition is in effect, i.e. if level() <= PARTITION. + */ + DecoratedKey partitionKey(); + + /** + * Partition level deletion. Only valid in a partition. + */ + DeletionTime partitionLevelDeletion(); + + /** + * Current clustering key. Only valid if positioned within/on a row/unfiltered, i.e. if level() <= ROW. + * For rows, this will be Clustering, and range tombstone markers will use prefix. + */ + ClusteringPrefix clusteringKey(); + + /** + * Liveness info for the current row's clustering key. Only valid within a row. + */ + LivenessInfo clusteringKeyLivenessInfo(); + + /** + * Row level deletion. Only valid within a row or on a range deletion. In the latter case, reports the new + * deletion being set. + */ + DeletionTime rowLevelDeletion(); + + /** + * Currently open range deletion. This tracks the last set range deletion, LIVE if none has been seen. + * If positioned on a range tombstone marker, this will report the _previous_ deletion. + */ + DeletionTime activeRangeDeletion(); + + /** + * Metadata for the current column. Only valid within/on a column, i.e. if + * level() <= SIMPLE/COMPLEX_COLUMN. + * In a merged complex column this may be different from cell().column() because it contains the most up-to-date + * version while individual cell sources will report their own. + */ + ColumnMetadata column(); + + /** + * Deletion of the current complex column. Only valid within/on a complex column, i.e. if + * level() == COMPLEX_COLUMN[_CELL]. + */ + DeletionTime complexColumnDeletion(); + + /** + * Current cell. This may be a column or a cell within a complex column. Only valid if positioned on a cell, + * which may be a simple column or a cell in a complex one, i.e. level() == SIMPLE_COLUMN or COMPLEX_COLUMN_CELL. + */ + Cell cell(); + + /** + * @return number of bytes processed. This should be used as progress indication together with bytesTotal. + */ + long bytesProcessed(); + /** + * @return number of bytes total. This should be used as progress indication together with bytesProcessed. + */ + long bytesTotal(); + + void close(); + + static SSTableCursor empty() + { + return new SSTableCursor() + { + boolean initialized = false; + + public Type advance() + { + initialized = true; + return Type.EXHAUSTED; + } + + public Type type() + { + return initialized ? Type.EXHAUSTED : Type.UNINITIALIZED; + } + + public DecoratedKey partitionKey() + { + return null; + } + + public DeletionTime partitionLevelDeletion() + { + return null; + } + + public ClusteringPrefix clusteringKey() + { + return null; + } + + public LivenessInfo clusteringKeyLivenessInfo() + { + return null; + } + + public DeletionTime rowLevelDeletion() + { + return null; + } + + public DeletionTime activeRangeDeletion() + { + return null; + } + + public DeletionTime complexColumnDeletion() + { + return null; + } + + public ColumnMetadata column() + { + return null; + } + + public Cell cell() + { + return null; + } + + public long bytesProcessed() + { + return 0; + } + + public long bytesTotal() + { + return 0; + } + + public void close() + { + // nothing + } + }; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/SSTableCursorMerger.java b/src/java/org/apache/cassandra/io/sstable/compaction/SSTableCursorMerger.java new file mode 100644 index 000000000000..89ff5d521003 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/SSTableCursorMerger.java @@ -0,0 +1,463 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.compaction; + +import java.util.Comparator; +import java.util.List; + +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringBoundary; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Cells; +import org.apache.cassandra.db.rows.ColumnMetadataVersionComparator; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.Merger; +import org.apache.cassandra.utils.Reducer; + +/** + * Cursor merger, which employs the Merger object to combine multiple cursors into a single stream. + * + * Most of the complexity of this class is in applying hierarchical deletions, of which there are four kinds: + * - partition-level deletion + * - range tombstones + * - row-level deletion + * - complex column deletion + * In addition to the values for each level (which we must report to the consumer), we also track combined + * - activeRangeDeletion (newest of partition-level and active range tombstone) + * - mergedRowDeletion (newest of active and row-level deletion) + * - mergedCellDeletion (newest of merged row and complex column deletion) + * and use the mergedCellDeletion to remove no-longer active cells. + */ +public class SSTableCursorMerger extends Reducer implements SSTableCursor +{ + private final Merger merger; + private final MergeListener mergeListener; + + private Type currentType; + private DecoratedKey currentPartitionKey; + private DeletionTime partitionLevelDeletion; + private ClusteringPrefix currentClusteringKey; + private DeletionTime rowLevelDeletion; + private LivenessInfo currentLivenessInfo; + private DeletionTime activeRangeDeletion; // uses partitionLevelDeletion as base instead of LIVE (which makes the logic a little simpler) + private DeletionTime mergedRowDeletion; // the deletion that applies to this row, merge(activeRangeDeletion, rowLevelDeletion) + private DeletionTime mergedCellDeletion; // the deletion that applies to this cell, merge(mergedRowDeletion, complexColumnDeletion) + + private ColumnMetadata columnMetadata; + private DeletionTime complexColumnDeletion; + + private Cell currentCell; + private int currentIndex; + private int numMergedVersions = 0; + + public SSTableCursorMerger(List cursors, TableMetadata metadata) + { + this(cursors, metadata, NO_MERGE_LISTENER); + } + + public SSTableCursorMerger(List cursors, TableMetadata metadata, MergeListener mergeListener) + { + assert !cursors.isEmpty(); + this.mergeListener = mergeListener; + this.merger = new Merger<>(cursors, + x -> { + x.advance(); + return x; + }, + SSTableCursor::close, + mergeComparator(metadata), + this); + this.currentType = Type.UNINITIALIZED; + } + + public Type type() + { + return currentType; + } + + public DecoratedKey partitionKey() + { + return currentPartitionKey; + } + + public DeletionTime partitionLevelDeletion() + { + return partitionLevelDeletion; + } + + public ClusteringPrefix clusteringKey() + { + return currentClusteringKey; + } + + public LivenessInfo clusteringKeyLivenessInfo() + { + return currentLivenessInfo; + } + + public DeletionTime rowLevelDeletion() + { + // Note: this is used for both range tombstone markers and rows. For the former we default to the + // partition-level deletion if no (newer) range tombstone is in effect, but we must report this case + // as LIVE to any consumer. + return rowLevelDeletion == partitionLevelDeletion ? DeletionTime.LIVE : rowLevelDeletion; + } + + public DeletionTime activeRangeDeletion() + { + return activeRangeDeletion == partitionLevelDeletion ? DeletionTime.LIVE : activeRangeDeletion; + } + + public DeletionTime complexColumnDeletion() + { + return complexColumnDeletion; + } + + public ColumnMetadata column() + { + return columnMetadata; + } + + public Cell cell() + { + return currentCell; + } + + public long bytesProcessed() + { + long bytesProcessed = 0; + for (SSTableCursor cursor : merger.allSources()) + bytesProcessed += cursor.bytesProcessed(); + return bytesProcessed; + } + + public long bytesTotal() + { + long bytesTotal = 0; + for (SSTableCursor cursor : merger.allSources()) + bytesTotal += cursor.bytesTotal(); + return bytesTotal; + } + + public void close() + { + merger.close(); // this will also close the inputs via the supplied onClose consumer + } + + public Type advance() + { + if (currentType == Type.RANGE_TOMBSTONE) + activeRangeDeletion = rowLevelDeletion; + + // We don't need to use hasNext because the streams finish on Level.EXHAUSTED. + // If the reducer returns null, get next entry. + while (merger.next() == null) {} + + return currentType; + } + + public void onKeyChange() + { + currentIndex = -1; + numMergedVersions = 0; + } + + public void reduce(int idx, SSTableCursor current) + { + ++numMergedVersions; + if (currentIndex == -1) + { + currentIndex = idx; + currentType = current.type(); + switch (currentType) + { + case COMPLEX_COLUMN: + columnMetadata = current.column(); + complexColumnDeletion = current.complexColumnDeletion(); + return; + case SIMPLE_COLUMN: + mergedCellDeletion = mergedRowDeletion; + case COMPLEX_COLUMN_CELL: + Cell cell = current.cell(); + if (!mergedCellDeletion.deletes(cell)) + currentCell = cell; + else + currentCell = null; + return; + case ROW: + currentClusteringKey = current.clusteringKey(); + currentLivenessInfo = current.clusteringKeyLivenessInfo(); + rowLevelDeletion = current.rowLevelDeletion(); + return; + case RANGE_TOMBSTONE: + currentClusteringKey = current.clusteringKey(); + rowLevelDeletion = current.rowLevelDeletion(); + return; + case PARTITION: + currentPartitionKey = current.partitionKey(); + partitionLevelDeletion = current.partitionLevelDeletion(); + return; + case EXHAUSTED: + default: + return; + } + } + else + { + switch (currentType) + { + case COMPLEX_COLUMN: + if ((ColumnMetadataVersionComparator.INSTANCE.compare(columnMetadata, current.column()) < 0)) + columnMetadata = current.column(); + if (current.complexColumnDeletion().supersedes(complexColumnDeletion)) + complexColumnDeletion = current.complexColumnDeletion(); + return; + case SIMPLE_COLUMN: + case COMPLEX_COLUMN_CELL: + Cell cell = current.cell(); + if (!mergedCellDeletion.deletes(cell)) + currentCell = currentCell != null + ? Cells.reconcile(currentCell, cell) + : cell; + return; + case ROW: + currentLivenessInfo = LivenessInfo.merge(currentLivenessInfo, current.clusteringKeyLivenessInfo()); + rowLevelDeletion = DeletionTime.merge(rowLevelDeletion, current.rowLevelDeletion()); + return; + case RANGE_TOMBSTONE: + rowLevelDeletion = DeletionTime.merge(rowLevelDeletion, current.rowLevelDeletion()); + return; + case PARTITION: + partitionLevelDeletion = DeletionTime.merge(partitionLevelDeletion, current.partitionLevelDeletion()); + return; + case EXHAUSTED: + default: + return; + } + } + } + + public SSTableCursor getReduced() + { + mergeListener.onItem(this, numMergedVersions); + + switch (currentType) + { + case COMPLEX_COLUMN_CELL: + if (currentCell == null) + return null; + break; + case COMPLEX_COLUMN: + if (complexColumnDeletion.supersedes(mergedRowDeletion)) + mergedCellDeletion = complexColumnDeletion; + else + { + complexColumnDeletion = DeletionTime.LIVE; + mergedCellDeletion = mergedRowDeletion; + } + break; + case SIMPLE_COLUMN: + if (currentCell == null) + return null; + columnMetadata = currentCell.column(); + break; + case ROW: + if (rowLevelDeletion.supersedes(activeRangeDeletion)) + mergedRowDeletion = rowLevelDeletion; + else + { + rowLevelDeletion = DeletionTime.LIVE; + mergedRowDeletion = activeRangeDeletion; + } + + if (mergedRowDeletion.deletes(currentLivenessInfo)) + currentLivenessInfo = LivenessInfo.EMPTY; + break; + case RANGE_TOMBSTONE: + if (!rowLevelDeletion.supersedes(activeRangeDeletion)) + { + // The new deletion is older than some of the active. We need to check if this is the end of the + // deletion that is currently active, or something else (some previous start or end that got itself + // deleted). To do this, check all active deletions for the sources that did not take part in this + // tombstone -- if something newer is still active, we should be using that deletion time instead. + + // For example, consider the merge of deletions over 1-6 with time 3, and 3-9 with time 2: + // at 1 we have active=LIVE row=3 other=LIVE and switch from LIVE to 3, i.e. return row=3 + // at 3 we have active=3 row=2 other=3 and switch from 3 to 3, i.e. issue nothing + // at 6 we have active=3 row=LIVE other=2 and switch from 3 to 2, i.e. return row=2 + // at 9 we have active=2 row=LIVE other=LIVE and switch from 2 to LIVE, i.e. return row=LIVE + // (where active stands for activeRangeDeletion, row - rowLevelDeletion and other - otherActive) + + if (activeRangeDeletion.equals(rowLevelDeletion)) + return null; // nothing to report, old and new are the same + DeletionTime otherActive = gatherDeletions(partitionLevelDeletion, merger.allGreaterValues()); + if (!rowLevelDeletion.supersedes(otherActive)) + { + if (activeRangeDeletion.equals(otherActive)) + return null; // this deletion is fully covered by other sources, nothing has changed + else + rowLevelDeletion = otherActive; // a newer deletion was closed, use the still valid from others + } + } // otherwise this is introducing a deletion that beats all and should be reported + + currentClusteringKey = adjustClusteringKeyForMarker(currentClusteringKey, + activeRangeDeletion != partitionLevelDeletion, + rowLevelDeletion != partitionLevelDeletion); + break; + case PARTITION: + activeRangeDeletion = partitionLevelDeletion; + break; + } + + return this; + } + + private DeletionTime gatherDeletions(DeletionTime initialValue, Iterable sources) + { + DeletionTime collected = initialValue; + for (SSTableCursor cursor : sources) + { + if (cursor.type() == Type.ROW || cursor.type() == Type.RANGE_TOMBSTONE) + collected = DeletionTime.merge(collected, cursor.activeRangeDeletion()); + } + return collected; + } + + /** + * Adjust the clustering key for the type of range tombstone marker needed. For the different marker types we have + * equal but separate clustering kinds. As a the new marker may be the result of combining multiple different ones, + * the type of marker we have to issue is not guaranteed to be the type of marker we got as input (for example, + * if one deletion ends at 2 exclusive but another starts at 2 inclusive, these two markers have equal clustering + * keys, but their merge is not the same as either, but a boundary of the exclusive-end-inclusive-start type). + */ + private static ClusteringPrefix adjustClusteringKeyForMarker(ClusteringPrefix clusteringKey, boolean activeBefore, boolean activeAfter) + { + if (activeBefore & activeAfter) + { + // We may need to upgrade from a pair of bounds to a boundary. + switch (clusteringKey.kind()) + { + case EXCL_END_INCL_START_BOUNDARY: + case INCL_END_EXCL_START_BOUNDARY: + return clusteringKey; // already a boundary, good + case INCL_START_BOUND: + case EXCL_END_BOUND: + return ClusteringBoundary.create(ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY, + clusteringKey); + case EXCL_START_BOUND: + case INCL_END_BOUND: + return ClusteringBoundary.create(ClusteringPrefix.Kind.INCL_END_EXCL_START_BOUNDARY, + clusteringKey); + default: + throw new AssertionError(); + } + } + else if (activeBefore) + { + // Partition-level deletion can cause one side of a boundary to be dropped. + // Note that because we can have many deletions clashing on the same position, we may have even picked up + // the clustering key from an overwritten open marker. + switch (clusteringKey.kind()) + { + case EXCL_END_BOUND: + case INCL_END_BOUND: + return clusteringKey; // already a close bound, good + case EXCL_END_INCL_START_BOUNDARY: + case INCL_START_BOUND: + return ClusteringBound.create(ClusteringPrefix.Kind.EXCL_END_BOUND, + clusteringKey); + case INCL_END_EXCL_START_BOUNDARY: + case EXCL_START_BOUND: + return ClusteringBound.create(ClusteringPrefix.Kind.INCL_END_BOUND, + clusteringKey); + default: + throw new AssertionError(); + } + } + else if (activeAfter) + { + switch (clusteringKey.kind()) + { + case EXCL_START_BOUND: + case INCL_START_BOUND: + return clusteringKey; // already an open bound, good + case EXCL_END_INCL_START_BOUNDARY: + case EXCL_END_BOUND: + return ClusteringBound.create(ClusteringPrefix.Kind.INCL_START_BOUND, + clusteringKey); + case INCL_END_EXCL_START_BOUNDARY: + case INCL_END_BOUND: + return ClusteringBound.create(ClusteringPrefix.Kind.EXCL_START_BOUND, + clusteringKey); + default: + throw new AssertionError(); + } + } + else + throw new AssertionError(); + } + + public interface MergeListener + { + void onItem(SSTableCursor cursor, int numVersions); + } + + static MergeListener NO_MERGE_LISTENER = (cursor, numVersions) -> {}; + + public static Comparator mergeComparator(TableMetadata metadata) + { + ClusteringComparator clusteringComparator = metadata.comparator; + return (a, b) -> + { + // Since we are advancing the sources together, a difference in levels means that either: + // - we compared partition/clustering/column keys before, they were different, and we did not advance one of + // the sources into the partition/row's content + // - one of the sources exhausted the partition/row/column's content and is now producing the next + // In either case the other source is still producing content for a partition/row/column that should be + // exhausted before we have to look at that key again. + if (a.type().level != b.type().level) + return Integer.compare(a.type().level, b.type().level); + + // If the sources are at the same level, we are guaranteed by the order and comparison above that all + // keys above this level match and thus we only need to compare the current. + switch (a.type()) + { + case COMPLEX_COLUMN_CELL: + return a.cell().column().cellPathComparator().compare(a.cell().path(), b.cell().path()); + case SIMPLE_COLUMN: + case COMPLEX_COLUMN: + return a.column().compareTo(b.column()); + case ROW: + case RANGE_TOMBSTONE: + return clusteringComparator.compare(a.clusteringKey(), b.clusteringKey()); + case PARTITION: + return a.partitionKey().compareTo(b.partitionKey()); + case EXHAUSTED: + default: + return 0; + } + }; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/SkipEmptyDataCursor.java b/src/java/org/apache/cassandra/io/sstable/compaction/SkipEmptyDataCursor.java new file mode 100644 index 000000000000..001cdf5bbcf9 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/SkipEmptyDataCursor.java @@ -0,0 +1,311 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.compaction; + +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.schema.ColumnMetadata; + +/** + * Wrapper that skips empty data. This is done by only reporting a header (complex column/row/partition) with no + * deletion or timestamp after valid content (cell, range tombstone, header with deletion/timestamp) is found; if no + * such content is found, the source is advanced to the next header without reporting the empty block to the consumer. + * + * In other words, in response to a single advance() call, this will take multiple steps in the source cursor until it + * reaches content, and when it does it reports the highest-level header it has had to go through to reach that content. + * On the next advance() call it will descend one level in the logical hierarchy and report the next header. This + * repeats until it reaches the level of the content. After reporting the level of the non-empty content, the next + * advance() call will repeat the procedure, starting with searching the input for non-empty content. + * + * Here's a sample evolution of input and output: + * [input] [output] + * UNINITIALIZED UNINITIALIZED + * PARTITION Pa + * ROW Raa + * SIMPLE_COLUMN Caa PARTITION Pa + * ROW Raa + * SIMPLE_COLUMN Caa + * ROW Rab + * SIMPLE_COLUMN Cab ROW Rab + * SIMPLE_COLUMN Cab + * PARTITION Pb + * ROW Rba + * PARTITION Pc + * RANGE_TOMBSTONE Tca PARTITION Pc + * RANGE_TOMBSTONE Tca + * RANGE_TOMBSTONE Tcb RANGE_TOMBSTONE Tcb + * EXHAUSTED EXHAUSTED + * + * To report a header it is sufficient to just issue its level and pass on the data from the wrapped cursor, because + * cursors always make the upper-level data (e.g. partition key) available while they advance within that level. + */ +public class SkipEmptyDataCursor implements SSTableCursor +{ + private final SSTableCursor wrapped; + private Type type = Type.UNINITIALIZED; + + public SkipEmptyDataCursor(SSTableCursor wrapped) + { + this.wrapped = wrapped; + } + + public Type advance() + { + Type current = wrapped.type(); + if (current != type) + return type = advanceOurLevel(current); + + type = wrapped.advance(); + while (true) + { + switch (type) + { + case EXHAUSTED: + return type; + case SIMPLE_COLUMN: + case COMPLEX_COLUMN_CELL: + case RANGE_TOMBSTONE: + // we are good, we have content + return type; + case COMPLEX_COLUMN: + if (!complexColumnDeletion().isLive()) + return type; // we have to report this column even without any cells + if (advanceToComplexCell()) + return type; // we found a cell and should now report the column + // There is no cell of this complex column. We may have advance to another column, row or partition. + break; + case ROW: + if (!rowLevelDeletion().isLive() || !clusteringKeyLivenessInfo().isEmpty()) + return type; // we have to report this row even without any columns + if (advanceToColumn()) + return type; // we have reached a cell, but we must still report the row + // There is no cell. We may have advanced to a new row or new partition. + break; + case PARTITION: + if (!partitionLevelDeletion().isLive()) + return type; // we have to report this partition even without any content + if (advanceToNonEmptyRow()) + return type; // We have reached a cell (or RT). We must report the partition, then the row. + // The wrapped cursor still returns their information (pkey, ckey etc.) + // No rows or all empty. We must have advanced to new partition or exhausted. + break; + default: + throw new AssertionError(); + } + type = wrapped.type(); + } + } + + /** + * Called to report content that has been advanced to, but not yet reported. This will descend one level in the + * logical hierarchy towards the target and return the resulting position. + * For example, on seeing a row header we first advance to a non-empty cell and report the header, and on the + * following advance() call report the cell we have advanced to. This method takes care of the latter part. + */ + private Type advanceOurLevel(Type target) + { + switch (type) + { + case PARTITION: + switch (target) + { + case COMPLEX_COLUMN_CELL: + case SIMPLE_COLUMN: + case COMPLEX_COLUMN: + return Type.ROW; + case ROW: + case RANGE_TOMBSTONE: + return target; + default: + throw new AssertionError(); + } + case ROW: + switch (target) + { + case COMPLEX_COLUMN_CELL: + return Type.COMPLEX_COLUMN; + case SIMPLE_COLUMN: + case COMPLEX_COLUMN: + return target; + default: + throw new AssertionError(); + } + case COMPLEX_COLUMN: + switch (target) + { + case COMPLEX_COLUMN_CELL: + return target; + default: + throw new AssertionError(); + } + + default: + // can't have any differences in any other case + throw new AssertionError(); + } + } + + private boolean advanceToComplexCell() + { + Type current = wrapped.advance(); + switch (current) + { + case COMPLEX_COLUMN_CELL: + return true; + case SIMPLE_COLUMN: + case COMPLEX_COLUMN: + case ROW: + case RANGE_TOMBSTONE: + case PARTITION: + case EXHAUSTED: + return false; + default: + throw new AssertionError(); + } + } + + private boolean advanceToColumn() + { + Type current = wrapped.advance(); + while (true) + { + switch (current) + { + case SIMPLE_COLUMN: + return true; + case COMPLEX_COLUMN: + if (!complexColumnDeletion().isLive()) + return true; + if (advanceToComplexCell()) + return true; + // There is no cell, skip this complex column. We may have a new column, or a new partition or row. + break; + case ROW: + case RANGE_TOMBSTONE: + case PARTITION: + case EXHAUSTED: + return false; + case COMPLEX_COLUMN_CELL: + // can't jump directly to cell without going through COMPLEX_COLUMN + default: + throw new AssertionError(); + } + current = wrapped.type(); + } + } + + private boolean advanceToNonEmptyRow() + { + Type current = wrapped.advance(); + while (true) + { + switch (current) + { + case RANGE_TOMBSTONE: + // we have content + return true; + case ROW: + if (!rowLevelDeletion().isLive() || !clusteringKeyLivenessInfo().isEmpty()) + return true; // we have to report this row even without any cells + if (advanceToColumn()) + return true; + // There is no column. We may have advanced to a new row or new partition. + break; + case PARTITION: + case EXHAUSTED: + return false; + case SIMPLE_COLUMN: + case COMPLEX_COLUMN: + case COMPLEX_COLUMN_CELL: + // Can't jump directly from partition to cell. + default: + throw new AssertionError(); + } + current = wrapped.type(); + } + } + + public Type type() + { + return type; + } + + public DecoratedKey partitionKey() + { + return wrapped.partitionKey(); + } + + public DeletionTime partitionLevelDeletion() + { + return wrapped.partitionLevelDeletion(); + } + + public ClusteringPrefix clusteringKey() + { + return wrapped.clusteringKey(); + } + + public LivenessInfo clusteringKeyLivenessInfo() + { + return wrapped.clusteringKeyLivenessInfo(); + } + + public DeletionTime rowLevelDeletion() + { + return wrapped.rowLevelDeletion(); + } + + public DeletionTime activeRangeDeletion() + { + return wrapped.activeRangeDeletion(); + } + + public DeletionTime complexColumnDeletion() + { + return wrapped.complexColumnDeletion(); + } + + public ColumnMetadata column() + { + return wrapped.column(); + } + + public Cell cell() + { + return wrapped.cell(); + } + + public long bytesProcessed() + { + return wrapped.bytesProcessed(); + } + + public long bytesTotal() + { + return wrapped.bytesTotal(); + } + + public void close() + { + wrapped.close(); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/SortedStringTableCursor.java b/src/java/org/apache/cassandra/io/sstable/compaction/SortedStringTableCursor.java new file mode 100644 index 000000000000..c36f460df47a --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/SortedStringTableCursor.java @@ -0,0 +1,489 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.compaction; + +import java.io.IOException; + +import com.google.common.util.concurrent.RateLimiter; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBoundOrBoundary; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.Columns; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.UnfilteredValidation; +import org.apache.cassandra.db.marshal.ByteArrayAccessor; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.UnfilteredSerializer; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.vint.VIntCoding; + +/** + * Cursor over sstable data files. + * Supports both BIG and BTI formats (which differ only in index and whose data file formats are identical). + */ +public class SortedStringTableCursor implements SSTableCursor +{ + private final RandomAccessReader dataFile; + private final SSTableReader sstable; + private final DeserializationHelper helper; + private final SerializationHeader header; + + private DecoratedKey partitionKey; + private ClusteringPrefix clusteringKey; + + private int rowFlags; + private int currentColumnIndex; + private int columnsToRead; + private ColumnMetadata[] columns; + private final ColumnMetadata[] columnsReusableArray; + private final ColumnMetadata[] regularColumns; + private final ColumnMetadata[] staticColumns; + private ColumnMetadata columnMetadata; + private int cellsLeftInColumn; + private Cell currentCell; + + private DeletionTime partitionLevelDeletion; + private DeletionTime activeRangeDeletion; + private DeletionTime rowLevelDeletion; + private LivenessInfo rowLivenessInfo; + private DeletionTime complexColumnDeletion; + + private final long endPosition; + private final long startPosition; + + private Type currentType = Type.UNINITIALIZED; + + public SortedStringTableCursor(SSTableReader sstable) + { + this(sstable, sstable.openDataReader(ReadPattern.SEQUENTIAL), null); + } + + public SortedStringTableCursor(SSTableReader sstable, Range range) + { + this(sstable, sstable.openDataReader(ReadPattern.SEQUENTIAL), range); + } + + public SortedStringTableCursor(SSTableReader sstable, Range tokenRange, RateLimiter limiter) + { + this(sstable, sstable.openDataReader(limiter, ReadPattern.SEQUENTIAL), tokenRange); + } + + public SortedStringTableCursor(SSTableReader sstable, RandomAccessReader dataFile, Range tokenRange) + { + try + { + this.dataFile = dataFile; + this.header = sstable.header; + this.helper = new DeserializationHelper(sstable.metadata(), sstable.descriptor.version.correspondingMessagingVersion(), DeserializationHelper.Flag.LOCAL); + this.sstable = sstable; + this.activeRangeDeletion = DeletionTime.LIVE; + this.regularColumns = toArray(header.columns(false)); + this.staticColumns = toArray(header.columns(true)); + this.columnsReusableArray = new ColumnMetadata[Math.max(regularColumns.length, staticColumns.length)]; + + SSTableReader.PartitionPositionBounds bounds = tokenRange == null ? sstable.getPositionsForFullRange() + : sstable.getPositionsForBounds(Range.makeRowRange(tokenRange)); + if (bounds != null) + { + this.startPosition = bounds.lowerPosition; + this.endPosition = bounds.upperPosition; + } + else + { + // The range is empty. Rather than fail, use 0/0 as bounds which will not return any data. + this.endPosition = this.startPosition = 0; + } + } + catch (Throwable t) + { + dataFile.close(); + throw t; + } + } + + private static ColumnMetadata[] toArray(Columns columns) + { + ColumnMetadata[] array = new ColumnMetadata[columns.size()]; + int index = 0; + for (ColumnMetadata cm : columns) + array[index++] = cm; + assert index == array.length; + return array; + } + + private boolean consumePartitionHeader() throws IOException + { + if (dataFile.getFilePointer() == endPosition) + { + currentType = Type.EXHAUSTED; + return false; + } + else if (dataFile.getFilePointer() > endPosition) + { + throw new IOException(String.format("Consuming partition header at %s, but end position is %s", + dataFile.getFilePointer(), endPosition)); + } + + currentType = Type.PARTITION; + partitionKey = sstable.decorateKey(ByteBufferUtil.readWithShortLength(dataFile)); + partitionLevelDeletion = DeletionTime.getSerializer(sstable.descriptor.version).deserialize(dataFile); + if (!partitionLevelDeletion.validate()) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, "partitionLevelDeletion="+partitionLevelDeletion.toString()); + if (!activeRangeDeletion.isLive()) + throw new IOException(String.format("Invalid active range tombstone at the beginning of partition %s: %s", + partitionKey.toString(), + activeRangeDeletion)); + rowLevelDeletion = null; + rowLivenessInfo = null; + complexColumnDeletion = null; + currentCell = null; + columnMetadata = null; + clusteringKey = null; + return true; + } + + private boolean consumeUnfilteredHeader() throws IOException + { + boolean haveData; + do + { + rowFlags = dataFile.readUnsignedByte(); + if (UnfilteredSerializer.isEndOfPartition(rowFlags)) + return false; + + int rowExtendedFlags = UnfilteredSerializer.readExtendedFlags(dataFile, rowFlags); + + switch (UnfilteredSerializer.kind(rowFlags)) + { + case ROW: + haveData = consumeRowHeader(rowExtendedFlags); + currentType = Type.ROW; + break; + case RANGE_TOMBSTONE_MARKER: + haveData = consumeRangeTombstoneMarker(); + currentType = Type.RANGE_TOMBSTONE; + break; + default: + throw new AssertionError(); + } + } + while (!haveData); + complexColumnDeletion = null; + currentCell = null; + columnMetadata = null; + return true; + } + + private boolean consumeRangeTombstoneMarker() throws IOException + { + ClusteringBoundOrBoundary bound = ClusteringBoundOrBoundary.serializer.deserialize(dataFile, helper.version, header.clusteringTypes()); + + if (header.isForSSTable()) + { + dataFile.readUnsignedVInt(); // marker size + dataFile.readUnsignedVInt(); // previous unfiltered size + } + + if (bound.kind().isEnd()) + { + DeletionTime endDeletion = header.readDeletionTime(dataFile); + if (!endDeletion.equals(activeRangeDeletion)) + throw new IOException(String.format("Invalid tombstone end boundary in partition %s, expected %s was %s", + partitionKey.toString(), + activeRangeDeletion, + endDeletion)); + } + + if (bound.kind().isStart()) + rowLevelDeletion = header.readDeletionTime(dataFile); + else + rowLevelDeletion = DeletionTime.LIVE; + + if (!rowLevelDeletion.validate()) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, "rowLevelDeletion="+rowLevelDeletion.toString()); + + clusteringKey = bound; + return true; + } + + /** + * @return false if empty + * @throws IOException + */ + private boolean consumeRowHeader(int rowExtendedFlags) throws IOException + { + boolean isStatic = UnfilteredSerializer.isStatic(rowExtendedFlags); + + if (isStatic) + { + if (!header.hasStatic()) + throw new IOException(String.format("Static row encountered in partition %s on table without static columns", + partitionKey.toString())); + + clusteringKey = Clustering.STATIC_CLUSTERING; + } + else + clusteringKey = Clustering.serializer.deserialize(dataFile, helper.version, header.clusteringTypes()); + + if (header.isForSSTable()) + { + dataFile.readUnsignedVInt(); // Skip row size + dataFile.readUnsignedVInt(); // previous unfiltered size + } + + boolean hasTimestamp = (rowFlags & UnfilteredSerializer.HAS_TIMESTAMP) != 0; + boolean hasTTL = (rowFlags & UnfilteredSerializer.HAS_TTL) != 0; + boolean hasDeletion = (rowFlags & UnfilteredSerializer.HAS_DELETION) != 0; + // shadowable deletions are obsolete + boolean hasAllColumns = (rowFlags & UnfilteredSerializer.HAS_ALL_COLUMNS) != 0; + ColumnMetadata[] headerColumns = isStatic ? staticColumns : regularColumns; + + if (hasTimestamp) + { + long timestamp = header.readTimestamp(dataFile); + int ttl = hasTTL ? header.readTTL(dataFile) : LivenessInfo.NO_TTL; + long localDeletionTime = hasTTL ? header.readLocalDeletionTime(dataFile) : LivenessInfo.NO_EXPIRATION_TIME; + localDeletionTime = Cell.decodeLocalDeletionTime(localDeletionTime, ttl, helper); + rowLivenessInfo = LivenessInfo.withExpirationTime(timestamp, ttl, localDeletionTime); + if (rowLivenessInfo.isExpiring() && (rowLivenessInfo.ttl() < 0 || rowLivenessInfo.localExpirationTime() < 0)) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, "rowLivenessInfo="+rowLivenessInfo.toString()); + } + else + rowLivenessInfo = LivenessInfo.EMPTY; + + if (hasDeletion) + { + rowLevelDeletion = header.readDeletionTime(dataFile); + if (!rowLevelDeletion.validate()) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, "rowLevelDeletion="+rowLevelDeletion.toString()); + } + else + rowLevelDeletion = DeletionTime.LIVE; + + if (hasAllColumns) + { + columns = headerColumns; + columnsToRead = headerColumns.length; + } + else + { + columns = columnsReusableArray; + columnsToRead = Columns.serializer.deserializeSubset(headerColumns, dataFile, columns); + } + + if (!hasTimestamp && !hasDeletion && columnsToRead == 0) + return false; + + this.cellsLeftInColumn = 0; + this.currentColumnIndex = 0; + return true; + } + + public boolean consumeColumn() throws IOException + { + while (true) + { + if (cellsLeftInColumn == 0) + { + if (currentColumnIndex == columnsToRead) + return false; + + columnMetadata = columns[currentColumnIndex++]; + assert helper.includes(columnMetadata); // we are fetching all columns + if (columnMetadata.isComplex()) + { + helper.startOfComplexColumn(columnMetadata); + DeletionTime complexDeletion = DeletionTime.LIVE; + if ((rowFlags & UnfilteredSerializer.HAS_COMPLEX_DELETION) != 0) + { + complexDeletion = header.readDeletionTime(dataFile); + if (!complexDeletion.validate()) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, + "complexColumnDeletion="+complexDeletion.toString()+" column="+columnMetadata.name); + if (helper.isDroppedComplexDeletion(complexDeletion)) + complexDeletion = DeletionTime.LIVE; + } + + cellsLeftInColumn = (int) dataFile.readUnsignedVInt(); + + currentType = Type.COMPLEX_COLUMN; + complexColumnDeletion = complexDeletion; + return true; + // not issuing helper.endOfComplexColumn, but that should be okay + } + else + { + currentType = Type.SIMPLE_COLUMN; + Cell cell = Cell.serializer.deserialize(dataFile, rowLivenessInfo, columnMetadata, header, helper, ByteArrayAccessor.instance); + if (cell.hasInvalidDeletions()) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, cell.toString()); + if (!helper.isDropped(cell, false)) + { + currentCell = cell; + return true; + } + } + } + } + } + + public boolean consumeComplexCell() throws IOException + { + while (cellsLeftInColumn > 0) + { + --cellsLeftInColumn; + Cell cell = Cell.serializer.deserialize(dataFile, rowLivenessInfo, columnMetadata, header, helper, ByteArrayAccessor.instance); + if (cell.hasInvalidDeletions()) + UnfilteredValidation.handleInvalid(sstable.metadata(), partitionKey, sstable, cell.toString()); + if (!helper.isDropped(cell, true)) + { + currentType = Type.COMPLEX_COLUMN_CELL; + currentCell = cell; + return true; + } + } + return false; + } + + public Type advance() + { + if (currentType == Type.RANGE_TOMBSTONE) + activeRangeDeletion = rowLevelDeletion; + + try + { + switch (currentType) + { + case EXHAUSTED: + throw new IllegalStateException("Cursor advanced after exhaustion."); + case COMPLEX_COLUMN_CELL: + case COMPLEX_COLUMN: + if (consumeComplexCell()) + return currentType; + // else fall through + case SIMPLE_COLUMN: + case ROW: + if (consumeColumn()) + return currentType; + // else fall through + case RANGE_TOMBSTONE: + case PARTITION: + if (consumeUnfilteredHeader()) + return currentType; + + consumePartitionHeader(); + return currentType; + case UNINITIALIZED: + dataFile.seek(this.startPosition); + consumePartitionHeader(); + return currentType; + default: + throw new AssertionError(); + } + } + catch (CorruptSSTableException e) + { + sstable.markSuspect(); + throw e; + } + catch (IOException | IndexOutOfBoundsException | VIntCoding.VIntOutOfRangeException | AssertionError e) + { + sstable.markSuspect(); + throw new CorruptSSTableException(e, dataFile.getFile().path()); + } + } + + public Type type() + { + return currentType; + } + + public DecoratedKey partitionKey() + { + return partitionKey; + } + + public DeletionTime partitionLevelDeletion() + { + return partitionLevelDeletion; + } + + public ClusteringPrefix clusteringKey() + { + return clusteringKey; + } + + public LivenessInfo clusteringKeyLivenessInfo() + { + return rowLivenessInfo; + } + + public DeletionTime rowLevelDeletion() + { + return rowLevelDeletion; + } + + public DeletionTime activeRangeDeletion() + { + return activeRangeDeletion; + } + + public DeletionTime complexColumnDeletion() + { + return complexColumnDeletion; + } + + public ColumnMetadata column() + { + return columnMetadata; + } + + public Cell cell() + { + return currentCell; + } + + public long bytesProcessed() + { + return dataFile.getFilePointer() - startPosition; + } + + public long bytesTotal() + { + return endPosition - startPosition; + } + + public void close() + { + FileUtils.closeQuietly(dataFile); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/compaction/cursors.md b/src/java/org/apache/cassandra/io/sstable/compaction/cursors.md new file mode 100644 index 000000000000..d97c8be26646 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/compaction/cursors.md @@ -0,0 +1,273 @@ + + +# Compaction process optimization + +This is a refactoring of the sstable content iteration mechanisms to strip much of the avoidable overhead. The main idea +is the restructuring of the iteration into one stream of cells mixed with column, row and partition headers, which +avoids having to recreate the `Row`s and `UnfilteredRowIterator`s together with all the infrastructure needed to merge +them. + +The data are exposed via a stateful `SSTableCursor` object, which starts uninitialized and can be `advance()`-d to +obtain items of the stream. The cursor exposes the state of iteration, including: + +- the current partition key +- the current clustering key +- the current column +- the current cell (which may include a cell path) + +together with an indication of the "level" in the sstable hierarchy that the current position is at. On a valid +position, the latter can be `PARTITION`, `ROW`/`RANGE_TOMBSTONE`, `SIMPLE/COMPLEX_COLUMN` or `COMPLEX_COLUMN_CELL`, +indicating that advancing has ended up, respectively, at the start of a new partition, new row, new range tombstone +marker, new simple column cell, the start of a complex column, or a cell in the complex column. If on an upper level of +the hierarchy, the details about features of the lower levels (e.g. clustering key on `PARTITION`, cell on `ROW` +or `PARTITION`) are invalid, but the information for all higher levels (e.g. partition key on `ROW`) is present. + +Cursors' state (i.e. position in the stream) can be compared by comparing the keys in order. More importantly, when we +iterate several cursors in order while merging, we can observe that a cursor positioned on a higher level of the +hierarchy must have a position later than the cursors that are still producing items at a lower level and thus +comparisons can be done by only comparing the level and then (if levels match), the key at the common level. This is +crucial for the efficiency of merging. + +Note: we also considered using a "level change" flag instead of stopping on headers (i.e. instead of advancing through +`PARTITION(pk1), ROW(ck1), SIMPLE_COLUMN(c1), ROW(ck2), SIMPLE_COLUMN(c2)` to +list `PARTITION(pk1, ck1, c1), ROW(ck2, c2)`). While it does look like with this we can still efficiently compare via +the level, we need to somehow consume the level advance on matching comparison, which is non-trivial and error-prone. +For example, consider merging: + +- `PARTITION(pk1, ck1, c1)`, `ROW(ck3, c2)` +- `PARTITION(pk1, ck2, c3)` + +Cell, row and partition deletions are directly reported by methods that return the relevant deletion times. Range +tombstone markers are reported on the rows level with the deletion time they switch to by the `rowLevelDeletion()` +method (i.e. the open time if it's an open bound or a boundary, or LIVE if it's a close bound). The currently active +deletion time is also tracked and reported through the `activeRangeDeletion()` method; note that if the stream is +positioned or a range tombstone marker, it reports the deletion active _before_ it, so that both deletions are +available (useful both for reconstructing range tombstone markers on write, and for merging, where we need to know the +active range deletion before the position on the sources that are positioned later in the stream). The merge cursor +takes care of applying the active deletion (the newest of complex-column, range, row- and partition-level deletion) to +the data it processes to remove any deleted data and tombstones. + +There are a couple of further differences with iterators: + +- Static rows are listed as the first row in a partition, only if they are not empty — separating them is only + useful for reverse iteration which cursors don't aim to support. +- Cursors only iterate on data files, which avoids walking the partition index. This means less resilience to error, but + in compactions this is not a problem as we abort on error. +- "Shadowable row deletions" (a deprecated feature which is no longer in use) are not reported as such. + +Cursors don't currently support all of the functionality of merging over sstable iterators. For details of the +limitations, see the TODO list below. + +Beyond the above, the implementation is straight-forward: + +- `SSTableCursor` is the main abstraction, a cursor over sstables. +- `SortedStringTableCursor` is the main implementation of `SSTableCursor` which walks over an sstable data file and + extracts its data stream. To do this it reimplements the functionality of the deserializers for parsing partition, row + and column headers and relies on an instance of the deserializer to read cells. Supports both BIG and BTI formats + (which differ only in index and whose data file formats are identical). +- `SSTableCursorMerger` implements merging several `SSTableCursor`s into one. This is implemented via an extracted merge + core from `MergeIterator` configured to work on cursors. +- `PurgeCursor` implements removal of collectable tombstones. +- `SkipEmptyDataCursor` delays the reporting of headers until content is found, in order to avoid creating empty complex + columns, rows or partitions in the compacted view. +- `CompactionCursor` sets up a merger over multiple sstable cursors for compaction and implements writing a cursor into + a new sstable. Note: we currently still create an in-memory row to be able to send it to the serializer for writing. +- `CompactionTask.CompactionOperationCursor` is a cursor counterpart of `CompactionTask.CompactionOperationIterator`. + The former is chosen if cursors can support the compaction, i.e. if it is known that a secondary index is not in use, + that the compaction strategy supports cursors (initially we only intend to release this + for `UnifiedCompactionStrategy`; even afterwards, `TieredCompactionStrategy` would need special support), and that a + garbage collection compaction is not requested. + +Additionally, + +- `IteratorFromCursor` converts a cursor into an unfiltered partition iterator for testing and can also be used as a + reference of the differences. + +### Further work + +- Writing without going through `Row`, i.e. sending individual cells to the writer instead of the in-memory `Row` + objects, using a refactoring similar to what is currently done to write rows instead of partitions, should improve + performance further. + +- Secondary indexes currently can't use cursors, because we do not construct the right input for their listeners. Doing + this may require reconstructing rows for the merge, which is something we definitely do not want to do in the normal + case. It would probably be better to find the specific needs of SAI and support only them, and leave legacy / custom + indexes to use iterators (note: since TPC is not going to be developed further, we no longer plan to fully replace the + iterators with this). + +- Garbage collection compaction, i.e. compaction using tombstones from newer non-participating sstables to delete as + much as possible from the compacted sstables, could be implemented for cursors too. + +- If we are going to support all compaction strategies, it may be beneficial to restore levelled compaction's sstable + concatenation scanner. However, this will only save one comparison per partition, so I doubt it's really worth doing. + +## Benchmark results collected during development (most recent results first) + +Perhaps most relevant at this time are the differences between `iterateThroughCompactionCursor` vs +`iterateThroughCompactionIterator` (sending the compaction of two sstables to a null writer). Other meaninful +comparisons are `iterateThroughCursor` vs `iterateThroughTableScanner` +(iterating the content of a single sstable without merging) and `iterateThroughMergeCursor` vs +`iterateThroughMergeIterator` (iterating the merge of two sstables, similar to `iterateThroughCompactionCursor` +but without constructing in-memory rows). + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 818.816 ± 46.403 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursorWithLimiter 0 1 false DEFAULT 0.3 10 2 avgt 10 841.232 ± 41.720 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1731.936 ± 72.224 ms/op +CompactionBreakdownBenchmark.iterateThroughCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 480.876 ± 45.553 ms/op +CompactionBreakdownBenchmark.iterateThroughCursorToIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 602.826 ± 42.933 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 719.327 ± 41.238 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1406.228 ± 74.427 ms/op +CompactionBreakdownBenchmark.iterateThroughPartitionIndexIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 340.876 ± 27.913 ms/op +CompactionBreakdownBenchmark.iterateThroughTableScanner 0 1 false DEFAULT 0.3 10 2 avgt 10 1039.944 ± 89.224 ms/op +``` + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 812.300 ± 35.196 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1799.127 ± 100.290 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false BLOB_CLUSTER_KEY 0.3 10 2 avgt 10 874.638 ± 46.639 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false BLOB_CLUSTER_KEY 0.3 10 2 avgt 10 1813.990 ± 89.474 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false BLOB_VALUE 0.3 10 2 avgt 10 850.173 ± 37.608 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false BLOB_VALUE 0.3 10 2 avgt 10 1773.747 ± 82.984 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false MANY_CLUSTER_KEYS 0.3 10 2 avgt 10 1501.582 ± 91.084 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false MANY_CLUSTER_KEYS 0.3 10 2 avgt 10 2470.640 ± 79.072 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false MANY_FIELDS 0.3 10 2 avgt 10 2643.602 ± 85.875 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false MANY_FIELDS 0.3 10 2 avgt 10 3095.176 ± 71.408 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false WIDE_PARTITIONS 0.3 10 2 avgt 10 524.839 ± 19.716 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false WIDE_PARTITIONS 0.3 10 2 avgt 10 564.349 ± 20.299 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false COMPLEX_COLUMNS_INSERT 0.3 10 2 avgt 10 1735.086 ± 92.367 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false COMPLEX_COLUMNS_INSERT 0.3 10 2 avgt 10 2730.369 ± 93.184 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false COMPLEX_COLUMNS_UPDATE_SET 0.3 10 2 avgt 10 1691.803 ± 83.824 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false COMPLEX_COLUMNS_UPDATE_SET 0.3 10 2 avgt 10 2671.245 ± 90.731 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false COMPLEX_COLUMNS_UPDATE_ADD 0.3 10 2 avgt 10 1541.798 ± 96.077 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false COMPLEX_COLUMNS_UPDATE_ADD 0.3 10 2 avgt 10 2649.346 ± 101.025 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false TOMBSTONES 0.3 10 2 avgt 10 971.576 ± 76.193 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false TOMBSTONES 0.3 10 2 avgt 10 1908.025 ± 80.601 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false TOMBSTONES_WIDE 0.3 10 2 avgt 10 594.306 ± 11.245 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false TOMBSTONES_WIDE 0.3 10 2 avgt 10 701.378 ± 15.859 ms/op +``` + +``` +Benchmark (compaction) (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBenchmark.compactSSTables SizeTieredCompactionStrategy 0 1 false DEFAULT 0.3 10 2 avgt 10 4151.567 ± 393.045 ms/op +CompactionBenchmark.compactSSTables UnifiedCompactionStrategy 0 1 false DEFAULT 0.3 10 2 avgt 10 3097.753 ± 90.189 ms/op +``` + +With tombstone and purging support, no row reconstructing in `iterateThroughCompactionCursor`. + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBenchmark.compactSSTables 0 1 false DEFAULT 0.3 10 2 avgt 10 3275.471 ± 99.962 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 726.345 ± 53.326 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursorWithLimiter 0 1 false DEFAULT 0.3 10 2 avgt 10 705.488 ± 40.847 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1820.006 ± 92.980 ms/op +CompactionBreakdownBenchmark.iterateThroughCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 463.956 ± 32.461 ms/op +CompactionBreakdownBenchmark.iterateThroughCursorToIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 595.363 ± 42.723 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 699.307 ± 44.041 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1434.870 ± 84.047 ms/op +CompactionBreakdownBenchmark.iterateThroughPartitionIndexIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 347.405 ± 25.364 ms/op +CompactionBreakdownBenchmark.iterateThroughTableScanner 0 1 false DEFAULT 0.3 10 2 avgt 10 1014.317 ± 90.495 ms/op +CompactionBreakdownBenchmark.scannerToCompactionWriter 0 1 false DEFAULT 0.3 10 2 avgt 10 2000.747 ± 76.360 ms/op +``` + +With tombstone and purging support + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 701.806 ± 42.179 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursorWithLimiter 0 1 false DEFAULT 0.3 10 2 avgt 10 710.938 ± 41.999 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1777.159 ± 87.460 ms/op +CompactionBreakdownBenchmark.iterateThroughCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 457.567 ± 27.544 ms/op +CompactionBreakdownBenchmark.iterateThroughCursorToIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 592.213 ± 36.001 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 747.161 ± 52.323 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1500.166 ± 87.268 ms/op +CompactionBreakdownBenchmark.iterateThroughPartitionIndexIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 345.971 ± 24.798 ms/op +CompactionBreakdownBenchmark.iterateThroughTableScanner 0 1 false DEFAULT 0.3 10 2 avgt 10 1040.594 ± 82.862 ms/op +CompactionBreakdownBenchmark.scannerToCompactionWriter 0 1 false DEFAULT 0.3 10 2 avgt 10 1968.898 ± 98.314 ms/op +CompactionBenchmark.compactSSTables 0 1 false DEFAULT 0.3 10 2 avgt 10 3072.202 ± 104.127 ms/op +``` + +Cell-level stream, deserialized cells, recombined rows on write + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 799.025 ± 26.943 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursorWithLimiter 0 1 false DEFAULT 0.3 10 2 avgt 10 795.218 ± 17.373 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1996.695 ± 50.663 ms/op +CompactionBreakdownBenchmark.iterateThroughCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 488.837 ± 9.936 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 788.899 ± 20.713 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1698.324 ± 199.704 ms/op +CompactionBreakdownBenchmark.iterateThroughPartitionIndexIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 394.046 ± 11.057 ms/op +CompactionBreakdownBenchmark.iterateThroughTableScanner 0 1 false DEFAULT 0.3 10 2 avgt 10 1250.365 ± 45.029 ms/op +``` + +With direct write, row stream + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 960.035 ± 21.594 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursorWithLimiter 0 1 false DEFAULT 0.3 10 2 avgt 10 966.950 ± 43.067 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 2062.935 ± 30.653 ms/op +CompactionBenchmark.compactSSTables 0 1 false DEFAULT 0.3 10 2 avgt 10 3294.247 ± 101.186 ms/op +``` + +With progress indication and rate limiting, row stream, converted to iterator + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 964.176 ± 19.588 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionCursorWithLimiter 0 1 false DEFAULT 0.3 10 2 avgt 10 1000.995 ± 28.157 ms/op Note: has synchronization, uncontended in this test +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 2043.687 ± 35.059 ms/op +CompactionBenchmark.compactSSTables 0 1 false DEFAULT 0.3 10 2 avgt 10 3575.346 ± 94.917 ms/op +``` + +With CompactionCursor, merge through cursor, deserialized rows, no progress/deletions/indexes + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 944.843 ± 13.718 ms/op +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 2070.773 ± 43.277 ms/op +CompactionBenchmark.compactSSTables 0 1 false DEFAULT 0.3 10 2 avgt 9 3329.419 ± 81.107 ms/op +``` + +Initial implementation, row stream, converted to iterator + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCompactionIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 2064.699 ± 27.296 ms/op +CompactionBreakdownBenchmark.iterateThroughCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 597.398 ± 20.261 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 949.216 ± 13.484 ms/op +CompactionBreakdownBenchmark.iterateThroughMergeIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 1619.175 ± 39.231 ms/op +CompactionBreakdownBenchmark.iterateThroughPartitionIndexIterator 0 1 false DEFAULT 0.3 10 2 avgt 10 413.512 ± 100.120 ms/op +CompactionBreakdownBenchmark.iterateThroughTableScanner 0 1 false DEFAULT 0.3 10 2 avgt 10 1167.989 ± 37.205 ms/op +CompactionBreakdownBenchmark.scannerToCompctionWriter 0 1 false DEFAULT 0.3 10 2 avgt 9 2516.367 ± 82.761 ms/op +CompactionBenchmark.compactSSTables 0 1 false DEFAULT 0.3 10 2 avgt 9 4622.173 ± 157.001 ms/op +``` + +For information only -- skipping row body + +``` +Benchmark (compactionMbSecThrottle) (compactors) (compression) (dataBuilder) (overlapRatio) (size) (sstableCount) Mode Cnt Score Error Units +CompactionBreakdownBenchmark.iterateThroughCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 402.963 ± 11.514 ms/op - 200 +CompactionBreakdownBenchmark.iterateThroughMergeCursor 0 1 false DEFAULT 0.3 10 2 avgt 10 668.527 ± 13.703 ms/op - 300 +CompactionBreakdownBenchmark.iterateThroughTableScanner 0 1 false DEFAULT 0.3 10 2 avgt 10 942.351 ± 24.458 ms/op - 200 +``` + diff --git a/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterMetrics.java b/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterMetrics.java index ee5fc3c111fc..4357732bed36 100644 --- a/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterMetrics.java +++ b/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterMetrics.java @@ -65,11 +65,56 @@ protected R map(SSTableReader r) /** * Off heap memory used by bloom filter */ - public final GaugeProvider bloomFilterOffHeapMemoryUsed = newGaugeProvider("BloomFilterOffHeapMemoryUsed", + public final GaugeProvider bloomFilterOffHeapMemoryUsed = bloomFilterOffHeapMemoryUsedAwareCFSGaugeProvider("BloomFilterOffHeapMemoryUsed", 0L, SSTableReaderWithFilter::getFilterOffHeapSize, Long::sum); + public final GaugeProvider approximateBloomFilterOffHeapMemoryUsed = newGaugeProvider("ApproximateBloomFilterOffHeapMemoryUsed", + 0L, + SSTableReaderWithFilter::getApproximateBloomFilterMemorySize, + Long::sum); + + public final GaugeProvider loadedBloomFilter = newGaugeProvider("LoadedBloomFilter", + 0L, + r -> r.isBloomFilterLoaded() ? 1L : 0L, + Long::sum); + + public final GaugeProvider lazyBloomFilter = newGaugeProvider("LazyBloomFilter", + 0L, + r -> r.isLazyBloomFilter() ? 1L : 0L, + Long::sum); + + public final GaugeProvider passThroughBloomFilter = newGaugeProvider("PassThroughBloomFilter", + 0L, + r -> r.isPassThroughBloomFilter() ? 1L : 0L, + Long::sum); + + public final GaugeProvider lazyBloomFilterByRequestRate = newGaugeProvider("LazyBloomFilterByRequestRate", + 0L, + r -> r.isLazyBloomFilterByRequestRateCriteria() ? 1L : 0L, + Long::sum); + + public final GaugeProvider lazyBloomFilterByRequestCount = newGaugeProvider("LazyBloomFilterByRequestCount", + 0L, + r -> r.isLazyBloomFilterByRequestCountCriteria() ? 1L : 0L, + Long::sum); + + public final GaugeProvider lazyBloomFilterHits = newGaugeProvider("LazyBloomFilterHits", + 0L, + r -> r.getFilterTracker().getLazyBloomFilterHitCount(), + Long::sum); + + public final GaugeProvider loadedBloomFilterHits = newGaugeProvider("LoadedBloomFilterHits", + 0L, + r -> r.getFilterTracker().getLoadedBloomFilterHitCount(), + Long::sum); + + public final GaugeProvider passThroughBloomFilterHits = newGaugeProvider("PassThroughBloomFilterHits", + 0L, + r -> r.getFilterTracker().getPassThroughBloomFilterHitCount(), + Long::sum); + /** * False positive ratio of bloom filter */ @@ -110,6 +155,15 @@ protected R map(SSTableReader r) recentBloomFilterFalsePositives, bloomFilterDiskSpaceUsed, bloomFilterOffHeapMemoryUsed, + approximateBloomFilterOffHeapMemoryUsed, + loadedBloomFilter, + lazyBloomFilter, + passThroughBloomFilter, + lazyBloomFilterByRequestRate, + lazyBloomFilterByRequestCount, + lazyBloomFilterHits, + loadedBloomFilterHits, + passThroughBloomFilterHits, bloomFilterFalseRatio, recentBloomFilterFalseRatio); diff --git a/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterTracker.java b/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterTracker.java index 362926338f53..d86f6143abd1 100644 --- a/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterTracker.java +++ b/src/java/org/apache/cassandra/io/sstable/filter/BloomFilterTracker.java @@ -17,66 +17,276 @@ */ package org.apache.cassandra.io.sstable.filter; +import com.codahale.metrics.Meter; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.BloomFilter; +import org.apache.cassandra.utils.FilterFactory; + import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.LongAdder; -public class BloomFilterTracker +public abstract class BloomFilterTracker { - private final LongAdder falsePositiveCount = new LongAdder(); - private final LongAdder truePositiveCount = new LongAdder(); - private final LongAdder trueNegativeCount = new LongAdder(); - private final AtomicLong lastFalsePositiveCount = new AtomicLong(); - private final AtomicLong lastTruePositiveCount = new AtomicLong(); - private final AtomicLong lastTrueNegativeCount = new AtomicLong(); - - public void addFalsePositive() - { - falsePositiveCount.increment(); - } + public abstract void addFalsePositive(); + public abstract void addTruePositive(); + public abstract void addTrueNegative(); + /** + * Count a lookup that uses {@link FilterFactory#AlwaysPresentForLazyLoading}. + */ + public abstract void addLazyBloomFilterHit(); + /** + * Count a lookup that uses a loaded {@link BloomFilter}. + */ + public abstract void addLoadedBloomFilterHit(); + /** + * Count a lookup that uses {@link FilterFactory#AlwaysPresent} due to lazy bloom filter + * loading failure or due to reaching bloom filter memory limit. + */ + public abstract void addPassThroughBloomFilterHit(); + public abstract long getFalsePositiveCount(); + public abstract long getRecentFalsePositiveCount(); + public abstract double getRecentFalsePositiveRate(); + public abstract long getTruePositiveCount(); + public abstract long getRecentTruePositiveCount(); + public abstract double getRecentTruePositiveRate(); + public abstract long getTrueNegativeCount(); + public abstract long getRecentTrueNegativeCount(); + public abstract double getRecentTrueNegativeRate(); + public abstract long getLazyBloomFilterHitCount(); + public abstract long getLoadedBloomFilterHitCount(); + public abstract long getPassThroughBloomFilterHitCount(); - public void addTruePositive() + public static BloomFilterTracker createNoopTracker() { - truePositiveCount.increment(); + return NoopBloomFilterTracker.instance; } - public void addTrueNegative() + public static BloomFilterTracker createMeterTracker() { - trueNegativeCount.increment(); + return new MeterBloomFilterTracker(); } - public long getFalsePositiveCount() + private static class MeterBloomFilterTracker extends BloomFilterTracker { - return falsePositiveCount.sum(); - } + private final Meter falsePositiveCount = new Meter(); + private final Meter truePositiveCount = new Meter(); + private final Meter trueNegativeCount = new Meter(); + private final Meter lazyBloomFilterHitCount = new Meter(); + private final Meter loadedBloomFilterHitCount = new Meter(); + private final Meter passThroughBloomFilterHitCount = new Meter(); + private final AtomicLong lastFalsePositiveCount = new AtomicLong(); + private final AtomicLong lastTruePositiveCount = new AtomicLong(); + private final AtomicLong lastTrueNegativeCount = new AtomicLong(); - public long getRecentFalsePositiveCount() - { - long fpc = getFalsePositiveCount(); - long last = lastFalsePositiveCount.getAndSet(fpc); - return fpc - last; - } + @Override + public void addFalsePositive() + { + falsePositiveCount.mark(); + } - public long getTruePositiveCount() - { - return truePositiveCount.sum(); - } + @Override + public void addTruePositive() + { + truePositiveCount.mark(); + } - public long getRecentTruePositiveCount() - { - long tpc = getTruePositiveCount(); - long last = lastTruePositiveCount.getAndSet(tpc); - return tpc - last; - } + @Override + public void addTrueNegative() + { + trueNegativeCount.mark(); + } - public long getTrueNegativeCount() - { - return trueNegativeCount.sum(); + @Override + public void addLazyBloomFilterHit() + { + lazyBloomFilterHitCount.mark(); + } + + @Override + public void addLoadedBloomFilterHit() + { + loadedBloomFilterHitCount.mark(); + } + + @Override + public void addPassThroughBloomFilterHit() + { + passThroughBloomFilterHitCount.mark(); + } + + @Override + public long getFalsePositiveCount() + { + return falsePositiveCount.getCount(); + } + + public long getRecentFalsePositiveCount() + { + long fpc = getFalsePositiveCount(); + long last = lastFalsePositiveCount.getAndSet(fpc); + return fpc - last; + } + + @Override + public double getRecentFalsePositiveRate() + { + return falsePositiveCount.getFifteenMinuteRate(); + } + + @Override + public long getTruePositiveCount() + { + return truePositiveCount.getCount(); + } + + public long getRecentTruePositiveCount() + { + long tpc = getTruePositiveCount(); + long last = lastTruePositiveCount.getAndSet(tpc); + return tpc - last; + } + + @Override + public double getRecentTruePositiveRate() + { + return truePositiveCount.getFifteenMinuteRate(); + } + + @Override + public long getTrueNegativeCount() + { + return trueNegativeCount.getCount(); + } + + public long getRecentTrueNegativeCount() + { + long tnc = getTrueNegativeCount(); + long last = lastTrueNegativeCount.getAndSet(tnc); + return tnc - last; + } + @Override + public double getRecentTrueNegativeRate() + { + return trueNegativeCount.getFifteenMinuteRate(); + } + + @Override + public long getLazyBloomFilterHitCount() + { + return lazyBloomFilterHitCount.getCount(); + } + + @Override + public long getLoadedBloomFilterHitCount() + { + return loadedBloomFilterHitCount.getCount(); + } + + @Override + public long getPassThroughBloomFilterHitCount() + { + return passThroughBloomFilterHitCount.getCount(); + } } - public long getRecentTrueNegativeCount() + /** + * Bloom filter tracker that does nothing and always returns 0 for all counters. + * + * Bloom Filter tracking is managed on the CFS level, so there is no reason to count anything if an SSTable does not + * belong (yet) to a CFS. This tracker is used initially on SSTableReaders and is overwritten during setup + * in {@link SSTableReader#setupOnline()} or {@link SSTableReader#setupOnline(ColumnFamilyStore)}}. + */ + private static class NoopBloomFilterTracker extends BloomFilterTracker { - long tnc = getTrueNegativeCount(); - long last = lastTrueNegativeCount.getAndSet(tnc); - return tnc - last; + static final NoopBloomFilterTracker instance = new NoopBloomFilterTracker(); + + @Override + public void addFalsePositive() {} + + @Override + public void addTruePositive() {} + + @Override + public void addTrueNegative() {} + + @Override + public void addLazyBloomFilterHit() {} + + @Override + public void addLoadedBloomFilterHit() {} + + @Override + public void addPassThroughBloomFilterHit() {} + + @Override + public long getFalsePositiveCount() + { + return 0; + } + + @Override + public long getRecentFalsePositiveCount() + { + return 0; + } + + @Override + public double getRecentFalsePositiveRate() + { + return 0; + } + + @Override + public long getTruePositiveCount() + { + return 0; + } + + @Override + public long getRecentTruePositiveCount() + { + return 0; + } + @Override + public double getRecentTruePositiveRate() + { + return 0; + } + + @Override + public long getTrueNegativeCount() + { + return 0; + } + + @Override + public long getRecentTrueNegativeCount() + { + return 0; + } + + @Override + public double getRecentTrueNegativeRate() + { + return 0; + } + + @Override + public long getLazyBloomFilterHitCount() + { + return 0; + } + + @Override + public long getLoadedBloomFilterHitCount() + { + return 0; + } + + @Override + public long getPassThroughBloomFilterHitCount() + { + return 0; + } } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/AbstractKeyFetcher.java b/src/java/org/apache/cassandra/io/sstable/format/AbstractKeyFetcher.java new file mode 100644 index 000000000000..3a65a2f70c2b --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/format/AbstractKeyFetcher.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.format; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.io.FSReadError; +import org.apache.cassandra.io.sstable.IKeyFetcher; +import org.apache.cassandra.io.util.RandomAccessReader; + +public abstract class AbstractKeyFetcher implements IKeyFetcher +{ + private final RandomAccessReader reader; + + protected AbstractKeyFetcher(RandomAccessReader reader) + { + this.reader = reader; + } + + @Override + public DecoratedKey apply(long keyOffset) + { + if (keyOffset < 0) + return null; + + try + { + reader.seek(keyOffset); + if (reader.isEOF()) + return null; + + return readKey(reader); + } + catch (IOException e) + { + throw new FSReadError(new IOException("Failed to read key from " + reader.getChannel().file(), e), reader.getChannel().file()); + } + } + + public abstract DecoratedKey readKey(RandomAccessReader reader) throws IOException; + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + AbstractKeyFetcher that = (AbstractKeyFetcher) o; + return Objects.equals(reader.getChannel().file(), that.reader.getChannel().file()); + } + + @Override + public int hashCode() + { + return Objects.hash(reader.getChannel().file()); + } + + @Override + public String toString() + { + return String.format("KeyFetcher{file=%s}", reader.getChannel().file()); + } + + @Override + public void close() + { + reader.close(); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/format/CompressionInfoComponent.java b/src/java/org/apache/cassandra/io/sstable/format/CompressionInfoComponent.java index 0e24fa991d72..cfadf555d91e 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/CompressionInfoComponent.java +++ b/src/java/org/apache/cassandra/io/sstable/format/CompressionInfoComponent.java @@ -29,30 +29,37 @@ import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.SliceDescriptor; public class CompressionInfoComponent { - public static CompressionMetadata maybeLoad(Descriptor descriptor, Set components) + public static CompressionMetadata maybeLoad(Descriptor descriptor, Set components, SliceDescriptor sliceDescriptor) { if (components.contains(Components.COMPRESSION_INFO)) - return load(descriptor); + return load(descriptor, sliceDescriptor); return null; } - public static CompressionMetadata loadIfExists(Descriptor descriptor) + public static CompressionMetadata loadIfExists(Descriptor descriptor, SliceDescriptor sliceDescriptor) { if (descriptor.fileFor(Components.COMPRESSION_INFO).exists()) - return load(descriptor); + return load(descriptor, sliceDescriptor); return null; } public static CompressionMetadata load(Descriptor descriptor) + { + return load(descriptor, SliceDescriptor.NONE); + } + + public static CompressionMetadata load(Descriptor descriptor, SliceDescriptor sliceDescriptor) { return CompressionMetadata.open(descriptor.fileFor(Components.COMPRESSION_INFO), descriptor.fileFor(Components.DATA).length(), - descriptor.version.hasMaxCompressedLength()); + descriptor.version.hasMaxCompressedLength(), + sliceDescriptor); } /** diff --git a/src/java/org/apache/cassandra/io/sstable/format/DataComponent.java b/src/java/org/apache/cassandra/io/sstable/format/DataComponent.java index 9367cb444d80..39f2b5b42922 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/DataComponent.java +++ b/src/java/org/apache/cassandra/io/sstable/format/DataComponent.java @@ -18,9 +18,13 @@ package org.apache.cassandra.io.sstable.format; +import java.util.Optional; + import org.apache.cassandra.config.Config.FlushCompression; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.io.compress.CompressedSequentialWriter; +import org.apache.cassandra.io.compress.EncryptedSequentialWriter; +import org.apache.cassandra.io.compress.Encryptor; import org.apache.cassandra.io.compress.ICompressor; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; @@ -43,13 +47,24 @@ public static SequentialWriter buildWriter(Descriptor descriptor, if (metadata.params.compression.isEnabled()) { final CompressionParams compressionParams = buildCompressionParams(metadata, operationType, flushCompression); + final ICompressor compressor = compressionParams.getSstableCompressor(); - return new CompressedSequentialWriter(descriptor.fileFor(Components.DATA), - descriptor.fileFor(Components.COMPRESSION_INFO), - descriptor.fileFor(Components.DIGEST), - options, - compressionParams, - metadataCollector); + // Check if this is encryption-only (no actual compression) + if (compressor instanceof Encryptor) + { + return new EncryptedSequentialWriter(descriptor.fileFor(Components.DATA), + options, + compressor); + } + else + { + return new CompressedSequentialWriter(descriptor.fileFor(Components.DATA), + descriptor.fileFor(Components.COMPRESSION_INFO), + descriptor.fileFor(Components.DIGEST), + options, + compressionParams, + metadataCollector); + } } else { @@ -88,13 +103,21 @@ private static CompressionParams buildCompressionParams(TableMetadata metadata, case fast: if (!compressor.recommendedUses().contains(ICompressor.Uses.FAST_COMPRESSION)) { - // The default compressor is generally fast (LZ4 with 16KiB block size) - compressionParams = CompressionParams.DEFAULT; + compressionParams = CompressionParams.FAST; + break; + } + // else fall through + case adaptive: + if (!compressor.recommendedUses().contains(ICompressor.Uses.FAST_COMPRESSION)) + { + compressionParams = CompressionParams.FAST_ADAPTIVE; break; } // else fall through case table: default: + compressionParams = Optional.ofNullable(compressionParams.forUse(ICompressor.Uses.FAST_COMPRESSION)) + .orElse(compressionParams); break; } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/FilterComponent.java b/src/java/org/apache/cassandra/io/sstable/format/FilterComponent.java index 9f99d7dac0cd..a759ae3f958b 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/FilterComponent.java +++ b/src/java/org/apache/cassandra/io/sstable/format/FilterComponent.java @@ -31,18 +31,20 @@ import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileInputStreamPlus; import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.BloomFilter; import org.apache.cassandra.utils.BloomFilterSerializer; import org.apache.cassandra.utils.FilterFactory; import org.apache.cassandra.utils.IFilter; +import static org.apache.cassandra.config.CassandraRelevantProperties.BF_FP_CHANCE_TOLERANCE; +import static org.apache.cassandra.config.CassandraRelevantProperties.BF_RECREATE_ON_FP_CHANCE_CHANGE; + public class FilterComponent { private static final Logger logger = LoggerFactory.getLogger(FilterComponent.class); - final static boolean rebuildFilterOnFPChanceChange = false; - final static double filterFPChanceTolerance = 0d; - private FilterComponent() { } @@ -72,6 +74,12 @@ public static IFilter load(Descriptor descriptor) throws IOException public static void save(IFilter filter, Descriptor descriptor, boolean deleteOnFailure) throws IOException { + if (!filter.isSerializable()) + { + logger.info("Skipped saving non-serializable bloom filter {} for {} to disk", filter, descriptor); + return; + } + File filterFile = descriptor.fileFor(Components.FILTER); try (FileOutputStreamPlus stream = filterFile.newOutputStream(File.WriteMode.OVERWRITE)) { @@ -116,13 +124,18 @@ else if (!components.contains(Components.FILTER) || Double.isNaN(currentFPChance return null; } - else if (!isFPChanceDiffNegligible(desiredFPChance, currentFPChance) && rebuildFilterOnFPChanceChange) + else if (!isFPChanceDiffNegligible(desiredFPChance, currentFPChance) && BF_RECREATE_ON_FP_CHANCE_CHANGE.getBoolean()) { if (logger.isTraceEnabled()) logger.trace("Bloom filter for {} will not be loaded because fpChance has changed from {} to {} and the filter should be recreated", descriptor, currentFPChance, desiredFPChance); return null; } + else if (BloomFilter.lazyLoading() && !SchemaConstants.isLocalSystemKeyspace(metadata.keyspace)) + { + logger.debug("postponing bloom filter deserialization for {}", descriptor.fileFor(Components.FILTER)); + return FilterFactory.AlwaysPresentForLazyLoading; + } try { @@ -140,11 +153,11 @@ else if (!isFPChanceDiffNegligible(desiredFPChance, currentFPChance) && rebuildF static boolean shouldUseBloomFilter(double fpChance) { - return !(Math.abs(1 - fpChance) <= filterFPChanceTolerance); + return !(Math.abs(1 - fpChance) <= BF_FP_CHANCE_TOLERANCE.getDouble()); } static boolean isFPChanceDiffNegligible(double fpChance1, double fpChance2) { - return Math.abs(fpChance1 - fpChance2) <= filterFPChanceTolerance; + return Math.abs(fpChance1 - fpChance2) <= BF_FP_CHANCE_TOLERANCE.getDouble(); } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/IndexComponent.java b/src/java/org/apache/cassandra/io/sstable/format/IndexComponent.java index 45dfc62b2c8b..fbf39bd626f6 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/IndexComponent.java +++ b/src/java/org/apache/cassandra/io/sstable/format/IndexComponent.java @@ -19,27 +19,28 @@ package org.apache.cassandra.io.sstable.format; import org.apache.cassandra.cache.ChunkCache; +import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.IOOptions; import org.apache.cassandra.io.sstable.SSTable; -import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.FileHandle; public class IndexComponent { - public static FileHandle.Builder fileBuilder(File file, IOOptions ioOptions, ChunkCache chunkCache) + public static FileHandle.Builder fileBuilder(Descriptor descriptor, Component component, IOOptions ioOptions, ChunkCache chunkCache) { - return new FileHandle.Builder(file).withChunkCache(chunkCache) - .mmapped(ioOptions.indexDiskAccessMode); + return StorageProvider.instance.primaryIndexWriteTimeFileHandleBuilderFor(descriptor, component, ioOptions.indexDiskAccessMode, chunkCache, OperationType.UNKNOWN); } public static FileHandle.Builder fileBuilder(Component component, SSTable ssTable) { - return fileBuilder(ssTable.descriptor.fileFor(component), ssTable.ioOptions, ssTable.chunkCache); + return fileBuilder(ssTable.descriptor, component, ssTable.ioOptions, ssTable.chunkCache); } - public static FileHandle.Builder fileBuilder(Component component, SSTable.Builder builder) + public static FileHandle.Builder fileBuilder(Component component, SSTable.Builder builder, OperationType operationType) { - return fileBuilder(builder.descriptor.fileFor(component), builder.getIOOptions(), builder.getChunkCache()); + return StorageProvider.instance.primaryIndexWriteTimeFileHandleBuilderFor(builder.descriptor, component, builder.getIOOptions().indexDiskAccessMode, builder.getChunkCache(), operationType); } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java index 654880c2c175..2a76f7ea88e5 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java @@ -60,6 +60,8 @@ public interface SSTableFormat Set allComponents(); + Set requiredComponents(); + Set primaryComponents(); /** diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java index 535fdf577c69..21fae148ad2b 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.lang.ref.WeakReference; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -28,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -37,12 +39,11 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; -import com.google.common.collect.Ordering; -import com.google.common.primitives.Longs; import com.google.common.util.concurrent.RateLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +60,8 @@ import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.db.lifecycle.AbstractLogTransaction; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.EncodingStats; import org.apache.cassandra.db.rows.UnfilteredRowIterator; @@ -74,17 +77,19 @@ import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.IKeyFetcher; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.IVerifier; import org.apache.cassandra.io.sstable.KeyIterator; import org.apache.cassandra.io.sstable.KeyReader; import org.apache.cassandra.io.sstable.SSTable; -import org.apache.cassandra.io.sstable.SSTableIdFactory; import org.apache.cassandra.io.sstable.SSTableIdentityIterator; import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.io.sstable.SSTableWatcher; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.metadata.CompactionMetadata; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.ChannelProxy; import org.apache.cassandra.io.util.CheckedFunction; import org.apache.cassandra.io.util.DataIntegrityMetadata; @@ -93,21 +98,25 @@ import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; +import org.apache.cassandra.io.util.SliceDescriptor; import org.apache.cassandra.metrics.RestorableMeter; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.utils.BloomFilter; import org.apache.cassandra.utils.EstimatedHistogram; import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Interval; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.JVMStabilityInspector; -import org.apache.cassandra.utils.NativeLibrary; import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.concurrent.Ref; +import org.apache.cassandra.utils.concurrent.RefCounted; import org.apache.cassandra.utils.concurrent.SelfRefCounted; import org.apache.cassandra.utils.concurrent.SharedCloseable; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; @@ -151,9 +160,9 @@ *

    * TODO: fill in details about Tracker and lifecycle interactions for tools, and for compaction strategies */ -public abstract class SSTableReader extends SSTable implements UnfilteredSource, SelfRefCounted, Comparable +public abstract class SSTableReader extends SSTable implements UnfilteredSource, SelfRefCounted, Comparable, CompactionSSTable { - private static final Logger logger = LoggerFactory.getLogger(SSTableReader.class); + protected static final Logger logger = LoggerFactory.getLogger(SSTableReader.class); private static final boolean TRACK_ACTIVITY = CassandraRelevantProperties.DISABLE_SSTABLE_ACTIVITY_TRACKING.getBoolean(); @@ -200,15 +209,6 @@ private UniqueIdentifier(long unixMicros, long clockSeqAndNode) public final UniqueIdentifier instanceId = TimeUUID.Generator.nextTimeUUID(UNIQUE_IDENTIFIER_FACTORY); - public static final Comparator firstKeyComparator = (o1, o2) -> o1.getFirst().compareTo(o2.getFirst()); - public static final Ordering firstKeyOrdering = Ordering.from(firstKeyComparator); - public static final Comparator lastKeyComparator = (o1, o2) -> o1.getLast().compareTo(o2.getLast()); - - public static final Comparator idComparator = Comparator.comparing(t -> t.descriptor.id, SSTableIdFactory.COMPARATOR); - public static final Comparator idReverseComparator = idComparator.reversed(); - - public static final Comparator sizeComparator = (o1, o2) -> Longs.compare(o1.onDiskLength(), o2.onDiskLength()); - /** * maxDataAge is a timestamp in local server time (e.g. Global.currentTimeMilli) which represents an upper bound * to the newest piece of data stored in the sstable. In other words, this sstable does not contain items created @@ -269,6 +269,19 @@ public enum OpenReason protected final FileHandle dfile; + // Unlike readMeter, which is global and tracks access to data files, this meter + // is incremented as soon as the partition index is accessed with SinglePartitionReadCommand EQ. This includes + // the case where the sstable does not contain the partition the query was looking for. + // we use a restorable meter to gain access to the moving averages, we don't + // really restore it from disk + protected final Optional partitionIndexReadMeter = BloomFilter.lazyLoading() + ? Optional.of(BloomFilter.lazyLoadingWindow() > 0 + // when window > 0, use rate at given window + ? RestorableMeter.builder().withWindow(BloomFilter.lazyLoadingWindow()).build() + // when window <= 0, it only cares about absolute count + : RestorableMeter.builder().build()) + : Optional.empty(); + // technically isCompacted is not necessary since it should never be unreferenced unless it is also compacted, // but it seems like a good extra layer of protection against reference counting bugs to not delete data based on that alone public final AtomicBoolean isSuspect = new AtomicBoolean(false); @@ -278,15 +291,15 @@ public enum OpenReason public final SerializationHeader header; - private final InstanceTidier tidy; + protected final InstanceTidier tidy; private final Ref selfRef; private RestorableMeter readMeter; private volatile double crcCheckChance; - protected final DecoratedKey first; - protected final DecoratedKey last; + public final DecoratedKey first; + public final DecoratedKey last; public final AbstractBounds bounds; private final Interval interval; @@ -299,12 +312,12 @@ public enum OpenReason * @param sstables SSTables to calculate key count * @return estimated key count */ - public static long getApproximateKeyCount(Iterable sstables) + public static long getApproximateKeyCount(Iterable sstables) { long count = -1; if (Iterables.isEmpty(sstables)) - return count; + return 0; boolean failed = false; ICardinality cardinality = null; @@ -315,7 +328,7 @@ public static long getApproximateKeyCount(Iterable sstables) try { - CompactionMetadata metadata = StatsComponent.load(sstable.descriptor).compactionMetadata(); + CompactionMetadata metadata = sstable.getCompactionMetadata().orElse(null); // If we can't load the CompactionMetadata, we are forced to estimate the keys using the index // summary. (CASSANDRA-10676) if (metadata == null) @@ -356,6 +369,30 @@ public static long getApproximateKeyCount(Iterable sstables) return count; } + /** + * The key cardinality estimator for the sstable, if it can be loaded. + * + * @return the sstable key cardinality estimator created during flush/compaction, or {@code null} if that estimator + * cannot be loaded for any reason. + */ + @VisibleForTesting + public ICardinality keyCardinalityEstimator() + { + if (openReason == OpenReason.EARLY) + return null; + + try + { + CompactionMetadata metadata = getCompactionMetadata().orElse(null); + return metadata == null ? null : metadata.cardinalityEstimator; + } + catch (IOException e) + { + logger.warn("Reading cardinality from Statistics.db failed for {}.", this, e); + return null; + } + } + public static SSTableReader open(SSTable.Owner owner, Descriptor descriptor) { return open(owner, descriptor, null); @@ -410,6 +447,7 @@ public static SSTableReader open(Owner owner, boolean validate, boolean isOffline) { + components = SSTableWatcher.instance.discoverComponents(descriptor, components); SSTableReaderLoadingBuilder builder = descriptor.getFormat().getReaderFactory().loadingBuilder(descriptor, metadata, components); return builder.build(owner, validate, !isOffline); @@ -433,6 +471,11 @@ public static Collection openAll(SSTable.Owner owner, Set openAll(SSTable.Owner owner, Set builder, Owner owner) { super(builder, owner); @@ -474,6 +525,7 @@ protected SSTableReader(Builder builder, Owner owner) this.dfile = builder.getDataFile(); this.maxDataAge = builder.getMaxDataAge(); this.openReason = builder.getOpenReason(); + this.compactionMetadata = builder.getCompactionMetadata(); this.first = builder.getFirst(); this.last = builder.getLast(); this.interval = first == null || last == null ? null : Interval.create(first, last, this); @@ -498,9 +550,10 @@ public DecoratedKey getLast() return last; } - public Interval getInterval() + @Override + public Interval getInterval() { - return interval; + return (Interval) interval; } @Override @@ -552,14 +605,32 @@ public int hashCode() return this.descriptor.hashCode(); } + @Override public String getFilename() { return dfile.path(); } + @Override + public Descriptor getDescriptor() + { + return descriptor; + } + + @Override + public Path getFile() + { + return descriptor.pathFor(Components.DATA); + } + + public SliceDescriptor getDataFileSliceDescriptor() + { + return dfile.sliceDescriptor; + } + public void setupOnline() { - owner().ifPresent(o -> setCrcCheckChance(o.getCrcCheckChance())); + owner().ifPresent(o -> setCrcCheckChance(o.getCrcCheckChance())); } /** @@ -631,6 +702,7 @@ protected final > B unbuildTo(B builder, boolean sharedC b.setDataFile(sharedCopy ? sharedCopyOrNull(dfile) : dfile); b.setStatsMetadata(sstableMetadata); + b.setCompactionMetadata(compactionMetadata); b.setSerializationHeader(header); b.setMaxDataAge(maxDataAge); b.setOpenReason(openReason); @@ -649,6 +721,29 @@ public RestorableMeter getReadMeter() return readMeter; } + @VisibleForTesting + @Nullable + public RestorableMeter getPartitionIndexReadMeter() + { + return partitionIndexReadMeter.orElse(null); + } + + /** + * Called by {@link org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy} and other compaction + * strategies to determine the read hotness of this sstables, this method returna a "read hotness" which is + * calculated by looking at the last two hours read rate and dividing this number by the estimated number of keys. + *

    + * Note that some system tables do not have read meters, in which case this method will return zero. + * + * @return the last two hours read rate per estimated key + */ + @Override + public double hotness() + { + // system tables don't have read meters, just use 0.0 for the hotness + return readMeter == null ? 0.0 : readMeter.twoHourRate() / estimatedKeys(); + } + /** * All the resources which should be released upon closing this sstable reader are registered with in * {@link GlobalTidy}. This method lets close a provided resource explicitly any time and unregister it from @@ -717,12 +812,14 @@ public long getCompressionMetadataOffHeapSize() if (!compression) return 0; - return getCompressionMetadata().offHeapSize(); + CompressionMetadata metadata = dfile.compressionMetadata().orElse(null); + return metadata != null ? metadata.offHeapSize() : 0; } /** * Calculates an estimate of the number of keys in the sstable represented by this reader. */ + @Override public abstract long estimatedKeys(); /** @@ -761,6 +858,45 @@ public List getPositionsForRanges(Collection + * By allowing some imprecision, this method may be faster/avoid reads to the data file that are otherwise + * necessary. In practice, the positions returned by this method may be up to "one key off" for each bound of + * each range. In other words, say we pass range `(t_s, t_e]`, and let's denote by `k_i` the ith key in the + * underlying sstable. And suppose that `getPositionsForRanges([(t_s, t_e]])` returns `(k_s, k_e)` (with `s` < `e`), + * where `k_i` means the position to i-th key in the sstable. Then this method applied to this same range may return + * either one of `(k_s, k_e)` (same result), `(k_s-1, k_e)`, `(k_s, k_e+1)`, or `(k_s-1, k_e+1)` + *

    + * Also note that as a consequence of this, the returned list of position bounds may have some strict overlap + * (the method could return something along the lines of `[(0, 100), (80, 200)]`). But all the starting positions + * and all the ending positions will still be ordered. + */ + public List getApproximatePositionsForRanges(Collection> ranges) + { + List positions = new ArrayList<>(); + for (Range range : Range.normalize(ranges)) + { + assert !range.isWrapAround() || range.right.isMinimum(); + AbstractBounds bounds = Range.makeRowRange(range); + PartitionPositionBounds pb = getApproximatePositionsForBounds(bounds); + if (pb != null) + positions.add(pb); + } + return positions; + } + + /** + * This is to {@link #getPositionsForBounds(AbstractBounds)} what {@link #getApproximatePositionsForRanges(Collection)} + * is to {@link #getPositionsForRanges(Collection)}. + */ + public PartitionPositionBounds getApproximatePositionsForBounds(AbstractBounds bounds) + { + // Return the exact positions by default; this can be overridden by concrete sstable implementations. + return getPositionsForBounds(bounds); + } + /** * Get a list of data positions in this SSTable that correspond to the given list of bounds. This method will remove * non-covered intervals, but will not correct order or overlap in the supplied list, e.g. if bounds overlap, the @@ -791,19 +927,26 @@ public List getPositionsForBoundsIterator(Iterator bounds) { - long left = getPosition(bounds.left, bounds.inclusiveLeft() ? Operator.GE : Operator.GT); + long rieLeft = getPosition(bounds.left, bounds.inclusiveLeft() ? Operator.GE : Operator.GT); // Note: getPosition will apply a moved start if the sstable is in MOVED_START state. - if (left < 0) // empty range + if (rieLeft < 0) // empty range return null; - - long right = bounds.right.isMinimum() ? -1 - : getPosition(bounds.right, bounds.inclusiveRight() ? Operator.GT - : Operator.GE); - if (right < 0) // right is beyond end + long left = rieLeft; + + long rieRight = bounds.right.isMinimum() ? -1 + : getPosition(bounds.right, bounds.inclusiveRight() ? Operator.GT + : Operator.GE); + long right; + if (rieRight != -1) + right = rieRight; + else // right is beyond end right = uncompressedLength(); // this should also be correct for EARLY readers - if (left >= right) // empty range + if (left >= right) + { + // empty range return null; + } return new PartitionPositionBounds(left, right); } @@ -842,18 +985,34 @@ public long onDiskSizeForPartitionPositions(Collection } else { - final CompressionMetadata compressionMetadata = getCompressionMetadata(); - long lastEnd = 0; - for (PartitionPositionBounds position : positionBounds) + CompressionMetadata compressionMetadata = dfile.compressionMetadata().orElse(null); + if (compressionMetadata != null) { - // The end of the chunk that contains the last required byte from the range. - long upperChunkEnd = compressionMetadata.chunkFor(position.upperPosition - 1).chunkEnd(); - // The start of the chunk that contains the first required byte from the range. - long lowerChunkStart = compressionMetadata.chunkFor(position.lowerPosition).offset; - if (lowerChunkStart < lastEnd) // if regions include the same chunk, count it only once - lowerChunkStart = lastEnd; - total += upperChunkEnd - lowerChunkStart; - lastEnd = upperChunkEnd; + long lastEnd = 0; + for (PartitionPositionBounds position : positionBounds) + { + assert position.lowerPosition >= 0 : "the partition lower cannot be negative"; + if (position.upperPosition == position.lowerPosition) + { + continue; + } + assert position.upperPosition >= position.lowerPosition : "the partition upper position cannot be lower than lower position"; + + // The end of the chunk that contains the last required byte from the range. + long upperChunkEnd = compressionMetadata.chunkFor(position.upperPosition - 1).chunkEnd(); + // The start of the chunk that contains the first required byte from the range. + long lowerChunkStart = compressionMetadata.chunkFor(position.lowerPosition).offset; + if (lowerChunkStart < lastEnd) // if regions include the same chunk, count it only once + lowerChunkStart = lastEnd; + total += upperChunkEnd - lowerChunkStart; + lastEnd = upperChunkEnd; + } + } + else + { + // For encrypted files without compression metadata, just sum the ranges + for (PartitionPositionBounds position : positionBounds) + total += position.upperPosition - position.lowerPosition; } } return total; @@ -891,7 +1050,7 @@ public final long getPosition(PartitionPosition key, * @param op The Operator defining matching keys: the nearest key to the target matching the operator wins. * @param updateStats true if updating stats and cache * @param listener a listener used to handle internal events - * @return The index entry corresponding to the key, or null if the key is not present + * @return The index entry corresponding to the key, or -1 if the key is not present */ protected long getPosition(PartitionPosition key, Operator op, @@ -945,6 +1104,7 @@ public KeyIterator keyIterator() throws IOException * Returns the length in bytes of the (uncompressed) data for this SSTable. For compressed files, this is not * the same thing as the on disk size (see {@link #onDiskLength()}). */ + @Override public long uncompressedLength() { return dfile.dataLength(); @@ -967,11 +1127,23 @@ public double tokenSpaceCoverage() * The length in bytes of the on disk size for this SSTable. For compressed files, this is not the same thing * as the data length (see {@link #uncompressedLength()}). */ + @Override public long onDiskLength() { return dfile.onDiskLength; } + public long onDiskComponentsSize() + { + long total = 0; + for (Component component : components()) + { + total += FileUtils.size(descriptor.pathFor(component)); + } + + return total; + } + @VisibleForTesting public double getCrcCheckChance() { @@ -995,7 +1167,7 @@ public void setCrcCheckChance(double crcCheckChance) *

    * Calling it multiple times is usually buggy. */ - public void markObsolete(Runnable tidier) + public void markObsolete(AbstractLogTransaction.ReaderTidier tidier) { if (logger.isTraceEnabled()) logger.trace("Marking {} compacted", getFilename()); @@ -1010,11 +1182,24 @@ public void markObsolete(Runnable tidier) } } + @Override public boolean isMarkedCompacted() { return tidy.global.obsoletion != null; } + /** + * Used by CNDB to detect sstables marked as obsolete (compacted). Without obtaining the actual + * {@link SSTableReader} instance. See {@link #isMarkedCompacted()} that performs the same check for an + * exisiting reader instance. + * @see #isMarkedCompacted() + * @see #markObsolete(AbstractLogTransaction.ReaderTidier) + */ + public static boolean isMarkedCompacted(Descriptor descriptor) + { + return GlobalTidy.hasTidier(descriptor); + } + public void markSuspect() { if (logger.isTraceEnabled()) @@ -1029,11 +1214,18 @@ public void unmarkSuspect() isSuspect.getAndSet(false); } + @Override public boolean isMarkedSuspect() { return isSuspect.get(); } + @Override + public boolean isSuitableForCompaction() + { + return !isMarkedSuspect() && openReason != SSTableReader.OpenReason.EARLY; + } + /** * Direct I/O SSTableScanner over a defined range of tokens. * @@ -1044,7 +1236,8 @@ public ISSTableScanner getScanner(Range range) { if (range == null) return getScanner(); - return getScanner(Collections.singletonList(range)); + else + return getScanner(Collections.singletonList(range)); } /** @@ -1086,7 +1279,6 @@ public ISSTableScanner getScanner(Iterator> bo return new SSTableSimpleScanner(this, getPositionsForBoundsIterator(boundsIterator)); } - /** * Create a {@link FileDataInput} for the data file of the sstable represented by this reader. This method returns * a newly opened resource which must be closed by the caller. @@ -1095,7 +1287,9 @@ public ISSTableScanner getScanner(Iterator> bo */ public FileDataInput getFileDataInput(long position) { - return dfile.createReader(position); + // While `FileDataInput` supports a `seek` method in practive, it's an interface predominently made for + // sequential access. If random access on the returned input is desired, caller should use `#openDataReader` + return dfile.createReader(position, ReadPattern.SEQUENTIAL); } /** @@ -1117,7 +1311,7 @@ public void createLinks(String snapshotDirectoryPath) public void createLinks(String snapshotDirectoryPath, RateLimiter rateLimiter) { - createLinks(descriptor, components, snapshotDirectoryPath, rateLimiter); + createLinks(descriptor, components(), snapshotDirectoryPath, rateLimiter); } public static void createLinks(Descriptor descriptor, Set components, String snapshotDirectoryPath) @@ -1129,7 +1323,8 @@ public static void createLinks(Descriptor descriptor, Set components, { for (Component component : components) { - File sourceFile = descriptor.fileFor(component); + // Convert a potential RemotePath to a local one since RemotePaths don't support hard links. + File sourceFile = new File(descriptor.fileFor(component).absolutePath()); if (!sourceFile.exists()) continue; if (null != limiter) @@ -1139,38 +1334,40 @@ public static void createLinks(Descriptor descriptor, Set components, } } + @Override public boolean isRepaired() { return sstableMetadata.repairedAt != ActiveRepairService.UNREPAIRED_SSTABLE; } /** - * Reads the key stored at the position saved in SASI. - *

    - * When SASI is created, it uses key locations retrieved from {@link KeyReader#keyPositionForSecondaryIndex()}. - * This method is to read the key stored at such position. It is up to the concrete SSTable format implementation - * what that position means and which file it refers. The only requirement is that it is consistent with what - * {@link KeyReader#keyPositionForSecondaryIndex()} returns. + * Returns an instance of {@link IKeyFetcher} that can be used to fetch keys from this SSTable. * - * @return key if found, {@code null} otherwise + * @param isForSASI whether the key fetcher is for SASI index - SASI indexes may use a different source of keys + * depending on the SSTableFormat (for example index file vs data file). If false, the keys are + * fetched from the data file. */ - public abstract DecoratedKey keyAtPositionFromSecondaryIndex(long keyPositionFromSecondaryIndex) throws IOException; + public abstract IKeyFetcher openKeyFetcher(boolean isForSASI); + @Override public boolean isPendingRepair() { return sstableMetadata.pendingRepair != ActiveRepairService.NO_PENDING_REPAIR; } + @Override public TimeUUID getPendingRepair() { return sstableMetadata.pendingRepair; } + @Override public long getRepairedAt() { return sstableMetadata.repairedAt; } + @Override public boolean isTransient() { return sstableMetadata.isTransient; @@ -1199,6 +1396,7 @@ public abstract static class Operator final static class Equals extends Operator { + @Override public int apply(int comparison) { return -comparison; @@ -1207,6 +1405,7 @@ public int apply(int comparison) final static class GreaterThanOrEqualTo extends Operator { + @Override public int apply(int comparison) { return comparison >= 0 ? 0 : 1; @@ -1215,6 +1414,7 @@ public int apply(int comparison) final static class GreaterThan extends Operator { + @Override public int apply(int comparison) { return comparison > 0 ? 0 : 1; @@ -1232,6 +1432,7 @@ public EstimatedHistogram getEstimatedCellPerPartitionCount() return sstableMetadata.estimatedCellPerPartitionCount; } + @Override public double getEstimatedDroppableTombstoneRatio(long gcBefore) { return sstableMetadata.getEstimatedDroppableTombstoneRatio(gcBefore); @@ -1247,21 +1448,25 @@ public double getCompressionRatio() return sstableMetadata.compressionRatio; } + @Override public long getMinTimestamp() { return sstableMetadata.minTimestamp; } + @Override public long getMaxTimestamp() { return sstableMetadata.maxTimestamp; } + @Override public long getMinLocalDeletionTime() { return sstableMetadata.minLocalDeletionTime; } + @Override public long getMaxLocalDeletionTime() { return sstableMetadata.maxLocalDeletionTime; @@ -1274,6 +1479,7 @@ public long getMaxLocalDeletionTime() * cell tombstone, no range tombstone maker and no expiring columns), but having it return {@code true} doesn't * guarantee it contains any as it may simply have non-expired cells. */ + @Override public boolean mayHaveTombstones() { // A sstable is guaranteed to have no tombstones if minLocalDeletionTime is still set to its default, @@ -1308,6 +1514,7 @@ public int getAvgColumnSetPerRow() : (sstableMetadata.totalRows == 0 ? 0 : (int) (sstableMetadata.totalColumnsSet / sstableMetadata.totalRows)); } + @Override public int getSSTableLevel() { return sstableMetadata.sstableLevel; @@ -1316,12 +1523,21 @@ public int getSSTableLevel() /** * Mutate sstable level with a lock to avoid racing with entire-sstable-streaming and then reload sstable metadata */ + @Override public void mutateLevelAndReload(int newLevel) throws IOException { - synchronized (tidy.global) + try { - descriptor.getMetadataSerializer().mutateLevel(descriptor, newLevel); - reloadSSTableMetadata(); + synchronized (tidy.global) + { + descriptor.getMetadataSerializer().mutateLevel(descriptor, newLevel); + reloadSSTableMetadata(); + } + } + catch (IOException e) + { + markSuspect(); + throw e; } } @@ -1356,15 +1572,26 @@ public StatsMetadata getSSTableMetadata() return sstableMetadata; } - public RandomAccessReader openDataReader(RateLimiter limiter) + /** + * This method accesses directly the compactionMetadata field without loading it from disk in case it was not loaded yet. + * It is used only by ForwardingSSTableReader in tests. + * @return the internal value of the field + */ + @VisibleForTesting + Optional getCompactionMetadataUnsafe() + { + return compactionMetadata; + } + + public RandomAccessReader openDataReader(RateLimiter limiter, ReadPattern accessPattern) { assert limiter != null; - return dfile.createReader(limiter); + return dfile.createReader(limiter, accessPattern); } - public RandomAccessReader openDataReader() + public RandomAccessReader openDataReader(ReadPattern accessPattern) { - return dfile.createReader(); + return dfile.createReader(accessPattern); } public RandomAccessReader openDataReaderForScan() @@ -1375,7 +1602,7 @@ public RandomAccessReader openDataReaderForScan() public void trySkipFileCacheBefore(DecoratedKey key) { long position = getPosition(key, SSTableReader.Operator.GE); - NativeLibrary.trySkipCache(descriptor.fileFor(Components.DATA).absolutePath(), 0, position < 0 ? 0 : position); + INativeLibrary.instance.trySkipCache(getDataFile(), 0, position < 0 ? 0 : position); } public ChannelProxy getDataChannel() @@ -1401,6 +1628,14 @@ public void incrementReadCount() readMeter.mark(); } + /** + * Increment the total read count and read rate for accessing partition index. + */ + public void incrementIndexReadCount() + { + partitionIndexReadMeter.ifPresent(RestorableMeter::mark); + } + public EncodingStats stats() { // We could return sstable.header.stats(), but this may not be as accurate than the actual sstable stats (see @@ -1408,16 +1643,19 @@ public EncodingStats stats() return sstableMetadata.encodingStats; } + @Override public Ref tryRef() { return selfRef.tryRef(); } + @Override public Ref selfRef() { return selfRef; } + @Override public Ref ref() { return selfRef.ref(); @@ -1453,10 +1691,13 @@ public void addTo(Ref.IdentityCollection identities) } /** - * The method verifies whether the sstable may contain the provided key. The method does approximation using - * Bloom filter if it is present and if it is not, performs accurate check in the index. + * @return true if global reference exists for the physical sstable corresponding to the provided descriptor. */ - public abstract boolean mayContainAssumingKeyIsInRange(DecoratedKey key); + @VisibleForTesting + public static boolean hasGlobalReference(Descriptor descriptor) + { + return GlobalTidy.exists(descriptor); + } /** * One instance per SSTableReader we create. @@ -1472,7 +1713,7 @@ protected static final class InstanceTidier implements Tidy private final Descriptor descriptor; private final WeakReference owner; - private List closeables; + private List closeables; private Runnable runOnClose; private boolean isReplaced = false; @@ -1487,7 +1728,7 @@ protected static final class InstanceTidier implements Tidy public void setup(SSTableReader reader, boolean trackHotness, Collection closeables) { // get a new reference to the shared descriptor-type tidy - this.globalRef = GlobalTidy.get(reader); + this.globalRef = GlobalTidy.get(reader.getDescriptor()); this.global = globalRef.get(); if (trackHotness) global.ensureReadMeter(); @@ -1502,6 +1743,12 @@ private InstanceTidier(Descriptor descriptor, Owner owner) this.owner = new WeakReference<>(owner); } + public void addCloseable(AutoCloseable closeable) + { + if (closeable != null) + closeables.add(closeable); // Last added is first to be closed. + } + @Override public void tidy() { @@ -1524,66 +1771,75 @@ public void tidy() barrier = null; } - ScheduledExecutors.nonPeriodicTasks.execute(new Runnable() - { - public void run() - { - if (logger.isTraceEnabled()) - logger.trace("Async instance tidier for {}, before barrier", descriptor); + Runnable cleanup = new CleanupTask(barrier); + ScheduledExecutors.nonPeriodicTasks.execute(cleanup); + } - if (barrier != null) - barrier.await(); + public String name() + { + return descriptor.toString(); + } - if (logger.isTraceEnabled()) - logger.trace("Async instance tidier for {}, after barrier", descriptor); + private class CleanupTask implements Runnable + { + private final OpOrder.Barrier barrier; - Throwable exceptions = null; - if (runOnClose != null) try - { - runOnClose.run(); - } - catch (RuntimeException | Error ex) - { - logger.error("Failed to run on-close listeners for sstable " + descriptor.baseFile(), ex); - exceptions = ex; - } + public CleanupTask(OpOrder.Barrier barrier) { + this.barrier = barrier; + } - Throwable closeExceptions = Throwables.close(null, Iterables.filter(closeables, Objects::nonNull)); - if (closeExceptions != null) - { - logger.error("Failed to close some sstable components of " + descriptor.baseFile(), closeExceptions); - exceptions = Throwables.merge(exceptions, closeExceptions); - } + @Override + public void run() + { + if (logger.isTraceEnabled()) + logger.trace("Async instance tidier for {}, before barrier", descriptor); - try - { - globalRef.release(); - } - catch (RuntimeException | Error ex) - { - logger.error("Failed to release the global ref of " + descriptor.baseFile(), ex); - exceptions = Throwables.merge(exceptions, ex); - } + if (barrier != null) + barrier.await(); - if (exceptions != null) - JVMStabilityInspector.inspectThrowable(exceptions); + if (logger.isTraceEnabled()) + logger.trace("Async instance tidier for {}, after barrier", descriptor); - if (logger.isTraceEnabled()) - logger.trace("Async instance tidier for {}, completed", descriptor); + Throwable exceptions = null; + if (runOnClose != null) try + { + runOnClose.run(); + } + catch (RuntimeException | Error ex) + { + logger.error("Failed to run on-close listeners for sstable " + descriptor.baseFile(), ex); + exceptions = ex; } - @Override - public String toString() + Throwable closeExceptions = Throwables.close(null, Iterables.filter(closeables, Objects::nonNull)); + if (closeExceptions != null) { - return "Tidy " + descriptor.ksname + '.' + descriptor.cfname + '-' + descriptor.id; + logger.error("Failed to close some sstable components of " + descriptor.baseFile(), closeExceptions); + exceptions = Throwables.merge(exceptions, closeExceptions); } - }); - } - @Override - public String name() - { - return descriptor.toString(); + try + { + globalRef.release(); + } + catch (RuntimeException | Error ex) + { + logger.error("Failed to release the global ref of " + descriptor.baseFile(), ex); + exceptions = Throwables.merge(exceptions, ex); + } + + if (exceptions != null) + JVMStabilityInspector.inspectThrowable(exceptions); + + if (logger.isTraceEnabled()) + logger.trace("Async instance tidier for {}, completed", descriptor); + } + + @Override + public String toString() + { + return "Tidy " + descriptor.ksname + '.' + descriptor.cfname + '-' + descriptor.id; + } } } @@ -1595,7 +1851,7 @@ public String name() * and stash a reference to it to be released when they are. Once all such references are * released, this shared tidy will be performed. */ - static final class GlobalTidy implements Tidy + public static final class GlobalTidy implements RefCounted.Tidy { static final WeakReference> NULL = new WeakReference<>(null); // keyed by descriptor, mapping to the shared GlobalTidy for that descriptor @@ -1609,11 +1865,11 @@ static final class GlobalTidy implements Tidy // sstable have been released private WeakReference> readMeterSyncFuture = NULL; // shared state managing if the logical sstable has been compacted; this is used in cleanup - private volatile Runnable obsoletion; + private volatile AbstractLogTransaction.ReaderTidier obsoletion; - GlobalTidy(final SSTableReader reader) + GlobalTidy(Descriptor descriptor) { - this.desc = reader.descriptor; + this.desc = descriptor; } void ensureReadMeter() @@ -1631,6 +1887,13 @@ void ensureReadMeter() return; } + if (!DatabaseDescriptor.supportsSSTableReadMeter()) + { + readMeter = RestorableMeter.createWithDefaultRates(); + readMeterSyncFuture = NULL; + return; + } + readMeter = SystemKeyspace.getSSTableReadMeter(desc.ksname, desc.cfname, desc.id); // sync the average read rate to system.sstable_activity every five minutes, starting one minute from now readMeterSyncFuture = new WeakReference<>(syncExecutor.scheduleAtFixedRate(this::maybePersistSSTableReadMeter, 1, 5, TimeUnit.MINUTES)); @@ -1655,48 +1918,142 @@ private void stopReadMeterPersistence() } } + /** + * Used by CNDB RepairRemoteStorageHandler to abort existing tidier before reloading sstable with orphan reference + * + * @return sstable reader tidier if exists + */ + @Nullable + public AbstractLogTransaction.ReaderTidier getTidier() + { + return obsoletion; + } + + /** + * Used by CNDB RepairRemoteStorageHandler to reset reader tidier before reloading sstable with orphan reference + * @param tidier new reader tidier for the global tidy. could be null + */ + public void setTidier(@Nullable AbstractLogTransaction.ReaderTidier tidier) + { + this.obsoletion = tidier; + } + public void tidy() { - lookup.remove(desc); + // Before proceeding with lookup.remove(desc) and with the tidier, + // make sure this instance is actually the one stored in the lookup. + // If there is no instance stored, or if the referent is not this + // instance, then this GlobalTidy instance was created in GlobalTidy.get() + // because of a race, and should not remove the real tidy from the lookup, + // or perform any cleanup + Ref existing = lookup.get(desc); + if (existing == null || !existing.refers(this)) + { + return; + } - if (obsoletion != null) - obsoletion.run(); + try + { + // don't ideally want to dropPageCache for the file until all instances have been released + StorageProvider.instance.invalidateFileSystemCache(desc, obsoletion != null); - // don't ideally want to dropPageCache for the file until all instances have been released - for (Component c : desc.discoverComponents()) - NativeLibrary.trySkipCache(desc.fileFor(c).absolutePath(), 0, 0); + if (obsoletion != null) + obsoletion.commit(); + } + finally + { + // remove reference after deleting local files, to avoid racing with {@link GlobalTidy#exists} + boolean removed = lookup.remove(desc, existing); + if (!removed) + { + throw new IllegalStateException("the reference changed behind our back? existing: " + existing + ", in lookup: " + lookup.get(desc)); + } + } } + @Override public String name() { return desc.toString(); } // get a new reference to the shared GlobalTidy for this sstable - public static Ref get(SSTableReader sstable) + public static Ref get(Descriptor descriptor) { - Descriptor descriptor = sstable.descriptor; - - while (true) + for (Ref globallySharedTidy = null;;) { - Ref ref = lookup.get(descriptor); - if (ref == null) + if (globallySharedTidy == null) + { + globallySharedTidy = lookup.get(descriptor); + } + if (globallySharedTidy != null) + { + // there's a potentialy alive ref in our lookup table; + // try to bump the counter + Ref newRef = globallySharedTidy.tryRef(); + if (newRef != null) + { + // the Ref was alive, bumping ref count succeeded. + // we're ok to return the newRef + return newRef; + } + else + { + // bumping ref count failed => ref count dropped to zero; tidy is in progress + // globallySharedTidy is a dead reference + // active waiting for tidy to complete and remove + // the old entry from the lookup table; + globallySharedTidy = null; + Thread.yield(); + } + } + else { - final GlobalTidy tidy = new GlobalTidy(sstable); - ref = new Ref<>(tidy, tidy); - Ref ex = lookup.putIfAbsent(descriptor, ref); - if (ex == null) - return ref; - ref = ex; + // there is no entry in the lookup table for this sstable + // let's create one and memoize it (if we're lucky) + + final GlobalTidy tidy = new GlobalTidy(descriptor); + Ref newRef = new Ref<>(tidy, tidy); + globallySharedTidy = lookup.putIfAbsent(descriptor, newRef); + if (globallySharedTidy != null) + { + // we raced with another put; tough luck, lets try again + // we've got to clean up the just-created Ref + // it's OK to close this Ref because GlobalTidy.tidy() is a no-op if + // the ref in lookup is different + newRef.close(); + } + else + { + // put succeeded; returning reference + return newRef; + } } + } + } - Ref newRef = ref.tryRef(); - if (newRef != null) - return newRef; + public static boolean exists(Descriptor descriptor) + { + return lookup.containsKey(descriptor); + } - // raced with tidy - lookup.remove(descriptor, ref); + private static boolean hasTidier(Descriptor descriptor) + { + Ref globalTidyRef = lookup.get(descriptor); + if (globalTidyRef != null) + { + try + { + GlobalTidy globalTidy = globalTidyRef.get(); + if (globalTidy != null) + return globalTidy.obsoletion != null; + } + catch (AssertionError e) + { + // ignore, we're just checking if the tidier exists + } } + return false; } } @@ -1732,6 +2089,12 @@ public final boolean equals(Object o) PartitionPositionBounds that = (PartitionPositionBounds) o; return lowerPosition == that.lowerPosition && upperPosition == that.upperPosition; } + + @Override + public String toString() + { + return String.format("(%d, %d)", lowerPosition, upperPosition); + } } public static class IndexesBounds @@ -1846,12 +2209,27 @@ public long logicalBytesOnDisk() private long bytesOnDisk(boolean logical) { long bytes = 0; - for (Component component : components) + for (Component component : components()) { // Only the data file is compressable. - bytes += logical && component == Components.DATA && compression - ? getCompressionMetadata().dataLength - : descriptor.fileFor(component).length(); + if (logical && component == Components.DATA && compression) + { + // For encrypted files or shallow readers, dfile may be null + if (dfile != null) + { + CompressionMetadata metadata = dfile.compressionMetadata().orElse(null); + bytes += metadata != null ? metadata.dataLength : dfile.dataLength(); + } + else + { + // For shallow readers without dfile, use file length from descriptor + bytes += descriptor.fileFor(component).length(); + } + } + else + { + bytes += descriptor.fileFor(component).length(); + } } return bytes; } @@ -1863,7 +2241,7 @@ public void maybePersistSSTableReadMeter() } /** - * Returns a new verifier for this sstable. Note that the reader must match the provided cfs. + * Returns a new verifier for this sstable. Note that the reader must match the provided cfs unless cfs is null. */ public abstract IVerifier getVerifier(ColumnFamilyStore cfs, OutputHandler outputHandler, @@ -1920,6 +2298,7 @@ public abstract static class Builder compactionMetadata; private OpenReason openReason; private SerializationHeader serializationHeader; private FileHandle dataFile; @@ -1946,6 +2325,13 @@ public B setStatsMetadata(StatsMetadata statsMetadata) return (B) this; } + public B setCompactionMetadata(Optional compactionMetadata) + { +// Preconditions.checkNotNull(compactionMetadata); + this.compactionMetadata = compactionMetadata != null ? compactionMetadata : Optional.empty(); + return (B) this; + } + public B setOpenReason(OpenReason openReason) { Preconditions.checkNotNull(openReason); @@ -1993,6 +2379,11 @@ public StatsMetadata getStatsMetadata() return statsMetadata; } + public Optional getCompactionMetadata() + { + return compactionMetadata; + } + public OpenReason getOpenReason() { return openReason; diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java index aedd860922e1..4bf7a9c28601 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java @@ -61,6 +61,7 @@ public SSTableReaderLoadingBuilder(SSTable.Builder builder) { this.descriptor = builder.descriptor; this.components = builder.getComponents() != null ? ImmutableSet.copyOf(builder.getComponents()) : TOCComponent.loadOrCreate(this.descriptor); + this.tableMetadataRef = builder.getTableMetadataRef() != null ? builder.getTableMetadataRef() : resolveTableMetadataRef(); this.ioOptions = builder.getIOOptions() != null ? builder.getIOOptions() : IOOptions.fromDatabaseDescriptor(); this.chunkCache = builder.getChunkCache() != null ? builder.getChunkCache() : ChunkCache.instance; diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderWithFilter.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderWithFilter.java index 5aac1d622c30..07db4322f33a 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderWithFilter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderWithFilter.java @@ -18,31 +18,47 @@ package org.apache.cassandra.io.sstable.format; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.Lists; +import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableReadsListener; import org.apache.cassandra.io.sstable.filter.BloomFilterTracker; +import org.apache.cassandra.utils.BloomFilter; +import org.apache.cassandra.utils.FilterFactory; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.IFilter; +import org.apache.cassandra.utils.concurrent.Ref; import static org.apache.cassandra.utils.concurrent.SharedCloseable.sharedCopyOrNull; public abstract class SSTableReaderWithFilter extends SSTableReader { - private final IFilter filter; + protected volatile IFilter filter; + private final AtomicBoolean bfDeserializationStarted = new AtomicBoolean(false); + private final boolean bloomFilterLazyLoading = BloomFilter.lazyLoading(); + private final int bloomFilterLazyLoadingWindow = BloomFilter.lazyLoadingWindow(); + private final long bloomFilterLazyLoadingThreshold = BloomFilter.lazyLoadingThreshold(); + protected volatile long approximateBloomFilterMemorySize; + private final BloomFilterTracker filterTracker; protected SSTableReaderWithFilter(Builder builder, Owner owner) { super(builder, owner); this.filter = Objects.requireNonNull(builder.getFilter()); - this.filterTracker = new BloomFilterTracker(); + this.filterTracker = owner().map(Owner::getBloomFilterTracker) + .orElseGet(BloomFilterTracker::createNoopTracker); } @Override @@ -69,9 +85,187 @@ protected boolean isPresentInFilter(IFilter.FilterKey key) @Override public boolean mayContainAssumingKeyIsInRange(DecoratedKey key) { - // if we don't have bloom filter(bf_fp_chance=1.0 or filter file is missing), - // we check index file instead. - return !filter.isInformative() && getPosition(key, Operator.EQ, false) >= 0 || filter.isPresent(key); + maybeDeserializeLazyBloomFilter(); + + if (filter.isInformative()) + { + recordBloomFilterHit(); + return filter.isPresent(key); + } + + if (isPassThroughBloomFilter()) + { + recordBloomFilterHit(); + return true; + } + + // Lazy bloom filters must check the index both to answer accurately and to drive the lazy-loading threshold. + return getPosition(key, Operator.EQ, false) >= 0 || filter.isPresent(key); + } + + protected boolean inBloomFilter(DecoratedKey dk) + { + recordBloomFilterHit(); + maybeDeserializeLazyBloomFilter(); + return filter.isPresent(dk); + } + + private void recordBloomFilterHit() + { + // There could be a race where async BF loading completes before calling filter.isPresent(key). + // That is acceptable because the counter records the state observed before this lookup. + if (isLazyBloomFilter()) + filterTracker.addLazyBloomFilterHit(); + else if (isPassThroughBloomFilter()) + filterTracker.addPassThroughBloomFilterHit(); + else + filterTracker.addLoadedBloomFilterHit(); + } + + /** + * If underlying bloom filter is {@link BloomFilter}. + */ + public boolean isBloomFilterLoaded() + { + return !isLazyBloomFilter() && !isPassThroughBloomFilter(); + } + + /** + * If underlying filter is {@link FilterFactory#AlwaysPresentForLazyLoading}. + */ + public boolean isLazyBloomFilter() + { + return filter == FilterFactory.AlwaysPresentForLazyLoading; + } + + /** + * If underlying filter is {@link FilterFactory#AlwaysPresent}: + * - BF file was not written when hitting BF memory limit during flush + * - BF deserialization hits BF memory limit + * - Lazy BF failed to load + */ + public boolean isPassThroughBloomFilter() + { + return filter == FilterFactory.AlwaysPresent; + } + + protected long computeExpectedBloomFilterMemorySize() + { + return FilterFactory.getFilterOffHeapSize(estimatedKeys(), metadata().params.bloomFilterFpChance); + } + + /** + * Returns the approximate off-heap memory size in bytes that the bloom filter for this SSTable would occupy when + * fully loaded based on estimated keys and false-positive chance, regardless of the current bloom filter state + * (either lazy BF or no BF due to memory limit or loading failure). + */ + public long getApproximateBloomFilterMemorySize() + { + return approximateBloomFilterMemorySize; + } + + @VisibleForTesting + public boolean isLazyBloomFilterByRequestRateCriteria() + { + return isLazyBloomFilter() + && bloomFilterLazyLoadingWindow > 0 + && bloomFilterLazyLoadingThreshold > 0 + && !partitionIndexHitRateExceedsThreshold(); + } + + public boolean isLazyBloomFilterByRequestCountCriteria() + { + return isLazyBloomFilter() + && (bloomFilterLazyLoadingThreshold == 0 + || bloomFilterLazyLoadingWindow <= 0 && !partitionIndexHitCountExceedsThreshold()); + } + + /** + * Defer BF deserialization when enabled to reduce memory pressure in use case where many sstables are not accessed frequently + * + * @return true if BF deserialization is attempted; false otherwise. + */ + @VisibleForTesting + boolean maybeDeserializeLazyBloomFilter() + { + if (!bloomFilterLazyLoading || filter != FilterFactory.AlwaysPresentForLazyLoading) + return false; + + Preconditions.checkState(partitionIndexReadMeter.isPresent(), "Read index meter should have been available"); + + boolean loadBloomFilter = false; + + // If the threshold was set to zero we always want to deserialize on first access + if (bloomFilterLazyLoadingThreshold == 0) + loadBloomFilter = true; + // otherwise, if window is <= 0 we use the threshold as an absolute count + else if (bloomFilterLazyLoadingWindow <= 0 && partitionIndexHitCountExceedsThreshold()) + loadBloomFilter = true; + // otherwise we look at the count in the specified window + else if (bloomFilterLazyLoadingWindow > 0 && partitionIndexHitRateExceedsThreshold()) + loadBloomFilter = true; + + if (!loadBloomFilter) + return false; + + // concurrent reads should only trigger async bloom filter deserialization once + if (!bfDeserializationStarted.compareAndSet(false, true)) + return false; + + Stage.IO.execute(() -> + { + logger.debug("Deserializing lazy bloom filter for {}", descriptor.baseFileURI()); + + // hold sstable reference to prevent sstable being released before bloom filter deserialization completes + Ref ref = tryRef(); + if (ref == null) + { + logger.error("Unable to reference sstable {}, will use pass-through bloom filter", descriptor.baseFileUri()); + filter = FilterFactory.AlwaysPresent; + } + else + { + try + { + // the only recoverable BF deserialization error is remote storage timeout; but it should be + // fine to continue with pass-through filter and wait for compaction to replace current sstable. + IFilter loaded = FilterComponent.load(descriptor); + if (loaded == null) + { + filter = FilterFactory.AlwaysPresent; + logger.error("Failed to deserialize lazy bloom filter, will use pass-through bloom filter"); + } + else + { + logger.debug("Successfuly loaded lazy bloom filter for {} with offheap size {}", descriptor.baseFileURI(), + FBUtilities.prettyPrintMemory(loaded.offHeapSize())); + + filter = loaded; + tidy.addCloseable(loaded); // close newly created bloom filter on sstable close + } + } + catch (IOException e) + { + logger.info("Bloom filter for " + descriptor + " could not be deserialized", e); + } + finally + { + ref.release(); + } + } + }); + + return true; + } + + private boolean partitionIndexHitRateExceedsThreshold() + { + return partitionIndexReadMeter.map(meter -> meter.rate(bloomFilterLazyLoadingWindow) >= bloomFilterLazyLoadingThreshold).orElse(false); + } + + private boolean partitionIndexHitCountExceedsThreshold() + { + return partitionIndexReadMeter.map(meter -> meter.count() >= bloomFilterLazyLoadingThreshold).orElse(false); } @Override @@ -122,6 +316,12 @@ public long getFilterOffHeapSize() { return filter.offHeapSize(); } + + @VisibleForTesting + public IFilter getFilter() + { + return filter; + } public abstract SSTableReaderWithFilter cloneAndReplace(IFilter filter); diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java index 28035a85da0b..6308640b6f7d 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java @@ -39,7 +39,9 @@ import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.AbstractIterator; @@ -75,12 +77,22 @@ protected SSTableScanner(S sstable, { assert sstable != null; - this.dfile = sstable.openDataReaderForScan(); - this.sstable = sstable; - this.columns = columns; - this.dataRange = dataRange; - this.rangeIterator = rangeIterator; - this.listener = listener; + RandomAccessReader dfile = null; + try + { + dfile = sstable.openDataReader(ReadPattern.SEQUENTIAL); + this.sstable = sstable; + this.columns = columns; + this.dataRange = dataRange; + this.rangeIterator = rangeIterator; + this.listener = listener; + } + catch (Throwable t) + { + FileUtils.closeQuietly(dfile); + throw t; + } + this.dfile = dfile; } protected static List> makeBounds(SSTableReader sstable, DataRange dataRange) diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java index a649fbea4c33..445bf9b9a7e9 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java @@ -40,7 +40,7 @@ /// Simple SSTable scanner that reads sequentially through an SSTable without using the index. /// /// This is a significant improvement for the performance of compaction over using the full-blown DataRange-capable -/// [SSTableScanner] and enables correct calculation of data sizes to process. +/// SSTable scanners and enables correct calculation of data sizes to process. public class SSTableSimpleScanner implements ISSTableScanner { @@ -126,6 +126,12 @@ public Set getBackingSSTables() return ImmutableSet.of(sstable); } + @Override + public int level() + { + return sstable.getSSTableLevel(); + } + public TableMetadata metadata() { return sstable.metadata(); diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java index 6714b93a2c0a..8b4b58853229 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java @@ -29,6 +29,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import javax.annotation.Nullable; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; @@ -37,6 +39,7 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.compaction.writers.SSTableDataSink; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.AbstractBounds; @@ -49,6 +52,7 @@ import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.SSTableFlushObserver; import org.apache.cassandra.io.sstable.SSTableZeroCopyWriter; +import org.apache.cassandra.io.sstable.StorageHandler; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.sstable.metadata.MetadataComponent; @@ -66,7 +70,7 @@ * {@link Builder}, a {@link LifecycleNewTracker} and {@link SSTable.Owner} instances. Implementing classes should * not extend that list and all the additional properties should be included in the builder. */ -public abstract class SSTableWriter extends SSTable implements Transactional +public abstract class SSTableWriter extends SSTable implements Transactional, SSTableDataSink { private final static Logger logger = LoggerFactory.getLogger(SSTableWriter.class); @@ -111,7 +115,7 @@ protected SSTableWriter(Builder builder, LifecycleNewTracker lifecycleNewT // sstable files were created before the sstable is registered in the lifecycle transaction, which may lead // to a race such that the sstable is listed as completed due to the lack of the transaction file before // anything is actually written to it. - Set existingComponents = Sets.filter(components, c -> descriptor.fileFor(c).exists()); + Set existingComponents = Sets.filter(components(), c -> descriptor.fileFor(c).exists()); assert existingComponents.isEmpty() : String.format("Cannot create a new SSTable in directory %s as component files %s already exist there", descriptor.directory, existingComponents); @@ -124,7 +128,7 @@ protected SSTableWriter(Builder builder, LifecycleNewTracker lifecycleNewT this.observers = Collections.unmodifiableList(observers); for (Index.Group group : builder.getIndexGroups()) { - SSTableFlushObserver observer = group.getFlushObserver(descriptor, lifecycleNewTracker, metadata.getLocal()); + SSTableFlushObserver observer = group.getFlushObserver(descriptor, lifecycleNewTracker, metadata.getLocal(), keyCount); if (observer != null) { observer.begin(); @@ -155,7 +159,7 @@ protected void handleConstructionFailure(Throwable ex) logger.warn("Failed to open " + descriptor + " for writing", ex); for (int i = observers.size()-1; i >= 0; i--) observers.get(i).abort(ex); - descriptor.getFormat().deleteOrphanedComponents(descriptor, components); + descriptor.getFormat().deleteOrphanedComponents(descriptor, components()); lifecycleNewTracker.untrackNew(this); } @@ -233,10 +237,15 @@ public SSTableWriter setTokenSpaceCoverage(double rangeSpanned) return this; } - public void setOpenResult(boolean openResult) - { - txnProxy.openResult = openResult; - } + /** + * Open the resultant SSTableReader after it has been fully written. + * + * @param storageHandler the underlying storage handler. This is used in case of a failure opening the + * SSTableReader to call the `StorageHandler#onOpeningWrittenSSTableFailure` callback, which + * in some implementations may attempt to recover from the error. If `null`, the said callback + * will not be called on failure. + */ + public abstract void openResult(@Nullable StorageHandler storageHandler); /** * Open the resultant SSTableReader before it has been fully written. @@ -258,11 +267,12 @@ public void setOpenResult(boolean openResult) protected abstract SSTableReader openFinal(SSTableReader.OpenReason openReason); - public SSTableReader finish(boolean openResult) + public SSTableReader finish(boolean openResult, @Nullable StorageHandler storageHandler) { - this.setOpenResult(openResult); - observers.forEach(SSTableFlushObserver::complete); - txnProxy.finish(); + prepareToCommit(); + if (openResult) + openResult(storageHandler); + txnProxy.commit(); return finished(); } @@ -279,7 +289,28 @@ public SSTableReader finished() // finalise our state on disk, including renaming public final void prepareToCommit() { - txnProxy.prepareToCommit(); + Throwable err = null; + try + { + txnProxy.prepareToCommit(); + } + catch (Throwable t) + { + err = t; + throw t; + } + finally + { + // do not notify in case of error + if (err == null) + { + // need to generate all index files before commit, so they will be included in txn log + observers.forEach(obs -> obs.complete(this)); + + // track newly written sstable after index files are written + lifecycleNewTracker.trackNewWritten(this); + } + } } // notify sstable flush observer about sstable writer switched @@ -290,16 +321,6 @@ public final void onSSTableWriterSwitched() public final Throwable commit(Throwable accumulate) { - try - { - observers.forEach(SSTableFlushObserver::complete); - } - catch (Throwable t) - { - // Return without advancing to COMMITTED, which will trigger abort() when the Transactional closes... - return Throwables.merge(accumulate, t); - } - return txnProxy.commit(accumulate); } @@ -332,7 +353,7 @@ public final void abort() } } - protected Map finalizeMetadata() + protected final Map finalizeMetadata() { return metadataCollector.finalizeMetadata(getPartitioner().getClass().getCanonicalName(), metadata().params.bloomFilterFpChance, @@ -344,11 +365,6 @@ protected Map finalizeMetadata() last.retainable().getKey()); } - protected StatsMetadata statsMetadata() - { - return (StatsMetadata) finalizeMetadata().get(MetadataType.STATS); - } - public void releaseMetadataOverhead() { metadataCollector.release(); @@ -372,7 +388,6 @@ protected class TransactionalProxy extends AbstractTransactional private final Supplier> transactionals; private SSTableReader finalReader; - private boolean openResult; private boolean finalReaderAccessed; public TransactionalProxy(Supplier> transactionals) @@ -380,17 +395,64 @@ public TransactionalProxy(Supplier> transactionals) this.transactionals = transactionals; } + public void openResult(@Nullable StorageHandler storageHandler) + { + openResultInternal(storageHandler); + } + // finalise our state on disk, including renaming protected void doPrepare() { transactionals.get().forEach(Transactional::prepareToCommit); - new StatsComponent(finalizeMetadata()).save(descriptor); + new StatsComponent(descriptor, finalizeMetadata()).save(descriptor); // save the table of components - TOCComponent.updateTOC(descriptor, components); + TOCComponent.updateTOC(descriptor, components()); + } - if (openResult) + protected void openResultInternal(@Nullable StorageHandler storageHandler) + { + try + { finalReader = openFinal(SSTableReader.OpenReason.NORMAL); + } + catch (Throwable t) + { + if (storageHandler != null) + { + Map finalMetadata = finalizeMetadata(); + StatsMetadata stats = (StatsMetadata) finalMetadata.get(MetadataType.STATS); + + // Try to get file pointers, but handle the case where channels may already be closed + long compressedSize = 0; + long uncompressedSize = 0; + try + { + compressedSize = getOnDiskFilePointer(); + uncompressedSize = getFilePointer(); + } + catch (IllegalStateException e) + { + // Channel may already be closed due to the error - use 0 as fallback + logger.debug("Could not get file pointers after error (channel likely closed): {}", e.getMessage()); + } + + finalReader = storageHandler.onOpeningWrittenSSTableFailure(SSTableReader.OpenReason.NORMAL, + descriptor, + components(), + compressedSize, + uncompressedSize, + stats, + first, + last, + stats.totalRows, + t); + } + else + { + throw Throwables.unchecked(t); + } + } } protected Throwable doCommit(Throwable accumulate) @@ -508,7 +570,7 @@ private static Set indexComponents(Collection indexGroup Set components = new HashSet<>(); for (Index.Group group : indexGroups) { - components.addAll(group.getComponents()); + components.addAll(group.componentsForNewSSTable()); } return components; diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java index 46b65140c54e..4e0d9e573117 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java @@ -26,6 +26,7 @@ import org.apache.cassandra.db.SerializationHeader; import org.apache.cassandra.db.rows.RangeTombstoneMarker; import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; import org.apache.cassandra.db.rows.SerializationHelper; import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredSerializer; @@ -56,6 +57,9 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable protected DeletionTime openMarker = DeletionTime.LIVE; protected DeletionTime startOpenMarker = DeletionTime.LIVE; + private DecoratedKey lastKey; + private DeletionTime lastPartitionLevelDeletion; + // Sequence control, also used to add empty static row if `addStaticRow` is not called. private enum State { @@ -104,6 +108,9 @@ public void start(DecoratedKey key, DeletionTime partitionLevelDeletion) throws ByteBufferUtil.writeWithShortLength(key.getKey(), writer); DeletionTime.getSerializer(version).serialize(partitionLevelDeletion, writer); + lastKey = key; + lastPartitionLevelDeletion = partitionLevelDeletion; + if (!header.hasStatic()) { this.headerLength = writer.position() - initialPosition; @@ -127,6 +134,17 @@ public void addStaticRow(Row staticRow) throws IOException public void addUnfiltered(Unfiltered unfiltered) throws IOException { + if (state == State.AWAITING_STATIC_ROW) + { + if (unfiltered.isRow() && ((Row) unfiltered).isStatic()) + { + addStaticRow((Row) unfiltered); + return; + } + + addStaticRow(Rows.EMPTY_STATIC_ROW); + } + checkState(state == State.AWAITING_ROWS); long pos = currentPosition(); @@ -155,6 +173,8 @@ public void addUnfiltered(Unfiltered unfiltered) throws IOException protected long finish() throws IOException { + if (state == State.AWAITING_STATIC_ROW) + addStaticRow(Rows.EMPTY_STATIC_ROW); checkState(state == State.AWAITING_ROWS); state = State.COMPLETED; @@ -162,6 +182,9 @@ protected long finish() throws IOException long endPosition = currentPosition(); unfilteredSerializer.writeEndOfPartition(writer); + lastKey = null; + lastPartitionLevelDeletion = null; + return endPosition; } @@ -174,4 +197,14 @@ public long getInitialPosition() { return initialPosition; } + + public DecoratedKey getLastKey() + { + return lastKey; + } + + public DeletionTime getLastPartitionLevelDeletion() + { + return lastPartitionLevelDeletion; + } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTableReaderLoadingBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTableReaderLoadingBuilder.java index 4b647549cdd9..7190a3ee725a 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableReaderLoadingBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableReaderLoadingBuilder.java @@ -25,6 +25,7 @@ import org.apache.cassandra.io.sstable.format.bti.BtiFormat; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.sstable.metadata.ValidationMetadata; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.IFilter; @@ -58,11 +59,12 @@ protected FileHandle.Builder dataFileBuilder(StatsMetadata statsMetadata) int bufferSize = ioOptions.diskOptimizationStrategy.bufferSize(recordSize); if (dataFileBuilder == null) - dataFileBuilder = new FileHandle.Builder(descriptor.fileFor(BtiFormat.Components.DATA)); + dataFileBuilder = StorageProvider.instance.fileHandleBuilderFor(descriptor, BtiFormat.Components.DATA); dataFileBuilder.bufferSize(bufferSize); dataFileBuilder.withChunkCache(chunkCache); dataFileBuilder.mmapped(ioOptions.defaultDiskAccessMode); + dataFileBuilder.slice(statsMetadata.zeroCopyMetadata); return dataFileBuilder; } diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java index e8fbea22d279..96769bfa832e 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java @@ -39,12 +39,13 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.db.ClusteringComparator; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.LivenessInfo; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.partitions.ImmutableBTreePartition; import org.apache.cassandra.db.partitions.Partition; @@ -68,6 +69,7 @@ import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.ByteBufferUtil; @@ -84,7 +86,7 @@ public abstract class SortedTableScrubber imp { private final static Logger logger = LoggerFactory.getLogger(SortedTableScrubber.class); - protected final ColumnFamilyStore cfs; + protected final CompactionRealm realm; protected final LifecycleTransaction transaction; protected final File destination; protected final IScrubber.Options options; @@ -107,33 +109,33 @@ public abstract class SortedTableScrubber imp protected int emptyPartitions; - protected SortedTableScrubber(ColumnFamilyStore cfs, + protected SortedTableScrubber(CompactionRealm realm, LifecycleTransaction transaction, OutputHandler outputHandler, Options options) { this.sstable = (R) transaction.onlyOne(); Preconditions.checkNotNull(sstable.metadata()); - assert sstable.metadata().keyspace.equals(cfs.getKeyspaceName()); - if (!sstable.descriptor.cfname.equals(cfs.metadata().name)) + assert sstable.metadata().keyspace.equals(realm.getKeyspaceName()); + if (!sstable.descriptor.cfname.equals(realm.metadata().name)) { - logger.warn("Descriptor points to a different table {} than metadata {}", sstable.descriptor.cfname, cfs.metadata().name); + logger.warn("Descriptor points to a different table {} than metadata {}", sstable.descriptor.cfname, realm.metadata().name); } try { - sstable.metadata().validateCompatibility(cfs.metadata()); + sstable.metadata().validateCompatibility(realm.metadata()); } catch (ConfigurationException ex) { - logger.warn("Descriptor points to a different table {} than metadata {}", sstable.descriptor.cfname, cfs.metadata().name); + logger.warn("Descriptor points to a different table {} than metadata {}", sstable.descriptor.cfname, realm.metadata().name); } - this.cfs = cfs; + this.realm = realm; this.transaction = transaction; this.outputHandler = outputHandler; this.options = options; - this.destination = cfs.getDirectories().getLocationForDisk(cfs.getDiskBoundaries().getCorrectDiskForSSTable(sstable)); - this.isCommutative = cfs.metadata().isCounter(); + this.destination = realm.getDirectories().getLocationForDisk(realm.getDiskBoundaries().getCorrectDiskForSSTable(sstable)); + this.isCommutative = realm.metadata().isCounter(); List toScrub = Collections.singletonList(sstable); @@ -146,15 +148,16 @@ protected SortedTableScrubber(ColumnFamilyStore cfs, { approximateKeyCount = 0; } - this.expectedBloomFilterSize = Math.max(cfs.metadata().params.minIndexInterval, approximateKeyCount); + this.expectedBloomFilterSize = Math.max(realm.metadata().params.minIndexInterval, approximateKeyCount); // loop through each partition, deserializing to check for damage. // We'll also loop through the index at the same time, using the position from the index to recover if the // partition header (key or data size) is corrupt. (This means our position in the index file will be one // partition "ahead" of the data file.) - this.dataFile = transaction.isOffline() - ? sstable.openDataReader() - : sstable.openDataReader(CompactionManager.instance.getRateLimiter()); + boolean isOffline = options.overrideTxnIsOffline ? false : transaction.isOffline(); + this.dataFile = isOffline + ? sstable.openDataReader(ReadPattern.SEQUENTIAL) + : sstable.openDataReader(CompactionManager.instance.getRateLimiter(), ReadPattern.SEQUENTIAL); this.scrubInfo = new ScrubInfo(dataFile, sstable, fileAccessLock.readLock()); @@ -180,15 +183,15 @@ public static void deleteOrphanedComponents(Descriptor descriptor, Set scrub() { List finished = new ArrayList<>(); outputHandler.output("Scrubbing %s (%s)", sstable, FBUtilities.prettyPrintMemory(dataFile.length())); - try (SSTableRewriter writer = SSTableRewriter.construct(cfs, transaction, false, sstable.maxDataAge); + try (SSTableRewriter writer = SSTableRewriter.construct(realm, transaction, false, sstable.maxDataAge); Refs refs = Refs.ref(Collections.singleton(sstable))) { StatsMetadata metadata = sstable.getSSTableMetadata(); - writer.switchWriter(CompactionManager.createWriter(cfs, destination, expectedBloomFilterSize, metadata.repairedAt, metadata.pendingRepair, metadata.isTransient, sstable, transaction)); + writer.switchWriter(CompactionManager.createWriter(realm, destination, expectedBloomFilterSize, metadata.repairedAt, metadata.pendingRepair, metadata.isTransient, sstable, transaction)); scrubInternal(writer); @@ -205,11 +208,14 @@ public void scrub() } finally { - if (transaction.isOffline()) + boolean isOffline = options.overrideTxnIsOffline ? false : transaction.isOffline(); + if (isOffline) finished.forEach(sstable -> sstable.selfRef().release()); } outputSummary(finished); + + return finished; // already released } protected abstract void scrubInternal(SSTableRewriter writer) throws IOException; @@ -238,13 +244,13 @@ private SSTableReader writeOutOfOrderPartitions(StatsMetadata metadata) // out of order partitions/rows, but no bad partition found - we can keep our repairedAt time long repairedAt = badPartitions > 0 ? ActiveRepairService.UNREPAIRED_SSTABLE : sstable.getSSTableMetadata().repairedAt; SSTableReader newInOrderSstable; - try (SSTableWriter inOrderWriter = CompactionManager.createWriter(cfs, destination, expectedBloomFilterSize, repairedAt, metadata.pendingRepair, metadata.isTransient, sstable, transaction)) + try (SSTableWriter inOrderWriter = CompactionManager.createWriter(realm, destination, expectedBloomFilterSize, repairedAt, metadata.pendingRepair, metadata.isTransient, sstable, transaction)) { for (Partition partition : outOfOrder) inOrderWriter.append(partition.unfilteredIterator()); inOrderWriter.setRepairedAt(-1); inOrderWriter.setMaxDataAge(sstable.maxDataAge); - newInOrderSstable = inOrderWriter.finish(true); + newInOrderSstable = inOrderWriter.finish(true, null); } transaction.update(newInOrderSstable, false); outputHandler.warn("%d out of order partition (or partitions without of order rows) found while scrubbing %s; " + @@ -252,18 +258,18 @@ private SSTableReader writeOutOfOrderPartitions(StatsMetadata metadata) return newInOrderSstable; } - protected abstract UnfilteredRowIterator withValidation(UnfilteredRowIterator iter, String filename); + protected abstract UnfilteredRowIterator withValidation(UnfilteredRowIterator iter, File file); @Override @VisibleForTesting public ScrubResult scrubWithResult() { - scrub(); - return new ScrubResult(goodPartitions, badPartitions, emptyPartitions); + List scrubbed = scrub(); + return new ScrubResult(goodPartitions, badPartitions, emptyPartitions, scrubbed); } @Override - public CompactionInfo.Holder getScrubInfo() + public TableOperation getScrubInfo() { return scrubInfo; } @@ -275,7 +281,7 @@ protected String keyString(DecoratedKey key) try { - return cfs.metadata().partitionKeyType.getString(key.getKey()); + return realm.metadata().partitionKeyType.getString(key.getKey()); } catch (Exception e) { @@ -288,8 +294,8 @@ protected boolean tryAppend(DecoratedKey prevKey, DecoratedKey key, SSTableRewri // OrderCheckerIterator will check, at iteration time, that the rows are in the proper order. If it detects // that one row is out of order, it will stop returning them. The remaining rows will be sorted and added // to the outOfOrder set that will be later written to a new SSTable. - try (OrderCheckerIterator sstableIterator = new OrderCheckerIterator(getIterator(key), cfs.metadata().comparator); - UnfilteredRowIterator iterator = withValidation(sstableIterator, dataFile.getPath())) + try (OrderCheckerIterator sstableIterator = new OrderCheckerIterator(getIterator(key), realm.metadata().comparator); + UnfilteredRowIterator iterator = withValidation(sstableIterator, dataFile.getFile())) { if (prevKey != null && prevKey.compareTo(key) > 0) { @@ -358,7 +364,7 @@ protected void throwIfCannotContinue(DecoratedKey key, Throwable th) } - public static class ScrubInfo extends CompactionInfo.Holder + public static class ScrubInfo extends AbstractTableOperation { private final RandomAccessReader dataFile; private final SSTableReader sstable; @@ -373,18 +379,18 @@ public ScrubInfo(RandomAccessReader dataFile, SSTableReader sstable, Lock fileRe scrubCompactionId = nextTimeUUID(); } - public CompactionInfo getCompactionInfo() + public OperationProgress getProgress() { fileReadLock.lock(); try { - return new CompactionInfo(sstable.metadata(), - OperationType.SCRUB, - dataFile.getFilePointer(), - dataFile.length(), - scrubCompactionId, - ImmutableSet.of(sstable), - File.getPath(sstable.getFilename()).getParent().toString()); + return new OperationProgress(sstable.metadata(), + OperationType.SCRUB, + dataFile.getFilePointer(), + dataFile.length(), + scrubCompactionId, + ImmutableSet.of(sstable), + File.getPath(sstable.getFilename()).getParent().toString()); } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java index f68e1c968455..042819547433 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java @@ -19,6 +19,7 @@ package org.apache.cassandra.io.sstable.format; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.util.Collection; import java.util.Collections; @@ -28,25 +29,27 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; -import java.util.function.LongPredicate; + +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.compaction.CompactionController; -import org.apache.cassandra.db.compaction.CompactionInfo; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.dht.LocalPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.IVerifier; import org.apache.cassandra.io.sstable.KeyIterator; @@ -56,6 +59,7 @@ import org.apache.cassandra.io.util.DataIntegrityMetadata; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.ByteBufferUtil; @@ -68,7 +72,7 @@ public abstract class SortedTableVerifier imp { private final static Logger logger = LoggerFactory.getLogger(SortedTableVerifier.class); - protected final ColumnFamilyStore cfs; + protected final @Nullable CompactionRealm realm; protected final R sstable; protected final ReadWriteLock fileAccessLock; @@ -87,16 +91,18 @@ public abstract class SortedTableVerifier imp protected final OutputHandler outputHandler; - public SortedTableVerifier(ColumnFamilyStore cfs, R sstable, OutputHandler outputHandler, boolean isOffline, Options options) + public SortedTableVerifier(CompactionRealm realm, R sstable, OutputHandler outputHandler, boolean isOffline, Options options) { - this.cfs = cfs; + Preconditions.checkArgument(realm != null || !options.mutateRepairStatus); + + this.realm = realm; this.sstable = sstable; this.outputHandler = outputHandler; this.fileAccessLock = new ReentrantReadWriteLock(); this.dataFile = isOffline - ? sstable.openDataReader() - : sstable.openDataReader(CompactionManager.instance.getRateLimiter()); + ? sstable.openDataReader(ReadPattern.SEQUENTIAL) + : sstable.openDataReader(CompactionManager.instance.getRateLimiter(), ReadPattern.SEQUENTIAL); this.verifyInfo = new VerifyInfo(dataFile, sstable, fileAccessLock.readLock()); this.options = options; this.isOffline = isOffline; @@ -111,7 +117,7 @@ protected void deserializeBloomFilter(SSTableReader sstable) throws IOException } } - public CompactionInfo.Holder getVerifyInfo() + public AbstractTableOperation getVerifyInfo() { return verifyInfo; } @@ -127,8 +133,8 @@ protected void markAndThrow(Throwable cause, boolean mutateRepaired) { try { - sstable.mutateRepairedAndReload(ActiveRepairService.UNREPAIRED_SSTABLE, sstable.getPendingRepair(), sstable.isTransient()); - cfs.getTracker().notifySSTableRepairedStatusChanged(Collections.singleton(sstable)); + // note that it additionally uses a lock and verification + realm.mutateRepairedWithLock(Collections.singleton(sstable), ActiveRepairService.UNREPAIRED_SSTABLE, sstable.getPendingRepair(), sstable.isTransient()); } catch (IOException ioe) { @@ -152,7 +158,9 @@ public void verify() verifyBloomFilter(); - if (options.checkOwnsTokens && !isOffline && !(cfs.getPartitioner() instanceof LocalPartitioner)) + verifyStorageAttachedIndexes(); + + if (options.checkOwnsTokens && !isOffline && !(sstable.getPartitioner() instanceof LocalPartitioner)) { if (verifyOwnedRanges() == 0) return; @@ -183,6 +191,52 @@ protected void verifyBloomFilter() } } + protected void verifyStorageAttachedIndexes() + { + if (realm == null || isOffline) + return; + + try + { + SecondaryIndexManager indexManager = realm.getIndexManager(); + if (indexManager != null) + { + // Quick verification skips checksum validation + boolean validateChecksum = !options.quick; + outputHandler.debug("Validating storage attached indexes for %s", sstable); + logger.debug("SortedTableVerifier: Validating SAI for {}", sstable.getFilename()); + // Pass true for throwOnIncomplete to ensure incomplete indexes throw exceptions + boolean complete = indexManager.validateSSTableAttachedIndexes(Collections.singleton(sstable), true, validateChecksum); + if (!complete) + { + // This shouldn't happen since we're throwing on incomplete, but just in case + throw new IOException("SAI validation failed for " + sstable.getFilename()); + } + } + else + { + logger.debug("SortedTableVerifier: No index manager for {}", sstable.getFilename()); + } + } + catch (IllegalStateException e) + { + // Convert IllegalStateException from incomplete indexes to CorruptSSTableException + outputHandler.warn(e); + markAndThrow(new CorruptSSTableException(e, sstable.getFilename())); + } + catch (UncheckedIOException e) + { + // Convert UncheckedIOException from SAI validation to CorruptSSTableException + outputHandler.warn(e); + markAndThrow(new CorruptSSTableException(e.getCause(), sstable.getFilename())); + } + catch (Throwable t) + { + outputHandler.warn(t); + markAndThrow(t); + } + } + protected void verifySSTableMetadata() { outputHandler.output("Deserializing sstable metadata for %s ", sstable); @@ -218,7 +272,7 @@ protected int verifyOwnedRanges() outputHandler.debug("Checking that all tokens are owned by the current node"); try (KeyIterator iter = sstable.keyIterator()) { - ownedRanges = Range.normalize(tokenLookup.apply(cfs.metadata.keyspace)); + ownedRanges = Range.normalize(tokenLookup.apply(sstable.getKeyspaceName())); if (ownedRanges.isEmpty()) return 0; RangeOwnHelper rangeOwnHelper = new RangeOwnHelper(ownedRanges); @@ -268,21 +322,19 @@ protected void verifySSTable() { outputHandler.output("Extended Verify requested, proceeding to inspect values"); - try (VerifyController verifyController = new VerifyController(cfs); - KeyReader indexIterator = sstable.keyReader()) + try (KeyReader indexIterator = sstable.keyReader()) { - if (indexIterator.dataPosition() != 0) + if (indexIterator.dataPosition() != sstable.getDataFileSliceDescriptor().dataStart) markAndThrow(new RuntimeException("First row position from index != 0: " + indexIterator.dataPosition())); - List> ownedRanges = isOffline ? Collections.emptyList() : Range.normalize(tokenLookup.apply(cfs.metadata().keyspace)); + List> ownedRanges = isOffline ? Collections.emptyList() : Range.normalize(tokenLookup.apply(sstable.getKeyspaceName())); RangeOwnHelper rangeOwnHelper = new RangeOwnHelper(ownedRanges); DecoratedKey prevKey = null; while (!dataFile.isEOF()) { - if (verifyInfo.isStopRequested()) - throw new CompactionInterruptedException(verifyInfo.getCompactionInfo()); + verifyInfo.throwIfStopRequested(); long rowStart = dataFile.getFilePointer(); outputHandler.debug("Reading row at %d", rowStart); @@ -297,7 +349,7 @@ protected void verifySSTable() markAndThrow(th); } - if (options.checkOwnsTokens && ownedRanges.size() > 0 && !(cfs.getPartitioner() instanceof LocalPartitioner)) + if (options.checkOwnsTokens && !ownedRanges.isEmpty() && !(sstable.getPartitioner() instanceof LocalPartitioner)) { try { @@ -338,7 +390,9 @@ protected void verifySSTable() if (key == null || dataSize > dataFile.length()) markAndThrow(new RuntimeException(String.format("key = %s, dataSize=%d, dataFile.length() = %d", key, dataSize, dataFile.length()))); - try (UnfilteredRowIterator iterator = SSTableIdentityIterator.create(sstable, dataFile, key)) + //mimic the scrub read path + try (UnfilteredRowIterator identity = SSTableIdentityIterator.create(sstable, dataFile, key); + UnfilteredRowIterator iterator = UnfilteredRowIterators.withValidation(identity, dataFile.getFile())) { verifyPartition(key, iterator); } @@ -469,7 +523,7 @@ public boolean check(DecoratedKey key) } } - protected static class VerifyInfo extends CompactionInfo.Holder + protected static class VerifyInfo extends AbstractTableOperation { private final RandomAccessReader dataFile; private final SSTableReader sstable; @@ -484,17 +538,17 @@ public VerifyInfo(RandomAccessReader dataFile, SSTableReader sstable, Lock fileR verificationCompactionId = TimeUUID.Generator.nextTimeUUID(); } - public CompactionInfo getCompactionInfo() + public OperationProgress getProgress() { fileReadLock.lock(); try { - return new CompactionInfo(sstable.metadata(), - OperationType.VERIFY, - dataFile.getFilePointer(), - dataFile.length(), - verificationCompactionId, - ImmutableSet.of(sstable)); + return new OperationProgress(sstable.metadata(), + OperationType.VERIFY, + dataFile.getFilePointer(), + dataFile.length(), + verificationCompactionId, + ImmutableSet.of(sstable)); } catch (Exception e) { @@ -511,18 +565,4 @@ public boolean isGlobal() return false; } } - - protected static class VerifyController extends CompactionController - { - public VerifyController(ColumnFamilyStore cfs) - { - super(cfs, Integer.MAX_VALUE); - } - - @Override - public LongPredicate getPurgeEvaluator(DecoratedKey key) - { - return time -> false; - } - } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java index 473176be5bb7..dc0d0e139c4d 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; +import javax.annotation.concurrent.NotThreadSafe; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -33,6 +34,8 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionPurger; import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.writers.SSTableDataSink; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.guardrails.Threshold; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; @@ -49,6 +52,7 @@ import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.compress.CompressedSequentialWriter; import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.io.compress.EncryptedSequentialWriter; import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; @@ -56,6 +60,7 @@ import org.apache.cassandra.io.sstable.SSTableFlushObserver; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.DataPosition; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.SequentialWriter; @@ -69,25 +74,30 @@ import org.apache.cassandra.utils.concurrent.Transactional; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; /** * A generic implementation of a writer which assumes the existence of some partition index and bloom filter. */ -public abstract class SortedTableWriter

    extends SSTableWriter +@NotThreadSafe +public abstract class SortedTableWriter

    extends SSTableWriter implements SSTableDataSink { private final static Logger logger = LoggerFactory.getLogger(SortedTableWriter.class); + private final boolean isInternalKeyspace; // TODO dataWriter is not needed to be directly accessible - we can access everything we need for the dataWriter // from a partition writer protected final SequentialWriter dataWriter; protected final I indexWriter; protected final P partitionWriter; - private final FileHandle.Builder dataFileBuilder = new FileHandle.Builder(descriptor.fileFor(Components.DATA)); + private final FileHandle.Builder dataFileBuilder = StorageProvider.instance.fileHandleBuilderFor(descriptor, Components.DATA); private DecoratedKey lastWrittenKey; private DataPosition dataMark; private long lastEarlyOpenLength; private final Supplier crcCheckChanceSupplier; + private Throwable failure = null; + public SortedTableWriter(Builder builder, LifecycleNewTracker lifecycleNewTracker, SSTable.Owner owner) { super(builder, lifecycleNewTracker, owner); @@ -111,15 +121,23 @@ public SortedTableWriter(Builder builder, LifecycleNewTracker lifecy this.dataWriter = dataWriter; this.indexWriter = indexWriter; this.partitionWriter = partitionWriter; + this.isInternalKeyspace = SchemaConstants.isInternalKeyspace(metadata.keyspace); } catch (RuntimeException | Error ex) { Throwables.closeNonNullAndAddSuppressed(ex, partitionWriter, indexWriter, dataWriter); handleConstructionFailure(ex); + failure = ex; throw ex; } } + private void assertNotBroken() + { + if (failure != null) + throw new AssertionError("Cannot use a broken writer", failure); + } + /** * Appends partition data to this writer. * @@ -131,16 +149,16 @@ public SortedTableWriter(Builder builder, LifecycleNewTracker lifecy @Override public final AbstractRowIndexEntry append(UnfilteredRowIterator partition) { + assertNotBroken(); + if (partition.isEmpty()) return null; try { - if (!verifyPartition(partition.partitionKey())) + if (!startPartition(partition.partitionKey(), partition.partitionLevelDeletion())) return null; - startPartition(partition.partitionKey(), partition.partitionLevelDeletion()); - AbstractRowIndexEntry indexEntry; if (header.hasStatic()) addStaticRow(partition.partitionKey(), partition.staticRow()); @@ -154,14 +172,59 @@ public final AbstractRowIndexEntry append(UnfilteredRowIterator partition) } catch (BufferOverflowException boe) { + failure = boe; throw new PartitionSerializationException(partition, boe); } catch (IOException e) { + failure = e; throw new FSWriteError(e, getFilename()); } } + @Override + public final void addUnfiltered(Unfiltered unfiltered) + { + assertNotBroken(); + + try + { + if (unfiltered.isRow()) + { + Row row = (Row) unfiltered; + if (row.isStatic()) + addStaticRow(requireNonNull(partitionWriter.getLastKey()), row); + else + addRow(requireNonNull(partitionWriter.getLastKey()), row); + } + else + { + addRangeTomstoneMarker((RangeTombstoneMarker) unfiltered); + } + } + catch (IOException | RuntimeException ex) + { + failure = ex; + throw new FSWriteError(ex, getFilename()); + } + } + + @Override + public final AbstractRowIndexEntry endPartition() + { + assertNotBroken(); + + try + { + return endPartition(requireNonNull(partitionWriter.getLastKey()), partitionWriter.getLastPartitionLevelDeletion()); + } + catch (IOException | RuntimeException ex) + { + failure = ex; + throw new FSWriteError(ex, getFilename()); + } + } + private boolean verifyPartition(DecoratedKey key) { assert key != null : "Keys must not be null"; // empty keys ARE allowed b/c of indexed column values @@ -173,17 +236,33 @@ private boolean verifyPartition(DecoratedKey key) } if (lastWrittenKey != null && lastWrittenKey.compareTo(key) >= 0) - throw new RuntimeException(String.format("Last written key %s >= current key %s, writing into %s", lastWrittenKey, key, getFilename())); + throw new AssertionError("Last written key " + lastWrittenKey + " >= current key " + key + " writing into " + getDataFile()); return true; } - private void startPartition(DecoratedKey key, DeletionTime partitionLevelDeletion) throws IOException + @Override + public boolean startPartition(DecoratedKey key, DeletionTime partitionLevelDeletion) throws IOException { - partitionWriter.start(key, partitionLevelDeletion); - metadataCollector.updatePartitionDeletion(partitionLevelDeletion); + assertNotBroken(); + + if (!verifyPartition(key)) + return false; + + try + { + partitionWriter.start(key, partitionLevelDeletion); + metadataCollector.updatePartitionDeletion(partitionLevelDeletion); - onStartPartition(key); + onStartPartition(key); + } + catch (IOException | RuntimeException ex) + { + failure = ex; + throw ex; + } + + return true; } private void addStaticRow(DecoratedKey key, Row row) throws IOException @@ -347,16 +426,57 @@ protected FileHandle openDataFile(long lengthOverride, StatsMetadata statsMetada FileHandle dataFile; - try (CompressionMetadata compressionMetadata = compression ? ((CompressedSequentialWriter) dataWriter).open(lengthOverride) : null) + CompressionMetadata compressionMetadata = null; + if (compression) + { + if (dataWriter instanceof CompressedSequentialWriter) + { + compressionMetadata = ((CompressedSequentialWriter) dataWriter).open(lengthOverride); + } + else if (dataWriter instanceof EncryptedSequentialWriter) + { + // For encrypted writers, we need to create encryption-specific compression metadata + compressionMetadata = CompressionMetadata.encryptedOnly(metadata.getLocal().params.compression); + } + } + + try + { + FileHandle.Builder builder = dataFileBuilder.mmapped(ioOptions.defaultDiskAccessMode) + .withMmappedRegionsCache(mmappedRegionsCache) + .withChunkCache(chunkCache) + .bufferSize(dataBufferSize) + .withCrcCheckChance(crcCheckChanceSupplier); + + if (compressionMetadata != null) + { + builder.withCompressionMetadata(compressionMetadata); + } + + if (dataWriter instanceof EncryptedSequentialWriter) + { + ((EncryptedSequentialWriter) dataWriter).updateFileHandle(builder, lengthOverride); + } + else + { + builder.withLengthOverride(lengthOverride); + } + + dataFile = builder.complete(); + } + finally { - dataFile = dataFileBuilder.mmapped(ioOptions.defaultDiskAccessMode) - .withMmappedRegionsCache(mmappedRegionsCache) - .withChunkCache(chunkCache) - .withCompressionMetadata(compressionMetadata) - .bufferSize(dataBufferSize) - .withCrcCheckChance(crcCheckChanceSupplier) - .withLengthOverride(lengthOverride) - .complete(); + if (compressionMetadata != null) + { + try + { + compressionMetadata.close(); + } + catch (Exception e) + { + // ignore + } + } } try @@ -364,7 +484,7 @@ protected FileHandle openDataFile(long lengthOverride, StatsMetadata statsMetada if (chunkCache != null) { if (lastEarlyOpenLength != 0 && dataFile.dataLength() > lastEarlyOpenLength) - chunkCache.invalidatePosition(dataFile, lastEarlyOpenLength); + dataFile.rebuffererFactory().invalidateIfCached(lastEarlyOpenLength); } lastEarlyOpenLength = dataFile.dataLength(); } @@ -379,6 +499,9 @@ protected FileHandle openDataFile(long lengthOverride, StatsMetadata statsMetada private void guardPartitionThreshold(Threshold guardrail, DecoratedKey key, long size) { + if (isInternalKeyspace) + return; + if (guardrail.triggersOn(size, null)) { String message = String.format("%s.%s:%s on sstable %s", @@ -392,6 +515,9 @@ private void guardPartitionThreshold(Threshold guardrail, DecoratedKey key, long private void guardCollectionSize(DecoratedKey partitionKey, Row row) { + if (isInternalKeyspace) + return; + if (!Guardrails.collectionSize.enabled() && !Guardrails.itemsPerCollection.enabled()) return; @@ -418,16 +544,22 @@ private void guardCollectionSize(DecoratedKey partitionKey, Row row) !Guardrails.itemsPerCollection.triggersOn(cellsCount, null)) continue; - String keyString = metadata.getLocal().primaryKeyAsCQLLiteral(partitionKey.getKey(), row.clustering()); - String msg = String.format("%s in row %s in table %s", + String msg = String.format("%s in table %s", column.name.toString(), - keyString, metadata); - Guardrails.collectionSize.guard(cellsSize, msg, true, null); - Guardrails.itemsPerCollection.guard(cellsCount, msg, true, null); + Guardrails.collectionSize.guard(cellsSize, msg, false, null); + Guardrails.itemsPerCollection.guard(cellsCount, msg, false, null); } } + protected void invalidateCacheAtPreviousBoundary(FileHandle dfile, long newBoundary) + { + if (lastEarlyOpenLength != 0 && newBoundary > lastEarlyOpenLength) + dfile.rebuffererFactory().invalidateIfCached(lastEarlyOpenLength); + + lastEarlyOpenLength = newBoundary; + } + protected static abstract class AbstractIndexWriter extends AbstractTransactional implements Transactional { protected final Descriptor descriptor; @@ -442,7 +574,7 @@ protected AbstractIndexWriter(Builder b) this.metadata = b.getTableMetadataRef(); this.components = b.getComponents(); - bf = FilterFactory.getFilter(b.getKeyCount(), b.getTableMetadataRef().getLocal().params.bloomFilterFpChance); + bf = FilterFactory.getFilterForWrite(b.getKeyCount(), b.getTableMetadataRef().getLocal().params.bloomFilterFpChance, b.getOperationType()); } protected void flushBf() @@ -506,6 +638,8 @@ public B addDefaultComponents(Collection indexGroups) return (B) this; } + protected abstract OperationType getOperationType(); + protected abstract SequentialWriter openDataWriter(); protected abstract I openIndexWriter(SequentialWriter dataWriter); diff --git a/src/java/org/apache/cassandra/io/sstable/format/StatsComponent.java b/src/java/org/apache/cassandra/io/sstable/format/StatsComponent.java index 25042e331fac..abc336944243 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/StatsComponent.java +++ b/src/java/org/apache/cassandra/io/sstable/format/StatsComponent.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; @@ -36,17 +37,16 @@ import org.apache.cassandra.io.sstable.metadata.MetadataType; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.sstable.metadata.ValidationMetadata; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.SequentialWriter; -import org.apache.cassandra.io.util.SequentialWriterOption; import org.apache.cassandra.schema.TableMetadata; public class StatsComponent { + public final Descriptor descriptor; public final Map metadata; - public StatsComponent(Map metadata) + public StatsComponent(Descriptor descriptor, Map metadata) { + this.descriptor = descriptor; this.metadata = ImmutableMap.copyOf(metadata); } @@ -67,7 +67,7 @@ public static StatsComponent load(Descriptor descriptor, MetadataType... types) throw new CorruptSSTableException(e, descriptor.fileFor(Components.STATS)); } - return new StatsComponent(metadata); + return new StatsComponent(descriptor, metadata); } public SerializationHeader.Component serializationHeader() @@ -75,14 +75,19 @@ public SerializationHeader.Component serializationHeader() return (SerializationHeader.Component) metadata.get(MetadataType.HEADER); } - public SerializationHeader serializationHeader(TableMetadata metadata) + public SerializationHeader serializationHeader(Descriptor descriptor, TableMetadata metadata) + { + return serializationHeader(descriptor, metadata, false); + } + + public SerializationHeader serializationHeader(Descriptor descriptor, TableMetadata metadata, boolean isOfflineTool) { SerializationHeader.Component header = serializationHeader(); if (header != null) { try { - return header.toHeader(metadata); + return header.toHeader(descriptor.toString(), metadata, descriptor.version, isOfflineTool); } catch (UnknownColumnException ex) { @@ -108,18 +113,22 @@ public StatsMetadata statsMetadata() return (StatsMetadata) metadata.get(MetadataType.STATS); } + public StatsComponent with(ValidationMetadata validationMetadata) + { + Map newMetadata = new EnumMap<>(metadata); + newMetadata.put(MetadataType.VALIDATION, validationMetadata); + return new StatsComponent(descriptor, newMetadata); + } + public void save(Descriptor desc) { - File file = desc.fileFor(Components.STATS); - try (SequentialWriter out = new SequentialWriter(file, SequentialWriterOption.DEFAULT)) + try { - desc.getMetadataSerializer().serialize(metadata, out, desc.version); - out.finish(); + desc.getMetadataSerializer().rewriteSSTableMetadata(desc, metadata); } catch (IOException e) { - throw new FSWriteError(e, file.path()); + throw new FSWriteError(e); } } - } diff --git a/src/java/org/apache/cassandra/io/sstable/format/TOCComponent.java b/src/java/org/apache/cassandra/io/sstable/format/TOCComponent.java index 883550081c63..55ac2f4d8ea1 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/TOCComponent.java +++ b/src/java/org/apache/cassandra/io/sstable/format/TOCComponent.java @@ -31,12 +31,14 @@ import com.google.common.collect.Collections2; import com.google.common.collect.Sets; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.SSTableWatcher; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; @@ -124,7 +126,32 @@ public static Set loadOrCreate(Descriptor descriptor) { try { - return TOCComponent.loadTOC(descriptor); + try + { + // Try loading TOC first without discovering components or checking file existence. + return loadTOC(descriptor, false); + } + catch (FileNotFoundException | NoSuchFileException e) + { + SSTableWatcher.instance.discoverComponents(descriptor); + + // Try loading TOC again after discovering components, still without existence checks + try + { + return loadTOC(descriptor, false); + } + catch (FileNotFoundException | NoSuchFileException e2) + { + // Still no TOC, create it from discovered components + Set components = descriptor.discoverComponents(); + if (components.isEmpty()) + return components; // sstable doesn't exist yet + + components.add(Components.TOC); + updateTOC(descriptor, components); + return components; + } + } } catch (FileNotFoundException | NoSuchFileException e) { @@ -140,7 +167,7 @@ public static Set loadOrCreate(Descriptor descriptor) return components; // sstable doesn't exist yet components.add(Components.TOC); - TOCComponent.updateTOC(descriptor, components); + updateTOC(descriptor, components); return components; } @@ -149,9 +176,38 @@ public static Set loadOrCreate(Descriptor descriptor) */ public static void rewriteTOC(Descriptor descriptor, Collection components) { + if (components.isEmpty()) + return; + File tocFile = descriptor.fileFor(Components.TOC); - if (!tocFile.tryDelete()) - logger.error("Failed to delete TOC component for {}", descriptor); - updateTOC(descriptor, components); + // As this method *re*-write the TOC (and is currently only called by "unregisterComponents"), it should only + // be called in contexts where the TOC is expected to exist. If it doesn't, there is probably something + // unexpected happening, so we log relevant information to help diagnose a potential earlier problem. + // But in principle, this isn't a big deal for this method, and we still end up with the TOC in the state we + // expect. + if (!tocFile.exists()) + { + // Note: we pass a dummy runtime exception as a simple way to get a stack-trace. Knowing from where this + // is called in this case is likely useful information. + logger.warn("Was asked to 'rewrite' TOC file {} for sstable {}, but it does not exists. The file will be created but this is unexpected. The components to 'overwrite' are: {}", tocFile, descriptor, components, new RuntimeException()); + } + + Set componentNames = new TreeSet<>(Collections2.transform(components, Component::name)); + try + { + FileUtils.write(tocFile, new ArrayList<>(componentNames), CREATE, TRUNCATE_EXISTING, SYNC); + } + catch (RuntimeException ex) + { + throw new RuntimeException("Exception occurred while writing to " + tocFile, + ex.getCause() != null ? ex.getCause() : ex); + } + } + + public static void maybeAdd(Descriptor descriptor, Component component) throws IOException + { + Set toc = loadOrCreate(descriptor); + if (!toc.isEmpty() && toc.add(component)) + rewriteTOC(descriptor, toc); } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/Version.java b/src/java/org/apache/cassandra/io/sstable/format/Version.java index 2b214ab56d5b..2aaf618ad03d 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/Version.java +++ b/src/java/org/apache/cassandra/io/sstable/format/Version.java @@ -20,6 +20,8 @@ import java.util.Objects; import java.util.regex.Pattern; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; + /** * A set of feature flags associated with a SSTable format @@ -59,7 +61,7 @@ protected Version(SSTableFormat format, String version) public abstract boolean hasIsTransient(); public abstract boolean hasMetadataChecksum(); - + /** * This format raises the legacy int year 2038 limit to 2106 by using an uint instead */ @@ -101,8 +103,24 @@ protected Version(SSTableFormat format, String version) */ public abstract boolean hasPartitionLevelDeletionsPresenceMarker(); + /** + * Records in th stats if the sstable has any partition deletions. Note that this is DSE specific as we had this + * field in some BTI versions in a different place than in the OSS. + */ + public abstract boolean hasMisplacedPartitionLevelDeletionsPresenceMarker(); + public abstract boolean hasKeyRange(); + /** + * @return True if the indices (row and partition) can be encrypted. + */ + public abstract boolean indicesAreEncrypted(); + + /** + * @return True if the metadata (statistics) can be encrypted. + */ + public abstract boolean metadataIsEncrypted(); + /** * @param ver SSTable version * @return True if the given version string matches the format. @@ -143,4 +161,24 @@ public int hashCode() { return Objects.hash(version, format.name()); } + + // the fields below are present only in DSE but we do not use them here; though in order to be able to read + // DSE sstables we need to at least skip that data + public abstract boolean hasZeroCopyMetadata(); + + public abstract boolean hasIncrementalNodeSyncMetadata(); + + // TODO TBD + public abstract boolean hasMaxColumnValueLengths(); + + public abstract ByteComparable.Version getByteComparableVersion(); + + /** + * Whether we expect that sstable has explicitly frozen tuples in its {@link org.apache.cassandra.db.SerializationHeader}. + * If {@code false}, we don't try to fix non-frozen tuples that are not types of dropped columns and fail loading + * the sstable. If {@code true}, we try to fix non-frozen tuples and load the sstable. + * + * See this for reference. + */ + public abstract boolean hasImplicitlyFrozenTuples(); } diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java index 46f8a3bf2b6c..d6eb32cb82e0 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java @@ -29,7 +29,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,11 +39,11 @@ import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.io.sstable.Component; -import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.GaugeProvider; import org.apache.cassandra.io.sstable.IScrubber; import org.apache.cassandra.io.sstable.MetricsProviders; +import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.filter.BloomFilterMetrics; import org.apache.cassandra.io.sstable.format.AbstractSSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableFormat; @@ -62,6 +61,7 @@ import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; import static org.apache.cassandra.io.sstable.format.SSTableFormat.Components.DATA; @@ -97,6 +97,11 @@ public static class Types extends SSTableFormat.Components.Types FILTER, STATS); + // Used by CNDB, preserved from CC 4.0 + private static final Set REQUIRED_COMPONENTS = ImmutableSet.of(DATA, + PRIMARY_INDEX, + STATS); + private static final Set PRIMARY_COMPONENTS = ImmutableSet.of(DATA, PRIMARY_INDEX); @@ -171,6 +176,12 @@ public Set allComponents() return Components.ALL_COMPONENTS; } + @Override + public Set requiredComponents() + { + return Components.REQUIRED_COMPONENTS; + } + @Override public Set primaryComponents() { @@ -228,7 +239,7 @@ public void deleteOrphanedComponents(Descriptor descriptor, Set compo private void delete(Descriptor desc, List components) { - logger.info("Deleting sstable: {}", desc); + logger.debug("Deleting sstable: {}", desc); if (components.remove(DATA)) components.add(0, DATA); // DATA component should be first @@ -376,6 +387,8 @@ static class BigVersion extends Version private final boolean hasKeyRange; private final boolean hasUintDeletionTime; private final boolean hasTokenSpaceCoverage; + private final boolean indicesAreEncrypted; + private final boolean metadataIsEncrypted; /** * CASSANDRA-9067: 4.0 bloom filter representation changed (two longs just swapped) @@ -387,27 +400,34 @@ static class BigVersion extends Version { super(format, version); + boolean oOrLater = version.compareTo("o") >= 0; + boolean nOrLater = oOrLater || version.compareTo("n") >= 0; + boolean mOrLater = nOrLater || version.compareTo("m") >= 0; + isLatestVersion = version.compareTo(current_version) == 0; // Note that, we probably forgot to change that to 40 for N version, and therefore we cannot do it now. correspondingMessagingVersion = version.compareTo("oa") >= 0 ? MessagingService.VERSION_50 : MessagingService.VERSION_30; - hasCommitLogLowerBound = version.compareTo("mb") >= 0; - hasCommitLogIntervals = version.compareTo("mc") >= 0; - hasAccurateMinMax = version.matches("(m[d-z])|(n[a-z])"); // deprecated in 'oa' and to be removed after 'oa' - hasLegacyMinMax = version.matches("(m[a-z])|(n[a-z])"); // deprecated in 'oa' and to be removed after 'oa' + hasCommitLogLowerBound = mOrLater && version.compareTo("mb") >= 0; + hasCommitLogIntervals = mOrLater && version.compareTo("mc") >= 0; + hasAccurateMinMax = mOrLater && version.compareTo("md") >= 0 && !oOrLater; // deprecated in 'nc' and to be removed in 'oa' + hasLegacyMinMax = mOrLater && !oOrLater; // deprecated in 'nc' and to be removed in 'oa' // When adding a new version you might need to add it here - hasOriginatingHostId = version.compareTo("nb") >= 0 || version.matches("(m[e-z])"); - hasMaxCompressedLength = version.compareTo("na") >= 0; - hasPendingRepair = version.compareTo("na") >= 0; - hasIsTransient = version.compareTo("na") >= 0; - hasMetadataChecksum = version.compareTo("na") >= 0; - hasOldBfFormat = version.compareTo("na") < 0; - hasImprovedMinMax = version.compareTo("oa") >= 0; - hasPartitionLevelDeletionPresenceMarker = version.compareTo("oa") >= 0; - hasKeyRange = version.compareTo("oa") >= 0; - hasUintDeletionTime = version.compareTo("oa") >= 0; - hasTokenSpaceCoverage = version.compareTo("oa") >= 0; + hasOriginatingHostId = nOrLater && version.compareTo("nb") >= 0 || mOrLater && !nOrLater && version.compareTo("me") >= 0; + hasMaxCompressedLength = nOrLater; + hasPendingRepair = nOrLater; + hasIsTransient = nOrLater; + hasMetadataChecksum = nOrLater; + hasOldBfFormat = !nOrLater; + hasImprovedMinMax = oOrLater; + hasPartitionLevelDeletionPresenceMarker = oOrLater; + hasKeyRange = oOrLater; + hasUintDeletionTime = oOrLater; + hasTokenSpaceCoverage = oOrLater; + // Encryption is not supported in big format + indicesAreEncrypted = false; + metadataIsEncrypted = false; } @Override @@ -500,6 +520,12 @@ public boolean hasPartitionLevelDeletionsPresenceMarker() return hasPartitionLevelDeletionPresenceMarker; } + @Override + public boolean hasMisplacedPartitionLevelDeletionsPresenceMarker() + { + return false; + } + @Override public boolean hasUIntDeletionTime() { @@ -512,6 +538,18 @@ public boolean hasKeyRange() return hasKeyRange; } + @Override + public boolean indicesAreEncrypted() + { + return indicesAreEncrypted; + } + + @Override + public boolean metadataIsEncrypted() + { + return metadataIsEncrypted; + } + @Override public boolean isCompatible() { @@ -523,6 +561,36 @@ public boolean isCompatibleForStreaming() { return isCompatible() && version.charAt(0) == current_version.charAt(0); } + + @Override + public boolean hasZeroCopyMetadata() + { + return false; + } + + @Override + public boolean hasIncrementalNodeSyncMetadata() + { + return false; + } + + @Override + public boolean hasMaxColumnValueLengths() + { + return false; + } + + @Override + public ByteComparable.Version getByteComparableVersion() + { + return ByteComparable.Version.OSS41; + } + + @Override + public boolean hasImplicitlyFrozenTuples() + { + return true; + } } private static class BigTableSpecificMetricsProviders implements MetricsProviders diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java index 801982d5ec59..a34a12becf61 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java @@ -32,7 +32,6 @@ import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.io.ISerializer; -import org.apache.cassandra.io.sstable.IndexInfo; import org.apache.cassandra.io.sstable.format.SortedTablePartitionWriter; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.util.DataOutputBuffer; diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java index 84e02217d565..2c72d08c19e3 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java @@ -19,6 +19,7 @@ package org.apache.cassandra.io.sstable.format.big; import java.io.IOException; +import java.util.Optional; import java.util.OptionalInt; import org.slf4j.Logger; @@ -35,6 +36,7 @@ import org.apache.cassandra.io.sstable.format.IndexComponent; import org.apache.cassandra.io.sstable.format.SortedTableReaderLoadingBuilder; import org.apache.cassandra.io.sstable.format.StatsComponent; +import org.apache.cassandra.io.sstable.format.TOCComponent; import org.apache.cassandra.io.sstable.format.big.BigFormat.Components; import org.apache.cassandra.io.sstable.indexsummary.IndexSummary; import org.apache.cassandra.io.sstable.indexsummary.IndexSummaryBuilder; @@ -74,11 +76,12 @@ protected void openComponents(BigTableReader.Builder builder, SSTable.Owner owne if (online && builder.getTableMetadataRef().getLocal().params.caching.cacheKeys()) builder.setKeyCache(new KeyCache(CacheService.instance.keyCache)); - StatsComponent statsComponent = StatsComponent.load(descriptor, MetadataType.STATS, MetadataType.HEADER, MetadataType.VALIDATION); - builder.setSerializationHeader(statsComponent.serializationHeader(builder.getTableMetadataRef().getLocal())); + StatsComponent statsComponent = StatsComponent.load(descriptor, MetadataType.STATS, MetadataType.HEADER, MetadataType.VALIDATION, MetadataType.COMPACTION); + builder.setSerializationHeader(statsComponent.serializationHeader(descriptor, builder.getTableMetadataRef().getLocal(), !online)); checkArgument(!online || builder.getSerializationHeader() != null); builder.setStatsMetadata(statsComponent.statsMetadata()); + builder.setCompactionMetadata(Optional.ofNullable(statsComponent.compactionMetadata())); if (descriptor.version.hasKeyRange() && statsComponent.statsMetadata() != null) { builder.setFirst(tableMetadataRef.getLocal().partitioner.decorateKey(statsComponent.statsMetadata().firstKey)); @@ -137,7 +140,7 @@ protected void openComponents(BigTableReader.Builder builder, SSTable.Owner owne } } - try (CompressionMetadata compressionMetadata = CompressionInfoComponent.maybeLoad(descriptor, components)) + try (CompressionMetadata compressionMetadata = CompressionInfoComponent.maybeLoad(descriptor, components, statsComponent.statsMetadata().zeroCopyMetadata)) { builder.setDataFile(dataFileBuilder(builder.getStatsMetadata()) .withCompressionMetadata(compressionMetadata) @@ -146,7 +149,25 @@ protected void openComponents(BigTableReader.Builder builder, SSTable.Owner owne } if (builder.getFilter() == null) + { builder.setFilter(FilterFactory.AlwaysPresent); + logger.warn("Could not recreate or deserialize existing bloom filter, continuing with a pass-through " + + "bloom filter but this will significantly impact reads performance"); + } + else if (rebuildFilter) + { + if (validationMetadata.bloomFilterFPChance != tableMetadataRef.getLocal().params.bloomFilterFpChance) + { + StatsComponent.load(descriptor, MetadataType.values()) + .with(validationMetadata.withBloomFilterFPChance(tableMetadataRef.getLocal().params.bloomFilterFpChance)) + .save(descriptor); + } + if (descriptor.fileFor(Components.FILTER).exists()) + TOCComponent.maybeAdd(descriptor, Components.FILTER); + } + + if (rebuildSummary && descriptor.fileFor(Components.SUMMARY).exists()) + TOCComponent.maybeAdd(descriptor, Components.SUMMARY); if (builder.getComponents().contains(Components.PRIMARY_INDEX)) builder.setIndexFile(indexFileBuilder(builder.getIndexSummary()).complete()); @@ -162,7 +183,7 @@ protected void openComponents(BigTableReader.Builder builder, SSTable.Owner owne public KeyReader buildKeyReader(TableMetrics tableMetrics) throws IOException { StatsComponent statsComponent = StatsComponent.load(descriptor, MetadataType.STATS, MetadataType.HEADER, MetadataType.VALIDATION); - SerializationHeader header = statsComponent.serializationHeader(tableMetadataRef.getLocal()); + SerializationHeader header = statsComponent.serializationHeader(descriptor, tableMetadataRef.getLocal()); try (FileHandle indexFile = indexFileBuilder(null).complete()) { return createKeyReader(indexFile, header, tableMetrics); @@ -300,7 +321,7 @@ private FileHandle.Builder indexFileBuilder(IndexSummary indexSummary) : OptionalInt.empty(); if (indexFileBuilder == null) - indexFileBuilder = IndexComponent.fileBuilder(descriptor.fileFor(Components.PRIMARY_INDEX), ioOptions, chunkCache) + indexFileBuilder = IndexComponent.fileBuilder(descriptor, Components.PRIMARY_INDEX, ioOptions, chunkCache) .bufferSize(indexBufferSize.orElse(DiskOptimizationStrategy.MAX_BUFFER_SIZE)); indexBufferSize.ifPresent(indexFileBuilder::bufferSize); diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java index 692cadf34df4..845a62c96ee8 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java @@ -51,14 +51,15 @@ import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.Downsampling; +import org.apache.cassandra.io.sstable.IKeyFetcher; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.IVerifier; -import org.apache.cassandra.io.sstable.IndexInfo; import org.apache.cassandra.io.sstable.KeyReader; import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.SSTableReadsListener; import org.apache.cassandra.io.sstable.SSTableReadsListener.SelectionReason; import org.apache.cassandra.io.sstable.SSTableReadsListener.SkippingReason; +import org.apache.cassandra.io.sstable.format.AbstractKeyFetcher; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter; import org.apache.cassandra.io.sstable.format.big.BigFormat.Components; @@ -68,8 +69,10 @@ import org.apache.cassandra.io.sstable.keycache.KeyCache; import org.apache.cassandra.io.sstable.keycache.KeyCacheSupport; import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.utils.ByteBufferUtil; @@ -100,6 +103,7 @@ public BigTableReader(Builder builder, SSTable.Owner owner) this.indexSummary = builder.getIndexSummary(); this.rowIndexEntrySerializer = new RowIndexEntry.Serializer(descriptor.version, header, owner != null ? owner.getMetrics() : null); this.keyCache = Objects.requireNonNull(builder.getKeyCache()); + this.approximateBloomFilterMemorySize = isBloomFilterLoaded() ? filter.offHeapSize() : computeExpectedBloomFilterMemorySize(); } @Override @@ -169,10 +173,10 @@ public DecoratedKey firstKeyBeyond(PartitionPosition token) if (ifile == null) return null; - String path = null; + File path = null; try (FileDataInput in = ifile.createReader(sampledPosition)) { - path = in.getPath(); + path = in.getFile(); while (!in.isEOF()) { ByteBuffer indexKey = ByteBufferUtil.readWithShortLength(in); @@ -255,7 +259,7 @@ public RowIndexEntry getRowIndexEntry(PartitionPosition key, if (searchOp == Operator.EQ) { assert key instanceof DecoratedKey; // EQ only make sense if the key is a valid row key - if (!isPresentInFilter((IFilter.FilterKey) key)) + if (!inBloomFilter((DecoratedKey)key)) { notifySkipped(SkippingReason.BLOOM_FILTER, listener, operator, updateStats); return null; @@ -286,11 +290,13 @@ public RowIndexEntry getRowIndexEntry(PartitionPosition key, // next index position because the searched key can be greater the last key of the index interval checked if it // is lesser than the first key of next interval (and in that case we must return the position of the first key // of the next interval). + listener.onSSTablePartitionIndexAccessed(this); + int i = 0; - String path = null; + File path = null; try (FileDataInput in = ifile.createReader(sampledPosition)) { - path = in.getPath(); + path = in.getFile(); while (!in.isEOF()) { i++; @@ -335,7 +341,7 @@ public RowIndexEntry getRowIndexEntry(PartitionPosition key, { DecoratedKey keyInDisk = decorateKey(ByteBufferUtil.readWithShortLength(fdi)); if (!keyInDisk.equals(key)) - throw new AssertionError(String.format("%s != %s in %s", keyInDisk, key, fdi.getPath())); + throw new AssertionError(String.format("%s != %s in %s", keyInDisk, key, fdi.getFile())); } } @@ -377,23 +383,16 @@ protected long getPosition(PartitionPosition key, } @Override - public DecoratedKey keyAtPositionFromSecondaryIndex(long keyPositionFromSecondaryIndex) throws IOException + public IKeyFetcher openKeyFetcher(boolean isForSASI) { - DecoratedKey key; - try (FileDataInput in = ifile.createReader(keyPositionFromSecondaryIndex)) + return new AbstractKeyFetcher(isForSASI ? openIndexReader(ReadPattern.RANDOM) : openDataReader(ReadPattern.SEQUENTIAL)) { - if (in.isEOF()) - return null; - - key = decorateKey(ByteBufferUtil.readWithShortLength(in)); - - // hint read path about key location if caching is enabled - // this saves index summary lookup and index file iteration which whould be pretty costly - // especially in presence of promoted column indexes - cacheKey(key, rowIndexEntrySerializer.deserialize(in)); - } - - return key; + @Override + public DecoratedKey readKey(RandomAccessReader reader) throws IOException + { + return decorateKey(ByteBufferUtil.readWithShortLength(reader)); + } + }; } @Override @@ -467,10 +466,10 @@ public Iterable getKeySamples(final Range range) return Iterables.transform(indexSummary.getKeySamples(range), bytes -> decorateKey(ByteBuffer.wrap(bytes))); } - public RandomAccessReader openIndexReader() + public RandomAccessReader openIndexReader(ReadPattern pattern) { if (ifile != null) - return ifile.createReader(); + return ifile.createReader(pattern); return null; } @@ -482,7 +481,7 @@ public FileHandle getIndexFile() @Override public IVerifier getVerifier(ColumnFamilyStore cfs, OutputHandler outputHandler, boolean isOffline, IVerifier.Options options) { - Preconditions.checkArgument(cfs.metadata().equals(metadata())); + Preconditions.checkArgument(cfs == null || cfs.metadata().equals(metadata())); return new BigTableVerifier(cfs, this, outputHandler, isOffline, options); } @@ -515,7 +514,9 @@ protected final Builder unbuildTo(Builder builder, boolean sharedCopy) @Override public SSTableReaderWithFilter cloneAndReplace(IFilter filter) { - return unbuildTo(new Builder(descriptor).setFilter(filter), true).build(owner().orElse(null), true, true); + BigTableReader replacement = unbuildTo(new Builder(descriptor).setFilter(filter), true).build(owner().orElse(null), true, true); + replacement.approximateBloomFilterMemorySize = approximateBloomFilterMemorySize; + return replacement; } /** @@ -528,10 +529,12 @@ public SSTableReaderWithFilter cloneAndReplace(IFilter filter) */ private SSTableReader cloneAndReplace(DecoratedKey newFirst, OpenReason reason) { - return unbuildTo(new Builder(descriptor), true) + BigTableReader replacement = unbuildTo(new Builder(descriptor), true) .setFirst(newFirst) .setOpenReason(reason) .build(owner().orElse(null), true, true); + replacement.approximateBloomFilterMemorySize = approximateBloomFilterMemorySize; + return replacement; } /** @@ -545,11 +548,13 @@ private SSTableReader cloneAndReplace(DecoratedKey newFirst, OpenReason reason) */ private BigTableReader cloneAndReplace(DecoratedKey newFirst, OpenReason reason, IndexSummary newSummary) { - return unbuildTo(new Builder(descriptor).setIndexSummary(newSummary), true) + BigTableReader replacement = unbuildTo(new Builder(descriptor).setIndexSummary(newSummary), true) .setIndexSummary(newSummary) .setFirst(newFirst) .setOpenReason(reason) .build(owner().orElse(null), true, true); + replacement.approximateBloomFilterMemorySize = approximateBloomFilterMemorySize; + return replacement; } public SSTableReader cloneWithRestoredStart(DecoratedKey restoredStart) diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java index 83243529c4c9..ae4a7e1480dd 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Iterator; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.db.DataRange; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; @@ -60,8 +61,19 @@ private BigTableScanner(BigTableReader sstable, SSTableReadsListener listener) { super(sstable, columns, dataRange, rangeIterator, listener); - this.ifile = sstable.openIndexReader(); - this.rowIndexEntrySerializer = new RowIndexEntry.Serializer(sstable.descriptor.version, sstable.header, sstable.owner().map(SSTable.Owner::getMetrics).orElse(null)); + + RandomAccessReader ifile = null; + try + { + ifile = sstable.openIndexReader(ReadPattern.SEQUENTIAL); + this.rowIndexEntrySerializer = new RowIndexEntry.Serializer(sstable.descriptor.version, sstable.header, sstable.owner().map(SSTable.Owner::getMetrics).orElse(null)); + } + catch (Throwable t) + { + FileUtils.closeQuietly(ifile); + throw t; + } + this.ifile = ifile; } private void seekToCurrentRangeStart() @@ -106,6 +118,11 @@ protected BigScanningIterator doCreateIterator() return new BigScanningIterator(); } + @Override + public int level() { + return sstable.getSSTableLevel(); + } + protected class BigScanningIterator extends SSTableScanner.BaseKeyScanningIterator { private DecoratedKey nextKey; diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScrubber.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScrubber.java index dc991f491fb1..dc832caa78c4 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScrubber.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScrubber.java @@ -23,7 +23,6 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.rows.UnfilteredRowIterators; @@ -31,6 +30,7 @@ import org.apache.cassandra.io.sstable.SSTableRewriter; import org.apache.cassandra.io.sstable.format.SortedTableScrubber; import org.apache.cassandra.io.sstable.format.big.BigFormat.Components; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.utils.ByteBufferUtil; @@ -76,9 +76,9 @@ public BigTableScrubber(ColumnFamilyStore cfs, } @Override - protected UnfilteredRowIterator withValidation(UnfilteredRowIterator iter, String filename) + protected UnfilteredRowIterator withValidation(UnfilteredRowIterator iter, File file) { - return options.checkData && !isIndex ? UnfilteredRowIterators.withValidation(iter, filename) : iter; + return options.checkData && !isIndex ? UnfilteredRowIterators.withValidation(iter, file) : iter; } @Override @@ -107,8 +107,7 @@ protected void scrubInternal(SSTableRewriter writer) throws IOException while (!dataFile.isEOF()) { - if (scrubInfo.isStopRequested()) - throw new CompactionInterruptedException(scrubInfo.getCompactionInfo()); + scrubInfo.throwIfStopRequested(); long partitionStart = dataFile.getFilePointer(); outputHandler.debug("Reading row at %d", partitionStart); @@ -117,8 +116,8 @@ protected void scrubInternal(SSTableRewriter writer) throws IOException try { ByteBuffer raw = ByteBufferUtil.readWithShortLength(dataFile); - if (!cfs.metadata.getLocal().isIndex()) - cfs.metadata.getLocal().partitionKeyType.validate(raw); + if (!realm.metadataRef().getLocal().isIndex()) + realm.metadataRef().getLocal().partitionKeyType.validate(raw); key = sstable.decorateKey(raw); } catch (Throwable th) @@ -181,8 +180,8 @@ protected void scrubInternal(SSTableRewriter writer) throws IOException key = sstable.decorateKey(currentIndexKey); try { - if (!cfs.metadata.getLocal().isIndex()) - cfs.metadata.getLocal().partitionKeyType.validate(key.getKey()); + if (!realm.metadataRef().getLocal().isIndex()) + realm.metadataRef().getLocal().partitionKeyType.validate(key.getKey()); dataFile.seek(dataStartFromIndex); if (tryAppend(prevKey, key, writer)) diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableVerifier.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableVerifier.java index 70df3e1c0a1f..86d242866655 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableVerifier.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableVerifier.java @@ -126,7 +126,7 @@ private String dateString(long time) private void deserializeIndexSummary(SSTableReader sstable) throws IOException { - IndexSummaryComponent summaryComponent = IndexSummaryComponent.load(sstable.descriptor.fileFor(Components.SUMMARY), cfs.metadata()); + IndexSummaryComponent summaryComponent = IndexSummaryComponent.load(sstable.descriptor.fileFor(Components.SUMMARY), sstable.metadata()); if (summaryComponent == null) throw new NoSuchFileException("Index summary component of sstable " + sstable.descriptor + " is missing"); FileUtils.closeQuietly(summaryComponent.indexSummary); diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java index bf8bb79ce276..dc355b284666 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import com.google.common.collect.ImmutableSet; @@ -49,6 +50,10 @@ import org.apache.cassandra.io.sstable.indexsummary.IndexSummaryBuilder; import org.apache.cassandra.io.sstable.keycache.KeyCache; import org.apache.cassandra.io.sstable.keycache.KeyCacheSupport; +import org.apache.cassandra.io.sstable.metadata.CompactionMetadata; +import org.apache.cassandra.io.sstable.metadata.MetadataComponent; +import org.apache.cassandra.io.sstable.metadata.MetadataType; +import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.DataPosition; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.FileUtils; @@ -146,7 +151,9 @@ private BigTableReader openInternal(IndexSummaryBuilder.ReadableBoundary boundar try { - builder.setStatsMetadata(statsMetadata()); + Map finalMetadata = finalizeMetadata(); + builder.setStatsMetadata((StatsMetadata) finalMetadata.get(MetadataType.STATS)); + builder.setCompactionMetadata(Optional.ofNullable((CompactionMetadata) finalMetadata.get(MetadataType.COMPACTION))); EstimatedHistogram partitionSizeHistogram = builder.getStatsMetadata().estimatedPartitionSize; if (boundary != null) @@ -232,6 +239,12 @@ public SSTableReader openFinal(SSTableReader.OpenReason openReason) return openInternal(null, openReason); } + @Override + public void openResult(@javax.annotation.Nullable org.apache.cassandra.io.sstable.StorageHandler storageHandler) + { + txnProxy.openResult(storageHandler); + } + /** * Encapsulates writing the index and filter for an SSTable. The state of this object is not valid until it has been closed. */ @@ -251,7 +264,7 @@ protected IndexWriter(Builder b, SequentialWriter dataWriter) super(b); this.rowIndexEntrySerializer = b.getRowIndexEntrySerializer(); writer = new SequentialWriter(b.descriptor.fileFor(Components.PRIMARY_INDEX), b.getIOOptions().writerOptions); - builder = IndexComponent.fileBuilder(Components.PRIMARY_INDEX, b).withMmappedRegionsCache(b.getMmappedRegionsCache()); + builder = IndexComponent.fileBuilder(Components.PRIMARY_INDEX, b, b.operationType).withMmappedRegionsCache(b.getMmappedRegionsCache()); summary = new IndexSummaryBuilder(b.getKeyCount(), b.getTableMetadataRef().getLocal().params.minIndexInterval, Downsampling.BASE_SAMPLING_LEVEL); // register listeners to be alerted when the data files are flushed writer.setPostFlushListener(summary::markIndexSynced); @@ -279,7 +292,7 @@ public void append(DecoratedKey key, RowIndexEntry indexEntry, long dataEnd, Byt } catch (IOException e) { - throw new FSWriteError(e, writer.getPath()); + throw new FSWriteError(e, writer.getFile()); } long indexEnd = writer.position(); @@ -314,7 +327,7 @@ protected void doPrepare() // truncate index file long position = writer.position(); writer.prepareToCommit(); - FileUtils.truncate(writer.getPath(), position); + FileUtils.truncate(writer.getFile(), position); // save summary summary.prepareToCommit(); @@ -383,6 +396,12 @@ public MmappedRegionsCache getMmappedRegionsCache() return ensuringInBuildInternalContext(mmappedRegionsCache); } + @Override + protected OperationType getOperationType() + { + return ensuringInBuildInternalContext(operationType); + } + @Override protected SequentialWriter openDataWriter() { diff --git a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java b/src/java/org/apache/cassandra/io/sstable/format/big/IndexInfo.java similarity index 98% rename from src/java/org/apache/cassandra/io/sstable/IndexInfo.java rename to src/java/org/apache/cassandra/io/sstable/format/big/IndexInfo.java index 350a98eb9eaf..6399e9e60c13 100644 --- a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/IndexInfo.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.io.sstable; +package org.apache.cassandra.io.sstable.format.big; import java.io.IOException; import java.util.List; @@ -28,7 +28,6 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.io.ISerializer; import org.apache.cassandra.io.sstable.format.Version; -import org.apache.cassandra.io.sstable.format.big.RowIndexEntry; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.utils.ObjectSizes; diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/IndexState.java b/src/java/org/apache/cassandra/io/sstable/format/big/IndexState.java index 754b34b06571..50326970aa30 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/IndexState.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/IndexState.java @@ -25,7 +25,6 @@ import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.ClusteringPrefix; import org.apache.cassandra.io.sstable.AbstractSSTableIterator; -import org.apache.cassandra.io.sstable.IndexInfo; import org.apache.cassandra.io.util.DataPosition; import org.apache.cassandra.io.util.FileHandle; diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java b/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java index 9d2ab6e93a9a..d2130767b8ef 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java @@ -33,7 +33,6 @@ import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.io.ISerializer; import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; -import org.apache.cassandra.io.sstable.IndexInfo; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.sstable.format.big.BigFormat.Components; import org.apache.cassandra.io.util.DataInputPlus; @@ -394,7 +393,7 @@ private void checkSize(int entries, int bytes) String msg = String.format("Query %s attempted to access a large RowIndexEntry estimated to be %d bytes " + "in-memory (total entries: %d, total bytes: %d) but the max allowed is %s;" + " query aborted (see row_index_read_size_fail_threshold)", - command.toCQLString(), estimatedMemory, entries, bytes, failThreshold); + command.toRedactedCQLString(), estimatedMemory, entries, bytes, failThreshold); MessageParams.remove(ParamType.ROW_INDEX_READ_SIZE_WARN); MessageParams.add(ParamType.ROW_INDEX_READ_SIZE_FAIL, estimatedMemory); diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormat.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormat.java index e7703c6a0612..f08e88ae2227 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormat.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormat.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import com.google.common.base.Preconditions; @@ -29,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; @@ -53,6 +55,7 @@ import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** * Bigtable format with trie indices. See BTIFormat.md for the format documentation. @@ -79,6 +82,12 @@ public static class Types extends AbstractSSTableFormat.Components.Types public final static Component ROW_INDEX = Types.ROW_INDEX.getSingleton(); + // Used by CNDB, preserved from CC 4.0 + private static final Set REQUIRED_COMPONENTS = ImmutableSet.of(DATA, + PARTITION_INDEX, + ROW_INDEX, + STATS); + private final static Set PRIMARY_COMPONENTS = ImmutableSet.of(DATA, PARTITION_INDEX); @@ -121,6 +130,11 @@ public static boolean is(SSTableFormat format) return format.name().equals(NAME); } + public static BtiFormat getInstance() + { + return (BtiFormat) Objects.requireNonNull(DatabaseDescriptor.getSSTableFormats().get(NAME), "Unknown SSTable format: " + NAME); + } + public static boolean isSelected() { return is(DatabaseDescriptor.getSelectedSSTableFormat()); @@ -150,6 +164,12 @@ public BtiTableReaderFactory getReaderFactory() return readerFactory; } + @Override + public Set requiredComponents() + { + return Components.REQUIRED_COMPONENTS; + } + @Override public Set primaryComponents() { @@ -213,7 +233,7 @@ public void deleteOrphanedComponents(Descriptor descriptor, Set compo private void delete(Descriptor desc, List components) { - logger.info("Deleting sstable: {}", desc); + logger.debug("Deleting sstable: {}", desc); if (components.remove(SSTableFormat.Components.DATA)) components.add(0, SSTableFormat.Components.DATA); // DATA component should be first @@ -256,7 +276,7 @@ public SSTableReaderLoadingBuilder loadi @Override public Pair readKeyRange(Descriptor descriptor, IPartitioner partitioner) throws IOException { - return PartitionIndex.readFirstAndLastKey(descriptor.fileFor(Components.PARTITION_INDEX), partitioner); + return PartitionIndex.readFirstAndLastKey(descriptor, Components.PARTITION_INDEX, partitioner, descriptor.version.getByteComparableVersion()); } @Override @@ -286,8 +306,32 @@ public long estimateSize(SSTableWriter.SSTableSizeParameters parameters) static class BtiVersion extends Version { - public static final String current_version = "da"; - public static final String earliest_supported_version = "da"; + private static final Logger logger = LoggerFactory.getLogger(BtiVersion.class); + + public static final String current_version = CassandraRelevantProperties.TRIE_INDEX_FORMAT_VERSION.getString(); + + static + { + logger.info("Trie index format current version: {}", current_version); + } + + public static final String earliest_supported_version = "aa"; + + // aa (DSE 6.0): trie index format + // ab (DSE pre-6.8): ILLEGAL - handled as 'b' (predates 'ba'). Pre-GA "LABS" releases of DSE 6.8 used this + // sstable version. + // ac (DSE 6.0.11, 6.7.6): corrected sstable min/max clustering (DB-3691/CASSANDRA-14861) + // ad (DSE 6.0.14, 6.7.11): added hostId of the node from which the sstable originated (DB-4629) + // b (DSE early 6.8 "LABS") has some of 6.8 features but not all + // ba (DSE 6.8): encrypted indices and metadata + // new BloomFilter serialization format + // add incremental NodeSync information to metadata + // improved min/max clustering representation + // presence marker for partition level deletions + // bb (DSE 6.8.5): added hostId of the node from which the sstable originated (DB-4629) + // ca (DSE-DB aka Stargazer based on OSS 4.0): bb fields without maxColumnValueLengths + all OSS fields + // cb (OSS 5.0): token space coverage + // cc : added explicitly frozen tuples in header, non-frozen UDT columns dropping support // versions aa-cz are not supported in OSS // da (5.0): initial version of the BIT format @@ -295,14 +339,67 @@ static class BtiVersion extends Version private final boolean isLatestVersion; + /** + * DB-2648/CASSANDRA-9067: DSE 6.8/OSS 4.0 bloom filter representation changed (bitset data is no longer stored + * as BIG_ENDIAN longs, which avoids some redundant bit twiddling). + */ + private final boolean hasOldBfFormat; + private final boolean hasAccurateLegacyMinMax; + private final boolean hasOriginatingHostId; + private final boolean hasMaxColumnValueLengths; + private final boolean hasImprovedMinMax; + private final boolean hasLegacyMinMax; + private final boolean hasZeroCopyMetadata; + private final boolean hasIncrementalNodeSyncMetadata; + private final boolean hasIsTransient; + private final boolean hasTokenSpaceCoverage; + private final boolean hasMisplacedPartitionLevelDeletionsPresenceMarker; + + private final int correspondingMessagingVersion; + private final ByteComparable.Version byteComparableVersion; + private final boolean hasPartitionLevelDeletionsPresenceMarker; + private final boolean hasKeyRange; + private final boolean hasUIntDeletionTime; + private final boolean hasImplicitlyFrozenTuples; + private final boolean indicesAreEncrypted; + private final boolean metadataIsEncrypted; BtiVersion(BtiFormat format, String version) { super(format, version); + boolean dOrLater = version.compareTo("d") >= 0; + boolean cOrLater = dOrLater || version.startsWith("c"); + boolean bOrLater = cOrLater || version.startsWith("b"); + boolean aOrLater = bOrLater || version.startsWith("a"); + isLatestVersion = version.compareTo(current_version) == 0; correspondingMessagingVersion = MessagingService.VERSION_50; + byteComparableVersion = version.compareTo("da") >= 0 ? ByteComparable.Version.OSS50 + : version.compareTo("ca") >= 0 ? ByteComparable.Version.OSS41 + : ByteComparable.Version.LEGACY; + hasOldBfFormat = aOrLater && !bOrLater; + hasImprovedMinMax = bOrLater; + hasLegacyMinMax = aOrLater && !bOrLater; + hasAccurateLegacyMinMax = !bOrLater && version.compareTo("ac") >= 0; + hasOriginatingHostId = bOrLater && version.compareTo("bb") >= 0 || !bOrLater && version.compareTo("ad") >= 0; + hasIsTransient = cOrLater; + hasTokenSpaceCoverage = version.compareTo("cb") >= 0; + hasMisplacedPartitionLevelDeletionsPresenceMarker = bOrLater && !dOrLater; + hasPartitionLevelDeletionsPresenceMarker = dOrLater; + hasKeyRange = dOrLater; + hasUIntDeletionTime = dOrLater; + + hasMaxColumnValueLengths = bOrLater && !cOrLater; // DSE only field + hasZeroCopyMetadata = bOrLater && !cOrLater; // DSE only field + hasIncrementalNodeSyncMetadata = bOrLater && !cOrLater; // DSE only field + + hasImplicitlyFrozenTuples = version.compareTo("cc") < 0 || version.compareTo("da") >= 0; // `da` is found in C* 5.0 and CC `main-5.0`, and both have implicitly frozen tuples + + // encryption support - enabled for DSE 6.8 (ba) and later, and for BTI format (da) and later + indicesAreEncrypted = (bOrLater && version.compareTo("ba") >= 0) || dOrLater; + metadataIsEncrypted = (bOrLater && version.compareTo("ba") >= 0) || dOrLater; } @Override @@ -341,10 +438,17 @@ public boolean hasPendingRepair() return true; } + // this field is not present in DSE @Override public boolean hasIsTransient() { - return true; + return hasIsTransient; + } + + @Override + public ByteComparable.Version getByteComparableVersion() + { + return byteComparableVersion; } @Override @@ -356,47 +460,65 @@ public boolean hasMetadataChecksum() @Override public boolean hasOldBfFormat() { - return false; + return hasOldBfFormat; } @Override public boolean hasAccurateMinMax() { - return true; + return hasAccurateLegacyMinMax; } public boolean hasLegacyMinMax() { - return false; + return hasLegacyMinMax; } @Override public boolean hasOriginatingHostId() { - return true; + return hasOriginatingHostId; } @Override public boolean hasImprovedMinMax() { - return true; + return hasImprovedMinMax; } @Override public boolean hasTokenSpaceCoverage() { - return true; + return hasTokenSpaceCoverage; } @Override public boolean hasPartitionLevelDeletionsPresenceMarker() { - return true; + return hasPartitionLevelDeletionsPresenceMarker; + } + + @Override + public boolean hasMisplacedPartitionLevelDeletionsPresenceMarker() + { + return hasMisplacedPartitionLevelDeletionsPresenceMarker; } @Override public boolean hasKeyRange() { - return true; + return hasKeyRange; + } + + @Override + public boolean indicesAreEncrypted() + { + return indicesAreEncrypted; + } + + @Override + public boolean metadataIsEncrypted() + { + return metadataIsEncrypted; } @Override @@ -414,9 +536,32 @@ public boolean isCompatibleForStreaming() @Override public boolean hasUIntDeletionTime() { - return true; + return hasUIntDeletionTime; } + @Override + public boolean hasZeroCopyMetadata() + { + return hasZeroCopyMetadata; + } + + @Override + public boolean hasIncrementalNodeSyncMetadata() + { + return hasIncrementalNodeSyncMetadata; + } + + @Override + public boolean hasMaxColumnValueLengths() + { + return hasMaxColumnValueLengths; + } + + @Override + public boolean hasImplicitlyFrozenTuples() + { + return hasImplicitlyFrozenTuples; + } } private static class BtiTableSpecificMetricsProviders implements MetricsProviders diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java index 9a65be1137bb..d6c300ecbf39 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java @@ -44,15 +44,18 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.IKeyFetcher; import org.apache.cassandra.io.sstable.IVerifier; import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.SSTableReadsListener; import org.apache.cassandra.io.sstable.SSTableReadsListener.SelectionReason; import org.apache.cassandra.io.sstable.SSTableReadsListener.SkippingReason; +import org.apache.cassandra.io.sstable.format.AbstractKeyFetcher; import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.IFilter; import org.apache.cassandra.utils.OutputHandler; @@ -78,6 +81,7 @@ public BtiTableReader(Builder builder, SSTable.Owner owner) super(builder, owner); this.rowIndexFile = builder.getRowIndexFile(); this.partitionIndex = builder.getPartitionIndex(); + this.approximateBloomFilterMemorySize = isBloomFilterLoaded() ? filter.offHeapSize() : computeExpectedBloomFilterMemorySize(); } protected final Builder unbuildTo(Builder builder, boolean sharedCopy) @@ -106,7 +110,7 @@ protected List setupInstance(boolean trackHotness) */ protected boolean filterFirst() { - return openReason == OpenReason.MOVED_START; + return openReason == OpenReason.MOVED_START || sstableMetadata.zeroCopyMetadata.exists(); } /** @@ -116,7 +120,7 @@ protected boolean filterFirst() */ protected boolean filterLast() { - return openReason == OpenReason.EARLY && partitionIndex instanceof PartitionIndexEarly; + return openReason == OpenReason.EARLY && partitionIndex instanceof PartitionIndexEarly || sstableMetadata.zeroCopyMetadata.exists(); } public long estimatedKeys() @@ -166,6 +170,77 @@ protected TrieIndexEntry getRowIndexEntry(PartitionPosition key, throw new IllegalArgumentException("Invalid op: " + operator); } + private TrieIndexEntry getApproximatePosition(PartitionPosition key, Operator op, boolean isLeftBound) + { + assert op == GT || op == GE; + // We currently only need this method in contexts where neither early opening nor zero copy transfer are used, + // which means we don't have to worry about `filterFirst`/`filterLast`. We could expand that method to support + // those, but it's unclear it will ever be needed, and this would require proper testing, so leaving aside for now. + assert openReason != OpenReason.MOVED_START : "Early opening is not supported with this method"; + assert !sstableMetadata.zeroCopyMetadata.exists() : "SSTables with zero copy metadata are not supported"; + + try (PartitionIndex.Reader reader = partitionIndex.openReader()) + { + return reader.ceiling(key, (pos, assumeNoMatch, compareKey) -> { + // The goal of the overall method, compared to `getPosition`, is to avoid reading the data file. If + // whatever partition we look at has a row index (`pos >= 0`), then `retrieveEntryIfAcceptable` may + // read the row index file, but it will never read the data file, so we can use it like in `getPosition`. + if (pos >= 0) + return retrieveEntryIfAcceptable(op, compareKey, pos, assumeNoMatch); + + // If `assumeNoMatch == false`, then it means we've matched a prefix of the searched key. This means + // `pos` points to a key `K` in the sstable that is "the closest on" to `searchKey`, but it may be + // before, equal or after `searchKey`. In `getPosition`, `retrieveEntryIfAcceptable` reads the + // actual key in data file to decide which case we're on, and base on that whether we want that entry, + // or the one after that (in order). But here, we explicitly want to avoid that read to the data file, + // so: + // - if it is a left bound, we eagerly accept a prefix entry. If `K > searchKey`, then that's the + // "best" answer anyway. If `K < searchKey`, then returning the "next" key would have been "best", + // but returning `K` is only "one off" and still covers `searchKey`, so it is acceptable. + // - if it is a right bound, then we do not accept a prefix. Whatever `K` is, we will only accept the + // "next" key (`assumeNoMatch` will then be `true`). If it happened that `K < searchKey`, then + // we were right to not return `K` and the "next" key is the best choice. If `K > searchKey`, + // then we're again "one off" compared to the best option, but as we cover `searchKey`, it is + // acceptable. + // We didn't mention `K = searchKey` above because whether it falls in the camp of `>` or `<` in the + // cases above depend on whether `searchOp` is GT or GE, but the overall resonable extend there + // otherwise. + return isLeftBound || assumeNoMatch ? new TrieIndexEntry(~pos) : null; + }); + } + catch (IOException e) + { + markSuspect(); + throw new CorruptSSTableException(e, rowIndexFile.path()); + } + } + + @Override + public PartitionPositionBounds getApproximatePositionsForBounds(AbstractBounds bounds) + { + TrieIndexEntry rieLeft = getApproximatePosition(bounds.left, bounds.inclusiveLeft() ? Operator.GE : Operator.GT, true); + if (rieLeft == null) // empty range + return null; + long left = rieLeft.position; + + TrieIndexEntry rieRight = bounds.right.isMinimum() + ? null + : getApproximatePosition(bounds.right, bounds.inclusiveRight() ? Operator.GT : Operator.GE, false); + long right; + if (rieRight != null) + right = rieRight.position; + else // right is beyond end + right = uncompressedLength(); // this should also be correct for EARLY readers + + if (left >= right) + { + // empty range + return null; + } + + return new PartitionPositionBounds(left, right); + } + /** * Called by {@link #getRowIndexEntry} above (via Reader.ceiling/floor) to check if the position satisfies the full * key constraint. This is called once if there is a prefix match (which can be in any relationship with the sought @@ -209,15 +284,16 @@ private TrieIndexEntry retrieveEntryIfAcceptable(Operator searchOp, PartitionPos } @Override - public DecoratedKey keyAtPositionFromSecondaryIndex(long keyPositionFromSecondaryIndex) throws IOException + public IKeyFetcher openKeyFetcher(boolean isForSASI) { - try (RandomAccessReader reader = openDataReader()) + return new AbstractKeyFetcher(openDataReader(ReadPattern.RANDOM)) { - reader.seek(keyPositionFromSecondaryIndex); - if (reader.isEOF()) - return null; - return decorateKey(ByteBufferUtil.readWithShortLength(reader)); - } + @Override + public DecoratedKey readKey(RandomAccessReader reader) throws IOException + { + return decorateKey(ByteBufferUtil.readWithShortLength(reader)); + } + }; } TrieIndexEntry getExactPosition(DecoratedKey dk, @@ -230,12 +306,14 @@ TrieIndexEntry getExactPosition(DecoratedKey dk, return null; } - if (!isPresentInFilter(dk)) + if (!inBloomFilter(dk)) { notifySkipped(SkippingReason.BLOOM_FILTER, listener, EQ, updateStats); return null; } + listener.onSSTablePartitionIndexAccessed(this); + try (PartitionIndex.Reader reader = partitionIndex.openReader()) { long indexPos = reader.exactCandidate(dk); @@ -384,7 +462,9 @@ public UnfilteredRowIterator rowIterator(FileDataInput dataFileInput, @Override public BtiTableReader cloneAndReplace(IFilter filter) { - return unbuildTo(new Builder(descriptor).setFilter(filter), true).build(owner().orElse(null), true, true); + BtiTableReader replacement = unbuildTo(new Builder(descriptor).setFilter(filter), true).build(owner().orElse(null), true, true); + replacement.approximateBloomFilterMemorySize = approximateBloomFilterMemorySize; + return replacement; } @Override @@ -418,10 +498,12 @@ public BtiTableReader cloneWithNewStart(DecoratedKey newStart) */ private BtiTableReader cloneAndReplace(DecoratedKey newFirst, OpenReason reason) { - return unbuildTo(new Builder(descriptor), true) + BtiTableReader replacement = unbuildTo(new Builder(descriptor), true) .setFirst(newFirst) .setOpenReason(reason) .build(owner().orElse(null), true, true); + replacement.approximateBloomFilterMemorySize = approximateBloomFilterMemorySize; + return replacement; } @Override @@ -467,7 +549,7 @@ public UnfilteredPartitionIterator partitionIterator(ColumnFilter columnFilter, @Override public IVerifier getVerifier(ColumnFamilyStore cfs, OutputHandler outputHandler, boolean isOffline, IVerifier.Options options) { - Preconditions.checkArgument(cfs.metadata().equals(metadata())); + Preconditions.checkArgument(cfs == null || cfs.metadata().equals(metadata())); return new BtiTableVerifier(cfs, this, outputHandler, isOffline, options); } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReaderLoadingBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReaderLoadingBuilder.java index fa408adc5d0e..73625c07da1c 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReaderLoadingBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReaderLoadingBuilder.java @@ -19,6 +19,7 @@ package org.apache.cassandra.io.sstable.format.bti; import java.io.IOException; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,10 +33,14 @@ import org.apache.cassandra.io.sstable.format.FilterComponent; import org.apache.cassandra.io.sstable.format.SortedTableReaderLoadingBuilder; import org.apache.cassandra.io.sstable.format.StatsComponent; +import org.apache.cassandra.io.sstable.format.TOCComponent; +import org.apache.cassandra.io.sstable.format.big.BigFormat; import org.apache.cassandra.io.sstable.format.bti.BtiFormat.Components; import org.apache.cassandra.io.sstable.metadata.MetadataType; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.sstable.metadata.ValidationMetadata; +import org.apache.cassandra.io.sstable.metadata.ZeroCopyMetadata; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.utils.FilterFactory; @@ -68,8 +73,8 @@ private KeyReader createKeyReader(StatsMetadata statsMetadata) throws IOExceptio { checkNotNull(statsMetadata); - try (PartitionIndex index = PartitionIndex.load(partitionIndexFileBuilder(), tableMetadataRef.getLocal().partitioner, false); - CompressionMetadata compressionMetadata = CompressionInfoComponent.maybeLoad(descriptor, components); + try (PartitionIndex index = PartitionIndex.load(partitionIndexFileBuilder(), tableMetadataRef.getLocal().partitioner, false, descriptor.version.getByteComparableVersion()); + CompressionMetadata compressionMetadata = CompressionInfoComponent.maybeLoad(descriptor, components, statsMetadata.zeroCopyMetadata); FileHandle dFile = dataFileBuilder(statsMetadata).withCompressionMetadata(compressionMetadata) .withCrcCheckChance(() -> tableMetadataRef.getLocal().params.crcCheckChance) .complete(); @@ -88,11 +93,12 @@ protected void openComponents(BtiTableReader.Builder builder, SSTable.Owner owne { try { - StatsComponent statsComponent = StatsComponent.load(descriptor, MetadataType.STATS, MetadataType.VALIDATION, MetadataType.HEADER); - builder.setSerializationHeader(statsComponent.serializationHeader(builder.getTableMetadataRef().getLocal())); + StatsComponent statsComponent = StatsComponent.load(descriptor, MetadataType.STATS, MetadataType.VALIDATION, MetadataType.HEADER, MetadataType.COMPACTION); + builder.setSerializationHeader(statsComponent.serializationHeader(descriptor, builder.getTableMetadataRef().getLocal(), !online)); checkArgument(!online || builder.getSerializationHeader() != null); builder.setStatsMetadata(statsComponent.statsMetadata()); + builder.setCompactionMetadata(Optional.ofNullable(statsComponent.compactionMetadata())); ValidationMetadata validationMetadata = statsComponent.validationMetadata(); validatePartitioner(builder.getTableMetadataRef().getLocal(), validationMetadata); @@ -106,8 +112,17 @@ protected void openComponents(BtiTableReader.Builder builder, SSTable.Owner owne IFilter filter = buildBloomFilter(statsComponent.statsMetadata()); builder.setFilter(filter); FilterComponent.save(filter, descriptor, false); + if (validationMetadata.bloomFilterFPChance != tableMetadataRef.getLocal().params.bloomFilterFpChance) + { + StatsComponent.load(descriptor, MetadataType.values()) + .with(validationMetadata.withBloomFilterFPChance(tableMetadataRef.getLocal().params.bloomFilterFpChance)) + .save(descriptor); + } + if (descriptor.fileFor(Components.FILTER).exists()) + TOCComponent.maybeAdd(descriptor, BigFormat.Components.FILTER); } + if (builder.getFilter() == null) builder.setFilter(FilterFactory.AlwaysPresent); @@ -123,7 +138,7 @@ protected void openComponents(BtiTableReader.Builder builder, SSTable.Owner owne if (builder.getComponents().contains(Components.PARTITION_INDEX)) { - builder.setPartitionIndex(openPartitionIndex(!builder.getFilter().isInformative())); + builder.setPartitionIndex(openPartitionIndex(!builder.getFilter().isInformative(), statsComponent.statsMetadata().zeroCopyMetadata)); if (builder.getFirst() == null || builder.getLast() == null) { builder.setFirst(builder.getPartitionIndex().firstKey()); @@ -131,7 +146,7 @@ protected void openComponents(BtiTableReader.Builder builder, SSTable.Owner owne } } - try (CompressionMetadata compressionMetadata = CompressionInfoComponent.maybeLoad(descriptor, components)) + try (CompressionMetadata compressionMetadata = CompressionInfoComponent.maybeLoad(descriptor, components, statsComponent.statsMetadata().zeroCopyMetadata)) { builder.setDataFile(dataFileBuilder(builder.getStatsMetadata()) .withCompressionMetadata(compressionMetadata) @@ -142,7 +157,7 @@ protected void openComponents(BtiTableReader.Builder builder, SSTable.Owner owne catch (IOException | RuntimeException | Error ex) { // in case of failure, close only those components which have been opened in this try-catch block - Throwables.closeAndAddSuppressed(ex, builder.getPartitionIndex(), builder.getRowIndexFile(), builder.getDataFile(), builder.getFilter()); + Throwables.closeNonNullAndAddSuppressed(ex, builder.getPartitionIndex(), builder.getRowIndexFile(), builder.getDataFile(), builder.getFilter()); throw ex; } } @@ -165,18 +180,18 @@ private IFilter buildBloomFilter(StatsMetadata statsMetadata) throws IOException } catch (IOException | RuntimeException | Error ex) { - Throwables.closeAndAddSuppressed(ex, bf); + Throwables.closeNonNullAndAddSuppressed(ex, bf); throw ex; } return bf; } - private PartitionIndex openPartitionIndex(boolean preload) throws IOException + private PartitionIndex openPartitionIndex(boolean preload, ZeroCopyMetadata zeroCopyMetadata) throws IOException { try (FileHandle indexFile = partitionIndexFileBuilder().complete()) { - return PartitionIndex.load(indexFile, tableMetadataRef.getLocal().partitioner, preload); + return PartitionIndex.load(indexFile, tableMetadataRef.getLocal().partitioner, preload, zeroCopyMetadata, descriptor.version.getByteComparableVersion()); } catch (IOException ex) { @@ -190,7 +205,7 @@ private FileHandle.Builder rowIndexFileBuilder() assert rowIndexFileBuilder == null || rowIndexFileBuilder.file.equals(descriptor.fileFor(Components.ROW_INDEX)); if (rowIndexFileBuilder == null) - rowIndexFileBuilder = new FileHandle.Builder(descriptor.fileFor(Components.ROW_INDEX)); + rowIndexFileBuilder = StorageProvider.instance.fileHandleBuilderFor(descriptor, Components.ROW_INDEX); rowIndexFileBuilder.withChunkCache(chunkCache); rowIndexFileBuilder.mmapped(ioOptions.indexDiskAccessMode); @@ -203,7 +218,7 @@ private FileHandle.Builder partitionIndexFileBuilder() assert partitionIndexFileBuilder == null || partitionIndexFileBuilder.file.equals(descriptor.fileFor(Components.PARTITION_INDEX)); if (partitionIndexFileBuilder == null) - partitionIndexFileBuilder = new FileHandle.Builder(descriptor.fileFor(Components.PARTITION_INDEX)); + partitionIndexFileBuilder = StorageProvider.instance.fileHandleBuilderFor(descriptor, Components.PARTITION_INDEX); partitionIndexFileBuilder.withChunkCache(chunkCache); partitionIndexFileBuilder.mmapped(ioOptions.indexDiskAccessMode); diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScanner.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScanner.java index 4507ccf7f5e4..1460bfbb2272 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScanner.java @@ -62,6 +62,11 @@ protected BtiScanningIterator doCreateIterator() return new BtiScanningIterator(); } + @Override + public int level() { + return sstable.getSSTableLevel(); + } + protected class BtiScanningIterator extends SSTableScanner.BaseKeyScanningIterator implements Closeable { private PartitionIterator iterator; diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScrubber.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScrubber.java index 238ed7e7de58..1f6cbc8ec475 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScrubber.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableScrubber.java @@ -24,7 +24,6 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.TypeSizes; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.rows.UnfilteredRowIterator; @@ -33,6 +32,7 @@ import org.apache.cassandra.io.sstable.SSTableRewriter; import org.apache.cassandra.io.sstable.format.SortedTableScrubber; import org.apache.cassandra.io.sstable.format.bti.BtiFormat.Components; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; @@ -52,7 +52,7 @@ public BtiTableScrubber(ColumnFamilyStore cfs, { super(cfs, transaction, outputHandler, options); - boolean hasIndexFile = sstable.getComponents().contains(Components.PARTITION_INDEX); + boolean hasIndexFile = sstable.components().contains(Components.PARTITION_INDEX); this.isIndex = cfs.isIndex(); this.partitionKeyType = cfs.metadata.get().partitionKeyType; if (!hasIndexFile) @@ -87,19 +87,18 @@ private ScrubPartitionIterator openIndexIterator() } @Override - protected UnfilteredRowIterator withValidation(UnfilteredRowIterator iter, String filename) + protected UnfilteredRowIterator withValidation(UnfilteredRowIterator iter, File file) { - return options.checkData && !isIndex ? UnfilteredRowIterators.withValidation(iter, filename) : iter; + return options.checkData && !isIndex ? UnfilteredRowIterators.withValidation(iter, file) : iter; } @Override public void scrubInternal(SSTableRewriter writer) { - if (indexAvailable() && indexIterator.dataPosition() != 0) + if (indexAvailable() && indexIterator.dataPosition() != sstable.getDataFileSliceDescriptor().dataStart) { - outputHandler.warn("First position reported by index should be 0, was " + - indexIterator.dataPosition() + - ", continuing without index."); + outputHandler.warn("First position reported by index should be {}, was {}, continuing without index.", + sstable.getDataFileSliceDescriptor().dataStart, indexIterator.dataPosition()); indexIterator.close(); indexIterator = null; } @@ -108,8 +107,7 @@ public void scrubInternal(SSTableRewriter writer) while (!dataFile.isEOF()) { - if (scrubInfo.isStopRequested()) - throw new CompactionInterruptedException(scrubInfo.getCompactionInfo()); + scrubInfo.throwIfStopRequested(); // position in a data file where the partition starts long dataStart = dataFile.getFilePointer(); diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableVerifier.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableVerifier.java index 6125af805b03..d6f76eea8c90 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableVerifier.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableVerifier.java @@ -33,6 +33,12 @@ public BtiTableVerifier(ColumnFamilyStore cfs, BtiTableReader sstable, OutputHan protected void verifyPartition(DecoratedKey key, UnfilteredRowIterator iterator) { + if (options.validateAllRows) + { + // validate all rows and cells + while (iterator.hasNext()) + iterator.next(); + } // The trie writers abort if supplied with badly ordered or duplicate row keys. Verification is not necessary. // no-op, just open and close partition. } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java index c91db2ecc866..598c6deb1f9d 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java @@ -19,14 +19,20 @@ import java.io.IOException; import java.util.Collection; +import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; +import javax.annotation.Nullable; + import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionTime; import org.apache.cassandra.db.compaction.OperationType; @@ -41,17 +47,28 @@ import org.apache.cassandra.io.sstable.format.IndexComponent; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableReader.OpenReason; +import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SortedTableWriter; import org.apache.cassandra.io.sstable.format.bti.BtiFormat.Components; +import org.apache.cassandra.io.sstable.metadata.CompactionMetadata; +import org.apache.cassandra.io.sstable.metadata.MetadataComponent; +import org.apache.cassandra.io.sstable.metadata.MetadataType; +import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.DataPosition; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.MmappedRegionsCache; import org.apache.cassandra.io.util.SequentialWriter; +import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.IFilter; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.io.compress.EncryptedSequentialWriter; +import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.schema.TableMetadata; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; @@ -82,7 +99,7 @@ protected TrieIndexEntry createRowIndexEntry(DecoratedKey key, DeletionTime part } @SuppressWarnings({ "resource", "RedundantSuppression" }) // dataFile is closed along with the reader - private BtiTableReader openInternal(OpenReason openReason, boolean isFinal, Supplier partitionIndexSupplier) + private BtiTableReader openInternal(OpenReason openReason, long lengthOverride, Supplier partitionIndexSupplier) { IFilter filter = null; FileHandle dataFile = null; @@ -95,11 +112,13 @@ private BtiTableReader openInternal(OpenReason openReason, boolean isFinal, Supp try { - builder.setStatsMetadata(statsMetadata()); + Map finalMetadata = finalizeMetadata(); + builder.setStatsMetadata((StatsMetadata) finalMetadata.get(MetadataType.STATS)); + builder.setCompactionMetadata(Optional.ofNullable((CompactionMetadata)finalMetadata.get(MetadataType.COMPACTION))); partitionIndex = partitionIndexSupplier.get(); rowIndexFile = indexWriter.rowIndexFHBuilder.complete(); - dataFile = openDataFile(isFinal ? NO_LENGTH_OVERRIDE : dataWriter.getLastFlushOffset(), builder.getStatsMetadata()); + dataFile = openDataFile(lengthOverride, builder.getStatsMetadata()); filter = indexWriter.getFilterCopy(); return builder.setPartitionIndex(partitionIndex) @@ -121,11 +140,12 @@ private BtiTableReader openInternal(OpenReason openReason, boolean isFinal, Supp @Override public void openEarly(Consumer callWhenReady) { - long dataLength = dataWriter.position(); + // Because the partition index writer is one partition behind, we want the file to stop at the start of the + // last partition that was written. + long dataLength = partitionWriter.getInitialPosition(); indexWriter.buildPartial(dataLength, partitionIndex -> { - indexWriter.rowIndexFHBuilder.withLengthOverride(indexWriter.rowIndexWriter.getLastFlushOffset()); - BtiTableReader reader = openInternal(OpenReason.EARLY, false, () -> partitionIndex); + BtiTableReader reader = openInternal(OpenReason.EARLY, dataLength, () -> partitionIndex); callWhenReady.accept(reader); }); } @@ -151,7 +171,13 @@ protected SSTableReader openFinal(OpenReason openReason) if (maxDataAge < 0) maxDataAge = Clock.Global.currentTimeMillis(); - return openInternal(openReason, true, indexWriter::completedPartitionIndex); + return openInternal(openReason, NO_LENGTH_OVERRIDE, indexWriter::completedPartitionIndex); + } + + @Override + public void openResult(@javax.annotation.Nullable org.apache.cassandra.io.sstable.StorageHandler storageHandler) + { + txnProxy.openResult(storageHandler); } /** @@ -168,18 +194,66 @@ protected static class IndexWriter extends SortedTableWriter.AbstractIndexWriter private DataPosition riMark; private DataPosition piMark; + @Nullable + private final TableMetrics tableMetrics; + IndexWriter(Builder b, SequentialWriter dataWriter) { super(b); - rowIndexWriter = new SequentialWriter(descriptor.fileFor(Components.ROW_INDEX), b.getIOOptions().writerOptions); - rowIndexFHBuilder = IndexComponent.fileBuilder(Components.ROW_INDEX, b).withMmappedRegionsCache(b.getMmappedRegionsCache()); - partitionIndexWriter = new SequentialWriter(descriptor.fileFor(Components.PARTITION_INDEX), b.getIOOptions().writerOptions); - partitionIndexFHBuilder = IndexComponent.fileBuilder(Components.PARTITION_INDEX, b).withMmappedRegionsCache(b.getMmappedRegionsCache()); - partitionIndex = new PartitionIndexBuilder(partitionIndexWriter, partitionIndexFHBuilder); + + // Check if encryption is enabled (following trie-index pattern) + boolean compression = b.getComponents().contains(SSTableFormat.Components.COMPRESSION_INFO); + TableMetadata metadata = b.getTableMetadataRef().getLocal(); + CompressionParams params = metadata.params.compression; + ICompressor encryptor = compression ? params.getSstableCompressor().encryptionOnly() : null; + + if (encryptor != null) + { + // Create encrypted writers and configure FileHandle builders for encryption + CompressionMetadata compressionMetadata = CompressionMetadata.encryptedOnly(params); + rowIndexWriter = new EncryptedSequentialWriter(descriptor.fileFor(Components.ROW_INDEX), + b.getIOOptions().writerOptions, + encryptor); + rowIndexFHBuilder = IndexComponent.fileBuilder(Components.ROW_INDEX, b, b.operationType) + .withMmappedRegionsCache(b.getMmappedRegionsCache()) + .withCompressionMetadata(compressionMetadata) + .maybeEncrypted(true); + + partitionIndexWriter = new EncryptedSequentialWriter(descriptor.fileFor(Components.PARTITION_INDEX), + b.getIOOptions().writerOptions, + encryptor); + partitionIndexFHBuilder = IndexComponent.fileBuilder(Components.PARTITION_INDEX, b, b.operationType) + .withMmappedRegionsCache(b.getMmappedRegionsCache()) + .withCompressionMetadata(compressionMetadata) + .maybeEncrypted(true); + } + else + { + // Create regular writers + rowIndexWriter = new SequentialWriter(descriptor.fileFor(Components.ROW_INDEX), b.getIOOptions().writerOptions); + rowIndexFHBuilder = IndexComponent.fileBuilder(Components.ROW_INDEX, b, b.operationType) + .withMmappedRegionsCache(b.getMmappedRegionsCache()); + partitionIndexWriter = new SequentialWriter(descriptor.fileFor(Components.PARTITION_INDEX), b.getIOOptions().writerOptions); + partitionIndexFHBuilder = IndexComponent.fileBuilder(Components.PARTITION_INDEX, b, b.operationType) + .withMmappedRegionsCache(b.getMmappedRegionsCache()); + } + partitionIndex = new PartitionIndexBuilder(partitionIndexWriter, partitionIndexFHBuilder, descriptor.version.getByteComparableVersion()); + // register listeners to be alerted when the data files are flushed partitionIndexWriter.setPostFlushListener(partitionIndex::markPartitionIndexSynced); rowIndexWriter.setPostFlushListener(partitionIndex::markRowIndexSynced); dataWriter.setPostFlushListener(partitionIndex::markDataSynced); + + + // The per-table bloom filter memory is tracked when: + // 1. Periodic early open: Opens incomplete sstables when size threshold is hit during writing. + // The BF memory usage is tracked via Tracker. + // 2. Completion early open: Opens completed sstables when compaction results in multiple sstables. + // The BF memory usage is tracked via Tracker. + // 3. A new sstable is first created here if early-open is not enabled. + tableMetrics = DatabaseDescriptor.getSSTablePreemptiveOpenIntervalInMiB() <= 0 ? ColumnFamilyStore.metricsForIfPresent(metadata.id) : null; + if (tableMetrics != null && bf != null) + tableMetrics.inFlightBloomFilterOffHeapMemoryUsed.getAndAdd(bf.offHeapSize()); } public long append(DecoratedKey key, AbstractRowIndexEntry indexEntry) throws IOException @@ -214,7 +288,14 @@ public long append(DecoratedKey key, AbstractRowIndexEntry indexEntry) throws IO public boolean buildPartial(long dataPosition, Consumer callWhenReady) { - return partitionIndex.buildPartial(callWhenReady, rowIndexWriter.position(), dataPosition); + long rowIndexPosition = rowIndexWriter.position(); + return partitionIndex.buildPartial(partitionIndex -> + { + rowIndexFHBuilder.withLengthOverride(rowIndexPosition); + callWhenReady.accept(partitionIndex); + rowIndexFHBuilder.withLengthOverride(NO_LENGTH_OVERRIDE); + }, + rowIndexPosition, dataPosition); } public void mark() @@ -238,7 +319,12 @@ protected void doPrepare() // truncate index file rowIndexWriter.prepareToCommit(); - rowIndexFHBuilder.withLengthOverride(rowIndexWriter.getLastFlushOffset()); + + // For encrypted writers, we don't use getLastFlushOffset() as the length override + if (!(rowIndexWriter instanceof EncryptedSequentialWriter)) + { + rowIndexFHBuilder.withLengthOverride(rowIndexWriter.getLastFlushOffset()); + } complete(); } @@ -252,6 +338,16 @@ void complete() throws FSWriteError { partitionIndex.complete(); partitionIndexCompleted = true; + + // Update FileHandle builders for encrypted writers + if (rowIndexWriter instanceof EncryptedSequentialWriter) + { + ((EncryptedSequentialWriter) rowIndexWriter).updateFileHandle(rowIndexFHBuilder, rowIndexWriter.position()); + } + if (partitionIndexWriter instanceof EncryptedSequentialWriter) + { + ((EncryptedSequentialWriter) partitionIndexWriter).updateFileHandle(partitionIndexFHBuilder, partitionIndexWriter.position()); + } } catch (IOException e) { @@ -262,11 +358,19 @@ void complete() throws FSWriteError PartitionIndex completedPartitionIndex() { complete(); - rowIndexFHBuilder.withLengthOverride(NO_LENGTH_OVERRIDE); - partitionIndexFHBuilder.withLengthOverride(NO_LENGTH_OVERRIDE); + // For encrypted writers, the length override has been set by updateFileHandle() + // Don't reset it to NO_LENGTH_OVERRIDE + if (!(rowIndexWriter instanceof EncryptedSequentialWriter)) + { + rowIndexFHBuilder.withLengthOverride(NO_LENGTH_OVERRIDE); + } + if (!(partitionIndexWriter instanceof EncryptedSequentialWriter)) + { + partitionIndexFHBuilder.withLengthOverride(NO_LENGTH_OVERRIDE); + } try { - return PartitionIndex.load(partitionIndexFHBuilder, metadata.getLocal().partitioner, false); + return PartitionIndex.load(partitionIndexFHBuilder, metadata.getLocal().partitioner, false, descriptor.version.getByteComparableVersion()); } catch (IOException e) { @@ -287,6 +391,8 @@ protected Throwable doAbort(Throwable accumulate) @Override protected Throwable doPostCleanup(Throwable accumulate) { + if (tableMetrics != null && bf != null) + tableMetrics.inFlightBloomFilterOffHeapMemoryUsed.getAndAdd(-bf.offHeapSize()); return Throwables.close(accumulate, bf, partitionIndex, rowIndexWriter, partitionIndexWriter); } } @@ -324,17 +430,26 @@ public MmappedRegionsCache getMmappedRegionsCache() return ensuringInBuildInternalContext(mmappedRegionsCache); } + @Override + protected OperationType getOperationType() + { + return ensuringInBuildInternalContext(operationType); + } + @Override protected SequentialWriter openDataWriter() { checkState(!dataWriterOpened, "Data writer has been already opened."); - return DataComponent.buildWriter(descriptor, - getTableMetadataRef().getLocal(), - getIOOptions().writerOptions, - getMetadataCollector(), - ensuringInBuildInternalContext(operationType), - getIOOptions().flushCompression); + SequentialWriter sequentialWriter = DataComponent.buildWriter(descriptor, + getTableMetadataRef().getLocal(), + getIOOptions().writerOptions, + getMetadataCollector(), + ensuringInBuildInternalContext(operationType), + getIOOptions().flushCompression); + dataWriterOpened = true; + + return sequentialWriter; } @Override @@ -382,7 +497,7 @@ protected BtiTableWriter buildInternal(LifecycleNewTracker lifecycleNewTracker, } catch (RuntimeException | Error ex) { - Throwables.closeAndAddSuppressed(ex, mmappedRegionsCache); + Throwables.closeNonNullAndAddSuppressed(ex, mmappedRegionsCache); throw ex; } finally diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndex.java b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndex.java index cfdfe37eede2..112a14c85528 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndex.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndex.java @@ -28,20 +28,25 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.metadata.ZeroCopyMetadata; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.tries.SerializationNode; import org.apache.cassandra.io.tries.TrieNode; import org.apache.cassandra.io.tries.TrieSerializer; import org.apache.cassandra.io.tries.ValueIterator; import org.apache.cassandra.io.tries.Walker; import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.PageAware; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.io.util.Rebufferer; import org.apache.cassandra.io.util.SizedInts; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.cassandra.utils.concurrent.Ref; import org.apache.cassandra.utils.concurrent.SharedCloseable; @@ -73,24 +78,37 @@ public class PartitionIndex implements SharedCloseable private final DecoratedKey first; private final DecoratedKey last; private final long root; + /** Key to apply when a caller asks for a full index. Normally null, but set to first for zero-copied indexes. */ + private final DecoratedKey filterFirst; + /** Key to apply when a caller asks for a full index. Normally null, but set to last for zero-copied indexes. */ + private final DecoratedKey filterLast; + + public final ByteComparable.Version version; public static final long NOT_FOUND = Long.MIN_VALUE; public static final int FOOTER_LENGTH = 3 * 8; private static final int FLAG_HAS_HASH_BYTE = 8; - @VisibleForTesting - public PartitionIndex(FileHandle fh, long trieRoot, long keyCount, DecoratedKey first, DecoratedKey last) + public PartitionIndex(FileHandle fh, long trieRoot, long keyCount, DecoratedKey first, DecoratedKey last, ByteComparable.Version version) + { + this(fh, trieRoot, keyCount, first, last, null, null, version); + } + + public PartitionIndex(FileHandle fh, long trieRoot, long keyCount, DecoratedKey first, DecoratedKey last, DecoratedKey filterFirst, DecoratedKey filterLast, ByteComparable.Version version) { this.keyCount = keyCount; this.fh = fh.sharedCopy(); this.first = first; this.last = last; this.root = trieRoot; + this.filterFirst = filterFirst; + this.filterLast = filterLast; + this.version = version; } protected PartitionIndex(PartitionIndex src) { - this(src.fh, src.root, src.keyCount, src.first, src.last); + this(src.fh, src.root, src.keyCount, src.first, src.last, src.filterFirst, src.filterLast, src.version); } static class Payload @@ -169,23 +187,37 @@ public void addTo(Ref.IdentityCollection identities) public static PartitionIndex load(FileHandle.Builder fhBuilder, IPartitioner partitioner, - boolean preload) throws IOException + boolean preload, + ByteComparable.Version version) throws IOException + { + return load(fhBuilder, partitioner, preload, null, version); + } + + public static PartitionIndex load(FileHandle.Builder fhBuilder, + IPartitioner partitioner, + boolean preload, + ZeroCopyMetadata zeroCopyMetadata, + ByteComparable.Version version) throws IOException { try (FileHandle fh = fhBuilder.complete()) { - return load(fh, partitioner, preload); + return load(fh, partitioner, preload, zeroCopyMetadata, version); } } - public static Pair readFirstAndLastKey(File file, IPartitioner partitioner) throws IOException + public static Pair readFirstAndLastKey(Descriptor descriptor, Component component, IPartitioner partitioner, ByteComparable.Version version) throws IOException { - try (PartitionIndex index = load(new FileHandle.Builder(file), partitioner, false)) + try (PartitionIndex index = load(StorageProvider.instance.fileHandleBuilderFor(descriptor, component), partitioner, false, version)) { return Pair.create(index.firstKey(), index.lastKey()); } } - public static PartitionIndex load(FileHandle fh, IPartitioner partitioner, boolean preload) throws IOException + public static PartitionIndex load(FileHandle fh, + IPartitioner partitioner, + boolean preload, + ZeroCopyMetadata zeroCopyMetadata, + ByteComparable.Version version) throws IOException { try (FileDataInput rdr = fh.createReader(fh.dataLength() - FOOTER_LENGTH)) { @@ -207,7 +239,26 @@ public static PartitionIndex load(FileHandle fh, IPartitioner partitioner, boole logger.trace("Checksum {}", csum); // Note: trace is required so that reads aren't optimized away. } - return new PartitionIndex(fh, root, keyCount, first, last); + DecoratedKey filterFirst = null; + DecoratedKey filterLast = null; + + // Adjust keys estimate plus bounds if ZeroCopy, otherwise we would see un-owned data from the index: + if (zeroCopyMetadata != null && zeroCopyMetadata.exists() && partitioner != null) + { + DecoratedKey newFirst = partitioner.decorateKey(zeroCopyMetadata.firstKey()); + DecoratedKey newLast = partitioner.decorateKey(zeroCopyMetadata.lastKey()); + if (!newFirst.equals(first)) + { + filterFirst = first = newFirst; + } + if (!newLast.equals(last)) + { + filterLast = last = newLast; + } + keyCount = zeroCopyMetadata.estimatedKeys(); + } + + return new PartitionIndex(fh, root, keyCount, first, last, filterFirst, filterLast, version); } } @@ -225,7 +276,7 @@ public Throwable close(Throwable accumulate) public Reader openReader() { - return new Reader(this); + return new Reader(this, version); } protected IndexPosIterator allKeysIterator() @@ -235,7 +286,7 @@ protected IndexPosIterator allKeysIterator() protected Rebufferer instantiateRebufferer() { - return fh.instantiateRebufferer(null); + return fh.instantiateRebufferer(null, ReadPattern.SEQUENTIAL); } @@ -270,9 +321,9 @@ public interface Acceptor */ public static class Reader extends Walker { - protected Reader(PartitionIndex index) + protected Reader(PartitionIndex index, ByteComparable.Version version) { - super(index.instantiateRebufferer(), index.root); + super(index.instantiateRebufferer(), index.root, version); } /** @@ -388,9 +439,6 @@ protected int payloadSize() */ public static class IndexPosIterator extends ValueIterator { - static final long INVALID = -1; - long pos = INVALID; - /** * @param index PartitionIndex to use for the iteration. *

    @@ -399,12 +447,12 @@ public static class IndexPosIterator extends ValueIterator */ public IndexPosIterator(PartitionIndex index) { - super(index.instantiateRebufferer(), index.root); + super(index.instantiateRebufferer(), index.root, index.filterFirst, index.filterLast, LeftBoundTreatment.ADMIT_PREFIXES, index.version); } IndexPosIterator(PartitionIndex index, PartitionPosition start, PartitionPosition end) { - super(index.instantiateRebufferer(), index.root, start, end, true); + super(index.instantiateRebufferer(), index.root, start, end, LeftBoundTreatment.ADMIT_PREFIXES, index.version); } /** @@ -412,18 +460,12 @@ public IndexPosIterator(PartitionIndex index) */ protected long nextIndexPos() { - // without missing positions, we save and reuse the unreturned position. - if (pos == INVALID) - { - pos = nextPayloadedNode(); - if (pos == INVALID) - return NOT_FOUND; - } - - go(pos); + return nextValueAsLong(this::getCurrentIndexPos, NOT_FOUND); + } - pos = INVALID; // make sure next time we call nextPayloadedNode() again - return getIndexPos(buf, payloadPosition(), payloadFlags()); // this should not throw + private long getCurrentIndexPos() + { + return getIndexPos(buf, payloadPosition(), payloadFlags()); } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexBuilder.java index 803f43efe2af..99556a3699ce 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexBuilder.java @@ -22,7 +22,6 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.io.tries.IncrementalTrieWriter; -import org.apache.cassandra.io.tries.Walker; import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.SequentialWriter; import org.apache.cassandra.utils.ByteBufferUtil; @@ -33,11 +32,13 @@ *

    * The files created by this builder are read by {@link PartitionIndex}. */ -class PartitionIndexBuilder implements AutoCloseable +// Used by CNDB +public class PartitionIndexBuilder implements AutoCloseable { private final SequentialWriter writer; private final IncrementalTrieWriter trieWriter; private final FileHandle.Builder fhBuilder; + private final ByteComparable.Version version; // the last synced data file position private long dataSyncPosition; @@ -60,10 +61,11 @@ class PartitionIndexBuilder implements AutoCloseable private DecoratedKey lastWrittenKey; private PartitionIndex.Payload lastPayload; - public PartitionIndexBuilder(SequentialWriter writer, FileHandle.Builder fhBuilder) + public PartitionIndexBuilder(SequentialWriter writer, FileHandle.Builder fhBuilder, ByteComparable.Version version) { + this.version = version; this.writer = writer; - this.trieWriter = IncrementalTrieWriter.open(PartitionIndex.TRIE_SERIALIZER, writer); + this.trieWriter = IncrementalTrieWriter.open(PartitionIndex.TRIE_SERIALIZER, writer, version); this.fhBuilder = fhBuilder; } @@ -110,7 +112,14 @@ private void refreshReadableBoundary() try (FileHandle fh = fhBuilder.withLengthOverride(writer.getLastFlushOffset()).complete()) { - PartitionIndex pi = new PartitionIndexEarly(fh, partialIndexTail.root(), partialIndexTail.count(), firstKey, partialIndexLastKey, partialIndexTail.cutoff(), partialIndexTail.tail()); + PartitionIndex pi = new PartitionIndexEarly(fh, + partialIndexTail.root(), + partialIndexTail.count(), + firstKey.retainable(), + partialIndexLastKey.retainable(), + partialIndexTail.cutoff(), + partialIndexTail.tail(), + version); partialIndexConsumer.accept(pi); partialIndexConsumer = null; } @@ -136,7 +145,7 @@ public void addEntry(DecoratedKey decoratedKey, long position) throws IOExceptio } else { - int diffPoint = ByteComparable.diffPoint(lastKey, decoratedKey, Walker.BYTE_COMPARABLE_VERSION); + int diffPoint = ByteComparable.diffPoint(lastKey, decoratedKey, version); ByteComparable prevPrefix = ByteComparable.cut(lastKey, Math.max(diffPoint, lastDiffPoint)); trieWriter.add(prevPrefix, lastPayload); lastWrittenKey = lastKey; diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexEarly.java b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexEarly.java index 6b056b701f5d..797e78e0d030 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexEarly.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIndexEarly.java @@ -23,6 +23,7 @@ import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.Rebufferer; import org.apache.cassandra.io.util.TailOverridingRebufferer; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** * Early-opened partition index. Part of the data is already written to file, but some nodes, including the ones in the @@ -35,9 +36,9 @@ class PartitionIndexEarly extends PartitionIndex final ByteBuffer tail; public PartitionIndexEarly(FileHandle fh, long trieRoot, long keyCount, DecoratedKey first, DecoratedKey last, - long cutoff, ByteBuffer tail) + long cutoff, ByteBuffer tail, ByteComparable.Version version) { - super(fh, trieRoot, keyCount, first, last); + super(fh, trieRoot, keyCount, first, last, null, last, version); this.cutoff = cutoff; this.tail = tail; } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReader.java b/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReader.java index 3bfd2903fc89..09f7cfe4e676 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReader.java @@ -30,6 +30,7 @@ import org.apache.cassandra.io.tries.Walker; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.io.util.SizedInts; import org.apache.cassandra.utils.bytecomparable.ByteComparable; @@ -65,7 +66,7 @@ public static class IndexInfo public RowIndexReader(FileHandle file, long root, Version version) { - super(file.instantiateRebufferer(null), root); + super(file.instantiateRebufferer(null, ReadPattern.SEQUENTIAL), root, version.getByteComparableVersion()); this.version = version; } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReverseIterator.java b/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReverseIterator.java index 0d7878973b43..c1fc52f69ad9 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReverseIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/RowIndexReverseIterator.java @@ -23,7 +23,9 @@ import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.sstable.format.bti.RowIndexReader.IndexInfo; import org.apache.cassandra.io.tries.ReverseValueIterator; +import org.apache.cassandra.io.tries.ValueIterator; import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.ReadPattern; import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** @@ -36,7 +38,7 @@ class RowIndexReverseIterator extends ReverseValueIterator trie; + private final ByteComparable.Version byeComparableVersion; private ByteComparable prevMax = null; private ByteComparable prevSep = null; RowIndexWriter(ClusteringComparator comparator, DataOutputPlus out, Version version) { this.comparator = comparator; - this.trie = IncrementalTrieWriter.open(RowIndexReader.getSerializer(version), out); + this.byeComparableVersion = version != null ? version.getByteComparableVersion() : null; + this.trie = IncrementalTrieWriter.open(RowIndexReader.getSerializer(version), out, byeComparableVersion); } void reset() @@ -79,8 +80,8 @@ public long complete(long endPos) throws IOException // Add a separator after the last section, so that greater inputs can be quickly rejected. // To maximize its efficiency we add it with the length of the last added separator. int i = 0; - ByteSource max = prevMax.asComparableBytes(Walker.BYTE_COMPARABLE_VERSION); - ByteSource sep = prevSep.asComparableBytes(Walker.BYTE_COMPARABLE_VERSION); + ByteSource max = prevMax.asComparableBytes(byeComparableVersion); + ByteSource sep = prevSep.asComparableBytes(byeComparableVersion); int c; while ((c = max.next()) == sep.next() && c != ByteSource.END_OF_STREAM) ++i; diff --git a/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryManager.java b/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryManager.java index 7de33ba2f117..e64397d71a27 100644 --- a/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryManager.java +++ b/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryManager.java @@ -102,8 +102,8 @@ private IndexSummaryManager(Supplier> indexSummariesProvider) long indexSummarySizeInMB = DatabaseDescriptor.getIndexSummaryCapacityInMiB(); int interval = DatabaseDescriptor.getIndexSummaryResizeIntervalInMinutes(); - logger.info("Initializing index summary manager with a memory pool size of {} MB and a resize interval of {} minutes", - indexSummarySizeInMB, interval); + logger.debug("Initializing index summary manager with a memory pool size of {} MB and a resize interval of {} minutes", + indexSummarySizeInMB, interval); setMemoryPoolCapacityInMB(DatabaseDescriptor.getIndexSummaryCapacityInMiB()); setResizeIntervalInMinutes(DatabaseDescriptor.getIndexSummaryResizeIntervalInMinutes()); @@ -219,7 +219,7 @@ private Pair> getRestributionTransactio { View view = cfStore.getTracker().getView(); allSSTables = ImmutableSet.copyOf(view.select(SSTableSet.CANONICAL)); - nonCompacting = ImmutableSet.copyOf(view.getUncompacting(allSSTables)); + nonCompacting = ImmutableSet.copyOf(view.getNoncompacting(allSSTables)); } while (null == (txn = cfStore.getTracker().tryModify(nonCompacting, OperationType.INDEX_SUMMARY))); @@ -283,9 +283,9 @@ public void redistributeSummaries() throws IOException * @return a list of new SSTableReader instances */ @VisibleForTesting - public static List redistributeSummaries(IndexSummaryRedistribution redistribution) throws IOException + public static > List redistributeSummaries(IndexSummaryRedistribution redistribution) throws IOException { - return (List) CompactionManager.instance.runAsActiveCompaction(redistribution, redistribution::redistributeSummaries); + return CompactionManager.instance.runIndexSummaryRedistribution(redistribution); } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryRedistribution.java b/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryRedistribution.java index d27969719ab0..e2ace80e537a 100644 --- a/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryRedistribution.java +++ b/src/java/org/apache/cassandra/io/sstable/indexsummary/IndexSummaryRedistribution.java @@ -33,9 +33,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.compaction.CompactionInfo; -import org.apache.cassandra.db.compaction.CompactionInfo.Unit; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.io.sstable.Downsampling; @@ -51,7 +49,7 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; -public class IndexSummaryRedistribution extends CompactionInfo.Holder +public class IndexSummaryRedistribution extends AbstractTableOperation { private static final Logger logger = LoggerFactory.getLogger(IndexSummaryRedistribution.class); @@ -119,8 +117,7 @@ public > List redistributeSu double totalReadsPerSec = 0.0; for (T sstable : redistribute) { - if (isStopRequested()) - throw new CompactionInterruptedException(getCompactionInfo()); + throwIfStopRequested(); if (sstable.getReadMeter() != null) { @@ -173,8 +170,7 @@ private > List adjustSamplin remainingSpace = memoryPoolCapacity; for (T sstable : sstables) { - if (isStopRequested()) - throw new CompactionInterruptedException(getCompactionInfo()); + throwIfStopRequested(); int minIndexInterval = sstable.metadata().params.minIndexInterval; int maxIndexInterval = sstable.metadata().params.maxIndexInterval; @@ -271,8 +267,7 @@ else if (targetNumEntries < currentNumEntries * DOWNSAMPLE_THESHOLD && newSampli toDownsample.addAll(forceUpsample); for (ResampleEntry entry : toDownsample) { - if (isStopRequested()) - throw new CompactionInterruptedException(getCompactionInfo()); + throwIfStopRequested(); T sstable = entry.sstable; logger.trace("Re-sampling index summary for {} from {}/{} to {}/{} of the original number of entries", @@ -355,9 +350,14 @@ static > Pair, List components, DataOutputPlus out, Version version) throws IOException; + void serialize(Map components, DataOutputPlus out, Descriptor descriptor) throws IOException; /** * Deserialize specified metadata components from given descriptor. @@ -100,4 +99,11 @@ public interface IMetadataSerializer * Replace the sstable metadata file ({@code -Statistics.db}) with the given components. */ void rewriteSSTableMetadata(Descriptor descriptor, Map currentComponents) throws IOException; + + /** + * Updates the sstable metadata components (works similarly to {@link #rewriteSSTableMetadata(Descriptor, Map)} but + * only updates the provided components rather than replacing the whole metadata map). + */ + void updateSSTableMetadata(Descriptor descriptor, Map updatedComponents) throws IOException; + } diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java index 7b841c7cd8de..d2cd3fbdb790 100644 --- a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java +++ b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java @@ -65,9 +65,9 @@ static EstimatedHistogram defaultCellPerPartitionCountHistogram() static EstimatedHistogram defaultPartitionSizeHistogram() { - // EH of 155 can track a max value of 3520571548412 i.e. 3.5TB - return new EstimatedHistogram(155); - + // EH of 150 can track a max value of 1414838745986, i.e., ~ 1.5PB + // see {@link MetadataCollectorTest#testFindMaxSampleWithoutOverflow} for details + return new EstimatedHistogram(150); } static TombstoneHistogram defaultTombstoneDropTimeHistogram() @@ -101,7 +101,9 @@ public static StatsMetadata defaultStatsMetadata() false, true, ByteBufferUtil.EMPTY_BYTE_BUFFER, - ByteBufferUtil.EMPTY_BYTE_BUFFER); + ByteBufferUtil.EMPTY_BYTE_BUFFER, + Collections.emptyMap(), + ZeroCopyMetadata.EMPTY); } protected EstimatedHistogram estimatedPartitionSize = defaultPartitionSizeHistogram(); @@ -177,6 +179,12 @@ public MetadataCollector(Iterable sstables, ClusteringComparator commitLogIntervals(intervals.build()); } + public MetadataCollector(Iterable sstables, ClusteringComparator comparator, int level) + { + this(sstables, comparator); + sstableLevel(level); + } + public MetadataCollector addKey(ByteBuffer key) { long hashed = MurmurHash.hash2_64(key, key.position(), key.remaining(), 0); @@ -363,7 +371,7 @@ public Map finalizeMetadata(String partitioner, components.put(MetadataType.VALIDATION, new ValidationMetadata(partitioner, bloomFilterFPChance)); components.put(MetadataType.STATS, new StatsMetadata(estimatedPartitionSize, estimatedCellPerPartitionCount, - commitLogIntervals, + commitLogIntervals != null ? commitLogIntervals : IntervalSet.empty(), timestampTracker.min(), timestampTracker.max(), localDeletionTimeTracker.min(), @@ -385,7 +393,9 @@ public Map finalizeMetadata(String partitioner, isTransient, hasPartitionLevelDeletions, firstKey, - lastKey)); + lastKey, + Collections.emptyMap(), + ZeroCopyMetadata.EMPTY)); components.put(MetadataType.COMPACTION, new CompactionMetadata(cardinality)); components.put(MetadataType.HEADER, header.toComponent()); return components; diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java index 5ecb582b04e6..d2791598978d 100644 --- a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java +++ b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java @@ -19,32 +19,35 @@ import java.io.FileNotFoundException; import java.io.IOException; -import java.util.Collections; +import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.EnumMap; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.function.UnaryOperator; import java.util.zip.CRC32; import com.google.common.base.Throwables; -import com.google.common.collect.Lists; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.schema.CompressionParams; import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.io.util.DataOutputStreamPlus; import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileDataInput; -import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.io.util.FileInputStreamPlus; import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt; @@ -64,24 +67,54 @@ public class MetadataSerializer implements IMetadataSerializer private static final int CHECKSUM_LENGTH = 4; // CRC32 - public void serialize(Map components, DataOutputPlus out, Version version) throws IOException + public void serialize(Map components, DataOutputPlus out, Descriptor descriptor) throws IOException { + Version version = descriptor.version; boolean checksum = version.hasMetadataChecksum(); CRC32 crc = new CRC32(); + final int componentsCount = components.size(); + // sort components by type - List sortedComponents = Lists.newArrayList(components.values()); - Collections.sort(sortedComponents); + MetadataComponent[] sortedComponents = components.values().toArray(new MetadataComponent[componentsCount]); + Arrays.sort(sortedComponents); // write number of component - out.writeInt(components.size()); - updateChecksumInt(crc, components.size()); + out.writeInt(componentsCount); + updateChecksumInt(crc, componentsCount); maybeWriteChecksum(crc, out, version); + ICompressor encryptor = getEncryptor(descriptor, true); + ByteBuffer[] componentsSerializations = new ByteBuffer[componentsCount]; + + // serialize and possibly encrypt components + for (int i = 0; i < componentsCount; ++i) + { + MetadataComponent metadataComponent = sortedComponents[i]; + MetadataType componentType = metadataComponent.getType(); + int size = componentType.serializer.serializedSize(version, metadataComponent); + + try (DataOutputBuffer dob = new DataOutputBuffer(size)) + { + componentType.serializer.serialize(version, metadataComponent, dob); + if (encryptor != null) + { + ByteBuffer encrypted = ByteBuffer.allocate(encryptor.initialCompressedBufferLength(size)); + encryptor.compress(dob.buffer(), encrypted); + encrypted.flip(); + componentsSerializations[i] = encrypted; + } + else + { + componentsSerializations[i] = dob.buffer(); + } + } + } + // build and write toc - int lastPosition = 4 + (8 * sortedComponents.size()) + (checksum ? 2 * CHECKSUM_LENGTH : 0); - Map sizes = new EnumMap<>(MetadataType.class); - for (MetadataComponent component : sortedComponents) + int lastPosition = 4 + (8 * componentsCount) + (checksum ? 2 * CHECKSUM_LENGTH : 0); + for (int i = 0; i < componentsCount; ++i) { + MetadataComponent component = sortedComponents[i]; MetadataType type = component.getType(); // serialize type out.writeInt(type.ordinal()); @@ -89,24 +122,18 @@ public void serialize(Map components, DataOutpu // serialize position out.writeInt(lastPosition); updateChecksumInt(crc, lastPosition); - int size = type.serializer.serializedSize(version, component); + int size = componentsSerializations[i].remaining(); lastPosition += size + (checksum ? CHECKSUM_LENGTH : 0); - sizes.put(type, size); } maybeWriteChecksum(crc, out, version); - // serialize components - for (MetadataComponent component : sortedComponents) + // copy components to output + for (int i = 0; i < componentsCount; ++i) { - byte[] bytes; - try (DataOutputBuffer dob = new DataOutputBuffer(sizes.get(component.getType()))) - { - component.getType().serializer.serialize(version, component, dob); - bytes = dob.getData(); - } + ByteBuffer bytes = componentsSerializations[i]; out.write(bytes); - - crc.reset(); crc.update(bytes); + crc.reset(); + crc.update(bytes); maybeWriteChecksum(crc, out, version); } } @@ -130,7 +157,7 @@ public Map deserialize(Descriptor descriptor, E } else { - try (RandomAccessReader r = RandomAccessReader.open(statsFile)) + try (FileInputStreamPlus r = new FileInputStreamPlus(statsFile)) { components = deserialize(descriptor, r, types); } @@ -144,7 +171,7 @@ public MetadataComponent deserialize(Descriptor descriptor, MetadataType type) t } public Map deserialize(Descriptor descriptor, - FileDataInput in, + FileInputStreamPlus in, EnumSet selectedTypes) throws IOException { @@ -155,7 +182,7 @@ public Map deserialize(Descriptor descriptor, * Read TOC */ - int length = (int) in.bytesRemaining(); + int length = (int) in.getChannel().size(); int count = in.readInt(); updateChecksumInt(crc, count); @@ -186,6 +213,7 @@ public Map deserialize(Descriptor descriptor, MetadataType[] allMetadataTypes = MetadataType.values(); Map components = new EnumMap<>(MetadataType.class); + ICompressor encryptor = getEncryptor(descriptor, false); for (int i = 0; i < count; i++) { @@ -198,11 +226,22 @@ public Map deserialize(Descriptor descriptor, } byte[] buffer = new byte[isChecksummed ? lengths[i] - CHECKSUM_LENGTH : lengths[i]]; + int bufLen = buffer.length; in.readFully(buffer); crc.reset(); crc.update(buffer); maybeValidateChecksum(crc, in, descriptor); - try (DataInputBuffer dataInputBuffer = new DataInputBuffer(buffer)) + + if (encryptor != null) + { + // Because we only use the encryption component, we are guaranteed that the serialization will fit + // within the buffer we already have, and that we can decrypt in place + // (see org.apache.cassandra.io.compress.Encryptor.canDecompressInPlace). + assert encryptor.canDecompressInPlace(); + bufLen = encryptor.uncompress(buffer, 0, buffer.length, buffer, 0); + } + + try (DataInputBuffer dataInputBuffer = new DataInputBuffer(buffer, 0, bufLen)) { components.put(type, type.serializer.deserialize(descriptor.version, dataInputBuffer)); } @@ -211,7 +250,7 @@ public Map deserialize(Descriptor descriptor, return components; } - private static void maybeValidateChecksum(CRC32 crc, FileDataInput in, Descriptor descriptor) throws IOException + private static void maybeValidateChecksum(CRC32 crc, DataInputPlus in, Descriptor descriptor) throws IOException { if (!descriptor.version.hasMetadataChecksum()) return; @@ -268,7 +307,7 @@ public void rewriteSSTableMetadata(Descriptor descriptor, Map updatedComponents) throws IOException + { + Map currentComponents = deserialize(descriptor, EnumSet.allOf(MetadataType.class)); + currentComponents.putAll(updatedComponents); + rewriteSSTableMetadata(descriptor, currentComponents); + } + + /** + * Read the compression info file pointed by the given descriptor and create the corresponding encryptor. + * + * Returns null if no encryption applies (version doesn't support it, compression is not applied, or the applicable + * compression does not include encryption). + */ + // Package-private for testing + static CompressionParams testCompressionParams = null; + + private ICompressor getEncryptor(Descriptor desc, boolean writeTime) + { + if (!desc.version.metadataIsEncrypted()) + return null; + + // For testing, use the provided compression params + if (testCompressionParams != null) + { + ICompressor compressor = testCompressionParams.getSstableCompressor(); + if (compressor != null) + return compressor.encryptionOnly(); + return null; + } + + File compressionFile = desc.fileFor(Components.COMPRESSION_INFO); + + try + { + // Read the compression metadata from file + // We pass a small compressedLength as we only need the parameters, not the actual chunk offsets. + // CompressionMetadata is ref-counted and holds the chunk offsets in off-heap Memory, so it must be + // closed once the parameters have been extracted. + try (CompressionMetadata cm = CompressionMetadata.open(compressionFile, 1024, false)) + { + // Note: we use only the encryption component, without any compression. The reason for doing this is to + // avoid having to allocate (and save the size of) an additional buffer to hold the larger uncompressed + // serialization on reads. + ICompressor compressor = cm.parameters.getSstableCompressor(); + if (compressor != null) + return compressor.encryptionOnly(); + return null; + } + } + catch (Throwable t) + { + // If we can't read the compression metadata, assume no encryption. + // During flush, the compression file may not be accessible yet in some implementations + // causing FSReadError. Catch Throwable to handle both Exception and Error. + logger.debug("Could not read compression metadata for {}: {}", desc, t.getMessage()); + return null; + } + } } diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java b/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java index b509e3f3ae6d..286c7c979813 100644 --- a/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java +++ b/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java @@ -19,13 +19,16 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +48,7 @@ import org.apache.cassandra.serializers.AbstractTypeSerializer; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.EstimatedHistogram; +import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.UUIDSerializer; import org.apache.cassandra.utils.streamhist.TombstoneHistogram; @@ -91,6 +95,7 @@ public class StatsMetadata extends MetadataComponent * deletions in this sstable. Obviously, this is pretty imprecise: a single partition deletion in the sstable * means we have to assume _any_ key may have a partition deletion. This is still likely useful as workloads that * does not use partition level deletions, or only very rarely, are probably not that rare. + * * TODO we could replace this by a small bloom-filter instead; the only downside being that we'd have to care about * the size of this bloom filters not getting out of hands, and it's a tiny bit unclear if it's worth the added * complexity. @@ -99,6 +104,9 @@ public class StatsMetadata extends MetadataComponent public final ByteBuffer firstKey; public final ByteBuffer lastKey; + private final ImmutableMap maxColumnValueLengths; + + public final ZeroCopyMetadata zeroCopyMetadata; public StatsMetadata(EstimatedHistogram estimatedPartitionSize, EstimatedHistogram estimatedCellPerPartitionCount, @@ -124,7 +132,9 @@ public StatsMetadata(EstimatedHistogram estimatedPartitionSize, boolean isTransient, boolean hasPartitionLevelDeletions, ByteBuffer firstKey, - ByteBuffer lastKey) + ByteBuffer lastKey, + Map maxColumnValueLengths, + ZeroCopyMetadata zeroCopyMetadata) { this.estimatedPartitionSize = estimatedPartitionSize; this.estimatedCellPerPartitionCount = estimatedCellPerPartitionCount; @@ -152,6 +162,8 @@ public StatsMetadata(EstimatedHistogram estimatedPartitionSize, this.hasPartitionLevelDeletions = hasPartitionLevelDeletions; this.firstKey = firstKey; this.lastKey = lastKey; + this.maxColumnValueLengths = ImmutableMap.copyOf(maxColumnValueLengths); + this.zeroCopyMetadata = zeroCopyMetadata; } public MetadataType getType() @@ -209,7 +221,9 @@ public StatsMetadata mutateLevel(int newLevel) isTransient, hasPartitionLevelDeletions, firstKey, - lastKey); + lastKey, + maxColumnValueLengths, + zeroCopyMetadata); } public StatsMetadata mutateRepairedMetadata(long newRepairedAt, TimeUUID newPendingRepair, boolean newIsTransient) @@ -238,7 +252,9 @@ public StatsMetadata mutateRepairedMetadata(long newRepairedAt, TimeUUID newPend newIsTransient, hasPartitionLevelDeletions, firstKey, - lastKey); + lastKey, + maxColumnValueLengths, + zeroCopyMetadata); } @Override @@ -272,6 +288,8 @@ public boolean equals(Object o) .append(hasPartitionLevelDeletions, that.hasPartitionLevelDeletions) .append(firstKey, that.firstKey) .append(lastKey, that.lastKey) + .append(maxColumnValueLengths, that.maxColumnValueLengths) + .append(zeroCopyMetadata, that.zeroCopyMetadata) .build(); } @@ -302,18 +320,21 @@ public int hashCode() .append(hasPartitionLevelDeletions) .append(firstKey) .append(lastKey) + .append(maxColumnValueLengths) + .append(zeroCopyMetadata) .build(); } public static class StatsMetadataSerializer implements IMetadataComponentSerializer { private static final Logger logger = LoggerFactory.getLogger(StatsMetadataSerializer.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.MINUTES); private final AbstractTypeSerializer typeSerializer = new AbstractTypeSerializer(); public int serializedSize(Version version, StatsMetadata component) throws IOException { - int size = 0; + long size = 0; size += EstimatedHistogram.serializer.serializedSize(component.estimatedPartitionSize); size += EstimatedHistogram.serializer.serializedSize(component.estimatedCellPerPartitionCount); size += CommitLogPosition.serializer.serializedSize(component.commitLogIntervals.upperBound().orElse(CommitLogPosition.NONE)); @@ -329,11 +350,11 @@ public int serializedSize(Version version, StatsMetadata component) throws IOExc // min column names size += 4; ClusteringBound minClusteringValues = component.coveredClustering.start(); - size += minClusteringValues.size() * 2 /* short length */ + minClusteringValues.dataSize(); + size += countUntilNull(minClusteringValues.getBufferArray()) * 2L /* short length */ + minClusteringValues.dataSize(); // max column names size += 4; ClusteringBound maxClusteringValues = component.coveredClustering.end(); - size += maxClusteringValues.size() * 2 /* short length */ + maxClusteringValues.dataSize(); + size += countUntilNull(maxClusteringValues.getBufferArray()) * 2L /* short length */ + maxClusteringValues.dataSize(); } else if (version.hasImprovedMinMax()) { @@ -354,16 +375,42 @@ else if (version.hasImprovedMinMax()) size += TimeUUID.sizeInBytes(); } + // we do not have zero copy metadata, but we need to support loading such sstables + if (version.hasZeroCopyMetadata()) + { + size += 1; + if (component.zeroCopyMetadata.exists()) + size += ZeroCopyMetadata.serializer.serializedSize(component.zeroCopyMetadata); + } + + // we do not have node sync metadata + if (version.hasIncrementalNodeSyncMetadata()) + { + size += Long.BYTES; + } + + if (version.hasMaxColumnValueLengths()) + { + size += 4; // num columns + for (Map.Entry entry : component.maxColumnValueLengths.entrySet()) + size += ByteBufferUtil.serializedSizeWithVIntLength(entry.getKey()) + 4; // column name, max value length + } + if (version.hasIsTransient()) { size += TypeSizes.sizeof(component.isTransient); } + if (version.hasMisplacedPartitionLevelDeletionsPresenceMarker()) + { + size += TypeSizes.sizeof(component.hasPartitionLevelDeletions); + } + if (version.hasOriginatingHostId()) { size += 1; // boolean: is originatingHostId present if (component.originatingHostId != null) - size += UUIDSerializer.serializer.serializedSize(component.originatingHostId, version.correspondingMessagingVersion()); + size += (int) UUIDSerializer.serializer.serializedSize(component.originatingHostId, version.correspondingMessagingVersion()); } if (version.hasPartitionLevelDeletionsPresenceMarker()) @@ -371,11 +418,6 @@ else if (version.hasImprovedMinMax()) size += TypeSizes.sizeof(component.hasPartitionLevelDeletions); } - if (version.hasImprovedMinMax() && version.hasLegacyMinMax()) - { - size = improvedMinMaxSize(version, component, size); - } - if (version.hasKeyRange()) { size += ByteBufferUtil.serializedSizeWithVIntLength(component.firstKey); @@ -387,15 +429,15 @@ else if (version.hasImprovedMinMax()) size += Double.BYTES; } - return size; + return Math.toIntExact(size); } - private int improvedMinMaxSize(Version version, StatsMetadata component, int size) + private long improvedMinMaxSize(Version version, StatsMetadata component, long size) { size += typeSerializer.serializedListSize(component.clusteringTypes); size += Slice.serializer.serializedSize(component.coveredClustering, - version.correspondingMessagingVersion(), - component.clusteringTypes); + version.correspondingMessagingVersion(), + component.clusteringTypes); return size; } @@ -472,11 +514,47 @@ else if (version.hasImprovedMinMax()) } } + // we do not produce such sstables, but we need to be able to rewrite the metadata + if (version.hasZeroCopyMetadata()) + { + if (component.zeroCopyMetadata != null && component.zeroCopyMetadata.exists()) + { + out.writeByte(1); + ZeroCopyMetadata.serializer.serialize(component.zeroCopyMetadata, out); + } + else + { + out.writeByte(0); + } + } + + // we do not have node sync metadata + if (version.hasIncrementalNodeSyncMetadata()) + { + out.writeLong(Long.MAX_VALUE); + } + + // left for being able to import DSE sstables, not used + if (version.hasMaxColumnValueLengths()) + { + out.writeInt(component.maxColumnValueLengths.size()); + for (Map.Entry entry : component.maxColumnValueLengths.entrySet()) + { + ByteBufferUtil.writeWithVIntLength(entry.getKey(), out); + out.writeInt(entry.getValue()); + } + } + if (version.hasIsTransient()) { out.writeBoolean(component.isTransient); } + if (version.hasMisplacedPartitionLevelDeletionsPresenceMarker()) + { + out.writeBoolean(component.hasPartitionLevelDeletions); + } + if (version.hasOriginatingHostId()) { if (component.originatingHostId != null) @@ -495,11 +573,6 @@ else if (version.hasImprovedMinMax()) out.writeBoolean(component.hasPartitionLevelDeletions); } - if (version.hasImprovedMinMax() && version.hasLegacyMinMax()) - { - serializeImprovedMinMax(version, component, out); - } - if (version.hasKeyRange()) { ByteBufferUtil.writeWithVIntLength(component.firstKey, out); @@ -620,25 +693,51 @@ else if (version.hasImprovedMinMax()) pendingRepair = TimeUUID.deserialize(in); } - boolean isTransient = version.hasIsTransient() && in.readBoolean(); + ZeroCopyMetadata zeroCopyMetadata = ZeroCopyMetadata.EMPTY; + if (version.hasZeroCopyMetadata() && in.readByte() != 0) + { + zeroCopyMetadata = ZeroCopyMetadata.serializer.deserialize(in); + } - UUID originatingHostId = null; - if (version.hasOriginatingHostId() && in.readByte() != 0) - originatingHostId = UUIDSerializer.serializer.deserialize(in, 0); + if (version.hasIncrementalNodeSyncMetadata()) + { + noSpamLogger.warn("Ignoring incremental node sync metadata from {} as it is not supported", in); + in.readLong(); + } + + // left for being able to import DSE sstables, not used + final Map maxColumnValueLengths; + if (version.hasMaxColumnValueLengths()) + { + int colCount = in.readInt(); + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(colCount); + + for (int i = 0; i < colCount; i++) + builder.put(ByteBufferUtil.readWithVIntLength(in), in.readInt()); + maxColumnValueLengths = builder.build(); + } + else + { + maxColumnValueLengths = Collections.emptyMap(); + } + + boolean isTransient = version.hasIsTransient() && in.readBoolean(); // If not recorded, the only time we can guarantee there is no partition level deletion is if there is no // deletion at all. Otherwise, we have to assume there may be some. boolean hasPartitionLevelDeletions = minLocalDeletionTime != Cell.NO_DELETION_TIME; - if (version.hasPartitionLevelDeletionsPresenceMarker()) + if (version.hasMisplacedPartitionLevelDeletionsPresenceMarker()) { hasPartitionLevelDeletions = in.readBoolean(); } - if (version.hasImprovedMinMax() && version.hasLegacyMinMax()) + UUID originatingHostId = null; + if (version.hasOriginatingHostId() && in.readByte() != 0) + originatingHostId = UUIDSerializer.serializer.deserialize(in, 0); + + if (version.hasPartitionLevelDeletionsPresenceMarker()) { - // improvedMinMax will be in this place until legacyMinMax is removed - clusteringTypes = typeSerializer.deserializeList(in); - coveredClustering = Slice.serializer.deserialize(in, version.correspondingMessagingVersion(), clusteringTypes); + hasPartitionLevelDeletions = in.readBoolean(); } ByteBuffer firstKey = null; @@ -679,7 +778,9 @@ else if (version.hasImprovedMinMax()) isTransient, hasPartitionLevelDeletions, firstKey, - lastKey); + lastKey, + maxColumnValueLengths, + zeroCopyMetadata); } private int countUntilNull(ByteBuffer[] bufferArray) diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/ValidationMetadata.java b/src/java/org/apache/cassandra/io/sstable/metadata/ValidationMetadata.java index 0eda8eb7753e..c980705c94b0 100644 --- a/src/java/org/apache/cassandra/io/sstable/metadata/ValidationMetadata.java +++ b/src/java/org/apache/cassandra/io/sstable/metadata/ValidationMetadata.java @@ -69,6 +69,11 @@ public int hashCode() return result; } + public ValidationMetadata withBloomFilterFPChance(double bloomFilterFpChance) + { + return new ValidationMetadata(partitioner, bloomFilterFpChance); + } + public static class ValidationMetadataSerializer implements IMetadataComponentSerializer { public int serializedSize(Version version, ValidationMetadata component) throws IOException diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/ZeroCopyMetadata.java b/src/java/org/apache/cassandra/io/sstable/metadata/ZeroCopyMetadata.java new file mode 100644 index 000000000000..ad64dad52b16 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/metadata/ZeroCopyMetadata.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.sstable.metadata; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import com.google.common.primitives.Longs; + +import org.apache.cassandra.io.ISerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.util.SliceDescriptor; +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * Metadata related to sstables copied via zero copy, stored in the {@link StatsMetadata}. ZCS allows for streaming + * part of the data file while streaming the original index and stats files (thus data file positions included by the + * indexes refer to the orignal data file rather than to the streamed slice). This metadata contains information about + * slice of the data file that was streamed this way. + *

      + *
    • {@link #dataStart}: Disk offset in the original data file where the first streamed partition begins (inclusive)
    • + *
    • {@link #dataEnd}: Disk offset in the original data file where the last streamed row ends (exclusive)
    • + *
    • {@link #firstKey}: Byte representation of the first key in the streamed sstable slice
    • + *
    • {@link #lastKey}: Byte representation of the last key in the streamed sstable slice
    • + *
    • {@link #chunkSize}: If the file is written/read in chunks, this is the size in bytes of the chunk, otherwise 0
    • + *
    • {@link #estimatedKeys}: Estimated keys contained in the sstable
    • + *
    + * When the sstable is written in chunks, the {@link #dataStart} might not match the actual offset as it needs to be + * aligned by the {@link #chunkSize}. In such case, if you need to access the actual start offset, please use + * {@link #sliceStart}. For example: + *
    + * 0                16               32               48               64               80               96
    + * |----------------|----------------|----------------|----------------|----------------|----------------|
    + * | chunk 1        | chunk 2        | chunk 3        | chunk 4        | chunk 5        | chunk 6        |
    + * |----------------|----------------|----------------|----------------|----------------|----------------|
    + *  #key1      #key2                    #key3      #key4                    #key5     #key6     #key7
    + * 
    + * Say the slice ZCS sends is from chunk 3 to chunk 5. The first key is 3 and the last key is 5. The start offset + * is 34 (the exact position) and the start offset aligned is 32 (the position aligned to the chunk size). The end + * offset is 78 (the exact position). + * The transferred data file looks as follows: + *
    + * 0                16               32               48
    + * |----------------|----------------|----------------|
    + * | chunk 3        | chunk 4        | chunk 5        |
    + * |----------------|----------------|----------------|
    + *    #key3      #key4                    #key5
    + * 
    + * So to get the actual start offset of the first key, that is 2, you need to calculate is as follows: + * {@code dataStart - sliceStart}. When you get a position of say key 4, it is 45, so you need to calculate + * the actual position in slice as follows: {@code 45 - sliceStart}, which gives you 12 - the key 4 position in + * the local slice. + */ +public class ZeroCopyMetadata extends SliceDescriptor +{ + public static final Serializer serializer = new Serializer(); + public static final ZeroCopyMetadata EMPTY = new ZeroCopyMetadata(0, 0, 0, 0, null, null); + + private final long estimatedKeys; + private final ByteBuffer firstKey; + private final ByteBuffer lastKey; + + public ZeroCopyMetadata(long dataStart, long dataEnd, int chunkSize, long estimatedKeys, ByteBuffer firstKey, ByteBuffer lastKey) + { + super(dataStart, dataEnd, chunkSize); + this.estimatedKeys = estimatedKeys; + this.firstKey = firstKey; + this.lastKey = lastKey; + } + + public ByteBuffer firstKey() + { + return this.firstKey.duplicate(); + } + + public ByteBuffer lastKey() + { + return this.lastKey.duplicate(); + } + + public long estimatedKeys() + { + return this.estimatedKeys; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ZeroCopyMetadata that = (ZeroCopyMetadata) o; + return estimatedKeys == that.estimatedKeys + && Objects.equals(firstKey, that.firstKey) + && Objects.equals(lastKey, that.lastKey); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), estimatedKeys, firstKey, lastKey); + } + + public static class Serializer implements ISerializer + { + @Override + public long serializedSize(ZeroCopyMetadata metadata) + { + if (!metadata.exists()) + return 0; + + return 3 * Longs.BYTES + Integer.BYTES + ByteBufferUtil.serializedSizeWithShortLength(metadata.firstKey) + ByteBufferUtil.serializedSizeWithShortLength(metadata.lastKey); + } + + @Override + public void serialize(ZeroCopyMetadata component, DataOutputPlus out) throws IOException + { + out.writeLong(component.dataStart); + out.writeLong(component.dataEnd); + out.writeInt(component.chunkSize); + out.writeLong(component.estimatedKeys); + ByteBufferUtil.writeWithShortLength(component.firstKey.duplicate(), out); + ByteBufferUtil.writeWithShortLength(component.lastKey.duplicate(), out); + } + + @Override + public ZeroCopyMetadata deserialize(DataInputPlus in) throws IOException + { + return new ZeroCopyMetadata( + in.readLong(), + in.readLong(), + in.readInt(), + in.readLong(), + ByteBufferUtil.readWithShortLength(in), + ByteBufferUtil.readWithShortLength(in)); + } + } +} diff --git a/src/java/org/apache/cassandra/io/storage/StorageProvider.java b/src/java/org/apache/cassandra/io/storage/StorageProvider.java new file mode 100644 index 000000000000..c1c2380c9faa --- /dev/null +++ b/src/java/org/apache/cassandra/io/storage/StorageProvider.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.storage; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cache.ChunkCache; +import org.apache.cassandra.config.Config; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.INativeLibrary; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_STORAGE_PROVIDER; + +/** + * The storage provider is used to support directory creation and remote/local conversion for remote storage. + * The default implementation {@link DefaultProvider} is based on local file system. + */ +public interface StorageProvider +{ + Logger logger = LoggerFactory.getLogger(StorageProvider.class); + + StorageProvider instance = !CUSTOM_STORAGE_PROVIDER.isPresent() + ? new DefaultProvider() + : FBUtilities.construct(CUSTOM_STORAGE_PROVIDER.getString(), "storage provider"); + + enum DirectoryType + { + DATA("data_file_directories"), + LOCAL_SYSTEM_DATA("local_system_data_file_directories"), + METADATA("metadata_directory"), + COMMITLOG("commit_log_directory"), + HINTS("hints_directory"), + SAVED_CACHES("saved_caches_directory"), + CDC("cdc_raw_directory"), + SNAPSHOT("snapshot_directory"), + NODES("nodes_local_directory"), + LOG_TRANSACTION("log_transaction_directory"), + LOGS("logs_directory"), + TEMP("temp_directory"), + OTHERS("other_directory"); + + final String name; + + final boolean readable; + final boolean writable; + + DirectoryType(String name) + { + this.name = name; + this.readable = true; + this.writable = true; + } + } + + /** + * @return local path if given path is remote path, otherwise returns itself + */ + File getLocalPath(File path); + + /** + * @return local path if given path is remote path, otherwise returns itself + */ + Path getLocalPath(Path path); + + /** + * update the given path with open options for the sstable components + */ + File withOpenOptions(File ret, Component component); + + /** + * Create data directories for given table + * + * @param ksMetadata The keyspace metadata, can be null. This is used when schema metadata is + * not available in {@link Schema}, eg. CNDB backup & restore + * @param tableMetadata the metadata of the table + * @param dirs current local data directories + * @return data directories that are created + */ + Directories.DataDirectory[] createDataDirectories(@Nullable KeyspaceMetadata ksMetadata, TableMetadata tableMetadata, Directories.DataDirectory[] dirs); + + /** + * Create directory for the given path and type, either locally or remotely if any remote storage parameters are passed in. + * + * @param dir the directory absolute path to create + * @param type the type of directory to create + * @return the actual directory path, which can be either local or remote; or null if directory can't be created + */ + File createDirectory(String dir, DirectoryType type); + + /** + * Remove the give file from any local cache, for example the OS page cache, or at least it tries to. + * @param file the file that is no longer required in the file system caches + */ + void invalidateFileSystemCache(File file); + + /** + * Remove the give sstable from any local cache, for example the OS page cache, or at least it tries to. + * + * @param descriptor the descriptor for the sstable that is no longer required in the file system caches + * @param tidied whether ReaderTidier has been run, aka. deleting sstable files. + */ + void invalidateFileSystemCache(Descriptor descriptor, boolean tidied); + + /** + * Creates a new {@link FileHandle.Builder} for the given sstable component. + *

    + * The returned builder will be configured with the appropriate "access mode" (mmap or not), and the "chunk cache" + * will have been set if appropriate. + * + * @param descriptor descriptor for the sstable whose handler is built. + * @param component sstable component for which to build the handler. + * @return a new {@link FileHandle.Builder} for the provided sstable component with access mode and chunk cache + * configured as appropriate. + */ + FileHandle.Builder fileHandleBuilderFor(Descriptor descriptor, Component component); + + /** + * Creates a new {@link FileHandle.Builder} for the given primary index component during primary index writing time. + *

    + * The returned builder will be configured with the appropriate "access mode" (mmap or not), and the "chunk cache" + * will have been set if appropriate. + * + * @param descriptor descriptor for the sstable whose handler is built. + * @param component sstable component for which to build the handler. + * @param operationType the operation for current primary index writer + * @return a new {@link FileHandle.Builder} for the provided primary index component with access mode and chunk cache + * configured as appropriate. + */ + FileHandle.Builder primaryIndexWriteTimeFileHandleBuilderFor(Descriptor descriptor, Component component, Config.DiskAccessMode diskAccessMode, ChunkCache chunkCache, OperationType operationType); + + /** + * Creates a new {@link FileHandle.Builder} for the given SAI component. + *

    + * The returned builder will be configured with the appropriate "access mode" (mmap or not), and the "chunk cache" + * will have been set if appropriate. + * + * @param component index component for which to build the handler. + * @return a new {@link FileHandle.Builder} for the provided SAI component with access mode and chunk cache + * configured as appropriate. + */ + FileHandle.Builder fileHandleBuilderFor(IndexComponent.ForRead component); + + /** + * Creates a new {@link FileChannel} to read the given file, that is suitable for reading the file "at write time", + * that is typcally for when we need to access the partially written file to complete checksum. + * + * @param file the file to be read + * @return a new {@link FileChannel} for the provided file + */ + default FileChannel writeTimeReadFileChannelFor(File file) throws IOException + { + return FileChannel.open(file.toPath(), StandardOpenOption.READ); + } + + /** + * Creates a new {@link FileHandle.Builder} for the given SAI component and context (for index with per-index files), + * that is suitable for reading the component during index build, that is typcally for when we need to access the + * component to complete the writing of another related component. + *

    + * Other the fact that this method will be called a different time, it's requirements are the same than for + * {@link #fileHandleBuilderFor(IndexComponent.ForRead)}. + * + * @param component index component for which to build the handler. + * @return a new {@link FileHandle.Builder} for the provided SAI component with access mode and chunk cache + * configured as appropriate. + */ + FileHandle.Builder indexBuildTimeFileHandleBuilderFor(IndexComponent.ForRead component); + + class DefaultProvider implements StorageProvider + { + @Override + public File getLocalPath(File path) + { + return path; + } + + @Override + public Path getLocalPath(Path path) + { + return path; + } + + @Override + public File withOpenOptions(File ret, Component component) + { + return ret; + } + + @Override + public Directories.DataDirectory[] createDataDirectories(@Nullable KeyspaceMetadata ksMetadata, TableMetadata tableMetadata, Directories.DataDirectory[] dirs) + { + // data directories are already created in DatabadeDescriptor#createAllDirectories + return dirs; + } + + @Override + public File createDirectory(String dir, DirectoryType type) + { + File ret = new File(dir); + PathUtils.createDirectoriesIfNotExists(ret.toPath()); + return ret; + } + + @Override + public void invalidateFileSystemCache(File file) + { + INativeLibrary.instance.trySkipCache(file, 0, 0); + if (ChunkCache.instance != null) + ChunkCache.instance.invalidateFile(file); + } + + @Override + public void invalidateFileSystemCache(Descriptor desc, boolean tidied) + { + for (Component c : desc.discoverComponents()) + StorageProvider.instance.invalidateFileSystemCache(desc.fileFor(c)); + } + + @Override + @SuppressWarnings("resource") + public FileHandle.Builder fileHandleBuilderFor(Descriptor descriptor, Component component) + { + return new FileHandle.Builder(descriptor.fileFor(component)); + } + + @Override + public FileHandle.Builder primaryIndexWriteTimeFileHandleBuilderFor(Descriptor descriptor, Component component, Config.DiskAccessMode diskAccessMode, ChunkCache chunkCache, OperationType operationType) + { + // By default, no difference between accesses during sstable writing and "at query time", but subclasses may need + // to differenciate both. + return fileHandleBuilderFor(descriptor, component) + .mmapped(diskAccessMode) + .withChunkCache(chunkCache); + } + + @Override + @SuppressWarnings("resource") + public FileHandle.Builder fileHandleBuilderFor(IndexComponent.ForRead component) + { + File file = component.file(); + if (logger.isTraceEnabled()) + { + logger.trace(component.parent().logMessage("Opening {} file handle for {} ({})"), + file, FBUtilities.prettyPrintMemory(file.length())); + } + var builder = new FileHandle.Builder(file); + // Comments on why we don't use adviseRandom for some components where you might expect it: + // + // KD_TREE: no adviseRandom because we do a large bulk read on startup, queries later may + // benefit from adviseRandom but there's no way to split those apart + // POSTINGS_LISTS: for common terms with 1000s of rows, adviseRandom seems likely to + // make it slower; no way to get cardinality at this point in the code + // (and we already have shortcut code for the common 1:1 vector case) + // so we leave it alone here + if (component.componentType() == IndexComponentType.TERMS_DATA + || component.componentType() == IndexComponentType.VECTOR + || component.componentType() == IndexComponentType.PRIMARY_KEY_TRIE) + { + builder = builder.adviseRandom(); + } + return builder.mmapped(true); + } + + @Override + public FileHandle.Builder indexBuildTimeFileHandleBuilderFor(IndexComponent.ForRead component) + { + // By default, no difference between accesses "at flush time" and "at query time", but subclasses may need + // to differenciate both. + return fileHandleBuilderFor(component); + } + } +} diff --git a/src/java/org/apache/cassandra/io/tries/BaseValueIterator.java b/src/java/org/apache/cassandra/io/tries/BaseValueIterator.java new file mode 100644 index 000000000000..3bafd538c678 --- /dev/null +++ b/src/java/org/apache/cassandra/io/tries/BaseValueIterator.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.tries; + +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +import org.apache.cassandra.io.util.Rebufferer; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +public abstract class BaseValueIterator> extends Walker +{ + protected static final long NOT_PREPARED = -2; + protected final ByteSource limit; + protected final TransitionBytesCollector collector; + protected IterationPosition stack; + protected long next; + + public BaseValueIterator(Rebufferer source, long root, ByteSource limit, boolean collecting, ByteComparable.Version version) + { + super(source, root, version); + this.limit = limit; + collector = collecting ? new TransitionBytesCollector(byteComparableVersion) : null; + } + + /** + * Returns the payload node position. + *

    + * This method must be async-read-safe, see {@link #advanceNode()}. + */ + protected long nextPayloadedNode() + { + if (next != NOT_PREPARED) + { + long toReturn = next; + next = NOT_PREPARED; + return toReturn; + } + else + return advanceNode(); + } + + protected boolean hasNext() + { + if (next == NOT_PREPARED) + next = advanceNode(); + return next != NONE; + } + + protected VALUE nextValue(Supplier supplier) + { + long node = nextPayloadedNode(); + if (node == NONE) + return null; + go(node); + return supplier.get(); + } + + protected long nextValueAsLong(LongSupplier supplier, long valueIfNone) + { + long node = nextPayloadedNode(); + if (node == NONE) + return valueIfNone; + go(node); + return supplier.getAsLong(); + } + + protected ByteComparable collectedKey() + { + assert collector != null : "Cannot get a collected value from a non-collecting iterator"; + return collector.toByteComparable(); + } + + protected abstract long advanceNode(); + + protected enum LeftBoundTreatment + { + ADMIT_PREFIXES, + ADMIT_EXACT, + GREATER + } + + protected static class IterationPosition + { + final long node; + final int limit; + final IterationPosition prev; + int childIndex; + + IterationPosition(long node, int childIndex, int limit, IterationPosition prev) + { + super(); + this.node = node; + this.childIndex = childIndex; + this.limit = limit; + this.prev = prev; + } + + @Override + public String toString() + { + return String.format("[Node %d, child %d, limit %d]", node, childIndex, limit); + } + } +} diff --git a/src/java/org/apache/cassandra/io/tries/IncrementalDeepTrieWriterPageAware.java b/src/java/org/apache/cassandra/io/tries/IncrementalDeepTrieWriterPageAware.java index c4b550c7dd08..ab32ad1b2a2e 100644 --- a/src/java/org/apache/cassandra/io/tries/IncrementalDeepTrieWriterPageAware.java +++ b/src/java/org/apache/cassandra/io/tries/IncrementalDeepTrieWriterPageAware.java @@ -26,6 +26,7 @@ import javax.annotation.concurrent.NotThreadSafe; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** * This class is a variant of {@link IncrementalTrieWriterPageAware} which is able to build even very deep @@ -38,19 +39,19 @@ * and thus stack overflow failures. */ @NotThreadSafe -public class IncrementalDeepTrieWriterPageAware extends IncrementalTrieWriterPageAware +class IncrementalDeepTrieWriterPageAware extends IncrementalTrieWriterPageAware { private final int maxRecursionDepth; - public IncrementalDeepTrieWriterPageAware(TrieSerializer trieSerializer, DataOutputPlus dest, int maxRecursionDepth) + IncrementalDeepTrieWriterPageAware(TrieSerializer trieSerializer, DataOutputPlus dest, int maxRecursionDepth, ByteComparable.Version version) { - super(trieSerializer, dest); + super(trieSerializer, dest, version); this.maxRecursionDepth = maxRecursionDepth; } - public IncrementalDeepTrieWriterPageAware(TrieSerializer trieSerializer, DataOutputPlus dest) + IncrementalDeepTrieWriterPageAware(TrieSerializer trieSerializer, DataOutputPlus dest, ByteComparable.Version version) { - this(trieSerializer, dest, 64); + this(trieSerializer, dest, 64, version); } /** diff --git a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriter.java b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriter.java index e2c1e4c845e5..5d1cb2d78216 100644 --- a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriter.java +++ b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriter.java @@ -75,8 +75,8 @@ interface PartialTail /** * Construct a suitable trie writer. */ - static IncrementalTrieWriter open(TrieSerializer trieSerializer, DataOutputPlus dest) + static IncrementalTrieWriter open(TrieSerializer trieSerializer, DataOutputPlus dest, ByteComparable.Version version) { - return new IncrementalDeepTrieWriterPageAware<>(trieSerializer, dest); + return new IncrementalDeepTrieWriterPageAware<>(trieSerializer, dest, version); } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterBase.java b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterBase.java index c46099a5d658..fe3ae6ffe655 100644 --- a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterBase.java +++ b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterBase.java @@ -39,9 +39,11 @@ public abstract class IncrementalTrieWriterBase serializer, DEST dest, NODE root) + protected IncrementalTrieWriterBase(TrieSerializer serializer, DEST dest, NODE root, ByteComparable.Version version) { + this.version = version; this.serializer = serializer; this.dest = dest; this.stack.addLast(root); @@ -69,20 +71,20 @@ public void add(ByteComparable next, VALUE value) throws IOException { ++count; int stackpos = 0; - ByteSource sn = next.asComparableBytes(Walker.BYTE_COMPARABLE_VERSION); + ByteSource sn = next.asComparableBytes(version); int n = sn.next(); if (prev != null) { - ByteSource sp = prev.asComparableBytes(Walker.BYTE_COMPARABLE_VERSION); + ByteSource sp = prev.asComparableBytes(version); int p = sp.next(); while ( n == p ) { assert n != ByteSource.END_OF_STREAM : String.format("Incremental trie requires unique sorted keys, got equal %s(%s) after %s(%s).", next, - next.byteComparableAsString(Walker.BYTE_COMPARABLE_VERSION), + next.byteComparableAsString(version), prev, - prev.byteComparableAsString(Walker.BYTE_COMPARABLE_VERSION)); + prev.byteComparableAsString(version)); ++stackpos; n = sn.next(); @@ -90,9 +92,9 @@ public void add(ByteComparable next, VALUE value) throws IOException } assert p < n : String.format("Incremental trie requires sorted keys, got %s(%s) after %s(%s).", next, - next.byteComparableAsString(Walker.BYTE_COMPARABLE_VERSION), + next.byteComparableAsString(version), prev, - prev.byteComparableAsString(Walker.BYTE_COMPARABLE_VERSION)); + prev.byteComparableAsString(version)); } prev = next; diff --git a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterPageAware.java b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterPageAware.java index 2274975e48e0..f4b5899132af 100644 --- a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterPageAware.java +++ b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterPageAware.java @@ -28,6 +28,7 @@ import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** * Incremental builders of on-disk tries which packs trie stages into disk cache pages. @@ -103,9 +104,9 @@ public class IncrementalTrieWriterPageAware return c; }; - IncrementalTrieWriterPageAware(TrieSerializer trieSerializer, DataOutputPlus dest) + IncrementalTrieWriterPageAware(TrieSerializer trieSerializer, DataOutputPlus dest, ByteComparable.Version version) { - super(trieSerializer, dest, new Node<>((byte) 0)); + super(trieSerializer, dest, new Node<>((byte) 0), version); this.maxBytesPerPage = dest.maxBytesInPage(); } diff --git a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterSimple.java b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterSimple.java index 6620b2e1c844..7f34010f8e66 100644 --- a/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterSimple.java +++ b/src/java/org/apache/cassandra/io/tries/IncrementalTrieWriterSimple.java @@ -23,6 +23,7 @@ import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; /** * Incremental builder of on-disk tries. Takes sorted input. @@ -44,9 +45,9 @@ public class IncrementalTrieWriterSimple { private long position = 0; - public IncrementalTrieWriterSimple(TrieSerializer trieSerializer, DataOutputPlus dest) + public IncrementalTrieWriterSimple(TrieSerializer trieSerializer, DataOutputPlus dest, ByteComparable.Version version) { - super(trieSerializer, dest, new Node<>((byte) 0)); + super(trieSerializer, dest, new Node<>((byte) 0), version); } @Override diff --git a/src/java/org/apache/cassandra/io/tries/ReverseValueIterator.java b/src/java/org/apache/cassandra/io/tries/ReverseValueIterator.java index 27c199a68523..057d1b0c27b1 100644 --- a/src/java/org/apache/cassandra/io/tries/ReverseValueIterator.java +++ b/src/java/org/apache/cassandra/io/tries/ReverseValueIterator.java @@ -28,40 +28,39 @@ *

    * The main utility of this class is the {@link #nextPayloadedNode()} method, which lists all nodes that contain a * payload within the requested bounds. The treatment of the bounds is non-standard (see - * {@link #ReverseValueIterator(Rebufferer, long, ByteComparable, ByteComparable, boolean)}), necessary to properly walk - * tries of prefixes and separators. + * {@link #ReverseValueIterator(Rebufferer, long, ByteComparable, ByteComparable, LeftBoundTreatment, ByteComparable.Version)}), necessary to + * properly walk tries of prefixes and separators. */ @NotThreadSafe -public class ReverseValueIterator> extends Walker +public class ReverseValueIterator> extends BaseValueIterator { static final int NOT_AT_LIMIT = Integer.MIN_VALUE; - private final ByteSource limit; - private IterationPosition stack; - private long next; - private boolean reportingPrefixes; + private LeftBoundTreatment reportingPrefixes; + private boolean popOnAdvance = true; - static class IterationPosition + protected ReverseValueIterator(Rebufferer source, long root, ByteComparable.Version version) { - final long node; - final int limit; - final IterationPosition prev; - int childIndex; + this(source, root, false, version); + } + + protected ReverseValueIterator(Rebufferer source, long root, boolean collecting, ByteComparable.Version version) + { + super(source, root, null, collecting, version); - public IterationPosition(long node, int childIndex, int limit, IterationPosition prev) + try { - super(); - this.node = node; - this.childIndex = childIndex; - this.limit = limit; - this.prev = prev; + initializeNoRightBound(root, NOT_AT_LIMIT, LeftBoundTreatment.GREATER); + } + catch (Throwable t) + { + super.close(); + throw t; } } - protected ReverseValueIterator(Rebufferer source, long root) + protected ReverseValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, LeftBoundTreatment admitPrefix, ByteComparable.Version version) { - super(source, root); - limit = null; - initializeNoRightBound(root, NOT_AT_LIMIT, false); + this(source, root, start, end, admitPrefix, false, version); } /** @@ -75,18 +74,25 @@ protected ReverseValueIterator(Rebufferer source, long root) * * This behaviour is shared with the forward counterpart {@link ValueIterator}. */ - protected ReverseValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, boolean admitPrefix) + protected ReverseValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, LeftBoundTreatment admitPrefix, boolean collecting, ByteComparable.Version version) { - super(source, root); - limit = start != null ? start.asComparableBytes(BYTE_COMPARABLE_VERSION) : null; + super(source, root, start != null ? start.asComparableBytes(version) : null, collecting, version); - if (end != null) - initializeWithRightBound(root, end.asComparableBytes(BYTE_COMPARABLE_VERSION), admitPrefix, limit != null); - else - initializeNoRightBound(root, limit != null ? limit.next() : NOT_AT_LIMIT, admitPrefix); + try + { + if (end != null) + initializeWithRightBound(root, end.asComparableBytes(byteComparableVersion), admitPrefix, limit != null); + else + initializeNoRightBound(root, limit != null ? limit.next() : NOT_AT_LIMIT, admitPrefix); + } + catch (Throwable t) + { + super.close(); + throw t; + } } - void initializeWithRightBound(long root, ByteSource endStream, boolean admitPrefix, boolean hasLimit) + void initializeWithRightBound(long root, ByteSource endStream, LeftBoundTreatment admitPrefix, boolean hasLimit) { IterationPosition prev = null; boolean atLimit = hasLimit; @@ -112,37 +118,31 @@ void initializeWithRightBound(long root, ByteSource endStream, boolean admitPref break; prev = new IterationPosition(position, childIndex, limitByte, prev); + if (collector != null) + collector.add(s); go(transition(childIndex)); // childIndex is positive, this transition must exist } // Advancing now gives us first match. childIndex = -1 - childIndex; stack = new IterationPosition(position, childIndex, limitByte, prev); - next = advanceNode(); + next = NOT_PREPARED; + popOnAdvance = false; } - private void initializeNoRightBound(long root, int limitByte, boolean admitPrefix) + private void initializeNoRightBound(long root, int limitByte, LeftBoundTreatment admitPrefix) { go(root); stack = new IterationPosition(root, -1 - search(256), limitByte, null); - next = advanceNode(); + if (hasPayload()) + next = root; + else + next = NOT_PREPARED; + popOnAdvance = false; reportingPrefixes = admitPrefix; } - - - /** - * Returns the position of the next node with payload contained in the iterated span. - */ - protected long nextPayloadedNode() - { - long toReturn = next; - if (next != -1) - next = advanceNode(); - return toReturn; - } - - long advanceNode() + protected long advanceNode() { if (stack == null) return -1; @@ -150,6 +150,15 @@ long advanceNode() long child; int transitionByte; + if (collector != null) + { + // We need to pop the last character unless we have not yet advanced to an entry. + if (popOnAdvance) + collector.pop(); + else + popOnAdvance = true; + } + go(stack.node); while (true) { @@ -163,7 +172,7 @@ long advanceNode() if (beyondLimit) { assert stack.limit >= 0; // we are at a limit position (not in a node that's completely within the span) - reportingPrefixes = false; // there exists a smaller child than limit, no longer should report prefixes + reportingPrefixes = null; // there exists a smaller child than limit, no longer should report prefixes } } else @@ -182,16 +191,19 @@ long advanceNode() // If we are fully inside the covered space, report. // Note that on the exact match of the limit, stackTop.limit would be END_OF_STREAM. // This comparison rejects the exact match; if we wanted to include it, we could test < 0 instead. - if (stackTop.limit == NOT_AT_LIMIT) + if (stackTop.limit == NOT_AT_LIMIT || stackTop.limit == ByteSource.END_OF_STREAM && reportingPrefixes == LeftBoundTreatment.ADMIT_EXACT) return stackTop.node; - else if (reportingPrefixes) + else if (reportingPrefixes == LeftBoundTreatment.ADMIT_PREFIXES) { - reportingPrefixes = false; // if we are at limit position only report one prefix, the closest + reportingPrefixes = null; // if we are at limit position only report one prefix, the closest return stackTop.node; } // else skip this payload } + if (collector != null) + collector.pop(); + if (stack == null) // exhausted whole trie return NONE; go(stack.node); @@ -211,6 +223,8 @@ else if (reportingPrefixes) l = limit.next(); stack = new IterationPosition(child, transitionRange(), l, stack); + if (collector != null) + collector.add(transitionByte); } else { diff --git a/src/java/org/apache/cassandra/io/tries/ValueIterator.java b/src/java/org/apache/cassandra/io/tries/ValueIterator.java index 6ddbebad2a2d..d4237c70657c 100644 --- a/src/java/org/apache/cassandra/io/tries/ValueIterator.java +++ b/src/java/org/apache/cassandra/io/tries/ValueIterator.java @@ -28,133 +28,73 @@ *

    * The main utility of this class is the {@link #nextPayloadedNode()} method, which lists all nodes that contain a * payload within the requested bounds. The treatment of the bounds is non-standard (see - * {@link #ValueIterator(Rebufferer, long, ByteComparable, ByteComparable, boolean)}), necessary to properly walk - * tries of prefixes and separators. + * {@link #ValueIterator(Rebufferer, long, ByteComparable, ByteComparable, LeftBoundTreatment, boolean, ByteComparable.Version)}), necessary to + * properly walk tries of prefixes and separators. */ @NotThreadSafe -public class ValueIterator> extends Walker +public class ValueIterator> extends BaseValueIterator { - private final ByteSource limit; - private final TransitionBytesCollector collector; - protected IterationPosition stack; - private long next; - public static class IterationPosition + protected ValueIterator(Rebufferer source, long root, ByteComparable.Version version) { - final long node; - final int limit; - final IterationPosition prev; - int childIndex; + this(source, root, false, version); + } - public IterationPosition(long node, int childIndex, int limit, IterationPosition prev) + protected ValueIterator(Rebufferer source, long root, boolean collecting, ByteComparable.Version version) + { + super(source, root, null, collecting, version); + + try { - super(); - this.node = node; - this.childIndex = childIndex; - this.limit = limit; - this.prev = prev; + initializeNoLeftBound(root, 256); } - - @Override - public String toString() + catch (Throwable t) { - return String.format("[Node %d, child %d, limit %d]", node, childIndex, limit); + super.close(); + throw t; } } - protected ValueIterator(Rebufferer source, long root) + protected ValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, LeftBoundTreatment admitPrefix, ByteComparable.Version version) { - this(source, root, false); - } - - protected ValueIterator(Rebufferer source, long root, boolean collecting) - { - super(source, root); - limit = null; - collector = collecting ? new TransitionBytesCollector() : null; - initializeNoLeftBound(root, 256); - } - - protected ValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, boolean admitPrefix) - { - this(source, root, start, end, admitPrefix, false); + this(source, root, start, end, admitPrefix, false, version); } /** - * Constrained iterator. The end position is always treated as inclusive, and we have two possible treatments for - * the start: + * Constrained iterator. The end position is always treated as inclusive, and we have three possible treatments for + * the start, specified in admitPrefix: *

      - *
    • When {@code admitPrefix=false}, exact matches and any prefixes of the start are excluded. - *
    • When {@code admitPrefix=true}, the longest prefix of the start present in the trie is also included, + *
    • When {@code GREATER}, exact matches and any prefixes of the start are excluded. + *
    • When {@code ADMIT_EXACT}, exact matches are included. + *
    • When {@code ADMIT_PREFIXES}, the longest prefix of the start present in the trie is also included, * provided that there is no entry in the trie between that prefix and the start. An exact match also * satisfies this and is included. *
    * This behaviour is shared with the reverse counterpart {@link ReverseValueIterator}. */ - protected ValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, boolean admitPrefix, boolean collecting) + protected ValueIterator(Rebufferer source, long root, ByteComparable start, ByteComparable end, LeftBoundTreatment admitPrefix, boolean collecting, ByteComparable.Version version) { - super(source, root); - limit = end != null ? end.asComparableBytes(BYTE_COMPARABLE_VERSION) : null; - collector = collecting ? new TransitionBytesCollector() : null; - - if (start != null) - initializeWithLeftBound(root, start.asComparableBytes(BYTE_COMPARABLE_VERSION), admitPrefix, limit != null); - else - initializeNoLeftBound(root, limit != null ? limit.next() : 256); + super(source, root, end != null ? end.asComparableBytes(version) : null, collecting, version); + + try + { + if (start != null) + initializeWithLeftBound(root, start.asComparableBytes(byteComparableVersion), admitPrefix, limit != null); + else + initializeNoLeftBound(root, limit != null ? limit.next() : 256); + } + catch (Throwable t) + { + super.close(); + throw t; + } } - private void initializeWithLeftBound(long root, ByteSource startStream, boolean admitPrefix, boolean atLimit) + private void initializeWithLeftBound(long root, ByteSource start, LeftBoundTreatment admitPrefix, boolean atLimit) { - IterationPosition prev = null; - int childIndex; - int limitByte; - long payloadedNode = -1; - try { - // Follow start position while we still have a prefix, stacking path and saving prefixes. - go(root); - while (true) - { - int s = startStream.next(); - childIndex = search(s); - - // For a separator trie the latest payload met along the prefix is a potential match for start - if (admitPrefix) - { - if (childIndex == 0 || childIndex == -1) - { - if (hasPayload()) - payloadedNode = position; - } - else - { - payloadedNode = -1; - } - } - - limitByte = 256; - if (atLimit) - { - limitByte = limit.next(); - if (s < limitByte) - atLimit = false; - } - if (childIndex < 0) - break; - - prev = new IterationPosition(position, childIndex, limitByte, prev); - go(transition(childIndex)); // child index is positive, transition must exist - } - - childIndex = -1 - childIndex - 1; - stack = new IterationPosition(position, childIndex, limitByte, prev); - - // Advancing now gives us first match if we didn't find one already. - if (payloadedNode != -1) - next = payloadedNode; - else - next = advanceNode(); + descendWith(start.next(), start, atLimit ? limit.next() : 256, null, root, admitPrefix); } catch (Throwable t) { @@ -173,7 +113,7 @@ private void initializeNoLeftBound(long root, int limitByte) if (hasPayload()) next = root; else - next = advanceNode(); + next = NOT_PREPARED; } catch (Throwable t) { @@ -183,28 +123,120 @@ private void initializeNoLeftBound(long root, int limitByte) } /** - * Returns the payload node position without advancing. + * Skip to the given key or the closest after it in iteration order. Inclusive when admitPrefix = ADMIT_EXACT, + * exclusive when GREATER (ADMIT_PREFIXES is not supported). + * Requires that the iterator is collecting bytes. + * To get the next entry, use getNextPayloadedNode as normal. */ - protected long peekNode() + protected void skipTo(ByteComparable skipTo, LeftBoundTreatment admitPrefix) { - return next; + assert skipTo != null; + assert collector != null : "Cannot skip without collecting bytes"; + // TODO: Figure out what you need to know to say if an earlier prefix would still be acceptable + // to support skipping with ADMIT_PREFIXES. + assert admitPrefix != LeftBoundTreatment.ADMIT_PREFIXES : "Skipping with ADMIT_PREFIXES is not supported"; + if (stack == null) + return; // exhausted whole trie + ByteSource skipToBytes = skipTo.asComparableBytes(byteComparableVersion); + int pos; + int nextByte = skipToBytes.next(); + final int collectedLength = collector.pos; + final byte[] collectedBytes = collector.bytes; + for (pos = 0; pos < collectedLength; ++pos) + { + if (nextByte != collectedBytes[pos]) + { + if (nextByte < collectedBytes[pos]) + return; // the position we are already advanced to is beyond skipTo + else + break; // matched a prefix of skipTo, now we need to advance through the rest of it + } + nextByte = skipToBytes.next(); + } + int upLevels = collectedLength - pos; + IterationPosition stack = this.stack; + for (int i = 0; i < upLevels; ++i) + stack = stack.prev; + collector.pos = pos; + + descendWith(nextByte, skipToBytes, stack.limit, stack.prev, stack.node, admitPrefix); } - /** - * Returns the position of the next node with payload contained in the iterated span. - */ - protected long nextPayloadedNode() + private void descendWith(int skipToFirstByte, ByteSource skipToRest, int limitByte, IterationPosition stackPrev, long startNode, LeftBoundTreatment admitPrefix) { - long toReturn = next; - if (next != -1) - next = advanceNode(); - return toReturn; + int childIndex; + long payloadedNode = NOT_PREPARED; + // Follow start position while we still have a prefix, stacking path and saving prefixes. + go(startNode); + while (true) + { + childIndex = search(skipToFirstByte); + + // For a separator trie the latest payload met along the prefix is a potential match for start + payloadedNode = maybeCollectPayloadedNode(admitPrefix, childIndex, payloadedNode); + + if (childIndex < 0) + break; + + stackPrev = new IterationPosition(position, childIndex, limitByte, stackPrev); + if (collector != null) + collector.add(skipToFirstByte); + go(transition(childIndex)); // child index is positive, transition must exist + + if (limitByte < 256) + { + if (skipToFirstByte == limitByte) + limitByte = limit.next(); + else if (skipToFirstByte < limitByte) + limitByte = 256; + else // went beyond the limit + { + stack = null; + next = NONE; + return; + } + } + skipToFirstByte = skipToRest.next(); + } + + childIndex = -1 - childIndex - 1; + stack = new IterationPosition(position, childIndex, limitByte, stackPrev); + + // Advancing now gives us first match if we didn't find one already. + next = maybeAcceptPayloadedNode(admitPrefix, skipToFirstByte, payloadedNode); + } + + private long maybeAcceptPayloadedNode(LeftBoundTreatment admitPrefix, int trailingByte, long payloadedNode) + { + switch (admitPrefix) + { + case ADMIT_PREFIXES: + return payloadedNode; + case ADMIT_EXACT: + if (trailingByte == ByteSource.END_OF_STREAM && hasPayload()) + return position; + // else fall through + case GREATER: + default: + return NOT_PREPARED; + } } - protected ByteComparable nextCollectedValue() + private long maybeCollectPayloadedNode(LeftBoundTreatment admitPrefix, int childIndex, long payloadedNode) { - assert collector != null : "Cannot get a collected value from a non-collecting iterator"; - return collector.toByteComparable(); + if (admitPrefix == LeftBoundTreatment.ADMIT_PREFIXES) + { + if (childIndex == 0 || childIndex == -1) + { + if (hasPayload()) + payloadedNode = position; + } + else + { + payloadedNode = NOT_PREPARED; + } + } + return payloadedNode; } protected long advanceNode() @@ -225,7 +257,7 @@ protected long advanceNode() if (collector != null) collector.pop(); if (stack == null) // exhausted whole trie - return -1; + return NONE; go(stack.node); continue; } diff --git a/src/java/org/apache/cassandra/io/tries/Walker.java b/src/java/org/apache/cassandra/io/tries/Walker.java index 0b2b86ca946e..54f79f729e85 100644 --- a/src/java/org/apache/cassandra/io/tries/Walker.java +++ b/src/java/org/apache/cassandra/io/tries/Walker.java @@ -20,13 +20,13 @@ import java.io.IOException; import java.io.PrintStream; import java.nio.ByteBuffer; -import java.util.Arrays; import javax.annotation.concurrent.NotThreadSafe; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.util.PageAware; import org.apache.cassandra.io.util.Rebufferer; import org.apache.cassandra.io.util.Rebufferer.BufferHolder; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; import org.apache.lucene.util.ArrayUtil; @@ -44,9 +44,9 @@ public class Walker> implements AutoCloseable { /** Value used to indicate a branch (e.g. lesser/greaterBranch) does not exist. */ - public static int NONE = TrieNode.NONE; + public static final int NONE = TrieNode.NONE; - private final Rebufferer source; + protected final Rebufferer source; protected final long root; // State relating to current node. @@ -60,16 +60,17 @@ public class Walker> implements AutoCloseable protected long greaterBranch; protected long lesserBranch; - // Version of the byte comparable conversion to use - public static final ByteComparable.Version BYTE_COMPARABLE_VERSION = ByteComparable.Version.OSS50; + // Version of the byte comparable conversion to use -- trie-based indices use the 6.0 conversion + public final ByteComparable.Version byteComparableVersion; /** * Creates a walker. Rebufferer must be aligned and with a buffer size that is at least 4k. */ - public Walker(Rebufferer source, long root) + public Walker(Rebufferer source, long root, ByteComparable.Version version) { this.source = source; this.root = root; + this.byteComparableVersion = version; try { bh = source.rebuffer(root); @@ -201,7 +202,7 @@ public interface Extractor */ public int follow(ByteComparable key) { - ByteSource stream = key.asComparableBytes(BYTE_COMPARABLE_VERSION); + ByteSource stream = key.asComparableBytes(byteComparableVersion); go(root); while (true) { @@ -226,7 +227,7 @@ public int followWithGreater(ByteComparable key) { greaterBranch = NONE; - ByteSource stream = key.asComparableBytes(BYTE_COMPARABLE_VERSION); + ByteSource stream = key.asComparableBytes(byteComparableVersion); go(root); while (true) { @@ -252,7 +253,7 @@ public int followWithLesser(ByteComparable key) { lesserBranch = NONE; - ByteSource stream = key.asComparableBytes(BYTE_COMPARABLE_VERSION); + ByteSource stream = key.asComparableBytes(byteComparableVersion); go(root); while (true) { @@ -282,7 +283,7 @@ public RESULT prefix(ByteComparable key, Extractor ex { RESULT payload = null; - ByteSource stream = key.asComparableBytes(BYTE_COMPARABLE_VERSION); + ByteSource stream = key.asComparableBytes(byteComparableVersion); go(root); while (true) { @@ -321,7 +322,7 @@ public RESULT prefixAndNeighbours(ByteComparable key, Extractor RESULT prefixAndNeighbours(ByteComparable key, Extractor ByteSource.fixedLength(value, 0, value.length); + return ByteComparable.preencoded(byteComparableVersion, value, 0, value.length); } @Override public String toString() { - return String.format("[Bytes %s, pos %d]", Arrays.toString(bytes), pos); + return ByteBufferUtil.bytesToHex(ByteBuffer.wrap(bytes, 0, pos)); } } } diff --git a/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java b/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java index a8b61dd193ca..31425aab61aa 100644 --- a/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java +++ b/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java @@ -58,4 +58,10 @@ public double getCrcCheckChance() { return 0; // Only valid for compressed files. } + + @Override + public long adjustPosition(long position) + { + return position; + } } diff --git a/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java b/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java index 13b7c9d44141..6b6b72bea838 100644 --- a/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java @@ -25,12 +25,14 @@ import org.apache.cassandra.utils.memory.BufferPools; -/** - * Buffer manager used for reading from a ChunkReader when cache is not in use. Instances of this class are - * reader-specific and thus do not need to be thread-safe since the reader itself isn't. - * - * The instances reuse themselves as the BufferHolder to avoid having to return a new object for each rebuffer call. - */ +/// Buffer manager used for reading from a [ChunkReader] when cache is not in use. They use a buffer produced by the +/// "networking" buffer pool, which is the one to be used for buffers that are not to be retained for a long time +/// (the lifetime of this object is contained by the lifetime of a [RandomAccessReader] which is contained in a read +/// operation's lifetime). +/// +/// Instances of this class are reader-specific and thus do not need to be thread-safe since the reader itself isn't. +/// +/// The instances reuse themselves as the BufferHolder to avoid having to return a new object for each rebuffer call. public abstract class BufferManagingRebufferer implements Rebufferer, Rebufferer.BufferHolder { protected final ChunkReader source; @@ -42,14 +44,20 @@ public abstract class BufferManagingRebufferer implements Rebufferer, Rebufferer protected BufferManagingRebufferer(ChunkReader wrapped) { this.source = wrapped; - buffer = BufferPools.forChunkCache().get(wrapped.chunkSize(), wrapped.preferredBufferType()).order(ByteOrder.BIG_ENDIAN); + // Note: This class uses the networking buffer pool which makes better sense for short-lifetime buffers. + // Because this is meant to be used when the chunk cache is disabled, it also makes sense to use any memory + // that may have been allocated for in-flight data by using the chunk-cache pool. + // However, if some new functionality decides to use this class in the presence of the chunk cache (e.g. + // cache-bypassing compaction), using the chunk-cache pool here will certainly cause hard-to-diagnose issues + // that we would prefer to avoid. + buffer = BufferPools.forNetworking().get(wrapped.chunkSize(), wrapped.preferredBufferType()).order(ByteOrder.BIG_ENDIAN); buffer.limit(0); } @Override public void closeReader() { - BufferPools.forChunkCache().put(buffer); + BufferPools.forNetworking().put(buffer); source.releaseUnderlyingResources(); offset = -1; } @@ -87,6 +95,12 @@ public double getCrcCheckChance() return source.getCrcCheckChance(); } + @Override + public long adjustPosition(long position) + { + return source.adjustPosition(position); + } + @Override public String toString() { @@ -97,7 +111,7 @@ public String toString() public ByteBuffer buffer() { - return buffer; + return buffer.duplicate(); } public long offset() diff --git a/src/java/org/apache/cassandra/io/util/BufferedDataOutputStreamPlus.java b/src/java/org/apache/cassandra/io/util/BufferedDataOutputStreamPlus.java index a712ba6e4c18..0dbb8f03c696 100644 --- a/src/java/org/apache/cassandra/io/util/BufferedDataOutputStreamPlus.java +++ b/src/java/org/apache/cassandra/io/util/BufferedDataOutputStreamPlus.java @@ -227,7 +227,7 @@ public void writeDouble(double v) throws IOException } @DontInline - private void writeSlow(long bytes, int count) throws IOException + protected void writeSlow(long bytes, int count) throws IOException { assert buffer != null : "Attempt to use a closed data output"; int origCount = count; diff --git a/src/java/org/apache/cassandra/io/util/ChannelProxy.java b/src/java/org/apache/cassandra/io/util/ChannelProxy.java index 81665beecdb9..9ca43c3263c3 100644 --- a/src/java/org/apache/cassandra/io/util/ChannelProxy.java +++ b/src/java/org/apache/cassandra/io/util/ChannelProxy.java @@ -18,14 +18,19 @@ package org.apache.cassandra.io.util; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.StandardOpenOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.io.FSReadError; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; +import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.concurrent.RefCounted; import org.apache.cassandra.utils.concurrent.SharedCloseableImpl; @@ -40,6 +45,7 @@ */ public final class ChannelProxy extends SharedCloseableImpl { + private static final Logger log = LoggerFactory.getLogger(ChannelProxy.class); private final File file; private final String filePath; private final FileChannel channel; @@ -52,15 +58,10 @@ public static FileChannel openChannel(File file) } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } } - public ChannelProxy(String path) - { - this (new File(path)); - } - public ChannelProxy(File file) { this(file, openChannel(file)); @@ -120,7 +121,7 @@ public void tidy() */ public final ChannelProxy newChannel() { - return new ChannelProxy(filePath); + return new ChannelProxy(file); } public ChannelProxy sharedCopy() @@ -130,7 +131,12 @@ public ChannelProxy sharedCopy() public String filePath() { - return filePath; + return file.path(); + } + + public File getFile() + { + return file; } public File file() @@ -145,9 +151,10 @@ public int read(ByteBuffer buffer, long position) // FIXME: consider wrapping in a while loop return channel.read(buffer, position); } - catch (IOException e) + catch (Throwable e) { - throw new FSReadError(e, filePath); + JVMStabilityInspector.inspectThrowable(e); + throw new FSReadError(e, filePath()); } } @@ -157,9 +164,10 @@ public long transferTo(long position, long count, WritableByteChannel target) { return channel.transferTo(position, count, target); } - catch (IOException e) + catch (Throwable e) { - throw new FSReadError(e, filePath); + JVMStabilityInspector.inspectThrowable(e); + throw new FSReadError(e, filePath()); } } @@ -169,9 +177,10 @@ public MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) { return channel.map(mode, position, size); } - catch (IOException e) + catch (Throwable e) { - throw new FSReadError(e, filePath); + JVMStabilityInspector.inspectThrowable(e); + throw new FSReadError(e, filePath()); } } @@ -183,13 +192,17 @@ public long size() } catch (IOException e) { - throw new FSReadError(e, filePath); + throw new FSReadError(e, filePath()); } } - public int getFileDescriptor() + /** + * Apply FADV_DONTNEED to the file region. + */ + public void trySkipCache(long offset, long length) { - return NativeLibrary.getfd(channel); + int fd = INativeLibrary.instance.getfd(channel); + INativeLibrary.instance.trySkipCache(fd, offset, length, file.absolutePath()); } @Override diff --git a/src/java/org/apache/cassandra/io/util/ChecksumWriter.java b/src/java/org/apache/cassandra/io/util/ChecksumWriter.java index 194602c550ea..66c27da783c5 100644 --- a/src/java/org/apache/cassandra/io/util/ChecksumWriter.java +++ b/src/java/org/apache/cassandra/io/util/ChecksumWriter.java @@ -28,6 +28,7 @@ import javax.annotation.Nonnull; import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.compress.CompressedSequentialWriter; public class ChecksumWriter { @@ -88,11 +89,39 @@ public void appendDirect(ByteBuffer bb, boolean checksumIncrementalResult) } } + /** + * Checksum the given buffer and append the partial checksum after its end. + * Full checksum is updated to reflect the checksum bytes. + * + * Assumes the buffer has enough capacity to fit the extra 4 bytes, and leaves the buffer ready + * for writing (i.e. setting position to 0 and limit to the input limit + 4). + */ + public void appendToBuf(ByteBuffer bb) + { + incrementalChecksum.update(bb); + bb.limit(bb.capacity()); + int incrementalChecksumValue = (int) incrementalChecksum.getValue(); + bb.putInt(incrementalChecksumValue); + bb.flip(); + fullChecksum.update(bb); + incrementalChecksum.reset(); + bb.flip(); + } + public void writeFullChecksum(@Nonnull File digestFile) + { + writeFullChecksum(digestFile, fullChecksum.getValue()); + } + + /** + * Write given checksum into the digest file. This is used when {@link CompressedSequentialWriter} is reset and truncated, + * and we need to recompute digest for the whole file. + */ + public static void writeFullChecksum(@Nonnull File digestFile, long checksum) { try (FileOutputStreamPlus fos = new FileOutputStreamPlus(digestFile)) { - fos.write(String.valueOf(fullChecksum.getValue()).getBytes(StandardCharsets.UTF_8)); + fos.write(String.valueOf(checksum).getBytes(StandardCharsets.UTF_8)); fos.flush(); fos.getChannel().force(true); } diff --git a/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java b/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java index 8f0206e2a014..4780f02f9591 100644 --- a/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java +++ b/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java @@ -23,16 +23,21 @@ public final class ChecksummedRandomAccessReader { - @SuppressWarnings({ "resource", "RedundantSuppression" }) // The Rebufferer owns both the channel and the validator and handles closing both. public static RandomAccessReader open(File file, File crcFile) throws IOException + { + return open(file, crcFile, 0); + } + + @SuppressWarnings({"resource", "RedundantSuppression"}) // The Rebufferer owns both the channel and the validator and handles closing both. + public static RandomAccessReader open(File file, File crcFile, long startOffset) throws IOException { ChannelProxy channel = new ChannelProxy(file); try { DataIntegrityMetadata.ChecksumValidator validator = new DataIntegrityMetadata.ChecksumValidator(ChecksumType.CRC32, RandomAccessReader.open(crcFile), - file.path()); - Rebufferer rebufferer = new ChecksummedRebufferer(channel, validator); + file); + Rebufferer rebufferer = new ChecksummedRebufferer(channel, validator, startOffset); return new RandomAccessReader.RandomAccessReaderWithOwnChannel(rebufferer); } catch (Throwable t) diff --git a/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java b/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java index bc8695fd24d6..487545f4be11 100644 --- a/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java @@ -27,9 +27,9 @@ class ChecksummedRebufferer extends BufferManagingRebufferer { private final DataIntegrityMetadata.ChecksumValidator validator; - ChecksummedRebufferer(ChannelProxy channel, DataIntegrityMetadata.ChecksumValidator validator) + ChecksummedRebufferer(ChannelProxy channel, DataIntegrityMetadata.ChecksumValidator validator, long startOffset) { - super(new SimpleChunkReader(channel, channel.size(), BufferType.ON_HEAP, validator.chunkSize)); + super(new SimpleChunkReader(channel, channel.size(), BufferType.ON_HEAP, validator.chunkSize, startOffset)); this.validator = validator; } @@ -49,7 +49,7 @@ public BufferHolder rebuffer(long desiredPosition) } catch (IOException e) { - throw new CorruptFileException(e, channel().filePath()); + throw new CorruptFileException(e, channel().getFile()); } return this; diff --git a/src/java/org/apache/cassandra/io/util/ChunkReader.java b/src/java/org/apache/cassandra/io/util/ChunkReader.java index 779e7c35f94a..605d4b3d65af 100644 --- a/src/java/org/apache/cassandra/io/util/ChunkReader.java +++ b/src/java/org/apache/cassandra/io/util/ChunkReader.java @@ -34,7 +34,11 @@ public interface ChunkReader extends RebuffererFactory * Read the chunk at the given position, attempting to fill the capacity of the given buffer. * The filled buffer must be positioned at 0, with limit set at the size of the available data. * The source may have requirements for the positioning and/or size of the buffer (e.g. chunk-aligned and - * chunk-sized). These must be satisfied by the caller. + * chunk-sized). These must be satisfied by the caller. + *

    + * If the reader is created for a partial file described by {@link SliceDescriptor}, the provided position refers + * to the original file, not the slice, that is, the caller can provide only the position from the range of the + * slice (i.e. {@link SliceDescriptor#sliceStart} (incl) ... {@link SliceDescriptor#sliceEnd} (excl)). */ void readChunk(long position, ByteBuffer buffer); @@ -50,4 +54,17 @@ public interface ChunkReader extends RebuffererFactory BufferType preferredBufferType(); default void releaseUnderlyingResources() {} + + /** + * In some cases we may end up with both compressed and uncompressed data for the same file in + * the cache. This type is used to distinguish between them. + */ + enum ReaderType + { + SIMPLE, + COMPRESSED; + /** The number of types. Declared as a constant to avoid allocating on values(). */ + public static final int COUNT = ReaderType.values().length; + } + ReaderType type(); } diff --git a/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java b/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java index b6b3c9a6a267..50514d1a6628 100644 --- a/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java +++ b/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java @@ -31,22 +31,28 @@ import org.apache.cassandra.io.compress.CompressionMetadata; import org.apache.cassandra.io.compress.CorruptBlockException; import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.utils.ChecksumType; import org.apache.cassandra.utils.Closeable; +import org.apache.cassandra.utils.memory.BufferPools; public abstract class CompressedChunkReader extends AbstractReaderFileProxy implements ChunkReader { final CompressionMetadata metadata; final int maxCompressedLength; final Supplier crcCheckChanceSupplier; + protected final long startOffset; + protected final long onDiskStartOffset; - protected CompressedChunkReader(ChannelProxy channel, CompressionMetadata metadata, Supplier crcCheckChanceSupplier) + protected CompressedChunkReader(ChannelProxy channel, CompressionMetadata metadata, Supplier crcCheckChanceSupplier, long startOffset) { - super(channel, metadata.dataLength); + super(channel, metadata.dataLength + startOffset); this.metadata = metadata; this.maxCompressedLength = metadata.maxCompressedLength(); this.crcCheckChanceSupplier = crcCheckChanceSupplier; + this.startOffset = startOffset; assert Integer.bitCount(metadata.chunkLength()) == 1; //must be a power of two + this.onDiskStartOffset = startOffset == 0 ? 0 : metadata.chunkFor(startOffset).offset; } protected CompressedChunkReader forScan() @@ -69,12 +75,15 @@ boolean shouldCheckCrc() @Override public String toString() { - return String.format("CompressedChunkReader.%s(%s - %s, chunk length %d, data length %d)", + return String.format(startOffset > 0 + ? "CompressedChunkReader.%s(%s - %s, chunk length %d, data length %d, slice offset %s)" + : "CompressedChunkReader.%s(%s - %s, chunk length %d, data length %d)", getClass().getSimpleName(), channel.filePath(), metadata.compressor().getClass().getSimpleName(), metadata.chunkLength(), - metadata.dataLength); + metadata.dataLength, + startOffset); } @Override @@ -116,7 +125,7 @@ default void close() } - ByteBuffer read(CompressionMetadata.Chunk chunk, boolean shouldCheckCrc) throws CorruptBlockException; + void read(ByteBuffer compressed, int length, CompressionMetadata.Chunk chunk, long chunkOffset, boolean shouldCheckCrc) throws CorruptBlockException; } private static class RandomAccessCompressedReader implements CompressedReader @@ -131,25 +140,25 @@ private RandomAccessCompressedReader(ChannelProxy channel, CompressionMetadata m } @Override - public ByteBuffer read(CompressionMetadata.Chunk chunk, boolean shouldCheckCrc) throws CorruptBlockException + public void read(ByteBuffer compressed, int length, CompressionMetadata.Chunk chunk, long chunkOffset, boolean shouldCheckCrc) throws CorruptBlockException { - int length = shouldCheckCrc ? chunk.length + Integer.BYTES // compressed length + checksum length - : chunk.length; - ByteBuffer compressed = bufferHolder.getBuffer(length); - if (channel.read(compressed, chunk.offset) != length) - throw new CorruptBlockException(channel.filePath(), chunk); - compressed.flip(); - compressed.limit(chunk.length); + compressed.limit(length); + if (channel.read(compressed, chunkOffset) != length) + throw new CorruptBlockException(channel.getFile(), chunk); if (shouldCheckCrc) { + // compute checksum of the compressed data + compressed.position(0).limit(chunk.length); int checksum = (int) ChecksumType.CRC32.of(compressed); + // the remaining bytes are the checksum compressed.limit(length); - if (compressed.getInt() != checksum) - throw new CorruptBlockException(channel.filePath(), chunk); - compressed.position(0).limit(chunk.length); + int storedChecksum = compressed.getInt(); + if (storedChecksum != checksum) + throw new CorruptBlockException(channel.getFile(), chunk, storedChecksum, checksum); } - return compressed; + + compressed.position(0).limit(chunk.length); } } @@ -167,16 +176,14 @@ private ScanCompressedReader(ChannelProxy channel, CompressionMetadata metadata, } @Override - public ByteBuffer read(CompressionMetadata.Chunk chunk, boolean shouldCheckCrc) throws CorruptBlockException + public void read(ByteBuffer compressed, int length, CompressionMetadata.Chunk chunk, long chunkOffset, boolean shouldCheckCrc) throws CorruptBlockException { - int length = shouldCheckCrc ? chunk.length + Integer.BYTES // compressed length + checksum length - : chunk.length; - ByteBuffer compressed = bufferHolder.getBuffer(length); + compressed.limit(length); int copied = 0; while (copied < length) { - readAheadBuffer.fill(chunk.offset + copied); + readAheadBuffer.fill(chunkOffset + copied); int leftToRead = length - copied; if (readAheadBuffer.remaining() >= leftToRead) copied += readAheadBuffer.read(compressed, leftToRead); @@ -184,18 +191,19 @@ public ByteBuffer read(CompressionMetadata.Chunk chunk, boolean shouldCheckCrc) copied += readAheadBuffer.read(compressed, readAheadBuffer.remaining()); } - compressed.flip(); - compressed.limit(chunk.length); - if (shouldCheckCrc) { + // compute checksum of the compressed data + compressed.position(0).limit(chunk.length); int checksum = (int) ChecksumType.CRC32.of(compressed); + // the remaining bytes are the checksum compressed.limit(length); - if (compressed.getInt() != checksum) - throw new CorruptBlockException(channel.filePath(), chunk); - compressed.position(0).limit(chunk.length); + int storedChecksum = compressed.getInt(); + if (storedChecksum != checksum) + throw new CorruptBlockException(channel.getFile(), chunk, storedChecksum, checksum); } - return compressed; + + compressed.position(0).limit(chunk.length); } @Override @@ -222,15 +230,26 @@ public void close() } } + public ReaderType type() + { + return ReaderType.COMPRESSED; + } + public static class Standard extends CompressedChunkReader { private final CompressedReader reader; private final CompressedReader scanReader; + // we read the raw compressed bytes into this buffer, then uncompressed them into the provided one. public Standard(ChannelProxy channel, CompressionMetadata metadata, Supplier crcCheckChanceSupplier) { - super(channel, metadata, crcCheckChanceSupplier); + this(channel, metadata, crcCheckChanceSupplier, 0); + } + + public Standard(ChannelProxy channel, CompressionMetadata metadata, Supplier crcCheckChanceSupplier, long startOffset) + { + super(channel, metadata, crcCheckChanceSupplier, startOffset); reader = new RandomAccessCompressedReader(channel, metadata); int readAheadBufferSize = DatabaseDescriptor.getCompressedReadAheadBufferSize(); @@ -264,43 +283,50 @@ public void readChunk(long position, ByteBuffer uncompressed) CompressionMetadata.Chunk chunk = metadata.chunkFor(position); boolean shouldCheckCrc = shouldCheckCrc(); + int length = shouldCheckCrc ? chunk.length + Integer.BYTES // compressed length + checksum length + : chunk.length; - CompressedReader readFrom = (scanReader != null && scanReader.allocated()) ? scanReader : reader; - if (chunk.length < maxCompressedLength) + long chunkOffset = chunk.offset - onDiskStartOffset; + boolean shouldDecompress = chunk.length < maxCompressedLength; + if (shouldDecompress || shouldCheckCrc) // when we need to read the CRC too, follow the decompression path to avoid a second channel read call { - ByteBuffer compressed = readFrom.read(chunk, shouldCheckCrc); - uncompressed.clear(); + ByteBuffer compressed = BufferPools.forNetworking().getAtLeast(length, metadata.compressor().preferredBufferType()); try { - metadata.compressor().uncompress(compressed, uncompressed); + CompressedReader readFrom = (scanReader != null && scanReader.allocated()) ? scanReader : reader; + readFrom.read(compressed, length, chunk, chunkOffset, shouldCheckCrc); + uncompressed.clear(); + + try + { + if (shouldDecompress) + metadata.compressor().uncompress(compressed, uncompressed); + else + uncompressed.put(compressed); + } + catch (IOException e) + { + throw new CorruptBlockException(channel.getFile(), chunk, e); + } } - catch (IOException e) + finally { - throw new CorruptBlockException(channel.filePath(), chunk, e); + BufferPools.forNetworking().put(compressed); } } else { uncompressed.position(0).limit(chunk.length); - if (channel.read(uncompressed, chunk.offset) != chunk.length) - throw new CorruptBlockException(channel.filePath(), chunk); - - if (shouldCheckCrc) - { - uncompressed.flip(); - int checksum = (int) ChecksumType.CRC32.of(uncompressed); - - ByteBuffer scratch = ByteBuffer.allocate(Integer.BYTES); - if (channel.read(scratch, chunk.offset + chunk.length) != Integer.BYTES - || scratch.getInt(0) != checksum) - throw new CorruptBlockException(channel.filePath(), chunk); - } + if (channel.read(uncompressed, chunkOffset) != chunk.length) + throw new CorruptBlockException(channel.getFile(), chunk); } uncompressed.flip(); } catch (CorruptBlockException e) { + StorageProvider.instance.invalidateFileSystemCache(channel.getFile()); + // Make sure reader does not see stale data. uncompressed.position(0).limit(0); throw new CorruptSSTableException(e, channel.filePath()); @@ -316,6 +342,10 @@ public void close() super.close(); } + + public void invalidateIfCached(long position) + { + } } public static class Mmap extends CompressedChunkReader @@ -324,7 +354,12 @@ public static class Mmap extends CompressedChunkReader public Mmap(ChannelProxy channel, CompressionMetadata metadata, MmappedRegions regions, Supplier crcCheckChanceSupplier) { - super(channel, metadata, crcCheckChanceSupplier); + this(channel, metadata, regions, crcCheckChanceSupplier, 0); + } + + public Mmap(ChannelProxy channel, CompressionMetadata metadata, MmappedRegions regions, Supplier crcCheckChanceSupplier, long startOffset) + { + super(channel, metadata, crcCheckChanceSupplier, startOffset); this.regions = regions; } @@ -341,26 +376,25 @@ public void readChunk(long position, ByteBuffer uncompressed) MmappedRegions.Region region = regions.floor(chunk.offset); long segmentOffset = region.offset(); - int chunkOffset = Ints.checkedCast(chunk.offset - segmentOffset); + int chunkOffsetInSegment = Ints.checkedCast(chunk.offset - segmentOffset); ByteBuffer compressedChunk = region.buffer(); - compressedChunk.position(chunkOffset).limit(chunkOffset + chunk.length); - - uncompressed.clear(); - try { if (shouldCheckCrc()) { + compressedChunk.position(chunkOffsetInSegment).limit(chunkOffsetInSegment + chunk.length); int checksum = (int) ChecksumType.CRC32.of(compressedChunk); compressedChunk.limit(compressedChunk.capacity()); - if (compressedChunk.getInt() != checksum) - throw new CorruptBlockException(channel.filePath(), chunk); - - compressedChunk.position(chunkOffset).limit(chunkOffset + chunk.length); + int storedChecksum = compressedChunk.getInt(); + if (storedChecksum != checksum) + throw new CorruptBlockException(channel.getFile(), chunk, storedChecksum, checksum); } + compressedChunk.position(chunkOffsetInSegment).limit(chunkOffsetInSegment + chunk.length); + uncompressed.clear(); + if (chunk.length < maxCompressedLength) metadata.compressor().uncompress(compressedChunk, uncompressed); else @@ -368,7 +402,7 @@ public void readChunk(long position, ByteBuffer uncompressed) } catch (IOException e) { - throw new CorruptBlockException(channel.filePath(), chunk, e); + throw new CorruptBlockException(channel.getFile(), chunk, e); } uncompressed.flip(); } @@ -385,5 +419,10 @@ public void close() regions.closeQuietly(); super.close(); } + + @Override + public void invalidateIfCached(long position) + { + } } } diff --git a/src/java/org/apache/cassandra/io/util/CorruptFileException.java b/src/java/org/apache/cassandra/io/util/CorruptFileException.java index 875d06f537c4..37e1cea46fc0 100644 --- a/src/java/org/apache/cassandra/io/util/CorruptFileException.java +++ b/src/java/org/apache/cassandra/io/util/CorruptFileException.java @@ -21,11 +21,16 @@ @SuppressWarnings("serial") public class CorruptFileException extends RuntimeException { - public final String filePath; + public final File file; - public CorruptFileException(Exception cause, String filePath) + public CorruptFileException(Exception cause, File file) { super(cause); - this.filePath = filePath; + this.file = file; + } + + public File getFile() + { + return file; } } diff --git a/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java b/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java index aef3614da62b..080e80ee914b 100644 --- a/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java +++ b/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java @@ -23,6 +23,7 @@ import java.util.zip.CheckedInputStream; import java.util.zip.Checksum; +import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.utils.ChecksumType; public class DataIntegrityMetadata @@ -32,21 +33,32 @@ public static class ChecksumValidator implements Closeable private final ChecksumType checksumType; private final RandomAccessReader reader; public final int chunkSize; + private final File dataFile; public ChecksumValidator(File dataFile, File crcFile) throws IOException { this(ChecksumType.CRC32, RandomAccessReader.open(crcFile), - dataFile.absolutePath()); + dataFile); } - public ChecksumValidator(ChecksumType checksumType, RandomAccessReader reader, String dataFilename) throws IOException + public ChecksumValidator(ChecksumType checksumType, RandomAccessReader reader, File dataFile) throws IOException { this.checksumType = checksumType; this.reader = reader; + this.dataFile = dataFile; chunkSize = reader.readInt(); } + @VisibleForTesting + protected ChecksumValidator(ChecksumType checksumType, RandomAccessReader reader, int chunkSize) + { + this.checksumType = checksumType; + this.reader = reader; + this.dataFile = null; + this.chunkSize = chunkSize; + } + public void seek(long offset) { long start = chunkStart(offset); @@ -64,7 +76,7 @@ public void validate(byte[] bytes, int start, int end) throws IOException int calculatedValue = (int) checksumType.of(bytes, start, end); int storedValue = reader.readInt(); if (calculatedValue != storedValue) - throw new IOException(String.format("Corrupted file: integrity check (%s) failed for %s: %d != %d", checksumType.name(), reader.getPath(), storedValue, calculatedValue)); + throw new IOException(String.format("Corrupted file: integrity check (%s) failed for %s: %d != %d", checksumType.name(), dataFile, storedValue, calculatedValue)); } /** @@ -78,7 +90,7 @@ public void validate(ByteBuffer buffer) throws IOException int calculatedValue = (int) checksumType.of(buffer); int storedValue = reader.readInt(); if (calculatedValue != storedValue) - throw new IOException(String.format("Corrupted file: integrity check (%s) failed for %s: %d != %d", checksumType.name(), reader.getPath(), storedValue, calculatedValue)); + throw new IOException(String.format("Corrupted file: integrity check (%s) failed for %s: %d != %d", checksumType.name(), dataFile, storedValue, calculatedValue)); } public void close() diff --git a/src/java/org/apache/cassandra/io/util/DataOutputPlus.java b/src/java/org/apache/cassandra/io/util/DataOutputPlus.java index f8bc95953164..ed75c1b19791 100644 --- a/src/java/org/apache/cassandra/io/util/DataOutputPlus.java +++ b/src/java/org/apache/cassandra/io/util/DataOutputPlus.java @@ -170,6 +170,7 @@ default int maxBytesInPage() /** * Pad this with zeroes until the next page boundary. If the destination position * is already at a page boundary, do not do anything. + * @throws IOException */ default void padToPageBoundary() throws IOException { @@ -194,4 +195,4 @@ default long paddedPosition() { throw new UnsupportedOperationException(); } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/io/util/EmptyRebufferer.java b/src/java/org/apache/cassandra/io/util/EmptyRebufferer.java index 7f54a6b180f2..ddaf809f797a 100644 --- a/src/java/org/apache/cassandra/io/util/EmptyRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/EmptyRebufferer.java @@ -51,6 +51,12 @@ public double getCrcCheckChance() return 0; } + @Override + public long adjustPosition(long position) + { + return position; + } + @Override public BufferHolder rebuffer(long position) { @@ -68,4 +74,15 @@ public Rebufferer instantiateRebufferer(boolean isScan) { return this; } + + @Override + public int chunkSize() + { + return -1; + } + + @Override + public void invalidateIfCached(long position) + { + } } diff --git a/src/java/org/apache/cassandra/io/util/EncryptedChunkReader.java b/src/java/org/apache/cassandra/io/util/EncryptedChunkReader.java new file mode 100644 index 000000000000..98565ab5477a --- /dev/null +++ b/src/java/org/apache/cassandra/io/util/EncryptedChunkReader.java @@ -0,0 +1,299 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.compress.BufferType; +import org.apache.cassandra.io.compress.CorruptBlockException; +import org.apache.cassandra.io.compress.EncryptedSequentialWriter; +import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.utils.ChecksumType; +import org.apache.cassandra.utils.memory.BufferPools; + +import static org.apache.cassandra.io.compress.EncryptedSequentialWriter.CHUNK_SIZE; +import static org.apache.cassandra.io.compress.EncryptedSequentialWriter.FOOTER_LENGTH; + +/** + * Reader for encryption-only files written using EncryptedSequentialWriter. + * + * These files are written in chunks, where each page has some of its size reserved for metadata (e.g. CRC, length, + * IV). The metadata is visible only to this class, but to avoid having to define a mapping between file and logical + * positions, the file skips over the space assigned to the metadata. + * + * In other words, to access e.g. content at position 0x12E34F with chunk size 0x1000, we read 0x1000 encrypted chunk + * bytes at position 0x12E000 in the file, decrypt the content and then position the buffer on offset 0x34F. The + * buffer's limit will be lower than 0x1000 (typically by at least 33 bytes) and if we read (or skip over) a sequence + * that reaches this limit the position will jump to the beginning of the next chunk (see adjustPosition). + * + * Both the encrypted chunk size (given by the CHUNK_SIZE constant) and the decrypted (i.e. usable) size (calculated as + * the largest that must fit CHUNK_SIZE) are fixed. + * + * For comparison, in compressed files the chunk size is equal to the uncompressed/usable size, while the + * compressed size varies. There are unrelated compressed and uncompressed positions which are resolved + * using an in-memory offsets mapping. + * + * Used for primary indices (both partition and row) where most pages store page-packed tries, where compression is + * not beneficial and in-memory offset overhead (given the chunk size of 4k) would be prohibitive. + */ +public abstract class EncryptedChunkReader extends AbstractReaderFileProxy implements ChunkReader +{ + private static final Logger logger = LoggerFactory.getLogger(EncryptedChunkReader.class); + + final int maxBytesInPage; + + final CompressionParams compressionParams; + final ICompressor encryptor; + + EncryptedChunkReader(ChannelProxy channel, long fileLength, CompressionParams params, ICompressor encryptor, int maxBytesInPage) + { + super(channel, fileLength); + this.compressionParams = params; + this.encryptor = encryptor; + this.maxBytesInPage = maxBytesInPage; + } + + public long adjustPosition(long position) + { + if (inChunkOffset(position) < maxBytesInPage) + return position; + + return position - maxBytesInPage + CHUNK_SIZE; + } + + private static long inChunkOffset(long position) + { + return position & (CHUNK_SIZE - 1); + } + + public ReaderType type() + { + return ReaderType.COMPRESSED; + } + + public boolean shouldCheckCrc() + { + return compressionParams.shouldCheckCrc(); + } + + protected ByteBuffer decrypt(ByteBuffer input, int start, ByteBuffer output, long position) throws IOException + { + assert output.capacity() == CHUNK_SIZE; + + if (shouldCheckCrc()) + { + input.position(start).limit(start + CHUNK_SIZE - 4); + int checksum = (int) ChecksumType.CRC32.of(input); + + //Change the limit to include the checksum + input.limit(start + CHUNK_SIZE); + if (input.getInt() != checksum) + throw new CorruptBlockException(channel.getFile(), position, CHUNK_SIZE); + } + + int length = input.getInt(start + CHUNK_SIZE - FOOTER_LENGTH); + output.clear(); + input.position(start).limit(start + length); + encryptor.uncompress(input, output); + output.flip(); + + return output; + } + + @Override + public int chunkSize() + { + return CHUNK_SIZE; + } + + public Rebufferer instantiateRebufferer() + { + return new BufferManagingRebufferer.Aligned(this); + } + + @Override + public Rebufferer instantiateRebufferer(boolean isScan) + { + return instantiateRebufferer(); + } + + @Override + public void invalidateIfCached(long position) + { + // Encrypted chunks are not cached, so nothing to invalidate + } + + @Override + public String toString() + { + return String.format("EncryptedChunkReader.%s(%s - %s, chunk length %d, data length %d)", + getClass().getSimpleName(), + channel.filePath(), + encryptor.getClass().getSimpleName(), + CHUNK_SIZE, + fileLength); + } + + public static Standard createStandard(ChannelProxy channel, + ICompressor encryptor, + CompressionParams compressionParams, + long fileLength, + long overrideLength) + { + int maxBytesInPage = EncryptedSequentialWriter.maxBytesInPage(encryptor); + + if (overrideLength <= 0) + { + // For encrypted files, we need to calculate the logical data length + // Each chunk can hold maxBytesInPage of actual data + // Calculate how many complete chunks we have + long numChunks = fileLength / CHUNK_SIZE; + // Calculate the logical data that can be stored + overrideLength = numChunks * maxBytesInPage; + // If there's a partial last chunk, add its data + long lastChunkSize = fileLength % CHUNK_SIZE; + if (lastChunkSize > 0) { + // The last chunk might have less data + overrideLength += Math.max(0, lastChunkSize - (CHUNK_SIZE - maxBytesInPage)); + } + } + + return new Standard(channel, compressionParams, encryptor, overrideLength, maxBytesInPage); + } + + public static Mmap createMmap(ChannelProxy channel, + MmappedRegions regions, + ICompressor encryptor, + CompressionParams compressionParams, + long fileLength, + long overrideLength) + { + int maxBytesInPage = EncryptedSequentialWriter.maxBytesInPage(encryptor); + + if (overrideLength <= 0) + { + // For encrypted files, we need to calculate the logical data length + // Each chunk can hold maxBytesInPage of actual data + // Calculate how many complete chunks we have + long numChunks = fileLength / CHUNK_SIZE; + // Calculate the logical data that can be stored + overrideLength = numChunks * maxBytesInPage; + // If there's a partial last chunk, add its data + long lastChunkSize = fileLength % CHUNK_SIZE; + if (lastChunkSize > 0) { + // The last chunk might have less data + overrideLength += Math.max(0, lastChunkSize - (CHUNK_SIZE - maxBytesInPage)); + } + } + return new Mmap(channel, regions, compressionParams, encryptor, overrideLength, maxBytesInPage); + } + + static class Standard extends EncryptedChunkReader + { + Standard(ChannelProxy channel, CompressionParams params, ICompressor encryptor, long dataLength, int maxBytesInPage) + { + super(channel, dataLength, params, encryptor, maxBytesInPage); + } + + @Override + public void readChunk(long position, ByteBuffer buffer) + { + assert inChunkOffset(position) == 0 : "Access must always be aligned"; + assert buffer.capacity() >= CHUNK_SIZE; + + ByteBuffer input; + + if (encryptor.canDecompressInPlace()) + { + input = buffer.duplicate(); + } + else + { + input = BufferPools.forNetworking().get(CHUNK_SIZE, BufferType.preferredForCompression()); + } + + try + { + input.position(0).limit(CHUNK_SIZE); + channel.read(input, position); + decrypt(input, 0, buffer, position); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + finally + { + if (!encryptor.canDecompressInPlace()) + { + BufferPools.forNetworking().put(input); + } + } + } + + @Override + public BufferType preferredBufferType() + { + return BufferType.preferredForCompression(); + } + } + + static class Mmap extends EncryptedChunkReader + { + final MmappedRegions regions; + + Mmap(ChannelProxy channel, MmappedRegions regions, CompressionParams params, ICompressor encryptor, long dataLength, int maxBytesInPage) + { + super(channel, dataLength, params, encryptor, maxBytesInPage); + this.regions = regions; + } + + @Override + public void readChunk(long position, ByteBuffer buffer) + { + assert inChunkOffset(position) == 0 : "Access must always be aligned"; + assert buffer.capacity() >= CHUNK_SIZE; + + MmappedRegions.Region r = regions.floor(position); + try + { + decrypt(r.buffer(), (int) (position - r.offset()), buffer, position); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public BufferType preferredBufferType() + { + return BufferType.preferredForCompression(); + } + + @Override + public void close() + { + regions.closeQuietly(); + super.close(); + } + } +} diff --git a/src/java/org/apache/cassandra/io/util/File.java b/src/java/org/apache/cassandra/io/util/File.java index de415388ed9e..fc801f00435b 100644 --- a/src/java/org/apache/cassandra/io/util/File.java +++ b/src/java/org/apache/cassandra/io/util/File.java @@ -23,12 +23,12 @@ import java.io.UncheckedIOException; import java.net.URI; import java.nio.channels.FileChannel; +import java.nio.file.*; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.Paths; // checkstyle: permit this import import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Consumer; @@ -143,9 +143,6 @@ public File(FileSystem fs, String first, String... more) */ public File(Path path) { - if (path != null && path.getFileSystem() != filesystem) - throw new IllegalArgumentException("Incompatible file system; path FileSystem (" + path.getFileSystem() + ") is not the same reference (" + filesystem + ")"); - this.path = path; } @@ -261,6 +258,47 @@ public void move(File to) PathUtils.rename(toPathForRead(), to.toPathForWrite()); } + public void copy(File target, StandardCopyOption options) + { + PathUtils.copy(toPathForRead(), target.toPathForWrite(), options); + } + + /** + * Constructs a relative path between this path and a given path. + */ + public File relativize(File other) + { + Path relative = toPathForRead().relativize(other.toPathForRead()); + return new File(relative); + } + + /** + * Resolves give path against this path's parent path + */ + public File resolveSibling(String path) + { + Path sibling = toPathForRead().resolveSibling(path); + return new File(sibling); + } + + /** + * Resolves give path against this path + */ + public File resolve(String path) + { + Path sibling = toPathForRead().resolve(path); + return new File(sibling); + } + + /** + * Resolves give path against this path + */ + public File resolve(File path) + { + Path sibling = toPathForRead().resolve(path.toPathForRead()); + return new File(sibling); + } + /** * @return the length of the file if it exists and if we can read it; 0 otherwise. */ @@ -718,6 +756,11 @@ public int compareTo(File that) return this.path.compareTo(that.path); } + public URI toUri() + { + return Objects.requireNonNull(path).toUri(); + } + public java.io.File toJavaIOFile() { return path == null ? new java.io.File("") // checkstyle: permit this instantiation diff --git a/src/java/org/apache/cassandra/io/util/FileDataInput.java b/src/java/org/apache/cassandra/io/util/FileDataInput.java index 1059b0111cab..fdaacf3a22d4 100644 --- a/src/java/org/apache/cassandra/io/util/FileDataInput.java +++ b/src/java/org/apache/cassandra/io/util/FileDataInput.java @@ -22,7 +22,7 @@ public interface FileDataInput extends RewindableDataInput, Closeable { - String getPath(); + File getFile(); boolean isEOF() throws IOException; diff --git a/src/java/org/apache/cassandra/io/util/FileHandle.java b/src/java/org/apache/cassandra/io/util/FileHandle.java index 7e4b214128ac..eeff2f56a93d 100644 --- a/src/java/org/apache/cassandra/io/util/FileHandle.java +++ b/src/java/org/apache/cassandra/io/util/FileHandle.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.io.util; +import java.nio.ByteOrder; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -24,11 +25,15 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.RateLimiter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.cache.ChunkCache; import org.apache.cassandra.config.Config; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.compress.CompressionMetadata; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.io.compress.EncryptedSequentialWriter; +import org.apache.cassandra.io.compress.Encryptor; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.concurrent.Ref; import org.apache.cassandra.utils.concurrent.RefCounted; @@ -48,9 +53,14 @@ */ public class FileHandle extends SharedCloseableImpl { + private static final Logger logger = LoggerFactory.getLogger(FileHandle.class); + public final ChannelProxy channel; public final long onDiskLength; + private final ByteOrder order; + + public final SliceDescriptor sliceDescriptor; /* * Rebufferer factory to use when constructing RandomAccessReaders @@ -66,13 +76,17 @@ private FileHandle(Cleanup cleanup, ChannelProxy channel, RebuffererFactory rebuffererFactory, CompressionMetadata compressionMetadata, - long onDiskLength) + ByteOrder order, + long onDiskLength, + SliceDescriptor sliceDescriptor) { super(cleanup); this.rebuffererFactory = rebuffererFactory; this.channel = channel; this.compressionMetadata = Optional.ofNullable(compressionMetadata); + this.order = order; this.onDiskLength = onDiskLength; + this.sliceDescriptor = sliceDescriptor; } private FileHandle(FileHandle copy) @@ -81,7 +95,9 @@ private FileHandle(FileHandle copy) channel = copy.channel; rebuffererFactory = copy.rebuffererFactory; compressionMetadata = copy.compressionMetadata; + order = copy.order; onDiskLength = copy.onDiskLength; + sliceDescriptor = copy.sliceDescriptor; } /** @@ -99,7 +115,7 @@ public String path() public long dataLength() { - return compressionMetadata.map(c -> c.dataLength).orElseGet(rebuffererFactory::fileLength); + return rebuffererFactory.fileLength(); } public RebuffererFactory rebuffererFactory() @@ -125,18 +141,39 @@ public FileHandle sharedCopy() } /** - * Create {@link RandomAccessReader} with configured method of reading content of the file. + * Create {@link RandomAccessReader} with configured method of reading content of the file. Positions the reader + * at the start of the file or the start of the data if the handle is created for a slice (see {@link SliceDescriptor}). * * @return RandomAccessReader for the file */ public RandomAccessReader createReader() { - return createReader(null); + return createReader(ReadPattern.RANDOM); } public RandomAccessReader createReaderForScan() { - return createReader(null, true); + return createReader(null, true, sliceDescriptor.dataStart, ReadPattern.SEQUENTIAL); + } + + public RandomAccessReader createReader(ReadPattern accessPattern) + { + return createReader(null, accessPattern); + } + + public RandomAccessReader createReader(RateLimiter limiter, ReadPattern accessPattern) + { + return createReader(limiter, false, sliceDescriptor.dataStart, accessPattern); + } + + public RandomAccessReader createReader(long position) + { + return createReader(null, false, position, ReadPattern.RANDOM); + } + + public RandomAccessReader createReader(long position, ReadPattern accessPattern) + { + return createReader(null, false, position, accessPattern); } /** @@ -144,38 +181,19 @@ public RandomAccessReader createReaderForScan() * Reading from file will be rate limited by given {@link RateLimiter}. * * @param limiter RateLimiter to use for rate limiting read + * @param position Position in the file to start reading from + * @param accessPattern the access pattern expected for the reads made against the returned reader (this is + * mostly a hint that may optimize the reader for the provided patter, typically enabling + * prefetching for {@link ReadPattern#SEQUENTIAL}). * @return RandomAccessReader for the file */ - public RandomAccessReader createReader(RateLimiter limiter) - { - return createReader(limiter, false); - } - - public RandomAccessReader createReader(RateLimiter limiter, boolean forScan) + public RandomAccessReader createReader(RateLimiter limiter, boolean forScan, long position, ReadPattern accessPattern) { - return new RandomAccessReader(instantiateRebufferer(limiter, forScan)); - } - - public FileDataInput createReader(long position) - { - RandomAccessReader reader = createReader(); - try - { - reader.seek(position); - return reader; - } - catch (Throwable t) - { - try - { - reader.close(); - } - catch (Throwable t2) - { - t.addSuppressed(t2); - } - throw t; - } + assert position >= 0 : "Position must be non-negative - file: " + channel.filePath() + ", position: " + position; + Rebufferer.BufferHolder bufferHolder = position > 0 + ? Rebufferer.emptyBufferHolderAt(position) + : Rebufferer.EMPTY; + return new RandomAccessReader(instantiateRebufferer(limiter, forScan, accessPattern), order, bufferHolder); } /** @@ -189,19 +207,25 @@ public void dropPageCache(long before) if (before >= metadata.dataLength) return 0L; else - return metadata.chunkFor(before).offset; - }).orElse(before); - NativeLibrary.trySkipCache(channel.getFileDescriptor(), 0, position, file().absolutePath()); + return metadata.chunkFor(before).offset - metadata.chunkFor(sliceDescriptor.sliceStart).offset; + }).orElse(before - sliceDescriptor.sliceStart); + + if (position > 0) + channel.trySkipCache(0, position); + else + channel.trySkipCache(0, onDiskLength); } - public Rebufferer instantiateRebufferer(RateLimiter limiter) + public Rebufferer instantiateRebufferer(RateLimiter limiter, ReadPattern accessPattern) { - return instantiateRebufferer(limiter, false); + return instantiateRebufferer(limiter, false, accessPattern); } - public Rebufferer instantiateRebufferer(RateLimiter limiter, boolean forScan) + public Rebufferer instantiateRebufferer(RateLimiter limiter, boolean forScan, ReadPattern accessPattern) { - Rebufferer rebufferer = rebuffererFactory.instantiateRebufferer(forScan); + Rebufferer rebufferer = accessPattern == ReadPattern.SEQUENTIAL + ? PrefetchingRebufferer.withPrefetching(rebuffererFactory) + : rebuffererFactory.instantiateRebufferer(forScan); if (limiter != null) rebufferer = new LimitingRebufferer(rebufferer, limiter, DiskOptimizationStrategy.MAX_BUFFER_SIZE); @@ -216,17 +240,14 @@ private static class Cleanup implements RefCounted.Tidy final ChannelProxy channel; final RebuffererFactory rebufferer; final CompressionMetadata compressionMetadata; - final Optional chunkCache; private Cleanup(ChannelProxy channel, RebuffererFactory rebufferer, - CompressionMetadata compressionMetadata, - ChunkCache chunkCache) + CompressionMetadata compressionMetadata) { this.channel = channel; this.rebufferer = rebufferer; this.compressionMetadata = compressionMetadata; - this.chunkCache = Optional.ofNullable(chunkCache); } public String name() @@ -236,7 +257,8 @@ public String name() public void tidy() { - chunkCache.ifPresent(cache -> cache.invalidateFile(name())); + // Note: we cannot release data held by the chunk cache at this point, because this would release data that + // is pre-cached by early open. Release is done during SSTableReader cleanup. See EarlyOpenCachingTest. try { if (compressionMetadata != null) @@ -272,9 +294,13 @@ public static class Builder private ChunkCache chunkCache; private int bufferSize = RandomAccessReader.DEFAULT_BUFFER_SIZE; private BufferType bufferType = BufferType.OFF_HEAP; + private ByteOrder order = ByteOrder.BIG_ENDIAN; + private boolean mmapped = false; private long lengthOverride = -1; private MmappedRegionsCache mmappedRegionsCache; + private SliceDescriptor sliceDescriptor = SliceDescriptor.NONE; + private boolean adviseRandom = false; public Builder(File file) { @@ -362,6 +388,17 @@ public Builder bufferType(BufferType bufferType) return this; } + /** + * Set the byte order to apply to each buffer. + * @param order + * @return + */ + public Builder order(ByteOrder order) + { + this.order = order; + return this; + } + /** * Override the file length. * @@ -375,6 +412,25 @@ public Builder withLengthOverride(long lengthOverride) return this; } + public Builder slice(SliceDescriptor sliceDescriptor) + { + this.sliceDescriptor = sliceDescriptor; + return this; + } + + public Builder adviseRandom() + { + adviseRandom = true; + return this; + } + + public Builder maybeEncrypted(boolean encrypted) + { + // For encrypted files, we need to ensure compressionMetadata is available + // This is needed because encrypted files use the compression framework + return this; + } + /** * Complete building {@link FileHandle}. */ @@ -394,8 +450,17 @@ public FileHandle complete(Function channelProxyFactory) compressionMetadata = this.compressionMetadata != null ? this.compressionMetadata.sharedCopy() : null; channel = channelProxyFactory.apply(file); - long fileLength = (compressionMetadata != null) ? compressionMetadata.compressedFileLength : channel.size(); - long length = lengthOverride > 0 ? lengthOverride : fileLength; + long fileLength; + if (compressionMetadata != null && compressionMetadata.useActualFileSize) + { + // For encrypted-only files, we need to use the actual file size + fileLength = channel.size(); + } + else + { + fileLength = (compressionMetadata != null) ? compressionMetadata.compressedFileLength : channel.size(); + } + long length = lengthOverride >= 0 ? lengthOverride : fileLength; RebuffererFactory rebuffererFactory; if (length == 0) @@ -406,32 +471,69 @@ else if (mmapped) { if (compressionMetadata != null) { - regions = mmappedRegionsCache != null ? mmappedRegionsCache.getOrCreate(channel, compressionMetadata, bufferSize) - : MmappedRegions.map(channel, compressionMetadata); - rebuffererFactory = maybeCached(new CompressedChunkReader.Mmap(channel, compressionMetadata, regions, crcCheckChanceSupplier)); + // Check if this is encryption rather than compression + if (compressionMetadata.compressor() instanceof Encryptor) + { + // For encrypted files, we need to map the actual file size, not logical data length + // MmappedRegions maps physical file regions, not logical data + Encryptor encryptor = (Encryptor) compressionMetadata.compressor(); + + // Map the actual file size, with chunks aligned to CHUNK_SIZE + int chunkSize = EncryptedSequentialWriter.CHUNK_SIZE; + regions = mmappedRegionsCache != null ? mmappedRegionsCache.getOrCreate(channel, fileLength, chunkSize, sliceDescriptor.sliceStart) + : MmappedRegions.map(channel, fileLength, chunkSize, sliceDescriptor.sliceStart, adviseRandom); + // For encrypted files without explicit length override, pass -1 to let EncryptedChunkReader calculate the logical length + long encryptedOverrideLength = (lengthOverride >= 0) ? length : -1; + rebuffererFactory = EncryptedChunkReader.createMmap(channel, regions, encryptor, compressionMetadata.parameters, fileLength, encryptedOverrideLength); + } + else + { + regions = mmappedRegionsCache != null ? mmappedRegionsCache.getOrCreate(channel, compressionMetadata, bufferSize, sliceDescriptor.sliceStart) + : MmappedRegions.map(channel, compressionMetadata, sliceDescriptor.sliceStart, adviseRandom); + rebuffererFactory = maybeCached(new CompressedChunkReader.Mmap(channel, compressionMetadata, regions, crcCheckChanceSupplier, sliceDescriptor.sliceStart)); + } } else { - regions = mmappedRegionsCache != null ? mmappedRegionsCache.getOrCreate(channel, length, bufferSize) - : MmappedRegions.map(channel, length, bufferSize); - rebuffererFactory = new MmapRebufferer(channel, length, regions); + regions = mmappedRegionsCache != null ? mmappedRegionsCache.getOrCreate(channel, sliceDescriptor.dataEndOr(length), bufferSize, sliceDescriptor.sliceStart) + : MmappedRegions.map(channel, sliceDescriptor.dataEndOr(length), bufferSize, sliceDescriptor.sliceStart, adviseRandom); + rebuffererFactory = new MmapRebufferer(channel, sliceDescriptor.dataEndOr(length), regions); + } } else { + if (adviseRandom) + logger.warn("adviseRandom ignored for non-mmapped FileHandle {}", file); + if (compressionMetadata != null) { - rebuffererFactory = maybeCached(new CompressedChunkReader.Standard(channel, compressionMetadata, crcCheckChanceSupplier)); + // Check if this is encryption rather than compression + if (compressionMetadata.compressor() instanceof Encryptor) + { + Encryptor encryptor = (Encryptor) compressionMetadata.compressor(); + // For encrypted files without explicit length override, pass -1 to let EncryptedChunkReader calculate the logical length + long encryptedOverrideLength = (lengthOverride >= 0) ? length : -1; + rebuffererFactory = EncryptedChunkReader.createStandard(channel, encryptor, compressionMetadata.parameters, fileLength, encryptedOverrideLength); + } + else + { + rebuffererFactory = maybeCached(new CompressedChunkReader.Standard(channel, compressionMetadata, crcCheckChanceSupplier, sliceDescriptor.sliceStart)); + } } else { int chunkSize = DiskOptimizationStrategy.roundForCaching(bufferSize, ChunkCache.roundUp); - rebuffererFactory = maybeCached(new SimpleChunkReader(channel, length, bufferType, chunkSize)); + if (sliceDescriptor.chunkSize > 0 && sliceDescriptor.chunkSize < chunkSize) + // if the chunk size in the slice was smaller than the one we used in the rebufferer, + // we could end up aligning the file position to the value lower than the slice start + chunkSize = sliceDescriptor.chunkSize; + rebuffererFactory = maybeCached(new SimpleChunkReader(channel, sliceDescriptor.dataEndOr(length), bufferType, chunkSize, sliceDescriptor.sliceStart)); } } - Cleanup cleanup = new Cleanup(channel, rebuffererFactory, compressionMetadata, chunkCache); + Cleanup cleanup = new Cleanup(channel, rebuffererFactory, compressionMetadata); - FileHandle fileHandle = new FileHandle(cleanup, channel, rebuffererFactory, compressionMetadata, length); + FileHandle fileHandle = new FileHandle(cleanup, channel, rebuffererFactory, compressionMetadata, order, length, sliceDescriptor); return fileHandle; } catch (Throwable t) @@ -444,7 +546,7 @@ else if (mmapped) private RebuffererFactory maybeCached(ChunkReader reader) { if (chunkCache != null && chunkCache.capacity() > 0) - return chunkCache.wrap(reader); + return chunkCache.maybeWrap(reader); return reader; } } diff --git a/src/java/org/apache/cassandra/io/util/FileInputStreamPlus.java b/src/java/org/apache/cassandra/io/util/FileInputStreamPlus.java index 2bd57a99d2a7..bfb694ca8f7c 100644 --- a/src/java/org/apache/cassandra/io/util/FileInputStreamPlus.java +++ b/src/java/org/apache/cassandra/io/util/FileInputStreamPlus.java @@ -84,4 +84,10 @@ public void close() throws IOException } } } + + @Override + public String toString() + { + return file.toString(); + } } diff --git a/src/java/org/apache/cassandra/io/util/FileSegmentInputStream.java b/src/java/org/apache/cassandra/io/util/FileSegmentInputStream.java index a58521527ddc..255a46a16338 100644 --- a/src/java/org/apache/cassandra/io/util/FileSegmentInputStream.java +++ b/src/java/org/apache/cassandra/io/util/FileSegmentInputStream.java @@ -26,19 +26,20 @@ */ public class FileSegmentInputStream extends DataInputBuffer implements FileDataInput { - private final String filePath; + private final File file; private final long offset; - public FileSegmentInputStream(ByteBuffer buffer, String filePath, long offset) + public FileSegmentInputStream(ByteBuffer buffer, File file, long offset) { super(buffer, false); - this.filePath = filePath; + this.file = file; this.offset = offset; } - public String getPath() + @Override + public File getFile() { - return filePath; + return file; } private long size() @@ -61,7 +62,7 @@ public void seek(long pos) if (pos < 0 || pos > size()) throw new IllegalArgumentException(String.format("Unable to seek to position %d in %s (%d bytes) in partial mode", pos, - getPath(), + getFile(), size())); diff --git a/src/java/org/apache/cassandra/io/util/FileUtils.java b/src/java/org/apache/cassandra/io/util/FileUtils.java index 7027d6e114e2..eeef01ddeb70 100644 --- a/src/java/org/apache/cassandra/io/util/FileUtils.java +++ b/src/java/org/apache/cassandra/io/util/FileUtils.java @@ -19,7 +19,9 @@ import java.io.BufferedWriter; import java.io.Closeable; +import java.io.DataInput; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -48,9 +50,10 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.RateLimiter; import org.slf4j.Logger; @@ -60,6 +63,7 @@ import org.apache.cassandra.io.FSErrorHandler; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.utils.DseLegacy; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.SyncUtil; @@ -82,6 +86,7 @@ public final class FileUtils private static final Class clsDirectBuffer; private static final MethodHandle mhDirectBufferCleaner; + private static final MethodHandle mhDirectBufferAttachment; private static final MethodHandle mhCleanerClean; static @@ -91,6 +96,8 @@ public final class FileUtils clsDirectBuffer = Class.forName("sun.nio.ch.DirectBuffer"); Method mDirectBufferCleaner = clsDirectBuffer.getMethod("cleaner"); mhDirectBufferCleaner = MethodHandles.lookup().unreflect(mDirectBufferCleaner); + Method mDirectBufferAttachment = clsDirectBuffer.getMethod("attachment"); + mhDirectBufferAttachment = MethodHandles.lookup().unreflect(mDirectBufferAttachment); Method mCleanerClean = mDirectBufferCleaner.getReturnType().getMethod("clean"); mhCleanerClean = MethodHandles.lookup().unreflect(mCleanerClean); @@ -233,12 +240,13 @@ public static void copyWithOutConfirm(File from, File to) { try { - Files.copy(from.toPath(), to.toPath()); + if (from.exists()) + Files.copy(from.toPath(), to.toPath()); } catch (IOException e) { if (logger.isTraceEnabled()) - logger.trace("Could not copy file" + from + " to " + to, e); + logger.trace("Could not copy file " + from + " to " + to, e); } } @@ -265,7 +273,11 @@ public static void copyWithConfirm(File from, File to) public static void truncate(String path, long size) { - File file = new File(path); + truncate(new File(path), size); + } + + public static void truncate(File file, long size) + { try (FileChannel channel = file.newReadWriteChannel()) { channel.truncate(size); @@ -302,15 +314,15 @@ public static void closeQuietly(AutoCloseable c) } } - public static void close(Closeable... cs) throws IOException + public static void close(AutoCloseable... cs) throws IOException { close(Arrays.asList(cs)); } - public static void close(Iterable cs) throws IOException + public static void close(Iterable cs) throws IOException { Throwable e = null; - for (Closeable c : cs) + for (AutoCloseable c : cs) { try { @@ -327,6 +339,17 @@ public static void close(Iterable cs) throws IOException maybeFail(e, IOException.class); } + public static void closeQuietly(Closeable... cs) + { + closeQuietly(Arrays.asList(cs)); + } + + public static void closeQuietly(AutoCloseable... cs) + { + for (AutoCloseable c : cs) + closeQuietly(c); + } + public static void closeQuietly(Iterable cs) { for (AutoCloseable c : cs) @@ -359,7 +382,7 @@ public static boolean isContained(File folder, File file) return folder.isAncestorOf(file); } - public static void clean(ByteBuffer buffer) + public static void clean(ByteBuffer buffer, boolean withAttachment) { if (buffer == null || !buffer.isDirect()) return; @@ -370,10 +393,18 @@ public static void clean(ByteBuffer buffer) try { - Object cleaner = mhDirectBufferCleaner.bindTo(buffer).invoke(); + Object buf = buffer; + if (withAttachment) + { + while (mhDirectBufferCleaner.bindTo(buf).invoke() == null && mhDirectBufferAttachment.bindTo(buf).invoke() != null && mhDirectBufferAttachment.bindTo(buf).invoke().getClass().isInstance(clsDirectBuffer)) + { + buf = mhDirectBufferAttachment.bindTo(buf).invoke(); + } + } + + Object cleaner = mhDirectBufferCleaner.bindTo(buf).invoke(); if (cleaner != null) { - // ((DirectBuffer) buf).cleaner().clean(); mhCleanerClean.bindTo(cleaner).invoke(); } } @@ -387,6 +418,16 @@ public static void clean(ByteBuffer buffer) } } + public static void clean(ByteBuffer buffer) + { + clean(buffer, false); + } + + public static void cleanWithAttachment(ByteBuffer buffer) + { + clean(buffer, true); + } + public static long parseFileSize(String value) { long result; @@ -528,6 +569,26 @@ public FileVisitResult visitFileFailed(Path path, IOException e) throws IOExcept return sizeArr[0]; } + public static void copyTo(DataInput in, OutputStream out, int length) throws IOException + { + byte[] buffer = new byte[64 * 1024]; + int copiedBytes = 0; + + while (copiedBytes + buffer.length < length) + { + in.readFully(buffer); + out.write(buffer); + copiedBytes += buffer.length; + } + + if (copiedBytes < length) + { + int left = length - copiedBytes; + in.readFully(buffer, 0, left); + out.write(buffer, 0, left); + } + } + public static void append(File file, String ... lines) { if (file.exists()) @@ -696,6 +757,16 @@ public static boolean isSubDirectory(File parent, File child) { return parent.isAncestorOf(child); } + + /** + * Handle large file system by returning {@code Long.MAX_VALUE} when the size overflows. + * @param size returned by the Java's FileStore methods + * @return the size or {@code Long.MAX_VALUE} if the size was bigger than {@code Long.MAX_VALUE} + */ + public static long handleLargeFileSystem(long size) + { + return size < 0 ? Long.MAX_VALUE : size; + } /** @deprecated See CASSANDRA-16926 */ @Deprecated(since = "4.1") @@ -762,15 +833,15 @@ private FileUtils() * @param source the directory containing the files to move * @param target the directory where the files must be moved */ - public static void moveRecursively(Path source, Path target) throws IOException + public static void moveRecursively(File source, File target) throws IOException { logger.info("Moving {} to {}" , source, target); - if (Files.isDirectory(source)) + if (source.isDirectory()) { - Files.createDirectories(target); + target.tryCreateDirectories(); - for (File f : new File(source).tryList()) + for (File f : source.tryList()) { String fileName = f.name(); moveRecursively(source.resolve(fileName), target.resolve(fileName)); @@ -780,43 +851,62 @@ public static void moveRecursively(Path source, Path target) throws IOException } else { - if (Files.exists(target)) + if (target.exists()) { logger.warn("Cannot move the file {} to {} as the target file already exists." , source, target); } else { - Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES); - Files.delete(source); + source.copy(target, StandardCopyOption.COPY_ATTRIBUTES); + source.delete(); } } } + @VisibleForTesting + /** @deprecated See CNDB-1707 */ + @Deprecated(since = "5.0") + public static void moveRecursively(Path source, Path target) throws IOException + { + moveRecursively(new File(source), new File(target)); + } + /** * Deletes the specified directory if it is empty * - * @param path the path to the directory + * @param file the path to the directory */ - public static void deleteDirectoryIfEmpty(Path path) throws IOException + public static void deleteDirectoryIfEmpty(File file) throws IOException { - Preconditions.checkArgument(Files.isDirectory(path), String.format("%s is not a directory", path)); + Preconditions.checkArgument(file.isDirectory(), String.format("%s is not a directory", file)); try { - logger.info("Deleting directory {}", path); - Files.delete(path); + logger.info("Deleting directory {}", file); + Files.delete(file.toPath()); } catch (DirectoryNotEmptyException e) { - try (Stream paths = Files.list(path)) - { - String content = paths.map(p -> p.getFileName().toString()).collect(Collectors.joining(", ")); - - logger.warn("Cannot delete the directory {} as it is not empty. (Content: {})", path, content); - } + String content = Arrays.stream(file.tryList()).map(File::name).collect(Collectors.joining(", ")); + logger.warn("Cannot delete the directory {} as it is not empty. (Content: {})", file, content); } } + @VisibleForTesting + /** @deprecated See CNDB-1707 */ + @Deprecated(since = "5.0") + public static void deleteDirectoryIfEmpty(Path path) throws IOException + { + deleteDirectoryIfEmpty(new File(path)); + } + + /** @deprecated See CNDB-1707 */ + @Deprecated(since = "5.0") + public static long size(Path path) + { + return PathUtils.size(path); + } + public static int getBlockSize(File directory) { File f = FileUtils.createTempFile("block-size-test", ".tmp", directory); @@ -835,4 +925,114 @@ public static int getBlockSize(File directory) f.tryDelete(); } } -} \ No newline at end of file + + @DseLegacy + public static void deleteRecursive(Path dir) + { + deleteRecursive(dir, false); + } + + @DseLegacy + public static void deleteRecursive(Path dir, boolean failOnError) + { + if (failOnError) + { + PathUtils.deleteRecursive(dir); + } + else + { + PathUtils.deleteQuietly(dir); + } + } + + @DseLegacy + public static Path getPath(String pathOrURI) + { + return PathUtils.getPath(pathOrURI); + } + + @DseLegacy + public static void createDirectory(Path directory) + { + // yes, use create*Directories*, because that's the semantics of createDirectory in DSE + PathUtils.createDirectoriesIfNotExists(directory); + } + + @DseLegacy + public static void appendAndSync(Path file, String... lines) + { + appendAndSync(new File(file), lines); + } + + @DseLegacy + public static void delete(Path path) + { + PathUtils.delete(path); + } + + @DseLegacy + public static void deleteContent(Path path) + { + PathUtils.deleteContent(path); + } + + @DseLegacy + public static List listPaths(Path dir) + { + return PathUtils.listPaths(dir); + } + + @DseLegacy + public static List listPaths(Path dir, Predicate filter) + { + return PathUtils.listPaths(dir, filter); + } + + /** + * List paths that match this absolute path. + * + * This method is more efficient than {@link #listPaths(Path, Predicate)} if underlying file system can apply + * prefix filter earlier. + */ + public static List listPathsWithAbsolutePath(String absolutePath) + { + return listPathsWithAbsolutePath(FileUtils.getPath(absolutePath)); + } + + public static List listPathsWithAbsolutePath(Path absolutePath) + { + Path parent = absolutePath.getParent(); + String prefix = absolutePath.getFileName().toString(); + + return FileUtils.listPaths(parent, p -> FileUtils.fileNameMatchesPrefix(p.toString(), prefix)); + } + + /** + * Memory optimized, zero-copy version of the common {@code FileUtils.getFileName(path).startsW + * ith(prefix)} idiom. + * + * @param pathStr The path whose filename portion we want to match against. + * @param prefix The prefix to match. + * + * @return True if matching, false otherwise. + */ + public static boolean fileNameMatchesPrefix(String pathStr, String prefix) + { + int pathLen = pathStr.length(); + int prefixLen = prefix.length(); + if (pathLen == 0) + return false; + + int sepIndex = pathLen - 2; // Skip the separator if the strings ends with it + for (; sepIndex >= 0; sepIndex--) + if (pathStr.charAt(sepIndex) == '/') + break; + + return pathStr.regionMatches(false, // Linux is case-sensitive, so let's optimize for that + sepIndex >= 0 ? sepIndex + 1 : 0, + prefix, + 0, + prefixLen); + } + +} diff --git a/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java b/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java index bcbf2ef58a7e..5d5702ee58cf 100644 --- a/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java @@ -69,4 +69,4 @@ public String toString() { return "LimitingRebufferer[" + limiter + "]:" + wrapped; } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/io/util/Memory.java b/src/java/org/apache/cassandra/io/util/Memory.java index 1e6f6a215049..9be05dd76ab9 100644 --- a/src/java/org/apache/cassandra/io/util/Memory.java +++ b/src/java/org/apache/cassandra/io/util/Memory.java @@ -21,7 +21,6 @@ import java.nio.ByteBuffer; import net.nicoulaj.compilecommand.annotations.Inline; - import org.apache.cassandra.utils.FastByteOperations; import org.apache.cassandra.utils.concurrent.Ref; import org.apache.cassandra.utils.memory.LittleEndianMemoryUtil; @@ -217,7 +216,7 @@ public Memory copy(long newSize) public void free() { - if (peer != 0) MemoryUtil.free(peer); + if (peer != 0) MemoryUtil.free(peer, size); else assert size == 0; peer = 0; } @@ -286,4 +285,59 @@ protected static String toString(long peer, long size) { return String.format("Memory@[%x..%x)", peer, peer + size); } + + public static class LongArray implements AutoCloseable + { + public final Memory memory; + private final long size; + + public LongArray(long size) + { + assert size >= 0; + this.memory = size > 0 ? Memory.allocate(size << 3) : null; + this.size = size; + } + + public LongArray(SafeMemory memory, long cnt) + { + assert cnt <= memory.size >> 3; + this.memory = memory; + this.size = cnt; + } + + public void set(long offset, long value) + { + checkBounds(offset); + memory.setLong(offset << 3, value); + } + + public long get(long offset) + { + checkBounds(offset); + return memory.getLong(offset << 3); + } + + public long size() + { + return size; + } + + public long memoryUsed() + { + return memory != null ? memory.size() : 0; + } + + @Override + public void close() + { + if (memory != null) + memory.close(); + } + + private void checkBounds(long offset) + { + if (memory == null || offset < 0 || offset >= size) + throw new IndexOutOfBoundsException(); + } + } } diff --git a/src/java/org/apache/cassandra/io/util/MmapRebufferer.java b/src/java/org/apache/cassandra/io/util/MmapRebufferer.java index 884bc9718642..63246f46209d 100644 --- a/src/java/org/apache/cassandra/io/util/MmapRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/MmapRebufferer.java @@ -46,6 +46,18 @@ public Rebufferer instantiateRebufferer(boolean isScan) return this; } + @Override + public int chunkSize() + { + // We're not producing chunks + return -1; + } + + @Override + public void invalidateIfCached(long position) + { + } + @Override public void close() { diff --git a/src/java/org/apache/cassandra/io/util/MmappedRegions.java b/src/java/org/apache/cassandra/io/util/MmappedRegions.java index 5cbc65163ef8..e0b0e2750c21 100644 --- a/src/java/org/apache/cassandra/io/util/MmappedRegions.java +++ b/src/java/org/apache/cassandra/io/util/MmappedRegions.java @@ -19,8 +19,6 @@ package org.apache.cassandra.io.util; import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; @@ -28,20 +26,20 @@ import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.concurrent.RefCounted; import org.apache.cassandra.utils.concurrent.SharedCloseableImpl; import static java.util.stream.Stream.of; +import static org.apache.cassandra.config.CassandraRelevantProperties.MMAPPED_MAX_SEGMENT_SIZE_IN_MB; import static org.apache.cassandra.utils.Throwables.perform; public class MmappedRegions extends SharedCloseableImpl { - /** - * In a perfect world, MAX_SEGMENT_SIZE would be final, but we need to test with a smaller size - */ - public static int MAX_SEGMENT_SIZE = Integer.MAX_VALUE; + /** In a perfect world, MAX_SEGMENT_SIZE would be final, but we need to test with a smaller size */ + public static int MAX_SEGMENT_SIZE = MMAPPED_MAX_SEGMENT_SIZE_IN_MB.getInt(Integer.MAX_VALUE); /** * When we need to grow the arrays, we add this number of region slots @@ -65,22 +63,21 @@ public class MmappedRegions extends SharedCloseableImpl */ private volatile State copy; - private MmappedRegions(State state, long length, int chunkSize) + private MmappedRegions(State state, CompressionMetadata metadata, long uncompressedSliceOffset) { super(new Tidier(state)); this.state = state; - if (length > 0) - { - updateState(length, chunkSize); - } + updateState(metadata, uncompressedSliceOffset); this.copy = new State(state); } - private MmappedRegions(State state, CompressionMetadata metadata) + private MmappedRegions(State state, long length, int chunkSize) { super(new Tidier(state)); this.state = state; - updateState(metadata); + if (length > 0) + updateState(length, chunkSize); + this.copy = new State(state); } @@ -92,27 +89,34 @@ private MmappedRegions(MmappedRegions original) public static MmappedRegions empty(ChannelProxy channel) { - return new MmappedRegions(new State(channel), 0, 0); + return new MmappedRegions(new State(channel, 0, false), 0, 0); } /** - * @param channel file to map. the MmappedRegions instance will hold shared copy of given channel. - * @param metadata + * Create memory mapped regions for the given compressed file. + * + * @param channel file to map. The {@link MmappedRegions} instance will hold shared copy of given channel. + * @param metadata compression metadata for the mapped file, cannot be null. A shared copy of the metadata is not + * created, so it needs to me managed by the caller. + * @param uncompressedSliceOffset if the file represents a slice of the origial file, this is the offset of the slice in + * the original file (in uncompressed data), namely the value of {@link SliceDescriptor#sliceStart}. + * @param adviseRandom whether to apply MADV_RANDOM to mapped regions * @return new instance */ - public static MmappedRegions map(ChannelProxy channel, CompressionMetadata metadata) + public static MmappedRegions map(ChannelProxy channel, CompressionMetadata metadata, long uncompressedSliceOffset, boolean adviseRandom) { if (metadata == null) throw new IllegalArgumentException("metadata cannot be null"); - State state = new State(channel); - return new MmappedRegions(state, metadata); + State state = new State(channel, metadata.chunkFor(uncompressedSliceOffset).offset, adviseRandom); + return new MmappedRegions(state, metadata, uncompressedSliceOffset); } - public static MmappedRegions map(ChannelProxy channel, long length, int chunkSize) + public static MmappedRegions map(ChannelProxy channel, long length, int chunkSize, long uncompressedSliceOffset, boolean adviseRandom) { if (length <= 0) throw new IllegalArgumentException("Length must be positive"); - State state = new State(channel); + + State state = new State(channel, uncompressedSliceOffset, adviseRandom); return new MmappedRegions(state, length, chunkSize); } @@ -132,10 +136,8 @@ private boolean isCopy() /** * Extends this collection of mmapped regions up to the provided total length. - * - * @return {@code true} if new regions have been created */ - public boolean extend(long length, int chunkSize) + public void extend(long length, int chunkSize) { // We cannot enforce length to be a multiple of chunkSize (at the very least the last extend on a file // will not satisfy this), so we hope the caller knows what they are doing. @@ -145,12 +147,10 @@ public boolean extend(long length, int chunkSize) assert !isCopy() : "Copies cannot be extended"; if (length <= state.length) - return false; + return; - int initialRegions = state.last; updateState(length, chunkSize); copy = new State(state); - return state.last > initialRegions; } /** @@ -159,7 +159,7 @@ public boolean extend(long length, int chunkSize) * * @return {@code true} if new regions have been created */ - public boolean extend(CompressionMetadata compressionMetadata, int chunkSize) + public boolean extend(CompressionMetadata compressionMetadata, int chunkSize, long uncompressedSliceOffset) { assert !isCopy() : "Copies cannot be extended"; @@ -170,7 +170,7 @@ public boolean extend(CompressionMetadata compressionMetadata, int chunkSize) if (compressionMetadata.compressedFileLength - state.length <= MAX_SEGMENT_SIZE) updateState(compressionMetadata.compressedFileLength, chunkSize); else - updateState(compressionMetadata); + updateState(compressionMetadata, uncompressedSliceOffset); copy = new State(state); return state.last > initialRegions; @@ -194,35 +194,38 @@ private void updateState(long length, int chunkSize) } } - private void updateState(CompressionMetadata metadata) + private void updateState(CompressionMetadata metadata, long uncompressedSliceOffset) { - long lastSegmentOffset = state.getPosition(); - long offset = metadata.getDataOffsetForChunkOffset(lastSegmentOffset); + long uncompressedPosition = metadata.getDataOffsetForChunkOffset(state.getPosition()); // uncompressed position of the current compressed chunk in the original (compressed) file + long compressedPosition = state.getPosition(); // position on disk of the current compressed chunk in the original (compressed) file long segmentSize = 0; - while (offset < metadata.dataLength) + assert metadata.chunkFor(uncompressedPosition).offset == compressedPosition; + + while (uncompressedPosition - uncompressedSliceOffset < metadata.dataLength) { - CompressionMetadata.Chunk chunk = metadata.chunkFor(offset); + // chunk contains the position on disk in the original file + CompressionMetadata.Chunk chunk = metadata.chunkFor(uncompressedPosition); //Reached a new mmap boundary if (segmentSize + chunk.length + 4 > MAX_SEGMENT_SIZE) { if (segmentSize > 0) { - state.add(lastSegmentOffset, segmentSize); - lastSegmentOffset += segmentSize; + state.add(compressedPosition, segmentSize); + compressedPosition += segmentSize; segmentSize = 0; } } - segmentSize += chunk.length + 4; //checksum - offset += metadata.chunkLength(); + segmentSize += chunk.length + 4; // compressed size of the chunk including 4 bytes of checksum + uncompressedPosition += metadata.chunkLength(); // uncompressed size of the chunk } if (segmentSize > 0) - state.add(lastSegmentOffset, segmentSize); + state.add(compressedPosition, segmentSize); - state.length = lastSegmentOffset + segmentSize; + state.length = compressedPosition + segmentSize; } public boolean isValid(ChannelProxy channel) @@ -235,6 +238,11 @@ public boolean isEmpty() return state.isEmpty(); } + /** + * Get the region containing the given position + * + * @param position the position on disk (not in the uncompressed data) in the original file (not in the slice) + */ public Region floor(long position) { assert !isCleanedUp() : "Attempted to use closed region"; @@ -264,27 +272,11 @@ public Region(long offset, ByteBuffer buffer) this.buffer = buffer; } - @Override public ByteBuffer buffer() { return buffer.duplicate(); } - @Override - public FloatBuffer floatBuffer() - { - // this does an implicit duplicate(), so we need to expose it directly to avoid doing it twice unnecessarily - return buffer.asFloatBuffer(); - } - - @Override - public IntBuffer intBuffer() - { - // this does an implicit duplicate(), so we need to expose it directly to avoid doing it twice unnecessarily - return buffer.asIntBuffer(); - } - - @Override public long offset() { return offset; @@ -295,7 +287,6 @@ public long end() return offset + buffer.capacity(); } - @Override public void release() { // only released after no readers are present @@ -329,22 +320,33 @@ private static final class State */ private int last; - private State(ChannelProxy channel) + /** The position of the first region of the slice in the original file (if the file is compressed, the offset + * refers to position on disk, not the uncompressed data) */ + private final long onDiskSliceOffset; + + /** whether to apply fadv_random to mapped regions */ + private final boolean adviseRandom; + + private State(ChannelProxy channel, long onDiskSliceOffset, boolean adviseRandom) { this.channel = channel.sharedCopy(); + this.adviseRandom = adviseRandom; this.buffers = new ByteBuffer[REGION_ALLOC_SIZE]; this.offsets = new long[REGION_ALLOC_SIZE]; this.length = 0; this.last = -1; + this.onDiskSliceOffset = onDiskSliceOffset; } private State(State original) { this.channel = original.channel; + this.adviseRandom = original.adviseRandom; this.buffers = original.buffers; this.offsets = original.offsets; this.length = original.length; this.last = original.last; + this.onDiskSliceOffset = original.onDiskSliceOffset; } private boolean isEmpty() @@ -354,12 +356,13 @@ private boolean isEmpty() private boolean isValid(ChannelProxy channel) { + // todo maybe extend validation to verify slice offset? return this.channel.filePath().equals(channel.filePath()); } private Region floor(long position) { - assert 0 <= position && position <= length : String.format("%d > %d", position, length); + assert onDiskSliceOffset <= position && position <= length : String.format("%d > %d", position, length); int idx = Arrays.binarySearch(offsets, 0, last + 1, position); assert idx != -1 : String.format("Bad position %d for regions %s, last %d in %s", position, Arrays.toString(offsets), last, channel); @@ -371,12 +374,30 @@ private Region floor(long position) private long getPosition() { - return last < 0 ? 0 : offsets[last] + buffers[last].capacity(); + return last < 0 ? onDiskSliceOffset : offsets[last] + buffers[last].capacity(); } + /** + * Add a new region to the state + * @param pos the position on disk (not in the uncompressed data) in the original file (not the slice) + * @param size the size of the region + */ private void add(long pos, long size) { - ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, pos, size); + // For encrypted files, ensure we don't try to map beyond the actual file size + long mappingOffset = pos - onDiskSliceOffset; + long channelSize = channel.size(); + if (mappingOffset + size > channelSize) { + // Adjust size to not exceed channel size + size = Math.max(0, channelSize - mappingOffset); + if (size == 0) { + return; // Nothing to map + } + } + + var buffer = channel.map(FileChannel.MapMode.READ_ONLY, mappingOffset, size); + if (adviseRandom) + INativeLibrary.instance.adviseRandom(buffer, size, channel.filePath()); ++last; diff --git a/src/java/org/apache/cassandra/io/util/MmappedRegionsCache.java b/src/java/org/apache/cassandra/io/util/MmappedRegionsCache.java index e3ebc34609d1..3fd5e421a64a 100644 --- a/src/java/org/apache/cassandra/io/util/MmappedRegionsCache.java +++ b/src/java/org/apache/cassandra/io/util/MmappedRegionsCache.java @@ -45,10 +45,10 @@ public class MmappedRegionsCache implements AutoCloseable * @param length length of the file * @return a shared copy of the cached mmapped regions */ - public MmappedRegions getOrCreate(ChannelProxy channel, long length, int bufferSize) + public MmappedRegions getOrCreate(ChannelProxy channel, long length, int bufferSize, long uncompressedSliceOffset) { Preconditions.checkState(!closed); - MmappedRegions regions = cache.computeIfAbsent(channel.file(), ignored -> MmappedRegions.map(channel, length, bufferSize)); + MmappedRegions regions = cache.computeIfAbsent(channel.file(), ignored -> MmappedRegions.map(channel, length, bufferSize, uncompressedSliceOffset, false)); Preconditions.checkArgument(regions.isValid(channel)); regions.extend(length, bufferSize); return regions.sharedCopy(); @@ -62,12 +62,12 @@ public MmappedRegions getOrCreate(ChannelProxy channel, long length, int bufferS * @param metadata compression metadata of the file * @return a shared copy of the cached mmapped regions */ - public MmappedRegions getOrCreate(ChannelProxy channel, CompressionMetadata metadata, int bufferSize) + public MmappedRegions getOrCreate(ChannelProxy channel, CompressionMetadata metadata, int bufferSize, long uncompressedSliceOffset) { Preconditions.checkState(!closed); - MmappedRegions regions = cache.computeIfAbsent(channel.file(), ignored -> MmappedRegions.map(channel, metadata)); + MmappedRegions regions = cache.computeIfAbsent(channel.file(), ignored -> MmappedRegions.map(channel, metadata, uncompressedSliceOffset, false)); Preconditions.checkArgument(regions.isValid(channel)); - regions.extend(metadata, bufferSize); + regions.extend(metadata, bufferSize, uncompressedSliceOffset); return regions.sharedCopy(); } diff --git a/src/java/org/apache/cassandra/io/util/PathUtils.java b/src/java/org/apache/cassandra/io/util/PathUtils.java index 8ddd939b4c09..7282373db7cd 100644 --- a/src/java/org/apache/cassandra/io/util/PathUtils.java +++ b/src/java/org/apache/cassandra/io/util/PathUtils.java @@ -23,12 +23,15 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.UncheckedIOException; +import java.net.URI; import java.nio.channels.FileChannel; +import java.nio.file.*; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; @@ -47,12 +50,14 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.RateLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +67,7 @@ import org.apache.cassandra.io.FSError; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.utils.NoSpamLogger; import static java.nio.file.StandardOpenOption.APPEND; @@ -70,7 +76,6 @@ import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; import static java.util.Collections.unmodifiableSet; -import static org.apache.cassandra.config.CassandraRelevantProperties.USE_NIX_RECURSIVE_DELETE; import static org.apache.cassandra.utils.Throwables.merge; /** @@ -78,7 +83,7 @@ * * This class tries to apply uniform IOException handling, and does not propagate IOException except for NoSuchFileException. * Any harmless/application error exceptions are propagated as UncheckedIOException, and anything else as an FSReadError or FSWriteError. - * Semantically this is a little incoherent throughout the codebase, as we intercept IOException haphazardly and treaat + * Semantically this is a little incoherent throughout the codebase, as we intercept IOException haphazardly and treat * it inconsistently - we should ideally migrate to using {@link #propagate(IOException, Path, boolean)} et al globally. */ public final class PathUtils @@ -94,6 +99,17 @@ public final class PathUtils private static final Logger logger = LoggerFactory.getLogger(PathUtils.class); private static final NoSpamLogger nospam1m = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + private static final boolean USE_NIX_RECURSIVE_DELETE = CassandraRelevantProperties.USE_NIX_RECURSIVE_DELETE.getBoolean(); + + private static final CopyOption[] ATOMIC_MOVE_OPTIONS = { + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE + }; + + private static final CopyOption[] REPLACE_EXISTING_OPTIONS = { + StandardCopyOption.REPLACE_EXISTING + }; + private static Consumer onDeletion = path -> {}; public static FileChannel newReadChannel(Path path) throws NoSuchFileException @@ -388,7 +404,7 @@ private static void deleteRecursiveUsingNixCommand(Path path, boolean quietly) */ public static void deleteRecursive(Path path) { - if (USE_NIX_RECURSIVE_DELETE.getBoolean() && path.getFileSystem() == java.nio.file.FileSystems.getDefault()) + if (USE_NIX_RECURSIVE_DELETE && path.getFileSystem() == FileSystems.getDefault()) { deleteRecursiveUsingNixCommand(path, false); return; @@ -401,6 +417,26 @@ public static void deleteRecursive(Path path) delete(path); } + /** + * Deletes all files and subdirectories under "path", + * ignoring IOExceptions along the way. + * @param path file to be deleted + */ + public static void deleteQuietly(Path path) + { + if (USE_NIX_RECURSIVE_DELETE && path.getFileSystem() == FileSystems.getDefault()) + { + deleteRecursiveUsingNixCommand(path, true); + return; + } + + if (isDirectory(path)) + forEach(path, PathUtils::deleteQuietly); + + // The directory is now empty so now it can be smoked + tryDelete(path); + } + /** * Deletes all files and subdirectories under "path". * @param path file to be deleted @@ -408,7 +444,7 @@ public static void deleteRecursive(Path path) */ public static void deleteRecursive(Path path, RateLimiter rateLimiter) { - if (USE_NIX_RECURSIVE_DELETE.getBoolean() && path.getFileSystem() == java.nio.file.FileSystems.getDefault()) + if (USE_NIX_RECURSIVE_DELETE && path.getFileSystem() == java.nio.file.FileSystems.getDefault()) { deleteRecursiveUsingNixCommand(path, false); return; @@ -431,6 +467,49 @@ private static void deleteRecursive(Path path, RateLimiter rateLimiter, Consumer delete(path, rateLimiter); } + /** + * Recursively delete the content of the directory, but not the directory itself. + * @param dirPath directory for which content should be deleted + */ + public static void deleteContent(Path dirPath) + { + if (isDirectory(dirPath)) + forEach(dirPath, PathUtils::deleteRecursive); + } + + /** + * List all paths in this directory + * @param dirPath directory for which to list all paths + * @return list of all paths contained in the given directory + */ + public static List listPaths(Path dirPath) + { + return listPaths(dirPath, p -> true); + } + + /** + * List paths in this directory that match the filter + * @param dirPath directory for which to list all paths matching the given filter + * @param filter predicate used to filter paths + * @return filtered list of paths contained in the given directory + */ + public static List listPaths(Path dirPath, Predicate filter) + { + try (Stream stream = Files.list(dirPath)) + { + return (consistentDirectoryListings ? stream.sorted() : stream).filter(filter).collect(Collectors.toList()); + } + catch(NotDirectoryException | NoSuchFileException ex) + { + // Don't throw if the file does not exist or is not a directory + return ImmutableList.of(); + } + catch(IOException ex) + { + throw new FSReadError(ex, dirPath); + } + } + /** * Schedules deletion of all file and subdirectories under "dir" on JVM shutdown. * @param dir Directory to be deleted @@ -454,7 +533,7 @@ public static boolean tryRename(Path from, Path to) logger.trace("Renaming {} to {}", from, to); try { - atomicMoveWithFallback(from, to); + atomicMoveWithFallback(StorageProvider.instance.getLocalPath(from), StorageProvider.instance.getLocalPath(to)); return true; } catch (IOException e) @@ -469,7 +548,7 @@ public static void rename(Path from, Path to) logger.trace("Renaming {} to {}", from, to); try { - atomicMoveWithFallback(from, to); + atomicMoveWithFallback(StorageProvider.instance.getLocalPath(from), StorageProvider.instance.getLocalPath(to)); } catch (IOException e) { @@ -487,12 +566,28 @@ private static void atomicMoveWithFallback(Path from, Path to) throws IOExceptio { try { - Files.move(from, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.move(from, to, ATOMIC_MOVE_OPTIONS); } catch (AtomicMoveNotSupportedException e) { logger.trace("Could not do an atomic move", e); - Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); + Files.move(from, to, REPLACE_EXISTING_OPTIONS); + } + } + + /** + * Copy a file to a target file + */ + public static void copy(Path from, Path to, StandardCopyOption option) + { + logger.trace("Copying {} to {}", from, to); + try + { + Files.copy(from, to, option); + } + catch (IOException e) + { + throw new RuntimeException(String.format("Failed to copy %s to %s", from, to), e); } } @@ -599,6 +694,41 @@ public static Path toCanonicalPath(Path file) return toRealPath(parent).resolve(parent.relativize(file)); } + /** + * @param path to check file szie + * @return file size or 0 if failed to get file size + */ + public static long size(Path path) + { + try + { + return Files.size(path); + } + catch (IOException e) + { + // it's possible that between the time that the caller has checked if the file exists and the time it retrieves the creation time, + // the file is actually deleted. File.length() returns a positive value only if the file is valid, otherwise it returns 0L, here + // we do the same + return 0; + } + } + + /** + * @param pathOrURI path or uri in string + * @return nio Path + */ + public static Path getPath(String pathOrURI) + { + try + { + return Paths.get(URI.create(pathOrURI)); + } + catch (IllegalArgumentException ex) + { + return Paths.get(pathOrURI); + } + } + private static Path toRealPath(Path path) { try diff --git a/src/java/org/apache/cassandra/io/util/PrefetchingRebufferer.java b/src/java/org/apache/cassandra/io/util/PrefetchingRebufferer.java new file mode 100644 index 000000000000..1ee925344fe4 --- /dev/null +++ b/src/java/org/apache/cassandra/io/util/PrefetchingRebufferer.java @@ -0,0 +1,535 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.util; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.CompletableFuture; // checkstyle: permit this import +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Meter; +import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.metrics.DefaultNameFactory; +import org.apache.cassandra.metrics.MetricNameFactory; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.Throwables; + +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +/// A rebufferer that prefetches the next N buffers in sequential order. +/// +/// *Not* thread-safe (does not need to be as readers aren't). +public class PrefetchingRebufferer implements Rebufferer +{ + private static final Logger LOGGER = LoggerFactory.getLogger(PrefetchingRebufferer.class); + private static final NoSpamLogger NO_SPAM_LOGGER = NoSpamLogger.getLogger(LOGGER, 1, TimeUnit.MINUTES); + + private static final int PREFETCHING_SIZE_KB = CassandraRelevantProperties.READ_PREFETCHING_SIZE_KB.getInt(); + private static final double PREFETCHING_WINDOW = CassandraRelevantProperties.READ_PREFETCHING_WINDOW.getDouble(); + private static final int PREFETCHING_THREADS = CassandraRelevantProperties.READ_PREFETCHING_THREADS.getInt(FBUtilities.getAvailableProcessors()); + + private static final boolean ENABLED = PREFETCHING_SIZE_KB > 0; + + @VisibleForTesting + public static final PrefetchingMetrics metrics; + + private static final ExecutorPlus executor; + + static + { + if (ENABLED) + { + // Technically, we re-validate the window in the ctor for good measure, but in practice, not point in waiting + // until the first prefetching read happens before erroring if the configuration is incorrect. + Preconditions.checkArgument(PREFETCHING_WINDOW >= 0 && PREFETCHING_WINDOW <= 1, "Invalid prefetching window value: %s", PREFETCHING_WINDOW); + Preconditions.checkArgument(PREFETCHING_THREADS > 0, "Invalid prefetching threads: %s", PREFETCHING_THREADS); + + LOGGER.info("Prefetching is enabled for sequential reads (e.g. range queries, compactions); size={}kb and window={}", PREFETCHING_SIZE_KB, PREFETCHING_WINDOW); + + metrics = new PrefetchingMetrics(); + executor = executorFactory().withJmxInternal().pooled("ReadPrefetching", PREFETCHING_THREADS); + } + else + { + metrics = null; + executor = null; + } + } + + /// Rebufferer factory on top of which prefetching is applied. + private final RebuffererFactory source; + + /// Number or [Rebufferer] we're already created. We only create up to [#prefetchSize] + 1 rebufferer and then reuse + /// them, but we instantiate them lazily so this tell use if we have created our max already. + private int createdRebuffers; + + /// The rebufferer that generated the last [BufferHolder] returned by [#rebuffer]. We cannot reuse that rebufferer + /// until the next call to [#rebuffer], because only then can we assume the buffer was released. This rebufferer is + /// why we create [#prefetchSize] + 1 rebufferers. + private ReusedRebufferer lastReturnedRebufferer; + + /// As mentioned above, we reuse rebufferer (to save allocations) and this queue contains rebufferers that have + /// been created but are not currently used by a [PrefetchedEntry] in [#queue]. + private final Deque unusedRebufferers; + + /// The buffers that are being/have been prefetched (but have not yet be requested by [#rebuffer]). + private final Deque queue; + + /// The number of buffers prefetched when prefetch is triggered (). + private final int prefetchSize; + + /// The minimum number of buffers that should be prefetched; as soon as we have less than this number of prefetched + /// buffers, we fetch up to [#prefetchSize]. + private final int windowSize; + + /** We expect the buffer size to be a power of 2, this is the mask for aligning to the buffer size */ + private final int alignmentMask; + + private PrefetchingRebufferer(RebuffererFactory source) + { + this(source, PREFETCHING_SIZE_KB * 1024, PREFETCHING_WINDOW); + } + + @VisibleForTesting + PrefetchingRebufferer(RebuffererFactory source, int prefetchingSize, double window) + { + assert Integer.bitCount(source.chunkSize()) == 1 : String.format("%d must be a power of two", source.chunkSize()); + assert prefetchingSize > 0 : String.format("prefetching size %d must be > 0", prefetchingSize); + assert window >= 0 && window <= 1 : String.format("prefetching window %f must be in [0, 1]", window); + + this.source = source; + this.prefetchSize = (int)Math.ceil((double)prefetchingSize / source.chunkSize()); + this.windowSize = (int)Math.ceil(window * prefetchSize); + this.unusedRebufferers = new ArrayDeque<>(prefetchSize); + this.queue = new ArrayDeque<>(prefetchSize); + this.alignmentMask = -source.chunkSize(); + } + + public static Rebufferer withPrefetching(RebuffererFactory factory) + { + if (!ENABLED) + return factory.instantiateRebufferer(false); + + int chunkSize = factory.chunkSize(); + // No `chunkSize` typically means mmap, where prefetching doesn't make sense anyway (the OS already does it) + if (chunkSize <= 0) + return factory.instantiateRebufferer(false); + + if (Integer.bitCount(chunkSize) != 1) + { + NO_SPAM_LOGGER.debug("Prefecting is enabled for SEQUENTIAL reads on {} but the chunk size {} is not a " + + "power of two (should only true for zero-copied files); will not prefetch.", + factory.channel().filePath(), chunkSize); + return factory.instantiateRebufferer(false); + } + + return new PrefetchingRebufferer(factory); + } + + private int chunkSize() + { + return source.chunkSize(); + } + + @Override + public BufferHolder rebuffer(long position) + { + // Only now do we know it is safe to reuse the rebufferer from the previous call to this method. + if (lastReturnedRebufferer != null) + unusedRebufferers.add(lastReturnedRebufferer); + + if (position >= fileLength()) + { + assert position == fileLength() : "Requested seek to " + position + " but file length is " + fileLength(); + return Rebufferer.EMPTY; + } + + long pageAlignedPos = position & alignmentMask; + + PrefetchedEntry entry = queue.poll(); + boolean isNonSequential = false; + + // Release any prefetched buffers that are before the requested position. + while (entry != null && entry.position < pageAlignedPos) + { + isNonSequential = true; + unusedRebufferers.add(entry.release()); + entry = queue.poll(); + } + + // If the next entry matches, use it, but also trigger futher prefetch if necessary. + if (entry != null && entry.position == pageAlignedPos) + { + prefetch(pageAlignedPos + chunkSize()); + + if (!entry.isReady()) + metrics.notReady.mark(); + + BufferHolder holder = entry.get(); + lastReturnedRebufferer = entry.forReuse(); + return holder; + } + + // We get here in 2 cases: + // 1. `entry == null`: this is either the first call to this rebufferer, or we've seeked forward since the + // last `rebuffer` and all the prefected entries where discared by the loop above. + // 2. `entry.position > pageAlignedPos` (we have prefected entries, but they are "later" in the file). This + // means the code has jumped backward since the last `rebuffer`. We don't want our prefetching queue to grow + // unbounded if we get a series of backward seek, so we need to release those prefecth, at least if they + // don't fall within the "current" prefetching window. But for now, we simply release all prefetched entries + // because: + // - if this is used for a genuinely sequential process, like compaction/scrub, then we shouldn't seek + // backward at all, and if we do it's exceptional, to handle some problem/retry, and being optimal in those + // rare case is not a priority. + // - if this is used for actual user reads, then this rebuffer should sit on top of the chunk cache anyway, + // and so those prefetched entries will still be in the cache even if we remove them from our own queue, and + // we're not really losing anything (arguably the queue of this rebuffer is a tad superfluous when we sit + // on top of the chunk cache for that reason, but that queue exists mainly for when that's not the case). + // In both cases, we ensure the prefecth queue is empty, then prefetch from the current position, and wait on + // the first entry. + while (entry != null) + { + isNonSequential = true; + unusedRebufferers.add(entry.release()); + entry = queue.poll(); + } + prefetch(pageAlignedPos); + + if (isNonSequential) + metrics.nonSequentialRequest.mark(); + + entry = queue.poll(); + assert entry != null; // the call to `prefetch` should ensure this. + + // We call prefetch for the next entry again because we just pulled an entry so we may have to restore our + // correct number of prefetched. But depending on the window, this will probably not do anything. + // We do this before waiting on our current entry to trigger prefetch as soon as possible. + prefetch(pageAlignedPos + chunkSize()); + + // Note that we don't increment the `notReady` metric here, because we just triggered the read, so we know + // it's not going to be ready (unless we're extremely lucky) and if this is due to prior seek, we're already + // recorded it (and if it's just the first call to `rebuffer`, it's normal, we don't need to track it). + BufferHolder holder = entry.get(); + lastReturnedRebufferer = entry.forReuse(); + + return holder; + } + + /// Trigger prefetches of the next N buffers unless they are already in the queue. + /// + /// This method does not block, it just submits prefetch to the executor and then return immediately. + /// + /// @param pageAlignedPosition the position in the file, already aligned to a page + private void prefetch(long pageAlignedPosition) + { + // see caller, prefetch is only called with an empty queue or if the requested position is exactly at the + // beginning of the queue after purging all older entries + assert queue.isEmpty() || pageAlignedPosition == queue.peekFirst().position : + String.format("Unexpected prefetching position %d, first: %s, last: %s", pageAlignedPosition, queue.peekFirst(), queue.peekLast()); + + // Only trigger prefetch if we have less than our window already prefetech. + if (queue.size() > windowSize) + return; + + long firstPositionToPrefetch = queue.isEmpty() ? pageAlignedPosition : queue.peekLast().position + chunkSize(); + int toPrefetch = prefetchSize - queue.size(); + + // We trigger all the prefetch on the executor, and so in parallel (within the constraint of the executor). + for (int i = 0; i < toPrefetch; i++) + { + long prefetchPosition = firstPositionToPrefetch + ((long)i * chunkSize()); + if (prefetchPosition >= source.fileLength()) + break; + + ReusedRebufferer rebufferer = unusedRebufferers.poll(); + // The +1 is because we also have `lastReturnedRebufferer` on top of what is prefetched + if (rebufferer == null && createdRebuffers < prefetchSize + 1) + { + rebufferer = ReusedRebufferer.create(source); + ++createdRebuffers; + } + // We must have gotten a rebufferer: we know the queue is smaller than `prefetchSize`, so either we had + // created less than `prefetchSize` rebuffererer yet, and we just created one above, or some rebufferer must + // have been unused. + assert rebufferer != null; + PrefetchedEntry newEntry = new PrefetchedEntry(prefetchPosition, rebufferer); + queue.addLast(newEntry); + + CompletableFuture prevUseFuture = rebufferer.prevUseFuture; + // If `prevUseFuture` is not null, this effectively means that the rebufferer previous usage was for a + // prefetched entry we didn't wait on before release (we discarded the entry). But even though we didn't + // need the prefetched value, the prefetching was triggered, and we need to make certain it is done before + // we reuse the underlying buffer. + executor.execute(() -> { + if (prevUseFuture != null) + { + try + { + prevUseFuture.join(); + } + catch (Exception e) + { + // We ignore expections here because it has already been handling (_including_ being logged + // within `PrefetchedEntry#release`. + } + } + newEntry.triggerPrefetch(); + }); + } + } + + @Override + public ChannelProxy channel() + { + return source.channel(); + } + + @Override + public long fileLength() + { + return source.fileLength(); + } + + @Override + public double getCrcCheckChance() + { + return source.getCrcCheckChance(); + } + + @Override + public long adjustPosition(long position) + { + return position; + } + + @Override + public void close() + { + assert unusedRebufferers.isEmpty() : "buffers should have been released"; + assert queue.isEmpty() : "Prefetched buffers should have been released"; + source.close(); + } + + @Override + public void closeReader() + { + // First, release any inflight prefetch (moving the rebuffer to `unusedRebufferers` temporarily) + queue.forEach(entry -> unusedRebufferers.add(entry.release())); + queue.clear(); + + unusedRebufferers.forEach(r -> { + if (r.prevUseFuture == null) + r.rebufferer.closeReader(); + else + r.prevUseFuture.whenComplete((_1, _2) -> r.rebufferer.closeReader()); + }); + unusedRebufferers.clear(); + } + + @Override + public String toString() + { + return String.format("Prefetching rebufferer: (%d/%d) buffers, %d buffer size", prefetchSize, windowSize, chunkSize()); + } + + /// Represents an ongoing or completed prefetching of one "chunk" (buffed) + private static final class PrefetchedEntry + { + private final long position; + private final ReusedRebufferer rebufferer; + private final CompletableFuture future; + private boolean reused; + + PrefetchedEntry(long position, ReusedRebufferer rebufferer) + { + this.position = position; + this.rebufferer = rebufferer; + this.future = new CompletableFuture<>(); + + metrics.prefetched.mark(); + } + + /// Called on an [PrefetchingRebufferer#executor] to do the actual (blocking) prefetching. + /// This is the only method of this class that is not executed on the thread of the [PrefetchingRebufferer] + /// that creates it (meaning, the thread on which the [PrefetchingRebufferer#rebuffer(long)] method is called). + void triggerPrefetch() + { + try + { + future.complete(rebufferer.rebuffer(position)); + } + catch (Exception e) + { + future.completeExceptionally(e); + } + } + + /// Must only be called after a successfull [#get] (when the prefetched entry has been consumed and the + /// buffer returned); returns a [ReusedRebufferer] ready for reuse. + ReusedRebufferer forReuse() + { + assert !reused; + assert isReady() : "Should not have been called on incomplete prefetch"; + reused = true; + return rebufferer.prepareForReuse(null); + } + + /// Called when the entry is discarded, to release the underlying buffer once the prefetch complete (since we + /// won't use the result of that prefetch, we need to release the buffer ourselves). + ReusedRebufferer release() + { + // We should call one of `forReuse` or this for each entry, but only one, or we risk putting the same + // rebufferer in `unusedRebufferers` twice, which would be a problem + assert !reused; + reused = true; + + return rebufferer.prepareForReuse(future.whenComplete((buffer, error) -> { + try + { + if (buffer != null) + { + buffer.release(); + metrics.unused.mark(); + } + + // We shouldn't fail, but we're also explicitly not using the result of that prefetch, so no reason + // to do more than warn. + if (error != null) + LOGGER.warn("Error during prefetching; but this prefetch is discarded", error); + } + catch (Throwable t) + { + // Erroring during release "could" be more problematic, so log a genuine error + LOGGER.error("Failed to release (discarded) prefetched buffer", t); + } + })); + } + + boolean isReady() + { + return future.isDone(); + } + + BufferHolder get() + { + try + { + return future.join(); + } + catch (Throwable t) + { + // Remove `CompletionException` and any other wrapping as code upstream may not expect them. + throw Throwables.cleaned(t); + } + } + + @Override + public String toString() + { + return String.format("Position: %d, Done: %s", position, future.isDone()); + } + } + + private static class ReusedRebufferer + { + private final @Nullable CompletableFuture prevUseFuture; + private final Rebufferer rebufferer; + + private ReusedRebufferer(@Nullable CompletableFuture prevUseFuture, Rebufferer rebufferer) + { + this.prevUseFuture = prevUseFuture; + this.rebufferer = rebufferer; + } + + static ReusedRebufferer create(RebuffererFactory factory) + { + return new ReusedRebufferer(null, factory.instantiateRebufferer(false)); + } + + BufferHolder rebuffer(long position) + { + return rebufferer.rebuffer(position); + } + + ReusedRebufferer prepareForReuse(@Nullable CompletableFuture lastUseFuture) + { + return new ReusedRebufferer(lastUseFuture, rebufferer); + } + } + + @VisibleForTesting + public static class PrefetchingMetrics + { + /// Total number of buffers that were prefetched. + final Meter prefetched; + + /// Total number of buffers that were prefetched but were not used (either due to a non-sequential access, or + /// to the rebufferer being closed before the end of the underlying file). + final Meter unused; + + /// Total number of buffers that were prefetched but were not ready when they were requested and the caller + /// had to wait. A large number of those means that the configuration of pre-fetching should be adjusted. + final Meter notReady; + + /// Number of times a [PrefetchingRebufferer#rebuffer] call was not respecting sequential acess (meaning the + /// position requested was not exactly for the chunk following the previous call) + final Meter nonSequentialRequest; + + PrefetchingMetrics() + { + MetricNameFactory factory = new DefaultNameFactory("ReadPrefetching", ""); + prefetched = Metrics.meter(factory.createMetricName("Prefetched")); + unused = Metrics.meter(factory.createMetricName("Unused")); + notReady = Metrics.meter(factory.createMetricName("NotReady")); + nonSequentialRequest = Metrics.meter(factory.createMetricName("NonSequentialRequest")); + } + + @VisibleForTesting + void reset() + { + prefetched.mark(-prefetched.getCount()); + unused.mark(-unused.getCount()); + notReady.mark(-notReady.getCount()); + nonSequentialRequest.mark(-nonSequentialRequest.getCount()); + } + + @Override + public String toString() + { + if (prefetched.getCount() == 0) + return "No prefetching yet"; + + return String.format("Prefetched: [%s], Unused: [%s] (%.2f), Not ready: [%s] (%.2f), Non sequential request: [%s]", + prefetched.getCount(), + unused.getCount(), (double) unused.getCount() / prefetched.getCount(), + notReady.getCount(), (double) notReady.getCount() / prefetched.getCount(), + nonSequentialRequest.getCount()); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/io/util/RandomAccessReader.java b/src/java/org/apache/cassandra/io/util/RandomAccessReader.java index b89e59eb5291..c45bc78d322a 100644 --- a/src/java/org/apache/cassandra/io/util/RandomAccessReader.java +++ b/src/java/org/apache/cassandra/io/util/RandomAccessReader.java @@ -18,8 +18,11 @@ package org.apache.cassandra.io.util; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.ByteOrder; - +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.LongBuffer; import javax.annotation.concurrent.NotThreadSafe; import com.google.common.primitives.Ints; @@ -28,7 +31,7 @@ import org.apache.cassandra.io.util.Rebufferer.BufferHolder; @NotThreadSafe -public class RandomAccessReader extends RebufferingInputStream implements FileDataInput +public class RandomAccessReader extends RebufferingInputStream implements FileDataInput, io.github.jbellis.jvector.disk.RandomAccessReader { // The default buffer size when the client doesn't specify it public static final int DEFAULT_BUFFER_SIZE = 4096; @@ -37,17 +40,20 @@ public class RandomAccessReader extends RebufferingInputStream implements FileDa private long markedPointer; final Rebufferer rebufferer; - protected BufferHolder bufferHolder = Rebufferer.EMPTY; + private BufferHolder bufferHolder; + private final ByteOrder order; /** * Only created through Builder * * @param rebufferer Rebufferer to use */ - protected RandomAccessReader(Rebufferer rebufferer) + RandomAccessReader(Rebufferer rebufferer, ByteOrder order, BufferHolder bufferHolder) { - super(Rebufferer.EMPTY.buffer()); + super(bufferHolder.buffer(), false); + this.bufferHolder = bufferHolder; this.rebufferer = rebufferer; + this.order = order; } /** @@ -63,12 +69,143 @@ public void reBuffer() private void reBufferAt(long position) { + position = rebufferer.adjustPosition(position); bufferHolder.release(); - bufferHolder = rebufferer.rebuffer(position); - buffer = bufferHolder.buffer(); - buffer.position(Ints.checkedCast(position - bufferHolder.offset())); + if (position == length()) + { + bufferHolder = Rebufferer.emptyBufferHolderAt(position); + buffer = bufferHolder.buffer(); + } + else + { + bufferHolder = Rebufferer.EMPTY; // prevents double release if the call below fails + bufferHolder = rebufferer.rebuffer(position); + buffer = bufferHolder.buffer(); + buffer.position(Ints.checkedCast(position - bufferHolder.offset())); + } + buffer.order(order); + } - assert buffer.order() == ByteOrder.BIG_ENDIAN : "Buffer must have BIG ENDIAN byte ordering"; + public ByteOrder order() + { + return order; + } + + @Override + public void read(float[] dest, int offset, int count) throws IOException + { + for (int inBuffer = buffer.remaining() / Float.BYTES; + inBuffer < count; + inBuffer = buffer.remaining() / Float.BYTES) + { + if (inBuffer >= 1) + { + // read as much as we can from the buffer + readFloats(buffer, order, dest, offset, inBuffer); + offset += inBuffer; + count -= inBuffer; + } + + if (buffer.remaining() > 0) + { + // read the buffer-spanning value using the slow path + dest[offset++] = readFloat(); + --count; + } + else + reBuffer(); + } + + readFloats(buffer, order, dest, offset, count); + } + + @Override + public void readFully(long[] dest) throws IOException + { + read(dest, 0, dest.length); + } + + public void read(long[] dest, int offset, int count) throws IOException + { + for (int inBuffer = buffer.remaining() / Long.BYTES; + inBuffer < count; + inBuffer = buffer.remaining() / Long.BYTES) + { + if (inBuffer >= 1) + { + // read as much as we can from the buffer + readLongs(buffer, order, dest, offset, inBuffer); + offset += inBuffer; + count -= inBuffer; + } + + if (buffer.remaining() > 0) + { + // read the buffer-spanning value using the slow path + dest[offset++] = readLong(); + --count; + } + else + reBuffer(); + } + + readLongs(buffer, order, dest, offset, count); + } + + @Override + public void read(int[] dest, int offset, int count) throws IOException + { + for (int inBuffer = buffer.remaining() / Integer.BYTES; + inBuffer < count; + inBuffer = buffer.remaining() / Integer.BYTES) + { + if (inBuffer >= 1) + { + // read as much as we can from the buffer + readInts(buffer, order, dest, offset, inBuffer); + offset += inBuffer; + count -= inBuffer; + } + + if (buffer.remaining() > 0) + { + // read the buffer-spanning value using the slow path + dest[offset++] = readInt(); + --count; + } + else + reBuffer(); + } + + readInts(buffer, order, dest, offset, count); + } + + private static void readFloats(ByteBuffer buffer, ByteOrder order, float[] dest, int offset, int count) + { + FloatBuffer floatBuffer = updateBufferByteOrderIfNeeded(buffer, order).asFloatBuffer(); + floatBuffer.get(dest, offset, count); + buffer.position(buffer.position() + count * Float.BYTES); + } + + private static void readLongs(ByteBuffer buffer, ByteOrder order, long[] dest, int offset, int count) + { + LongBuffer longBuffer = updateBufferByteOrderIfNeeded(buffer, order).asLongBuffer(); + longBuffer.get(dest, offset, count); + buffer.position(buffer.position() + count * Long.BYTES); + } + + private static void readInts(ByteBuffer buffer, ByteOrder order, int[] dest, int offset, int count) + { + IntBuffer intBuffer = updateBufferByteOrderIfNeeded(buffer, order).asIntBuffer(); + intBuffer.get(dest, offset, count); + buffer.position(buffer.position() + count * Integer.BYTES); + } + + private static ByteBuffer updateBufferByteOrderIfNeeded(ByteBuffer buffer, ByteOrder order) + { + return buffer.order() != order + ? buffer.duplicate().order(order) + : buffer; // Note: ?: rather than if to hit one-liner inlining path } @Override @@ -84,9 +221,10 @@ protected long current() return bufferHolder.offset() + buffer.position(); } - public String getPath() + @Override + public File getFile() { - return getChannel().filePath(); + return getChannel().getFile(); } public ChannelProxy getChannel() @@ -158,7 +296,6 @@ public void close() // close needs to be idempotent. if (buffer == null) return; - bufferHolder.release(); rebufferer.closeReader(); buffer = null; @@ -205,7 +342,7 @@ public void seek(long newPosition) if (newPosition > length()) throw new IllegalArgumentException(String.format("Unable to seek to position %d in %s (%d bytes) in read-only mode", - newPosition, getPath(), length())); + newPosition, getFile(), length())); reBufferAt(newPosition); } @@ -295,7 +432,7 @@ static class RandomAccessReaderWithOwnChannel extends RandomAccessReader { RandomAccessReaderWithOwnChannel(Rebufferer rebufferer) { - super(rebufferer); + super(rebufferer, ByteOrder.BIG_ENDIAN, Rebufferer.EMPTY); } @Override @@ -341,4 +478,5 @@ public static RandomAccessReader open(File file) throw t; } } + } diff --git a/src/java/org/apache/cassandra/io/util/ReadPattern.java b/src/java/org/apache/cassandra/io/util/ReadPattern.java new file mode 100644 index 000000000000..a670af79c1c6 --- /dev/null +++ b/src/java/org/apache/cassandra/io/util/ReadPattern.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.util; + +public enum ReadPattern +{ + SEQUENTIAL, RANDOM +} diff --git a/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java b/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java index 5d38e80e3fb0..64a26e5594e3 100644 --- a/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java +++ b/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java @@ -33,4 +33,11 @@ public interface ReaderFileProxy extends AutoCloseable * Needed for tests. Returns the table's CRC check chance, which is only set for compressed tables. */ double getCrcCheckChance(); + + /** + * Called before rebuffering to allow for position adjustments. + * This is used to enable files with holes (e.g. encryption data) where we still want to be able to write and read + * sequences of bytes (e.g. keys) that span over a hole. + */ + long adjustPosition(long position); } diff --git a/src/java/org/apache/cassandra/io/util/Rebufferer.java b/src/java/org/apache/cassandra/io/util/Rebufferer.java index a7fbc7149d51..0c0bee8d86f8 100644 --- a/src/java/org/apache/cassandra/io/util/Rebufferer.java +++ b/src/java/org/apache/cassandra/io/util/Rebufferer.java @@ -19,8 +19,6 @@ package org.apache.cassandra.io.util; import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; /** * Rebufferer for reading data by a RandomAccessReader. @@ -49,16 +47,6 @@ interface BufferHolder */ ByteBuffer buffer(); - default FloatBuffer floatBuffer() - { - throw new UnsupportedOperationException(); - } - - default IntBuffer intBuffer() - { - throw new UnsupportedOperationException(); - } - /** * Position in the file of the start of the buffer. */ @@ -93,4 +81,28 @@ public void release() // nothing to do } }; + + static BufferHolder emptyBufferHolderAt(long offset) + { + return new BufferHolder() + { + @Override + public ByteBuffer buffer() + { + return EMPTY.buffer(); + } + + @Override + public long offset() + { + return offset; + } + + @Override + public void release() + { + // nothing to do + } + }; + } } diff --git a/src/java/org/apache/cassandra/io/util/RebuffererFactory.java b/src/java/org/apache/cassandra/io/util/RebuffererFactory.java index 192fb8ea0cd8..77759f5cb64e 100644 --- a/src/java/org/apache/cassandra/io/util/RebuffererFactory.java +++ b/src/java/org/apache/cassandra/io/util/RebuffererFactory.java @@ -29,4 +29,12 @@ public interface RebuffererFactory extends ReaderFileProxy { Rebufferer instantiateRebufferer(boolean isScan); + + /** + * If the {@link Rebufferer} created by this factory rebuffered in "chunks" of a fixed size, the size (> 0) of those + * chunks. Otherwise, this should return a value <= 0. + */ + int chunkSize(); + + void invalidateIfCached(long position); } diff --git a/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java b/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java index b7ae205e9661..32cb2b69c3d8 100644 --- a/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java +++ b/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java @@ -45,7 +45,14 @@ public abstract class RebufferingInputStream extends DataInputStreamPlus impleme protected RebufferingInputStream(ByteBuffer buffer) { - Preconditions.checkArgument(buffer == null || buffer.order() == ByteOrder.BIG_ENDIAN, "Buffer must have BIG ENDIAN byte ordering"); + this(buffer, true); + } + + protected RebufferingInputStream(ByteBuffer buffer, boolean validateByteOrder) + { + if (validateByteOrder) + Preconditions.checkArgument(buffer == null || buffer.order() == ByteOrder.BIG_ENDIAN, + "Buffer must have BIG ENDIAN byte ordering"); this.buffer = buffer; } @@ -135,7 +142,7 @@ public void readFully(ByteBuffer dst) throws IOException } @DontInline - protected long readPrimitiveSlowly(int bytes) throws IOException + protected long readBigEndianPrimitiveSlowly(int bytes) throws IOException { long result = 0; for (int i = 0; i < bytes; i++) @@ -194,8 +201,10 @@ public short readShort() throws IOException { if (buffer.remaining() >= 2) return buffer.getShort(); - else - return (short) readPrimitiveSlowly(2); + var result = (short) readBigEndianPrimitiveSlowly(2); + if (buffer.order() == ByteOrder.LITTLE_ENDIAN) + return Short.reverseBytes(result); + return result; } @Override @@ -209,8 +218,10 @@ public char readChar() throws IOException { if (buffer.remaining() >= 2) return buffer.getChar(); - else - return (char) readPrimitiveSlowly(2); + var result = (char) readBigEndianPrimitiveSlowly(2); + if (buffer.order() == ByteOrder.LITTLE_ENDIAN) + return Character.reverseBytes(result); + return result; } @Override @@ -218,8 +229,10 @@ public int readInt() throws IOException { if (buffer.remaining() >= 4) return buffer.getInt(); - else - return (int) readPrimitiveSlowly(4); + var result = (int) readBigEndianPrimitiveSlowly(4); + if (buffer.order() == ByteOrder.LITTLE_ENDIAN) + return Integer.reverseBytes(result); + return result; } @Override @@ -227,8 +240,10 @@ public long readLong() throws IOException { if (buffer.remaining() >= 8) return buffer.getLong(); - else - return readPrimitiveSlowly(8); + var result = readBigEndianPrimitiveSlowly(8); + if (buffer.order() == ByteOrder.LITTLE_ENDIAN) + return Long.reverseBytes(result); + return result; } @Override @@ -286,8 +301,10 @@ public float readFloat() throws IOException { if (buffer.remaining() >= 4) return buffer.getFloat(); - else - return Float.intBitsToFloat((int)readPrimitiveSlowly(4)); + var intBits = (int) readBigEndianPrimitiveSlowly(4); + if (buffer.order() == ByteOrder.LITTLE_ENDIAN) + intBits = Integer.reverseBytes(intBits); + return Float.intBitsToFloat(intBits); } @Override @@ -295,8 +312,10 @@ public double readDouble() throws IOException { if (buffer.remaining() >= 8) return buffer.getDouble(); - else - return Double.longBitsToDouble(readPrimitiveSlowly(8)); + var longBits = readBigEndianPrimitiveSlowly(8); + if (buffer.order() == ByteOrder.LITTLE_ENDIAN) + longBits = Long.reverseBytes(longBits); + return Double.longBitsToDouble(longBits); } @Override diff --git a/src/java/org/apache/cassandra/io/util/SafeMemory.java b/src/java/org/apache/cassandra/io/util/SafeMemory.java index 4482d96019bd..cca8df90e128 100644 --- a/src/java/org/apache/cassandra/io/util/SafeMemory.java +++ b/src/java/org/apache/cassandra/io/util/SafeMemory.java @@ -88,7 +88,7 @@ public void tidy() { /** see {@link Memory#Memory(long)} re: null pointers*/ if (peer != 0) - MemoryUtil.free(peer); + MemoryUtil.free(peer, size); } public String name() diff --git a/src/java/org/apache/cassandra/io/util/SequentialWriter.java b/src/java/org/apache/cassandra/io/util/SequentialWriter.java index c3a90732eead..12bc3d5f441d 100644 --- a/src/java/org/apache/cassandra/io/util/SequentialWriter.java +++ b/src/java/org/apache/cassandra/io/util/SequentialWriter.java @@ -23,6 +23,7 @@ import java.nio.file.StandardOpenOption; import java.util.function.LongConsumer; +import io.github.jbellis.jvector.disk.IndexWriter; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.utils.SyncUtil; @@ -34,11 +35,10 @@ * Adds buffering, mark, and fsyncing to OutputStream. We always fsync on close; we may also * fsync incrementally if Config.trickle_fsync is enabled. */ -public class SequentialWriter extends BufferedDataOutputStreamPlus implements Transactional +public class SequentialWriter extends BufferedDataOutputStreamPlus implements Transactional, IndexWriter { // absolute path to the given file - private final String filePath; - private final File file; + protected final File file; // Offset for start of buffer relative to underlying file protected long bufferOffset; @@ -105,17 +105,21 @@ protected Throwable doAbort(Throwable accumulate) } // TODO: we should specify as a parameter if we permit an existing file or not - private static FileChannel openChannel(File file) + private static FileChannel openChannel(File file, boolean readable) { try { if (file.exists()) { - return FileChannel.open(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE); + StandardOpenOption[] options = readable ? new StandardOpenOption[] {StandardOpenOption.WRITE, StandardOpenOption.READ} + : new StandardOpenOption[] {StandardOpenOption.WRITE}; + return FileChannel.open(file.toPath(), options); } else { - FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); + StandardOpenOption[] options = readable ? new StandardOpenOption[] {StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW} + : new StandardOpenOption[] {StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW}; + FileChannel channel = FileChannel.open(file.toPath(), options); try { SyncUtil.trySyncDir(file.parent()); @@ -141,7 +145,7 @@ private static FileChannel openChannel(File file) */ public SequentialWriter(File file) { - this(file, SequentialWriterOption.DEFAULT); + this(file, SequentialWriterOption.DEFAULT); } /** @@ -163,17 +167,26 @@ public SequentialWriter(File file, SequentialWriterOption option) */ public SequentialWriter(File file, SequentialWriterOption option, boolean strictFlushing) { - this(file, option.allocateBuffer(), option, strictFlushing); + this(file, false, option, strictFlushing); } protected SequentialWriter(File file, ByteBuffer buffer, SequentialWriterOption option, boolean strictFlushing) { - super(openChannel(file), buffer); + super(openChannel(file, false), buffer); + this.strictFlushing = strictFlushing; + this.fchannel = (FileChannel)channel; + + this.file = file; + this.option = option; + } + + public SequentialWriter(File file, boolean readable, SequentialWriterOption option, boolean strictFlushing) + { + super(openChannel(file, readable), option.allocateBuffer()); this.strictFlushing = strictFlushing; this.fchannel = (FileChannel)channel; this.file = file; - this.filePath = file.absolutePath(); this.option = option; } @@ -201,7 +214,7 @@ protected void syncDataOnlyInternal() } catch (IOException e) { - throw new FSWriteError(e, getPath()); + throw new FSWriteError(e, getFile()); } } @@ -219,15 +232,18 @@ protected void syncInternal() @Override protected void doFlush(int count) { - flushData(); - - if (option.trickleFsync()) + if (buffer.position() > 0) { - bytesSinceTrickleFsync += buffer.position(); - if (bytesSinceTrickleFsync >= option.trickleFsyncByteInterval()) + flushData(); + + if (option.trickleFsync()) { - syncDataOnlyInternal(); - bytesSinceTrickleFsync = 0; + bytesSinceTrickleFsync += buffer.position(); + if (bytesSinceTrickleFsync >= option.trickleFsyncByteInterval()) + { + syncDataOnlyInternal(); + bytesSinceTrickleFsync = 0; + } } } @@ -255,7 +271,7 @@ protected void flushData() } catch (IOException e) { - throw new FSWriteError(e, getPath()); + throw new FSWriteError(e, getFile()); } if (runPostFlush != null) runPostFlush.accept(getLastFlushOffset()); @@ -273,6 +289,8 @@ public long position() return current(); } + // Page management using on-disk pages + @Override public int maxBytesInPage() { @@ -297,6 +315,29 @@ public long paddedPosition() return PageAware.padded(position()); } + public void updateFileHandle(FileHandle.Builder fhBuilder) + { + updateFileHandle(fhBuilder, -1); + } + + public void updateFileHandle(FileHandle.Builder fhBuilder, long dataLength) + { + // Set actual length to avoid having to read it off the file system. + fhBuilder.withLengthOverride(dataLength > 0 ? dataLength : lastFlushOffset); + } + + /** + * Some writers cannot feasibly calculate the exact length of a file. If any user needs to be able to store + * metadata at the end, they should use this function to ensure the content to be written can be addressed + * using `fileLength - bytesNeeded`. + * + * See PartitionIndexBuilder#complete and PartitionIndex#load for usage example. + */ + public void establishEndAddressablePosition(int bytesNeeded) throws IOException + { + // Nothing to do when file length can be exactly determined. + } + /** * Returns the current file pointer of the underlying on-disk file. * Note that since write works by buffering data, the value of this will increase by buffer @@ -324,15 +365,10 @@ public long length() } catch (IOException e) { - throw new FSReadError(e, getPath()); + throw new FSReadError(e, getFile()); } } - public String getPath() - { - return filePath; - } - public File getFile() { return file; @@ -385,7 +421,7 @@ public void resetAndTruncate(DataPosition mark) } catch (IOException e) { - throw new FSReadError(e, getPath()); + throw new FSReadError(e, getFile()); } bufferOffset = truncateTarget; @@ -406,7 +442,7 @@ public void truncate(long toSize) } catch (IOException e) { - throw new FSWriteError(e, getPath()); + throw new FSWriteError(e, getFile()); } } @@ -427,6 +463,13 @@ public final Throwable commit(Throwable accumulate) return txnProxy.commit(accumulate); } + /** + * Stop the operation after errors, i.e. close and release all held resources. + * + * Do not use this to interrupt a write operation running in another thread. + * This is thread-unsafe, releasing and cleaning the buffer while it is being written can have disastrous + * consequences (e.g. SIGSEGV). + */ @Override public final Throwable abort(Throwable accumulate) { @@ -434,7 +477,7 @@ public final Throwable abort(Throwable accumulate) } @Override - public final void close() + public void close() { if (option.finishOnClose()) txnProxy.finish(); @@ -466,7 +509,7 @@ protected TransactionalProxy txnProxy() */ protected static class BufferedFileWriterMark implements DataPosition { - final long pointer; + public final long pointer; public BufferedFileWriterMark(long pointer) { diff --git a/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java b/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java index fec1216bf4e4..b7e0f63bfe31 100644 --- a/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java +++ b/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java @@ -26,19 +26,30 @@ class SimpleChunkReader extends AbstractReaderFileProxy implements ChunkReader { private final int bufferSize; private final BufferType bufferType; + private final long startOffset; SimpleChunkReader(ChannelProxy channel, long fileLength, BufferType bufferType, int bufferSize) + { + this(channel, fileLength, bufferType, bufferSize, 0); + } + + SimpleChunkReader(ChannelProxy channel, long fileLength, BufferType bufferType, int bufferSize, long startOffset) { super(channel, fileLength); this.bufferSize = bufferSize; this.bufferType = bufferType; + this.startOffset = startOffset; } @Override public void readChunk(long position, ByteBuffer buffer) { + long readPosition = position - startOffset; + if (readPosition < 0) + throw new IllegalArgumentException("Trying to read from a negative read position: " + readPosition + " startOffset " + startOffset + " position " + position); + buffer.clear(); - channel.read(buffer, position); + channel.read(buffer, readPosition); buffer.flip(); } @@ -58,11 +69,24 @@ public BufferType preferredBufferType() public Rebufferer instantiateRebufferer(boolean forScan) { if (Integer.bitCount(bufferSize) == 1) + { + assert startOffset == (startOffset & -bufferSize) : "startOffset must be aligned to buffer size"; return new BufferManagingRebufferer.Aligned(this); + } else return new BufferManagingRebufferer.Unaligned(this); } + @Override + public void invalidateIfCached(long position) + { + } + + public ReaderType type() + { + return ReaderType.SIMPLE; + } + @Override public String toString() { @@ -72,4 +96,4 @@ public String toString() bufferSize, fileLength()); } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/io/util/SliceDescriptor.java b/src/java/org/apache/cassandra/io/util/SliceDescriptor.java new file mode 100644 index 000000000000..27a997ab481b --- /dev/null +++ b/src/java/org/apache/cassandra/io/util/SliceDescriptor.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.io.util; + +import java.util.Objects; +import java.util.StringJoiner; + +public class SliceDescriptor +{ + public static final SliceDescriptor NONE = new SliceDescriptor(0, 0, 0); + + /** + * The position of the beginning of the data in the original file (inclusive). + */ + public final long dataStart; + + /** + * The position of the end of the data in the original file (exclusive). + */ + public final long dataEnd; + + /** + * The size of the chunk to which the slice is aligned. + */ + public final int chunkSize; + + /** + * The position of beginning of this slice in the original file (inclusive). It is the {@link #dataStart} + * aligned to the chunk size. + */ + public final long sliceStart; + + /** + * The position of the end of this slice in the original file (exclusive). It is the {@link #dataEnd} + * aligned to the chunk size. {@code sliceEnd - sliceStart} is equals to the actual size of the partial file. + */ + public final long sliceEnd; + + public SliceDescriptor(long dataStart, long dataEnd, int chunkSize) + { + this.dataStart = dataStart; + this.dataEnd = dataEnd; + this.chunkSize = chunkSize; + + this.sliceStart = chunkSize == 0 ? dataStart : dataStart & -chunkSize; + this.sliceEnd = chunkSize == 0 ? dataEnd : (chunkSize + dataEnd - 1) & -chunkSize; + } + + public boolean exists() + { + return dataStart > 0 || dataEnd > 0; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SliceDescriptor that = (SliceDescriptor) o; + return dataStart == that.dataStart && dataEnd == that.dataEnd && chunkSize == that.chunkSize; + } + + @Override + public int hashCode() + { + return Objects.hash(dataStart, dataEnd, chunkSize); + } + + @Override + public String toString() + { + return new StringJoiner(", ", SliceDescriptor.class.getSimpleName() + "[", "]") + .add("dataStart=" + dataStart) + .add("dataEnd=" + dataEnd) + .add("chunkSize=" + chunkSize) + .add("sliceStart=" + sliceStart) + .add("sliceEnd=" + sliceEnd) + .toString(); + } + + public long dataEndOr(long dataEndIfNotExists) + { + return exists() ? dataEnd : dataEndIfNotExists; + } +} diff --git a/src/java/org/apache/cassandra/io/util/TailOverridingRebufferer.java b/src/java/org/apache/cassandra/io/util/TailOverridingRebufferer.java index 8bd92e93dc71..eb9704494d91 100644 --- a/src/java/org/apache/cassandra/io/util/TailOverridingRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/TailOverridingRebufferer.java @@ -65,6 +65,15 @@ public long fileLength() return cutoff + tail.limit(); } + @Override + public long adjustPosition(long position) + { + if (position < cutoff) + return super.adjustPosition(position); + else + return position; + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/io/util/WrappingRebufferer.java b/src/java/org/apache/cassandra/io/util/WrappingRebufferer.java index 5fbe5eaa0040..adb0a894c992 100644 --- a/src/java/org/apache/cassandra/io/util/WrappingRebufferer.java +++ b/src/java/org/apache/cassandra/io/util/WrappingRebufferer.java @@ -74,6 +74,12 @@ public double getCrcCheckChance() return wrapped.getCrcCheckChance(); } + @Override + public long adjustPosition(long position) + { + return wrapped.adjustPosition(position); + } + @Override public void close() { @@ -118,4 +124,5 @@ public void release() } buffer = null; } -} \ No newline at end of file + +} diff --git a/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java index 345e75918e1d..1f3508100968 100644 --- a/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java +++ b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java @@ -20,13 +20,11 @@ import java.util.Map; +import org.apache.cassandra.nodes.Nodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; @@ -73,8 +71,8 @@ public final String getRack(InetAddressAndPort endpoint) { if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) return getLocalRack(); - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.RACK) == null) + String rack = Nodes.getRack(endpoint, null); + if (rack == null) { if (savedEndpoints == null) savedEndpoints = SystemKeyspace.loadDcRackInfo(); @@ -82,7 +80,7 @@ public final String getRack(InetAddressAndPort endpoint) return savedEndpoints.get(endpoint).get("rack"); return DEFAULT_RACK; } - return state.getApplicationState(ApplicationState.RACK).value; + return rack; } @Override @@ -90,8 +88,8 @@ public final String getDatacenter(InetAddressAndPort endpoint) { if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) return getLocalDatacenter(); - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.DC) == null) + String dc = Nodes.getDataCenter(endpoint, null); + if (dc == null) { if (savedEndpoints == null) savedEndpoints = SystemKeyspace.loadDcRackInfo(); @@ -99,6 +97,6 @@ public final String getDatacenter(InetAddressAndPort endpoint) return savedEndpoints.get(endpoint).get("data_center"); return DEFAULT_DC; } - return state.getApplicationState(ApplicationState.DC).value; + return dc; } } diff --git a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java index 31e60e611dd8..2871f66d1185 100644 --- a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java +++ b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java @@ -20,18 +20,21 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import com.google.common.base.Preconditions; - +import org.cliffc.high_scale_lib.NonBlockingHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Mutation; @@ -48,7 +51,6 @@ import org.apache.cassandra.service.WriteResponseHandler; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.FBUtilities; -import org.cliffc.high_scale_lib.NonBlockingHashMap; /** * A abstract parent for all replication strategies. @@ -73,6 +75,11 @@ protected AbstractReplicationStrategy(String keyspaceName, TokenMetadata tokenMe this.keyspaceName = keyspaceName; } + public TokenMetadata getTokenMetadata() + { + return tokenMetadata; + } + public EndpointsForRange getCachedReplicas(long ringVersion, Token t) { return replicas.get(ringVersion, t); @@ -92,9 +99,13 @@ public EndpointsForToken getNaturalReplicasForToken(RingPosition searchPositi public EndpointsForRange getNaturalReplicas(RingPosition searchPosition) { + ArrayList sortedTokens = tokenMetadata.sortedTokens(); + if (sortedTokens.isEmpty()) + return EndpointsForRange.empty(new Range<>(tokenMetadata.partitioner.getMinimumToken(), tokenMetadata.partitioner.getMinimumToken())); + Token searchToken = searchPosition.getToken(); long currentRingVersion = tokenMetadata.getRingVersion(); - Token keyToken = TokenMetadata.firstToken(tokenMetadata.sortedTokens(), searchToken); + Token keyToken = TokenMetadata.firstToken(sortedTokens, searchToken); EndpointsForRange endpoints = getCachedReplicas(currentRingVersion, keyToken); if (endpoints == null) { @@ -273,6 +284,23 @@ public EndpointsByRange getRangeAddresses(TokenMetadata metadata) return map.build(); } + public Set getAllEndpoints() + { + return tokenMetadata.cloneOnlyTokenMap().getAllEndpoints(); + } + + public EndpointsForRange getEndpointsForFullRange() + { + Range replicaRange = new Range<>(DatabaseDescriptor.getPartitioner().getMinimumToken(), DatabaseDescriptor.getPartitioner().getMinimumToken()); + Set allEndpoints = tokenMetadata.cloneOnlyTokenMap().getAllEndpoints(); + EndpointsForRange.Builder replicas = new EndpointsForRange.Builder(replicaRange, allEndpoints.size()); + + for (InetAddressAndPort ep : allEndpoints) + replicas.add(new Replica(ep, replicaRange, true)); + + return replicas.build(); + } + public RangesByEndpoint getAddressReplicas() { return getAddressReplicas(tokenMetadata.cloneOnlyTokenMap()); @@ -283,6 +311,14 @@ public RangesAtEndpoint getAddressReplicas(InetAddressAndPort endpoint) return getAddressReplicas(tokenMetadata.cloneOnlyTokenMap(), endpoint); } + /** + * Returns the number of token-owning nodes. + */ + protected int getSizeOfRingMemebers() + { + return tokenMetadata.getAllRingMembers().size(); + } + public RangesAtEndpoint getPendingAddressRanges(TokenMetadata metadata, Token pendingToken, InetAddressAndPort pendingAddress) { return getPendingAddressRanges(metadata, Collections.singleton(pendingToken), pendingAddress); @@ -370,6 +406,14 @@ public static AbstractReplicationStrategy createReplicationStrategy(String keysp return strategy; } + /** + * Whether this strategy partitions data across the ring + */ + public boolean isPartitioned() + { + return true; + } + /** * Before constructing the ARS we first give it a chance to prepare the options map in any way it * would like to. For example datacenter auto-expansion or other templating to make the user interface @@ -427,11 +471,11 @@ public static Class getClass(String cls) throws Con if ("org.apache.cassandra.locator.OldNetworkTopologyStrategy".equals(className)) // see CASSANDRA-16301 throw new ConfigurationException("The support for the OldNetworkTopologyStrategy has been removed in C* version 4.0. The keyspace strategy should be switch to NetworkTopologyStrategy"); - Class strategyClass = FBUtilities.classForName(className, "replication strategy"); - if (!AbstractReplicationStrategy.class.isAssignableFrom(strategyClass)) - { - throw new ConfigurationException(String.format("Specified replication strategy class (%s) is not derived from AbstractReplicationStrategy", className)); - } + @SuppressWarnings("unchecked") + Class strategyClass = + (Class) FBUtilities.classForNameWithoutInitialization(className, + "replication strategy", + AbstractReplicationStrategy.class); return strategyClass; } @@ -467,7 +511,18 @@ protected void validateExpectedOptions() throws ConfigurationException for (String key : configOptions.keySet()) { if (!expectedOptions.contains(key)) - throw new ConfigurationException(String.format("Unrecognized strategy option {%s} passed to %s for keyspace %s", key, getClass().getSimpleName(), keyspaceName)); + { + String message = String.format("Unrecognized strategy option {%s} passed to %s for keyspace %s", key, getClass().getSimpleName(), keyspaceName); + + if (CassandraRelevantProperties.DATACENTER_SKIP_NAME_VALIDATION.getBoolean()) + { + logger.warn("{}=true. Ignoring: {}", CassandraRelevantProperties.DATACENTER_SKIP_NAME_VALIDATION.getKey(), message); + } + else + { + throw new ConfigurationException(message); + } + } } } diff --git a/src/java/org/apache/cassandra/locator/DefaultTokenMetadataProvider.java b/src/java/org/apache/cassandra/locator/DefaultTokenMetadataProvider.java new file mode 100644 index 000000000000..0b0be05c08c6 --- /dev/null +++ b/src/java/org/apache/cassandra/locator/DefaultTokenMetadataProvider.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.locator; + +public class DefaultTokenMetadataProvider implements TokenMetadataProvider +{ + private volatile TokenMetadata tokenMetadata; + + public DefaultTokenMetadataProvider() + { + this.tokenMetadata = new TokenMetadata(); + } + + @Override + public TokenMetadata getTokenMetadata() + { + return tokenMetadata; + } + + @Override + public TokenMetadata getTokenMetadataForKeyspace(String keyspace) + { + return tokenMetadata; + } + + /** @deprecated See STAR-1032 */ + @Deprecated(forRemoval = true, since = "CC 4.0") // since we can select TMDP implementation via config, this method is no longer needed + public void replaceTokenMetadata(TokenMetadata newTokenMetadata) + { + this.tokenMetadata = newTokenMetadata; + } +} diff --git a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java index bb652b6cff15..012eb7cc4499 100644 --- a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java +++ b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java @@ -20,10 +20,17 @@ import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; @@ -32,14 +39,10 @@ import com.codahale.metrics.Snapshot; import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.net.LatencySubscribers; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MBeanWrapper; @@ -55,6 +58,10 @@ public class DynamicEndpointSnitch extends AbstractEndpointSnitch implements Lat private static final double ALPHA = 0.75; // set to 0.75 to make EDS more biased to towards the newer values private static final int WINDOW_SIZE = 100; + // these need not be volatile; eventually the snitch will see the update and that's good enough + private double replicaLatencyQuantile = CassandraRelevantProperties.DYNAMIC_ENDPOINT_SNITCH_QUANTILE.getDouble(); + private boolean quantizeToMillis = CassandraRelevantProperties.DYNAMIC_ENDPOINT_SNITCH_QUANTIZE_TO_MILLIS.getBoolean(); + private volatile int dynamicUpdateInterval = DatabaseDescriptor.getDynamicUpdateInterval(); private volatile int dynamicResetInterval = DatabaseDescriptor.getDynamicResetInterval(); private volatile double dynamicBadnessThreshold = DatabaseDescriptor.getDynamicBadnessThreshold(); @@ -267,21 +274,17 @@ public int compareEndpoints(InetAddressAndPort target, Replica a1, Replica a2) public void receiveTiming(InetAddressAndPort host, long latency, TimeUnit unit) // this is cheap { - ExponentiallyDecayingReservoir sample = samples.get(host); - if (sample == null) - { - ExponentiallyDecayingReservoir maybeNewSample = new ExponentiallyDecayingReservoir(WINDOW_SIZE, ALPHA); - sample = samples.putIfAbsent(host, maybeNewSample); - if (sample == null) - sample = maybeNewSample; - } - sample.update(unit.toMillis(latency)); + ExponentiallyDecayingReservoir sample = samples.computeIfAbsent(host, k -> new ExponentiallyDecayingReservoir(WINDOW_SIZE, ALPHA)); + if (quantizeToMillis) + sample.update(unit.toMillis(latency)); + else + sample.update(unit.toNanos(latency)); } @VisibleForTesting public void updateScores() // this is expensive { - if (!StorageService.instance.isInitialized()) + if (!DynamicSnitchSeverityProvider.instance.isReady()) return; if (!registered) { @@ -302,17 +305,17 @@ public void updateScores() // this is expensive // We're going to weight the latency for each host against the worst one we see, to // arrive at sort of a 'badness percentage' for them. First, find the worst for each: - HashMap newScores = new HashMap<>(); + HashMap newScores = new HashMap<>(samples.size()); for (Map.Entry entry : snapshots.entrySet()) { - double mean = entry.getValue().getMedian(); - if (mean > maxLatency) - maxLatency = mean; + double replicaLatency = entry.getValue().getValue(replicaLatencyQuantile); + if (replicaLatency > maxLatency) + maxLatency = replicaLatency; } // now make another pass to do the weighting based on the maximums we found before for (Map.Entry entry : snapshots.entrySet()) { - double score = entry.getValue().getMedian() / maxLatency; + double score = entry.getValue().getValue(replicaLatencyQuantile) / maxLatency; // finally, add the severity without any weighting, since hosts scale this relative to their own load and the size of the task causing the severity. // "Severity" is basically a measure of compaction activity (CASSANDRA-3722). if (USE_SEVERITY) @@ -377,28 +380,49 @@ public void setSeverity(double severity) public static void addSeverity(double severity) { - Gossiper.instance.addLocalApplicationState(ApplicationState.SEVERITY, StorageService.instance.valueFactory.severity(severity)); + DynamicSnitchSeverityProvider.instance.setSeverity(FBUtilities.getBroadcastAddressAndPort(), severity); } @VisibleForTesting public static double getSeverity(InetAddressAndPort endpoint) { - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null) - return 0.0; - - VersionedValue event = state.getApplicationState(ApplicationState.SEVERITY); - if (event == null) - return 0.0; - - return Double.parseDouble(event.value); + return DynamicSnitchSeverityProvider.instance.getSeverity(endpoint); } + @Override public double getSeverity() { return getSeverity(FBUtilities.getBroadcastAddressAndPort()); } + @Override + public void setQuantile(double quantile) + { + if (quantile < 0.0 || quantile > 1.0 || Double.isNaN(quantile)) + { + throw new IllegalArgumentException(quantile + " is not in [0..1]"); + } + replicaLatencyQuantile = quantile; + } + + @Override + public double getQuantile() + { + return replicaLatencyQuantile; + } + + @Override + public void setQuantizationToMillis(boolean enabled) + { + quantizeToMillis = enabled; + } + + @Override + public boolean getQuantizationToMillis() + { + return quantizeToMillis; + } + public boolean isWorthMergingForRangeQuery(ReplicaCollection merged, ReplicaCollection l1, ReplicaCollection l2) { if (!subsnitch.isWorthMergingForRangeQuery(merged, l1, l2)) @@ -438,4 +462,28 @@ public boolean validate(Set datacenters, Set racks) { return subsnitch.validate(datacenters, racks); } + + @Override + public InetAddressAndPort getPreferredAddress(InetAddressAndPort remoteEndpoint) + { + return subsnitch.getPreferredAddress(remoteEndpoint); + } + + @Override + public boolean acceptsNodesFromSameRack(int rf, int rackCount) + { + return subsnitch.acceptsNodesFromSameRack(rf, rackCount); + } + + @Override + public Predicate filterByAffinityForReads(String keyspace) + { + return subsnitch.filterByAffinityForReads(keyspace); + } + + @Override + public Predicate filterByAffinityForWrites(String keyspace) + { + return subsnitch.filterByAffinityForWrites(keyspace); + } } diff --git a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java index dd07b80e6649..6117f5736bb6 100644 --- a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java +++ b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java @@ -59,4 +59,23 @@ public interface DynamicEndpointSnitchMBean * @return the current manually injected Severity. */ public double getSeverity(); + + /** + * set replica latency quantile used for replica score computation + * @param quantile (0.0 - 1.0); for default see + * {@link org.apache.cassandra.config.CassandraRelevantProperties#DYNAMIC_ENDPOINT_SNITCH_QUANTILE} + */ + public void setQuantile(double quantile); + + /** + * @return the replica latency quantile currently used for replica score computation + */ + public double getQuantile(); + + /** + * set replica latency quantization to 1ms + */ + public void setQuantizationToMillis(boolean enabled); + + public boolean getQuantizationToMillis(); } diff --git a/src/java/org/apache/cassandra/locator/DynamicSnitchSeverityProvider.java b/src/java/org/apache/cassandra/locator/DynamicSnitchSeverityProvider.java new file mode 100644 index 000000000000..f022e318fa27 --- /dev/null +++ b/src/java/org/apache/cassandra/locator/DynamicSnitchSeverityProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.locator; + +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.DYNAMIC_SNITCH_SEVERITY_PROVIDER; + + +/** + * Class to abstract gossiper out of dynamic snitch + */ +public interface DynamicSnitchSeverityProvider +{ + DynamicSnitchSeverityProvider instance = DYNAMIC_SNITCH_SEVERITY_PROVIDER.isPresent() + ? FBUtilities.construct(DYNAMIC_SNITCH_SEVERITY_PROVIDER.getString(), + "Dynamic Snitch Severity Provider") + : new DefaultProvider(); + + /** + * @return true if initialization is completed and ready to update dynamic snitch scores + */ + boolean isReady(); + + /** + * update the severity for given endpoint + * + * @param endpoint endpoint to be updated + * @param severity severity for the endpoint + */ + void setSeverity(InetAddressAndPort endpoint, double severity); + + /** + * @return severity for the endpoint or 0.0 if not found + */ + double getSeverity(InetAddressAndPort endpoint); + + class DefaultProvider implements DynamicSnitchSeverityProvider + { + @Override + public boolean isReady() + { + return StorageService.instance.isInitialized(); + } + + @Override + public void setSeverity(InetAddressAndPort endpoint, double severity) + { + if (!endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + throw new UnsupportedOperationException("Default severity provider only supports setting local severity, but got " + endpoint); + + Gossiper.instance.addLocalApplicationState(ApplicationState.SEVERITY, StorageService.instance.valueFactory.severity(severity)); + } + + @Override + public double getSeverity(InetAddressAndPort endpoint) + { + EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); + if (state == null) + return 0.0; + + VersionedValue event = state.getApplicationState(ApplicationState.SEVERITY); + if (event == null) + return 0.0; + + return Double.parseDouble(event.value); + } + } +} diff --git a/src/java/org/apache/cassandra/locator/EndpointSnitchInfo.java b/src/java/org/apache/cassandra/locator/EndpointSnitchInfo.java index d836cd18062b..4ae60a8e740f 100644 --- a/src/java/org/apache/cassandra/locator/EndpointSnitchInfo.java +++ b/src/java/org/apache/cassandra/locator/EndpointSnitchInfo.java @@ -19,16 +19,24 @@ import java.net.UnknownHostException; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.utils.MBeanWrapper; public class EndpointSnitchInfo implements EndpointSnitchInfoMBean { - public static void create() + public static void registerMBean() { MBeanWrapper.instance.registerMBean(new EndpointSnitchInfo(), "org.apache.cassandra.db:type=EndpointSnitchInfo"); } + @VisibleForTesting + public static void unregisterMBean() + { + MBeanWrapper.instance.unregisterMBean("org.apache.cassandra.db:type=EndpointSnitchInfo"); + } + public String getDatacenter(String host) throws UnknownHostException { return DatabaseDescriptor.getEndpointSnitch().getDatacenter(InetAddressAndPort.getByName(host)); diff --git a/src/java/org/apache/cassandra/locator/EverywhereStrategy.java b/src/java/org/apache/cassandra/locator/EverywhereStrategy.java new file mode 100644 index 000000000000..b6ac8b9d35bc --- /dev/null +++ b/src/java/org/apache/cassandra/locator/EverywhereStrategy.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.locator; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.ConfigurationException; + +/** + * Strategy that replicate data on every {@code live} node. + * + *

    This strategy is a {@code MultiDatacentersStrategy}. By consequence, it will handle properly local consistency levels. + * Nevertheless, as the data is replicated on every node, consistency levels such as QUORUM should not be used + * on clusters having more than 5 nodes.

    + * + *

    During bootstrap the time at which the data will be available is unknown and if the bootstrap is performed with + * autobootstrap=false on a seed node, there will be no data locally until rebuild is run.

    + * + */ +public class EverywhereStrategy extends AbstractReplicationStrategy +{ + public EverywhereStrategy(String keyspaceName, + TokenMetadata tokenMetadata, + IEndpointSnitch snitch, + Map configOptions) throws ConfigurationException + { + super(keyspaceName, tokenMetadata, snitch, configOptions); + } + + @Override + public EndpointsForRange calculateNaturalReplicas(Token searchToken, TokenMetadata tokenMetadata) + { + // Even if primary range repairs do not make a lot of sense for this strategy we want the behavior to be + // correct if somebody use it. + // Primary range repair expect the first endpoint of the list to be the primary range owner. + Set replicas = new LinkedHashSet<>(); + Iterator iter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), searchToken, false); + + if (iter.hasNext()) + { + Token end = iter.next(); + Token start = tokenMetadata.getPredecessor(end); + Range range = new Range<>(start, end); + + InetAddressAndPort endpoint = tokenMetadata.getEndpoint(end); + replicas.add(Replica.fullReplica(endpoint, range)); + + while (iter.hasNext()) + { + endpoint = tokenMetadata.getEndpoint(iter.next()); + replicas.add(Replica.fullReplica(endpoint, range)); + } + } + + return EndpointsForRange.copyOf(replicas); + } + + @Override + public ReplicationFactor getReplicationFactor() + { + return ReplicationFactor.fullOnly(getSizeOfRingMemebers()); + } + + @Override + public void validateOptions() throws ConfigurationException + { + // noop + } + + @Override + public void maybeWarnOnOptions() + { + // noop + } + + @Override + public Collection recognizedOptions() + { + return Collections.emptyList(); + } + + /** + * CASSANDRA-12510 added a check that forbids decommission when the number of + * nodes will drop below the RF for a given keyspace. This check is breaking on + * EverywhereStrategy because all nodes replicate the keyspace, so this check does + * not make sense for partitioned keyspaces such as LocalStrategy and EverywhereStrategy. + * + * @return false because the data is not partitioned across the ring. + */ + @Override + public boolean isPartitioned() + { + return false; + } +} diff --git a/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java b/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java index 5aa7791e633b..510a90363d59 100644 --- a/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java +++ b/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java @@ -18,8 +18,8 @@ package org.apache.cassandra.locator; -import java.util.concurrent.atomic.AtomicReference; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,8 +27,8 @@ import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; @@ -88,8 +88,8 @@ public String getDatacenter(InetAddressAndPort endpoint) if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) return myDC; - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (epState == null || epState.getApplicationState(ApplicationState.DC) == null) + String dc = Nodes.getDataCenter(endpoint, null); + if (dc == null) { if (psnitch == null) { @@ -102,7 +102,7 @@ public String getDatacenter(InetAddressAndPort endpoint) else return psnitch.getDatacenter(endpoint); } - return epState.getApplicationState(ApplicationState.DC).value; + return dc; } /** @@ -116,8 +116,8 @@ public String getRack(InetAddressAndPort endpoint) if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) return myRack; - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (epState == null || epState.getApplicationState(ApplicationState.RACK) == null) + String rack = Nodes.getRack(endpoint, null); + if (rack == null) { if (psnitch == null) { @@ -130,7 +130,7 @@ public String getRack(InetAddressAndPort endpoint) else return psnitch.getRack(endpoint); } - return epState.getApplicationState(ApplicationState.RACK).value; + return rack; } public void gossiperStarting() diff --git a/src/java/org/apache/cassandra/locator/IEndpointSnitch.java b/src/java/org/apache/cassandra/locator/IEndpointSnitch.java index 0120391265d1..e39af287f676 100644 --- a/src/java/org/apache/cassandra/locator/IEndpointSnitch.java +++ b/src/java/org/apache/cassandra/locator/IEndpointSnitch.java @@ -19,7 +19,9 @@ import java.net.InetSocketAddress; import java.util.Set; +import java.util.function.Predicate; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.utils.FBUtilities; /** @@ -94,4 +96,45 @@ default boolean validate(Set datacenters, Set racks) { return true; } + + /** + * Get this endpoint address to advertise for connections to provided remote endpoint. + */ + default InetAddressAndPort getPreferredAddress(InetAddressAndPort remoteEndpoint) + { + return FBUtilities.getBroadcastAddressAndPort(); + } + + /** + * Given the following {@code rf} and {@code rackCount}, returns true if nodes from the same rack should be accepted + * according to {@link AbstractReplicationStrategy#calculateNaturalReplicas(Token, TokenMetadata)} + * implementations, false otherwise. + *

    + * Always returns true by default. + */ + default boolean acceptsNodesFromSameRack(int rf, int rackCount) + { + return true; + } + + /** + * Filters the given {@code addresses} by affinity to the given keyspace for read requests. + *

    + * Always returns true by default. + */ + default Predicate filterByAffinityForReads(String keyspace) + { + return replica -> true; + } + + + /** + * Filters the given {@code addresses} by affinity to the given keyspace for write requests. + *

    + * Always returns true by default. + */ + default Predicate filterByAffinityForWrites(String keyspace) + { + return replica -> true; + } } diff --git a/src/java/org/apache/cassandra/locator/InetAddressAndPort.java b/src/java/org/apache/cassandra/locator/InetAddressAndPort.java index c954a05b1ce4..be86d5fd5692 100644 --- a/src/java/org/apache/cassandra/locator/InetAddressAndPort.java +++ b/src/java/org/apache/cassandra/locator/InetAddressAndPort.java @@ -25,15 +25,22 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; -import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.google.common.net.HostAndPort; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; @@ -58,6 +65,7 @@ public final class InetAddressAndPort extends InetSocketAddress implements Comparable, Serializable { private static final long serialVersionUID = 0; + private static final Logger logger = LoggerFactory.getLogger(InetAddressAndPort.class); //Store these here to avoid requiring DatabaseDescriptor to be loaded. DatabaseDescriptor will set //these when it loads the config. A lot of unit tests won't end up loading DatabaseDescriptor. @@ -311,6 +319,52 @@ public static void initializeDefaultPort(int port) defaultPort = port; } + public static List stringify(Iterable endpoints) + { + return stringify(endpoints, true); + } + + public static List stringify(Iterable endpoints, boolean withPort) + { + List stringEndpoints = new ArrayList<>(); + for (InetAddressAndPort ep : endpoints) + { + stringEndpoints.add(ep.getHostAddress(withPort)); + } + return stringEndpoints; + } + + /** + * Parses a comma-separated list of hosts to a set of {@link InetAddressAndPort} + * + * @param value the comma-separated list of hosts to parse + * @param failOnError whether to fail when encountering an invalid hostname + * @return the set of parsed {@link InetAddressAndPort} + */ + public static Set parseHosts(String value, boolean failOnError) + { + Set hosts = new HashSet<>(); + for (String host : Splitter.on(',').split(value)) + { + try + { + hosts.add(InetAddressAndPort.getByName(host)); + } + catch (UnknownHostException e) + { + if (failOnError) + { + throw new IllegalArgumentException("Failed to parse host: " + host, e); + } + else + { + logger.warn("Invalid ip address {} from input={}", host, value); + } + } + } + return hosts; + } + static int getDefaultPort() { return defaultPort; diff --git a/src/java/org/apache/cassandra/locator/LocalStrategy.java b/src/java/org/apache/cassandra/locator/LocalStrategy.java index 0e3a9185feda..64ab89c272e1 100644 --- a/src/java/org/apache/cassandra/locator/LocalStrategy.java +++ b/src/java/org/apache/cassandra/locator/LocalStrategy.java @@ -80,4 +80,10 @@ public Collection recognizedOptions() // LocalStrategy doesn't expect any options. return Collections.emptySet(); } + + @Override + public boolean isPartitioned() + { + return false; + } } diff --git a/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java b/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java index 1615d8e5f57f..5eb9716903af 100644 --- a/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java +++ b/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java @@ -17,19 +17,30 @@ */ package org.apache.cassandra.locator; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; import java.util.Map.Entry; +import java.util.Set; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.dht.Datacenters; import org.apache.cassandra.dht.Range; -import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; import org.apache.cassandra.locator.TokenMetadata.Topology; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ClientState; @@ -38,11 +49,6 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import com.google.common.collect.Sets; - /** *

    * This Replication Strategy takes a property file that gives the intended @@ -116,7 +122,8 @@ private static final class DatacenterEndpoints int rackCount, int nodeCount, EndpointsForRange.Builder replicas, - Set> racks) + Set> racks, + IEndpointSnitch snitch) { this.replicas = replicas; this.racks = racks; @@ -124,7 +131,7 @@ private static final class DatacenterEndpoints this.rfLeft = Math.min(rf.allReplicas, nodeCount); // If there aren't enough racks in this DC to fill the RF, we'll still use at least one node from each rack, // and the difference is to be filled by the first encountered nodes. - acceptableRackRepeats = rf.allReplicas - rackCount; + acceptableRackRepeats = snitch.acceptsNodesFromSameRack(rf.allReplicas, rackCount) ? rf.allReplicas - rackCount : 0; // if we have fewer replicas than rf calls for, reduce transients accordingly int reduceTransients = rf.allReplicas - this.rfLeft; @@ -178,8 +185,12 @@ boolean done() @Override public EndpointsForRange calculateNaturalReplicas(Token searchToken, TokenMetadata tokenMetadata) { - // we want to preserve insertion order so that the first added endpoint becomes primary ArrayList sortedTokens = tokenMetadata.sortedTokens(); + // handle the case of an empty ring and return an empty EndpointsForRange + if (sortedTokens.isEmpty()) + return EndpointsForRange.empty(new Range<>(tokenMetadata.partitioner.getMinimumToken(), tokenMetadata.partitioner.getMinimumToken())); + + // we want to preserve insertion order so that the first added endpoint becomes primary Token replicaEnd = TokenMetadata.firstToken(sortedTokens, searchToken); Token replicaStart = tokenMetadata.getPredecessor(replicaEnd); Range replicatedRange = new Range<>(replicaStart, replicaEnd); @@ -207,7 +218,7 @@ public EndpointsForRange calculateNaturalReplicas(Token searchToken, TokenMetada if (rf.allReplicas <= 0 || nodeCount <= 0) continue; - DatacenterEndpoints dcEndpoints = new DatacenterEndpoints(rf, sizeOrZero(racks.get(dc)), nodeCount, builder, seenRacks); + DatacenterEndpoints dcEndpoints = new DatacenterEndpoints(rf, sizeOrZero(racks.get(dc)), nodeCount, builder, seenRacks, snitch); dcs.put(dc, dcEndpoints); ++dcsToFill; } @@ -343,7 +354,7 @@ public void maybeWarnOnOptions(ClientState state) { if (!SchemaConstants.isSystemKeyspace(keyspaceName)) { - ImmutableMultimap dcsNodes = Multimaps.index(StorageService.instance.getTokenMetadata().getAllMembers(), snitch::getDatacenter); + ImmutableMultimap dcsNodes = Multimaps.index(StorageService.instance.getTokenMetadataForKeyspace(keyspaceName).getAllMembers(), snitch::getDatacenter); for (Entry e : this.configOptions.entrySet()) { diff --git a/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java b/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java index 3a9b161356f7..bf38f4eb46b9 100644 --- a/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java +++ b/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java @@ -24,8 +24,10 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.Callable; import com.google.common.collect.Sets; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,13 +35,10 @@ import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.ResourceWatcher; -import org.apache.cassandra.utils.WrappedRunnable; -import org.apache.commons.lang3.StringUtils; /** - *

    * Used to determine if two IP's are in the same datacenter or on the same rack. - *

    + * * Based on a properties file in the following format: * * 10.0.0.13=DC1:RAC2 @@ -71,13 +70,7 @@ public PropertyFileSnitch(int refreshPeriodInSeconds) throws ConfigurationExcept try { FBUtilities.resourceToFile(SNITCH_PROPERTIES_FILENAME); - Runnable runnable = new WrappedRunnable() - { - protected void runMayThrow() throws ConfigurationException - { - reloadConfiguration(true); - } - }; + Callable runnable = () -> reloadConfiguration(true); ResourceWatcher.watch(SNITCH_PROPERTIES_FILENAME, runnable, refreshPeriodInSeconds * 1000); } catch (ConfigurationException ex) @@ -137,7 +130,7 @@ public String getRack(InetAddressAndPort endpoint) return info[1]; } - public void reloadConfiguration(boolean isUpdate) throws ConfigurationException + public boolean reloadConfiguration(boolean isUpdate) throws ConfigurationException { HashMap reloadedMap = new HashMap<>(); String[] reloadedDefaultDCRack = null; @@ -161,9 +154,9 @@ public void reloadConfiguration(boolean isUpdate) throws ConfigurationException { String[] newDefault = value.split(":"); if (newDefault.length < 2) - reloadedDefaultDCRack = new String[] { "default", "default" }; + reloadedDefaultDCRack = new String[]{ "default", "default" }; else - reloadedDefaultDCRack = new String[] { newDefault[0].trim(), newDefault[1].trim() }; + reloadedDefaultDCRack = new String[]{ newDefault[0].trim(), newDefault[1].trim() }; } else { @@ -179,9 +172,9 @@ public void reloadConfiguration(boolean isUpdate) throws ConfigurationException } String[] token = value.split(":"); if (token.length < 2) - token = new String[] { "default", "default" }; + token = new String[]{ "default", "default" }; else - token = new String[] { token[0].trim(), token[1].trim() }; + token = new String[]{ token[0].trim(), token[1].trim() }; reloadedMap.put(host, token); } } @@ -198,19 +191,20 @@ public void reloadConfiguration(boolean isUpdate) throws ConfigurationException reloadedMap.put(localAddress, localInfo); if (isUpdate && !livenessCheck(reloadedMap, reloadedDefaultDCRack)) - return; + return false; - if (logger.isTraceEnabled()) + if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : reloadedMap.entrySet()) sb.append(entry.getKey()).append(':').append(Arrays.toString(entry.getValue())).append(", "); - logger.trace("Loaded network topology from property file: {}", StringUtils.removeEnd(sb.toString(), ", ")); + logger.debug("Loaded network topology from property file: {}", StringUtils.removeEnd(sb.toString(), ", ")); } - defaultDCRack = reloadedDefaultDCRack; endpointMap = reloadedMap; + + //noinspection ConstantConditions if (StorageService.instance != null) // null check tolerates circular dependency; see CASSANDRA-4145 { if (isUpdate) @@ -220,13 +214,18 @@ public void reloadConfiguration(boolean isUpdate) throws ConfigurationException } if (gossipStarted) + { StorageService.instance.gossipSnitchInfo(); + return true; + } + + return false; } /** * We cannot update rack or data-center for a live node, see CASSANDRA-10243. * - * @param reloadedMap - the new map of hosts to dc:rack properties + * @param reloadedMap - the new map of hosts to dc:rack properties * @param reloadedDefaultDCRack - the default dc:rack or null if no default * @return true if we can continue updating (no live host had dc or rack updated) */ @@ -236,14 +235,14 @@ private static boolean livenessCheck(HashMap reloa // host quickly and interrupt the loop. Otherwise we only check the live hosts that were either // in the old set or in the new set Set hosts = Arrays.equals(defaultDCRack, reloadedDefaultDCRack) - ? Sets.intersection(StorageService.instance.getLiveRingMembers(), // same default - Sets.union(endpointMap.keySet(), reloadedMap.keySet())) - : StorageService.instance.getLiveRingMembers(); // default updated + ? Sets.intersection(StorageService.instance.getLiveRingMembers(), // same default + Sets.union(endpointMap.keySet(), reloadedMap.keySet())) + : StorageService.instance.getLiveRingMembers(); // default updated for (InetAddressAndPort host : hosts) { - String[] origValue = endpointMap.containsKey(host) ? endpointMap.get(host) : defaultDCRack; - String[] updateValue = reloadedMap.containsKey(host) ? reloadedMap.get(host) : reloadedDefaultDCRack; + String[] origValue = endpointMap.getOrDefault(host, defaultDCRack); + String[] updateValue = reloadedMap.getOrDefault(host, reloadedDefaultDCRack); if (!Arrays.equals(origValue, updateValue)) { @@ -251,7 +250,7 @@ private static boolean livenessCheck(HashMap reloa origValue, updateValue, host); - return false; + return false; } } diff --git a/src/java/org/apache/cassandra/locator/ReplicaLayout.java b/src/java/org/apache/cassandra/locator/ReplicaLayout.java index 1e939b2fc42b..62fb3236042c 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaLayout.java +++ b/src/java/org/apache/cassandra/locator/ReplicaLayout.java @@ -19,6 +19,7 @@ package org.apache.cassandra.locator; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; @@ -26,6 +27,7 @@ import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.utils.FBUtilities; import java.util.Set; @@ -330,7 +332,7 @@ public static ReplicaLayout.ForTokenRead forTokenReadLiveSorted(AbstractReplicat { EndpointsForToken replicas = replicationStrategy.getNaturalReplicasForToken(token); replicas = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); - replicas = replicas.filter(FailureDetector.isReplicaAlive); + replicas = replicas.filter(IFailureDetector.isReplicaAlive); return new ReplicaLayout.ForTokenRead(replicationStrategy, replicas); } @@ -343,6 +345,18 @@ static ReplicaLayout.ForRangeRead forRangeReadLiveSorted(AbstractReplicationStra { EndpointsForRange replicas = replicationStrategy.getNaturalReplicas(range.right); replicas = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); + replicas = replicas.filter(IFailureDetector.isReplicaAlive); + return new ReplicaLayout.ForRangeRead(replicationStrategy, range, replicas); + } + + // note that: range may span multiple vnodes + public static ReplicaLayout.ForRangeRead forFullRangeReadLiveSorted(AbstractReplicationStrategy replicationStrategy, AbstractBounds range) + { + Preconditions.checkState(range.left.equals(DatabaseDescriptor.getPartitioner().getMinimumToken().minKeyBound())); + Preconditions.checkState(range.right.equals(DatabaseDescriptor.getPartitioner().getMinimumToken().minKeyBound())); + + EndpointsForRange replicas = replicationStrategy.getEndpointsForFullRange(); + replicas = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); replicas = replicas.filter(FailureDetector.isReplicaAlive); return new ReplicaLayout.ForRangeRead(replicationStrategy, range, replicas); } diff --git a/src/java/org/apache/cassandra/locator/ReplicaPlan.java b/src/java/org/apache/cassandra/locator/ReplicaPlan.java index 31dc2491fcf2..09ecc492e0c2 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaPlan.java +++ b/src/java/org/apache/cassandra/locator/ReplicaPlan.java @@ -18,15 +18,16 @@ package org.apache.cassandra.locator; +import java.util.function.Predicate; +import java.util.function.Supplier; + import com.google.common.collect.Iterables; + import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.AbstractBounds; -import java.util.function.Predicate; -import java.util.function.Supplier; - public interface ReplicaPlan, P extends ReplicaPlan> { Keyspace keyspace(); diff --git a/src/java/org/apache/cassandra/locator/ReplicaPlans.java b/src/java/org/apache/cassandra/locator/ReplicaPlans.java index 88e19c459796..db9cda308799 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaPlans.java +++ b/src/java/org/apache/cassandra/locator/ReplicaPlans.java @@ -30,6 +30,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -58,7 +59,7 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.UnavailableException; -import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexStatusManager; import org.apache.cassandra.schema.SchemaConstants; @@ -67,7 +68,6 @@ import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; import org.apache.cassandra.utils.FBUtilities; - import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.filter; import static org.apache.cassandra.db.ConsistencyLevel.EACH_QUORUM; @@ -83,11 +83,11 @@ public class ReplicaPlans { private static final Logger logger = LoggerFactory.getLogger(ReplicaPlans.class); - private static final Range FULL_TOKEN_RANGE = new Range<>(DatabaseDescriptor.getPartitioner().getMinimumToken(), DatabaseDescriptor.getPartitioner().getMinimumToken()); - private static final int REQUIRED_BATCHLOG_REPLICA_COUNT = Math.max(1, Math.min(2, CassandraRelevantProperties.REQUIRED_BATCHLOG_REPLICA_COUNT.getInt())); + private static final Range FULL_TOKEN_RANGE = new Range<>(DatabaseDescriptor.getPartitioner().getMinimumToken(), DatabaseDescriptor.getPartitioner().getMinimumToken()); + static { int batchlogReplicaCount = CassandraRelevantProperties.REQUIRED_BATCHLOG_REPLICA_COUNT.getInt(); @@ -127,7 +127,7 @@ public static boolean isSufficientLiveReplicasForRead(AbstractReplicationStrateg } } - static void assureSufficientLiveReplicasForRead(AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, Endpoints liveReplicas) throws UnavailableException + public static void assureSufficientLiveReplicasForRead(AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, Endpoints liveReplicas) throws UnavailableException { assureSufficientLiveReplicas(replicationStrategy, consistencyLevel, liveReplicas, consistencyLevel.blockFor(replicationStrategy), 1); } @@ -230,26 +230,31 @@ public static ReplicaPlan.ForWrite forLocalBatchlogWrite() } /** - * Requires that the provided endpoints are alive. Converts them to their relevant system replicas. - * Note that the liveAndDown collection and live are equal to the provided endpoints. + * Returns endpoint for a batchlog write. * * @param isAny if batch consistency level is ANY, in which case a local node will be picked + * @param preferLocalRack if true, a random endpoint from the local rack will be preferred for batch storage + * @param keyspaceName the name of the keyspace used to compute batch storage endpoints */ - public static ReplicaPlan.ForWrite forBatchlogWrite(boolean isAny) throws UnavailableException + public static ReplicaPlan.ForWrite forBatchlogWrite(boolean isAny, boolean preferLocalRack, String keyspaceName) throws UnavailableException { // A single case we write not for range or token, but multiple mutations to many tokens Token token = DatabaseDescriptor.getPartitioner().getMinimumToken(); - TokenMetadata.Topology topology = StorageService.instance.getTokenMetadata().cachedOnlyTokenMap().getTopology(); + TokenMetadata.Topology topology = StorageService.instance.getTokenMetadataForKeyspace(keyspaceName).cachedOnlyTokenMap().getTopology(); IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); Multimap localEndpoints = HashMultimap.create(topology.getDatacenterRacks() .get(snitch.getLocalDatacenter())); + // Replicas are picked manually: // - replicas should be alive according to the failure detector // - replicas should be in the local datacenter // - choose min(2, number of qualifying candiates above) // - allow the local node to be the only replica only if it's a single-node DC - Collection chosenEndpoints = filterBatchlogEndpoints(false, snitch.getLocalRack(), localEndpoints); + Collection chosenEndpoints = filterBatchlogEndpoints(preferLocalRack, snitch.getLocalRack(), localEndpoints); + + Predicate endpointPredicate = snitch.filterByAffinityForWrites(keyspaceName); + chosenEndpoints = chosenEndpoints.stream().filter(endpointPredicate).collect(Collectors.toSet());; // Batchlog is hosted by either one node or two nodes from different racks. ConsistencyLevel consistencyLevel = chosenEndpoints.size() == 1 ? ConsistencyLevel.ONE : ConsistencyLevel.TWO; @@ -281,10 +286,10 @@ public static Collection filterBatchlogEndpoints(boolean pre Multimap endpoints) { return DatabaseDescriptor.getBatchlogEndpointStrategy().useDynamicSnitchScores && DatabaseDescriptor.isDynamicEndpointSnitch() - ? filterBatchlogEndpointsDynamic(preferLocalRack,localRack, endpoints, FailureDetector.isEndpointAlive) + ? filterBatchlogEndpointsDynamic(preferLocalRack,localRack, endpoints, IFailureDetector.isEndpointAlive) : filterBatchlogEndpointsRandom(preferLocalRack, localRack, endpoints, Collections::shuffle, - FailureDetector.isEndpointAlive, + IFailureDetector.isEndpointAlive, ThreadLocalRandom.current()::nextInt); } @@ -315,7 +320,7 @@ private static ListMultimap validate(boolean preferL if (!(DatabaseDescriptor.getBatchlogEndpointStrategy().preferLocalRack || preferLocalRack) && validated.size() - validated.get(localRack).size() >= REQUIRED_BATCHLOG_REPLICA_COUNT) { - // we have enough endpoints in other racks + // if the local rack should not be preferred and there are enough nodes in other racks, remove it: validated.removeAll(localRack); } @@ -349,9 +354,9 @@ public static Collection filterBatchlogEndpointsRandom(boole if (validated.keySet().size() == 1) { /* - * we have only 1 `other` rack to select replicas from (whether it be the local rack or a single non-local rack) - * pick two random nodes from there; we are guaranteed to have at least two nodes in the single remaining rack - * because of the preceding if block. + * if we have only 1 `other` rack to select replicas from (whether it be the local rack or a single non-local rack), + * pick two random nodes from there and return early; + * we are guaranteed to have at least two nodes in the single remaining rack because of the above if block. */ List otherRack = Lists.newArrayList(validated.values()); shuffle.accept(otherRack); @@ -359,6 +364,7 @@ public static Collection filterBatchlogEndpointsRandom(boole } // randomize which racks we pick from if more than 2 remaining + Collection racks; if (validated.keySet().size() == REQUIRED_BATCHLOG_REPLICA_COUNT) { @@ -457,7 +463,7 @@ public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, ReplicaLayout.ForTokenWrite liveAndDown, Selector selector) throws UnavailableException { - return forWrite(keyspace, consistencyLevel, liveAndDown, FailureDetector.isReplicaAlive, selector); + return forWrite(keyspace, consistencyLevel, liveAndDown, IFailureDetector.isReplicaAlive, selector); } private static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, ReplicaLayout.ForTokenWrite liveAndDown, Predicate isAlive, Selector selector) throws UnavailableException @@ -470,6 +476,12 @@ public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel { assert liveAndDown.replicationStrategy() == live.replicationStrategy() : "ReplicaLayout liveAndDown and live should be derived from the same replication strategy."; + + // used by CNDB to filter out write replicas + IEndpointSnitch endpointSnitch = DatabaseDescriptor.getEndpointSnitch(); + Predicate writeEndpointFilter = endpointSnitch.filterByAffinityForWrites(keyspace.getName()); + live = live.filter(r -> writeEndpointFilter.test(r.endpoint())); + AbstractReplicationStrategy replicationStrategy = liveAndDown.replicationStrategy(); EndpointsForToken contacts = selector.select(consistencyLevel, liveAndDown, live); assureSufficientLiveReplicasForWrite(replicationStrategy, consistencyLevel, live.all(), liveAndDown.pending()); @@ -624,7 +636,7 @@ public static ReplicaPlan.ForPaxosWrite forPaxos(Keyspace keyspace, DecoratedKey liveAndDown = liveAndDown.filter(InOurDc.replicas()); } - ReplicaLayout.ForTokenWrite live = liveAndDown.filter(FailureDetector.isReplicaAlive); + ReplicaLayout.ForTokenWrite live = liveAndDown.filter(IFailureDetector.isReplicaAlive); // TODO: this should use assureSufficientReplicas int participants = liveAndDown.all().size(); @@ -720,8 +732,11 @@ public static ReplicaPlan.ForTokenRead forRead(Keyspace keyspace, SpeculativeRetryPolicy retry) { AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - EndpointsForToken candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, ReplicaLayout.forTokenReadLiveSorted(replicationStrategy, token).natural()); - EndpointsForToken contacts = contactForRead(replicationStrategy, consistencyLevel, retry.equals(AlwaysSpeculativeRetryPolicy.INSTANCE), candidates); + IEndpointSnitch endpointSnitch = DatabaseDescriptor.getEndpointSnitch(); + EndpointsForToken candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, ReplicaLayout.forTokenReadLiveSorted(replicationStrategy, token).natural()) + .filter(endpointSnitch.filterByAffinityForReads(keyspace.getName())); + EndpointsForToken contacts = contactForRead(replicationStrategy, consistencyLevel, retry.equals(AlwaysSpeculativeRetryPolicy.INSTANCE), candidates) + .filter(endpointSnitch.filterByAffinityForReads(keyspace.getName())); assureSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, contacts); return new ReplicaPlan.ForTokenRead(keyspace, replicationStrategy, consistencyLevel, candidates, contacts); @@ -741,8 +756,11 @@ public static ReplicaPlan.ForRangeRead forRangeRead(Keyspace keyspace, int vnodeCount) { AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - EndpointsForRange candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, ReplicaLayout.forRangeReadLiveSorted(replicationStrategy, range).natural()); - EndpointsForRange contacts = contactForRead(replicationStrategy, consistencyLevel, false, candidates); + IEndpointSnitch endpointSnitch = DatabaseDescriptor.getEndpointSnitch(); + EndpointsForRange candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, ReplicaLayout.forRangeReadLiveSorted(replicationStrategy, range).natural()) + .filter(endpointSnitch.filterByAffinityForReads(keyspace.getName())); + EndpointsForRange contacts = contactForRead(replicationStrategy, consistencyLevel, false, candidates) + .filter(endpointSnitch.filterByAffinityForReads(keyspace.getName())); assureSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, contacts); return new ReplicaPlan.ForRangeRead(keyspace, replicationStrategy, consistencyLevel, range, candidates, contacts, vnodeCount); diff --git a/src/java/org/apache/cassandra/locator/ReplicationFactor.java b/src/java/org/apache/cassandra/locator/ReplicationFactor.java index ee971d900dcf..35f2d16184a6 100644 --- a/src/java/org/apache/cassandra/locator/ReplicationFactor.java +++ b/src/java/org/apache/cassandra/locator/ReplicationFactor.java @@ -28,6 +28,8 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.nodes.IPeerInfo; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.utils.FBUtilities; public class ReplicationFactor @@ -73,7 +75,10 @@ static void validate(int totalRF, int transientRF) "Transient nodes are not allowed with multiple tokens"); Stream endpoints = Stream.concat(Gossiper.instance.getLiveMembers().stream(), Gossiper.instance.getUnreachableMembers().stream()); List badVersionEndpoints = endpoints.filter(Predicates.not(FBUtilities.getBroadcastAddressAndPort()::equals)) - .filter(endpoint -> Gossiper.instance.getReleaseVersion(endpoint) != null && Gossiper.instance.getReleaseVersion(endpoint).major < 4) + .map(endpoint -> Nodes.peers().get(endpoint)) + .filter(Objects::nonNull) + .filter(info -> info.getReleaseVersion() != null && info.getReleaseVersion().major < 4) + .map(IPeerInfo::getPeerAddressAndPort) .collect(Collectors.toList()); if (!badVersionEndpoints.isEmpty()) throw new IllegalArgumentException("Transient replication is not supported in mixed version clusters with nodes < 4.0. Bad nodes: " + badVersionEndpoints); diff --git a/src/java/org/apache/cassandra/locator/SystemReplicas.java b/src/java/org/apache/cassandra/locator/SystemReplicas.java index 456bae5a5272..421c5c680a61 100644 --- a/src/java/org/apache/cassandra/locator/SystemReplicas.java +++ b/src/java/org/apache/cassandra/locator/SystemReplicas.java @@ -19,20 +19,25 @@ package org.apache.cassandra.locator; import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import com.google.common.collect.Collections2; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; public class SystemReplicas { - private static final Map systemReplicas = new ConcurrentHashMap<>(); public static final Range FULL_RANGE = new Range<>(DatabaseDescriptor.getPartitioner().getMinimumToken(), DatabaseDescriptor.getPartitioner().getMinimumToken()); + // System replicas cache: entries expire after 1 day of being unused to avoid growing indefinitely + private static final Cache systemReplicas = Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.DAYS).build(); + + private static Replica createSystemReplica(InetAddressAndPort endpoint) { return new Replica(endpoint, FULL_RANGE, true); @@ -44,7 +49,7 @@ private static Replica createSystemReplica(InetAddressAndPort endpoint) */ public static Replica getSystemReplica(InetAddressAndPort endpoint) { - return systemReplicas.computeIfAbsent(endpoint, SystemReplicas::createSystemReplica); + return systemReplicas.get(endpoint, SystemReplicas::createSystemReplica); } public static EndpointsForRange getSystemReplicas(Collection endpoints) diff --git a/src/java/org/apache/cassandra/locator/TokenMetadata.java b/src/java/org/apache/cassandra/locator/TokenMetadata.java index 7cb3a449948f..df0b13fffbd2 100644 --- a/src/java/org/apache/cassandra/locator/TokenMetadata.java +++ b/src/java/org/apache/cassandra/locator/TokenMetadata.java @@ -42,7 +42,7 @@ import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.BiMultiValMap; @@ -152,7 +152,32 @@ private TokenMetadata(BiMultiValMap tokenToEndpointMa @VisibleForTesting public TokenMetadata cloneWithNewPartitioner(IPartitioner newPartitioner) { - return new TokenMetadata(tokenToEndpointMap, endpointToHostIdMap, topology, newPartitioner); + lock.readLock().lock(); + try + { + return new TokenMetadata(tokenToEndpointMap, endpointToHostIdMap, topology, newPartitioner); + } + finally + { + lock.readLock().unlock(); + } + } + + /** + * To be used by tests only (via {@link org.apache.cassandra.service.StorageService#setPartitionerUnsafe}). + */ + public TokenMetadata cloneWithNewSnitch(IEndpointSnitch snitch) + { + lock.readLock().lock(); + try + { + var clonedTopology = topology.unbuild().withSnitchSupplier(() -> snitch).build(); + return new TokenMetadata(tokenToEndpointMap, endpointToHostIdMap, clonedTopology, partitioner); + } + finally + { + lock.readLock().unlock(); + } } private ArrayList sortTokens() @@ -246,6 +271,82 @@ public void updateNormalTokens(Multimap endpointToken } } + /** + * Used by CNDB to update endpoint address for given normal tokens + */ + public void updateAddressForNormalTokens(Collection tokens, InetAddressAndPort existing, InetAddressAndPort current) + { + assert tokens != null && !tokens.isEmpty(); + assert existing != null && current != null; + + lock.writeLock().lock(); + try + { + // make sure token matches and current endpoint is not in the ring. + Multimap endpointToTokensMap = tokenToEndpointMap.inverse(); + Collection oldNodeTokens = endpointToTokensMap.get(existing); + if (!tokens.containsAll(oldNodeTokens) || !oldNodeTokens.containsAll(tokens)) + { + throw new RuntimeException(String.format("Node %s is trying to replace node %s with tokens %s with a " + + "different set of tokens %s.", current, existing, oldNodeTokens, + tokens)); + } + if (endpointToTokensMap.containsKey(current)) + { + throw new RuntimeException(String.format("Node %s already has normal tokens, can't replace %s", current, existing)); + } + + // make sure they are not bootstrap. + Multimap endpointToBootstrapTokensMap = bootstrapTokens.inverse(); + if (endpointToBootstrapTokensMap.containsKey(existing)) + { + throw new RuntimeException(String.format("Node %s is trying to replace node %s with bootstrapping tokens.", current, existing)); + } + if (endpointToBootstrapTokensMap.containsKey(current)) + { + throw new RuntimeException(String.format("Node %s already has bootstrap tokens, can't replace %s", current, existing)); + } + + // make sure they are not leaving + if (leavingEndpoints.contains(existing)) + { + throw new RuntimeException(String.format("Node %s is trying to replace node %s in leaving state.", current, existing)); + } + if (leavingEndpoints.contains(current)) + { + throw new RuntimeException(String.format("Node %s is already in leaving, can't replace %s", current, existing)); + } + + // make sure they are not moving + if (movingEndpoints.stream().anyMatch(p -> p.right.equals(existing))) + { + throw new RuntimeException(String.format("Node %s is trying to replace node %s in moving state.", current, existing)); + } + if (movingEndpoints.stream().anyMatch(p -> p.right.equals(current))) + { + throw new RuntimeException(String.format("Node %s is already in moving, can't replace %s", current, existing)); + } + + // Note that there is no need to validate replacementToOriginal which is updated together with bootstrapTokens + Topology.Builder topologyBuilder = topology.unbuild(); + topologyBuilder.removeEndpoint(existing); + topologyBuilder.addEndpoint(current); + + for (Token token : tokens) + tokenToEndpointMap.put(token, current); + + topology = topologyBuilder.build(); + + sortedTokens = sortTokens(); + } + finally + { + lock.writeLock().unlock(); + } + + + } + /** * Store an end-point to host ID mapping. Each ID must be unique, and * cannot be changed after the fact. @@ -283,13 +384,53 @@ public void updateHostIds(Map hostIdToEndpointMap) } } + + /** + * Used by CNDB to update endpoint address for given host id + */ + public void updateAddressForHostId(UUID hostId, InetAddressAndPort existing, InetAddressAndPort current) + { + assert hostId != null; + assert existing != null; + assert current != null; + + lock.writeLock().lock(); + try + { + InetAddressAndPort storedEp = endpointToHostIdMap.inverse().get(hostId); + if (storedEp != null && !storedEp.equals(existing)) + { + throw new RuntimeException(String.format("Endpoint mismatch between stored endpoint %s and expected endpoint %s (id=%s)", + storedEp, + existing, + hostId)); + } + + UUID storedId = endpointToHostIdMap.get(existing); + if (storedId != null && !storedId.equals(hostId)) + { + throw new RuntimeException(String.format("Host id mismatch for existing endpoint %s and existing id %s and provided id=%s)", + existing, + storedId, + hostId)); + } + + endpointToHostIdMap.remove(existing); + endpointToHostIdMap.forcePut(current, hostId); + } + finally + { + lock.writeLock().unlock(); + } + + } private void updateEndpointToHostIdMap(UUID hostId, InetAddressAndPort endpoint) { InetAddressAndPort storedEp = endpointToHostIdMap.inverse().get(hostId); if (storedEp != null) { - if (!storedEp.equals(endpoint) && (FailureDetector.instance.isAlive(storedEp))) + if (!storedEp.equals(endpoint) && (IFailureDetector.instance.isAlive(storedEp))) { throw new RuntimeException(String.format("Host ID collision between active endpoint %s and %s (id=%s)", storedEp, @@ -387,6 +528,9 @@ private void addBootstrapTokens(Collection tokens, InetAddressAndPort end } } + /** + * Used by C* node replacement to add replacement tokens as bootstrapping tokens + */ public void addReplaceTokens(Collection replacingTokens, InetAddressAndPort newNode, InetAddressAndPort oldNode) { assert replacingTokens != null && !replacingTokens.isEmpty(); @@ -1164,6 +1308,19 @@ public int getSizeOfMovingEndpoints() } } + public Set getAllRingMembers() + { + lock.readLock().lock(); + try + { + return ImmutableSet.copyOf(tokenToEndpointMap.valueSet()); + } + finally + { + lock.readLock().unlock(); + } + } + public static int firstTokenIndex(final ArrayList ring, Token start, boolean insertMin) { assert ring.size() > 0; @@ -1492,7 +1649,7 @@ static Builder builder(Supplier snitchSupplier) static Topology empty() { - return builder(() -> DatabaseDescriptor.getEndpointSnitch()).build(); + return builder(DatabaseDescriptor::getEndpointSnitch).build(); } private static class Builder @@ -1503,7 +1660,7 @@ private static class Builder private final Map> dcRacks; /** reverse-lookup map for endpoint to current known dc/rack assignment */ private final Map> currentLocations; - private final Supplier snitchSupplier; + private Supplier snitchSupplier; Builder(Supplier snitchSupplier) { @@ -1575,7 +1732,7 @@ private void doRemoveEndpoint(InetAddressAndPort ep, Pair curren Builder updateEndpoint(InetAddressAndPort ep) { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); + IEndpointSnitch snitch = snitchSupplier.get(); if (snitch == null || !currentLocations.containsKey(ep)) return this; @@ -1585,7 +1742,7 @@ Builder updateEndpoint(InetAddressAndPort ep) Builder updateEndpoints() { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); + IEndpointSnitch snitch = snitchSupplier.get(); if (snitch == null) return this; @@ -1607,6 +1764,12 @@ private void updateEndpoint(InetAddressAndPort ep, IEndpointSnitch snitch) doAddEndpoint(ep, dc, rack); } + Builder withSnitchSupplier(Supplier snitchSupplier) + { + this.snitchSupplier = snitchSupplier; + return this; + } + Topology build() { return new Topology(this); diff --git a/src/java/org/apache/cassandra/locator/TokenMetadataProvider.java b/src/java/org/apache/cassandra/locator/TokenMetadataProvider.java new file mode 100644 index 000000000000..085ec6b47abd --- /dev/null +++ b/src/java/org/apache/cassandra/locator/TokenMetadataProvider.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.locator; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_TMD_PROVIDER_PROPERTY; + +/** + * Provides access to the {@link TokenMetadata} instance used by this node. + */ +public interface TokenMetadataProvider +{ + TokenMetadataProvider instance = CUSTOM_TMD_PROVIDER_PROPERTY.isPresent() + ? FBUtilities.construct(CUSTOM_TMD_PROVIDER_PROPERTY.getString(), + "Token Metadata Provider") + : new DefaultTokenMetadataProvider(); + + /** + * Returns the default TokenMetadata instance. + */ + TokenMetadata getTokenMetadata(); + + /** + * Returns the per-keyspace TokenMetadata instance. + */ + TokenMetadata getTokenMetadataForKeyspace(String keyspace); + + @VisibleForTesting + /** @deprecated See STAR-1032 */ + @Deprecated(forRemoval = true, since = "CC 4.0") // since we can select TMDP implementation via config, this method is no longer needed + void replaceTokenMetadata(TokenMetadata newTokenMetadata); +} diff --git a/src/java/org/apache/cassandra/metrics/AllRequestsMetrics.java b/src/java/org/apache/cassandra/metrics/AllRequestsMetrics.java new file mode 100644 index 000000000000..ae4ee7dfb7e9 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/AllRequestsMetrics.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.cassandra.metrics; + + +import com.codahale.metrics.Meter; + +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + + +public class AllRequestsMetrics extends ClientRequestMetrics +{ + public final Meter invalid; + public final Meter otherErrors; + + public AllRequestsMetrics(String scope, String prefix) + { + super(scope, prefix); + invalid = Metrics.meter(factory.createMetricName(namePrefix + "Invalid")); + otherErrors = Metrics.meter(factory.createMetricName(namePrefix + "OtherErrors")); + } + + @Override + public void release() + { + super.release(); + Metrics.remove(factory.createMetricName(namePrefix + "Invalid")); + Metrics.remove(factory.createMetricName(namePrefix + "OtherErrors")); + } +} diff --git a/src/java/org/apache/cassandra/metrics/AutoRepairMetrics.java b/src/java/org/apache/cassandra/metrics/AutoRepairMetrics.java new file mode 100644 index 000000000000..e1355dbfa6cd --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/AutoRepairMetrics.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType; +import org.apache.cassandra.repair.autorepair.AutoRepairUtils; +import org.apache.cassandra.repair.autorepair.AutoRepair; +import org.apache.cassandra.service.AutoRepairService; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +/** + * Metrics related to AutoRepair. + */ +public class AutoRepairMetrics +{ + public final Gauge repairsInProgress; + public final Gauge nodeRepairTimeInSec; + public final Gauge clusterRepairTimeInSec; + public final Gauge longestUnrepairedSec; + public final Gauge repairStartLagSec; + public final Gauge succeededTokenRangesCount; + public final Gauge failedTokenRangesCount; + public final Gauge skippedTokenRangesCount; + public final Gauge skippedTablesCount; + public final Gauge totalMVTablesConsideredForRepair; + public final Gauge totalDisabledRepairTables; + public final Gauge totalBytesToRepair; + public final Gauge bytesAlreadyRepaired; + public final Gauge totalKeyspaceRepairPlansToRepair; + public final Gauge keyspaceRepairPlansAlreadyRepaired; + + public Counter repairTurnMyTurn; + public Counter repairTurnMyTurnDueToPriority; + public Counter repairTurnMyTurnForceRepair; + public Counter repairDelayedByReplica; + public Counter repairDelayedBySchedule; + + private final RepairType repairType; + + private volatile int repairStartLagSecVal; + + public AutoRepairMetrics(RepairType repairType) + { + this.repairType = repairType; + AutoRepairMetricsFactory factory = new AutoRepairMetricsFactory(repairType); + + repairsInProgress = Metrics.register(factory.createMetricName("RepairsInProgress"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).isRepairInProgress() ? 1 : 0; + } + }); + + nodeRepairTimeInSec = Metrics.register(factory.createMetricName("NodeRepairTimeInSec"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getNodeRepairTimeInSec(); + } + }); + + clusterRepairTimeInSec = Metrics.register(factory.createMetricName("ClusterRepairTimeInSec"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getClusterRepairTimeInSec(); + } + }); + + skippedTokenRangesCount = Metrics.register(factory.createMetricName("SkippedTokenRangesCount"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getSkippedTokenRangesCount(); + } + }); + + skippedTablesCount = Metrics.register(factory.createMetricName("SkippedTablesCount"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getSkippedTablesCount(); + } + }); + + longestUnrepairedSec = Metrics.register(factory.createMetricName("LongestUnrepairedSec"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getLongestUnrepairedSec(); + } + }); + + repairStartLagSec = Metrics.register(factory.createMetricName("RepairStartLagSec"), new Gauge() + { + public Integer getValue() + { + return repairStartLagSecVal; + } + }); + + succeededTokenRangesCount = Metrics.register(factory.createMetricName("SucceededTokenRangesCount"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getSucceededTokenRangesCount(); + } + }); + + failedTokenRangesCount = Metrics.register(factory.createMetricName("FailedTokenRangesCount"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getFailedTokenRangesCount(); + } + }); + + repairTurnMyTurn = Metrics.counter(factory.createMetricName("RepairTurnMyTurn")); + repairTurnMyTurnDueToPriority = Metrics.counter(factory.createMetricName("RepairTurnMyTurnDueToPriority")); + repairTurnMyTurnForceRepair = Metrics.counter(factory.createMetricName("RepairTurnMyTurnForceRepair")); + + repairDelayedByReplica = Metrics.counter(factory.createMetricName("RepairDelayedByReplica")); + repairDelayedBySchedule = Metrics.counter(factory.createMetricName("RepairDelayedBySchedule")); + + totalMVTablesConsideredForRepair = Metrics.register(factory.createMetricName("TotalMVTablesConsideredForRepair"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getTotalMVTablesConsideredForRepair(); + } + }); + + totalDisabledRepairTables = Metrics.register(factory.createMetricName("TotalDisabledRepairTables"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getTotalDisabledTablesRepairCount(); + } + }); + totalBytesToRepair = Metrics.register(factory.createMetricName("TotalBytesToRepair"), new Gauge() + { + public Long getValue() + { + return AutoRepair.instance.getRepairState(repairType).getTotalBytesToRepair(); + } + }); + bytesAlreadyRepaired = Metrics.register(factory.createMetricName("BytesAlreadyRepaired"), new Gauge() + { + public Long getValue() + { + return AutoRepair.instance.getRepairState(repairType).getBytesAlreadyRepaired(); + } + }); + totalKeyspaceRepairPlansToRepair = Metrics.register(factory.createMetricName("TotalKeyspaceRepairPlansToRepair"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getTotalKeyspaceRepairPlansToRepair(); + } + }); + keyspaceRepairPlansAlreadyRepaired = Metrics.register(factory.createMetricName("KeyspaceRepairPlansAlreadyRepaired"), new Gauge() + { + public Integer getValue() + { + return AutoRepair.instance.getRepairState(repairType).getKeyspaceRepairPlansAlreadyRepaired(); + } + }); + } + + public void recordTurn(AutoRepairUtils.RepairTurn turn) + { + switch (turn) + { + case MY_TURN: + repairTurnMyTurn.inc(); + break; + case MY_TURN_FORCE_REPAIR: + repairTurnMyTurnForceRepair.inc(); + break; + case MY_TURN_DUE_TO_PRIORITY: + repairTurnMyTurnDueToPriority.inc(); + break; + default: + throw new RuntimeException(String.format("Unrecoginized turn: %s", turn.name())); + } + this.repairStartLagSecVal = 0; + } + + /** + * Record perceived lag in scheduling repair. + *

    + * Takes the current time and subtracts it from the given last repair finish time. It then compares the difference + * with the min repair interval for this repair type, and if that value is greater than 0, records it. + */ + public void recordRepairStartLag(long lastFinishTimeInMs) + { + long now = AutoRepair.instance.currentTimeMs(); + long deltaFinish = now - lastFinishTimeInMs; + long deltaMinRepairInterval = deltaFinish - AutoRepairService.instance + .getAutoRepairConfig().getRepairMinInterval(repairType) + .toMilliseconds(); + this.repairStartLagSecVal = deltaMinRepairInterval > 0 ? (int) MILLISECONDS.toSeconds(deltaMinRepairInterval) : 0; + } + + @VisibleForTesting + protected static class AutoRepairMetricsFactory implements MetricNameFactory + { + private static final String TYPE = "AutoRepair"; + @VisibleForTesting + protected final String repairType; + + protected AutoRepairMetricsFactory(RepairType repairType) + { + this.repairType = repairType.toString().toLowerCase(); + } + + @Override + public CassandraMetricsRegistry.MetricName createMetricName(String metricName) + { + StringBuilder mbeanName = new StringBuilder(); + mbeanName.append(DefaultNameFactory.GROUP_NAME).append(':'); + mbeanName.append("type=").append(TYPE); + mbeanName.append(",name=").append(metricName); + mbeanName.append(",repairType=").append(repairType); + + StringBuilder scope = new StringBuilder(); + scope.append("repairType=").append(repairType); + + return new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME, TYPE.toLowerCase(), + metricName, scope.toString(), mbeanName.toString()); + } + } +} diff --git a/src/java/org/apache/cassandra/metrics/AutoRepairMetricsManager.java b/src/java/org/apache/cassandra/metrics/AutoRepairMetricsManager.java new file mode 100644 index 000000000000..e97ce34e5a73 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/AutoRepairMetricsManager.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * AutoRepair metrics manager holding all the auto-repair related metrics. + */ +public class AutoRepairMetricsManager +{ + private static final Map metrics = new ConcurrentHashMap<>(); + + public static AutoRepairMetrics getMetrics(RepairType repairType) + { + return metrics.computeIfAbsent(repairType, k -> new AutoRepairMetrics(repairType)); + } +} diff --git a/src/java/org/apache/cassandra/metrics/BatchMetrics.java b/src/java/org/apache/cassandra/metrics/BatchMetrics.java index 9bea16211694..db69cf7e126b 100644 --- a/src/java/org/apache/cassandra/metrics/BatchMetrics.java +++ b/src/java/org/apache/cassandra/metrics/BatchMetrics.java @@ -17,7 +17,9 @@ */ package org.apache.cassandra.metrics; +import com.codahale.metrics.Counter; import com.codahale.metrics.Histogram; +import org.apache.cassandra.cql3.statements.BatchStatement; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; @@ -25,14 +27,54 @@ public class BatchMetrics { private static final MetricNameFactory factory = new DefaultNameFactory("Batch"); + public final Counter numLoggedBatches; + public final Counter numUnloggedBatches; + public final Counter numCounterBatches; + public final Histogram partitionsPerLoggedBatch; public final Histogram partitionsPerUnloggedBatch; public final Histogram partitionsPerCounterBatch; + public final Histogram columnsPerLoggedBatch; + public final Histogram columnsPerUnloggedBatch; + public final Histogram columnsPerCounterBatch; + public BatchMetrics() { + numLoggedBatches = Metrics.counter(factory.createMetricName("NumLoggedBatches")); + numUnloggedBatches = Metrics.counter(factory.createMetricName("NumUnloggedBatches")); + numCounterBatches = Metrics.counter(factory.createMetricName("NumCounterBatches")); + partitionsPerLoggedBatch = Metrics.histogram(factory.createMetricName("PartitionsPerLoggedBatch"), false); partitionsPerUnloggedBatch = Metrics.histogram(factory.createMetricName("PartitionsPerUnloggedBatch"), false); partitionsPerCounterBatch = Metrics.histogram(factory.createMetricName("PartitionsPerCounterBatch"), false); + + columnsPerLoggedBatch = Metrics.histogram(factory.createMetricName("ColumnsPerLoggedBatch"), false); + columnsPerUnloggedBatch = Metrics.histogram(factory.createMetricName("ColumnsPerUnloggedBatch"), false); + columnsPerCounterBatch = Metrics.histogram(factory.createMetricName("ColumnsPerCounterBatch"), false); + } + + public void update(BatchStatement.Type batchType, int updatedPartitions, int updatedColumns) + { + switch (batchType) + { + case LOGGED: + numLoggedBatches.inc(); + partitionsPerLoggedBatch.update(updatedPartitions); + columnsPerLoggedBatch.update(updatedColumns); + break; + case COUNTER: + numCounterBatches.inc(); + partitionsPerCounterBatch.update(updatedPartitions); + columnsPerCounterBatch.update(updatedColumns); + break; + case UNLOGGED: + numUnloggedBatches.inc(); + partitionsPerUnloggedBatch.update(updatedPartitions); + columnsPerUnloggedBatch.update(updatedColumns); + break; + default: + throw new IllegalStateException("Unexpected batch type: " + batchType); + } } } diff --git a/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java b/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java index 71373b35e886..49fd8ff33ee0 100644 --- a/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java +++ b/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java @@ -17,51 +17,34 @@ */ package org.apache.cassandra.metrics; -import com.codahale.metrics.Gauge; -import com.codahale.metrics.Meter; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.utils.memory.BufferPool; -import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; - -public class BufferPoolMetrics +public interface BufferPoolMetrics { - /** Total number of hits */ - public final Meter hits; - - /** Total number of misses */ - public final Meter misses; - - /** Total threshold for a certain type of buffer pool*/ - public final Gauge capacity; - - /** Total size of buffer pools, in bytes, including overflow allocation */ - public final Gauge size; - - /** Total size, in bytes, of active buffered being used from the pool currently + overflow */ - public final Gauge usedSize; - - /** - * Total size, in bytes, of direct or heap buffers allocated by the pool but not part of the pool - * either because they are too large to fit or because the pool has exceeded its maximum limit or because it's - * on-heap allocation. - */ - public final Gauge overflowSize; - - public BufferPoolMetrics(String scope, BufferPool bufferPool) + static BufferPoolMetrics create(String name, BufferPool bufferPool) { - MetricNameFactory factory = new DefaultNameFactory("BufferPool", scope); + return CassandraRelevantProperties.USE_MICROMETER.getBoolean() + ? new MicrometerBufferPoolMetrics(name, bufferPool) + : new CodahaleBufferPoolMetrics(name, bufferPool); + } - hits = Metrics.meter(factory.createMetricName("Hits")); + void markHit(); - misses = Metrics.meter(factory.createMetricName("Misses")); + long hits(); - capacity = Metrics.register(factory.createMetricName("Capacity"), bufferPool::memoryUsageThreshold); + void markMissed(); - overflowSize = Metrics.register(factory.createMetricName("OverflowSize"), bufferPool::overflowMemoryInBytes); + long misses(); - usedSize = Metrics.register(factory.createMetricName("UsedSize"), bufferPool::usedSizeInBytes); + long overflowSize(); - size = Metrics.register(factory.createMetricName("Size"), bufferPool::sizeInBytes); - } + long usedSize(); + long size(); + + /** + * used to register alias for 3.0/3.11 compatibility + */ + void register3xAlias(); } diff --git a/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java b/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java index 654bb059d16e..2a069d7e88e3 100644 --- a/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java @@ -30,19 +30,50 @@ public class CASClientRequestMetrics extends ClientRequestMetrics public final Counter unfinishedCommit; public final Meter unknownResult; - public CASClientRequestMetrics(String scope) + // latencies for 4 paxos phases + public final LatencyMetrics prepareLatency; + public final LatencyMetrics createProposalLatency; + public final LatencyMetrics proposeLatency; + public final LatencyMetrics commitLatency; + + // latency for backoff when there is contention + public final LatencyMetrics contentionBackoffLatency; + + // num of replicas that are missing MRC + public final Counter missingMostRecentCommit; + + public CASClientRequestMetrics(String scope, String namePrefix) { - super(scope); - contention = Metrics.histogram(factory.createMetricName("ContentionHistogram"), false); - unfinishedCommit = Metrics.counter(factory.createMetricName("UnfinishedCommit")); - unknownResult = Metrics.meter(factory.createMetricName("UnknownResult")); + super(scope, namePrefix); + + contention = Metrics.histogram(factory.createMetricName(namePrefix + "ContentionHistogram"), false); + unfinishedCommit = Metrics.counter(factory.createMetricName(namePrefix + "UnfinishedCommit")); + unknownResult = Metrics.meter(factory.createMetricName(namePrefix + "UnknownResult")); + + prepareLatency = new LatencyMetrics(factory, namePrefix + "Prepare"); + createProposalLatency = new LatencyMetrics(factory, namePrefix + "CreateProposal"); + proposeLatency = new LatencyMetrics(factory, namePrefix + "Propose"); + commitLatency = new LatencyMetrics(factory, namePrefix + "Commit"); + + contentionBackoffLatency = new LatencyMetrics(factory, namePrefix + "ContentionBackoff"); + + missingMostRecentCommit = Metrics.counter(factory.createMetricName(namePrefix + "MissingMostRecentCommit")); } public void release() { super.release(); - Metrics.remove(factory.createMetricName("ContentionHistogram")); - Metrics.remove(factory.createMetricName("UnfinishedCommit")); - Metrics.remove(factory.createMetricName("UnknownResult")); + Metrics.remove(factory.createMetricName(namePrefix + "ContentionHistogram")); + Metrics.remove(factory.createMetricName(namePrefix + "UnfinishedCommit")); + Metrics.remove(factory.createMetricName(namePrefix + "UnknownResult")); + + prepareLatency.release(); + createProposalLatency.release(); + proposeLatency.release(); + commitLatency.release(); + + contentionBackoffLatency.release(); + + Metrics.remove(factory.createMetricName(namePrefix + "MissingMostRecentCommit")); } } diff --git a/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java b/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java index 87c0d5354132..5789f27e8407 100644 --- a/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java @@ -36,20 +36,20 @@ public class CASClientWriteRequestMetrics extends CASClientRequestMetrics public final Counter conditionNotMet; - public CASClientWriteRequestMetrics(String scope) + public CASClientWriteRequestMetrics(String scope, String namePrefix) { - super(scope); - mutationSize = Metrics.histogram(factory.createMetricName("MutationSizeHistogram"), false); + super(scope, namePrefix); + mutationSize = Metrics.histogram(factory.createMetricName(namePrefix + "MutationSizeHistogram"), false); // scope for this metric was changed in 4.0; adding backward compatibility - conditionNotMet = Metrics.counter(factory.createMetricName("ConditionNotMet"), - DefaultNameFactory.createMetricName("ClientRequest", "ConditionNotMet", "CASRead")); + conditionNotMet = Metrics.counter(factory.createMetricName(namePrefix + "ConditionNotMet"), + DefaultNameFactory.createMetricName("ClientRequest", namePrefix + "ConditionNotMet", "CASRead")); } public void release() { super.release(); - Metrics.remove(factory.createMetricName("ConditionNotMet"), - DefaultNameFactory.createMetricName("ClientRequest", "ConditionNotMet", "CASRead")); - Metrics.remove(factory.createMetricName("MutationSizeHistogram")); + Metrics.remove(factory.createMetricName(namePrefix + "ConditionNotMet"), + DefaultNameFactory.createMetricName("ClientRequest", namePrefix + "ConditionNotMet", "CASRead")); + Metrics.remove(factory.createMetricName(namePrefix + "MutationSizeHistogram")); } } diff --git a/src/java/org/apache/cassandra/metrics/CacheMetrics.java b/src/java/org/apache/cassandra/metrics/CacheMetrics.java index 34746bcee94b..7402386c6c27 100644 --- a/src/java/org/apache/cassandra/metrics/CacheMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CacheMetrics.java @@ -17,98 +17,44 @@ */ package org.apache.cassandra.metrics; -import java.util.function.DoubleSupplier; - -import com.google.common.annotations.VisibleForTesting; - -import com.codahale.metrics.*; import org.apache.cassandra.cache.CacheSize; +import org.apache.cassandra.service.CacheService; -import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; +import static org.apache.cassandra.config.CassandraRelevantProperties.USE_MICROMETER; /** * Metrics for {@code ICache}. */ -public class CacheMetrics +public interface CacheMetrics { - /** Cache capacity in bytes */ - public final Gauge capacity; - /** Total size of cache, in bytes */ - public final Gauge size; - /** Total number of cache entries */ - public final Gauge entries; + static CacheMetrics create(CacheService.CacheType cacheType, CacheSize cache) + { + return USE_MICROMETER.getBoolean() + ? new MicrometerCacheMetrics(cacheType.micrometerMetricsPrefix(), cache) + : new CodahaleCacheMetrics(cacheType.toString(), cache); + } - /** Total number of cache hits */ - public final Meter hits; - /** Total number of cache misses */ - public final Meter misses; - /** Total number of cache requests */ - public final Meter requests; + long requests(); - /** all time cache hit rate */ - public final Gauge hitRate; - /** 1m hit rate */ - public final Gauge oneMinuteHitRate; - /** 5m hit rate */ - public final Gauge fiveMinuteHitRate; - /** 15m hit rate */ - public final Gauge fifteenMinuteHitRate; + long capacity(); - protected final MetricNameFactory factory; + long size(); - /** - * Create metrics for given cache. - * - * @param type Type of Cache to identify metrics. - * @param cache Cache to measure metrics - */ - public CacheMetrics(String type, CacheSize cache) - { - factory = new DefaultNameFactory("Cache", type); + long entries(); - capacity = Metrics.register(factory.createMetricName("Capacity"), cache::capacity); - size = Metrics.register(factory.createMetricName("Size"), cache::weightedSize); - entries = Metrics.register(factory.createMetricName("Entries"), cache::size); + long hits(); - hits = Metrics.meter(factory.createMetricName("Hits")); - misses = Metrics.meter(factory.createMetricName("Misses")); - requests = Metrics.meter(factory.createMetricName("Requests")); + long misses(); - hitRate = - Metrics.register(factory.createMetricName("HitRate"), - ratioGauge(hits::getCount, requests::getCount)); - oneMinuteHitRate = - Metrics.register(factory.createMetricName("OneMinuteHitRate"), - ratioGauge(hits::getOneMinuteRate, requests::getOneMinuteRate)); - fiveMinuteHitRate = - Metrics.register(factory.createMetricName("FiveMinuteHitRate"), - ratioGauge(hits::getFiveMinuteRate, requests::getFiveMinuteRate)); - fifteenMinuteHitRate = - Metrics.register(factory.createMetricName("FifteenMinuteHitRate"), - ratioGauge(hits::getFifteenMinuteRate, requests::getFifteenMinuteRate)); - } + double hitRate(); - @VisibleForTesting - public void reset() - { - // No actual reset happens. The Meter counter is put to zero but will not reset the moving averages - // It rather injects a weird value into them. - // This method is being only used by CacheMetricsTest and CachingBench so fixing this issue was acknowledged - // but not considered mandatory to be fixed now (CASSANDRA-16228) - hits.mark(-hits.getCount()); - misses.mark(-misses.getCount()); - requests.mark(-requests.getCount()); - } + double hitOneMinuteRate(); + double hitFiveMinuteRate(); + double hitFifteenMinuteRate(); - private static RatioGauge ratioGauge(DoubleSupplier numeratorSupplier, DoubleSupplier denominatorSupplier) - { - return new RatioGauge() - { - @Override - public Ratio getRatio() - { - return Ratio.of(numeratorSupplier.getAsDouble(), denominatorSupplier.getAsDouble()); - } - }; - } + double requestsFifteenMinuteRate(); + + void recordHits(int count); + + void recordMisses(int count); } diff --git a/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java b/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java index 598e484f6bdf..32cfdfdd11ab 100644 --- a/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java +++ b/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java @@ -68,20 +68,30 @@ public Counter counter(MetricName name, MetricName alias) } public Meter meter(MetricName name) + { + return meter(name, false); + } + + public Meter meter(MetricName name, boolean gaugeCompatible) { Meter meter = meter(name.getMetricName()); - registerMBean(meter, name.getMBeanName()); + registerMBean(meter, name.getMBeanName(), gaugeCompatible); return meter; } - public Meter meter(MetricName name, MetricName alias) + public Meter meter(MetricName name, MetricName alias, boolean gaugeCompatible) { - Meter meter = meter(name); - registerAlias(name, alias); + Meter meter = meter(name, gaugeCompatible); + registerAlias(name, alias, gaugeCompatible); return meter; } + public Meter meter(MetricName name, MetricName alias) + { + return meter(name, alias, false); + } + public Histogram histogram(MetricName name, boolean considerZeroes) { Histogram histogram = register(name, new ClearableHistogram(new DecayingEstimatedHistogramReservoir(considerZeroes))); @@ -217,6 +227,11 @@ public boolean remove(MetricName name, MetricName... aliases) } public void registerMBean(Metric metric, ObjectName name) + { + registerMBean(metric, name, false); + } + + public void registerMBean(Metric metric, ObjectName name, boolean gaugeCompatible) { AbstractBean mbean; @@ -229,7 +244,18 @@ else if (metric instanceof Histogram) else if (metric instanceof Timer) mbean = new JmxTimer((Timer) metric, name, TimeUnit.SECONDS, DEFAULT_TIMER_UNIT); else if (metric instanceof Metered) - mbean = new JmxMeter((Metered) metric, name, TimeUnit.SECONDS); + { + // If a gauge compatible meter is requested, create a special implementation which + // also yields a 'Value' attribute for backwards compatibility. + if (gaugeCompatible) + { + mbean = new JmxMeterGaugeCompatible((Metered) metric, name, TimeUnit.SECONDS); + } + else + { + mbean = new JmxMeter((Metered) metric, name, TimeUnit.SECONDS); + } + } else throw new IllegalArgumentException("Unknown metric type: " + metric.getClass()); @@ -238,11 +264,16 @@ else if (metric instanceof Metered) } private void registerAlias(MetricName existingName, MetricName aliasName) + { + registerAlias(existingName, aliasName, false); + } + + private void registerAlias(MetricName existingName, MetricName aliasName, boolean gaugeCompatible) { Metric existing = Metrics.getMetrics().get(existingName.getMetricName()); assert existing != null : existingName + " not registered"; - registerMBean(existing, aliasName.getMBeanName()); + registerMBean(existing, aliasName.getMBeanName(), gaugeCompatible); } private void removeAlias(MetricName name) @@ -334,6 +365,8 @@ public interface JmxHistogramMBean extends MetricMBean long[] values(); long[] getRecentValues(); + + default void clear() {} } private static class JmxHistogram extends AbstractBean implements JmxHistogramMBean @@ -435,6 +468,13 @@ public synchronized long[] getRecentValues() last = now; return delta; } + + @Override + public void clear() + { + if (metric instanceof ClearableHistogram) + ((ClearableHistogram) metric).clear(); + } } public interface JmxCounterMBean extends MetricMBean @@ -531,6 +571,30 @@ private String calculateRateUnit(TimeUnit unit) } } + public interface JmxMeterGaugeCompatibleMBean extends JmxMeterMBean, JmxGaugeMBean {} + + /** + * An implementation of {@link JmxMeter} that is compatible with {@link JmxGaugeMBean} in that it also + * implements {@link JmxGaugeMBean}. This is useful for metrics that were migrated from {@link JmxGauge} + * to {@link JmxMeter} like {@link TableMetrics#bytesAnticompacted} and + * {@link TableMetrics#bytesMutatedAnticompaction}. + */ + private static class JmxMeterGaugeCompatible extends JmxMeter implements JmxMeterGaugeCompatibleMBean + { + + private JmxMeterGaugeCompatible(Metered metric, ObjectName objectName, TimeUnit rateUnit) + { + super(metric, objectName, rateUnit); + } + + @Override + public Object getValue() + { + return getCount(); + } + } + + public interface JmxTimerMBean extends JmxMeterMBean { double getMin(); @@ -770,11 +834,6 @@ public MetricName(String group, String type, String name, String scope, String m { throw new IllegalArgumentException("Name needs to be specified"); } - if (scope != null && scope.contains(name)) - { - throw new IllegalArgumentException("Scope cannot contain name, this is not neccessary and will cause performance issues. " + - "Scope: " + scope + " Name: " + name); - } this.group = group; this.type = type; this.name = name; diff --git a/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java b/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java index 8195aafbf33c..bf623379e183 100644 --- a/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java @@ -1,4 +1,5 @@ /* + * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -9,78 +10,67 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * */ package org.apache.cassandra.metrics; -import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; -import com.codahale.metrics.Timer; import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.benmanes.caffeine.cache.stats.CacheStats; import com.github.benmanes.caffeine.cache.stats.StatsCounter; + +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.cache.ChunkCache; -import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; +import static org.apache.cassandra.config.CassandraRelevantProperties.USE_MICROMETER; -/** - * Metrics for {@code ICache}. - */ -public class ChunkCacheMetrics extends CacheMetrics implements StatsCounter +public interface ChunkCacheMetrics extends StatsCounter, CacheMetrics { - /** Latency of misses */ - public final Timer missLatency; - - /** - * Create metrics for the provided chunk cache. - * - * @param cache Chunk cache to measure metrics - */ - public ChunkCacheMetrics(ChunkCache cache) + static ChunkCacheMetrics create(ChunkCache cache) { - super("ChunkCache", cache); - missLatency = Metrics.timer(factory.createMetricName("MissLatency")); + return create(cache, "chunk_cache"); } - @Override - public void recordHits(int count) + static ChunkCacheMetrics create(ChunkCache cache, String prefix) { - requests.mark(count); - hits.mark(count); + return USE_MICROMETER.getBoolean() + ? new MicrometerChunkCacheMetrics(cache, prefix) + : new CodahaleChunkCacheMetrics(cache); } @Override - public void recordMisses(int count) - { - requests.mark(count); - misses.mark(count); - } + void recordHits(int count); @Override - public void recordLoadSuccess(long loadTime) - { - missLatency.update(loadTime, TimeUnit.NANOSECONDS); - } + void recordMisses(int count); @Override - public void recordLoadFailure(long loadTime) - { - } + void recordLoadSuccess(long loadTime); @Override - public void recordEviction(int weight, RemovalCause cause) - { - } + void recordLoadFailure(long loadTime); + + @Override + void recordEviction(int weight, RemovalCause cause); + + void recordEviction(); + + double missLatency(); + + long entries(); @Nonnull @Override - public CacheStats snapshot() - { - return CacheStats.of(hits.getCount(), misses.getCount(), missLatency.getCount(), 0L, missLatency.getCount(), 0L, 0L); - } + CacheStats snapshot(); + + @VisibleForTesting + void reset(); } diff --git a/src/java/org/apache/cassandra/metrics/ClientMetrics.java b/src/java/org/apache/cassandra/metrics/ClientMetrics.java index 5616571b9a2a..8643f49e7a48 100644 --- a/src/java/org/apache/cassandra/metrics/ClientMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ClientMetrics.java @@ -30,6 +30,7 @@ import com.google.common.annotations.VisibleForTesting; +import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; @@ -55,13 +56,19 @@ public final class ClientMetrics @SuppressWarnings({ "unused", "FieldCanBeLocal" }) private Gauge pausedConnectionsGauge; private Meter connectionPaused; + private Meter requestDiscarded; private Meter requestDispatched; - private Meter timedOutBeforeProcessing; + public Meter timedOutBeforeProcessing; + public Meter timedOutBeforeAsyncProcessing; + public Timer queueTime; // time between Message creation and execution on NTR + public Counter totalQueueTime; // total queue time (in nanoseconds) for use in histogram timer + public Timer asyncQueueTime; // time between Message creation and execution on an async stage. This includes the time recorded in queueTime metric. + public Counter totalAsyncQueueTime; // total async queue time (in nanoseconds) for use in histogram timer + private Meter protocolException; private Meter unknownException; - private Timer queueTime; private ClientMetrics() { @@ -88,6 +95,7 @@ public void pauseConnection() connectionPaused.mark(); pausedConnections.incrementAndGet(); } + public void unpauseConnection() { pausedConnections.decrementAndGet(); } public void markRequestDiscarded() { requestDiscarded.mark(); } @@ -104,6 +112,35 @@ public List allConnectedClients() return clients; } + public void markTimedOutBeforeAsyncProcessing() + { + timedOutBeforeAsyncProcessing.mark(); + } + + /** + * Record time between Message creation and execution on NTR. + * @param value time elapsed + * @param unit time unit + */ + public void recordQueueTime(long value, TimeUnit unit) + { + queueTime.update(value, unit); + totalQueueTime.inc(TimeUnit.NANOSECONDS.convert(value, unit)); + } + + /** + * Record time between Message creation and execution on an async stage, if present. + * Note that this includes the queue time previously recorded before execution on the NTR stage, + * so for a given request, asyncQueueTime >= queueTime. + * @param value time elapsed + * @param unit time unit + */ + public void recordAsyncQueueTime(long value, TimeUnit unit) + { + asyncQueueTime.update(value, unit); + totalAsyncQueueTime.inc(TimeUnit.NANOSECONDS.convert(value, unit)); + } + public void markProtocolException() { protocolException.mark(); @@ -142,12 +179,18 @@ public long getCount() authFailure = registerMeter("AuthFailure"); pausedConnections = new AtomicInteger(); + connectionPaused = registerMeter("ConnectionPaused"); pausedConnectionsGauge = registerGauge("PausedConnections", pausedConnections::get); connectionPaused = registerMeter("ConnectionPaused"); requestDiscarded = registerMeter("RequestDiscarded"); requestDispatched = registerMeter("RequestDispatched"); timedOutBeforeProcessing = registerMeter("TimedOutBeforeProcessing"); + timedOutBeforeAsyncProcessing = registerMeter("TimedOutBeforeAsyncProcessing"); + totalQueueTime = registerCounter("TotalQueueTime"); + asyncQueueTime = registerTimer("AsyncQueueTime"); + totalAsyncQueueTime = registerCounter("TotalAsyncQueueTime"); + protocolException = registerMeter("ProtocolException"); unknownException = registerMeter("UnknownException"); @@ -227,4 +270,9 @@ public void queueTime(long value, TimeUnit unit) { queueTime.update(value, unit); } + + private Counter registerCounter(String name) + { + return Metrics.counter(factory.createMetricName(name)); + } } diff --git a/src/java/org/apache/cassandra/metrics/ClientRangeRequestMetrics.java b/src/java/org/apache/cassandra/metrics/ClientRangeRequestMetrics.java index c974651381b5..a44bc61a1dbd 100644 --- a/src/java/org/apache/cassandra/metrics/ClientRangeRequestMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ClientRangeRequestMetrics.java @@ -33,15 +33,15 @@ public class ClientRangeRequestMetrics extends ClientRequestMetrics */ public final Histogram roundTrips; - public ClientRangeRequestMetrics(String scope) + public ClientRangeRequestMetrics(String scope, String namePrefix) { - super(scope); - roundTrips = Metrics.histogram(factory.createMetricName("RoundTripsPerReadHistogram"), false); + super(scope, namePrefix); + roundTrips = Metrics.histogram(factory.createMetricName(namePrefix + "RoundTripsPerReadHistogram"), false); } public void release() { super.release(); - Metrics.remove(factory.createMetricName("RoundTripsPerReadHistogram")); + Metrics.remove(factory.createMetricName(namePrefix + "RoundTripsPerReadHistogram")); } } diff --git a/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java b/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java index 408087051e2a..a56538bf47b5 100644 --- a/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java @@ -29,7 +29,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; -public class ClientRequestMetrics extends LatencyMetrics +public class ClientRequestMetrics { public final Meter timeouts; public final Meter unavailables; @@ -40,18 +40,35 @@ public class ClientRequestMetrics extends LatencyMetrics public final Meter localRequests; public final Meter remoteRequests; - public ClientRequestMetrics(String scope) - { - super("ClientRequest", scope); + /** + * this is the metric that measures the actual execution time of a certain request; + * for example, the duration of StorageProxy::readRegular method for regular reads + */ + public final LatencyMetrics executionTimeMetrics; + + /** + * this is the metric that measures the time a request spent in the system; + * for example, the duration between requestTime and the end of StorageProxy::readRegular method for regular reads + */ + public final LatencyMetrics serviceTimeMetrics; - timeouts = Metrics.meter(factory.createMetricName("Timeouts")); - unavailables = Metrics.meter(factory.createMetricName("Unavailables")); - failures = Metrics.meter(factory.createMetricName("Failures")); - aborts = Metrics.meter(factory.createMetricName("Aborts")); - tombstoneAborts = Metrics.meter(factory.createMetricName("TombstoneAborts")); - readSizeAborts = Metrics.meter(factory.createMetricName("ReadSizeAborts")); - localRequests = Metrics.meter(factory.createMetricName("LocalRequests")); - remoteRequests = Metrics.meter(factory.createMetricName("RemoteRequests")); + protected final String namePrefix; + protected final MetricNameFactory factory; + + public ClientRequestMetrics(String scope, String prefix) + { + namePrefix = prefix; + factory = new DefaultNameFactory("ClientRequest", scope); + timeouts = Metrics.meter(factory.createMetricName(namePrefix + "Timeouts")); + unavailables = Metrics.meter(factory.createMetricName(namePrefix + "Unavailables")); + failures = Metrics.meter(factory.createMetricName(namePrefix + "Failures")); + aborts = Metrics.meter(factory.createMetricName(namePrefix + "Aborts")); + tombstoneAborts = Metrics.meter(factory.createMetricName(namePrefix + "TombstoneAborts")); + readSizeAborts = Metrics.meter(factory.createMetricName(namePrefix + "ReadSizeAborts")); + localRequests = Metrics.meter(factory.createMetricName(namePrefix + "LocalRequests")); + remoteRequests = Metrics.meter(factory.createMetricName(namePrefix + "RemoteRequests")); + executionTimeMetrics = new LatencyMetrics(factory, namePrefix); + serviceTimeMetrics = new LatencyMetrics(factory, namePrefix + "ServiceTime"); } public void markAbort(Throwable cause) @@ -71,14 +88,15 @@ else if (cause instanceof ReadSizeAbortException) public void release() { - super.release(); - Metrics.remove(factory.createMetricName("Timeouts")); - Metrics.remove(factory.createMetricName("Unavailables")); - Metrics.remove(factory.createMetricName("Failures")); - Metrics.remove(factory.createMetricName("Aborts")); - Metrics.remove(factory.createMetricName("TombstoneAborts")); - Metrics.remove(factory.createMetricName("ReadSizeAborts")); - Metrics.remove(factory.createMetricName("LocalRequests")); - Metrics.remove(factory.createMetricName("RemoteRequests")); + Metrics.remove(factory.createMetricName(namePrefix + "Timeouts")); + Metrics.remove(factory.createMetricName(namePrefix + "Unavailables")); + Metrics.remove(factory.createMetricName(namePrefix + "Failures")); + Metrics.remove(factory.createMetricName(namePrefix + "Aborts")); + Metrics.remove(factory.createMetricName(namePrefix + "TombstoneAborts")); + Metrics.remove(factory.createMetricName(namePrefix + "ReadSizeAborts")); + Metrics.remove(factory.createMetricName(namePrefix + "LocalRequests")); + Metrics.remove(factory.createMetricName(namePrefix + "RemoteRequests")); + executionTimeMetrics.release(); + serviceTimeMetrics.release(); } } diff --git a/src/java/org/apache/cassandra/metrics/ClientRequestsMetrics.java b/src/java/org/apache/cassandra/metrics/ClientRequestsMetrics.java new file mode 100644 index 000000000000..ed2473102639 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/ClientRequestsMetrics.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import java.util.EnumMap; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.db.ConsistencyLevel; + +public class ClientRequestsMetrics +{ + public final ClientRequestMetrics readMetrics; + public final ClientRangeRequestMetrics rangeMetrics; + public final ClientWriteRequestMetrics writeMetrics; + public final CASClientWriteRequestMetrics casWriteMetrics; + public final CASClientRequestMetrics casReadMetrics; + public final ViewWriteMetrics viewWriteMetrics; + public final AllRequestsMetrics allRequestsMetrics; + private final Map readMetricsMap; + private final Map writeMetricsMap; + + public ClientRequestsMetrics() + { + this(""); + } + + /** + * CassandraMetricsRegistry requires unique metrics name, otherwise it returns previous metrics. + * CNDB will create coordinator metrics with unique name prefix for each tenant + */ + public ClientRequestsMetrics(String namePrefix) + { + readMetrics = new ClientRequestMetrics("Read", namePrefix); + rangeMetrics = new ClientRangeRequestMetrics("RangeSlice", namePrefix); + writeMetrics = new ClientWriteRequestMetrics("Write", namePrefix); + casWriteMetrics = new CASClientWriteRequestMetrics("CASWrite", namePrefix); + casReadMetrics = new CASClientRequestMetrics("CASRead", namePrefix); + viewWriteMetrics = new ViewWriteMetrics("ViewWrite", namePrefix); + allRequestsMetrics = new AllRequestsMetrics("All", namePrefix); + readMetricsMap = new EnumMap<>(ConsistencyLevel.class); + writeMetricsMap = new EnumMap<>(ConsistencyLevel.class); + for (ConsistencyLevel level : ConsistencyLevel.values()) + { + readMetricsMap.put(level, new ClientRequestMetrics("Read-" + level.name(), namePrefix)); + writeMetricsMap.put(level, new ClientWriteRequestMetrics("Write-" + level.name(), namePrefix)); + } + } + + public ClientRequestMetrics readMetricsForLevel(ConsistencyLevel level) + { + return readMetricsMap.get(level); + } + + public ClientWriteRequestMetrics writeMetricsForLevel(ConsistencyLevel level) + { + return writeMetricsMap.get(level); + } + + /** + * When we want to reset metrics, say in a test env, it is not enough to create a new {@link ClientRequestsMetrics} + * object because the internal histograms would be initialized with the already registered, existing instances. + * In order to unregister and make the constructor really create new metrics histograms, we need to call this method + * on the old instance first. + */ + @VisibleForTesting + public void release() + { + readMetrics.release(); + rangeMetrics.release(); + writeMetrics.release(); + casWriteMetrics.release(); + casReadMetrics.release(); + readMetricsMap.values().forEach(ClientRequestMetrics::release); + writeMetricsMap.values().forEach(ClientRequestMetrics::release); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/metrics/ClientRequestsMetricsHolder.java b/src/java/org/apache/cassandra/metrics/ClientRequestsMetricsHolder.java deleted file mode 100644 index 26f2913263e6..000000000000 --- a/src/java/org/apache/cassandra/metrics/ClientRequestsMetricsHolder.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.metrics; - -import java.util.EnumMap; -import java.util.Map; - -import org.apache.cassandra.db.ConsistencyLevel; - -public final class ClientRequestsMetricsHolder -{ - public static final ClientRequestMetrics readMetrics = new ClientRequestMetrics("Read"); - public static final ClientWriteRequestMetrics writeMetrics = new ClientWriteRequestMetrics("Write"); - public static final CASClientWriteRequestMetrics casWriteMetrics = new CASClientWriteRequestMetrics("CASWrite"); - public static final CASClientRequestMetrics casReadMetrics = new CASClientRequestMetrics("CASRead"); - public static final ViewWriteMetrics viewWriteMetrics = new ViewWriteMetrics("ViewWrite"); - - public static final Map readMetricsMap = new EnumMap<>(ConsistencyLevel.class); - public static final Map writeMetricsMap = new EnumMap<>(ConsistencyLevel.class); - - static - { - for (ConsistencyLevel level : ConsistencyLevel.values()) - { - readMetricsMap.put(level, new ClientRequestMetrics("Read-" + level.name())); - writeMetricsMap.put(level, new ClientWriteRequestMetrics("Write-" + level.name())); - } - } - - public static ClientRequestMetrics readMetricsForLevel(ConsistencyLevel level) - { - return readMetricsMap.get(level); - } - - public static ClientWriteRequestMetrics writeMetricsForLevel(ConsistencyLevel level) - { - return writeMetricsMap.get(level); - } -} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/metrics/ClientRequestsMetricsProvider.java b/src/java/org/apache/cassandra/metrics/ClientRequestsMetricsProvider.java new file mode 100644 index 000000000000..04e1b94d9286 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/ClientRequestsMetricsProvider.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + + +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_CLIENT_REQUEST_METRICS_PROVIDER_PROPERTY; + +/** + * Provides access to the {@link ClientRequestsMetrics} instance used by this node + * and provides per-tenant metrics in CNDB. + */ +public interface ClientRequestsMetricsProvider +{ + ClientRequestsMetricsProvider instance = CUSTOM_CLIENT_REQUEST_METRICS_PROVIDER_PROPERTY.isPresent() + ? FBUtilities.construct(CUSTOM_CLIENT_REQUEST_METRICS_PROVIDER_PROPERTY.getString(), + "Client Request Metrics Provider") + : new DefaultClientRequestsMetricsProvider(); + + ClientRequestsMetrics metrics(String keyspace); + + class DefaultClientRequestsMetricsProvider implements ClientRequestsMetricsProvider + { + private final ClientRequestsMetrics metrics = new ClientRequestsMetrics(""); + + @Override + public ClientRequestsMetrics metrics(String keyspace) + { + return metrics; + } + } +} diff --git a/src/java/org/apache/cassandra/metrics/ClientWriteRequestMetrics.java b/src/java/org/apache/cassandra/metrics/ClientWriteRequestMetrics.java index 50427af0735f..69d3e6edaa0d 100644 --- a/src/java/org/apache/cassandra/metrics/ClientWriteRequestMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ClientWriteRequestMetrics.java @@ -33,15 +33,15 @@ public class ClientWriteRequestMetrics extends ClientRequestMetrics */ public final Histogram mutationSize; - public ClientWriteRequestMetrics(String scope) + public ClientWriteRequestMetrics(String scope, String namePrefix) { - super(scope); - mutationSize = Metrics.histogram(factory.createMetricName("MutationSizeHistogram"), false); + super(scope, namePrefix); + mutationSize = Metrics.histogram(factory.createMetricName(namePrefix + "MutationSizeHistogram"), false); } public void release() { super.release(); - Metrics.remove(factory.createMetricName("MutationSizeHistogram")); + Metrics.remove(factory.createMetricName(namePrefix + "MutationSizeHistogram")); } } diff --git a/src/java/org/apache/cassandra/metrics/CodahaleBufferPoolMetrics.java b/src/java/org/apache/cassandra/metrics/CodahaleBufferPoolMetrics.java new file mode 100644 index 000000000000..efb4b9b2b9e5 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/CodahaleBufferPoolMetrics.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import com.codahale.metrics.Gauge; + +import com.codahale.metrics.Meter; +import org.apache.cassandra.utils.memory.BufferPool; + +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +class CodahaleBufferPoolMetrics implements BufferPoolMetrics +{ + /** Total number of hits */ + private final Meter hits; + + /** Total number of misses */ + private final Meter misses; + + /** Total size of buffer pools, in bytes, including overflow allocation */ + private final Gauge size; + + /** Total size, in bytes, of active buffered being used from the pool currently + overflow */ + private final Gauge usedSize; + + /** + * Total size, in bytes, of direct or heap buffers allocated by the pool but not part of the pool + * either because they are too large to fit or because the pool has exceeded its maximum limit or because it's + * on-heap allocation. + */ + private final Gauge overflowSize; + + public CodahaleBufferPoolMetrics(String scope, BufferPool bufferPool) + { + MetricNameFactory factory = new DefaultNameFactory("BufferPool", scope); + + hits = Metrics.meter(factory.createMetricName("Hits")); + + misses = Metrics.meter(factory.createMetricName("Misses")); + + overflowSize = Metrics.register(factory.createMetricName("OverflowSize"), bufferPool::overflowMemoryInBytes); + + usedSize = Metrics.register(factory.createMetricName("UsedSize"), bufferPool::usedSizeInBytes); + + size = Metrics.register(factory.createMetricName("Size"), bufferPool::sizeInBytes); + } + + @Override + public void markHit() + { + hits.mark(); + } + + @Override + public long hits() + { + return hits.getCount(); + } + + @Override + public void markMissed() + { + misses.mark(); + } + + @Override + public long misses() + { + return misses.getCount(); + } + + @Override + public long overflowSize() + { + return overflowSize.getValue(); + } + + @Override + public long usedSize() + { + return usedSize.getValue(); + } + + @Override + public long size() + { + return size.getValue(); + } + + @Override + public void register3xAlias() + { + MetricNameFactory legacyFactory = new DefaultNameFactory("BufferPool"); + Metrics.registerMBean(misses, legacyFactory.createMetricName("Misses").getMBeanName()); + Metrics.registerMBean(size, legacyFactory.createMetricName("Size").getMBeanName()); + } +} diff --git a/src/java/org/apache/cassandra/metrics/CodahaleCacheMetrics.java b/src/java/org/apache/cassandra/metrics/CodahaleCacheMetrics.java new file mode 100644 index 000000000000..daceeea9f3c0 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/CodahaleCacheMetrics.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + + +import java.util.function.DoubleSupplier; + +import com.google.common.annotations.VisibleForTesting; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Meter; +import com.codahale.metrics.Timer; +import org.apache.cassandra.cache.CacheSize; + +import static java.lang.Double.isInfinite; +import static java.lang.Double.isNaN; +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +/** + * Metrics for {@code ICache}. + */ +public class CodahaleCacheMetrics implements CacheMetrics +{ + /** Cache capacity in bytes */ + public final Gauge capacity; + /** Total size of cache, in bytes */ + public final Gauge size; + /** Total number of cache entries */ + public final Gauge entries; + + /** Total number of cache hits */ + public final Meter hits; + /** Total number of cache misses */ + public final Meter misses; + /** Total number of cache requests */ + public final Meter requests; + + /** all time cache hit rate */ + public final Gauge hitRate; + /** 1m hit rate */ + public final Gauge oneMinuteHitRate; + /** 5m hit rate */ + public final Gauge fiveMinuteHitRate; + /** 15m hit rate */ + public final Gauge fifteenMinuteHitRate; + + public final String cacheType; + + private final MetricNameFactory factory; + + /** + * Create metrics for given cache. + * + * @param type Type of Cache to identify metrics. + * @param cache Cache to measure metrics + */ + public CodahaleCacheMetrics(String type, final CacheSize cache) + { + cacheType = type; + factory = new DefaultNameFactory("Cache", type); + + capacity = registerGauge("Capacity", cache::capacity); + size = registerGauge("Size", cache::weightedSize); + entries = registerGauge("Entries", cache::size); + + requests = registerMeter("Requests"); + + hits = registerMeter("Hits"); + misses = registerMeter("Misses"); + + hitRate = registerGauge("HitRate", ratioGauge(hits::getCount, requests::getCount)); + oneMinuteHitRate = registerGauge("OneMinuteHitRate", ratioGauge(hits::getOneMinuteRate, requests::getOneMinuteRate)); + fiveMinuteHitRate = registerGauge("FiveMinuteHitRate", ratioGauge(hits::getFiveMinuteRate, requests::getFiveMinuteRate)); + fifteenMinuteHitRate = registerGauge("FifteenMinuteHitRate", ratioGauge(hits::getFifteenMinuteRate, requests::getFifteenMinuteRate)); + } + + @Override + public long requests() + { + return requests.getCount(); + } + + @Override + public long capacity() + { + return capacity.getValue(); + } + + @Override + public long size() + { + return size.getValue(); + } + + @Override + public long entries() + { + return entries.getValue(); + } + + @Override + public long hits() + { + return hits.getCount(); + } + + @Override + public long misses() + { + return misses.getCount(); + } + + @Override + public double hitRate() + { + return hitRate.getValue(); + } + + @Override + public double hitOneMinuteRate() + { + return oneMinuteHitRate.getValue(); + } + + @Override + public double hitFiveMinuteRate() + { + return fiveMinuteHitRate.getValue(); + } + + @Override + public double hitFifteenMinuteRate() + { + return fifteenMinuteHitRate.getValue(); + } + + @Override + public double requestsFifteenMinuteRate() + { + return requests.getFifteenMinuteRate(); + } + + @Override + public void recordHits(int count) + { + requests.mark(); + hits.mark(count); + } + + @Override + public void recordMisses(int count) + { + requests.mark(); + misses.mark(count); + } + + /** + * Computes the ratio between the specified numerator and denominator + * + * @param numerator the numerator + * @param denominator the denominator + * @return the ratio between the numerator and the denominator + */ + private static double ratio(double numerator, double denominator) + { + if (isNaN(denominator) || isInfinite(denominator) || denominator == 0) + return Double.NaN; + + return numerator / denominator; + } + + protected final Gauge registerGauge(String name, Gauge gauge) + { + return Metrics.register(factory.createMetricName(name), gauge); + } + + protected final Meter registerMeter(String name) + { + return Metrics.meter(factory.createMetricName(name)); + } + + protected final Timer registerTimer(String name) + { + return Metrics.timer(factory.createMetricName(name)); + } + + @VisibleForTesting + public void reset() + { + // No actual reset happens. The Meter counter is put to zero but will not reset the moving averages + // It rather injects a weird value into them. + // This method is being only used by CacheMetricsTest and CachingBench so fixing this issue was acknowledged + // but not considered mandatory to be fixed now (CASSANDRA-16228) + hits.mark(-hits.getCount()); + misses.mark(-misses.getCount()); + requests.mark(-requests.getCount()); + } + + /** + * Returns a {@code Gauge} that will compute the ratio between the number supplied by the suppliers. + * + *

    {@code RatioGauge} create {@code Ratio} objects for each call which is a bit inefficcient. That method + * computes the ratio using a simple method call.

    + * + * @param numeratorSupplier the supplier for the numerator + * @param denominatorSupplier the supplier for the denominator + * @return a {@code Gauge} that will compute the ratio between the number supplied by the suppliers + */ + public static Gauge ratioGauge(DoubleSupplier numeratorSupplier, DoubleSupplier denominatorSupplier) + { + return () -> ratio(numeratorSupplier.getAsDouble(), denominatorSupplier.getAsDouble()); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/metrics/CodahaleChunkCacheMetrics.java b/src/java/org/apache/cassandra/metrics/CodahaleChunkCacheMetrics.java new file mode 100644 index 000000000000..52393414d13d --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/CodahaleChunkCacheMetrics.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + +import com.google.common.annotations.VisibleForTesting; + +import com.codahale.metrics.Timer; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import org.apache.cassandra.cache.ChunkCache; +import org.apache.cassandra.utils.FBUtilities; + +/** + * Codahale implementation for the chunk cache metrics. + */ +public class CodahaleChunkCacheMetrics implements ChunkCacheMetrics +{ + /** Metrics in common with ICache implementations */ + private final CodahaleCacheMetrics metrics; + + /** Latency of misses */ + public final Timer missLatency; + + /** + * Create metrics for the provided chunk cache. + * + * @param cache Chunk cache to measure metrics + */ + CodahaleChunkCacheMetrics(ChunkCache cache) + { + metrics = new CodahaleCacheMetrics("ChunkCache", cache); + missLatency = metrics.registerTimer("MissLatency"); + } + + @Override + public void recordHits(int count) + { + metrics.requests.mark(count); + metrics.hits.mark(count); + } + + @Override + public void recordMisses(int count) + { + metrics.requests.mark(count); + metrics.misses.mark(count); + } + + @Override + public void recordLoadSuccess(long loadTime) + { + missLatency.update(loadTime, TimeUnit.NANOSECONDS); + } + + @Override + public void recordLoadFailure(long loadTime) + { + } + + @Override + public void recordEviction(int weight, RemovalCause cause) + { + } + + @Override + public void recordEviction() + { + } + + @Override + public double hitRate() + { + return metrics.hitRate.getValue(); + } + + @Override + public double hitOneMinuteRate() + { + return metrics.hitOneMinuteRate(); + } + + @Override + public double hitFiveMinuteRate() + { + return metrics.hitFiveMinuteRate(); + } + + @Override + public double hitFifteenMinuteRate() + { + return metrics.hitFifteenMinuteRate(); + } + + @Override + public double requestsFifteenMinuteRate() + { + return metrics.requestsFifteenMinuteRate(); + } + + @Override + public long requests() + { + return metrics.requests.getCount(); + } + + @Override + public long misses() + { + return metrics.misses.getCount(); + } + + @Override + public long hits() + { + return metrics.hits.getCount(); + } + + @Override + public double missLatency() + { + return missLatency.getOneMinuteRate(); + } + + @Override + public long capacity() + { + return metrics.capacity.getValue(); + } + + @Override + public long size() + { + return metrics.size.getValue(); + } + + @Override + public long entries() + { + return metrics.entries(); + } + + @Nonnull + @Override + public CacheStats snapshot() + { + return CacheStats.of(metrics.hits.getCount(), metrics.misses.getCount(), missLatency.getCount(), 0L, missLatency.getCount(), 0L, 0L); + } + + @Override + @VisibleForTesting + public void reset() + { + metrics.reset(); + } + + @Override + public String toString() + { + return "Chunk cache metrics: " + System.lineSeparator() + + "Miss latency in seconds: " + missLatency() + System.lineSeparator() + + "Misses count: " + misses() + System.lineSeparator() + + "Hits count: " + hits() + System.lineSeparator() + + "Cache requests count: " + requests() + System.lineSeparator() + + "Moving hit rate: " + hitRate() + System.lineSeparator() + + "Num entries: " + entries() + System.lineSeparator() + + "Size in memory: " + FBUtilities.prettyPrintMemory(size()) + System.lineSeparator() + + "Capacity: " + FBUtilities.prettyPrintMemory(capacity()); + } +} diff --git a/src/java/org/apache/cassandra/metrics/CodehaleNativeMemoryMetrics.java b/src/java/org/apache/cassandra/metrics/CodehaleNativeMemoryMetrics.java new file mode 100644 index 000000000000..c7ffe9b05e99 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/CodehaleNativeMemoryMetrics.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Gauge; +import org.apache.cassandra.utils.memory.MemoryUtil; + +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +public class CodehaleNativeMemoryMetrics implements NativeMemoryMetrics +{ + private static final Logger logger = LoggerFactory.getLogger(CodehaleNativeMemoryMetrics.class); + + private final MetricNameFactory factory; + + /** Total size of memory allocated directly by calling Native.malloc via {@link MemoryUtil}, bypassing the JVM. + * This is in addition to nio direct memory, for example off-heap memtables will use this type of memory. */ + private final Gauge rawNativeMemory; + + /** + * Total size of memory used by bloom filters, part of {@link this#rawNativeMemory}. + */ + private final Gauge bloomFilterMemory; + + /** The memory allocated for direct byte buffers, aligned or otherwise, without counting any padding due to alignment. + * If {@code -Dio.netty.directMemory} is not set to {@code 0}, the direct memory used by Netty is not included in this value. */ + private final Gauge usedNioDirectMemory; + + /** The total memory allocated for direct byte buffers, aligned or otherwise, including any padding due to alignment. + * If -Dsun.nio.PageAlignDirectMemory=true is not set then this will be identical to usedNioDirectMemory. + * If {@code -Dio.netty.directMemory} is not set to {@code 0}, the direct memory used by Netty is not included in this value. */ + private final Gauge totalNioDirectMemory; + + /** The memory used by direct byte buffers allocated via the Netty library. These buffers are used for network communications. + * A limit can be set with "-Dio.netty.maxDirectMemory". When this property is zero (the default in jvm.options), then + * Netty will use the JVM NIO direct memory. Therefore, this value will be included in {@link #usedNioDirectMemory} + * and {@link #totalNioDirectMemory} only when the property is set to zero, otherwise this value is extra. */ + private final Gauge networkDirectMemory; + + /** The total number of direct byte buffers allocated, aligned or otherwise. */ + private final Gauge nioDirectBufferCount; + + /** The total memory allocated, including direct byte buffers, network direct memory, and raw malloc memory */ + private final Gauge totalMemory; + + public CodehaleNativeMemoryMetrics() + { + factory = new DefaultNameFactory("NativeMemory"); + + if (directBufferPool == null) + logger.error("Direct memory buffer pool MBean not present, native memory metrics will be missing for nio buffers"); + + rawNativeMemory = Metrics.register(factory.createMetricName("RawNativeMemory"), this::rawNativeMemory); + bloomFilterMemory = Metrics.register(factory.createMetricName("BloomFilterMemory"), this::bloomFilterMemory); + usedNioDirectMemory = Metrics.register(factory.createMetricName("UsedNioDirectMemory"), this::usedNioDirectMemory); + totalNioDirectMemory = Metrics.register(factory.createMetricName("TotalNioDirectMemory"), this::totalNioDirectMemory); + networkDirectMemory = Metrics.register(factory.createMetricName("NetworkDirectMemory"), this::networkDirectMemory); + nioDirectBufferCount = Metrics.register(factory.createMetricName("NioDirectBufferCount"), this::nioDirectBufferCount); + totalMemory = Metrics.register(factory.createMetricName("TotalMemory"), this::totalMemory); + } + + @Override + public long usedNioDirectMemoryValue() + { + return usedNioDirectMemory.getValue(); + } +} diff --git a/src/java/org/apache/cassandra/metrics/CompactionMetrics.java b/src/java/org/apache/cassandra/metrics/CompactionMetrics.java index 0fe1ec7418ce..ba7056721aa9 100644 --- a/src/java/org/apache/cassandra/metrics/CompactionMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CompactionMetrics.java @@ -18,8 +18,12 @@ package org.apache.cassandra.metrics; import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import com.codahale.metrics.CachedGauge; import com.codahale.metrics.Counter; +import com.codahale.metrics.DerivativeGauge; import com.codahale.metrics.Gauge; import com.codahale.metrics.Meter; @@ -27,15 +31,22 @@ import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.AbstractTableOperation; +import org.apache.cassandra.db.compaction.CompactionAggregateStatistics; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.CompactionStrategyStatistics; +import org.apache.cassandra.db.compaction.TableOperation; +import org.apache.cassandra.exceptions.UnknownKeyspaceException; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; /** - * Metrics for compaction. + * Metrics for the compaction executor. Note that several different operations execute on the compaction + * executor, for example index or view building. These operations are abstracted by {@link AbstractTableOperation} + * but previously we would refer to these operations as "compactions", so this incorrect name may still be + * found in the metrics that are exported to the users. */ public class CompactionMetrics { @@ -46,14 +57,43 @@ public class CompactionMetrics /** Estimated number of compactions remaining to perform, group by keyspace and then table name */ public final Gauge>> pendingTasksByTableName; - /** Number of completed compactions since server [re]start */ + /** Write amplification of compactions (bytes compacted / bytes flushed), group by keyspace and then table name */ + public final Gauge>> writeAmplificationByTableName; + + /** Number of completed operations since server [re]start */ public final Gauge completedTasks; - /** Total number of compactions since server [re]start */ + /** Total number of operations since server [re]start */ public final Meter totalCompactionsCompleted; - /** Total number of bytes compacted since server [re]start */ + /** Total number of failed compactions since server [re]start */ + public final Counter totalCompactionsFailed; + /** Total number of bytes processed by operations since server [re]start */ public final Counter bytesCompacted; + /** Estimated compressed bytes compacted since server [re]start, computed by scaling uncompressed bytes by the compression ratio */ + public final Counter compressedBytesCompacted; /** Time spent redistributing index summaries */ public final Timer indexSummaryRedistributionTime; + /** Recent/current throughput of compactions take */ + public final Meter bytesCompactedThroughput; + + /** + * The compaction strategy information for each table. Cached, because its computation might be fairly expensive. + */ + public final CachedGauge> aggregateCompactions; + + /* + * The compaction metrics below are derivatives of the complex compaction statistics metric aggregateCompactions. + */ + + /** Number of currently running compactions for all tables */ + public final DerivativeGauge, Integer> runningCompactions; + /** Mean read throughput of currently running compactions in bytes per second */ + public final DerivativeGauge, Double> meanCompactionReadThroughput; + /** Mean write throughput of currently running compactions in bytes per second */ + public final DerivativeGauge, Double> meanCompactionWriteThroughput; + /** Total bytes to compact from currently running compactions */ + public final DerivativeGauge, Long> runningCompactionsTotalBytes; + /** Remaining bytes to compact from currently running compactions */ + public final DerivativeGauge, Long> runningCompactionsRemainingBytes; /** Total number of compactions that have had sstables drop out of them */ public final Counter compactionsReduced; @@ -64,74 +104,87 @@ public class CompactionMetrics /** Total number of compactions which have outright failed due to lack of disk space */ public final Counter compactionsAborted; + /** Total number of deleted expired SSTables */ + public final Meter removedExpiredSSTables; + /** Total number compactions that consisted of only expired SSTables */ + public final Meter deleteOnlyCompactions; + + public final Gauge>>> overlapsMap; + public CompactionMetrics(final ExecutorPlus... collectors) { - pendingTasks = Metrics.register(factory.createMetricName("PendingTasks"), new Gauge() - { - public Integer getValue() + pendingTasks = Metrics.register(factory.createMetricName("PendingTasks"), () -> { + int n = 0; + // add estimate number of compactions need to be done + for (String keyspaceName : Schema.instance.getKeyspaces()) { - int n = 0; - // add estimate number of compactions need to be done - for (String keyspaceName : Schema.instance.getKeyspaces()) - { - for (ColumnFamilyStore cfs : Keyspace.open(keyspaceName).getColumnFamilyStores()) - n += cfs.getCompactionStrategyManager().getEstimatedRemainingTasks(); - } - // add number of currently running compactions - return n + CompactionManager.instance.active.getCompactions().size(); + for (ColumnFamilyStore cfs : getColumnFamilyStores(keyspaceName)) + n += cfs.getCompactionStrategy().getEstimatedRemainingTasks(); } + // add number of currently running compactions + return n + CompactionManager.instance.active.getTableOperations().size(); }); - pendingTasksByTableName = Metrics.register(factory.createMetricName("PendingTasksByTableName"), - new Gauge>>() - { - @Override - public Map> getValue() + pendingTasksByTableName = Metrics.register(factory.createMetricName("PendingTasksByTableName"), () -> { + Map> resultMap = new HashMap<>(); + // estimation of compactions need to be done + for (String keyspaceName : Schema.instance.getKeyspaces()) { - Map> resultMap = new HashMap<>(); - // estimation of compactions need to be done - for (String keyspaceName : Schema.instance.getKeyspaces()) + for (ColumnFamilyStore cfs : getColumnFamilyStores(keyspaceName)) { - for (ColumnFamilyStore cfs : Keyspace.open(keyspaceName).getColumnFamilyStores()) + int taskNumber = cfs.getCompactionStrategy().getEstimatedRemainingTasks(); + if (taskNumber > 0) { - int taskNumber = cfs.getCompactionStrategyManager().getEstimatedRemainingTasks(); - if (taskNumber > 0) + if (!resultMap.containsKey(keyspaceName)) { - if (!resultMap.containsKey(keyspaceName)) - { - resultMap.put(keyspaceName, new HashMap<>()); - } - resultMap.get(keyspaceName).put(cfs.getTableName(), taskNumber); + resultMap.put(keyspaceName, new HashMap<>()); } + resultMap.get(keyspaceName).put(cfs.getTableName(), taskNumber); } } + } - // currently running compactions - for (CompactionInfo.Holder compaction : CompactionManager.instance.active.getCompactions()) + // currently running compactions + // TODO DB-2701 - this includes all operations (previous behaviour), if we wanted only real + // compactions we could remove this block of code and call getTotalCompactions() from the strategy managers + for (TableOperation op : CompactionManager.instance.active.getTableOperations()) + { + TableMetadata metaData = op.getProgress().metadata(); + if (metaData == null) { - TableMetadata metaData = compaction.getCompactionInfo().getTableMetadata(); - if (metaData == null) - { - continue; - } - if (!resultMap.containsKey(metaData.keyspace)) - { - resultMap.put(metaData.keyspace, new HashMap<>()); - } + continue; + } + if (!resultMap.containsKey(metaData.keyspace)) + { + resultMap.put(metaData.keyspace, new HashMap<>()); + } - Map tableNameToCountMap = resultMap.get(metaData.keyspace); - if (tableNameToCountMap.containsKey(metaData.name)) - { - tableNameToCountMap.put(metaData.name, - tableNameToCountMap.get(metaData.name) + 1); - } - else - { - tableNameToCountMap.put(metaData.name, 1); - } + Map tableNameToCountMap = resultMap.get(metaData.keyspace); + if (tableNameToCountMap.containsKey(metaData.name)) + { + tableNameToCountMap.put(metaData.name, + tableNameToCountMap.get(metaData.name) + 1); + } + else + { + tableNameToCountMap.put(metaData.name, 1); } - return resultMap; } + return resultMap; + }); + + writeAmplificationByTableName = Metrics.register(factory.createMetricName("WriteAmplificationByTableName"), () -> { + Map> resultMap = new HashMap<>(); + + for (String keyspaceName : Schema.instance.getKeyspaces()) + { + Map ksMap = new HashMap<>(); + resultMap.put(keyspaceName, ksMap); + for (ColumnFamilyStore cfs : getColumnFamilyStores(keyspaceName)) + ksMap.put(cfs.getTableName(), cfs.getWA()); + } + + return resultMap; }); completedTasks = Metrics.register(factory.createMetricName("CompletedTasks"), new Gauge() @@ -145,12 +198,154 @@ public Long getValue() } }); totalCompactionsCompleted = Metrics.meter(factory.createMetricName("TotalCompactionsCompleted")); + totalCompactionsFailed = Metrics.counter(factory.createMetricName("FailedCompactions")); bytesCompacted = Metrics.counter(factory.createMetricName("BytesCompacted")); + compressedBytesCompacted = Metrics.counter(factory.createMetricName("CompressedBytesCompacted")); + bytesCompactedThroughput = Metrics.meter(factory.createMetricName("BytesCompactedThroughput")); // compaction failure metrics compactionsReduced = Metrics.counter(factory.createMetricName("CompactionsReduced")); sstablesDropppedFromCompactions = Metrics.counter(factory.createMetricName("SSTablesDroppedFromCompaction")); compactionsAborted = Metrics.counter(factory.createMetricName("CompactionsAborted")); indexSummaryRedistributionTime = Metrics.timer(factory.createMetricName("IndexSummaryRedistributionTime")); + + removedExpiredSSTables = Metrics.meter(factory.createMetricName("ExpiredSSTablesDropped")); + deleteOnlyCompactions = Metrics.meter(factory.createMetricName("DeleteOnlyCompactions")); + + aggregateCompactions = Metrics.register(factory.createMetricName("AggregateCompactions"), + // TODO 50 ms is 100x less than the default report interval of our distributed test harness (Fallout) at + // the moment of writing this. This implies that even a bigger timeout might be OK. + new CachedGauge>(50, TimeUnit.MILLISECONDS) + { + @Override + protected List loadValue() + { + List ret = new ArrayList<>(); + for (String keyspaceName : Schema.instance.getKeyspaces()) + { + // Scan all the compactions strategies of all tables and find those that have compactions in progress. + for (ColumnFamilyStore cfs : getColumnFamilyStores(keyspaceName)) + // For those return the statistics. + ret.addAll(cfs.getCompactionStrategy().getStatistics()); + } + + return ret; + } + }); + + overlapsMap = Metrics.register(factory.createMetricName("MaxOverlapsMap"), + new CachedGauge>>>(50, TimeUnit.MILLISECONDS) + { + public Map>> loadValue() + { + Map>> ret = new HashMap<>(); + for (String keyspaceName : Schema.instance.getKeyspaces()) + { + Map> ksMap = new HashMap<>(); + ret.put(keyspaceName, ksMap); + for (ColumnFamilyStore cfs : getColumnFamilyStores(keyspaceName)) + { + Map overlaps = cfs.getCompactionStrategy().getMaxOverlapsMap(); + ksMap.put(cfs.getTableName(), overlaps); + } + } + return ret; + } + }); + + runningCompactions = Metrics.register(factory.createMetricName("RunningCompactions"), + new DerivativeGauge, Integer>(aggregateCompactions) + { + @Override + protected Integer transform(List value) + { + return deriveSafeAggregateStatisticsStream(value) + .mapToInt(CompactionAggregateStatistics::numCompactionsInProgress) + .sum(); + } + }); + meanCompactionReadThroughput = Metrics.register(factory.createMetricName("MeanCompactionReadThroughput"), + new DerivativeGauge, Double>(aggregateCompactions) + { + @Override + protected Double transform(List value) + { + return deriveSafeAggregateStatisticsStream(value) + // Don't take into account aggregates for which there are no running compactions + .filter(s -> s.numCompactionsInProgress() > 0) + .mapToDouble(CompactionAggregateStatistics::readThroughput) + .average() + .orElse(0.0); + } + }); + meanCompactionWriteThroughput = Metrics.register(factory.createMetricName("MeanCompactionWriteThroughput"), + new DerivativeGauge, Double>(aggregateCompactions) + { + @Override + protected Double transform(List value) + { + return deriveSafeAggregateStatisticsStream(value) + // Don't take into account aggregates for which there are no running compactions + .filter(s -> s.numCompactionsInProgress() > 0) + .mapToDouble(CompactionAggregateStatistics::writeThroughput) + .average() + .orElse(0.0); + } + }); + runningCompactionsTotalBytes = Metrics.register(factory.createMetricName("RunningCompactionsTotalBytes"), + new DerivativeGauge, Long>(aggregateCompactions) + { + @Override + protected Long transform(List value) + { + return deriveSafeAggregateStatisticsStream(value) + .mapToLong(CompactionAggregateStatistics::tot) + .sum(); + } + }); + runningCompactionsRemainingBytes = Metrics.register(factory.createMetricName("RunningCompactionsRemainingBytes"), + new DerivativeGauge, Long>(aggregateCompactions) + { + @Override + protected Long transform(List value) + { + return deriveSafeAggregateStatisticsStream(value) + .mapToLong(s -> s.tot() - s.read()) + .sum(); + } + }); + } + + /** + * Returns a list of all ColumnFamilyStores in a keyspace if it exists. + * If the keyspace does not exist, returns an empty list. + * This is useful to avoid throwing when a keyspace gets dropped while we are iterating all keyspaces. + */ + private static Collection getColumnFamilyStores(String keyspaceName) + { + try + { + return Keyspace.open(keyspaceName).getColumnFamilyStores(); + } + catch (UnknownKeyspaceException e) + { + return Collections.emptyList(); + } + } + + /** + * Needed because deriving from a CachedGauge might hit a NullPointerException until we move to a version of + * dropwizard's metrics-core where https://github.com/dropwizard/metrics/pull/711 / + * https://github.com/dropwizard/metrics/pull/1566 are fixed (currently targeting metrics-core 4.1.7). + * + * @param aggregateCompactions The cached compaction strategy statistics to derive from. + * + * @return A stream (potentially empty) of the aggregate statistics corresponding to the given strategy statistics. + */ + private static Stream deriveSafeAggregateStatisticsStream(List aggregateCompactions) + { + if (aggregateCompactions == null) + return Stream.empty(); + return aggregateCompactions.stream().flatMap(s -> s.aggregates().stream()); } } diff --git a/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java b/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java index cfca6460421b..af64c27e72d8 100644 --- a/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java +++ b/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java @@ -23,6 +23,7 @@ import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLongArray; @@ -44,6 +45,8 @@ import static java.lang.Math.min; import static org.apache.cassandra.config.CassandraRelevantProperties.DECAYING_ESTIMATED_HISTOGRAM_RESERVOIR_STRIPE_COUNT; +import org.apache.cassandra.config.CassandraRelevantProperties; + /** * A decaying histogram reservoir where values collected during each minute will be twice as significant as the values * collected in the previous minute. Measured values are collected in variable sized buckets, using small buckets in the @@ -88,6 +91,7 @@ public class DecayingEstimatedHistogramReservoir implements SnapshottingReservoi { private static final Logger logger = LoggerFactory.getLogger(DecayingEstimatedHistogramReservoir.class); private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 5L, TimeUnit.MINUTES); + public static final boolean USE_DSE_COMPATIBLE_HISTOGRAM_BOUNDARIES = CassandraRelevantProperties.USE_DSE_COMPATIBLE_HISTOGRAM_BOUNDARIES.getBoolean(); /** * The default number of decayingBuckets. Use this bucket count to reduce memory allocation for bucket offsets. */ @@ -100,16 +104,33 @@ public class DecayingEstimatedHistogramReservoir implements SnapshottingReservoi private static final int[] DISTRIBUTION_PRIMES = new int[] { 17, 19, 23, 29 }; // The offsets used with a default sized bucket array without a separate bucket for zero values. - public static final long[] DEFAULT_WITHOUT_ZERO_BUCKET_OFFSETS = EstimatedHistogram.newOffsets(DEFAULT_BUCKET_COUNT, false); + private static final long[] DEFAULT_WITHOUT_ZERO_BUCKET_OFFSETS = EstimatedHistogram.newCassandraOffsets(DEFAULT_BUCKET_COUNT, false); // The offsets used with a default sized bucket array with a separate bucket for zero values. - public static final long[] DEFAULT_WITH_ZERO_BUCKET_OFFSETS = EstimatedHistogram.newOffsets(DEFAULT_BUCKET_COUNT, true); + private static final long[] DEFAULT_WITH_ZERO_BUCKET_OFFSETS = EstimatedHistogram.newCassandraOffsets(DEFAULT_BUCKET_COUNT, true); private static final int TABLE_BITS = 4; private static final int TABLE_MASK = -1 >>> (32 - TABLE_BITS); private static final float[] LOG2_TABLE = computeTable(TABLE_BITS); private static final float log2_12_recp = (float) (1d / slowLog2(1.2d)); + // DSE COMPATIBILITY CHANGES START + // The DSE-compatible offsets used with a default sized bucket array without a separate bucket for zero values. + private static final long[] DEFAULT_DSE_WITHOUT_ZERO_BUCKET_OFFSETS = newDseOffsets(DEFAULT_BUCKET_COUNT, false); + + // The DSE-compatibleoffsets used with a default sized bucket array with a separate bucket for zero values. + private static final long[] DEFAULT_DSE_WITH_ZERO_BUCKET_OFFSETS = newDseOffsets(DEFAULT_BUCKET_COUNT, true); + + /** Values for calculating buckets and indexes */ + final static int subBucketCount = 8; // number of sub-buckets in each bucket + final static int subBucketHalfCount = subBucketCount / 2; + final static int unitMagnitude = 0; // power of two of the unit in bucket zero (2^0 = 1) + final static int subBucketCountMagnitude = 3; // power of two of the number of sub buckets + final static int subBucketHalfCountMagnitude = subBucketCountMagnitude - 1; // power of two of half the number of sub-buckets + final static long subBucketMask = (long)(subBucketCount - 1) << unitMagnitude; + final static int leadingZeroCountBase = 64 - unitMagnitude - subBucketHalfCountMagnitude - 1; + // DSE COMPATIBILITY CHANGES END + private static float[] computeTable(int bits) { float[] table = new float[1 << bits]; @@ -219,18 +240,17 @@ public DecayingEstimatedHistogramReservoir(boolean considerZeroes, if (bucketCount == DEFAULT_BUCKET_COUNT) { - if (considerZeroes == true) - { - bucketOffsets = DEFAULT_WITH_ZERO_BUCKET_OFFSETS; - } + if (USE_DSE_COMPATIBLE_HISTOGRAM_BOUNDARIES) + bucketOffsets = considerZeroes ? DEFAULT_DSE_WITH_ZERO_BUCKET_OFFSETS : DEFAULT_DSE_WITHOUT_ZERO_BUCKET_OFFSETS; else - { - bucketOffsets = DEFAULT_WITHOUT_ZERO_BUCKET_OFFSETS; - } + bucketOffsets = considerZeroes ? DEFAULT_WITH_ZERO_BUCKET_OFFSETS : DEFAULT_WITHOUT_ZERO_BUCKET_OFFSETS; } else { - bucketOffsets = EstimatedHistogram.newOffsets(bucketCount, considerZeroes); + if (USE_DSE_COMPATIBLE_HISTOGRAM_BOUNDARIES) + bucketOffsets = newDseOffsets(bucketCount, considerZeroes); + else + bucketOffsets = EstimatedHistogram.newOffsets(bucketCount, considerZeroes); } nStripes = stripes; @@ -280,6 +300,9 @@ public int stripedIndex(int offsetIndex, int stripe) @VisibleForTesting public static int findIndex(long[] bucketOffsets, long value) { + if (USE_DSE_COMPATIBLE_HISTOGRAM_BOUNDARIES) + return findIndexDse(bucketOffsets, value); + // values below zero are nonsense, but we have never failed when presented them value = max(value, 0); @@ -300,6 +323,66 @@ public static int findIndex(long[] bucketOffsets, long value) return value <= bucketOffsets[firstCandidate] ? firstCandidate : firstCandidate + 1; } + /** + * this is almost a copy-paste from DSE DecayingEstimatedHistogram::BucketProperties::getIndex + * Almost, because: + * 1. C* and DSE differently implement the "considerZeroes" flag. + * The zeroesCorrection variable is used to adjust the index in the C* case. + *

    + * 2. C* and DSE differently implement the histogram overflow. + * In DSE, there is a separate flag isOverflowed which is set when a value doesn't fit in buckets; the getIndex + * function is supposed to always return index for an actual bucket. + * In C* there is a special bucket for overflowed values, and the findIndex function is supposed to return + * the index of this additional bucket if the value doesn't fit in the regular buckets. + * This is the origin of the min() function in the return statement. + * + * @param bucketOffsets the offsets of the histogram buckets (upper inclusive bounds) + * @param value the value with which we want to update the histogram + * @return index of the bucket that keeps track of the value OR the index of the last bucket which is used for + * overflowed values + */ + private static int findIndexDse(long[] bucketOffsets, long value) + { + if (value < 0) { + throw new ArrayIndexOutOfBoundsException("Histogram recorded value cannot be negative."); + } + + // Calculates the number of powers of two by which the value is greater than the biggest value that fits in + // bucket 0. This is the bucket index since each successive bucket can hold a value 2x greater. + // The mask maps small values to bucket 0. + final int bucketIndex = leadingZeroCountBase - Long.numberOfLeadingZeros(value | subBucketMask); + + // For bucketIndex 0, this is just value, so it may be anywhere in 0 to subBucketCount. + // For other bucketIndex, this will always end up in the top half of subBucketCount: assume that for some bucket + // k > 0, this calculation will yield a value in the bottom half of 0 to subBucketCount. Then, because of how + // buckets overlap, it would have also been in the top half of bucket k-1, and therefore would have + // returned k-1 in getBucketIndex(). Since we would then shift it one fewer bits here, it would be twice as big, + // and therefore in the top half of subBucketCount. + final int subBucketIndex = (int)(value >>> (bucketIndex + unitMagnitude)); + + //assert(subBucketIndex < subBucketCount); + //assert(bucketIndex == 0 || (subBucketIndex >= subBucketHalfCount)); + // Calculate the index for the first entry that will be used in the bucket (halfway through subBucketCount). + // For bucketIndex 0, all subBucketCount entries may be used, but bucketBaseIndex is still set in the middle. + final int bucketBaseIndex = (bucketIndex + 1) << subBucketHalfCountMagnitude; + + // Calculate the offset in the bucket. This subtraction will result in a positive value in all buckets except + // the 0th bucket (since a value in that bucket may be less than half the bucket's 0 to subBucketCount range). + // However, this works out since we give bucket 0 twice as much space. + final int offsetInBucket = subBucketIndex - subBucketHalfCount; + + // The following is the equivalent of ((subBucketIndex - subBucketHalfCount) + bucketBaseIndex, + final int dseBucket = bucketBaseIndex + offsetInBucket; + + // DSE bucket for zero values always exists, and during snapshot creation it is added to the second bucket + // if the histogram should not "considerZeroes". + // Cassandra does that differently. We either have or have not a separate bucket for zeroes. + // Thus, we should subtract 1 from the DSE index if we don't consider zeroes AND the value > 0. + final int zeroesCorrection = bucketOffsets[0] > 0 && value > 0 ? 1 : 0; + + return min(bucketOffsets.length, bucketBaseIndex + offsetInBucket - zeroesCorrection); + } + /** * Returns the logical number of buckets where recorded values are stored. The actual number of physical buckets * is size() * stripeCount() @@ -657,7 +740,7 @@ public void dump(OutputStream output) * The decaying buckets will be used for quantile calculations and mean values, but the non decaying buckets will be * exposed for calls to {@link Snapshot#getValues()}. */ - static class EstimatedHistogramReservoirSnapshot extends AbstractSnapshot + public static class EstimatedHistogramReservoirSnapshot extends AbstractSnapshot { private final long[] values; private long count; @@ -699,6 +782,11 @@ public long[] getValues() return values; } + public long[] getOffsets() + { + return bucketOffsets; + } + @Override public int size() { @@ -855,4 +943,49 @@ public String toString() return "[" + min + ',' + max + ']'; } } + + /** + * this is almost a copy-paste from DSE DecayingEstimatedHistogram::makeOffsets, except that it's been adjusted + * to the C*-specific ability of specifying the number of buckets. + * Please note, that DSE bucket offsets are inclusive lower bounds and C* bucket offsets are inclusive upper bounds. + * For simplicity, we use the same bucket offsets in both cases, but this means there might be a slight + * difference for any samples that are exactly on the bucket boundary. I think we can safely ignore that. + * + * @param size the number of regular buckets to create; the special bucket for overflow values is not included + * in this count + * @param considerZeroes whether to include a separate bucket for zero values + * @return the offsets for the buckets; in that context offsets mean the upper inclusive bounds of each bucket + * the name "offset" stays for historic reasons. + * + */ + public static long[] newDseOffsets(int size, boolean considerZeroes) + { + ArrayList ret = new ArrayList<>(); + if (considerZeroes) + ret.add(0L); + + for (int i = 1; i <= subBucketCount && ret.size() < size; i++) + { + ret.add((long) i); + } + + long last = subBucketCount; + long unit = 1 << (unitMagnitude + 1); + + while (ret.size() < size) + { + for (int i = 0; i < subBucketHalfCount; i++) + { + assert last + unit > last : "Overflow in DSE histogram bucket calculation; too big size requested: " + size; + last += unit; + ret.add(last); + if (ret.size() >= size) + break; + } + unit *= 2; + } + + assert ret.size() == size : "DSE histogram bucket count mismatch: " + ret.size() + " != " + size; + return ret.stream().mapToLong(i->i).toArray(); + } } diff --git a/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java b/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java index 91a680e50d99..03ab030d55fe 100644 --- a/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java +++ b/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java @@ -17,10 +17,10 @@ */ package org.apache.cassandra.metrics; -import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; import com.codahale.metrics.Meter; import com.codahale.metrics.Timer; @@ -42,7 +42,7 @@ public class DroppedMessageMetrics static { - EnumMap aliases = new EnumMap<>(Verb.class); + Map aliases = new HashMap<>(); aliases.put(Verb.BATCH_REMOVE_REQ, "BATCH_REMOVE"); aliases.put(Verb.BATCH_STORE_REQ, "BATCH_STORE"); aliases.put(Verb.COUNTER_MUTATION_REQ, "COUNTER_MUTATION"); @@ -53,7 +53,7 @@ public class DroppedMessageMetrics aliases.put(Verb.READ_REPAIR_REQ, "READ_REPAIR"); aliases.put(Verb.REQUEST_RSP, "REQUEST_RESPONSE"); - REQUEST_VERB_ALIAS = Maps.immutableEnumMap(aliases); + REQUEST_VERB_ALIAS = ImmutableMap.copyOf(aliases); } /** Number of dropped messages */ @@ -86,4 +86,12 @@ public DroppedMessageMetrics(Verb verb) crossNodeDroppedLatency = Metrics.timer(createMetricName(TYPE, "CrossNodeDroppedLatency", scope)); } } + + public DroppedMessageMetrics(String type, String scope) + { + MetricNameFactory factory = new DefaultNameFactory(type, scope); + dropped = Metrics.meter(factory.createMetricName("Dropped")); + internalDroppedLatency = Metrics.timer(factory.createMetricName("InternalDroppedLatency")); + crossNodeDroppedLatency = Metrics.timer(factory.createMetricName("CrossNodeDroppedLatency")); + } } diff --git a/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java b/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java index bcff38957715..f12ce71b20bd 100644 --- a/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java +++ b/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.codahale.metrics.Counter; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; import com.github.benmanes.caffeine.cache.Caffeine; @@ -50,6 +51,10 @@ public final class HintsServiceMetrics .executor(ImmediateExecutor.INSTANCE) .build(address -> Metrics.histogram(factory.createMetricName("Hint_delays-"+address.toString().replace(':', '.')), false)); + + public static final Counter hintsOnDisk = Metrics.counter(factory.createMetricName("HintsOnDisk")); + public static final Counter corruptedHintsOnDisk = Metrics.counter(factory.createMetricName("CorruptedHintsOnDisk")); + public static void updateDelayMetrics(InetAddressAndPort endpoint, long delay) { if (delay <= 0) diff --git a/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java b/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java index cc8dae9f4cfa..7090dffb82fe 100644 --- a/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java +++ b/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java @@ -17,10 +17,14 @@ */ package org.apache.cassandra.metrics; +import java.lang.reflect.InvocationTargetException; + import com.codahale.metrics.Gauge; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.InboundMessageHandlers; import org.apache.cassandra.metrics.CassandraMetricsRegistry.MetricName; +import org.apache.cassandra.utils.FBUtilities; +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_INTERNODE_INBOUND_METRICS_PROVIDER_PROPERTY; /** * Metrics for internode connections. @@ -42,12 +46,41 @@ public class InternodeInboundMetrics private final MetricName throttledCount; private final MetricName throttledNanos; + /** + * Factory method to create metrics for given inbound message handlers. + * This ensures the metrics provider is used when configured. + * + * @param peer IP address and port to use for metrics label + * @param handlers the inbound message handlers + * @return new InternodeInboundMetrics instance + */ + public static InternodeInboundMetrics create(InetAddressAndPort peer, InboundMessageHandlers handlers) + { + if (CUSTOM_INTERNODE_INBOUND_METRICS_PROVIDER_PROPERTY.isPresent()) + { + Class klass = FBUtilities.classForName(CUSTOM_INTERNODE_INBOUND_METRICS_PROVIDER_PROPERTY.getString(), "Internode Inbound Metrics Provider"); + try + { + return klass.getDeclaredConstructor(InetAddressAndPort.class, InboundMessageHandlers.class).newInstance(peer, handlers); + } + catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) + { + throw new RuntimeException(e); + } + } + else + { + return new InternodeInboundMetrics(peer, handlers); + } + } + /** * Create metrics for given inbound message handlers. * * @param peer IP address and port to use for metrics label */ - public InternodeInboundMetrics(InetAddressAndPort peer, InboundMessageHandlers handlers) + protected InternodeInboundMetrics(InetAddressAndPort peer, InboundMessageHandlers handlers) { // ipv6 addresses will contain colons, which are invalid in a JMX ObjectName MetricNameFactory factory = new DefaultNameFactory("InboundConnection", peer.getHostAddressAndPortForJMX()); diff --git a/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java b/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java index 2b0348eb5556..421bbc78ae40 100644 --- a/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java +++ b/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java @@ -17,10 +17,14 @@ */ package org.apache.cassandra.metrics; +import java.lang.reflect.InvocationTargetException; + import com.codahale.metrics.Gauge; import com.codahale.metrics.Meter; import org.apache.cassandra.net.OutboundConnections; +import org.apache.cassandra.utils.FBUtilities; +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_INTERNODE_OUTBOUND_METRICS_PROVIDER_PROPERTY; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; import org.apache.cassandra.locator.InetAddressAndPort; @@ -109,12 +113,41 @@ public class InternodeOutboundMetrics private final MetricNameFactory factory; + /** + * Factory method to create InternodeOutboundMetrics instances. + * If a custom provider is configured, it will be used instead of the default implementation. + * + * @param ip IP address to use for metrics label + * @param messagingPool the OutboundConnections instance + * @return an InternodeOutboundMetrics instance + */ + public static InternodeOutboundMetrics create(InetAddressAndPort ip, OutboundConnections messagingPool) + { + if (CUSTOM_INTERNODE_OUTBOUND_METRICS_PROVIDER_PROPERTY.isPresent()) + { + Class klass = FBUtilities.classForName(CUSTOM_INTERNODE_OUTBOUND_METRICS_PROVIDER_PROPERTY.getString(), "Internode Outbound Metrics Provider"); + try + { + return klass.getDeclaredConstructor(InetAddressAndPort.class, OutboundConnections.class).newInstance(ip, messagingPool); + } + catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) + { + throw new RuntimeException(e); + } + } + else + { + return new InternodeOutboundMetrics(ip, messagingPool); + } + } + /** * Create metrics for given connection pool. * * @param ip IP address to use for metrics label */ - public InternodeOutboundMetrics(InetAddressAndPort ip, final OutboundConnections messagingPool) + protected InternodeOutboundMetrics(InetAddressAndPort ip, final OutboundConnections messagingPool) { // ipv6 addresses will contain colons, which are invalid in a JMX ObjectName address = ip.getHostAddressAndPortForJMX(); diff --git a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java index ae15bbf95a04..a2301b8526f7 100644 --- a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java +++ b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java @@ -86,6 +86,22 @@ public class KeyspaceMetrics public final Histogram sstablesPerRangeReadHistogram; /** Tombstones scanned in queries on this Keyspace */ public final Histogram tombstoneScannedHistogram; + /** Time spent flushing memtables */ + public final Counter flushTime; + public final Counter storageAttachedIndexBuildTime; + + /** Time spent writing SAI */ + public final Counter storageAttachedIndexWritingTimeForIndexBuild; + public final Counter storageAttachedIndexWritingTimeForCompaction; + public final Counter storageAttachedIndexWritingTimeForFlush; + public final Counter storageAttachedIndexWritingTimeForOther; + + /** Time spent writing memtables during compaction */ + public final Counter compactionTime; + + /** Shadowed keys scan metrics **/ + public final Histogram shadowedKeysScannedHistogram; + public final Histogram shadowedKeysLoopsHistogram; /** Live cells scanned in queries on this Keyspace */ public final Histogram liveScannedHistogram; /** Column update time delta on this Keyspace */ @@ -136,6 +152,19 @@ public class KeyspaceMetrics public final Counter outOfRangeTokenWrites; /** Lifetime count of paxos requests for keys outside the node's owned token ranges for this keyspace **/ public final Counter outOfRangeTokenPaxosRequests; + /** Coordinator read metrics */ + public final Timer coordinatorReadLatency; + /** Coordinator CAS read metrics */ + public final Timer coordinatorCasReadLatency; + /** Coordinator range metrics */ + public final Timer coordinatorScanLatency; + /** Coordinator write metrics */ + public final Timer coordinatorWriteLatency; + /** Coordinator CAS write metrics */ + public final Timer coordinatorCasWriteLatency; + /** Time spent waiting for free memtable space, either on- or off-heap */ + public final Histogram waitingOnFreeMemtableSpace; + public final Counter deleteRequests; /* * Metrics for inconsistencies detected between repaired data sets across replicas. These @@ -184,6 +213,12 @@ public class KeyspaceMetrics public final ImmutableMap, ImmutableMap>> formatSpecificGauges; + public final Meter bytesAnticompacted; + public final Meter bytesMutatedAnticompaction; + public final Meter bytesPreviewed; + public final Meter tokenRangesPreviewedDesynchronized; + public final Meter bytesPreviewedDesynchronized; + public final MetricNameFactory factory; private final Keyspace keyspace; @@ -238,6 +273,15 @@ public KeyspaceMetrics(final Keyspace ks) sstablesPerReadHistogram = createKeyspaceHistogram("SSTablesPerReadHistogram", true); sstablesPerRangeReadHistogram = createKeyspaceHistogram("SSTablesPerRangeReadHistogram", true); tombstoneScannedHistogram = createKeyspaceHistogram("TombstoneScannedHistogram", false); + flushTime = createKeyspaceCounter("FlushTime", v -> v.flushTime.getCount()); + storageAttachedIndexBuildTime = createKeyspaceCounter("StorageAttachedIndexBuildTime", v -> v.storageAttachedIndexBuildTime.getCount()); + storageAttachedIndexWritingTimeForIndexBuild = createKeyspaceCounter("StorageAttachedIndexWritingTimeForIndexBuild", v -> v.storageAttachedIndexWritingTimeForIndexBuild.getCount()); + storageAttachedIndexWritingTimeForCompaction = createKeyspaceCounter("StorageAttachedIndexWritingTimeForCompaction", v -> v.storageAttachedIndexWritingTimeForCompaction.getCount()); + storageAttachedIndexWritingTimeForFlush = createKeyspaceCounter("StorageAttachedIndexWritingTimeForFlush", v -> v.storageAttachedIndexWritingTimeForFlush.getCount()); + storageAttachedIndexWritingTimeForOther = createKeyspaceCounter("StorageAttachedIndexWritingTimeForOther", v -> v.storageAttachedIndexWritingTimeForOther.getCount()); + compactionTime = createKeyspaceCounter("CompactionTime", v -> v.compactionTime.getCount()); + shadowedKeysScannedHistogram = createKeyspaceHistogram("ShadowedKeysScannedHistogram", false); + shadowedKeysLoopsHistogram = createKeyspaceHistogram("ShadowedKeysLoopsHistogram", false); liveScannedHistogram = createKeyspaceHistogram("LiveScannedHistogram", false); colUpdateTimeDeltaHistogram = createKeyspaceHistogram("ColUpdateTimeDeltaHistogram", false); viewLockAcquireTime = createKeyspaceTimer("ViewLockAcquireTime"); @@ -263,6 +307,14 @@ public KeyspaceMetrics(final Keyspace ks) partitionsValidated = createKeyspaceHistogram("PartitionsValidated", false); bytesValidated = createKeyspaceHistogram("BytesValidated", false); + coordinatorReadLatency = createKeyspaceTimer("CoordinatorReadLatency"); + coordinatorCasReadLatency = createKeyspaceTimer("CoordinatorCasReadLatency"); + coordinatorScanLatency = createKeyspaceTimer("CoordinatorScanLatency"); + coordinatorWriteLatency = createKeyspaceTimer("CoordinatorWriteLatency"); + coordinatorCasWriteLatency = createKeyspaceTimer("CoordinatorCasWriteLatency"); + waitingOnFreeMemtableSpace = createKeyspaceHistogram("WaitingOnFreeMemtableSpace", false); + deleteRequests = createKeyspaceCounter("DeleteRequests", metric -> metric.deleteRequests.getCount()); + confirmedRepairedInconsistencies = createKeyspaceMeter("RepairedDataInconsistenciesConfirmed"); unconfirmedRepairedInconsistencies = createKeyspaceMeter("RepairedDataInconsistenciesUnconfirmed"); @@ -292,6 +344,11 @@ public KeyspaceMetrics(final Keyspace ks) outOfRangeTokenReads = createKeyspaceCounter("ReadOutOfRangeToken"); outOfRangeTokenWrites = createKeyspaceCounter("WriteOutOfRangeToken"); outOfRangeTokenPaxosRequests = createKeyspaceCounter("PaxosOutOfRangeToken"); + bytesAnticompacted = createKeyspaceMeter("BytesAnticompacted"); + bytesMutatedAnticompaction = createKeyspaceMeter("BytesMutatedAnticompaction"); + bytesPreviewed = createKeyspaceMeter("BytesPreviewed"); + tokenRangesPreviewedDesynchronized = createKeyspaceMeter("TokenRangesPreviewedDesynchronized"); + bytesPreviewedDesynchronized = createKeyspaceMeter("BytesPreviewedDesynchronized"); } /** diff --git a/src/java/org/apache/cassandra/metrics/LinearFit.java b/src/java/org/apache/cassandra/metrics/LinearFit.java new file mode 100644 index 000000000000..869cac4f581a --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/LinearFit.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import org.apache.cassandra.utils.Pair; + +public class LinearFit +{ + /** + * Computes the intercept and slope for the best linear fit to the given values. + */ + public static Pair interceptSlopeFor(PairedSlidingWindowReservoir.IntIntPair[] values) + { + double xSum = 0; + double ySum = 0; + for (var pair : values) + { + xSum += pair.x; + ySum += pair.y; + } + double xMean = xSum / values.length; + double yMean = ySum / values.length; + + double covariance = 0; + double variance = 0; + for (var pair : values) + { + double dX = pair.x - xMean; + double dY = pair.y - yMean; + covariance += dX * dY; + variance += dX * dX; + } + + // if all points have the same X value, return the Y:X ratio. this does the right thing + // for `estimateCost` + if (variance == 0) + return Pair.create(0.0, yMean / xMean); + + double slope = covariance / variance; + double intercept = yMean - slope * xMean; + return Pair.create(intercept, slope); + } +} diff --git a/src/java/org/apache/cassandra/metrics/MessagingMetrics.java b/src/java/org/apache/cassandra/metrics/MessagingMetrics.java index bef6d087373f..fd34bd9b330b 100644 --- a/src/java/org/apache/cassandra/metrics/MessagingMetrics.java +++ b/src/java/org/apache/cassandra/metrics/MessagingMetrics.java @@ -17,7 +17,6 @@ */ package org.apache.cassandra.metrics; -import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -58,13 +57,14 @@ public static class DCLatencyRecorder implements LatencyConsumer public final Timer dcLatency; public final Timer allLatency; - DCLatencyRecorder(Timer dcLatency, Timer allLatency) + public DCLatencyRecorder(Timer dcLatency, Timer allLatency) { this.dcLatency = dcLatency; this.allLatency = allLatency; } - public void accept(long timeTaken, TimeUnit units) + @Override + public void accept(Verb verb, long timeTaken, TimeUnit units) { if (timeTaken > 0) { @@ -94,41 +94,49 @@ private static final class DroppedForVerb } private final Timer allLatency; - public final ConcurrentHashMap dcLatency; - public final EnumMap internalLatency; + public final Map dcLatency = new ConcurrentHashMap<>(); + public final Map internalLatency = new ConcurrentHashMap<>(); // total dropped message counts for server lifetime - private final Map droppedMessages = new EnumMap<>(Verb.class); + private final Map droppedMessages = new ConcurrentHashMap<>(); + + // dropped mutations by table + private final Map droppedMutationsByTable = new ConcurrentHashMap<>(); public MessagingMetrics() { allLatency = Metrics.timer(factory.createMetricName("CrossNodeLatency")); - dcLatency = new ConcurrentHashMap<>(); - internalLatency = new EnumMap<>(Verb.class); - for (Verb verb : Verb.VERBS) + for (Verb verb : Verb.getValues()) + { internalLatency.put(verb, Metrics.timer(factory.createMetricName(verb + "-WaitLatency"))); - for (Verb verb : Verb.values()) droppedMessages.put(verb, new DroppedForVerb(verb)); + } } - public DCLatencyRecorder internodeLatencyRecorder(InetAddressAndPort from) + public LatencyConsumer internodeLatencyRecorder(InetAddressAndPort from) { String dcName = DatabaseDescriptor.getEndpointSnitch().getDatacenter(from); - DCLatencyRecorder dcUpdater = dcLatency.get(dcName); - if (dcUpdater == null) - dcUpdater = dcLatency.computeIfAbsent(dcName, k -> new DCLatencyRecorder(Metrics.timer(factory.createMetricName(dcName + "-Latency")), allLatency)); + DCLatencyRecorder dcUpdater = dcLatency.computeIfAbsent(dcName, + k -> new DCLatencyRecorder(Metrics.timer(factory.createMetricName(dcName + "-Latency")), + allLatency)); return dcUpdater; } - public void recordInternalLatency(Verb verb, long timeTaken, TimeUnit units) + public void recordInternalLatency(Verb verb, InetAddressAndPort from, long timeTaken, TimeUnit units) { if (timeTaken > 0) - internalLatency.get(verb).update(timeTaken, units); + { + // We need to potentially compute absent entries if this is a custom verb + // that is not present in the Verb.getValues() list because it was + // instantiated after the Messaging metrics was created. + Timer latency = internalLatency.computeIfAbsent(verb, v -> Metrics.timer(factory.createMetricName(v + "-WaitLatency"))); + latency.update(timeTaken, units); + } } public void recordSelfDroppedMessage(Verb verb) { - recordDroppedMessage(droppedMessages.get(verb), false); + recordDroppedMessage(droppedMessages.computeIfAbsent(verb, v -> new DroppedForVerb(v)), false); } public void recordSelfDroppedMessage(Verb verb, long timeElapsed, TimeUnit timeUnit) @@ -141,14 +149,44 @@ public void recordInternodeDroppedMessage(Verb verb, long timeElapsed, TimeUnit recordDroppedMessage(verb, timeElapsed, timeUnit, true); } + @Override + public void recordMessageStageProcessingTime(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit unit) + { + // NOOP + } + + @Override + public void recordTotalMessageProcessingTime(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit unit) + { + // NOOP + } + public void recordDroppedMessage(Message message, long timeElapsed, TimeUnit timeUnit) { recordDroppedMessage(message.verb(), timeElapsed, timeUnit, message.isCrossNode()); + + if (message.verb() == Verb.MUTATION_REQ && message.payload instanceof org.apache.cassandra.db.Mutation) + { + org.apache.cassandra.db.Mutation mutation = (org.apache.cassandra.db.Mutation) message.payload; + for (org.apache.cassandra.db.partitions.PartitionUpdate update : mutation.getPartitionUpdates()) + { + String tableKey = update.metadata().keyspace + '.' + update.metadata().name; + DroppedMessageMetrics tableMetrics = droppedMutationsByTable.get(tableKey); + if (tableMetrics == null) + tableMetrics = droppedMutationsByTable.computeIfAbsent(tableKey, + k -> new DroppedMessageMetrics("DroppedMutations", k)); + tableMetrics.dropped.mark(); + if (message.isCrossNode()) + tableMetrics.crossNodeDroppedLatency.update(timeElapsed, timeUnit); + else + tableMetrics.internalDroppedLatency.update(timeElapsed, timeUnit); + } + } } public void recordDroppedMessage(Verb verb, long timeElapsed, TimeUnit timeUnit, boolean isCrossNode) { - recordDroppedMessage(droppedMessages.get(verb), timeElapsed, timeUnit, isCrossNode); + recordDroppedMessage(droppedMessages.computeIfAbsent(verb, v -> new DroppedForVerb(v)), timeElapsed, timeUnit, isCrossNode); } private static void recordDroppedMessage(DroppedForVerb droppedMessages, long timeTaken, TimeUnit units, boolean isCrossNode) @@ -184,6 +222,14 @@ public Map getDroppedMessages() map.put(entry.getKey().toString(), (int) entry.getValue().metrics.dropped.getCount()); return map; } + + public Map getDroppedMutationsByTable() + { + Map map = new HashMap<>(droppedMutationsByTable.size()); + for (Map.Entry entry : droppedMutationsByTable.entrySet()) + map.put(entry.getKey(), entry.getValue().dropped.getCount()); + return map; + } private void logDroppedMessages() { @@ -222,6 +268,6 @@ public int resetAndConsumeDroppedErrors(Consumer messageConsumer) public void resetDroppedMessages() { droppedMessages.replaceAll((u, v) -> new DroppedForVerb(new DroppedMessageMetrics(u))); + droppedMutationsByTable.clear(); } - } diff --git a/src/java/org/apache/cassandra/metrics/MicrometerBufferPoolMetrics.java b/src/java/org/apache/cassandra/metrics/MicrometerBufferPoolMetrics.java new file mode 100644 index 000000000000..e5368eb05640 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/MicrometerBufferPoolMetrics.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import com.codahale.metrics.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import org.apache.cassandra.utils.memory.BufferPool; + +public class MicrometerBufferPoolMetrics extends MicrometerMetrics implements BufferPoolMetrics +{ + private static final String METRICS_PREFIX = "buffer_pool"; + + public static final String TOTAL_SIZE_BYTES = METRICS_PREFIX + "_total_size_bytes"; + public static final String USED_SIZE_BYTES = METRICS_PREFIX + "_used_size_bytes"; + public static final String OVERFLOW_SIZE_BYTES = METRICS_PREFIX + "_overflow_size_bytes"; + public static final String OVERFLOW_ALLOCATIONS = METRICS_PREFIX + "_overflow_allocations"; + public static final String POOL_ALLOCATIONS = METRICS_PREFIX + "_pool_allocations"; + public static final String NAME_TAG = "pool_name"; + + private final String scope; + private final BufferPool bufferPool; + + /** Total number of hits */ + private final Meter hits; + + /** Total number of misses */ + private final Meter misses; + + public MicrometerBufferPoolMetrics(String scope, BufferPool bufferPool) + { + super(); + + this.scope = scope; + this.bufferPool = bufferPool; + this.hits = new Meter(); + this.misses = new Meter(); + } + + @Override + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + super.register(newRegistry, newTags.and(NAME_TAG, scope)); + + gauge(TOTAL_SIZE_BYTES, bufferPool, BufferPool::sizeInBytes); + gauge(USED_SIZE_BYTES, bufferPool, BufferPool::usedSizeInBytes); + gauge(OVERFLOW_SIZE_BYTES, bufferPool, BufferPool::overflowMemoryInBytes); + gauge(OVERFLOW_ALLOCATIONS, misses, Meter::getMeanRate); + gauge(POOL_ALLOCATIONS, hits, Meter::getMeanRate); + } + + @Override + public void markHit() + { + hits.mark(); + } + + @Override + public long hits() + { + return hits.getCount(); + } + + public void markMissed() + { + misses.mark(); + } + + @Override + public long misses() + { + return misses.getCount(); + } + + @Override + public long overflowSize() + { + return bufferPool.overflowMemoryInBytes(); + } + + @Override + public long usedSize() + { + return bufferPool.usedSizeInBytes(); + } + + @Override + public long size() + { + return bufferPool.sizeInBytes(); + } + + @Override + public void register3xAlias() + { + // Not implemented + } +} diff --git a/src/java/org/apache/cassandra/metrics/MicrometerCacheMetrics.java b/src/java/org/apache/cassandra/metrics/MicrometerCacheMetrics.java new file mode 100644 index 000000000000..663702147822 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/MicrometerCacheMetrics.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.common.annotations.VisibleForTesting; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import org.apache.cassandra.cache.CacheSize; +import org.apache.cassandra.utils.ExpMovingAverage; +import org.apache.cassandra.utils.MovingAverage; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +public class MicrometerCacheMetrics extends MicrometerMetrics implements CacheMetrics +{ + @VisibleForTesting + final static long hitRateUpdateIntervalNanos = TimeUnit.MILLISECONDS.toNanos(100); + + private final String metricsPrefix; + private final CacheSize cache; + private final MovingAverage hitRate; + private final AtomicLong hitRateUpdateTime; + + private volatile Counter misses; + private volatile Counter hits; + private volatile Counter requests; + private volatile double totalRequestsLastUpdate; + + public MicrometerCacheMetrics(String metricsPrefix, CacheSize cache) + { + this.metricsPrefix = metricsPrefix; + this.cache = cache; + this.hitRate = ExpMovingAverage.decayBy1000(); + this.hitRateUpdateTime = new AtomicLong(nanoTime()); + + this.misses = counter(metricsPrefix + "_misses"); + this.hits = counter(metricsPrefix + "_hits"); + this.requests = counter(metricsPrefix + "_requests"); + } + + @Override + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + super.register(newRegistry, newTags); + + gauge(metricsPrefix + "_capacity", cache, CacheSize::capacity); + gauge(metricsPrefix + "_size", cache, CacheSize::weightedSize); + gauge(metricsPrefix + "_num_entries", cache, CacheSize::size); + gauge(metricsPrefix + "_hit_rate", hitRate, MovingAverage::get); + + this.misses = counter(metricsPrefix + "_misses"); + this.hits = counter(metricsPrefix + "_hits"); + this.requests = counter(metricsPrefix + "_requests"); + } + + @Override + public long requests() + { + return (long) (misses.count() + hits.count()); + } + + @Override + public long capacity() + { + return cache.capacity(); + } + + @Override + public long size() + { + return cache.weightedSize(); + } + + @Override + public long entries() + { + return cache.size(); + } + + @Override + public long hits() + { + return (long) hits.count(); + } + + @Override + public long misses() + { + return (long) misses.count(); + } + + @Override + public double hitRate() + { + return hitRate.get(); + } + + @Override + public double hitOneMinuteRate() + { + return Double.NaN; + } + + @Override + public double hitFiveMinuteRate() + { + return Double.NaN; + } + + @Override + public double hitFifteenMinuteRate() + { + return Double.NaN; + } + + @Override + public double requestsFifteenMinuteRate() + { + return Double.NaN; + } + + @Override + public void recordHits(int count) + { + hits.increment(count); + updateHitRate(); + } + + @Override + public void recordMisses(int count) + { + misses.increment(count); + updateHitRate(); + } + + private void updateHitRate() + { + long lastUpdate = hitRateUpdateTime.get(); + long now = nanoTime(); + if (now - lastUpdate > hitRateUpdateIntervalNanos) + { + if (hitRateUpdateTime.compareAndSet(lastUpdate, now)) + { + double hitCount = hits.count(); + double numRequests = hitCount + misses.count(); + double delta = numRequests - totalRequestsLastUpdate; + requests.increment(delta); + totalRequestsLastUpdate = numRequests; + if (numRequests > 0) + hitRate.update(hitCount / numRequests); + else + hitRate.update(0); + } + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/metrics/MicrometerChunkCacheMetrics.java b/src/java/org/apache/cassandra/metrics/MicrometerChunkCacheMetrics.java new file mode 100644 index 000000000000..840a9d19382f --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/MicrometerChunkCacheMetrics.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + +import com.google.common.annotations.VisibleForTesting; + +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import org.apache.cassandra.cache.CacheSize; +import org.apache.cassandra.utils.FBUtilities; + +/** + * Micrometer implementation for the chunk cache metrics. + */ +public class MicrometerChunkCacheMetrics extends MicrometerMetrics implements ChunkCacheMetrics +{ + private final CacheSize cache; + private final String metricsPrefix; + + private volatile MicrometerCacheMetrics metrics; + private volatile Timer missLatency; + private volatile Counter evictions; + private final ConcurrentHashMap evitictionByRemovalCause = new ConcurrentHashMap<>(); + + public MicrometerChunkCacheMetrics(CacheSize cache, String metricsPrefix) + { + this.cache = cache; + this.metricsPrefix = metricsPrefix; + + registerMetrics(registryWithTags().left, registryWithTags().right); + } + + private void registerMetrics(MeterRegistry registry, Tags tags) + { + this.metrics = new MicrometerCacheMetrics(metricsPrefix, cache); + this.metrics.register(registry, tags); + + this.missLatency = timer(metricsPrefix + "_miss_latency_seconds"); + this.evictions = counter(metricsPrefix + "_evictions"); + + for (RemovalCause cause : RemovalCause.values()) + { + evitictionByRemovalCause.put(cause, counter(metricsPrefix + "_evictions_" + cause.toString().toLowerCase())); + } + } + + @Override + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + super.register(newRegistry, newTags); + registerMetrics(newRegistry, newTags); + } + + @Override + public void recordMisses(int count) + { + metrics.recordMisses(count); + } + + @Override + public void recordLoadSuccess(long val) + { + missLatency.record(val, TimeUnit.NANOSECONDS); + } + + @Override + public void recordLoadFailure(long val) + { + } + + @Override + public void recordEviction(int weight, RemovalCause removalCause) { + if (removalCause.wasEvicted()) + { + evictions.increment(1); + } + Counter counter = evitictionByRemovalCause.get(removalCause); + if (counter != null) { + counter.increment(1); + } + } + + @Override + public void recordEviction() + { + evictions.increment(); + } + + @Override + public void recordHits(int count) + { + metrics.recordHits(count); + } + + @Override + public double hitRate() + { + return metrics.hitRate(); + } + + @Override + public double hitOneMinuteRate() + { + return metrics.hitOneMinuteRate(); + } + + @Override + public double hitFiveMinuteRate() + { + return metrics.hitFiveMinuteRate(); + } + + @Override + public double hitFifteenMinuteRate() + { + return metrics.hitFifteenMinuteRate(); + } + + @Override + public double requestsFifteenMinuteRate() + { + return metrics.requestsFifteenMinuteRate(); + } + + @Override + public long requests() + { + return metrics.requests(); + } + + @Override + public long misses() + { + return metrics.misses(); + } + + @Override + public long hits() + { + return metrics.hits(); + } + + @Override + public double missLatency() + { + return missLatency.mean(TimeUnit.NANOSECONDS); + } + + @Override + public long capacity() + { + return metrics.capacity(); + } + + @Override + public long size() + { + return metrics.size(); + } + + public long entries() + { + return metrics.entries(); + } + + @Override + @VisibleForTesting + public void reset() + { + // This method is only used for unit tests, and unit tests only use the codahale implementation + throw new UnsupportedOperationException("This was not expected to be called and should be implemented if required"); + } + + @Nonnull + @Override + public CacheStats snapshot() + { + return CacheStats.of(metrics.hits(), metrics.misses(), missLatency.count(), + 0L, (long) missLatency.totalTime(TimeUnit.NANOSECONDS), (long) evictions.count(), 0L); + } + + @Override + public String toString() + { + return "Chunk cache metrics: " + System.lineSeparator() + + "Miss latency in seconds: " + missLatency() + System.lineSeparator() + + "Misses count: " + misses() + System.lineSeparator() + + "Hits count: " + hits() + System.lineSeparator() + + "Cache requests count: " + requests() + System.lineSeparator() + + "Moving hit rate: " + hitRate() + System.lineSeparator() + + "Num entries: " + entries() + System.lineSeparator() + + "Size in memory: " + FBUtilities.prettyPrintMemory(size()) + System.lineSeparator() + + "Capacity: " + FBUtilities.prettyPrintMemory(capacity()); + } + + public Map getEvictionCountByRemovalCause() + { + Map result = new HashMap<>(); + for (Map.Entry entry : evitictionByRemovalCause.entrySet()) + { + result.put(entry.getKey(), entry.getValue().count()); + } + return result; + } +} diff --git a/src/java/org/apache/cassandra/metrics/MicrometerMetrics.java b/src/java/org/apache/cassandra/metrics/MicrometerMetrics.java new file mode 100644 index 000000000000..c8116df2d676 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/MicrometerMetrics.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import java.util.function.ToDoubleFunction; + +import com.google.common.base.Preconditions; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.cassandra.utils.Pair; + +public abstract class MicrometerMetrics +{ + private volatile Pair registryWithTags; + + protected MicrometerMetrics() + { + this.registryWithTags = Pair.create(new SimpleMeterRegistry(), Tags.empty()); + } + + public Counter counter(String name) + { + Pair current = registryWithTags; + return current.left.counter(name, current.right); + } + + public Counter counter(String name, Tags tags) + { + Pair current = registryWithTags; + return current.left.counter(name, current.right.and(tags)); + } + + public Timer timer(String name) + { + return timer(name, false); + } + + public Timer timer(String name, boolean publishHistogram) + { + return timer(name, publishHistogram, Tags.empty()); + } + + public Timer timer(String name, boolean publishHistogram, Tags tags) + { + Pair current = registryWithTags; + Timer.Builder builder = Timer.builder(name).tags(current.right.and(tags)); + if (publishHistogram) + builder = builder.publishPercentileHistogram(); + + return builder.register(current.left); + } + + public T gauge(String name, T obj, ToDoubleFunction fcn) + { + Pair current = registryWithTags; + return current.left.gauge(name, current.right, obj, fcn); + } + + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + Preconditions.checkArgument(!this.registryWithTags.left.equals(newRegistry), "Cannot set the same registry twice!"); + this.registryWithTags = Pair.create(newRegistry, newTags); + } + + public Pair registryWithTags() + { + return this.registryWithTags; + } +} diff --git a/src/java/org/apache/cassandra/metrics/MicrometerNativeMemoryMetrics.java b/src/java/org/apache/cassandra/metrics/MicrometerNativeMemoryMetrics.java new file mode 100644 index 000000000000..5e6e660fd07b --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/MicrometerNativeMemoryMetrics.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; + +public class MicrometerNativeMemoryMetrics extends MicrometerMetrics implements NativeMemoryMetrics +{ + private static final Logger logger = LoggerFactory.getLogger(MicrometerNativeMemoryMetrics.class); + + private static final String METRICS_PREFIX = "jvm_native_memory"; + + public static final String RAW_NATIVE_MEMORY = METRICS_PREFIX + "_raw_native_memory"; + public static final String BLOOM_FILTER_MEMORY = METRICS_PREFIX + "_bloom_filter_memory"; + public static final String COMPRESSION_METADATA_MEMORY = METRICS_PREFIX + "_compression_metadata_memory"; + public static final String NETWORK_DIRECT_MEMORY = METRICS_PREFIX + "_network_direct_memory"; + public static final String USED_NIO_DIRECT_MEMORY = METRICS_PREFIX + "_used_nio_direct_memory"; + public static final String TOTAL_NIO_MEMORY = METRICS_PREFIX + "_total_nio_direct_memory"; + public static final String NIO_DIRECT_BUFFER_COUNT = METRICS_PREFIX + "_nio_direct_buffer_count"; + public static final String TOTAL_MEMORY = METRICS_PREFIX + "_total_memory"; + + public MicrometerNativeMemoryMetrics() + { + if (directBufferPool == null) + logger.error("Direct memory buffer pool MBean not present, native memory metrics will be missing for nio buffers"); + } + + @Override + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + super.register(newRegistry, newTags); + + gauge(RAW_NATIVE_MEMORY, this, NativeMemoryMetrics::rawNativeMemory); + gauge(BLOOM_FILTER_MEMORY, this, NativeMemoryMetrics::bloomFilterMemory); + gauge(COMPRESSION_METADATA_MEMORY, this, NativeMemoryMetrics::compressionMetadataMemory); + gauge(NETWORK_DIRECT_MEMORY, this, NativeMemoryMetrics::networkDirectMemory); + gauge(USED_NIO_DIRECT_MEMORY, this, NativeMemoryMetrics::usedNioDirectMemory); + gauge(TOTAL_NIO_MEMORY, this, NativeMemoryMetrics::totalNioDirectMemory); + gauge(NIO_DIRECT_BUFFER_COUNT, this, NativeMemoryMetrics::nioDirectBufferCount); + gauge(TOTAL_MEMORY, this, NativeMemoryMetrics::totalMemory); + } + + @Override + public long usedNioDirectMemoryValue() + { + return usedNioDirectMemory(); + } +} diff --git a/src/java/org/apache/cassandra/metrics/MinMaxAvgMetric.java b/src/java/org/apache/cassandra/metrics/MinMaxAvgMetric.java index b65f52f486b7..6e985536bc15 100644 --- a/src/java/org/apache/cassandra/metrics/MinMaxAvgMetric.java +++ b/src/java/org/apache/cassandra/metrics/MinMaxAvgMetric.java @@ -44,20 +44,20 @@ public MinMaxAvgMetric(MetricNameFactory factory, String namePrefix) this.factory = factory; this.namePrefix = namePrefix; - minGauge = Metrics.register(factory.createMetricName(namePrefix + "Min"), () -> min); - maxGauge = Metrics.register(factory.createMetricName(namePrefix + "Max"), () -> max); - avgGauge = Metrics.register(factory.createMetricName(namePrefix + "Avg"), () -> numSamples > 0 ? ((double) sum) / numSamples : 0); - stddevGauge = Metrics.register(factory.createMetricName(namePrefix + "StdDev"), () -> stddev()); - numSamplesGauge = Metrics.register(factory.createMetricName(namePrefix + "NumSamples"), () -> numSamples); + minGauge = Metrics.register(factory.createMetricName(namePrefix + " Min"), () -> min); + maxGauge = Metrics.register(factory.createMetricName(namePrefix + " Max"), () -> max); + avgGauge = Metrics.register(factory.createMetricName(namePrefix + " Avg"), () -> numSamples > 0 ? ((double) sum) / numSamples : 0); + stddevGauge = Metrics.register(factory.createMetricName(namePrefix + " StdDev"), () -> stddev()); + numSamplesGauge = Metrics.register(factory.createMetricName(namePrefix + " NumSamples"), () -> numSamples); } public void release() { - Metrics.remove(factory.createMetricName(namePrefix + "Min")); - Metrics.remove(factory.createMetricName(namePrefix + "Max")); - Metrics.remove(factory.createMetricName(namePrefix + "Avg")); - Metrics.remove(factory.createMetricName(namePrefix + "StdDev")); - Metrics.remove(factory.createMetricName(namePrefix + "NumSamples")); + Metrics.remove(factory.createMetricName(namePrefix + " Min")); + Metrics.remove(factory.createMetricName(namePrefix + " Max")); + Metrics.remove(factory.createMetricName(namePrefix + " Avg")); + Metrics.remove(factory.createMetricName(namePrefix + " StdDev")); + Metrics.remove(factory.createMetricName(namePrefix + " NumSamples")); } public void reset() diff --git a/src/java/org/apache/cassandra/metrics/NativeMemoryMetrics.java b/src/java/org/apache/cassandra/metrics/NativeMemoryMetrics.java new file mode 100644 index 000000000000..1909733680ae --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/NativeMemoryMetrics.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import java.lang.management.BufferPoolMXBean; +import java.lang.management.ManagementFactory; + +import io.netty.util.internal.PlatformDependent; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.utils.BloomFilter; +import org.apache.cassandra.utils.memory.MemoryUtil; + +public interface NativeMemoryMetrics +{ + BufferPoolMXBean directBufferPool = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class) + .stream() + .filter(bpMBean -> bpMBean.getName().equals("direct")) + .findFirst() + .orElse(null); + + NativeMemoryMetrics instance = CassandraRelevantProperties.USE_MICROMETER.getBoolean() + ? new MicrometerNativeMemoryMetrics() + : new CodehaleNativeMemoryMetrics(); + + long usedNioDirectMemoryValue(); + + default long rawNativeMemory() + { + return MemoryUtil.allocated(); + } + + default long bloomFilterMemory() + { + return BloomFilter.memoryLimiter.memoryAllocated(); + } + + default long compressionMetadataMemory() + { + return CompressionMetadata.nativeMemoryAllocated(); + } + + default long usedNioDirectMemory() + { + return directBufferPool == null ? 0 : directBufferPool.getMemoryUsed(); + } + + default long totalNioDirectMemory() + { + return directBufferPool == null ? 0 : directBufferPool.getTotalCapacity(); + } + + default long nioDirectBufferCount() + { + return directBufferPool == null ? 0 : directBufferPool.getCount(); + } + + default long networkDirectMemory() + { + return PlatformDependent.usedDirectMemory(); + } + + default boolean usingNioMemoryForNetwork() + { + return !PlatformDependent.useDirectBufferNoCleaner(); + } + + default long totalMemory() + { + // Use totalNioDirectMemory() instead of usedNioDirectMemory() because without + // -Dsun.nio.PageAlignDirectMemory=true the two are identical. If someone adds + // this flag again, we would prefer to include the JVM padding in the total memory. + // Also only add the network memory if it's not allocated as NIO direct memory + return rawNativeMemory() + totalNioDirectMemory() + (usingNioMemoryForNetwork() ? 0 : networkDirectMemory()); + } +} diff --git a/src/java/org/apache/cassandra/metrics/PairedSlidingWindowReservoir.java b/src/java/org/apache/cassandra/metrics/PairedSlidingWindowReservoir.java new file mode 100644 index 000000000000..0d512d801c84 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/PairedSlidingWindowReservoir.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +/** + * A Reservoir-like class that tracks the last N paired measurements. + */ +public class PairedSlidingWindowReservoir +{ + private final IntIntPair[] measurements; + private long count; + + public PairedSlidingWindowReservoir(int size) + { + this.measurements = new IntIntPair[size]; + this.count = 0L; + } + + public int size() + { + if (this.count >= this.measurements.length) + return this.measurements.length; + + synchronized (this) + { + return (int) Math.min(this.count, this.measurements.length); + } + } + + public synchronized void update(int value1, int value2) + { + this.measurements[(int) (this.count++ % this.measurements.length)] = new IntIntPair(value1, value2); + } + + public PairedSnapshot getSnapshot() + { + var values = new IntIntPair[this.size()]; + System.arraycopy(this.measurements, 0, values, 0, values.length); + return new PairedSnapshot(values); + } + + /** + * A pair of ints. "y" and "x" are used to imply that the first value is the + * dependent one for a LinearFit computation. + */ + public static class IntIntPair + { + public final int x; + public final int y; + + IntIntPair(int x, int y) + { + this.y = y; + this.x = x; + } + + @Override + public String toString() + { + return String.format("(%d,%d)", x, y); + } + } + + public static class PairedSnapshot + { + public final IntIntPair[] values; + + public PairedSnapshot(IntIntPair[] values) + { + this.values = values; + } + } +} diff --git a/src/java/org/apache/cassandra/metrics/QuickSlidingWindowReservoir.java b/src/java/org/apache/cassandra/metrics/QuickSlidingWindowReservoir.java new file mode 100644 index 000000000000..1a897ef3877a --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/QuickSlidingWindowReservoir.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import com.codahale.metrics.Reservoir; +import com.codahale.metrics.Snapshot; +import com.codahale.metrics.UniformSnapshot; + +/** + * A reservoir that stores the last N measurements, following the same design + * as com.codahale.metrics.SlidingWindowReservoir but adding a snapshot-free getMean(). + */ +public class QuickSlidingWindowReservoir implements Reservoir +{ + private final long[] measurements; + private long count; + + public QuickSlidingWindowReservoir(int size) + { + this.measurements = new long[size]; + this.count = 0L; + } + + public int size() + { + if (this.count >= this.measurements.length) + return this.measurements.length; + + synchronized (this) + { + return (int) Math.min(this.count, this.measurements.length); + } + } + + public synchronized void update(long value) + { + this.measurements[(int) (this.count++ % this.measurements.length)] = value; + } + + /** + * Returns the mean of the values in the reservoir, without synchronization. (Generally, + * new values will be just as valid as the old ones.) For a strictly consistent view, + * use {@link #getSnapshot()}. + */ + public double getMean() + { + final int sz = size(); + + if (sz == 0) + return 0.0; + + double sum = 0.0; + for (int i = 0; i < sz; ++i) + sum += this.measurements[i]; + + return sum / sz; + } + + public Snapshot getSnapshot() + { + long[] values = new long[this.size()]; + synchronized (this) + { + System.arraycopy(this.measurements, 0, values, 0, values.length); + } + return new UniformSnapshot(values); + } +} diff --git a/src/java/org/apache/cassandra/metrics/ReadCoordinationMetrics.java b/src/java/org/apache/cassandra/metrics/ReadCoordinationMetrics.java new file mode 100644 index 000000000000..2f7aedad49b4 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/ReadCoordinationMetrics.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import com.google.common.annotations.VisibleForTesting; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Histogram; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.locator.InetAddressAndPort; + +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +/** + * Metrics for read coordination behaviors. + */ +public final class ReadCoordinationMetrics +{ + private static final MetricNameFactory factory = new DefaultNameFactory("ReadCoordination"); + + public static final Counter nonreplicaRequests = Metrics.counter(factory.createMetricName("LocalNodeNonreplicaRequests")); + public static final Counter preferredOtherReplicas = Metrics.counter(factory.createMetricName("PreferredOtherReplicas")); + + private static final ConcurrentMap replicaLatencies = new ConcurrentHashMap<>(); + + public static void updateReplicaLatency(InetAddressAndPort address, long latency, TimeUnit timeUnit) + { + if (latency >= DatabaseDescriptor.getReadRpcTimeout(timeUnit)) + return; // don't track timeouts + + Histogram histogram = replicaLatencies.get(address); + + // avoid computeIfAbsent() call on the common path + if (null == histogram) + histogram = replicaLatencies.computeIfAbsent(address, ReadCoordinationMetrics::createHistogram); + + histogram.update(latency); + } + + private static Histogram createHistogram(InetAddressAndPort h) + { + CassandraMetricsRegistry.MetricName metricName = DefaultNameFactory.createMetricName("ReadCoordination", "ReplicaLatency", h.getHostAddressAndPort().replace(':', '.')); + return Metrics.histogram(metricName, false); + } + + @VisibleForTesting + static Histogram getReplicaLatencyHistogram(InetAddressAndPort address) + { + return replicaLatencies.get(address); + } +} diff --git a/src/java/org/apache/cassandra/metrics/RepairMetrics.java b/src/java/org/apache/cassandra/metrics/RepairMetrics.java index 27dbbd31181c..364e4ae80e83 100644 --- a/src/java/org/apache/cassandra/metrics/RepairMetrics.java +++ b/src/java/org/apache/cassandra/metrics/RepairMetrics.java @@ -19,7 +19,7 @@ package org.apache.cassandra.metrics; import java.util.Collections; -import java.util.EnumMap; +import java.util.HashMap; import java.util.Map; import com.google.common.annotations.VisibleForTesting; @@ -44,9 +44,9 @@ public class RepairMetrics static { - Map retries = new EnumMap<>(Verb.class); - Map timeout = new EnumMap<>(Verb.class); - Map failure = new EnumMap<>(Verb.class); + Map retries = new HashMap<>(); + Map timeout = new HashMap<>(); + Map failure = new HashMap<>(); for (Verb verb : RepairMessage.ALLOWS_RETRY) { retries.put(verb, Metrics.histogram(DefaultNameFactory.createMetricName(TYPE_NAME, "Retries-" + verb.name(), null), false)); diff --git a/src/java/org/apache/cassandra/metrics/ReplicaResponseSizeMetrics.java b/src/java/org/apache/cassandra/metrics/ReplicaResponseSizeMetrics.java new file mode 100644 index 000000000000..c158fe840964 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/ReplicaResponseSizeMetrics.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.metrics; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Histogram; + +import org.apache.cassandra.config.CassandraRelevantProperties; + +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +/** + * Metrics for tracking result sizes coming from replicas/writers to coordinators. + */ +public class ReplicaResponseSizeMetrics +{ + private static final String TYPE = "ReplicaResponseSize"; + + /** + * Controls whether replica response size metrics collection is enabled. + */ + private static final boolean METRICS_ENABLED = CassandraRelevantProperties.REPLICA_RESPONSE_SIZE_METRICS_ENABLED.getBoolean(); + + /** Total bytes received from replicas in response messages */ + public static final Counter totalBytesReceived = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "TotalBytesReceived", null)); + + /** Histogram of response sizes from replicas */ + public static final Histogram bytesReceivedPerResponse = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "BytesReceivedPerResponse", null), true); + + /** Total bytes received from replicas in read responses */ + public static final Counter readResponseBytesReceived = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "ReadResponseBytesReceived", null)); + + /** Histogram of read response sizes from replicas */ + public static final Histogram readResponseBytesPerResponse = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "ReadResponseBytesPerResponse", null), true); + + /** Total bytes received from replicas in write responses */ + public static final Counter writeResponseBytesReceived = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "WriteResponseBytesReceived", null)); + + /** Histogram of write response sizes from replicas */ + public static final Histogram writeResponseBytesPerResponse = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "WriteResponseBytesPerResponse", null), true); + + /** + * Check if metrics collection is enabled + * @return true if metrics are enabled, false otherwise + */ + public static boolean isMetricsEnabled() + { + return METRICS_ENABLED; + } + + /** + * Record the size of a read response received from a replica + * @param responseSize the size of the response in bytes + */ + public static void recordReadResponseSize(int responseSize) + { + if (!METRICS_ENABLED) + return; + + totalBytesReceived.inc(responseSize); + bytesReceivedPerResponse.update(responseSize); + readResponseBytesReceived.inc(responseSize); + readResponseBytesPerResponse.update(responseSize); + } + + /** + * Record the size of a write response received from a replica + * @param responseSize the size of the response in bytes + */ + public static void recordWriteResponseSize(int responseSize) + { + if (!METRICS_ENABLED) + return; + + totalBytesReceived.inc(responseSize); + bytesReceivedPerResponse.update(responseSize); + writeResponseBytesReceived.inc(responseSize); + writeResponseBytesPerResponse.update(responseSize); + } +} diff --git a/src/java/org/apache/cassandra/metrics/RestorableMeter.java b/src/java/org/apache/cassandra/metrics/RestorableMeter.java index ea3fddeb2be6..166cef2b0e2a 100644 --- a/src/java/org/apache/cassandra/metrics/RestorableMeter.java +++ b/src/java/org/apache/cassandra/metrics/RestorableMeter.java @@ -19,14 +19,20 @@ package org.apache.cassandra.metrics; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + import static java.lang.Math.exp; import com.codahale.metrics.Clock; /** - * A meter metric which measures mean throughput as well as fifteen-minute and two-hour + * A meter metric which measures mean throughput as well as one-minute, five-minute, fifteen-minute and two-hour * exponentially-weighted moving average throughputs. * * This is based heavily on the Meter and EWMA classes from codahale/yammer metrics. @@ -35,10 +41,21 @@ */ public class RestorableMeter { - private static final long TICK_INTERVAL = TimeUnit.SECONDS.toNanos(5); + public static final Set AVAILABLE_WINDOWS = Set.of(1, 5, 15, 120); + + public static final long TICK_INTERVAL = TimeUnit.SECONDS.toNanos(5); private static final double NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + @Nullable + private final RestorableEWMA m1Rate; + + @Nullable + private final RestorableEWMA m5Rate; + + @Nullable private final RestorableEWMA m15Rate; + + @Nullable private final RestorableEWMA m120Rate; private final AtomicLong count = new AtomicLong(); @@ -47,29 +64,125 @@ public class RestorableMeter private final Clock clock = Clock.defaultClock(); /** - * Creates a new, uninitialized RestorableMeter. + * Creates a new RestorableMeter with given ewmas. */ - public RestorableMeter() + private RestorableMeter(@Nullable RestorableEWMA m1Rate, @Nullable RestorableEWMA m5Rate, @Nullable RestorableEWMA m15Rate, @Nullable RestorableEWMA m120Rate) { - this.m15Rate = new RestorableEWMA(TimeUnit.MINUTES.toSeconds(15)); - this.m120Rate = new RestorableEWMA(TimeUnit.MINUTES.toSeconds(120)); + this.m1Rate = m1Rate; + this.m5Rate = m5Rate; + this.m15Rate = m15Rate; + this.m120Rate = m120Rate; this.startTime = this.clock.getTick(); this.lastTick = new AtomicLong(startTime); } /** - * Restores a RestorableMeter from the last seen 15m and 2h rates. + * Restores a RestorableMeter from the last seen 15m and 2h rates. 1m and 5m rates are not initialized. + * * @param lastM15Rate the last-seen 15m rate, in terms of events per second * @param lastM120Rate the last seen 2h rate, in terms of events per second */ + @VisibleForTesting public RestorableMeter(double lastM15Rate, double lastM120Rate) { - this.m15Rate = new RestorableEWMA(lastM15Rate, TimeUnit.MINUTES.toSeconds(15)); - this.m120Rate = new RestorableEWMA(lastM120Rate, TimeUnit.MINUTES.toSeconds(120)); + this.m1Rate = null; + this.m5Rate = null; + this.m15Rate = ewma(15, lastM15Rate); + this.m120Rate = ewma(120, lastM120Rate); this.startTime = this.clock.getTick(); this.lastTick = new AtomicLong(startTime); } + public static Builder builder() + { + return new Builder(); + } + + /** + * Create a restorable meter with default rates (15m and 120m) + */ + public static RestorableMeter createWithDefaultRates() + { + return new Builder().withM15Rate().withM120Rate().build(); + } + + public static class Builder + { + private RestorableEWMA m1Rate; + private RestorableEWMA m5Rate; + private RestorableEWMA m15Rate; + private RestorableEWMA m120Rate; + + public Builder withWindow(int window) + { + switch (window) + { + case 1 : return withM1Rate(); + case 5 : return withM5Rate(); + case 15 : return withM15Rate(); + case 120 : return withM120Rate(); + default : throw new IllegalArgumentException(String.format("Found invalid window=%s, available windows: %s", window, AVAILABLE_WINDOWS)); + } + } + + public Builder withM1Rate() + { + Preconditions.checkState(m1Rate == null); + this.m1Rate = ewma(1); + return this; + } + + public Builder withM5Rate() + { + Preconditions.checkState(m5Rate == null); + this.m5Rate = ewma(5); + return this; + } + + public Builder withM15Rate() + { + Preconditions.checkState(m15Rate == null); + this.m15Rate = ewma(15); + return this; + } + + public Builder withM15Rate(double lastM15Rate) + { + Preconditions.checkState(m15Rate == null); + this.m15Rate = ewma(15, lastM15Rate); + return this; + } + + public Builder withM120Rate() + { + Preconditions.checkState(m120Rate == null); + this.m120Rate = ewma(120); + return this; + } + + public Builder withM120Rate(double lastM120Rate) + { + Preconditions.checkState(m120Rate == null); + this.m120Rate = ewma(120, lastM120Rate); + return this; + } + + public RestorableMeter build() + { + return new RestorableMeter(m1Rate, m5Rate, m15Rate, m120Rate); + } + } + + private static RestorableEWMA ewma(int minute, double lastRate) + { + return new RestorableEWMA(lastRate, TimeUnit.MINUTES.toSeconds(minute)); + } + + private static RestorableEWMA ewma(int minute) + { + return new RestorableEWMA(TimeUnit.MINUTES.toSeconds(minute)); + } + /** * Updates the moving averages as needed. */ @@ -86,8 +199,10 @@ private void tickIfNecessary() final long requiredTicks = age / TICK_INTERVAL; for (long i = 0; i < requiredTicks; i++) { - m15Rate.tick(); - m120Rate.tick(); + if (m1Rate != null) m1Rate.tick(); + if (m5Rate != null) m5Rate.tick(); + if (m15Rate != null) m15Rate.tick(); + if (m120Rate != null) m120Rate.tick(); } } } @@ -110,8 +225,30 @@ public void mark(long n) { tickIfNecessary(); count.addAndGet(n); - m15Rate.update(n); - m120Rate.update(n); + if (m1Rate != null) m1Rate.update(n); + if (m5Rate != null) m5Rate.update(n); + if (m15Rate != null) m15Rate.update(n); + if (m120Rate != null) m120Rate.update(n); + } + + /** + * Returns the 1-minute rate in terms of events per second. This DOES NOT carry the previous rate when restored. + */ + public double oneMinuteRate() + { + Preconditions.checkNotNull(m1Rate); + tickIfNecessary(); + return m1Rate.rate(); + } + + /** + * Returns the 5-minute rate in terms of events per second. This DOES NOT carry the previous rate when restored. + */ + public double fiveMinuteRate() + { + Preconditions.checkNotNull(m5Rate); + tickIfNecessary(); + return m5Rate.rate(); } /** @@ -119,6 +256,7 @@ public void mark(long n) */ public double fifteenMinuteRate() { + Preconditions.checkNotNull(m15Rate); tickIfNecessary(); return m15Rate.rate(); } @@ -128,10 +266,27 @@ public double fifteenMinuteRate() */ public double twoHourRate() { + Preconditions.checkNotNull(m120Rate); tickIfNecessary(); return m120Rate.rate(); } + /** + * @param window window of time in minutes + * @return rate in terms of events per second with given window. + */ + public double rate(int window) + { + switch (window) + { + case 1 : return oneMinuteRate(); + case 5 : return fiveMinuteRate(); + case 15 : return fifteenMinuteRate(); + case 120 : return twoHourRate(); + default : throw new IllegalArgumentException(String.format("Found invalid window=%s, available windows: %s", window, AVAILABLE_WINDOWS)); + } + } + /** * The total number of events that have occurred since this object was created. Note that the previous count * is *not* carried over when a RestorableMeter is restored. diff --git a/src/java/org/apache/cassandra/metrics/Sampler.java b/src/java/org/apache/cassandra/metrics/Sampler.java index 4c4739b32984..3e48bf22efbc 100644 --- a/src/java/org/apache/cassandra/metrics/Sampler.java +++ b/src/java/org/apache/cassandra/metrics/Sampler.java @@ -106,7 +106,7 @@ void format(SamplingManager.ResultBuilder resultBuilder, PrintStream ps) private long endTimeNanos = -1; - public void addSample(final T item, final int value) + public void addSample(final T item, final long value) { if (isEnabled()) samplerExecutor.submit(() -> insert(item, value)); diff --git a/src/java/org/apache/cassandra/metrics/TableMetrics.java b/src/java/org/apache/cassandra/metrics/TableMetrics.java index b2a8e61fe4ca..b2bbae3d7464 100644 --- a/src/java/org/apache/cassandra/metrics/TableMetrics.java +++ b/src/java/org/apache/cassandra/metrics/TableMetrics.java @@ -17,34 +17,49 @@ */ package org.apache.cassandra.metrics; +import java.lang.ref.WeakReference; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; import java.util.Iterator; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; import java.util.function.Predicate; +import java.util.stream.Stream; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.codahale.metrics.CachedGauge; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; import com.codahale.metrics.Metric; import com.codahale.metrics.Timer; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.View; import org.apache.cassandra.db.memtable.Memtable; @@ -57,10 +72,14 @@ import org.apache.cassandra.metrics.Sampler.SamplerType; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.EstimatedHistogram; import org.apache.cassandra.utils.ExpMovingAverage; +import org.apache.cassandra.utils.Hex; import org.apache.cassandra.utils.MovingAverage; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.concurrent.OpOrder; +import org.apache.cassandra.utils.concurrent.Refs; import static java.util.concurrent.TimeUnit.MICROSECONDS; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; @@ -71,6 +90,63 @@ */ public class TableMetrics { + // CNDB will set this to MetricsAggregation.AGGREGATED in order to support large number of tenants and dedicated + // tenants with a large number of tables + public static final String TABLE_METRICS_DEFAULT_HISTOGRAMS_AGGREGATION = + CassandraRelevantProperties.TABLE_METRICS_DEFAULT_HISTOGRAMS_AGGREGATION.getString(); + + // CNDB will set this to false as it does not need global metrics since the aggregation is done in Prometheus + public static final boolean EXPORT_GLOBAL_METRICS = CassandraRelevantProperties.TABLE_METRICS_EXPORT_GLOBALS.getBoolean(); + + private static final Logger logger = LoggerFactory.getLogger(TableMetrics.class); + + public static final String TABLE_EXTENSIONS_HISTOGRAMS_METRICS_KEY = "HISTOGRAM_METRICS"; + + public enum MetricsAggregation + { + AGGREGATED((byte) 0x00), + INDIVIDUAL((byte) 0x01); + + public final byte val; + + MetricsAggregation(byte val) + { + this.val = val; + } + + public static MetricsAggregation fromMetadata(TableMetadata metadata) + { + MetricsAggregation defaultValue = MetricsAggregation.valueOf(TABLE_METRICS_DEFAULT_HISTOGRAMS_AGGREGATION); + ByteBuffer bb = null; + try + { + bb = metadata.params.extensions.get(TABLE_EXTENSIONS_HISTOGRAMS_METRICS_KEY); + return bb == null ? defaultValue : MetricsAggregation.fromByte(bb.get(bb.position())); // do not change the position of the ByteBuffer! + } + catch (BufferUnderflowException | IllegalStateException ex) + { + logger.error("Failed to decode metadata extensions for metrics aggregation ({}), using default value {}", bb, defaultValue); + return defaultValue; + } + } + + public static MetricsAggregation fromByte(byte val) throws IllegalStateException + { + for (MetricsAggregation aggr : values()) + { + if (aggr.val == val) + return aggr; + } + + throw new IllegalStateException("Invalid byte: " + val); + } + + public String asCQLString() + { + return "0x" + Hex.bytesToHex(val); + } + } + /** * stores metrics that will be rolled into a single global metric */ @@ -79,9 +155,9 @@ public class TableMetrics private static final MetricNameFactory GLOBAL_FACTORY = new AllTableMetricNameFactory("Table"); private static final MetricNameFactory GLOBAL_ALIAS_FACTORY = new AllTableMetricNameFactory("ColumnFamily"); - public final static LatencyMetrics GLOBAL_READ_LATENCY = new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Read"); - public final static LatencyMetrics GLOBAL_WRITE_LATENCY = new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Write"); - public final static LatencyMetrics GLOBAL_RANGE_LATENCY = new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Range"); + public final static Optional GLOBAL_READ_LATENCY = EXPORT_GLOBAL_METRICS ? Optional.of(new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Read")) : Optional.empty(); + public final static Optional GLOBAL_WRITE_LATENCY = EXPORT_GLOBAL_METRICS ? Optional.of(new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Write")) : Optional.empty(); + public final static Optional GLOBAL_RANGE_LATENCY = EXPORT_GLOBAL_METRICS ? Optional.of(new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Range")) : Optional.empty(); /** Total amount of data stored in the memtable that resides on-heap, including column related overhead and partitions overwritten. */ public final Gauge memtableOnHeapDataSize; @@ -105,27 +181,70 @@ public class TableMetrics public final Gauge estimatedPartitionSizeHistogram; /** Approximate number of keys in table. */ public final Gauge estimatedPartitionCount; + /** This function is used to calculate estimated partition count in sstables and store the calculated value for the + * current set of sstables. */ + public final LongSupplier estimatedPartitionCountInSSTables; + /** A cached version of the estimated partition count in sstables, used by compaction. This value will be more + * precise when the table has a small number of partitions that keep getting written to. */ + public final Gauge estimatedPartitionCountInSSTablesCached; /** Histogram of estimated number of columns. */ public final Gauge estimatedColumnCountHistogram; + /** Approximate number of rows in SSTable*/ + public final Gauge estimatedRowCount; /** Histogram of the number of sstable data files accessed per single partition read */ public final TableHistogram sstablesPerReadHistogram; /** Histogram of the number of sstable data files accessed per partition range read */ public final TableHistogram sstablesPerRangeReadHistogram; + /** An approximate measure of how long it takes to read a partition from an sstable, in nanoseconds. This is + * a moving average of a very rough approximation: the total latency for a single partition + * read command divided by the number of sstables that were accessed for that command. + * Therefore it currently includes other costs, which is not ideal but it does give a rough estimate. + * since disk costs would dominate computing costs. */ + public final MovingAverage sstablePartitionReadLatency; /** (Local) read metrics */ - public final LatencyMetrics readLatency; + public final TableLatencyMetrics readLatency; /** (Local) range slice metrics */ - public final LatencyMetrics rangeLatency; + public final TableLatencyMetrics rangeLatency; /** (Local) write metrics */ - public final LatencyMetrics writeLatency; + public final TableLatencyMetrics writeLatency; + /** The number of single partition read requests, including those dropped due to timeouts */ + public final Counter readRequests; + /** The number of range read requests, including those dropped due to timeouts */ + public final Counter rangeRequests; + /** The number of write requests in storage layer, including errors */ + public final Counter writeRequests; + /** The number of delete requests */ + public final Counter deleteRequests; /** Estimated number of tasks pending for this table */ public final Counter pendingFlushes; /** Total number of bytes flushed since server [re]start */ public final Counter bytesFlushed; + /** The average flushed size for sstables, which is derived from {@link this#bytesFlushed}. */ + public final MovingAverage flushSize; /** The average on-disk flushed size for sstables. */ - public final MovingAverage flushSizeOnDisk; + private final MovingAverage flushSizeOnDisk; + /** The average number of sstables created on flush. */ + public final MovingAverage flushSegmentCount; + /** The average duration per 1Kb of data flushed, in nanoseconds. */ + public final MovingAverage flushTimePerKb; + /** Time spent in flushing memtables */ + public final Counter flushTime; + public final Counter storageAttachedIndexBuildTime; + public final Counter storageAttachedIndexWritingTimeForIndexBuild; + public final Counter storageAttachedIndexWritingTimeForCompaction; + public final Counter storageAttachedIndexWritingTimeForFlush; + public final Counter storageAttachedIndexWritingTimeForOther; + /** Total number of bytes inserted into memtables since server [re]start. */ + public final Counter bytesInserted; /** Total number of bytes written by compaction since server [re]start */ public final Counter compactionBytesWritten; - /** Estimate of number of pending compactios for this table */ + /** Total number of bytes read by compaction since server [re]start */ + public final Counter compactionBytesRead; + /** The average duration per 1Kb of data compacted, in nanoseconds. */ + public final MovingAverage compactionTimePerKb; + /** Time spent in writing sstables during compaction */ + public final Counter compactionTime; + /** Estimate of number of pending compactions for this table */ public final Gauge pendingCompactions; /** Number of SSTables on disk for this CF */ public final Gauge liveSSTableCount; @@ -147,10 +266,20 @@ public class TableMetrics public final Gauge maxPartitionSize; /** Size of the smallest compacted partition */ public final Gauge meanPartitionSize; + /** False positive ratio of bloom filter */ + public final Gauge bloomFilterFalseRatio; + + public final AtomicLong inFlightBloomFilterOffHeapMemoryUsed = new AtomicLong(0); /** Off heap memory used by compression meta data*/ public final Gauge compressionMetadataOffHeapMemoryUsed; + + /** Shadowed keys scan metrics **/ + public final TableHistogram shadowedKeysScannedHistogram; + public final TableHistogram shadowedKeysLoopsHistogram; + /** Tombstones scanned in queries on this CF */ public final TableHistogram tombstoneScannedHistogram; + public final Counter tombstoneScannedCounter; /** Live rows scanned in queries on this CF */ public final TableHistogram liveScannedHistogram; /** Column update time delta on this CF */ @@ -176,11 +305,11 @@ public class TableMetrics */ public final Counter tombstoneWarnings; /** CAS Prepare metrics */ - public final LatencyMetrics casPrepare; + public final TableLatencyMetrics casPrepare; /** CAS Propose metrics */ - public final LatencyMetrics casPropose; + public final TableLatencyMetrics casPropose; /** CAS Commit metrics */ - public final LatencyMetrics casCommit; + public final TableLatencyMetrics casCommit; /** percent of the data that is repaired */ public final Gauge percentRepaired; /** Reports the size of sstables in repaired, unrepaired, and any ongoing repair buckets */ @@ -202,15 +331,26 @@ public class TableMetrics /** number of partitions read creating merkle trees */ public final TableHistogram partitionsValidated; /** number of bytes read while doing anticompaction */ - public final Counter bytesAnticompacted; + public final TableMeter bytesAnticompacted; /** number of bytes where the whole sstable was contained in a repairing range so that we only mutated the repair status */ - public final Counter bytesMutatedAnticompaction; + public final TableMeter bytesMutatedAnticompaction; + /** number of bytes that were scanned during preview repair */ + public final TableMeter bytesPreviewed; + /** number of desynchronized token ranges that were detected during preview repair */ + public final TableMeter tokenRangesPreviewedDesynchronized; + /** number of desynchronized bytes that were detected during preview repair */ + public final TableMeter bytesPreviewedDesynchronized; /** ratio of how much we anticompact vs how much we could mutate the repair status*/ public final Gauge mutatedAnticompactionGauge; - public final SnapshottingTimer coordinatorReadLatency; - public final Timer coordinatorScanLatency; - public final SnapshottingTimer coordinatorWriteLatency; + public final TableTimer coordinatorReadLatency; + public final TableTimer coordinatorCasReadLatency; + public final TableTimer coordinatorScanLatency; + public final TableTimer coordinatorCasWriteLatency; + public final TableTimer coordinatorWriteLatency; + + /** Time spent waiting for free memtable space, either on- or off-heap */ + public final TableHistogram waitingOnFreeMemtableSpace; private final MetricNameFactory factory; private final MetricNameFactory aliasFactory; @@ -282,6 +422,12 @@ public class TableMetrics public final ImmutableMap, ImmutableMap>> formatSpecificGauges; + /** + * This property determines if new metrics dedicated to this table are created, or if keyspace metrics are + * used instead. + * */ + public final MetricsAggregation metricsAggregation; + private static Pair totalNonSystemTablesSize(Predicate predicate) { long total = 0; @@ -314,7 +460,7 @@ private static Pair totalNonSystemTablesSize(Predicate globalPercentRepaired = Metrics.register(GLOBAL_FACTORY.createMetricName("PercentRepaired"), - new Gauge() + new Gauge() { public Double getValue() { @@ -326,21 +472,21 @@ public Double getValue() }); public static final Gauge globalBytesRepaired = Metrics.register(GLOBAL_FACTORY.createMetricName("BytesRepaired"), - () -> totalNonSystemTablesSize(SSTableReader::isRepaired).left); + () -> totalNonSystemTablesSize(SSTableReader::isRepaired).left); - public static final Gauge globalBytesUnrepaired = + public static final Gauge globalBytesUnrepaired = Metrics.register(GLOBAL_FACTORY.createMetricName("BytesUnrepaired"), () -> totalNonSystemTablesSize(s -> !s.isRepaired() && !s.isPendingRepair()).left); - public static final Gauge globalBytesPendingRepair = + public static final Gauge globalBytesPendingRepair = Metrics.register(GLOBAL_FACTORY.createMetricName("BytesPendingRepair"), () -> totalNonSystemTablesSize(SSTableReader::isPendingRepair).left); public final Meter readRepairRequests; public final Meter shortReadProtectionRequests; - + public final Meter replicaFilteringProtectionRequests; - + /** * This histogram records the maximum number of rows {@link org.apache.cassandra.service.reads.ReplicaFilteringProtection} * caches at a point in time per query. With no replica divergence, this is equivalent to the maximum number of @@ -401,6 +547,9 @@ public static long[] addHistogram(long[] sums, long[] buckets) */ public TableMetrics(final ColumnFamilyStore cfs, ReleasableMetric memtableMetrics) { + metricsAggregation = MetricsAggregation.fromMetadata(cfs.metadata()); + logger.trace("Using {} histograms for table={}", metricsAggregation, cfs.metadata()); + factory = new TableMetricNameFactory(cfs, "Table"); aliasFactory = new TableMetricNameFactory(cfs, "ColumnFamily"); @@ -480,20 +629,20 @@ public String toString(ByteBuffer value) samplers.put(SamplerType.READ_TOMBSTONE_COUNT, topReadPartitionTombstoneCount); samplers.put(SamplerType.READ_SSTABLE_COUNT, topReadPartitionSSTableCount); - memtableColumnsCount = createTableGauge("MemtableColumnsCount", + memtableColumnsCount = createTableGauge("MemtableColumnsCount", () -> cfs.getTracker().getView().getCurrentMemtable().operationCount()); // MemtableOnHeapSize naming deprecated in 4.0 - memtableOnHeapDataSize = createTableGaugeWithDeprecation("MemtableOnHeapDataSize", "MemtableOnHeapSize", + memtableOnHeapDataSize = createTableGaugeWithDeprecation("MemtableOnHeapDataSize", "MemtableOnHeapSize", () -> Memtable.getMemoryUsage(cfs.getTracker().getView().getCurrentMemtable()).ownsOnHeap, new GlobalTableGauge("MemtableOnHeapDataSize")); // MemtableOffHeapSize naming deprecated in 4.0 - memtableOffHeapDataSize = createTableGaugeWithDeprecation("MemtableOffHeapDataSize", "MemtableOffHeapSize", + memtableOffHeapDataSize = createTableGaugeWithDeprecation("MemtableOffHeapDataSize", "MemtableOffHeapSize", () -> Memtable.getMemoryUsage(cfs.getTracker().getView().getCurrentMemtable()).ownsOffHeap, new GlobalTableGauge("MemtableOnHeapDataSize")); - - memtableLiveDataSize = createTableGauge("MemtableLiveDataSize", + + memtableLiveDataSize = createTableGauge("MemtableLiveDataSize", () -> cfs.getTracker().getView().getCurrentMemtable().getLiveDataSize()); // AllMemtablesHeapSize naming deprecated in 4.0 @@ -527,26 +676,95 @@ public Long getValue() estimatedPartitionSizeHistogram = createTableGauge("EstimatedPartitionSizeHistogram", "EstimatedRowSizeHistogram", () -> combineHistograms(cfs.getSSTables(SSTableSet.CANONICAL), SSTableReader::getEstimatedPartitionSize), null); - + + estimatedPartitionCountInSSTables = new LongSupplier() + { + // Since the sstables only change when the tracker view changes, we can cache the value. + AtomicReference, Long>> collected = new AtomicReference<>(Pair.create(new WeakReference<>(null), 0L)); + + public long getAsLong() + { + final View currentView = cfs.getTracker().getView(); + final Pair, Long> currentCollected = collected.get(); + if (currentView != currentCollected.left.get()) + { + Refs refs = Refs.tryRef(currentView.select(SSTableSet.CANONICAL)); + if (refs != null) + { + try (refs) + { + long collectedValue = SSTableReader.getApproximateKeyCount(refs); + final Pair, Long> newCollected = Pair.create(new WeakReference<>(currentView), collectedValue); + collected.compareAndSet(currentCollected, newCollected); // okay if failed, a different thread did it + return collectedValue; + } + } + // If we can't reference, simply return the previous collected value; it can only result in a delay + // in reporting the correct key count. + } + return currentCollected.right; + } + }; estimatedPartitionCount = createTableGauge("EstimatedPartitionCount", "EstimatedRowCount", new Gauge() { public Long getValue() { - long memtablePartitions = 0; + long estimatedPartitions = estimatedPartitionCountInSSTables.getAsLong(); for (Memtable memtable : cfs.getTracker().getView().getAllMemtables()) - memtablePartitions += memtable.partitionCount(); + estimatedPartitions += memtable.partitionCount(); + return estimatedPartitions; + } + }, null); + estimatedPartitionCountInSSTablesCached = new CachedGauge(1, TimeUnit.SECONDS) + { + public Long loadValue() + { + return estimatedPartitionCountInSSTables.getAsLong(); + } + }; + + estimatedColumnCountHistogram = createTableGauge("EstimatedColumnCountHistogram", "EstimatedColumnCountHistogram", + () -> combineHistograms(cfs.getSSTables(SSTableSet.CANONICAL), + SSTableReader::getEstimatedCellPerPartitionCount), null); + + estimatedRowCount = createTableGauge("EstimatedRowCount", "EstimatedRowCount", new CachedGauge<>(1, TimeUnit.SECONDS) + { + public Long loadValue() + { + long memtableRows = 0; + OpOrder.Group readGroup = null; + try + { + for (Memtable memtable : cfs.getTracker().getView().getAllMemtables()) + { + if (readGroup == null) + { + readGroup = memtable.readOrdering().start(); + } + memtableRows += Memtable.estimateRowCount(memtable); + } + } + finally + { + if (readGroup != null) + readGroup.close(); + } + + long sstableRows = 0; try(ColumnFamilyStore.RefViewFragment refViewFragment = cfs.selectAndReference(View.selectFunction(SSTableSet.CANONICAL))) { - return SSTableReader.getApproximateKeyCount(refViewFragment.sstables) + memtablePartitions; + for (SSTableReader reader: refViewFragment.sstables) + { + sstableRows += reader.getTotalRows(); + } } + return sstableRows + memtableRows; } }, null); - estimatedColumnCountHistogram = createTableGauge("EstimatedColumnCountHistogram", "EstimatedColumnCountHistogram", - () -> combineHistograms(cfs.getSSTables(SSTableSet.CANONICAL), - SSTableReader::getEstimatedCellPerPartitionCount), null); - - sstablesPerReadHistogram = createTableHistogram("SSTablesPerReadHistogram", cfs.keyspace.metric.sstablesPerReadHistogram, true); - sstablesPerRangeReadHistogram = createTableHistogram("SSTablesPerRangeReadHistogram", cfs.keyspace.metric.sstablesPerRangeReadHistogram, true); + + sstablesPerReadHistogram = createTableHistogram("SSTablesPerReadHistogram", cfs.getKeyspaceMetrics().sstablesPerReadHistogram, true); + sstablesPerRangeReadHistogram = createTableHistogram("SSTablesPerRangeReadHistogram", cfs.getKeyspaceMetrics().sstablesPerRangeReadHistogram, true); + sstablePartitionReadLatency = ExpMovingAverage.decayBy100(); compressionRatio = createTableGauge("CompressionRatio", new Gauge() { public Double getValue() @@ -619,16 +837,35 @@ public Long getValue() } }); - readLatency = createLatencyMetrics("Read", cfs.keyspace.metric.readLatency, GLOBAL_READ_LATENCY); - writeLatency = createLatencyMetrics("Write", cfs.keyspace.metric.writeLatency, GLOBAL_WRITE_LATENCY); - rangeLatency = createLatencyMetrics("Range", cfs.keyspace.metric.rangeLatency, GLOBAL_RANGE_LATENCY); + readLatency = createLatencyMetrics("Read", cfs.getKeyspaceMetrics().readLatency, GLOBAL_READ_LATENCY); + writeLatency = createLatencyMetrics("Write", cfs.getKeyspaceMetrics().writeLatency, GLOBAL_WRITE_LATENCY); + rangeLatency = createLatencyMetrics("Range", cfs.getKeyspaceMetrics().rangeLatency, GLOBAL_RANGE_LATENCY); + + readRequests = createTableCounter("ReadRequests"); + rangeRequests = createTableCounter("RangeRequests"); + writeRequests = createTableCounter("WriteRequests"); + deleteRequests = createTableCounter("DeleteRequests"); + pendingFlushes = createTableCounter("PendingFlushes"); bytesFlushed = createTableCounter("BytesFlushed"); + flushSize = ExpMovingAverage.decayBy100(); flushSizeOnDisk = ExpMovingAverage.decayBy1000(); + flushSegmentCount = ExpMovingAverage.decayBy1000(); + flushTimePerKb = ExpMovingAverage.decayBy100(); + flushTime = createTableCounter("FlushTime"); + storageAttachedIndexBuildTime = createTableCounter("StorageAttachedIndexBuildTime"); + storageAttachedIndexWritingTimeForIndexBuild = createTableCounter("StorageAttachedIndexWritingTimeForIndexBuild"); + storageAttachedIndexWritingTimeForCompaction = createTableCounter("StorageAttachedIndexWritingTimeForCompaction"); + storageAttachedIndexWritingTimeForFlush = createTableCounter("StorageAttachedIndexWritingTimeForFlush"); + storageAttachedIndexWritingTimeForOther= createTableCounter("StorageAttachedIndexWritingTimeForOther"); + bytesInserted = createTableCounter("BytesInserted"); compactionBytesWritten = createTableCounter("CompactionBytesWritten"); - pendingCompactions = createTableGauge("PendingCompactions", () -> cfs.getCompactionStrategyManager().getEstimatedRemainingTasks()); - liveSSTableCount = createTableGauge("LiveSSTableCount", () -> cfs.getTracker().getView().liveSSTables().size()); + compactionBytesRead = createTableCounter("CompactionBytesRead"); + compactionTimePerKb = ExpMovingAverage.decayBy100(); + compactionTime = createTableCounter("CompactionTime"); + pendingCompactions = createTableGauge("PendingCompactions", () -> cfs.getCompactionStrategy().getEstimatedRemainingTasks()); + liveSSTableCount = createTableGauge("LiveSSTableCount", () -> cfs.getLiveSSTables().size()); oldVersionSSTableCount = createTableGauge("OldVersionSSTableCount", new Gauge() { public Integer getValue() @@ -752,6 +989,39 @@ public Long getValue() return count > 0 ? sum / count : 0; } }); + bloomFilterFalseRatio = createTableGauge("BloomFilterFalseRatio", new Gauge() + { + public Double getValue() + { + long falsePositiveCount = cfs.getBloomFilterFalsePositiveCount(); + long truePositiveCount = cfs.getBloomFilterTruePositiveCount(); + long trueNegativeCount = cfs.getBloomFilterTrueNegativeCount(); + + if (falsePositiveCount == 0L && truePositiveCount == 0L) + return 0d; + return (double) falsePositiveCount / (truePositiveCount + falsePositiveCount + trueNegativeCount); + } + }, new Gauge() // global gauge + { + public Double getValue() + { + long falsePositiveCount = 0L; + long truePositiveCount = 0L; + long trueNegativeCount = 0L; + for (Keyspace keyspace : Keyspace.all()) + { + for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) + { + falsePositiveCount += cfs.getBloomFilterFalsePositiveCount(); + truePositiveCount += cfs.getBloomFilterTruePositiveCount(); + trueNegativeCount += cfs.getBloomFilterTrueNegativeCount(); + } + } + if (falsePositiveCount == 0L && truePositiveCount == 0L) + return 0d; + return (double) falsePositiveCount / (truePositiveCount + falsePositiveCount + trueNegativeCount); + } + }); compressionMetadataOffHeapMemoryUsed = createTableGauge("CompressionMetadataOffHeapMemoryUsed", new Gauge() { public Long getValue() @@ -770,12 +1040,18 @@ public Long getValue() additionalWrites = createTableCounter("AdditionalWrites"); additionalWriteLatencyNanos = createTableGauge("AdditionalWriteLatencyNanos", () -> MICROSECONDS.toNanos(cfs.additionalWriteLatencyMicros)); - tombstoneScannedHistogram = createTableHistogram("TombstoneScannedHistogram", cfs.keyspace.metric.tombstoneScannedHistogram, false); - liveScannedHistogram = createTableHistogram("LiveScannedHistogram", cfs.keyspace.metric.liveScannedHistogram, false); - colUpdateTimeDeltaHistogram = createTableHistogram("ColUpdateTimeDeltaHistogram", cfs.keyspace.metric.colUpdateTimeDeltaHistogram, false); - coordinatorReadLatency = createTableTimer("CoordinatorReadLatency"); - coordinatorScanLatency = createTableTimer("CoordinatorScanLatency"); - coordinatorWriteLatency = createTableTimer("CoordinatorWriteLatency"); + tombstoneScannedHistogram = createTableHistogram("TombstoneScannedHistogram", cfs.getKeyspaceMetrics().tombstoneScannedHistogram, false); + shadowedKeysScannedHistogram = createTableHistogram("ShadowedKeysScannedHistogram", cfs.getKeyspaceMetrics().shadowedKeysScannedHistogram, false); + shadowedKeysLoopsHistogram = createTableHistogram("ShadowedKeysLoopsHistogram", cfs.getKeyspaceMetrics().shadowedKeysLoopsHistogram, false); + tombstoneScannedCounter = createTableCounter("TombstoneScannedCounter"); + liveScannedHistogram = createTableHistogram("LiveScannedHistogram", cfs.getKeyspaceMetrics().liveScannedHistogram, false); + colUpdateTimeDeltaHistogram = createTableHistogram("ColUpdateTimeDeltaHistogram", cfs.getKeyspaceMetrics().colUpdateTimeDeltaHistogram, false); + coordinatorReadLatency = createTableTimer("CoordinatorReadLatency", cfs.getKeyspaceMetrics().coordinatorReadLatency); + coordinatorCasReadLatency = createTableTimer("CoordinatorCasReadLatency", cfs.getKeyspaceMetrics().coordinatorCasReadLatency); + coordinatorScanLatency = createTableTimer("CoordinatorScanLatency", cfs.getKeyspaceMetrics().coordinatorScanLatency); + coordinatorWriteLatency = createTableTimer("CoordinatorWriteLatency", cfs.getKeyspaceMetrics().coordinatorWriteLatency); + coordinatorCasWriteLatency = createTableTimer("CoordinatorCasWriteLatency", cfs.getKeyspaceMetrics().coordinatorCasWriteLatency); + waitingOnFreeMemtableSpace = createTableHistogram("WaitingOnFreeMemtableSpace", cfs.getKeyspaceMetrics().waitingOnFreeMemtableSpace, false); // We do not want to capture view mutation specific metrics for a view // They only makes sense to capture on the base table @@ -786,8 +1062,8 @@ public Long getValue() } else { - viewLockAcquireTime = createTableTimer("ViewLockAcquireTime", cfs.keyspace.metric.viewLockAcquireTime); - viewReadTime = createTableTimer("ViewReadTime", cfs.keyspace.metric.viewReadTime); + viewLockAcquireTime = createTableTimer("ViewLockAcquireTime", cfs.getKeyspaceMetrics().viewLockAcquireTime); + viewReadTime = createTableTimer("ViewReadTime", cfs.getKeyspaceMetrics().viewReadTime); } trueSnapshotsSize = createTableGauge("SnapshotsSize", cfs::trueSnapshotsSize); @@ -798,25 +1074,28 @@ public Long getValue() tombstoneFailures = createTableCounter("TombstoneFailures"); tombstoneWarnings = createTableCounter("TombstoneWarnings"); - casPrepare = createLatencyMetrics("CasPrepare", cfs.keyspace.metric.casPrepare); - casPropose = createLatencyMetrics("CasPropose", cfs.keyspace.metric.casPropose); - casCommit = createLatencyMetrics("CasCommit", cfs.keyspace.metric.casCommit); + casPrepare = createLatencyMetrics("CasPrepare", cfs.getKeyspaceMetrics().casPrepare, Optional.empty()); + casPropose = createLatencyMetrics("CasPropose", cfs.getKeyspaceMetrics().casPropose, Optional.empty()); + casCommit = createLatencyMetrics("CasCommit", cfs.getKeyspaceMetrics().casCommit, Optional.empty()); repairsStarted = createTableCounter("RepairJobsStarted"); repairsCompleted = createTableCounter("RepairJobsCompleted"); - anticompactionTime = createTableTimer("AnticompactionTime", cfs.keyspace.metric.anticompactionTime); - validationTime = createTableTimer("ValidationTime", cfs.keyspace.metric.validationTime); - repairSyncTime = createTableTimer("RepairSyncTime", cfs.keyspace.metric.repairSyncTime); - - bytesValidated = createTableHistogram("BytesValidated", cfs.keyspace.metric.bytesValidated, false); - partitionsValidated = createTableHistogram("PartitionsValidated", cfs.keyspace.metric.partitionsValidated, false); - bytesAnticompacted = createTableCounter("BytesAnticompacted"); - bytesMutatedAnticompaction = createTableCounter("BytesMutatedAnticompaction"); + anticompactionTime = createTableTimer("AnticompactionTime", cfs.getKeyspaceMetrics().anticompactionTime); + validationTime = createTableTimer("ValidationTime", cfs.getKeyspaceMetrics().validationTime); + repairSyncTime = createTableTimer("RepairSyncTime", cfs.getKeyspaceMetrics().repairSyncTime); + + bytesValidated = createTableHistogram("BytesValidated", cfs.getKeyspaceMetrics().bytesValidated, false); + partitionsValidated = createTableHistogram("PartitionsValidated", cfs.getKeyspaceMetrics().partitionsValidated, false); + bytesAnticompacted = createTableMeter("BytesAnticompacted", cfs.getKeyspaceMetrics().bytesAnticompacted); + bytesMutatedAnticompaction = createTableMeter("BytesMutatedAnticompaction", cfs.getKeyspaceMetrics().bytesMutatedAnticompaction); + bytesPreviewed = createTableMeter("BytesPreviewed", cfs.getKeyspaceMetrics().bytesPreviewed); + tokenRangesPreviewedDesynchronized = createTableMeter("TokenRangesPreviewedDesynchronized", cfs.getKeyspaceMetrics().tokenRangesPreviewedDesynchronized); + bytesPreviewedDesynchronized = createTableMeter("BytesPreviewedDesynchronized", cfs.getKeyspaceMetrics().bytesPreviewedDesynchronized); mutatedAnticompactionGauge = createTableGauge("MutatedAnticompactionGauge", () -> { - double bytesMutated = bytesMutatedAnticompaction.getCount(); - double bytesAnticomp = bytesAnticompacted.getCount(); + double bytesMutated = bytesMutatedAnticompaction.table.getCount(); + double bytesAnticomp = bytesAnticompacted.table.getCount(); if (bytesAnticomp + bytesMutated > 0) return bytesMutated / (bytesAnticomp + bytesMutated); return 0.0; @@ -827,11 +1106,11 @@ public Long getValue() replicaFilteringProtectionRequests = createTableMeter("ReplicaFilteringProtectionRequests"); rfpRowsCachedPerQuery = createHistogram("ReplicaFilteringProtectionRowsCachedPerQuery", true); - confirmedRepairedInconsistencies = createTableMeter("RepairedDataInconsistenciesConfirmed", cfs.keyspace.metric.confirmedRepairedInconsistencies); - unconfirmedRepairedInconsistencies = createTableMeter("RepairedDataInconsistenciesUnconfirmed", cfs.keyspace.metric.unconfirmedRepairedInconsistencies); + confirmedRepairedInconsistencies = createTableMeter("RepairedDataInconsistenciesConfirmed", cfs.getKeyspaceMetrics().confirmedRepairedInconsistencies); + unconfirmedRepairedInconsistencies = createTableMeter("RepairedDataInconsistenciesUnconfirmed", cfs.getKeyspaceMetrics().unconfirmedRepairedInconsistencies); - repairedDataTrackingOverreadRows = createTableHistogram("RepairedDataTrackingOverreadRows", cfs.keyspace.metric.repairedDataTrackingOverreadRows, false); - repairedDataTrackingOverreadTime = createTableTimer("RepairedDataTrackingOverreadTime", cfs.keyspace.metric.repairedDataTrackingOverreadTime); + repairedDataTrackingOverreadRows = createTableHistogram("RepairedDataTrackingOverreadRows", cfs.getKeyspaceMetrics().repairedDataTrackingOverreadRows, false); + repairedDataTrackingOverreadTime = createTableTimer("RepairedDataTrackingOverreadTime", cfs.getKeyspaceMetrics().repairedDataTrackingOverreadTime); unleveledSSTables = createTableGauge("UnleveledSSTables", cfs::getUnleveledSSTables, () -> { // global gauge @@ -843,27 +1122,33 @@ public Long getValue() return cnt; }); - clientTombstoneWarnings = createTableMeter("ClientTombstoneWarnings", cfs.keyspace.metric.clientTombstoneWarnings); - clientTombstoneAborts = createTableMeter("ClientTombstoneAborts", cfs.keyspace.metric.clientTombstoneAborts); + clientTombstoneWarnings = createTableMeter("ClientTombstoneWarnings", cfs.getKeyspaceMetrics().clientTombstoneWarnings); + clientTombstoneAborts = createTableMeter("ClientTombstoneAborts", cfs.getKeyspaceMetrics().clientTombstoneAborts); - coordinatorReadSizeWarnings = createTableMeter("CoordinatorReadSizeWarnings", cfs.keyspace.metric.coordinatorReadSizeWarnings); - coordinatorReadSizeAborts = createTableMeter("CoordinatorReadSizeAborts", cfs.keyspace.metric.coordinatorReadSizeAborts); - coordinatorReadSize = createTableHistogram("CoordinatorReadSize", cfs.keyspace.metric.coordinatorReadSize, false); + coordinatorReadSizeWarnings = createTableMeter("CoordinatorReadSizeWarnings", cfs.getKeyspaceMetrics().coordinatorReadSizeWarnings); + coordinatorReadSizeAborts = createTableMeter("CoordinatorReadSizeAborts", cfs.getKeyspaceMetrics().coordinatorReadSizeAborts); + coordinatorReadSize = createTableHistogram("CoordinatorReadSize", cfs.getKeyspaceMetrics().coordinatorReadSize, false); - localReadSizeWarnings = createTableMeter("LocalReadSizeWarnings", cfs.keyspace.metric.localReadSizeWarnings); - localReadSizeAborts = createTableMeter("LocalReadSizeAborts", cfs.keyspace.metric.localReadSizeAborts); - localReadSize = createTableHistogram("LocalReadSize", cfs.keyspace.metric.localReadSize, false); + localReadSizeWarnings = createTableMeter("LocalReadSizeWarnings", cfs.getKeyspaceMetrics().localReadSizeWarnings); + localReadSizeAborts = createTableMeter("LocalReadSizeAborts", cfs.getKeyspaceMetrics().localReadSizeAborts); + localReadSize = createTableHistogram("LocalReadSize", cfs.getKeyspaceMetrics().localReadSize, false); - rowIndexSizeWarnings = createTableMeter("RowIndexSizeWarnings", cfs.keyspace.metric.rowIndexSizeWarnings); - rowIndexSizeAborts = createTableMeter("RowIndexSizeAborts", cfs.keyspace.metric.rowIndexSizeAborts); - rowIndexSize = createTableHistogram("RowIndexSize", cfs.keyspace.metric.rowIndexSize, false); + rowIndexSizeWarnings = createTableMeter("RowIndexSizeWarnings", cfs.getKeyspaceMetrics().rowIndexSizeWarnings); + rowIndexSizeAborts = createTableMeter("RowIndexSizeAborts", cfs.getKeyspaceMetrics().rowIndexSizeAborts); + rowIndexSize = createTableHistogram("RowIndexSize", cfs.getKeyspaceMetrics().rowIndexSize, false); - tooManySSTableIndexesReadWarnings = createTableMeter("TooManySSTableIndexesReadWarnings", cfs.keyspace.metric.tooManySSTableIndexesReadWarnings); - tooManySSTableIndexesReadAborts = createTableMeter("TooManySSTableIndexesReadAborts", cfs.keyspace.metric.tooManySSTableIndexesReadAborts); + tooManySSTableIndexesReadWarnings = createTableMeter("TooManySSTableIndexesReadWarnings", cfs.getKeyspaceMetrics().tooManySSTableIndexesReadWarnings); + tooManySSTableIndexesReadAborts = createTableMeter("TooManySSTableIndexesReadAborts", cfs.getKeyspaceMetrics().tooManySSTableIndexesReadAborts); formatSpecificGauges = createFormatSpecificGauges(cfs); } + @VisibleForTesting + public MovingAverage flushSizeOnDisk() + { + return flushSizeOnDisk; + } + private Memtable.MemoryUsage getMemoryUsageWithIndexes(ColumnFamilyStore cfs) { Memtable.MemoryUsage usage = Memtable.newMemoryUsage(); @@ -873,9 +1158,78 @@ private Memtable.MemoryUsage getMemoryUsageWithIndexes(ColumnFamilyStore cfs) return usage; } - public void updateSSTableIterated(int count) + public void incLiveRows(long liveRows) + { + liveScannedHistogram.update(liveRows); + } + + public void incShadowedKeys(long numLoops, long numShadowedKeys) + { + shadowedKeysLoopsHistogram.update(numLoops); + shadowedKeysScannedHistogram.update(numShadowedKeys); + } + + public void incTombstones(long tombstones, boolean triggerWarning) + { + tombstoneScannedHistogram.update(tombstones); + tombstoneScannedCounter.inc(tombstones); + + if (triggerWarning) + tombstoneWarnings.inc(); + } + + public void incBytesFlushed(long inputSize, long outputSize, long elapsedNanos) + { + bytesFlushed.inc(outputSize); + flushSize.update(outputSize); + // this assumes that at least 1 Kb was flushed, which should always be the case, then rounds down + flushTimePerKb.update(elapsedNanos / (double) Math.max(1, inputSize / 1024L)); + } + + public void updateStorageAttachedIndexBuildTime(long totalTimeSpentNanos) + { + storageAttachedIndexBuildTime.inc(TimeUnit.NANOSECONDS.toMicros(totalTimeSpentNanos)); + } + + public void updateStorageAttachedIndexWritingTime(long totalTimeSpentNanos, OperationType opType) + { + long totalTimeSpentMicros = TimeUnit.NANOSECONDS.toMicros(totalTimeSpentNanos); + switch (opType) + { + case INDEX_BUILD: + storageAttachedIndexWritingTimeForIndexBuild.inc(totalTimeSpentMicros); + break; + case COMPACTION: + storageAttachedIndexWritingTimeForCompaction.inc(totalTimeSpentMicros); + break; + case FLUSH: + storageAttachedIndexWritingTimeForFlush.inc(totalTimeSpentMicros); + break; + default: + storageAttachedIndexWritingTimeForOther.inc(totalTimeSpentMicros); + } + } + + public void memTableFlushCompleted(long totalTimeSpentNanos) { + flushTime.inc(TimeUnit.NANOSECONDS.toMicros(totalTimeSpentNanos)); + } + + public void incBytesCompacted(long inputDiskSize, long outputDiskSize, long elapsedMillis) + { + compactionBytesRead.inc(inputDiskSize); + compactionBytesWritten.inc(outputDiskSize); + compactionTime.inc(TimeUnit.MILLISECONDS.toMicros(elapsedMillis)); + // only update compactionTimePerKb when there are non-expired sstables (inputDiskSize > 0) + if (inputDiskSize > 0) + compactionTimePerKb.update(1024.0 * elapsedMillis / inputDiskSize); + } + + public void updateSSTableIterated(int count, int intersectingCount, long elapsedNanos) { sstablesPerReadHistogram.update(count); + + if (intersectingCount > 0) + sstablePartitionReadLatency.update(elapsedNanos / (double) intersectingCount); } public void updateSSTableIteratedInRangeRead(int count) @@ -984,17 +1338,17 @@ protected Counter createTableCounter(final String name, final String alias) Metrics.register(GLOBAL_FACTORY.createMetricName(name), GLOBAL_ALIAS_FACTORY.createMetricName(alias), new Gauge() - { - public Long getValue() - { - long total = 0; - for (Metric cfGauge : ALL_TABLE_METRICS.get(name)) - { - total += ((Counter) cfGauge).getCount(); - } - return total; - } - }); + { + public Long getValue() + { + long total = 0; + for (Metric cfGauge : ALL_TABLE_METRICS.get(name)) + { + total += ((Counter) cfGauge).getCount(); + } + return total; + } + }); } return cfCounter; } @@ -1055,13 +1409,22 @@ protected TableHistogram createTableHistogram(String name, Histogram keyspaceHis protected TableHistogram createTableHistogram(String name, String alias, Histogram keyspaceHistogram, boolean considerZeroes) { - Histogram cfHistogram = Metrics.histogram(factory.createMetricName(name), aliasFactory.createMetricName(alias), considerZeroes); - register(name, alias, cfHistogram); - return new TableHistogram(cfHistogram, - keyspaceHistogram, - Metrics.histogram(GLOBAL_FACTORY.createMetricName(name), - GLOBAL_ALIAS_FACTORY.createMetricName(alias), - considerZeroes)); + Histogram globalHistogram = null; + if (EXPORT_GLOBAL_METRICS) + { + globalHistogram = Metrics.histogram(GLOBAL_FACTORY.createMetricName(name), + GLOBAL_ALIAS_FACTORY.createMetricName(alias), + considerZeroes); + } + + Histogram tableHistogram = null; + if (metricsAggregation == MetricsAggregation.INDIVIDUAL) + { + tableHistogram = Metrics.histogram(factory.createMetricName(name), aliasFactory.createMetricName(alias), considerZeroes); + register(name, alias, tableHistogram); + } + + return new TableHistogram(tableHistogram, keyspaceHistogram, globalHistogram); } protected Histogram createTableHistogram(String name, boolean considerZeroes) @@ -1078,11 +1441,20 @@ protected Histogram createTableHistogram(String name, String alias, boolean cons protected TableTimer createTableTimer(String name, Timer keyspaceTimer) { - Timer cfTimer = Metrics.timer(factory.createMetricName(name), aliasFactory.createMetricName(name)); - register(name, name, keyspaceTimer); - Timer global = Metrics.timer(GLOBAL_FACTORY.createMetricName(name), GLOBAL_ALIAS_FACTORY.createMetricName(name)); + Timer globalTimer = null; + if (EXPORT_GLOBAL_METRICS) + { + globalTimer = Metrics.timer(GLOBAL_FACTORY.createMetricName(name), GLOBAL_ALIAS_FACTORY.createMetricName(name)); + } + + Timer tableTimer = null; + if (metricsAggregation == MetricsAggregation.INDIVIDUAL) + { + tableTimer = Metrics.timer(factory.createMetricName(name), aliasFactory.createMetricName(name)); + register(name, name, keyspaceTimer); + } - return new TableTimer(cfTimer, keyspaceTimer, global); + return new TableTimer(tableTimer, keyspaceTimer, globalTimer); } protected SnapshottingTimer createTableTimer(String name) @@ -1099,18 +1471,38 @@ protected TableMeter createTableMeter(String name, Meter keyspaceMeter) protected TableMeter createTableMeter(String name, String alias, Meter keyspaceMeter) { - Meter meter = Metrics.meter(factory.createMetricName(name), aliasFactory.createMetricName(alias)); - register(name, alias, meter); - return new TableMeter(meter, - keyspaceMeter, - Metrics.meter(GLOBAL_FACTORY.createMetricName(name), - GLOBAL_ALIAS_FACTORY.createMetricName(alias))); + Meter globalMeter = null; + if (EXPORT_GLOBAL_METRICS) + { + globalMeter = Metrics.meter(GLOBAL_FACTORY.createMetricName(name), + GLOBAL_ALIAS_FACTORY.createMetricName(alias)); + } + + Meter tableMeter = null; + if (metricsAggregation == MetricsAggregation.INDIVIDUAL) + { + tableMeter = Metrics.meter(factory.createMetricName(name), aliasFactory.createMetricName(alias)); + register(name, alias, tableMeter); + } + + return new TableMeter(tableMeter, keyspaceMeter, globalMeter); } - private LatencyMetrics createLatencyMetrics(String namePrefix, LatencyMetrics ... parents) + private TableLatencyMetrics createLatencyMetrics(String namePrefix, LatencyMetrics keyspace, Optional global) { - LatencyMetrics metric = new LatencyMetrics(factory, namePrefix, parents); - all.add(metric::release); + TableLatencyMetrics metric; + if (metricsAggregation == MetricsAggregation.INDIVIDUAL) + { + LatencyMetrics[] parents = Stream.of(Optional.of(keyspace), global).filter(Optional::isPresent) + .map(Optional::get).toArray(LatencyMetrics[]::new); + LatencyMetrics innerMetrics = new LatencyMetrics(factory, namePrefix, parents); + metric = new TableLatencyMetrics.IndividualTableLatencyMetrics(innerMetrics); + } + else + { + metric = new TableLatencyMetrics.AggregatingTableLatencyMetrics(keyspace, global); + } + all.add(metric); return metric; } @@ -1162,17 +1554,105 @@ private void releaseMetric(String tableMetricName, String cfMetricName, String t } } + public interface TableLatencyMetrics extends ReleasableMetric + { + void addNano(long latencyNanos); + + LatencyMetrics tableOrKeyspaceMetric(); + + /** + * Used when {@link MetricsAggregation#AGGREGATED} is set for this table. + *
    + * Table latency metrics that forwards all calls to the first parent metric (keyspace metric by convention). + * Thanks to the forwarding, the table doesn't have to maintain its own metrics. The metrics for this table + * are aggregated with metrics comming from other tables that use {@link MetricsAggregation#AGGREGATED}. + */ + class AggregatingTableLatencyMetrics implements TableLatencyMetrics + { + private final LatencyMetrics keyspace; + private final Optional global; + + public AggregatingTableLatencyMetrics(LatencyMetrics keyspace, Optional global) + { + this.keyspace = keyspace; + this.global = global; + Preconditions.checkState(keyspace != null, "Keyspace metrics should not be null"); + } + + @Override + public void addNano(long latencyNanos) + { + keyspace.addNano(latencyNanos); + global.ifPresent(g -> g.addNano(latencyNanos)); + } + + @Override + public LatencyMetrics tableOrKeyspaceMetric() + { + return keyspace; + } + + @Override + public void release() + { + // noop + } + } + + /** + * Used when {@link MetricsAggregation#INDIVIDUAL} is set for this table. + *
    + * Table latency metrics that don't aggreagte, i.e. the given table maintains its own latency metrics. + */ + class IndividualTableLatencyMetrics implements TableLatencyMetrics + { + private final LatencyMetrics latencyMetrics; + + public IndividualTableLatencyMetrics(LatencyMetrics latencyMetrics) + { + this.latencyMetrics = latencyMetrics; + } + + @Override + public void addNano(long latencyNanos) + { + latencyMetrics.addNano(latencyNanos); + } + + @Override + public LatencyMetrics tableOrKeyspaceMetric() + { + return latencyMetrics; + } + + @Override + public void release() + { + latencyMetrics.release(); + } + } + } + public static class TableMeter { public final Meter[] all; - public final Meter table; - public final Meter global; - - private TableMeter(Meter table, Meter keyspace, Meter global) + @Nullable + private final Meter table; + private final Meter keyspace; + + /** + * Table meter wrapper that forwards updates to all provided non-null meters. + * + * @param table meter that is {@code null} if the metrics are not collected indidually for each table, see {@link TableMetrics#metricsAggregation}. + * @param keyspace meter + * @param global meter that is {@code null} if global metrics are not collected, see {@link TableMetrics#EXPORT_GLOBAL_METRICS} + */ + private TableMeter(@Nullable Meter table, Meter keyspace, @Nullable Meter global) { + Preconditions.checkState(keyspace != null, "Keyspace meter can't be null"); this.table = table; - this.global = global; - this.all = new Meter[]{table, keyspace, global}; + this.keyspace = keyspace; + this.all = Stream.of(table, keyspace, global).filter(Objects::nonNull).toArray(Meter[]::new); } public void mark() @@ -1182,46 +1662,82 @@ public void mark() meter.mark(); } } + + public void mark(long n) + { + for (Meter meter : all) + { + meter.mark(n); + } + } + + public Meter tableOrKeyspaceMeter() + { + return table == null ? keyspace : table; + } } public static class TableHistogram { - public final Histogram[] all; - public final Histogram cf; - public final Histogram global; - - private TableHistogram(Histogram cf, Histogram keyspace, Histogram global) + private final Histogram[] all; + @Nullable + private final Histogram table; + private final Histogram keyspace; + + /** + * Table histogram wrapper that forwards updates to all provided non-null histograms. + * + * @param table histogram that is {@code null} if the metrics are not collected indidually for each table, see {@link TableMetrics#metricsAggregation}. + * @param keyspace histogram + * @param global histogram that is {@code null} if global metrics are not collected, see {@link TableMetrics#EXPORT_GLOBAL_METRICS} + */ + private TableHistogram(@Nullable Histogram table, Histogram keyspace, @Nullable Histogram global) { - this.cf = cf; - this.global = global; - this.all = new Histogram[]{cf, keyspace, global}; + Preconditions.checkState(keyspace != null, "Keyspace histogram can't be null"); + this.table = table; + this.keyspace = keyspace; + this.all = Stream.of(table, keyspace, global).filter(Objects::nonNull).toArray(Histogram[]::new); } public void update(long i) { - for(Histogram histo : all) + for (Histogram histo : all) { histo.update(i); } } + + public Histogram tableOrKeyspaceHistogram() + { + return table == null ? keyspace : table; + } } - public static class TableTimer + public static class TableTimer { - public final Timer[] all; - public final Timer cf; - public final Timer global; - - private TableTimer(Timer cf, Timer keyspace, Timer global) + private final Timer[] all; + @Nullable + private final T cf; + private final T keyspace; + + /** + * Table timer wrapper that forwards updates to all provided non-null timers. + * + * @param cf timer that is {@code null} if the metrics are not collected indidually for each table, see {@link TableMetrics#metricsAggregation}. + * @param keyspace timer + * @param global timer that is {@code null} if global metrics are not collected, see {@link TableMetrics#EXPORT_GLOBAL_METRICS} + */ + private TableTimer(@Nullable T cf, T keyspace, @Nullable T global) { + Preconditions.checkState(keyspace != null, "Keyspace timer can't be null"); this.cf = cf; - this.global = global; - this.all = new Timer[]{cf, keyspace, global}; + this.keyspace = keyspace; + this.all = Stream.of(cf, keyspace, global).filter(Objects::nonNull).map(t -> (Timer) t).toArray(Timer[]::new); } public void update(long i, TimeUnit unit) { - for(Timer timer : all) + for (Timer timer : all) { timer.update(i, unit); } @@ -1232,6 +1748,11 @@ public Context time() return new Context(all); } + public T tableOrKeyspaceTimer() + { + return cf == null ? keyspace : cf; + } + public static class Context implements AutoCloseable { private final long start; @@ -1262,7 +1783,7 @@ static class TableMetricNameFactory implements MetricNameFactory TableMetricNameFactory(ColumnFamilyStore cfs, String type) { this.keyspaceName = cfs.getKeyspaceName(); - this.tableName = cfs.name; + this.tableName = cfs.getTableName(); this.isIndex = cfs.isIndex(); this.type = type; } diff --git a/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java b/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java index 934350399945..d0e49ff5eeea 100644 --- a/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java +++ b/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java @@ -18,6 +18,9 @@ package org.apache.cassandra.metrics; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import com.codahale.metrics.Counter; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; @@ -29,6 +32,8 @@ public class TrieMemtableMetricsView private static final String CONTENTION_TIME = "Contention time"; private static final String LAST_FLUSH_SHARD_SIZES = "Shard sizes during last flush"; + private static final Map perTableMetrics = new ConcurrentHashMap<>(); + // the number of memtable puts that did not need to wait on write lock public final Counter uncontendedPuts; @@ -42,9 +47,18 @@ public class TrieMemtableMetricsView public final MinMaxAvgMetric lastFlushShardDataSizes; private final TrieMemtableMetricNameFactory factory; + private final String keyspace; + private final String table; + + public static TrieMemtableMetricsView getOrCreate(String keyspace, String table) + { + return perTableMetrics.computeIfAbsent(getKey(keyspace, table), k -> new TrieMemtableMetricsView(keyspace, table)); + } - public TrieMemtableMetricsView(String keyspace, String table) + private TrieMemtableMetricsView(String keyspace, String table) { + this.keyspace = keyspace; + this.table = table; factory = new TrieMemtableMetricNameFactory(keyspace, table); uncontendedPuts = Metrics.counter(factory.createMetricName(UNCONTENDED_PUTS)); @@ -55,6 +69,8 @@ public TrieMemtableMetricsView(String keyspace, String table) public void release() { + perTableMetrics.remove(getKey(keyspace, table)); + Metrics.remove(factory.createMetricName(UNCONTENDED_PUTS)); Metrics.remove(factory.createMetricName(CONTENDED_PUTS)); contentionTime.release(); @@ -87,4 +103,9 @@ public CassandraMetricsRegistry.MetricName createMetricName(String metricName) return new CassandraMetricsRegistry.MetricName(groupName, type, metricName, keyspace + "." + table, mbeanName.toString()); } } + + private static String getKey(String keyspace, String table) + { + return keyspace + "." + table; + } } diff --git a/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java b/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java index 98363d413db4..360c359bf4e0 100644 --- a/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java @@ -30,14 +30,15 @@ public class ViewWriteMetrics extends ClientRequestMetrics public final Counter viewReplicasSuccess; // time between when mutation is applied to local memtable to when CL.ONE is achieved on MV public final Timer viewWriteLatency; + public final Gauge viewPendingMutations; - public ViewWriteMetrics(String scope) + public ViewWriteMetrics(String scope, String namePrefix) { - super(scope); - viewReplicasAttempted = Metrics.counter(factory.createMetricName("ViewReplicasAttempted")); - viewReplicasSuccess = Metrics.counter(factory.createMetricName("ViewReplicasSuccess")); - viewWriteLatency = Metrics.timer(factory.createMetricName("ViewWriteLatency")); - Metrics.register(factory.createMetricName("ViewPendingMutations"), new Gauge() + super(scope, namePrefix); + viewReplicasAttempted = Metrics.counter(factory.createMetricName(namePrefix + "ViewReplicasAttempted")); + viewReplicasSuccess = Metrics.counter(factory.createMetricName(namePrefix + "ViewReplicasSuccess")); + viewWriteLatency = Metrics.timer(factory.createMetricName(namePrefix + "ViewWriteLatency")); + viewPendingMutations = Metrics.register(factory.createMetricName(namePrefix + "ViewPendingMutations"), new Gauge() { public Long getValue() { @@ -49,9 +50,9 @@ public Long getValue() public void release() { super.release(); - Metrics.remove(factory.createMetricName("ViewReplicasAttempted")); - Metrics.remove(factory.createMetricName("ViewReplicasSuccess")); - Metrics.remove(factory.createMetricName("ViewWriteLatency")); - Metrics.remove(factory.createMetricName("ViewPendingMutations")); + Metrics.remove(factory.createMetricName(namePrefix + "ViewReplicasAttempted")); + Metrics.remove(factory.createMetricName(namePrefix + "ViewReplicasSuccess")); + Metrics.remove(factory.createMetricName(namePrefix + "ViewWriteLatency")); + Metrics.remove(factory.createMetricName(namePrefix + "ViewPendingMutations")); } } diff --git a/src/java/org/apache/cassandra/net/AbstractMessageHandler.java b/src/java/org/apache/cassandra/net/AbstractMessageHandler.java index 5b5b8b7f1ad7..ec3a9d773b6d 100644 --- a/src/java/org/apache/cassandra/net/AbstractMessageHandler.java +++ b/src/java/org/apache/cassandra/net/AbstractMessageHandler.java @@ -33,7 +33,6 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.EventLoop; -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; @@ -219,6 +218,11 @@ public boolean process(Frame frame) throws IOException return true; } + /** + * React to the decoder being reactivated + */ + protected abstract void onDecoderReactivated(); + private boolean processIntactFrame(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException { if (frame.isSelfContained) @@ -311,7 +315,7 @@ private void onReserveCapacityRegained(Limit endpointReserve, Limit globalReserv decoder.reactivate(); if (decoder.isActive()) - ClientMetrics.instance.unpauseConnection(); + onDecoderReactivated(); } } catch (Throwable t) diff --git a/src/java/org/apache/cassandra/net/AsyncStreamingOutputPlus.java b/src/java/org/apache/cassandra/net/AsyncStreamingOutputPlus.java index 915e8a31b604..fd7ba305ea01 100644 --- a/src/java/org/apache/cassandra/net/AsyncStreamingOutputPlus.java +++ b/src/java/org/apache/cassandra/net/AsyncStreamingOutputPlus.java @@ -33,6 +33,7 @@ import io.netty.channel.FileRegion; import io.netty.channel.WriteBufferWaterMark; import io.netty.handler.ssl.SslHandler; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.util.DataOutputStreamPlus; import org.apache.cassandra.net.SharedDefaultFileRegion.SharedFileChannel; @@ -156,8 +157,8 @@ class Holder */ public long writeFileToChannel(FileChannel file, RateLimiter limiter) throws IOException { - if (channel.pipeline().get(SslHandler.class) != null) - // each batch is loaded into ByteBuffer, 64KiB is more BufferPool friendly. + if (channel.pipeline().get(SslHandler.class) != null || !DatabaseDescriptor.nettyZerocopyEnabled()) + // each batch is loaded into ByteBuffer, 64kb is more BufferPool friendly. return writeFileToChannel(file, limiter, 1 << 16); else // write files in 1MiB chunks, since there may be blocking work performed to fetch it from disk, @@ -170,17 +171,17 @@ long writeFileToChannel(FileChannel fc, RateLimiter limiter, int batchSize) thro { final long length = fc.size(); long bytesTransferred = 0; + assert fc.position() == 0; try { while (bytesTransferred < length) { int toWrite = (int) min(batchSize, length - bytesTransferred); - final long position = bytesTransferred; writeToChannel(bufferSupplier -> { ByteBuffer outBuffer = bufferSupplier.get(toWrite); - long read = fc.read(outBuffer, position); + long read = fc.read(outBuffer); if (read != toWrite) throw new IOException(String.format("could not read required number of bytes from " + "file to be streamed: read %d bytes, wanted %d bytes", diff --git a/src/java/org/apache/cassandra/net/CustomParamsSerializer.java b/src/java/org/apache/cassandra/net/CustomParamsSerializer.java index c6c72fe6cbbd..ae03072001ea 100644 --- a/src/java/org/apache/cassandra/net/CustomParamsSerializer.java +++ b/src/java/org/apache/cassandra/net/CustomParamsSerializer.java @@ -49,7 +49,7 @@ public void serialize(Map t, DataOutputPlus out, int version) th @Override public long serializedSize(Map t, int version) { - int size = TypeSizes.sizeofUnsignedVInt(t.size()); + long size = TypeSizes.sizeofUnsignedVInt(t.size()); for (Map.Entry e : t.entrySet()) { size += TypeSizes.sizeof(e.getKey()); diff --git a/src/java/org/apache/cassandra/net/CustomResponseVerbHandlerProvider.java b/src/java/org/apache/cassandra/net/CustomResponseVerbHandlerProvider.java new file mode 100644 index 000000000000..9f016b1a600c --- /dev/null +++ b/src/java/org/apache/cassandra/net/CustomResponseVerbHandlerProvider.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.net; + +import java.util.function.Supplier; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.utils.FBUtilities; + +/** + * Priovides a response handler for response messages ({@link org.apache.cassandra.net.Verb#REQUEST_RSP} and + * {@link org.apache.cassandra.net.Verb#FAILURE_RSP}). + * Defaults to {@link ResponseVerbHandler#instance}. + */ +public interface CustomResponseVerbHandlerProvider extends Supplier> +{ + CustomResponseVerbHandlerProvider instance = CassandraRelevantProperties.CUSTOM_RESPONSE_VERB_HANDLER_PROVIDER.getString() == null ? + () -> ResponseVerbHandler.instance : + FBUtilities.construct(CassandraRelevantProperties.CUSTOM_RESPONSE_VERB_HANDLER_PROVIDER.getString(), "custom response verb handler"); + + IVerbHandler get(); + +} diff --git a/src/java/org/apache/cassandra/net/EndpointMessagingVersions.java b/src/java/org/apache/cassandra/net/EndpointMessagingVersions.java index dceffc7cc1e3..84601c5ee95b 100644 --- a/src/java/org/apache/cassandra/net/EndpointMessagingVersions.java +++ b/src/java/org/apache/cassandra/net/EndpointMessagingVersions.java @@ -20,6 +20,7 @@ import java.net.UnknownHostException; import java.util.Collections; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import org.cliffc.high_scale_lib.NonBlockingHashMap; @@ -27,6 +28,7 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.NoSpamLogger; /** * Map of hosts to their known current messaging versions. @@ -35,10 +37,20 @@ public class EndpointMessagingVersions { public volatile int minClusterVersion = MessagingService.current_version; private static final Logger logger = LoggerFactory.getLogger(EndpointMessagingVersions.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 60L, TimeUnit.SECONDS); // protocol versions of the other nodes in the cluster private final ConcurrentMap versions = new NonBlockingHashMap<>(); + public EndpointMessagingVersions() + { + } + + private EndpointMessagingVersions(EndpointMessagingVersions versions) + { + this.versions.putAll(versions.versions); + } + /** * @return the last version associated with address, or @param version if this is the first such version */ @@ -69,7 +81,7 @@ public int get(InetAddressAndPort endpoint) if (v == null) { // we don't know the version. assume current. we'll know soon enough if that was incorrect. - logger.trace("Assuming current protocol version for {}", endpoint); + noSpamLogger.debug("Assuming current protocol version for {}", endpoint); return MessagingService.current_version; } else @@ -96,4 +108,9 @@ public boolean knows(InetAddressAndPort endpoint) { return versions.containsKey(endpoint); } + + public EndpointMessagingVersions copy() + { + return new EndpointMessagingVersions(this); + } } diff --git a/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java b/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java index e91fe639a3d2..d8b992125ffa 100644 --- a/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java +++ b/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java @@ -356,7 +356,7 @@ else if (initiate.acceptVersions.max < accept.min) else { if (initiate.type.isStreaming()) - setupStreamingPipeline(initiate.from, ctx); + setupStreamingPipeline(initiate.from, useMessagingVersion, ctx); else setupMessagingPipeline(initiate.from, useMessagingVersion, initiate.acceptVersions.max, ctx.pipeline()); } @@ -428,7 +428,7 @@ private void failHandshake(Channel channel) } } - private void setupStreamingPipeline(InetAddressAndPort from, ChannelHandlerContext ctx) + private void setupStreamingPipeline(InetAddressAndPort from, int streamingVersion, ChannelHandlerContext ctx) { handshakeTimeout.cancel(true); assert initiate.framing == Framing.UNPROTECTED; @@ -447,10 +447,11 @@ private void setupStreamingPipeline(InetAddressAndPort from, ChannelHandlerConte // we can't infer the type of streaming connection at this point, // so we use CONTROL unconditionally; it's ugly but does what we want // (establishes an AsyncStreamingInputPlus) + channel.attr(NettyStreamingChannel.STREAMING_VERSION_ATTR).set(streamingVersion); NettyStreamingChannel streamingChannel = new NettyStreamingChannel(channel, StreamingChannel.Kind.CONTROL); pipeline.replace(this, "streamInbound", streamingChannel); executorFactory().startThread(String.format("Stream-Deserializer-%s-%s", from, channel.id()), - new StreamDeserializingTask(null, streamingChannel, current_version)); + new StreamDeserializingTask(null, streamingChannel, streamingVersion)); logger.info("{} streaming connection established, version = {}, framing = {}, encryption = {}", SocketFactory.channelId(from, @@ -459,7 +460,7 @@ private void setupStreamingPipeline(InetAddressAndPort from, ChannelHandlerConte (InetSocketAddress) channel.localAddress(), ConnectionType.STREAMING, channel.id().asShortText()), - current_version, + streamingVersion, initiate.framing, SocketFactory.encryptionConnectionSummary(pipeline.channel())); } diff --git a/src/java/org/apache/cassandra/net/InboundConnectionSettings.java b/src/java/org/apache/cassandra/net/InboundConnectionSettings.java index 448da62cbb8c..93b485d2f3a7 100644 --- a/src/java/org/apache/cassandra/net/InboundConnectionSettings.java +++ b/src/java/org/apache/cassandra/net/InboundConnectionSettings.java @@ -120,7 +120,7 @@ public InboundConnectionSettings withAcceptMessaging(AcceptVersions acceptMessag acceptMessaging, acceptStreaming, socketFactory, handlers); } - public InboundConnectionSettings withAcceptStreaming(AcceptVersions acceptMessaging) + public InboundConnectionSettings withAcceptStreaming(AcceptVersions acceptStreaming) { return new InboundConnectionSettings(authenticator, bindAddress, encryption, socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes, diff --git a/src/java/org/apache/cassandra/net/InboundMessageCallbacks.java b/src/java/org/apache/cassandra/net/InboundMessageCallbacks.java index ffa4243b9d10..9984fe27c78f 100644 --- a/src/java/org/apache/cassandra/net/InboundMessageCallbacks.java +++ b/src/java/org/apache/cassandra/net/InboundMessageCallbacks.java @@ -94,6 +94,13 @@ interface InboundMessageCallbacks /** * Invoked at the very end of execution of the message-processing task, no matter the outcome of processing. + * timeElapsed is the duration on message processing in the relevant stage */ void onExecuted(int messageSize, Header header, long timeElapsed, TimeUnit unit); + + /** + * Invoked at the very end of execution of the message-processing task, no matter the outcome of processing. + * timeElapsed is the duration of the whole messaging processing, including deserialization, stage queue wait time + */ + void onMessageHandlingCompleted(Header header, long timeElapsed, TimeUnit unit); } diff --git a/src/java/org/apache/cassandra/net/InboundMessageHandler.java b/src/java/org/apache/cassandra/net/InboundMessageHandler.java index 50a42e7b718b..307a084014ba 100644 --- a/src/java/org/apache/cassandra/net/InboundMessageHandler.java +++ b/src/java/org/apache/cassandra/net/InboundMessageHandler.java @@ -75,7 +75,7 @@ public class InboundMessageHandler extends AbstractMessageHandler private final ConnectionType type; private final InetAddressAndPort self; private final InetAddressAndPort peer; - private final int version; + final int version; private final InboundMessageCallbacks callbacks; private final Consumer> consumer; @@ -118,6 +118,12 @@ public class InboundMessageHandler extends AbstractMessageHandler this.consumer = consumer; } + @Override + protected void onDecoderReactivated() + { + // No-op for this implementation, as the InboundMessageHandler should not use ClientMetrics + } + protected boolean processOneContainedMessage(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve) throws IOException { ByteBuffer buf = bytes.get(); @@ -146,14 +152,14 @@ protected boolean processOneContainedMessage(ShareableBytes bytes, Limit endpoin receivedBytes += size; if (size <= largeThreshold) - processSmallMessage(bytes, size, header); + processSmallMessage(bytes, size, header, currentTimeNanos); else - processLargeMessage(bytes, size, header); + processLargeMessage(bytes, size, header, currentTimeNanos); return true; } - private void processSmallMessage(ShareableBytes bytes, int size, Header header) + private void processSmallMessage(ShareableBytes bytes, int size, Header header, long messageProcessingStartTimeNanos) { ByteBuffer buf = bytes.get(); final int begin = buf.position(); @@ -191,13 +197,13 @@ private void processSmallMessage(ShareableBytes bytes, int size, Header header) } if (null != message) - dispatch(new ProcessSmallMessage(message, size)); + dispatch(new ProcessSmallMessage(message, size, messageProcessingStartTimeNanos)); } // for various reasons, it's possible for a large message to be contained in a single frame - private void processLargeMessage(ShareableBytes bytes, int size, Header header) + private void processLargeMessage(ShareableBytes bytes, int size, Header header, long handlingStartNanos) { - new LargeMessage(size, header, bytes.sliceAndConsume(size).share()).schedule(); + new LargeMessage(size, header, bytes.sliceAndConsume(size).share(), handlingStartNanos).schedule(); } /* @@ -219,7 +225,7 @@ protected boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpo callbacks.onHeaderArrived(size, header, currentTimeNanos - header.createdAtNanos, NANOSECONDS); receivedBytes += buf.remaining(); - largeMessage = new LargeMessage(size, header, expired); + largeMessage = new LargeMessage(size, header, expired, currentTimeNanos); largeMessage.supply(frame); return true; } @@ -314,19 +320,23 @@ protected void fatalExceptionCaught(Throwable cause) */ private class LargeMessage extends AbstractMessageHandler.LargeMessage

    { - private LargeMessage(int size, Header header, boolean isExpired) + private long handlingStartNanos; + + private LargeMessage(int size, Header header, boolean isExpired, long handlingStartNanos) { super(size, header, header.expiresAtNanos, isExpired); + this.handlingStartNanos = handlingStartNanos; } - private LargeMessage(int size, Header header, ShareableBytes bytes) + private LargeMessage(int size, Header header, ShareableBytes bytes, long handlingStartNanos) { super(size, header, header.expiresAtNanos, bytes); + this.handlingStartNanos = handlingStartNanos; } private void schedule() { - dispatch(new ProcessLargeMessage(this)); + dispatch(new ProcessLargeMessage(this, handlingStartNanos)); } protected void onComplete() @@ -396,7 +406,7 @@ private void dispatch(ProcessMessage task) if (state != null) state.trace("{} message received from {}", header.verb, header.from); callbacks.onDispatched(task.size(), header); - header.verb.stage.execute(ExecutorLocals.create(state), task); + header.verb.stage.execute(task, ExecutorLocals.create(state)); } private abstract class ProcessMessage implements Runnable @@ -441,10 +451,13 @@ public void run() releaseResources(); - callbacks.onExecuted(size(), header, approxTime.now() - approxStartTimeNanos, NANOSECONDS); + long now = approxTime.now(); + callbacks.onExecuted(size(), header, now - approxStartTimeNanos, NANOSECONDS); + callbacks.onMessageHandlingCompleted(header, now - handlingStartNanos(), NANOSECONDS); } } + abstract long handlingStartNanos(); abstract int size(); abstract Header header(); abstract Message provideMessage(); @@ -455,11 +468,19 @@ private class ProcessSmallMessage extends ProcessMessage { private final int size; private final Message message; + private final long handlingStartNanos; - ProcessSmallMessage(Message message, int size) + ProcessSmallMessage(Message message, int size, long handlingStartNanos) { this.size = size; this.message = message; + this.handlingStartNanos = handlingStartNanos; + } + + @Override + long handlingStartNanos() + { + return handlingStartNanos; } int size() @@ -481,10 +502,18 @@ Message provideMessage() private class ProcessLargeMessage extends ProcessMessage { private final LargeMessage message; + private final long handlingStartNanos; - ProcessLargeMessage(LargeMessage message) + ProcessLargeMessage(LargeMessage message, long handlingStartNanos) { this.message = message; + this.handlingStartNanos = handlingStartNanos; + } + + @Override + long handlingStartNanos() + { + return handlingStartNanos; } int size() diff --git a/src/java/org/apache/cassandra/net/InboundMessageHandlers.java b/src/java/org/apache/cassandra/net/InboundMessageHandlers.java index c7b946350d09..b6810ac7afad 100644 --- a/src/java/org/apache/cassandra/net/InboundMessageHandlers.java +++ b/src/java/org/apache/cassandra/net/InboundMessageHandlers.java @@ -31,9 +31,6 @@ import org.apache.cassandra.metrics.InternodeInboundMetrics; import org.apache.cassandra.net.Message.Header; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; - /** * An aggregation of {@link InboundMessageHandler}s for all connections from a peer. * @@ -88,8 +85,10 @@ public interface MessageConsumer extends Consumer> public interface GlobalMetricCallbacks { LatencyConsumer internodeLatencyRecorder(InetAddressAndPort to); - void recordInternalLatency(Verb verb, long timeElapsed, TimeUnit timeUnit); + void recordInternalLatency(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit timeUnit); void recordInternodeDroppedMessage(Verb verb, long timeElapsed, TimeUnit timeUnit); + void recordMessageStageProcessingTime(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit unit); + void recordTotalMessageProcessingTime(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit unit); } public InboundMessageHandlers(InetAddressAndPort self, @@ -129,7 +128,7 @@ public InboundMessageHandlers(InetAddressAndPort self, largeCallbacks = makeMessageCallbacks(peer, largeCounters, globalMetricCallbacks, messageConsumer); legacyCallbacks = makeMessageCallbacks(peer, legacyCounters, globalMetricCallbacks, messageConsumer); - metrics = new InternodeInboundMetrics(peer, this); + metrics = InternodeInboundMetrics.create(peer, this); } InboundMessageHandler createHandler(FrameDecoder frameDecoder, ConnectionType type, Channel channel, int version) @@ -201,9 +200,9 @@ private static InboundMessageCallbacks makeMessageCallbacks(InetAddressAndPort p @Override public void onHeaderArrived(int messageSize, Header header, long timeElapsed, TimeUnit unit) { - // do not log latency if we are within error bars of zero - if (timeElapsed > unit.convert(approxTime.error(), NANOSECONDS)) - internodeLatency.accept(timeElapsed, unit); + // log latency even if we are within error bars of zero + // log negative numbers too; we are interested in the distribution, not precise values + internodeLatency.accept(header.verb, timeElapsed, unit); } @Override @@ -264,13 +263,20 @@ public void onDispatched(int messageSize, Header header) @Override public void onExecuting(int messageSize, Header header, long timeElapsed, TimeUnit unit) { - globalMetrics.recordInternalLatency(header.verb, timeElapsed, unit); + globalMetrics.recordInternalLatency(header.verb, header.from, timeElapsed, unit); } @Override public void onExecuted(int messageSize, Header header, long timeElapsed, TimeUnit unit) { counters.removePending(messageSize); + globalMetrics.recordMessageStageProcessingTime(header.verb, header.from, timeElapsed, unit); + } + + @Override + public void onMessageHandlingCompleted(Header header, long timeElapsed, TimeUnit unit) + { + globalMetrics.recordTotalMessageProcessingTime(header.verb, header.from, timeElapsed, unit); } @Override @@ -431,6 +437,13 @@ private long sumCounters(ToLongFunction mapping) + mapping.applyAsLong(legacyCounters); } + @VisibleForTesting + public void assertHandlersMessagingVersion(int expectedVersion) + { + for (InboundMessageHandler handler : handlers) + assert handler.version == expectedVersion : "Expected all handlers to be at version " + expectedVersion + " but found " + handler.version; + } + interface HandlerProvider { InboundMessageHandler provide(FrameDecoder decoder, diff --git a/src/java/org/apache/cassandra/net/InboundSink.java b/src/java/org/apache/cassandra/net/InboundSink.java index 9d68ba1aa078..f323f905e765 100644 --- a/src/java/org/apache/cassandra/net/InboundSink.java +++ b/src/java/org/apache/cassandra/net/InboundSink.java @@ -27,7 +27,9 @@ import net.openhft.chronicle.core.util.ThrowingConsumer; import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.index.IndexBuildInProgressException; import org.apache.cassandra.index.IndexNotAvailableException; +import org.apache.cassandra.index.sai.utils.AbortedOperationException; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.utils.NoSpamLogger; @@ -85,7 +87,8 @@ public void fail(Message.Header header, Throwable failure) InetAddressAndPort to = header.respondTo() != null ? header.respondTo() : header.from; Message response = Message.failureResponse(header.id, header.expiresAtNanos, - RequestFailureReason.forException(failure)); + RequestFailureReason.forException(failure), + header.verb); messaging.send(response, to); } } @@ -100,12 +103,24 @@ public void accept(Message message) { fail(message.header, t); - if (t instanceof TombstoneOverwhelmingException || t instanceof IndexNotAvailableException) + // The site throwing AbortedOperationException is responsible for logging it. + if (t instanceof AbortedOperationException) + return; + + if (t instanceof TombstoneOverwhelmingException || + t instanceof IndexNotAvailableException || + t instanceof IndexBuildInProgressException) + { noSpamLogger.error(t.getMessage()); + } else if (t instanceof RuntimeException) + { throw (RuntimeException) t; + } else + { throw new RuntimeException(t); + } } } diff --git a/src/java/org/apache/cassandra/net/LatencyConsumer.java b/src/java/org/apache/cassandra/net/LatencyConsumer.java index 3f10d4146a13..cc466d78af95 100644 --- a/src/java/org/apache/cassandra/net/LatencyConsumer.java +++ b/src/java/org/apache/cassandra/net/LatencyConsumer.java @@ -21,5 +21,5 @@ public interface LatencyConsumer { - void accept(long timeElapsed, TimeUnit unit); + void accept(Verb verb, long timeElapsed, TimeUnit unit); } diff --git a/src/java/org/apache/cassandra/net/LatencySubscribers.java b/src/java/org/apache/cassandra/net/LatencySubscribers.java index 823e6d0b4917..a8cdfea544ac 100644 --- a/src/java/org/apache/cassandra/net/LatencySubscribers.java +++ b/src/java/org/apache/cassandra/net/LatencySubscribers.java @@ -64,12 +64,14 @@ public void add(InetAddressAndPort address, long latency, TimeUnit unit) /** * Track latency information for the dynamic snitch * - * @param cb the callback associated with this message -- this lets us know if it's a message type we're interested in - * @param address the host that replied to the message + * @param cb the callback associated with this message -- this lets us know if it's a message type we're interested in + * @param responseVerb the verb to respond the request + * @param address the host that replied to the message + * @param isTimeout if the request has timed out */ - public void maybeAdd(RequestCallback cb, InetAddressAndPort address, long latency, TimeUnit unit) + public void maybeAdd(RequestCallback cb, Verb responseVerb, InetAddressAndPort address, long latency, TimeUnit unit, boolean isTimeout) { - if (cb.trackLatencyForSnitch()) + if (cb.trackLatencyForSnitch(responseVerb, isTimeout)) add(address, latency, unit); } } diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java index 705061562a27..ab3f61c1f837 100644 --- a/src/java/org/apache/cassandra/net/Message.java +++ b/src/java/org/apache/cassandra/net/Message.java @@ -26,13 +26,13 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.primitives.Ints; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nullable; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.io.IVersionedAsymmetricSerializer; @@ -54,9 +54,17 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt; import static org.apache.cassandra.net.MessagingService.VERSION_40; import static org.apache.cassandra.net.MessagingService.VERSION_50; +import static org.apache.cassandra.net.MessagingService.VERSION_DSE_68; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_10; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_11; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_12; +import static org.apache.cassandra.net.MessagingService.VERSION_DS_20; import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; -import static org.apache.cassandra.utils.vint.VIntCoding.*; +import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize; +import static org.apache.cassandra.utils.vint.VIntCoding.getUnsignedVInt; +import static org.apache.cassandra.utils.vint.VIntCoding.getUnsignedVInt32; +import static org.apache.cassandra.utils.vint.VIntCoding.skipUnsignedVInt; /** * Immutable main unit of internode communication - what used to be {@code MessageIn} and {@code MessageOut} fused @@ -204,19 +212,19 @@ public static Message synthetic(InetAddressAndPort from, Verb verb, T pay public static Message out(Verb verb, T payload, long expiresAtNanos) { - return outWithParam(nextId(), verb, expiresAtNanos, payload, 0, null, null); + return outWithParam(nextId(), verb, expiresAtNanos, payload, 0, null, null).build(); } public static Message outWithFlag(Verb verb, T payload, MessageFlag flag) { assert !verb.isResponse(); - return outWithParam(nextId(), verb, 0, payload, flag.addTo(0), null, null); + return outWithParam(nextId(), verb, 0, payload, flag.addTo(0), null, null).build(); } public static Message outWithFlags(Verb verb, T payload, MessageFlag flag1, MessageFlag flag2) { assert !verb.isResponse(); - return outWithParam(nextId(), verb, 0, payload, flag2.addTo(flag1.addTo(0)), null, null); + return outWithParam(nextId(), verb, 0, payload, flag2.addTo(flag1.addTo(0)), null, null).build(); } public static Message outWithFlags(Verb verb, T payload, Dispatcher.RequestTime requestTime, List flags) @@ -239,20 +247,20 @@ public static Message outWithFlags(Verb verb, T payload, Dispatcher.Reque @VisibleForTesting static Message outWithParam(long id, Verb verb, T payload, ParamType paramType, Object paramValue) { - return outWithParam(id, verb, 0, payload, paramType, paramValue); + return outWithParam(id, verb, 0, payload, paramType, paramValue).build(); } - private static Message outWithParam(long id, Verb verb, long expiresAtNanos, T payload, ParamType paramType, Object paramValue) + private static Builder outWithParam(long id, Verb verb, long expiresAtNanos, T payload, ParamType paramType, Object paramValue) { return outWithParam(id, verb, expiresAtNanos, payload, 0, paramType, paramValue); } - private static Message outWithParam(long id, Verb verb, long expiresAtNanos, T payload, int flags, ParamType paramType, Object paramValue) + private static Builder outWithParam(long id, Verb verb, long expiresAtNanos, T payload, int flags, ParamType paramType, Object paramValue) { return withParam(getBroadcastAddressAndPort(), id, verb, expiresAtNanos, payload, flags, paramType, paramValue); } - private static Message withParam(InetAddressAndPort from, long id, Verb verb, long expiresAtNanos, T payload, int flags, ParamType paramType, Object paramValue) + private static Builder withParam(InetAddressAndPort from, long id, Verb verb, long expiresAtNanos, T payload, int flags, ParamType paramType, Object paramValue) { if (payload == null) throw new IllegalArgumentException(); @@ -260,8 +268,14 @@ private static Message withParam(InetAddressAndPort from, long id, Verb v long createdAtNanos = approxTime.now(); if (expiresAtNanos == 0) expiresAtNanos = verb.expiresAtNanos(createdAtNanos); - - return new Message<>(new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, buildParams(paramType, paramValue)), payload); + return new Builder().ofVerb(verb) + .withPayload(payload) + .from(from) + .withId(id) + .withExpiresAt(expiresAtNanos) + .withCreatedAt(createdAtNanos) + .withFlags(flags) + .withParams(buildParams(paramType, paramValue)); } public static Message internalResponse(Verb verb, T payload) @@ -274,16 +288,22 @@ public static Message internalResponse(Verb verb, T payload) * Used by the {@code MultiRangeReadCommand} to split multi-range responses from a replica * into single-range responses. */ - public static Message remoteResponse(InetAddressAndPort from, Verb verb, T payload) + public static Message remoteResponse(InetAddressAndPort from, Verb verb, Map params, T payload) { assert verb.isResponse(); long createdAtNanos = approxTime.now(); long expiresAtNanos = verb.expiresAtNanos(createdAtNanos); - return new Message<>(new Header(0, verb, from, createdAtNanos, expiresAtNanos, 0, NO_PARAMS), payload); + return new Message<>(new Header(0, verb, from, createdAtNanos, expiresAtNanos, 0, params), payload); } /** Builds a response Message with provided payload, and all the right fields inferred from request Message */ public Message responseWith(T payload) + { + return outWithParam(id(), verb().responseVerb, expiresAtNanos(), payload, null, null).build(); + } + + /** Builds a response Message builder with provided payload, and all the right fields inferred from request Message */ + public Builder responseWithBuilder(T payload) { return outWithParam(id(), verb().responseVerb, expiresAtNanos(), payload, null, null); } @@ -294,15 +314,21 @@ public Message emptyResponse() return responseWith(NoPayload.noPayload); } + /** Builds a response Builder with no payload, to allow for adding custom params if needed */ + public Builder emptyResponseBuilder() + { + return responseWithBuilder(NoPayload.noPayload); + } + /** Builds a failure response Message with an explicit reason, and fields inferred from request Message */ public Message failureResponse(RequestFailureReason reason) { - return failureResponse(id(), expiresAtNanos(), reason); + return failureResponse(id(), expiresAtNanos(), reason, verb()); } - static Message failureResponse(long id, long expiresAtNanos, RequestFailureReason reason) + static Message failureResponse(long id, long expiresAtNanos, RequestFailureReason reason, Verb requestVerb) { - return outWithParam(id, Verb.FAILURE_RSP, expiresAtNanos, reason, null, null); + return outWithParam(id, Verb.FAILURE_RSP, expiresAtNanos, reason, ParamType.REQUEST_VERB_NAME, requestVerb.name()).build(); } public Message withPayload(V newPayload) @@ -342,7 +368,7 @@ public Message withParams(Map values) return new Message<>(header.withParams(values), payload); } - private static final EnumMap NO_PARAMS = new EnumMap<>(ParamType.class); + public static final EnumMap NO_PARAMS = new EnumMap<>(ParamType.class); private static Map buildParams(ParamType type, Object value) { @@ -388,7 +414,7 @@ private static Map addParams(Map params, M private static final AtomicInteger nextId = new AtomicInteger(0); - private static long nextId() + public static long nextId() { long id; do @@ -527,7 +553,21 @@ public Map params() @Nullable public Map customParams() { - return (Map) params.get(ParamType.CUSTOM_MAP); + return (Map) params.get(ParamType.CUSTOM_MAP); + } + + public int flags() + { + return flags; + } + + /** + * Keyspace that is beeing traced by the trace session attached to this message (if any). + */ + @Nullable + public String traceKeyspace() + { + return (String) params.get(ParamType.TRACE_KEYSPACE); } } @@ -545,6 +585,8 @@ public static class Builder private boolean hasId; + private Message cachedMessage; + private Builder() { } @@ -652,7 +694,31 @@ public Message build() if (payload == null) throw new IllegalArgumentException(); - return new Message<>(new Header(hasId ? id : nextId(), verb, from, createdAtNanos, expiresAtNanos, flags, params), payload); + return doBuild(hasId ? id : nextId()); + } + + public int currentPayloadSize(int version) + { + // use dummy id just for the sake of computing the serialized size + Message tmp = doBuild(0); + cachedMessage = tmp; + return tmp.payloadSize(version); + } + + private Message doBuild(long id) + { + if (verb == null) + throw new IllegalArgumentException(); + if (from == null) + throw new IllegalArgumentException(); + if (payload == null) + throw new IllegalArgumentException(); + + Message tmp = new Message<>(new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, params), payload); + if (cachedMessage != null) + tmp.maybeCachePayloadSize(cachedMessage); + + return tmp; } } @@ -1098,6 +1164,11 @@ private int payloadSize(Message message, int version) private int serializedSize40; private int serializedSize50; + private int serializedSizeDS10; + private int serializedSizeDS11; + private int serializedSizeDS12; + private int serializedSizeDS20; + private int serializedSizeDSE68; /** * Serialized size of the entire message, for the provided messaging version. Caches the calculated value. @@ -1114,15 +1185,40 @@ public int serializedSize(int version) if (serializedSize50 == 0) serializedSize50 = serializer.serializedSize(this, VERSION_50); return serializedSize50; + case VERSION_DS_10: + if (serializedSizeDS10 == 0) + serializedSizeDS10 = serializer.serializedSize(this, VERSION_DS_10); + return serializedSizeDS10; + case VERSION_DS_11: + if (serializedSizeDS11 == 0) + serializedSizeDS11 = serializer.serializedSize(this, VERSION_DS_11); + return serializedSizeDS11; + case VERSION_DS_12: + if (serializedSizeDS12 == 0) + serializedSizeDS12 = serializer.serializedSize(this, VERSION_DS_12); + return serializedSizeDS12; + case VERSION_DS_20: + if (serializedSizeDS20 == 0) + serializedSizeDS20 = (int) serializer.serializedSize(this, VERSION_DS_20); + return serializedSizeDS20; + case VERSION_DSE_68: + if (serializedSizeDSE68 == 0) + serializedSizeDSE68 = serializer.serializedSize(this, VERSION_DSE_68); + return serializedSizeDSE68; default: throw new IllegalStateException("Unkown serialization version " + version); } } - private int payloadSize40 = -1; - private int payloadSize50 = -1; + private int payloadSize40 = -1; + private int payloadSize50 = -1; + private int payloadSizeDS10 = -1; + private int payloadSizeDS11 = -1; + private int payloadSizeDS12 = -1; + private int payloadSizeDS20 = -1; + private int payloadSizeDSE68 = -1; - private int payloadSize(int version) + public int payloadSize(int version) { switch (version) { @@ -1134,12 +1230,52 @@ private int payloadSize(int version) if (payloadSize50 < 0) payloadSize50 = serializer.payloadSize(this, VERSION_50); return payloadSize50; - + case VERSION_DS_10: + if (payloadSizeDS10 < 0) + payloadSizeDS10 = serializer.payloadSize(this, VERSION_DS_10); + return payloadSizeDS10; + case VERSION_DS_11: + if (payloadSizeDS11 < 0) + payloadSizeDS11 = serializer.payloadSize(this, VERSION_DS_11); + return payloadSizeDS11; + case VERSION_DS_12: + if (payloadSizeDS12 < 0) + payloadSizeDS12 = serializer.payloadSize(this, VERSION_DS_12); + return payloadSizeDS12; + case VERSION_DS_20: + if (payloadSizeDS20 < 0) + payloadSizeDS20 = serializer.payloadSize(this, VERSION_DS_20); + return payloadSizeDS20; + case VERSION_DSE_68: + if (payloadSizeDSE68 < 0) + payloadSizeDSE68 = serializer.payloadSize(this, VERSION_DSE_68); + return payloadSizeDSE68; default: throw new IllegalStateException("Unkown serialization version " + version); } } + protected void maybeCachePayloadSize(Message other) + { + if (payload == other.payload) + { + if (other.payloadSize40 > 0) + payloadSize40 = other.payloadSize40; + if (other.payloadSize50 > 0) + payloadSize50 = other.payloadSize50; + if (other.payloadSizeDS10 > 0) + payloadSizeDS10 = other.payloadSizeDS10; + if (other.payloadSizeDS11 > 0) + payloadSizeDS11 = other.payloadSizeDS11; + if (other.payloadSizeDS12 > 0) + payloadSizeDS12 = other.payloadSizeDS12; + if (other.payloadSizeDS20 > 0) + payloadSizeDS20 = other.payloadSizeDS20; + if (other.payloadSizeDSE68 > 0) + payloadSizeDSE68 = other.payloadSizeDSE68; + } + } + static class OversizedMessageException extends RuntimeException { OversizedMessageException(int size) diff --git a/src/java/org/apache/cassandra/net/MessageDelivery.java b/src/java/org/apache/cassandra/net/MessageDelivery.java index 36001c4988fd..7b13028a7a1b 100644 --- a/src/java/org/apache/cassandra/net/MessageDelivery.java +++ b/src/java/org/apache/cassandra/net/MessageDelivery.java @@ -31,6 +31,6 @@ public interface MessageDelivery public void respond(V response, Message message); public default void respondWithFailure(RequestFailureReason reason, Message message) { - send(Message.failureResponse(message.id(), message.expiresAtNanos(), reason), message.respondTo()); + send(Message.failureResponse(message.id(), message.expiresAtNanos(), reason, message.verb()), message.respondTo()); } } diff --git a/src/java/org/apache/cassandra/net/MessagingService.java b/src/java/org/apache/cassandra/net/MessagingService.java index 94586b41c850..d7870096c06e 100644 --- a/src/java/org/apache/cassandra/net/MessagingService.java +++ b/src/java/org/apache/cassandra/net/MessagingService.java @@ -22,8 +22,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -37,6 +39,7 @@ import io.netty.util.concurrent.Future; //checkstyle: permit this import import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.exceptions.RequestFailureReason; @@ -44,6 +47,7 @@ import org.apache.cassandra.locator.Replica; import org.apache.cassandra.metrics.MessagingMetrics; import org.apache.cassandra.service.AbstractWriteResponseHandler; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.concurrent.AsyncPromise; @@ -52,7 +56,7 @@ import static java.util.Collections.synchronizedList; import static java.util.concurrent.TimeUnit.MINUTES; import static org.apache.cassandra.concurrent.Stage.MUTATION; -import static org.apache.cassandra.config.CassandraRelevantProperties.NON_GRACEFUL_SHUTDOWN; +import static org.apache.cassandra.config.CassandraRelevantProperties.*; import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.Throwables.maybeFail; @@ -213,30 +217,46 @@ public enum Version { /** @deprecated See CASSANDRA-18314 */ @Deprecated(since = "5.0") - VERSION_30(10), + VERSION_30(MessagingService.VERSION_30, false), /** @deprecated See CASSANDRA-18314 */ @Deprecated(since = "5.0") - VERSION_3014(11), - VERSION_40(12), + VERSION_3014(MessagingService.VERSION_3014, false), + VERSION_40(MessagingService.VERSION_40, false), // c14227 TTL overflow, 'uint' timestamps - VERSION_50(13); + VERSION_50(MessagingService.VERSION_50, true), + VERSION_DS_10(MessagingService.VERSION_DS_10, false), // DS Converged Cassandra 4.0 + VERSION_DS_11(MessagingService.VERSION_DS_11, false), + VERSION_DS_12(MessagingService.VERSION_DS_12, false), // adds index hints (CNDB-13129) + VERSION_DS_20(MessagingService.VERSION_DS_20, true), // DS Converged Cassandra 5.0 + VERSION_DSE_68(MessagingService.VERSION_DSE_68, false), // DSE 6.8 + ; + + public static final Version CURRENT = VERSION_DS_20; // TODO - we should consider what should be there - also there is CASSANDRA-19126 which changes the logic here public final int value; + public final boolean supportsExtendedDeletionTime; - Version(int value) + Version(int value, boolean extendedDeletionTime) { this.value = value; + this.supportsExtendedDeletionTime = extendedDeletionTime; } + @VisibleForTesting public static List supportedVersions() { List versions = Lists.newArrayList(); for (Version version : values()) - if (minimum_version <= version.value) + if (minimum_version <= version.value && version.value <= current_version) versions.add(version); return Collections.unmodifiableList(versions); } + + public static boolean supportsExtendedDeletionTime(int value) + { + return Version.values()[versionOrdinalMap.get(value)].supportsExtendedDeletionTime; + } } // Maintance Note: // Try to keep Version enum in-sync for testing. By having the versions in the enum tests can get access without forcing this class @@ -249,12 +269,20 @@ public static List supportedVersions() public static final int VERSION_3014 = 11; public static final int VERSION_40 = 12; public static final int VERSION_50 = 13; // c14227 TTL overflow, 'uint' timestamps + public static final int VERSION_DS_10 = 100; // DS Converged Cassandra 4.0 + // Current DataStax version while we have serialization differences. + // If differences get merged upstream then we can revert to OS versioning. + public static final int VERSION_DS_11 = 101; // adds ann_options (CNDB-12456) + public static final int VERSION_DS_12 = 102; // adds index hints (CNDB-13129) + public static final int VERSION_DS_20 = 110; // DS Converged Cassandra 5.0 public static final int minimum_version = VERSION_40; - public static final int maximum_version = VERSION_50; + public static final int maximum_version = VERSION_DS_20; // we want to use a modified behavior for the tools and clients - that is, since they are not running a server, they // should not need to run in a compatibility mode. They should be able to connect to the server regardless whether // it uses messaving version 4 or 5 - public static final int current_version = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_50; + public static final int current_version = currentVersion(); + // DSE 6.8 version for backward compatibility + public static final int VERSION_DSE_68 = 168; static AcceptVersions accept_messaging; static AcceptVersions accept_streaming; static @@ -262,15 +290,30 @@ public static List supportedVersions() if (DatabaseDescriptor.isClientInitialized()) { accept_messaging = new AcceptVersions(minimum_version, maximum_version); - accept_streaming = new AcceptVersions(minimum_version, maximum_version); + accept_streaming = new AcceptVersions(Math.min(VERSION_40, maximum_version), maximum_version); } else { accept_messaging = new AcceptVersions(minimum_version, current_version); - accept_streaming = new AcceptVersions(current_version, current_version); + // Streaming negotiates any common version in [VERSION_40, current_version]; this is only safe because every + // streaming serializer (stream headers, control/file messages, compressed payloads) is wire-compatible across + // that range. + accept_streaming = new AcceptVersions(Math.min(VERSION_40, current_version), current_version); + } + } + static Map versionOrdinalMap = Arrays.stream(Version.values()).collect(Collectors.toMap(v -> v.value, Enum::ordinal)); + + @Deprecated(since = "5.0") // remove when cndb no longer supports bdp/6.8-cndb + private static int currentVersion() + { + int version = CassandraRelevantProperties.DS_CURRENT_MESSAGING_VERSION.getInt(); + for (Version v : Version.values()) + { + if (v.value == version) + return version; } + throw new IllegalArgumentException("Unsupported current messaging version: " + version); } - static Map versionOrdinalMap = Arrays.stream(Version.values()).collect(Collectors.toMap(v -> v.value, v -> v.ordinal())); /** * This is an optimisation to speed up the translation of the serialization @@ -288,6 +331,9 @@ public static int getVersionOrdinal(int version) return ordinal; } + public final static boolean GRACEFUL_CLOSE = !NON_GRACEFUL_CLOSE.getBoolean(); + public final static boolean UNUSED_CONNECTION_MONITORING = !DISABLE_UNUSED_CONNECTION_MONITORING.getBoolean(); + private static class MSHandle { public static final MessagingService instance = new MessagingService(false); @@ -325,14 +371,18 @@ public static MessagingService instance() @VisibleForTesting MessagingService(boolean testOnly) { - this(testOnly, new EndpointMessagingVersions(), new MessagingMetrics()); + this(testOnly, new EndpointMessagingVersions(), + CUSTOM_MESSAGING_METRICS_PROVIDER_PROPERTY.isPresent() ? + FBUtilities.construct(CUSTOM_MESSAGING_METRICS_PROVIDER_PROPERTY.getString(), "Messaging Metrics Provider") : + new MessagingMetrics()); } @VisibleForTesting MessagingService(boolean testOnly, EndpointMessagingVersions versions, MessagingMetrics metrics) { super(testOnly, versions, metrics); - OutboundConnections.scheduleUnusedConnectionMonitoring(this, ScheduledExecutors.scheduledTasks, 1L, TimeUnit.HOURS); + if (UNUSED_CONNECTION_MONITORING) + OutboundConnections.scheduleUnusedConnectionMonitoring(this, ScheduledExecutors.scheduledTasks, 1L, TimeUnit.HOURS); } @Override @@ -467,7 +517,7 @@ private void doSend(Message message, InetAddressAndPort to, ConnectionType speci // expire the callback if the message failed to enqueue (failed to establish a connection or exceeded queue capacity) while (true) { - OutboundConnections connections = getOutbound(to); + OutboundConnections connections = getOutbound(to, true); try { connections.enqueue(message, specifyConnection); @@ -509,11 +559,19 @@ public void closeOutbound(InetAddressAndPort to) */ void closeOutboundNow(OutboundConnections connections) { - connections.close(true).addListener( + connections.close(GRACEFUL_CLOSE).addListener( future -> channelManagers.remove(connections.template().to, connections) ); } + // Used by CNDB + public void closeOutboundNow(InetAddressAndPort to) + { + OutboundConnections pool = channelManagers.get(to); + if (pool != null) + closeOutboundNow(pool); + } + /** * Only to be invoked once we believe the connections will never be used again. */ @@ -583,14 +641,14 @@ public void shutdown(long timeout, TimeUnit units, boolean shutdownGracefully, b isShuttingDown = true; logger.info("Waiting for messaging service to quiesce"); // We may need to schedule hints on the mutation stage, so it's erroneous to shut down the mutation stage first - assert !MUTATION.executor().isShutdown(); + assert !MUTATION.isShutdown(); if (shutdownGracefully) { callbacks.shutdownGracefully(); List> closing = new ArrayList<>(); for (OutboundConnections pool : channelManagers.values()) - closing.add(pool.close(true)); + closing.add(pool.close(GRACEFUL_CLOSE)); long deadline = nanoTime() + units.toNanos(timeout); maybeFail(() -> FutureCombiner.nettySuccessListener(closing).get(timeout, units), @@ -617,15 +675,25 @@ public void shutdown(long timeout, TimeUnit units, boolean shutdownGracefully, b closing.add(pool.close(false)); long deadline = nanoTime() + units.toNanos(timeout); - maybeFail(() -> FutureCombiner.nettySuccessListener(closing).get(timeout, units), - () -> { - if (shutdownExecutors) - shutdownExecutors(deadline); - }, - () -> ExecutorUtils.awaitTermination(timeout, units, inboundExecutors), - () -> callbacks.awaitTerminationUntil(deadline), - inboundSink::clear, - outboundSink::clear); + try + { + maybeFail(() -> FutureCombiner.nettySuccessListener(closing).get(timeout, units), + () -> { + if (shutdownExecutors) + shutdownExecutors(deadline); + }, + () -> ExecutorUtils.awaitTermination(timeout, units, inboundExecutors), + () -> callbacks.awaitTerminationUntil(deadline), + inboundSink::clear, + outboundSink::clear); + } + catch (Throwable t) + { + if (NON_GRACEFUL_SHUTDOWN.getBoolean()) + logger.info("Timeout when waiting for messaging service shutdown", t); + else + throw t; + } } } @@ -641,7 +709,7 @@ public void shutdownAbrubtly() isShuttingDown = true; logger.info("Waiting for messaging service to quiesce"); // We may need to schedule hints on the mutation stage, so it's erroneous to shut down the mutation stage first - assert !MUTATION.executor().isShutdown(); + assert !MUTATION.isShutdown(); callbacks.shutdownNow(false); inboundSockets.close(); @@ -659,10 +727,10 @@ private void shutdownExecutors(long deadlineNanos) throws TimeoutException, Inte socketFactory.awaitTerminationUntil(deadlineNanos); } - private OutboundConnections getOutbound(InetAddressAndPort to) + private OutboundConnections getOutbound(InetAddressAndPort to, boolean tryRegister) { OutboundConnections connections = channelManagers.get(to); - if (connections == null) + if (connections == null && tryRegister) connections = OutboundConnections.tryRegister(channelManagers, to, new OutboundConnectionSettings(to).withDefaults(ConnectionCategory.MESSAGING)); return connections; } @@ -700,4 +768,60 @@ public void waitUntilListening() throws InterruptedException { inboundSockets.open().await(); } + + /** + * Returns the endpoints for the given keyspace that are known to be alive and are using a messaging version older + * than the given version. + * + * @param keyspace a keyspace + * @param version a messaging version + * @return a set of alive endpoints in the given keyspace with messaging version below the given version + */ + public Set endpointsWithVersionBelow(String keyspace, int version) + { + Set nodes = new HashSet<>(); + for (InetAddressAndPort node : StorageService.instance.getTokenMetadataForKeyspace(keyspace).getAllEndpoints()) + { + if (versions.knows(node) && versions.getRaw(node) < version) + nodes.add(node); + } + return nodes; + } + + /** + * Returns the endpoints for the given keyspace that are known to be alive and have a connection whose + * messaging version is older than the given version. To be used for example when we want to be sure a message + * can be serialized to all endpoints, according to their negotiated version at connection time. + * + * @param keyspace a keyspace + * @param version a messaging version + * @return a set of alive endpoints in the given keyspace with messaging version below the given version + */ + public Set endpointsWithConnectionsOnVersionBelow(String keyspace, int version) + { + Set nodes = new HashSet<>(); + for (InetAddressAndPort node : StorageService.instance.getTokenMetadataForKeyspace(keyspace).getAllEndpoints()) + { + if (hasConnectionWithVersionBelow(node, version)) + nodes.add(node); + } + return nodes; + } + + private boolean hasConnectionWithVersionBelow(InetAddressAndPort node, int version) + { + OutboundConnections connections = getOutbound(node, false); + + if (connections == null) + return false; + + for (ConnectionType type : ConnectionType.MESSAGING_TYPES) + { + OutboundConnection connection = connections.connectionFor(type); + if (connection != null && connection.messagingVersion() < version) + return true; + } + + return false; + } } diff --git a/src/java/org/apache/cassandra/net/MessagingServiceMBean.java b/src/java/org/apache/cassandra/net/MessagingServiceMBean.java index 55e3a063673c..1e57835e1fd1 100644 --- a/src/java/org/apache/cassandra/net/MessagingServiceMBean.java +++ b/src/java/org/apache/cassandra/net/MessagingServiceMBean.java @@ -108,6 +108,11 @@ public interface MessagingServiceMBean * dropped message counts for server lifetime */ public Map getDroppedMessages(); + + /** + * dropped mutation counts by table for server lifetime + */ + public Map getDroppedMutationsByTable(); /** * Total number of timeouts happened on this node diff --git a/src/java/org/apache/cassandra/net/MessagingServiceMBeanImpl.java b/src/java/org/apache/cassandra/net/MessagingServiceMBeanImpl.java index 3b89834a3116..b762332edbb5 100644 --- a/src/java/org/apache/cassandra/net/MessagingServiceMBeanImpl.java +++ b/src/java/org/apache/cassandra/net/MessagingServiceMBeanImpl.java @@ -219,6 +219,12 @@ public Map getDroppedMessages() { return metrics.getDroppedMessages(); } + + @Override + public Map getDroppedMutationsByTable() + { + return metrics.getDroppedMutationsByTable(); + } @Override public long getTotalTimeouts() diff --git a/src/java/org/apache/cassandra/net/OutboundConnection.java b/src/java/org/apache/cassandra/net/OutboundConnection.java index cfb9f1ffc03e..16d2026eed8d 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnection.java +++ b/src/java/org/apache/cassandra/net/OutboundConnection.java @@ -1120,6 +1120,8 @@ void onCompletedHandshake(Result result) assert !state.isClosed(); MessagingSuccess success = result.success(); + messagingVersion = success.messagingVersion; + settings.endpointToVersion.set(settings.to, messagingVersion); debug.onConnect(success.messagingVersion, settings); state.disconnected().maintenance.cancel(false); @@ -1461,6 +1463,8 @@ public Future close(boolean flushQueue) try { + logger.debug("Closing connection {}", id()); + // note that we never clear the queue, to ensure that an enqueue has the opportunity to remove itself // if it raced with close, to potentially requeue the message on a replacement connection diff --git a/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java b/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java index 63b99a45dbcd..877931561480 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java +++ b/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java @@ -345,6 +345,12 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) int peerMessagingVersion = msg.maxMessagingVersion; logger.trace("received second handshake message from peer {}, msg = {}", settings.connectTo, msg); + logger.debug("Summary of messaging versions while connecting to {} " + + "peer messaging version {} request messaging version {} " + + "max accept version {} min accept version {}", + settings.connectTo, peerMessagingVersion, settings.acceptVersions.max, + settings.acceptVersions.max, settings.acceptVersions.min); + FrameEncoder frameEncoder = null; Result result; assert useMessagingVersion > 0; @@ -379,6 +385,8 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) } } + logger.debug("Result of connection initiation for {} is {}", settings.connectTo, result.outcome); + ChannelPipeline pipeline = ctx.pipeline(); if (result.isSuccess()) { diff --git a/src/java/org/apache/cassandra/net/OutboundConnectionSettings.java b/src/java/org/apache/cassandra/net/OutboundConnectionSettings.java index ccc74f0aba4f..62da4ebb2336 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnectionSettings.java +++ b/src/java/org/apache/cassandra/net/OutboundConnectionSettings.java @@ -18,6 +18,8 @@ package org.apache.cassandra.net; +import java.util.Objects; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -348,6 +350,13 @@ public EndpointMessagingVersions endpointToVersion() public InetAddressAndPort from() { + InetAddressAndPort from = this.from; + InetAddressAndPort preferredLocalAddress = DatabaseDescriptor.getEndpointSnitch() != null + ? DatabaseDescriptor.getEndpointSnitch().getPreferredAddress(connectTo()) + : null; + if (!Objects.equals(preferredLocalAddress, from)) + from = preferredLocalAddress; + return from != null ? from : FBUtilities.getBroadcastAddressAndPort(); } @@ -476,7 +485,10 @@ public OutboundConnectionSettings withDefaults(ConnectionCategory category) applicationSendQueueReserveGlobalCapacityInBytes(), tcpNoDelay(), flushLowWaterMark, flushHighWaterMark, tcpConnectTimeoutInMS(), tcpUserTimeoutInMS(category), acceptVersions(category), - from(), socketFactory(), callbacks(), debug(), endpointToVersion()); + from(), socketFactory(), callbacks(), debug(), + // If a set of versions is passed, make sure we do a copy of it, as the version might be later updated + // depending on the handshake result (i.e. nodes might handshake a different version) + endpointToVersion().copy()); } private static boolean isInLocalDC(IEndpointSnitch snitch, InetAddressAndPort localHost, InetAddressAndPort remoteHost) diff --git a/src/java/org/apache/cassandra/net/OutboundConnections.java b/src/java/org/apache/cassandra/net/OutboundConnections.java index aacc2b44736b..3a6a9ac3e85b 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnections.java +++ b/src/java/org/apache/cassandra/net/OutboundConnections.java @@ -36,12 +36,12 @@ import io.netty.util.concurrent.Future; //checkstyle: permit this import import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.metrics.InternodeOutboundMetrics; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static java.lang.Math.max; import static org.apache.cassandra.config.CassandraRelevantProperties.OTCP_LARGE_MESSAGE_THRESHOLD; -import static org.apache.cassandra.gms.Gossiper.instance; import static org.apache.cassandra.net.FrameEncoderCrc.HEADER_AND_TRAILER_LENGTH; import static org.apache.cassandra.net.MessagingService.current_version; import static org.apache.cassandra.net.ConnectionType.URGENT_MESSAGES; @@ -106,7 +106,7 @@ static OutboundConnections tryRegister(ConcurrentMap if (existing == null) { - connections.metrics = new InternodeOutboundMetrics(settings.to, connections); + connections.metrics = InternodeOutboundMetrics.create(settings.to, connections); connections.metricsReady.signalAll(); } else @@ -311,9 +311,9 @@ private void closeUnusedSinceLastRun() continue; if (cur.small == prev.small && cur.large == prev.large && cur.urgent == prev.urgent - && !instance.isKnownEndpoint(connections.template.to)) + && !Nodes.isKnownEndpoint(connections.template.to)) { - logger.info("Closing outbound connections to {}, as inactive and not known by Gossiper", + logger.info("Closing outbound connections to {}, as inactive and not known", connections.template.to); // close entirely if no traffic and the endpoint is unknown messagingService.closeOutboundNow(connections); diff --git a/src/java/org/apache/cassandra/net/OutboundSink.java b/src/java/org/apache/cassandra/net/OutboundSink.java index 34c72dbc3a11..16fea0890312 100644 --- a/src/java/org/apache/cassandra/net/OutboundSink.java +++ b/src/java/org/apache/cassandra/net/OutboundSink.java @@ -18,6 +18,7 @@ package org.apache.cassandra.net; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BiConsumer; import java.util.function.BiPredicate; import org.apache.cassandra.locator.InetAddressAndPort; @@ -27,7 +28,7 @@ * * Default sink {@link Sink} used by {@link MessagingService} is {@link MessagingService#doSend(Message, InetAddressAndPort, ConnectionType)}, which proceeds to * send messages over the network, but it can be overridden to filter out certain messages, record the fact - * of attempted delivery, or delay they delivery. + * of attempted delivery, delay the delivery, or perform some action after delivery occurs. * * This facility is most useful for test code. */ @@ -56,6 +57,24 @@ public void accept(Message message, InetAddressAndPort to, ConnectionType con } } + private static class PostSink implements Sink + { + final BiConsumer, InetAddressAndPort> postSink; + final Sink sink; + + private PostSink(BiConsumer, InetAddressAndPort> postSink, Sink sink) + { + this.postSink = postSink; + this.sink = sink; + } + + public void accept(Message message, InetAddressAndPort to, ConnectionType connectionType) + { + sink.accept(message, to, connectionType); + postSink.accept(message, to); + } + } + private volatile Sink sink; private static final AtomicReferenceFieldUpdater sinkUpdater = AtomicReferenceFieldUpdater.newUpdater(OutboundSink.class, Sink.class, "sink"); @@ -75,6 +94,18 @@ public void add(BiPredicate, InetAddressAndPort> allow) sinkUpdater.updateAndGet(this, sink -> new Filtered(allow, sink)); } + /** + * Add a method that gets called after {@link OutboundSink#accept(Message, InetAddressAndPort, ConnectionType)}. + * + *

    This is useful if you want to perform additional work after a message has been sent to the sink.

    + * + * @param post the method to call after {@link OutboundSink#accept}. + */ + public void addPost(BiConsumer, InetAddressAndPort> post) + { + sinkUpdater.updateAndGet(this, sink -> new PostSink(post, sink)); + } + public void remove(BiPredicate, InetAddressAndPort> allow) { sinkUpdater.updateAndGet(this, sink -> without(sink, allow)); diff --git a/src/java/org/apache/cassandra/net/ParamType.java b/src/java/org/apache/cassandra/net/ParamType.java index 77c0f32771ff..2fe712d66969 100644 --- a/src/java/org/apache/cassandra/net/ParamType.java +++ b/src/java/org/apache/cassandra/net/ParamType.java @@ -25,6 +25,7 @@ import org.apache.cassandra.utils.Int64Serializer; import org.apache.cassandra.utils.RangesSerializer; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.StringSerializer; import static java.lang.Math.max; @@ -57,7 +58,15 @@ public enum ParamType CUSTOM_MAP (14, CustomParamsSerializer.serializer), SNAPSHOT_RANGES (15, RangesSerializer.serializer), TOO_MANY_REFERENCED_INDEXES_WARN (16, Int32Serializer.serializer), - TOO_MANY_REFERENCED_INDEXES_FAIL (17, Int32Serializer.serializer); + TOO_MANY_REFERENCED_INDEXES_FAIL (17, Int32Serializer.serializer), + /** + * Messages with tracing sessions are decorated with the traced keyspace. + */ + TRACE_KEYSPACE (18, StringSerializer.serializer), + /** + * Failure response messages contain verb name of the incoming message. + */ + REQUEST_VERB_NAME (19, StringSerializer.serializer); final int id; final IVersionedSerializer serializer; diff --git a/src/java/org/apache/cassandra/net/RequestCallback.java b/src/java/org/apache/cassandra/net/RequestCallback.java index 14e0169b858a..2e69086598b7 100644 --- a/src/java/org/apache/cassandra/net/RequestCallback.java +++ b/src/java/org/apache/cassandra/net/RequestCallback.java @@ -18,9 +18,11 @@ package org.apache.cassandra.net; import java.util.Map; +import javax.annotation.Nullable; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.sensors.RequestSensors; /** * implementors of {@link RequestCallback} need to make sure that any public methods @@ -57,10 +59,10 @@ default boolean invokeOnFailure() } /** - * @return true if this callback is on the read path and its latency should be + * @return true if this callback is on the read path or it's expired counter leader response then its latency should be * given as input to the dynamic snitch. */ - default boolean trackLatencyForSnitch() + default boolean trackLatencyForSnitch(Verb responseVerb, boolean isTimeout) { return false; } @@ -80,4 +82,13 @@ static boolean isTimeout(Map failureRe return failureReasonByEndpoint.values().stream().allMatch(RequestFailureReason.TIMEOUT::equals); } + /** + * @return the {@link RequestSensors} associated with the request to track sensors as reported by response replicas. + * If null, sensor tracking will be disabled for this request. + */ + @Nullable + default RequestSensors getRequestSensors() + { + return null; + } } diff --git a/src/java/org/apache/cassandra/net/RequestCallbacks.java b/src/java/org/apache/cassandra/net/RequestCallbacks.java index ee63c5a3e652..edd23974f577 100644 --- a/src/java/org/apache/cassandra/net/RequestCallbacks.java +++ b/src/java/org/apache/cassandra/net/RequestCallbacks.java @@ -31,11 +31,13 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.IMutation; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.metrics.InternodeOutboundMetrics; import org.apache.cassandra.service.AbstractWriteResponseHandler; +import org.apache.cassandra.service.paxos.Commit; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -63,6 +65,8 @@ public class RequestCallbacks implements OutboundMessageCallbacks private final ScheduledExecutorPlus executor = executorFactory().scheduled("Callback-Map-Reaper", DISCARD); private final ConcurrentMap callbacks = new ConcurrentHashMap<>(); + private volatile boolean shutdown; + RequestCallbacks(MessagingService messagingService) { this.messagingService = messagingService; @@ -75,7 +79,7 @@ public class RequestCallbacks implements OutboundMessageCallbacks * @return the registered {@link CallbackInfo} for this id and peer, or {@code null} if unset or expired. */ @Nullable - CallbackInfo get(long id, InetAddressAndPort peer) + public CallbackInfo get(long id, InetAddressAndPort peer) { return callbacks.get(key(id, peer)); } @@ -93,10 +97,17 @@ public CallbackInfo remove(long id, InetAddressAndPort peer) /** * Register the provided {@link RequestCallback}, inferring expiry and id from the provided {@link Message}. */ + @VisibleForTesting public void addWithExpiration(RequestCallback cb, Message message, InetAddressAndPort to) { // mutations need to call the overload assert message.verb() != Verb.MUTATION_REQ && message.verb() != Verb.COUNTER_MUTATION_REQ; + if (shutdown) + { + if (logger.isTraceEnabled()) + logger.trace("Received request after messaging service shutdown so ignoring it"); + return; + } CallbackInfo previous = callbacks.put(key(message.id(), to), new CallbackInfo(message, to, cb)); assert previous == null : format("Callback already exists for id %d/%s! (%s)", message.id(), to, previous); } @@ -104,7 +115,13 @@ public void addWithExpiration(RequestCallback cb, Message message, InetAdd public void addWithExpiration(AbstractWriteResponseHandler cb, Message message, Replica to) { assert message.verb() == Verb.MUTATION_REQ || message.verb() == Verb.COUNTER_MUTATION_REQ || message.verb() == Verb.PAXOS_COMMIT_REQ; - CallbackInfo previous = callbacks.put(key(message.id(), to.endpoint()), new CallbackInfo(message, to.endpoint(), cb)); + if (shutdown) + { + if (logger.isTraceEnabled()) + logger.trace("Received request after messaging service shutdown so ignoring it"); + return; + } + CallbackInfo previous = callbacks.put(key(message.id(), to.endpoint()), new WriteCallbackInfo(message, to.endpoint(), cb)); assert previous == null : format("Callback already exists for id %d/%s! (%s)", message.id(), to.endpoint(), previous); } @@ -148,7 +165,7 @@ private void forceExpire() private void onExpired(CallbackInfo info) { - messagingService.latencySubscribers.maybeAdd(info.callback, info.peer, info.timeout(), NANOSECONDS); + messagingService.latencySubscribers.maybeAdd(info.callback, info.requestVerb.responseVerb, info.peer, info.timeout(), NANOSECONDS, true); InternodeOutboundMetrics.totalExpiredCallbacks.mark(); messagingService.markExpiredCallback(info.peer); @@ -159,6 +176,7 @@ private void onExpired(CallbackInfo info) void shutdownNow(boolean expireCallbacks) { + shutdown = true; executor.shutdownNow(); if (expireCallbacks) forceExpire(); @@ -166,6 +184,7 @@ void shutdownNow(boolean expireCallbacks) void shutdownGracefully() { + shutdown = true; expire(); if (!callbacks.isEmpty()) executor.schedule(this::shutdownGracefully, 100L, MILLISECONDS); @@ -235,13 +254,15 @@ public static class CallbackInfo final InetAddressAndPort peer; public final RequestCallback callback; + public final Verb requestVerb; - private CallbackInfo(Message message, InetAddressAndPort peer, RequestCallback callback) + public CallbackInfo(Message message, InetAddressAndPort peer, RequestCallback callback) { this.createdAtNanos = message.createdAtNanos(); this.expiresAtNanos = message.expiresAtNanos(); this.peer = peer; this.callback = callback; + this.requestVerb = message.verb(); } public long timeout() @@ -265,6 +286,34 @@ public String toString() } } + static class WriteCallbackInfo extends CallbackInfo + { + // either a Mutation, or a Paxos Commit (MessageOut) + private final Object mutation; + + @VisibleForTesting + WriteCallbackInfo(Message message, InetAddressAndPort peer, RequestCallback callback) + { + super(message, peer, callback); + this.mutation = message.payload; + } + + /** + * Used for sensors tracking. + */ + public IMutation iMutation() + { + return iMutation(mutation); + } + + private static IMutation iMutation(Object object) + { + assert object instanceof Commit || object instanceof IMutation : object; + return object instanceof Commit ? ((Commit) object).makeMutation() + : (IMutation) object; + } + } + @Override public void onOverloaded(Message message, InetAddressAndPort peer) { diff --git a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java index 1cee468cd3d7..3c036aab54df 100644 --- a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java +++ b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java @@ -17,16 +17,27 @@ */ package org.apache.cassandra.net; +import java.util.Optional; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.Sensor; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.Type; +import org.apache.cassandra.service.paxos.v1.AbstractPaxosCallback; +import org.apache.cassandra.service.reads.ReadCallback; import org.apache.cassandra.tracing.Tracing; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; -class ResponseVerbHandler implements IVerbHandler +public class ResponseVerbHandler implements IVerbHandler { public static final ResponseVerbHandler instance = new ResponseVerbHandler(); @@ -47,15 +58,93 @@ public void doVerb(Message message) long latencyNanos = approxTime.now() - callbackInfo.createdAtNanos; Tracing.trace("Processing response from {}", message.from()); - RequestCallback cb = callbackInfo.callback; + RequestCallback cb = callbackInfo.callback; if (message.isFailureResponse()) { cb.onFailure(message.from(), (RequestFailureReason) message.payload); } else { - MessagingService.instance().latencySubscribers.maybeAdd(cb, message.from(), latencyNanos, NANOSECONDS); + MessagingService.instance().latencySubscribers.maybeAdd(cb, message.verb(), message.from(), latencyNanos, NANOSECONDS, false); + trackReplicaSensors(callbackInfo, message); cb.onResponse(message); } } + + private void trackReplicaSensors(RequestCallbacks.CallbackInfo callbackInfo, Message message) + { + RequestSensors sensors = callbackInfo.callback.getRequestSensors(); + if (sensors == null) + return; + + if (callbackInfo instanceof RequestCallbacks.WriteCallbackInfo) + { + RequestCallbacks.WriteCallbackInfo writerInfo = (RequestCallbacks.WriteCallbackInfo) callbackInfo; + IMutation mutation = writerInfo.iMutation(); + if (mutation == null) + return; + + for (PartitionUpdate pu : mutation.getPartitionUpdates()) + { + Context context = Context.from(pu.metadata()); + if (pu.metadata().isIndex()) continue; + incrementSensor(sensors, context, Type.WRITE_BYTES, message); + // Paxos commit responses also include READ_BYTES + incrementSensor(sensors, context, Type.READ_BYTES, message); + } + } + else if (callbackInfo.callback instanceof ReadCallback) + { + ReadCallback readCallback = (ReadCallback) callbackInfo.callback; + Context context = Context.from(readCallback.command()); + incrementSensor(sensors, context, Type.READ_BYTES, message); + } + // Covers Paxos V1 Prepare and Propose callbacks. Paxos V1 Commit callback is a regular WriteCallbackInfo + else if (callbackInfo.callback instanceof AbstractPaxosCallback) + { + AbstractPaxosCallback paxosCallback = (AbstractPaxosCallback) callbackInfo.callback; + Context context = Context.from(paxosCallback.getMetadata()); + incrementSensor(sensors, context, Type.READ_BYTES, message); + incrementSensor(sensors, context, Type.WRITE_BYTES, message); + } + // Covers Paxos V2 Prepare, Propose and Commit callbacks + else if (callbackInfo.callback instanceof org.apache.cassandra.service.paxos.PaxosPrepare) + { + org.apache.cassandra.service.paxos.PaxosPrepare paxosCallback = (org.apache.cassandra.service.paxos.PaxosPrepare) callbackInfo.callback; + Context context = Context.from(paxosCallback.getTableMetadata()); + incrementSensor(sensors, context, Type.READ_BYTES, message); + incrementSensor(sensors, context, Type.WRITE_BYTES, message); + } + else if (callbackInfo.callback instanceof org.apache.cassandra.service.paxos.PaxosPropose) + { + org.apache.cassandra.service.paxos.PaxosPropose paxosCallback = (org.apache.cassandra.service.paxos.PaxosPropose) callbackInfo.callback; + Context context = Context.from(paxosCallback.getTableMetadata()); + incrementSensor(sensors, context, Type.READ_BYTES, message); + incrementSensor(sensors, context, Type.WRITE_BYTES, message); + } + else if (callbackInfo.callback instanceof org.apache.cassandra.service.paxos.PaxosCommit) + { + org.apache.cassandra.service.paxos.PaxosCommit paxosCallback = (org.apache.cassandra.service.paxos.PaxosCommit) callbackInfo.callback; + Context context = Context.from(paxosCallback.getTableMetadata()); + incrementSensor(sensors, context, Type.READ_BYTES, message); + incrementSensor(sensors, context, Type.WRITE_BYTES, message); + } + } + + /** + * Increments the sensor for the given context and type based on the value encoded in the replica response message. + */ + private void incrementSensor(RequestSensors sensors, Context context, Type type, Message message) + { + Optional sensor = sensors.getSensor(context, type); + if (sensor.isEmpty()) + return; + + Optional customParam = SensorsCustomParams.paramForRequestSensor(sensor.get()); + if (customParam.isEmpty()) + return; + + double sensorValue = SensorsCustomParams.sensorValueFromInternodeResponse(message, customParam.get()); + sensors.incrementSensor(context, type, sensorValue); + } } diff --git a/src/java/org/apache/cassandra/net/StartupClusterConnectivityChecker.java b/src/java/org/apache/cassandra/net/StartupClusterConnectivityChecker.java index 0197a6b34613..082948b569a8 100644 --- a/src/java/org/apache/cassandra/net/StartupClusterConnectivityChecker.java +++ b/src/java/org/apache/cassandra/net/StartupClusterConnectivityChecker.java @@ -209,6 +209,14 @@ private void sendPingMessages(Set peers, Map large = Message.out(PING_REQ, PingRequest.forLarge); for (InetAddressAndPort peer : peers) { + boolean known = MessagingService.instance().versions.knows(peer); + logger.debug("Peer {} is known with version {}", peer, known ? MessagingService.instance().versions.getRaw(peer) : "null"); + // DSE 6.8/6.9 advertises itself with value higher than VERSION_40, thus we need to compare it with VERSION_DSE_68 + boolean detectedDse = known && MessagingService.instance().versions.getRaw(peer) >= MessagingService.VERSION_DSE_68; + if (MessagingService.instance().versions.get(peer) < MessagingService.VERSION_40 || detectedDse) + // DSE 6.x doesn't support PING_REQ, and while C* 3.x does, PING only improves rolling restart times. CASSANDRA-13993 → CASSANDRA-14447 + continue; + MessagingService.instance().sendWithCallback(small, peer, responseHandler, SMALL_MESSAGES); MessagingService.instance().sendWithCallback(large, peer, responseHandler, LARGE_MESSAGES); } diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java index c85f0ddeca44..404f5a14fa84 100644 --- a/src/java/org/apache/cassandra/net/Verb.java +++ b/src/java/org/apache/cassandra/net/Verb.java @@ -19,10 +19,14 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToLongFunction; +import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -34,6 +38,8 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.CounterMutation; import org.apache.cassandra.db.CounterMutationVerbHandler; +import org.apache.cassandra.db.MultiRangeReadCommand; +import org.apache.cassandra.db.MultiRangeReadResponse; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.MutationVerbHandler; import org.apache.cassandra.db.ReadCommand; @@ -110,125 +116,129 @@ /** * Note that priorities except P0 are presently unused. P0 corresponds to urgent, i.e. what used to be the "Gossip" connection. */ -public enum Verb +public class Verb { - MUTATION_RSP (60, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - MUTATION_REQ (0, P3, writeTimeout, MUTATION, () -> Mutation.serializer, () -> MutationVerbHandler.instance, MUTATION_RSP ), - HINT_RSP (61, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - HINT_REQ (1, P4, writeTimeout, MUTATION, () -> HintMessage.serializer, () -> HintVerbHandler.instance, HINT_RSP ), - READ_REPAIR_RSP (62, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - READ_REPAIR_REQ (2, P1, writeTimeout, MUTATION, () -> Mutation.serializer, () -> ReadRepairVerbHandler.instance, READ_REPAIR_RSP ), - BATCH_STORE_RSP (65, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - BATCH_STORE_REQ (5, P3, writeTimeout, MUTATION, () -> Batch.serializer, () -> BatchStoreVerbHandler.instance, BATCH_STORE_RSP ), - BATCH_REMOVE_RSP (66, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - BATCH_REMOVE_REQ (6, P3, writeTimeout, MUTATION, () -> TimeUUID.Serializer.instance, () -> BatchRemoveVerbHandler.instance, BATCH_REMOVE_RSP ), - - PAXOS_PREPARE_RSP (93, P2, writeTimeout, REQUEST_RESPONSE, () -> PrepareResponse.serializer, () -> ResponseVerbHandler.instance ), - PAXOS_PREPARE_REQ (33, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> PrepareVerbHandler.instance, PAXOS_PREPARE_RSP ), - PAXOS_PROPOSE_RSP (94, P2, writeTimeout, REQUEST_RESPONSE, () -> BooleanSerializer.serializer, () -> ResponseVerbHandler.instance ), - PAXOS_PROPOSE_REQ (34, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> ProposeVerbHandler.instance, PAXOS_PROPOSE_RSP ), - PAXOS_COMMIT_RSP (95, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - PAXOS_COMMIT_REQ (35, P2, writeTimeout, MUTATION, () -> Agreed.serializer, () -> PaxosCommit.requestHandler, PAXOS_COMMIT_RSP ), - - TRUNCATE_RSP (79, P0, truncateTimeout, REQUEST_RESPONSE, () -> TruncateResponse.serializer, () -> ResponseVerbHandler.instance ), - TRUNCATE_REQ (19, P0, truncateTimeout, MUTATION, () -> TruncateRequest.serializer, () -> TruncateVerbHandler.instance, TRUNCATE_RSP ), - - COUNTER_MUTATION_RSP (84, P1, counterTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - COUNTER_MUTATION_REQ (24, P2, counterTimeout, COUNTER_MUTATION, () -> CounterMutation.serializer, () -> CounterMutationVerbHandler.instance, COUNTER_MUTATION_RSP), - - READ_RSP (63, P2, readTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, () -> ResponseVerbHandler.instance ), - READ_REQ (3, P3, readTimeout, READ, () -> ReadCommand.serializer, () -> ReadCommandVerbHandler.instance, READ_RSP ), - RANGE_RSP (69, P2, rangeTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, () -> ResponseVerbHandler.instance ), - RANGE_REQ (9, P3, rangeTimeout, READ, () -> ReadCommand.serializer, () -> ReadCommandVerbHandler.instance, RANGE_RSP ), - - GOSSIP_DIGEST_SYN (14, P0, longTimeout, GOSSIP, () -> GossipDigestSyn.serializer, () -> GossipDigestSynVerbHandler.instance ), - GOSSIP_DIGEST_ACK (15, P0, longTimeout, GOSSIP, () -> GossipDigestAck.serializer, () -> GossipDigestAckVerbHandler.instance ), - GOSSIP_DIGEST_ACK2 (16, P0, longTimeout, GOSSIP, () -> GossipDigestAck2.serializer, () -> GossipDigestAck2VerbHandler.instance ), - GOSSIP_SHUTDOWN (29, P0, rpcTimeout, GOSSIP, () -> GossipShutdown.serializer, () -> GossipShutdownVerbHandler.instance ), - - ECHO_RSP (91, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - ECHO_REQ (31, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> EchoVerbHandler.instance, ECHO_RSP ), - PING_RSP (97, P1, pingTimeout, GOSSIP, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - PING_REQ (37, P1, pingTimeout, GOSSIP, () -> PingRequest.serializer, () -> PingVerbHandler.instance, PING_RSP ), - - // P1 because messages can be arbitrarily large or aren't crucial - SCHEMA_PUSH_RSP (98, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - SCHEMA_PUSH_REQ (18, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> SchemaPushVerbHandler.instance, SCHEMA_PUSH_RSP ), - SCHEMA_PULL_RSP (88, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> ResponseVerbHandler.instance ), - SCHEMA_PULL_REQ (28, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaPullVerbHandler.instance, SCHEMA_PULL_RSP ), - SCHEMA_VERSION_RSP (80, P1, rpcTimeout, MIGRATION, () -> UUIDSerializer.serializer, () -> ResponseVerbHandler.instance ), - SCHEMA_VERSION_REQ (20, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaVersionVerbHandler.instance, SCHEMA_VERSION_RSP ), + private static final List verbs = new ArrayList<>(); + + public static List getValues() + { + return ImmutableList.copyOf(verbs); + } + + public static Verb MUTATION_RSP = new Verb("MUTATION_RSP", 60, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance); + public static Verb MUTATION_REQ = new Verb("MUTATION_REQ", 0, P3, writeTimeout, MUTATION, () -> Mutation.serializer, () -> MutationVerbHandler.instance, MUTATION_RSP); + public static Verb HINT_RSP = new Verb("HINT_RSP", 61, P1, hintTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb HINT_REQ = new Verb("HINT_REQ", 1, P4, hintTimeout, MUTATION, () -> HintMessage.serializer, () -> HintVerbHandler.instance, HINT_RSP ); + public static Verb READ_REPAIR_RSP = new Verb("READ_REPAIR_RSP", 62, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb READ_REPAIR_REQ = new Verb("READ_REPAIR_REQ", 2, P1, writeTimeout, MUTATION, () -> Mutation.serializer, () -> ReadRepairVerbHandler.instance, READ_REPAIR_RSP ); + public static Verb BATCH_STORE_RSP = new Verb("BATCH_STORE_RSP", 65, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb BATCH_STORE_REQ = new Verb("BATCH_STORE_REQ", 5, P3, writeTimeout, MUTATION, () -> Batch.serializer, () -> BatchStoreVerbHandler.instance, BATCH_STORE_RSP ); + public static Verb BATCH_REMOVE_RSP = new Verb("BATCH_REMOVE_RSP", 66, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb BATCH_REMOVE_REQ = new Verb("BATCH_REMOVE_REQ", 6, P3, writeTimeout, MUTATION, () -> TimeUUID.Serializer.instance, () -> BatchRemoveVerbHandler.instance, BATCH_REMOVE_RSP ); + + public static Verb PAXOS_PREPARE_RSP = new Verb("PAXOS_PREPARE_RSP", 93, P2, writeTimeout, REQUEST_RESPONSE, () -> PrepareResponse.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS_PREPARE_REQ = new Verb("PAXOS_PREPARE_REQ", 33, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> PrepareVerbHandler.instance, PAXOS_PREPARE_RSP ); + public static Verb PAXOS_PROPOSE_RSP = new Verb("PAXOS_PROPOSE_RSP", 94, P2, writeTimeout, REQUEST_RESPONSE, () -> BooleanSerializer.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS_PROPOSE_REQ = new Verb("PAXOS_PROPOSE_REQ", 34, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> ProposeVerbHandler.instance, PAXOS_PROPOSE_RSP ); + public static Verb PAXOS_COMMIT_RSP = new Verb("PAXOS_COMMIT_RSP", 95, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS_COMMIT_REQ = new Verb("PAXOS_COMMIT_REQ", 35, P2, writeTimeout, MUTATION, () -> Agreed.serializer, () -> PaxosCommit.requestHandler, PAXOS_COMMIT_RSP ); + + public static Verb TRUNCATE_RSP = new Verb("TRUNCATE_RSP", 79, P0, truncateTimeout, REQUEST_RESPONSE, () -> TruncateResponse.serializer, () -> ResponseVerbHandler.instance ); + public static Verb TRUNCATE_REQ = new Verb("TRUNCATE_REQ", 19, P0, truncateTimeout, MUTATION, () -> TruncateRequest.serializer, () -> TruncateVerbHandler.instance, TRUNCATE_RSP ); + + public static Verb GOSSIP_DIGEST_SYN = new Verb("GOSSIP_DIGEST_SYN", 14, P0, longTimeout, GOSSIP, () -> GossipDigestSyn.serializer, () -> GossipDigestSynVerbHandler.instance ); + public static Verb GOSSIP_DIGEST_ACK = new Verb("GOSSIP_DIGEST_ACK", 15, P0, longTimeout, GOSSIP, () -> GossipDigestAck.serializer, () -> GossipDigestAckVerbHandler.instance ); + public static Verb GOSSIP_DIGEST_ACK2 = new Verb("GOSSIP_DIGEST_ACK2", 16, P0, longTimeout, GOSSIP, () -> GossipDigestAck2.serializer, () -> GossipDigestAck2VerbHandler.instance ); + public static Verb GOSSIP_SHUTDOWN = new Verb("GOSSIP_SHUTDOWN", 29, P0, rpcTimeout, GOSSIP, () -> GossipShutdown.serializer, () -> GossipShutdownVerbHandler.instance ); + public static Verb COUNTER_MUTATION_RSP = new Verb("COUNTER_MUTATION_RSP", 84, P1, counterTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb COUNTER_MUTATION_REQ = new Verb("COUNTER_MUTATION_REQ", 24, P2, counterTimeout, COUNTER_MUTATION, () -> CounterMutation.serializer, () -> CounterMutationVerbHandler.instance, COUNTER_MUTATION_RSP); + + public static Verb READ_RSP = new Verb("READ_RSP", 63, P2, readTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, () -> ResponseVerbHandler.instance ); + public static Verb READ_REQ = new Verb("READ_REQ", 3, P3, readTimeout, READ, () -> ReadCommand.serializer, () -> ReadCommandVerbHandler.instance, READ_RSP ); + public static Verb RANGE_RSP = new Verb("RANGE_RSP", 69, P2, rangeTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, () -> ResponseVerbHandler.instance ); + public static Verb RANGE_REQ = new Verb("RANGE_REQ", 9, P3, rangeTimeout, READ, () -> ReadCommand.serializer, () -> ReadCommandVerbHandler.instance, RANGE_RSP ); + public static Verb MULTI_RANGE_RSP = new Verb("MULTI_RANGE_RSP", 67, P2, rangeTimeout, REQUEST_RESPONSE, () -> MultiRangeReadResponse.serializer, () -> ResponseVerbHandler.instance ); + public static Verb MULTI_RANGE_REQ = new Verb("MULTI_RANGE_REQ", 7, P3, rangeTimeout, READ, () -> MultiRangeReadCommand.serializer, () -> ReadCommandVerbHandler.instance, MULTI_RANGE_RSP ); + + public static Verb ECHO_RSP = new Verb("ECHO_RSP", 91, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb ECHO_REQ = new Verb("ECHO_REQ", 31, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> EchoVerbHandler.instance, ECHO_RSP ); + public static Verb PING_RSP = new Verb("PING_RSP", 97, P1, pingTimeout, GOSSIP, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PING_REQ = new Verb("PING_REQ", 37, P1, pingTimeout, GOSSIP, () -> PingRequest.serializer, () -> PingVerbHandler.instance, PING_RSP ); + + // public static Verb P1 because messages can be arbitrarily large or aren't crucial + public static Verb SCHEMA_PUSH_RSP = new Verb("SCHEMA_PUSH_RSP", 98, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb SCHEMA_PUSH_REQ = new Verb("SCHEMA_PUSH_REQ", 18, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> SchemaPushVerbHandler.instance, SCHEMA_PUSH_RSP ); + public static Verb SCHEMA_PULL_RSP = new Verb("SCHEMA_PULL_RSP", 88, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> ResponseVerbHandler.instance ); + public static Verb SCHEMA_PULL_REQ = new Verb("SCHEMA_PULL_REQ", 28, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaPullVerbHandler.instance, SCHEMA_PULL_RSP ); + public static Verb SCHEMA_VERSION_RSP = new Verb("SCHEMA_VERSION_RSP", 80, P1, rpcTimeout, MIGRATION, () -> UUIDSerializer.serializer, () -> ResponseVerbHandler.instance ); + public static Verb SCHEMA_VERSION_REQ = new Verb("SCHEMA_VERSION_REQ", 20, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaVersionVerbHandler.instance, SCHEMA_VERSION_RSP ); // repair; mostly doesn't use callbacks and sends responses as their own request messages, with matching sessions by uuid; should eventually harmonize and make idiomatic // for the repair messages that implement retry logic, use rpcTimeout so the single request fails faster, then retries can be used to recover - REPAIR_RSP (100, P1, repairTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - VALIDATION_RSP (102, P1, repairValidationRspTimeout, ANTI_ENTROPY, () -> ValidationResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - VALIDATION_REQ (101, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> ValidationRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - SYNC_RSP (104, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> SyncResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - SYNC_REQ (103, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> SyncRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - PREPARE_MSG (105, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> PrepareMessage.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - SNAPSHOT_MSG (106, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> SnapshotMessage.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - CLEANUP_MSG (107, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> CleanupMessage.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - PREPARE_CONSISTENT_RSP (109, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> PrepareConsistentResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - PREPARE_CONSISTENT_REQ (108, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> PrepareConsistentRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - FINALIZE_PROPOSE_MSG (110, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FinalizePropose.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - FINALIZE_PROMISE_MSG (111, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FinalizePromise.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - FINALIZE_COMMIT_MSG (112, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FinalizeCommit.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - FAILED_SESSION_MSG (113, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FailSession.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - STATUS_RSP (115, P1, repairTimeout, ANTI_ENTROPY, () -> StatusResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - STATUS_REQ (114, P1, repairTimeout, ANTI_ENTROPY, () -> StatusRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ), - - REPLICATION_DONE_RSP (82, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - REPLICATION_DONE_REQ (22, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ReplicationDoneVerbHandler.instance, REPLICATION_DONE_RSP), - SNAPSHOT_RSP (87, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - SNAPSHOT_REQ (27, P0, rpcTimeout, MISC, () -> SnapshotCommand.serializer, () -> SnapshotVerbHandler.instance, SNAPSHOT_RSP ), - - PAXOS2_COMMIT_REMOTE_REQ (38, P2, writeTimeout, MUTATION, () -> Mutation.serializer, () -> MutationVerbHandler.instance, MUTATION_RSP ), - PAXOS2_COMMIT_REMOTE_RSP (39, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - PAXOS2_PREPARE_RSP (50, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, () -> ResponseVerbHandler.instance ), - PAXOS2_PREPARE_REQ (40, P2, writeTimeout, MUTATION, () -> PaxosPrepare.requestSerializer, () -> PaxosPrepare.requestHandler, PAXOS2_PREPARE_RSP ), - PAXOS2_PREPARE_REFRESH_RSP (51, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepareRefresh.responseSerializer, () -> ResponseVerbHandler.instance ), - PAXOS2_PREPARE_REFRESH_REQ (41, P2, writeTimeout, MUTATION, () -> PaxosPrepareRefresh.requestSerializer, () -> PaxosPrepareRefresh.requestHandler, PAXOS2_PREPARE_REFRESH_RSP ), - PAXOS2_PROPOSE_RSP (52, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPropose.responseSerializer, () -> ResponseVerbHandler.instance ), - PAXOS2_PROPOSE_REQ (42, P2, writeTimeout, MUTATION, () -> PaxosPropose.requestSerializer, () -> PaxosPropose.requestHandler, PAXOS2_PROPOSE_RSP ), - PAXOS2_COMMIT_AND_PREPARE_RSP (53, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, () -> ResponseVerbHandler.instance ), - PAXOS2_COMMIT_AND_PREPARE_REQ (43, P2, writeTimeout, MUTATION, () -> PaxosCommitAndPrepare.requestSerializer, () -> PaxosCommitAndPrepare.requestHandler, PAXOS2_COMMIT_AND_PREPARE_RSP ), - PAXOS2_REPAIR_RSP (54, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.responseSerializer, () -> ResponseVerbHandler.instance ), - PAXOS2_REPAIR_REQ (44, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.requestSerializer, () -> PaxosRepair.requestHandler, PAXOS2_REPAIR_RSP ), - PAXOS2_CLEANUP_START_PREPARE_RSP (55, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupHistory.serializer, () -> ResponseVerbHandler.instance ), - PAXOS2_CLEANUP_START_PREPARE_REQ (45, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosStartPrepareCleanup.serializer, () -> PaxosStartPrepareCleanup.verbHandler, PAXOS2_CLEANUP_START_PREPARE_RSP ), - PAXOS2_CLEANUP_RSP (56, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - PAXOS2_CLEANUP_REQ (46, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupRequest.serializer, () -> PaxosCleanupRequest.verbHandler, PAXOS2_CLEANUP_RSP ), - PAXOS2_CLEANUP_RSP2 (57, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupResponse.serializer, () -> PaxosCleanupResponse.verbHandler ), - PAXOS2_CLEANUP_FINISH_PREPARE_RSP(58, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - PAXOS2_CLEANUP_FINISH_PREPARE_REQ(47, P2, repairTimeout, IMMEDIATE, () -> PaxosCleanupHistory.serializer, () -> PaxosFinishPrepareCleanup.verbHandler, PAXOS2_CLEANUP_FINISH_PREPARE_RSP), - PAXOS2_CLEANUP_COMPLETE_RSP (59, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), - PAXOS2_CLEANUP_COMPLETE_REQ (48, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupComplete.serializer, () -> PaxosCleanupComplete.verbHandler, PAXOS2_CLEANUP_COMPLETE_RSP ), + public static Verb REPAIR_RSP = new Verb("REPAIR_RSP", 100, P1, repairTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb VALIDATION_RSP = new Verb("VALIDATION_RSP", 102, P1, repairValidationRspTimeout, ANTI_ENTROPY, () -> ValidationResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb VALIDATION_REQ = new Verb("VALIDATION_REQ", 101, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> ValidationRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb SYNC_RSP = new Verb("SYNC_RSP", 104, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> SyncResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb SYNC_REQ = new Verb("SYNC_REQ", 103, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> SyncRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb PREPARE_MSG = new Verb("PREPARE_MSG", 105, P1, prepareWithBackoffTimeout, ANTI_ENTROPY, () -> PrepareMessage.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb SNAPSHOT_MSG = new Verb("SNAPSHOT_MSG", 106, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> SnapshotMessage.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb CLEANUP_MSG = new Verb("CLEANUP_MSG", 107, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> CleanupMessage.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb PREPARE_CONSISTENT_RSP = new Verb("PREPARE_CONSISTENT_RSP", 109, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> PrepareConsistentResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb PREPARE_CONSISTENT_REQ = new Verb("PREPARE_CONSISTENT_REQ", 108, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> PrepareConsistentRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb FINALIZE_PROPOSE_MSG = new Verb("FINALIZE_PROPOSE_MSG", 110, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FinalizePropose.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb FINALIZE_PROMISE_MSG = new Verb("FINALIZE_PROMISE_MSG", 111, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FinalizePromise.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb FINALIZE_COMMIT_MSG = new Verb("FINALIZE_COMMIT_MSG", 112, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FinalizeCommit.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb FAILED_SESSION_MSG = new Verb("FAILED_SESSION_MSG", 113, P1, repairWithBackoffTimeout, ANTI_ENTROPY, () -> FailSession.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb STATUS_RSP = new Verb("STATUS_RSP", 115, P1, repairTimeout, ANTI_ENTROPY, () -> StatusResponse.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + public static Verb STATUS_REQ = new Verb("STATUS_REQ", 114, P1, repairTimeout, ANTI_ENTROPY, () -> StatusRequest.serializer, () -> RepairMessageVerbHandler.instance(), REPAIR_RSP ); + + public static Verb REPLICATION_DONE_RSP = new Verb("REPLICATION_DONE_RSP", 82, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb REPLICATION_DONE_REQ = new Verb("REPLICATION_DONE_REQ", 22, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ReplicationDoneVerbHandler.instance, REPLICATION_DONE_RSP); + public static Verb SNAPSHOT_RSP = new Verb("SNAPSHOT_RSP", 87, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb SNAPSHOT_REQ = new Verb("SNAPSHOT_REQ", 27, P0, rpcTimeout, MISC, () -> SnapshotCommand.serializer, () -> SnapshotVerbHandler.instance, SNAPSHOT_RSP ); + + public static Verb PAXOS2_COMMIT_REMOTE_REQ=new Verb("PAXOS2_COMMIT_REMOTE_REQ",38, P2, writeTimeout, MUTATION, () -> Mutation.serializer, () -> MutationVerbHandler.instance, MUTATION_RSP ); + public static Verb PAXOS2_COMMIT_REMOTE_RSP=new Verb("PAXOS2_COMMIT_REMOTE_RSP",39, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_PREPARE_RSP = new Verb("PAXOS2_PREPARE_RSP", 50, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_PREPARE_REQ = new Verb("PAXOS2_PREPARE_REQ", 40, P2, writeTimeout, MUTATION, () -> PaxosPrepare.requestSerializer, () -> PaxosPrepare.requestHandler, PAXOS2_PREPARE_RSP ); + public static Verb PAXOS2_PREPARE_REFRESH_RSP= new Verb("PAXOS2_PREPARE_REFRESH_RSP", 51, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepareRefresh.responseSerializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_PREPARE_REFRESH_REQ= new Verb("PAXOS2_PREPARE_REFRESH_REQ", 41, P2, writeTimeout, MUTATION, () -> PaxosPrepareRefresh.requestSerializer,() -> PaxosPrepareRefresh.requestHandler, PAXOS2_PREPARE_REFRESH_RSP); + public static Verb PAXOS2_PROPOSE_RSP = new Verb("PAXOS2_PROPOSE_RSP", 52, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPropose.responseSerializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_PROPOSE_REQ = new Verb("PAXOS2_PROPOSE_REQ", 42, P2, writeTimeout, MUTATION, () -> PaxosPropose.requestSerializer, () -> PaxosPropose.requestHandler, PAXOS2_PROPOSE_RSP ); + public static Verb PAXOS2_COMMIT_AND_PREPARE_RSP= new Verb("PAXOS2_COMMIT_AND_PREPARE_RSP", 53, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_COMMIT_AND_PREPARE_REQ= new Verb("PAXOS2_COMMIT_AND_PREPARE_REQ", 43, P2, writeTimeout, MUTATION, () -> PaxosCommitAndPrepare.requestSerializer, () -> PaxosCommitAndPrepare.requestHandler, PAXOS2_COMMIT_AND_PREPARE_RSP ); + public static Verb PAXOS2_REPAIR_RSP = new Verb("PAXOS2_REPAIR_RSP", 54, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.responseSerializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_REPAIR_REQ = new Verb("PAXOS2_REPAIR_REQ", 44, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.requestSerializer, () -> PaxosRepair.requestHandler, PAXOS2_REPAIR_RSP ); + public static Verb PAXOS2_CLEANUP_START_PREPARE_RSP = new Verb("PAXOS2_CLEANUP_START_PREPARE_RSP", 55, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupHistory.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_CLEANUP_START_PREPARE_REQ = new Verb("PAXOS2_CLEANUP_START_PREPARE_REQ", 45, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosStartPrepareCleanup.serializer, () -> PaxosStartPrepareCleanup.verbHandler, PAXOS2_CLEANUP_START_PREPARE_RSP ); + public static Verb PAXOS2_CLEANUP_RSP = new Verb("PAXOS2_CLEANUP_RSP", 56, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_CLEANUP_REQ = new Verb("PAXOS2_CLEANUP_REQ", 46, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupRequest.serializer, () -> PaxosCleanupRequest.verbHandler, PAXOS2_CLEANUP_RSP ); + public static Verb PAXOS2_CLEANUP_RSP2 = new Verb("PAXOS2_CLEANUP_RSP2", 57, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupResponse.serializer, () -> PaxosCleanupResponse.verbHandler ); + public static Verb PAXOS2_CLEANUP_FINISH_PREPARE_RSP= new Verb("PAXOS2_CLEANUP_FINISH_PREPARE_RSP", 58, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_CLEANUP_FINISH_PREPARE_REQ= new Verb("PAXOS2_CLEANUP_FINISH_PREPARE_REQ", 47, P2, repairTimeout, IMMEDIATE, () -> PaxosCleanupHistory.serializer, () -> PaxosFinishPrepareCleanup.verbHandler, PAXOS2_CLEANUP_FINISH_PREPARE_RSP); + public static Verb PAXOS2_CLEANUP_COMPLETE_RSP = new Verb("PAXOS2_CLEANUP_COMPLETE_RSP", 59, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ); + public static Verb PAXOS2_CLEANUP_COMPLETE_REQ = new Verb("PAXOS2_CLEANUP_COMPLETE_REQ", 48, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupComplete.serializer, () -> PaxosCleanupComplete.verbHandler, PAXOS2_CLEANUP_COMPLETE_RSP); // generic failure response - FAILURE_RSP (99, P0, noTimeout, REQUEST_RESPONSE, () -> RequestFailureReason.serializer, () -> ResponseVerbHandler.instance ), + public static Verb FAILURE_RSP = new Verb("FAILURE_RSP", 99, P0, noTimeout, REQUEST_RESPONSE, () -> RequestFailureReason.serializer, CustomResponseVerbHandlerProvider.instance ); // dummy verbs - _TRACE (30, P1, rpcTimeout, TRACING, () -> NoPayload.serializer, () -> null ), - _SAMPLE (49, P1, rpcTimeout, INTERNAL_RESPONSE, () -> NoPayload.serializer, () -> null ), - _TEST_1 (10, P0, writeTimeout, IMMEDIATE, () -> NoPayload.serializer, () -> null ), - _TEST_2 (11, P1, rpcTimeout, IMMEDIATE, () -> NoPayload.serializer, () -> null ), + public static Verb _TRACE = new Verb("_TRACE", 30, P1, rpcTimeout, TRACING, () -> NoPayload.serializer, () -> null ); + public static Verb _SAMPLE = new Verb("_SAMPLE", 49, P1, rpcTimeout, INTERNAL_RESPONSE, () -> NoPayload.serializer, () -> null ); + public static Verb _TEST_1 = new Verb("_TEST_1", 10, P0, writeTimeout, IMMEDIATE, () -> NoPayload.serializer, () -> null ); + public static Verb _TEST_2 = new Verb("_TEST_2", 11, P1, rpcTimeout, IMMEDIATE, () -> NoPayload.serializer, () -> null ); /** @deprecated See CASSANDRA-15066 */ @Deprecated(since = "4.0") - REQUEST_RSP (4, P1, rpcTimeout, REQUEST_RESPONSE, () -> null, () -> ResponseVerbHandler.instance ), + public static Verb REQUEST_RSP = new Verb("REQUEST_RSP", 4, P1, rpcTimeout, REQUEST_RESPONSE, () -> null, CustomResponseVerbHandlerProvider.instance ); /** @deprecated See CASSANDRA-15066 */ @Deprecated(since = "4.0") - INTERNAL_RSP (23, P1, rpcTimeout, INTERNAL_RESPONSE, () -> null, () -> ResponseVerbHandler.instance ), + public static Verb INTERNAL_RSP = new Verb("INTERNAL_RSP", 23, P1, rpcTimeout, INTERNAL_RESPONSE, () -> null, () -> ResponseVerbHandler.instance ); - // largest used ID: 116 + // largest used public static Verb ID: 116 // CUSTOM VERBS - UNUSED_CUSTOM_VERB (CUSTOM, - 0, P1, rpcTimeout, INTERNAL_RESPONSE, () -> null, () -> null ), - ; - - public static final List VERBS = ImmutableList.copyOf(Verb.values()); + public static Verb UNUSED_CUSTOM_VERB = new Verb("UNUSED_CUSTOM_VERB", CUSTOM,0,P1, rpcTimeout, INTERNAL_RESPONSE, () -> null, () -> null ); public enum Priority { @@ -245,6 +255,7 @@ public enum Kind CUSTOM } + private final String name; public final int id; public final Priority priority; public final Stage stage; @@ -262,38 +273,37 @@ public enum Kind * NOTE: we use a Supplier to avoid loading the dependent classes until necessary. */ private final Supplier> serializer; - private final Supplier> handler; + private Supplier> handler; public final Verb responseVerb; private final ToLongFunction expiration; - /** * Verbs it's okay to drop if the request has been queued longer than the request timeout. These * all correspond to client requests or something triggered by them; we don't want to * drop internal messages like bootstrap or repair notifications. */ - Verb(int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler) + Verb(String name, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler) { - this(id, priority, expiration, stage, serializer, handler, null); + this(name, id, priority, expiration, stage, serializer, handler, null); } - Verb(int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler, Verb responseVerb) + Verb(String name, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler, Verb responseVerb) { - this(NORMAL, id, priority, expiration, stage, serializer, handler, responseVerb); + this(name, NORMAL, id, priority, expiration, stage, serializer, handler, responseVerb); } - Verb(Kind kind, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler) + Verb(String name, Kind kind, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler) { - this(kind, id, priority, expiration, stage, serializer, handler, null); + this(name, kind, id, priority, expiration, stage, serializer, handler, null); } - Verb(Kind kind, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler, Verb responseVerb) + Verb(String name, Kind kind, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler, Verb responseVerb) { this.stage = stage; if (id < 0) - throw new IllegalArgumentException("Verb id must be non-negative, got " + id + " for verb " + name()); + throw new IllegalArgumentException("Verb id must be non-negative, got " + id + " for verb " + name); if (kind == CUSTOM) { @@ -307,12 +317,15 @@ public enum Kind throw new AssertionError("Invalid verb id " + id + " - we only allow ids between 0 and " + (CUSTOM_VERB_START - MAX_CUSTOM_VERB_ID)); this.id = id; } + this.name = name; this.priority = priority; this.serializer = serializer; this.handler = handler; this.responseVerb = responseVerb; this.expiration = expiration; this.kind = kind; + + verbs.add(this); } public IVersionedAsymmetricSerializer serializer() @@ -380,6 +393,17 @@ ToLongFunction unsafeSetExpiration(ToLongFunction expiration return original; } + @Override + public String toString() + { + return name(); + } + + public String name() + { + return name; + } + // This is the largest number we can store in 2 bytes using VIntCoding (1 bit per byte is used to indicate if there is more data coming). // When generating ids we count *down* from this number private static final int CUSTOM_VERB_START = (1 << (7 * 2)) - 1; @@ -389,12 +413,12 @@ ToLongFunction unsafeSetExpiration(ToLongFunction expiration private static final int MAX_CUSTOM_VERB_ID = 1000; private static final Verb[] idToVerbMap; - private static final Verb[] idToCustomVerbMap; - private static final int minCustomId; + private static volatile Verb[] idToCustomVerbMap; + private static volatile int minCustomId; static { - Verb[] verbs = values(); + List verbs = getValues(); int max = -1; int minCustom = Integer.MAX_VALUE; for (Verb v : verbs) @@ -430,8 +454,7 @@ ToLongFunction unsafeSetExpiration(ToLongFunction expiration break; case CUSTOM: int relativeId = idForCustomVerb(v.id); - if (customIdMap[relativeId] != null) - throw new IllegalArgumentException("cannot have two custom verbs that map to the same id: " + v + " and " + customIdMap[relativeId]); + assertCustomIdIsUnused(customIdMap, relativeId, v.name); customIdMap[relativeId] = v; break; default: @@ -443,6 +466,12 @@ ToLongFunction unsafeSetExpiration(ToLongFunction expiration idToCustomVerbMap = customIdMap; } + private static void assertCustomIdIsUnused(Verb[] customIdMap, int id, String name) + { + if (id < customIdMap.length && customIdMap[id] != null) + throw new IllegalArgumentException("cannot have two custom verbs that map to the same id: " + name + " and " + customIdMap[id]); + } + public static Verb fromId(int id) { Verb[] verbs = idToVerbMap; @@ -458,12 +487,82 @@ public static Verb fromId(int id) } /** - * calculate an id for a custom verb + * Convert to/from relative and absolute id for a custom verb. + * + *
    {@code
    +     *          relId = idForCustomVerb(absId)
    +     *          absId = idForCustomVerb(relId).
    +     * }
    + * + *

    Relative ids can be used for indexing idToCustomVerbMap. Absolute ids exist to distinguish + * regular verbs from custom verbs in the id space.

    + * + * @param id the relative or absolute id. + * @return a relative id if {@code id} is absolute, or absolute id if {@code id} is relative. */ private static int idForCustomVerb(int id) { return CUSTOM_VERB_START - id; } + + /** + * Add a new custom verb to the list of verbs. + * + *

    While we could dynamically generate an {@code id} for callers, it's safer to have users + * explicitly control the id space since it prevents nodes with different versions disagreeing on which + * verb has which id, e.g. during upgrade.

    + * + * @param name the name of the new verb. + * @param id the identifier for this custom verb (must be relative id and >= 0 && <= MAX_CUSTOM_VERB_ID). + * @param priority the priority of the new verb. + * @param expiration an optional timeout for this verb. @see VerbTimeouts. + * @param stage The stage this verb should execute in. + * @param serializer A method to serialize this verb + * @param handler A method to handle this verb when received by the network + * @param responseVerb The verb to respond with (optional) + * @return A Verb for the newly added verb. + */ + public static synchronized Verb addCustomVerb(String name, int id, Priority priority, ToLongFunction expiration, Stage stage, Supplier> serializer, Supplier> handler, Verb responseVerb) + { + assertNameIsUnused(name); + assertCustomIdIsUnused(idToCustomVerbMap, id, name); + Verb verb = new Verb(name, CUSTOM, id, priority, expiration, stage, serializer, handler, responseVerb); + + int absoluteId = idForCustomVerb(id); + minCustomId = Math.min(absoluteId, minCustomId); + + Verb[] newMap = Arrays.copyOf(idToCustomVerbMap, CUSTOM_VERB_START - minCustomId + 1); + System.arraycopy(idToCustomVerbMap, 0, newMap, 0, idToCustomVerbMap.length); + newMap[id] = verb; + idToCustomVerbMap = newMap; + + return verb; + } + + // Callers must take care of synchronizing to protect against concurrent updates to verbs. + private static void assertNameIsUnused(String name) + { + if (verbs.stream().map(v -> v.name).collect(Collectors.toList()).contains(name)) + throw new IllegalArgumentException("Verb name '" + name + "' already exists"); + } + + /** + * Decorates the specified verb handler with the provided method. + * + *

    An example use case is to run a custom method after every write request.

    + * + * @param verbs the list of verbs whose handlers should be wrapped by {@code decoratorFn}. + * @param decoratorFn the method that decorates the handlers in verbs. + */ + public static synchronized void decorateHandler(List verbs, Function, IVerbHandler> decoratorFn) + { + for (Verb v : verbs) + { + IVerbHandler handler = v.handler(); + final IVerbHandler decoratedHandler = decoratorFn.apply(handler); + v.handler = () -> decoratedHandler; + } + } } @SuppressWarnings("unused") @@ -471,6 +570,7 @@ class VerbTimeouts { static final ToLongFunction rpcTimeout = DatabaseDescriptor::getRpcTimeout; static final ToLongFunction writeTimeout = DatabaseDescriptor::getWriteRpcTimeout; + static final ToLongFunction hintTimeout = DatabaseDescriptor::getHintsRpcTimeout; static final ToLongFunction readTimeout = DatabaseDescriptor::getReadRpcTimeout; static final ToLongFunction rangeTimeout = DatabaseDescriptor::getRangeRpcTimeout; static final ToLongFunction counterTimeout = DatabaseDescriptor::getCounterWriteRpcTimeout; @@ -491,4 +591,11 @@ class VerbTimeouts return longTimeout.applyAsLong(units); return rpcTimeout.applyAsLong(units); }; + static final ToLongFunction prepareTimeout = DatabaseDescriptor::getRepairPrepareMessageTimeout; + static final ToLongFunction prepareWithBackoffTimeout = units -> { + if (!DatabaseDescriptor.getRepairRetrySpec().isEnabled()) + return prepareTimeout.applyAsLong(units); + return rpcTimeout.applyAsLong(units); + }; + static final ToLongFunction repairMsgTimeout= DatabaseDescriptor::getRepairRpcTimeout; } diff --git a/src/java/org/apache/cassandra/nodes/CC4NodesFileReader.java b/src/java/org/apache/cassandra/nodes/CC4NodesFileReader.java new file mode 100644 index 000000000000..f84005ea5e84 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/CC4NodesFileReader.java @@ -0,0 +1,522 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.nodes; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.CassandraVersion; +import org.msgpack.jackson.dataformat.MessagePackFactory; + +/** + * Reads CC4's file-based node metadata (msgpack format) and converts + * to CC5 {@link LocalInfo} and {@link PeerInfo} objects. + * + * CC4 stores node metadata in file-based MessagePack format at + * {@code {metadata_dir}/nodes/local} and {@code {metadata_dir}/nodes/peers}. + * During a CC4-to-CC5 upgrade, this reader provides a migration path + * by reading those files and producing CC5-compatible objects. + */ +final class CC4NodesFileReader +{ + private static final Logger logger = LoggerFactory.getLogger(CC4NodesFileReader.class); + + private CC4NodesFileReader() + { + } + + /** + * @return true if a CC4 nodes directory exists in the metadata directory + */ + static boolean hasCC4NodesDirectory() + { + return getCC4NodesDirectory() != null; + } + + /** + * Try to read local node info from CC4's file-based store. + * + * @return a CC5 {@link LocalInfo} populated from CC4 data, or null if the CC4 nodes directory or local file doesn't exist + * @throws RuntimeException if the CC4 local file exists but cannot be read or parsed + */ + static LocalInfo tryReadLocalInfo() + { + Path nodesDir = getCC4NodesDirectory(); + if (nodesDir == null) + return null; + + Path localPath = nodesDir.resolve("local"); + Path backupPath = nodesDir.resolve("local.old"); + Path tempPath = nodesDir.resolve("local.txn"); + + maybeRecoverTransaction(localPath, backupPath, tempPath); + + if (!Files.exists(localPath)) + { + logger.debug("CC4 local metadata file not found at {}", localPath); + return null; + } + + try + { + ObjectMapper mapper = createCC4ObjectMapper(); + ObjectReader reader = mapper.readerFor(CC4LocalInfo.class); + + CC4LocalInfo cc4Local; + try (InputStream in = new BufferedInputStream(Files.newInputStream(localPath, StandardOpenOption.READ))) + { + cc4Local = reader.readValue(in); + } + + LocalInfo info = convertLocalInfo(cc4Local); + logger.info("Successfully read CC4 local node metadata from {} (host_id={})", localPath, info.getHostId()); + return info; + } + catch (Exception e) + { + throw new RuntimeException("Failed to read CC4 local metadata from " + localPath + + "; aborting upgrade to prevent generating a new node identity", e); + } + } + + /** + * Try to read peer info from CC4's file-based store. + * + * @return a stream of CC5 {@link PeerInfo} objects, or an empty stream if the CC4 nodes directory or peers file doesn't exist + * @throws RuntimeException if the CC4 peers file exists but cannot be read or parsed + */ + static Stream tryReadPeers() + { + Path nodesDir = getCC4NodesDirectory(); + if (nodesDir == null) + return Stream.empty(); + + Path peersPath = nodesDir.resolve("peers"); + Path backupPath = nodesDir.resolve("peers.old"); + Path tempPath = nodesDir.resolve("peers.txn"); + + maybeRecoverTransaction(peersPath, backupPath, tempPath); + + if (!Files.exists(peersPath)) + { + logger.debug("CC4 peers metadata file not found at {}", peersPath); + return Stream.empty(); + } + + try + { + ObjectMapper mapper = createCC4ObjectMapper(); + ObjectReader reader = mapper.readerFor(new TypeReference>() {}); + + Collection cc4Peers; + try (InputStream in = new BufferedInputStream(Files.newInputStream(peersPath, StandardOpenOption.READ))) + { + cc4Peers = reader.readValue(in); + } + + logger.info("Successfully read {} CC4 peer entries from {}", cc4Peers.size(), peersPath); + return cc4Peers.stream().map(CC4NodesFileReader::convertPeerInfo); + } + catch (Exception e) + { + throw new RuntimeException("Failed to read CC4 peers metadata from " + peersPath + + "; aborting upgrade", e); + } + } + + /** + * Renames the CC4 nodes directory to {@code nodes.migrated} so subsequent restarts + * do not attempt migration again. + */ + static void archiveCC4NodesDirectory() + { + Path nodesDir = getCC4NodesDirectory(); + if (nodesDir == null) + return; + + Path archivedDir = nodesDir.resolveSibling("nodes.migrated"); + try + { + Files.move(nodesDir, archivedDir, StandardCopyOption.ATOMIC_MOVE); + logger.info("Archived CC4 nodes directory from {} to {}", nodesDir, archivedDir); + } + catch (IOException e) + { + logger.warn("Failed to archive CC4 nodes directory from {} to {}; " + + "migration will be attempted again on next restart but is idempotent", + nodesDir, archivedDir, e); + } + } + + private static Path getCC4NodesDirectory() + { + try + { + Path metadataDir = DatabaseDescriptor.getMetadataDirectory().toPath(); + Path nodesDir = metadataDir.resolve("nodes"); + if (!Files.isDirectory(nodesDir)) + { + logger.debug("CC4 nodes directory not found at {}", nodesDir); + return null; + } + return nodesDir; + } + catch (Exception e) + { + logger.debug("Could not determine CC4 nodes directory", e); + return null; + } + } + + /** + * Handle CC4's transactional file recovery: if a .old backup exists, + * it means a transaction was interrupted. Restore from .old. + */ + private static void maybeRecoverTransaction(Path originalPath, Path backupPath, Path tempPath) + { + try + { + if (Files.exists(backupPath)) + { + logger.warn("CC4 backup file {} exists, recovering from interrupted transaction", backupPath); + Files.deleteIfExists(originalPath); + Files.move(backupPath, originalPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + Files.deleteIfExists(tempPath); + } + } + catch (IOException e) + { + throw new RuntimeException("Failed to recover CC4 transaction from " + backupPath + + "; aborting upgrade because node metadata may be in an inconsistent state", e); + } + } + + private static ObjectMapper createCC4ObjectMapper() + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + messagePackFactory.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + ObjectMapper objectMapper = new ObjectMapper(messagePackFactory); // checkstyle: permit this instantiation + objectMapper.registerModule(createMsgpackModule()); + return objectMapper; + } + + /** + * Creates a Jackson module with custom deserializers matching CC4's SerHelper. + * These handle CC4's binary serialization format for UUIDs, InetAddressAndPort, + * Tokens, CassandraVersion, and TruncationRecord. + */ + private static SimpleModule createMsgpackModule() + { + SimpleModule module = new SimpleModule(); + module.addDeserializer(CassandraVersion.class, new CassandraVersionDeserializer()); + module.addDeserializer(Token.class, new TokenDeserializer()); + module.addDeserializer(InetAddressAndPort.class, new InetAddressAndPortDeserializer()); + module.addDeserializer(UUID.class, new UUIDDeserializer()); + module.addDeserializer(CC4TruncationRecord.class, new TruncationRecordDeserializer()); + return module; + } + + // ---- Conversion methods ---- + + private static LocalInfo convertLocalInfo(CC4LocalInfo cc4) + { + LocalInfo info = new LocalInfo(); + // Common fields from NodeInfo + if (cc4.hostId != null) + info.setHostId(cc4.hostId); + if (cc4.dataCenter != null) + info.setDataCenter(cc4.dataCenter); + if (cc4.rack != null) + info.setRack(cc4.rack); + if (cc4.releaseVersion != null) + info.setReleaseVersion(cc4.releaseVersion); + if (cc4.schemaVersion != null) + info.setSchemaVersion(cc4.schemaVersion); + if (cc4.tokens != null && !cc4.tokens.isEmpty()) + info.setTokens(cc4.tokens); + if (cc4.nativeTransportAddressAndPort != null) + info.setNativeTransportAddressAndPort(cc4.nativeTransportAddressAndPort); + + // LocalInfo-specific fields + if (cc4.broadcastAddressAndPort != null) + info.setBroadcastAddressAndPort(cc4.broadcastAddressAndPort); + if (cc4.listenAddressAndPort != null) + info.setListenAddressAndPort(cc4.listenAddressAndPort); + if (cc4.clusterName != null) + info.setClusterName(cc4.clusterName); + if (cc4.bootstrapped != null) + { + try + { + info.setBootstrapState(SystemKeyspace.BootstrapState.valueOf(cc4.bootstrapped.name())); + } + catch (IllegalArgumentException e) + { + logger.warn("Unknown CC4 bootstrap state: {}", cc4.bootstrapped); + } + } + if (cc4.truncatedAt != null && !cc4.truncatedAt.isEmpty()) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : cc4.truncatedAt.entrySet()) + { + CC4TruncationRecord cc4Rec = entry.getValue(); + builder.put(entry.getKey(), new TruncationRecord(cc4Rec.position, cc4Rec.truncatedAt)); + } + info.setTruncationRecords(builder.build()); + } + if (cc4.partitioner != null) + { + try + { + @SuppressWarnings("unchecked") + Class partitionerClass = + (Class) Class.forName(cc4.partitioner); + info.setPartitionerClass(partitionerClass); + } + catch (ClassNotFoundException e) + { + logger.error("Unknown CC4 partitioner class '{}', falling back to configured partitioner '{}'", + cc4.partitioner, DatabaseDescriptor.getPartitioner().getClass().getName()); + info.setPartitionerClass(DatabaseDescriptor.getPartitioner().getClass()); + } + } + return info; + } + + private static PeerInfo convertPeerInfo(CC4PeerInfo cc4) + { + PeerInfo info = new PeerInfo(); + // Common fields from NodeInfo + if (cc4.hostId != null) + info.setHostId(cc4.hostId); + if (cc4.dataCenter != null) + info.setDataCenter(cc4.dataCenter); + if (cc4.rack != null) + info.setRack(cc4.rack); + if (cc4.releaseVersion != null) + info.setReleaseVersion(cc4.releaseVersion); + if (cc4.schemaVersion != null) + info.setSchemaVersion(cc4.schemaVersion); + if (cc4.tokens != null && !cc4.tokens.isEmpty()) + info.setTokens(cc4.tokens); + if (cc4.nativeTransportAddressAndPort != null) + info.setNativeTransportAddressAndPort(cc4.nativeTransportAddressAndPort); + + // PeerInfo-specific fields + if (cc4.peer != null) + info.setPeerAddressAndPort(cc4.peer); + if (cc4.preferred != null) + info.setPreferredAddressAndPort(cc4.preferred); + + return info; + } + + // ---- CC4 deserialization target classes ---- + + @JsonIgnoreProperties(ignoreUnknown = true) + static class CC4LocalInfo + { + @JsonProperty("host_id") + UUID hostId; + @JsonProperty("data_center") + String dataCenter; + @JsonProperty("rack") + String rack; + @JsonProperty("release_version") + CassandraVersion releaseVersion; + @JsonProperty("schema_version") + UUID schemaVersion; + @JsonProperty("tokens") + Collection tokens; + @JsonProperty("native_transport_address_and_port") + InetAddressAndPort nativeTransportAddressAndPort; + @JsonProperty("broadcast_address_and_port") + InetAddressAndPort broadcastAddressAndPort; + @JsonProperty("listen_address_and_port") + InetAddressAndPort listenAddressAndPort; + @JsonProperty("cluster_name") + String clusterName; + @JsonProperty("bootstrapped") + CC4BootstrapState bootstrapped; + @JsonProperty("truncated_at") + Map truncatedAt; + @JsonProperty("partitioner") + String partitioner; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class CC4PeerInfo + { + @JsonProperty("host_id") + UUID hostId; + @JsonProperty("data_center") + String dataCenter; + @JsonProperty("rack") + String rack; + @JsonProperty("release_version") + CassandraVersion releaseVersion; + @JsonProperty("schema_version") + UUID schemaVersion; + @JsonProperty("tokens") + Collection tokens; + @JsonProperty("native_transport_address_and_port") + InetAddressAndPort nativeTransportAddressAndPort; + @JsonProperty("peer") + InetAddressAndPort peer; + @JsonProperty("preferred_ip") + InetAddressAndPort preferred; + + @JsonCreator + CC4PeerInfo(@JsonProperty("peer") InetAddressAndPort peer) + { + this.peer = peer; + } + } + + enum CC4BootstrapState + { + NEEDS_BOOTSTRAP, + COMPLETED, + IN_PROGRESS, + DECOMMISSIONED + } + + static class CC4TruncationRecord + { + final CommitLogPosition position; + final long truncatedAt; + + CC4TruncationRecord(CommitLogPosition position, long truncatedAt) + { + this.position = position; + this.truncatedAt = truncatedAt; + } + } + + // ---- Custom deserializers matching CC4's SerHelper ---- + + private static final class CassandraVersionDeserializer extends JsonDeserializer + { + @Override + public CassandraVersion deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException + { + return new CassandraVersion(jsonParser.getText()); + } + } + + private static final class TokenDeserializer extends JsonDeserializer + { + @Override + public Token deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException + { + return DatabaseDescriptor.getPartitioner().getTokenFactory().fromByteArray(ByteBuffer.wrap(jsonParser.getBinaryValue())); + } + } + + private static final class InetAddressAndPortDeserializer extends JsonDeserializer + { + @Override + public InetAddressAndPort deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException + { + if (jsonParser.isExpectedStartArrayToken()) + { + jsonParser.nextToken(); + InetAddress address = InetAddress.getByAddress(jsonParser.getBinaryValue()); + jsonParser.nextToken(); + int port = jsonParser.getIntValue(); + jsonParser.nextToken(); + return InetAddressAndPort.getByAddressOverrideDefaults(address, port); + } + try + { + return InetAddressAndPort.getByAddress(jsonParser.getBinaryValue()); + } + catch (JsonParseException e) + { + jsonParser.nextToken(); + return InetAddressAndPort.getByName(jsonParser.getText()); + } + } + } + + private static final class UUIDDeserializer extends JsonDeserializer + { + @Override + public UUID deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException + { + jsonParser.isExpectedStartArrayToken(); + jsonParser.nextToken(); + long msb = jsonParser.getLongValue(); + jsonParser.nextToken(); + long lsb = jsonParser.getLongValue(); + jsonParser.nextToken(); + return new UUID(msb, lsb); + } + } + + private static final class TruncationRecordDeserializer extends JsonDeserializer + { + @Override + public CC4TruncationRecord deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException + { + jsonParser.isExpectedStartArrayToken(); + jsonParser.nextToken(); + long truncatedAt = jsonParser.getLongValue(); + jsonParser.nextToken(); + long segmentId = jsonParser.getLongValue(); + jsonParser.nextToken(); + int position = jsonParser.getIntValue(); + jsonParser.nextToken(); + return new CC4TruncationRecord(new CommitLogPosition(segmentId, position), truncatedAt); + } + } +} diff --git a/src/java/org/apache/cassandra/nodes/CC4UpgradeNodesPersistence.java b/src/java/org/apache/cassandra/nodes/CC4UpgradeNodesPersistence.java new file mode 100644 index 000000000000..738ea6efcdea --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/CC4UpgradeNodesPersistence.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link NodesPersistence} subclass that migrates CC4 file-based node metadata + * to CC5 system tables on first load. + * + * This class is instantiated by {@link Nodes} when a CC4 nodes directory is detected. + * It reads CC4's MessagePack metadata via {@link CC4NodesFileReader}, persists the + * migrated data to the system tables, and returns it. + */ +public class CC4UpgradeNodesPersistence extends NodesPersistence +{ + private static final Logger logger = LoggerFactory.getLogger(CC4UpgradeNodesPersistence.class); + + @Override + public LocalInfo loadLocal() + { + LocalInfo migrated = CC4NodesFileReader.tryReadLocalInfo(); + if (migrated != null) + { + logger.info("Migrating local node metadata from CC4 file-based store (host_id={})", migrated.getHostId()); + saveLocal(migrated); + return migrated; + } + logger.debug("No CC4 local metadata found to migrate, falling back to system tables"); + return super.loadLocal(); + } + + @Override + public Stream loadPeers() + { + List migrated = CC4NodesFileReader.tryReadPeers().collect(Collectors.toList()); + if (!migrated.isEmpty()) + { + logger.info("Migrating {} peer entries from CC4 file-based store", migrated.size()); + for (PeerInfo peer : migrated) + savePeer(peer); + } + + CC4NodesFileReader.archiveCC4NodesDirectory(); + + if (migrated.isEmpty()) + { + logger.warn("No CC4 peer metadata found to migrate, falling back to system tables"); + return super.loadPeers(); + } + + return migrated.stream(); + } +} diff --git a/src/java/org/apache/cassandra/nodes/ILocalInfo.java b/src/java/org/apache/cassandra/nodes/ILocalInfo.java new file mode 100644 index 000000000000..1e4d5c2ab116 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/ILocalInfo.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import java.util.UUID; + +import com.google.common.collect.ImmutableMap; + +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.CassandraVersion; + +public interface ILocalInfo extends INodeInfo +{ + InetAddressAndPort getBroadcastAddressAndPort(); + + SystemKeyspace.BootstrapState getBootstrapState(); + + String getClusterName(); + + CassandraVersion getCqlVersion(); + + InetAddressAndPort getListenAddressAndPort(); + + ProtocolVersion getNativeProtocolVersion(); + + Class getPartitionerClass(); + + ImmutableMap getTruncationRecords(); + + @Override + LocalInfo duplicate(); +} diff --git a/src/java/org/apache/cassandra/nodes/INodeInfo.java b/src/java/org/apache/cassandra/nodes/INodeInfo.java new file mode 100644 index 000000000000..7984694690d3 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/INodeInfo.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import java.util.Collection; +import java.util.UUID; +import javax.annotation.Nonnull; + +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.CassandraVersion; + +public interface INodeInfo> extends Cloneable +{ + UUID getHostId(); + + String getDataCenter(); + + String getRack(); + + CassandraVersion getReleaseVersion(); + + UUID getSchemaVersion(); + + @Nonnull + Collection getTokens(); + + InetAddressAndPort getNativeTransportAddressAndPort(); + + T duplicate(); +} diff --git a/src/java/org/apache/cassandra/nodes/INodesPersistence.java b/src/java/org/apache/cassandra/nodes/INodesPersistence.java new file mode 100644 index 000000000000..23fb8a20aaee --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/INodesPersistence.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import java.util.stream.Stream; + +import org.apache.cassandra.locator.InetAddressAndPort; + +public interface INodesPersistence +{ + public static final INodesPersistence NO_NODES_PERSISTENCE = new INodesPersistence() + { + @Override + public LocalInfo loadLocal() + { + return null; + } + + @Override + public void saveLocal(LocalInfo info) + { + // no-op + } + + @Override + public void syncLocal() + { + // no-op + } + + @Override + public Stream loadPeers() + { + return Stream.empty(); + } + + @Override + public void savePeer(PeerInfo info) + { + // no-op + } + + @Override + public void deletePeer(InetAddressAndPort endpoint) + { + // no-op + } + + @Override + public void syncPeers() + { + // no-op + } + }; + + LocalInfo loadLocal(); + + void saveLocal(LocalInfo info); + + void syncLocal(); + + Stream loadPeers(); + + void savePeer(PeerInfo info); + + void deletePeer(InetAddressAndPort endpoint); + + void syncPeers(); +} diff --git a/src/java/org/apache/cassandra/nodes/IPeerInfo.java b/src/java/org/apache/cassandra/nodes/IPeerInfo.java new file mode 100644 index 000000000000..693109581097 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/IPeerInfo.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import org.apache.cassandra.locator.InetAddressAndPort; + +public interface IPeerInfo extends INodeInfo +{ + InetAddressAndPort getPeerAddressAndPort(); + + InetAddressAndPort getPreferredAddressAndPort(); + + boolean isRemoved(); + + boolean isExisting(); + + @Override + PeerInfo duplicate(); +} diff --git a/src/java/org/apache/cassandra/nodes/LocalInfo.java b/src/java/org/apache/cassandra/nodes/LocalInfo.java new file mode 100644 index 000000000000..3839044c8bf8 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/LocalInfo.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.nodes; + +import java.net.InetAddress; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.concurrent.NotThreadSafe; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import org.apache.cassandra.db.SystemKeyspace.BootstrapState; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.ImmutableUtils; + +@NotThreadSafe +public final class LocalInfo extends NodeInfo implements ILocalInfo +{ + private volatile InetAddressAndPort broadcastAddressAndPort; + private volatile BootstrapState bootstrapState; + private volatile String clusterName; + private volatile CassandraVersion cqlVersion; + private volatile InetAddressAndPort listenAddressAndPort; + private volatile ProtocolVersion nativeProtocolVersion; + private volatile Class partitionerClass; + private volatile ImmutableMap truncationRecords = ImmutableMap.of(); + + @Override + public InetAddressAndPort getBroadcastAddressAndPort() + { + return broadcastAddressAndPort; + } + + public LocalInfo setBroadcastAddressAndPort(InetAddressAndPort broadcastAddressAndPort) + { + this.broadcastAddressAndPort = broadcastAddressAndPort; + return this; + } + + @Override + public BootstrapState getBootstrapState() + { + return bootstrapState; + } + + public LocalInfo setBootstrapState(BootstrapState bootstrapState) + { + this.bootstrapState = bootstrapState; + return this; + } + + @Override + public String getClusterName() + { + return clusterName; + } + + public LocalInfo setClusterName(String clusterName) + { + this.clusterName = clusterName; + return this; + } + + @Override + public CassandraVersion getCqlVersion() + { + return cqlVersion; + } + + public LocalInfo setCqlVersion(CassandraVersion cqlVersion) + { + this.cqlVersion = cqlVersion; + return this; + } + + @Override + public InetAddressAndPort getListenAddressAndPort() + { + return listenAddressAndPort; + } + + public LocalInfo setListenAddressAndPort(InetAddressAndPort listenAddressAndPort) + { + this.listenAddressAndPort = listenAddressAndPort; + return this; + } + + public LocalInfo setListenAddressOnly(InetAddress address, int defaultPort) + { + this.listenAddressAndPort = getAddressAndPort(getListenAddressAndPort(), address, defaultPort); + return this; + } + + @Override + public ProtocolVersion getNativeProtocolVersion() + { + return nativeProtocolVersion; + } + + public LocalInfo setNativeProtocolVersion(ProtocolVersion nativeProtocolVersion) + { + this.nativeProtocolVersion = nativeProtocolVersion; + return this; + } + + @Override + public Class getPartitionerClass() + { + return partitionerClass; + } + + public LocalInfo setPartitionerClass(Class partitionerClass) + { + this.partitionerClass = partitionerClass; + return this; + } + + @Override + public ImmutableMap getTruncationRecords() + { + return truncationRecords; + } + + public LocalInfo setTruncationRecords(Map truncationRecords) + { + this.truncationRecords = ImmutableMap.copyOf(truncationRecords); + return this; + } + + public LocalInfo removeTruncationRecord(UUID tableId) + { + return setTruncationRecords(ImmutableUtils.without(getTruncationRecords(), tableId)); + } + + public LocalInfo addTruncationRecord(UUID tableId, TruncationRecord truncationRecord) + { + return setTruncationRecords(ImmutableUtils.withAddedOrUpdated(getTruncationRecords(), tableId, truncationRecord)); + } + + @Override + public LocalInfo duplicate() + { + try + { + return (LocalInfo) clone(); + } + catch (CloneNotSupportedException e) + { + throw new AssertionError(e); + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof LocalInfo)) return false; + if (!super.equals(o)) return false; + LocalInfo localInfo = (LocalInfo) o; + return Objects.equals(getBroadcastAddressAndPort(), localInfo.getBroadcastAddressAndPort()) + && getBootstrapState() == localInfo.getBootstrapState() + && Objects.equals(getClusterName(), localInfo.getClusterName()) + && Objects.equals(getCqlVersion(), localInfo.getCqlVersion()) + && Objects.equals(getListenAddressAndPort(), localInfo.getListenAddressAndPort()) + && Objects.equals(getNativeProtocolVersion(), localInfo.getNativeProtocolVersion()) + && Objects.equals(getPartitionerClass(), localInfo.getPartitionerClass()) + && Objects.equals(getTruncationRecords(), localInfo.getTruncationRecords()); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), + getBroadcastAddressAndPort(), + getBootstrapState(), + getClusterName(), + getCqlVersion(), + getListenAddressAndPort(), + getNativeProtocolVersion(), + getPartitionerClass(), + getTruncationRecords()); + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .appendSuper(super.toString()) + .append("broadcastAddress", getBroadcastAddressAndPort()) + .append("bootstrapState", getBootstrapState()) + .append("clusterName", getClusterName()) + .append("cqlVersion", getCqlVersion()) + .append("listenAddress", getListenAddressAndPort()) + .append("nativeProtocolVersion", getNativeProtocolVersion()) + .append("partitioner", getPartitionerClass()) + .append("truncationRecords", getTruncationRecords()) + .toString(); + } +} diff --git a/src/java/org/apache/cassandra/nodes/NodeInfo.java b/src/java/org/apache/cassandra/nodes/NodeInfo.java new file mode 100644 index 000000000000..6bbe44d8aa92 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/NodeInfo.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.nodes; + +import java.net.InetAddress; +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.CassandraVersion; + +@SuppressWarnings("unchecked") +public abstract class NodeInfo> implements INodeInfo +{ + private volatile UUID hostId; + private volatile String dataCenter; + private volatile String rack; + private volatile CassandraVersion releaseVersion; + private volatile UUID schemaVersion; + + @Nonnull + private volatile ImmutableSet tokens = ImmutableSet.of(); + + private volatile InetAddressAndPort nativeTransportAddressAndPort; + + @Override + public UUID getHostId() + { + return hostId; + } + + public T setHostId(UUID hostId) + { + this.hostId = hostId; + return (T) this; + } + + @Override + public String getDataCenter() + { + return dataCenter; + } + + public T setDataCenter(String dataCenter) + { + this.dataCenter = dataCenter; + return (T) this; + } + + @Override + public String getRack() + { + return rack; + } + + public T setRack(String rack) + { + this.rack = rack; + return (T) this; + } + + @Override + public CassandraVersion getReleaseVersion() + { + return releaseVersion; + } + + public T setReleaseVersion(CassandraVersion releaseVersion) + { + this.releaseVersion = releaseVersion; + return (T) this; + } + + @Override + public UUID getSchemaVersion() + { + return schemaVersion; + } + + public T setSchemaVersion(UUID schemaVersion) + { + this.schemaVersion = schemaVersion; + return (T) this; + } + + @Override + public @Nonnull + Collection getTokens() + { + return tokens; + } + + public T setTokens(@Nonnull Iterable tokens) + { + Preconditions.checkNotNull(tokens); + this.tokens = ImmutableSet.copyOf(tokens); + return (T) this; + } + + @Override + public InetAddressAndPort getNativeTransportAddressAndPort() + { + return nativeTransportAddressAndPort; + } + + public T setNativeTransportAddressAndPort(InetAddressAndPort nativeTransportAddressAndPort) + { + this.nativeTransportAddressAndPort = nativeTransportAddressAndPort; + return (T) this; + } + + public T setNativeTransportAddressOnly(InetAddress address, int defaultPort) + { + this.nativeTransportAddressAndPort = getAddressAndPort(getNativeTransportAddressAndPort(), address, defaultPort); + return (T) this; + } + + InetAddressAndPort getAddressAndPort(InetAddressAndPort current, InetAddress newAddress, int defaultPort) + { + if (newAddress == null) + { + return null; + } + else + { + int port = current != null && current.getPort() > 0 ? current.getPort() : defaultPort; + return InetAddressAndPort.getByAddressOverrideDefaults(newAddress, port); + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof NodeInfo)) return false; + NodeInfo nodeInfo = (NodeInfo) o; + return Objects.equals(getHostId(), nodeInfo.getHostId()) + && Objects.equals(getDataCenter(), nodeInfo.getDataCenter()) + && Objects.equals(getRack(), nodeInfo.getRack()) + && Objects.equals(getReleaseVersion(), nodeInfo.getReleaseVersion()) + && Objects.equals(getSchemaVersion(), nodeInfo.getSchemaVersion()) + && Objects.equals(getTokens(), nodeInfo.getTokens()) + && Objects.equals(getNativeTransportAddressAndPort(), nodeInfo.getNativeTransportAddressAndPort()); + } + + @Override + public int hashCode() + { + return Objects.hash(getHostId(), + getDataCenter(), + getRack(), + getReleaseVersion(), + getSchemaVersion(), + getTokens(), + getNativeTransportAddressAndPort()); + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("hostId", getHostId()) + .append("dataCenter", getDataCenter()) + .append("rack", getRack()) + .append("releaseVersion", getReleaseVersion()) + .append("schemaVersion", getSchemaVersion()) + .append("tokens", getTokens()) + .append("nativeTransportAddress", getNativeTransportAddressAndPort()) + .toString(); + } +} diff --git a/src/java/org/apache/cassandra/nodes/Nodes.java b/src/java/org/apache/cassandra/nodes/Nodes.java new file mode 100644 index 000000000000..b1949efc09e8 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/Nodes.java @@ -0,0 +1,527 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.cliffc.high_scale_lib.NonBlockingHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.NODES_DISABLE_PERSISTING_TO_SYSTEM_KEYSPACE; +import static org.apache.cassandra.config.CassandraRelevantProperties.NODES_PERSISTENCE_CLASS; + +/** + * Provides access and updates the locally stored information about this and other nodes. The information is cached in + * memory in a thread-safe way and additionally stored using the provided implementation of {@link INodesPersistence}, + * which is {@link NodesPersistence} by default and stores everything in system keyspace. + */ +public class Nodes +{ + private static final Logger logger = LoggerFactory.getLogger(Nodes.class); + + @VisibleForTesting + private final ExecutorService updateExecutor; + + private final INodesPersistence nodesPersistence; + + private final Peers peers; + private final Local local; + + private static class InstanceHolder + { + // put into subclass for lazy initialization + private static final Nodes instance = new Nodes(); + } + + /** + * Returns the singleton instance + */ + public static Nodes getInstance() + { + return InstanceHolder.instance; + } + + /** + * Returns the singleton instance of {@link Peers} + */ + public static Peers peers() + { + return getInstance().getPeers(); + } + + /** + * Returns the singleton instance of {@link Local} + */ + public static Local local() + { + return getInstance().getLocal(); + } + + /** + * Returns information about the node with the given address - if the node address matches the local (broadcast) + * address, the returned object is a {@link LocalInfo}. Otherwise, it is {@link PeerInfo} (or {@code null} if no + * informatino is available). + */ + @Nullable + public static INodeInfo localOrPeerInfo(InetAddressAndPort endpoint) + { + return Objects.equals(endpoint, FBUtilities.getBroadcastAddressAndPort()) ? local().get() : peers().get(endpoint); + } + + public static Optional> localOrPeerInfoOpt(InetAddressAndPort endpoint) + { + return Optional.ofNullable(localOrPeerInfo(endpoint)); + } + + /** + * @see #updateLocalOrPeer(InetAddressAndPort, UnaryOperator, boolean, boolean) + */ + public static INodeInfo updateLocalOrPeer(InetAddressAndPort endpoint, UnaryOperator> update) + { + return updateLocalOrPeer(endpoint, update, false); + } + + /** + * @see #updateLocalOrPeer(InetAddressAndPort, UnaryOperator, boolean, boolean) + */ + public static INodeInfo updateLocalOrPeer(InetAddressAndPort endpoint, UnaryOperator> update, boolean blocking) + { + return updateLocalOrPeer(endpoint, update, blocking, false); + } + + /** + * Updates either local or peer information in a thread-safe way, depeending on whether the provided address matches + * the local (broadcast) address. + * + * @see Local#updateLocalOrPeer(InetAddressAndPort, UnaryOperator, boolean, boolean) + * @see Peers#updateLocalOrPeer(InetAddressAndPort, UnaryOperator, boolean, boolean) + */ + public static INodeInfo updateLocalOrPeer(InetAddressAndPort endpoint, UnaryOperator> update, boolean blocking, boolean force) + { + if (Objects.equals(endpoint, FBUtilities.getBroadcastAddressAndPort())) + return local().update(info -> (LocalInfo) update.apply(info), blocking, force); + else + return peers().update(endpoint, info -> (PeerInfo) update.apply(info), blocking, force); + } + + public void forcePersist() + { + local().forcePersist(); + peers().forcePersist(); + } + + /** + * Checks whether the provided address is local or known peer address. + */ + public static boolean isKnownEndpoint(InetAddressAndPort endpoint) + { + return localOrPeerInfo(endpoint) != null; + } + + public static UUID getHostId(InetAddressAndPort endpoint, UUID defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getHostId() : defaultValue; + } + + public static String getDataCenter(InetAddressAndPort endpoint, String defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getDataCenter() : defaultValue; + } + + public static String getRack(InetAddressAndPort endpoint, String defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getRack() : defaultValue; + } + + public static CassandraVersion getReleaseVersion(InetAddressAndPort endpoint, CassandraVersion defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getReleaseVersion() : defaultValue; + } + + public static UUID getSchemaVersion(InetAddressAndPort endpoint, UUID defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getSchemaVersion() : defaultValue; + } + + public static Collection getTokens(InetAddressAndPort endpoint, Collection defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getTokens() : defaultValue; + } + + public static InetAddressAndPort getNativeTransportAddressAndPort(InetAddressAndPort endpoint, InetAddressAndPort defaultValue) + { + INodeInfo info = localOrPeerInfo(endpoint); + return info != null ? info.getNativeTransportAddressAndPort() : defaultValue; + } + + /** + * Initializes singleton instance of {@link Nodes}. If it is not a Cassandra server process or + * {@code cassandra.nodes.disablePersitingToSystemKeyspace} is set to {@code true}, the instance does not persist + * stored information anywhere. + */ + private Nodes() + { + this(createNodesPersistence(), ExecutorFactory.Global.executorFactory().sequential("nodes-info-persistence")); + } + + private static INodesPersistence createNodesPersistence() + { + String nodesPersistenceClassName = NODES_PERSISTENCE_CLASS.getString(); + if (nodesPersistenceClassName != null) + { + try + { + return FBUtilities.instanceOrConstruct(nodesPersistenceClassName, "INodesPersistence implementation (" + NODES_PERSISTENCE_CLASS.getKey() + ")"); + } + catch (Exception e) + { + throw new RuntimeException("Failed to instantiate " + nodesPersistenceClassName, e); + } + } + if (!DatabaseDescriptor.isDaemonInitialized() || NODES_DISABLE_PERSISTING_TO_SYSTEM_KEYSPACE.getBoolean()) + return INodesPersistence.NO_NODES_PERSISTENCE; + if (CC4NodesFileReader.hasCC4NodesDirectory()) + return new CC4UpgradeNodesPersistence(); + return new NodesPersistence(); + } + + @VisibleForTesting + public Nodes(INodesPersistence nodesPersistence, ExecutorService updateExecutor) + { + this.updateExecutor = updateExecutor; + this.nodesPersistence = nodesPersistence; + this.local = new Local().load(); + this.peers = new Peers().load(); + } + + public void reload() + { + this.local.load(); + this.peers.load(); + } + + public Peers getPeers() + { + return peers; + } + + public Local getLocal() + { + return local; + } + + /** + * Wait for all in-flight updates to complete. Only used for testing to coordinate with other unsafe operations. + */ + @VisibleForTesting + public void awaitInflightUpdateCompletion() + { + local.awaitInflightUpdateCompletion(); + peers.awaitInflightUpdateCompletion(); + } + + private Runnable wrapPersistenceTask(String name, Runnable task) + { + return () -> { + try + { + task.run(); + } + catch (RuntimeException ex) + { + logger.error("Unexpected exception - " + name, ex); + throw ex; + } + }; + } + + public class Peers + { + private final NonBlockingHashMap internalMap = new NonBlockingHashMap<>(); + + public void forcePersist() + { + for (PeerInfo info : internalMap.values()) + { + internalMap.computeIfPresent(info.getPeerAddressAndPort(), (key, existingPeerInfo) -> { + if (existingPeerInfo.isExisting()) + save(existingPeerInfo, existingPeerInfo, true, true); + else + delete(info.getPeerAddressAndPort(), true); + return existingPeerInfo; + }); + } + } + + public IPeerInfo update(InetAddressAndPort peer, UnaryOperator update) + { + return update(peer, update, false); + } + + public IPeerInfo update(InetAddressAndPort peer, UnaryOperator update, boolean blocking) + { + return update(peer, update, blocking, false); + } + + /** + * Updates peer information in a thread-safe way. + * + * @param peer address of a peer to be updated + * @param update update function, which receives a copy of the current {@link PeerInfo} and is expected to + * return the updated {@link PeerInfo}; the function may apply updates directly on the received + * copy and return it + * @param blocking if set, the method will block until the changes are persisted + * @param force the update will be persisted even if no changes are made + * @return the updated object + */ + public IPeerInfo update(InetAddressAndPort peer, UnaryOperator update, boolean blocking, boolean force) + { + return internalMap.compute(peer, (key, existingPeerInfo) -> { + PeerInfo updated = existingPeerInfo == null + ? update.apply(new PeerInfo().setPeerAddressAndPort(peer)) + : update.apply(existingPeerInfo.duplicate()); // since we operate on mutable objects, we don't want to let the update function to operate on the live object + + if (updated.getPeerAddressAndPort() == null) + updated.setPeerAddressAndPort(peer); + else + Preconditions.checkArgument(Objects.equals(updated.getPeerAddressAndPort(), peer)); + + updated.setRemoved(false); + save(existingPeerInfo, updated, blocking, force); + return updated; + }); + } + + /** + * @param peer peer to remove + * @param blocking block until the removal is persisted and synced + * @param hard remove also the transient state instead of just setting {@link PeerInfo#isRemoved()} state + * @return the remove + */ + public IPeerInfo remove(InetAddressAndPort peer, boolean blocking, boolean hard) + { + AtomicReference removed = new AtomicReference<>(); + internalMap.computeIfPresent(peer, (key, existingPeerInfo) -> { + delete(peer, blocking); + existingPeerInfo.setRemoved(true); + removed.set(existingPeerInfo); + return hard ? null : existingPeerInfo; + }); + return removed.get(); + } + + /** + * Returns a peer information for a given address if the peer is known. Otherwise, returns {@code null}. + * Note that you should never try to manually cast the returned object to a mutable instnace and modify it. + */ + @Nullable + public IPeerInfo get(InetAddressAndPort peer) + { + return internalMap.get(peer); + } + + /** + * Returns optional of a peer information for a given address. + * Note that you should never try to manually cast the returned object to a mutable instnace and modify it. + */ + public Optional getOpt(InetAddressAndPort peer) + { + return Optional.ofNullable(get(peer)); + } + + /** + * Returns a stream of all known peers. + * Note that you should never try to manually cast the returned objects to a mutable instnaces and modify it. + */ + public Stream get() + { + return internalMap.values().stream().map(IPeerInfo.class::cast); + } + + private void save(PeerInfo previousInfo, PeerInfo newInfo, boolean blocking, boolean force) + { + if (!force && Objects.equals(previousInfo, newInfo)) + { + logger.trace("Saving peer skipped: {}", previousInfo); + return; + } + + logger.trace("Saving peer: {}, blocking = {}, force = {}", newInfo, blocking, force); + Future f = updateExecutor.submit(wrapPersistenceTask("saving peer information: " + newInfo, () -> { + nodesPersistence.savePeer(newInfo); + logger.trace("Saved peer: {}", newInfo); + if (blocking) + nodesPersistence.syncPeers(); + })); + if (blocking) + FBUtilities.waitOnFuture(f); + } + + private void awaitInflightUpdateCompletion() + { + FBUtilities.waitOnFuture(updateExecutor.submit(() -> {})); + } + + private Peers load() + { + logger.trace("Loading peers..."); + nodesPersistence.loadPeers().forEach(info -> internalMap.compute(info.getPeerAddressAndPort(), (key, existingPeerInfo) -> info)); + if (logger.isTraceEnabled()) + logger.trace("Loaded peers: {}", internalMap.values().stream().collect(Collectors.toList())); + return this; + } + + private void delete(InetAddressAndPort peer, boolean blocking) + { + if (logger.isTraceEnabled()) + logger.trace("Deleting peer " + peer + ", blocking = " + blocking, new Throwable()); + Future f = updateExecutor.submit(wrapPersistenceTask("deleting peer information: " + peer, () -> { + nodesPersistence.deletePeer(peer); + logger.trace("Deleted peer {}", peer); + if (blocking) + nodesPersistence.syncPeers(); + })); + + if (blocking) + FBUtilities.waitOnFuture(f); + } + } + + public class Local + { + private final NonBlockingHashMap internalMap = new NonBlockingHashMap<>(); + private final InetAddressAndPort localInfoKey = InetAddressAndPort.getLoopbackAddress(); + + public void forcePersist() + { + internalMap.computeIfPresent(localInfoKey, (key, existingLocalInfo) -> { + save(existingLocalInfo, existingLocalInfo, true, true); + return existingLocalInfo; + }); + } + + /** + * @see #update(UnaryOperator, boolean, boolean) + */ + public ILocalInfo update(UnaryOperator update) + { + return update(update, false); + } + + /** + * @see #update(UnaryOperator, boolean, boolean) + */ + public ILocalInfo update(UnaryOperator update, boolean blocking) + { + return update(update, blocking, false); + } + + /** + * Updates local node information in a thread-safe way. + * + * @param update update function, which receives a copy of the current {@link LocalInfo} and is expected to + * return the updated {@link LocalInfo}; the function may apply updates directly on the received + * copy and return it + * @param blocking if set, the method will block until the changes are persisted + * @param force the update will be persisted even if no changes are made + * @return a copy of updated object + */ + public ILocalInfo update(UnaryOperator update, boolean blocking, boolean force) + { + return internalMap.compute(localInfoKey, (key, existingLocalInfo) -> { + LocalInfo updated = existingLocalInfo == null + ? update.apply(new LocalInfo()) + : update.apply(existingLocalInfo.duplicate()); // since we operate on mutable objects, we don't want to let the update function to operate on the live object + save(existingLocalInfo, updated, blocking, force); + return updated; + }); + } + + /** + * Returns information about the local node (if present). + * Note that you should never try to manually cast the returned object to a mutable instnace and modify it. + */ + public ILocalInfo get() + { + return internalMap.get(localInfoKey); + } + + private void save(LocalInfo previousInfo, LocalInfo newInfo, boolean blocking, boolean force) + { + if (!force && Objects.equals(previousInfo, newInfo)) + { + logger.trace("Saving local skipped: {}", previousInfo); + return; + } + + Future f = updateExecutor.submit(wrapPersistenceTask("saving local node information: " + newInfo, () -> { + nodesPersistence.saveLocal(newInfo); + logger.trace("Saving local: {}, blocking = {}, force = {}", newInfo, blocking, force); + if (blocking) + nodesPersistence.syncLocal(); + })); + + if (blocking) + FBUtilities.waitOnFuture(f); + } + + private void awaitInflightUpdateCompletion() + { + FBUtilities.waitOnFuture(updateExecutor.submit(() -> {})); + } + + private Local load() + { + logger.trace("Loading local..."); + internalMap.compute(localInfoKey, (key, existingLocalInfo) -> { + LocalInfo info = nodesPersistence.loadLocal(); + return info != null ? info : new LocalInfo(); + }); + if (logger.isTraceEnabled()) + logger.trace("Loaded local: {}", internalMap.get(localInfoKey)); + return this; + } + } +} diff --git a/src/java/org/apache/cassandra/nodes/NodesPersistence.java b/src/java/org/apache/cassandra/nodes/NodesPersistence.java new file mode 100644 index 000000000000..a49b326a81b6 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/NodesPersistence.java @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.nodes; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.RejectedExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.cql3.UntypedResultSet.Row; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.io.util.RebufferingInputStream; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.Throwables; + +import static java.lang.String.format; +import static org.apache.cassandra.db.SystemKeyspace.LEGACY_PEERS; +import static org.apache.cassandra.db.SystemKeyspace.LOCAL; +import static org.apache.cassandra.db.SystemKeyspace.PEERS_V2; +import static org.apache.cassandra.db.SystemKeyspace.forceBlockingFlush; + +public class NodesPersistence implements INodesPersistence +{ + private static final Logger logger = LoggerFactory.getLogger(NodesPersistence.class); + + private static final List COMMON_COLUMNS = ImmutableList.of("data_center", + "rack", + "host_id", + "release_version", + "schema_version", + "tokens"); + + private static final String INSERT_LOCAL_STMT = format("INSERT INTO system.%s (" + + " key, " + + " bootstrapped, " + + " broadcast_address, " + + " broadcast_port, " + + " cluster_name, " + + " cql_version, " + + " listen_address, " + + " listen_port, " + + " native_protocol_version, " + + " partitioner, " + + " rpc_address, " + + " rpc_port, " + + " truncated_at, " + + " %s) " + + "VALUES (%s)", LOCAL, + StringUtils.join(COMMON_COLUMNS, ", "), + StringUtils.repeat("?", ", ", 13 + COMMON_COLUMNS.size())); + + private static final String INSERT_PEER_STMT = format("INSERT INTO system.%s (" + + " peer, " + + " peer_port, " + + " preferred_ip, " + + " preferred_port, " + + " native_address, " + + " native_port, " + + " %s) " + + "VALUES (%s)", PEERS_V2, + StringUtils.join(COMMON_COLUMNS, ", "), + StringUtils.repeat("?", ", ", 6 + COMMON_COLUMNS.size())); + + private static final String INSERT_LEGACY_PEER_STMT = format("INSERT INTO system.%s (" + + " peer, " + + " preferred_ip, " + + " rpc_address, " + + " %s) " + + "VALUES (%s)", LEGACY_PEERS, + StringUtils.join(COMMON_COLUMNS, ", "), + StringUtils.repeat("?", ", ", 3 + COMMON_COLUMNS.size())); + + private static final String DELETE_PEER_STMT = format("DELETE FROM system.%s " + + "WHERE peer = ? AND peer_port = ?", PEERS_V2); + + private static final String DELETE_LEGACY_PEER_STMT = format("DELETE FROM system.%s " + + "WHERE peer = ?", LEGACY_PEERS); + + + @Override + public LocalInfo loadLocal() + { + UntypedResultSet results = QueryProcessor.executeInternal("SELECT * FROM system." + LOCAL + " WHERE key = ?", LOCAL); + if (results == null || results.isEmpty()) + return null; + Row row = results.one(); + LocalInfo info = readCommonInfo(new LocalInfo(), row); + info.setBroadcastAddressAndPort(readInetAddressAndPort(row, "broadcast_address", "broadcast_port", DatabaseDescriptor.getStoragePort())) + .setListenAddressAndPort(readInetAddressAndPort(row, "listen_address", "listen_port", DatabaseDescriptor.getStoragePort())) + .setNativeTransportAddressAndPort(readInetAddressAndPort(row, "rpc_address", "rpc_port", DatabaseDescriptor.getNativeTransportPort())) + .setBootstrapState(readBootstrapState(row, "bootstrapped")) + .setClusterName(row.has("cluster_name") ? row.getString("cluster_name") : null) + .setCqlVersion(readCassandraVersion(row, "cql_version")) + .setNativeProtocolVersion(readNativeProtocol(row, "native_protocol_version")) + .setPartitionerClass(readPartitionerClass(row, "partitioner")) + .setTruncationRecords(readTruncationRecords(row, "truncated_at")); + return info; + } + + @Override + public void saveLocal(LocalInfo info) + { + Object[] values = ArrayUtils.addAll(new Object[]{ "local", + info.getBootstrapState() != null ? info.getBootstrapState().name() : null, + serializeAddress(info.getBroadcastAddressAndPort()), + serializePort(info.getBroadcastAddressAndPort()), + info.getClusterName(), + serializeCassandraVersion(info.getCqlVersion()), + serializeAddress(info.getListenAddressAndPort()), + serializePort(info.getListenAddressAndPort()), + serializeProtocolVersion(info.getNativeProtocolVersion()), + info.getPartitionerClass() != null ? info.getPartitionerClass().getName() : null, + serializeAddress(info.getNativeTransportAddressAndPort()), + serializePort(info.getNativeTransportAddressAndPort()), + serializeTruncationRecords(info.getTruncationRecords()) }, serializeCommonInfo(info)); + + QueryProcessor.executeInternal(INSERT_LOCAL_STMT, values); + } + + private String serializeProtocolVersion(ProtocolVersion protocolVersion) + { + return protocolVersion == null ? null : String.valueOf(protocolVersion.asInt()); + } + + @Override + public void syncLocal() + { + try + { + forceBlockingFlush(LOCAL); + } + catch (RejectedExecutionException ex) + { + logger.warn("Could not flush peers table because the thread pool has shut down", ex); + } + } + + @Override + public Stream loadPeers() + { + UntypedResultSet results = QueryProcessor.executeInternal("SELECT * FROM system." + PEERS_V2); + if (results == null || results.isEmpty()) + return Stream.empty(); + return StreamSupport.stream(results.spliterator(), false).map(row -> { + PeerInfo info = readCommonInfo(new PeerInfo(), row); + info.setPeerAddressAndPort(readInetAddressAndPort(row, "peer", "peer_port", DatabaseDescriptor.getStoragePort())) + .setPreferredAddressAndPort(readInetAddressAndPort(row, "preferred_ip", "preferred_port", DatabaseDescriptor.getStoragePort())) + .setNativeTransportAddressAndPort(readInetAddressAndPort(row, "native_address", "native_port", DatabaseDescriptor.getNativeTransportPort())); + return info; + }); + } + + @Override + public void savePeer(PeerInfo info) + { + Object[] peersValues = ArrayUtils.addAll(new Object[]{ serializeAddress(info.getPeerAddressAndPort()), + serializePort(info.getPeerAddressAndPort()), + serializeAddress(info.getPreferredAddressAndPort()), + serializePort(info.getPreferredAddressAndPort()), + serializeAddress(info.getNativeTransportAddressAndPort()), + serializePort(info.getNativeTransportAddressAndPort()), + }, serializeCommonInfo(info)); + QueryProcessor.executeInternal(INSERT_PEER_STMT, peersValues); + + Object[] legacyPeersValues = ArrayUtils.addAll(new Object[]{ serializeAddress(info.getPeerAddressAndPort()), + serializeAddress(info.getPreferredAddressAndPort()), + serializeAddress(info.getNativeTransportAddressAndPort()), + }, serializeCommonInfo(info)); + + QueryProcessor.executeInternal(INSERT_LEGACY_PEER_STMT, legacyPeersValues); + } + + @Override + public void deletePeer(InetAddressAndPort endpoint) + { + QueryProcessor.executeInternal(DELETE_PEER_STMT, serializeAddress(endpoint), serializePort(endpoint)); + QueryProcessor.executeInternal(DELETE_LEGACY_PEER_STMT, serializeAddress(endpoint)); + } + + @Override + public void syncPeers() + { + try + { + forceBlockingFlush(LEGACY_PEERS, PEERS_V2); + } + catch (RejectedExecutionException ex) + { + logger.warn("Could not flush peers table because the thread pool has shut down", ex); + } + } + + private > T readCommonInfo(T info, Row row) + { + info.setDataCenter(row.has("data_center") ? row.getString("data_center") : null) + .setRack(row.has("rack") ? row.getString("rack") : null) + .setHostId(row.has("host_id") ? row.getUUID("host_id") : null) + .setReleaseVersion(readCassandraVersion(row, "release_version")) + .setSchemaVersion(row.has("schema_version") ? row.getUUID("schema_version") : null) + .setTokens(readTokens(row, "tokens")); + return info; + } + + private InetAddressAndPort readInetAddressAndPort(Row row, String addressCol, String portCol, int defaultPort) + { + InetAddress address = row.has(addressCol) ? row.getInetAddress(addressCol) : null; + if (address == null) + return null; + int port = row.has(portCol) ? row.getInt(portCol) : defaultPort; + return InetAddressAndPort.getByAddressOverrideDefaults(address, port); + } + + private CassandraVersion readCassandraVersion(Row row, String col) + { + String v = row.has(col) ? row.getString(col) : null; + if (v == null) + return null; + return new CassandraVersion(v); + } + + private Collection readTokens(Row row, String col) + { + Set tokensStrings = row.has(col) ? row.getSet(col, UTF8Type.instance) : new HashSet<>(); + Token.TokenFactory factory = DatabaseDescriptor.getPartitioner().getTokenFactory(); + List tokens = new ArrayList<>(tokensStrings.size()); + for (String tk : tokensStrings) + tokens.add(factory.fromString(tk)); + return tokens; + } + + private SystemKeyspace.BootstrapState readBootstrapState(Row row, String col) + { + String s = row.has(col) ? row.getString(col) : null; + if (s == null) + return null; + + return SystemKeyspace.BootstrapState.valueOf(s); + } + + @SuppressWarnings("unchecked") + private Class readPartitionerClass(Row row, String col) + { + String s = row.has(col) ? row.getString(col) : null; + if (s == null) + return null; + + try + { + return (Class) Class.forName(s); + } + catch (ClassNotFoundException e) + { + throw Throwables.unchecked(e); + } + } + + private ProtocolVersion readNativeProtocol(Row row, String col) + { + String s = row.has(col) ? row.getString(col) : null; + if (s == null) + return null; + + return ProtocolVersion.decode(Integer.parseInt(s), true); + } + + private Map readTruncationRecords(Row row, String col) + { + Map raw = row.has(col) ? row.getMap(col, UUIDType.instance, BytesType.instance) : ImmutableMap.of(); + if (raw == null) + return null; + + return raw.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> truncationRecordFromBlob(e.getValue()))); + } + + private TruncationRecord truncationRecordFromBlob(ByteBuffer bytes) + { + try (RebufferingInputStream in = new DataInputBuffer(bytes, true)) + { + return new TruncationRecord(CommitLogPosition.serializer.deserialize(in), in.available() > 0 ? in.readLong() : Long.MIN_VALUE); + } + catch (IOException e) + { + throw Throwables.unchecked(e); + } + } + + public static int serializePort(InetAddressAndPort addressAndPort) + { + return addressAndPort != null ? addressAndPort.getPort() : -1; + } + + public static InetAddress serializeAddress(InetAddressAndPort addressAndPort) + { + return addressAndPort != null ? addressAndPort.getAddress() : null; + } + + public static String serializeCassandraVersion(CassandraVersion version) + { + return version != null ? version.toString() : null; + } + + public static Set serializeTokens(Collection tokens) + { + if (tokens.isEmpty()) + return Collections.emptySet(); + Set s = new HashSet<>(tokens.size()); + for (Token tk : tokens) + s.add(tk.getPartitioner().getTokenFactory().toString(tk)); + return s; + } + + private Object[] serializeCommonInfo(NodeInfo info) + { + return new Object[]{ info.getDataCenter(), + info.getRack(), + info.getHostId(), + serializeCassandraVersion(info.getReleaseVersion()), + info.getSchemaVersion(), + serializeTokens(info.getTokens()) }; + } + + public static ByteBuffer serializeTruncationRecord(TruncationRecord truncationRecord) + { + try (DataOutputBuffer out = DataOutputBuffer.scratchBuffer.get()) + { + CommitLogPosition.serializer.serialize(truncationRecord.position, out); + out.writeLong(truncationRecord.truncatedAt); + return out.asNewBuffer(); + } + catch (IOException e) + { + throw Throwables.unchecked(e); + } + } + + public static Map serializeTruncationRecords(Map truncationRecords) + { + return truncationRecords.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> serializeTruncationRecord(e.getValue()))); + } +} diff --git a/src/java/org/apache/cassandra/nodes/PeerInfo.java b/src/java/org/apache/cassandra/nodes/PeerInfo.java new file mode 100644 index 000000000000..576246c5df37 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/PeerInfo.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.nodes; + +import java.util.Objects; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +import org.apache.cassandra.locator.InetAddressAndPort; + +public final class PeerInfo extends NodeInfo implements IPeerInfo +{ + private volatile InetAddressAndPort peerAddressAndPort; + private volatile InetAddressAndPort preferredAddressAndPort; + private volatile boolean removed; + + @Override + public InetAddressAndPort getPeerAddressAndPort() + { + return peerAddressAndPort; + } + + public PeerInfo setPeerAddressAndPort(InetAddressAndPort peerAddressAndPort) + { + this.peerAddressAndPort = peerAddressAndPort; + return this; + } + + @Override + public InetAddressAndPort getPreferredAddressAndPort() + { + return preferredAddressAndPort; + } + + public PeerInfo setPreferredAddressAndPort(InetAddressAndPort preferredAddressAndPort) + { + this.preferredAddressAndPort = preferredAddressAndPort; + return this; + } + + @Override + public boolean isRemoved() + { + return removed; + } + + @Override + public boolean isExisting() + { + return !isRemoved(); + } + + PeerInfo setRemoved(boolean removed) + { + this.removed = removed; + return this; + } + + @Override + public PeerInfo duplicate() + { + try + { + return (PeerInfo) clone(); + } + catch (CloneNotSupportedException e) + { + throw new AssertionError(e); + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof PeerInfo)) return false; + if (!super.equals(o)) return false; + PeerInfo peerInfo = (PeerInfo) o; + return isRemoved() == peerInfo.isRemoved() + && Objects.equals(getPeerAddressAndPort(), peerInfo.getPeerAddressAndPort()) + && Objects.equals(getPreferredAddressAndPort(), peerInfo.getPreferredAddressAndPort()); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), + getPeerAddressAndPort(), + getPreferredAddressAndPort(), + isRemoved()); + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .appendSuper(super.toString()) + .append("peer", getPeerAddressAndPort()) + .append("preferredIp", getPreferredAddressAndPort()) + .append("isRemoved", isRemoved()) + .toString(); + } +} diff --git a/src/java/org/apache/cassandra/nodes/TruncationRecord.java b/src/java/org/apache/cassandra/nodes/TruncationRecord.java new file mode 100644 index 000000000000..eb98159027d7 --- /dev/null +++ b/src/java/org/apache/cassandra/nodes/TruncationRecord.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.nodes; + +import java.util.Objects; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +import org.apache.cassandra.db.commitlog.CommitLogPosition; + +public final class TruncationRecord +{ + public final CommitLogPosition position; + public final long truncatedAt; + + public TruncationRecord(CommitLogPosition position, long truncatedAt) + { + this.position = position; + this.truncatedAt = truncatedAt; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TruncationRecord that = (TruncationRecord) o; + return truncatedAt == that.truncatedAt && + Objects.equals(position, that.position); + } + + @Override + public int hashCode() + { + return Objects.hash(position, truncatedAt); + } + + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("position", position) + .append("truncatedAt", truncatedAt) + .toString(); + } +} diff --git a/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java b/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java index 857af698473d..833bc51cfde5 100644 --- a/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java +++ b/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java @@ -21,8 +21,10 @@ import javax.annotation.Nullable; +import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; /** * Notification sent after SSTables are added to their {@link org.apache.cassandra.db.ColumnFamilyStore}. @@ -36,17 +38,39 @@ public class SSTableAddedNotification implements INotification @Nullable private final Memtable memtable; + /** The type of operation that created the sstables */ + public final OperationType operationType; + + /** The id of the operation that created the sstables, if available */ + public final Optional operationId; + /** - * Creates a new {@code SSTableAddedNotification} for the specified SSTables and optional memtable. + * Creates a new {@code SSTableAddedNotification} for the specified SSTables and optional memtable using + * an unknown operation type. * * @param added the added SSTables * @param memtable the memtable from which the tables come when they have been added due to a memtable flush, * or {@code null} if they don't come from a flush */ public SSTableAddedNotification(Iterable added, @Nullable Memtable memtable) + { + this(added, memtable, OperationType.UNKNOWN, Optional.empty()); + } + + /** + * Creates a new {@code SSTableAddedNotification} for the specified SSTables and optional memtable. + * + * @param added the added SSTables + * @param memtable the memtable from which the tables come when they have been added due to a memtable flush, + * or {@code null} if they don't come from a flush + * @param operationType the type of operation that created the sstables + */ + public SSTableAddedNotification(Iterable added, @Nullable Memtable memtable, OperationType operationType, Optional operationId) { this.added = added; this.memtable = memtable; + this.operationType = operationType; + this.operationId = operationId; } /** @@ -59,4 +83,14 @@ public Optional memtable() { return Optional.ofNullable(memtable); } + + /** + * @return true if curent notification is due to streaming sstables + */ + public boolean fromStreaming() + { + return operationType == OperationType.STREAM + || operationType == OperationType.REGION_DECOMMISSION + || operationType == OperationType.REGION_REPAIR; + } } diff --git a/src/java/org/apache/cassandra/notifications/SSTableAddingNotification.java b/src/java/org/apache/cassandra/notifications/SSTableAddingNotification.java new file mode 100644 index 000000000000..fe56ff57dfe3 --- /dev/null +++ b/src/java/org/apache/cassandra/notifications/SSTableAddingNotification.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.notifications; + +import java.util.Optional; +import javax.annotation.Nullable; + +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; + +/** + * Notification sent before SSTables are added to their {@link org.apache.cassandra.db.ColumnFamilyStore}. + */ +public class SSTableAddingNotification implements INotification +{ + /** The SSTables to be added*/ + public final Iterable adding; + + /** The memtable from which the sstables come when they need to be added due to a flush, {@code null} otherwise. */ + @Nullable + private final Memtable memtable; + + /** The type of operation that created the sstables */ + public final OperationType operationType; + + /** The id of the operation that created the sstables, if available */ + public final Optional operationId; + + /** + * Creates a new {@code SSTableAddingNotification} for the specified SSTables and optional memtable. + * + * @param adding the SSTables to be added + * @param memtable the memtable from which the sstables come when they need to be added due to a memtable flush, + * or {@code null} if they don't come from a flush + * @param operationType the type of operation that created the sstables + * @param operationId the id of the operation (transaction) that created the sstables, or empty if no id is available + */ + public SSTableAddingNotification(Iterable adding, @Nullable Memtable memtable, OperationType operationType, Optional operationId) + { + this.adding = adding; + this.memtable = memtable; + this.operationType = operationType; + this.operationId = operationId; + } + + /** + * Returns the memtable from which the sstables come when they need to be addeddue to a memtable flush. If not, an + * empty Optional should be returned. + * + * @return the origin memtable in case of a flush, {@link Optional#empty()} otherwise + */ + public Optional memtable() + { + return Optional.ofNullable(memtable); + } +} diff --git a/src/java/org/apache/cassandra/notifications/SSTableListChangedNotification.java b/src/java/org/apache/cassandra/notifications/SSTableListChangedNotification.java index 7ca574bf16f4..b6d0ed878da5 100644 --- a/src/java/org/apache/cassandra/notifications/SSTableListChangedNotification.java +++ b/src/java/org/apache/cassandra/notifications/SSTableListChangedNotification.java @@ -18,20 +18,24 @@ package org.apache.cassandra.notifications; import java.util.Collection; +import java.util.Optional; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; public class SSTableListChangedNotification implements INotification { public final Collection removed; public final Collection added; - public final OperationType compactionType; + public final OperationType operationType; + public final Optional operationId; - public SSTableListChangedNotification(Collection added, Collection removed, OperationType compactionType) + public SSTableListChangedNotification(Collection added, Collection removed, OperationType operationType, Optional operationId) { this.removed = removed; this.added = added; - this.compactionType = compactionType; + this.operationType = operationType; + this.operationId = operationId; } } diff --git a/src/java/org/apache/cassandra/notifications/SSTableMetadataChanged.java b/src/java/org/apache/cassandra/notifications/SSTableMetadataChanged.java deleted file mode 100644 index 83cfe60bc73c..000000000000 --- a/src/java/org/apache/cassandra/notifications/SSTableMetadataChanged.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.notifications; - -import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.io.sstable.metadata.StatsMetadata; - -public class SSTableMetadataChanged implements INotification -{ - public final SSTableReader sstable; - public final StatsMetadata oldMetadata; - - public SSTableMetadataChanged(SSTableReader levelChanged, StatsMetadata oldMetadata) - { - this.sstable = levelChanged; - this.oldMetadata = oldMetadata; - } -} diff --git a/src/java/org/apache/cassandra/notifications/TruncationNotification.java b/src/java/org/apache/cassandra/notifications/TruncationNotification.java index 345dd17e290c..271ae0d00bf5 100644 --- a/src/java/org/apache/cassandra/notifications/TruncationNotification.java +++ b/src/java/org/apache/cassandra/notifications/TruncationNotification.java @@ -17,16 +17,20 @@ */ package org.apache.cassandra.notifications; +import org.apache.cassandra.db.commitlog.CommitLogPosition; + /** * Fired during truncate, after the memtable has been flushed but before any * snapshot is taken and SSTables are discarded */ public class TruncationNotification implements INotification { + public final CommitLogPosition replayAfter; public final long truncatedAt; - public TruncationNotification(long truncatedAt) + public TruncationNotification(CommitLogPosition replayAfter, long truncatedAt) { + this.replayAfter = replayAfter; this.truncatedAt = truncatedAt; } } diff --git a/src/java/org/apache/cassandra/repair/AbstractRepairTask.java b/src/java/org/apache/cassandra/repair/AbstractRepairTask.java index 94cc3545c210..577c9eab1ce5 100644 --- a/src/java/org/apache/cassandra/repair/AbstractRepairTask.java +++ b/src/java/org/apache/cassandra/repair/AbstractRepairTask.java @@ -71,6 +71,7 @@ private List submitRepairSessions(TimeUUID parentSession, keyspace, options.getParallelism(), isIncremental, + options.isPushRepair(), options.isPullRepair(), options.getPreviewKind(), options.optimiseStreams(), diff --git a/src/java/org/apache/cassandra/repair/LocalSyncTask.java b/src/java/org/apache/cassandra/repair/LocalSyncTask.java index 379ba4b2a1b6..241232a19e69 100644 --- a/src/java/org/apache/cassandra/repair/LocalSyncTask.java +++ b/src/java/org/apache/cassandra/repair/LocalSyncTask.java @@ -119,6 +119,7 @@ protected void startSync() Tracing.traceRepair(message); StreamPlan plan = createStreamPlan(); + logger.info("{} {} {}", previewKind.logPrefix(desc.sessionId), "Starting streaming plan with id", plan.getPlanId()); ctx.streamExecutor().execute(plan); planPromise.setSuccess(plan); } diff --git a/src/java/org/apache/cassandra/repair/ParentRepairSessionListener.java b/src/java/org/apache/cassandra/repair/ParentRepairSessionListener.java new file mode 100644 index 000000000000..72b3f02d2c5d --- /dev/null +++ b/src/java/org/apache/cassandra/repair/ParentRepairSessionListener.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.repair; + +import java.util.concurrent.Future; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.FBUtilities; + +public interface ParentRepairSessionListener +{ + ParentRepairSessionListener instance = CassandraRelevantProperties.REPAIR_PARENT_SESSION_LISTENER.isPresent() + ? FBUtilities.construct(CassandraRelevantProperties.REPAIR_PARENT_SESSION_LISTENER.getString(), + "Parent Repair Session Listener") + : new NoopParentRepairSessionListener(); + + /** + * Call when parent repair session is registered + */ + void onRegistered(TimeUUID sessionId, ActiveRepairService.ParentRepairSession session); + + /** + * Call when parent repair session is removed + */ + void onRemoved(TimeUUID sessionId, ActiveRepairService.ParentRepairSession session); + + /** + * Call when validation task started for given repair session + */ + void onValidation(RepairJobDesc desc, Future validationTask); + + /** + * Call when sync task started for given repair session + */ + void onSync(RepairJobDesc desc, Future syncTask); + + static class NoopParentRepairSessionListener implements ParentRepairSessionListener + { + @Override + public void onRegistered(TimeUUID sessionId, ActiveRepairService.ParentRepairSession session) + { + } + + @Override + public void onRemoved(TimeUUID sessionId, ActiveRepairService.ParentRepairSession session) + { + } + + @Override + public void onValidation(RepairJobDesc desc, Future validationTask) + { + } + + @Override + public void onSync(RepairJobDesc desc, Future syncTask) + { + } + } +} diff --git a/src/java/org/apache/cassandra/repair/PreviewRepairTask.java b/src/java/org/apache/cassandra/repair/PreviewRepairTask.java index edee11cf2007..600e64610add 100644 --- a/src/java/org/apache/cassandra/repair/PreviewRepairTask.java +++ b/src/java/org/apache/cassandra/repair/PreviewRepairTask.java @@ -26,6 +26,7 @@ import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; @@ -85,10 +86,10 @@ public Future performUnsafe(ExecutorPlus executor, Sche else { message = (previewKind == PreviewKind.REPAIRED ? "Repaired data is inconsistent\n" : "Preview complete\n") + summary; - RepairMetrics.previewFailures.inc(); if (previewKind == PreviewKind.REPAIRED) maybeSnapshotReplicas(parentSession, keyspace, result.results.get()); // we know its present as summary used it } + emitMetrics(summary); successMessage += "; " + message; coordinator.notification(message); @@ -96,6 +97,21 @@ public Future performUnsafe(ExecutorPlus executor, Sche }); } + private void emitMetrics(SyncStatSummary summary) + { + if (!summary.isEmpty()) + RepairMetrics.previewFailures.inc(); + + summary.getTotals().forEach((key, table) -> { + if (table.isCounter()) + return; + + ColumnFamilyStore cfs = Keyspace.open(key.left).getColumnFamilyStore(key.right); + cfs.metric.tokenRangesPreviewedDesynchronized.mark(table.getRanges()); + cfs.metric.bytesPreviewedDesynchronized.mark(table.getBytes()); + }); + } + private void maybeSnapshotReplicas(TimeUUID parentSession, String keyspace, List results) { if (!DatabaseDescriptor.snapshotOnRepairedDataMismatch()) diff --git a/src/java/org/apache/cassandra/repair/RepairCoordinator.java b/src/java/org/apache/cassandra/repair/RepairCoordinator.java index 091819c65d7b..f2e1a1bf049f 100644 --- a/src/java/org/apache/cassandra/repair/RepairCoordinator.java +++ b/src/java/org/apache/cassandra/repair/RepairCoordinator.java @@ -17,10 +17,12 @@ */ package org.apache.cassandra.repair; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -32,6 +34,7 @@ import java.util.function.Function; import java.util.function.Supplier; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -39,6 +42,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.common.util.concurrent.SettableFuture; import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.net.Verb; @@ -72,7 +76,6 @@ import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.repair.state.CoordinatorState; import org.apache.cassandra.schema.SchemaConstants; -import org.apache.cassandra.schema.SystemDistributedKeyspace; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus; import org.apache.cassandra.service.ClientState; @@ -94,6 +97,7 @@ public class RepairCoordinator implements Runnable, ProgressEventNotifier, Repai { private static final Logger logger = LoggerFactory.getLogger(RepairCoordinator.class); + private final SettableFuture result = SettableFuture.create(); private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1); public final CoordinatorState state; @@ -102,7 +106,8 @@ public class RepairCoordinator implements Runnable, ProgressEventNotifier, Repai private final Function getLocalReplicas; private final List listeners = new ArrayList<>(); - private final AtomicReference firstError = new AtomicReference<>(null); + @VisibleForTesting + protected final AtomicReference firstError = new AtomicReference<>(null); final SharedContext ctx; final Scheduler validationScheduler; @@ -201,6 +206,8 @@ private void skip(String msg) private void success(String msg) { + result.set(null); + state.phase.success(msg); fireProgressEvent(jmxEvent(ProgressEventType.SUCCESS, msg)); ctx.repair().recordRepairStatus(state.cmd, ActiveRepairService.ParentRepairStatus.COMPLETED, @@ -208,13 +215,15 @@ private void success(String msg) complete(null); } - private void fail(String reason) + protected void fail(String reason) { if (reason == null) { Throwable error = firstError.get(); - reason = error != null ? error.toString() : "Some repair failed"; + reason = (error != null && error.getMessage() != null) ? error.getMessage() : "Some repair failed"; } + result.setException(new RuntimeException(reason)); + state.phase.fail(reason); ParticipateState p = ctx.repair().participate(state.id); if (p != null) @@ -268,6 +277,11 @@ private void complete(String msg) Keyspace.open(state.keyspace).metric.repairTime.update(durationMillis, TimeUnit.MILLISECONDS); } + public java.util.concurrent.Future getResult() + { + return result; + } + public void run() { try @@ -348,7 +362,7 @@ private TraceState maybeCreateTraceState(Iterable columnFamil for (ColumnFamilyStore cfs : columnFamilyStores) cfsb.append(", ").append(cfs.getKeyspaceName()).append(".").append(cfs.name); - TimeUUID sessionId = Tracing.instance.newSession(Tracing.TraceType.REPAIR); + TimeUUID sessionId = Tracing.instance.newSession(ClientState.forInternalCalls(), Tracing.TraceType.REPAIR); TraceState traceState = Tracing.instance.begin("repair", ImmutableMap.of("keyspace", state.keyspace, "columnFamilies", cfsb.substring(2))); traceState.enableActivityNotification(tag); @@ -370,6 +384,10 @@ private void notifyStarting() private NeighborsAndRanges getNeighborsAndRanges() throws RepairException { + // if it's offline service, don't check token metadata and storage service. + if (state.options.isOfflineService()) + return createNeighbordAndRangesForOfflineService(state.options); + Set allNeighbors = new HashSet<>(); List commonRanges = new ArrayList<>(); @@ -382,6 +400,7 @@ private NeighborsAndRanges getNeighborsAndRanges() throws RepairException EndpointsForRange neighbors = ctx.repair().getNeighbors(state.keyspace, keyspaceLocalRanges, range, state.options.getDataCenters(), state.options.getHosts()); + // local RF = 1 or given range is not part of local range, neighbors would be empty. if (neighbors.isEmpty()) { if (state.options.ignoreUnreplicatedKeyspaces()) @@ -416,11 +435,41 @@ private NeighborsAndRanges getNeighborsAndRanges() throws RepairException return new NeighborsAndRanges(shouldExcludeDeadParticipants, allNeighbors, commonRanges); } + @VisibleForTesting + public static NeighborsAndRanges createNeighbordAndRangesForOfflineService(RepairOption options) + { + Preconditions.checkArgument(!options.getHosts().isEmpty(), "There should be at least 1 host when repairing via offline service"); + Preconditions.checkArgument(!options.getRanges().isEmpty(), "Token ranges must be specified when repairing via offline service. " + + "Please specify at least one token range which all hosts have in common."); + + Set allNeighbors = new HashSet<>(); + List commonRanges = new ArrayList<>(); + + for (String host : options.getHosts()) + { + try + { + InetAddressAndPort endpoint = InetAddressAndPort.getByName(host.trim()); + if (!endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + allNeighbors.add(endpoint); + } + catch (UnknownHostException e) + { + throw new IllegalArgumentException("Unknown host specified " + host, e); + } + } + + Preconditions.checkArgument(!allNeighbors.isEmpty(), "There should be at least 1 neighbor when repairing via offline service"); + + commonRanges.add(new CommonRange(allNeighbors, Collections.emptySet(), options.getRanges())); + return new NeighborsAndRanges(false, allNeighbors, commonRanges); + } + private void maybeStoreParentRepairStart(String[] cfnames) { if (!state.options.isPreview()) { - SystemDistributedKeyspace.startParentRepair(state.id, state.keyspace, cfnames, state.options); + RepairProgressReporter.instance.onParentRepairStarted(state.id, state.keyspace, cfnames, state.options); } } @@ -428,7 +477,7 @@ private void maybeStoreParentRepairSuccess(Collection> successfulRa { if (!state.options.isPreview()) { - SystemDistributedKeyspace.successfulParentRepair(state.id, successfulRanges); + RepairProgressReporter.instance.onParentRepairSucceeded(state.id, successfulRanges); } } @@ -436,7 +485,7 @@ private void maybeStoreParentRepairFailure(Throwable error) { if (!state.options.isPreview()) { - SystemDistributedKeyspace.failParentRepair(state.id, error); + RepairProgressReporter.instance.onParentRepairFailed(state.id, error); } } diff --git a/src/java/org/apache/cassandra/repair/RepairJob.java b/src/java/org/apache/cassandra/repair/RepairJob.java index c54336a6b39d..2424a5982869 100644 --- a/src/java/org/apache/cassandra/repair/RepairJob.java +++ b/src/java/org/apache/cassandra/repair/RepairJob.java @@ -47,9 +47,8 @@ import org.apache.cassandra.repair.asymmetric.HostDifferences; import org.apache.cassandra.repair.asymmetric.PreferedNodeFilter; import org.apache.cassandra.repair.asymmetric.ReduceHelper; -import org.apache.cassandra.schema.SystemDistributedKeyspace; -import org.apache.cassandra.service.paxos.cleanup.PaxosCleanup; import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.service.paxos.cleanup.PaxosCleanup; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MerkleTrees; @@ -207,7 +206,7 @@ public void onSuccess(List stats) if (!session.previewKind.isPreview()) { logger.info("{} {}.{} is fully synced", session.previewKind.logPrefix(session.getId()), desc.keyspace, desc.columnFamily); - SystemDistributedKeyspace.successfulRepairJob(session.getId(), desc.keyspace, desc.columnFamily); + RepairProgressReporter.instance.onRepairSucceeded(session.getId(), desc.keyspace, desc.columnFamily); } cfs.metric.repairsCompleted.inc(); trySuccess(new RepairResult(desc, stats)); @@ -225,7 +224,7 @@ public void onFailure(Throwable t) if (!session.previewKind.isPreview()) { logger.warn("{} {}.{} sync failed", session.previewKind.logPrefix(session.getId()), desc.keyspace, desc.columnFamily); - SystemDistributedKeyspace.failedRepairJob(session.getId(), desc.keyspace, desc.columnFamily, t); + RepairProgressReporter.instance.onRepairFailed(session.getId(), desc.keyspace, desc.columnFamily, t); } cfs.metric.repairsCompleted.inc(); tryFailure(t instanceof NoSuchRepairSessionExceptionWrapper @@ -287,6 +286,7 @@ private List createStandardSyncTasks(List trees) ctx.broadcastAddressAndPort(), this::isTransient, session.isIncremental, + session.pushRepair, session.pullRepair, session.previewKind); } @@ -298,6 +298,7 @@ static List createStandardSyncTasks(SharedContext ctx, InetAddressAndPort local, Predicate isTransient, boolean isIncremental, + boolean pushRepair, boolean pullRepair, PreviewKind previewKind) { @@ -321,14 +322,14 @@ static List createStandardSyncTasks(SharedContext ctx, if (differences.isEmpty()) continue; - SyncTask task; + SyncTask task = null; if (r1.endpoint.equals(local) || r2.endpoint.equals(local)) { TreeResponse self = r1.endpoint.equals(local) ? r1 : r2; TreeResponse remote = r2.endpoint.equals(local) ? r1 : r2; - // pull only if local is full - boolean requestRanges = !isTransient.test(self.endpoint); + // pull only if local is full; additionally check for push repair + boolean requestRanges = !isTransient.test(self.endpoint) && !pushRepair; // push only if remote is full; additionally check for pull repair boolean transferRanges = !isTransient.test(remote.endpoint) && !pullRepair; @@ -341,16 +342,19 @@ static List createStandardSyncTasks(SharedContext ctx, } else if (isTransient.test(r1.endpoint) || isTransient.test(r2.endpoint)) { + Preconditions.checkArgument(!pushRepair, "Push Repair doesn't support transient replica"); + // Stream only from transient replica TreeResponse streamFrom = isTransient.test(r1.endpoint) ? r1 : r2; TreeResponse streamTo = isTransient.test(r1.endpoint) ? r2 : r1; task = new AsymmetricRemoteSyncTask(ctx, desc, streamTo.endpoint, streamFrom.endpoint, differences, previewKind); } - else + else if (!pushRepair) { task = new SymmetricRemoteSyncTask(ctx, desc, r1.endpoint, r2.endpoint, differences, previewKind); } - syncTasks.add(task); + if (task != null) + syncTasks.add(task); } trees.get(i).trees.release(); } @@ -403,6 +407,7 @@ private NoSuchRepairSessionExceptionWrapper(NoSuchRepairSessionException wrapped private List createOptimisedSyncingSyncTasks(List trees) { + Preconditions.checkArgument(!session.pushRepair, "Push Repair doesn't support optimized sync"); return createOptimisedSyncingSyncTasks(ctx, desc, trees, diff --git a/src/java/org/apache/cassandra/repair/RepairJobDesc.java b/src/java/org/apache/cassandra/repair/RepairJobDesc.java index dba336b5e79e..e3d43ffe5784 100644 --- a/src/java/org/apache/cassandra/repair/RepairJobDesc.java +++ b/src/java/org/apache/cassandra/repair/RepairJobDesc.java @@ -79,12 +79,14 @@ public UUID determanisticId() @Override public String toString() { - return "[repair #" + sessionId + " on " + keyspace + "/" + columnFamily + ", " + ranges + "]"; + String parentSessionId = this.parentSessionId == null ? "" : " (parent session id: #" + this.parentSessionId + ")"; + return "[repair #" + sessionId + parentSessionId + " on " + keyspace + "/" + columnFamily + ", " + ranges + "]"; } public String toString(PreviewKind previewKind) { - return '[' + previewKind.logPrefix() + " #" + sessionId + " on " + keyspace + "/" + columnFamily + ", " + ranges + "]"; + String parentSessionId = this.parentSessionId == null ? "" : " (parent session id: #" + this.parentSessionId + ")"; + return '[' + previewKind.logPrefix() + " #" + sessionId + parentSessionId + " on " + keyspace + "/" + columnFamily + ", " + ranges + "]"; } @Override @@ -152,7 +154,7 @@ public RepairJobDesc deserialize(DataInputPlus in, int version) throws IOExcepti public long serializedSize(RepairJobDesc desc, int version) { - int size = TypeSizes.sizeof(desc.parentSessionId != null); + long size = TypeSizes.sizeof(desc.parentSessionId != null); if (desc.parentSessionId != null) size += TimeUUID.sizeInBytes(); size += TimeUUID.sizeInBytes(); diff --git a/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java b/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java index 621aacc378da..3d3b487c95ef 100644 --- a/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java +++ b/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.concurrent.Future; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; @@ -52,6 +53,20 @@ import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.TimeUUID; +import static org.apache.cassandra.net.Verb.CLEANUP_MSG; +import static org.apache.cassandra.net.Verb.FAILED_SESSION_MSG; +import static org.apache.cassandra.net.Verb.FINALIZE_COMMIT_MSG; +import static org.apache.cassandra.net.Verb.FINALIZE_PROMISE_MSG; +import static org.apache.cassandra.net.Verb.FINALIZE_PROPOSE_MSG; +import static org.apache.cassandra.net.Verb.PREPARE_CONSISTENT_REQ; +import static org.apache.cassandra.net.Verb.PREPARE_CONSISTENT_RSP; +import static org.apache.cassandra.net.Verb.PREPARE_MSG; +import static org.apache.cassandra.net.Verb.SNAPSHOT_MSG; +import static org.apache.cassandra.net.Verb.STATUS_REQ; +import static org.apache.cassandra.net.Verb.STATUS_RSP; +import static org.apache.cassandra.net.Verb.SYNC_REQ; +import static org.apache.cassandra.net.Verb.VALIDATION_REQ; + /** * Handles all repair related message. * @@ -102,262 +117,261 @@ public void doVerb(final Message message) RepairJobDesc desc = message.payload.desc; try { - switch (message.verb()) + if (message.verb() == PREPARE_MSG) { - case PREPARE_MSG: + PrepareMessage prepareMessage = (PrepareMessage) message.payload; + logger.debug("Preparing, {}", prepareMessage); + ParticipateState state = new ParticipateState(ctx.clock(), message.from(), prepareMessage); + if (!ctx.repair().register(state)) { - PrepareMessage prepareMessage = (PrepareMessage) message.payload; - logger.debug("Preparing, {}", prepareMessage); - ParticipateState state = new ParticipateState(ctx.clock(), message.from(), prepareMessage); - if (!ctx.repair().register(state)) - { - replyDedup(ctx.repair().participate(state.id), message); - return; - } - if (!ctx.repair().verifyCompactionsPendingThreshold(prepareMessage.parentRepairSession, prepareMessage.previewKind)) - { - // error is logged in verifyCompactionsPendingThreshold - state.phase.fail("Too many pending compactions"); - - sendFailureResponse(message); - return; - } + replyDedup(ctx.repair().participate(state.id), message); + return; + } + if (!ctx.repair().verifyDiskHeadroomThreshold(prepareMessage.parentRepairSession, prepareMessage.previewKind)) + { + // error is logged in verifyDiskHeadroomThreshold + state.phase.fail("Not enough disk headroom to perform repair"); + sendFailureResponse(message); + return; + } + if (!ctx.repair().verifyCompactionsPendingThreshold(prepareMessage.parentRepairSession, prepareMessage.previewKind)) + { + // error is logged in verifyCompactionsPendingThreshold + state.phase.fail("Too many pending compactions"); - List columnFamilyStores = new ArrayList<>(prepareMessage.tableIds.size()); - for (TableId tableId : prepareMessage.tableIds) - { - ColumnFamilyStore columnFamilyStore = ColumnFamilyStore.getIfExists(tableId); - if (columnFamilyStore == null) - { - String reason = String.format("Table with id %s was dropped during prepare phase of repair", - tableId); - state.phase.fail(reason); - logErrorAndSendFailureResponse(reason, message); - return; - } - columnFamilyStores.add(columnFamilyStore); - } - state.phase.accept(); - ctx.repair().registerParentRepairSession(prepareMessage.parentRepairSession, - message.from(), - columnFamilyStores, - prepareMessage.ranges, - prepareMessage.isIncremental, - prepareMessage.repairedAt, - prepareMessage.isGlobal, - prepareMessage.previewKind); - sendAck(message); + sendFailureResponse(message); + return; } - break; - case SNAPSHOT_MSG: + List columnFamilyStores = new ArrayList<>(prepareMessage.tableIds.size()); + for (TableId tableId : prepareMessage.tableIds) { - logger.debug("Snapshotting {}", desc); - ParticipateState state = ctx.repair().participate(desc.parentSessionId); - if (state == null) + ColumnFamilyStore columnFamilyStore = ColumnFamilyStore.getIfExists(tableId); + if (columnFamilyStore == null) { - logErrorAndSendFailureResponse("Unknown repair " + desc.parentSessionId, message); - return; - } - final ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(desc.keyspace, desc.columnFamily); - if (cfs == null) - { - String reason = String.format("Table %s.%s was dropped during snapshot phase of repair %s", - desc.keyspace, desc.columnFamily, desc.parentSessionId); + String reason = String.format("Table with id %s was dropped during prepare phase of repair", + tableId); state.phase.fail(reason); logErrorAndSendFailureResponse(reason, message); return; } + columnFamilyStores.add(columnFamilyStore); + } + state.phase.accept(); + ctx.repair().registerParentRepairSession(prepareMessage.parentRepairSession, + message.from(), + columnFamilyStores, + prepareMessage.ranges, + prepareMessage.isIncremental, + prepareMessage.repairedAt, + prepareMessage.isGlobal, + prepareMessage.previewKind); + sendAck(message); + } + else if (message.verb() == SNAPSHOT_MSG) + { + logger.debug("Snapshotting {}", desc); + ParticipateState state = ctx.repair().participate(desc.parentSessionId); + if (state == null) + { + logErrorAndSendFailureResponse("Unknown repair " + desc.parentSessionId, message); + return; + } + final ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(desc.keyspace, desc.columnFamily); + if (cfs == null) + { + String reason = String.format("Table %s.%s was dropped during snapshot phase of repair %s", + desc.keyspace, desc.columnFamily, desc.parentSessionId); + state.phase.fail(reason); + logErrorAndSendFailureResponse(reason, message); + return; + } - ActiveRepairService.ParentRepairSession prs = ctx.repair().getParentRepairSession(desc.parentSessionId); - if (prs.setHasSnapshots()) + ActiveRepairService.ParentRepairSession prs = ctx.repair().getParentRepairSession(desc.parentSessionId); + if (prs.setHasSnapshots()) + { + state.getOrCreateJob(desc).snapshot(); + TableRepairManager repairManager = cfs.getRepairManager(); + if (prs.isGlobal) { - state.getOrCreateJob(desc).snapshot(); - TableRepairManager repairManager = cfs.getRepairManager(); - if (prs.isGlobal) - { - repairManager.snapshot(desc.parentSessionId.toString(), prs.getRanges(), false); - } - else - { - repairManager.snapshot(desc.parentSessionId.toString(), desc.ranges, true); - } - logger.debug("Enqueuing response to snapshot request {} to {}", desc.sessionId, message.from()); + repairManager.snapshot(desc.parentSessionId.toString(), prs.getRanges(), false); } - sendAck(message); + else + { + repairManager.snapshot(desc.parentSessionId.toString(), desc.ranges, true); + } + logger.debug("Enqueuing response to snapshot request {} to {}", desc.sessionId, message.from()); } - break; + sendAck(message); + } + else if (message.verb() == VALIDATION_REQ) + { + ValidationRequest validationRequest = (ValidationRequest) message.payload; + logger.debug("Validating {}", validationRequest); - case VALIDATION_REQ: + ParticipateState participate = ctx.repair().participate(desc.parentSessionId); + if (participate == null) { - ValidationRequest validationRequest = (ValidationRequest) message.payload; - logger.debug("Validating {}", validationRequest); + logErrorAndSendFailureResponse("Unknown repair " + desc.parentSessionId, message); + return; + } - ParticipateState participate = ctx.repair().participate(desc.parentSessionId); - if (participate == null) + ValidationState vState = new ValidationState(ctx.clock(), desc, message.from()); + if (!register(message, participate, vState, + participate::register, + (d, i) -> participate.validation(d))) + return; + try + { + // trigger read-only compaction + ColumnFamilyStore store = ColumnFamilyStore.getIfExists(desc.keyspace, desc.columnFamily); + if (store == null) { - logErrorAndSendFailureResponse("Unknown repair " + desc.parentSessionId, message); + String msg = String.format("Table %s.%s was dropped during validation phase of repair %s", desc.keyspace, desc.columnFamily, desc.parentSessionId); + vState.phase.fail(msg); + logErrorAndSendFailureResponse(msg, message); return; } - ValidationState vState = new ValidationState(ctx.clock(), desc, message.from()); - if (!register(message, participate, vState, - participate::register, - (d, i) -> participate.validation(d))) - return; try { - // trigger read-only compaction - ColumnFamilyStore store = ColumnFamilyStore.getIfExists(desc.keyspace, desc.columnFamily); - if (store == null) - { - String msg = String.format("Table %s.%s was dropped during validation phase of repair %s", desc.keyspace, desc.columnFamily, desc.parentSessionId); - vState.phase.fail(msg); - logErrorAndSendFailureResponse(msg, message); - return; - } - - try - { - ctx.repair().consistent.local.maybeSetRepairing(desc.parentSessionId); - } - catch (Throwable t) - { - JVMStabilityInspector.inspectThrowable(t); - vState.phase.fail(t.toString()); - logErrorAndSendFailureResponse(t.toString(), message); - return; - } - PreviewKind previewKind; - try - { - previewKind = previewKind(desc.parentSessionId); - } - catch (NoSuchRepairSessionException e) - { - logger.warn("Parent repair session {} has been removed, failing repair", desc.parentSessionId); - vState.phase.fail(e); - sendFailureResponse(message); - return; - } - vState.phase.accept(); - sendAck(message); - - Validator validator = new Validator(ctx, vState, validationRequest.nowInSec, - isIncremental(desc.parentSessionId), previewKind); - if (acceptMessage(ctx, validationRequest, message.from())) - { - ctx.validationManager().submitValidation(store, validator); - } - else - { - validator.fail(new RepairOutOfTokenRangeException(validationRequest.desc.ranges)); - } + ctx.repair().consistent.local.maybeSetRepairing(desc.parentSessionId); } catch (Throwable t) { - vState.phase.fail(t); - throw t; + JVMStabilityInspector.inspectThrowable(t); + vState.phase.fail(t.toString()); + logErrorAndSendFailureResponse(t.toString(), message); + return; } - } - break; - - case SYNC_REQ: - { - // forwarded sync request - SyncRequest request = (SyncRequest) message.payload; - logger.debug("Syncing {}", request); - - ParticipateState participate = ctx.repair().participate(desc.parentSessionId); - if (participate == null) + PreviewKind previewKind; + try { - logErrorAndSendFailureResponse("Unknown repair " + desc.parentSessionId, message); - return; + previewKind = previewKind(desc.parentSessionId); } - SyncState state = new SyncState(ctx.clock(), desc, request.initiator, request.src, request.dst); - if (!register(message, participate, state, - participate::register, - participate::sync)) + catch (NoSuchRepairSessionException e) + { + logger.warn("Parent repair session {} has been removed, failing repair", desc.parentSessionId); + vState.phase.fail(e); + sendFailureResponse(message); return; - state.phase.accept(); - StreamingRepairTask task = new StreamingRepairTask(ctx, state, desc, - request.initiator, - request.src, - request.dst, - request.ranges, - isIncremental(desc.parentSessionId) ? desc.parentSessionId : null, - request.previewKind, - request.asymmetric); - task.run(); + } + vState.phase.accept(); sendAck(message); - } - break; - case CLEANUP_MSG: + Validator validator = new Validator(ctx, vState, validationRequest.nowInSec, + isIncremental(desc.parentSessionId), previewKind); + if (acceptMessage(ctx, validationRequest, message.from())) + { + Future validationFuture = ctx.validationManager().submitValidation(store, validator); + ParentRepairSessionListener.instance.onValidation(desc, validationFuture); + } + else + { + validator.fail(new RepairOutOfTokenRangeException(validationRequest.desc.ranges)); + } + } + catch (Throwable t) { - logger.debug("cleaning up repair"); - CleanupMessage cleanup = (CleanupMessage) message.payload; - ParticipateState state = ctx.repair().participate(cleanup.parentRepairSession); - if (state != null) - state.phase.success("Cleanup message recieved"); - ctx.repair().removeParentRepairSession(cleanup.parentRepairSession); - sendAck(message); + vState.phase.fail(t); + throw t; } - break; - - case PREPARE_CONSISTENT_REQ: - ctx.repair().consistent.local.handlePrepareMessage(message); - break; - - case PREPARE_CONSISTENT_RSP: - ctx.repair().consistent.coordinated.handlePrepareResponse(message); - break; - - case FINALIZE_PROPOSE_MSG: - ctx.repair().consistent.local.handleFinalizeProposeMessage(message); - break; - - case FINALIZE_PROMISE_MSG: - ctx.repair().consistent.coordinated.handleFinalizePromiseMessage(message); - break; - - case FINALIZE_COMMIT_MSG: - ctx.repair().consistent.local.handleFinalizeCommitMessage(message); - break; - - case FAILED_SESSION_MSG: - FailSession failure = (FailSession) message.payload; - sendAck(message); - ParticipateState p = ctx.repair().participate(failure.sessionID); - if (p != null) - p.phase.fail("Failure message from " + message.from()); - ctx.repair().consistent.coordinated.handleFailSessionMessage(failure); - ctx.repair().consistent.local.handleFailSessionMessage(message.from(), failure); - break; - - case STATUS_REQ: - ctx.repair().consistent.local.handleStatusRequest(message.from(), (StatusRequest) message.payload); - break; - - case STATUS_RSP: - ctx.repair().consistent.local.handleStatusResponse(message.from(), (StatusResponse) message.payload); - break; + } + else if (message.verb() == SYNC_REQ) + { + // forwarded sync request + SyncRequest request = (SyncRequest) message.payload; + logger.debug("Syncing {}", request); - default: + ParticipateState participate = ctx.repair().participate(desc.parentSessionId); + if (participate == null) + { + logErrorAndSendFailureResponse("Unknown repair " + desc.parentSessionId, message); + return; + } + SyncState state = new SyncState(ctx.clock(), desc, request.initiator, request.src, request.dst); + if (!register(message, participate, state, + participate::register, + participate::sync)) + return; + state.phase.accept(); + StreamingRepairTask task = new StreamingRepairTask(ctx, state, desc, + request.initiator, + request.src, + request.dst, + request.ranges, + isIncremental(desc.parentSessionId) ? desc.parentSessionId : null, + request.previewKind, + request.asymmetric); + Future syncFuture = task.execute(); + ParentRepairSessionListener.instance.onSync(desc, syncFuture); + sendAck(message); + } + else if (message.verb() == CLEANUP_MSG) + { + CleanupMessage cleanup = (CleanupMessage) message.payload; + logger.debug("Cleaning up parent repair session {}", cleanup.parentRepairSession); + ParticipateState state = ctx.repair().participate(cleanup.parentRepairSession); + if (state != null) + state.phase.success("Cleanup message recieved"); + ctx.repair().removeParentRepairSession(cleanup.parentRepairSession); + sendAck(message); + } + else if (message.verb() == PREPARE_CONSISTENT_REQ) + { + ctx.repair().consistent.local.handlePrepareMessage(message); + } + else if (message.verb() == PREPARE_CONSISTENT_RSP) + { + ctx.repair().consistent.coordinated.handlePrepareResponse(message); + } + else if (message.verb() == FINALIZE_PROPOSE_MSG) + { + ctx.repair().consistent.local.handleFinalizeProposeMessage(message); + } + else if (message.verb() == FINALIZE_PROMISE_MSG) + { + ctx.repair().consistent.coordinated.handleFinalizePromiseMessage(message); + } + else if (message.verb() == FINALIZE_COMMIT_MSG) + { + ctx.repair().consistent.local.handleFinalizeCommitMessage(message); + } + else if (message.verb() == FAILED_SESSION_MSG) + { + FailSession failure = (FailSession) message.payload; + sendAck(message); + ParticipateState p = ctx.repair().participate(failure.sessionID); + if (p != null) + p.phase.fail("Failure message from " + message.from()); + ctx.repair().consistent.coordinated.handleFailSessionMessage(failure); + ctx.repair().consistent.local.handleFailSessionMessage(message.from(), failure); + } + else if (message.verb() == STATUS_REQ) + { + ctx.repair().consistent.local.handleStatusRequest(message.from(), (StatusRequest) message.payload); + } + else if (message.verb() == STATUS_RSP) + { + ctx.repair().consistent.local.handleStatusResponse(message.from(), (StatusResponse) message.payload); + } + else + { ctx.repair().handleMessage(message); - break; } } catch (Exception e) { - logger.error("Got error, removing parent repair session"); if (desc != null && desc.parentSessionId != null) { + logger.error("Got error processing {}, removing parent repair session {}", message.verb(), desc.parentSessionId); ParticipateState parcipate = ctx.repair().participate(desc.parentSessionId); if (parcipate != null) parcipate.phase.fail(e); ctx.repair().removeParentRepairSession(desc.parentSessionId); } + else + logger.error("Got error processing {}, removing parent repair session", message.verb()); throw new RuntimeException(e); } } diff --git a/src/java/org/apache/cassandra/repair/RepairProgressReporter.java b/src/java/org/apache/cassandra/repair/RepairProgressReporter.java new file mode 100644 index 000000000000..b78797107abe --- /dev/null +++ b/src/java/org/apache/cassandra/repair/RepairProgressReporter.java @@ -0,0 +1,98 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.cassandra.repair; + +import java.util.Collection; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.repair.messages.RepairOption; +import org.apache.cassandra.schema.SystemDistributedKeyspace; +import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.FBUtilities; + +public interface RepairProgressReporter +{ + RepairProgressReporter instance = CassandraRelevantProperties.REPAIR_PROGRESS_REPORTER.isPresent() + ? FBUtilities.construct(CassandraRelevantProperties.REPAIR_PROGRESS_REPORTER.getString(), + "Repair Progress Reporter") + : new DefaultRepairProgressReporter(); + + void onParentRepairStarted(TimeUUID parentSession, String keyspaceName, String[] cfnames, RepairOption options); + + void onParentRepairSucceeded(TimeUUID parentSession, Collection> successfulRanges); + + void onParentRepairFailed(TimeUUID parentSession, Throwable t); + + void onRepairsStarted(TimeUUID id, TimeUUID parentRepairSession, String keyspaceName, String[] cfnames, CommonRange commonRange); + + void onRepairsFailed(TimeUUID id, String keyspaceName, String[] cfnames, Throwable t); + + void onRepairFailed(TimeUUID id, String keyspaceName, String cfname, Throwable t); + + void onRepairSucceeded(TimeUUID id, String keyspaceName, String cfname); + + class DefaultRepairProgressReporter implements RepairProgressReporter + { + @Override + public void onParentRepairStarted(TimeUUID parentSession, String keyspaceName, String[] cfnames, RepairOption options) + { + SystemDistributedKeyspace.startParentRepair(parentSession, keyspaceName, cfnames, options); + } + + @Override + public void onParentRepairSucceeded(TimeUUID parentSession, Collection> successfulRanges) + { + SystemDistributedKeyspace.successfulParentRepair(parentSession, successfulRanges); + } + + @Override + public void onParentRepairFailed(TimeUUID parentSession, Throwable t) + { + SystemDistributedKeyspace.failParentRepair(parentSession, t); + } + + @Override + public void onRepairsStarted(TimeUUID id, TimeUUID parentRepairSession, String keyspaceName, String[] cfnames, CommonRange commonRange) + { + SystemDistributedKeyspace.startRepairs(id, parentRepairSession, keyspaceName, cfnames, commonRange); + } + + @Override + public void onRepairsFailed(TimeUUID id, String keyspaceName, String[] cfnames, Throwable t) + { + SystemDistributedKeyspace.failRepairs(id, keyspaceName, cfnames, t); + } + + @Override + public void onRepairFailed(TimeUUID id, String keyspaceName, String cfname, Throwable t) + { + SystemDistributedKeyspace.failedRepairJob(id, keyspaceName, cfname, t); + } + + @Override + public void onRepairSucceeded(TimeUUID id, String keyspaceName, String cfname) + { + SystemDistributedKeyspace.successfulRepairJob(id, keyspaceName, cfname); + } + } +} diff --git a/src/java/org/apache/cassandra/repair/RepairSession.java b/src/java/org/apache/cassandra/repair/RepairSession.java index 7ec64502eb2f..179fb87415b6 100644 --- a/src/java/org/apache/cassandra/repair/RepairSession.java +++ b/src/java/org/apache/cassandra/repair/RepairSession.java @@ -57,7 +57,6 @@ import org.apache.cassandra.repair.messages.SyncResponse; import org.apache.cassandra.repair.messages.ValidationResponse; import org.apache.cassandra.repair.state.SessionState; -import org.apache.cassandra.schema.SystemDistributedKeyspace; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.tracing.Tracing; @@ -114,6 +113,7 @@ public class RepairSession extends AsyncFuture implements I public final SessionState state; public final RepairParallelism parallelismDegree; + public final boolean pushRepair; public final boolean pullRepair; /** Range to repair */ @@ -144,6 +144,7 @@ public class RepairSession extends AsyncFuture implements I * @param commonRange ranges to repair * @param keyspace name of keyspace * @param parallelismDegree specifies the degree of parallelism when calculating the merkle trees + * @param pushRepair true if the repair should be one way pushing differences to remote host * @param pullRepair true if the repair should be one way (from remote host to this host and only applicable between two hosts--see RepairOption) * @param repairPaxos true if incomplete paxos operations should be completed as part of repair * @param paxosOnly true if we should only complete paxos operations, not run a normal repair @@ -156,6 +157,7 @@ public RepairSession(SharedContext ctx, String keyspace, RepairParallelism parallelismDegree, boolean isIncremental, + boolean pushRepair, boolean pullRepair, PreviewKind previewKind, boolean optimiseStreams, @@ -170,6 +172,7 @@ public RepairSession(SharedContext ctx, assert cfnames.length > 0 : "Repairing no column families seems pointless, doesn't it"; this.state = new SessionState(ctx.clock(), parentRepairSession, keyspace, cfnames, commonRange); this.parallelismDegree = parallelismDegree; + this.pushRepair = pushRepair; this.isIncremental = isIncremental; this.previewKind = previewKind; this.pullRepair = pullRepair; @@ -299,7 +302,7 @@ public void start(ExecutorPlus executor) Tracing.traceRepair("Syncing range {}", state.commonRange); if (!previewKind.isPreview() && !paxosOnly) { - SystemDistributedKeyspace.startRepairs(getId(), state.parentRepairSession, state.keyspace, state.cfnames, state.commonRange); + RepairProgressReporter.instance.onRepairsStarted(getId(), state.parentRepairSession, state.keyspace, state.cfnames, state.commonRange); } if (state.commonRange.endpoints.isEmpty()) @@ -310,7 +313,7 @@ public void start(ExecutorPlus executor) trySuccess(new RepairSessionResult(state.id, state.keyspace, state.commonRange.ranges, Lists.newArrayList(), state.commonRange.hasSkippedReplicas)); if (!previewKind.isPreview()) { - SystemDistributedKeyspace.failRepairs(getId(), state.keyspace, state.cfnames, new RuntimeException(message)); + RepairProgressReporter.instance.onRepairsFailed(getId(), state.keyspace, state.cfnames, new RuntimeException(message)); } return; } @@ -327,7 +330,7 @@ public void start(ExecutorPlus executor) tryFailure(e); if (!previewKind.isPreview()) { - SystemDistributedKeyspace.failRepairs(getId(), state.keyspace, state.cfnames, e); + RepairProgressReporter.instance.onRepairsFailed(getId(), state.keyspace, state.cfnames, e); } return; } diff --git a/src/java/org/apache/cassandra/repair/StreamingRepairTask.java b/src/java/org/apache/cassandra/repair/StreamingRepairTask.java index 0f84d66893ef..a6af3e0481b8 100644 --- a/src/java/org/apache/cassandra/repair/StreamingRepairTask.java +++ b/src/java/org/apache/cassandra/repair/StreamingRepairTask.java @@ -36,6 +36,7 @@ import org.apache.cassandra.streaming.StreamEvent; import org.apache.cassandra.streaming.StreamEventHandler; import org.apache.cassandra.streaming.StreamPlan; +import org.apache.cassandra.streaming.StreamResultFuture; import org.apache.cassandra.streaming.StreamState; import org.apache.cassandra.streaming.StreamOperation; import org.apache.cassandra.utils.TimeUUID; @@ -50,7 +51,7 @@ * StreamingRepairTask performs data streaming between two remote replicas, neither of which is repair coordinator. * Task will send {@link SyncResponse} message back to coordinator upon streaming completion. */ -public class StreamingRepairTask implements Runnable, StreamEventHandler +public class StreamingRepairTask implements StreamEventHandler { private static final Logger logger = LoggerFactory.getLogger(StreamingRepairTask.class); @@ -79,14 +80,14 @@ public StreamingRepairTask(SharedContext ctx, SyncState state, RepairJobDesc des this.previewKind = previewKind; } - public void run() + public StreamResultFuture execute() { logger.info("[streaming task #{}] Performing {}streaming repair of {} ranges with {}", desc.sessionId, asymmetric ? "asymmetric " : "", ranges.size(), dst); long start = approxTime.now(); StreamPlan streamPlan = createStreamPlan(dst); logger.info("[streaming task #{}] Stream plan created in {}ms", desc.sessionId, MILLISECONDS.convert(approxTime.now() - start, NANOSECONDS)); state.phase.start(); - ctx.streamExecutor().execute(streamPlan); + return ctx.streamExecutor().execute(streamPlan); } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/repair/SyncTask.java b/src/java/org/apache/cassandra/repair/SyncTask.java index a3b1a574937d..4f4da3dbf895 100644 --- a/src/java/org/apache/cassandra/repair/SyncTask.java +++ b/src/java/org/apache/cassandra/repair/SyncTask.java @@ -77,6 +77,9 @@ public SyncNodePair nodePair() */ public final void run() { + if (logger.isTraceEnabled()) + logger.trace("{} Starting sync {} <-> {}", previewKind.logPrefix(desc.sessionId), nodePair.coordinator, nodePair.peer); + startTime = ctx.clock().currentTimeMillis(); // choose a repair method based on the significance of the difference diff --git a/src/java/org/apache/cassandra/repair/ValidationManager.java b/src/java/org/apache/cassandra/repair/ValidationManager.java index e3598cd38f87..cdf9892aad3e 100644 --- a/src/java/org/apache/cassandra/repair/ValidationManager.java +++ b/src/java/org/apache/cassandra/repair/ValidationManager.java @@ -31,16 +31,21 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.compaction.CompactionInterruptedException; +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.TopPartitionTracker; import org.apache.cassandra.repair.state.ValidationState; +import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MerkleTree; import org.apache.cassandra.utils.MerkleTrees; +import org.apache.cassandra.utils.NonThrowingCloseable; public class ValidationManager implements IValidationManager { @@ -124,25 +129,33 @@ public static void doValidation(ColumnFamilyStore cfs, Validator validator) thro { state.phase.start(vi.estimatedPartitions(), vi.getEstimatedBytes()); MerkleTrees trees = createMerkleTrees(vi, validator.desc.ranges, cfs); - // validate the CF as we iterate over it - validator.prepare(cfs, trees, topPartitionCollector); - while (vi.hasNext()) + TableOperation op = vi.getCompactionIterator().getOperation(); + try (NonThrowingCloseable cls = CompactionManager.instance.active.onOperationStart(op)) { - try (UnfilteredRowIterator partition = vi.next()) + // validate the CF as we iterate over it + validator.prepare(cfs, trees, topPartitionCollector); + while (vi.hasNext()) { - validator.add(partition); - state.partitionsProcessed++; - state.bytesRead = vi.getBytesRead(); - if (state.partitionsProcessed % 1024 == 0) // update every so often - state.updated(); + try (UnfilteredRowIterator partition = vi.next()) + { + validator.add(partition); + state.partitionsProcessed++; + state.bytesRead = vi.getBytesRead(); + if (state.partitionsProcessed % 1024 == 0) // update every so often + state.updated(); + } } + validator.complete(); } - validator.complete(); } finally { cfs.metric.bytesValidated.update(state.estimatedTotalBytes); cfs.metric.partitionsValidated.update(state.partitionsProcessed); + if (validator.getPreviewKind() != PreviewKind.NONE) + { + cfs.metric.bytesPreviewed.mark(state.estimatedTotalBytes); + } if (topPartitionCollector != null) cfs.topPartitions.merge(topPartitionCollector); } @@ -198,6 +211,7 @@ public Object call() throws IOException // we need to inform the remote end of our failure, otherwise it will hang on repair forever validator.fail(e); logger.error("Validation failed.", e); + JVMStabilityInspector.inspectThrowable(e); throw e; } return this; diff --git a/src/java/org/apache/cassandra/repair/ValidationPartitionIterator.java b/src/java/org/apache/cassandra/repair/ValidationPartitionIterator.java index a8f457d782fc..88fa9d847f09 100644 --- a/src/java/org/apache/cassandra/repair/ValidationPartitionIterator.java +++ b/src/java/org/apache/cassandra/repair/ValidationPartitionIterator.java @@ -20,6 +20,7 @@ import java.util.Map; +import org.apache.cassandra.db.compaction.CompactionIterator; import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; @@ -30,4 +31,5 @@ public abstract class ValidationPartitionIterator extends AbstractUnfilteredPart public abstract long estimatedPartitions(); public abstract long getBytesRead(); public abstract Map, Long> getRangePartitionCounts(); + public abstract CompactionIterator getCompactionIterator(); } diff --git a/src/java/org/apache/cassandra/repair/autorepair/AutoRepair.java b/src/java/org/apache/cassandra/repair/autorepair/AutoRepair.java new file mode 100644 index 000000000000..04e7e30d636c --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/AutoRepair.java @@ -0,0 +1,634 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.repair.autorepair; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Uninterruptibles; + +import org.apache.cassandra.config.DurationSpec; +import org.apache.cassandra.repair.RepairCoordinator; +import org.apache.cassandra.utils.Clock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ScheduledExecutorPlus; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.Tables; +import org.apache.cassandra.service.AutoRepairService; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn; +import org.apache.cassandra.utils.concurrent.Condition; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.progress.ProgressEvent; +import org.apache.cassandra.utils.progress.ProgressEventType; +import org.apache.cassandra.utils.progress.ProgressListener; + +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.MY_TURN; +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.MY_TURN_DUE_TO_PRIORITY; +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.MY_TURN_FORCE_REPAIR; +import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition; + +/** + * AutoRepair scheduler responsible for running different types of repairs. + */ +public class AutoRepair +{ + private static final Logger logger = LoggerFactory.getLogger(AutoRepair.class); + private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS"); + + @VisibleForTesting + protected static Supplier timeFunc = Clock.Global::currentTimeMillis; + + // Sleep for 5 seconds if repair finishes quickly to flush JMX metrics; it happens only for Cassandra nodes with tiny amount of data. + public static DurationSpec.IntSecondsBound SLEEP_IF_REPAIR_FINISHES_QUICKLY = new DurationSpec.IntSecondsBound("5s"); + + @VisibleForTesting + public Map repairStates; + + @VisibleForTesting + protected Map repairExecutors; + + protected Map repairRunnableExecutors; + + @VisibleForTesting + // Auto-repair is likely to be run on multiple nodes independently, we want to avoid running multiple repair + // sessions on overlapping datasets at the same time. Shuffling keyspaces reduces the likelihood of this happening. + protected static Consumer> shuffleFunc = java.util.Collections::shuffle; + + @VisibleForTesting + protected static BiConsumer sleepFunc = Uninterruptibles::sleepUninterruptibly; + + @VisibleForTesting + public boolean isSetupDone = false; + public static AutoRepair instance = new AutoRepair(); + + public volatile boolean isShutDown = false; + + private AutoRepair() + { + // Private constructor to prevent instantiation + } + + public void setup() + { + // Ensure setup is done only once; this is only for unit tests + // For production, this method should be called only once. + synchronized (this) + { + if (isSetupDone) + { + return; + } + repairExecutors = new EnumMap<>(AutoRepairConfig.RepairType.class); + repairRunnableExecutors = new EnumMap<>(AutoRepairConfig.RepairType.class); + repairStates = new EnumMap<>(AutoRepairConfig.RepairType.class); + AutoRepairConfig config = DatabaseDescriptor.getAutoRepairConfig(); + + for (AutoRepairConfig.RepairType repairType : AutoRepairConfig.RepairType.values()) + { + repairExecutors.put(repairType, executorFactory().scheduled(false, "AutoRepair-Repair-" + repairType.getConfigName(), Thread.NORM_PRIORITY)); + repairRunnableExecutors.put(repairType, executorFactory().scheduled(false, "AutoRepair-RepairRunnable-" + repairType.getConfigName(), Thread.NORM_PRIORITY)); + repairStates.put(repairType, AutoRepairConfig.RepairType.getAutoRepairState(repairType, config)); + } + + AutoRepairUtils.setup(); + + for (AutoRepairConfig.RepairType repairType : AutoRepairConfig.RepairType.values()) + { + if (config.isAutoRepairEnabled(repairType)) + AutoRepairService.instance.checkCanRun(repairType); + + repairExecutors.get(repairType).scheduleWithFixedDelay( + () -> repair(repairType), + config.getInitialSchedulerDelay(repairType).toSeconds(), + config.getRepairCheckInterval().toSeconds(), + TimeUnit.SECONDS); + } + isSetupDone = true; + } + } + + /** + * @return The current observed system time in ms. + */ + public long currentTimeMs() + { + return timeFunc.get(); + } + + // repair runs a repair session of the given type synchronously. + public void repair(AutoRepairConfig.RepairType repairType) + { + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + if (!config.isAutoRepairEnabled(repairType)) + { + logger.debug("Auto-repair is disabled for repair type {}", repairType); + return; + } + if (!config.isMixedMajorVersionRepairEnabled() && AutoRepairUtils.hasMultipleLiveMajorVersions()) + { + logger.info("Auto-repair is disabled when nodes in the cluster have different major versions"); + return; + } + if (AutoRepairUtils.hasNodesBelowMinimumVersion()) + { + logger.info("Auto-repair is disabled because some nodes are running unsupported versions " + + "(auto-repair requires all nodes to be above version {})", + AutoRepairUtils.LAST_UNSUPPORTED_VERSION_FOR_AUTO_REPAIR); + return; + } + AutoRepairService.instance.checkCanRun(repairType); + AutoRepairState repairState = repairStates.get(repairType); + try + { + String localDC = DatabaseDescriptor.getLocalDataCenter(); + if (config.getIgnoreDCs(repairType).contains(localDC)) + { + logger.info("Not running repair as this node belongs to datacenter {}", localDC); + return; + } + + // refresh the longest unrepaired node + repairState.setLongestUnrepairedNode(AutoRepairUtils.getHostWithLongestUnrepairTime(repairType)); + + //consistency level to use for local query + UUID myId = StorageService.instance.getTokenMetadata().getHostId(FBUtilities.getBroadcastAddressAndPort()); + if (myId == null) + { + logger.warn("Could not resolve local host ID, skipping repair cycle for repair type {}", repairType); + return; + } + + // If it's too soon to run repair, don't bother checking if it's our turn. + if (tooSoonToRunRepair(repairType, repairState, config, myId)) + { + return; + } + + RepairTurn turn = AutoRepairUtils.myTurnToRunRepair(repairType, myId); + if (turn == MY_TURN || turn == MY_TURN_DUE_TO_PRIORITY || turn == MY_TURN_FORCE_REPAIR) + { + repairState.recordTurn(turn); + repairState.setBytesAlreadyRepaired(0L); + repairState.setKeyspaceRepairPlansAlreadyRepaired(0); + // For normal auto repair, we will use primary range only repairs (Repair with -pr option). + // For some cases, we may set the auto_repair_primary_token_range_only flag to false then we will do repair + // without -pr. We may also do force repair for certain node that we want to repair all the data on one node + // When doing force repair, we want to repair without -pr. + boolean primaryRangeOnly = config.getRepairPrimaryTokenRangeOnly(repairType) + && turn != MY_TURN_FORCE_REPAIR; + + long startTimeInMillis = timeFunc.get(); + logger.info("My host id: {}, my turn to run repair...repair primary-ranges only? {}", myId, + config.getRepairPrimaryTokenRangeOnly(repairType)); + AutoRepairUtils.updateStartAutoRepairHistory(repairType, myId, timeFunc.get(), turn); + + repairState.setRepairKeyspaceCount(0); + repairState.setRepairInProgress(true); + repairState.setTotalTablesConsideredForRepair(0); + repairState.setTotalMVTablesConsideredForRepair(0); + + CollectedRepairStats collectedRepairStats = new CollectedRepairStats(); + + List keyspaces = new ArrayList<>(); + Keyspace.all().forEach(keyspaces::add); + // Filter out keyspaces and tables to repair and group into a map by keyspace. + Map> keyspacesAndTablesToRepair = new LinkedHashMap<>(); + for (Keyspace keyspace : keyspaces) + { + if (!AutoRepairUtils.shouldConsiderKeyspace(keyspace)) + { + continue; + } + List tablesToBeRepairedList = retrieveTablesToBeRepaired(keyspace, config, repairType, repairState, collectedRepairStats); + keyspacesAndTablesToRepair.put(keyspace.getName(), tablesToBeRepairedList); + } + + // Separate out the keyspaces and tables to repair based on their priority, with each repair plan representing a uniquely occuring priority. + List repairPlans = PrioritizedRepairPlan.build(keyspacesAndTablesToRepair, repairType, shuffleFunc, primaryRangeOnly); + repairState.updateRepairScheduleStatistics(repairPlans); + + // calculate the repair assignments for each priority:keyspace. + Iterator repairAssignmentsIterator = config.getTokenRangeSplitterInstance(repairType).getRepairAssignments(primaryRangeOnly, repairPlans); + + int keyspaceRepairAssignmentsAlreadyRepaired = 0; + while (repairAssignmentsIterator.hasNext()) + { + KeyspaceRepairAssignments repairAssignments = repairAssignmentsIterator.next(); + List assignments = repairAssignments.getRepairAssignments(); + if (assignments.isEmpty()) + { + keyspaceRepairAssignmentsAlreadyRepaired++; + logger.info("Skipping repairs for priorityBucket={} for keyspace={} since it yielded no assignments", repairAssignments.getPriority(), repairAssignments.getKeyspaceName()); + continue; + } + + logger.info("Submitting repairs for priorityBucket={} for keyspace={} with assignmentCount={} and keyspaceRepairAssignmentsAlreadyRepaired={}/{}", + repairAssignments.getPriority(), repairAssignments.getKeyspaceName(), repairAssignments.getRepairAssignments().size(), + keyspaceRepairAssignmentsAlreadyRepaired, repairState.getTotalKeyspaceRepairPlansToRepair()); + repairKeyspace(repairType, primaryRangeOnly, repairAssignments.getKeyspaceName(), repairAssignments.getRepairAssignments(), collectedRepairStats); + keyspaceRepairAssignmentsAlreadyRepaired++; + repairState.setKeyspaceRepairPlansAlreadyRepaired(keyspaceRepairAssignmentsAlreadyRepaired); + } + + cleanupAndUpdateStats(turn, repairType, repairState, myId, startTimeInMillis, collectedRepairStats); + } + else + { + logger.info("Waiting for my turn..."); + } + } + catch (Exception e) + { + logger.error("Exception in autorepair:", e); + } + } + + private void repairKeyspace(AutoRepairConfig.RepairType repairType, boolean primaryRangeOnly, String keyspaceName, List repairAssignments, CollectedRepairStats collectedRepairStats) + { + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + AutoRepairState repairState = repairStates.get(repairType); + + // evaluate over each keyspace's repair assignments. + repairState.setRepairKeyspaceCount(repairState.getRepairKeyspaceCount() + 1); + + int totalRepairAssignments = repairAssignments.size(); + long keyspaceStartTime = timeFunc.get(); + RepairAssignment previousAssignment = null; + long tableStartTime = timeFunc.get(); + int totalProcessedAssignments = 0; + Set> ranges = new HashSet<>(); + long bytesAlreadyRepaired = repairState.getBytesAlreadyRepaired(); + for (RepairAssignment curRepairAssignment : repairAssignments) + { + try + { + totalProcessedAssignments++; + boolean repairOneTableAtATime = !config.getRepairByKeyspace(repairType); + if (previousAssignment != null && repairOneTableAtATime && !previousAssignment.tableNames.equals(curRepairAssignment.tableNames)) + { + // In the repair assignment, all the tables are appended sequentially. + // Check if we have a different table, and if so, we should reset the table start time. + tableStartTime = timeFunc.get(); + } + previousAssignment = curRepairAssignment; + if (!config.isAutoRepairEnabled(repairType)) + { + logger.error("Auto-repair for type {} is disabled hence not running repair", repairType); + repairState.setRepairInProgress(false); + return; + } + if (AutoRepairUtils.keyspaceMaxRepairTimeExceeded(repairType, keyspaceStartTime, repairAssignments.size())) + { + collectedRepairStats.skippedTokenRanges += totalRepairAssignments - totalProcessedAssignments; + logger.info("Keyspace took too much time to repair hence skipping it {}", + keyspaceName); + break; + } + if (repairOneTableAtATime && AutoRepairUtils.tableMaxRepairTimeExceeded(repairType, tableStartTime)) + { + collectedRepairStats.skippedTokenRanges += 1; + logger.info("Table took too much time to repair hence skipping it table name {}.{}, token range {}", + keyspaceName, curRepairAssignment.tableNames, curRepairAssignment.tokenRange); + continue; + } + + Range tokenRange = curRepairAssignment.getTokenRange(); + logger.debug("Current Token Left side {}, right side {}", + tokenRange.left.toString(), + tokenRange.right.toString()); + + ranges.add(curRepairAssignment.getTokenRange()); + if ((totalProcessedAssignments % config.getRepairThreads(repairType) == 0) || + (totalProcessedAssignments == totalRepairAssignments)) + { + boolean success = false; + int retryCount = 0; + Future f = null; + while (retryCount <= config.getRepairMaxRetries(repairType)) + { + RepairCoordinator task = repairState.getRepairRunnable(keyspaceName, + Lists.newArrayList(curRepairAssignment.getTableNames()), + ranges, primaryRangeOnly); + RepairProgressListener listener = new RepairProgressListener(repairType); + task.addProgressListener(listener); + f = repairRunnableExecutors.get(repairType).submit(task); + try + { + long jobStartTime = timeFunc.get(); + listener.await(config.getRepairSessionTimeout(repairType)); + success = listener.isSuccess(); + soakAfterRepair(jobStartTime, config.getRepairTaskMinDuration().toMilliseconds()); + } + catch (InterruptedException e) + { + logger.error("Exception in cond await:", e); + } + if (success) + { + break; + } + else if (retryCount < config.getRepairMaxRetries(repairType)) + { + boolean cancellationStatus = f.cancel(true); + logger.warn("Repair failed for range {}-{} for {} tables {} with cancellationStatus: {} retrying after {} seconds...", + tokenRange.left, tokenRange.right, + keyspaceName, curRepairAssignment.getTableNames(), + cancellationStatus, config.getRepairRetryBackoff(repairType).toSeconds()); + sleepFunc.accept(config.getRepairRetryBackoff(repairType).toSeconds(), TimeUnit.SECONDS); + } + retryCount++; + } + //check repair status + if (success) + { + logger.info("Repair completed for range {}-{} for {} tables {}, total assignments: {}," + + "processed assignments: {}", tokenRange.left, tokenRange.right, + keyspaceName, curRepairAssignment.getTableNames(), totalRepairAssignments, totalProcessedAssignments); + collectedRepairStats.succeededTokenRanges += ranges.size(); + } + else + { + boolean cancellationStatus = true; + if (f != null) + { + cancellationStatus = f.cancel(true); + } + //in the future we can add retry, etc. + logger.error("Repair failed for range {}-{} for {} tables {} after {} retries, total assignments: {}," + + "processed assignments: {}, cancellationStatus: {}", tokenRange.left, tokenRange.right, keyspaceName, + curRepairAssignment.getTableNames(), retryCount, totalRepairAssignments, totalProcessedAssignments, cancellationStatus); + collectedRepairStats.failedTokenRanges += ranges.size(); + } + ranges.clear(); + } + bytesAlreadyRepaired += curRepairAssignment.getEstimatedBytes(); + repairState.setBytesAlreadyRepaired(bytesAlreadyRepaired); + logger.info("Repair completed for {} tables {}, range {}, bytesAlreadyRepaired {}/{}", + keyspaceName, curRepairAssignment.getTableNames(), curRepairAssignment.getTokenRange(), bytesAlreadyRepaired, repairState.getTotalBytesToRepair()); + } + catch (Exception e) + { + logger.error("Exception while repairing keyspace {}:", keyspaceName, e); + } + } + } + + private boolean tooSoonToRunRepair(AutoRepairConfig.RepairType repairType, AutoRepairState repairState, AutoRepairConfig config, UUID myId) + { + if (repairState.getLastRepairTime() == 0) + { + // the node has either just booted or has not run repair before, + // we should check for the node's repair history in the DB + repairState.setLastRepairTime(AutoRepairUtils.getLastRepairTimeForNode(repairType, myId)); + } + /* + * check if it is too soon to run repair. one of the reason we + * should not run frequent repair is that repair triggers + * memtable flush + */ + long timeElapsedSinceLastRepair = TimeUnit.MILLISECONDS.toSeconds(timeFunc.get() - repairState.getLastRepairTime()); + if (timeElapsedSinceLastRepair < config.getRepairMinInterval(repairType).toSeconds()) + { + logger.info("Too soon to run repair, last repair was done {} seconds ago", + timeElapsedSinceLastRepair); + return true; + } + return false; + } + + private List retrieveTablesToBeRepaired(Keyspace keyspace, AutoRepairConfig config, AutoRepairConfig.RepairType repairType, AutoRepairState repairState, CollectedRepairStats collectedRepairStats) + { + Tables tables = keyspace.getMetadata().tables; + List tablesToBeRepaired = new ArrayList<>(); + Iterator iter = tables.iterator(); + while (iter.hasNext()) + { + repairState.setTotalTablesConsideredForRepair(repairState.getTotalTablesConsideredForRepair() + 1); + TableMetadata tableMetadata = iter.next(); + String tableName = tableMetadata.name; + + ColumnFamilyStore columnFamilyStore = keyspace.getColumnFamilyStore(tableName); + if (!columnFamilyStore.metadata().params.autoRepair.repairEnabled(repairType)) + { + logger.info("Repair is disabled for keyspace {} for tables: {}", keyspace.getName(), tableName); + repairState.setTotalDisabledTablesRepairCount(repairState.getTotalDisabledTablesRepairCount() + 1); + collectedRepairStats.skippedTables++; + continue; + } + + // this is done to make autorepair safe as running repair on table with more sstables + // may have its own challenges + int totalSSTables = columnFamilyStore.getLiveSSTables().size(); + if (totalSSTables > config.getRepairSSTableCountHigherThreshold(repairType)) + { + logger.info("Too many SSTables for repair for table {}.{}" + + "totalSSTables {}", keyspace.getName(), tableName, totalSSTables); + collectedRepairStats.skippedTables++; + continue; + } + + tablesToBeRepaired.add(tableName); + + // See if we should repair MVs as well that are associated with this given table + List mvs = AutoRepairUtils.getAllMVs(repairType, keyspace, tableMetadata); + if (!mvs.isEmpty()) + { + tablesToBeRepaired.addAll(mvs); + repairState.setTotalMVTablesConsideredForRepair(repairState.getTotalMVTablesConsideredForRepair() + mvs.size()); + } + } + return tablesToBeRepaired; + } + + private void cleanupAndUpdateStats(RepairTurn turn, AutoRepairConfig.RepairType repairType, AutoRepairState repairState, UUID myId, + long startTimeInMillis, CollectedRepairStats collectedRepairStats) throws InterruptedException + { + //if it was due to priority then remove it now + if (turn == MY_TURN_DUE_TO_PRIORITY) + { + logger.info("Remove current host from priority list"); + AutoRepairUtils.removePriorityStatus(repairType, myId); + } + long repairScheduleElapsedInMillis = timeFunc.get() - startTimeInMillis; + if (repairScheduleElapsedInMillis < SLEEP_IF_REPAIR_FINISHES_QUICKLY.toMilliseconds()) + { + //If repair finished quickly, happens for Cassandra cluster with empty (or tiny) data, in such cases, + //wait for some duration so that the JMX metrics can detect the repairInProgress + logger.info("Wait for {}ms for repair type {}.", SLEEP_IF_REPAIR_FINISHES_QUICKLY.toMilliseconds() - repairScheduleElapsedInMillis, repairType); + Thread.sleep(SLEEP_IF_REPAIR_FINISHES_QUICKLY.toMilliseconds() - repairScheduleElapsedInMillis); + } + repairState.setFailedTokenRangesCount(collectedRepairStats.failedTokenRanges); + repairState.setSucceededTokenRangesCount(collectedRepairStats.succeededTokenRanges); + repairState.setSkippedTokenRangesCount(collectedRepairStats.skippedTokenRanges); + repairState.setSkippedTablesCount(collectedRepairStats.skippedTables); + repairState.setNodeRepairTimeInSec((int) TimeUnit.MILLISECONDS.toSeconds(timeFunc.get() - startTimeInMillis)); + long timeInHours = TimeUnit.SECONDS.toHours(repairState.getNodeRepairTimeInSec()); + logger.info("Local {} repair time {} hour(s), stats: repairKeyspaceCount {}, " + + "repairTokenRangesSuccessCount {}, repairTokenRangesFailureCount {}, " + + "repairTokenRangesSkipCount {}, repairTablesSkipCount {}", repairType, timeInHours, repairState.getRepairKeyspaceCount(), + repairState.getSucceededTokenRangesCount(), repairState.getFailedTokenRangesCount(), + repairState.getSkippedTokenRangesCount(), repairState.getSkippedTablesCount()); + if (repairState.getLastRepairTime() != 0) + { + repairState.setClusterRepairTimeInSec((int) TimeUnit.MILLISECONDS.toSeconds(timeFunc.get() - + repairState.getLastRepairTime())); + logger.info("Cluster repair time for repair type {}: {} day(s)", repairType, + TimeUnit.SECONDS.toDays(repairState.getClusterRepairTimeInSec())); + } + repairState.setLastRepairTime(timeFunc.get()); + repairState.setRepairInProgress(false); + + AutoRepairUtils.updateFinishAutoRepairHistory(repairType, myId, timeFunc.get()); + } + + public AutoRepairState getRepairState(AutoRepairConfig.RepairType repairType) + { + return repairStates.get(repairType); + } + + private void soakAfterRepair(long startTimeMilis, long minDurationMilis) + { + long currentTime = timeFunc.get(); + long timeElapsed = currentTime - startTimeMilis; + if (timeElapsed < minDurationMilis) + { + long timeToSoak = minDurationMilis - timeElapsed; + logger.info("Soaking for {} ms after repair", timeToSoak); + sleepFunc.accept(timeToSoak, TimeUnit.MILLISECONDS); + } + } + + static class CollectedRepairStats + { + int failedTokenRanges = 0; + int succeededTokenRanges = 0; + int skippedTokenRanges = 0; + int skippedTables = 0; + } + + @VisibleForTesting + protected static class RepairProgressListener implements ProgressListener + { + private final AutoRepairConfig.RepairType repairType; + @VisibleForTesting + protected boolean success; + @VisibleForTesting + protected final Condition condition = newOneTimeCondition(); + + public RepairProgressListener(AutoRepairConfig.RepairType repairType) + { + this.repairType = repairType; + } + + public void await(DurationSpec.IntSecondsBound repairSessionTimeout) throws InterruptedException + { + //if for some reason we don't hear back on repair progress for sometime + if (!condition.await(repairSessionTimeout.to(TimeUnit.SECONDS), TimeUnit.SECONDS)) + { + success = false; + } + } + + public boolean isSuccess() + { + return success; + } + + @Override + public void progress(String tag, ProgressEvent event) + { + ProgressEventType type = event.getType(); + String message = String.format("[%s] %s", format.format(timeFunc.get()), event.getMessage()); + if (type == ProgressEventType.ERROR) + { + logger.error("Repair failure for repair {}: {}", repairType.toString(), message); + success = false; + condition.signalAll(); + } + if (type == ProgressEventType.PROGRESS) + { + message = message + " (progress: " + (int) event.getProgressPercentage() + "%)"; + logger.debug("Repair progress for repair {}: {}", repairType.toString(), message); + } + if (type == ProgressEventType.COMPLETE) + { + logger.debug("Repair completed for repair {}: {}", repairType.toString(), message); + success = true; + condition.signalAll(); + } + } + } + + public synchronized void shutdownBlocking() throws ExecutionException, InterruptedException + { + if (!isSetupDone) + { + // By default, executors within AutoRepair are not initialized as the feature is opt-in. + // If the AutoRepair has not been set up, then there is no need to worry about shutting it down + return; + } + if (isShutDown) + { + throw new IllegalStateException("AutoRepair has already been shut down"); + } + isShutDown = true; + for (AutoRepairConfig.RepairType repairType : AutoRepairConfig.RepairType.values()) + { + repairRunnableExecutors.get(repairType).shutdown(); + repairExecutors.get(repairType).shutdown(); + } + logger.info("Paused AutoRepair"); + } + + public Map getRepairExecutors() + { + return repairExecutors; + } + + public Map getRepairRunnableExecutors() + { + return repairRunnableExecutors; + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java new file mode 100644 index 000000000000..5df1dddb627d --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java @@ -0,0 +1,619 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.repair.autorepair; + +import java.io.Serializable; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import javax.annotation.Nonnull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DurationSpec; +import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.FBUtilities; + +/** + * Defines configurations for AutoRepair. + */ +public class AutoRepairConfig implements Serializable +{ + // Enable/Disable the auto-repair scheduler. + // If set to false, the scheduler thread will not be started. + // If set to true, the repair scheduler thread will be created. The thread will + // check for secondary configuration available for each repair type (full, incremental, + // and preview_repaired), and based on that, it will schedule repairs. + public volatile Boolean enabled; + // Time interval between successive checks to see if ongoing repairs are complete or if it is time to schedule + // repairs. + public final DurationSpec.IntSecondsBound repair_check_interval = new DurationSpec.IntSecondsBound("5m"); + // The scheduler needs to adjust its order when nodes leave the ring. Deleted hosts are tracked in metadata + // for a specified duration to ensure they are indeed removed before adjustments are made to the schedule. + public volatile DurationSpec.IntSecondsBound history_clear_delete_hosts_buffer_interval = new DurationSpec.IntSecondsBound("2h"); + // Minimum duration for the execution of a single repair task. This prevents the scheduler from overwhelming + // the node by scheduling too many repair tasks in a short period of time. + public volatile DurationSpec.LongSecondsBound repair_task_min_duration = new DurationSpec.LongSecondsBound("5s"); + // by default repair is disabled if there are mixed major versions detected, but you can enable it using this flag + public volatile boolean mixed_major_version_repair_enabled = false; + + // global_settings overides Options.defaultOptions for all repair types + public volatile Options global_settings; + + public static final Class DEFAULT_SPLITTER = RepairTokenRangeSplitter.class; + + // make transient so gets consturcted in the implementation. + private final transient Map tokenRangeSplitters = new EnumMap<>(RepairType.class); + + public enum RepairType implements Serializable + { + FULL, + INCREMENTAL, + PREVIEW_REPAIRED; + + private final String configName; + + RepairType() + { + this.configName = name().toLowerCase(); + } + + /** + * @return Format of the repair type as it should be represented in configuration. + * Canonically this is the enum name in lowerCase. + */ + public String getConfigName() + { + return configName; + } + + public static AutoRepairState getAutoRepairState(RepairType repairType, AutoRepairConfig config) + { + switch (repairType) + { + case FULL: + return new FullRepairState(config); + case INCREMENTAL: + return new IncrementalRepairState(config); + case PREVIEW_REPAIRED: + return new PreviewRepairedState(config); + } + + throw new IllegalArgumentException("Invalid repair type: " + repairType); + } + + /** + * Case-insensitive parsing of the repair type string into {@link RepairType} + * + * @param repairTypeStr the repair type string + * @return the {@link RepairType} represented by the {@code repairTypeStr} string + * @throws IllegalArgumentException when the repair type string does not match any repair type + */ + public static RepairType parse(String repairTypeStr) + { + return RepairType.valueOf(Objects.requireNonNull(repairTypeStr, "repairTypeStr cannot be null").toUpperCase()); + } + } + + // repair_type_overrides overrides the global_settings for a specific repair type. String used as key instead + // of enum to allow lower case key in yaml. + public volatile ConcurrentMap repair_type_overrides = Maps.newConcurrentMap(); + + public AutoRepairConfig() + { + this(false); + } + + public AutoRepairConfig(boolean enabled) + { + this.enabled = enabled; + global_settings = Options.getDefaultOptions(); + } + + public DurationSpec.IntSecondsBound getRepairCheckInterval() + { + return repair_check_interval; + } + + public boolean isAutoRepairSchedulingEnabled() + { + return CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean() && enabled; + } + + @VisibleForTesting + public void setAutoRepairSchedulingEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public boolean isMixedMajorVersionRepairEnabled() + { + return mixed_major_version_repair_enabled; + } + + public DurationSpec.IntSecondsBound getAutoRepairHistoryClearDeleteHostsBufferInterval() + { + return history_clear_delete_hosts_buffer_interval; + } + + public void startScheduler() + { + enabled = true; + AutoRepair.instance.setup(); + } + + public void setAutoRepairHistoryClearDeleteHostsBufferInterval(String duration) + { + history_clear_delete_hosts_buffer_interval = new DurationSpec.IntSecondsBound(duration); + } + + public DurationSpec.LongSecondsBound getRepairTaskMinDuration() + { + return repair_task_min_duration; + } + + public void setRepairTaskMinDuration(String duration) + { + repair_task_min_duration = new DurationSpec.LongSecondsBound(duration); + } + + public boolean isAutoRepairEnabled(RepairType repairType) + { + return enabled && applyOverrides(repairType, opt -> opt.enabled); + } + + public void setAutoRepairEnabled(RepairType repairType, boolean enabled) + { + getOptions(repairType).enabled = enabled; + } + + public void setRepairByKeyspace(RepairType repairType, boolean repairByKeyspace) + { + getOptions(repairType).repair_by_keyspace = repairByKeyspace; + } + + public boolean getRepairByKeyspace(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.repair_by_keyspace); + } + + public int getRepairThreads(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.number_of_repair_threads); + } + + public void setRepairThreads(RepairType repairType, int repairThreads) + { + getOptions(repairType).number_of_repair_threads = repairThreads; + } + + public DurationSpec.IntSecondsBound getRepairMinInterval(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.min_repair_interval); + } + + public void setRepairMinInterval(RepairType repairType, String minRepairInterval) + { + getOptions(repairType).min_repair_interval = new DurationSpec.IntSecondsBound(minRepairInterval); + } + + public int getRepairSSTableCountHigherThreshold(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.sstable_upper_threshold); + } + + public void setRepairSSTableCountHigherThreshold(RepairType repairType, int sstableHigherThreshold) + { + getOptions(repairType).sstable_upper_threshold = sstableHigherThreshold; + } + + public DurationSpec.IntSecondsBound getAutoRepairTableMaxRepairTime(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.table_max_repair_time); + } + + public void setAutoRepairTableMaxRepairTime(RepairType repairType, String autoRepairTableMaxRepairTime) + { + getOptions(repairType).table_max_repair_time = new DurationSpec.IntSecondsBound(autoRepairTableMaxRepairTime); + } + + public Set getIgnoreDCs(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.ignore_dcs); + } + + public void setIgnoreDCs(RepairType repairType, Set ignoreDCs) + { + getOptions(repairType).ignore_dcs = ignoreDCs; + } + + public boolean getRepairPrimaryTokenRangeOnly(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.repair_primary_token_range_only); + } + + public void setRepairPrimaryTokenRangeOnly(RepairType repairType, boolean primaryTokenRangeOnly) + { + getOptions(repairType).repair_primary_token_range_only = primaryTokenRangeOnly; + } + + public int getParallelRepairPercentage(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.parallel_repair_percentage); + } + + public void setParallelRepairPercentage(RepairType repairType, int percentage) + { + getOptions(repairType).parallel_repair_percentage = percentage; + } + + public int getParallelRepairCount(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.parallel_repair_count); + } + + public void setParallelRepairCount(RepairType repairType, int count) + { + getOptions(repairType).parallel_repair_count = count; + } + + public boolean getAllowParallelReplicaRepair(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.allow_parallel_replica_repair); + } + + public void setAllowParallelReplicaRepair(RepairType repairType, boolean enabled) + { + getOptions(repairType).allow_parallel_replica_repair = enabled; + } + + public boolean getAllowParallelReplicaRepairAcrossSchedules(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.allow_parallel_replica_repair_across_schedules); + } + + public void setAllowParallelReplicaRepairAcrossSchedules(RepairType repairType, boolean enabled) + { + getOptions(repairType).allow_parallel_replica_repair_across_schedules = enabled; + } + + public boolean getMaterializedViewRepairEnabled(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.materialized_view_repair_enabled); + } + + public void setMaterializedViewRepairEnabled(RepairType repairType, boolean enabled) + { + getOptions(repairType).materialized_view_repair_enabled = enabled; + } + + public void setForceRepairNewNode(RepairType repairType, boolean forceRepairNewNode) + { + getOptions(repairType).force_repair_new_node = forceRepairNewNode; + } + + public boolean getForceRepairNewNode(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.force_repair_new_node); + } + + public ParameterizedClass getTokenRangeSplitter(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.token_range_splitter); + } + + public IAutoRepairTokenRangeSplitter getTokenRangeSplitterInstance(RepairType repairType) + { + return tokenRangeSplitters.computeIfAbsent(repairType, + key -> newAutoRepairTokenRangeSplitter(key, getTokenRangeSplitter(key))); + } + + public void setInitialSchedulerDelay(RepairType repairType, String initialSchedulerDelay) + { + getOptions(repairType).initial_scheduler_delay = new DurationSpec.IntSecondsBound(initialSchedulerDelay); + } + + public DurationSpec.IntSecondsBound getInitialSchedulerDelay(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.initial_scheduler_delay); + } + + public DurationSpec.IntSecondsBound getRepairSessionTimeout(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.repair_session_timeout); + } + + public void setRepairSessionTimeout(RepairType repairType, String repairSessionTimeout) + { + getOptions(repairType).repair_session_timeout = new DurationSpec.IntSecondsBound(repairSessionTimeout); + } + + public int getRepairMaxRetries(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.repair_max_retries); + } + + public void setRepairMaxRetries(RepairType repairType, int maxRetries) + { + getOptions(repairType).repair_max_retries = maxRetries; + } + + public DurationSpec.LongSecondsBound getRepairRetryBackoff(RepairType repairType) + { + return applyOverrides(repairType, opt -> opt.repair_retry_backoff); + } + + public void setRepairRetryBackoff(RepairType repairType, String interval) + { + getOptions(repairType).repair_retry_backoff = new DurationSpec.LongSecondsBound(interval); + } + + public boolean getMixedMajorVersionRepairEnabled() + { + return this.mixed_major_version_repair_enabled; + } + + public void setMixedMajorVersionRepairEnabled(boolean enabled) + { + this.mixed_major_version_repair_enabled = enabled; + } + + @VisibleForTesting + static IAutoRepairTokenRangeSplitter newAutoRepairTokenRangeSplitter(RepairType repairType, ParameterizedClass parameterizedClass) throws ConfigurationException + { + try + { + Class tokenRangeSplitterClass; + final String className; + if (parameterizedClass.class_name != null && !parameterizedClass.class_name.isEmpty()) + { + className = parameterizedClass.class_name.contains(".") ? + parameterizedClass.class_name : + "org.apache.cassandra.repair.autorepair." + parameterizedClass.class_name; + tokenRangeSplitterClass = + FBUtilities.classForNameWithoutInitialization(className, + "token_range_splitter", + IAutoRepairTokenRangeSplitter.class); + } + else + { + // If token_range_splitter.class_name is not defined, just use default, this is for convenience. + tokenRangeSplitterClass = AutoRepairConfig.DEFAULT_SPLITTER; + } + try + { + Map parameters = parameterizedClass.parameters != null ? parameterizedClass.parameters : Collections.emptyMap(); + // first attempt to initialize with RepairType and Map arguments. + return tokenRangeSplitterClass.getConstructor(RepairType.class, Map.class).newInstance(repairType, parameters); + } + catch (NoSuchMethodException nsme) + { + // fall back on no argument constructor. + return tokenRangeSplitterClass.getConstructor().newInstance(); + } + } + catch (Exception ex) + { + throw new ConfigurationException("Unable to create instance of IAutoRepairTokenRangeSplitter", ex); + } + } + + // Options configures auto-repair behavior for a given repair type. + // All fields can be modified dynamically. + public static class Options implements Serializable + { + // defaultOptions defines the default auto-repair behavior when no overrides are defined + @VisibleForTesting + private static Map defaultOptions; + + private static Map initializeDefaultOptions() + { + Map options = new EnumMap<>(AutoRepairConfig.RepairType.class); + options.put(AutoRepairConfig.RepairType.FULL, getDefaultOptions()); + options.put(RepairType.INCREMENTAL, getDefaultOptions()); + options.put(RepairType.PREVIEW_REPAIRED, getDefaultOptions()); + + return options; + } + + public static Map getDefaultOptionsMap() + { + if (defaultOptions == null) + { + synchronized (AutoRepairConfig.class) + { + if (defaultOptions == null) + { + defaultOptions = initializeDefaultOptions(); + } + } + } + return defaultOptions; + } + + public Options() + { + } + + @VisibleForTesting + protected static Options getDefaultOptions() + { + Options opts = new Options(); + + opts.enabled = false; + opts.repair_by_keyspace = true; + opts.number_of_repair_threads = 1; + opts.parallel_repair_count = 3; + opts.parallel_repair_percentage = 3; + opts.allow_parallel_replica_repair = false; + opts.allow_parallel_replica_repair_across_schedules = true; + opts.sstable_upper_threshold = 50000; + opts.ignore_dcs = new HashSet<>(); + opts.repair_primary_token_range_only = true; + opts.force_repair_new_node = false; + opts.table_max_repair_time = new DurationSpec.IntSecondsBound("6h"); + opts.materialized_view_repair_enabled = false; + opts.token_range_splitter = new ParameterizedClass(DEFAULT_SPLITTER.getName(), Collections.emptyMap()); + opts.initial_scheduler_delay = new DurationSpec.IntSecondsBound("5m"); + opts.repair_session_timeout = new DurationSpec.IntSecondsBound("3h"); + opts.min_repair_interval = new DurationSpec.IntSecondsBound("24h"); + + return opts; + } + + // Enable/Disable full or incremental or previewed_repair auto repair + public volatile Boolean enabled; + // If true, attempts to group tables in the same keyspace into one repair; otherwise, each table is repaired + // individually. + public volatile Boolean repair_by_keyspace; + // Number of threads to use for each repair job scheduled by the scheduler. Similar to the -j option in nodetool + // repair. + public volatile Integer number_of_repair_threads; + // Number of nodes running repair in parallel. If parallel_repair_percentage is set, the larger value is used. + public volatile Integer parallel_repair_count; + // Percentage of nodes in the cluster running repair in parallel. If parallel_repair_count is set, the larger value + // is used. Recommendation is that the repair cycle on the cluster should finish within gc_grace_seconds. + public volatile Integer parallel_repair_percentage; + // Whether to allow a node to take its turn running repair while one or more of its replicas are running repair. + // Defaults to false, as running repairs concurrently on replicas can increase load and also cause + // anticompaction conflicts while running incremental repair. + public volatile Boolean allow_parallel_replica_repair; + // An addition to allow_parallel_replica_repair that also blocks repairs when replicas (including this node itself) + // are repairing in any schedule. For example, if a replica is executing full repairs, a value of false will + // prevent starting incremental repairs for this node. Defaults to true and is only evaluated when + // allow_parallel_replica_repair is false. + public volatile Boolean allow_parallel_replica_repair_across_schedules; + // Threshold to skip repairing tables with too many SSTables. Defaults to 10,000 SSTables to avoid penalizing good + // tables. + public volatile Integer sstable_upper_threshold; + // Minimum duration between repairing the same node again. This is useful for tiny clusters, such as + // clusters with 5 nodes that finish repairs quickly. The default is 24 hours. This means that if the scheduler + // completes one round on all nodes in less than 24 hours, it will not start a new repair round on a given node + // until 24 hours have passed since the last repair. + public volatile DurationSpec.IntSecondsBound min_repair_interval; + // Avoid running repairs in specific data centers. By default, repairs run in all data centers. Specify data + // centers to exclude in this list. Note that repair sessions will still consider all replicas from excluded + // data centers. Useful if you have keyspaces that are not replicated in certain data centers, and you want to + // not run repair schedule in certain data centers. + public volatile Set ignore_dcs; + // Repair only the primary ranges owned by a node. Equivalent to the -pr option in nodetool repair. Defaults + // to true. General advice is to keep this true. + public volatile Boolean repair_primary_token_range_only; + // Force immediate repair on new nodes after they join the ring. + public volatile Boolean force_repair_new_node; + // Maximum time allowed for repairing one table on a given node. If exceeded, the repair proceeds to the + // next table. + public volatile DurationSpec.IntSecondsBound table_max_repair_time; + // Repairs materialized views if true. + public volatile Boolean materialized_view_repair_enabled; + /** + * Splitter implementation to use for generating repair assignments. + *

    + * The default is {@link RepairTokenRangeSplitter}. The class should implement {@link IAutoRepairTokenRangeSplitter} + * and have a constructor accepting ({@link RepairType}, {@link java.util.Map}) + */ + public volatile ParameterizedClass token_range_splitter; + // After a node restart, wait for this much delay before scheduler starts running repair; this is to avoid starting repair immediately after a node restart. + public volatile DurationSpec.IntSecondsBound initial_scheduler_delay; + // Timeout for retrying stuck repair sessions. + public volatile DurationSpec.IntSecondsBound repair_session_timeout; + // Maximum number of retries for a repair session. + public volatile Integer repair_max_retries = 3; + // Backoff time before retrying a repair session. + public volatile DurationSpec.LongSecondsBound repair_retry_backoff = new DurationSpec.LongSecondsBound("30s"); + + public String toString() + { + return "Options{" + + "enabled=" + enabled + + ", repair_by_keyspace=" + repair_by_keyspace + + ", number_of_repair_threads=" + number_of_repair_threads + + ", parallel_repair_count=" + parallel_repair_count + + ", parallel_repair_percentage=" + parallel_repair_percentage + + ", allow_parallel_replica_repair=" + allow_parallel_replica_repair + + ", allow_parallel_replica_repair_across_schedules=" + allow_parallel_replica_repair_across_schedules + + ", sstable_upper_threshold=" + sstable_upper_threshold + + ", min_repair_interval=" + min_repair_interval + + ", ignore_dcs=" + ignore_dcs + + ", repair_primary_token_range_only=" + repair_primary_token_range_only + + ", force_repair_new_node=" + force_repair_new_node + + ", table_max_repair_time=" + table_max_repair_time + + ", materialized_view_repair_enabled=" + materialized_view_repair_enabled + + ", token_range_splitter=" + token_range_splitter + + ", intial_scheduler_delay=" + initial_scheduler_delay + + ", repair_session_timeout=" + repair_session_timeout + + '}'; + } + } + + @Nonnull + protected Options getOptions(RepairType repairType) + { + return repair_type_overrides.computeIfAbsent(repairType.getConfigName(), k -> new Options()); + } + + private static T getOverride(Options options, Function optionSupplier) + { + return options != null ? optionSupplier.apply(options) : null; + } + + @VisibleForTesting + protected T applyOverrides(RepairType repairType, Function optionSupplier) + { + // Check option by repair type first + Options repairTypeOverrides = getOptions(repairType); + T val = optionSupplier.apply(repairTypeOverrides); + + if (val != null) + return val; + + // Check option in global settings + if (global_settings != null) + { + val = getOverride(global_settings, optionSupplier); + + if (val != null) + return val; + } + + // Otherwise check defaults + return getOverride(Options.getDefaultOptionsMap().get(repairType), optionSupplier); + } + + public String toString() + { + return "AutoRepairConfig{" + + "enabled=" + enabled + + ", repair_check_interval=" + repair_check_interval + + ", history_clear_delete_hosts_buffer_interval=" + history_clear_delete_hosts_buffer_interval + + ", repair_task_min_duration=" + repair_task_min_duration + + ", global_settings=" + global_settings + + ", repair_type_overrides=" + repair_type_overrides + + "}"; + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/AutoRepairState.java b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairState.java new file mode 100644 index 000000000000..2686c7e734e7 --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairState.java @@ -0,0 +1,386 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.repair.autorepair; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.view.TableViews; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.metrics.AutoRepairMetricsManager; +import org.apache.cassandra.metrics.AutoRepairMetrics; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType; +import org.apache.cassandra.repair.autorepair.AutoRepairUtils.AutoRepairHistory; +import org.apache.cassandra.repair.RepairParallelism; +import org.apache.cassandra.repair.RepairCoordinator; +import org.apache.cassandra.repair.messages.RepairOption; +import org.apache.cassandra.service.AutoRepairService; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.utils.Clock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * AutoRepairState represents the state of automated repair for a given repair type. + */ +public abstract class AutoRepairState +{ + protected static final Logger logger = LoggerFactory.getLogger(AutoRepairState.class); + private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS"); + @VisibleForTesting + protected static Supplier timeFunc = Clock.Global::currentTimeMillis; + + @VisibleForTesting + protected final RepairType repairType; + @VisibleForTesting + protected AutoRepairConfig config; + @VisibleForTesting + protected int totalTablesConsideredForRepair = 0; + @VisibleForTesting + protected long lastRepairTimeInMs; + @VisibleForTesting + protected int nodeRepairTimeInSec = 0; + @VisibleForTesting + protected int clusterRepairTimeInSec = 0; + @VisibleForTesting + protected boolean repairInProgress = false; + @VisibleForTesting + protected int repairKeyspaceCount = 0; + @VisibleForTesting + protected int totalMVTablesConsideredForRepair = 0; + @VisibleForTesting + protected int totalDisabledTablesRepairCount = 0; + @VisibleForTesting + protected int failedTokenRangesCount = 0; + @VisibleForTesting + protected int succeededTokenRangesCount = 0; + @VisibleForTesting + protected int skippedTokenRangesCount = 0; + @VisibleForTesting + protected int skippedTablesCount = 0; + @VisibleForTesting + protected long totalBytesToRepair = 0; + @VisibleForTesting + protected long bytesAlreadyRepaired = 0; + @VisibleForTesting + protected int totalKeyspaceRepairPlansToRepair = 0; + @VisibleForTesting + protected int keyspaceRepairPlansAlreadyRepaired = 0; + @VisibleForTesting + protected AutoRepairHistory longestUnrepairedNode; + protected final AutoRepairMetrics metrics; + + protected AutoRepairState(RepairType repairType, AutoRepairConfig config) + { + metrics = AutoRepairMetricsManager.getMetrics(repairType); + this.repairType = repairType; + this.config = config; + } + + public abstract RepairCoordinator getRepairRunnable(String keyspace, List tables, Set> ranges, boolean primaryRangeOnly); + + protected RepairCoordinator getRepairRunnable(String keyspace, RepairOption options) + { + return new RepairCoordinator(StorageService.instance, StorageService.nextRepairCommand.incrementAndGet(), + options, keyspace); + } + + public void updateRepairScheduleStatistics(List repairPlans) + { + setTotalBytesToRepair(repairPlans.stream(). + flatMap(repairPlan -> repairPlan.getKeyspaceRepairPlans(). + stream()).mapToLong(KeyspaceRepairPlan::getEstimatedBytes).sum()); + setTotalKeyspaceRepairPlansToRepair(repairPlans.stream().mapToInt(repairPlan -> repairPlan.getKeyspaceRepairPlans().size()).sum()); + } + + public long getLastRepairTime() + { + return lastRepairTimeInMs; + } + + public void setTotalTablesConsideredForRepair(int count) + { + totalTablesConsideredForRepair = count; + } + + public int getTotalTablesConsideredForRepair() + { + return totalTablesConsideredForRepair; + } + + public void setLastRepairTime(long lastRepairTime) + { + lastRepairTimeInMs = lastRepairTime; + } + + public int getClusterRepairTimeInSec() + { + return clusterRepairTimeInSec; + } + + public int getNodeRepairTimeInSec() + { + return nodeRepairTimeInSec; + } + + public void setRepairInProgress(boolean repairInProgress) + { + this.repairInProgress = repairInProgress; + } + + public boolean isRepairInProgress() + { + return repairInProgress; + } + + public int getLongestUnrepairedSec() + { + if (longestUnrepairedNode == null) + { + return 0; + } + return (int) TimeUnit.MILLISECONDS.toSeconds(timeFunc.get() - longestUnrepairedNode.getLastRepairFinishTime()); + } + + public void setTotalMVTablesConsideredForRepair(int count) + { + totalMVTablesConsideredForRepair = count; + } + + public int getTotalMVTablesConsideredForRepair() + { + return totalMVTablesConsideredForRepair; + } + + public void setNodeRepairTimeInSec(int elapsed) + { + nodeRepairTimeInSec = elapsed; + } + + public void setClusterRepairTimeInSec(int seconds) + { + clusterRepairTimeInSec = seconds; + } + + public void setRepairKeyspaceCount(int count) + { + repairKeyspaceCount = count; + } + + public int getRepairKeyspaceCount() + { + return repairKeyspaceCount; + } + + public void setLongestUnrepairedNode(AutoRepairHistory longestUnrepairedNode) + { + this.longestUnrepairedNode = longestUnrepairedNode; + } + + public void setFailedTokenRangesCount(int count) + { + failedTokenRangesCount = count; + } + + public int getFailedTokenRangesCount() + { + return failedTokenRangesCount; + } + + public void setSucceededTokenRangesCount(int count) + { + succeededTokenRangesCount = count; + } + + public int getSucceededTokenRangesCount() + { + return succeededTokenRangesCount; + } + + public void setSkippedTokenRangesCount(int count) + { + skippedTokenRangesCount = count; + } + + public int getSkippedTokenRangesCount() + { + return skippedTokenRangesCount; + } + + public void setSkippedTablesCount(int count) + { + skippedTablesCount = count; + } + + public int getSkippedTablesCount() + { + return skippedTablesCount; + } + + public void recordTurn(AutoRepairUtils.RepairTurn turn) + { + metrics.recordTurn(turn); + } + + public void setTotalDisabledTablesRepairCount(int count) + { + totalDisabledTablesRepairCount = count; + } + + public int getTotalDisabledTablesRepairCount() + { + return totalDisabledTablesRepairCount; + } + + public void setTotalBytesToRepair(long totalBytesToRepair) + { + this.totalBytesToRepair = totalBytesToRepair; + } + + public long getTotalBytesToRepair() + { + return totalBytesToRepair; + } + + public void setBytesAlreadyRepaired(long bytesAlreadyRepaired) + { + this.bytesAlreadyRepaired = bytesAlreadyRepaired; + } + + public long getBytesAlreadyRepaired() + { + return bytesAlreadyRepaired; + } + + public void setTotalKeyspaceRepairPlansToRepair(int totalKeyspaceRepairPlansToRepair) + { + this.totalKeyspaceRepairPlansToRepair = totalKeyspaceRepairPlansToRepair; + } + + public int getTotalKeyspaceRepairPlansToRepair() + { + return totalKeyspaceRepairPlansToRepair; + } + + public void setKeyspaceRepairPlansAlreadyRepaired(int keyspaceRepairPlansAlreadyRepaired) + { + this.keyspaceRepairPlansAlreadyRepaired = keyspaceRepairPlansAlreadyRepaired; + } + + public int getKeyspaceRepairPlansAlreadyRepaired() + { + return keyspaceRepairPlansAlreadyRepaired; + } +} + +class PreviewRepairedState extends AutoRepairState +{ + public PreviewRepairedState(AutoRepairConfig config) + { + super(RepairType.PREVIEW_REPAIRED, config); + } + + @Override + public RepairCoordinator getRepairRunnable(String keyspace, List tables, Set> ranges, boolean primaryRangeOnly) + { + RepairOption option = new RepairOption(RepairParallelism.PARALLEL, primaryRangeOnly, false, false, + AutoRepairService.instance.getAutoRepairConfig().getRepairThreads(repairType), ranges, + !ranges.isEmpty(), false, false, false, PreviewKind.REPAIRED, false, true, false, false, false); + + option.getColumnFamilies().addAll(tables); + + return getRepairRunnable(keyspace, option); + } +} + +class IncrementalRepairState extends AutoRepairState +{ + public IncrementalRepairState(AutoRepairConfig config) + { + super(RepairType.INCREMENTAL, config); + } + + @Override + public RepairCoordinator getRepairRunnable(String keyspace, List tables, Set> ranges, boolean primaryRangeOnly) + { + RepairOption option = new RepairOption(RepairParallelism.PARALLEL, primaryRangeOnly, true, false, + AutoRepairService.instance.getAutoRepairConfig().getRepairThreads(repairType), ranges, + !ranges.isEmpty(), false, false, false, PreviewKind.NONE, true, true, false, false, false); + + option.getColumnFamilies().addAll(filterOutUnsafeTables(keyspace, tables)); + + return getRepairRunnable(keyspace, option); + } + + @VisibleForTesting + protected List filterOutUnsafeTables(String keyspaceName, List tables) + { + Keyspace keyspace = Keyspace.open(keyspaceName); + + return tables.stream() + .filter(table -> { + ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(table); + TableViews views = keyspace.viewManager.forTable(cfs.metadata().id); + if (views != null && !views.isEmpty()) + { + logger.debug("Skipping incremental repair for {}.{} as it has materialized views", keyspaceName, table); + return false; + } + + if (cfs.metadata().params != null && cfs.metadata().params.cdc) + { + logger.debug("Skipping incremental repair for {}.{} as it has CDC enabled", keyspaceName, table); + return false; + } + + return true; + }).collect(Collectors.toList()); + } +} + +class FullRepairState extends AutoRepairState +{ + public FullRepairState(AutoRepairConfig config) + { + super(RepairType.FULL, config); + } + + @Override + public RepairCoordinator getRepairRunnable(String keyspace, List tables, Set> ranges, boolean primaryRangeOnly) + { + RepairOption option = new RepairOption(RepairParallelism.PARALLEL, primaryRangeOnly, false, false, + AutoRepairService.instance.getAutoRepairConfig().getRepairThreads(repairType), ranges, + !ranges.isEmpty(), false, false, false, PreviewKind.NONE, true, true, false, false, false); + + option.getColumnFamilies().addAll(tables); + + return getRepairRunnable(keyspace, option); + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/AutoRepairUtils.java b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairUtils.java new file mode 100644 index 000000000000..111b600fd3ff --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairUtils.java @@ -0,0 +1,1431 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.repair.autorepair; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import com.clearspring.analytics.stream.cardinality.CardinalityMergeException; +import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus; +import com.clearspring.analytics.stream.cardinality.ICardinality; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Splitter; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.metadata.CompactionMetadata; +import org.apache.cassandra.io.sstable.metadata.MetadataType; +import org.apache.cassandra.locator.EndpointsByRange; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.LocalStrategy; + +import org.apache.cassandra.utils.CassandraVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.cql3.statements.ModificationStatement; +import org.apache.cassandra.cql3.statements.SelectStatement; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.NetworkTopologyStrategy; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.metrics.AutoRepairMetricsManager; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SystemDistributedKeyspace; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.ViewMetadata; +import org.apache.cassandra.serializers.SetSerializer; +import org.apache.cassandra.serializers.UUIDSerializer; +import org.apache.cassandra.service.AutoRepairService; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.concurrent.Refs; + +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.MY_TURN; +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.MY_TURN_DUE_TO_PRIORITY; +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.NOT_MY_TURN; +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.RepairTurn.MY_TURN_FORCE_REPAIR; +import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; + +/** + * This class serves as a utility class for AutoRepair. It contains various helper APIs + * to store/retrieve repair status, decide whose turn is next, etc. + */ +public class AutoRepairUtils +{ + private static final Logger logger = LoggerFactory.getLogger(AutoRepairUtils.class); + static final String COL_REPAIR_TYPE = "repair_type"; + static final String COL_HOST_ID = "host_id"; + static final String COL_REPAIR_START_TS = "repair_start_ts"; + static final String COL_REPAIR_FINISH_TS = "repair_finish_ts"; + static final String COL_REPAIR_PRIORITY = "repair_priority"; + static final String COL_DELETE_HOSTS = "delete_hosts"; // this set stores the host ids which think the row should be deleted + static final String COL_REPAIR_TURN = "repair_turn"; // this record the last repair turn. Normal turn or turn due to priority + static final String COL_DELETE_HOSTS_UPDATE_TIME = "delete_hosts_update_time"; // the time when delete hosts are updated + static final String COL_FORCE_REPAIR = "force_repair"; // if set to true, the node will do non-primary range repair + + static final String SELECT_REPAIR_HISTORY = String.format( + "SELECT * FROM %s.%s WHERE %s = ?", SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_REPAIR_TYPE); + static final String SELECT_REPAIR_PRIORITY = String.format( + "SELECT * FROM %s.%s WHERE %s = ?", SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY, COL_REPAIR_TYPE); + static final String DEL_REPAIR_PRIORITY = String.format( + "DELETE %s[?] FROM %s.%s WHERE %s = ?", COL_REPAIR_PRIORITY, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY, COL_REPAIR_TYPE); + static final String ADD_PRIORITY_HOST = String.format( + "UPDATE %s.%s SET %s = %s + ? WHERE %s = ?", SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY, COL_REPAIR_PRIORITY, COL_REPAIR_PRIORITY, COL_REPAIR_TYPE); + + static final String INSERT_NEW_REPAIR_HISTORY = String.format( + "INSERT INTO %s.%s (%s, %s, %s, %s, %s, %s) values (?, ? ,?, ?, {}, ?) IF NOT EXISTS", + SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_REPAIR_TYPE, + COL_HOST_ID, COL_REPAIR_START_TS, COL_REPAIR_FINISH_TS, COL_DELETE_HOSTS, COL_DELETE_HOSTS_UPDATE_TIME); + + static final String ADD_HOST_ID_TO_DELETE_HOSTS = String.format( + "UPDATE %s.%s SET %s = %s + ?, %s = ? WHERE %s = ? AND %s = ? IF EXISTS" + , SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_DELETE_HOSTS, + COL_DELETE_HOSTS, COL_DELETE_HOSTS_UPDATE_TIME, COL_REPAIR_TYPE, COL_HOST_ID); + + static final String DEL_AUTO_REPAIR_HISTORY = String.format( + "DELETE FROM %s.%s WHERE %s = ? AND %s = ?" + , SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_REPAIR_TYPE, + COL_HOST_ID); + + static final String RECORD_START_REPAIR_HISTORY = String.format( + "UPDATE %s.%s SET %s= ?, repair_turn = ? WHERE %s = ? AND %s = ?" + , SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_REPAIR_START_TS, + COL_REPAIR_TYPE, COL_HOST_ID); + + static final String RECORD_FINISH_REPAIR_HISTORY = String.format( + "UPDATE %s.%s SET %s= ?, %s=false WHERE %s = ? AND %s = ?" + , SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_REPAIR_FINISH_TS, + COL_FORCE_REPAIR, COL_REPAIR_TYPE, COL_HOST_ID); + + static final String CLEAR_DELETE_HOSTS = String.format( + "UPDATE %s.%s SET %s= {} WHERE %s = ? AND %s = ?" + , SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_DELETE_HOSTS, + COL_REPAIR_TYPE, COL_HOST_ID); + + static final String SET_FORCE_REPAIR = String.format( + "UPDATE %s.%s SET %s=true WHERE %s = ? AND %s = ?" + , SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_FORCE_REPAIR, + COL_REPAIR_TYPE, COL_HOST_ID); + + static final String SELECT_LAST_REPAIR_TIME_FOR_NODE = String.format( + "SELECT %s FROM %s.%s WHERE %s = ? AND %s = ?", COL_REPAIR_FINISH_TS, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + SystemDistributedKeyspace.AUTO_REPAIR_HISTORY, COL_REPAIR_TYPE, COL_HOST_ID); + + static ModificationStatement delStatementRepairHistory; + static SelectStatement selectStatementRepairHistory; + static ModificationStatement delStatementPriorityStatus; + static SelectStatement selectStatementRepairPriority; + static SelectStatement selectLastRepairTimeForNode; + static ModificationStatement addPriorityHost; + static ModificationStatement insertNewRepairHistoryStatement; + static ModificationStatement recordStartRepairHistoryStatement; + static ModificationStatement recordFinishRepairHistoryStatement; + static ModificationStatement addHostIDToDeleteHostsStatement; + static ModificationStatement clearDeleteHostsStatement; + static ModificationStatement setForceRepairStatement; + static ConsistencyLevel internalQueryCL; + + public enum RepairTurn + { + MY_TURN, + NOT_MY_TURN, + MY_TURN_DUE_TO_PRIORITY, + MY_TURN_FORCE_REPAIR + } + + public static void setup() + { + selectStatementRepairHistory = (SelectStatement) QueryProcessor.getStatement(SELECT_REPAIR_HISTORY, ClientState + .forInternalCalls()); + selectStatementRepairPriority = (SelectStatement) QueryProcessor.getStatement(SELECT_REPAIR_PRIORITY, ClientState + .forInternalCalls()); + selectLastRepairTimeForNode = (SelectStatement) QueryProcessor.getStatement(SELECT_LAST_REPAIR_TIME_FOR_NODE, ClientState + .forInternalCalls()); + delStatementPriorityStatus = (ModificationStatement) QueryProcessor.getStatement(DEL_REPAIR_PRIORITY, ClientState + .forInternalCalls()); + addPriorityHost = (ModificationStatement) QueryProcessor.getStatement(ADD_PRIORITY_HOST, ClientState + .forInternalCalls()); + insertNewRepairHistoryStatement = (ModificationStatement) QueryProcessor.getStatement(INSERT_NEW_REPAIR_HISTORY, ClientState + .forInternalCalls()); + recordStartRepairHistoryStatement = (ModificationStatement) QueryProcessor.getStatement(RECORD_START_REPAIR_HISTORY, ClientState + .forInternalCalls()); + recordFinishRepairHistoryStatement = (ModificationStatement) QueryProcessor.getStatement(RECORD_FINISH_REPAIR_HISTORY, ClientState + .forInternalCalls()); + addHostIDToDeleteHostsStatement = (ModificationStatement) QueryProcessor.getStatement(ADD_HOST_ID_TO_DELETE_HOSTS, ClientState + .forInternalCalls()); + setForceRepairStatement = (ModificationStatement) QueryProcessor.getStatement(SET_FORCE_REPAIR, ClientState + .forInternalCalls()); + clearDeleteHostsStatement = (ModificationStatement) QueryProcessor.getStatement(CLEAR_DELETE_HOSTS, ClientState + .forInternalCalls()); + delStatementRepairHistory = (ModificationStatement) QueryProcessor.getStatement(DEL_AUTO_REPAIR_HISTORY, ClientState + .forInternalCalls()); + Keyspace autoRepairKS = Schema.instance.getKeyspaceInstance(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME); + internalQueryCL = autoRepairKS.getReplicationStrategy().getClass() == NetworkTopologyStrategy.class ? + ConsistencyLevel.LOCAL_QUORUM : ConsistencyLevel.ONE; + } + + public static class AutoRepairHistory + { + UUID hostId; + String repairTurn; + long lastRepairStartTime; + long lastRepairFinishTime; + Set deleteHosts; + long deleteHostsUpdateTime; + boolean forceRepair; + + public AutoRepairHistory(UUID hostId, String repairTurn, long lastRepairStartTime, long lastRepairFinishTime, + Set deleteHosts, long deleteHostsUpdateTime, boolean forceRepair) + { + this.hostId = hostId; + this.repairTurn = repairTurn; + this.lastRepairStartTime = lastRepairStartTime; + this.lastRepairFinishTime = lastRepairFinishTime; + this.deleteHosts = deleteHosts; + if (this.deleteHosts == null) + { + this.deleteHosts = new HashSet<>(); + } + this.deleteHostsUpdateTime = deleteHostsUpdateTime; + this.forceRepair = forceRepair; + } + + public String toString() + { + return MoreObjects.toStringHelper(this). + add("hostId", hostId). + add("repairTurn", repairTurn). + add("lastRepairStartTime", lastRepairStartTime). + add("lastRepairFinishTime", lastRepairFinishTime). + add("deleteHosts", deleteHosts). + toString(); + } + + public boolean isRepairRunning() + { + // if a repair history record has start time later than finish time, it means the repair is running + return lastRepairStartTime > lastRepairFinishTime; + } + + public long getLastRepairFinishTime() + { + return lastRepairFinishTime; + } + } + + public static class CurrentRepairStatus + { + public Set hostIdsWithOnGoingRepair; // hosts that is running repair + public Set hostIdsWithOnGoingForceRepair; // hosts that is running repair because of force repair + Set priority; + public AutoRepairHistory myRepairHistory; + List historiesWithoutOnGoingRepair; // hosts that is NOT running repair + + public CurrentRepairStatus(List repairHistories, Set priority, UUID myId) + { + hostIdsWithOnGoingRepair = new HashSet<>(); + hostIdsWithOnGoingForceRepair = new HashSet<>(); + historiesWithoutOnGoingRepair = new ArrayList<>(); + + for (AutoRepairHistory history : repairHistories) + { + if (history.isRepairRunning()) + { + if (history.forceRepair) + { + hostIdsWithOnGoingForceRepair.add(history.hostId); + } + else + { + hostIdsWithOnGoingRepair.add(history.hostId); + } + } + else + { + historiesWithoutOnGoingRepair.add(history); + } + if (history.hostId.equals(myId)) + { + myRepairHistory = history; + } + } + this.priority = priority; + } + + public Set getAllHostsWithOngoingRepair() + { + return Sets.union(hostIdsWithOnGoingRepair, hostIdsWithOnGoingForceRepair); + } + + public String toString() + { + return MoreObjects.toStringHelper(this). + add("hostIdsWithOnGoingRepair", hostIdsWithOnGoingRepair). + add("hostIdsWithOnGoingForceRepair", hostIdsWithOnGoingForceRepair). + add("historiesWithoutOnGoingRepair", historiesWithoutOnGoingRepair). + add("priority", priority). + add("myRepairHistory", myRepairHistory). + toString(); + } + } + + @VisibleForTesting + public static List getAutoRepairHistory(RepairType repairType) + { + UntypedResultSet repairHistoryResult; + + ResultMessage.Rows repairStatusRows = selectStatementRepairHistory.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, Lists.newArrayList(ByteBufferUtil.bytes(repairType.toString()))), Dispatcher.RequestTime.forImmediateExecution()); + repairHistoryResult = UntypedResultSet.create(repairStatusRows.result); + + List repairHistories = new ArrayList<>(); + if (!repairHistoryResult.isEmpty()) + { + for (UntypedResultSet.Row row : repairHistoryResult) + { + UUID hostId = row.getUUID(COL_HOST_ID); + String repairTurn = null; + if (row.has(COL_REPAIR_TURN)) + repairTurn = row.getString(COL_REPAIR_TURN); + long lastRepairStartTime = row.getLong(COL_REPAIR_START_TS, 0); + long lastRepairFinishTime = row.getLong(COL_REPAIR_FINISH_TS, 0); + Set deleteHosts = row.getSet(COL_DELETE_HOSTS, UUIDType.instance); + long deleteHostsUpdateTime = row.getLong(COL_DELETE_HOSTS_UPDATE_TIME, 0); + boolean forceRepair = row.has(COL_FORCE_REPAIR) && row.getBoolean(COL_FORCE_REPAIR); + repairHistories.add(new AutoRepairHistory(hostId, repairTurn, lastRepairStartTime, lastRepairFinishTime, + deleteHosts, deleteHostsUpdateTime, forceRepair)); + } + return repairHistories; + } + logger.info("No repair history found"); + return null; + } + + // A host may add itself in delete hosts for some other hosts due to restart or some temp gossip issue. If a node's record + // delete_hosts is not growing for more than 2 hours, we consider it as a normal node so we clear the delete_hosts for that node + public static void clearDeleteHosts(RepairType repairType, UUID hostId) + { + clearDeleteHostsStatement.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(hostId))), Dispatcher.RequestTime.forImmediateExecution()); + } + + public static void setForceRepairNewNode(RepairType repairType) + { + // this function will be called when a node bootstrap finished + UUID hostId = StorageService.instance.getTokenMetadata().getHostId(FBUtilities.getBroadcastAddressAndPort()); + if (hostId == null) + { + logger.warn("Could not resolve local host ID, skipping setForceRepairNewNode for repair type {}", repairType); + return; + } + // insert the data first + insertNewRepairHistory(repairType, currentTimeMillis(), currentTimeMillis()); + setForceRepair(repairType, hostId); + } + + public static void setForceRepair(RepairType repairType, Set hosts) + { + // this function is used by nodetool + for (InetAddressAndPort host : hosts) + { + UUID hostId = StorageService.instance.getTokenMetadata().getHostId(host); + if (hostId == null) + { + logger.warn("Could not resolve host ID for {}, skipping setForceRepair for repair type {}", host, repairType); + continue; + } + setForceRepair(repairType, hostId); + } + } + + public static void setForceRepair(RepairType repairType, UUID hostId) + { + setForceRepairStatement.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(hostId))), + Dispatcher.RequestTime.forImmediateExecution()); + + logger.info("Set force repair repair type: {}, node: {}", repairType, hostId); + } + + public static long getLastRepairTimeForNode(RepairType repairType, UUID hostId) + { + ResultMessage.Rows rows = selectLastRepairTimeForNode.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList( + ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(hostId))), + Dispatcher.RequestTime.forImmediateExecution()); + UntypedResultSet repairTime = UntypedResultSet.create(rows.result); + if (repairTime.isEmpty()) + { + return 0; + } + return repairTime.one().getLong(COL_REPAIR_FINISH_TS); + } + + @VisibleForTesting + public static CurrentRepairStatus getCurrentRepairStatus(RepairType repairType, List autoRepairHistories, UUID myId) + { + if (autoRepairHistories != null) + { + return new CurrentRepairStatus(autoRepairHistories, getPriorityHostIds(repairType), myId); + } + return null; + } + + /** + * Checks whether the cluster has multiple major versions + * @return + * true if more than one major versions are detected + * false if only one major version is detected + * + */ + public static boolean hasMultipleLiveMajorVersions() + { + Set liveEndpoints = Gossiper.instance.getLiveMembers(); + Set majorVersions = new HashSet<>(); + for (InetAddressAndPort endpoint : liveEndpoints) + { + CassandraVersion releaseVersion = Gossiper.instance.getReleaseVersion(endpoint); + if (releaseVersion != null) + { + majorVersions.add(releaseVersion.major); + } + } + return majorVersions.size() > 1; + } + + /** + * Last version that does not support auto-repair. + * All nodes in the cluster must be running a version above this to enable auto-repair. + * Versions at or below this version (5.0.7) do not support auto-repair. + */ + @VisibleForTesting + static final CassandraVersion LAST_UNSUPPORTED_VERSION_FOR_AUTO_REPAIR = new CassandraVersion("5.0.7"); + + /** + * Checks whether any node in the cluster is running an unsupported version for auto-repair. + * + * @return true if any live node has a version at or below 5.0.7 (unsupported) or has an unknown version, + * false if all nodes are running versions above 5.0.7 (supported) + */ + public static boolean hasNodesBelowMinimumVersion() + { + if (!CassandraRelevantProperties.AUTOREPAIR_CHECK_MIN_VERSION.getBoolean()) + return false; + + Set liveEndpoints = Gossiper.instance.getLiveMembers(); + for (InetAddressAndPort endpoint : liveEndpoints) + { + CassandraVersion releaseVersion = Gossiper.instance.getReleaseVersion(endpoint); + if (releaseVersion == null) + { + logger.warn("Cannot determine version for endpoint {}, blocking auto-repair", endpoint); + return true; + } + if (releaseVersion.compareTo(LAST_UNSUPPORTED_VERSION_FOR_AUTO_REPAIR) <= 0) + { + logger.info("Endpoint {} is running version {} which does not support auto-repair " + + "(auto-repair requires version above {})", + endpoint, releaseVersion, LAST_UNSUPPORTED_VERSION_FOR_AUTO_REPAIR); + return true; + } + } + return false; + } + + @VisibleForTesting + protected static TreeSet getHostIdsInCurrentRing(RepairType repairType, Set allNodesInRing) + { + TreeSet hostIdsInCurrentRing = new TreeSet<>(); + for (InetAddressAndPort node : allNodesInRing) + { + String nodeDC = DatabaseDescriptor.getEndpointSnitch().getDatacenter(node); + if (AutoRepairService.instance.getAutoRepairConfig().getIgnoreDCs(repairType).contains(nodeDC)) + { + logger.info("Ignore node {} because its datacenter is {}", node, nodeDC); + continue; + } + /* + * Check if endpoint state exists in gossip or not. If it + * does not then this maybe a ghost node so ignore it + */ + if (Gossiper.instance.isAlive(node)) + { + UUID hostId = StorageService.instance.getTokenMetadata().getHostId(node); + if (hostId == null) + { + logger.warn("Could not resolve host ID for node {}, skipping", node); + } + else + { + hostIdsInCurrentRing.add(hostId); + } + } + else + { + logger.warn("Node is not present in Gossip cache node {}, node data center {}", node, nodeDC); + } + } + return hostIdsInCurrentRing; + } + + public static TreeSet getHostIdsInCurrentRing(RepairType repairType) + { + Set allNodesInRing = StorageService.instance.getTokenMetadata().getAllEndpoints(); + return getHostIdsInCurrentRing(repairType, allNodesInRing); + } + + // This function will return the host ID for the node which has not been repaired for longest time + public static AutoRepairHistory getHostWithLongestUnrepairTime(RepairType repairType) + { + List autoRepairHistories = getAutoRepairHistory(repairType); + return getHostWithLongestUnrepairTime(autoRepairHistories); + } + + /** + * Convenience method to resolve the broadcast address of a host id + * + * @return broadcast address if it exists, otherwise null. + */ + @Nullable + private static InetAddressAndPort getBroadcastAddress(UUID hostId) + { + return StorageService.instance.getEndpointForHostId(hostId); + } + + /** + * @return Map of broadcast address to host id, if a broadcast address cannot be found for a host, it is + * not included in the map. + */ + private static Map getBroadcastAddressToHostIdMap(Set hosts) + { + // Get a mapping of endpoint : host id + Map broadcastAddressMap = new HashMap<>(hosts.size()); + for (UUID hostId : hosts) + { + InetAddressAndPort broadcastAddress = getBroadcastAddress(hostId); + if (broadcastAddress == null) + { + logger.warn("Could not resolve broadcast address from host id {} in ClusterMetadata can't accurately " + + "determine if this node is a replica of the local node.", hostId); + } + else + { + broadcastAddressMap.put(broadcastAddress, hostId); + } + } + return broadcastAddressMap; + } + + /** + * @return Mapping of unique replication strategy to keyspaces using that strategy that we care about repairing. + */ + private static Map> getReplicationStrategies() + { + // Collect all unique replication strategies among all keyspaces. + Map> replicationStrategies = new HashMap<>(); + for (Keyspace keyspace : Keyspace.all()) + { + if (AutoRepairUtils.shouldConsiderKeyspace(keyspace)) + { + replicationStrategies.computeIfAbsent(keyspace.getReplicationStrategy(), k -> new ArrayList<>()) + .add(keyspace.getName()); + } + } + return replicationStrategies; + } + + /** + * Collects all hosts being repaired among all active repair schedules and their schedule if + * {@link AutoRepairConfig#getAllowParallelReplicaRepairAcrossSchedules(RepairType)} is true for this repairType. + * Accepts the currently evaluated repairType's schedule as an optimization to avoid grabbing its repair status an + * additional time. + * + * @param myRepairType The repair type schedule being evaluated. + * @param myRepairStatus The repair status for that repair type. + * @return All hosts among active schedules currently being repaired. + */ + private static Map getHostsBeingRepaired(RepairType myRepairType, CurrentRepairStatus myRepairStatus) + { + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + + Map hostsBeingRepaired = myRepairStatus.getAllHostsWithOngoingRepair().stream() + .collect(Collectors.toMap((h) -> h, (v) -> myRepairType)); + + // If we don't allow repairing across schedules, iterate over other enabled schedules and include hosts + // actively being repaired. + if (!config.getAllowParallelReplicaRepairAcrossSchedules(myRepairType)) + { + for (RepairType repairType : RepairType.values()) + { + if (myRepairType == repairType) + continue; + + if (config.isAutoRepairEnabled(repairType)) + { + CurrentRepairStatus repairStatus = getCurrentRepairStatus(repairType, getAutoRepairHistory(repairType), null); + if (repairStatus != null) + { + for (UUID hostId : repairStatus.getAllHostsWithOngoingRepair()) + { + hostsBeingRepaired.putIfAbsent(hostId, repairType); + } + } + } + } + } + return hostsBeingRepaired; + } + + /** + * Identifies the most eligible host to repair for nodes preceding or equal to this nodes' lastRepairFinishTime. + * The criteria for this is to find the node with the oldest last repair finish time of which none of its replicas + * are currently under repair. + * + * @return The most eligible host to repair or null if no candidates before and including this nodes' current repair status. + */ + @VisibleForTesting + public static AutoRepairHistory getMostEligibleHostToRepair(RepairType repairType, CurrentRepairStatus currentRepairStatus, UUID myId) + { + // 0. If this repairType allows parallel replica repair, short circuit and return the host with the longest unrepair time + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + if (config.getAllowParallelReplicaRepair(repairType)) + { + return getHostWithLongestUnrepairTime(currentRepairStatus.historiesWithoutOnGoingRepair); + } + + // 1. Sort repair histories from oldest completed to newest + Stream finishedRepairHistories = currentRepairStatus.historiesWithoutOnGoingRepair + .stream() + .sorted(Comparator.comparingLong(h -> h.lastRepairFinishTime)); + + // 2. Optimization: Truncate repair histories after myId so we don't evaluate anything more recent as if we + // aren't interested in anything that isn't this node. + final AtomicBoolean myHistoryFound = new AtomicBoolean(false); + finishedRepairHistories = finishedRepairHistories + .filter(history -> { + if (myHistoryFound.get()) return false; // Stop processing after finding myId + myHistoryFound.set(history.hostId.equals(myId)); + return true; + }); + + // If there are any hosts with ongoing repair, filter the repair histories to not include nodes whose replicas + // are ongoing repair. + Map hostsBeingRepairedToRepairType = getHostsBeingRepaired(repairType, currentRepairStatus); + + // 3. If I am already actively being repaired in another schedule, defer submitting repairs; if already + // repairing for this type, return node so it can take its turn. + RepairType alreadyRepairingType = hostsBeingRepairedToRepairType.get(myId); + if (alreadyRepairingType != null) + { + if (repairType != alreadyRepairingType) + { + logger.info("Deferring repair because I am already actively repairing in schedule {}", hostsBeingRepairedToRepairType.get(myId)); + AutoRepairMetricsManager.getMetrics(repairType).repairDelayedBySchedule.inc(); + return null; + } + else if (currentRepairStatus.myRepairHistory != null) + { + // if the repair type matches this repair, assume the node was restarted while repairing, return node + // so it can take its turn. + logAlreadyMyTurn(); + return currentRepairStatus.myRepairHistory; + } + } + + if (!hostsBeingRepairedToRepairType.isEmpty()) + { + // 4. Extract InetAddresses for each UUID as replicas are identified by their address. + Map hostsBeingRepaired = getBroadcastAddressToHostIdMap(hostsBeingRepairedToRepairType.keySet()); + + // 5. Collect unique replication strategies and group them up with their keyspaces. + Map> replicationStrategies = getReplicationStrategies(); + + // 6. Filter out repair histories who have a replica being repaired, note that this is lazy, given the stream + // is completed using findFirst, it should stop as soon as the matching criteria is met. + finishedRepairHistories = finishedRepairHistories.filter((history) -> !hasReplicaWithOngoingRepair(history, + myId, + repairType, + hostsBeingRepaired, + hostsBeingRepairedToRepairType, + replicationStrategies)); + } + + // 7. Select the first (oldest lastRepairFinishTime) repair history without replicas being repaired + return finishedRepairHistories.findFirst().orElse(null); + } + + + /** + * @param eligibleHistory History of node to check + * @param myId Host id of this node, if the repair history is for this node, additional logging will take place. + * @param myRepairType repair type being evaluated + * @param hostsBeingRepaired Hosts being repaired. + * @param hostIdToRepairType mapping of hosts being repaired to the repair type its being repaired for. + * @param replicationStrategies Mapping of unique replication strategies to keyspaces having that strategy. + * @return Whether the host for the given eligibleRepairHistory has any replicas in hostsBeingRepaired. + */ + private static boolean hasReplicaWithOngoingRepair(AutoRepairHistory eligibleHistory, + UUID myId, + RepairType myRepairType, + Map hostsBeingRepaired, + Map hostIdToRepairType, + Map> replicationStrategies) + { + // If no broadcast address found for this host id in cluster metadata, just skip it, a node should always + // see itself in cluster metadata. + InetAddressAndPort eligibleBroadcastAddress = getBroadcastAddress(eligibleHistory.hostId); + if (eligibleBroadcastAddress == null) + { + return true; + } + + // For each replication strategy, determine if host being repaired is a replica of the local node. + for (Map.Entry> entry : replicationStrategies.entrySet()) + { + AbstractReplicationStrategy replicationStrategy = entry.getKey(); + EndpointsByRange endpointsByRange = replicationStrategy.getRangeAddresses(StorageService.instance.getTokenMetadata().cachedOnlyTokenMap()); + + // get ranges of the eligible address for the given replication strategy. + RangesAtEndpoint rangesAtEndpoint = StorageService.instance.getReplicas(replicationStrategy, eligibleBroadcastAddress); + for (Replica replica : rangesAtEndpoint) + { + // get the endpoints involved in this range. + EndpointsForRange endpointsForRange = endpointsByRange.get(replica.range()); + // For each host in this range... + for (InetAddressAndPort inetAddressAndPort : endpointsForRange.endpoints()) + { + // If the address of the node in the range belongs to a host being repaired, return true. + UUID hostId = hostsBeingRepaired.get(inetAddressAndPort); + if (hostId != null) + { + // log if the repair history matches the current running node. + InetAddressAndPort myBroadcastAddress = getBroadcastAddress(myId); + if (myBroadcastAddress != null && myBroadcastAddress.equals(eligibleBroadcastAddress)) + { + logger.info("Deferring repair because replica {} ({}) with shared ranges for " + + "{} keyspace(s) (e.g. {}) is currently taking its turn for schedule {}", + hostId, inetAddressAndPort, entry.getValue().size(), entry.getValue().get(0), + hostIdToRepairType.get(hostId)); + AutoRepairMetricsManager.getMetrics(myRepairType).repairDelayedByReplica.inc(); + } + else if (logger.isDebugEnabled()) + { + logger.debug("Not considering node {} ({}) for repair as it has replica {} ({}) with " + + "shared ranges for {} keyspace(s) (e.g. {}) which is currently taking its " + + "turn for schedule {}", + eligibleHistory.hostId, eligibleBroadcastAddress, + hostId, inetAddressAndPort, entry.getValue().size(), entry.getValue().get(0), + hostIdToRepairType.get(hostId)); + } + return true; + } + } + } + } + + // No replicas found of eligible host. + return false; + } + + private static AutoRepairHistory getHostWithLongestUnrepairTime(List autoRepairHistories) + { + if (autoRepairHistories == null) + { + return null; + } + AutoRepairHistory rst = null; + long oldestTimestamp = Long.MAX_VALUE; + for (AutoRepairHistory autoRepairHistory : autoRepairHistories) + { + if (autoRepairHistory.lastRepairFinishTime < oldestTimestamp) + { + rst = autoRepairHistory; + oldestTimestamp = autoRepairHistory.lastRepairFinishTime; + } + } + return rst; + } + + public static int getMaxNumberOfNodeRunAutoRepair(RepairType repairType, int groupSize) + { + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + if (groupSize == 0) + { + return Math.max(config.getParallelRepairCount(repairType), 1); + } + // we will use the max number from config between auto_repair_parallel_repair_count_in_group and auto_repair_parallel_repair_percentage_in_group + int value = Math.max(groupSize * config.getParallelRepairPercentage(repairType) / 100, + config.getParallelRepairCount(repairType)); + // make sure at least one node getting repaired + return Math.max(1, value); + } + + private static void logAlreadyMyTurn() + { + logger.warn("This node already was considered to having an ongoing repair for this repair type, must have " + + "been restarted, taking my turn back"); + } + + @VisibleForTesting + public static RepairTurn myTurnToRunRepair(RepairType repairType, UUID myId) + { + try + { + Set allNodesInRing = StorageService.instance.getTokenMetadata().getAllEndpoints(); + logger.info("Total nodes in ring {}", allNodesInRing.size()); + TreeSet hostIdsInCurrentRing = getHostIdsInCurrentRing(repairType, allNodesInRing); + logger.info("Total nodes qualified for repair {}", hostIdsInCurrentRing.size()); + + List autoRepairHistories = getAutoRepairHistory(repairType); + Set autoRepairHistoryIds = new HashSet<>(); + + // 1. Remove any node that is not part of group based on gossip info + if (autoRepairHistories != null) + { + for (AutoRepairHistory nodeHistory : autoRepairHistories) + { + autoRepairHistoryIds.add(nodeHistory.hostId); + // clear delete_hosts if the node's delete hosts is not growing for more than two hours + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + if (!nodeHistory.deleteHosts.isEmpty() + && config.getAutoRepairHistoryClearDeleteHostsBufferInterval().toSeconds() < TimeUnit.MILLISECONDS.toSeconds( + currentTimeMillis() - nodeHistory.deleteHostsUpdateTime + )) + { + clearDeleteHosts(repairType, nodeHistory.hostId); + logger.info("Delete hosts for {} for repair type {} has not been updated for more than {} seconds. Delete hosts has been cleared. Delete hosts before clear {}" + , nodeHistory.hostId, repairType, config.getAutoRepairHistoryClearDeleteHostsBufferInterval(), nodeHistory.deleteHosts); + } + else if (!hostIdsInCurrentRing.contains(nodeHistory.hostId)) + { + if (nodeHistory.deleteHosts.size() > Math.max(2, hostIdsInCurrentRing.size() * 0.5)) + { + // More than half of the groups thinks the record should be deleted + logger.info("{} think {} is orphan node, will delete auto repair history for repair type {}.", nodeHistory.deleteHosts, nodeHistory.hostId, repairType); + deleteAutoRepairHistory(repairType, nodeHistory.hostId); + } + else + { + // I think this host should be deleted + logger.info("I({}) think {} is not part of ring, vote to delete it for repair type {}.", myId, nodeHistory.hostId, repairType); + addHostIdToDeleteHosts(repairType, myId, nodeHistory.hostId); + } + } + } + } + + // 2. Add node to auto repair history table if a node is in gossip info + for (UUID hostId : hostIdsInCurrentRing) + { + if (!autoRepairHistoryIds.contains(hostId)) + { + logger.info("{} for repair type {} doesn't exist in the auto repair history table, insert a new record.", repairType, hostId); + insertNewRepairHistory(repairType, hostId, currentTimeMillis(), currentTimeMillis()); + } + } + + // refresh auto repair histories + autoRepairHistories = getAutoRepairHistory(repairType); + if (autoRepairHistories == null) + { + logger.error("No record found"); + return NOT_MY_TURN; + } + + // get updated current repair status + CurrentRepairStatus currentRepairStatus = getCurrentRepairStatus(repairType, autoRepairHistories, myId); + if (logger.isDebugEnabled()) + { + logger.debug("Latest repair status {}", currentRepairStatus); + } + //check if I am forced to run repair + for (AutoRepairHistory history : currentRepairStatus.historiesWithoutOnGoingRepair) + { + if (history.forceRepair && history.hostId.equals(myId)) + { + return MY_TURN_FORCE_REPAIR; + } + } + + // check if node was already indicated as having an ongoing repair, this may happen when a node restarts + // before finishing repairing. + if (currentRepairStatus.getAllHostsWithOngoingRepair().contains(myId)) + { + logAlreadyMyTurn(); + + // use the previously chosen turn. + if (currentRepairStatus.myRepairHistory != null && currentRepairStatus.myRepairHistory.repairTurn != null) + { + return RepairTurn.valueOf(currentRepairStatus.myRepairHistory.repairTurn); + } + else + { + return MY_TURN; + } + } + + int parallelRepairNumber = getMaxNumberOfNodeRunAutoRepair(repairType, autoRepairHistories.size()); + logger.info("Will run repairs concurrently on {} node(s)", parallelRepairNumber); + if (parallelRepairNumber > currentRepairStatus.hostIdsWithOnGoingRepair.size()) + { + UUID priorityHostId = null; + if (currentRepairStatus.priority != null) + { + for (UUID priorityID : currentRepairStatus.priority) + { + // remove ids doesn't belong to this ring + if (!hostIdsInCurrentRing.contains(priorityID)) + { + logger.info("{} is not part of the current ring, will be removed from priority list.", priorityID); + removePriorityStatus(repairType, priorityID); + } + else + { + priorityHostId = priorityID; + break; + } + } + } + + if (priorityHostId != null && !myId.equals(priorityHostId)) + { + logger.info("Priority list is not empty and I'm not the first node in the list, not my turn." + + "First node in priority list is {}", getBroadcastAddress(priorityHostId)); + return NOT_MY_TURN; + } + + if (myId.equals(priorityHostId)) + { + //I have a priority for repair hence its my turn now + return MY_TURN_DUE_TO_PRIORITY; + } + + // Determine if this node is the most eligible host to repair. + AutoRepairHistory nodeToBeRepaired = getMostEligibleHostToRepair(repairType, currentRepairStatus, myId); + if (nodeToBeRepaired != null) + { + if (nodeToBeRepaired.hostId.equals(myId)) + { + logger.info("This node is selected to be repaired for repair type {}", repairType); + return MY_TURN; + } + + // log which node is next, which is helpful for debugging + logger.info("Next node to be repaired for repair type {}: {} ({})", repairType, + getBroadcastAddress(nodeToBeRepaired.hostId), + nodeToBeRepaired); + } + + // If this node is not identified as most eligible, set the repair lag time. + if (currentRepairStatus.myRepairHistory != null) + { + AutoRepairMetricsManager.getMetrics(repairType) + .recordRepairStartLag(currentRepairStatus.myRepairHistory.lastRepairFinishTime); + } + } + else if (currentRepairStatus.hostIdsWithOnGoingForceRepair.contains(myId)) + { + return MY_TURN_FORCE_REPAIR; + } + // for some reason I was not done with the repair hence resume (maybe node restart in-between, etc.) + return currentRepairStatus.hostIdsWithOnGoingRepair.contains(myId) ? MY_TURN : NOT_MY_TURN; + } + catch (Exception e) + { + logger.error("Exception while deciding node's turn:", e); + } + return NOT_MY_TURN; + } + + static void deleteAutoRepairHistory(RepairType repairType, UUID hostId) + { + //delete the given hostId + delStatementRepairHistory.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(hostId))), Dispatcher.RequestTime.forImmediateExecution()); + } + + static void updateStartAutoRepairHistory(RepairType repairType, UUID myId, long timestamp, RepairTurn turn) + { + recordStartRepairHistoryStatement.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(ByteBufferUtil.bytes(timestamp), + ByteBufferUtil.bytes(turn.name()), + ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(myId) + )), Dispatcher.RequestTime.forImmediateExecution()); + } + + static void updateFinishAutoRepairHistory(RepairType repairType, UUID myId, long timestamp) + { + recordFinishRepairHistoryStatement.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(ByteBufferUtil.bytes(timestamp), + ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(myId) + )), Dispatcher.RequestTime.forImmediateExecution()); + logger.info("Auto repair finished for {}", myId); + } + + public static void insertNewRepairHistory(RepairType repairType, UUID hostId, long startTime, long finishTime) + { + try + { + Keyspace autoRepairKS = Schema.instance.getKeyspaceInstance(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME); + ConsistencyLevel cl = autoRepairKS.getReplicationStrategy().getClass() == NetworkTopologyStrategy.class ? + ConsistencyLevel.LOCAL_SERIAL : null; + + UntypedResultSet resultSet; + ResultMessage.Rows resultMessage = (ResultMessage.Rows) insertNewRepairHistoryStatement.execute( + QueryState.forInternalCalls(), QueryOptions.create(internalQueryCL, Lists.newArrayList( + ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(hostId), + ByteBufferUtil.bytes(startTime), + ByteBufferUtil.bytes(finishTime), + ByteBufferUtil.bytes(currentTimeMillis()) + ), false, PageSize.NONE, null, cl, ProtocolVersion.CURRENT, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME), + Dispatcher.RequestTime.forImmediateExecution()); + resultSet = UntypedResultSet.create(resultMessage.result); + boolean applied = resultSet.one().getBoolean(ModificationStatement.CAS_RESULT_COLUMN.toString()); + if (applied) + { + logger.info("Successfully inserted a new auto repair history record for host id: {}", hostId); + } + else + { + logger.info("Record exists, no need to insert again for host id: {}", hostId); + } + } + catch (Exception e) + { + logger.error("Exception in inserting new repair history:", e); + } + } + + public static void insertNewRepairHistory(RepairType repairType, long startTime, long finishTime) + { + UUID hostId = StorageService.instance.getTokenMetadata().getHostId(FBUtilities.getBroadcastAddressAndPort()); + insertNewRepairHistory(repairType, hostId, startTime, finishTime); + } + + public static void addHostIdToDeleteHosts(RepairType repairType, UUID myID, UUID hostToBeDeleted) + { + SetSerializer serializer = SetSerializer.getInstance(UUIDSerializer.instance, UTF8Type.instance.comparatorSet); + addHostIDToDeleteHostsStatement.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(serializer.serialize(new HashSet<>(Arrays.asList(myID))), + ByteBufferUtil.bytes(currentTimeMillis()), + ByteBufferUtil.bytes(repairType.toString()), + ByteBufferUtil.bytes(hostToBeDeleted) + )), Dispatcher.RequestTime.forImmediateExecution()); + } + + public static void addPriorityHosts(RepairType repairType, Set hosts) + { + Set hostIds = new HashSet<>(); + for (InetAddressAndPort host : hosts) + { + //find hostId from IP address + UUID hostId = StorageService.instance.getTokenMetadata().getHostId(host); + if (hostId != null) + { + hostIds.add(hostId); + logger.info("Add host {} to the priority list", hostId); + } + else + { + logger.warn("Could not resolve host ID for {}, skipping addPriorityHosts", host); + } + } + if (!hostIds.isEmpty()) + { + SetSerializer serializer = SetSerializer.getInstance(UUIDSerializer.instance, UTF8Type.instance.comparatorSet); + addPriorityHost.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(serializer.serialize(hostIds), + ByteBufferUtil.bytes(repairType.toString()))), + Dispatcher.RequestTime.forImmediateExecution()); + } + } + + static void removePriorityStatus(RepairType repairType, UUID hostId) + { + logger.info("Remove host {} from priority list", hostId); + delStatementPriorityStatus.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, + Lists.newArrayList(ByteBufferUtil.bytes(hostId), + ByteBufferUtil.bytes(repairType.toString()))), + Dispatcher.RequestTime.forImmediateExecution()); + } + + public static Set getPriorityHostIds(RepairType repairType) + { + UntypedResultSet repairPriorityResult; + + ResultMessage.Rows repairPriorityRows = selectStatementRepairPriority.execute(QueryState.forInternalCalls(), + QueryOptions.forInternalCalls(internalQueryCL, Lists.newArrayList(ByteBufferUtil.bytes(repairType.toString()))), Dispatcher.RequestTime.forImmediateExecution()); + repairPriorityResult = UntypedResultSet.create(repairPriorityRows.result); + + Set priorities = null; + if (!repairPriorityResult.isEmpty()) + { + // there should be only one row + UntypedResultSet.Row row = repairPriorityResult.one(); + priorities = row.getSet(COL_REPAIR_PRIORITY, UUIDType.instance); + } + if (priorities != null) + { + return priorities; + } + return Collections.emptySet(); + } + + public static Set getPriorityHosts(RepairType repairType) + { + Set hosts = new HashSet<>(); + for (UUID hostId : getPriorityHostIds(repairType)) + { + InetAddressAndPort broadcastAddress = getBroadcastAddress(hostId); + if (broadcastAddress == null) + { + logger.warn("Could not resolve broadcastAddress for {}, skipping considering it as a priority host", hostId); + continue; + } + hosts.add(broadcastAddress); + } + return hosts; + } + + public static boolean shouldConsiderKeyspace(Keyspace ks) + { + AbstractReplicationStrategy replicationStrategy = ks.getReplicationStrategy(); + boolean repair = true; + if (replicationStrategy instanceof NetworkTopologyStrategy) + { + Set datacenters = ((NetworkTopologyStrategy) replicationStrategy).getDatacenters(); + String localDC = DatabaseDescriptor.getEndpointSnitch().getDatacenter(FBUtilities.getBroadcastAddressAndPort()); + if (!datacenters.contains(localDC)) + { + repair = false; + } + } + if (replicationStrategy instanceof LocalStrategy) + { + repair = false; + } + if (ks.getName().equalsIgnoreCase(SchemaConstants.TRACE_KEYSPACE_NAME)) + { + // by default, ignore the tables under system_traces as they do not have + // that much important data + repair = false; + } + return repair; + } + + public static boolean tableMaxRepairTimeExceeded(RepairType repairType, long startTime) + { + long tableRepairTimeSoFar = TimeUnit.MILLISECONDS.toSeconds + (currentTimeMillis() - startTime); + return AutoRepairService.instance.getAutoRepairConfig().getAutoRepairTableMaxRepairTime(repairType).toSeconds() < + tableRepairTimeSoFar; + } + + public static boolean keyspaceMaxRepairTimeExceeded(RepairType repairType, long startTime, int numOfTablesToBeRepaired) + { + long keyspaceRepairTimeSoFar = TimeUnit.MILLISECONDS.toSeconds((currentTimeMillis() - startTime)); + return (long) AutoRepairService.instance.getAutoRepairConfig().getAutoRepairTableMaxRepairTime(repairType).toSeconds() * + numOfTablesToBeRepaired < keyspaceRepairTimeSoFar; + } + + public static List getAllMVs(RepairType repairType, Keyspace keyspace, TableMetadata tableMetadata) + { + List allMvs = new ArrayList<>(); + if (AutoRepairService.instance.getAutoRepairConfig().getMaterializedViewRepairEnabled(repairType) && keyspace.getMetadata().views != null) + { + Iterator views = keyspace.getMetadata().views.forTable(tableMetadata.id).iterator(); + while (views.hasNext()) + { + String viewName = views.next().name(); + logger.info("Adding MV to the list {}.{}.{}", keyspace.getName(), tableMetadata.name, viewName); + allMvs.add(viewName); + } + } + return allMvs; + } + + public static Collection> split(Range tokenRange, int numberOfSplits) + { + Collection> ranges; + Optional splitter = DatabaseDescriptor.getPartitioner().splitter(); + if (!splitter.isPresent()) + { + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 30, TimeUnit.MINUTES, "Partitioner {} does not support splitting, falling back to splitting by token range", DatabaseDescriptor.getPartitioner()); + ranges = Collections.singleton(tokenRange); + } + else + { + ranges = splitter.get().split(Collections.singleton(tokenRange), numberOfSplits); + } + return ranges; + } + + /** + * Finds a list of SSTables for a given {@code repairType}, + * {@code keyspace}, {@code table}, and {@code tokenRange} and then it internally calls + * another API {@code AutoRepairUtils.getSizesForRangeOfSSTables}, which figures out the estimated data size. + * + * @param repairType the repair type (e.g., FULL, INCREMENTAL) + * @param keyspace the keyspace name + * @param table the table name + * @param tokenRange the token range to evaluate + * @return an estimate representing the number of partitions, size in range, and total size + */ + static SizeEstimate getRangeSizeEstimate(RepairType repairType, String keyspace, String table, Range tokenRange) + { + logger.debug("Calculating size estimate for {}.{} for range {}", keyspace, table, tokenRange); + try (Refs refs = RepairTokenRangeSplitter.getSSTableReaderRefs(repairType, keyspace, table, tokenRange)) + { + SizeEstimate estimate = getSizesForRangeOfSSTables(repairType, keyspace, table, tokenRange, refs); + logger.debug("Generated size estimate {}", estimate); + return estimate; + } + } + /** + * Calculates the size estimation qualified to be repaired for a given {@code repairType}, + * {@code keyspace}, {@code table}, {@code tokenRange}, and {@code refs}. + *

    + * If the compression is enabled, then the size will be an estimate, otherwise it will be accurate. + *

    + * + * @param repairType + * @param keyspace + * @param table + * @param tokenRange + * @param refs + * @return an estimate representing the number of partitions, size in range, and total size + */ + static SizeEstimate getSizesForRangeOfSSTables(RepairType repairType, String keyspace, String table, + Range tokenRange, Refs refs) + { + List> singletonRange = Collections.singletonList(tokenRange); + ICardinality cardinality = new HyperLogLogPlus(13, 25); + long approxBytesInRange = 0L; + long totalBytes = 0L; + + for (SSTableReader reader : refs) + { + try + { + if (reader.openReason == SSTableReader.OpenReason.EARLY) + continue; + CompactionMetadata metadata = (CompactionMetadata) reader.descriptor.getMetadataSerializer().deserialize(reader.descriptor, MetadataType.COMPACTION); + if (metadata != null) + cardinality = cardinality.merge(metadata.cardinalityEstimator); + + // use onDiskLength, which is the actual size of the SSTable data file. + long sstableSize = reader.onDiskLength(); + totalBytes += sstableSize; + + // get the on disk size for the token range, note for compressed data this includes the full + // chunks the start and end ranges are found in. + long approximateRangeBytesInSSTable = reader.onDiskSizeForPartitionPositions(reader.getPositionsForRanges(singletonRange)); + approxBytesInRange += Math.min(approximateRangeBytesInSSTable, sstableSize); + } + catch (IOException | CardinalityMergeException e) + { + logger.error("Error calculating size estimate for {}.{} for range {} on {}", keyspace, table, tokenRange, reader, e); + } + } + + long partitions = 0L; + if (totalBytes > 0) + { + // use the ratio from size to estimate the partitions in the range as well + double ratio = approxBytesInRange / (double) totalBytes; + partitions = (long) Math.max(1, Math.ceil(cardinality.cardinality() * ratio)); + } + return new SizeEstimate(repairType, keyspace, table, tokenRange, partitions, approxBytesInRange, totalBytes); + } + + /** + * Calculates the token ranges owned by this node for a given keyspace. + * + * @param primaryRangeOnly whether to use only primary token ranges or include replicated ones + * @param keyspaceName the name of the keyspace + * @return one or more token ranges owned by this node + */ + public static List> getTokenRanges(boolean primaryRangeOnly, String keyspaceName) + { + // Collect all applicable token ranges + Collection> wrappedRanges; + if (primaryRangeOnly) + { + wrappedRanges = StorageService.instance.getPrimaryRanges(keyspaceName); + } + else + { + wrappedRanges = StorageService.instance.getLocalRanges(keyspaceName); + } + + // Unwrap each range as we need to account for ranges that overlap the ring + List> ranges = new ArrayList<>(); + for (Range wrappedRange : wrappedRanges) + { + ranges.addAll(wrappedRange.unwrap()); + } + return ranges; + } + + /** + * Calculates the total bytes to be repaired for a given keyspace and list of tables. + * + * @param repairType the repair type (e.g., FULL, INCREMENTAL) + * @param keyspaceName the name of the keyspace + * @param tableNames the list of tables + * @return a key-value map where the key is {@code keyspaceName.tableName} and the value is the number of bytes + * to be repaired. + */ + public static Map, SizeEstimate>> calcTotalBytesToBeRepaired(RepairType repairType, String keyspaceName, List tableNames, List> tokenRanges) + { + Map, SizeEstimate>> ksTablesEstimatedBytes = new HashMap<>(); + for (String tableName : tableNames) + { + String ksTable = getKeyspaceTableName(keyspaceName, tableName); + ksTablesEstimatedBytes.computeIfAbsent(ksTable, k -> new HashMap<>()); + Map, SizeEstimate> tokenToSize = ksTablesEstimatedBytes.get(ksTable); + for (Range tokenRange : tokenRanges) + { + SizeEstimate tableAssignments = getRangeSizeEstimate(repairType, keyspaceName, tableName, tokenRange); + tokenToSize.put(tokenRange, tableAssignments); + } + } + return ksTablesEstimatedBytes; + } + + public static String getKeyspaceTableName(String keyspace, String table) + { + return keyspace + "." + table; + } + + /** + * Represents a size estimate by both bytes and partition count for a given keyspace and table for a token range. + */ + @VisibleForTesting + protected static class SizeEstimate + { + public final RepairType repairType; + public final String keyspace; + public final String table; + public final Range tokenRange; + public final long partitions; + public final long sizeInRange; + public final long totalSize; + /** + * Size to consider in the repair. For incremental repair, we want to consider the total size + * of the estimate as we have to factor in anticompacting the entire SSTable. + * For full repair, just use the size containing the range. + */ + public final long sizeForRepair; + + public SizeEstimate(RepairType repairType, + String keyspace, String table, Range tokenRange, + long partitions, long sizeInRange, long totalSize) + { + this.repairType = repairType; + this.keyspace = keyspace; + this.table = table; + this.tokenRange = tokenRange; + this.partitions = partitions; + this.sizeInRange = sizeInRange; + this.totalSize = totalSize; + + this.sizeForRepair = repairType == RepairType.INCREMENTAL ? totalSize : sizeInRange; + } + + @Override + public String toString() + { + return "SizeEstimate{" + + "repairType=" + repairType + + ", keyspace='" + keyspace + '\'' + + ", table='" + table + '\'' + + ", tokenRange=" + tokenRange + + ", partitions=" + partitions + + ", sizeInRange=" + sizeInRange + + ", totalSize=" + totalSize + + ", sizeForRepair=" + sizeForRepair + + '}'; + } + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/FixedSplitTokenRangeSplitter.java b/src/java/org/apache/cassandra/repair/autorepair/FixedSplitTokenRangeSplitter.java new file mode 100644 index 000000000000..1b2c80b74271 --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/FixedSplitTokenRangeSplitter.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.repair.autorepair; + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.service.AutoRepairService; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; + +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.split; + +/** + * An implementation that splits token ranges into a fixed number of subranges. + */ +public class FixedSplitTokenRangeSplitter implements IAutoRepairTokenRangeSplitter +{ + private static final Logger logger = LoggerFactory.getLogger(FixedSplitTokenRangeSplitter.class); + + /** + * Selecting the default value is tricky. If we select a small number, individual repairs would be heavy. + * On the other hand, if we select a large number, too many repair sessions would be created. + *

    + * If vnodes are configured using num_tokens, attempts to evenly subdivide subranges by each range + * using the following formula: + *

    + * Math.max(1, numberOfSubranges / tokens.size()) + *

    + * To maintain balance, 32 serves as a good default that accommodates both vnodes and non-vnodes effectively. + */ + public static final int DEFAULT_NUMBER_OF_SUBRANGES = 32; + + /** + * Number of evenly split subranges to create for each node that repair runs for. + *

    + * If vnodes are configured using num_tokens, attempts to evenly subdivide subranges by each range. + * For example, for num_tokens: 16 and number_of_subranges: 32, 2 (32/16) + * repair assignments will be created for each token range. At least one repair assignment will be + * created for each token range. + */ + static final String NUMBER_OF_SUBRANGES = "number_of_subranges"; + + private final AutoRepairConfig.RepairType repairType; + private int numberOfSubranges; + + public FixedSplitTokenRangeSplitter(AutoRepairConfig.RepairType repairType, Map parameters) + { + this.repairType = repairType; + + numberOfSubranges = Integer.parseInt(parameters.getOrDefault(NUMBER_OF_SUBRANGES, Integer.toString(DEFAULT_NUMBER_OF_SUBRANGES))); + } + + @Override + public Iterator getRepairAssignments(boolean primaryRangeOnly, List repairPlans) + { + return new RepairAssignmentIterator(repairPlans) + { + @Override + protected KeyspaceRepairAssignments next(int priority, KeyspaceRepairPlan repairPlan) + { + return getRepairAssignmentsForKeyspace(primaryRangeOnly, priority, repairPlan); + } + }; + } + + private KeyspaceRepairAssignments getRepairAssignmentsForKeyspace(boolean primaryRangeOnly, int priority, KeyspaceRepairPlan repairPlan) + { + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + List repairAssignments = new ArrayList<>(); + String keyspaceName = repairPlan.getKeyspaceName(); + List tableNames = repairPlan.getTableNames(); + + Collection> tokens = AutoRepairUtils.getTokenRanges(primaryRangeOnly, keyspaceName); + boolean byKeyspace = config.getRepairByKeyspace(repairType); + // collect all token ranges. + List> allRanges = new ArrayList<>(); + // this is done to avoid micro splits in the case of vnodes + int splitsPerRange = Math.max(1, numberOfSubranges / tokens.size()); + for (Range token : tokens) + { + allRanges.addAll(split(token, splitsPerRange)); + } + + if (byKeyspace) + { + + // This calculation is the best effort for the FixedSplitTokenRangeSplitter. + // In practice, this metric may not give you an accurate view in case of uneven data distribution. + long totalBytes = repairPlan.getEstimatedBytes(); + long bytesPerRange = Math.max(1, totalBytes / splitsPerRange); + for (Range splitRange : allRanges) + { + // add repair assignment for each range entire keyspace's tables + repairAssignments.add(new RepairAssignment(splitRange, keyspaceName, tableNames, bytesPerRange)); + } + } + else + { + // add repair assignment per table + for (String tableName : tableNames) + { + long totalBytes = repairPlan.getTableEstimatedBytes(AutoRepairUtils.getKeyspaceTableName(keyspaceName, tableName)); + long bytesPerRange = Math.max(1, totalBytes / splitsPerRange); + for (Range splitRange : allRanges) + { + repairAssignments.add(new RepairAssignment(splitRange, keyspaceName, Collections.singletonList(tableName), bytesPerRange)); + } + } + } + return new KeyspaceRepairAssignments(priority, keyspaceName, repairAssignments); + } + + @Override + public void setParameter(String key, String value) + { + if (!key.equals(NUMBER_OF_SUBRANGES)) + { + throw new IllegalArgumentException("Unexpected parameter '" + key + "', must be " + NUMBER_OF_SUBRANGES); + } + logger.info("Setting {} to {} for repair type {}", key, value, repairType); + this.numberOfSubranges = Integer.parseInt(value); + } + + @Override + public Map getParameters() + { + return Collections.singletonMap(NUMBER_OF_SUBRANGES, Integer.toString(numberOfSubranges)); + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/IAutoRepairTokenRangeSplitter.java b/src/java/org/apache/cassandra/repair/autorepair/IAutoRepairTokenRangeSplitter.java new file mode 100644 index 000000000000..8b82eac296db --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/IAutoRepairTokenRangeSplitter.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.repair.autorepair; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.config.ParameterizedClass; + +/** + * Interface that defines how to generate {@link KeyspaceRepairAssignments}. + *

    + * The default is {@link RepairTokenRangeSplitter} which aims to provide sensible defaults for all repair types. + *

    + * Custom implementations class should require a constructor accepting + * ({@link AutoRepairConfig.RepairType}, {@link java.util.Map}) with the {@link java.util.Map} parameter accepting + * custom configuration for your splitter. If such a constructor does not exist, + * {@link AutoRepairConfig#newAutoRepairTokenRangeSplitter(AutoRepairConfig.RepairType, ParameterizedClass)} + * will fall back on invoking a default zero argument constructor. + */ +public interface IAutoRepairTokenRangeSplitter +{ + /** + * Split the token range you wish to repair into multiple assignments. + * The autorepair framework will repair the assignments from returned subrange iterator in the sequence it's + * provided. + * @param primaryRangeOnly Whether to repair only this node's primary ranges or all of its ranges. + * @param repairPlans A list of ordered prioritized repair plans to generate assignments for in order. + * @return iterator of repair assignments, with each element representing a grouping of repair assignments for a given keyspace. + * The iterator is traversed lazily {@link KeyspaceRepairAssignments} at a time with the intent to try to get the + * most up-to-date representation of your data (e.g. how much data exists and is unrepaired at a given time). + */ + Iterator getRepairAssignments(boolean primaryRangeOnly, List repairPlans); + + /** + * Update a configuration parameter. This is meant to be used by nodetool setautorepairconfig to + * update configuration dynamically. + * @param key parameter to update + * @param value The value to set to. + */ + default void setParameter(String key, String value) + { + throw new IllegalArgumentException(this.getClass().getName() + " does not support custom configuration"); + } + + /** + * @return custom configuration. This is meant to be used by nodetool getautorepairconfig for + * retrieving the splitter configuration. + */ + default Map getParameters() + { + return Collections.emptyMap(); + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/KeyspaceRepairAssignments.java b/src/java/org/apache/cassandra/repair/autorepair/KeyspaceRepairAssignments.java new file mode 100644 index 000000000000..3ea91e9922f9 --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/KeyspaceRepairAssignments.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.repair.autorepair; + +import java.util.List; + +/** + * A grouping of repair assignments that were generated for a particular keyspace for a given priority. + */ +public class KeyspaceRepairAssignments +{ + private final int priority; + private final String keyspaceName; + private final List repairAssignments; + + public KeyspaceRepairAssignments(int priority, String keyspaceName, List repairAssignments) + { + this.priority = priority; + this.keyspaceName = keyspaceName; + this.repairAssignments = repairAssignments; + } + + public int getPriority() + { + return priority; + } + + public String getKeyspaceName() + { + return keyspaceName; + } + + public List getRepairAssignments() + { + return repairAssignments; + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/KeyspaceRepairPlan.java b/src/java/org/apache/cassandra/repair/autorepair/KeyspaceRepairPlan.java new file mode 100644 index 000000000000..750e56e9d2d3 --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/KeyspaceRepairPlan.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.repair.autorepair; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; + +/** + * Encapsulates an intent to repair the given keyspace's tables + */ +public class KeyspaceRepairPlan +{ + private final String keyspaceName; + + private final List tableNames; + + @VisibleForTesting + public Map, AutoRepairUtils.SizeEstimate>> ksTablesEstimatedBytes; + + public KeyspaceRepairPlan(String keyspaceName, List tableNames, Map, AutoRepairUtils.SizeEstimate>> ksTablesEstimatedBytes) + { + this.keyspaceName = keyspaceName; + this.tableNames = tableNames; + this.ksTablesEstimatedBytes = ksTablesEstimatedBytes; + } + + public String getKeyspaceName() + { + return keyspaceName; + } + + public List getTableNames() + { + return tableNames; + } + + public long getEstimatedBytes() + { + return ksTablesEstimatedBytes.values().stream() + .flatMap(tableMap -> tableMap.values().stream()) + .mapToLong(sizeEstimate -> sizeEstimate.sizeForRepair) + .sum(); + } + + public long getTableEstimatedBytes(String keyspaceTableName) + { + return ksTablesEstimatedBytes.getOrDefault(keyspaceTableName, + Collections.emptyMap()).values().stream().mapToLong(sizeEstimate -> sizeEstimate.sizeForRepair).sum(); + } + + public AutoRepairUtils.SizeEstimate getSizeEstimate(String keyspaceTableName, Range tokenRange) + { + return ksTablesEstimatedBytes == null ? null + : ksTablesEstimatedBytes.getOrDefault(keyspaceTableName, null) == null ? null + : ksTablesEstimatedBytes.get(keyspaceTableName).get(tokenRange); + } + + @Override + public boolean equals(Object o) + { + if (o == null || getClass() != o.getClass()) return false; + KeyspaceRepairPlan that = (KeyspaceRepairPlan) o; + return Objects.equals(keyspaceName, that.keyspaceName) && Objects.equals(tableNames, that.tableNames) + && Objects.equals(ksTablesEstimatedBytes, that.ksTablesEstimatedBytes); + } + + @Override + public int hashCode() + { + return Objects.hash(keyspaceName, tableNames, ksTablesEstimatedBytes); + } + + @Override + public String toString() + { + return "KeyspaceRepairPlan{" + + "keyspaceName='" + keyspaceName + '\'' + + ", tableNames=" + tableNames + + ", ksTablesEstimatedBytes=" + ksTablesEstimatedBytes + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/PrioritizedRepairPlan.java b/src/java/org/apache/cassandra/repair/autorepair/PrioritizedRepairPlan.java new file mode 100644 index 000000000000..4457ccdd348e --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/PrioritizedRepairPlan.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.repair.autorepair; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +import org.apache.cassandra.db.ColumnFamilyStore; + +/** + * Encapsulates a devised plan to repair tables, grouped by their keyspace and a given priority. This is used + * by {@link AutoRepair} to pass in an organized plan to + * {@link IAutoRepairTokenRangeSplitter#getRepairAssignments(boolean, List)} which + * can iterate over this plan in order to generate {@link RepairAssignment}s. + */ +public class PrioritizedRepairPlan +{ + private final int priority; + + private final List keyspaceRepairPlans; + + public PrioritizedRepairPlan(int priority, List keyspaceRepairPlans) + { + this.priority = priority; + this.keyspaceRepairPlans = keyspaceRepairPlans; + } + + public int getPriority() + { + return priority; + } + + public List getKeyspaceRepairPlans() + { + return keyspaceRepairPlans; + } + + @Override + public boolean equals(Object o) + { + if (o == null || getClass() != o.getClass()) return false; + PrioritizedRepairPlan that = (PrioritizedRepairPlan) o; + return priority == that.priority && Objects.equals(keyspaceRepairPlans, that.keyspaceRepairPlans); + } + + @Override + public int hashCode() + { + return Objects.hash(priority, keyspaceRepairPlans); + } + + @Override + public String toString() + { + return "PrioritizedRepairPlan{" + + "priority=" + priority + + ", keyspaceRepairPlans=" + keyspaceRepairPlans + + '}'; + } + + /** + * Builds a list of {@link PrioritizedRepairPlan}s for the given keyspace and table map, ordered by priority from + * highest to lowest, where priority is derived from table schema's defined priority for the given repair type. + *

    + * If a keyspace has tables with differing priorities, those tables will be included in the PrioritizedRepairPlan + * for their given priority. + * + * @param keyspacesToTableNames A mapping keyspace to table names + * @param repairType The repair type that is being executed + * @param orderFunc A function to order keyspace and tables in the returned plan. + * @return Ordered list of plan's by table priorities. + */ + public static List build(Map> keyspacesToTableNames, AutoRepairConfig.RepairType repairType, Consumer> orderFunc, boolean primaryRangeOnly) + { + // Build a map of priority -> (keyspace -> tables) + Map>> plans = new HashMap<>(); + for (Map.Entry> keyspaceToTableNames : keyspacesToTableNames.entrySet()) + { + String keyspaceName = keyspaceToTableNames.getKey(); + for (String tableName : keyspaceToTableNames.getValue()) + { + int priority = getPriority(repairType, keyspaceName, tableName); + Map> keyspacesForPriority = plans.computeIfAbsent(priority, p -> new HashMap<>()); + List tableNamesAtPriority = keyspacesForPriority.computeIfAbsent(keyspaceName, k -> new ArrayList<>()); + tableNamesAtPriority.add(tableName); + } + } + + // Extract map into a List ordered by priority from highest to lowest. + List planList = new ArrayList<>(plans.size()); + TreeSet priorities = new TreeSet<>(Comparator.reverseOrder()); + priorities.addAll(plans.keySet()); + for (int priority : priorities) + { + Map> keyspacesAndTables = plans.get(priority); + List keyspaceRepairPlans = new ArrayList<>(keyspacesAndTables.size()); + planList.add(new PrioritizedRepairPlan(priority, keyspaceRepairPlans)); + + // Order keyspace and table names based on the input function (typically, this would shuffle the keyspace + // and table names randomly). + List keyspaceNames = new ArrayList<>(keyspacesAndTables.keySet()); + orderFunc.accept(keyspaceNames); + + for (String keyspaceName : keyspaceNames) + { + List tableNames = keyspacesAndTables.get(keyspaceName); + orderFunc.accept(tableNames); + KeyspaceRepairPlan keyspaceRepairPlan = + new KeyspaceRepairPlan(keyspaceName, new ArrayList<>(tableNames), + AutoRepairUtils.calcTotalBytesToBeRepaired(repairType, keyspaceName, tableNames, AutoRepairUtils.getTokenRanges(primaryRangeOnly, keyspaceName))); + keyspaceRepairPlans.add(keyspaceRepairPlan); + } + } + + return planList; + } + + /** + * @return The priority of the given table if defined, otherwise 0. + */ + private static int getPriority(AutoRepairConfig.RepairType repairType, String keyspaceName, String tableName) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(keyspaceName, tableName); + return cfs != null ? cfs.metadata().params.autoRepair.priority() : 0; + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/RepairAssignment.java b/src/java/org/apache/cassandra/repair/autorepair/RepairAssignment.java new file mode 100644 index 000000000000..6e07399aad7d --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/RepairAssignment.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.repair.autorepair; + +import java.util.List; +import java.util.Objects; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; + +/** + * Defines a repair assignment to be issued by the autorepair framework. + */ +public class RepairAssignment +{ + final Range tokenRange; + + final String keyspaceName; + + final List tableNames; + + protected final long estimatedBytes; + + public RepairAssignment(Range tokenRange, String keyspaceName, List tableNames, long estimatedBytes) + { + this.tokenRange = tokenRange; + this.keyspaceName = keyspaceName; + this.tableNames = tableNames; + this.estimatedBytes = estimatedBytes; + } + + public Range getTokenRange() + { + return tokenRange; + } + + public String getKeyspaceName() + { + return keyspaceName; + } + + public List getTableNames() + { + return tableNames; + } + + public long getEstimatedBytes() + { + return estimatedBytes; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RepairAssignment that = (RepairAssignment) o; + return Objects.equals(tokenRange, that.tokenRange) && Objects.equals(keyspaceName, that.keyspaceName) + && Objects.equals(tableNames, that.tableNames) && Objects.equals(estimatedBytes, that.estimatedBytes); + } + + @Override + public int hashCode() + { + return Objects.hash(tokenRange, keyspaceName, tableNames, estimatedBytes); + } + + @Override + public String toString() + { + return "RepairAssignment{" + + "tokenRange=" + tokenRange + + ", keyspaceName='" + keyspaceName + '\'' + + ", tableNames=" + tableNames + + ", estimatedBytes=" + estimatedBytes + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/RepairAssignmentIterator.java b/src/java/org/apache/cassandra/repair/autorepair/RepairAssignmentIterator.java new file mode 100644 index 000000000000..44d9f5ef5e55 --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/RepairAssignmentIterator.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.repair.autorepair; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Convenience {@link Iterator} implementation to assist implementations of + * {@link IAutoRepairTokenRangeSplitter#getRepairAssignments(boolean, List)} by passing {@link KeyspaceRepairPlan} + * to a custom {@link #next(int, KeyspaceRepairPlan)} method in priority order. + */ +public abstract class RepairAssignmentIterator implements Iterator +{ + private final Iterator repairPlanIterator; + + private Iterator currentIterator = null; + private PrioritizedRepairPlan currentPlan = null; + + public RepairAssignmentIterator(List repairPlans) + { + this.repairPlanIterator = repairPlans.iterator(); + } + + private synchronized Iterator currentIterator() + { + if (currentIterator == null || !currentIterator.hasNext()) + { + // Advance the repair plan iterator if the current repair plan is exhausted, but only + // if there are more repair plans. + if (repairPlanIterator.hasNext()) + { + currentPlan = repairPlanIterator.next(); + currentIterator = currentPlan.getKeyspaceRepairPlans().iterator(); + } + } + return currentIterator; + } + + @Override + public boolean hasNext() + { + Iterator iterator = currentIterator(); + return (iterator != null && iterator.hasNext()); + } + + @Override + public KeyspaceRepairAssignments next() + { + if (!hasNext()) + { + throw new NoSuchElementException("No remaining repair plans"); + } + + final KeyspaceRepairPlan repairPlan = currentIterator().next(); + return next(currentPlan.getPriority(), repairPlan); + } + + /** + * Invoked by {@link #next()} with the next {@link KeyspaceRepairPlan} for the given priority. + * @param priority current priority being processed. + * @param repairPlan the next keyspace repair plan to process + * @return assignments for the given keyspace at this priority. Should never return null, if one desires to + * short-circuit the iterator, override {@link #hasNext()}. + */ + protected abstract KeyspaceRepairAssignments next(int priority, KeyspaceRepairPlan repairPlan); +} diff --git a/src/java/org/apache/cassandra/repair/autorepair/RepairTokenRangeSplitter.java b/src/java/org/apache/cassandra/repair/autorepair/RepairTokenRangeSplitter.java new file mode 100644 index 000000000000..4e44b06208b3 --- /dev/null +++ b/src/java/org/apache/cassandra/repair/autorepair/RepairTokenRangeSplitter.java @@ -0,0 +1,810 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.repair.autorepair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DataStorageSpec; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.lifecycle.SSTableIntervalTree; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.db.lifecycle.View; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.service.AutoRepairService; +import org.apache.cassandra.utils.concurrent.Refs; + +import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.split; + +/** + * The default implementation of {@link IAutoRepairTokenRangeSplitter} that attempts to: + *

      + *
    1. Create smaller, consistent repair times
    2. + *
    3. Minimize the impact on hosts
    4. + *
    5. Reduce overstreaming
    6. + *
    7. Reduce number of repairs
    8. + *
    + *

    + * To achieve these goals, this implementation inspects SSTable metadata to estimate the bytes and number of partitions + * within a range and splits it accordingly to bound the size of the token ranges used for repair assignments. + *

    + *

    + * Refer to + * Auto Repair documentation for this implementation + * for a more thorough breakdown of this implementation. + *

    + *

    + * While this splitter has a lot of tuning parameters, the expectation is that the established default configuration + * shall be sensible for all {@link org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType}'s. The following + * configuration parameters are offered. + *

    + * + *

    Configuration parameters:

    + *
      + *
    • bytes_per_assignment – Target size (in compressed bytes) for each repair. Throttles incremental repair + * and anticompaction per schedule after incremental repairs are enabled.
    • + * + *
    • max_bytes_per_schedule – Maximum data (in compressed bytes) to cover in a single schedule. Acts as a + * throttle for the repair cycle workload. Tune this up if writes are outpacing repair, or down if repairs are too + * disruptive. Alternatively, adjust {@code min_repair_interval}.
    • + * + *
    • partitions_per_assignment – Maximum number of partitions per repair assignment. Limits the number of + * partitions in Merkle tree leaves to prevent overstreaming.
    • + * + *
    • max_tables_per_assignment – Maximum number of tables to include in a single repair assignment. + * Especially useful for keyspaces with many tables. Prevents excessive batching of tables that exceed other + * parameters like {@code bytes_per_assignment} or {@code partitions_per_assignment}.
    • + *
    + */ +public class RepairTokenRangeSplitter implements IAutoRepairTokenRangeSplitter +{ + private static final Logger logger = LoggerFactory.getLogger(RepairTokenRangeSplitter.class); + + // Default max bytes to 100TiB, which is much more readable than Long.MAX_VALUE + private static final DataStorageSpec.LongBytesBound MAX_BYTES = new DataStorageSpec.LongBytesBound(102_400, DataStorageSpec.DataStorageUnit.GIBIBYTES); + + /** + * The target bytes that should be included in a repair assignment + */ + static final String BYTES_PER_ASSIGNMENT = "bytes_per_assignment"; + + /** + * Maximum number of partitions to include in a repair assignment + */ + static final String PARTITIONS_PER_ASSIGNMENT = "partitions_per_assignment"; + + /** + * Maximum number of tables to include in a repair assignment if {@link AutoRepairConfig.Options#repair_by_keyspace} + * is enabled + */ + static final String MAX_TABLES_PER_ASSIGNMENT = "max_tables_per_assignment"; + + /** + * The maximum number of bytes to cover in an individual schedule + */ + static final String MAX_BYTES_PER_SCHEDULE = "max_bytes_per_schedule"; + + static final List PARAMETERS = Arrays.asList(BYTES_PER_ASSIGNMENT, PARTITIONS_PER_ASSIGNMENT, MAX_TABLES_PER_ASSIGNMENT, MAX_BYTES_PER_SCHEDULE); + + private final AutoRepairConfig.RepairType repairType; + + private final Map givenParameters = new HashMap<>(); + + private DataStorageSpec.LongBytesBound bytesPerAssignment; + private long partitionsPerAssignment; + private int maxTablesPerAssignment; + private DataStorageSpec.LongBytesBound maxBytesPerSchedule; + + /** + * Established default for each {@link org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType}, meant to + * choose sensible defaults for each. + *

    + * Defaults if not specified for the given repair type: + *

  • + *
      bytes_per_assignment: 50GiB
    + *
      partitions_per_assignment: 1048576 (2^20)
    + *
      max_tables_per_assignment: 64
    + *
      max_bytes_per_schedule: 1000GiB
    + *
  • + * It's expected that these defaults should work well for everything except incremental, where we set + * max_bytes_per_schedule to 100GiB. This should strike a good balance between the amount of data that will be + * repaired during an initial migration to incremental repair and should move the entire repaired set from + * unrepaired to repaired at steady state, assuming not more the 100GiB of data is written to a node per + * min_repair_interval. + */ + private static final Map DEFAULTS_BY_REPAIR_TYPE = new EnumMap(AutoRepairConfig.RepairType.class) {{ + put(AutoRepairConfig.RepairType.FULL, RepairTypeDefaults.builder(AutoRepairConfig.RepairType.FULL) + .build()); + // Restrict incremental repair to 100GiB max bytes per schedule to confine the amount of possible autocompaction. + put(AutoRepairConfig.RepairType.INCREMENTAL, RepairTypeDefaults.builder(AutoRepairConfig.RepairType.INCREMENTAL) + .withMaxBytesPerSchedule(new DataStorageSpec.LongBytesBound("100GiB")) + .build()); + put(AutoRepairConfig.RepairType.PREVIEW_REPAIRED, RepairTypeDefaults.builder(AutoRepairConfig.RepairType.PREVIEW_REPAIRED) + .build()); + }}; + + public RepairTokenRangeSplitter(AutoRepairConfig.RepairType repairType, Map parameters) + { + this.repairType = repairType; + this.givenParameters.putAll(parameters); + + reinitParameters(); + } + + private void reinitParameters() + { + RepairTypeDefaults defaults = DEFAULTS_BY_REPAIR_TYPE.get(repairType); + + DataStorageSpec.LongBytesBound bytesPerAssignmentTmp = getPropertyOrDefault(BYTES_PER_ASSIGNMENT, DataStorageSpec.LongBytesBound::new, defaults.bytesPerAssignment); + DataStorageSpec.LongBytesBound maxBytesPerScheduleTmp = getPropertyOrDefault(MAX_BYTES_PER_SCHEDULE, DataStorageSpec.LongBytesBound::new, defaults.maxBytesPerSchedule); + + // Validate that bytesPerAssignment <= maxBytesPerSchedule + if (bytesPerAssignmentTmp.toBytes() > maxBytesPerScheduleTmp.toBytes()) + { + throw new IllegalArgumentException(String.format("%s='%s' cannot be greater than %s='%s' for %s", + BYTES_PER_ASSIGNMENT, + bytesPerAssignmentTmp, + MAX_BYTES_PER_SCHEDULE, + maxBytesPerScheduleTmp, + repairType.getConfigName())); + } + + bytesPerAssignment = bytesPerAssignmentTmp; + maxBytesPerSchedule = maxBytesPerScheduleTmp; + + partitionsPerAssignment = getPropertyOrDefault(PARTITIONS_PER_ASSIGNMENT, Long::parseLong, defaults.partitionsPerAssignment); + maxTablesPerAssignment = getPropertyOrDefault(MAX_TABLES_PER_ASSIGNMENT, Integer::parseInt, defaults.maxTablesPerAssignment); + + logger.info("Configured {}[{}] with {}={}, {}={}, {}={}, {}={}", RepairTokenRangeSplitter.class.getName(), + repairType.getConfigName(), + BYTES_PER_ASSIGNMENT, bytesPerAssignment, + PARTITIONS_PER_ASSIGNMENT, partitionsPerAssignment, + MAX_TABLES_PER_ASSIGNMENT, maxTablesPerAssignment, + MAX_BYTES_PER_SCHEDULE, maxBytesPerSchedule); + } + + private T getPropertyOrDefault(String propertyName, Function mapper, T defaultValue) + { + return Optional.ofNullable(this.givenParameters.get(propertyName)).map(mapper).orElse(defaultValue); + } + + @Override + public Iterator getRepairAssignments(boolean primaryRangeOnly, List repairPlans) + { + return new BytesBasedRepairAssignmentIterator(primaryRangeOnly, repairPlans); + } + + /** + * A custom {@link RepairAssignmentIterator} that confines the number of repair assignments to + * max_bytes_per_schedule. + */ + private class BytesBasedRepairAssignmentIterator extends RepairAssignmentIterator + { + + private final boolean primaryRangeOnly; + private long bytesSoFar = 0; + + BytesBasedRepairAssignmentIterator(boolean primaryRangeOnly, List repairPlans) + { + super(repairPlans); + this.primaryRangeOnly = primaryRangeOnly; + } + + @Override + protected KeyspaceRepairAssignments next(int priority, KeyspaceRepairPlan repairPlan) + { + // short circuit if we've accumulated too many bytes by returning a KeyspaceRepairAssignments with + // no assignments. We do this rather than returning false in hasNext() because we want to signal + // to AutoRepair that a keyspace generated no assignments. + if (bytesSoFar >= maxBytesPerSchedule.toBytes()) + { + return new KeyspaceRepairAssignments(priority, repairPlan.getKeyspaceName(), Collections.emptyList()); + } + + List> tokenRanges = AutoRepairUtils.getTokenRanges(primaryRangeOnly, repairPlan.getKeyspaceName()); + // shuffle token ranges to unbias selection of ranges + Collections.shuffle(tokenRanges); + List repairAssignments = new ArrayList<>(); + // Generate assignments for each range speparately + for (Range tokenRange : tokenRanges) + { + repairAssignments.addAll(getRepairAssignmentsForKeyspace(repairType, repairPlan, tokenRange)); + } + + FilteredRepairAssignments filteredRepairAssignments = filterRepairAssignments(priority, repairPlan.getKeyspaceName(), repairAssignments, bytesSoFar); + bytesSoFar = filteredRepairAssignments.newBytesSoFar; + return new KeyspaceRepairAssignments(priority, repairPlan.getKeyspaceName(), filteredRepairAssignments.repairAssignments); + } + } + + @VisibleForTesting + List getRepairAssignmentsForKeyspace(AutoRepairConfig.RepairType repairType, KeyspaceRepairPlan repairPlan, Range tokenRange) + { + List repairAssignments = new ArrayList<>(); + // this is used for batching minimal single assignment tables together + List currentAssignments = new ArrayList<>(); + + AutoRepairConfig config = AutoRepairService.instance.getAutoRepairConfig(); + + // If we can repair by keyspace, sort the tables by size so can batch the smallest ones together + boolean repairByKeyspace = config.getRepairByKeyspace(repairType); + List tablesToProcess = repairPlan.getTableNames(); + if (repairByKeyspace) + { + tablesToProcess = repairPlan.getTableNames().stream().sorted((t1, t2) -> { + ColumnFamilyStore cfs1 = ColumnFamilyStore.getIfExists(repairPlan.getKeyspaceName(), t1); + ColumnFamilyStore cfs2 = ColumnFamilyStore.getIfExists(repairPlan.getKeyspaceName(), t2); + // If for whatever reason the CFS is not retrievable, we can assume it has been deleted, so give the + // other cfs precedence. + if (cfs1 == null) + { + // cfs1 is lesser than because its null + return -1; + } + else if (cfs2 == null) + { + // cfs1 is greather than because cfs2 is null + return 1; + } + return Long.compare(cfs1.metric.totalDiskSpaceUsed.getCount(), cfs2.metric.totalDiskSpaceUsed.getCount()); + }).collect(Collectors.toList()); + } + + for (String tableName : tablesToProcess) + { + List tableAssignments = getRepairAssignmentsForTable(repairPlan, tableName, tokenRange); + + if (tableAssignments.isEmpty()) + continue; + + // if not repairing by keyspace don't attempt to batch them with others. + if (!repairByKeyspace) + { + repairAssignments.addAll(tableAssignments); + } + // If the table assignments are for the same token range, and we have room to add more tables to the current assignment + else if (tableAssignments.size() == 1 && + currentAssignments.size() < maxTablesPerAssignment && + (currentAssignments.isEmpty() || currentAssignments.get(0).getTokenRange().equals(tableAssignments.get(0).getTokenRange()))) + { + long currentAssignmentsBytes = getEstimatedBytes(currentAssignments); + long tableAssignmentsBytes = getEstimatedBytes(tableAssignments); + // only add assignments together if they don't exceed max bytes per schedule. + if (currentAssignmentsBytes + tableAssignmentsBytes < maxBytesPerSchedule.toBytes()) { + currentAssignments.addAll(tableAssignments); + } + else + { + // add table assignments by themselves + repairAssignments.addAll(tableAssignments); + } + } + else + { + if (!currentAssignments.isEmpty()) + { + repairAssignments.add(merge(currentAssignments)); + currentAssignments.clear(); + } + repairAssignments.addAll(tableAssignments); + } + } + + if (!currentAssignments.isEmpty()) + repairAssignments.add(merge(currentAssignments)); + + return repairAssignments; + } + + /** + * Given a repair type and map of sized-based repair assignments, confine them by maxBytesPerSchedule. + * + * @param repairAssignments the assignments to filter. + * @param bytesSoFar repair assignment bytes accumulated so far. + * @return A list of repair assignments confined by maxBytesPerSchedule. + */ + @VisibleForTesting + FilteredRepairAssignments filterRepairAssignments(int priority, String keyspaceName, List repairAssignments, long bytesSoFar) + { + // Confine repair assignments by maxBytesPerSchedule. + long bytesSoFarThisIteration = 0L; + long bytesNotRepaired = 0L; + int assignmentsNotRepaired = 0; + int assignmentsToRepair = 0; + int totalAssignments = 0; + + List assignmentsToReturn = new ArrayList<>(repairAssignments.size()); + for (SizedRepairAssignment repairAssignment : repairAssignments) + { + totalAssignments++; + // skip any repair assignments that would accumulate us past the maxBytesPerSchedule + if (bytesSoFar + repairAssignment.getEstimatedBytes() > maxBytesPerSchedule.toBytes()) + { + // log that repair assignment was skipped. + bytesNotRepaired += repairAssignment.getEstimatedBytes(); + assignmentsNotRepaired++; + logger.warn("Skipping {} because it would increase total repair bytes to {}", + repairAssignment, + getBytesOfMaxBytesPerSchedule(bytesSoFar + repairAssignment.getEstimatedBytes())); + } + else + { + bytesSoFar += repairAssignment.getEstimatedBytes(); + bytesSoFarThisIteration += repairAssignment.getEstimatedBytes(); + assignmentsToRepair++; + logger.info("Adding {}, increasing repair bytes to {}", + repairAssignment, + getBytesOfMaxBytesPerSchedule(bytesSoFar)); + assignmentsToReturn.add(repairAssignment); + } + } + + String message = "Returning {} assignment(s) for priorityBucket {} and keyspace {}, totaling {} ({} overall)"; + if (assignmentsNotRepaired != 0) + { + message += ". Skipping {} of {} assignment(s), totaling {}"; + if (repairType != AutoRepairConfig.RepairType.INCREMENTAL) + { + message += ". The entire primary range will not be repaired this schedule. " + + "Consider increasing maxBytesPerSchedule, reducing node density or monitoring to ensure " + + "all ranges do get repaired within gc_grace_seconds"; + logger.warn(message, assignmentsToRepair, priority, keyspaceName, + FileUtils.stringifyFileSize(bytesSoFarThisIteration), + getBytesOfMaxBytesPerSchedule(bytesSoFar), + assignmentsNotRepaired, totalAssignments, + FileUtils.stringifyFileSize(bytesNotRepaired)); + } + else + { + logger.info(message, assignmentsToRepair, priority, keyspaceName, + FileUtils.stringifyFileSize(bytesSoFarThisIteration), + getBytesOfMaxBytesPerSchedule(bytesSoFar), + assignmentsNotRepaired, totalAssignments, + FileUtils.stringifyFileSize(bytesNotRepaired)); + } + } + else + { + logger.info(message, assignmentsToRepair, priority, keyspaceName, + FileUtils.stringifyFileSize(bytesSoFarThisIteration), + getBytesOfMaxBytesPerSchedule(bytesSoFar)); + } + + return new FilteredRepairAssignments(assignmentsToReturn, bytesSoFar); + } + + @VisibleForTesting + static class FilteredRepairAssignments + { + final List repairAssignments; + final long newBytesSoFar; + + private FilteredRepairAssignments(List repairAssignments, long newBytesSoFar) + { + this.repairAssignments = repairAssignments; + this.newBytesSoFar = newBytesSoFar; + } + } + + private String getBytesOfMaxBytesPerSchedule(long bytes) + { + if (maxBytesPerSchedule.equals(MAX_BYTES)) + return FileUtils.stringifyFileSize(bytes); + else + return String.format("%s of %s", FileUtils.stringifyFileSize(bytes), maxBytesPerSchedule); + } + + /** + * @param repairAssignments The assignments to sum + * @return The sum of {@link SizedRepairAssignment#getEstimatedBytes()} of all given + * repairAssignments. + */ + @VisibleForTesting + protected static long getEstimatedBytes(List repairAssignments) + { + return repairAssignments + .stream() + .mapToLong(SizedRepairAssignment::getEstimatedBytes) + .sum(); + } + + @VisibleForTesting + static SizedRepairAssignment merge(List assignments) + { + if (assignments.isEmpty()) + throw new IllegalStateException("Cannot merge empty assignments"); + + Set mergedTableNames = new HashSet<>(); + Range referenceTokenRange = assignments.get(0).getTokenRange(); + String referenceKeyspaceName = assignments.get(0).getKeyspaceName(); + + for (SizedRepairAssignment assignment : assignments) + { + // These checks _should_ be unnecessary but are here to ensure that the assignments are consistent + if (!assignment.getTokenRange().equals(referenceTokenRange)) + throw new IllegalStateException("All assignments must have the same token range"); + if (!assignment.getKeyspaceName().equals(referenceKeyspaceName)) + throw new IllegalStateException("All assignments must have the same keyspace name"); + + mergedTableNames.addAll(assignment.getTableNames()); + } + + long sizeForAssignment = getEstimatedBytes(assignments); + return new SizedRepairAssignment(referenceTokenRange, referenceKeyspaceName, new ArrayList<>(mergedTableNames), + "full primary range for " + mergedTableNames.size() + " tables", sizeForAssignment); + } + + @VisibleForTesting + protected List getRepairAssignmentsForTable(KeyspaceRepairPlan repairPlan, String tableName, Range tokenRange) + { + AutoRepairUtils.SizeEstimate sizeEstimate = repairPlan.getSizeEstimate(AutoRepairUtils.getKeyspaceTableName(repairPlan.getKeyspaceName(), tableName), tokenRange); + if (sizeEstimate == null) + { + // Ideally, it should have been cached already inside the KeyspaceRepairPlan, but incase it was not, + // then recalculating it. It is a bit expensive, but necessary for the repair + logger.warn("The size estimate for {}.{} range {} was not pre-calculated, calculating on-demand", + repairPlan.getKeyspaceName(), tableName, tokenRange); + sizeEstimate = AutoRepairUtils.getRangeSizeEstimate(repairType, repairPlan.getKeyspaceName(), tableName, tokenRange); + } + return getRepairAssignments(sizeEstimate); + } + + private static void logSkippingTable(String keyspaceName, String tableName) + { + logger.warn("Could not resolve table data for {}.{} assuming it has since been deleted, skipping", keyspaceName, tableName); + } + + @VisibleForTesting + protected List getRepairAssignments(AutoRepairUtils.SizeEstimate estimate) + { + List repairAssignments = new ArrayList<>(); + + // since its possible for us to hit maxBytesPerSchedule before seeing all ranges, shuffle so there is chance + // at least of hitting all the ranges _eventually_ for the worst case scenarios + int totalExpectedSubRanges = 0; + if (estimate.sizeForRepair != 0) + { + boolean needsSplitting = estimate.sizeForRepair > bytesPerAssignment.toBytes() || estimate.partitions > partitionsPerAssignment; + if (needsSplitting) + { + totalExpectedSubRanges += calculateNumberOfSplits(estimate); + } + } + if (estimate.sizeForRepair == 0) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(estimate.keyspace, estimate.table); + + if (cfs == null) + { + logSkippingTable(estimate.keyspace, estimate.table); + return Collections.emptyList(); + } + + long memtableSize = cfs.getTracker().getView().getCurrentMemtable().getLiveDataSize(); + if (memtableSize > 0L) + { + logger.debug("Included {}.{} range {}, had no unrepaired SSTables, but memtableSize={}, adding single repair assignment", estimate.keyspace, estimate.table, estimate.tokenRange, memtableSize); + SizedRepairAssignment assignment = new SizedRepairAssignment(estimate.tokenRange, estimate.keyspace, Collections.singletonList(estimate.table), "full primary rangee for table with memtable only detected", memtableSize); + repairAssignments.add(assignment); + } + else + { + logger.debug("Included {}.{} range {}, has no SSTables or memtable data, but adding single repair assignment for entire range in case writes were missed", estimate.keyspace, estimate.table, estimate.tokenRange); + SizedRepairAssignment assignment = new SizedRepairAssignment(estimate.tokenRange, estimate.keyspace, Collections.singletonList(estimate.table), "full primary range for table with no data detected", 0L); + repairAssignments.add(assignment); + } + } + else + { + // Check if the estimate needs splitting based on the criteria + boolean needsSplitting = estimate.sizeForRepair > bytesPerAssignment.toBytes() || estimate.partitions > partitionsPerAssignment; + if (needsSplitting) + { + int numberOfSplits = calculateNumberOfSplits(estimate); + long approximateBytesPerSplit = estimate.sizeForRepair / numberOfSplits; + Collection> subranges = split(estimate.tokenRange, numberOfSplits); + for (Range subrange : subranges) + { + SizedRepairAssignment assignment = new SizedRepairAssignment(subrange, estimate.keyspace, Collections.singletonList(estimate.table), + String.format("subrange %d of %d", repairAssignments.size() + 1, totalExpectedSubRanges), + approximateBytesPerSplit); + repairAssignments.add(assignment); + } + } + else + { + // No splitting needed, repair the entire range as-is + SizedRepairAssignment assignment = new SizedRepairAssignment(estimate.tokenRange, estimate.keyspace, + Collections.singletonList(estimate.table), + "full primary range for table", estimate.sizeForRepair); + repairAssignments.add(assignment); + } + } + return repairAssignments; + } + + private int calculateNumberOfSplits(AutoRepairUtils.SizeEstimate estimate) + { + // Calculate the number of splits needed for size and partitions + int splitsForSize = (int) Math.ceil((double) estimate.sizeForRepair / bytesPerAssignment.toBytes()); + int splitsForPartitions = (int) Math.ceil((double) estimate.partitions / partitionsPerAssignment); + + // Split the token range into subranges based on whichever (partitions, bytes) would generate the most splits. + boolean splitBySize = splitsForSize > splitsForPartitions; + int splits = splitBySize ? splitsForSize : splitsForPartitions; + + // calculate approximation for logging purposes + long approximateBytesPerSplit = estimate.sizeForRepair / splits; + long approximatePartitionsPerSplit = estimate.partitions / splits; + + logger.info("Splitting {}.{} for range {} into {} sub ranges by {} (splitsForSize={}, splitsForPartitions={}, " + + "approximateBytesInRange={}, approximatePartitionsInRange={}, " + + "approximateBytesPerSplit={}, approximatePartitionsPerSplit={})", + estimate.keyspace, estimate.table, estimate.tokenRange, + splits, splitBySize ? "size" : "partitions", + splitsForSize, splitsForPartitions, + FileUtils.stringifyFileSize(estimate.sizeForRepair), estimate.partitions, + FileUtils.stringifyFileSize(approximateBytesPerSplit), approximatePartitionsPerSplit + ); + return splits; + } + + @VisibleForTesting + static Refs getSSTableReaderRefs(AutoRepairConfig.RepairType repairType, String keyspaceName, String tableName, Range tokenRange) + { + final ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(keyspaceName, tableName); + if (cfs == null) + { + logSkippingTable(keyspaceName, tableName); + return Refs.ref(Collections.emptyList()); + } + + Refs refs = null; + while (refs == null) + { + Iterable sstables = cfs.getTracker().getView().select(SSTableSet.CANONICAL); + SSTableIntervalTree tree = SSTableIntervalTree.buildSSTableIntervalTree(Lists.newArrayList(sstables)); + Range r = Range.makeRowRange(tokenRange); + List canonicalSSTables = View.sstablesInBounds(r.left, r.right, tree); + if (repairType == AutoRepairConfig.RepairType.INCREMENTAL) + { + canonicalSSTables = canonicalSSTables.stream().filter((sstable) -> !sstable.isRepaired()).collect(Collectors.toList()); + } + refs = Refs.tryRef(canonicalSSTables); + } + return refs; + } + + @Override + public void setParameter(String key, String value) + { + if (!PARAMETERS.contains(key)) + { + throw new IllegalArgumentException("Unexpected parameter '" + key + "', must be one of " + PARAMETERS); + } + + logger.info("Setting {} to {} for repair type {}", key, value, repairType); + givenParameters.put(key, value); + reinitParameters(); + } + + @Override + public Map getParameters() + { + final Map parameters = new LinkedHashMap<>(); + for (String parameter : PARAMETERS) + { + // Use the parameter as provided if present. + if (givenParameters.containsKey(parameter)) + { + parameters.put(parameter, givenParameters.get(parameter)); + continue; + } + + switch (parameter) + { + case BYTES_PER_ASSIGNMENT: + parameters.put(parameter, bytesPerAssignment.toString()); + continue; + case PARTITIONS_PER_ASSIGNMENT: + parameters.put(parameter, Long.toString(partitionsPerAssignment)); + continue; + case MAX_TABLES_PER_ASSIGNMENT: + parameters.put(parameter, Integer.toString(maxTablesPerAssignment)); + continue; + case MAX_BYTES_PER_SCHEDULE: + parameters.put(parameter, maxBytesPerSchedule.toString()); + continue; + default: + // not expected + parameters.put(parameter, ""); + } + } + return Collections.unmodifiableMap(parameters); + } + + /** + * Implementation of RepairAssignment that also assigns an estimation of bytes involved + * in the repair. + */ + @VisibleForTesting + protected static class SizedRepairAssignment extends RepairAssignment + { + + final String description; + + public SizedRepairAssignment(Range tokenRange, String keyspaceName, List tableNames) + { + this(tokenRange, keyspaceName, tableNames, "", 0L); + } + + public SizedRepairAssignment(Range tokenRange, String keyspaceName, List tableNames, + String description, + long estimatedBytes) + { + super(tokenRange, keyspaceName, tableNames, estimatedBytes); + this.description = description; + } + + /** + * @return Additional metadata about the repair assignment. + */ + public String getDescription() { + return description; + } + + /** + * Estimated bytes involved in the assignment. Typically Derived from {@link AutoRepairUtils.SizeEstimate#sizeForRepair}. + * + * @return estimated bytes involved in the assignment. + */ + public long getEstimatedBytes() + { + return estimatedBytes; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + SizedRepairAssignment that = (SizedRepairAssignment) o; + return estimatedBytes == that.estimatedBytes && Objects.equals(description, that.description); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), description, estimatedBytes); + } + + @Override + public String toString() + { + return "SizedRepairAssignment{" + + "description='" + description + '\'' + + ", tokenRange=" + tokenRange + + ", keyspaceName='" + keyspaceName + '\'' + + ", tableNames=" + tableNames + + ", estimatedBytes=" + FileUtils.stringifyFileSize(estimatedBytes) + + '}'; + } + } + + /** + * Conveinence builder for establishing defaults by repair type. + */ + protected static class RepairTypeDefaults + { + final AutoRepairConfig.RepairType repairType; + final DataStorageSpec.LongBytesBound bytesPerAssignment; + final long partitionsPerAssignment; + final int maxTablesPerAssignment; + final DataStorageSpec.LongBytesBound maxBytesPerSchedule; + + public RepairTypeDefaults(AutoRepairConfig.RepairType repairType, + DataStorageSpec.LongBytesBound bytesPerAssignment, + long partitionsPerAssignment, + int maxTablesPerAssignment, + DataStorageSpec.LongBytesBound maxBytesPerSchedule) + { + this.repairType = repairType; + this.bytesPerAssignment = bytesPerAssignment; + this.partitionsPerAssignment = partitionsPerAssignment; + this.maxTablesPerAssignment = maxTablesPerAssignment; + this.maxBytesPerSchedule = maxBytesPerSchedule; + } + + static RepairTypeDefaultsBuilder builder(AutoRepairConfig.RepairType repairType) + { + return new RepairTypeDefaultsBuilder(repairType); + } + + static class RepairTypeDefaultsBuilder + { + private final AutoRepairConfig.RepairType repairType; + private DataStorageSpec.LongBytesBound bytesPerAssignment = new DataStorageSpec.LongBytesBound("50GiB"); + // Aims to target at most 1 partitions per leaf assuming a merkle tree of depth 20 (2^20 = 1,048,576) + private long partitionsPerAssignment = 1_048_576; + private int maxTablesPerAssignment = 64; + private DataStorageSpec.LongBytesBound maxBytesPerSchedule = MAX_BYTES; + + private RepairTypeDefaultsBuilder(AutoRepairConfig.RepairType repairType) + { + this.repairType = repairType; + } + + @SuppressWarnings("unused") + public RepairTypeDefaultsBuilder withBytesPerAssignment(DataStorageSpec.LongBytesBound bytesPerAssignment) + { + this.bytesPerAssignment = bytesPerAssignment; + return this; + } + + @SuppressWarnings("unused") + public RepairTypeDefaultsBuilder withPartitionsPerAssignment(long partitionsPerAssignment) + { + this.partitionsPerAssignment = partitionsPerAssignment; + return this; + } + + @SuppressWarnings("unused") + public RepairTypeDefaultsBuilder withMaxTablesPerAssignment(int maxTablesPerAssignment) + { + this.maxTablesPerAssignment = maxTablesPerAssignment; + return this; + } + + public RepairTypeDefaultsBuilder withMaxBytesPerSchedule(DataStorageSpec.LongBytesBound maxBytesPerSchedule) + { + this.maxBytesPerSchedule = maxBytesPerSchedule; + return this; + } + + public RepairTokenRangeSplitter.RepairTypeDefaults build() + { + return new RepairTypeDefaults(repairType, bytesPerAssignment, partitionsPerAssignment, maxTablesPerAssignment, maxBytesPerSchedule); + } + } + } +} diff --git a/src/java/org/apache/cassandra/repair/consistent/LocalSessions.java b/src/java/org/apache/cassandra/repair/consistent/LocalSessions.java index e2bfe05eb2ac..b9f96ca4c409 100644 --- a/src/java/org/apache/cassandra/repair/consistent/LocalSessions.java +++ b/src/java/org/apache/cassandra/repair/consistent/LocalSessions.java @@ -35,13 +35,14 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import com.google.common.base.Verify; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -51,7 +52,16 @@ import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; -import org.apache.cassandra.db.compaction.CompactionInterruptedException; +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.db.compaction.AbstractCompactionTask; +import org.apache.cassandra.db.compaction.CleanupTask; +import org.apache.cassandra.db.compaction.CompactionRealm; +import org.apache.cassandra.db.compaction.CompactionSSTable; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.RepairFinishedCompactionTask; +import org.apache.cassandra.db.compaction.TableOperation; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.locator.RangesAtEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,6 +81,7 @@ import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.compaction.CompactionInterruptedException; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.UUIDType; import org.apache.cassandra.dht.IPartitioner; @@ -79,6 +90,8 @@ import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.net.Message; +import org.apache.cassandra.repair.NoSuchRepairSessionException; +import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.repair.messages.FailSession; import org.apache.cassandra.repair.messages.FinalizeCommit; import org.apache.cassandra.repair.messages.FinalizePromise; @@ -88,15 +101,14 @@ import org.apache.cassandra.repair.messages.RepairMessage; import org.apache.cassandra.repair.messages.StatusRequest; import org.apache.cassandra.repair.messages.StatusResponse; -import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; -import org.apache.cassandra.repair.NoSuchRepairSessionException; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.Pair; import static org.apache.cassandra.config.CassandraRelevantProperties.REPAIR_CLEANUP_INTERVAL_SECONDS; import static org.apache.cassandra.config.CassandraRelevantProperties.REPAIR_DELETE_TIMEOUT_SECONDS; @@ -338,6 +350,12 @@ public PendingStats getPendingStats(TableId tid, Collection> ranges return new PendingStats(cfs.getKeyspaceName(), cfs.name, pending.build(), finalized.build(), failed.build()); } + /** + * promotes (or demotes) data attached to an incremental repair session that has either completed successfully, + * or failed + * + * @return session ids whose data could not be released + */ public CleanupSummary cleanup(TableId tid, Collection> ranges, boolean force) { Iterable candidates = Iterables.filter(sessions.values(), @@ -346,10 +364,86 @@ public CleanupSummary cleanup(TableId tid, Collection> ranges, bool && Range.intersects(ls.ranges, ranges)); ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(tid); + Preconditions.checkNotNull(cfs); + Set sessionIds = Sets.newHashSet(Iterables.transform(candidates, s -> s.sessionID)); + return releaseRepairData(cfs, sessionIds, force); + } + private CleanupSummary releaseRepairData(ColumnFamilyStore cfs, Collection sessions, boolean force) + { + if (force) + { + Predicate predicate = sst -> { + TimeUUID session = sst.getPendingRepair(); + return session != null && sessions.contains(session); + }; + return cfs.runWithCompactionsDisabled(() -> doReleaseRepairData(cfs, sessions), + predicate, OperationType.STREAM, false, true, true, TableOperation.StopTrigger.CLEANUP); + } + else + { + return doReleaseRepairData(cfs, sessions); + } + } + + private CleanupSummary doReleaseRepairData(ColumnFamilyStore cfs, Collection sessions) + { + List> tasks = new ArrayList<>(sessions.size()); + for (TimeUUID session : sessions) + { + if (canCleanup(session)) + tasks.add(Pair.create(session, getRepairFinishedCompactionTask(cfs, session))); + } - return cfs.releaseRepairData(sessionIds, force); + return new CleanupTask(cfs, tasks).cleanup(); + } + + private RepairFinishedCompactionTask getRepairFinishedCompactionTask(ColumnFamilyStore cfs, TimeUUID session) + { + Set sstables = cfs.getPendingRepairSSTables(session); + if (sstables.isEmpty()) + return null; + + return getRepairFinishedCompactionTask(cfs, session, sstables); + } + + private RepairFinishedCompactionTask getRepairFinishedCompactionTask(CompactionRealm realm, + TimeUUID session, + Collection sstables) + { + long repairedAt = getFinalSessionRepairedAt(session); + boolean isTransient = sstables.iterator().next().isTransient(); + LifecycleTransaction txn = realm.tryModify(sstables, OperationType.COMPACTION); + return txn == null ? null : new RepairFinishedCompactionTask(realm, txn, session, repairedAt, isTransient); + } + + /** + * Some finalized repairs leave sstables behind that need cleaning. This generates the tasks to clean them up. + */ + public Collection getZombieRepairFinalizationTasks(CompactionRealm realm, Collection sstables) + { + Map> finalizations = new HashMap<>(); + for (CompactionSSTable sstable : sstables) + { + TableMetadata tableMetadata = Schema.instance.getTableMetadata(sstable.getKeyspaceName(), sstable.getColumnFamilyName()); + if (tableMetadata != null && sstable.isPendingRepair() && canCleanup(sstable.getPendingRepair())) + { + logger.debug("Going to cleanup sstable {} for already finalized repair {}", sstable.getPendingRepair(), tableMetadata.toDebugString()); + finalizations.computeIfAbsent(sstable.getPendingRepair(), pr -> new ArrayList<>()).add(sstable); + } + } + + return finalizations.entrySet() + .stream() + .map(entry -> getRepairFinishedCompactionTask(realm, entry.getKey(), entry.getValue())) + .filter(Predicates.notNull()) + .collect(Collectors.toList()); + } + + public boolean canCleanup(TimeUUID sessionID) + { + return !isSessionInProgress(sessionID); } /** @@ -383,7 +477,7 @@ public synchronized void start() int loadedSessionsCount = 0; Preconditions.checkArgument(!started, "LocalSessions.start can only be called once"); Preconditions.checkArgument(sessions.isEmpty(), "No sessions should be added before start"); - UntypedResultSet rows = QueryProcessor.executeInternalWithPaging(String.format("SELECT * FROM %s.%s", keyspace, table), 1000); + UntypedResultSet rows = QueryProcessor.executeInternalWithPaging(String.format("SELECT * FROM %s.%s", keyspace, table), PageSize.inRows(1000)); Map loadedSessions = new HashMap<>(); Map> initialLevels = new HashMap<>(); for (UntypedResultSet.Row row : rows) @@ -505,6 +599,8 @@ else if (!sessionHasData(session)) } else { + // If this happens too often or for a long time check sstables pending repair are not being + // left behind logger.warn("Skipping delete of LocalSession {} because it still contains sstables", session.sessionID); } } @@ -745,8 +841,8 @@ private boolean maybeSetStateAndSave(LocalSession session, @Nullable ConsistentS synchronized (session) { Preconditions.checkArgument(session.getState().canTransitionTo(state), - "Invalid state transition %s -> %s", - session.getState(), state); + "Invalid state transition %s -> %s for session %s", + session.getState(), state, session.sessionID); if (expected != null && session.getState() != expected) return false; logger.trace("Changing LocalSession state from {} -> {} for {}", session.getState(), state, session.sessionID); @@ -1134,7 +1230,7 @@ protected boolean sessionHasData(LocalSession session) { Predicate predicate = tid -> { ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(tid); - return cfs != null && cfs.getCompactionStrategyManager().hasDataForPendingRepair(session.sessionID); + return cfs != null && cfs.hasPendingRepairSSTables(session.sessionID); }; return Iterables.any(session.tableIds, predicate::test); diff --git a/src/java/org/apache/cassandra/repair/consistent/SyncStatSummary.java b/src/java/org/apache/cassandra/repair/consistent/SyncStatSummary.java index 855ad4bad344..820c6b011ba6 100644 --- a/src/java/org/apache/cassandra/repair/consistent/SyncStatSummary.java +++ b/src/java/org/apache/cassandra/repair/consistent/SyncStatSummary.java @@ -81,7 +81,7 @@ public String toString() } } - private static class Table + public static class Table { final String keyspace; @@ -94,7 +94,7 @@ private static class Table final Map, Session> sessions = new HashMap<>(); - Table(String keyspace, String table) + public Table(String keyspace, String table) { this.keyspace = keyspace; this.table = table; @@ -138,7 +138,7 @@ void calculateTotals() totalsCalculated = true; } - boolean isCounter() + public boolean isCounter() { TableMetadata tmd = Schema.instance.getTableMetadata(keyspace, table); return tmd != null && tmd.isCounter(); @@ -174,6 +174,16 @@ public String toString() } return output.toString(); } + + public long getBytes() + { + return this.bytes; + } + + public long getRanges() + { + return this.ranges.size(); + } } private final Map, Table> summaries = new HashMap<>(); @@ -233,6 +243,12 @@ private void calculateTotals() totalsCalculated = true; } + public Map, Table> getTotals() + { + calculateTotals(); + return summaries; + } + public String toString() { List> tables = Lists.newArrayList(summaries.keySet()); diff --git a/src/java/org/apache/cassandra/repair/consistent/admin/CleanupSummary.java b/src/java/org/apache/cassandra/repair/consistent/admin/CleanupSummary.java index f715cc98f5b6..52fdaa2a3c9f 100644 --- a/src/java/org/apache/cassandra/repair/consistent/admin/CleanupSummary.java +++ b/src/java/org/apache/cassandra/repair/consistent/admin/CleanupSummary.java @@ -22,7 +22,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; - import javax.management.openmbean.ArrayType; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; @@ -34,7 +33,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Sets; -import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.compaction.CompactionRealm; import org.apache.cassandra.utils.TimeUUID; public class CleanupSummary @@ -74,9 +73,9 @@ public CleanupSummary(String keyspace, String table, Set successful, S this.unsuccessful = unsuccessful; } - public CleanupSummary(ColumnFamilyStore cfs, Set successful, Set unsuccessful) + public CleanupSummary(CompactionRealm cfs, Set successful, Set unsuccessful) { - this(cfs.getKeyspaceName(), cfs.name, successful, unsuccessful); + this(cfs.getKeyspaceName(), cfs.getTableName(), successful, unsuccessful); } public static CleanupSummary add(CleanupSummary l, CleanupSummary r) diff --git a/src/java/org/apache/cassandra/repair/messages/PrepareMessage.java b/src/java/org/apache/cassandra/repair/messages/PrepareMessage.java index a2cd7e390338..9e516e4511ba 100644 --- a/src/java/org/apache/cassandra/repair/messages/PrepareMessage.java +++ b/src/java/org/apache/cassandra/repair/messages/PrepareMessage.java @@ -25,6 +25,8 @@ import com.google.common.base.Preconditions; +import org.apache.cassandra.config.CassandraRelevantProperties; + import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; @@ -40,6 +42,8 @@ public class PrepareMessage extends RepairMessage { + private static final boolean ALLOW_MIXED_REPAIR = CassandraRelevantProperties.ALLOW_MIXED_REPAIR.getBoolean(); + public final List tableIds; public final Collection> ranges; @@ -91,13 +95,15 @@ public int hashCode() private static final String MIXED_MODE_ERROR = "Some nodes involved in repair are on an incompatible major version. " + "Repair is not supported in mixed major version clusters. Note that " + "5.x nodes running in storage compatibility mode = 4 are considered " + - "4.x nodes."; + "4.x nodes. " + + "To allow repair during rolling upgrades, set -Dcassandra.allow_mixed_repair=true"; public static final IVersionedSerializer serializer = new IVersionedSerializer() { public void serialize(PrepareMessage message, DataOutputPlus out, int version) throws IOException { - Preconditions.checkArgument(version == MessagingService.current_version, MIXED_MODE_ERROR); + if (!ALLOW_MIXED_REPAIR) + Preconditions.checkArgument(version == MessagingService.current_version, MIXED_MODE_ERROR); out.writeInt(message.tableIds.size()); for (TableId tableId : message.tableIds) @@ -117,7 +123,8 @@ public void serialize(PrepareMessage message, DataOutputPlus out, int version) t public PrepareMessage deserialize(DataInputPlus in, int version) throws IOException { - Preconditions.checkArgument(version == MessagingService.current_version, MIXED_MODE_ERROR); + if (!ALLOW_MIXED_REPAIR) + Preconditions.checkArgument(version == MessagingService.current_version, MIXED_MODE_ERROR); int tableIdCount = in.readInt(); List tableIds = new ArrayList<>(tableIdCount); diff --git a/src/java/org/apache/cassandra/repair/messages/RepairMessage.java b/src/java/org/apache/cassandra/repair/messages/RepairMessage.java index f0cbf78f38ce..581c61a9d65f 100644 --- a/src/java/org/apache/cassandra/repair/messages/RepairMessage.java +++ b/src/java/org/apache/cassandra/repair/messages/RepairMessage.java @@ -18,16 +18,17 @@ package org.apache.cassandra.repair.messages; import java.util.Collections; -import java.util.EnumMap; -import java.util.EnumSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.*; import java.util.function.Supplier; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import org.apache.cassandra.nodes.INodeInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +43,7 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.repair.RepairJobDesc; import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.utils.Backoff; @@ -50,6 +52,7 @@ import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Future; +import static org.apache.cassandra.config.CassandraRelevantProperties.REPAIR_ALWAYS_CONSIDER_TIMEOUTS_SUPPORTED; import static org.apache.cassandra.net.MessageFlag.CALL_BACK_ON_FAILURE; /** @@ -59,12 +62,13 @@ */ public abstract class RepairMessage { - private enum ErrorHandling { NONE, TIMEOUT, RETRY } + @VisibleForTesting + enum ErrorHandling { NONE, TIMEOUT, RETRY } @VisibleForTesting static final CassandraVersion SUPPORTS_RETRY = new CassandraVersion("5.0.0-alpha2.SNAPSHOT"); private static final Map VERB_TIMEOUT_VERSIONS; public static final Set ALLOWS_RETRY; - private static final Set SUPPORTS_RETRY_WITHOUT_VERSION_CHECK = Collections.unmodifiableSet(EnumSet.of(Verb.CLEANUP_MSG)); + private static final Set SUPPORTS_RETRY_WITHOUT_VERSION_CHECK = Collections.unmodifiableSet(ImmutableSet.of(Verb.CLEANUP_MSG)); public static final RequestCallback NOOP_CALLBACK = new RequestCallback<>() { @Override @@ -81,7 +85,7 @@ public void onFailure(InetAddressAndPort from, RequestFailureReason failureReaso static { CassandraVersion timeoutVersion = new CassandraVersion("4.0.7-SNAPSHOT"); - EnumMap map = new EnumMap<>(Verb.class); + HashMap map = new HashMap<>(); map.put(Verb.VALIDATION_REQ, timeoutVersion); map.put(Verb.SYNC_REQ, timeoutVersion); map.put(Verb.VALIDATION_RSP, SUPPORTS_RETRY); @@ -95,7 +99,7 @@ public void onFailure(InetAddressAndPort from, RequestFailureReason failureReaso map.put(Verb.FAILED_SESSION_MSG, SUPPORTS_RETRY); VERB_TIMEOUT_VERSIONS = Collections.unmodifiableMap(map); - EnumSet allowsRetry = EnumSet.noneOf(Verb.class); + Set allowsRetry = new HashSet<>(); allowsRetry.add(Verb.PREPARE_MSG); allowsRetry.add(Verb.VALIDATION_REQ); allowsRetry.add(Verb.VALIDATION_RSP); @@ -275,15 +279,24 @@ public boolean invokeOnFailure() sendMessageWithRetries(ctx, allowRetry, request, verb, endpoint, callback); } - private static ErrorHandling errorHandlingSupported(SharedContext ctx, InetAddressAndPort from, Verb verb, TimeUUID parentSessionId) + @VisibleForTesting + static ErrorHandling errorHandlingSupported(SharedContext ctx, InetAddressAndPort from, Verb verb, TimeUUID parentSessionId) { if (SUPPORTS_RETRY_WITHOUT_VERSION_CHECK.contains(verb)) return ErrorHandling.RETRY; // Repair in mixed mode isn't fully supported, but also not activally blocked... so in the common case all participants // will be on the same version as this instance, so can avoid the lookup from gossip - CassandraVersion remoteVersion = ctx.gossiper().getReleaseVersion(from); + CassandraVersion remoteVersion = Nodes.localOrPeerInfoOpt(from).map(INodeInfo::getReleaseVersion).orElse(null); if (remoteVersion == null) { + /* + * In CNDB, repair services won't be added to the Nodes.peers() map, so there's no clear way + * to check the version of the remote peer. This is the reason why a system property is introduced + * to skip the version check, in case it's known that the deployed C* version supports repair message + * timeouts. + */ + if (areTimeoutsAlwaysSupported()) + return ErrorHandling.RETRY; if (VERB_TIMEOUT_VERSIONS.containsKey(verb)) { logger.warn("[#{}] Not failing repair due to remote host {} not supporting repair message timeouts (version is unknown)", parentSessionId, from); @@ -294,7 +307,7 @@ private static ErrorHandling errorHandlingSupported(SharedContext ctx, InetAddre if (remoteVersion.compareTo(SUPPORTS_RETRY) >= 0) return ErrorHandling.RETRY; CassandraVersion timeoutVersion = VERB_TIMEOUT_VERSIONS.get(verb); - if (timeoutVersion == null || remoteVersion.compareTo(timeoutVersion) >= 0) + if (timeoutVersion == null || remoteVersion.compareTo(timeoutVersion, true) >= 0) return ErrorHandling.TIMEOUT; return ErrorHandling.NONE; } @@ -309,4 +322,9 @@ public static void sendAck(SharedContext ctx, Message m { ctx.messaging().send(message.emptyResponse(), message.from()); } + + private static boolean areTimeoutsAlwaysSupported() + { + return REPAIR_ALWAYS_CONSIDER_TIMEOUTS_SUPPORTED.getBoolean(); + } } diff --git a/src/java/org/apache/cassandra/repair/messages/RepairOption.java b/src/java/org/apache/cassandra/repair/messages/RepairOption.java index f0508a3e4226..471fb4aa3221 100644 --- a/src/java/org/apache/cassandra/repair/messages/RepairOption.java +++ b/src/java/org/apache/cassandra/repair/messages/RepairOption.java @@ -46,6 +46,7 @@ public class RepairOption public static final String HOSTS_KEY = "hosts"; public static final String TRACE_KEY = "trace"; public static final String SUB_RANGE_REPAIR_KEY = "sub_range_repair"; + public static final String PUSH_REPAIR_KEY = "pushRepair"; public static final String PULL_REPAIR_KEY = "pullRepair"; public static final String FORCE_REPAIR_KEY = "forceRepair"; public static final String PREVIEW = "previewKind"; @@ -53,6 +54,7 @@ public class RepairOption public static final String IGNORE_UNREPLICATED_KS = "ignoreUnreplicatedKeyspaces"; public static final String REPAIR_PAXOS_KEY = "repairPaxos"; public static final String PAXOS_ONLY_KEY = "paxosOnly"; + public static final String OFFLINE_SERVICE = "offlineService"; // we don't want to push nodes too much for repair public static final int MAX_JOB_THREADS = 4; @@ -148,6 +150,11 @@ public static Set> parseRanges(String rangesStr, IPartitioner parti *

    * * + * + * + * + * + * * * @@ -164,6 +171,12 @@ public static Set> parseRanges(String rangesStr, IPartitioner parti * ranges to the same host multiple times * * + * + * + * + * + * * *
    pushRepair"true" if the repair should only stream data one way from local host to remote host.false
    pullRepair"true" if the repair should only stream data one way from a remote host to this host. * This is only allowed if exactly 2 hosts are specified along with a token range that they share.false
    offlineService"true" if current repair task is executed by an offline service which has no token metadata and + * it's not part of the ring. Repair should use tokens and hosts directly from repair options.false
    * @@ -180,6 +193,7 @@ public static RepairOption parse(Map options, IPartitioner parti PreviewKind previewKind = PreviewKind.valueOf(options.getOrDefault(PREVIEW, PreviewKind.NONE.toString())); boolean trace = Boolean.parseBoolean(options.get(TRACE_KEY)); boolean force = Boolean.parseBoolean(options.get(FORCE_REPAIR_KEY)); + boolean pushRepair = Boolean.parseBoolean(options.get(PUSH_REPAIR_KEY)); boolean pullRepair = Boolean.parseBoolean(options.get(PULL_REPAIR_KEY)); boolean ignoreUnreplicatedKeyspaces = Boolean.parseBoolean(options.get(IGNORE_UNREPLICATED_KS)); boolean repairPaxos = Boolean.parseBoolean(options.get(REPAIR_PAXOS_KEY)); @@ -190,6 +204,9 @@ public static RepairOption parse(Map options, IPartitioner parti Preconditions.checkArgument(!repairPaxos, "repairPaxos must be set to false for preview repairs"); Preconditions.checkArgument(!paxosOnly, "paxosOnly must be set to false for preview repairs"); } + boolean offlineService = Boolean.parseBoolean(options.get(OFFLINE_SERVICE)); + + Preconditions.checkArgument(!pullRepair || !pushRepair, "Cannot use pushRepair and pullRepair as the same time"); int jobThreads = 1; if (options.containsKey(JOB_THREADS_KEY)) @@ -206,7 +223,9 @@ public static RepairOption parse(Map options, IPartitioner parti boolean asymmetricSyncing = Boolean.parseBoolean(options.get(OPTIMISE_STREAMS_KEY)); - RepairOption option = new RepairOption(parallelism, primaryRange, incremental, trace, jobThreads, ranges, !ranges.isEmpty(), pullRepair, force, previewKind, asymmetricSyncing, ignoreUnreplicatedKeyspaces, repairPaxos, paxosOnly); + RepairOption option = new RepairOption(parallelism, primaryRange, incremental, trace, jobThreads, ranges, + !ranges.isEmpty(), pushRepair, pullRepair, force, previewKind, asymmetricSyncing, + ignoreUnreplicatedKeyspaces, repairPaxos, paxosOnly, offlineService); // data centers String dataCentersStr = options.get(DATACENTERS_KEY); @@ -281,6 +300,7 @@ else if (ranges.isEmpty()) private final boolean trace; private final int jobThreads; private final boolean isSubrangeRepair; + private final boolean pushRepair; private final boolean pullRepair; private final boolean forceRepair; private final PreviewKind previewKind; @@ -288,13 +308,17 @@ else if (ranges.isEmpty()) private final boolean ignoreUnreplicatedKeyspaces; private final boolean repairPaxos; private final boolean paxosOnly; + private final boolean offlineService; private final Collection columnFamilies = new HashSet<>(); private final Collection dataCenters = new HashSet<>(); private final Collection hosts = new HashSet<>(); private final Collection> ranges = new HashSet<>(); - public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean incremental, boolean trace, int jobThreads, Collection> ranges, boolean isSubrangeRepair, boolean pullRepair, boolean forceRepair, PreviewKind previewKind, boolean optimiseStreams, boolean ignoreUnreplicatedKeyspaces, boolean repairPaxos, boolean paxosOnly) + public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean incremental, boolean trace, + int jobThreads, Collection> ranges, boolean isSubrangeRepair, boolean pushRepair, + boolean pullRepair, boolean forceRepair, PreviewKind previewKind, boolean optimiseStreams, + boolean ignoreUnreplicatedKeyspaces, boolean repairPaxos, boolean paxosOnly, boolean offlineService) { this.parallelism = parallelism; @@ -303,6 +327,7 @@ public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean this.trace = trace; this.jobThreads = jobThreads; this.ranges.addAll(ranges); + this.pushRepair = pushRepair; this.isSubrangeRepair = isSubrangeRepair; this.pullRepair = pullRepair; this.forceRepair = forceRepair; @@ -311,6 +336,7 @@ public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean this.ignoreUnreplicatedKeyspaces = ignoreUnreplicatedKeyspaces; this.repairPaxos = repairPaxos; this.paxosOnly = paxosOnly; + this.offlineService = offlineService; } public RepairParallelism getParallelism() @@ -333,6 +359,16 @@ public boolean isTraced() return trace; } + public boolean isOfflineService() + { + return offlineService; + } + + public boolean isPushRepair() + { + return pushRepair; + } + public boolean isPullRepair() { return pullRepair; @@ -439,12 +475,14 @@ public String toString() ", hosts: " + hosts + ", previewKind: " + previewKind + ", # of ranges: " + ranges.size() + + ", push repair: " + pushRepair + ", pull repair: " + pullRepair + ", force repair: " + forceRepair + ", optimise streams: "+ optimiseStreams() + ", ignore unreplicated keyspaces: "+ ignoreUnreplicatedKeyspaces + ", repairPaxos: " + repairPaxos + ", paxosOnly: " + paxosOnly + + ", offline service: " + offlineService + ')'; } @@ -461,12 +499,14 @@ public Map asMap() options.put(SUB_RANGE_REPAIR_KEY, Boolean.toString(isSubrangeRepair)); options.put(TRACE_KEY, Boolean.toString(trace)); options.put(RANGES_KEY, Joiner.on(",").join(ranges)); + options.put(PUSH_REPAIR_KEY, Boolean.toString(pushRepair)); options.put(PULL_REPAIR_KEY, Boolean.toString(pullRepair)); options.put(FORCE_REPAIR_KEY, Boolean.toString(forceRepair)); options.put(PREVIEW, previewKind.toString()); options.put(OPTIMISE_STREAMS_KEY, Boolean.toString(optimiseStreams)); options.put(REPAIR_PAXOS_KEY, Boolean.toString(repairPaxos)); options.put(PAXOS_ONLY_KEY, Boolean.toString(paxosOnly)); + options.put(OFFLINE_SERVICE, Boolean.toString(offlineService)); return options; } } diff --git a/src/java/org/apache/cassandra/repair/messages/ValidationRequest.java b/src/java/org/apache/cassandra/repair/messages/ValidationRequest.java index 1e651a96d2d7..70b505223bbf 100644 --- a/src/java/org/apache/cassandra/repair/messages/ValidationRequest.java +++ b/src/java/org/apache/cassandra/repair/messages/ValidationRequest.java @@ -71,13 +71,13 @@ public int hashCode() public void serialize(ValidationRequest message, DataOutputPlus out, int version) throws IOException { RepairJobDesc.serializer.serialize(message.desc, out, version); - out.writeInt(version >= MessagingService.VERSION_50 ? CassandraUInt.fromLong(message.nowInSec) : (int) message.nowInSec); + out.writeInt(MessagingService.Version.supportsExtendedDeletionTime(version) ? CassandraUInt.fromLong(message.nowInSec) : (int) message.nowInSec); } public ValidationRequest deserialize(DataInputPlus dis, int version) throws IOException { RepairJobDesc desc = RepairJobDesc.serializer.deserialize(dis, version); - long nowInsec = version >= MessagingService.VERSION_50 ? CassandraUInt.toLong(dis.readInt()) : dis.readInt(); + long nowInsec = MessagingService.Version.supportsExtendedDeletionTime(version) ? CassandraUInt.toLong(dis.readInt()) : dis.readInt(); return new ValidationRequest(desc, nowInsec); } diff --git a/src/java/org/apache/cassandra/schema/AutoRepairParams.java b/src/java/org/apache/cassandra/schema/AutoRepairParams.java new file mode 100644 index 000000000000..5f05edab9dd8 --- /dev/null +++ b/src/java/org/apache/cassandra/schema/AutoRepairParams.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.schema; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.StringUtils; + +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig; + +import static java.lang.String.format; + +/** + * AutoRepair table parameters - used to define the auto-repair configuration for a table. + */ +public final class AutoRepairParams +{ + public enum Option + { + FULL_ENABLED, + INCREMENTAL_ENABLED, + PREVIEW_REPAIRED_ENABLED, + PRIORITY; + + @Override + public String toString() + { + return name().toLowerCase(); + } + } + + private final ImmutableMap options; + + public static final Map DEFAULT_OPTIONS = ImmutableMap.of( + Option.FULL_ENABLED.name().toLowerCase(), Boolean.toString(true), + Option.INCREMENTAL_ENABLED.name().toLowerCase(), Boolean.toString(true), + Option.PREVIEW_REPAIRED_ENABLED.name().toLowerCase(), Boolean.toString(true), + Option.PRIORITY.toString(), "0" + ); + + AutoRepairParams(Map options) + { + this.options = ImmutableMap.copyOf(options); + } + + public static final AutoRepairParams DEFAULT = + new AutoRepairParams(DEFAULT_OPTIONS); + + public static AutoRepairParams create(Map options) + { + Map optionsMap = new TreeMap<>(DEFAULT_OPTIONS); + if (options != null) + { + for (Map.Entry entry : options.entrySet()) + { + if (Arrays.stream(Option.values()).noneMatch(option -> option.toString().equalsIgnoreCase(entry.getKey()))) + { + throw new ConfigurationException(format("Unknown property '%s'", entry.getKey())); + } + optionsMap.put(entry.getKey(), entry.getValue()); + } + } + return new AutoRepairParams(optionsMap); + } + + public boolean repairEnabled(AutoRepairConfig.RepairType type) + { + String option = type.toString().toLowerCase() + "_enabled"; + String enabled = options.getOrDefault(option, DEFAULT_OPTIONS.get(option)); + return Boolean.parseBoolean(enabled); + } + + public int priority() + { + String priority = options.getOrDefault(Option.PRIORITY.toString(), DEFAULT_OPTIONS.get(Option.PRIORITY.toString())); + return Integer.parseInt(priority); + } + + public void validate() + { + for (Option option : Option.values()) + { + if (!options.containsKey(option.toString().toLowerCase())) + { + throw new ConfigurationException(format("Missing repair sub-option '%s'", option)); + } + } + if (options.get(Option.FULL_ENABLED.toString().toLowerCase()) != null && !isValidBoolean(options.get(Option.FULL_ENABLED.toString().toLowerCase()))) + { + throw new ConfigurationException(format("Invalid value %s for '%s' repair sub-option - must be a boolean", + options.get(Option.FULL_ENABLED.toString().toLowerCase()), + Option.FULL_ENABLED)); + } + if (options.get(Option.INCREMENTAL_ENABLED.toString().toLowerCase()) != null && !isValidBoolean(options.get(Option.INCREMENTAL_ENABLED.toString().toLowerCase()))) + { + throw new ConfigurationException(format("Invalid value %s for '%s' repair sub-option - must be a boolean", + options.get(Option.INCREMENTAL_ENABLED.toString().toLowerCase()), + Option.INCREMENTAL_ENABLED)); + } + if (options.get(Option.PREVIEW_REPAIRED_ENABLED.toString().toLowerCase()) != null && !isValidBoolean(options.get(Option.PREVIEW_REPAIRED_ENABLED.toString().toLowerCase()))) + { + throw new ConfigurationException(format("Invalid value %s for '%s' repair sub-option - must be a boolean", + options.get(Option.PREVIEW_REPAIRED_ENABLED.toString().toLowerCase()), + Option.PREVIEW_REPAIRED_ENABLED)); + } + if (options.get(Option.PRIORITY.toString().toLowerCase()) != null && !isValidInt(options.get(Option.PRIORITY.toString().toLowerCase()))) + { + throw new ConfigurationException(format("Invalid value %s for '%s' repair sub-option - must be an integer", + options.get(Option.PRIORITY.toString().toLowerCase()), + Option.PRIORITY)); + } + } + + public static boolean isValidBoolean(String value) + { + return StringUtils.equalsIgnoreCase(value, "true") || StringUtils.equalsIgnoreCase(value, "false"); + } + + public static boolean isValidInt(String value) + { + return StringUtils.isNumeric(value); + } + + public Map options() + { + return options; + } + + public static AutoRepairParams fromMap(Map map) + { + return create(map); + } + + public Map asMap() + { + return options; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("options", options) + .toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof AutoRepairParams)) + return false; + + AutoRepairParams cp = (AutoRepairParams) o; + + return options.equals(cp.options); + } + + @Override + public int hashCode() + { + return Objects.hash(options); + } +} diff --git a/src/java/org/apache/cassandra/schema/CQLTypeParser.java b/src/java/org/apache/cassandra/schema/CQLTypeParser.java index c79de881550e..392872c87c2a 100644 --- a/src/java/org/apache/cassandra/schema/CQLTypeParser.java +++ b/src/java/org/apache/cassandra/schema/CQLTypeParser.java @@ -19,7 +19,9 @@ import com.google.common.collect.ImmutableSet; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.CQLFragmentParser; +import org.apache.cassandra.cql3.CqlParser; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; @@ -50,7 +52,36 @@ public static AbstractType parse(String keyspace, String unparsed, Types user if (udt != null) return udt; - return parseRaw(unparsed).prepareInternal(keyspace, userTypes).getType(); + return parseRaw(unparsed).prepare(keyspace, userTypes).getType(); + } + + /** + * Parse the type for a dropped column in the schema. + *

    + * The reason we need a specific method for this is that when we record dropped column types, we "expand" user + * types into tuples ({@link AbstractType#expandUserTypes()}) and this in order to save us from having to preserve + * dropped user types definitions. But a consequence of that expansion is that we have to have some support for + * non-frozen tuples, since the dropped type could be a non-frozen UDT, and that differs from normal CQL where + * tuples are frozen by default and {@code tuple} is indistinguishable from {@code frozen>}. So, + * to handle this, we rely on the fact that types for dropped columns will have been recorded using + * {@link CQL3Type#toSchemaString()}, which explicitly handles the frozen/non-frozen difference for tuples, which + * this method makes use of. + *

    + * Concretely, while {@link #parse(String, String, Types)} will return a frozen type for {@code tuple<...>} + * (since again, tuple are frozen by default in CQL), this method will return a non-frozen type. + */ + public static AbstractType parseDroppedType(String keyspace, String unparsed) + { + + // fast path for the common case of a primitive type + if (PRIMITIVE_TYPES.contains(unparsed.toLowerCase())) + return CQL3Type.Native.valueOf(unparsed.toUpperCase()).getType(); + + // We can't have UDT in dropped types... + CQL3Type.Raw rawType = CQLFragmentParser.parseAny(CqlParser::comparatorTypeWithMultiCellTuple, + unparsed, + "CQL dropped type"); + return rawType.prepare(keyspace, Types.none()).getType(); } static CQL3Type.Raw parseRaw(String type) diff --git a/src/java/org/apache/cassandra/schema/ColumnMetadata.java b/src/java/org/apache/cassandra/schema/ColumnMetadata.java index f68a7b5ff364..708aeb32c7d3 100644 --- a/src/java/org/apache/cassandra/schema/ColumnMetadata.java +++ b/src/java/org/apache/cassandra/schema/ColumnMetadata.java @@ -18,31 +18,52 @@ package org.apache.cassandra.schema; import java.nio.ByteBuffer; -import java.util.*; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.function.Predicate; - import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.collect.Collections2; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.cql3.VariableSpecifications; import org.apache.cassandra.cql3.functions.masking.ColumnMask; import org.apache.cassandra.cql3.selection.Selectable; import org.apache.cassandra.cql3.selection.Selector; import org.apache.cassandra.cql3.selection.SimpleSelector; -import org.apache.cassandra.db.rows.*; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.MultiCellCapableType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ColumnData; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.MarshalException; import org.github.jamm.Unmetered; +import static java.lang.String.format; + @Unmetered public final class ColumnMetadata extends ColumnSpecification implements Selectable, Comparable { - public static final Comparator asymmetricColumnDataComparator = - (a, b) -> ((ColumnData) a).column().compareTo((ColumnMetadata) b); + public static final Comparator asymmetricColumnDataComparator = new Comparator() + { + @Override + public int compare(Object a, Object b) + { + return ((ColumnData) a).column().compareTo((ColumnMetadata) b); + } + }; public static final int NO_POSITION = -1; @@ -53,9 +74,9 @@ public enum ClusteringOrder /** * The type of CQL3 column this definition represents. - * There is 4 main type of CQL3 columns: those parts of the partition key, - * those parts of the clustering columns and amongst the others, regular and - * static ones. + * There are 5 types of columns: those parts of the partition key, + * those parts of the clustering columns and amongst the others, regular, + * static, and synthetic ones. * * IMPORTANT: this enum is serialized as toString() and deserialized by calling * Kind.valueOf(), so do not override toString() or rename existing values. @@ -63,18 +84,27 @@ public enum ClusteringOrder public enum Kind { // NOTE: if adding a new type, must modify comparisonOrder + SYNTHETIC, PARTITION_KEY, CLUSTERING, REGULAR, STATIC; + // it is not possible to add new Kinds after Synthetic without invasive changes to BTreeRow, which + // assumes that complex regulr/static columns are the last ones public boolean isPrimaryKeyKind() { return this == PARTITION_KEY || this == CLUSTERING; } - } + public static final ColumnIdentifier SYNTHETIC_SCORE_ID = ColumnIdentifier.getInterned("+:!score", true); + + /** + * Whether this is a dropped column. + */ + private final boolean isDropped; + public final Kind kind; /* @@ -90,6 +120,9 @@ public boolean isPrimaryKeyKind() private final Comparator asymmetricCellPathComparator; private final Comparator> cellComparator; + // When the kind is SYNTHETIC, this is the column from which the synthetic column is derived + public final ColumnIdentifier sythenticSourceColumn; + private int hash; /** @@ -104,10 +137,18 @@ public boolean isPrimaryKeyKind() @Nullable private final ColumnMask mask; + /** + * The type of CQL3 column this definition represents. + * Bit layout (from most to least significant): + * - Bits 61-63: Kind ordinal (3 bits, supporting up to 8 Kind values) + * - Bit 60: isComplex flag + * - Bits 48-59: position (12 bits, see assert) + * - Bits 0-47: name.prefixComparison (shifted right by 16) + */ private static long comparisonOrder(Kind kind, boolean isComplex, long position, ColumnIdentifier name) { assert position >= 0 && position < 1 << 12; - return (((long) kind.ordinal()) << 61) + return (((long) kind.ordinal()) << 61) | (isComplex ? 1L << 60 : 0) | (position << 48) | (name.prefixComparison >>> 16); @@ -153,6 +194,34 @@ public static ColumnMetadata staticColumn(String keyspace, String table, String return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.STATIC, null); } + /** + * Creates a new synthetic column metadata instance. + */ + public static ColumnMetadata syntheticScoreColumn(ColumnMetadata sourceColumn, AbstractType type) + { + return new ColumnMetadata(sourceColumn.ksName, sourceColumn.cfName, SYNTHETIC_SCORE_ID, type, NO_POSITION, Kind.SYNTHETIC, null, false, sourceColumn.name); + } + + /** + * Rebuild the metadata for a dropped column from its recorded data. + * + *

    Please note that this method expect that the provided arguments are those of a dropped column, and in + * particular that the type uses no UDT (any should have been expanded). If a column is being dropped, prefer + * {@link #asDropped()} to transform the existing column to a dropped one as this deal with type expansion directly. + */ + public static ColumnMetadata droppedColumn(String keyspace, + String table, + ColumnIdentifier name, + AbstractType type, + Kind kind, + @Nullable ColumnMask mask) + { + assert !kind.isPrimaryKeyKind(); + assert !type.referencesUserTypes() + : format("In %s.%s, dropped column %s type should not contain UDT; got %s" , keyspace, table, name, type); + return new ColumnMetadata(keyspace, table, name, type, NO_POSITION, kind, mask, true); + } + public ColumnMetadata(TableMetadata table, ByteBuffer name, AbstractType type, @@ -177,6 +246,31 @@ public ColumnMetadata(String ksName, int position, Kind kind, @Nullable ColumnMask mask) + { + this(ksName, cfName, name, type, position, kind, mask, false); + } + + public ColumnMetadata(String ksName, + String cfName, + ColumnIdentifier name, + AbstractType type, + int position, + Kind kind, + @Nullable ColumnMask mask, + boolean isDropped) + { + this(ksName, cfName, name, type, position, kind, mask, isDropped, null); + } + + public ColumnMetadata(String ksName, + String cfName, + ColumnIdentifier name, + AbstractType type, + int position, + Kind kind, + @Nullable ColumnMask mask, + boolean isDropped, + ColumnIdentifier sythenticSourceColumnName) { super(ksName, cfName, name, type); assert name != null && type != null && kind != null; @@ -191,57 +285,116 @@ public ColumnMetadata(String ksName, this.kind = kind; this.position = position; this.cellPathComparator = makeCellPathComparator(kind, type); - this.cellComparator = cellPathComparator == null ? ColumnData.comparator : (a, b) -> cellPathComparator.compare(a.path(), b.path()); - this.asymmetricCellPathComparator = cellPathComparator == null ? null : (a, b) -> cellPathComparator.compare(((Cell)a).path(), (CellPath) b); + assert kind != Kind.SYNTHETIC || cellPathComparator == null; + this.cellComparator = cellPathComparator == null ? ColumnData.comparator : new Comparator>() + { + @Override + public int compare(Cell a, Cell b) + { + return cellPathComparator.compare(a.path(), b.path()); + } + }; + this.asymmetricCellPathComparator = cellPathComparator == null ? null : new Comparator() + { + @Override + public int compare(Object a, Object b) + { + return cellPathComparator.compare(((Cell) a).path(), (CellPath) b); + } + }; this.comparisonOrder = comparisonOrder(kind, isComplex(), Math.max(0, position), name); this.mask = mask; + this.isDropped = isDropped; + + // Synthetic columns are the only ones that can have a source column + assert kind == Kind.SYNTHETIC || sythenticSourceColumnName == null; + this.sythenticSourceColumn = sythenticSourceColumnName; } private static Comparator makeCellPathComparator(Kind kind, AbstractType type) { if (kind.isPrimaryKeyKind() || !type.isMultiCell()) return null; + assert !type.isReversed() : "This should not happen because reversed types can be only constructed for " + + "clustering columns which are part of primary keys and should be excluded by the above condition"; - AbstractType nameComparator = type.isCollection() - ? ((CollectionType) type).nameComparator() - : ((UserType) type).nameComparator(); + AbstractType nameComparator = ((MultiCellCapableType) type).nameComparator(); - return (path1, path2) -> + return new Comparator() { - if (path1.size() == 0 || path2.size() == 0) + @Override + public int compare(CellPath path1, CellPath path2) { - if (path1 == CellPath.BOTTOM) - return path2 == CellPath.BOTTOM ? 0 : -1; - if (path1 == CellPath.TOP) - return path2 == CellPath.TOP ? 0 : 1; - return path2 == CellPath.BOTTOM ? 1 : -1; + if (path1.size() == 0 || path2.size() == 0) + { + if (path1 == CellPath.BOTTOM) + return path2 == CellPath.BOTTOM ? 0 : -1; + if (path1 == CellPath.TOP) + return path2 == CellPath.TOP ? 0 : 1; + return path2 == CellPath.BOTTOM ? 1 : -1; + } + + // This will get more complicated once we have non-frozen UDT and nested collections + assert path1.size() == 1 && path2.size() == 1; + return nameComparator.compare(path1.get(0), path2.get(0)); } - - // This will get more complicated once we have non-frozen UDT and nested collections - assert path1.size() == 1 && path2.size() == 1; - return nameComparator.compare(path1.get(0), path2.get(0)); }; } + /** + * Whether that is the column metadata of a dropped column. + */ + public boolean isDropped() + { + return isDropped; + } + public ColumnMetadata copy() { - return new ColumnMetadata(ksName, cfName, name, type, position, kind, mask); + return new ColumnMetadata(ksName, cfName, name, type, position, kind, mask, isDropped); + } + + public ColumnMetadata withNewKeyspace(String newKeyspace, Types udts) + { + return new ColumnMetadata(newKeyspace, cfName, name, type.withUpdatedUserTypes(udts), position, kind, mask, isDropped); } public ColumnMetadata withNewName(ColumnIdentifier newName) { - return new ColumnMetadata(ksName, cfName, newName, type, position, kind, mask); + return new ColumnMetadata(ksName, cfName, newName, type, position, kind, mask, isDropped); } public ColumnMetadata withNewType(AbstractType newType) { - return new ColumnMetadata(ksName, cfName, name, newType, position, kind, mask); + return new ColumnMetadata(ksName, cfName, name, newType, position, kind, mask, isDropped); } public ColumnMetadata withNewMask(@Nullable ColumnMask newMask) { - return new ColumnMetadata(ksName, cfName, name, type, position, kind, newMask); + return new ColumnMetadata(ksName, cfName, name, type, position, kind, newMask, isDropped); + } + + /** + * Transforms this (non-dropped) column metadata into one suitable when the column is dropped. + * + *

    This should be used when a column is dropped to create the relevant {@link DroppedColumn} record. + * + * @return the transformed metadata. It will be equivalent to {@code this} except that 1) its {@link #isDropped} + * method will return {@code true} and 2) any UDT within the column type will have been expanded to tuples (see + * {@link AbstractType#expandUserTypes()}). + */ + ColumnMetadata asDropped() + { + assert !isDropped : this + " was already dropped"; + return new ColumnMetadata(ksName, + cfName, + name, + type.expandUserTypes(), + position, + kind, + mask, + true); } public boolean isPartitionKey() @@ -395,7 +548,7 @@ public int compareTo(ColumnMetadata other) return 0; if (comparisonOrder != other.comparisonOrder) - return Long.compare(comparisonOrder, other.comparisonOrder); + return Long.compareUnsigned(comparisonOrder, other.comparisonOrder); return this.name.compareTo(other.name); } @@ -509,7 +662,7 @@ public void appendNameAndOrderTo(CqlBuilder builder) * * This is the same than the column type, except for non-frozen collections where it's the 'valueComparator' * of the collection. - * + * * This method should not be used to get value type of non-frozon UDT. */ public AbstractType cellValueType() @@ -530,6 +683,11 @@ public boolean isCounterColumn() return type.isCounter(); } + public boolean isSynthetic() + { + return kind == Kind.SYNTHETIC; + } + public Selector.Factory newSelectorFactory(TableMetadata table, AbstractType expectedType, List defs, VariableSpecifications boundNames) throws InvalidRequestException { return SimpleSelector.newFactory(this, addAndGetIndex(this, defs), false); @@ -539,4 +697,14 @@ public AbstractType getExactTypeIfKnown(String keyspace) { return type; } + + /** + * Validate whether the column definition is valid (mostly, that the type is valid for the type of column this is). + * + * @param isCounterTable whether the table the column is part of is a counter table. + */ + public void validate(boolean isCounterTable) + { + type.validateForColumn(name.bytes, isPrimaryKeyColumn(), isCounterTable, isDropped, false); + } } diff --git a/src/java/org/apache/cassandra/schema/CompactionParams.java b/src/java/org/apache/cassandra/schema/CompactionParams.java index 7da6b50280eb..8110cadff3ea 100644 --- a/src/java/org/apache/cassandra/schema/CompactionParams.java +++ b/src/java/org/apache/cassandra/schema/CompactionParams.java @@ -17,36 +17,33 @@ */ package org.apache.cassandra.schema; -import java.lang.reflect.InvocationTargetException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; -import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; + +import org.apache.cassandra.db.compaction.CompactionStrategy; +import org.apache.cassandra.db.compaction.CompactionStrategyOptions; import org.apache.cassandra.db.compaction.LeveledCompactionStrategy; import org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy; import org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy; import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.utils.FBUtilities; +import org.apache.commons.lang3.StringUtils; import static java.lang.String.format; import static org.apache.cassandra.config.CassandraRelevantProperties.DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES; public final class CompactionParams { - private static final Logger logger = LoggerFactory.getLogger(CompactionParams.class); - public enum Option { CLASS, @@ -76,16 +73,13 @@ public static Optional forName(String name) } } - public static final int DEFAULT_MIN_THRESHOLD = 4; - public static final int DEFAULT_MAX_THRESHOLD = 32; - public static final boolean DEFAULT_ENABLED = true; public static final TombstoneOption DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES_PROPERTY_VALUE = DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES.getEnum(TombstoneOption.NONE); public static final Map DEFAULT_THRESHOLDS = - ImmutableMap.of(Option.MIN_THRESHOLD.toString(), Integer.toString(DEFAULT_MIN_THRESHOLD), - Option.MAX_THRESHOLD.toString(), Integer.toString(DEFAULT_MAX_THRESHOLD)); + ImmutableMap.of(Option.MIN_THRESHOLD.toString(), Integer.toString(CompactionStrategyOptions.DEFAULT_MIN_THRESHOLD), + Option.MAX_THRESHOLD.toString(), Integer.toString(CompactionStrategyOptions.DEFAULT_MAX_THRESHOLD)); public static final CompactionParams DEFAULT; static @@ -93,8 +87,8 @@ public static Optional forName(String name) ParameterizedClass defaultCompaction = DatabaseDescriptor.getDefaultCompaction(); if (defaultCompaction == null) { - DEFAULT = new CompactionParams(SizeTieredCompactionStrategy.class, - DEFAULT_THRESHOLDS, + DEFAULT = new CompactionParams(UnifiedCompactionStrategy.class, + Collections.emptyMap(), DEFAULT_ENABLED, DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES_PROPERTY_VALUE); } @@ -105,20 +99,18 @@ public static Optional forName(String name) } } - private final Class klass; - private final ImmutableMap options; + private final CompactionStrategyOptions strategyOptions; private final boolean isEnabled; private final TombstoneOption tombstoneOption; - private CompactionParams(Class klass, Map options, boolean isEnabled, TombstoneOption tombstoneOption) + private CompactionParams(Class klass, Map options, boolean isEnabled, TombstoneOption tombstoneOption) { - this.klass = klass; - this.options = ImmutableMap.copyOf(options); + this.strategyOptions = new CompactionStrategyOptions(klass, options, true); this.isEnabled = isEnabled; this.tombstoneOption = tombstoneOption; } - public static CompactionParams create(Class klass, Map options) + public static CompactionParams create(Class klass, Map options) { boolean isEnabled = options.containsKey(Option.ENABLED.toString()) ? Boolean.parseBoolean(options.get(Option.ENABLED.toString())) @@ -134,14 +126,7 @@ public static CompactionParams create(Class allOptions = new HashMap<>(options); - if (supportsThresholdParams(klass)) - { - allOptions.putIfAbsent(Option.MIN_THRESHOLD.toString(), Integer.toString(DEFAULT_MIN_THRESHOLD)); - allOptions.putIfAbsent(Option.MAX_THRESHOLD.toString(), Integer.toString(DEFAULT_MAX_THRESHOLD)); - } - - return new CompactionParams(klass, allOptions, isEnabled, tombstoneOption); + return new CompactionParams(klass, new HashMap<>(options), isEnabled, tombstoneOption); } public static CompactionParams stcs(Map options) @@ -166,18 +151,12 @@ public static CompactionParams twcs(Map options) public int minCompactionThreshold() { - String threshold = options.get(Option.MIN_THRESHOLD.toString()); - return threshold == null - ? DEFAULT_MIN_THRESHOLD - : Integer.parseInt(threshold); + return strategyOptions.minCompactionThreshold(); } public int maxCompactionThreshold() { - String threshold = options.get(Option.MAX_THRESHOLD.toString()); - return threshold == null - ? DEFAULT_MAX_THRESHOLD - : Integer.parseInt(threshold); + return strategyOptions.maxCompactionThreshold(); } public TombstoneOption tombstoneOption() @@ -185,87 +164,14 @@ public TombstoneOption tombstoneOption() return tombstoneOption; } - public void validate() - { - try - { - Map unknownOptions = (Map) klass.getMethod("validateOptions", Map.class).invoke(null, options); - if (!unknownOptions.isEmpty()) - { - throw new ConfigurationException(format("Properties specified %s are not understood by %s", - unknownOptions.keySet(), - klass.getSimpleName())); - } - } - catch (NoSuchMethodException e) - { - logger.warn("Compaction strategy {} does not have a static validateOptions method. Validation ignored", - klass.getName()); - } - catch (InvocationTargetException e) - { - if (e.getTargetException() instanceof ConfigurationException) - throw (ConfigurationException) e.getTargetException(); - - Throwable cause = e.getCause() == null - ? e - : e.getCause(); - - throw new ConfigurationException(format("%s.validateOptions() threw an error: %s %s", - klass.getName(), - cause.getClass().getName(), - cause.getMessage()), - e); - } - catch (IllegalAccessException e) - { - throw new ConfigurationException("Cannot access method validateOptions in " + klass.getName(), e); - } - - String minThreshold = options.get(Option.MIN_THRESHOLD.toString()); - if (minThreshold != null && !StringUtils.isNumeric(minThreshold)) - { - throw new ConfigurationException(format("Invalid value %s for '%s' compaction sub-option - must be an integer", - minThreshold, - Option.MIN_THRESHOLD)); - } - - String maxThreshold = options.get(Option.MAX_THRESHOLD.toString()); - if (maxThreshold != null && !StringUtils.isNumeric(maxThreshold)) - { - throw new ConfigurationException(format("Invalid value %s for '%s' compaction sub-option - must be an integer", - maxThreshold, - Option.MAX_THRESHOLD)); - } - - if (minCompactionThreshold() <= 0 || maxCompactionThreshold() <= 0) - { - throw new ConfigurationException("Disabling compaction by setting compaction thresholds to 0 has been removed," - + " set the compaction option 'enabled' to false instead."); - } - - if (minCompactionThreshold() <= 1) - { - throw new ConfigurationException(format("Min compaction threshold cannot be less than 2 (got %d)", - minCompactionThreshold())); - } - - if (minCompactionThreshold() > maxCompactionThreshold()) - { - throw new ConfigurationException(format("Min compaction threshold (got %d) cannot be greater than max compaction threshold (got %d)", - minCompactionThreshold(), - maxCompactionThreshold())); - } - } - double defaultBloomFilterFbChance() { - return klass.equals(LeveledCompactionStrategy.class) ? 0.1 : 0.01; + return klass().equals(LeveledCompactionStrategy.class) ? 0.1 : 0.01; } - public Class klass() + public Class klass() { - return klass; + return strategyOptions.klass(); } /** @@ -273,7 +179,7 @@ public Class klass() */ public Map options() { - return options; + return strategyOptions.getOptions(); } public boolean isEnabled() @@ -296,56 +202,29 @@ public static CompactionParams fromMap(Map map) return create(classFromName(className), options); } - public static Class classFromName(String name) + public static Class classFromName(String name) { String className = name.contains(".") ? name : "org.apache.cassandra.db.compaction." + name; - Class strategyClass = FBUtilities.classForName(className, "compaction strategy"); - if (!AbstractCompactionStrategy.class.isAssignableFrom(strategyClass)) - { - throw new ConfigurationException(format("Compaction strategy class %s is not derived from AbstractReplicationStrategy", - className)); - } + Class strategyClass = + FBUtilities.classForNameWithoutInitialization(className, "compaction strategy", CompactionStrategy.class); return strategyClass; } - /* - * LCS doesn't, STCS and DTCS do - */ - @SuppressWarnings("unchecked") - public static boolean supportsThresholdParams(Class klass) - { - try - { - Map unrecognizedOptions = - (Map) klass.getMethod("validateOptions", Map.class) - .invoke(null, DEFAULT_THRESHOLDS); - - return unrecognizedOptions.isEmpty(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } - public Map asMap() { Map map = new HashMap<>(options()); - map.put(Option.CLASS.toString(), klass.getName()); + map.put(Option.CLASS.toString(), klass().getName()); return map; } @Override public String toString() { - return MoreObjects.toStringHelper(this) - .add("class", klass.getName()) - .add("options", options) - .toString(); + return strategyOptions.toString(); } @Override @@ -359,12 +238,12 @@ public boolean equals(Object o) CompactionParams cp = (CompactionParams) o; - return klass.equals(cp.klass) && options.equals(cp.options); + return strategyOptions.equals(cp.strategyOptions); } @Override public int hashCode() { - return Objects.hash(klass, options); + return Objects.hash(strategyOptions); } } diff --git a/src/java/org/apache/cassandra/schema/CompressionParams.java b/src/java/org/apache/cassandra/schema/CompressionParams.java index 0e7c3da13ab0..ecdb326e9601 100644 --- a/src/java/org/apache/cassandra/schema/CompressionParams.java +++ b/src/java/org/apache/cassandra/schema/CompressionParams.java @@ -30,7 +30,12 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cache.ChunkCache; import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.exceptions.ConfigurationException; @@ -38,12 +43,16 @@ import org.apache.cassandra.io.compress.*; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.util.PageAware; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.utils.FBUtilities; import static java.lang.String.format; public final class CompressionParams { + private static final Logger logger = LoggerFactory.getLogger(CompressionParams.class); + public static final int DEFAULT_CHUNK_LENGTH = 1024 * 16; public static final double DEFAULT_MIN_COMPRESS_RATIO = 0.0; // Since pre-4.0 versions do not understand the // new compression parameter we can't use a @@ -55,13 +64,27 @@ public final class CompressionParams public static final String ENABLED = "enabled"; public static final String MIN_COMPRESS_RATIO = "min_compress_ratio"; + public static final CompressionParams FAST = new CompressionParams(LZ4Compressor.create(Collections.emptyMap()), + DEFAULT_CHUNK_LENGTH, + calcMaxCompressedLength(DEFAULT_CHUNK_LENGTH, DEFAULT_MIN_COMPRESS_RATIO), + DEFAULT_MIN_COMPRESS_RATIO, + Collections.emptyMap()); + + public static final CompressionParams ADAPTIVE = new CompressionParams(AdaptiveCompressor.create(Collections.emptyMap()), + DEFAULT_CHUNK_LENGTH, + calcMaxCompressedLength(DEFAULT_CHUNK_LENGTH, DEFAULT_MIN_COMPRESS_RATIO), + DEFAULT_MIN_COMPRESS_RATIO, + Collections.emptyMap()); + + public static final CompressionParams FAST_ADAPTIVE = new CompressionParams(AdaptiveCompressor.createForFlush(Collections.emptyMap()), + DEFAULT_CHUNK_LENGTH, + calcMaxCompressedLength(DEFAULT_CHUNK_LENGTH, DEFAULT_MIN_COMPRESS_RATIO), + DEFAULT_MIN_COMPRESS_RATIO, + Collections.emptyMap()); + public static final CompressionParams DEFAULT = !CassandraRelevantProperties.DETERMINISM_SSTABLE_COMPRESSION_DEFAULT.getBoolean() ? noCompression() - : new CompressionParams(LZ4Compressor.create(Collections.emptyMap()), - DEFAULT_CHUNK_LENGTH, - calcMaxCompressedLength(DEFAULT_CHUNK_LENGTH, DEFAULT_MIN_COMPRESS_RATIO), - DEFAULT_MIN_COMPRESS_RATIO, - Collections.emptyMap()); + : DatabaseDescriptor.shouldUseAdaptiveCompressionByDefault() ? ADAPTIVE : FAST; public static final CompressionParams NOOP = new CompressionParams(NoopCompressor.create(Collections.emptyMap()), // 4 KiB is often the underlying disk block size @@ -223,6 +246,24 @@ public boolean isEnabled() return sstableCompressor != null; } + /** + * Specializes the compressor for given use. + * May cause reconfiguration of parameters on some compressors. + * Returns null if params are not compatible with the given use. + */ + public CompressionParams forUse(ICompressor.Uses use) + { + ICompressor specializedCompressor = this.sstableCompressor.forUse(use); + if (specializedCompressor == null) + return null; + + assert specializedCompressor.recommendedUses().contains(use); + if (specializedCompressor == sstableCompressor) + return this; + + return new CompressionParams(specializedCompressor, chunkLength, maxCompressedLength, minCompressRatio, otherOptions); + } + /** * Returns the SSTable compressor. * @return the SSTable compressor or {@code null} if compression is disabled. @@ -247,7 +288,7 @@ public int maxCompressedLength() return maxCompressedLength; } - private static Class parseCompressorClass(String className) throws ConfigurationException + private static Class parseCompressorClass(String className) throws ConfigurationException { if (className == null || className.isEmpty()) return null; @@ -255,15 +296,17 @@ private static Class parseCompressorClass(String className) throws Configurat className = className.contains(".") ? className : "org.apache.cassandra.io.compress." + className; try { - return Class.forName(className); + return FBUtilities.classForNameWithoutInitialization(className, "compression", ICompressor.class); } - catch (Exception e) + catch (ConfigurationException e) { - throw new ConfigurationException("Could not create Compression for type " + className, e); + if (e.getCause() instanceof ClassNotFoundException || e.getCause() instanceof NoClassDefFoundError) + throw new ConfigurationException("Could not create Compression for type " + className, e); + throw e; } } - private static ICompressor createCompressor(Class compressorClass, Map compressionOptions) throws ConfigurationException + private static ICompressor createCompressor(Class compressorClass, Map compressionOptions) throws ConfigurationException { if (compressorClass == null) { @@ -348,6 +391,10 @@ private static Integer parseChunkLength(String chLengthKB) throws ConfigurationE int parsed = Integer.parseInt(chLengthKB); if (parsed > Integer.MAX_VALUE / 1024) throw new ConfigurationException(format("Value of %s is too large (%s)", CHUNK_LENGTH_IN_KB,parsed)); + if (parsed * 1024 < PageAware.PAGE_SIZE && ChunkCache.instance != null && ChunkCache.instance.isEnabled()) + logger.warn("Chunk length {} KiB is smaller than the page size {} KiB. " + + "This is not recommended as it will cause wasted chunk cache space.", + parsed, PageAware.PAGE_SIZE / 1024); return 1024 * parsed; } catch (NumberFormatException e) @@ -473,6 +520,16 @@ public String chunkLengthInKB() return String.valueOf(chunkLength() / 1024); } + /** + * Whether CRC checks should be performed during decompression + * @return true if CRC checks should be performed + */ + public boolean shouldCheckCrc() + { + // CRC checks should be performed unless compression is disabled + return isEnabled(); + } + @Override public boolean equals(Object obj) { diff --git a/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java b/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java index a81affc26e34..21718ce1c0b0 100644 --- a/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java +++ b/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java @@ -68,7 +68,7 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin private MigrationCoordinator createMigrationCoordinator(MessagingService messagingService) { return new MigrationCoordinator(messagingService, - Stage.MIGRATION.executor(), + Stage.MIGRATION, ScheduledExecutors.scheduledTasks, MAX_OUTSTANDING_VERSION_REQUESTS, Gossiper.instance, @@ -212,7 +212,7 @@ synchronized SchemaTransformationResult applyMutations(Collection sche // apply the schema mutations and fetch the new versions of the altered keyspaces Keyspaces updatedKeyspaces = SchemaKeyspace.fetchKeyspaces(affectedKeyspaces); Set removedKeyspaces = affectedKeyspaces.stream().filter(ks -> !updatedKeyspaces.containsKeyspace(ks)).collect(Collectors.toSet()); - Keyspaces afterKeyspaces = before.getKeyspaces().withAddedOrReplaced(updatedKeyspaces).without(removedKeyspaces); + Keyspaces afterKeyspaces = before.getKeyspaces().withAddedOrUpdated(updatedKeyspaces).without(removedKeyspaces); Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before.getKeyspaces(), afterKeyspaces); UUID version = SchemaKeyspace.calculateSchemaDigest(); diff --git a/src/java/org/apache/cassandra/schema/DroppedColumn.java b/src/java/org/apache/cassandra/schema/DroppedColumn.java index 90dfe651f7e0..2ba9dabf4154 100644 --- a/src/java/org/apache/cassandra/schema/DroppedColumn.java +++ b/src/java/org/apache/cassandra/schema/DroppedColumn.java @@ -20,17 +20,36 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.db.marshal.AbstractType; + +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + public final class DroppedColumn { public final ColumnMetadata column; public final long droppedTime; // drop timestamp, in microseconds, yet with millisecond granularity + /** + * Creates a new dropped column record. + * + * @param column the metadata for the dropped column. This must be a dropped metadata, that is we should + * have {@code column.isDropped() == true}. + * @param droppedTime the time at which the column was dropped, in microseconds. + */ public DroppedColumn(ColumnMetadata column, long droppedTime) { + assert column.isDropped() : column.debugString() + " should be dropped"; this.column = column; this.droppedTime = droppedTime; } + public DroppedColumn withNewKeyspace(String newKeyspace, Types udts) + { + return new DroppedColumn(column.withNewKeyspace(newKeyspace, udts), droppedTime); + } + @Override public boolean equals(Object o) { @@ -51,9 +70,54 @@ public int hashCode() return Objects.hashCode(column, droppedTime); } + public String toCQLString() + { + return String.format("DROPPED COLUMN RECORD %s %s%s USING TIMESTAMP %d", + column.name.toCQLString(), + column.type.asCQL3Type().toSchemaString(), + column.isStatic() ? " static" : "", + droppedTime); + } + @Override public String toString() { return MoreObjects.toStringHelper(this).add("column", column).add("droppedTime", droppedTime).toString(); } + + /** + * A parsed dropped column record (from CREATE TABLE ... WITH DROPPED COLUMN RECORD ...). + */ + public static final class Raw + { + private final ColumnIdentifier name; + private final CQL3Type.Raw type; + private final boolean isStatic; + private final long timestamp; + + public Raw(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, long timestamp) + { + this.name = name; + this.type = type; + this.isStatic = isStatic; + this.timestamp = timestamp; + } + + public DroppedColumn prepare(String keyspace, String table, Types types) + { + ColumnMetadata.Kind kind = isStatic ? ColumnMetadata.Kind.STATIC : ColumnMetadata.Kind.REGULAR; + AbstractType parsedType = type.prepare(keyspace, types).getType(); + if (parsedType.referencesUserTypes()) + throw invalidRequest("Invalid type %s for DROPPED COLUMN RECORD on %s: dropped column types should " + + "not have user types", type, name); + + ColumnMetadata droppedColumn = ColumnMetadata.droppedColumn(keyspace, + table, + name, + parsedType, + kind, + null); + return new DroppedColumn(droppedColumn, timestamp); + } + } } diff --git a/src/java/org/apache/cassandra/schema/IndexMetadata.java b/src/java/org/apache/cassandra/schema/IndexMetadata.java index 08433fb20411..696f82ac98c1 100644 --- a/src/java/org/apache/cassandra/schema/IndexMetadata.java +++ b/src/java/org/apache/cassandra/schema/IndexMetadata.java @@ -20,10 +20,17 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.util.*; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -31,21 +38,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.UnknownIndexException; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.internal.CassandraIndex; import org.apache.cassandra.index.sai.StorageAttachedIndex; -import org.apache.cassandra.index.sasi.SASIIndex; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.UUIDSerializer; +import javax.annotation.Nullable; + import static org.apache.cassandra.schema.SchemaConstants.PATTERN_NON_WORD_CHAR; import static org.apache.cassandra.schema.SchemaConstants.isValidCharsName; @@ -58,16 +67,23 @@ public final class IndexMetadata public static final Serializer serializer = new Serializer(); + static final String INDEX_POSTFIX = "_idx"; /** * A mapping of user-friendly index names to their fully qualified index class names. */ private static final Map indexNameAliases = new ConcurrentHashMap<>(); + /** + * The fully qualified class names of the custom index implementations listed in the + * {@link CassandraRelevantProperties#TRUSTED_INDEX_IMPLEMENTATIONS} system property. + */ + private static final Set trustedIndexImplementations = ConcurrentHashMap.newKeySet(); + static { indexNameAliases.put(StorageAttachedIndex.NAME, StorageAttachedIndex.class.getCanonicalName()); indexNameAliases.put(StorageAttachedIndex.class.getSimpleName().toLowerCase(), StorageAttachedIndex.class.getCanonicalName()); - indexNameAliases.put(SASIIndex.class.getSimpleName(), SASIIndex.class.getCanonicalName()); + loadTrustedIndexImplementations(CassandraRelevantProperties.TRUSTED_INDEX_IMPLEMENTATIONS.getString()); } public enum Kind @@ -109,21 +125,52 @@ public static IndexMetadata fromIndexTargets(List targets, return new IndexMetadata(name, newOptions, kind); } - public static String generateDefaultIndexName(String table, ColumnIdentifier column) + /** + * Generates a default index name from the table and column names. + * Characters other than alphanumeric and underscore are removed. + * Long index names are truncated to fit the length allowing constructing filenames. + * + * @param table the table name + * @param column the column identifier. Can be null if the index is not column specific. + * @return the generated index name + */ + public static String generateDefaultIndexName(String table, @Nullable ColumnIdentifier column) { - return PATTERN_NON_WORD_CHAR.matcher(table + "_" + column.toString() + "_idx").replaceAll(""); + String indexNameUncleaned = table; + if (column != null) + indexNameUncleaned += '_' + column.toString(); + String indexNameUntrimmed = PATTERN_NON_WORD_CHAR.matcher(indexNameUncleaned).replaceAll(""); + String indexNameTrimmed = indexNameUntrimmed + .substring(0, + Math.min(calculateGeneratedIndexNameMaxLength(), + indexNameUntrimmed.length())); + return indexNameTrimmed + INDEX_POSTFIX; } - public static String generateDefaultIndexName(String table) + /** + * Calculates the maximum length of the generated index name to fit file names. + * It includes the generated suffixes in account. + * The calculation depends on how an index implements file names construciton from index names. + * This needs to be addressed, see CNDB-13240. + * + * @return the allowed length of the generated index name + */ + private static int calculateGeneratedIndexNameMaxLength() { - return PATTERN_NON_WORD_CHAR.matcher(table + "_" + "idx").replaceAll(""); + // Speculative assumption that uniqueness breaker will fit into 999. + // The value is used for trimming the index name if needed. + // Introducing validation of index name length is TODO for CNDB-13198. + int uniquenessSuffixLength = 4; + int indexNameAddition = uniquenessSuffixLength + INDEX_POSTFIX.length(); + + return SchemaConstants.INDEX_NAME_LENGTH - indexNameAddition; } public void validate(TableMetadata table) { - // TODO: address validating the length by CASSANDRA-20445 if (!isValidCharsName(name)) - throw new ConfigurationException("Illegal index name " + name); + throw new ConfigurationException(String.format("Index name must not be empty, or contain non-alphanumeric-underscore characters (got \"%s\")", + name)); if (kind == null) throw new ConfigurationException("Index kind is null for index " + name); @@ -133,13 +180,10 @@ public void validate(TableMetadata table) if (options == null || !options.containsKey(IndexTarget.CUSTOM_INDEX_OPTION_NAME)) throw new ConfigurationException(String.format("Required option missing for index %s : %s", name, IndexTarget.CUSTOM_INDEX_OPTION_NAME)); + // Find any aliases to the fully qualified index class name: + String className = expandAliases(options.get(IndexTarget.CUSTOM_INDEX_OPTION_NAME)); - // Get the fully qualified class name: - String className = getIndexClassName(); - - Class indexerClass = FBUtilities.classForName(className, "custom indexer"); - if (!Index.class.isAssignableFrom(indexerClass)) - throw new ConfigurationException(String.format("Specified Indexer class (%s) does not implement the Indexer interface", className)); + Class indexerClass = FBUtilities.classForNameWithoutInitialization(className, "custom indexer", Index.class); validateCustomIndexOptions(table, indexerClass, options); } } @@ -147,11 +191,64 @@ public void validate(TableMetadata table) public String getIndexClassName() { if (isCustom()) + return expandAliases(options.get(IndexTarget.CUSTOM_INDEX_OPTION_NAME)); + return CassandraIndex.class.getName(); + } + + public static String expandAliases(String className) + { + return indexNameAliases.getOrDefault(className.toLowerCase(), className); + } + + /** + * (Re)loads the trusted custom index implementations from the given comma-separated list of fully qualified + * class names, registering for each of them an alias by its simple class name so that users can reference it + * in {@code CREATE CUSTOM INDEX ... USING} without the package name. + */ + @VisibleForTesting + public static void loadTrustedIndexImplementations(String classNames) + { + for (String className : trustedIndexImplementations) + indexNameAliases.remove(simpleClassName(className).toLowerCase(), className); + trustedIndexImplementations.clear(); + + if (classNames == null) + return; + + for (String className : classNames.split(",")) { - String className = options.get(IndexTarget.CUSTOM_INDEX_OPTION_NAME); - return indexNameAliases.getOrDefault(className.toLowerCase(), className); + className = className.trim(); + if (className.isEmpty()) + continue; + + String simpleName = simpleClassName(className); + if (simpleName.equals(className)) + { + logger.warn("Ignoring trusted index implementation '{}' declared in the {} system property: " + + "a fully qualified class name is required", + className, CassandraRelevantProperties.TRUSTED_INDEX_IMPLEMENTATIONS.getKey()); + continue; + } + + trustedIndexImplementations.add(className); + indexNameAliases.put(simpleName.toLowerCase(), className); + logger.info("Registered a trusted secondary implementation {}", className); } - return CassandraIndex.class.getName(); + } + + /** + * Tells whether the given index class name, either fully qualified or an alias, is one of the custom index + * implementations trusted through the {@link CassandraRelevantProperties#TRUSTED_INDEX_IMPLEMENTATIONS} system + * property. Trusted implementations are limited by the {@code trusted_indexes_per_table} guardrail. + */ + public static boolean isTrustedIndexImplementation(String className) + { + return className != null && trustedIndexImplementations.contains(expandAliases(className)); + } + + private static String simpleClassName(String className) + { + return className.substring(className.lastIndexOf('.') + 1); } private void validateCustomIndexOptions(TableMetadata table, Class indexerClass, Map options) @@ -183,11 +280,11 @@ private void validateCustomIndexOptions(TableMetadata table, Class metadata) + { + TreeSet sortedNames = new TreeSet<>(); + for (IndexMetadata indexMetadata : metadata) + sortedNames.add(indexMetadata.name); + return String.join(",", sortedNames); + } + + public static Set toNames(Set indexes) + { + Set included = new HashSet<>(indexes.size()); + for (IndexMetadata i : indexes) + included.add(i.name); + return included; + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java index ae5a587b0f9d..ad1a3deb488b 100644 --- a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java +++ b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java @@ -21,11 +21,11 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Stream; - import javax.annotation.Nullable; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import org.apache.cassandra.config.DatabaseDescriptor; @@ -38,15 +38,17 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; +import org.apache.cassandra.locator.TokenMetadataProvider; import org.apache.cassandra.schema.Tables.TablesDiff; import org.apache.cassandra.schema.Types.TypesDiff; +import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; import org.apache.cassandra.schema.Views.ViewsDiff; -import org.apache.cassandra.service.StorageService; - -import static java.lang.String.format; +import org.apache.cassandra.service.ClientState; import static com.google.common.collect.Iterables.any; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Maps.transformValues; +import static java.lang.String.format; /** * An immutable representation of keyspace metadata (name, params, tables, types, and functions). @@ -114,6 +116,30 @@ public static KeyspaceMetadata virtual(String name, Tables tables) return new KeyspaceMetadata(name, Kind.VIRTUAL, KeyspaceParams.local(), tables, Views.none(), Types.none(), UserFunctions.none()); } + public KeyspaceMetadata rename(String newName) + { + Types newTypes = types.withNewKeyspace(newName); + UserFunctions newFunctions = userFunctions.withNewKeyspace(newName, newTypes); + Tables newTables = tables.withNewKeyspace(newName, newTypes); + + Views.Builder viewsBuilder = Views.builder(); + + for (ViewMetadata view : views) + { + TableMetadata newMetadata = newTables.getNullable(view.baseTableName); + TableMetadata.Builder tableBuilder = TableMetadata.builder(newName, view.metadata.name) + .partitioner(view.metadata.partitioner) + .kind(view.metadata.kind) + .params(view.metadata.params) + .flags(view.metadata.flags) + .addColumns(transform(view.metadata.columns(), c -> c.withNewKeyspace(newName, newTypes))) + .droppedColumns(transformValues(view.metadata.droppedColumns, c -> c.withNewKeyspace(newName, newTypes))); + viewsBuilder.put(new ViewMetadata(newMetadata.id, newMetadata.name, view.includeAllColumns, view.whereClause, tableBuilder.build())); + } + + return new KeyspaceMetadata(newName, kind, params, newTables, viewsBuilder.build(), newTypes, newFunctions); + } + public KeyspaceMetadata withSwapped(KeyspaceParams params) { return new KeyspaceMetadata(name, kind, params, tables, views, types, userFunctions); @@ -139,6 +165,38 @@ public KeyspaceMetadata withSwapped(UserFunctions functions) return new KeyspaceMetadata(name, kind, params, tables, views, types, functions); } + /** + * Returns a new instance of this {@link KeyspaceMetadata} which is obtained by applying the provided + * transformFunction to the {@link TableParams} of all the tables and views contained in + * this keyspace. + * + * @param transformFunction the function used to transform the table parameters + * @return a copy of this keyspace with table params transformed in all tables and views + */ + public KeyspaceMetadata withTransformedTableParams(java.util.function.Function transformFunction) + { + // Transform the params for all the tables + Tables newTables = tables.withTransformedParams(transformFunction); + Views.Builder newViews = Views.builder(); + + // Then transform the params for all the views + for (ViewMetadata view : views) + { + String baseTableName = view.baseTableName; + TableMetadata newBaseTable = newTables.getNullable(baseTableName); + Preconditions.checkNotNull(newBaseTable, "Table " + baseTableName + " is the base table of the view " + + view.name() + " but has not been found among the updated tables."); + + newViews.put(new ViewMetadata(view.baseTableId, + view.baseTableName, + view.includeAllColumns, + view.whereClause, + newBaseTable)); + } + + return new KeyspaceMetadata(name, kind, params, newTables, newViews.build(), types, userFunctions); + } + public KeyspaceMetadata empty() { return new KeyspaceMetadata(this.name, this.kind, this.params, Tables.none(), Views.none(), Types.none(), UserFunctions.none()); @@ -333,10 +391,10 @@ public String toCqlString(boolean withInternals, boolean ifNotExists) return builder.toString(); } - public void validate() + public void validate(ClientState clientState) { validateKeyspaceName(name, ConfigurationException::new); - params.validate(name, null); + params.validate(name, clientState); tablesAndViews().forEach(TableMetadata::validate); Set indexNames = new HashSet<>(); @@ -356,7 +414,7 @@ public AbstractReplicationStrategy createReplicationStrategy() { return AbstractReplicationStrategy.createReplicationStrategy(name, params.replication.klass, - StorageService.instance.getTokenMetadata(), + TokenMetadataProvider.instance.getTokenMetadataForKeyspace(name), DatabaseDescriptor.getEndpointSnitch(), params.replication.options); } diff --git a/src/java/org/apache/cassandra/schema/KeyspaceParams.java b/src/java/org/apache/cassandra/schema/KeyspaceParams.java index 539993e2b32a..644e6b832808 100644 --- a/src/java/org/apache/cassandra/schema/KeyspaceParams.java +++ b/src/java/org/apache/cassandra/schema/KeyspaceParams.java @@ -17,11 +17,22 @@ */ package org.apache.cassandra.schema; +import java.util.Collections; +import java.util.List; import java.util.Map; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.locator.NetworkTopologyStrategy; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SYSTEM_DISTRIBUTED_NTS_DC_OVERRIDE_PROPERTY; +import static org.apache.cassandra.config.CassandraRelevantProperties.SYSTEM_DISTRIBUTED_NTS_RF_OVERRIDE_PROPERTY; import org.apache.cassandra.service.ClientState; @@ -30,6 +41,8 @@ */ public final class KeyspaceParams { + private static final Logger logger = LoggerFactory.getLogger(KeyspaceParams.class); + public static final boolean DEFAULT_DURABLE_WRITES = true; /** @@ -55,6 +68,8 @@ public String toString() public final boolean durableWrites; public final ReplicationParams replication; + private static final Map SYSTEM_DISTRIBUTED_NTS_OVERRIDE = getSystemDistributedNtsOverride(); + public KeyspaceParams(boolean durableWrites, ReplicationParams replication) { this.durableWrites = durableWrites; @@ -86,6 +101,11 @@ public static KeyspaceParams simpleTransient(int replicationFactor) return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor)); } + public static KeyspaceParams everywhere() + { + return new KeyspaceParams(true, ReplicationParams.everywhere()); + } + public static KeyspaceParams nts(Object... args) { return new KeyspaceParams(true, ReplicationParams.nts(args)); @@ -96,6 +116,25 @@ public void validate(String name, ClientState state) replication.validate(name, state); } + /** + * Used to pick the default replication strategy for all distributed system keyspaces. + * The default will be SimpleStrategy and a hard coded RF factor. + *

    + * One can change this default to NTS by passing in system properties: + * -Dcassandra.system_distributed_replication_per_dc=3 + * -Dcassandra.system_distributed_replication_dc_names=cloud-east,cloud-west + */ + public static KeyspaceParams systemDistributed(int rf) + { + if (!SYSTEM_DISTRIBUTED_NTS_OVERRIDE.isEmpty()) + { + logger.info("Using override for distributed system keyspaces: {}", SYSTEM_DISTRIBUTED_NTS_OVERRIDE); + return create(true, SYSTEM_DISTRIBUTED_NTS_OVERRIDE); + } + + return simple(rf); + } + @Override public boolean equals(Object o) { @@ -124,4 +163,41 @@ public String toString() .add(Option.REPLICATION.toString(), replication) .toString(); } + + @VisibleForTesting + static Map getSystemDistributedNtsOverride() + { + int rfOverride = -1; + List dcOverride = Collections.emptyList(); + ImmutableMap.Builder ntsOverride = ImmutableMap.builder(); + + try + { + rfOverride = SYSTEM_DISTRIBUTED_NTS_RF_OVERRIDE_PROPERTY.getInt(-1); + dcOverride = Splitter.on(',').trimResults().omitEmptyStrings().splitToList(SYSTEM_DISTRIBUTED_NTS_DC_OVERRIDE_PROPERTY.getString(",")); + } + catch (RuntimeException ex) + { + logger.error("Error parsing system distributed replication override properties", ex); + } + + if (rfOverride != -1 && !dcOverride.isEmpty()) + { + // Validate reasonable defaults + if (rfOverride <= 0 || rfOverride > 5) + { + logger.error("Invalid value for {}", SYSTEM_DISTRIBUTED_NTS_RF_OVERRIDE_PROPERTY.getKey()); + } + else + { + for (String dc : dcOverride) + ntsOverride.put(dc, String.valueOf(rfOverride)); + + ntsOverride.put(ReplicationParams.CLASS, NetworkTopologyStrategy.class.getCanonicalName()); + return ntsOverride.build(); + } + } + + return Collections.emptyMap(); + } } diff --git a/src/java/org/apache/cassandra/schema/Keyspaces.java b/src/java/org/apache/cassandra/schema/Keyspaces.java index e9bd92c7c5d7..24f8e44dfbcd 100644 --- a/src/java/org/apache/cassandra/schema/Keyspaces.java +++ b/src/java/org/apache/cassandra/schema/Keyspaces.java @@ -129,25 +129,17 @@ public Keyspaces without(Collection names) return filter(k -> !names.contains(k.name)); } - public Keyspaces withAddedOrUpdated(KeyspaceMetadata keyspace) - { - return builder().add(Iterables.filter(this, k -> !k.name.equals(keyspace.name))) - .add(keyspace) - .build(); - } - /** * Returns a new {@link Keyspaces} equivalent to this one, but with the provided keyspace metadata either added (if * this {@link Keyspaces} does not have that keyspace), or replaced by the provided definition. * *

    Note that if this contains the provided keyspace, its pre-existing definition is discarded and completely - * replaced with the newly provided one. See {@link #withAddedOrUpdated(KeyspaceMetadata)} if you wish the provided - * definition to be "merged" with the existing one instead. + * replaced with the newly provided one. * * @param keyspace the keyspace metadata to add, or replace the existing definition with. * @return the newly created object. */ - public Keyspaces withAddedOrReplaced(KeyspaceMetadata keyspace) + public Keyspaces withAddedOrUpdated(KeyspaceMetadata keyspace) { return builder().add(Iterables.filter(this, k -> !k.name.equals(keyspace.name))) .add(keyspace) @@ -155,23 +147,18 @@ public Keyspaces withAddedOrReplaced(KeyspaceMetadata keyspace) } /** - * Calls {@link #withAddedOrReplaced(KeyspaceMetadata)} on all the keyspaces of the provided {@link Keyspaces}. + * Calls {@link #withAddedOrUpdated(Keyspaces)} on all the keyspaces of the provided {@link Keyspaces}. * * @param keyspaces the keyspaces to add, or replace if existing. * @return the newly created object. */ - public Keyspaces withAddedOrReplaced(Keyspaces keyspaces) + public Keyspaces withAddedOrUpdated(Keyspaces keyspaces) { return builder().add(Iterables.filter(this, k -> !keyspaces.containsKeyspace(k.name))) .add(keyspaces) .build(); } - public void validate() - { - keyspaces.values().forEach(KeyspaceMetadata::validate); - } - @Override public boolean equals(Object o) { diff --git a/src/java/org/apache/cassandra/schema/MemtableParams.java b/src/java/org/apache/cassandra/schema/MemtableParams.java index 7d88f6518ee5..b6b0afb502a4 100644 --- a/src/java/org/apache/cassandra/schema/MemtableParams.java +++ b/src/java/org/apache/cassandra/schema/MemtableParams.java @@ -20,6 +20,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -30,14 +31,21 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.InheritingClass; import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.memtable.SkipListMemtableFactory; +import org.apache.cassandra.db.memtable.TrieMemtableFactory; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.StorageCompatibilityMode; /** * Memtable types and options are specified with these parameters. Memtable classes must either contain a static @@ -50,7 +58,8 @@ */ public final class MemtableParams { - private final Memtable.Factory factory; + private static final Logger logger = LoggerFactory.getLogger(MemtableParams.class); + public final Memtable.Factory factory; private final String configurationKey; private MemtableParams(Memtable.Factory factory, String configurationKey) @@ -69,6 +78,44 @@ public Memtable.Factory factory() return factory; } + /** + * Returns a map representation of the memtable configuration for backward compatibility with CC 4.0. + * This is used when outputting schema in a format compatible with CC 4.0. + * + * For the "default" configuration key, we output an empty map {} to let each Cassandra version + * interpret "default" according to its own configuration. This ensures backward compatibility + * with CC 4.0 which uses an empty map to represent the default memtable configuration. + * + * For other configurations, CC 4.0 accepts both short class names (e.g., 'TrieMemtable') and + * fully qualified names (e.g., 'org.apache.cassandra.db.memtable.TrieMemtable'). For standard + * Cassandra memtables in the org.apache.cassandra.db.memtable package, we use short names and + * for custom memtables from other packages, we preserve the fully qualified class name. + */ + public Map toMapForCC4() + { + if ("default".equals(configurationKey)) + return ImmutableMap.of(); + + ParameterizedClass definition = CONFIGURATION_DEFINITIONS.get(configurationKey); + if (definition != null && definition.class_name != null) + { + Map map = new HashMap<>(); + String className = definition.class_name; + + if (className.startsWith("org.apache.cassandra.db.memtable.")) + { + className = className.substring("org.apache.cassandra.db.memtable.".length()); + } + + map.put("class", className); + if (definition.parameters != null) + map.putAll(definition.parameters); + return map; + } + // Fallback for unknown configurations + return ImmutableMap.of("class", configurationKey); + } + @Override public String toString() { @@ -96,8 +143,8 @@ public int hashCode() } private static final String DEFAULT_CONFIGURATION_KEY = "default"; - private static final Memtable.Factory DEFAULT_MEMTABLE_FACTORY = SkipListMemtableFactory.INSTANCE; - private static final ParameterizedClass DEFAULT_CONFIGURATION = SkipListMemtableFactory.CONFIGURATION; + private static final Memtable.Factory DEFAULT_MEMTABLE_FACTORY = TrieMemtableFactory.INSTANCE; + private static final ParameterizedClass DEFAULT_CONFIGURATION = TrieMemtableFactory.CONFIGURATION; private static final Map CONFIGURATION_DEFINITIONS = expandDefinitions(DatabaseDescriptor.getMemtableConfigurations()); private static final Map CONFIGURATIONS = new HashMap<>(); @@ -127,14 +174,25 @@ public static MemtableParams getWithFallback(String key) } catch (ConfigurationException e) { - LoggerFactory.getLogger(MemtableParams.class).error("Invalid memtable configuration \"" + key + "\" in schema. " + - "Falling back to default to avoid schema mismatch.\n" + - "Please ensure the correct definition is given in cassandra.yaml.", - e); + logger.error("Invalid memtable configuration \"" + key + "\" in schema. " + + "Falling back to default to avoid schema mismatch.\n" + + "Please ensure the correct definition is given in cassandra.yaml.", + e); return new MemtableParams(DEFAULT.factory(), key); } } + /** + * Useful for testing where we can provide a factory that produces spied instances of memtable so that we can + * modify behaviour of certains methods. + */ + // Used by CNDB + @VisibleForTesting + public static MemtableParams forTesting(Memtable.Factory factory, String configurationKey) + { + return new MemtableParams(factory, configurationKey); + } + @VisibleForTesting static Map expandDefinitions(Map memtableConfigurations) { @@ -228,19 +286,25 @@ private static Memtable.Factory getMemtableFactory(ParameterizedClass options) try { Memtable.Factory factory; - Class clazz = Class.forName(className); + Class clazz = Class.forName(className, false, MemtableParams.class.getClassLoader()); final Map parametersCopy = options.parameters != null ? new HashMap<>(options.parameters) : new HashMap<>(); try { Method factoryMethod = clazz.getDeclaredMethod("factory", Map.class); + if (!Memtable.Factory.class.isAssignableFrom(factoryMethod.getReturnType())) + throw new ClassCastException("Memtable factory method on " + className + + " must return " + Memtable.Factory.class.getName()); factory = (Memtable.Factory) factoryMethod.invoke(null, parametersCopy); } catch (NoSuchMethodException e) { // continue with FACTORY field Field factoryField = clazz.getDeclaredField("FACTORY"); + if (!Memtable.Factory.class.isAssignableFrom(factoryField.getType())) + throw new ClassCastException("Memtable FACTORY field on " + className + + " must be of type " + Memtable.Factory.class.getName()); factory = (Memtable.Factory) factoryField.get(null); } if (!parametersCopy.isEmpty()) @@ -255,4 +319,243 @@ private static Memtable.Factory getMemtableFactory(ParameterizedClass options) throw new ConfigurationException("Could not create memtable factory for class " + options, e); } } + + /** + * Attempts to read memtable configuration, with fallback for CC4 upgrade compatibility. + * + * CC4 stored memtable as {@code frozen>}, while CC5 uses text. + * During upgrades or in mixed clusters, the column may contain either format. + * + * This method uses byte-sniffing (detecting null bytes) to determine the actual + * format of the stored data. The storage_compatibility_mode determines the format + * to write, but doesn't guarantee the format of existing stored data. + * + * This defensive approach handles: + *

      + *
    • Upgrading from CC4 to CC5 (reads CC4 format, writes CC5 format)
    • + *
    • Running CC5 in HCD_1 mode (reads either format, writes CC4 format)
    • + *
    • Mixed clusters during rolling upgrades (reads both formats)
    • + *
    + * + * @param row The row containing the memtable column + * @param columnName The name of the memtable column + * @return MemtableParams instance, or DEFAULT if column is missing or invalid + */ + public static MemtableParams getWithCC4Fallback(UntypedResultSet.Row row, String columnName) + { + if (!row.has(columnName)) + return DEFAULT; + + String stringValue; + try + { + stringValue = row.getString(columnName); + } + catch (MarshalException e) + { + // CC4 map data may not be valid UTF-8, fall back to map parsing + return parseCC4MapFormat(row, columnName); + } + + // Check if this looks like binary data (contains null bytes from CC4's map serialization) + if (stringValue != null && stringValue.indexOf('\0') >= 0) + { + return parseCC4MapFormat(row, columnName); + } + + // Normal CC5 string value + return getWithFallback(stringValue); + } + + private static MemtableParams parseCC4MapFormat(UntypedResultSet.Row row, String columnName) + { + // This is likely CC4's frozen> serialization + // Try to read it as a map instead + try + { + ByteBuffer raw = row.getBytes(columnName); + Map cc4Map = MapType.getInstance(UTF8Type.instance, UTF8Type.instance, false).compose(raw); + + if (cc4Map == null || cc4Map.isEmpty()) + { + // Empty map in CC4 means "default" + logger.info("Detected CC4 empty memtable map for upgrade compatibility, using default"); + return DEFAULT; + } + + // Convert CC4 map format to CC5 configuration key + String className = cc4Map.get("class"); + if (className != null) + { + // CC4 used class names like "SkipListMemtable" or "TrieMemtable" + // Try to map to CC5 configuration keys + String configKey = mapCC4ClassNameToCC5Key(className); + logger.info("Detected CC4 memtable configuration '{}', mapped to CC5 key '{}'", + className, configKey); + return getWithFallback(configKey); + } + else + { + // CC4 map exists but has no "class" key - likely corrupted data + logger.warn("Detected CC4 memtable map without 'class' key, falling back to default"); + return DEFAULT; + } + } + catch (Exception e) + { + logger.warn("Failed to parse memtable column as CC4 map format, falling back to default", e); + return DEFAULT; + } + } + + private static String mapCC4ClassNameToCC5Key(String cc4ClassName) + { + // Handle both short names and fully qualified names + String shortName = cc4ClassName.contains(".") + ? cc4ClassName.substring(cc4ClassName.lastIndexOf('.') + 1) + : cc4ClassName; + + // Map common CC4 class names to CC5 configuration keys + switch (shortName) + { + case "SkipListMemtable": + return "skiplist"; + case "TrieMemtable": + return "trie"; + default: + // For unknown types, try the short name as-is + logger.warn("Unknown CC4 memtable class '{}', attempting to use as configuration key", shortName); + return shortName.toLowerCase(); + } + } + + /** + * Returns the memtable value as a map for CC4 compatibility mode. + * Used when storage_compatibility_mode is HCD_1 or CASSANDRA_4. + * + * @return Map representation for CC4 schema (frozen<map<text,text>>) + * @throws ConfigurationException if the memtable type is not compatible with CC4 + */ + public Map asSchemaValueMap() + { + return asSchemaValueMap(DatabaseDescriptor.getStorageCompatibilityMode()); + } + + /** + * Returns the memtable value as a map for CC4 compatibility mode. + * This overload exists for testing purposes. + * + * @param mode The storage compatibility mode to use + * @return Map representation for CC4 schema (frozen<map<text,text>>) + * @throws ConfigurationException if the memtable type is not compatible with CC4 + */ + @VisibleForTesting + Map asSchemaValueMap(StorageCompatibilityMode mode) + { + if (!mode.isBefore(CassandraVersion.CASSANDRA_5_0.major)) + throw new IllegalStateException("Cannot get map value in CC5 mode. Use asSchemaValueText() instead."); + + // CC4 writes empty map {} for "default" configuration + if ("default".equals(configurationKey)) + return ImmutableMap.of(); + + // Validate and map the configuration key to CC4 class name + // This also validates CC4 compatibility (rejects sharded types, unknown configs) + String className = mapCC5KeyToCC4ClassName(configurationKey); + + // Get the configuration definition to access parameters + ParameterizedClass definition = CONFIGURATION_DEFINITIONS.get(configurationKey); + + // Build the map with class name and any additional parameters + Map map = new HashMap<>(); + map.put("class", className); + if (definition != null && definition.parameters != null) + map.putAll(definition.parameters); + + return map; + } + + /** + * Returns the memtable value as text for CC5 mode. + * Used when storage_compatibility_mode is NONE. + * + * @return String representation for CC5 schema (text) + */ + public String asSchemaValueText() + { + return asSchemaValueText(DatabaseDescriptor.getStorageCompatibilityMode()); + } + + /** + * Returns the memtable value as text for CC5 mode. + * This overload exists for testing purposes. + * + * @param mode The storage compatibility mode to use + * @return String representation for CC5 schema (text) + * @throws IllegalStateException if called in CC4 compatibility mode + */ + @VisibleForTesting + String asSchemaValueText(StorageCompatibilityMode mode) + { + if (mode.isBefore(CassandraVersion.CASSANDRA_5_0.major)) + throw new IllegalStateException("Cannot get text value in CC4 compatibility mode. Use asSchemaValueMap() instead."); + + return configurationKey; + } + + /** + * Maps CC5 configuration key to CC4 class name, validating CC4 compatibility. + * This method combines validation and mapping for use in CC4 compatibility mode. + * + * @param configKey The CC5 configuration key (e.g., "trie", "skiplist") + * @return The corresponding CC4 class name (e.g., "TrieMemtable") + * @throws ConfigurationException if the configuration is not compatible with CC4 + */ + private static String mapCC5KeyToCC4ClassName(String configKey) + { + if (configKey == null || configKey.isEmpty()) + throw new ConfigurationException("Configuration key cannot be null or empty"); + + // Check if this is a CC5-only memtable type + // ShardedSkipListMemtable and related sharded types don't exist in CC4 + String lowerKey = configKey.toLowerCase(); + if (lowerKey.contains("sharded")) + { + throw new ConfigurationException( + String.format("Memtable configuration '%s' is not compatible with CC4. " + + "Sharded memtable types were introduced in CC5. " + + "Please use 'skiplist' or 'trie' when storage_compatibility_mode is HCD_1 or CASSANDRA_4.", + configKey)); + } + + // Check if the configuration key exists in CONFIGURATION_DEFINITIONS + // This ensures we're not trying to write an unknown/invalid configuration + ParameterizedClass definition = CONFIGURATION_DEFINITIONS.get(configKey); + if (definition == null) + { + throw new ConfigurationException( + String.format("Memtable configuration '%s' not found in cassandra.yaml. " + + "Cannot write to schema in CC4 compatibility mode.", + configKey)); + } + + // Get the class name from the definition and strip the package prefix + // CC4 accepts both short names (e.g., 'TrieMemtable') and fully qualified names, + // but we use short names for standard Cassandra memtables + String className = definition.class_name; + if (className == null || className.isEmpty()) + { + throw new ConfigurationException( + String.format("Memtable configuration '%s' has no class name defined.", + configKey)); + } + + // Strip the standard Cassandra memtable package prefix + if (className.startsWith("org.apache.cassandra.db.memtable.")) + { + className = className.substring("org.apache.cassandra.db.memtable.".length()); + } + + return className; + } } diff --git a/src/java/org/apache/cassandra/schema/MigrationCoordinator.java b/src/java/org/apache/cassandra/schema/MigrationCoordinator.java index 980f3c217056..86178abdbe47 100644 --- a/src/java/org/apache/cassandra/schema/MigrationCoordinator.java +++ b/src/java/org/apache/cassandra/schema/MigrationCoordinator.java @@ -49,9 +49,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.concurrent.FutureTask; import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.gms.ApplicationState; @@ -65,6 +66,7 @@ import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Pair; @@ -229,7 +231,7 @@ public String toString() private final BiConsumer> schemaUpdateCallback; private final Set lastPullFailures = new HashSet<>(); - final ExecutorPlus executor; + final Stage executor; /** * Creates but does not start migration coordinator instance. @@ -238,7 +240,7 @@ public String toString() * @param periodicCheckExecutor executor on which the periodic checks are scheduled */ MigrationCoordinator(MessagingService messagingService, - ExecutorPlus executor, + Stage executor, ScheduledExecutorService periodicCheckExecutor, int maxOutstandingVersionRequests, Gossiper gossiper, @@ -387,6 +389,11 @@ private boolean shouldPullFromEndpoint(InetAddressAndPort endpoint) if (messagingService.versions.getRaw(endpoint) != MessagingService.current_version) { + if (DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major) && messagingService.versions.getRaw(endpoint) == MessagingService.VERSION_50) + { + logger.debug("Allowing schema pull from {} because we are in {} compatibility mode", endpoint, DatabaseDescriptor.getStorageCompatibilityMode()); + return true; + } logger.debug("Not pulling schema from {} because their schema format is incompatible", endpoint); return false; } diff --git a/src/java/org/apache/cassandra/schema/ReplicationParams.java b/src/java/org/apache/cassandra/schema/ReplicationParams.java index 2998aa57ada8..2a75d1da2c91 100644 --- a/src/java/org/apache/cassandra/schema/ReplicationParams.java +++ b/src/java/org/apache/cassandra/schema/ReplicationParams.java @@ -58,6 +58,11 @@ static ReplicationParams simple(String replicationFactor) return new ReplicationParams(SimpleStrategy.class, ImmutableMap.of("replication_factor", replicationFactor)); } + static ReplicationParams everywhere() + { + return new ReplicationParams(EverywhereStrategy.class, ImmutableMap.of()); + } + static ReplicationParams nts(Object... args) { assert args.length % 2 == 0; @@ -74,7 +79,7 @@ static ReplicationParams nts(Object... args) public void validate(String name, ClientState state) { // Attempt to instantiate the ARS, which will throw a ConfigurationException if the options aren't valid. - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); + TokenMetadata tmd = StorageService.instance.getTokenMetadataForKeyspace(name); IEndpointSnitch eps = DatabaseDescriptor.getEndpointSnitch(); AbstractReplicationStrategy.validateReplicationStrategy(name, klass, tmd, eps, options, state); } diff --git a/src/java/org/apache/cassandra/schema/Schema.java b/src/java/org/apache/cassandra/schema/Schema.java index b704fd299700..e9f2b34cce36 100644 --- a/src/java/org/apache/cassandra/schema/Schema.java +++ b/src/java/org/apache/cassandra/schema/Schema.java @@ -52,6 +52,7 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.exceptions.UnknownKeyspaceException; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.LocalStrategy; @@ -93,7 +94,7 @@ public class Schema implements SchemaProvider private final Keyspaces localKeyspaces; - private volatile TableMetadataRefCache tableMetadataRefCache = TableMetadataRefCache.EMPTY; + private TableMetadataRefCache tableMetadataRefCache = TableMetadataRefCache.EMPTY; // Keyspace objects, one per keyspace. Only one instance should ever exist for any given keyspace. // We operate on loading map because we need to achieve atomic initialization with at-most-once semantics for @@ -161,7 +162,7 @@ public void loadFromDisk() * * @param ksm The metadata about keyspace */ - private synchronized void load(KeyspaceMetadata ksm) + public synchronized void load(KeyspaceMetadata ksm) { Preconditions.checkArgument(!SchemaConstants.isLocalSystemKeyspace(ksm.name)); KeyspaceMetadata previous = distributedKeyspaces.getNullable(ksm.name); @@ -212,8 +213,7 @@ public void unregisterListener(SchemaChangeListener listener) * Get keyspace instance by name * * @param keyspaceName The name of the keyspace - * - * @return Keyspace object or null if keyspace was not found + * @return Keyspace object or null if keyspace was not found, or if the keyspace has not completed construction yet */ @Override public Keyspace getKeyspaceInstance(String keyspaceName) @@ -243,12 +243,13 @@ public ColumnFamilyStore getColumnFamilyStoreInstance(TableId id) } @Override - public Keyspace maybeAddKeyspaceInstance(String keyspaceName, Supplier loadFunction) + public Keyspace maybeAddKeyspaceInstance(String keyspaceName, Supplier loadFunction) throws UnknownKeyspaceException { return keyspaceInstances.blockingLoadIfAbsent(keyspaceName, loadFunction); } - private Keyspace maybeRemoveKeyspaceInstance(String keyspaceName, Consumer unloadFunction) + // Used by CNDB + public Keyspace maybeRemoveKeyspaceInstance(String keyspaceName, Consumer unloadFunction) { try { @@ -323,6 +324,14 @@ public KeyspaceMetadata getKeyspaceMetadata(String keyspaceName) return null != ksm ? ksm : VirtualKeyspaceRegistry.instance.getKeyspaceMetadataNullable(keyspaceName); } + /** + * Returns keyspaces that partition data across the ring. + */ + public Keyspaces getPartitionedKeyspaces() + { + return distributedKeyspaces.filter(keyspace -> Keyspace.open(keyspace.name, this, true).getReplicationStrategy().isPartitioned()); + } + /** * Returns user keyspaces, that is all but {@link SchemaConstants#LOCAL_SYSTEM_KEYSPACE_NAMES}, * {@link SchemaConstants#REPLICATED_SYSTEM_KEYSPACE_NAMES} or virtual keyspaces. @@ -595,7 +604,8 @@ public void reloadSchemaAndAnnounceVersion() public synchronized void mergeAndUpdateVersion(SchemaTransformationResult result, boolean dropData) { result = localDiff(result); - assert result.after.getKeyspaces().stream().noneMatch(ksm -> ksm.params.replication.klass == LocalStrategy.class) : "LocalStrategy should not be used"; + assert CassandraRelevantProperties.TEST_ALLOW_LOCAL_STRATEGY.getBoolean() + || result.after.getKeyspaces().stream().noneMatch(ksm -> ksm.params.replication.klass == LocalStrategy.class) : "LocalStrategy should not be used"; schemaChangeNotifier.notifyPreChanges(result); merge(result.diff, dropData); updateVersion(result.after.getVersion()); @@ -689,9 +699,10 @@ private void createKeyspace(KeyspaceMetadata keyspace) { SchemaDiagnostics.keyspaceCreating(this, keyspace); load(keyspace); + Keyspace instance = null; if (Keyspace.isInitialized()) { - Keyspace.open(keyspace.name, this, true); + instance = Keyspace.open(keyspace.name, this, true); } schemaChangeNotifier.notifyKeyspaceCreated(keyspace); @@ -699,9 +710,9 @@ private void createKeyspace(KeyspaceMetadata keyspace) // If keyspace has been added, we need to recalculate pending ranges to make sure // we send mutations to the correct set of bootstrapping nodes. Refer CASSANDRA-15433. - if (keyspace.params.replication.klass != LocalStrategy.class && Keyspace.isInitialized()) + if (keyspace.params.replication.klass != LocalStrategy.class && instance != null) { - PendingRangeCalculatorService.calculatePendingRanges(Keyspace.open(keyspace.name, this, true).getReplicationStrategy(), keyspace.name); + PendingRangeCalculatorService.instance.calculatePendingRanges(instance.getReplicationStrategy(), keyspace.name); } } @@ -716,6 +727,7 @@ private void dropKeyspace(KeyspaceMetadata keyspaceMetadata, boolean dropData) if (keyspace == null) return; + logger.debug("Dropping keyspace {}", keyspaceMetadata.name); keyspaceMetadata.views.forEach(v -> dropView(keyspace, v, dropData)); keyspaceMetadata.tables.forEach(t -> dropTable(keyspace, t, dropData)); @@ -723,9 +735,11 @@ private void dropKeyspace(KeyspaceMetadata keyspaceMetadata, boolean dropData) Keyspace unloadedKeyspace = maybeRemoveKeyspaceInstance(keyspaceMetadata.name, ks -> { ks.unload(dropData); unload(keyspaceMetadata); + logger.debug("Instance removed for keyspace {}", ks.getName()); }); assert unloadedKeyspace == keyspace; + logger.debug("Awaiting on write barrier before dropping keyspace {}", keyspaceMetadata.name); Keyspace.writeOrder.awaitNewBarrier(); } else @@ -785,4 +799,26 @@ public Map> getOutstandingSchemaVersions() : Collections.emptyMap(); } -} \ No newline at end of file + /** + * @return whether or not the keyspace is a really system one (w/ LocalStrategy, unmodifiable, hardcoded) + * or it's having {@link LocalStrategy} + */ + public static boolean isKeyspaceWithLocalStrategy(String keyspaceName) + { + KeyspaceMetadata ksm = instance.getKeyspaceMetadata(keyspaceName); + return SchemaConstants.isLocalSystemKeyspace(keyspaceName) || + (ksm != null && ksm.params.replication.klass.equals(LocalStrategy.class)); + } + + /** + * Equivalent to {@link #isKeyspaceWithLocalStrategy(String)} but uses the provided keyspace metadata instead + * of getting the metadata from the schema manager + * + * @param keyspace the keyspace metadata to check + * @return if the provided keyspace uses local replication strategy + */ + public static boolean isKeyspaceWithLocalStrategy(KeyspaceMetadata keyspace) + { + return SchemaConstants.isLocalSystemKeyspace(keyspace.name) || keyspace.params.replication.klass.equals(LocalStrategy.class); + } +} diff --git a/src/java/org/apache/cassandra/schema/SchemaConstants.java b/src/java/org/apache/cassandra/schema/SchemaConstants.java index ff990f7cbb06..ed1385c41b24 100644 --- a/src/java/org/apache/cassandra/schema/SchemaConstants.java +++ b/src/java/org/apache/cassandra/schema/SchemaConstants.java @@ -56,28 +56,31 @@ public final class SchemaConstants public static final String DUMMY_KEYSPACE_OR_TABLE_NAME = "--dummy--"; /* system keyspace names (the ones with LocalStrategy replication strategy) */ - public static final Set LOCAL_SYSTEM_KEYSPACE_NAMES = - ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME); + public static final Set LOCAL_SYSTEM_KEYSPACE_NAMES = ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME); /* virtual table system keyspace names */ public static final Set VIRTUAL_SYSTEM_KEYSPACE_NAMES = ImmutableSet.of(VIRTUAL_VIEWS, VIRTUAL_SCHEMA); /* replicate system keyspace names (the ones with a "true" replication strategy) */ - public static final Set REPLICATED_SYSTEM_KEYSPACE_NAMES = - ImmutableSet.of(TRACE_KEYSPACE_NAME, AUTH_KEYSPACE_NAME, DISTRIBUTED_KEYSPACE_NAME); + public static final Set REPLICATED_SYSTEM_KEYSPACE_NAMES = ImmutableSet.of(TRACE_KEYSPACE_NAME, AUTH_KEYSPACE_NAME, DISTRIBUTED_KEYSPACE_NAME); + /** + * Longest acceptable file name. Longer names lead to file write or read errors. + */ + public static final int FILENAME_LENGTH = 255; + + /** * The longest permissible KS or CF name. * * Before CASSANDRA-16956, we used to care about not having the entire path longer than 255 characters because of * Windows support but this limit is by implementing CASSANDRA-16956 not in effect anymore. + * + * Note: This extended to 222 for CNDB tenant specific keyspaces. + * 222 is maximum filename length of 255 chars minus a separator char and + * 32 chars for table UUID. */ - public static final int NAME_LENGTH = 48; - - /** - * Longest acceptable file name. Longer names lead to too long file name error. - */ - public static final int FILENAME_LENGTH = 255; + public static final int NAME_LENGTH = FILENAME_LENGTH - 32 - 1; /** * Length of a table uuid as a hex string. @@ -90,6 +93,13 @@ public final class SchemaConstants */ public static final int TABLE_NAME_LENGTH = FILENAME_LENGTH - TABLE_UUID_AS_HEX_LENGTH - TABLE_DIRECTORY_NAME_SEPARATOR.length(); + /** Longest permissible index name, so no index can fail on file name error. + * It is based on the most restrictive requirement coming from SAI and calculated by + * {@link org.apache.cassandra.index.sai.disk.format.Version#calculateIndexNameAllowedLength}. + * The exact number is used here, since it will be in user's documentation. + */ + public static final int INDEX_NAME_LENGTH = 182; + // 59adb24e-f3cd-3e02-97f0-5b395827453f public static final UUID emptyVersion; @@ -185,8 +195,22 @@ public static Set getLocalAndReplicatedSystemTableNames() .addAll(SystemKeyspace.TABLE_NAMES) .addAll(SchemaKeyspaceTables.ALL) .addAll(TraceKeyspace.TABLE_NAMES) - .addAll(AuthKeyspace.TABLE_NAMES) - .addAll(SystemDistributedKeyspace.TABLE_NAMES) + .addAll(AuthKeyspace.tableNames()) + .addAll(SystemDistributedKeyspace.getTableNames()) .build(); } + + public static boolean isInternalKeyspace(String keyspaceName) + { + return isLocalSystemKeyspace(keyspaceName) + || isReplicatedSystemKeyspace(keyspaceName); + } + + /** + * @return whether or not the keyspace is a user keyspace + */ + public static boolean isUserKeyspace(String keyspaceName) + { + return !isInternalKeyspace(keyspaceName); + } } diff --git a/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java b/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java index 29243039dae1..79bc5bca1bd2 100644 --- a/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java +++ b/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java @@ -20,11 +20,15 @@ import com.google.common.collect.MapDifference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.diag.DiagnosticEventService; import org.apache.cassandra.schema.SchemaEvent.SchemaEventType; final class SchemaDiagnostics { + private static final Logger logger = LoggerFactory.getLogger(SchemaDiagnostics.class); private static final DiagnosticEventService service = DiagnosticEventService.instance(); private SchemaDiagnostics() @@ -74,6 +78,8 @@ static void keyspaceCreated(Schema schema, KeyspaceMetadata keyspace) static void keyspaceAltering(Schema schema, KeyspaceMetadata.KeyspaceDiff delta) { + if (logger.isTraceEnabled()) + logger.trace("Altering keyspace {}", delta.before.name); if (isEnabled(SchemaEventType.KS_ALTERING)) service.publish(new SchemaEvent(SchemaEventType.KS_ALTERING, schema, delta.after, delta.before, delta, null, null, null, null)); @@ -81,6 +87,8 @@ static void keyspaceAltering(Schema schema, KeyspaceMetadata.KeyspaceDiff delta) static void keyspaceAltered(Schema schema, KeyspaceMetadata.KeyspaceDiff delta) { + if (logger.isTraceEnabled()) + logger.trace("Keyspace {} altered", delta.before.name); if (isEnabled(SchemaEventType.KS_ALTERED)) service.publish(new SchemaEvent(SchemaEventType.KS_ALTERED, schema, delta.after, delta.before, delta, null, null, null, null)); @@ -95,6 +103,8 @@ static void keyspaceDropping(Schema schema, KeyspaceMetadata keyspace) static void keyspaceDropped(Schema schema, KeyspaceMetadata keyspace) { + if (logger.isTraceEnabled()) + logger.trace("Keyspace {} dropped", keyspace.name); if (isEnabled(SchemaEventType.KS_DROPPED)) service.publish(new SchemaEvent(SchemaEventType.KS_DROPPED, schema, keyspace, null, null, null, null, null, null)); @@ -158,6 +168,8 @@ static void tableAltered(Schema schema, TableMetadata table) static void tableDropping(Schema schema, TableMetadata table) { + if (logger.isTraceEnabled()) + logger.trace("Dropping table {}", table); if (isEnabled(SchemaEventType.TABLE_DROPPING)) service.publish(new SchemaEvent(SchemaEventType.TABLE_DROPPING, schema, null, null, null, table, null, null, null)); diff --git a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java index f7044b55f7fa..a0797aa65443 100644 --- a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java +++ b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java @@ -32,9 +32,20 @@ import org.slf4j.LoggerFactory; import org.antlr.runtime.RecognitionException; -import org.apache.cassandra.config.*; -import org.apache.cassandra.cql3.*; -import org.apache.cassandra.cql3.functions.*; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.FieldIdentifier; +import org.apache.cassandra.cql3.Terms; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.cql3.WhereClause; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.cql3.functions.FunctionResolver; +import org.apache.cassandra.cql3.functions.ScalarFunction; +import org.apache.cassandra.cql3.functions.UDAggregate; +import org.apache.cassandra.cql3.functions.UDFunction; +import org.apache.cassandra.cql3.functions.UserFunction; import org.apache.cassandra.cql3.functions.masking.ColumnMask; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.db.*; @@ -48,6 +59,7 @@ import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Simulate; @@ -55,12 +67,23 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; - +import org.apache.cassandra.config.CassandraRelevantProperties; import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORE_CORRUPTED_SCHEMA_TABLES; -import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_FLUSH_LOCAL_SCHEMA_CHANGES; +import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYSTEM; import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal; -import static org.apache.cassandra.schema.SchemaKeyspaceTables.*; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.AGGREGATES; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.ALL; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.COLUMNS; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.COLUMN_MASKS; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.DROPPED_COLUMNS; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.FUNCTIONS; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.INDEXES; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.KEYSPACES; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.TABLES; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.TRIGGERS; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.TYPES; +import static org.apache.cassandra.schema.SchemaKeyspaceTables.VIEWS; import static org.apache.cassandra.utils.Simulate.With.GLOBAL_CLOCK; /** @@ -78,7 +101,6 @@ private SchemaKeyspace() private static final Logger logger = LoggerFactory.getLogger(SchemaKeyspace.class); - private static final boolean FLUSH_SCHEMA_TABLES = TEST_FLUSH_LOCAL_SCHEMA_CHANGES.getBoolean(); private static final boolean IGNORE_CORRUPTED_SCHEMA_TABLES_PROPERTY_VALUE = IGNORE_CORRUPTED_SCHEMA_TABLES.getBoolean(); /** @@ -94,8 +116,47 @@ private SchemaKeyspace() + "keyspace_name text," + "durable_writes boolean," + "replication frozen>," + + "graph_engine text," + "PRIMARY KEY ((keyspace_name)))"); + // CC4-compatible schema with memtable as frozen> + // Used when storage_compatibility_mode is HCD_1 to support downgrade to CC4 + private static final TableMetadata TablesLegacy = + parse(TABLES, + "table definitions", + "CREATE TABLE %s (" + + "keyspace_name text," + + "table_name text," + + "allow_auto_snapshot boolean," + + "bloom_filter_fp_chance double," + + "caching frozen>," + + "comment text," + + "compaction frozen>," + + "compression frozen>," + + "memtable frozen>," // CC4 format for downgrade compatibility + + "crc_check_chance double," + + "dclocal_read_repair_chance double," // no longer used, left for drivers' sake + + "default_time_to_live int," + + "extensions frozen>," + + "flags frozen>," // SUPER, COUNTER, DENSE, COMPOUND + + "gc_grace_seconds int," + + "incremental_backups boolean," + + "id uuid," + + "max_index_interval int," + + "memtable_flush_period_in_ms int," + + "min_index_interval int," + + "nodesync frozen>," + + "read_repair_chance double," // no longer used, left for drivers' sake + + "speculative_retry text," + + "additional_write_policy text," + + "cdc boolean," + + "read_repair text," + + "PRIMARY KEY ((keyspace_name), table_name))"); + + // CC5 schema with memtable as text + // Used when storage_compatibility_mode is NONE (no downgrade support) + // auto_repair column is only included if AUTOREPAIR_ENABLE is true to avoid schema disagreement + // with pre-5.0.8 nodes that don't have this column in their system_schema.tables definition private static final TableMetadata Tables = parse(TABLES, "table definitions", @@ -108,7 +169,7 @@ private SchemaKeyspace() + "comment text," + "compaction frozen>," + "compression frozen>," - + "memtable text," + + "memtable text," // CC5 format + "crc_check_chance double," + "dclocal_read_repair_chance double," // no longer used, left for drivers' sake + "default_time_to_live int," @@ -120,11 +181,13 @@ private SchemaKeyspace() + "max_index_interval int," + "memtable_flush_period_in_ms int," + "min_index_interval int," + + "nodesync frozen>," + "read_repair_chance double," // no longer used, left for drivers' sake + "speculative_retry text," + "additional_write_policy text," + "cdc boolean," + "read_repair text," + + (CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean() ? "auto_repair frozen>," : "") + "PRIMARY KEY ((keyspace_name), table_name))"); private static final TableMetadata Columns = @@ -139,6 +202,7 @@ private SchemaKeyspace() + "kind text," + "position int," + "type text," + + "required_for_liveness boolean," + "PRIMARY KEY ((keyspace_name), table_name, column_name))"); private static final TableMetadata ColumnMasks = @@ -177,6 +241,46 @@ private SchemaKeyspace() + "options frozen>," + "PRIMARY KEY ((keyspace_name), table_name, trigger_name))"); + // CC4-compatible schema with memtable as frozen> + private static final TableMetadata ViewsLegacy = + parse(VIEWS, + "view definitions", + "CREATE TABLE %s (" + + "keyspace_name text," + + "view_name text," + + "base_table_id uuid," + + "base_table_name text," + + "where_clause text," + + "allow_auto_snapshot boolean," + + "bloom_filter_fp_chance double," + + "caching frozen>," + + "comment text," + + "compaction frozen>," + + "compression frozen>," + + "memtable frozen>," // CC4 format for downgrade compatibility + + "crc_check_chance double," + + "dclocal_read_repair_chance double," // no longer used, left for drivers' sake + + "default_time_to_live int," + + "extensions frozen>," + + "gc_grace_seconds int," + + "incremental_backups boolean," + + "id uuid," + + "include_all_columns boolean," + + "max_index_interval int," + + "memtable_flush_period_in_ms int," + + "min_index_interval int," + + "nodesync frozen>," + + "read_repair_chance double," // no longer used, left for drivers' sake + + "speculative_retry text," + + "additional_write_policy text," + + "cdc boolean," + + "version int," + + "read_repair text," + + "PRIMARY KEY ((keyspace_name), view_name))"); + + // CC5 schema with memtable as text + // auto_repair column is only included if AUTOREPAIR_ENABLE is true to avoid schema disagreement + // with pre-5.0.8 nodes that don't have this column in their system_schema.views definition private static final TableMetadata Views = parse(VIEWS, "view definitions", @@ -192,7 +296,7 @@ private SchemaKeyspace() + "comment text," + "compaction frozen>," + "compression frozen>," - + "memtable text," + + "memtable text," // CC5 format + "crc_check_chance double," + "dclocal_read_repair_chance double," // no longer used, left for drivers' sake + "default_time_to_live int," @@ -204,11 +308,14 @@ private SchemaKeyspace() + "max_index_interval int," + "memtable_flush_period_in_ms int," + "min_index_interval int," + + "nodesync frozen>," + "read_repair_chance double," // no longer used, left for drivers' sake + "speculative_retry text," + "additional_write_policy text," + "cdc boolean," + + "version int," + "read_repair text," + + (CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean() ? "auto_repair frozen>," : "") + "PRIMARY KEY ((keyspace_name), view_name))"); private static final TableMetadata Indexes = @@ -244,6 +351,9 @@ private SchemaKeyspace() + "language text," + "return_type text," + "called_on_null_input boolean," + + "deterministic boolean," + + "monotonic boolean," + + "monotonic_on frozen>," + "PRIMARY KEY ((keyspace_name), function_name, argument_types))"); private static final TableMetadata Aggregates = @@ -258,19 +368,49 @@ private SchemaKeyspace() + "return_type text," + "state_func text," + "state_type text," + + "deterministic boolean," + "PRIMARY KEY ((keyspace_name), aggregate_name, argument_types))"); - private static final List ALL_TABLE_METADATA = ImmutableList.of(Keyspaces, - Tables, - Columns, - ColumnMasks, - Triggers, - DroppedColumns, - Views, - Types, - Functions, - Aggregates, - Indexes); + /** + * Returns the list of schema table metadata based on current storage compatibility mode. + */ + private static List allTableMetadata() + { + return ImmutableList.of(Keyspaces, + // Use legacy schema (frozen) when in HCD_1 compatibility mode to support downgrade + tablesTableMetadata(), + Columns, + ColumnMasks, + Triggers, + DroppedColumns, + viewsTableMetadata(), + Types, + Functions, + Aggregates, + Indexes); + } + + /** + * Returns the appropriate Tables schema table metadata based on current storage compatibility mode. + * Uses TablesLegacy ({@code frozen} for memtable) in CC4 mode, Tables (text for memtable) otherwise. + */ + private static TableMetadata tablesTableMetadata() + { + return DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major) + ? TablesLegacy + : Tables; + } + + /** + * Returns the appropriate Views schema table metadata based on current storage compatibility mode. + * Uses ViewsLegacy ({@code frozen} for memtable) in CC4 mode, Views (text for memtable) otherwise. + */ + private static TableMetadata viewsTableMetadata() + { + return DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major) + ? ViewsLegacy + : Views; + } private static TableMetadata parse(String name, String description, String cql) { @@ -284,7 +424,7 @@ private static TableMetadata parse(String name, String description, String cql) public static KeyspaceMetadata metadata() { - return KeyspaceMetadata.create(SchemaConstants.SCHEMA_KEYSPACE_NAME, KeyspaceParams.local(), org.apache.cassandra.schema.Tables.of(ALL_TABLE_METADATA)); + return KeyspaceMetadata.create(SchemaConstants.SCHEMA_KEYSPACE_NAME, KeyspaceParams.local(), org.apache.cassandra.schema.Tables.of(allTableMetadata())); } static Collection convertSchemaDiffToMutations(KeyspacesDiff diff, long timestamp) @@ -357,7 +497,7 @@ static void truncate() private static void flush() { - if (!DatabaseDescriptor.isUnsafeSystem()) + if (!UNSAFE_SYSTEM.getBoolean()) ALL.forEach(table -> FBUtilities.waitOnFuture(getSchemaCFS(table).forceFlush(ColumnFamilyStore.FlushReason.INTERNALLY_FORCED))); } @@ -509,7 +649,7 @@ private static Mutation.SimpleBuilder makeDropKeyspaceMutation(KeyspaceMetadata Mutation.SimpleBuilder builder = Mutation.simpleBuilder(SchemaConstants.SCHEMA_KEYSPACE_NAME, decorate(Keyspaces, keyspace.name)) .timestamp(timestamp); - for (TableMetadata schemaTable : ALL_TABLE_METADATA) + for (TableMetadata schemaTable : allTableMetadata()) builder.update(schemaTable).delete(); return builder; @@ -520,7 +660,7 @@ private static void addTypeToSchemaMutation(UserType type, Mutation.SimpleBuilde mutation.update(Types) .row(type.getNameAsString()) .add("field_names", type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList())) - .add("field_types", type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList())); + .add("field_types", type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toSchemaString).collect(toList())); } private static void addDropTypeToSchemaMutation(UserType type, Mutation.SimpleBuilder builder) @@ -539,7 +679,7 @@ static Mutation.SimpleBuilder makeCreateTableMutation(KeyspaceMetadata keyspace, private static void addTableToSchemaMutation(TableMetadata table, boolean withColumnsAndTriggers, Mutation.SimpleBuilder builder) { - Row.SimpleBuilder rowBuilder = builder.update(Tables) + Row.SimpleBuilder rowBuilder = builder.update(tablesTableMetadata()) .row(table.name) .deletePrevious() .add("id", table.id.asUUID()) @@ -563,7 +703,12 @@ private static void addTableToSchemaMutation(TableMetadata table, boolean withCo } } - private static void addTableParamsToRowBuilder(TableParams params, Row.SimpleBuilder builder) + public static void addTableParamsToRowBuilder(TableParams params, Row.SimpleBuilder builder) + { + addTableParamsToRowBuilder(params, tablesTableMetadata(), builder); + } + + private static void addTableParamsToRowBuilder(TableParams params, TableMetadata schemaTable, Row.SimpleBuilder builder) { builder.add("bloom_filter_fp_chance", params.bloomFilterFpChance) .add("comment", params.comment) @@ -590,8 +735,14 @@ private static void addTableParamsToRowBuilder(TableParams params, Row.SimpleBui // As above, only add the memtable column if the table uses a non-default memtable configuration to avoid RTE // in mixed operation with pre-4.1 versioned node during upgrades. + // Write in CC4 format (map) or CC5 format (text) based on storage compatibility mode if (params.memtable != MemtableParams.DEFAULT) - builder.add("memtable", params.memtable.configurationKey()); + { + if (DatabaseDescriptor.getStorageCompatibilityMode().isBefore(CassandraVersion.CASSANDRA_5_0.major)) + builder.add("memtable", params.memtable.asSchemaValueMap()); + else + builder.add("memtable", params.memtable.asSchemaValueText()); + } // As above, only add the allow_auto_snapshot column if the value is not default (true) and // auto-snapshotting is enabled, to avoid RTE in pre-4.2 versioned node during upgrades @@ -602,6 +753,17 @@ private static void addTableParamsToRowBuilder(TableParams params, Row.SimpleBui // incremental_backups is enabled, to avoid RTE in pre-4.2 versioned node during upgrades if (!params.incrementalBackups) builder.add("incremental_backups", false); + + // Only add auto_repair column if: + // 1. The column exists in the schema (depends on AUTOREPAIR_ENABLE at class load time) + // 2. The scheduler is enabled (which includes AUTOREPAIR_ENABLE check) + // to avoid RTE in pre-5.1 versioned node during upgrades + if (schemaTable.getColumn(ByteBufferUtil.bytes("auto_repair")) != null + && DatabaseDescriptor.getRawConfig() != null + && DatabaseDescriptor.getAutoRepairConfig().isAutoRepairSchedulingEnabled()) + { + builder.add("auto_repair", params.autoRepair.asMap()); + } } private static void addAlterTableToSchemaMutation(TableMetadata oldTable, TableMetadata newTable, Mutation.SimpleBuilder builder) @@ -694,7 +856,7 @@ private static MapDifference triggersDiff(Triggers befo private static void addDropTableToSchemaMutation(TableMetadata table, Mutation.SimpleBuilder builder) { - builder.update(Tables).row(table.name).delete(); + builder.update(tablesTableMetadata()).row(table.name).delete(); for (ColumnMetadata column : table.columns()) dropColumnFromSchemaMutation(table, column, builder); @@ -711,9 +873,7 @@ private static void addDropTableToSchemaMutation(TableMetadata table, Mutation.S private static void addColumnToSchemaMutation(TableMetadata table, ColumnMetadata column, Mutation.SimpleBuilder builder) { - AbstractType type = column.type; - if (type instanceof ReversedType) - type = ((ReversedType) type).baseType; + AbstractType type = column.type.unwrap(); builder.update(Columns) .row(table.name, column.name.toString()) @@ -721,7 +881,7 @@ private static void addColumnToSchemaMutation(TableMetadata table, ColumnMetadat .add("kind", column.kind.toString().toLowerCase()) .add("position", column.position()) .add("clustering_order", column.clusteringOrder().toString().toLowerCase()) - .add("type", type.asCQL3Type().toString()); + .add("type", type.asCQL3Type().toSchemaString()); ColumnMask mask = column.getMask(); if (SchemaConstants.isReplicatedSystemKeyspace(table.keyspace)) @@ -753,7 +913,7 @@ private static void addColumnToSchemaMutation(TableMetadata table, ColumnMetadat for (int i = 0; i < numArgs; i++) { AbstractType argType = partialTypes.get(i); - types.add(argType.asCQL3Type().toString()); + types.add(argType.asCQL3Type().toSchemaString()); ByteBuffer argValue = partialValues.get(i); boolean isNull = argValue == null; @@ -781,7 +941,7 @@ private static void addDroppedColumnToSchemaMutation(TableMetadata table, Droppe builder.update(DroppedColumns) .row(table.name, column.column.name.toString()) .add("dropped_time", new Date(TimeUnit.MICROSECONDS.toMillis(column.droppedTime))) - .add("type", column.column.type.asCQL3Type().toString()) + .add("type", column.column.type.asCQL3Type().toSchemaString()) .add("kind", column.column.kind.toString().toLowerCase()); } @@ -805,7 +965,7 @@ private static void dropTriggerFromSchemaMutation(TableMetadata table, TriggerMe private static void addViewToSchemaMutation(ViewMetadata view, boolean includeColumns, Mutation.SimpleBuilder builder) { TableMetadata table = view.metadata; - Row.SimpleBuilder rowBuilder = builder.update(Views) + Row.SimpleBuilder rowBuilder = builder.update(viewsTableMetadata()) .row(view.name()) .deletePrevious() .add("include_all_columns", view.includeAllColumns) @@ -814,7 +974,7 @@ private static void addViewToSchemaMutation(ViewMetadata view, boolean includeCo .add("where_clause", view.whereClause.toCQLString()) .add("id", table.id.asUUID()); - addTableParamsToRowBuilder(table.params, rowBuilder); + addTableParamsToRowBuilder(table.params, viewsTableMetadata(), rowBuilder); if (includeColumns) { @@ -828,7 +988,7 @@ private static void addViewToSchemaMutation(ViewMetadata view, boolean includeCo private static void addDropViewToSchemaMutation(ViewMetadata view, Mutation.SimpleBuilder builder) { - builder.update(Views).row(view.name()).delete(); + builder.update(viewsTableMetadata()).row(view.name()).delete(); TableMetadata table = view.metadata; for (ColumnMetadata column : table.columns()) @@ -880,9 +1040,12 @@ private static void addFunctionToSchemaMutation(UDFunction function, Mutation.Si .row(function.name().name, function.argumentsList()) .add("body", function.body()) .add("language", function.language()) - .add("return_type", function.returnType().asCQL3Type().toString()) + .add("return_type", function.returnType().asCQL3Type().toSchemaString()) .add("called_on_null_input", function.isCalledOnNullInput()) - .add("argument_names", function.argNames().stream().map((c) -> bbToString(c.bytes)).collect(toList())); + .add("argument_names", function.argNames().stream().map((c) -> bbToString(c.bytes)).collect(toList())) + .add("deterministic", function.isDeterministic()) + .add("monotonic", function.isMonotonic()) + .add("monotonic_on", function.monotonicOn().stream().map((c) -> bbToString(c.bytes)).collect(toList())); } private static String bbToString(ByteBuffer bb) @@ -906,10 +1069,11 @@ private static void addAggregateToSchemaMutation(UDAggregate aggregate, Mutation { builder.update(Aggregates) .row(aggregate.name().name, aggregate.argumentsList()) - .add("return_type", aggregate.returnType().asCQL3Type().toString()) + .add("return_type", aggregate.returnType().asCQL3Type().toSchemaString()) .add("state_func", aggregate.stateFunction().name().name) - .add("state_type", aggregate.stateType().asCQL3Type().toString()) + .add("state_type", aggregate.stateType().asCQL3Type().toSchemaString()) .add("final_func", aggregate.finalFunction() != null ? aggregate.finalFunction().name().name : null) + .add("deterministic", aggregate.isDeterministic()) .add("initcond", aggregate.initialCondition() != null // must use the frozen state type here, as 'null' for unfrozen collections may mean 'empty' ? aggregate.stateType().freeze().asCQL3Type().toCQLLiteral(aggregate.initialCondition()) @@ -1026,11 +1190,12 @@ private static TableMetadata fetchTable(String keyspaceName, String tableName, T UntypedResultSet.Row row = rows.one(); Set flags = TableMetadata.Flag.fromStringSet(row.getFrozenSet("flags", UTF8Type.instance)); + boolean isCounter = flags.contains(TableMetadata.Flag.COUNTER); return TableMetadata.builder(keyspaceName, tableName, TableId.fromUUID(row.getUUID("id"))) .flags(flags) .params(createTableParamsFromRow(row)) - .addColumns(fetchColumns(keyspaceName, tableName, types, functions)) - .droppedColumns(fetchDroppedColumns(keyspaceName, tableName)) + .addColumns(fetchColumns(keyspaceName, tableName, types, functions, isCounter)) + .droppedColumns(fetchDroppedColumns(keyspaceName, tableName, flags.contains(TableMetadata.Flag.COUNTER))) .indexes(fetchIndexes(keyspaceName, tableName)) .triggers(fetchTriggers(keyspaceName, tableName)) .build(); @@ -1045,9 +1210,8 @@ static TableParams createTableParamsFromRow(UntypedResultSet.Row row) .comment(row.getString("comment")) .compaction(CompactionParams.fromMap(row.getFrozenTextMap("compaction"))) .compression(CompressionParams.fromMap(row.getFrozenTextMap("compression"))) - .memtable(MemtableParams.getWithFallback(row.has("memtable") - ? row.getString("memtable") - : null)) // memtable column was introduced in 4.1 + // Handles CC4 upgrade compatibility + .memtable(MemtableParams.getWithCC4Fallback(row, "memtable")) .defaultTimeToLive(row.getInt("default_time_to_live")) .extensions(row.getFrozenMap("extensions", UTF8Type.instance, BytesType.instance)) .gcGraceSeconds(row.getInt("gc_grace_seconds")) @@ -1070,10 +1234,16 @@ static TableParams createTableParamsFromRow(UntypedResultSet.Row row) if (row.has("incremental_backups")) builder.incrementalBackups(row.getBoolean("incremental_backups")); + // auto_repair column was introduced in 5.0.8 + if (row.has("auto_repair")) + { + builder.automatedRepair(AutoRepairParams.fromMap(row.getFrozenTextMap("auto_repair"))); + } + return builder.build(); } - private static List fetchColumns(String keyspace, String table, Types types, UserFunctions functions) + private static List fetchColumns(String keyspace, String table, Types types, UserFunctions functions, boolean isCounterTable) { String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, COLUMNS); UntypedResultSet columnRows = query(query, keyspace, table); @@ -1081,7 +1251,7 @@ private static List fetchColumns(String keyspace, String table, throw new MissingColumns("Columns not found in schema table for " + keyspace + '.' + table); List columns = new ArrayList<>(); - columnRows.forEach(row -> columns.add(createColumnFromRow(row, types, functions))); + columnRows.forEach(row -> columns.add(createColumnFromRow(row, types, functions, isCounterTable))); if (columns.stream().noneMatch(ColumnMetadata::isPartitionKey)) throw new MissingColumns("No partition key columns found in schema table for " + keyspace + "." + table); @@ -1090,7 +1260,7 @@ private static List fetchColumns(String keyspace, String table, } @VisibleForTesting - public static ColumnMetadata createColumnFromRow(UntypedResultSet.Row row, Types types, UserFunctions functions) + public static ColumnMetadata createColumnFromRow(UntypedResultSet.Row row, Types types, UserFunctions functions, boolean isCounterTable) { String keyspace = row.getString("keyspace_name"); String table = row.getString("table_name"); @@ -1104,7 +1274,10 @@ public static ColumnMetadata createColumnFromRow(UntypedResultSet.Row row, Types if (order == ClusteringOrder.DESC) type = ReversedType.getInstance(type); - ColumnIdentifier name = new ColumnIdentifier(row.getBytes("column_name_bytes"), row.getString("column_name")); + ByteBuffer columnNameBytes = row.getBytes("column_name_bytes"); + type.validateForColumn(columnNameBytes, kind.isPrimaryKeyKind(), isCounterTable, false, false); + + ColumnIdentifier name = new ColumnIdentifier(columnNameBytes, row.getString("column_name")); ColumnMask mask = null; String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ? AND column_name = ?", @@ -1155,19 +1328,19 @@ else if (!(function instanceof ScalarFunction)) return new ColumnMetadata(keyspace, table, name, type, position, kind, mask); } - private static Map fetchDroppedColumns(String keyspace, String table) + private static Map fetchDroppedColumns(String keyspace, String table, boolean isCounterTable) { String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, DROPPED_COLUMNS); Map columns = new HashMap<>(); for (UntypedResultSet.Row row : query(query, keyspace, table)) { - DroppedColumn column = createDroppedColumnFromRow(row); + DroppedColumn column = createDroppedColumnFromRow(row, isCounterTable); columns.put(column.column.name.bytes, column); } return columns; } - private static DroppedColumn createDroppedColumnFromRow(UntypedResultSet.Row row) + private static DroppedColumn createDroppedColumnFromRow(UntypedResultSet.Row row, boolean isCounterTable) { String keyspace = row.getString("keyspace_name"); String table = row.getString("table_name"); @@ -1177,14 +1350,17 @@ private static DroppedColumn createDroppedColumnFromRow(UntypedResultSet.Row row * them anymore), so before storing dropped columns in schema we expand UDTs to tuples. See expandUserTypes method. * Because of that, we can safely pass Types.none() to parse() */ - AbstractType type = CQLTypeParser.parse(keyspace, row.getString("type"), org.apache.cassandra.schema.Types.none()); + AbstractType type = CQLTypeParser.parseDroppedType(keyspace, row.getString("type")); ColumnMetadata.Kind kind = row.has("kind") ? ColumnMetadata.Kind.valueOf(row.getString("kind").toUpperCase()) : ColumnMetadata.Kind.REGULAR; assert kind == ColumnMetadata.Kind.REGULAR || kind == ColumnMetadata.Kind.STATIC : "Unexpected dropped column kind: " + kind; - ColumnMetadata column = new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, ColumnMetadata.NO_POSITION, kind, null); + // Pass isForOfflineTool=true for dropped columns to allow historical types like non-frozen tuples + // that were valid in older SSTable formats but are no longer allowed in current schema + type.validateForColumn(UTF8Type.instance.decompose(name), false, isCounterTable, true, true); + ColumnMetadata column = ColumnMetadata.droppedColumn(keyspace, table, ColumnIdentifier.getInterned(name, true), type, kind, null); long droppedTime = TimeUnit.MILLISECONDS.toMicros(row.getLong("dropped_time")); return new DroppedColumn(column, droppedTime); } @@ -1243,13 +1419,13 @@ private static ViewMetadata fetchView(String keyspaceName, String viewName, Type boolean includeAll = row.getBoolean("include_all_columns"); String whereClauseString = row.getString("where_clause"); - List columns = fetchColumns(keyspaceName, viewName, types, functions); + List columns = fetchColumns(keyspaceName, viewName, types, functions, false); TableMetadata metadata = TableMetadata.builder(keyspaceName, viewName, TableId.fromUUID(row.getUUID("id"))) .kind(TableMetadata.Kind.VIEW) .addColumns(columns) - .droppedColumns(fetchDroppedColumns(keyspaceName, viewName)) + .droppedColumns(fetchDroppedColumns(keyspaceName, viewName, false)) .params(createTableParamsFromRow(row)) .build(); @@ -1304,6 +1480,15 @@ private static UDFunction createUDFFromRow(UntypedResultSet.Row row, Types types String language = row.getString("language"); String body = row.getString("body"); boolean calledOnNullInput = row.getBoolean("called_on_null_input"); + boolean deterministic = row.has("deterministic") && row.getBoolean("deterministic"); + boolean monotonic = row.has("monotonic") && row.getBoolean("monotonic"); + + List monotonicOn = row.has("monotonic_on") + ? row.getFrozenList("monotonic_on", UTF8Type.instance) + .stream() + .map(arg -> new ColumnIdentifier(arg, true)) + .collect(toList()) + : Collections.emptyList(); /* * TODO: find a way to get rid of Schema.instance dependency; evaluate if the opimisation below makes a difference @@ -1332,12 +1517,12 @@ private static UDFunction createUDFFromRow(UntypedResultSet.Row row, Types types try { - return UDFunction.create(name, argNames, argTypes, returnType, calledOnNullInput, language, body); + return UDFunction.create(name, argNames, argTypes, returnType, calledOnNullInput, language, body, deterministic, monotonic, monotonicOn); } catch (InvalidRequestException e) { logger.error(String.format("Cannot load function '%s' from schema: this function won't be available (on this node)", name), e); - return UDFunction.createBrokenFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body, e); + return UDFunction.createBrokenFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body, deterministic, monotonic, monotonicOn, e); } } @@ -1369,8 +1554,9 @@ private static UDAggregate createUDAFromRow(UntypedResultSet.Row row, Collection FunctionName finalFunc = row.has("final_func") ? new FunctionName(ksName, row.getString("final_func")) : null; AbstractType stateType = row.has("state_type") ? CQLTypeParser.parse(ksName, row.getString("state_type"), types) : null; ByteBuffer initcond = row.has("initcond") ? Terms.asBytes(ksName, row.getString("initcond"), stateType) : null; + boolean deterministic = row.has("deterministic") && row.getBoolean("deterministic"); - return UDAggregate.create(functions, name, argTypes, returnType, stateFunc, finalFunc, stateType, initcond); + return UDAggregate.create(functions, name, argTypes, returnType, stateFunc, finalFunc, stateType, initcond, deterministic); } private static UntypedResultSet query(String query, Object... variables) @@ -1396,8 +1582,7 @@ static Set affectedKeyspaces(Collection mutations) static void applyChanges(Collection mutations) { mutations.forEach(Mutation::apply); - if (SchemaKeyspace.FLUSH_SCHEMA_TABLES) - SchemaKeyspace.flush(); + SchemaKeyspace.flush(); } static Keyspaces fetchKeyspaces(Set toFetch) diff --git a/src/java/org/apache/cassandra/schema/SchemaMutationsSerializer.java b/src/java/org/apache/cassandra/schema/SchemaMutationsSerializer.java index ba65c0d0c909..31e3c06adc34 100644 --- a/src/java/org/apache/cassandra/schema/SchemaMutationsSerializer.java +++ b/src/java/org/apache/cassandra/schema/SchemaMutationsSerializer.java @@ -32,6 +32,7 @@ public class SchemaMutationsSerializer implements IVersionedSerializer schema, DataOutputPlus out, int version) throws IOException { out.writeInt(schema.size()); @@ -39,6 +40,7 @@ public void serialize(Collection schema, DataOutputPlus out, int versi Mutation.serializer.serialize(mutation, out, version); } + @Override public Collection deserialize(DataInputPlus in, int version) throws IOException { int count = in.readInt(); @@ -50,9 +52,10 @@ public Collection deserialize(DataInputPlus in, int version) throws IO return schema; } + @Override public long serializedSize(Collection schema, int version) { - int size = TypeSizes.sizeof(schema.size()); + long size = TypeSizes.sizeof(schema.size()); for (Mutation mutation : schema) size += mutation.serializedSize(version); return size; diff --git a/src/java/org/apache/cassandra/schema/SchemaProvider.java b/src/java/org/apache/cassandra/schema/SchemaProvider.java index cbad42e530b7..05e0d09ee8a1 100644 --- a/src/java/org/apache/cassandra/schema/SchemaProvider.java +++ b/src/java/org/apache/cassandra/schema/SchemaProvider.java @@ -19,9 +19,11 @@ package org.apache.cassandra.schema; import java.util.function.Supplier; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.exceptions.UnknownKeyspaceException; import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.io.sstable.Descriptor; @@ -30,7 +32,7 @@ public interface SchemaProvider @Nullable Keyspace getKeyspaceInstance(String keyspaceName); - Keyspace maybeAddKeyspaceInstance(String keyspaceName, Supplier loadFunction); + Keyspace maybeAddKeyspaceInstance(String keyspaceName, Supplier loadFunction) throws UnknownKeyspaceException; @Nullable KeyspaceMetadata getKeyspaceMetadata(String keyspaceName); @@ -41,6 +43,7 @@ public interface SchemaProvider @Nullable TableMetadata getTableMetadata(String keyspace, String table); + @Nonnull default TableMetadata getExistingTableMetadata(TableId id) throws UnknownTableException { TableMetadata metadata = getTableMetadata(id); diff --git a/src/java/org/apache/cassandra/schema/SchemaTransformations.java b/src/java/org/apache/cassandra/schema/SchemaTransformations.java index 124f9a6ef645..6bf97a982d7e 100644 --- a/src/java/org/apache/cassandra/schema/SchemaTransformations.java +++ b/src/java/org/apache/cassandra/schema/SchemaTransformations.java @@ -18,7 +18,13 @@ package org.apache.cassandra.schema; +import java.nio.ByteBuffer; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.AlreadyExistsException; @@ -31,6 +37,8 @@ */ public class SchemaTransformations { + private static final Logger logger = LoggerFactory.getLogger(SchemaTransformations.class); + /** * Creates a schema transformation that adds the provided keyspace. * @@ -113,7 +121,33 @@ public static SchemaTransformation addTypes(Types toAdd, boolean ignoreIfExists) types = types.with(type); } - return schema.withAddedOrReplaced(keyspace.withSwapped(types)); + return schema.withAddedOrUpdated(keyspace.withSwapped(types)); + }; + } + + /** + * Creates a schema transformation that either add the provided type, or "update" (replace really) it to be the + * provided type. + * + *

    Please note that this usually unsafe: if the type exists, this replace it without any particular check + * and so could replace it with an incompatible version. This is used internally however for hard-coded tables + * (System ones, including DSE ones) to force the "last version". + * + * @param type the type to add/update. + * @return the created transformation. + */ + public static SchemaTransformation addOrUpdateType(UserType type) + { + return schema -> + { + KeyspaceMetadata keyspace = schema.getNullable(type.keyspace); + if (null == keyspace) + throw invalidRequest("Keyspace '%s' doesn't exist", type.keyspace); + + Types newTypes = keyspace.types.get(type.name).isPresent() + ? keyspace.types.withUpdatedUserType(type) + : keyspace.types.with(type); + return schema.withAddedOrUpdated(keyspace.withSwapped(newTypes)); }; } @@ -201,8 +235,38 @@ public Keyspaces apply(Keyspaces schema) updatedKeyspace = updatedKeyspace.withSwapped(updatedKeyspace.tables.with(updatedBuilder.build())); } } + + if (curKeyspace.types != null) + { + Set referencedTypes = new HashSet<>(); + for (TableMetadata table : updatedKeyspace.tables) + referencedTypes.addAll(table.getReferencedUserTypes()); + + boolean addedType; + do + { + addedType = false; + for (UserType currType : curKeyspace.types) + { + UserType desiredType = updatedKeyspace.types.getNullable(currType.name); + // Preserve only missing types that are still referenced by preserved tables/columns. + // This avoids changing schema digests by inheriting unrelated legacy system UDTs. + if (desiredType == null && referencedTypes.contains(currType.name)) + { + logger.debug("Preserving type {} for keyspace {}", currType.getNameAsString(), curKeyspace.name); + updatedKeyspace = updatedKeyspace.withSwapped(updatedKeyspace.types.with(currType)); + for (UserType type : curKeyspace.types) + if (currType.referencesUserType(type.name)) + referencedTypes.add(type.name); + addedType = true; + } + } + } + while (addedType); + } + } - return schema.withAddedOrReplaced(updatedKeyspace); + return schema.withAddedOrUpdated(updatedKeyspace); } }; } diff --git a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java b/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java index f324a5d6e682..39545a0254d2 100644 --- a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java +++ b/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java @@ -29,7 +29,8 @@ public interface SchemaUpdateHandlerFactory * different run modes (client, tool, daemon). * * @param online whether schema update handler should work online and be aware of the other nodes (when in daemon mode) - * @param updateSchemaCallback callback which will be called right after the shared schema is updated + * @param updateSchemaCallback callback which will be called right after the shared schema is updated, the args represent + * the schema transformation result and a flag whether the data should be actually removed for the dropped tables */ SchemaUpdateHandler getSchemaUpdateHandler(boolean online, BiConsumer updateSchemaCallback); } diff --git a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java b/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java index 340fea2a33c7..084e0ecd84db 100644 --- a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java +++ b/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java @@ -50,7 +50,8 @@ public SchemaUpdateHandlerFactory get() } else { - Class suhFactoryClass = FBUtilities.classForName(suhFactoryClassName, "schema update handler factory"); + Class suhFactoryClass = + FBUtilities.classForNameWithoutInitialization(suhFactoryClassName, "schema update handler factory", SchemaUpdateHandlerFactory.class); try { return suhFactoryClass.newInstance(); diff --git a/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java b/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java index b36ec64874f3..971e8926f43f 100644 --- a/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java +++ b/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java @@ -58,6 +58,7 @@ import static java.lang.String.format; +import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYSTEM; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; public final class SystemDistributedKeyspace @@ -83,8 +84,9 @@ private SystemDistributedKeyspace() * gen 4: compression chunk length reduced to 16KiB, memtable_flush_period_in_ms now unset on all tables in 4.0 * gen 5: add ttl and TWCS to repair_history tables * gen 6: add denylist table + * gen 7: add auto_repair_history and auto_repair_priority tables for AutoRepair feature */ - public static final long GENERATION = 6; + public static final long GENERATION = CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean() ? 7 : 6; public static final String REPAIR_HISTORY = "repair_history"; @@ -94,8 +96,28 @@ private SystemDistributedKeyspace() public static final String PARTITION_DENYLIST_TABLE = "partition_denylist"; - public static final Set TABLE_NAMES = ImmutableSet.of(REPAIR_HISTORY, PARENT_REPAIR_HISTORY, VIEW_BUILD_STATUS, PARTITION_DENYLIST_TABLE); - + public static final String AUTO_REPAIR_HISTORY = "auto_repair_history"; + + public static final String AUTO_REPAIR_PRIORITY = "auto_repair_priority"; + + private static final Set TABLE_NAMES = ImmutableSet.of(REPAIR_HISTORY, PARENT_REPAIR_HISTORY, + VIEW_BUILD_STATUS, PARTITION_DENYLIST_TABLE); + private static final Set TABLE_NAMES_WITH_AUTO_REPAIR = ImmutableSet.of(REPAIR_HISTORY, PARENT_REPAIR_HISTORY, + VIEW_BUILD_STATUS, PARTITION_DENYLIST_TABLE, + AUTO_REPAIR_HISTORY, AUTO_REPAIR_PRIORITY); + + /** + * Returns the set of table names for the system_distributed keyspace. + * When AUTOREPAIR_ENABLE is false, auto-repair tables are not included. + */ + public static Set getTableNames() + { + return CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean() + ? TABLE_NAMES_WITH_AUTO_REPAIR + : TABLE_NAMES; + } + + private static final TableMetadata RepairHistory = parse(REPAIR_HISTORY, "Repair history", @@ -161,6 +183,27 @@ private SystemDistributedKeyspace() + "PRIMARY KEY ((ks_name, table_name), key))") .build(); + public static final TableMetadata AutoRepairHistory = + parse(AUTO_REPAIR_HISTORY, + "Auto repair history for each node", + "CREATE TABLE %s (" + + "host_id uuid," + + "repair_type text," + + "repair_turn text," + + "repair_start_ts timestamp," + + "repair_finish_ts timestamp," + + "delete_hosts set," + + "delete_hosts_update_time timestamp," + + "force_repair boolean," + + "PRIMARY KEY (repair_type, host_id))").build(); + public static final TableMetadata AutoRepairPriority = + parse(AUTO_REPAIR_PRIORITY, + "Auto repair priority for each group", + "CREATE TABLE %s (" + + "repair_type text," + + "repair_priority set," + + "PRIMARY KEY (repair_type))").build(); + private static TableMetadata.Builder parse(String table, String description, String cql) { return CreateTableStatement.parse(format(cql, table), SchemaConstants.DISTRIBUTED_KEYSPACE_NAME) @@ -170,7 +213,20 @@ private static TableMetadata.Builder parse(String table, String description, Str public static KeyspaceMetadata metadata() { - return KeyspaceMetadata.create(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, KeyspaceParams.simple(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), Tables.of(RepairHistory, ParentRepairHistory, ViewBuildStatus, PartitionDenylistTable)); + Tables tables; + if (CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean()) + { + tables = Tables.of(RepairHistory, ParentRepairHistory, ViewBuildStatus, + PartitionDenylistTable, AutoRepairHistory, AutoRepairPriority); + } + else + { + tables = Tables.of(RepairHistory, ParentRepairHistory, ViewBuildStatus, + PartitionDenylistTable); + } + return KeyspaceMetadata.create(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + KeyspaceParams.systemDistributed(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), + tables); } public static void startParentRepair(TimeUUID parent_id, String keyspaceName, String[] cfnames, RepairOption options) @@ -394,7 +450,7 @@ private static void processSilent(String fmtQry, String... values) public static void forceBlockingFlush(String table, ColumnFamilyStore.FlushReason reason) { - if (!DatabaseDescriptor.isUnsafeSystem()) + if (!UNSAFE_SYSTEM.getBoolean()) FBUtilities.waitOnFuture(Keyspace.open(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME) .getColumnFamilyStore(table) .forceFlush(reason)); diff --git a/src/java/org/apache/cassandra/schema/TableId.java b/src/java/org/apache/cassandra/schema/TableId.java index ceeeec315ac5..14335af01563 100644 --- a/src/java/org/apache/cassandra/schema/TableId.java +++ b/src/java/org/apache/cassandra/schema/TableId.java @@ -66,6 +66,14 @@ public static TableId fromString(String idString) return new TableId(UUID.fromString(idString)); } + public static TableId fromHexString(String nonDashUUID) + { + ByteBuffer bytes = ByteBufferUtil.hexToBytes(nonDashUUID); + long msb = bytes.getLong(0); + long lsb = bytes.getLong(8); + return fromUUID(new UUID(msb, lsb)); + } + @Nullable public static Pair tableNameAndIdFromFilename(String filename) { @@ -79,14 +87,6 @@ public static Pair tableNameAndIdFromFilename(String filename) return Pair.create(tableName, id); } - private static TableId fromHexString(String nonDashUUID) - { - ByteBuffer bytes = ByteBufferUtil.hexToBytes(nonDashUUID); - long msb = bytes.getLong(0); - long lsb = bytes.getLong(8); - return fromUUID(new UUID(msb, lsb)); - } - /** * Creates the UUID of a system table. * diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java index 98bcf042ef3e..d5e188549dfd 100644 --- a/src/java/org/apache/cassandra/schema/TableMetadata.java +++ b/src/java/org/apache/cassandra/schema/TableMetadata.java @@ -26,10 +26,10 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import com.google.common.base.MoreObjects; @@ -39,8 +39,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.google.common.collect.*; import org.apache.cassandra.auth.DataResource; import org.apache.cassandra.config.DatabaseDescriptor; @@ -60,6 +59,7 @@ import org.apache.cassandra.db.marshal.EmptyType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -69,12 +69,12 @@ import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Maps.transformValues; import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.apache.cassandra.db.Directories.SECONDARY_INDEX_NAME_SEPARATOR; import static org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR; -import static org.apache.cassandra.schema.KeyspaceMetadata.validateKeyspaceName; import static org.apache.cassandra.schema.SchemaConstants.FILENAME_LENGTH; import static org.apache.cassandra.schema.SchemaConstants.TABLE_NAME_LENGTH; import static org.apache.cassandra.schema.SchemaConstants.isValidCharsName; @@ -82,7 +82,6 @@ @Unmetered public class TableMetadata implements SchemaElement { - private static final Logger logger = LoggerFactory.getLogger(TableMetadata.class); // Please note that currently the only one truly useful flag is COUNTER, as the rest of the flags were about // differencing between CQL tables and the various types of COMPACT STORAGE tables (pre-4.0). As those "compact" @@ -208,6 +207,12 @@ protected TableMetadata(Builder builder) regularAndStaticColumns = RegularAndStaticColumns.builder().addAll(builder.regularAndStaticColumns).build(); columns = ImmutableMap.copyOf(builder.columns); + assert columns.values().stream().noneMatch(ColumnMetadata::isDropped) : + "Invalid columns (contains dropped): " + columns.values() + .stream() + .map(ColumnMetadata::debugString) + .collect(Collectors.joining(", ")); + indexes = builder.indexes; triggers = builder.triggers; @@ -292,7 +297,7 @@ public boolean isCompactTable() { return false; } - + public boolean isIncrementalBackupsEnabled() { return params.incrementalBackups; @@ -448,6 +453,16 @@ public boolean hasStaticColumns() return !staticColumns().isEmpty(); } + public boolean hasVectorType() + { + for (ColumnMetadata column : columns.values()) + { + if (column.type.isVector()) + return true; + } + return false; + } + /** * @return {@code true} if the table has any masked column, {@code false} otherwise. */ @@ -479,28 +494,13 @@ public boolean dependsOn(Function function) public void validate() { - validateKeyspaceName(keyspace, this::prepareConfigurationException); + KeyspaceMetadata.validateKeyspaceName(keyspace, this::prepareConfigurationException); validateTableName(); params.validate(); - if (partitionKeyColumns.stream().anyMatch(c -> c.type.isCounter())) - except("PRIMARY KEY columns cannot contain counters"); - - // Mixing counter with non counter columns is not supported (#2614) - if (isCounter()) - { - for (ColumnMetadata column : regularAndStaticColumns) - if (!(column.type.isCounter()) && !isSuperColumnMapColumnName(column.name)) - except("Cannot have a non counter column (\"%s\") in a counter table", column.name); - } - else - { - for (ColumnMetadata column : regularAndStaticColumns) - if (column.type.isCounter()) - except("Cannot have a counter column (\"%s\") in a non counter table", column.name); - } + columns().forEach(c -> c.validate(isCounter())); // All tables should have a partition key if (partitionKeyColumns.isEmpty()) @@ -512,7 +512,7 @@ public void validate() private void validateTableName() { if (!isValidCharsName(name)) - except("Table name must not be empty or not contain non-alphanumeric-underscore characters (got \"%s\")", name); + except("Table name must not be empty or contain non-alphanumeric-underscore characters (got \"%s\")", name); if (name.length() > TABLE_NAME_LENGTH) except("Table name must not be more than %d characters long (got %d characters for \"%s\")", TABLE_NAME_LENGTH, name.length(), name); @@ -536,9 +536,42 @@ private void validateTableName() * table with counters to rename that weirdly name map to something more meaningful (it's not possible today * as after renaming the validation in {@link #validate} would trigger). */ - private static boolean isSuperColumnMapColumnName(ColumnIdentifier columnName) + public static boolean isSuperColumnMapColumnName(ByteBuffer columnName) + { + return !columnName.hasRemaining(); + } + + /** + * Method that compares two TableMetadata objects. This is a modified version of {@link #validateCompatibility} that is used + * when checking the compatibility between the schema metadata from an SSTable against the schema metadata from a + * CQL table. + *

    + * The serialization header of the SSTable does not contain exactly the same information available in the schema of + * a CQL table, so the comparison needs to be adapted to the information available. + *

    + * For example, the serialization header does not contain the partition key columns: it only contains the composite + * type of the whole partition key. For this reason, the comparison must be between the partition key types contained + * in the two metadata objects, rather than comparing the individual column as {@link #validateCompatibility} does. + * This comparison is sufficient anyway because the composite types are the same only if their components are of the + * same type and in the same order. + *

    + * Another difference worth pointing out is that this method compares the table name, but not the keyspace name or table id. + * This is to allow the comparison between externally generated SSTables and a CQL schema, in which case the keyspace name + * and table id may be different. + * + * @param other TableMetadata instance to compare against + */ + public void validateTableNameAndStructureCompatibility(TableMetadata other) { - return !columnName.bytes.hasRemaining(); + if (isIndex()) + return; + + validateTableName(other); + validateTableType(other); + // comparing the types of the partition keys rather than the individual columns, as explained in the comment above + validatePartitionKeyTypes(other); + validateClusteringColumns(other); + validateRegularAndStaticColumns(other); } public void validateCompatibility(TableMetadata previous) @@ -546,18 +579,51 @@ public void validateCompatibility(TableMetadata previous) if (isIndex()) return; + validateKeyspaceName(previous); + validateTableName(previous); + validateTableId(previous); + validateTableType(previous); + validatePartitionKeyColumns(previous); + validateClusteringColumns(previous); + validateRegularAndStaticColumns(previous); + } + + private void validateKeyspaceName(TableMetadata previous) + { if (!previous.keyspace.equals(keyspace)) except("Keyspace mismatch (found %s; expected %s)", keyspace, previous.keyspace); + } + private void validateTableName(TableMetadata previous) + { if (!previous.name.equals(name)) except("Table mismatch (found %s; expected %s)", name, previous.name); + } + private void validateTableId(TableMetadata previous) + { if (!previous.id.equals(id)) except("Table ID mismatch (found %s; expected %s)", id, previous.id); + } + private void validateTableType(TableMetadata previous) + { if (!previous.flags.equals(flags) && (!Flag.isCQLTable(flags) || Flag.isCQLTable(previous.flags))) except("Table type mismatch (found %s; expected %s)", flags, previous.flags); + } + + private void validatePartitionKeyTypes(TableMetadata previous) + { + if (!partitionKeyType.isCompatibleWith(previous.partitionKeyType)) + { + except("Partition keys of different types (found %s; expected %s)", + partitionKeyType, + previous.partitionKeyType); + } + } + private void validatePartitionKeyColumns(TableMetadata previous) + { if (previous.partitionKeyColumns.size() != partitionKeyColumns.size()) { except("Partition keys of different length (found %s; expected %s)", @@ -574,7 +640,10 @@ public void validateCompatibility(TableMetadata previous) previous.partitionKeyColumns.get(i).type); } } + } + private void validateClusteringColumns(TableMetadata previous) + { if (previous.clusteringColumns.size() != clusteringColumns.size()) { except("Clustering columns of different length (found %s; expected %s)", @@ -591,7 +660,10 @@ public void validateCompatibility(TableMetadata previous) previous.clusteringColumns.get(i).type); } } + } + private void validateRegularAndStaticColumns(TableMetadata previous) + { for (ColumnMetadata previousColumn : previous.regularAndStaticColumns) { ColumnMetadata column = getColumn(previousColumn.name); @@ -665,6 +737,7 @@ boolean changeAffectsPreparedStatements(TableMetadata updated) || !regularAndStaticColumns.equals(updated.regularAndStaticColumns) || !indexes.equals(updated.indexes) || params.defaultTimeToLive != updated.params.defaultTimeToLive + || params.cdc != updated.params.cdc || params.gcGraceSeconds != updated.params.gcGraceSeconds || ( !Flag.isCQLTable(flags) && Flag.isCQLTable(updated.flags) ); } @@ -697,6 +770,49 @@ boolean referencesUserType(ByteBuffer name) return any(columns(), c -> c.type.referencesUserType(name)); } + /** + * Create a copy of this {@code TableMetadata} for a new keyspace. + * Note that a new table id will be generated for the returned {@link TableMetadata}. + * + * @param newKeyspace the name of the new keyspace + * @param udts the user defined types of the new keyspace + * @return a copy of this {@code TableMetadata} for a new keyspace + */ + TableMetadata withNewKeyspace(String newKeyspace, + Types udts) + { + return builder(newKeyspace, name).partitioner(partitioner) + .kind(kind) + .params(params) + .flags(flags) + .addColumns(transform(columns(), c -> c.withNewKeyspace(newKeyspace, udts))) + .droppedColumns(transformValues(droppedColumns, c -> c.withNewKeyspace(newKeyspace, udts))) + .indexes(indexes) + .triggers(triggers) + .build(); + } + + /** + * Create a copy of this {@code TableMetadata} with new params computed by applying the transformFunction. + * Note that the table id will be maintained. + * + * @param transformFunction The function used to transform the params. + * @return a copy of this {@code TableMetadata} containing the transformed params. + */ + TableMetadata withTransformedParams(java.util.function.Function transformFunction) + { + return builder(keyspace, name, id) + .partitioner(partitioner) + .kind(kind) + .params(transformFunction.apply(params)) + .flags(flags) + .addColumns(columns()) + .droppedColumns(droppedColumns) + .indexes(indexes) + .triggers(triggers) + .build(); + } + public TableMetadata withUpdatedUserType(UserType udt) { if (!referencesUserType(udt.name)) @@ -718,6 +834,11 @@ protected void except(String format, Object... args) throw prepareConfigurationException(format, args); } + public PartitionUpdate.Factory partitionUpdateFactory() + { + return params.memtable.factory.partitionUpdateFactory(); + } + @Override public boolean equals(Object o) { @@ -1096,8 +1217,7 @@ public Builder addStaticColumn(ColumnIdentifier name, AbstractType type, @Nul public Builder addColumn(ColumnMetadata column) { - if (columns.containsKey(column.name.bytes)) - throw new IllegalArgumentException(); + assert !columns.containsKey(column.name.bytes) : column.name + " is already present"; switch (column.kind) { @@ -1145,7 +1265,19 @@ public Builder recordDeprecatedSystemColumn(String name, AbstractType type) public Builder recordColumnDrop(ColumnMetadata column, long timeMicros) { - droppedColumns.put(column.name.bytes, new DroppedColumn(column.withNewType(column.type.expandUserTypes()), timeMicros)); + return recordColumnDrop(new DroppedColumn(column.asDropped(), timeMicros)); + } + + public Builder recordColumnDrop(DroppedColumn dropped) + { + DroppedColumn previous = droppedColumns.get(dropped.column.name.bytes); + if (previous != null && previous.droppedTime > dropped.droppedTime) + throw new ConfigurationException(String.format("Invalid dropped column record for column %s in %s at " + + "%d: pre-existing record at %d is newer", + dropped.column.name, this.name, previous.droppedTime, + dropped.droppedTime)); + + droppedColumns.put(dropped.column.name.bytes, dropped); return this; } @@ -1386,7 +1518,7 @@ public void appendCqlTo(CqlBuilder builder, builder.append(" WITH ") .increaseIndent(); - appendTableOptions(builder, withInternals); + appendTableOptions(builder, withInternals, includeDroppedColumns); builder.decreaseIndent(); @@ -1395,9 +1527,6 @@ public void appendCqlTo(CqlBuilder builder, builder.newLine() .append("*/"); } - - if (includeDroppedColumns) - appendDropColumns(builder); } private void appendColumnDefinitions(CqlBuilder builder, @@ -1408,37 +1537,16 @@ private void appendColumnDefinitions(CqlBuilder builder, while (iter.hasNext()) { ColumnMetadata column = iter.next(); - // If the column has been re-added after a drop, we don't include it right away. Instead, we'll add the - // dropped one first below, then we'll issue the DROP and then the actual ADD for this column, thus - // simulating the proper sequence of events. - if (includeDroppedColumns && droppedColumns.containsKey(column.name.bytes)) - continue; - column.appendCqlTo(builder); if (hasSingleColumnPrimaryKey && column.isPartitionKey()) builder.append(" PRIMARY KEY"); - if (!hasSingleColumnPrimaryKey || (includeDroppedColumns && !droppedColumns.isEmpty()) || iter.hasNext()) + if (!hasSingleColumnPrimaryKey || iter.hasNext()) builder.append(','); builder.newLine(); } - - if (includeDroppedColumns) - { - Iterator iterDropped = droppedColumns.values().iterator(); - while (iterDropped.hasNext()) - { - DroppedColumn dropped = iterDropped.next(); - dropped.column.appendCqlTo(builder); - - if (!hasSingleColumnPrimaryKey || iterDropped.hasNext()) - builder.append(','); - - builder.newLine(); - } - } } void appendPrimaryKey(CqlBuilder builder) @@ -1469,7 +1577,7 @@ void appendPrimaryKey(CqlBuilder builder) .newLine(); } - void appendTableOptions(CqlBuilder builder, boolean withInternals) + void appendTableOptions(CqlBuilder builder, boolean withInternals, boolean includeDroppedColumns) { if (withInternals) builder.append("ID = ") @@ -1493,6 +1601,8 @@ void appendTableOptions(CqlBuilder builder, boolean withInternals) } else { + if (includeDroppedColumns) + appendDropColumns(builder); params.appendCqlTo(builder, isView()); } builder.append(";"); @@ -1500,31 +1610,11 @@ void appendTableOptions(CqlBuilder builder, boolean withInternals) private void appendDropColumns(CqlBuilder builder) { - for (Entry entry : droppedColumns.entrySet()) + for (DroppedColumn dropped : droppedColumns.values()) { - DroppedColumn dropped = entry.getValue(); - - builder.newLine() - .append("ALTER TABLE ") - .append(toString()) - .append(" DROP ") - .append(dropped.column.name) - .append(" USING TIMESTAMP ") - .append(dropped.droppedTime) - .append(';'); - - ColumnMetadata column = getColumn(entry.getKey()); - if (column != null) - { - builder.newLine() - .append("ALTER TABLE ") - .append(toString()) - .append(" ADD "); - - column.appendCqlTo(builder); - - builder.append(';'); - } + builder.append(dropped.toCQLString()) + .newLine() + .append("AND "); } } @@ -1560,7 +1650,7 @@ public String primaryKeyAsCQLLiteral(ByteBuffer partitionKey, Clustering clus if (partitionKeyType instanceof CompositeType) { - List> components = partitionKeyType.getComponents(); + List> components = partitionKeyType.subTypes(); int size = components.size(); literals = new String[size + clusteringSize]; ByteBuffer[] values = ((CompositeType) partitionKeyType).split(partitionKey); @@ -1757,13 +1847,13 @@ public void appendCqlTo(CqlBuilder builder, .append("*/"); } - void appendTableOptions(CqlBuilder builder, boolean internals) + void appendTableOptions(CqlBuilder builder, boolean internals, boolean includeDroppedColumns) { builder.append("COMPACT STORAGE") .newLine() .append("AND "); - super.appendTableOptions(builder, internals); + super.appendTableOptions(builder, internals, includeDroppedColumns); } public static ColumnMetadata getCompactValueColumn(RegularAndStaticColumns columns) diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java index 8f883f8f4783..50abcd768838 100644 --- a/src/java/org/apache/cassandra/schema/TableParams.java +++ b/src/java/org/apache/cassandra/schema/TableParams.java @@ -21,10 +21,13 @@ import java.util.Map; import java.util.Map.Entry; +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.utils.StorageCompatibilityMode; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.Attributes; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.exceptions.ConfigurationException; @@ -60,7 +63,8 @@ public enum Option ADDITIONAL_WRITE_POLICY, CRC_CHECK_CHANCE, CDC, - READ_REPAIR; + READ_REPAIR, + AUTO_REPAIR; @Override public String toString() @@ -69,6 +73,9 @@ public String toString() } } + @VisibleForTesting + public static StorageCompatibilityMode storageCompatibilityModeOverride = null; + public final String comment; public final boolean allowAutoSnapshot; public final double bloomFilterFpChance; @@ -88,6 +95,7 @@ public String toString() public final ImmutableMap extensions; public final boolean cdc; public final ReadRepairStrategy readRepair; + public final AutoRepairParams autoRepair; private TableParams(Builder builder) { @@ -112,6 +120,7 @@ private TableParams(Builder builder) extensions = builder.extensions; cdc = builder.cdc; readRepair = builder.readRepair; + autoRepair = builder.autoRepair; } public static Builder builder() @@ -139,7 +148,8 @@ public static Builder builder(TableParams params) .additionalWritePolicy(params.additionalWritePolicy) .extensions(params.extensions) .cdc(params.cdc) - .readRepair(params.readRepair); + .readRepair(params.readRepair) + .automatedRepair(params.autoRepair); } public Builder unbuild() @@ -149,11 +159,11 @@ public Builder unbuild() public void validate() { - compaction.validate(); + // compaction parameters are validated during CompactionParams construction compression.validate(); double minBloomFilterFpChanceValue = BloomCalculations.minSupportedBloomFilterFpChance(); - if (bloomFilterFpChance <= minBloomFilterFpChanceValue || bloomFilterFpChance > 1) + if (bloomFilterFpChance <= minBloomFilterFpChanceValue || bloomFilterFpChance > 1) { fail("%s must be larger than %s and less than or equal to 1.0 (got %s)", BLOOM_FILTER_FP_CHANCE, @@ -194,6 +204,8 @@ public void validate() if (cdc && memtable.factory().writesShouldSkipCommitLog()) fail("CDC cannot work if writes skip the commit log. Check your memtable configuration."); + + autoRepair.validate(); } private static void fail(String format, Object... args) @@ -217,7 +229,7 @@ public boolean equals(Object o) && allowAutoSnapshot == p.allowAutoSnapshot && bloomFilterFpChance == p.bloomFilterFpChance && crcCheckChance == p.crcCheckChance - && gcGraceSeconds == p.gcGraceSeconds + && gcGraceSeconds == p.gcGraceSeconds && incrementalBackups == p.incrementalBackups && defaultTimeToLive == p.defaultTimeToLive && memtableFlushPeriodInMs == p.memtableFlushPeriodInMs @@ -230,7 +242,8 @@ public boolean equals(Object o) && memtable.equals(p.memtable) && extensions.equals(p.extensions) && cdc == p.cdc - && readRepair == p.readRepair; + && readRepair == p.readRepair + && autoRepair.equals(p.autoRepair); } @Override @@ -254,7 +267,8 @@ public int hashCode() memtable, extensions, cdc, - readRepair); + readRepair, + autoRepair); } @Override @@ -280,17 +294,29 @@ public String toString() .add(EXTENSIONS.toString(), extensions) .add(CDC.toString(), cdc) .add(READ_REPAIR.toString(), readRepair) + .add(AUTO_REPAIR.toString(), autoRepair) .toString(); } public void appendCqlTo(CqlBuilder builder, boolean isView) { + StorageCompatibilityMode compatibilityMode = storageCompatibilityModeOverride != null + ? storageCompatibilityModeOverride + : StorageCompatibilityMode.current(); + boolean usePre50Schema = compatibilityMode.isBefore(5); + // option names should be in alphabetical order builder.append("additional_write_policy = ").appendWithSingleQuotes(additionalWritePolicy.toString()) - .newLine() - .append("AND allow_auto_snapshot = ").append(allowAutoSnapshot) - .newLine() - .append("AND bloom_filter_fp_chance = ").append(bloomFilterFpChance) + .newLine(); + + // Exclude allow_auto_snapshot in backward compatibility mode (new in 5.0) + if (!usePre50Schema) + { + builder.append("AND allow_auto_snapshot = ").append(allowAutoSnapshot) + .newLine(); + } + + builder.append("AND bloom_filter_fp_chance = ").append(bloomFilterFpChance) .newLine() .append("AND caching = ").append(caching.asMap()) .newLine() @@ -301,9 +327,15 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) .append("AND compaction = ").append(compaction.asMap()) .newLine() .append("AND compression = ").append(compression.asMap()) - .newLine() - .append("AND memtable = ").appendWithSingleQuotes(memtable.configurationKey()) - .newLine() + .newLine(); + + // Use map format for pre-5.0 compatibility, string format for 5.0 + if (usePre50Schema) + builder.append("AND memtable = ").append(memtable.toMapForCC4()); + else + builder.append("AND memtable = ").appendWithSingleQuotes(memtable.configurationKey()); + + builder.newLine() .append("AND crc_check_chance = ").append(crcCheckChance) .newLine(); @@ -320,10 +352,16 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) false) .newLine() .append("AND gc_grace_seconds = ").append(gcGraceSeconds) - .newLine() - .append("AND incremental_backups = ").append(incrementalBackups) - .newLine() - .append("AND max_index_interval = ").append(maxIndexInterval) + .newLine(); + + // Exclude incremental_backups in backward compatibility mode (new in 5.0) + if (!usePre50Schema) + { + builder.append("AND incremental_backups = ").append(incrementalBackups) + .newLine(); + } + + builder.append("AND max_index_interval = ").append(maxIndexInterval) .newLine() .append("AND memtable_flush_period_in_ms = ").append(memtableFlushPeriodInMs) .newLine() @@ -332,6 +370,13 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) .append("AND read_repair = ").appendWithSingleQuotes(readRepair.toString()) .newLine() .append("AND speculative_retry = ").appendWithSingleQuotes(speculativeRetry.toString()); + + if (DatabaseDescriptor.getRawConfig() != null + && DatabaseDescriptor.getAutoRepairConfig().isAutoRepairSchedulingEnabled()) + { + builder.newLine() + .append("AND auto_repair = ").append(autoRepair.asMap()); + } } public static final class Builder @@ -356,6 +401,7 @@ public static final class Builder private boolean cdc; private ReadRepairStrategy readRepair = ReadRepairStrategy.BLOCKING; + private AutoRepairParams autoRepair = AutoRepairParams.DEFAULT; public Builder() { } @@ -478,5 +524,11 @@ public Builder extensions(Map val) extensions = ImmutableMap.copyOf(val); return this; } + + public Builder automatedRepair(AutoRepairParams val) + { + autoRepair = val; + return this; + } } } diff --git a/src/java/org/apache/cassandra/schema/Tables.java b/src/java/org/apache/cassandra/schema/Tables.java index 0f8f6b31908a..c2236f478ef7 100644 --- a/src/java/org/apache/cassandra/schema/Tables.java +++ b/src/java/org/apache/cassandra/schema/Tables.java @@ -22,13 +22,18 @@ import java.util.Iterator; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; - import javax.annotation.Nullable; -import com.google.common.collect.*; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.MapDifference; +import com.google.common.collect.Maps; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.index.internal.CassandraIndex; @@ -179,6 +184,31 @@ public Tables withUpdatedUserType(UserType udt) : this; } + public Tables withNewKeyspace(String newName, Types udts) + { + Map updated = new HashMap<>(); + for (TableMetadata table : this) + { + updated.put(table.name, table.withNewKeyspace(newName, udts)); + } + + return builder().add(updated.values()).build(); + } + + public Tables withTransformedParams(Function transformFunction) + { + Map updated = new HashMap<>(); + + // We order the tables by dependencies so that vertices tables are + // processed before edges tables, in case graph constructs are used + for (TableMetadata table : this) + { + updated.put(table.name, table.withTransformedParams(transformFunction)); + } + + return builder().add(updated.values()).build(); + } + MapDifference indexesDiff(Tables other) { Map thisIndexTables = new HashMap<>(); diff --git a/src/java/org/apache/cassandra/schema/Types.java b/src/java/org/apache/cassandra/schema/Types.java index 0d264c4f492c..bf9430cbe50d 100644 --- a/src/java/org/apache/cassandra/schema/Types.java +++ b/src/java/org/apache/cassandra/schema/Types.java @@ -36,6 +36,7 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; @@ -109,6 +110,32 @@ public Iterable referencingUserType(ByteBuffer name) return Iterables.filter(types.values(), t -> t.referencesUserType(name) && !t.name.equals(name)); } + /** + * Returns the types ordered by dependencies. + * + * @return the types ordered by dependencies. + */ + private Set getTypesOrderedByDependencies() + { + Set orderedTypesByDependencies = new LinkedHashSet<>(); + for (UserType type : this) + { + recordNestedTypes(type, orderedTypesByDependencies); + } + return orderedTypesByDependencies; + } + + private void recordNestedTypes(AbstractType userType, Set userTypes) + { + for (AbstractType subType : userType.subTypes()) + { + recordNestedTypes(subType, userTypes); + } + + if (userType.isUDT()) + userTypes.add((UserType) userType); + } + public boolean isEmpty() { return types.isEmpty(); @@ -254,6 +281,33 @@ private static void addUserTypes(AbstractType type, Set types) types.add(((UserType) type).name); } + /** + * Changes the keyspace of all the types. + * + * @param newKeyspace the name of the new keyspace + * @return the new types + */ + public Types withNewKeyspace(String newKeyspace) + { + Map updatedTypes = new HashMap<>(); + + for (UserType originalType : getTypesOrderedByDependencies()) + { + UserType type = new UserType(newKeyspace, + originalType.name, + originalType.fieldNames(), + originalType.fieldTypes() + .stream() + .map(t -> t.withUpdatedUserTypes(updatedTypes.values())) + .collect(ImmutableList.toImmutableList()), + true); + + updatedTypes.put(type.name, type); + } + + return new Types(ImmutableSortedMap.copyOf(updatedTypes)); + } + public static final class Builder { final ImmutableSortedMap.Builder types = ImmutableSortedMap.naturalOrder(); @@ -384,15 +438,15 @@ boolean referencesUserType(RawUDT other) UserType prepare(String keyspace, Types types) { - List preparedFieldNames = - fieldNames.stream() - .map(FieldIdentifier::forInternalString) - .collect(toList()); - - List> preparedFieldTypes = - fieldTypes.stream() - .map(t -> t.prepareInternal(keyspace, types).getType()) - .collect(toList()); + ImmutableList preparedFieldNames = + fieldNames.stream() + .map(FieldIdentifier::forInternalString) + .collect(toImmutableList()); + + ImmutableList> preparedFieldTypes = + fieldTypes.stream() + .map(t -> t.prepare(keyspace, types).getType()) + .collect(toImmutableList()); return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes, true); } diff --git a/src/java/org/apache/cassandra/schema/UserFunctions.java b/src/java/org/apache/cassandra/schema/UserFunctions.java index b40c704d0b98..101687a59ff7 100644 --- a/src/java/org/apache/cassandra/schema/UserFunctions.java +++ b/src/java/org/apache/cassandra/schema/UserFunctions.java @@ -18,20 +18,30 @@ package org.apache.cassandra.schema; import java.nio.ByteBuffer; -import java.util.*; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.google.common.collect.*; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Iterables; -import org.apache.cassandra.cql3.functions.*; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.cql3.functions.UDAggregate; +import org.apache.cassandra.cql3.functions.UDFunction; +import org.apache.cassandra.cql3.functions.UserFunction; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; -import static java.util.stream.Collectors.toList; - import static com.google.common.collect.Iterables.any; +import static java.util.stream.Collectors.toList; /** * An immutable container for a keyspace's UDAs and UDFs. @@ -117,6 +127,14 @@ public UserFunctions withUpdatedUserType(UserType udt) return builder().add(udfs).add(udas).build(); } + public UserFunctions withNewKeyspace(String newKeyspace, Types udts) + { + Collection udfs = udfs().map(f -> f.withNewKeyspace(newKeyspace, udts)).collect(toList()); + Collection udas = udas().map(f -> f.withNewKeyspace(newKeyspace, udfs, udts)).collect(toList()); + + return builder().add(udfs).add(udas).build(); + } + /** * @return a stream of aggregates that use the provided function as either a state or a final function * @param function the referree function diff --git a/src/java/org/apache/cassandra/schema/ViewMetadata.java b/src/java/org/apache/cassandra/schema/ViewMetadata.java index df26a134e24e..71635c443c3f 100644 --- a/src/java/org/apache/cassandra/schema/ViewMetadata.java +++ b/src/java/org/apache/cassandra/schema/ViewMetadata.java @@ -210,7 +210,7 @@ public void appendCqlTo(CqlBuilder builder, .append(" WITH ") .increaseIndent(); - metadata.appendTableOptions(builder, internals); + metadata.appendTableOptions(builder, internals, false); } @Override diff --git a/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java b/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java index 1c437e6f2dc7..19a325e795c2 100644 --- a/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java +++ b/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java @@ -105,7 +105,7 @@ public void install() throws Exception return; } - FBUtilities.classForName(getProviderClassAsString(), "crypto provider"); + FBUtilities.classForNameWithoutInitialization(getProviderClassAsString(), "crypto provider", Provider.class); String providerName = getProviderName(); int providerPosition = getProviderPosition(providerName); diff --git a/src/java/org/apache/cassandra/security/CipherFactory.java b/src/java/org/apache/cassandra/security/CipherFactory.java index 4674fd17cd29..8a42f589c623 100644 --- a/src/java/org/apache/cassandra/security/CipherFactory.java +++ b/src/java/org/apache/cassandra/security/CipherFactory.java @@ -41,6 +41,7 @@ import io.netty.util.concurrent.FastThreadLocal; import org.apache.cassandra.concurrent.ImmediateExecutor; import org.apache.cassandra.config.TransparentDataEncryptionOptions; +import org.apache.cassandra.utils.FBUtilities; /** * A factory for loading encryption keys from {@link KeyProvider} instances. @@ -70,9 +71,10 @@ public CipherFactory(TransparentDataEncryptionOptions options) try { secureRandom = SecureRandom.getInstance("SHA1PRNG"); - Class keyProviderClass = (Class)Class.forName(options.key_provider.class_name); - Constructor ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class); - keyProvider = (KeyProvider)ctor.newInstance(options); + Class keyProviderClass = + FBUtilities.classForNameWithoutInitialization(options.key_provider.class_name, "key provider", KeyProvider.class); + Constructor ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class); + keyProvider = ctor.newInstance(options); } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java b/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java index 8f86831cc337..f3e1b811d593 100644 --- a/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java +++ b/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java @@ -33,10 +33,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.annotations.VisibleForTesting; + import io.netty.util.concurrent.FastThreadLocal; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.utils.logging.LoggingSupportFactory; +import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_USER_DEFINED_FUNCTIONS; + /** * Custom {@link SecurityManager} and {@link Policy} implementation that only performs access checks * if explicitly enabled. @@ -86,6 +90,16 @@ public static void install() if (installed) return; + // Skip SecurityManager installation if UDFs are disabled via system property. + // The SecurityManager is only used for sandboxing UDFs, so it's not needed when UDFs are disabled. + if (DISABLE_USER_DEFINED_FUNCTIONS.getBoolean()) + { + logger.info("Skipping ThreadAwareSecurityManager installation because UDFs are disabled via system property {}", + DISABLE_USER_DEFINED_FUNCTIONS.getKey()); + installed = true; // Mark as installed to prevent re-entry + return; + } + // this line is needed - we need to make sure AccessControlException is loaded before we install this SM // otherwise we may get into stackoverflow when javax.security is not allowed package, and ACE is tried to be // loaded when it is going to be thrown from SM (class loader triggers SM to verify javax.security, @@ -98,6 +112,16 @@ public static void install() installed = true; } + /** + * Reset the installed flag for testing purposes only. + * This allows tests to verify the install() method behavior. + */ + @VisibleForTesting + public static void resetInstalledFlagForTests() + { + installed = false; + } + static { // diff --git a/src/java/org/apache/cassandra/sensors/ActiveRequestSensors.java b/src/java/org/apache/cassandra/sensors/ActiveRequestSensors.java new file mode 100644 index 000000000000..9c4c418eaddc --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/ActiveRequestSensors.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; + +/** + * Groups {@link Sensor}s associated to a given request/response and related {@link Context}: this is the main entry + * point to create and modify sensors. More specifically: + *

      + *
    • Create a new sensor associated to the request/response via {@link #registerSensor(Context, Type)}.
    • + *
    • Increment the sensor value for the request/response via {@link #incrementSensor(Context, Type, double)}.
    • + *
    • Sync this request/response sensor value to the {@link SensorsRegistry} via {@link #syncAllSensors()}.
    • + *
    + * Sensor values related to a given request/response are isolated from other sensors, and the "same" sensor + * (for a given context and type) registered to different requests/responses will have a different value: in other words, + * there is no automatic synchronization or coordination across sensor values belonging to different + * {@link RequestSensors} objects, hence {@link #syncAllSensors()} MUST be invoked to propagate the sensors values + * at a global level to the {@link SensorsRegistry}. + *
    + * Please note instances of this class should be created via the configured {@link SensorsFactory}. + */ +public class ActiveRequestSensors implements RequestSensors +{ + private final Supplier sensorsRegistry; + + // Using Map of array values for performance reasons to avoid wrapping key into another Object (.eg. Pair(context,type)). + // Note that array values can contain NULL so be careful to filter NULLs when iterating over array + private final HashMap sensors = new LinkedHashMap<>(); + + private final Map latestSyncedValuePerSensor = new HashMap<>(); + + @VisibleForTesting + public ActiveRequestSensors() + { + this(() -> SensorsRegistry.instance); + } + + @VisibleForTesting + public ActiveRequestSensors(Supplier sensorsRegistry) + { + this.sensorsRegistry = sensorsRegistry; + } + + public synchronized void registerSensor(Context context, Type type) + { + Sensor[] typeSensors = sensors.computeIfAbsent(context, key -> + { + Sensor[] newTypeSensors = new Sensor[Type.values().length]; + newTypeSensors[type.ordinal()] = new Sensor(context, type); + return newTypeSensors; + }); + if (typeSensors[type.ordinal()] == null) + typeSensors[type.ordinal()] = new Sensor(context, type); + } + + public synchronized Optional getSensor(Context context, Type type) + { + return Optional.ofNullable(getSensorFast(context, type)); + } + + public synchronized Set getSensors(Predicate filter) + { + return sensors.values().stream().flatMap(Arrays::stream).filter(Objects::nonNull).filter(filter).collect(Collectors.toSet()); + } + + public synchronized void incrementSensor(Context context, Type type, double value) + { + Sensor sensor = getSensorFast(context, type); + if (sensor != null) + sensor.increment(value); + } + + public synchronized void syncAllSensors() + { + sensors.values().forEach(types -> { + for (int i = 0; i < types.length; i++) + { + if (types[i] != null) + { + Sensor sensor = types[i]; + double current = latestSyncedValuePerSensor.getOrDefault(sensor, 0d); + double update = sensor.getValue() - current; + if (update == 0d) + continue; + + latestSyncedValuePerSensor.put(sensor, sensor.getValue()); + sensorsRegistry.get().incrementSensor(sensor.getContext(), sensor.getType(), update); + } + } + }); + } + + /** + * To get best perfromance we are not returning Optional here + */ + @Nullable + private Sensor getSensorFast(Context context, Type type) + { + Sensor[] typeSensors = sensors.get(context); + if (typeSensors != null) + return typeSensors[type.ordinal()]; + + return null; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ActiveRequestSensors other = (ActiveRequestSensors) o; + return Objects.equals(sensors, other.sensors); + } + + @Override + public int hashCode() + { + return Objects.hash(sensors); + } + + @Override + public String toString() + { + return "ActiveRequestSensors{" + + "sensors=" + sensors + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/sensors/ActiveSensorsFactory.java b/src/java/org/apache/cassandra/sensors/ActiveSensorsFactory.java new file mode 100644 index 000000000000..7ae1ea058b7e --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/ActiveSensorsFactory.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Optional; + +/** + * Implementation of the {@link SensorsFactory} that creates: + *
      + *
    • a new {@link ActiveRequestSensors} instance for all keyspaces.
    • + *
    • a singleton {@link SensorEncoder} implementation that encodes the sensor name as {@literal "_REQUEST."} for request sensors and {@literal "_GLOBAL."} for global sensors.
    • + *
    + */ +public class ActiveSensorsFactory implements SensorsFactory +{ + private static final SensorEncoder SENSOR_ENCODER = new SensorEncoder() + { + @Override + public Optional encodeRequestSensorName(Sensor sensor) + { + return Optional.of(sensor.getType() + "_REQUEST." + sensor.getContext().getKeyspace() + '.' + sensor.getContext().getTable()); + } + + @Override + public Optional encodeGlobalSensorName(Sensor sensor) + { + return Optional.of(sensor.getType() + "_GLOBAL." + sensor.getContext().getKeyspace() + '.' + sensor.getContext().getTable()); + } + }; + + @Override + public RequestSensors createRequestSensors(String... keyspaces) + { + return new ActiveRequestSensors(); + } + + @Override + public SensorEncoder createSensorEncoder() + { + return SENSOR_ENCODER; + } +} diff --git a/src/java/org/apache/cassandra/sensors/Context.java b/src/java/org/apache/cassandra/sensors/Context.java new file mode 100644 index 000000000000..a82e534b1461 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/Context.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Objects; + +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.schema.TableMetadata; + +/** + * Represents the context for a (group of) {@link Sensor}(s), made up of: + *
      + *
    • The keyspace the sensor refers to.
    • + *
    • The table the sensor refers to.
    • + *
    • The related table id.
    • + *
    + */ +public class Context +{ + private final String keyspace; + private final String table; + private final String tableId; + + private final int hashCode; + + public Context(String keyspace, String table, String tableId) + { + this.keyspace = keyspace; + this.table = table; + this.tableId = tableId; + this.hashCode = Objects.hash(keyspace, table, tableId); + } + + public String getKeyspace() + { + return keyspace; + } + + public String getTable() + { + return table; + } + + public String getTableId() + { + return tableId; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Context context = (Context) o; + return Objects.equals(keyspace, context.keyspace) && Objects.equals(table, context.table) && Objects.equals(tableId, context.tableId); + } + + @Override + public int hashCode() + { + return hashCode; + } + + @Override + public String toString() + { + return "Context{" + + "keyspace='" + keyspace + '\'' + + ", table='" + table + '\'' + + ", tableId='" + tableId + '\'' + + '}'; + } + + public static Context from(ReadCommand command) + { + return from(command.metadata()); + } + + public static Context from(TableMetadata table) + { + return new Context(table.keyspace, table.name, table.id.toString()); + } + + public static Context from(IndexContext indexContext) + { + return new Context(indexContext.getKeyspace(), indexContext.getTable(), indexContext.getTableId().toString()); + } +} diff --git a/src/java/org/apache/cassandra/sensors/NoOpRequestSensors.java b/src/java/org/apache/cassandra/sensors/NoOpRequestSensors.java new file mode 100644 index 000000000000..dae952bd0d83 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/NoOpRequestSensors.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableSet; + +/** + * No-op implementation of {@link RequestSensors}. This is used when sensors are disabled. + */ +public class NoOpRequestSensors implements RequestSensors +{ + public static final NoOpRequestSensors instance = new NoOpRequestSensors(); + + @Override + public void registerSensor(Context context, Type type) + { + + } + + @Override + public Optional getSensor(Context context, Type type) + { + return Optional.empty(); + } + + @Override + public Set getSensors(Predicate filter) + { + return ImmutableSet.of(); + } + + @Override + public void incrementSensor(Context context, Type type, double value) + { + + } + + @Override + public void syncAllSensors() + { + + } +} diff --git a/src/java/org/apache/cassandra/sensors/RequestSensors.java b/src/java/org/apache/cassandra/sensors/RequestSensors.java new file mode 100644 index 000000000000..9ce2641bb418 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/RequestSensors.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Collection; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * Groups {@link Sensor}s associated to a given request/response and related {@link Context}: this is the main entry + * point to create and modify sensors. Actual implementations can be created via {@link SensorsFactory}. + */ +public interface RequestSensors +{ + /** + * Register a new sensor associated to the given context and type. It is up to the implementation to decide the + * idempotency of this operation. + * + * @param context the sensor context associated with the request/response + * @param type the type of the sensor + */ + void registerSensor(Context context, Type type); + + /** + * Returns the sensor associated to the given context and type, if any. + * + * @param context the sensor context associated with the request/response + * @param type the type of the sensor + * @return the sensor associated to the given context and type, if any + */ + Optional getSensor(Context context, Type type); + + /** + * Returns all the sensors that match the given filter + * + * @param filter a predicate applied to each sensor to decide if it should be included in the returned collection + * @return a collection of sensors matching the given predicate + */ + Collection getSensors(Predicate filter); + + /** + * Increment the sensor value associated to the given context and type by the given value. + * + * @param context the sensor context associated with the request/response + * @param type the type of the sensor + * @param value the value to increment the sensor by + */ + void incrementSensor(Context context, Type type, double value); + + /** + * Sync all the sensors values tracked for this request to the global {@link SensorsRegistry}. This method + * will be called at least once per request/response so it is recommended to make the implementation idempotent. + */ + void syncAllSensors(); +} diff --git a/src/java/org/apache/cassandra/sensors/RequestTracker.java b/src/java/org/apache/cassandra/sensors/RequestTracker.java new file mode 100644 index 000000000000..42a799779820 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/RequestTracker.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import org.apache.cassandra.concurrent.ExecutorLocals; + +/** + * Extends {@link ExecutorLocals} implementation to track and propagate {@link RequestSensors} associated to a given request/response. + */ +public class RequestTracker extends ExecutorLocals.Impl +{ + public static final RequestTracker instance = new RequestTracker(); + + private RequestTracker() + {} + + public RequestSensors get() + { + return ExecutorLocals.current().sensors; + } + + public void set(RequestSensors sensors) + { + ExecutorLocals current = ExecutorLocals.current(); + ExecutorLocals.Impl.set(current.traceState, current.clientWarnState, sensors, current.operationContext); + } +} diff --git a/src/java/org/apache/cassandra/sensors/Sensor.java b/src/java/org/apache/cassandra/sensors/Sensor.java new file mode 100644 index 000000000000..75519bf50bf4 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/Sensor.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Objects; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.AtomicDouble; + +/** + * Tracks the {@link #value} for a given measurement of a given {@link Type} and {@link Context}, during any + * request/response cycle. + *

    + * Sensors can be read (via {@link #getValue()}) but cannot be directly created or incremented, because their lifecycle + * and values are managed by the {@link RequestSensors} and {@link SensorsRegistry} classes, more specifically: + *
      + *
    • In order to track a given measurement for a given request/response, register a sensor of the related type via + * {@link RequestSensors#registerSensor(Type)}.
    • + *
    • Once registered, the sensor lifecycle spans across multiple request/response cycles, and its "global" + * value can be accessed via {@link SensorsRegistry}.
    • + *
    + */ +public class Sensor +{ + private final Context context; + private final Type type; + private final AtomicDouble value; + + private final int hashCode; + + protected Sensor(Context context, Type type) + { + this.context = context; + this.type = type; + this.value = new AtomicDouble(); + this.hashCode = Objects.hash(context, type); + } + + @VisibleForTesting + public void increment(double value) + { + this.value.addAndGet(value); + } + + public Context getContext() + { + return context; + } + + public Type getType() + { + return type; + } + + public double getValue() + { + return value.doubleValue(); + } + + @VisibleForTesting + public void reset() + { + value.set(0); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Sensor sensor = (Sensor) o; + return Objects.equals(context, sensor.context) && type == sensor.type; + } + + @Override + public int hashCode() + { + return hashCode; + } + + @Override + public String toString() + { + return "Sensor{" + + "context=" + context + + ", type=" + type + + ", value=" + value + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/sensors/SensorEncoder.java b/src/java/org/apache/cassandra/sensors/SensorEncoder.java new file mode 100644 index 000000000000..8894026b17f5 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/SensorEncoder.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Optional; + +/** + * Encodes sensor name as string to be used on the wire (let it be in internode messages as custom params or in native protocol + * messages as custom payloads). Note that the sensor value itself will always be encoded as bytes in the big endian order + * (see {@link SensorsCustomParams#sensorValueAsBytes(double)} and {@link SensorsCustomParams#sensorValueAsByteBuffer(double)}). + * Implementations should be very efficient as sensor names are potentially encoded with each request. They should also encode + * enough information to differentiate between sensors of the same type that belong to the same request but different + * keyspaces and/or tables. + */ +public interface SensorEncoder +{ + /** + * Encodes request sensor name as a string to be used on the wire. A request sensor tracks usage per request. See {@link RequestSensors}. + * + * @param sensor the sensor to encode + * @return the encoded sensor as a string. If the optional is empty, the sensor will not be encoded. + */ + Optional encodeRequestSensorName(Sensor sensor); + + /** + * Encodes global sensor name as a string to be used on the wire. A global sensor tracks usage globally across different requests. See {@link SensorsRegistry}. + * + * @param sensor the sensor to encode + * @return the encoded sensor as a string. If the optional is empty, the sensor will not be encoded. + */ + Optional encodeGlobalSensorName(Sensor sensor); +} diff --git a/src/java/org/apache/cassandra/sensors/SensorsCustomParams.java b/src/java/org/apache/cassandra/sensors/SensorsCustomParams.java new file mode 100644 index 000000000000..9303ef71d3bb --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/SensorsCustomParams.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.transport.ProtocolVersion; + +/** + * A utility class that groups methods to facilitate encoding sensors in native or internode protocol messages: + *
      + *
    • Sensors in internode messages: used to communicate sensors values from replicas to coordinators in the internode + * message response {@link Message.Header#customParams()} bytes map. + * See {@link SensorsCustomParams#addSensorsToInternodeResponse(RequestSensors, Message.Builder)} and + * {@link SensorsCustomParams#sensorValueFromInternodeResponse(Message, String)}.
    • + *
    • Sensors in native protocol messages: used to communicate sensors values from coordinator to upstream callers via the native protocol + * response {@link org.apache.cassandra.transport.Message#getCustomPayload()} bytes map. + * See {@link SensorsCustomParams#addSensorToCQLResponse(org.apache.cassandra.transport.Message.Response, ProtocolVersion, RequestSensors, Context, Type)}.
    • + *
    + */ +public final class SensorsCustomParams +{ + private static final SensorEncoder SENSOR_ENCODER = SensorsFactory.instance.createSensorEncoder(); + + private SensorsCustomParams() + { + } + + /** + * Utility method to encode sensor value as byte[] in the big endian order. + */ + public static byte[] sensorValueAsBytes(double value) + { + ByteBuffer buffer = ByteBuffer.allocate(Double.BYTES); + buffer.putDouble(value); + + return buffer.array(); + } + + /** + * Utility method to encode sensor value as ByteBuffer in the big endian order. + */ + public static ByteBuffer sensorValueAsByteBuffer(double value) + { + ByteBuffer buffer = ByteBuffer.allocate(Double.BYTES); + buffer.putDouble(value); + buffer.flip(); + return buffer; + } + + public static double sensorValueFromBytes(byte[] bytes) + { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + return buffer.getDouble(); + } + + /** + * Iterate over all sensors in the {@link RequestSensors} and encodes each sensor value by applying the given + * {@param valueFunction} in the internode response message as custom parameters. + * + * @param sensors the collection of sensors to encode in the response + * @param valueFunction the function to get the sensor value + * @param response the response message builder to add the sensors to + * @param the response message builder type + */ + public static void addSensorsToInternodeResponse(RequestSensors sensors, Function valueFunction, Message.Builder response) + { + Preconditions.checkNotNull(sensors); + Preconditions.checkNotNull(response); + + for (Sensor sensor : sensors.getSensors(ignored -> true)) + addSensorToInternodeResponse(response, sensor, valueFunction); + } + + /** + * Iterate over all sensors in the {@link RequestSensors} and encodes each sensor values in the internode response + * message as custom parameters. + * + * @param sensors the collection of sensors to encode in the response + * @param response the response message builder to add the sensors to + * @param the response message builder type + */ + public static void addSensorsToInternodeResponse(RequestSensors sensors, Message.Builder response) + { + addSensorsToInternodeResponse(sensors, Sensor::getValue, response); + } + + /** + * Reads the sensor value encoded in the response message header as {@link Message.Header#customParams()} bytes map. + * + * @param message the message to read the sensor value from + * @param customParam the name of the header in custom params to read the sensor value from + * @param the message type + * @return the sensor value + */ + public static double sensorValueFromInternodeResponse(Message message, String customParam) + { + if (customParam == null) + return 0.0; + + Map customParams = message.header.customParams(); + if (customParams == null) + return 0.0; + + byte[] readBytes = message.header.customParams().get(customParam); + if (readBytes == null) + return 0.0; + + return sensorValueFromBytes(readBytes); + } + + /** + * Adds a sensor of a given type and context to the native protocol response message encoded in the custom payload bytes map. + * If the sensor is already present in the custom payload, it will be overwritten. + * + * @param response the response message to add the sensors to + * @param protocolVersion the protocol version specified in query options to determine if custom payload is supported (should be V4 or later). + * @param sensors the requests sensors associated with the request to get the sensor values from. + * @param context the context of the sensor to add to the response + * @param type the type of the sensor to add to the response + */ + public static void addSensorToCQLResponse(org.apache.cassandra.transport.Message.Response response, + ProtocolVersion protocolVersion, + RequestSensors sensors, + Context context, + Type type) + { + if (!CassandraRelevantProperties.SENSORS_VIA_NATIVE_PROTOCOL.getBoolean()) + return; + + // Custom payload is not supported for protocol versions < 4 + if (protocolVersion.isSmallerThan(ProtocolVersion.V4)) + return; + + if (response == null || sensors == null) + return; + + Optional requestSensor = sensors.getSensor(context, type); + if (requestSensor.isEmpty()) + return; + + Optional headerName = SENSOR_ENCODER.encodeRequestSensorName(requestSensor.get()); + if (headerName.isEmpty()) + return; + + Map customPayload = response.getCustomPayload() == null ? new HashMap<>() : response.getCustomPayload(); + ByteBuffer bytes = SensorsCustomParams.sensorValueAsByteBuffer(requestSensor.get().getValue()); + customPayload.put(headerName.get(), bytes); + response.setCustomPayload(customPayload); + } + + private static void addSensorToInternodeResponse(Message.Builder response, Sensor requestSensor, Function valueFunction) + { + Optional requestParam = paramForRequestSensor(requestSensor); + if (requestParam.isEmpty()) + return; + + byte[] requestBytes = SensorsCustomParams.sensorValueAsBytes(valueFunction.apply(requestSensor)); + response.withCustomParam(requestParam.get(), requestBytes); + + Optional globalSensor = SensorsRegistry.instance.getSensor(requestSensor.getContext(), requestSensor.getType()); + if (globalSensor.isEmpty()) + return; + + Optional globalParam = paramForGlobalSensor(globalSensor.get()); + if (globalParam.isEmpty()) + return; + + byte[] globalBytes = SensorsCustomParams.sensorValueAsBytes(valueFunction.apply(globalSensor.get())); + response.withCustomParam(globalParam.get(), globalBytes); + } + + public static Optional paramForRequestSensor(Sensor sensor) + { + return SENSOR_ENCODER.encodeRequestSensorName(sensor); + } + + public static Optional paramForGlobalSensor(Sensor sensor) + { + return SENSOR_ENCODER.encodeGlobalSensorName(sensor); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/sensors/SensorsFactory.java b/src/java/org/apache/cassandra/sensors/SensorsFactory.java new file mode 100644 index 000000000000..6bc49896955f --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/SensorsFactory.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +import java.util.Optional; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.SENSORS_FACTORY; + +/** + * Provides a factory to customize the behaviour of sensors tracking in CNDB by providing two factory methods: + *
      + *
    • {@link SensorsFactory#createRequestSensors} provides a {@link RequestSensors} implementation to track sensors per keyspace.
    • + *
    • {@link SensorsFactory#createSensorEncoder} provides a {@link SensorEncoder} implementation to control how sensors are encoded as string on the wire.
    • + *
    + * The concrete implementation of this factory is configured by the {@link CassandraRelevantProperties#SENSORS_FACTORY} system property. + */ +public interface SensorsFactory +{ + SensorsFactory instance = SENSORS_FACTORY.getString() == null ? + new SensorsFactory() {} : + FBUtilities.construct(CassandraRelevantProperties.SENSORS_FACTORY.getString(), "sensors factory"); + + SensorEncoder NOOP_SENSOR_ENCODER = new SensorEncoder() + { + @Override + public Optional encodeRequestSensorName(Sensor sensor) + { + return Optional.empty(); + } + + @Override + public Optional encodeGlobalSensorName(Sensor sensor) + { + return Optional.empty(); + } + }; + + /** + * Creates {@link RequestSensors} for the given keyspaces. This method is invoked by coordinators and replicas when + * handling requests at various stages/thread pools (e.g. when processing CQL queries or when applying verbs). + * Consequently, implementations should be very efficient. + * + * @param keyspaces the keyspaces associated with the request. + * @return a {@link RequestSensors} instance. The default implementation returns a singleton no-op instance. + */ + default RequestSensors createRequestSensors(String... keyspaces) + { + return NoOpRequestSensors.instance; + } + + /** + * Create a {@link SensorEncoder} that will be invoked when encoding the sensor on the wire. The default implementation returns a noop encoder. + */ + default SensorEncoder createSensorEncoder() + { + return NOOP_SENSOR_ENCODER; + } +} diff --git a/src/java/org/apache/cassandra/sensors/SensorsRegistry.java b/src/java/org/apache/cassandra/sensors/SensorsRegistry.java new file mode 100644 index 000000000000..fa30338ca506 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/SensorsRegistry.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.sensors; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.Striped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaChangeListener; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.concurrent.Timer; + +/** + * This class tracks {@link Sensor}s at a "global" level, allowing to: + *
      + *
    • Getting or creating (if not existing) sensors of a given {@link Context} and {@link Type}.
    • + *
    • Accessing sensors by keyspace, table id or type.
    • + *
    + * The returned sensors are global, meaning that their value spans across requests/responses, but cannot be modified either + * directly or indirectly via this class (whose update methods are package protected). In order to modify a sensor value, + * it must be registered to a request/response via {@link RequestSensors#registerSensor(Context, Type)} and incremented via + * {@link RequestSensors#incrementSensor(Context, Type, double)}, then synced via {@link RequestSensors#syncAllSensors()}, which + * will update the related global sensors. + *

    + * Given sensors are tied to a context, that is to a given keyspace and table, their global instance will be deleted + * if the related keyspace/table is dropped. + *

    + * It's also possible to: + *
      + *
    • + * Register listeners via the {@link #registerListener(SensorsRegistryListener)} method. + * Such listeners will get notified on creation and removal of sensors. + *
    • + *
    • + * Unregister listeners via the {@link #unregisterListener(SensorsRegistryListener)} method. + * Such listeners will not be notified anymore about creation or removal of sensors. + *
    • + *
    + */ +public class SensorsRegistry implements SchemaChangeListener +{ + private static final int LOCK_SRIPES = 1024; + + public static final SensorsRegistry instance = new SensorsRegistry(); + private static final Logger logger = LoggerFactory.getLogger(SensorsRegistry.class); + + private final Timer asyncUpdater = Timer.INSTANCE; + + private final Striped stripedUpdateLock = Striped.readWriteLock(LOCK_SRIPES); // we stripe per keyspace + + private final Set keyspaces = Sets.newConcurrentHashSet(); + private final Set tableIds = Sets.newConcurrentHashSet(); + + // Using Map of array values for performance reasons to avoid wrapping key into another Object, e.g. Pair(context,type)). + // Note that array values can contain NULL so be careful to filter NULLs when iterating over array + private final ConcurrentMap identity = new ConcurrentHashMap<>(); + + private final ConcurrentMap> byKeyspace = new ConcurrentHashMap<>(); + private final ConcurrentMap> byTableId = new ConcurrentHashMap<>(); + private final ConcurrentMap> byType = new ConcurrentHashMap<>(); + + private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); + + private SensorsRegistry() + { + Schema.instance.registerListener(this); + } + + public void registerListener(SensorsRegistryListener listener) + { + listeners.add(listener); + logger.debug("Listener {} registered", listener); + } + + public void unregisterListener(SensorsRegistryListener listener) + { + listeners.remove(listener); + logger.debug("Listener {} unregistered", listener); + } + + public Optional getSensor(Context context, Type type) + { + return Optional.ofNullable(getSensorFast(context, type)); + } + + public Optional getOrCreateSensor(Context context, Type type) + { + return Optional.ofNullable(getOrCreateSensorFast(context, type)); + } + + protected void incrementSensor(Context context, Type type, double value) + { + Sensor sensor = getOrCreateSensorFast(context, type); + if (sensor != null) + sensor.increment(value); + } + + protected Future incrementSensorAsync(Context context, Type type, double value, long delay, TimeUnit unit) + { + return asyncUpdater.onTimeout(() -> + getOrCreateSensor(context, type).ifPresent(s -> s.increment(value)), + delay, unit); + } + + public Set getSensorsByKeyspace(String keyspace) + { + return Optional.ofNullable(byKeyspace.get(keyspace)).orElseGet(() -> ImmutableSet.of()); + } + + public Set getSensorsByTableId(String tableId) + { + return Optional.ofNullable(byTableId.get(tableId)).orElseGet(() -> ImmutableSet.of()); + } + + public Set getSensorsByType(Type type) + { + return Optional.ofNullable(byType.get(type.name())).orElseGet(() -> ImmutableSet.of()); + } + + public void removeSensorsByKeyspace(String keyspaceName) + { + stripedUpdateLock.getAt(getLockStripe(keyspaceName.hashCode())).writeLock().lock(); + try + { + byKeyspace.remove(keyspaceName); + + Set removed = removeSensorArrays(ImmutableSet.of(identity.values()), s -> s.getContext().getKeyspace().equals(keyspaceName)); + removed.forEach(this::notifyOnSensorRemoved); + + removeSensor(byTableId.values(), s -> s.getContext().getKeyspace().equals(keyspaceName)); + removeSensor(byType.values(), s -> s.getContext().getKeyspace().equals(keyspaceName)); + } + finally + { + stripedUpdateLock.getAt(getLockStripe(keyspaceName.hashCode())).writeLock().unlock(); + } + } + + public void removeSensorsByTableId(String keyspaceName, String tableId) + { + stripedUpdateLock.getAt(getLockStripe(keyspaceName.hashCode())).writeLock().lock(); + try + { + Set removed = removeSensorArrays(ImmutableSet.of(identity.values()), s -> s.getContext().getTableId().equals(tableId)); + removed.forEach(this::notifyOnSensorRemoved); + + byTableId.remove(tableId); + removeSensor(byType.values(), s -> s.getContext().getTableId().equals(tableId)); + } + finally + { + stripedUpdateLock.getAt(getLockStripe(keyspaceName.hashCode())).writeLock().unlock(); + } + } + + @Override + public void onCreateKeyspace(KeyspaceMetadata keyspace) + { + keyspaces.add(keyspace.name); + } + + @Override + public void onCreateTable(TableMetadata table) + { + tableIds.add(table.id.toString()); + } + + @Override + public void onDropKeyspace(KeyspaceMetadata keyspace, boolean dropData) + { + stripedUpdateLock.getAt(getLockStripe(keyspace.name.hashCode())).writeLock().lock(); + try + { + keyspaces.remove(keyspace.name); + byKeyspace.remove(keyspace.name); + + Set removed = removeSensorArrays(ImmutableSet.of(identity.values()), s -> s.getContext().getKeyspace().equals(keyspace.name)); + removed.forEach(this::notifyOnSensorRemoved); + + removeSensor(byTableId.values(), s -> s.getContext().getKeyspace().equals(keyspace.name)); + removeSensor(byType.values(), s -> s.getContext().getKeyspace().equals(keyspace.name)); + } + finally + { + stripedUpdateLock.getAt(getLockStripe(keyspace.name.hashCode())).writeLock().unlock(); + } + } + + @Override + public void onDropTable(TableMetadata table, boolean dropData) + { + stripedUpdateLock.getAt(getLockStripe(table.keyspace.hashCode())).writeLock().lock(); + try + { + String tableId = table.id.toString(); + tableIds.remove(tableId); + byTableId.remove(tableId); + + Set removed = removeSensorArrays(ImmutableSet.of(identity.values()), s -> s.getContext().getTableId().equals(tableId)); + removed.forEach(this::notifyOnSensorRemoved); + + removeSensor(byKeyspace.values(), s -> s.getContext().getTableId().equals(tableId)); + removeSensor(byType.values(), s -> s.getContext().getTableId().equals(tableId)); + } + finally + { + stripedUpdateLock.getAt(getLockStripe(table.keyspace.hashCode())).writeLock().unlock(); + } + } + + private static int getLockStripe(int hashCode) + { + return Math.abs(hashCode) % LOCK_SRIPES; + } + + /** + * Remove sensors from a collection of candidates based on the given predicate + * + * @param candidates the candidates to remove from + * @param accept the predicate used to select the sensors to remove + * @return the set of removed sensors + */ + private Set removeSensor(Collection> candidates, Predicate accept) + { + Set removed = new HashSet<>(); + + for (Collection sensors : candidates) + { + Iterator sensorIt = sensors.iterator(); + while (sensorIt.hasNext()) + { + Sensor sensor = sensorIt.next(); + if (!accept.test(sensor)) + continue; + + sensorIt.remove(); + removed.add(sensor); + } + } + + return removed; + } + + /** + * To get best perfromance we are not returning Optional here + */ + @Nullable + private Sensor getSensorFast(Context context, Type type) + { + Sensor[] typeSensors = identity.get(context); + return typeSensors != null ? typeSensors[type.ordinal()] : null; + } + + /** + * To get best perfromance we are not returning Optional here + */ + @Nullable + private Sensor getOrCreateSensorFast(Context context, Type type) + { + Sensor sensor = getSensorFast(context, type); + if (sensor != null) + return sensor; + + stripedUpdateLock.getAt(getLockStripe(context.getKeyspace().hashCode())).readLock().lock(); + try + { + if (!keyspaces.contains(context.getKeyspace()) || !tableIds.contains(context.getTableId())) + return null; + + Sensor[] typeSensors = identity.compute(context, (key, types) -> { + Sensor[] computed = types != null ? types : new Sensor[Type.values().length]; + if (computed[type.ordinal()] == null) + { + computed[type.ordinal()] = new Sensor(context, type); + notifyOnSensorCreated(computed[type.ordinal()]); + } + return computed; + }); + sensor = typeSensors[type.ordinal()]; + + Set keyspaceSet = byKeyspace.get(sensor.getContext().getKeyspace()); + keyspaceSet = keyspaceSet != null ? keyspaceSet : byKeyspace.computeIfAbsent(sensor.getContext().getKeyspace(), (ignored) -> Sets.newConcurrentHashSet()); + keyspaceSet.add(sensor); + + Set tableSet = byTableId.get(sensor.getContext().getTableId()); + tableSet = tableSet != null ? tableSet : byTableId.computeIfAbsent(sensor.getContext().getTableId(), (ignored) -> Sets.newConcurrentHashSet()); + tableSet.add(sensor); + + Set opSet = byType.get(sensor.getType().name()); + opSet = opSet != null ? opSet : byType.computeIfAbsent(sensor.getType().name(), (ignored) -> Sets.newConcurrentHashSet()); + opSet.add(sensor); + + return sensor; + } + finally + { + stripedUpdateLock.getAt(getLockStripe(context.getKeyspace().hashCode())).readLock().unlock(); + } + } + + /** + * Removes array of sensors if any sensor in the array matches the predicate. + * This function is used by `identity` map that holds an array of Sensors (each item in the array maps to Type) + */ + private Set removeSensorArrays(Collection> candidates, Predicate accept) + { + Set removed = new HashSet<>(); + + for (Collection sensors : candidates) + { + Iterator sensorIt = sensors.iterator(); + while (sensorIt.hasNext()) + { + List typeSensors = Arrays.stream(sensorIt.next()).filter(Objects::nonNull).collect(Collectors.toList()); + if (typeSensors.size() > 0 && accept.test(typeSensors.get(0))) + { + removed.addAll(typeSensors); + sensorIt.remove(); + } + } + } + + return removed; + } + + @VisibleForTesting + public void clear() + { + keyspaces.clear(); + tableIds.clear(); + identity.clear(); + byKeyspace.clear(); + byTableId.clear(); + byType.clear(); + } + + private void notifyOnSensorCreated(Sensor sensor) + { + tryNotifyListeners(sensor, SensorsRegistryListener::onSensorCreated, "created"); + } + + private void notifyOnSensorRemoved(Sensor sensor) + { + tryNotifyListeners(sensor, SensorsRegistryListener::onSensorRemoved, "removed"); + } + + private void tryNotifyListeners(Sensor sensor, BiConsumer notification, String action) + { + for (SensorsRegistryListener l: listeners) + { + try + { + notification.accept(l, sensor); + logger.trace("Listener {} correctly notified on sensor {} being {}", l, sensor, action); + } + catch (Throwable t) + { + logger.error("Failed to notify listener {} on sensor {} being {}", l, sensor, action); + } + } + } +} diff --git a/src/java/org/apache/cassandra/sensors/SensorsRegistryListener.java b/src/java/org/apache/cassandra/sensors/SensorsRegistryListener.java new file mode 100644 index 000000000000..9fc52f1a4b7f --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/SensorsRegistryListener.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +/** + * Listener that gets notified by the {@link SensorsRegistry} + * about the creation and removal of {@link Sensor}s. + */ +public interface SensorsRegistryListener +{ + /** + * React to the creation of a new sensor + * @param sensor the sensor just created + */ + void onSensorCreated(Sensor sensor); + + /** + * React to the removal of a sensor + * @param sensor the sensor just removed + */ + void onSensorRemoved(Sensor sensor); +} diff --git a/src/java/org/apache/cassandra/sensors/Type.java b/src/java/org/apache/cassandra/sensors/Type.java new file mode 100644 index 000000000000..25fad4e2e2bd --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/Type.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors; + +/** + * The type of the measurement a {@link Sensor} refers to. + */ +public enum Type +{ + INTERNODE_BYTES, + + READ_BYTES, + + WRITE_BYTES, + INDEX_WRITE_BYTES +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/sensors/read/TrackingRowIterator.java b/src/java/org/apache/cassandra/sensors/read/TrackingRowIterator.java new file mode 100644 index 000000000000..c6b64bc34d60 --- /dev/null +++ b/src/java/org/apache/cassandra/sensors/read/TrackingRowIterator.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sensors.read; + +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Sensor; +import org.apache.cassandra.sensors.Type; + +/** + * Increment {@link Type#READ_BYTES} {@link Sensor}s for a given {@link Context } by the data size of each iterated row and sync the sensor values + * when the iterator is closed. + */ +public class TrackingRowIterator extends Transformation +{ + private final RequestTracker requestTracker; + private final Context context; + + public TrackingRowIterator(Context context) + { + this.requestTracker = RequestTracker.instance; + this.context = context; + } + + @Override + public UnfilteredRowIterator applyToPartition(UnfilteredRowIterator iter) + { + return Transformation.apply(iter, this); + } + + @Override + public Row applyToStatic(Row row) + { + // TODO: Not worth tracking the static row? + return row; + } + + @Override + public Row applyToRow(Row row) + { + RequestSensors sensors = requestTracker.get(); + if (sensors != null && row.isRow()) + sensors.incrementSensor(context, Type.READ_BYTES, row.dataSize()); + + return row; + } + + @Override + protected void onClose() + { + super.onClose(); + + RequestSensors sensors = requestTracker.get(); + if (sensors != null) + sensors.syncAllSensors(); + } +} diff --git a/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java b/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java index 1be4d61f68ec..8561f2ca02f2 100644 --- a/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java +++ b/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java @@ -45,6 +45,8 @@ public void serializeList(List> types, DataOutputPlus out) throw serialize(type, out); } + // Used only in serialization header, when deserializing a type from the sstable header, + // not used in commit log or internode transport. public AbstractType deserialize(DataInputPlus in) throws IOException { ByteBuffer raw = ByteBufferUtil.readWithVIntLength(in); @@ -72,4 +74,4 @@ public long serializedListSize(List> types) size += serializedSize(type); return size; } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/serializers/CollectionSerializer.java b/src/java/org/apache/cassandra/serializers/CollectionSerializer.java index b514d2490b0a..ca3371857863 100644 --- a/src/java/org/apache/cassandra/serializers/CollectionSerializer.java +++ b/src/java/org/apache/cassandra/serializers/CollectionSerializer.java @@ -229,4 +229,45 @@ public void forEach(ByteBuffer input, Consumer action) throw new MarshalException("Not enough bytes to read a set"); } } + + /** + * Checks if the specified serialized collection contains the specified serialized collection element. + * + * @param elementType the type of the collection elements + * @param collection a serialized collection + * @param element a serialized collection element + * @param hasKeys whether the collection has keys, that is, it's a map + * @param getKeys whether to check keys or values + * @return {@code true} if the collection contains the element, {@code false} otherwise + */ + public static boolean contains(AbstractType elementType, + ByteBuffer collection, + ByteBuffer element, + boolean hasKeys, + boolean getKeys) + { + assert hasKeys || !getKeys; + int size = readCollectionSize(collection, ByteBufferAccessor.instance); + int offset = sizeOfCollectionSize(); + + for (int i = 0; i < size; i++) + { + // read the key (if the collection has keys) + if (hasKeys) + { + ByteBuffer key = readValue(collection, ByteBufferAccessor.instance, offset); + if (getKeys && elementType.compare(key, element) == 0) + return true; + offset += sizeOfValue(key, ByteBufferAccessor.instance); + } + + // read the value + ByteBuffer value = readValue(collection, ByteBufferAccessor.instance, offset); + if (!getKeys && elementType.compare(value, element) == 0) + return true; + offset += sizeOfValue(value, ByteBufferAccessor.instance); + } + + return false; + } } diff --git a/src/java/org/apache/cassandra/serializers/DateRangeSerializer.java b/src/java/org/apache/cassandra/serializers/DateRangeSerializer.java new file mode 100644 index 000000000000..c7cd84094f7b --- /dev/null +++ b/src/java/org/apache/cassandra/serializers/DateRangeSerializer.java @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.serializers; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.marshal.ValueAccessor; +import org.apache.cassandra.db.marshal.datetime.DateRange; +import org.apache.cassandra.db.marshal.datetime.DateRange.DateRangeBound.Precision; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.utils.ByteBufferUtil; + +///** +// * Responsible for {@link DateRange} serialization/deserialization with respect to the following format: +// * ------------------------- +// * [[]] +// * +// * Where: +// * +// * is a [byte] encoding of +// * - 0x00 - single value as in "2001-01-01" +// * - 0x01 - closed range as in "[2001-01-01 TO 2001-01-31]" +// * - 0x02 - open range high as in "[2001-01-01 TO *]" +// * - 0x03 - open range low as in "[* TO 2001-01-01]" +// * - 0x04 - both ranges open as in "[* TO *]" +// * - 0x05 - single value open as in "*" +// * +// * is an optional [long] millisecond offset from epoch. Absent for in [4,5], present otherwise. +// * Represents a single date value for = 0, the range start for in [1,2], or range end for = 3. +// * +// * is an optional [byte]s and represents the precision of field . Absent for in [4,5], present otherwise. +// * Possible values are: +// * - 0x00 - year +// * - 0x01 - month +// * - 0x02 - day +// * - 0x03 - hour +// * - 0x04 - minute +// * - 0x05 - second +// * - 0x06 - millisecond +// * +// * is an optional [long] millisecond offset from epoch. Represents the range end for = 1. Not present +// * otherwise. +// * +// * is an optional [byte] and represents the precision of field . Only present if = 1. Values +// * are the same as for . +// */ +public final class DateRangeSerializer extends TypeSerializer +{ + public static final DateRangeSerializer instance = new DateRangeSerializer(); + + // e.g. [2001-01-01] + private final static byte DATE_RANGE_TYPE_SINGLE_DATE = 0x00; + // e.g. [2001-01-01 TO 2001-01-31] + private final static byte DATE_RANGE_TYPE_CLOSED_RANGE = 0x01; + // e.g. [2001-01-01 TO *] + private final static byte DATE_RANGE_TYPE_OPEN_RANGE_HIGH = 0x02; + // e.g. [* TO 2001-01-01] + private final static byte DATE_RANGE_TYPE_OPEN_RANGE_LOW = 0x03; + // [* TO *] + private final static byte DATE_RANGE_TYPE_BOTH_OPEN_RANGE = 0x04; + // * + private final static byte DATE_RANGE_TYPE_SINGLE_DATE_OPEN = 0x05; + + /** + * Size of the single serialized DateRange boundary. As specified in @{@link DateRangeSerializer}. + * + * Tightly coupled with {@link #deserializeDateRangeLowerBound(int, Object, ValueAccessor)} and + * {@link #deserializeDateRangeUpperBound(int, Object, ValueAccessor)}. + */ + private final static int SERIALIZED_DATE_RANGE_BOUND_SIZE = TypeSizes.LONG_SIZE + TypeSizes.BYTE_SIZE; + + private static final List VALID_SERIALIZED_LENGTHS = ImmutableList.of( + // types: 0x04, 0x05 + Byte.BYTES, + // types: 0x00, 0x02, 0x03 + Byte.BYTES + Long.BYTES + Byte.BYTES, + // types: 0x01 + Byte.BYTES + Long.BYTES + Byte.BYTES + Long.BYTES + Byte.BYTES + ); + + + @Override + public ByteBuffer serialize(DateRange dateRange) + { + if (dateRange == null) + { + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + } + + byte rangeType = encodeType(dateRange); + + int bufferSize = 1; + if (!dateRange.getLowerBound().isUnbounded()) + { + bufferSize += 9; + } + if (dateRange.isUpperBoundDefined() && !dateRange.getUpperBound().isUnbounded()) + { + bufferSize += 9; + } + + try (DataOutputBuffer output = new DataOutputBuffer(bufferSize)) + { + output.writeByte(rangeType); + DateRange.DateRangeBound lowerBound = dateRange.getLowerBound(); + if (!lowerBound.isUnbounded()) + { + output.writeLong(lowerBound.getTimestamp().toEpochMilli()); + output.writeByte(lowerBound.getPrecision().toEncoded()); + } + + if (dateRange.isUpperBoundDefined()) + { + DateRange.DateRangeBound upperBound = dateRange.getUpperBound(); + if (!upperBound.isUnbounded()) + { + output.writeLong(upperBound.getTimestamp().toEpochMilli()); + output.writeByte(upperBound.getPrecision().toEncoded()); + } + } + return output.buffer(); + } + catch (IOException e) + { + throw new AssertionError("Unexpected error", e); + } + } + + @Override + public DateRange deserialize(V value, ValueAccessor accessor) + { + if (accessor.isEmpty(value)) + { + return null; + } + + try + { + byte type = accessor.toByte(value); + int offset = TypeSizes.BYTE_SIZE; + switch (type) + { + case DATE_RANGE_TYPE_SINGLE_DATE: + return new DateRange(deserializeDateRangeLowerBound(offset, value, accessor)); + case DATE_RANGE_TYPE_CLOSED_RANGE: + DateRange.DateRangeBound lowerBound = deserializeDateRangeLowerBound(offset, value, accessor); + offset += SERIALIZED_DATE_RANGE_BOUND_SIZE; + DateRange.DateRangeBound upperBound = deserializeDateRangeUpperBound(offset, value, accessor); + return new DateRange(lowerBound, upperBound); + case DATE_RANGE_TYPE_OPEN_RANGE_HIGH: + return new DateRange(deserializeDateRangeLowerBound(offset, value, accessor), DateRange.DateRangeBound.UNBOUNDED); + case DATE_RANGE_TYPE_OPEN_RANGE_LOW: + return new DateRange(DateRange.DateRangeBound.UNBOUNDED, deserializeDateRangeUpperBound(offset, value, accessor)); + case DATE_RANGE_TYPE_BOTH_OPEN_RANGE: + return new DateRange(DateRange.DateRangeBound.UNBOUNDED, DateRange.DateRangeBound.UNBOUNDED); + case DATE_RANGE_TYPE_SINGLE_DATE_OPEN: + return new DateRange(DateRange.DateRangeBound.UNBOUNDED); + default: + throw new IllegalArgumentException("Unknown date range type: " + type); + } + } + catch (IOException e) + { + throw new AssertionError("Unexpected error", e); + } + } + + @Override + public void validate(V value, ValueAccessor accessor) throws MarshalException + { + if (!VALID_SERIALIZED_LENGTHS.contains(accessor.size(value))) + { + throw new MarshalException(String.format("Date range should be have %s bytes, got %d instead.", VALID_SERIALIZED_LENGTHS, accessor.size(value))); + } + DateRange dateRange = deserialize(value, accessor); + validateDateRange(dateRange); + } + + @Override + public String toString(DateRange dateRange) + { + return dateRange == null ? "" : dateRange.formatToSolrString(); + } + + @Override + public Class getType() + { + return DateRange.class; + } + + private byte encodeType(DateRange dateRange) + { + if (dateRange.isUpperBoundDefined()) + { + if (dateRange.getLowerBound().isUnbounded()) + { + return dateRange.getUpperBound().isUnbounded() ? DATE_RANGE_TYPE_BOTH_OPEN_RANGE : DATE_RANGE_TYPE_OPEN_RANGE_LOW; + } + else + { + return dateRange.getUpperBound().isUnbounded() ? DATE_RANGE_TYPE_OPEN_RANGE_HIGH : DATE_RANGE_TYPE_CLOSED_RANGE; + } + } + else + { + return dateRange.getLowerBound().isUnbounded() ? DATE_RANGE_TYPE_SINGLE_DATE_OPEN : DATE_RANGE_TYPE_SINGLE_DATE; + } + } + + private DateRange.DateRangeBound deserializeDateRangeLowerBound(int offset, V value, ValueAccessor accessor) throws IOException + { + long epochMillis = accessor.getLong(value, offset); + offset += TypeSizes.LONG_SIZE; + Precision precision = Precision.fromEncoded(accessor.getByte(value, offset)); + return DateRange.DateRangeBound.lowerBound(Instant.ofEpochMilli(epochMillis), precision); + } + + private DateRange.DateRangeBound deserializeDateRangeUpperBound(int offset, V value, ValueAccessor accessor) throws IOException + { + long epochMillis = accessor.getLong(value, offset); + offset += TypeSizes.LONG_SIZE; + Precision precision = Precision.fromEncoded(accessor.getByte(value, offset)); + return DateRange.DateRangeBound.upperBound(Instant.ofEpochMilli(epochMillis), precision); + } + + private void validateDateRange(DateRange dateRange) + { + if (dateRange != null && !dateRange.getLowerBound().isUnbounded() && dateRange.isUpperBoundDefined() && !dateRange.getUpperBound().isUnbounded()) + { + if (dateRange.getLowerBound().getTimestamp().isAfter(dateRange.getUpperBound().getTimestamp())) + { + throw new MarshalException(String.format("Lower bound of a date range should be before upper bound, got: %s", + dateRange.formatToSolrString())); + } + } + } +} diff --git a/src/java/org/apache/cassandra/serializers/SimpleDateSerializer.java b/src/java/org/apache/cassandra/serializers/SimpleDateSerializer.java index 764565c384eb..b5da62e214bd 100644 --- a/src/java/org/apache/cassandra/serializers/SimpleDateSerializer.java +++ b/src/java/org/apache/cassandra/serializers/SimpleDateSerializer.java @@ -70,14 +70,7 @@ public static int dateStringToDays(String source) throws MarshalException { LocalDate parsed = formatter.parse(source, LocalDate::from); long millis = parsed.atStartOfDay(UTC).toInstant().toEpochMilli(); - if (millis < minSupportedDateMillis) - throw new MarshalException(String.format("Input date %s is less than min supported date %s", source, - ZonedDateTime.ofInstant(Instant.ofEpochMilli(minSupportedDateMillis), UTC).toString())); - if (millis > maxSupportedDateMillis) - throw new MarshalException(String.format("Input date %s is greater than max supported date %s", source, - ZonedDateTime.ofInstant(Instant.ofEpochMilli(maxSupportedDateMillis), UTC).toString())); - - return timeInMillisToDay(millis); + return timeInMillisToDay(source, millis); } catch (DateTimeParseException| ArithmeticException e1) { @@ -107,6 +100,23 @@ private static int parseRaw(String source) { public static int timeInMillisToDay(long millis) { + return timeInMillisToDay(null, millis); + } + + private static int timeInMillisToDay(String source, long millis) + { + if (millis < minSupportedDateMillis) + { + throw new MarshalException(String.format("Input date %s is less than min supported date %s", + null == source ? ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC).toLocalDate() : source, + ZonedDateTime.ofInstant(Instant.ofEpochMilli(minSupportedDateMillis), UTC).toLocalDate())); + } + if (millis > maxSupportedDateMillis) + { + throw new MarshalException(String.format("Input date %s is greater than max supported date %s", + null == source ? ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC).toLocalDate() : source, + ZonedDateTime.ofInstant(Instant.ofEpochMilli(maxSupportedDateMillis), UTC).toLocalDate())); + } return (int) (Duration.ofMillis(millis).toDays() - Integer.MIN_VALUE); } diff --git a/src/java/org/apache/cassandra/serializers/TupleSerializer.java b/src/java/org/apache/cassandra/serializers/TupleSerializer.java index afdf2484db12..0813a322ec72 100644 --- a/src/java/org/apache/cassandra/serializers/TupleSerializer.java +++ b/src/java/org/apache/cassandra/serializers/TupleSerializer.java @@ -19,16 +19,18 @@ import java.util.List; +import com.google.common.collect.ImmutableList; + import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.ValueAccessor; public class TupleSerializer extends BytesSerializer { - public final List> fields; + public final ImmutableList> fields; public TupleSerializer(List> fields) { - this.fields = fields; + this.fields = ImmutableList.copyOf(fields); } public void validate(V input, ValueAccessor accessor) throws MarshalException diff --git a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java index 12a917f048a5..ab83416a65e1 100644 --- a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.service; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,6 +28,7 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,9 +46,13 @@ import org.apache.cassandra.locator.ReplicaPlan.ForWrite; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.concurrent.Condition; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import org.apache.cassandra.metrics.ReplicaResponseSizeMetrics; +import org.apache.cassandra.net.MessagingService; import static java.lang.Long.MAX_VALUE; import static java.lang.Math.min; @@ -73,13 +79,16 @@ public abstract class AbstractWriteResponseHandler implements RequestCallback protected final Runnable callback; protected final WriteType writeType; - private static final AtomicIntegerFieldUpdater failuresUpdater = + protected static final AtomicIntegerFieldUpdater failuresUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractWriteResponseHandler.class, "failures"); private volatile int failures = 0; - private final Map failureReasonByEndpoint; + // Used by CNDB + protected final Map failureReasonByEndpoint; private final Dispatcher.RequestTime requestTime; private @Nullable final Supplier hintOnFailure; + private final RequestSensors requestSensors; + /** * Delegate to another WriteResponseHandler or possibly this one to track if the ideal consistency level was reached. * Will be set to null if ideal CL was not configured @@ -107,21 +116,22 @@ protected AbstractWriteResponseHandler(ForWrite replicaPlan, Runnable callback, this.hintOnFailure = hintOnFailure; this.failureReasonByEndpoint = new ConcurrentHashMap<>(); this.requestTime = requestTime; + this.requestSensors = RequestTracker.instance.get(); } - public void get() throws WriteTimeoutException, WriteFailureException + public int failures() { - long timeoutNanos = currentTimeoutNanos(); + return failures; + } - boolean signaled; - try - { - signaled = condition.await(timeoutNanos, NANOSECONDS); - } - catch (InterruptedException e) - { - throw new UncheckedInterruptedException(e); - } + public Map failureReasonByEndpoint() + { + return Collections.unmodifiableMap(failureReasonByEndpoint); + } + + public void get() throws WriteTimeoutException, WriteFailureException + { + boolean signaled = await(); if (!signaled) throwTimeout(); @@ -137,6 +147,19 @@ public void get() throws WriteTimeoutException, WriteFailureException } } + public boolean await() throws UncheckedInterruptedException + { + long timeoutNanos = currentTimeoutNanos(); + try + { + return condition.await(timeoutNanos, NANOSECONDS); + } + catch (InterruptedException e) + { + throw new UncheckedInterruptedException(e); + } + } + private void throwTimeout() { int blockedFor = blockFor(); @@ -149,7 +172,7 @@ private void throwTimeout() throw new WriteTimeoutException(writeType, replicaPlan.consistencyLevel(), acks, blockedFor); } - public final long currentTimeoutNanos() + public long currentTimeoutNanos() { long now = nanoTime(); long requestTimeout = writeType == COUNTER @@ -158,6 +181,27 @@ public final long currentTimeoutNanos() return requestTime.computeTimeout(now, requestTimeout); } + public ReplicaPlan.ForWrite replicaPlan() + { + return replicaPlan; + } + + public WriteType writeType() + { + return writeType; + } + + public Dispatcher.RequestTime requestTime() + { + return requestTime; + } + + // Used by CNDB + public Supplier hintOnFailure() + { + return hintOnFailure; + } + /** * Set a delegate ideal CL write response handler. Note that this could be the same as this * if the ideal CL and requested CL are the same. @@ -224,7 +268,7 @@ public final void expired() /** * @return the minimum number of endpoints that must respond. */ - protected int blockFor() + public int blockFor() { // During bootstrap, we have to include the pending endpoints or we may fail the consistency level // guarantees (see #833) @@ -236,7 +280,7 @@ protected int blockFor() * this needs to be aware of which nodes are live/down * @return the total number of endpoints the request can send to. */ - protected int candidateReplicaCount() + public int candidateReplicaCount() { if (replicaPlan.consistencyLevel().isDatacenterLocal()) return countInOurDc(replicaPlan.liveAndDown()).allReplicas(); @@ -252,7 +296,7 @@ public ConsistencyLevel consistencyLevel() /** * @return true if the message counts towards the blockFor() threshold */ - protected boolean waitingFor(InetAddressAndPort from) + public boolean waitingFor(InetAddressAndPort from) { return true; } @@ -260,7 +304,7 @@ protected boolean waitingFor(InetAddressAndPort from) /** * @return number of responses received */ - protected abstract int ackCount(); + public abstract int ackCount(); public Dispatcher.RequestTime getRequestTime() { @@ -271,8 +315,26 @@ public Dispatcher.RequestTime getRequestTime() * null message means "response from local write" */ public abstract void onResponse(Message msg); + + /** + * Track the size of a response message from a replica + * @param msg the response message + */ + protected void trackReplicaResponseSize(Message msg) + { + if (!ReplicaResponseSizeMetrics.isMetricsEnabled()) + return; + + // Only track remote responses (local responses have null from field) + // Also check that we have a valid payload and serializer + if (msg != null && msg.from() != null && msg.payload != null && msg.verb().serializer() != null) + { + int responseSize = msg.payloadSize(MessagingService.current_version); + ReplicaResponseSizeMetrics.recordWriteResponseSize(responseSize); + } + } - protected void signal() + public void signal() { //The ideal CL should only count as a strike if the requested CL was achieved. //If the requested CL is not achieved it's fine for the ideal CL to also not be achieved. @@ -290,6 +352,23 @@ protected void signal() callback.run(); } + /** + * @return true if condition is signaled either for success or failure + */ + @VisibleForTesting + public boolean isCompleted() + { + return condition.isSignalled(); + } + + /** + * @return true if condition is signaled for failure + */ + public boolean isCompletedExceptionally() + { + return isCompleted() && blockFor() + failures > candidateReplicaCount(); + } + @Override public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) { @@ -316,6 +395,12 @@ public boolean invokeOnFailure() return true; } + @Override + public RequestSensors getRequestSensors() + { + return requestSensors; + } + /** * Decrement the counter for all responses/expirations and if the counter * hits 0 check to see if the ideal consistency level (this write response handler) diff --git a/src/java/org/apache/cassandra/service/ActiveRepairService.java b/src/java/org/apache/cassandra/service/ActiveRepairService.java index e120122c0844..0fcf7196aca5 100644 --- a/src/java/org/apache/cassandra/service/ActiveRepairService.java +++ b/src/java/org/apache/cassandra/service/ActiveRepairService.java @@ -41,6 +41,9 @@ import com.google.common.collect.Lists; import com.google.common.collect.Multimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DurationSpec; @@ -50,6 +53,7 @@ import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.EndpointsByRange; import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.service.disk.usage.DiskUsageMonitor; import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.repair.state.CoordinatorState; import org.apache.cassandra.repair.state.ParticipateState; @@ -61,8 +65,6 @@ import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.AsyncPromise; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; @@ -86,6 +88,7 @@ import org.apache.cassandra.repair.NoSuchRepairSessionException; import org.apache.cassandra.service.paxos.PaxosRepair; import org.apache.cassandra.service.paxos.cleanup.PaxosCleanup; +import org.apache.cassandra.repair.ParentRepairSessionListener; import org.apache.cassandra.repair.RepairJobDesc; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.repair.RepairSession; @@ -124,6 +127,8 @@ import static org.apache.cassandra.net.Verb.PREPARE_MSG; import static org.apache.cassandra.repair.messages.RepairMessage.notDone; import static org.apache.cassandra.utils.Simulate.With.MONITORS; +import static org.apache.cassandra.net.Verb.SYNC_RSP; +import static org.apache.cassandra.net.Verb.VALIDATION_RSP; /** * ActiveRepairService is the starting point for manual "active" repairs. @@ -440,6 +445,7 @@ public RepairSession submitRepairSession(TimeUUID parentRepairSession, String keyspace, RepairParallelism parallelismDegree, boolean isIncremental, + boolean pushRepair, boolean pullRepair, PreviewKind previewKind, boolean optimiseStreams, @@ -459,7 +465,7 @@ public RepairSession submitRepairSession(TimeUUID parentRepairSession, return null; final RepairSession session = new RepairSession(ctx, validationScheduler, parentRepairSession, range, keyspace, - parallelismDegree, isIncremental, pullRepair, + parallelismDegree, isIncremental, pushRepair, pullRepair, previewKind, optimiseStreams, repairPaxos, paxosOnly, cfnames); repairs.getIfPresent(parentRepairSession).register(session.state); @@ -517,7 +523,11 @@ public synchronized void terminateSessions() { session.forceShutdown(cause); } + Collection> sessions = new ArrayList<>(parentRepairSessions.entrySet()); parentRepairSessions.clear(); + + for (Map.Entry e : sessions) + ParentRepairSessionListener.instance.onRemoved(e.getKey(), e.getValue()); } public void recordRepairStatus(int cmd, ParentRepairStatus parentRepairStatus, List messages) @@ -645,8 +655,26 @@ public boolean verifyCompactionsPendingThreshold(TimeUUID parentRepairSession, P return true; } + public boolean verifyDiskHeadroomThreshold(TimeUUID parentRepairSession, PreviewKind previewKind) + { + double diskUsage = DiskUsageMonitor.instance.getDiskUsage(); + double rejectRatio = getIncrementalRepairDiskHeadroomRejectRatio(); + + if (diskUsage + rejectRatio > 1) + { + logger.error("[{}] Rejecting incoming repair, disk usage ({}%) above threshold ({}%)", + previewKind.logPrefix(parentRepairSession), String.format("%.2f", diskUsage * 100), String.format("%.2f", (1 - rejectRatio) * 100)); + return false; + } + + return true; + } + public Future prepareForRepair(TimeUUID parentRepairSession, InetAddressAndPort coordinator, Set endpoints, RepairOption options, boolean isForcedRepair, List columnFamilyStores) { + if (!verifyDiskHeadroomThreshold(parentRepairSession, options.getPreviewKind())) + failRepair(parentRepairSession, "Rejecting incoming repair, disk usage above threshold"); // failRepair throws exception + if (!verifyCompactionsPendingThreshold(parentRepairSession, options.getPreviewKind())) failRepair(parentRepairSession, "Rejecting incoming repair, pending compactions above threshold"); // failRepair throws exception @@ -684,7 +712,7 @@ public Future prepareForRepair(TimeUUID parentRepairSession, InetAddressAndPo } } // implement timeout to bound the runtime of the future - long timeoutMillis = getRepairRetrySpec().isEnabled() ? getRepairRpcTimeout(MILLISECONDS) + long timeoutMillis = getRepairRetrySpec().isEnabled() ? getRepairPrepareMessageTimeout(MILLISECONDS) : getRpcTimeout(MILLISECONDS); ctx.optionalTasks().schedule(() -> { if (promise.isDone()) @@ -820,7 +848,9 @@ public synchronized void registerParentRepairSession(TimeUUID parentRepairSessio if (!parentRepairSessions.containsKey(parentRepairSession)) { - parentRepairSessions.put(parentRepairSession, new ParentRepairSession(coordinator, columnFamilyStores, ranges, isIncremental, repairedAt, isGlobal, previewKind)); + ParentRepairSession session = new ParentRepairSession(coordinator, columnFamilyStores, ranges, isIncremental, repairedAt, isGlobal, previewKind); + parentRepairSessions.put(parentRepairSession, session); + ParentRepairSessionListener.instance.onRegistered(parentRepairSession, session); } } @@ -858,6 +888,8 @@ public synchronized ParentRepairSession removeParentRepairSession(TimeUUID paren return null; String snapshotName = parentSessionId.toString(); + ParentRepairSessionListener.instance.onRemoved(parentSessionId, session); + if (session.hasSnapshots.get()) { snapshotExecutor.submit(() -> { @@ -885,12 +917,9 @@ public void handleMessage(Message message) if (session == null) { - switch (message.verb()) + if (message.verb() == VALIDATION_RSP || message.verb() == SYNC_RSP) { - case VALIDATION_RSP: - case SYNC_RSP: ctx.messaging().send(message.emptyResponse(), message.from()); - break; } if (payload instanceof ValidationResponse) { @@ -906,16 +935,13 @@ public void handleMessage(Message message) return; } - switch (message.verb()) + if (message.verb() == VALIDATION_RSP) { - case VALIDATION_RSP: - session.validationComplete(desc, (Message) message); - break; - case SYNC_RSP: - session.syncComplete(desc, (Message) message); - break; - default: - break; + session.validationComplete(desc, (Message) message); + } + else if (message.verb() == SYNC_RSP) + { + session.syncComplete(desc, (Message) message); } } @@ -1059,6 +1085,16 @@ public void setRepairPendingCompactionRejectThreshold(int value) DatabaseDescriptor.setRepairPendingCompactionRejectThreshold(value); } + public double getIncrementalRepairDiskHeadroomRejectRatio() + { + return DatabaseDescriptor.getRepairDiskHeadroomRejectRatio(); + } + + public void setIncrementalRepairDiskHeadroomRejectRatio(double value) + { + DatabaseDescriptor.setRepairDiskHeadroomRejectRatio(value); + } + /** * Remove any parent repair sessions matching predicate */ diff --git a/src/java/org/apache/cassandra/service/ActiveRepairServiceMBean.java b/src/java/org/apache/cassandra/service/ActiveRepairServiceMBean.java index 851dc6c802bb..c739b048d68f 100644 --- a/src/java/org/apache/cassandra/service/ActiveRepairServiceMBean.java +++ b/src/java/org/apache/cassandra/service/ActiveRepairServiceMBean.java @@ -74,4 +74,8 @@ public interface ActiveRepairServiceMBean int parentRepairSessionsCount(); public int getPaxosRepairParallelism(); public void setPaxosRepairParallelism(int v); + + public double getIncrementalRepairDiskHeadroomRejectRatio(); + + public void setIncrementalRepairDiskHeadroomRejectRatio(double value); } diff --git a/src/java/org/apache/cassandra/service/AutoRepairService.java b/src/java/org/apache/cassandra/service/AutoRepairService.java new file mode 100644 index 000000000000..52cd9b394087 --- /dev/null +++ b/src/java/org/apache/cassandra/service/AutoRepairService.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.service; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType; +import org.apache.cassandra.repair.autorepair.AutoRepairUtils; +import org.apache.cassandra.utils.MBeanWrapper; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implement all the MBeans for AutoRepair. + */ +public class AutoRepairService implements AutoRepairServiceMBean +{ + private static final Logger logger = LoggerFactory.getLogger(AutoRepairService.class); + + public static final String MBEAN_NAME = "org.apache.cassandra.db:type=AutoRepairService"; + + @VisibleForTesting + protected AutoRepairConfig config; + + public static final AutoRepairService instance = new AutoRepairService(); + + @VisibleForTesting + protected AutoRepairService() + { + } + + public static void setup() + { + if (!CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean()) + { + logger.info("Auto-repair service is disabled via JVM property"); + return; + } + instance.config = DatabaseDescriptor.getAutoRepairConfig(); + } + + static + { + if (CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean()) + { + MBeanWrapper.instance.registerMBean(instance, MBEAN_NAME); + } + } + + public void checkCanRun(String repairType) + { + checkCanRun(RepairType.parse(repairType)); + } + + public void checkCanRun(RepairType repairType) + { + if (!config.isAutoRepairSchedulingEnabled()) + throw new ConfigurationException("Auto-repair scheduler is disabled."); + + if (repairType != RepairType.INCREMENTAL) + return; + + if (config.getMaterializedViewRepairEnabled(repairType) && DatabaseDescriptor.isMaterializedViewsOnRepairEnabled()) + throw new ConfigurationException("Cannot run incremental repair while materialized view replay is enabled. Set materialized_views_on_repair_enabled to false."); + + if (DatabaseDescriptor.isCDCEnabled() && DatabaseDescriptor.isCDCOnRepairEnabled()) + throw new ConfigurationException("Cannot run incremental repair while CDC replay is enabled. Set cdc_on_repair_enabled to false."); + } + + public AutoRepairConfig getAutoRepairConfig() + { + return config; + } + + @Override + public boolean isAutoRepairDisabled() + { + return !CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean() + || config == null + || !config.isAutoRepairSchedulingEnabled(); + } + + @Override + public String getAutoRepairConfiguration() + { + StringBuilder sb = new StringBuilder(); + sb.append("repair scheduler configuration:"); + appendConfig(sb, "repair_check_interval", config.getRepairCheckInterval()); + appendConfig(sb, "repair_task_min_duration", config.getRepairTaskMinDuration()); + appendConfig(sb, "history_clear_delete_hosts_buffer_interval", config.getAutoRepairHistoryClearDeleteHostsBufferInterval()); + appendConfig(sb, "mixed_major_version_repair_enabled", config.getMixedMajorVersionRepairEnabled()); + for (RepairType repairType : RepairType.values()) + { + sb.append(formatRepairTypeConfig(repairType, config)); + } + return sb.toString(); + } + + @Override + public void setAutoRepairEnabled(String repairType, boolean enabled) + { + checkCanRun(repairType); + config.setAutoRepairEnabled(RepairType.parse(repairType), enabled); + } + + @Override + public void setRepairThreads(String repairType, int repairThreads) + { + config.setRepairThreads(RepairType.parse(repairType), repairThreads); + } + + @Override + public void setRepairPriorityForHosts(String repairType, String commaSeparatedHostSet) + { + Set hosts = InetAddressAndPort.parseHosts(commaSeparatedHostSet, false); + if (!hosts.isEmpty()) + { + AutoRepairUtils.addPriorityHosts(RepairType.parse(repairType), hosts); + } + } + + @Override + public void setForceRepairForHosts(String repairType, String commaSeparatedHostSet) + { + Set hosts = InetAddressAndPort.parseHosts(commaSeparatedHostSet, false); + if (!hosts.isEmpty()) + { + AutoRepairUtils.setForceRepair(RepairType.parse(repairType), hosts); + } + } + + @Override + public void setRepairMinInterval(String repairType, String minRepairInterval) + { + config.setRepairMinInterval(RepairType.parse(repairType), minRepairInterval); + } + + @Override + public void startScheduler() + { + config.startScheduler(); + } + + @Override + public void setAutoRepairHistoryClearDeleteHostsBufferDuration(String duration) + { + config.setAutoRepairHistoryClearDeleteHostsBufferInterval(duration); + } + + @Override + public void setAutoRepairMinRepairTaskDuration(String duration) + { + config.setRepairTaskMinDuration(duration); + } + + @Override + public void setRepairSSTableCountHigherThreshold(String repairType, int sstableHigherThreshold) + { + config.setRepairSSTableCountHigherThreshold(RepairType.parse(repairType), sstableHigherThreshold); + } + + @Override + public void setAutoRepairTableMaxRepairTime(String repairType, String autoRepairTableMaxRepairTime) + { + config.setAutoRepairTableMaxRepairTime(RepairType.parse(repairType), autoRepairTableMaxRepairTime); + } + + @Override + public void setIgnoreDCs(String repairType, Set ignoreDCs) + { + config.setIgnoreDCs(RepairType.parse(repairType), ignoreDCs); + } + + @Override + public void setPrimaryTokenRangeOnly(String repairType, boolean primaryTokenRangeOnly) + { + config.setRepairPrimaryTokenRangeOnly(RepairType.parse(repairType), primaryTokenRangeOnly); + } + + @Override + public void setParallelRepairPercentage(String repairType, int percentage) + { + config.setParallelRepairPercentage(RepairType.parse(repairType), percentage); + } + + @Override + public void setParallelRepairCount(String repairType, int count) + { + config.setParallelRepairCount(RepairType.parse(repairType), count); + } + + @Override + public void setAllowParallelReplicaRepair(String repairType, boolean enabled) + { + config.setAllowParallelReplicaRepair(RepairType.parse(repairType), enabled); + } + + @Override + public void setAllowParallelReplicaRepairAcrossSchedules(String repairType, boolean enabled) + { + config.setAllowParallelReplicaRepairAcrossSchedules(RepairType.parse(repairType), enabled); + } + + @Override + public void setMVRepairEnabled(String repairType, boolean enabled) + { + config.setMaterializedViewRepairEnabled(RepairType.parse(repairType), enabled); + } + + @Override + public void setRepairSessionTimeout(String repairType, String timeout) + { + config.setRepairSessionTimeout(RepairType.parse(repairType), timeout); + } + + @Override + public Set getOnGoingRepairHostIds(String repairType) + { + List histories = AutoRepairUtils.getAutoRepairHistory(RepairType.parse(repairType)); + if (histories == null) + { + return Collections.emptySet(); + } + Set hostIds = new HashSet<>(); + AutoRepairUtils.CurrentRepairStatus currentRepairStatus = new AutoRepairUtils.CurrentRepairStatus(histories, AutoRepairUtils.getPriorityHostIds(RepairType.parse(repairType)), null); + for (UUID id : currentRepairStatus.hostIdsWithOnGoingRepair) + { + hostIds.add(id.toString()); + } + for (UUID id : currentRepairStatus.hostIdsWithOnGoingForceRepair) + { + hostIds.add(id.toString()); + } + return Collections.unmodifiableSet(hostIds); + } + + @Override + public void setAutoRepairTokenRangeSplitterParameter(String repairType, String key, String value) + { + config.getTokenRangeSplitterInstance(RepairType.parse(repairType)).setParameter(key, value); + } + + @Override + public void setRepairByKeyspace(String repairType, boolean repairByKeyspace) + { + config.setRepairByKeyspace(RepairType.parse(repairType), repairByKeyspace); + } + + @Override + public void setAutoRepairMaxRetriesCount(String repairType, int retries) + { + config.setRepairMaxRetries(RepairType.parse(repairType), retries); + } + + @Override + public void setAutoRepairRetryBackoff(String repairType, String interval) + { + config.setRepairRetryBackoff(RepairType.parse(repairType), interval); + } + + @Override + public void setMixedMajorVersionRepairEnabled(boolean enabled) + { + config.setMixedMajorVersionRepairEnabled(enabled); + } + + private String formatRepairTypeConfig(RepairType repairType, AutoRepairConfig config) + { + StringBuilder sb = new StringBuilder(); + sb.append("\nconfiguration for repair_type: ").append(repairType.getConfigName()); + sb.append("\n\tenabled: ").append(config.isAutoRepairEnabled(repairType)); + // Only show configuration if enabled + if (config.isAutoRepairEnabled(repairType)) + { + Set priorityHosts = AutoRepairUtils.getPriorityHosts(repairType); + if (!priorityHosts.isEmpty()) + { + appendConfig(sb, "priority_hosts", Joiner.on(',').skipNulls().join(priorityHosts)); + } + + appendConfig(sb, "min_repair_interval", config.getRepairMinInterval(repairType)); + appendConfig(sb, "repair_by_keyspace", config.getRepairByKeyspace(repairType)); + appendConfig(sb, "number_of_repair_threads", config.getRepairThreads(repairType)); + appendConfig(sb, "sstable_upper_threshold", config.getRepairSSTableCountHigherThreshold(repairType)); + appendConfig(sb, "table_max_repair_time", config.getAutoRepairTableMaxRepairTime(repairType)); + appendConfig(sb, "ignore_dcs", config.getIgnoreDCs(repairType)); + appendConfig(sb, "repair_primary_token_range_only", config.getRepairPrimaryTokenRangeOnly(repairType)); + appendConfig(sb, "parallel_repair_count", config.getParallelRepairCount(repairType)); + appendConfig(sb, "parallel_repair_percentage", config.getParallelRepairPercentage(repairType)); + appendConfig(sb, "allow_parallel_replica_repair", config.getAllowParallelReplicaRepair(repairType)); + appendConfig(sb, "allow_parallel_replica_repair_across_schedules", config.getAllowParallelReplicaRepairAcrossSchedules(repairType)); + appendConfig(sb, "materialized_view_repair_enabled", config.getMaterializedViewRepairEnabled(repairType)); + appendConfig(sb, "initial_scheduler_delay", config.getInitialSchedulerDelay(repairType)); + appendConfig(sb, "repair_session_timeout", config.getRepairSessionTimeout(repairType)); + appendConfig(sb, "force_repair_new_node", config.getForceRepairNewNode(repairType)); + appendConfig(sb, "repair_max_retries", config.getRepairMaxRetries(repairType)); + appendConfig(sb, "repair_retry_backoff", config.getRepairRetryBackoff(repairType)); + + final ParameterizedClass splitterClass = config.getTokenRangeSplitter(repairType); + final String splitterClassName = splitterClass.class_name != null ? splitterClass.class_name : AutoRepairConfig.DEFAULT_SPLITTER.getName(); + appendConfig(sb, "token_range_splitter", splitterClassName); + Map tokenRangeSplitterParameters = config.getTokenRangeSplitterInstance(repairType).getParameters(); + if (!tokenRangeSplitterParameters.isEmpty()) + { + for (Map.Entry param : tokenRangeSplitterParameters.entrySet()) + { + appendConfig(sb, String.format("token_range_splitter.%s", param.getKey()), param.getValue()); + } + } + } + + return sb.toString(); + } + + private void appendConfig(StringBuilder sb, String config, T value) + { + sb.append(String.format("%s%s: %s", "\n\t", config, value)); + } +} diff --git a/src/java/org/apache/cassandra/service/AutoRepairServiceMBean.java b/src/java/org/apache/cassandra/service/AutoRepairServiceMBean.java new file mode 100644 index 000000000000..e4d554dd980d --- /dev/null +++ b/src/java/org/apache/cassandra/service/AutoRepairServiceMBean.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.service; + + +import java.util.Set; + +/** + * Defines all the MBeans exposed for AutoRepair. + */ +public interface AutoRepairServiceMBean +{ + public void setAutoRepairEnabled(String repairType, boolean enabled); + + public void setRepairThreads(String repairType, int repairThreads); + + public void setRepairPriorityForHosts(String repairType, String commaSeparatedHostSet); + + public void setForceRepairForHosts(String repairType, String commaSeparatedHostSet); + + public void setRepairMinInterval(String repairType, String minRepairInterval); + + void startScheduler(); + + public void setAutoRepairHistoryClearDeleteHostsBufferDuration(String duration); + + public void setAutoRepairMinRepairTaskDuration(String duration); + + public void setRepairSSTableCountHigherThreshold(String repairType, int ssTableHigherThreshold); + + public void setAutoRepairTableMaxRepairTime(String repairType, String autoRepairTableMaxRepairTime); + + public void setIgnoreDCs(String repairType, Set ignorDCs); + + public void setPrimaryTokenRangeOnly(String repairType, boolean primaryTokenRangeOnly); + + public void setParallelRepairPercentage(String repairType, int percentage); + + public void setParallelRepairCount(String repairType, int count); + + public void setAllowParallelReplicaRepair(String repairType, boolean enabled); + + public void setAllowParallelReplicaRepairAcrossSchedules(String repairType, boolean enabled); + + public void setMVRepairEnabled(String repairType, boolean enabled); + + public boolean isAutoRepairDisabled(); + + public String getAutoRepairConfiguration(); + + public void setRepairSessionTimeout(String repairType, String timeout); + + public Set getOnGoingRepairHostIds(String repairType); + + public void setAutoRepairTokenRangeSplitterParameter(String repairType, String key, String value); + + public void setRepairByKeyspace(String repairType, boolean repairByKeyspace); + + public void setAutoRepairMaxRetriesCount(String repairType, int retries); + + public void setAutoRepairRetryBackoff(String repairType, String interval); + + public void setMixedMajorVersionRepairEnabled(boolean enabled); +} diff --git a/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java b/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java index 0fa284770080..2982f2ebeaca 100644 --- a/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java +++ b/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java @@ -43,7 +43,7 @@ public BatchlogResponseHandler(AbstractWriteResponseHandler wrapped, int requ this.cleanup = cleanup; } - protected int ackCount() + public int ackCount() { return wrapped.ackCount(); } @@ -70,22 +70,22 @@ public void get() throws WriteTimeoutException, WriteFailureException wrapped.get(); } - protected int blockFor() + public int blockFor() { return wrapped.blockFor(); } - protected int candidateReplicaCount() + public int candidateReplicaCount() { return wrapped.candidateReplicaCount(); } - protected boolean waitingFor(InetAddressAndPort from) + public boolean waitingFor(InetAddressAndPort from) { return wrapped.waitingFor(from); } - protected void signal() + public void signal() { wrapped.signal(); } diff --git a/src/java/org/apache/cassandra/service/CacheService.java b/src/java/org/apache/cassandra/service/CacheService.java index 8240c2880f78..c7401f366eb8 100644 --- a/src/java/org/apache/cassandra/service/CacheService.java +++ b/src/java/org/apache/cassandra/service/CacheService.java @@ -28,6 +28,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.ImmutableTriple; import org.slf4j.Logger; @@ -47,6 +48,7 @@ import org.apache.cassandra.db.ClockAndCount; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.context.CounterContext; @@ -81,15 +83,22 @@ public class CacheService implements CacheServiceMBean public enum CacheType { - KEY_CACHE("KeyCache"), - ROW_CACHE("RowCache"), - COUNTER_CACHE("CounterCache"); + KEY_CACHE("KeyCache", "key_cache"), + ROW_CACHE("RowCache", "row_cache"), + COUNTER_CACHE("CounterCache", "counter_cache"); private final String name; + private final String micrometerMetricsPrefix; - CacheType(String typeName) + CacheType(String typeName, String micrometerMetricsPrefix) { - name = typeName; + this.name = typeName; + this.micrometerMetricsPrefix = micrometerMetricsPrefix; + } + + public String micrometerMetricsPrefix() + { + return micrometerMetricsPrefix; } public String toString() @@ -118,7 +127,7 @@ private CacheService() */ private AutoSavingCache initKeyCache() { - logger.info("Initializing key cache with capacity of {} MiBs.", DatabaseDescriptor.getKeyCacheSizeInMiB()); + logger.debug("Initializing key cache with capacity of {} MiBs.", DatabaseDescriptor.getKeyCacheSizeInMiB()); long keyCacheInMemoryCapacity = DatabaseDescriptor.getKeyCacheSizeInMiB() * 1024 * 1024; @@ -126,7 +135,15 @@ private AutoSavingCache initKeyCache() // where 48 = 40 bytes (average size of the key) + 8 bytes (size of value) ICache kc; kc = CaffeineCache.create(keyCacheInMemoryCapacity); - AutoSavingCache keyCache = new AutoSavingCache<>(kc, CacheType.KEY_CACHE, new KeyCacheSerializer()); + + AutoSavingCache keyCache = new AutoSavingCache<>(kc, CacheType.KEY_CACHE, new KeyCacheSerializer(), () -> { + Set liveDescriptors = Keyspace.allExisting() + .flatMap(keyspace -> keyspace.getColumnFamilyStores().stream() + .flatMap(cfs -> cfs.getLiveSSTables().stream() + .map(SSTableReader::getDescriptor))) + .collect(Collectors.toSet()); + return key -> liveDescriptors.contains(key.desc); + }); int keyCacheKeysToSave = DatabaseDescriptor.getKeyCacheKeysToSave(); @@ -140,16 +157,18 @@ private AutoSavingCache initKeyCache() */ private AutoSavingCache initRowCache() { - logger.info("Initializing row cache with capacity of {} MiBs", DatabaseDescriptor.getRowCacheSizeInMiB()); + logger.debug("Initializing row cache with capacity of {} MiBs", DatabaseDescriptor.getRowCacheSizeInMiB()); CacheProvider cacheProvider; String cacheProviderClassName = DatabaseDescriptor.getRowCacheSizeInMiB() > 0 ? DatabaseDescriptor.getRowCacheClassName() : "org.apache.cassandra.cache.NopCacheProvider"; try { - Class> cacheProviderClass = - (Class>) Class.forName(cacheProviderClassName); - cacheProvider = cacheProviderClass.newInstance(); + Class cacheProviderClass = + FBUtilities.classForNameWithoutInitialization(cacheProviderClassName, "row cache provider", CacheProvider.class); + @SuppressWarnings("unchecked") + CacheProvider typedCacheProvider = cacheProviderClass.newInstance(); + cacheProvider = typedCacheProvider; } catch (Exception e) { @@ -158,7 +177,7 @@ private AutoSavingCache initRowCache() // cache object ICache rc = cacheProvider.create(); - AutoSavingCache rowCache = new AutoSavingCache<>(rc, CacheType.ROW_CACHE, new RowCacheSerializer()); + AutoSavingCache rowCache = new AutoSavingCache<>(rc, CacheType.ROW_CACHE, new RowCacheSerializer(), null); int rowCacheKeysToSave = DatabaseDescriptor.getRowCacheKeysToSave(); @@ -169,27 +188,27 @@ private AutoSavingCache initRowCache() private AutoSavingCache initCounterCache() { - logger.info("Initializing counter cache with capacity of {} MiBs", DatabaseDescriptor.getCounterCacheSizeInMiB()); + logger.debug("Initializing counter cache with capacity of {} MiBs", DatabaseDescriptor.getCounterCacheSizeInMiB()); long capacity = DatabaseDescriptor.getCounterCacheSizeInMiB() * 1024 * 1024; AutoSavingCache cache = new AutoSavingCache<>(CaffeineCache.create(capacity), CacheType.COUNTER_CACHE, - new CounterCacheSerializer()); + new CounterCacheSerializer(), + null); int keysToSave = DatabaseDescriptor.getCounterCacheKeysToSave(); - logger.info("Scheduling counter cache save to every {} seconds (going to save {} keys).", - DatabaseDescriptor.getCounterCacheSavePeriod(), - keysToSave == Integer.MAX_VALUE ? "all" : keysToSave); + logger.debug("Scheduling counter cache save to every {} seconds (going to save {} keys).", + DatabaseDescriptor.getCounterCacheSavePeriod(), + keysToSave == Integer.MAX_VALUE ? "all" : keysToSave); cache.scheduleSaving(DatabaseDescriptor.getCounterCacheSavePeriod(), keysToSave); return cache; } - public int getRowCacheSavePeriodInSeconds() { return DatabaseDescriptor.getRowCacheSavePeriod(); diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java index 4fc4010e85b4..ae43b35f6e01 100644 --- a/src/java/org/apache/cassandra/service/CassandraDaemon.java +++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java @@ -22,12 +22,10 @@ import java.lang.management.MemoryPoolMXBean; import java.net.InetAddress; import java.net.UnknownHostException; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; import javax.management.ObjectName; import javax.management.StandardMBean; import javax.management.remote.JMXConnectorServer; @@ -63,6 +61,7 @@ import org.apache.cassandra.metrics.CassandraMetricsRegistry; import org.apache.cassandra.metrics.DefaultNameFactory; import org.apache.cassandra.net.StartupClusterConnectivityChecker; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; @@ -70,11 +69,11 @@ import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.streaming.StreamManager; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.JMXServerUtils; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.Mx4jTool; -import org.apache.cassandra.utils.NativeLibrary; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.logging.LoggingSupportFactory; @@ -251,7 +250,7 @@ protected void setup() logSystemInfo(logger); - NativeLibrary.tryMlockall(); + INativeLibrary.instance.tryMlockall(); CommitLog.instance.start(); @@ -342,13 +341,15 @@ protected void setup() // replay the log if necessary try { - CommitLog.instance.recoverSegmentsOnDisk(); + CommitLog.instance.recoverSegmentsOnDiskWithArchive(ColumnFamilyStore.FlushReason.STARTUP); } catch (IOException e) { throw new RuntimeException(e); } + Nodes.getInstance().reload(); + // Re-populate token metadata after commit log recover (new peers might be loaded onto system keyspace #10293) StorageService.instance.populateTokenMetadata(); @@ -413,7 +414,7 @@ protected void setup() for (final ColumnFamilyStore store : cfs.concatWithIndexes()) { store.reload(); //reload CFs in case there was a change of disk boundaries - if (store.getCompactionStrategyManager().shouldBeEnabled()) + if (store.compactionShouldBeEnabled()) { if (DatabaseDescriptor.getAutocompactionOnStartupEnabled()) { @@ -487,45 +488,41 @@ public void migrateSystemDataIfNeeded() throws IOException // the system keyspace location configured by the user (upgrade to 4.0) // 3) The system data are stored in the first data location and need to be moved to // the system keyspace location configured by the user (system_data_file_directory has been configured) - Path target = File.getPath(DatabaseDescriptor.getLocalSystemKeyspacesDataFileLocations()[0]); + File target = DatabaseDescriptor.getLocalSystemKeyspacesDataFileLocations()[0]; - String[] nonLocalSystemKeyspacesFileLocations = DatabaseDescriptor.getNonLocalSystemKeyspacesDataFileLocations(); - String[] sources = DatabaseDescriptor.useSpecificLocationForLocalSystemData() ? nonLocalSystemKeyspacesFileLocations + File[] nonLocalSystemKeyspacesFileLocations = DatabaseDescriptor.getNonLocalSystemKeyspacesDataFileLocations(); + File[] sources = DatabaseDescriptor.useSpecificLocationForLocalSystemData() ? nonLocalSystemKeyspacesFileLocations : Arrays.copyOfRange(nonLocalSystemKeyspacesFileLocations, 1, nonLocalSystemKeyspacesFileLocations.length); - for (String source : sources) + for (File dataFileLocation : sources) { - Path dataFileLocation = File.getPath(source); - - if (!Files.exists(dataFileLocation)) + if (!dataFileLocation.exists()) continue; - try (Stream locationChildren = Files.list(dataFileLocation)) + List keyspaceDirectories = new ArrayList<>(); + dataFileLocation.forEach(f -> { + if (SchemaConstants.isLocalSystemKeyspace(f.name())) + keyspaceDirectories.add(f); + }); + + for (File keyspaceDirectory : keyspaceDirectories) { - Path[] keyspaceDirectories = locationChildren.filter(p -> SchemaConstants.isLocalSystemKeyspace(p.getFileName().toString())) - .toArray(Path[]::new); + List tableDirectories = new ArrayList<>(); + keyspaceDirectory.forEach(f -> { + if (f.isDirectory() && SystemKeyspace.TABLES_SPLIT_ACROSS_MULTIPLE_DISKS.stream().noneMatch(t -> f.name().startsWith(t + '-'))) + tableDirectories.add(f); + }); - for (Path keyspaceDirectory : keyspaceDirectories) + for (File tableDirectory : tableDirectories) { - try (Stream keyspaceChildren = Files.list(keyspaceDirectory)) - { - Path[] tableDirectories = keyspaceChildren.filter(Files::isDirectory) - .filter(p -> SystemKeyspace.TABLES_SPLIT_ACROSS_MULTIPLE_DISKS.stream().noneMatch(t -> p.getFileName().toString().startsWith(t + '-'))) - .toArray(Path[]::new); - - for (Path tableDirectory : tableDirectories) - { - FileUtils.moveRecursively(tableDirectory, - target.resolve(dataFileLocation.relativize(tableDirectory))); - } + FileUtils.moveRecursively(tableDirectory, target.resolve(dataFileLocation.relativize(tableDirectory))); + } - if (!SchemaConstants.SYSTEM_KEYSPACE_NAME.equals(keyspaceDirectory.getFileName().toString())) - { - FileUtils.deleteDirectoryIfEmpty(keyspaceDirectory); - } - } + if (!SchemaConstants.SYSTEM_KEYSPACE_NAME.equals(keyspaceDirectory.name())) + { + FileUtils.deleteDirectoryIfEmpty(keyspaceDirectory); } } } @@ -612,11 +609,11 @@ public static void logSystemInfo(Logger logger) FBUtilities.prettyPrintMemory(Runtime.getRuntime().maxMemory())); for(MemoryPoolMXBean pool: ManagementFactory.getMemoryPoolMXBeans()) - logger.info("{} {}: {}", pool.getName(), pool.getType(), pool.getPeakUsage()); + logger.debug("{} {}: {}", pool.getName(), pool.getType(), pool.getPeakUsage()); - logger.info("Classpath: {}", JAVA_CLASS_PATH.getString()); + logger.debug("Classpath: {}", JAVA_CLASS_PATH.getString()); - logger.info("JVM Arguments: {}", ManagementFactory.getRuntimeMXBean().getInputArguments()); + logger.debug("JVM Arguments: {}", ManagementFactory.getRuntimeMXBean().getInputArguments()); } } @@ -893,12 +890,12 @@ static class NativeAccess implements NativeAccessMBean { public boolean isAvailable() { - return NativeLibrary.isAvailable(); + return INativeLibrary.instance.isAvailable(); } public boolean isMemoryLockable() { - return NativeLibrary.jnaMemoryLockable(); + return INativeLibrary.instance.jnaMemoryLockable(); } } diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java index e9948e142ea8..e46d17b532dc 100644 --- a/src/java/org/apache/cassandra/service/ClientState.java +++ b/src/java/org/apache/cassandra/service/ClientState.java @@ -59,7 +59,7 @@ import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MD5Digest; -import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_QUERY_HANDLER_CLASS; +import static org.apache.cassandra.config.CassandraRelevantProperties.*; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; /** @@ -118,7 +118,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()); } @@ -187,6 +187,13 @@ protected ClientState(ClientState source) this.clientOptions = source.clientOptions; } + private ClientState(AuthenticatedUser user) + { + this.isInternal = false; + this.remoteAddress = null; + this.user = user; + } + /** * @return a ClientState object for internal C* calls (not limited by any kind of auth). */ @@ -210,6 +217,14 @@ public static ClientState forExternalCalls(SocketAddress remoteAddress) return new ClientState((InetSocketAddress)remoteAddress); } + /** + * @return a ClientState object for internal calls with the given user logged in (not limited by any kind of auth). + */ + public static ClientState forExternalCalls(AuthenticatedUser user) + { + return new ClientState(user); + } + /** * Clone this ClientState object, but use the provided keyspace instead of the * keyspace in this ClientState object. @@ -593,25 +608,40 @@ public void ensureNotAnonymous() */ public boolean isOrdinaryUser() { - return !isSuper() && !isSystem(); + // check isSystem() before super user, system users should bypass all guardrails and permissions + if (ENABLE_GUARDRAILS_FOR_ANONYMOUS_USER.getBoolean()) + return !isSystem() && !isSuperIgnoreAnonymousUser(); + return !isSystem() && !isSuper(); } /** - * Checks if this user is a super user. + * Checks if this user is a super user. When authentication is disabled the anonymous user is considered + * a super user. */ public boolean isSuper() { return !DatabaseDescriptor.getAuthenticator().requireAuthentication() || (user != null && user.isSuper()); } + /** + * Checks if this user is a super user. An anonymous user is never considered a super user. + */ + public boolean isSuperIgnoreAnonymousUser() + { + return user != null && user.isSuper(); + } + /** * Checks if the user is the system user. * + * Returns true for both internal calls (isInternal) and external calls + * made by system users. + * * @return {@code true} if this user is the system user, {@code false} otherwise. */ public boolean isSystem() { - return isInternal; + return isInternal || (user != null && user.isSystem()); } public void ensureIsSuperuser(String message) diff --git a/src/java/org/apache/cassandra/service/ClientWarn.java b/src/java/org/apache/cassandra/service/ClientWarn.java index 7f67a1168a30..84e4f37de96d 100644 --- a/src/java/org/apache/cassandra/service/ClientWarn.java +++ b/src/java/org/apache/cassandra/service/ClientWarn.java @@ -18,7 +18,9 @@ package org.apache.cassandra.service; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.apache.cassandra.concurrent.ExecutorLocals; import org.apache.cassandra.utils.FBUtilities; @@ -40,14 +42,22 @@ public State get() public void set(State value) { ExecutorLocals current = ExecutorLocals.current(); - ExecutorLocals.Impl.set(current.traceState, value); + ExecutorLocals.Impl.set(current.traceState, value, current.sensors, current.operationContext); } public void warn(String text) + { + warn(text, null); + } + + /** + * Issue the given warning if this is the first time `key` is seen. + */ + public void warn(String text, Object key) { State state = get(); if (state != null) - state.add(text); + state.add(text, key); } public void captureWarnings() @@ -73,11 +83,16 @@ public static class State // This must be a thread-safe list. Even though it's wrapped in a ThreadLocal, it's propagated to each thread // from shared state, so multiple threads can reference the same State. private final List warnings = new CopyOnWriteArrayList<>(); + private final Set keysAdded = new HashSet<>(); - private void add(String warning) + private void add(String warning, Object key) { if (warnings.size() < FBUtilities.MAX_UNSIGNED_SHORT) + { + if (key != null && !keysAdded.add(key)) + return; warnings.add(maybeTruncate(warning)); + } } private static String maybeTruncate(String warning) diff --git a/src/java/org/apache/cassandra/service/DataResurrectionCheck.java b/src/java/org/apache/cassandra/service/DataResurrectionCheck.java index 4cf32781100d..53f0438f0c2c 100644 --- a/src/java/org/apache/cassandra/service/DataResurrectionCheck.java +++ b/src/java/org/apache/cassandra/service/DataResurrectionCheck.java @@ -19,6 +19,7 @@ package org.apache.cassandra.service; import java.io.IOException; +import java.nio.file.Files; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -45,6 +46,7 @@ import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.SchemaKeyspace; import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.Hex; import org.apache.cassandra.utils.JsonUtils; import org.apache.cassandra.utils.Pair; @@ -53,7 +55,6 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; -import static org.apache.cassandra.exceptions.StartupException.ERR_WRONG_DISK_STATE; import static org.apache.cassandra.exceptions.StartupException.ERR_WRONG_MACHINE_STATE; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -86,12 +87,24 @@ public Heartbeat(Instant lastHeartbeat) public void serializeToJsonFile(File outputFile) throws IOException { - JsonUtils.serializeToJsonFile(this, outputFile); + JsonUtils.serializeToJsonFileAtomic(this, outputFile); } public static Heartbeat deserializeFromJsonFile(File file) throws IOException { - return JsonUtils.deserializeFromJsonFile(Heartbeat.class, file); + byte[] bytes = Files.readAllBytes(file.toPath()); + try + { + return JsonUtils.deserializeFromJsonBytes(Heartbeat.class, bytes); + } + catch (IOException ex) + { + int maxLogBytes = Math.min(bytes.length, 1024); + String hexContent = bytes.length > 0 ? Hex.bytesToHex(bytes, 0, maxLogBytes) : "(empty)"; + LOGGER.error("Failed to deserialize heartbeat file {} (length: {} bytes, first {} bytes hex: {})", + file, bytes.length, maxLogBytes, hexContent, ex); + throw ex; + } } @Override @@ -134,7 +147,7 @@ static File getHeartbeatFile(Map config) } else { - String[] dataFileLocations = DatabaseDescriptor.getLocalSystemKeyspacesDataFileLocations(); + File[] dataFileLocations = DatabaseDescriptor.getLocalSystemKeyspacesDataFileLocations(); assert dataFileLocations.length != 0; heartbeatFile = new File(dataFileLocations[0], DEFAULT_HEARTBEAT_FILE); } @@ -173,7 +186,10 @@ public void execute(StartupChecksOptions options) throws StartupException } catch (IOException ex) { - throw new StartupException(ERR_WRONG_DISK_STATE, "Failed to deserialize heartbeat file " + heartbeatFile); + LOGGER.warn("Failed to deserialize heartbeat file {}. Falling back to file last modified time.", + heartbeatFile, ex); + Instant lastModified = Instant.ofEpochMilli(heartbeatFile.lastModified()); + heartbeat = new Heartbeat(lastModified); } if (heartbeat.lastHeartbeat == null) @@ -297,7 +313,7 @@ List getKeyspaces() List getTablesGcPeriods(String userKeyspace) { Optional keyspaceMetadata = SchemaKeyspace.fetchNonSystemKeyspaces().get(userKeyspace); - if (!keyspaceMetadata.isPresent()) + if (keyspaceMetadata.isEmpty()) return Collections.emptyList(); KeyspaceMetadata ksmd = keyspaceMetadata.get(); diff --git a/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java b/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java index e4b208b582fb..c53bc53df8dc 100644 --- a/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java @@ -23,14 +23,14 @@ import java.util.function.Supplier; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.WriteType; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.NetworkTopologyStrategy; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.net.Message; -import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.db.WriteType; import org.apache.cassandra.transport.Dispatcher; /** @@ -77,6 +77,7 @@ public DatacenterSyncWriteResponseHandler(ReplicaPlan.ForWrite replicaPlan, public void onResponse(Message message) { + trackReplicaResponseSize(message); try { String dataCenter = message == null @@ -102,7 +103,7 @@ public void onResponse(Message message) } } - protected int ackCount() + public int ackCount() { return acks.get(); } diff --git a/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java b/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java index f33d6607e1c2..29b170052597 100644 --- a/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java @@ -17,6 +17,9 @@ */ package org.apache.cassandra.service; +import java.util.function.Predicate; +import java.util.function.Supplier; + import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.WriteType; import org.apache.cassandra.locator.InOurDc; @@ -25,9 +28,6 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.transport.Dispatcher; -import java.util.function.Predicate; -import java.util.function.Supplier; - /** * This class blocks for a quorum of responses _in the local datacenter only_ (CL.LOCAL_QUORUM). */ @@ -61,7 +61,7 @@ public void onResponse(Message message) } @Override - protected boolean waitingFor(InetAddressAndPort from) + public boolean waitingFor(InetAddressAndPort from) { return waitingFor.test(from); } diff --git a/src/java/org/apache/cassandra/service/DecommissionHook.java b/src/java/org/apache/cassandra/service/DecommissionHook.java new file mode 100644 index 000000000000..11221260d049 --- /dev/null +++ b/src/java/org/apache/cassandra/service/DecommissionHook.java @@ -0,0 +1,140 @@ +/* + * Copyright IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.service; + +/** + * Work that must run on a node that is being decommissioned, before that node finishes leaving. + * + * Register with {@link StorageService#registerDecommissionHook(DecommissionHook)}; several hooks + * may be registered, and they run one after another in registration order. A hook only ever runs + * on the node being decommissioned, and only for {@code nodetool decommission} -- not on a + * graceful shutdown, not on {@code nodetool removenode} (where this node is already gone), and + * not on the nodes that merely observe a peer leaving. + * + *

    Where this runs

    + * + * {@link StorageService#decommission(boolean)} invokes hooks after {@code unbootstrap()} has + * returned and before it tears any subsystem down. At that point: + * + *
      + *
    • the batch log has completed its final replay, and hints have been transferred or dropped;
    • + *
    • all ranges have been streamed to their new owners;
    • + *
    • this node has left the ring -- {@code leaveRing()} removed it from {@code TokenMetadata}, + * announced LEFT and waited for the peers to notice, so coordinators no longer route + * mutations here. Note this needs LEFT, not LEAVING: {@code addLeavingEndpoint} only records + * the endpoint in a set that write routing never consults, so a LEAVING node stays a natural + * write replica;
    • + *
    • decommission has not yet stopped messaging, the native transport or the stages, so a hook + * may run CQL queries and coordinate against the rest of the cluster.
    • + *
    + * + *

    Caveats a hook has to live with

    + * + *
      + *
    • A late mutation can still arrive: a peer that has not yet processed LEFT may send + * one, and this node applies it, because {@code reject_out_of_token_range_requests} defaults + * to false. "No mutations" means none are routed here, not that none can land. Set that + * option to true if a hook needs the stronger guarantee.
    • + *
    • Hints do not help a hook's writes. {@code unbootstrap()} has already dealt with the hints + * this node held: by default ({@code transfer_hints_on_decommission}) it streamed them to a + * peer, otherwise it disabled hinted handoff and deleted them. Either way that happened + * before the hooks, so a hint a hook's write creates now is never transferred, and is + * lost when the process stops. A hook's write should meet its consistency level against live + * replicas rather than rely on being completed later.
    • + *
    • The native transport is only guaranteed not to have been shut down by the decommission; + * it may still be off for unrelated reasons (never started, {@code nodetool disablebinary}). + * A hook coordinating in-process via {@code QueryProcessor} does not depend on it.
    • + *
    + * + *

    Contract

    + * + * A hook may block for as long as it needs -- minutes or hours. Decommission does not proceed + * until every hook has returned, and {@code nodetool decommission} blocks for that whole time. + * Hooks run on the thread serving the decommission request, so they must not assume any particular + * executor. + * + * Within one process a hook runs at most once: hooks are only reached once {@code unbootstrap()} + * has succeeded, and from there the decommission always runs to completion, so a repeated + * {@code nodetool decommission} returns early rather than making a second pass. A decommission that + * fails before unbootstrap() completes never reaches the hooks at all, so a later retry runs + * them for the first time. That is not an across-restarts guarantee: {@code leaveRing()} persists + * NEEDS_BOOTSTRAP before the hooks run, so if the process is killed while a hook is still working, + * the node bootstraps back into the ring on restart and a later decommission runs every hook again. + * A hook that cannot tolerate that must make itself idempotent. + * + * If a hook fails, the remaining hooks still run and the decommission still completes -- by this + * point the data has streamed away and the node has left the ring, so there is nothing to roll back + * to, and refusing to finish would only strand the node (a retry is rejected because the node is no + * longer a ring member, and a restart would re-bootstrap it). The failure is logged, and + * {@code decommission} then throws naming the offending hooks, so a hook failure is never silent. + * Note what that means for tooling: a failure reported by {@code nodetool decommission} does not + * imply the decommission was not performed -- check the node's operation mode to tell the two apart. + * + * Anything a hook throws is reported this way, including an {@link Error}: escalating it to the JVM + * failure policy from here would strand the node in the window this design exists to avoid. + * + *

    Interruption

    + * + * A hook is not interrupted by the decommission itself: nothing in {@code decommission()} cancels a + * running hook, and there is no timeout, so an interrupt can only come from whoever holds the + * decommission thread -- typically the JMX client disconnecting or a caller cancelling the JMX + * invocation. A hook that blocks should therefore treat interruption as "the operator gave up on + * this call", not as "the decommission was aborted": the node has already left the ring and the + * decommission will finish regardless. + * + * Both standard responses to interruption are handled, and neither is a no-op: + * + *
      + *
    • Propagating {@link InterruptedException} aborts the hook chain. The hook is recorded + * as failed ({@code " (interrupted)"}), every hook after it is recorded as not run, and + * the decommission proceeds to its shutdown steps. The interrupt is not re-asserted: + * throwing {@code InterruptedException} already cleared the flag, and restoring it would fail + * the shutdown's blocking waits, so the remaining hooks are skipped rather than run against an + * interrupted thread. A hook that wants the rest of the chain to run must not let an + * {@code InterruptedException} escape.
    • + *
    • Catching it and restoring the flag -- {@code Thread.currentThread().interrupt()}, + * the usual idiom for a method that cannot throw -- returns normally and counts as success. + * The caller clears the flag (logging a warning) before invoking the next hook, so a restored + * interrupt never leaks into a later hook, into the shutdown sequence, or onto the pooled JMX + * handler thread that the decommission borrowed. The same clearing happens when a hook + * restores the flag and then throws something else, so the failure path leaks it no more than + * the success path does, and the flag is cleared once more after the last hook, so it can + * never outlive the chain.
    • + *
    + * + * The practical consequences for a hook author: the interrupt flag is always clear on entry, so a + * hook may block without first draining a stale interrupt; and setting the flag on the way out is + * harmless but pointless, because it is consumed immediately and cannot be used to signal anything + * to the decommission. A hook that wants to report a problem should throw instead. + */ +public interface DecommissionHook +{ + /** + * Identifies this hook in the decommission log lines. Should be short and stable. + */ + String name(); + + /** + * Runs the work. May block indefinitely; may run CQL queries. Throwing is reported and fails + * {@code nodetool decommission}, but does not stop the node from finishing its decommission. + * + * The interrupt flag is clear on entry and is cleared again after this returns. Letting an + * {@link InterruptedException} escape marks this hook as interrupted and skips the hooks + * registered after it; catching it and restoring the flag counts as success and does not + * affect the hooks that follow. See the class javadoc. + */ + void onDecommission() throws Exception; +} diff --git a/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java b/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java index 8b182942b23e..34b90326f0e4 100644 --- a/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java +++ b/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java @@ -54,6 +54,7 @@ public void handleCorruptSSTable(CorruptSSTableException e) logger.error("Stopping transports as disk_failure_policy is " + DatabaseDescriptor.getDiskFailurePolicy()); StorageService.instance.stopTransports(); break; + } } @@ -86,10 +87,10 @@ public void handleFSError(FSError e) } // for both read and write errors mark the path as unwritable. - DisallowedDirectories.maybeMarkUnwritable(new File(e.path)); + DisallowedDirectories.maybeMarkUnwritable(e.file); if (e instanceof FSReadError && shouldMaybeRemoveData(e)) { - File directory = DisallowedDirectories.maybeMarkUnreadable(new File(e.path)); + File directory = DisallowedDirectories.maybeMarkUnreadable(e.file); if (directory != null) Keyspace.removeUnreadableSSTables(directory); } diff --git a/src/java/org/apache/cassandra/service/FileSystemOwnershipCheck.java b/src/java/org/apache/cassandra/service/FileSystemOwnershipCheck.java index 3d69c9e7631c..fa58ee4a289c 100644 --- a/src/java/org/apache/cassandra/service/FileSystemOwnershipCheck.java +++ b/src/java/org/apache/cassandra/service/FileSystemOwnershipCheck.java @@ -95,18 +95,18 @@ public class FileSystemOwnershipCheck implements StartupCheck static final String INVALID_PROPERTY_VALUE = "invalid or missing value for property '%s'"; static final String READ_EXCEPTION = "error when checking for fs ownership file"; - private final Supplier> dirs; + private final Supplier> dirs; FileSystemOwnershipCheck() { this(() -> Iterables.concat(Arrays.asList(DatabaseDescriptor.getAllDataFileLocations()), Arrays.asList(DatabaseDescriptor.getCommitLogLocation(), DatabaseDescriptor.getSavedCachesLocation(), - DatabaseDescriptor.getHintsDirectory().absolutePath()))); + DatabaseDescriptor.getHintsDirectory()))); } @VisibleForTesting - FileSystemOwnershipCheck(Supplier> dirs) + FileSystemOwnershipCheck(Supplier> dirs) { this.dirs = dirs; } @@ -134,11 +134,11 @@ public void execute(StartupChecksOptions options) throws StartupException Map foundProperties = new HashMap<>(); // Step 1: Traverse the filesystem from each target dir upward, looking for marker files - for (String dataDir : dirs.get()) + for (File dataDir : dirs.get()) { logger.info("Checking for fs ownership details in file hierarchy for {}", dataDir); int foundFiles = 0; - Path dir = File.getPath(dataDir).normalize(); + Path dir = dataDir.toPath().normalize(); do { File tokenFile = resolve(dir, tokenFilename); @@ -163,7 +163,7 @@ public void execute(StartupChecksOptions options) throws StartupException dir = dir.getParent(); } while (dir != null); - foundPerTargetDir.put(dataDir, foundFiles); + foundPerTargetDir.put(dataDir.toString(), foundFiles); } // If a marker file couldn't be found for every target directory, error. diff --git a/src/java/org/apache/cassandra/service/Mutator.java b/src/java/org/apache/cassandra/service/Mutator.java new file mode 100644 index 000000000000..5cbd40291160 --- /dev/null +++ b/src/java/org/apache/cassandra/service/Mutator.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service; + +import java.util.Collection; +import javax.annotation.Nullable; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.CounterMutation; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.CasWriteUnknownResultException; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.IsBootstrappingException; +import org.apache.cassandra.exceptions.OverloadedException; +import org.apache.cassandra.exceptions.RequestFailureException; +import org.apache.cassandra.exceptions.RequestTimeoutException; +import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.exceptions.WriteTimeoutException; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.metrics.ClientRequestsMetrics; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.paxos.Commit; +import org.apache.cassandra.service.paxos.Paxos; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.TimeUUID; + +/** + * Facilitates mutations for counters, simple inserts, unlogged batches and LWTs. + * Used on the coordinator. + *
    + * The implementations may choose how and where to send the mutations. + *
    + * An instance of this interface implementation must be obtained via {@link MutatorProvider#instance}. + */ +public interface Mutator +{ + /** + * Where a Paxos commit dispatch originated. Passed to {@link #onCasCommit}. + */ + enum CasCommitOrigin + { + /** + * The coordinator's own CAS operation reached the commit phase: the proposal carries the + * update this coordinator built and successfully proposed. At most one per + * {@link #mutateCas} invocation. + */ + CLIENT_OPERATION, + /** + * The coordinator witnessed another proposer's accepted-but-uncommitted round and is + * completing it on their behalf. The payload is the foreign in-progress update, not + * anything this coordinator's client asked for; a SERIAL/LOCAL_SERIAL read can trigger it. + */ + REPAIR_IN_PROGRESS, + /** + * Re-transmission of an already-committed value to replicas that have not witnessed it + * (most-recent-commit refresh). Carries no new decision; the value was agreed earlier. + */ + REFRESH_COMMITTED + } + + /** + * Terminal outcome of a commit previously announced via {@link #onCasCommit}. Passed to + * {@link #onCasCommitCompleted}. {@link #APPLIED} and {@link #CONFIRMED_BY_PREPARE} are the two + * success outcomes (the value is durably visible at {@code consistencyLevel}); {@link #SUPERSEDED} + * and {@link #UNCONFIRMED} mean this coordinator did NOT confirm the commit — but the value is + * still decided and may become durable via another proposer or a background repair, so + * they are "not confirmed here", never "rolled back" (Paxos never un-decides an agreed value). + */ + enum CasCommitOutcome + { + /** + * Acknowledged by a {@code consistencyForCommit} quorum via a standalone, separately-awaited + * commit (the strongest confirmation). A read at {@code consistencyLevel} (or stronger) issued + * after this callback observes the value. Delivered for CLIENT_OPERATION under both engines, + * the v1 in-progress repair, and background Paxos repair. + */ + APPLIED, + /** + * Inferred-applied: the commit was fused into the following prepare (the v2 {@code begin()} + * "commit-and-prepare" optimization used to finish another proposer's round) and that prepare + * reached a {@code consistencyForConsensus} (serial) promise-quorum. Every replica applies the + * fused commit before answering the prepare, so a promise-quorum implies the commit reached + * that same quorum. Slightly weaker than {@link #APPLIED} (serial-quorum, inferred rather than + * a directly-awaited commit ack), but still means the value is durably visible. + */ + CONFIRMED_BY_PREPARE, + /** + * Decided but NOT confirmed by this coordinator: a higher ballot pre-empted the fused + * commit-and-prepare before a quorum was reached. Another proposer is finishing the round. + */ + SUPERSEDED, + /** + * Decided but NOT confirmed: the commit (or the fused prepare) timed out or failed before a + * quorum acknowledged it. The value may still be completed later by a repair. + */ + UNCONFIRMED + } + + /** + * Used for handling the given {@code mutations} as a logged batch. + */ + void mutateAtomically(Collection mutations, + ConsistencyLevel consistencyLevel, + boolean requireQuorumForRemove, + Dispatcher.RequestTime requestTime, + ClientRequestsMetrics metrics, + ClientState clientState) + throws UnavailableException, OverloadedException, WriteTimeoutException; + + /** + * Used for handling counter mutations on the coordinator level: + * - if coordinator is a replica, it will apply the counter mutation locally and forward the applied mutation to other counter replica + * - if coordinator is not a replica, it will forward the counter mutation to a counter leader which is a replica + */ + AbstractWriteResponseHandler mutateCounter(CounterMutation cm, String localDataCenter, Dispatcher.RequestTime requestTime); + + /** + * Used for handling counter mutations on the counter leader level + */ + AbstractWriteResponseHandler mutateCounterOnLeader(CounterMutation mutation, + String localDataCenter, + StorageProxy.WritePerformer performer, + Runnable callback, + Dispatcher.RequestTime requestTime); + + /** + * Used for standard inserts and unlogged batchs. + */ + AbstractWriteResponseHandler mutateStandard(Mutation mutation, + ConsistencyLevel consistencyLevel, + String localDataCenter, + StorageProxy.WritePerformer writePerformer, + Runnable callback, + WriteType writeType, + Dispatcher.RequestTime requestTime); + + /** + * Used for LWT mutation at the last (COMMIT) phase of Paxos. + */ + @Nullable + AbstractWriteResponseHandler mutatePaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime); + + /** + * Used for handling a whole CAS (LWT) operation: a single conditional statement or a + * conditional batch. This is the operation-level Paxos entry point, called exactly once per + * client operation regardless of the configured {@code paxos_variant} (the default + * implementation dispatches to the v2 engine or the legacy v1 flow), of internal + * prepare/propose retries under contention, and of the outcome. + *

    + * Completion is the return or throw of this call, on the calling thread: + *

      + *
    • returns {@code null}: the update applied;
    • + *
    • returns a {@link RowIterator}: the condition did not match (payload = current values);
    • + *
    • throws {@link CasWriteUnknownResultException}: the update's fate is unknown (a partial + * propose under the v1 engine). NOTE: the v2 engine does not throw this exception — its + * fate-unknown cases (a proposal superseded with possible side effects, or a partially + * accepted propose that times out) surface as plain timeout/failure exceptions with no + * distinguishing marker, so on v2 a propose-phase timeout must be treated as + * possibly-durable: a minority-accepted value can still be completed by a later repair;
    • + *
    • throws after {@link #onCasCommit} fired with {@link CasCommitOrigin#CLIENT_OPERATION}: + * the update was agreed by a quorum and its commit dispatched but not acknowledged in + * time. An agreed value is decided: it WILL be completed (by the dispatched commit, or + * by any later operation or repair touching the partition) even though this operation + * reported failure to the client;
    • + *
    • throws otherwise: this coordinator dispatched no commit (but see the v2 caveat above + * for propose-phase timeouts).
    • + *
    + */ + default RowIterator mutateCas(TableMetadata metadata, + DecoratedKey key, + CASRequest request, + ConsistencyLevel consistencyForPaxos, + ConsistencyLevel consistencyForCommit, + ClientState clientState, + long nowInSeconds, + Dispatcher.RequestTime requestTime) + throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, + InvalidRequestException, CasWriteUnknownResultException + { + return Paxos.useV2() + ? Paxos.cas(key, request, consistencyForPaxos, consistencyForCommit, clientState) + : StorageProxy.legacyCas(metadata.keyspace, metadata.name, key, request, consistencyForPaxos, + consistencyForCommit, clientState, nowInSeconds, requestTime); + } + + /** + * Callback invoked when a Paxos commit is DISPATCHED by this coordinator, under either Paxos + * engine (v1 or v2). Dispatched, not acknowledged: replicas may still miss it, in which case a + * later operation re-issues it (reported as {@link CasCommitOrigin#REFRESH_COMMITTED}). + *

    + * Multiplicity and threading: {@link CasCommitOrigin#CLIENT_OPERATION} fires at most once per + * {@link #mutateCas} call, inside that call, on the SAME thread, before the commit is + * dispatched/awaited — a wrapper can correlate the two without inspecting ballots (both + * engines invoke the whole operation synchronously on the request thread; any future + * refactoring of the engines must preserve this for CLIENT_OPERATION). The repair origins + * carry payloads this coordinator never originated and can fire any number of times, from CAS + * writes and SERIAL/LOCAL_SERIAL reads (on the request thread, while completing in-progress + * rounds they witness) and from background Paxos repair (on ARBITRARY threads, including + * messaging/response threads) — implementations must not assume request-thread context for + * repair origins. + *

    + * {@code consistencyLevel} is the consistency the commit is performed at for + * {@link CasCommitOrigin#CLIENT_OPERATION} and the v1/PaxosRepair repair sites; for the v2 + * engine's internal repair sites (where the commit piggybacks on the prepare exchange and has + * no commit consistency of its own) it is the consensus consistency (SERIAL/LOCAL_SERIAL) of + * the operation that triggered the repair. + *

    + * Empty proposals (serial reads, non-applying CAS) are never committed by either engine, so + * they never produce this callback. Low-level re-transmissions inside the v2 prepare exchange + * ({@code PaxosPrepareRefresh}) are transport details and are not reported. + *

    + * Implementations should not throw and must not block. Throwing is contained by the caller + * ({@link MutatorProvider#notifyCasCommit}): the exception is logged and ignored, never + * failing the operation, read or repair dispatching the commit. + */ + default void onCasCommit(Commit committed, ConsistencyLevel consistencyLevel, CasCommitOrigin origin) + { + // no-op + } + + /** + * TERMINAL completion for a commit previously announced via {@link #onCasCommit}: fires once the + * fate of that commit is known, carrying the {@link CasCommitOutcome outcome} (success or not). + * Where wired (see coverage below) it fires EXACTLY ONCE per {@link #onCasCommit} for the same + * ballot, letting an implementation that opened an operation on {@link #onCasCommit} close it out + * — pairing every announced commit with a success ({@link CasCommitOutcome#APPLIED} / + * {@link CasCommitOutcome#CONFIRMED_BY_PREPARE}) or a not-confirmed + * ({@link CasCommitOutcome#SUPERSEDED} / {@link CasCommitOutcome#UNCONFIRMED}) terminal. + *

    + * Unlike {@link #onCasCommit} (which fires before dispatch, for every decided value), + * this fires once the commit's fate is settled. A success outcome means the value is durably + * visible: a read at {@code consistencyLevel} (or stronger) issued after this returns observes it + * (with the usual caveat that at {@code ONE} the acknowledging replica may not be the one serving + * a later read). A not-confirmed outcome does NOT mean "not decided" (see {@link CasCommitOutcome}); + * the standard response is a deferred {@code LOCAL_SERIAL}/{@code SERIAL} read, which is + * self-correcting and will observe the value if/when it becomes durable. + *

    + * Coverage — a terminal is delivered for: + *

      + *
    • {@link CasCommitOrigin#CLIENT_OPERATION} under both engines (v1 {@code doPaxos}, v2 + * {@code Paxos.cas}): {@link CasCommitOutcome#APPLIED} when the awaited commit is + * acknowledged, else {@link CasCommitOutcome#UNCONFIRMED} — on the request thread, inside + * {@link #mutateCas}, after the CLIENT_OPERATION {@link #onCasCommit};
    • + *
    • the v2 {@code begin()}-path {@link CasCommitOrigin#REPAIR_IN_PROGRESS} and + * {@link CasCommitOrigin#REFRESH_COMMITTED} sites (commit fused into the following prepare): + * {@link CasCommitOutcome#CONFIRMED_BY_PREPARE} when that prepare reaches a promise-quorum, + * {@link CasCommitOutcome#SUPERSEDED} if pre-empted, else {@link CasCommitOutcome#UNCONFIRMED};
    • + *
    • the v1 in-progress {@link CasCommitOrigin#REPAIR_IN_PROGRESS} repair + * ({@code beginAndRepairPaxos}): {@link CasCommitOutcome#APPLIED} or + * {@link CasCommitOutcome#UNCONFIRMED};
    • + *
    • background {@code PaxosRepair} ({@link CasCommitOrigin#REPAIR_IN_PROGRESS} / + * {@link CasCommitOrigin#REFRESH_COMMITTED}): {@link CasCommitOutcome#APPLIED} only — this + * background state machine retries on failure rather than delivering a negative terminal, + * so a non-success outcome is not reported (best-effort, on an arbitrary repair thread).
    • + *
    + * NO terminal is delivered (only the dispatched {@link #onCasCommit} is) for the v1 + * {@link CasCommitOrigin#REFRESH_COMMITTED} fire-and-forget {@code sendCommit} (no awaited ack), + * nor for any commit performed at {@code consistencyForCommit == ANY} (which does not block for a + * replica ack). For those, rely on a deferred serial read. + *

    + * Threading and containment mirror {@link #onCasCommit}: implementations should not throw and + * must not block; a thrown exception is logged and ignored by + * {@link MutatorProvider#notifyCasCommitCompleted} and never fails the operation, read or repair. + */ + default void onCasCommitCompleted(Commit committed, ConsistencyLevel consistencyLevel, + CasCommitOrigin origin, CasCommitOutcome outcome) + { + // no-op + } + + /** + * Used to persist the given batch of mutations. Usually invoked as part of + * {@link #mutateAtomically(Collection, ConsistencyLevel, boolean, long, ClientRequestsMetrics, ClientState)}. + */ + void persistBatchlog(Collection mutations, Dispatcher.RequestTime requestTime, ReplicaPlan.ForWrite replicaPlan, TimeUUID batchUUID); + + /** + * Used to clear the given batch id. Usually invoked as part of + * {@link #mutateAtomically(Collection, ConsistencyLevel, boolean, long, ClientRequestsMetrics, ClientState)}. + */ + void clearBatchlog(String keyspace, Dispatcher.RequestTime requestTime, ReplicaPlan.ForWrite replicaPlan, TimeUUID batchUUID); + + /** + * Callback invoked when the given {@code mutation} is localy applied. + */ + default void onAppliedMutation(IMutation mutation) + { + // no-op + } + + /** + * Callback invoked when the given {@code counter} is localy applied. + */ + default void onAppliedCounter(IMutation counter, AbstractWriteResponseHandler handler) + { + // no-op + } + + /** + * Callback invoked when the given {@code proposal} is localy committed. + */ + default void onAppliedProposal(Commit proposal) + { + // no-op + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/MutatorProvider.java b/src/java/org/apache/cassandra/service/MutatorProvider.java new file mode 100644 index 000000000000..2a0cbc1352a8 --- /dev/null +++ b/src/java/org/apache/cassandra/service/MutatorProvider.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.service.paxos.Commit; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.NoSpamLogger; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_MUTATOR_CLASS; + +/** + * Provides an instance of {@link Mutator} that facilitates mutation writes for standard mutations, unlogged batches, + * counters and paxos commits (LWT)s. + *
    + * An implementation may choose to fallback to the default implementation ({@link StorageProxy.DefaultMutator}) + * obtained via {@link #getDefaultMutator()}. + */ +public abstract class MutatorProvider +{ + private static final Logger logger = LoggerFactory.getLogger(MutatorProvider.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + + // public so that the paxos engines (org.apache.cassandra.service.paxos) can reach the + // installed singleton for Mutator.onCasCommit notifications without re-constructing it + public static final Mutator instance = getCustomOrDefault(); + + /** + * Notifies the installed Mutator of a commit dispatch (see {@link Mutator#onCasCommit}), + * containing any exception a misbehaving implementation throws: a notification failure must + * never abort the paxos operation, serial read or repair that is dispatching the commit. + */ + public static void notifyCasCommit(Commit committed, ConsistencyLevel consistencyLevel, Mutator.CasCommitOrigin origin) + { + try + { + instance.onCasCommit(committed, consistencyLevel, origin); + } + catch (Throwable t) + { + // Let fatal errors (OOM etc.) reach the JVM failure policy before we swallow. + JVMStabilityInspector.inspectThrowable(t); + noSpamLogger.warn("Custom mutator onCasCommit({}) failed; ignoring", origin, t); + } + } + + /** + * Notifies the installed Mutator of the terminal outcome of a previously-announced commit (see + * {@link Mutator#onCasCommitCompleted}), containing any exception a misbehaving implementation + * throws: a notification failure must never abort the paxos operation, serial read or repair. + */ + public static void notifyCasCommitCompleted(Commit committed, ConsistencyLevel consistencyLevel, + Mutator.CasCommitOrigin origin, Mutator.CasCommitOutcome outcome) + { + try + { + instance.onCasCommitCompleted(committed, consistencyLevel, origin, outcome); + } + catch (Throwable t) + { + // Let fatal errors (OOM etc.) reach the JVM failure policy before we swallow. + JVMStabilityInspector.inspectThrowable(t); + noSpamLogger.warn("Custom mutator onCasCommitCompleted({}, {}) failed; ignoring", origin, outcome, t); + } + } + + public static Mutator getCustomOrDefault() + { + if (CUSTOM_MUTATOR_CLASS.isPresent()) + { + return FBUtilities.construct(CUSTOM_MUTATOR_CLASS.getString(), + "custom mutator class (set with " + CUSTOM_MUTATOR_CLASS.getKey() + ")"); + } + else + { + return getDefaultMutator(); + } + } + + public static Mutator getDefaultMutator() + { + return new StorageProxy.DefaultMutator(); + } +} diff --git a/src/java/org/apache/cassandra/service/NativeTransportService.java b/src/java/org/apache/cassandra/service/NativeTransportService.java index c51b29e80485..b24be8041dd4 100644 --- a/src/java/org/apache/cassandra/service/NativeTransportService.java +++ b/src/java/org/apache/cassandra/service/NativeTransportService.java @@ -38,7 +38,7 @@ import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.Server; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_EPOLL_ENABLED; @@ -129,7 +129,7 @@ synchronized void initialize() */ public void start() { - logger.info("Using Netty Version: {}", Version.identify().entrySet()); + logger.debug("Using Netty Version: {}", Version.identify().entrySet()); initialize(); servers.forEach(Server::start); } @@ -169,7 +169,7 @@ public static boolean useEpoll() { final boolean enableEpoll = NATIVE_EPOLL_ENABLED.getBoolean(); - if (enableEpoll && !Epoll.isAvailable() && NativeLibrary.osType == NativeLibrary.OSType.LINUX) + if (enableEpoll && !Epoll.isAvailable() && INativeLibrary.instance.isOS(INativeLibrary.OSType.LINUX)) logger.warn("epoll not available", Epoll.unavailabilityCause()); return enableEpoll && Epoll.isAvailable(); diff --git a/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java b/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java index cd096a888e9c..80586b7c50fc 100644 --- a/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java +++ b/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java @@ -19,8 +19,14 @@ package org.apache.cassandra.service; import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; +import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; @@ -30,9 +36,11 @@ import org.apache.cassandra.concurrent.SequentialExecutorPlus.AtLeastOnceTrigger; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.TokenMetadataProvider; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.utils.ExecutorUtils; +import static java.util.Objects.requireNonNull; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -44,29 +52,89 @@ public class PendingRangeCalculatorService // the executor will only run a single range calculation at a time while keeping at most one task queued in order // to trigger an update only after the most recent state change and not for each update individually - private final SequentialExecutorPlus executor = executorFactory() - .withJmxInternal() - .configureSequential("PendingRangeCalculator") - .withRejectedExecutionHandler((r, e) -> {}) // silently handle rejected tasks, this::update takes care of bookkeeping - .build(); - - private final AtLeastOnceTrigger update = executor.atLeastOnceTrigger(() -> { - PendingRangeCalculatorServiceDiagnostics.taskStarted(1); - long start = currentTimeMillis(); - Collection keyspaces = Schema.instance.distributedKeyspaces().names(); - for (String keyspaceName : keyspaces) - calculatePendingRanges(Keyspace.open(keyspaceName).getReplicationStrategy(), keyspaceName); - if (logger.isTraceEnabled()) - logger.trace("Finished PendingRangeTask for {} keyspaces in {}ms", keyspaces.size(), currentTimeMillis() - start); - PendingRangeCalculatorServiceDiagnostics.taskFinished(); - }); + private final SequentialExecutorPlus executor; + + private final Schema schema; + + private final AtLeastOnceTrigger update; + + private final Set keyspacesWithPendingRanges = new CopyOnWriteArraySet<>(); + + private final TokenMetadataProvider tokenMetadataProvider; + + private void doUpdate() + { + // repeat until all keyspaced are consumed + while (!keyspacesWithPendingRanges.isEmpty()) + { + long start = currentTimeMillis(); + + int updated = 0; + int total = 0; + PendingRangeCalculatorServiceDiagnostics.taskStarted(1); + try + { + Set keyspaces = new HashSet<>(keyspacesWithPendingRanges); + total = keyspaces.size(); + keyspacesWithPendingRanges.removeAll(keyspaces); // only remove those which were consumed + + Iterator it = keyspaces.iterator(); + while (it.hasNext()) + { + String keyspaceName = it.next(); + try + { + calculatePendingRanges(keyspaceName); + it.remove(); + updated++; + } + catch (RuntimeException | Error ex) + { + logger.error("Error calculating pending ranges for keyspace {}", keyspaceName, ex); + } + } + } + finally + { + PendingRangeCalculatorServiceDiagnostics.taskFinished(); + if (logger.isTraceEnabled()) + logger.trace("Finished PendingRangeTask for {} keyspaces out of {} in {}ms", updated, total, currentTimeMillis() - start); + } + } + } public PendingRangeCalculatorService() { + this("PendingRangeCalculator", Schema.instance); + } + + public PendingRangeCalculatorService(String executorName, Schema schema) + { + this(executorFactory().withJmxInternal() + .configureSequential(executorName) + .withRejectedExecutionHandler((r, e) -> {}) // silently handle rejected tasks, this::update takes care of bookkeeping + .build(), + TokenMetadataProvider.instance, + schema); + } + + public PendingRangeCalculatorService(SequentialExecutorPlus executor, TokenMetadataProvider tokenMetadataProvider, Schema schema) + { + this.executor = requireNonNull(executor); + this.tokenMetadataProvider = requireNonNull(tokenMetadataProvider); + this.schema = requireNonNull(schema); + this.update = executor.atLeastOnceTrigger(this::doUpdate); } public void update() { + update(keyspaceName -> true); + } + + public void update(Predicate keyspaceNamePredicate) + { + Collection affectedKeyspaces = schema.distributedKeyspaces().names().stream().filter(keyspaceNamePredicate).collect(Collectors.toList()); + keyspacesWithPendingRanges.addAll(affectedKeyspaces); boolean success = update.trigger(); if (!success) PendingRangeCalculatorServiceDiagnostics.taskRejected(1); else PendingRangeCalculatorServiceDiagnostics.taskCountChanged(1); @@ -77,16 +145,23 @@ public void blockUntilFinished() update.sync(); } - public void executeWhenFinished(Runnable runnable) { update.runAfter(runnable); } - // public & static for testing purposes - public static void calculatePendingRanges(AbstractReplicationStrategy strategy, String keyspaceName) + @VisibleForTesting + protected void calculatePendingRanges(String keyspaceName) + { + Keyspace keyspace = Keyspace.open(keyspaceName); + AbstractReplicationStrategy strategy = keyspace.getReplicationStrategy(); + calculatePendingRanges(strategy, keyspaceName); + } + + @VisibleForTesting + public void calculatePendingRanges(AbstractReplicationStrategy strategy, String keyspaceName) { - StorageService.instance.getTokenMetadata().calculatePendingRanges(strategy, keyspaceName); + tokenMetadataProvider.getTokenMetadataForKeyspace(keyspaceName).calculatePendingRanges(strategy, keyspaceName); } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/service/QueryInfoTracker.java b/src/java/org/apache/cassandra/service/QueryInfoTracker.java new file mode 100644 index 000000000000..85ccf457f632 --- /dev/null +++ b/src/java/org/apache/cassandra/service/QueryInfoTracker.java @@ -0,0 +1,346 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.service; + +import java.util.Collection; +import java.util.List; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.schema.TableMetadata; + +/** + * A tracker notified about executed queries. + * + *

    The goal of this interface is to provide enough information to accurately estimate how + * much "work" a query has performed. So while for writes this mostly just means passing the generated mutations, for + * reads this means passing the unfiltered result of the query. + * + *

    The tracker methods are called from {@link StorageProxy} and are thus "coordinator level". All + * user queries and internal distributed system table queries trigger a call to one of these methods. + * Internal local system table queries don't. + * + *

    For writes, the {@link #onWrite} method is only called for the "user write", but if that write triggers either + * secondary index or materialized views updates, those additional updates do not trigger additional calls. + * + *

    The tracker methods are called on write and read hot paths, so they should be as lightweight as possible. + */ +public interface QueryInfoTracker +{ + /** + * Called before every (non-LWT) write coordinated on the local node. + * + * @param state the state of the client that performed the write + * @param isLogged whether this is a logged batch write. + * @param mutations the mutations written by the write. + * @param consistencyLevel the consistency level of the write. + * @return a tracker that should be notified when either the read error out or completes successfully. + */ + WriteTracker onWrite(ClientState state, + boolean isLogged, + Collection mutations, + ConsistencyLevel consistencyLevel); + + /** + * Called before every non-range read coordinated on the local node. + * + * @param state the state of the client that performed the read + * @param table the metadata for the table read. + * @param commands the commands for the read performed. + * @param consistencyLevel the consistency level of the read. + * @return a tracker that should be notified when either the read error out or completes successfully. + */ + ReadTracker onRead(ClientState state, + TableMetadata table, + List commands, + ConsistencyLevel consistencyLevel); + + /** + * Called before every range read coordinated on the local node. + * + * @param state the state of the client that performed the range read + * @param table the metadata for the table read. + * @param command the command for the read performed. + * @param consistencyLevel the consistency level of the read. + * @return a tracker that should be notified when either the read error out or completes successfully. + */ + ReadTracker onRangeRead(ClientState state, + TableMetadata table, + PartitionRangeReadCommand command, + ConsistencyLevel consistencyLevel); + + /** + * Called before every LWT coordinated by the local node. + * + * @param state the state of the client that performed the LWT + * @param table the metadata of the table on which the LWT applies. + * @param key the partition key on which the LWT operates. + * @param serialConsistency the serial consistency of the LWT. + * @param commitConsistency the commit consistency of the LWT. + * @return a {@link LWTWriteTracker} objects whose methods are called as part of the LWT execution. + */ + LWTWriteTracker onLWTWrite(ClientState state, + TableMetadata table, + DecoratedKey key, + ConsistencyLevel serialConsistency, + ConsistencyLevel commitConsistency); + + /** + * A tracker that does nothing. + */ + QueryInfoTracker NOOP = new QueryInfoTracker() + { + @Override + public WriteTracker onWrite(ClientState state, + boolean isLogged, + Collection mutations, + ConsistencyLevel consistencyLevel) + { + return WriteTracker.NOOP; + } + + @Override + public ReadTracker onRead(ClientState state, + TableMetadata table, + List commands, + ConsistencyLevel consistencyLevel) + { + return ReadTracker.NOOP; + } + + @Override + public ReadTracker onRangeRead(ClientState state, + TableMetadata table, + PartitionRangeReadCommand command, + ConsistencyLevel consistencyLevel) + { + return ReadTracker.NOOP; + } + + @Override + public LWTWriteTracker onLWTWrite(ClientState state, + TableMetadata table, + DecoratedKey key, + ConsistencyLevel serialConsistency, + ConsistencyLevel commitConsistency) + { + return LWTWriteTracker.NOOP; + } + }; + + /** + * A tracker for a specific query. + * + *

    For the tracked query, exactly one of its method should be called. + */ + interface Tracker + { + /** + * Called when the tracked query completes successfully. + */ + void onDone(); + + /** + * Called when the tracked query completes with an error. + */ + void onError(Throwable exception); + } + + /** + * Tracker for a write query. + */ + interface WriteTracker extends Tracker + { + WriteTracker NOOP = new WriteTracker() + { + @Override + public void onDone() + { + } + + @Override + public void onError(Throwable exception) + { + } + }; + } + + /** + * Tracker for a read query. + */ + interface ReadTracker extends Tracker + { + /** + * Called just before the queries are sent to the replica plan contacts. + * Note that this callback method may be invoked more than once for a given read, + * e.g. range quries spanning multiple partitions are internally issued as a + * number of subrange requests to different replicas (with different + * ReplicaPlans). This callback is called at least once for a given read. + * + * @param replicaPlan the queried nodes. + */ + void onReplicaPlan(ReplicaPlan.ForRead replicaPlan); + + /** + * Called on every new reconciled partition. + * + * @param partitionKey the partition key. + */ + void onPartition(DecoratedKey partitionKey); + + /** + * Called on every row read. + * + * @param row the merged row. + */ + void onRow(Row row); + + /** + * Called on every partition after filtering and post-processing + * + * @param partitionKey + */ + void onFilteredPartition(DecoratedKey partitionKey); + + /** + * Called on every row after filtering and post-processing + * + * @param row the merged row. + */ + void onFilteredRow(Row row); + + ReadTracker NOOP = new ReadTracker() + { + @Override + public void onReplicaPlan(ReplicaPlan.ForRead replicaPlan) + { + } + + @Override + public void onPartition(DecoratedKey partitionKey) + { + } + + @Override + public void onRow(Row row) + { + } + + @Override + public void onFilteredPartition(DecoratedKey partitionKey) + { + } + + @Override + public void onFilteredRow(Row row) + { + } + + @Override + public void onDone() + { + } + + @Override + public void onError(Throwable exception) + { + } + }; + } + + /** + * Tracker for LWTs, used to get information on the actual work done by the LWT. + * + *

    For a given LWT, the tracker created by {@link #onLWTWrite} will first have its read + * methods called. Then, based on that read result and the LWT conditions, either the {@link #onNotApplied()} or + * the {@link #onApplied} method will be called. + */ + interface LWTWriteTracker extends ReadTracker + { + /** + * Called if the LWT this is tracking does not apply (it's condition evaluates to {@code false}). + */ + void onNotApplied(); + + /** + * Called if the LWT this is tracking does apply. + * + * @param update the update that is committed by the LWT. + */ + void onApplied(PartitionUpdate update); + + /** + * A tracker that does nothing. + */ + LWTWriteTracker NOOP = new LWTWriteTracker() + { + @Override + public void onReplicaPlan(ReplicaPlan.ForRead replicaPlan) + { + } + + @Override + public void onPartition(DecoratedKey partitionKey) + { + } + + @Override + public void onRow(Row row) + { + } + + @Override + public void onFilteredPartition(DecoratedKey partitionKey) + { + } + + @Override + public void onFilteredRow(Row row) + { + } + + @Override + public void onNotApplied() + { + } + + @Override + public void onApplied(PartitionUpdate update) + { + } + + @Override + public void onDone() + { + } + + @Override + public void onError(Throwable exception) + { + } + }; + + } +} diff --git a/src/java/org/apache/cassandra/service/QueryState.java b/src/java/org/apache/cassandra/service/QueryState.java index d4d4d73717f5..8aa6056468e1 100644 --- a/src/java/org/apache/cassandra/service/QueryState.java +++ b/src/java/org/apache/cassandra/service/QueryState.java @@ -19,6 +19,7 @@ import java.net.InetAddress; +import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.utils.FBUtilities; /** @@ -114,4 +115,15 @@ public InetAddress getClientAddress() { return clientState.getClientAddress(); } + + /** + * Checks if this user is an ordinary user (not a super or system user). + * + * @return {@code true} if this user is an ordinary user, {@code false} otherwise. + */ + public boolean isOrdinaryUser() + { + AuthenticatedUser user = getClientState().getUser(); + return !getClientState().isInternal && null != user && !user.isSystem() && !user.isSuper(); + } } diff --git a/src/java/org/apache/cassandra/service/RangeRelocator.java b/src/java/org/apache/cassandra/service/RangeRelocator.java index b63c105bd2f5..bb9daa8280bd 100644 --- a/src/java/org/apache/cassandra/service/RangeRelocator.java +++ b/src/java/org/apache/cassandra/service/RangeRelocator.java @@ -36,7 +36,7 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.RangeStreamer; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.EndpointsByReplica; import org.apache.cassandra.locator.EndpointsForRange; @@ -100,7 +100,7 @@ private static Multimap calculat tmdBefore, tmdAfter, keyspace, - Arrays.asList(new RangeStreamer.FailureDetectorSourceFilter(FailureDetector.instance), + Arrays.asList(new RangeStreamer.FailureDetectorSourceFilter(IFailureDetector.instance), new RangeStreamer.ExcludeLocalNodeFilter())); return RangeStreamer.convertPreferredEndpointsToWorkMap(preferredEndpoints); } diff --git a/src/java/org/apache/cassandra/service/StartupChecks.java b/src/java/org/apache/cassandra/service/StartupChecks.java index 934a17b06494..3c37cab141bf 100644 --- a/src/java/org/apache/cassandra/service/StartupChecks.java +++ b/src/java/org/apache/cassandra/service/StartupChecks.java @@ -44,40 +44,48 @@ import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import com.google.common.collect.Range; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vdurmont.semver4j.Semver; import net.jpountz.lz4.LZ4Factory; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DataStorageSpec; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.StartupChecksOptions; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.compaction.LeveledCompactionStrategy; +import org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy; +import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.StartupException; +import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.UUIDBasedSSTableId; +import org.apache.cassandra.io.sstable.format.bti.BtiFormat; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.locator.SimpleSnitch; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JavaUtils; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; import org.apache.cassandra.utils.SigarLibrary; import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_LOCAL_PORT; import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_PORT; -import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORE_KERNEL_BUG_1057843_CHECK; import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_VERSION; import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_VM_NAME; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -132,8 +140,7 @@ public enum StartupCheckType // The default set of pre-flight checks to run. Order is somewhat significant in that we probably // always want the system keyspace check run last, as this actually loads the schema for that // keyspace. All other checks should not require any schema initialization. - private final List DEFAULT_TESTS = ImmutableList.of(checkKernelBug1057843, - checkJemalloc, + private final List DEFAULT_TESTS = ImmutableList.of(checkJemalloc, checkLz4Native, checkValidLaunchDate, checkJMXPorts, @@ -149,8 +156,12 @@ public enum StartupCheckType checkDatacenter, checkRack, checkLegacyAuthTables, + checkYamlConfig, + checkTableSettings, new DataResurrectionCheck()); + private final static String WARN_SUFFIX = " This will impact your level of production support."; + public StartupChecks withDefaultTests() { preFlightChecks.addAll(DEFAULT_TESTS); @@ -191,64 +202,6 @@ public void verify(StartupChecksOptions options) throws StartupException } } - // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1057843 - public static final StartupCheck checkKernelBug1057843 = new StartupCheck() - { - @Override - public void execute(StartupChecksOptions startupChecksOptions) throws StartupException - { - if (startupChecksOptions.isDisabled(getStartupCheckType())) - return; - - if (!FBUtilities.isLinux) - return; - - Set directIOWritePaths = new HashSet<>(); - if (DatabaseDescriptor.getCommitLogWriteDiskAccessMode() == Config.DiskAccessMode.direct) - directIOWritePaths.add(new File(DatabaseDescriptor.getCommitLogLocation()).toPath()); - // TODO: add data directories when direct IO is supported for flushing and compaction - - if (!directIOWritePaths.isEmpty() && IGNORE_KERNEL_BUG_1057843_CHECK.getBoolean()) - { - logger.info("Ignoring check for the kernel bug 1057843 against the following paths configured to be accessed with Direct IO: {}", directIOWritePaths); - return; - } - - Set affectedFileSystemTypes = Set.of("ext4"); - Set affectedPaths = new HashSet<>(); - for (Path path : directIOWritePaths) - { - try - { - if (affectedFileSystemTypes.contains(Files.getFileStore(path).type().toLowerCase())) - affectedPaths.add(path); - } - catch (IOException e) - { - throw new StartupException(StartupException.ERR_WRONG_MACHINE_STATE, "Failed to determine file system type for path " + path, e); - } - } - - if (affectedPaths.isEmpty()) - return; - - Range affectedKernels = Range.closedOpen(new Semver("6.1.64", Semver.SemverType.LOOSE), - new Semver("6.1.66", Semver.SemverType.LOOSE)); - - Semver kernelVersion = FBUtilities.getKernelVersion(); - if (!affectedKernels.contains(kernelVersion.withClearedSuffixAndBuild())) - return; - - throw new StartupException(StartupException.ERR_WRONG_MACHINE_STATE, - String.format("Detected kernel version %s with affected file system types %s and direct IO enabled for paths %s. " + - "This combination is known to cause data corruption. To start Cassandra in this environment, " + - "you have to disable direct IO for the affected paths. If you are sure the verification provided " + - "a false positive result, you can suppress it by setting '" + IGNORE_KERNEL_BUG_1057843_CHECK.getKey() + "' system property to 'true'. " + - "Please see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1057843 for more information.", - kernelVersion, affectedFileSystemTypes, affectedPaths)); - } - }; - public static final StartupCheck checkJemalloc = new StartupCheck() { @Override @@ -383,7 +336,7 @@ private void checkOutOfMemoryHandling() if (!jvmOptionsContainsOneOf("-XX:OnOutOfMemoryError=")) logger.warn("The JVM is not configured to stop on OutOfMemoryError which can cause data corruption." + " Either upgrade your JRE to a version greater or equal to 8u92 and use -XX:+ExitOnOutOfMemoryError/-XX:+CrashOnOutOfMemoryError" - + " or use -XX:OnOutOfMemoryError=\";\" on your current JRE."); + + " or use -XX:OnOutOfMemoryError=\";\" on your current JRE." + WARN_SUFFIX); } } @@ -414,7 +367,7 @@ public void execute(StartupChecksOptions options) throws StartupException if (options.isDisabled(getStartupCheckType())) return; // Fail-fast if the native library could not be linked. - if (!NativeLibrary.isAvailable()) + if (!INativeLibrary.instance.isAvailable()) throw new StartupException(StartupException.ERR_WRONG_MACHINE_STATE, "The native library could not be initialized properly. "); } }; @@ -553,8 +506,8 @@ public void execute(StartupChecksOptions options) long maxMapCount = getMaxMapCount(); if (maxMapCount < EXPECTED_MAX_MAP_COUNT) logger.warn("Maximum number of memory map areas per process (vm.max_map_count) {} " + - "is too low, recommended value: {}, you can change it with sysctl.", - maxMapCount, EXPECTED_MAX_MAP_COUNT); + "is too low, recommended value: {}, you can change it with sysctl. {}", + maxMapCount, EXPECTED_MAX_MAP_COUNT, WARN_SUFFIX); } }; @@ -566,30 +519,30 @@ public void execute(StartupChecksOptions options) throws StartupException if (options.isDisabled(getStartupCheckType())) return; // check all directories(data, commitlog, saved cache) for existence and permission - Iterable dirs = Iterables.concat(Arrays.asList(DatabaseDescriptor.getAllDataFileLocations()), + Iterable dirs = Iterables.concat(Arrays.asList(DatabaseDescriptor.getAllDataFileLocations()), Arrays.asList(DatabaseDescriptor.getCommitLogLocation(), DatabaseDescriptor.getSavedCachesLocation(), - DatabaseDescriptor.getHintsDirectory().absolutePath())); - for (String dataDir : dirs) - { - logger.debug("Checking directory {}", dataDir); - File dir = new File(dataDir); - + DatabaseDescriptor.getHintsDirectory())); + for (File dir : dirs) { + logger.debug("Checking directory {}", dir); + // check that directories exist. - if (!dir.exists()) - { - logger.warn("Directory {} doesn't exist", dataDir); + if (!dir.exists()) { + logger.warn("Directory {} doesn't exist", dir); // if they don't, failing their creation, stop cassandra. if (!dir.tryCreateDirectories()) throw new StartupException(StartupException.ERR_WRONG_DISK_STATE, - "Has no permission to create directory "+ dataDir); + "Has no permission to create directory " + dir); } - + // if directories exist verify their permissions - if (!Directories.verifyFullPermissions(dir, dataDir)) + if (!Directories.verifyFullPermissions(dir)) throw new StartupException(StartupException.ERR_WRONG_DISK_STATE, - "Insufficient permissions on directory " + dataDir); + "Insufficient permissions on directory " + dir); } + if (DatabaseDescriptor.getAllDataFileLocations().length > 1) + logger.warn("Multiple {} data_dir configured. Best practice is to use a (striped) LVM. {}", + DatabaseDescriptor.getAllDataFileLocations().length, WARN_SUFFIX); } }; @@ -676,11 +629,11 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th } }; - for (String dataDir : DatabaseDescriptor.getAllDataFileLocations()) + for (File dataDir : DatabaseDescriptor.getAllDataFileLocations()) { try { - Files.walkFileTree(new File(dataDir).toPath(), sstableVisitor); + Files.walkFileTree(dataDir.toPath(), sstableVisitor); } catch (IOException e) { @@ -865,4 +818,132 @@ static Optional checkLegacyAuthTablesMessage() else return Optional.empty(); }; + + public static final StartupCheck checkYamlConfig = new StartupCheck() + { + @Override + public void execute(StartupChecksOptions options) throws StartupException + { + if (options.isDisabled(getStartupCheckType())) + return; + + if (Murmur3Partitioner.instance != DatabaseDescriptor.getPartitioner()) + logger.warn("Not using murmur3 partitioner ({}). {}", DatabaseDescriptor.getPartitioner().getClass().getName(), WARN_SUFFIX); + + if (DatabaseDescriptor.getEndpointSnitch() instanceof SimpleSnitch) + logger.warn("SimpleSnitch is only for dev/test environments. {}", WARN_SUFFIX); + + if (DatabaseDescriptor.getNumTokens() > 16) + logger.warn("num_tokens {} too high. Values over 16 poorly impact repairs and node bootstrapping/decommissioning. {}", + DatabaseDescriptor.getNumTokens(), WARN_SUFFIX); + + if (!BtiFormat.NAME.equals(DatabaseDescriptor.getSelectedSSTableFormat().name())) + logger.warn("Trie-based SSTables (bti) should always be the default (current is {}). {}", DatabaseDescriptor.getSelectedSSTableFormat().name(), WARN_SUFFIX); + + // ServerTestUtils.prepare() enables transient replication, so we have to skip this when we're inside a unit test + if(DatabaseDescriptor.isTransientReplicationEnabled() && "cassandra.testtag_IS_UNDEFINED".equals(CassandraRelevantProperties.TEST_CASSANDRA_TESTTAG.getString())) + throw new StartupException(StartupException.ERR_WRONG_CONFIG, "Transient Replication cannot be used in HCD."); + + if(DatabaseDescriptor.getMaterializedViewsEnabled()) + logger.warn("Materialised Views should not be enabled. {}", WARN_SUFFIX); + + // skip this when we're inside a unit test + if(DatabaseDescriptor.getSASIIndexesEnabled() && "cassandra.testtag_IS_UNDEFINED".equals(CassandraRelevantProperties.TEST_CASSANDRA_TESTTAG.getString())) + throw new StartupException(StartupException.ERR_WRONG_CONFIG, "SASI cannot be used in HCD. All SASI indexes must be dropped and this option disabled."); + + if(DatabaseDescriptor.enableDropCompactStorage()) + logger.warn("Using `DROP COMPACT STORAGE` on tables should not be enabled. {}", WARN_SUFFIX); + + if(null != DatabaseDescriptor.getDefaultCompaction() + && !UnifiedCompactionStrategy.class.getSimpleName().equals(DatabaseDescriptor.getDefaultCompaction().class_name)) + logger.warn("UnifiedCompactionStrategy should always be the default. {}", WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getTombstoneFailThreshold() > 100000) + logger.warn("Guardrails value {} for tombstone_failure_threshold is too high (>100000). {}", + DatabaseDescriptor.getGuardrailsConfig().getTombstoneFailThreshold(), WARN_SUFFIX); + + if (DatabaseDescriptor.getBatchSizeFailThresholdInKiB() > 640) + logger.warn("Guardrails value {} for batch_size_fail_threshold_in_kb is too high (>640). {}", + DatabaseDescriptor.getBatchSizeFailThresholdInKiB(), WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getColumnsPerTableFailThreshold() > 200) + logger.warn("Guardrails value {} for columns_per_table_fail_threshold is too high (>200). {}", + DatabaseDescriptor.getGuardrailsConfig().getColumnsPerTableFailThreshold(), WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getFieldsPerUDTFailThreshold() > 100) + logger.warn("Guardrails value {} for fields_per_udt_fail_threshold is too high (>100). {}", + DatabaseDescriptor.getGuardrailsConfig().getFieldsPerUDTFailThreshold(), WARN_SUFFIX); + + DataStorageSpec.LongBytesBound collectionSizeWarnThreshold = DatabaseDescriptor.getGuardrailsConfig().getCollectionSizeWarnThreshold(); + if (collectionSizeWarnThreshold != null && collectionSizeWarnThreshold.toKibibytes() > 10480) + logger.warn("Guardrails value {} for collection_size_warn_threshold is too high (>10480). {}", + collectionSizeWarnThreshold, WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getItemsPerCollectionWarnThreshold() > 200) + logger.warn("Guardrails value {} for items_per_collection_warn_threshold is too high (>200). {}", + DatabaseDescriptor.getGuardrailsConfig().getItemsPerCollectionWarnThreshold(), WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getTablesWarnThreshold() > 100) + logger.warn("Guardrails value {} for tables_warn_threshold is too high (>100). {}", + DatabaseDescriptor.getGuardrailsConfig().getTablesWarnThreshold(), WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getTablesFailThreshold() > 200) + logger.warn("Guardrails value {} for tables_fail_threshold is too high (>200). {}", + DatabaseDescriptor.getGuardrailsConfig().getTablesFailThreshold(), WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getInSelectCartesianProductFailThreshold() > 25) + logger.warn("Guardrails value {} for in_select_cartesian_product_fail_threshold is too high (>25). {}", + DatabaseDescriptor.getGuardrailsConfig().getInSelectCartesianProductFailThreshold(), WARN_SUFFIX); + + if (DatabaseDescriptor.getGuardrailsConfig().getPartitionKeysInSelectFailThreshold() > 20) + logger.warn("Guardrails value {} for partition_keys_in_select_fail_threshold is too high (>20). {}", + DatabaseDescriptor.getGuardrailsConfig().getPartitionKeysInSelectFailThreshold(), WARN_SUFFIX); + + if (!DatabaseDescriptor.getGuardrailsConfig().getWriteConsistencyLevelsDisallowed().contains(ConsistencyLevel.ANY)) + logger.warn("Guardrails value \"{}\" for write_consistency_levels_disallowed does not contain \"ANY\". {}", + StringUtils.join(DatabaseDescriptor.getGuardrailsConfig().getWriteConsistencyLevelsDisallowed(), ','), WARN_SUFFIX); + } + }; + + public static final StartupCheck checkTableSettings = new StartupCheck() + { + @Override + public void execute(StartupChecksOptions options) + { + if (options.isDisabled(getStartupCheckType())) + return; + + List stcsOrLcsTables = new ArrayList<>(); + List compactTables = new ArrayList<>(); + List nonSAITables = new ArrayList<>(); + for (String ksName : Schema.instance.getUserKeyspaces()) + { + KeyspaceMetadata ks = Schema.instance.getKeyspaceMetadata(ksName); + if (ks == null) + continue; + for (TableMetadata t : ks.tables) + { + if (SizeTieredCompactionStrategy.class == t.params.compaction.klass() || LeveledCompactionStrategy.class == t.params.compaction.klass()) + stcsOrLcsTables.add(t.keyspace + '.' + t.name); + if (t.isCompactTable()) + compactTables.add(t.keyspace + '.' + t.name); + for (IndexMetadata i : t.indexes) + if (!StorageAttachedIndex.class.getName().equals(i.getIndexClassName())) + nonSAITables.add(t.keyspace + '.' + t.name); + } + } + + if (!stcsOrLcsTables.isEmpty()) + logger.warn("The following tables using STCS and LCS should be altered to use UnifiedCompactionStrategy (UCS): {}. {}", + StringUtils.join(stcsOrLcsTables, ','), WARN_SUFFIX); + + if (!compactTables.isEmpty()) + logger.warn("The following tables are `WITH COMPACT STORAGE` and need to be manually migrated to normal tables (Avoid using `DROP COMPACT STORAGE`): {}. {}", + StringUtils.join(compactTables, ','), WARN_SUFFIX); + + if (!nonSAITables.isEmpty()) + logger.warn("The following tables with non-SAI indexes should be altered to use SAI: {}. {}", + StringUtils.join(nonSAITables, ','), WARN_SUFFIX); + } + }; } diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java index 5e966fa9d316..b84e5be15429 100644 --- a/src/java/org/apache/cassandra/service/StorageProxy.java +++ b/src/java/org/apache/cassandra/service/StorageProxy.java @@ -39,12 +39,11 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; +import javax.annotation.Nullable; import com.google.common.base.Preconditions; import com.google.common.cache.CacheLoader; import com.google.common.collect.Iterables; -import com.google.common.util.concurrent.Uninterruptibles; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +57,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.CounterMutation; +import org.apache.cassandra.db.CounterMutationCallback; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.IMutation; import org.apache.cassandra.db.Keyspace; @@ -70,6 +70,7 @@ import org.apache.cassandra.db.RejectException; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.TruncateRequest; +import org.apache.cassandra.db.WriteOptions; import org.apache.cassandra.db.WriteType; import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; import org.apache.cassandra.db.partitions.FilteredPartition; @@ -92,11 +93,12 @@ import org.apache.cassandra.exceptions.RequestFailureException; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.exceptions.RequestTimeoutException; +import org.apache.cassandra.exceptions.TruncateException; import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.exceptions.WriteFailureException; import org.apache.cassandra.exceptions.WriteTimeoutException; -import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.hints.Hint; import org.apache.cassandra.hints.HintsService; import org.apache.cassandra.locator.AbstractReplicationStrategy; @@ -111,6 +113,8 @@ import org.apache.cassandra.locator.Replicas; import org.apache.cassandra.metrics.CASClientRequestMetrics; import org.apache.cassandra.metrics.ClientRequestSizeMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetricsProvider; import org.apache.cassandra.metrics.DenylistMetrics; import org.apache.cassandra.metrics.ReadRepairMetrics; import org.apache.cassandra.metrics.StorageMetrics; @@ -125,6 +129,11 @@ import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.paxos.Ballot; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.ContentionStrategy; @@ -132,6 +141,7 @@ import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.service.paxos.v1.PrepareCallback; import org.apache.cassandra.service.paxos.v1.ProposeCallback; +import org.apache.cassandra.service.paxos.PaxosUtils; import org.apache.cassandra.service.reads.AbstractReadExecutor; import org.apache.cassandra.service.reads.ReadCallback; import org.apache.cassandra.service.reads.range.RangeCommands; @@ -149,20 +159,10 @@ import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import static com.google.common.collect.Iterables.concat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import static com.google.common.collect.Iterables.concat; -import static org.apache.commons.lang3.StringUtils.join; - import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casReadMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casWriteMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetricsForLevel; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.viewWriteMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetricsForLevel; import static org.apache.cassandra.net.Message.out; import static org.apache.cassandra.net.NoPayload.noPayload; import static org.apache.cassandra.net.Verb.BATCH_STORE_REQ; @@ -183,6 +183,7 @@ import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch; +import static org.apache.commons.lang3.StringUtils.join; public class StorageProxy implements StorageProxyMBean { @@ -199,6 +200,185 @@ public class StorageProxy implements StorageProxyMBean public static final StorageProxy instance = new StorageProxy(); + private static final Mutator mutator = MutatorProvider.instance; + + private static final boolean useDynamicSnitchForCounterLeader = CassandraRelevantProperties.USE_DYNAMIC_SNITCH_FOR_COUNTER_LEADER.getBoolean(); + + public static class DefaultMutator implements Mutator + { + @Override + public AbstractWriteResponseHandler mutateCounter(CounterMutation cm, String localDataCenter, Dispatcher.RequestTime requestTime) + { + return defaultMutateCounter(cm, localDataCenter, requestTime); + } + + @Override + public AbstractWriteResponseHandler mutateCounterOnLeader(CounterMutation mutation, + String localDataCenter, + StorageProxy.WritePerformer performer, + Runnable callback, + Dispatcher.RequestTime requestTime) + { + return performWrite(mutation, mutation.consistency(), localDataCenter, performer, callback, WriteType.COUNTER, requestTime); + } + + @Override + public AbstractWriteResponseHandler mutateStandard(Mutation mutation, ConsistencyLevel consistencyLevel, String localDataCenter, WritePerformer standardWritePerformer, Runnable callback, WriteType writeType, Dispatcher.RequestTime requestTime) + { + return performWrite(mutation, consistencyLevel, localDataCenter, standardWritePerformer, callback, writeType, requestTime); + } + + @Override + public AbstractWriteResponseHandler mutatePaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) + { + return defaultCommitPaxos(proposal, consistencyLevel, allowHints, requestTime); + } + + @Override + public void mutateAtomically(Collection mutations, ConsistencyLevel consistencyLevel, boolean requireQuorumForRemove, Dispatcher.RequestTime requestTime, ClientRequestsMetrics metrics, ClientState clientState) throws UnavailableException, OverloadedException, WriteTimeoutException + { + Tracing.trace("Determining replicas for atomic batch"); + long startTime = nanoTime(); + + QueryInfoTracker.WriteTracker writeTracker = queryTracker().onWrite(clientState, true, mutations, consistencyLevel); + + // Request sensors are utilized to track usages from replicas serving atomic batch request + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(mutations.stream().map(IMutation::getKeyspaceName).toArray(String[]::new)); + RequestTracker.instance.set(sensors); + + if (mutations.stream().anyMatch(mutation -> Keyspace.open(mutation.getKeyspaceName()).getReplicationStrategy().hasTransientReplicas())) + throw new AssertionError("Logged batches are unsupported with transient replication"); + + try + { + + // If we are requiring quorum nodes for removal, we upgrade consistency level to QUORUM unless we already + // require ALL, or EACH_QUORUM. This is so that *at least* QUORUM nodes see the update. + ConsistencyLevel batchConsistencyLevel = requireQuorumForRemove + ? ConsistencyLevel.QUORUM + : consistencyLevel; + + switch (consistencyLevel) + { + case ALL: + case EACH_QUORUM: + batchConsistencyLevel = consistencyLevel; + } + + ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forBatchlogWrite(batchConsistencyLevel == ConsistencyLevel.ANY, + false, + mutations.iterator().next().getKeyspaceName()); + + final TimeUUID batchUUID = nextTimeUUID(); + BatchlogCleanup cleanup = new BatchlogCleanup(mutations.size(), + () -> asyncRemoveFromBatchlog(replicaPlan, batchUUID, requestTime)); + + // add a handler for each mutation - includes checking availability, but doesn't initiate any writes, yet + List wrappers = wrapBatchResponseHandlers(mutations, consistencyLevel, batchConsistencyLevel, cleanup, requestTime, sensors); + + // write to the batchlog + syncWriteToBatchlog(mutations, replicaPlan, batchUUID, requestTime); + + // now actually perform the writes and wait for them to complete + // note this is the actual change between CC and OSS - OSS uses syncWriteBatchedMutations and does not + // include waiting for the batched mutations to complete + asyncWriteBatchedMutations(wrappers, requestTime); + + // wait for batched mutations to complete + for (StorageProxy.WriteResponseHandlerWrapper wrapper : wrappers) + wrapper.handler.get(); + + writeTracker.onDone(); + } + catch (UnavailableException e) + { + metrics.writeMetrics.unavailables.mark(); + metrics.writeMetricsForLevel(consistencyLevel).unavailables.mark(); + Tracing.trace("Unavailable"); + writeTracker.onError(e); + throw e; + } + catch (WriteTimeoutException e) + { + metrics.writeMetrics.timeouts.mark(); + metrics.writeMetricsForLevel(consistencyLevel).timeouts.mark(); + Tracing.trace("Write timeout; received {} of {} required replies", e.received, e.blockFor); + writeTracker.onError(e); + throw e; + } + catch (WriteFailureException e) + { + metrics.writeMetrics.failures.mark(); + metrics.writeMetricsForLevel(consistencyLevel).failures.mark(); + Tracing.trace("Write failure; received {} of {} required replies", e.received, e.blockFor); + writeTracker.onError(e); + throw e; + } + finally + { + long endTime = nanoTime(); + long latency = endTime - startTime; + long serviceLatency = endTime - requestTime.startedAtNanos(); + metrics.writeMetrics.executionTimeMetrics.addNano(latency); + metrics.writeMetrics.serviceTimeMetrics.addNano(serviceLatency); + metrics.writeMetricsForLevel(consistencyLevel).executionTimeMetrics.addNano(latency); + metrics.writeMetricsForLevel(consistencyLevel).serviceTimeMetrics.addNano(serviceLatency); + StorageProxy.updateCoordinatorWriteLatencyTableMetric(mutations, latency); + } + } + + @Override + public void clearBatchlog(String keyspace, Dispatcher.RequestTime requestTime, ReplicaPlan.ForWrite replicaPlan, TimeUUID batchUUID) + { + StorageProxy.asyncRemoveFromBatchlog(replicaPlan, batchUUID, requestTime); + } + + @Override + public void persistBatchlog(Collection mutations, Dispatcher.RequestTime requestTime, ReplicaPlan.ForWrite replicaPlan, TimeUUID batchUUID) + { + // write to the batchlog + StorageProxy.syncWriteToBatchlog(mutations, replicaPlan, batchUUID, requestTime); + } + + private List wrapBatchResponseHandlers(Collection mutations, + ConsistencyLevel consistencyLevel, + ConsistencyLevel batchConsistencyLevel, + BatchlogResponseHandler.BatchlogCleanup cleanup, + Dispatcher.RequestTime requestTime, + RequestSensors sensors) + { + List wrappers = new ArrayList<>(mutations.size()); + + // add a handler for each mutation - includes checking availability, but doesn't initiate any writes, yet + for (Mutation mutation : mutations) + { + // register the sensors for the mutation before the actual write is performed + for (PartitionUpdate pu: mutation.getPartitionUpdates()) + { + if (pu.metadata().isIndex()) continue; + sensors.registerSensor(Context.from(pu.metadata()), Type.WRITE_BYTES); + } + StorageProxy.WriteResponseHandlerWrapper wrapper = StorageProxy.wrapBatchResponseHandler(mutation, + consistencyLevel, + batchConsistencyLevel, + WriteType.BATCH, + cleanup, + requestTime); + // exit early if we can't fulfill the CL at this time. + wrappers.add(wrapper); + } + + return wrappers; + } + + private void asyncWriteBatchedMutations(List wrappers, Dispatcher.RequestTime requestTime) + throws WriteTimeoutException, OverloadedException + { + String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); + StorageProxy.asyncWriteBatchedMutations(wrappers, localDataCenter, Stage.MUTATION, requestTime); + } + } + private static volatile int maxHintsInProgress = 128 * FBUtilities.getAvailableProcessors(); private static final CacheLoader hintsInProgress = new CacheLoader() { @@ -213,6 +393,7 @@ public AtomicInteger load(InetAddressAndPort inetAddress) private static final PartitionDenylist partitionDenylist = new PartitionDenylist(); private volatile long logBlockingReadRepairAttemptsUntilNanos = Long.MIN_VALUE; + private static volatile QueryInfoTracker queryInfoTracker = QueryInfoTracker.NOOP; private StorageProxy() { @@ -222,11 +403,13 @@ private StorageProxy() { MBeanWrapper.instance.registerMBean(instance, MBEAN_NAME); HintsService.instance.registerMBean(); + PaxosUtils.instance.registerMBean(); standardWritePerformer = (mutation, targets, responseHandler, localDataCenter, requestTime) -> { assert mutation instanceof Mutation; sendToHintedReplicas((Mutation) mutation, targets, responseHandler, localDataCenter, Stage.MUTATION, requestTime); + mutator.onAppliedMutation(mutation); }; /* @@ -246,11 +429,9 @@ private StorageProxy() { EndpointsForToken selected = targets.contacts().withoutSelf(); Replicas.temporaryAssertFull(selected); // TODO CASSANDRA-14548 - Stage.COUNTER_MUTATION.executor() - .execute(counterWriteTask(mutation, targets.withContacts(selected), responseHandler, localDataCenter, requestTime)); + Stage.COUNTER_MUTATION.execute(counterWriteTask(mutation, targets.withContacts(selected), responseHandler, localDataCenter, requestTime)); }; - ReadRepairMetrics.init(); if (!Paxos.isLinearizable()) @@ -264,6 +445,23 @@ private StorageProxy() } } + /** + * Registers the provided query info tracker + * + *

    Note that only 1 query tracker can be registered at a time, so the provided tracker will unconditionally + * replace the currently registered tracker. + * + * @param tracker the tracker to register. + */ + public void registerQueryTracker(QueryInfoTracker tracker) { + Objects.requireNonNull(tracker); + queryInfoTracker = tracker; + } + + public static QueryInfoTracker queryTracker() { + return queryInfoTracker; + } + /** * Apply @param updates if and only if the current values in the row for @param key * match the provided @param conditions. The algorithm is "raw" Paxos: that is, Paxos @@ -323,9 +521,12 @@ public static RowIterator cas(String keyspaceName, key.toString(), keyspaceName, cfName)); } - return Paxos.useV2() - ? Paxos.cas(key, request, consistencyForPaxos, consistencyForCommit, clientState) - : legacyCas(keyspaceName, cfName, key, request, consistencyForPaxos, consistencyForCommit, clientState, nowInSeconds, requestTime); + // Delegate the whole operation through the Mutator SPI so a custom Mutator observes the + // begin and the completion of every CAS operation, whichever paxos_variant is configured + // (the default implementation dispatches to Paxos.cas or legacyCas). + TableMetadata metadata = Schema.instance.validateTable(keyspaceName, cfName); + return mutator.mutateCas(metadata, key, request, consistencyForPaxos, consistencyForCommit, + clientState, nowInSeconds, requestTime); } public static RowIterator legacyCas(String keyspaceName, @@ -339,50 +540,77 @@ public static RowIterator legacyCas(String keyspaceName, Dispatcher.RequestTime requestTime) throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException { + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(keyspaceName); + TableMetadata metadata = Schema.instance.validateTable(keyspaceName, cfName); + QueryInfoTracker.LWTWriteTracker lwtTracker = queryTracker().onLWTWrite(clientState, + metadata, + key, + consistencyForPaxos, + consistencyForCommit); + // Request sensors are utilized to track usages from replicas serving a cas request + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(keyspaceName); + Context context = Context.from(metadata); + sensors.registerSensor(context, Type.WRITE_BYTES); // track user table + paxos table write bytes + sensors.registerSensor(context, Type.READ_BYTES); // track user table + paxos table read bytes + RequestTracker.instance.set(sensors); try { - TableMetadata metadata = Schema.instance.validateTable(keyspaceName, cfName); + consistencyForPaxos.validateForCas(keyspaceName, clientState); + consistencyForCommit.validateForCasCommit(Keyspace.open(keyspaceName).getReplicationStrategy(), keyspaceName, clientState); Function> updateProposer = ballot -> { - // read the current values and check they validate the conditions - Tracing.trace("Reading existing values for CAS precondition"); - SinglePartitionReadCommand readCommand = request.readCommand(nowInSeconds); - ConsistencyLevel readConsistency = consistencyForPaxos == ConsistencyLevel.LOCAL_SERIAL ? ConsistencyLevel.LOCAL_QUORUM : ConsistencyLevel.QUORUM; - - FilteredPartition current; - try (RowIterator rowIter = readOne(readCommand, readConsistency, requestTime)) + long startTimeNanos = Clock.Global.nanoTime(); + try { - current = FilteredPartition.create(rowIter); - } + // read the current values and check they validate the conditions + Tracing.trace("Reading existing values for CAS precondition"); + SinglePartitionReadCommand readCommand = (SinglePartitionReadCommand) request.readCommand(nowInSeconds); + ConsistencyLevel readConsistency = consistencyForPaxos == ConsistencyLevel.LOCAL_SERIAL ? ConsistencyLevel.LOCAL_QUORUM : ConsistencyLevel.QUORUM; - if (!request.appliesTo(current)) - { - Tracing.trace("CAS precondition does not match current values {}", current); - casWriteMetrics.conditionNotMet.inc(); - return Pair.create(PartitionUpdate.emptyUpdate(metadata, key), current.rowIterator()); - } + FilteredPartition current; - // Create the desired updates - PartitionUpdate updates = request.makeUpdates(current, clientState, ballot); + try (RowIterator rowIter = readOne(readCommand, readConsistency, clientState, requestTime, lwtTracker)) + { + current = FilteredPartition.create(rowIter); + } - // Update the metrics before triggers potentially add mutations. - ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(updates); + if (!request.appliesTo(current)) + { + Tracing.trace("CAS precondition does not match current values {}", current); + lwtTracker.onNotApplied(); + lwtTracker.onDone(); + metrics.casWriteMetrics.conditionNotMet.inc(); + return Pair.create(PartitionUpdate.emptyUpdate(metadata, key), current.rowIterator()); + } - long size = updates.dataSize(); - casWriteMetrics.mutationSize.update(size); - writeMetricsForLevel(consistencyForPaxos).mutationSize.update(size); + // Create the desired updates + PartitionUpdate updates = request.makeUpdates(current, clientState, ballot); - // Apply triggers to cas updates. A consideration here is that - // triggers emit Mutations, and so a given trigger implementation - // may generate mutations for partitions other than the one this - // paxos round is scoped for. In this case, TriggerExecutor will - // validate that the generated mutations are targetted at the same - // partition as the initial updates and reject (via an - // InvalidRequestException) any which aren't. - updates = TriggerExecutor.instance.execute(updates); + // Update the metrics before triggers potentially add mutations. + ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(updates); + lwtTracker.onApplied(updates); + lwtTracker.onDone(); - return Pair.create(updates, null); + long size = updates.dataSize(); + metrics.casWriteMetrics.mutationSize.update(size); + metrics.writeMetricsForLevel(consistencyForPaxos).mutationSize.update(size); + + // Apply triggers to cas updates. A consideration here is that + // triggers emit Mutations, and so a given trigger implementation + // may generate mutations for partitions other than the one this + // paxos round is scoped for. In this case, TriggerExecutor will + // validate that the generated mutations are targetted at the same + // partition as the initial updates and reject (via an + // InvalidRequestException) any which aren't. + updates = TriggerExecutor.instance.execute(updates); + + return Pair.create(updates, null); + } + finally + { + metrics.casWriteMetrics.createProposalLatency.addNano(Clock.Global.nanoTime() - startTimeNanos); + } }; return doPaxos(metadata, @@ -390,44 +618,51 @@ public static RowIterator legacyCas(String keyspaceName, consistencyForPaxos, consistencyForCommit, consistencyForCommit, + clientState, requestTime, - casWriteMetrics, - updateProposer); + metrics.casWriteMetrics, + updateProposer, + false); } catch (CasWriteUnknownResultException e) { - casWriteMetrics.unknownResult.mark(); + metrics.casWriteMetrics.unknownResult.mark(); + lwtTracker.onError(e); throw e; } catch (CasWriteTimeoutException wte) { - casWriteMetrics.timeouts.mark(); - writeMetricsForLevel(consistencyForPaxos).timeouts.mark(); + metrics.casWriteMetrics.timeouts.mark(); + metrics.writeMetricsForLevel(consistencyForPaxos).timeouts.mark(); + lwtTracker.onError(wte); throw new CasWriteTimeoutException(wte.writeType, wte.consistency, wte.received, wte.blockFor, wte.contentions); } catch (ReadTimeoutException e) { - casWriteMetrics.timeouts.mark(); - writeMetricsForLevel(consistencyForPaxos).timeouts.mark(); + metrics.casWriteMetrics.timeouts.mark(); + metrics.writeMetricsForLevel(consistencyForPaxos).timeouts.mark(); + lwtTracker.onError(e); throw e; } catch (ReadAbortException e) { - casWriteMetrics.markAbort(e); - writeMetricsForLevel(consistencyForPaxos).markAbort(e); + metrics.casWriteMetrics.markAbort(e); + metrics.writeMetricsForLevel(consistencyForPaxos).markAbort(e); throw e; } catch (WriteFailureException | ReadFailureException e) { - casWriteMetrics.failures.mark(); - writeMetricsForLevel(consistencyForPaxos).failures.mark(); + metrics.casWriteMetrics.failures.mark(); + metrics.writeMetricsForLevel(consistencyForPaxos).failures.mark(); + lwtTracker.onError(e); throw e; } catch (UnavailableException e) { - casWriteMetrics.unavailables.mark(); - writeMetricsForLevel(consistencyForPaxos).unavailables.mark(); + metrics.casWriteMetrics.unavailables.mark(); + metrics.writeMetricsForLevel(consistencyForPaxos).unavailables.mark(); + lwtTracker.onError(e); throw e; } finally @@ -435,8 +670,11 @@ public static RowIterator legacyCas(String keyspaceName, // We track latency based on request processing time, since the amount of time that request spends in the queue // is not a representative metric of replica performance. long latency = nanoTime() - requestTime.startedAtNanos(); - casWriteMetrics.addNano(latency); - writeMetricsForLevel(consistencyForPaxos).addNano(latency); + metrics.casWriteMetrics.executionTimeMetrics.addNano(latency); + metrics.casWriteMetrics.serviceTimeMetrics.addNano(latency); + metrics.writeMetricsForLevel(consistencyForPaxos).executionTimeMetrics.addNano(latency); + metrics.writeMetricsForLevel(consistencyForPaxos).serviceTimeMetrics.addNano(latency); + Keyspace.openAndGetStore(metadata).metric.coordinatorCasWriteLatency.update(latency, NANOSECONDS); } } @@ -470,6 +708,7 @@ private static void recordCasContention(TableMetadata table, * {@link ConsistencyLevel#LOCAL_SERIAL}). * @param consistencyForReplayCommits the consistency for the commit phase of "replayed" in-progress operations. * @param consistencyForCommit the consistency for the commit phase of _this_ operation update. + * @param clientState the client state. * @param requestTime the nano time for the start of the query this is part of. This is the base time for * timeouts. * @param casMetrics the metrics to update for this operation. @@ -477,6 +716,7 @@ private static void recordCasContention(TableMetadata table, * this operation and 2) the result that the whole method should return. This can return {@code null} in the * special where, after having "prepared" (and thus potentially replayed in-progress upgdates), we don't want * to propose anything (the whole method then return {@code null}). + * @param skipCommitConsistencyValidation whether to skip {@link ConsistencyLevel#validateForCasCommit} for commit consistency * @return the second element of the pair returned by {@code createUpdateProposal} (for the last call of that method * if that method is called multiple times due to retries). */ @@ -485,9 +725,11 @@ private static RowIterator doPaxos(TableMetadata metadata, ConsistencyLevel consistencyForPaxos, ConsistencyLevel consistencyForReplayCommits, ConsistencyLevel consistencyForCommit, + ClientState clientState, Dispatcher.RequestTime requestTime, CASClientRequestMetrics casMetrics, - Function> createUpdateProposal) + Function> createUpdateProposal, + boolean skipCommitConsistencyValidation) throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException { int contentions = 0; @@ -495,9 +737,10 @@ private static RowIterator doPaxos(TableMetadata metadata, AbstractReplicationStrategy latestRs = keyspace.getReplicationStrategy(); try { - consistencyForPaxos.validateForCas(); - consistencyForReplayCommits.validateForCasCommit(latestRs); - consistencyForCommit.validateForCasCommit(latestRs); + consistencyForPaxos.validateForCas(metadata.keyspace, clientState); + consistencyForReplayCommits.validateForCasCommit(latestRs, metadata.keyspace, clientState); + if (!skipCommitConsistencyValidation) + consistencyForCommit.validateForCasCommit(latestRs, metadata.keyspace, clientState); long timeoutNanos = DatabaseDescriptor.getCasContentionTimeout(NANOSECONDS); long deadline = requestTime.computeDeadline(timeoutNanos); @@ -524,14 +767,34 @@ private static RowIterator doPaxos(TableMetadata metadata, Commit proposal = Commit.newProposal(ballot, proposalPair.left); Tracing.trace("CAS precondition is met; proposing client-requested updates for {}", ballot); - if (proposePaxos(proposal, replicaPlan, true, requestTime)) + if (proposePaxos(proposal, replicaPlan, true, requestTime, casMetrics)) { // We skip committing accepted updates when they are empty. This is an optimization which works // because we also skip replaying those same empty update in beginAndRepairPaxos (see the longer // comment there). As empty update are somewhat common (serial reads and non-applying CAS propose // them), this is worth bothering. if (!proposal.update.isEmpty()) - commitPaxos(proposal, consistencyForCommit, true, requestTime); + { + MutatorProvider.notifyCasCommit(proposal, consistencyForCommit, Mutator.CasCommitOrigin.CLIENT_OPERATION); + try + { + commitPaxos(proposal, consistencyForCommit, true, requestTime, casMetrics); + } + catch (RuntimeException e) + { + // proposePaxos already returned true, so the value is DECIDED; any failure to confirm + // the commit here (timeout, replica failure, interruption surfaced as + // UncheckedInterruptedException) leaves it decided-but-not-confirmed: report UNCONFIRMED + // before the failure surfaces to the client. (At CL=ANY commitPaxos does not block, so it + // does not throw here; that case delivers no terminal, only the dispatched onCasCommit.) + MutatorProvider.notifyCasCommitCompleted(proposal, consistencyForCommit, Mutator.CasCommitOrigin.CLIENT_OPERATION, Mutator.CasCommitOutcome.UNCONFIRMED); + throw e; + } + // commitPaxos blocks until a consistencyForCommit quorum acknowledged the commit + // (unless CL=ANY); reaching here means the value is now readable at that CL. + if (consistencyForCommit != ConsistencyLevel.ANY) + MutatorProvider.notifyCasCommitCompleted(proposal, consistencyForCommit, Mutator.CasCommitOrigin.CLIENT_OPERATION, Mutator.CasCommitOutcome.APPLIED); + } RowIterator result = proposalPair.right; if (result != null) Tracing.trace("CAS did not apply"); @@ -542,8 +805,7 @@ private static RowIterator doPaxos(TableMetadata metadata, Tracing.trace("Paxos proposal not accepted (pre-empted by a higher ballot)"); contentions++; - - Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), TimeUnit.MILLISECONDS); + PaxosUtils.applyPaxosContentionBackoff(casMetrics); // continue to retry } } @@ -604,13 +866,13 @@ private static PaxosBallotAndContention beginAndRepairPaxos(Dispatcher.RequestTi { Tracing.trace("Preparing {}", ballot); Commit toPrepare = Commit.newPrepare(key, metadata, ballot); - summary = preparePaxos(toPrepare, paxosPlan, requestTime); + summary = preparePaxos(toPrepare, paxosPlan, requestTime, casMetrics); if (!summary.promised) { Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting"); contentions++; // sleep a random amount to give the other proposer a chance to finish - Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS); + PaxosUtils.applyPaxosContentionBackoff(casMetrics); continue; } @@ -640,16 +902,31 @@ private static PaxosBallotAndContention beginAndRepairPaxos(Dispatcher.RequestTi Tracing.trace("Finishing incomplete paxos round {}", inProgress); casMetrics.unfinishedCommit.inc(); Commit refreshedInProgress = Commit.newProposal(ballot, inProgress.update); - if (proposePaxos(refreshedInProgress, paxosPlan, false, requestTime)) + if (proposePaxos(refreshedInProgress, paxosPlan, false, requestTime, casMetrics)) { - commitPaxos(refreshedInProgress, consistencyForCommit, false, requestTime); + MutatorProvider.notifyCasCommit(refreshedInProgress, consistencyForCommit, Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS); + try + { + commitPaxos(refreshedInProgress, consistencyForCommit, false, requestTime, casMetrics); + } + catch (WriteTimeoutException e) + { + // recovered value decided but the commit was not acknowledged in time: report UNCONFIRMED + // before the failure propagates. (CL=ANY does not block/throw here; no terminal then.) + MutatorProvider.notifyCasCommitCompleted(refreshedInProgress, consistencyForCommit, Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS, Mutator.CasCommitOutcome.UNCONFIRMED); + throw e; + } + // commitPaxos blocks until a consistencyForCommit quorum acknowledged the commit + // (unless CL=ANY); reaching here means the recovered value is now readable at that CL. + if (consistencyForCommit != ConsistencyLevel.ANY) + MutatorProvider.notifyCasCommitCompleted(refreshedInProgress, consistencyForCommit, Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS, Mutator.CasCommitOutcome.APPLIED); } else { Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting"); // sleep a random amount to give the other proposer a chance to finish contentions++; - Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS); + PaxosUtils.applyPaxosContentionBackoff(casMetrics); } continue; } @@ -659,9 +936,12 @@ private static PaxosBallotAndContention beginAndRepairPaxos(Dispatcher.RequestTi // Since we waited for quorum nodes, if some of them haven't seen the last commit (which may just be a timing issue, but may also // mean we lost messages), we pro-actively "repair" those nodes, and retry. Iterable missingMRC = summary.replicasMissingMostRecentCommit(metadata); - if (Iterables.size(missingMRC) > 0) + int missingMRCSize = Iterables.size(missingMRC); + if (missingMRCSize > 0) { Tracing.trace("Repairing replicas that missed the most recent commit"); + casMetrics.missingMostRecentCommit.inc(missingMRCSize); + MutatorProvider.notifyCasCommit(mostRecent, consistencyForCommit, Mutator.CasCommitOrigin.REFRESH_COMMITTED); sendCommit(mostRecent, missingMRC); // TODO: provided commits don't invalid the prepare we just did above (which they don't), we could just wait // for all the missingMRC to acknowledge this commit and then move on with proposing our value. But that means @@ -692,43 +972,53 @@ private static void sendCommit(Commit commit, Iterable repli MessagingService.instance().send(message, target); } - private static PrepareCallback preparePaxos(Commit toPrepare, ReplicaPlan.ForPaxosWrite replicaPlan, Dispatcher.RequestTime requestTime) + private static PrepareCallback preparePaxos(Commit toPrepare, ReplicaPlan.ForPaxosWrite replicaPlan, Dispatcher.RequestTime requestTime, + CASClientRequestMetrics casMetrics) throws WriteTimeoutException { - PrepareCallback callback = new PrepareCallback(toPrepare.update.partitionKey(), toPrepare.update.metadata(), replicaPlan.requiredParticipants(), replicaPlan.consistencyLevel(), requestTime); - Message message = Message.out(PAXOS_PREPARE_REQ, toPrepare); + long startTimeNanos = Clock.Global.nanoTime(); + try + { + PrepareCallback callback = new PrepareCallback(toPrepare.update.partitionKey(), toPrepare.update.metadata(), replicaPlan.requiredParticipants(), replicaPlan.consistencyLevel(), requestTime); + Message message = Message.out(PAXOS_PREPARE_REQ, toPrepare); - boolean hasLocalRequest = false; + boolean hasLocalRequest = false; - for (Replica replica: replicaPlan.contacts()) - { - if (replica.isSelf()) + for (Replica replica : replicaPlan.contacts()) { - hasLocalRequest = true; - PAXOS_PREPARE_REQ.stage.execute(() -> { - try - { - callback.onResponse(message.responseWith(doPrepare(toPrepare))); - } - catch (Exception ex) - { - logger.error("Failed paxos prepare locally", ex); - } - }); - } - else - { - MessagingService.instance().sendWithCallback(message, replica.endpoint(), callback); + if (replica.isSelf()) + { + hasLocalRequest = true; + PAXOS_PREPARE_REQ.stage.execute(() -> { + try + { + callback.onResponse(message.responseWith(doPrepare(toPrepare))); + } + catch (Exception ex) + { + logger.error("Failed paxos prepare locally", ex); + } + }); + } + else + { + MessagingService.instance().sendWithCallback(message, replica.endpoint(), callback); + } } - } - if (hasLocalRequest) - writeMetrics.localRequests.mark(); - else - writeMetrics.remoteRequests.mark(); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(toPrepare.update.metadata().keyspace); + if (hasLocalRequest) + metrics.writeMetrics.localRequests.mark(); + else + metrics.writeMetrics.remoteRequests.mark(); - callback.await(); - return callback; + callback.await(); + return callback; + } + finally + { + casMetrics.prepareLatency.addNano(Clock.Global.nanoTime() - startTimeNanos); + } } /** @@ -736,44 +1026,74 @@ private static PrepareCallback preparePaxos(Commit toPrepare, ReplicaPlan.ForPax * When {@param backoffIfPartial} is true, the proposer backs off when seeing the proposal being accepted by some but not a quorum. * The result of the cooresponding CAS in uncertain as the accepted proposal may or may not be spread to other nodes in later rounds. */ - private static boolean proposePaxos(Commit proposal, ReplicaPlan.ForPaxosWrite replicaPlan, boolean backoffIfPartial, Dispatcher.RequestTime requestTime) + private static boolean proposePaxos(Commit proposal, ReplicaPlan.ForPaxosWrite replicaPlan, boolean backoffIfPartial, + Dispatcher.RequestTime requestTime, CASClientRequestMetrics casMetrics) throws WriteTimeoutException, CasWriteUnknownResultException { - ProposeCallback callback = new ProposeCallback(replicaPlan.contacts().size(), replicaPlan.requiredParticipants(), !backoffIfPartial, replicaPlan.consistencyLevel(), requestTime); - Message message = Message.out(PAXOS_PROPOSE_REQ, proposal); - for (Replica replica : replicaPlan.contacts()) + long startTimeNanos = Clock.Global.nanoTime(); + try { - if (replica.isSelf()) - { - PAXOS_PROPOSE_REQ.stage.execute(() -> { - try - { - Message response = message.responseWith(doPropose(proposal)); - callback.onResponse(response); - } - catch (Exception ex) - { - logger.error("Failed paxos propose locally", ex); - } - }); - } - else + ProposeCallback callback = new ProposeCallback(proposal.update.metadata(), replicaPlan.contacts().size(), replicaPlan.requiredParticipants(), !backoffIfPartial, replicaPlan.consistencyLevel(), requestTime); + Message message = Message.out(PAXOS_PROPOSE_REQ, proposal); + for (Replica replica : replicaPlan.contacts()) { - MessagingService.instance().sendWithCallback(message, replica.endpoint(), callback); + if (replica.isSelf()) + { + PAXOS_PROPOSE_REQ.stage.execute(() -> { + try + { + Message response = message.responseWith(doPropose(proposal)); + callback.onResponse(response); + } + catch (Exception ex) + { + logger.error("Failed paxos propose locally", ex); + } + }); + } + else + { + MessagingService.instance().sendWithCallback(message, replica.endpoint(), callback); + } } - } - callback.await(); + callback.await(); - if (callback.isSuccessful()) - return true; + if (callback.isSuccessful()) + return true; - if (backoffIfPartial && !callback.isFullyRefused()) - throw new CasWriteUnknownResultException(replicaPlan.consistencyLevel(), callback.getAcceptCount(), replicaPlan.requiredParticipants()); + if (backoffIfPartial && !callback.isFullyRefused()) + throw new CasWriteUnknownResultException(replicaPlan.consistencyLevel(), callback.getAcceptCount(), replicaPlan.requiredParticipants()); + } + finally + { + casMetrics.proposeLatency.addNano(Clock.Global.nanoTime() - startTimeNanos); + } return false; } - private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) throws WriteTimeoutException + @Nullable + private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime, + CASClientRequestMetrics casMetrics) throws WriteTimeoutException + { + long startTimeNanos = Clock.Global.nanoTime(); + boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY; + AbstractWriteResponseHandler responseHandler = mutator.mutatePaxos(proposal, consistencyLevel, allowHints, requestTime); + if (shouldBlock && responseHandler != null) + { + try + { + responseHandler.get(); + } + finally + { + casMetrics.commitLatency.addNano(Clock.Global.nanoTime() - startTimeNanos); + } + } + } + + @Nullable + private static AbstractWriteResponseHandler defaultCommitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) throws WriteTimeoutException { boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY; Keyspace keyspace = Keyspace.open(proposal.update.metadata().keyspace); @@ -822,8 +1142,7 @@ private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLev } } - if (shouldBlock) - responseHandler.get(); + return responseHandler; } /** @@ -839,7 +1158,7 @@ public void runMayThrow() { try { - PaxosState.commitDirect(message.payload); + PaxosState.commitDirect(message.payload, p -> mutator.onAppliedProposal(p)); if (responseHandler != null) responseHandler.onResponse(null); } @@ -875,12 +1194,22 @@ protected Verb verb() * @param consistencyLevel the consistency level for the operation * @param requestTime object holding times when request got enqueued and started execution */ - public static void mutate(List mutations, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + public static void mutate(List mutations, + ConsistencyLevel consistencyLevel, + Dispatcher.RequestTime requestTime, + ClientRequestsMetrics metrics, + ClientState state) throws UnavailableException, OverloadedException, WriteTimeoutException, WriteFailureException { Tracing.trace("Determining replicas for mutation"); final String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); + QueryInfoTracker.WriteTracker writeTracker = queryTracker().onWrite(state, false, mutations, consistencyLevel); + + // Request sensors are utilized to track usages from replicas serving a write request + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(mutations.stream().map(IMutation::getKeyspaceName).toArray(String[]::new)); + RequestTracker.instance.set(sensors); + List> responseHandlers = new ArrayList<>(mutations.size()); WriteType plainWriteType = mutations.size() <= 1 ? WriteType.SIMPLE : WriteType.UNLOGGED_BATCH; @@ -888,22 +1217,31 @@ public static void mutate(List mutations, ConsistencyLevel { for (IMutation mutation : mutations) { + // register the sensors for the mutation before the actual write is performed + for (PartitionUpdate pu: mutation.getPartitionUpdates()) + { + if (pu.metadata().isIndex()) continue; + sensors.registerSensor(Context.from(pu.metadata()), Type.WRITE_BYTES); + } + if (mutation instanceof CounterMutation) responseHandlers.add(mutateCounter((CounterMutation)mutation, localDataCenter, requestTime)); else - responseHandlers.add(performWrite(mutation, consistencyLevel, localDataCenter, standardWritePerformer, null, plainWriteType, requestTime)); + responseHandlers.add(mutator.mutateStandard((Mutation)mutation, consistencyLevel, localDataCenter, standardWritePerformer, null, plainWriteType, requestTime)); } // upgrade to full quorum any failed cheap quorums for (int i = 0 ; i < mutations.size() ; ++i) { - if (!(mutations.get(i) instanceof CounterMutation)) // at the moment, only non-counter writes support cheap quorums + if (!(mutations.get(i) instanceof CounterMutation) && mutator instanceof DefaultMutator) // at the moment, only non-counter writes support cheap quorums responseHandlers.get(i).maybeTryAdditionalReplicas(mutations.get(i), standardWritePerformer, localDataCenter); } // wait for writes. throws TimeoutException if necessary for (AbstractWriteResponseHandler responseHandler : responseHandlers) responseHandler.get(); + + writeTracker.onDone(); } catch (WriteTimeoutException|WriteFailureException ex) { @@ -915,34 +1253,37 @@ public static void mutate(List mutations, ConsistencyLevel { if (ex instanceof WriteFailureException) { - writeMetrics.failures.mark(); - writeMetricsForLevel(consistencyLevel).failures.mark(); + metrics.writeMetrics.failures.mark(); + metrics.writeMetricsForLevel(consistencyLevel).failures.mark(); WriteFailureException fe = (WriteFailureException)ex; Tracing.trace("Write failure; received {} of {} required replies, failed {} requests", fe.received, fe.blockFor, fe.failureReasonByEndpoint.size()); } else { - writeMetrics.timeouts.mark(); - writeMetricsForLevel(consistencyLevel).timeouts.mark(); + metrics.writeMetrics.timeouts.mark(); + metrics.writeMetricsForLevel(consistencyLevel).timeouts.mark(); WriteTimeoutException te = (WriteTimeoutException)ex; Tracing.trace("Write timeout; received {} of {} required replies", te.received, te.blockFor); } + writeTracker.onError(ex); throw ex; } } catch (UnavailableException e) { - writeMetrics.unavailables.mark(); - writeMetricsForLevel(consistencyLevel).unavailables.mark(); + metrics.writeMetrics.unavailables.mark(); + metrics.writeMetricsForLevel(consistencyLevel).unavailables.mark(); Tracing.trace("Unavailable"); + writeTracker.onError(e); throw e; } catch (OverloadedException e) { - writeMetrics.unavailables.mark(); - writeMetricsForLevel(consistencyLevel).unavailables.mark(); + metrics.writeMetrics.unavailables.mark(); + metrics.writeMetricsForLevel(consistencyLevel).unavailables.mark(); Tracing.trace("Overloaded"); + writeTracker.onError(e); throw e; } finally @@ -950,8 +1291,10 @@ public static void mutate(List mutations, ConsistencyLevel // We track latency based on request processing time, since the amount of time that request spends in the queue // is not a representative metric of replica performance. long latency = nanoTime() - requestTime.startedAtNanos(); - writeMetrics.addNano(latency); - writeMetricsForLevel(consistencyLevel).addNano(latency); + metrics.writeMetrics.executionTimeMetrics.addNano(latency); + metrics.writeMetrics.serviceTimeMetrics.addNano(latency); + metrics.writeMetricsForLevel(consistencyLevel).executionTimeMetrics.addNano(latency); + metrics.writeMetricsForLevel(consistencyLevel).serviceTimeMetrics.addNano(latency); updateCoordinatorWriteLatencyTableMetric(mutations, latency); } } @@ -1003,18 +1346,19 @@ public boolean appliesLocally(Mutation mutation) * across all replicas. * * @param mutations the mutations to be applied across the replicas - * @param writeCommitLog if commitlog should be written + * @param writeOptions describes desired write properties * @param baseComplete time from epoch in ms that the local base mutation was(or will be) completed * @param requestTime object holding times when request got enqueued and started execution */ - public static void mutateMV(ByteBuffer dataKey, Collection mutations, boolean writeCommitLog, AtomicLong baseComplete, Dispatcher.RequestTime requestTime) - throws UnavailableException, OverloadedException, WriteTimeoutException + public static void mutateMV(ByteBuffer dataKey, Collection mutations, WriteOptions writeOptions, AtomicLong baseComplete, Dispatcher.RequestTime requestTime) + throws UnavailableException, OverloadedException, WriteTimeoutException { Tracing.trace("Determining replicas for mutation"); final String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); long startTime = nanoTime(); - + String ks = mutations.iterator().next().getKeyspaceName(); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(ks); try { @@ -1023,15 +1367,14 @@ public static void mutateMV(ByteBuffer dataKey, Collection mutations, if (StorageService.instance.isStarting() || StorageService.instance.isJoining() || StorageService.instance.isMoving()) { - BatchlogManager.store(Batch.createLocal(batchUUID, FBUtilities.timestampMicros(), - mutations), writeCommitLog); + BatchlogManager.store(Batch.createLocal(batchUUID, FBUtilities.timestampMicros(), mutations), writeOptions); } else { List wrappers = new ArrayList<>(mutations.size()); //non-local mutations rely on the base mutation commit-log entry for eventual consistency Set nonLocalMutations = new HashSet<>(mutations); - Token baseToken = StorageService.instance.getTokenMetadata().partitioner.getToken(dataKey); + Token baseToken = StorageService.instance.getTokenMetadataForKeyspace(ks).partitioner.getToken(dataKey); ConsistencyLevel consistencyLevel = ConsistencyLevel.ONE; @@ -1047,7 +1390,7 @@ public static void mutateMV(ByteBuffer dataKey, Collection mutations, Token tk = mutation.key().getToken(); AbstractReplicationStrategy replicationStrategy = Keyspace.open(keyspaceName).getReplicationStrategy(); Optional pairedEndpoint = ViewUtils.getViewNaturalEndpoint(replicationStrategy, baseToken, tk); - EndpointsForToken pendingReplicas = StorageService.instance.getTokenMetadata().pendingEndpointsForToken(tk, keyspaceName); + EndpointsForToken pendingReplicas = StorageService.instance.getTokenMetadataForKeyspace(keyspaceName).pendingEndpointsForToken(tk, keyspaceName); // if there are no paired endpoints there are probably range movements going on, so we write to the local batchlog to replay later if (!pairedEndpoint.isPresent()) @@ -1069,7 +1412,7 @@ public static void mutateMV(ByteBuffer dataKey, Collection mutations, { try { - mutation.apply(writeCommitLog); + mutation.apply(writeOptions); nonLocalMutations.remove(mutation); // won't trigger cleanup cleanup.decrement(); @@ -1093,13 +1436,14 @@ public static void mutateMV(ByteBuffer dataKey, Collection mutations, baseComplete, WriteType.BATCH, cleanup, - requestTime)); + requestTime, + metrics)); } } // Apply to local batchlog memtable in this thread if (!nonLocalMutations.isEmpty()) - BatchlogManager.store(Batch.createLocal(batchUUID, FBUtilities.timestampMicros(), nonLocalMutations), writeCommitLog); + BatchlogManager.store(Batch.createLocal(batchUUID, FBUtilities.timestampMicros(), nonLocalMutations), writeOptions); // Perform remote writes if (!wrappers.isEmpty()) @@ -1108,7 +1452,9 @@ public static void mutateMV(ByteBuffer dataKey, Collection mutations, } finally { - viewWriteMetrics.addNano(nanoTime() - startTime); + final long endTime = nanoTime(); + metrics.viewWriteMetrics.executionTimeMetrics.addNano(endTime - startTime); + metrics.viewWriteMetrics.serviceTimeMetrics.addNano(endTime - requestTime.startedAtNanos()); } } @@ -1116,7 +1462,8 @@ public static void mutateMV(ByteBuffer dataKey, Collection mutations, public static void mutateWithTriggers(List mutations, ConsistencyLevel consistencyLevel, boolean mutateAtomically, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + ClientState state) throws WriteTimeoutException, WriteFailureException, UnavailableException, OverloadedException, InvalidRequestException { if (DatabaseDescriptor.getPartitionDenylistEnabled() && DatabaseDescriptor.getDenylistWritesEnabled()) @@ -1139,23 +1486,25 @@ public static void mutateWithTriggers(List mutations, } Collection augmented = TriggerExecutor.instance.execute(mutations); + String keyspaceName = mutations.iterator().next().getKeyspaceName(); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(keyspaceName); boolean updatesView = Keyspace.open(mutations.iterator().next().getKeyspaceName()) .viewManager .updatesAffectView(mutations, true); long size = IMutation.dataSize(mutations); - writeMetrics.mutationSize.update(size); - writeMetricsForLevel(consistencyLevel).mutationSize.update(size); + metrics.writeMetrics.mutationSize.update(size); + metrics.writeMetricsForLevel(consistencyLevel).mutationSize.update(size); if (augmented != null) - mutateAtomically(augmented, consistencyLevel, updatesView, requestTime); + mutateAtomically(augmented, consistencyLevel, updatesView, requestTime, metrics, state); else { if (mutateAtomically || updatesView) - mutateAtomically((Collection) mutations, consistencyLevel, updatesView, requestTime); + mutateAtomically((Collection) mutations, consistencyLevel, updatesView, requestTime, metrics, state); else - mutate(mutations, consistencyLevel, requestTime); + mutate(mutations, consistencyLevel, requestTime, metrics, state); } } @@ -1166,96 +1515,22 @@ public static void mutateWithTriggers(List mutations, * After: remove the batchlog entry (after writing hints for the batch rows, if necessary). * * @param mutations the Mutations to be applied across the replicas - * @param consistency_level the consistency level for the operation + * @param consistencyLevel the consistency level for the operation * @param requireQuorumForRemove at least a quorum of nodes will see update before deleting batchlog * @param requestTime object holding times when request got enqueued and started execution */ public static void mutateAtomically(Collection mutations, - ConsistencyLevel consistency_level, + ConsistencyLevel consistencyLevel, boolean requireQuorumForRemove, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + ClientRequestsMetrics metrics, + ClientState clientState) throws UnavailableException, OverloadedException, WriteTimeoutException { - Tracing.trace("Determining replicas for atomic batch"); - long startTime = nanoTime(); - - List wrappers = new ArrayList<>(mutations.size()); - - if (mutations.stream().anyMatch(mutation -> Keyspace.open(mutation.getKeyspaceName()).getReplicationStrategy().hasTransientReplicas())) - throw new AssertionError("Logged batches are unsupported with transient replication"); - - try - { - - // If we are requiring quorum nodes for removal, we upgrade consistency level to QUORUM unless we already - // require ALL, or EACH_QUORUM. This is so that *at least* QUORUM nodes see the update. - ConsistencyLevel batchConsistencyLevel = requireQuorumForRemove - ? ConsistencyLevel.QUORUM - : consistency_level; - - switch (consistency_level) - { - case ALL: - case EACH_QUORUM: - batchConsistencyLevel = consistency_level; - } - - ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forBatchlogWrite(batchConsistencyLevel == ConsistencyLevel.ANY); - - final TimeUUID batchUUID = nextTimeUUID(); - BatchlogCleanup cleanup = new BatchlogCleanup(mutations.size(), - () -> asyncRemoveFromBatchlog(replicaPlan, batchUUID, requestTime)); - - // add a handler for each mutation - includes checking availability, but doesn't initiate any writes, yet - for (Mutation mutation : mutations) - { - WriteResponseHandlerWrapper wrapper = wrapBatchResponseHandler(mutation, - consistency_level, - batchConsistencyLevel, - WriteType.BATCH, - cleanup, - requestTime); - // exit early if we can't fulfill the CL at this time. - wrappers.add(wrapper); - } - - // write to the batchlog - syncWriteToBatchlog(mutations, replicaPlan, batchUUID, requestTime); - - // now actually perform the writes and wait for them to complete - syncWriteBatchedMutations(wrappers, Stage.MUTATION, requestTime); - } - catch (UnavailableException e) - { - writeMetrics.unavailables.mark(); - writeMetricsForLevel(consistency_level).unavailables.mark(); - Tracing.trace("Unavailable"); - throw e; - } - catch (WriteTimeoutException e) - { - writeMetrics.timeouts.mark(); - writeMetricsForLevel(consistency_level).timeouts.mark(); - Tracing.trace("Write timeout; received {} of {} required replies", e.received, e.blockFor); - throw e; - } - catch (WriteFailureException e) - { - writeMetrics.failures.mark(); - writeMetricsForLevel(consistency_level).failures.mark(); - Tracing.trace("Write failure; received {} of {} required replies", e.received, e.blockFor); - throw e; - } - finally - { - long latency = nanoTime() - startTime; - writeMetrics.addNano(latency); - writeMetricsForLevel(consistency_level).addNano(latency); - updateCoordinatorWriteLatencyTableMetric(mutations, latency); - } + mutator.mutateAtomically(mutations, consistencyLevel, requireQuorumForRemove, requestTime, metrics, clientState); } - private static void updateCoordinatorWriteLatencyTableMetric(Collection mutations, long latency) + public static void updateCoordinatorWriteLatencyTableMetric(Collection mutations, long latency) { if (null == mutations) { @@ -1305,7 +1580,7 @@ private static void syncWriteToBatchlog(Collection mutations, ReplicaP handler.get(); } - private static void asyncRemoveFromBatchlog(ReplicaPlan.ForWrite replicaPlan, TimeUUID uuid, Dispatcher.RequestTime requestTime) + protected static void asyncRemoveFromBatchlog(ReplicaPlan.ForWrite replicaPlan, TimeUUID uuid, Dispatcher.RequestTime requestTime) { Message message = Message.out(Verb.BATCH_REMOVE_REQ, uuid); for (Replica target : replicaPlan.contacts()) @@ -1338,20 +1613,30 @@ private static void asyncWriteBatchedMutations(List } } - private static void syncWriteBatchedMutations(List wrappers, Stage stage, Dispatcher.RequestTime requestTime) - throws WriteTimeoutException, OverloadedException + public static AbstractWriteResponseHandler getWriteResponseHandler(IMutation mutation, + ConsistencyLevel consistencyLevel, + @Nullable Runnable callback, + WriteType writeType, + Dispatcher.RequestTime requestTime) { - String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); + Keyspace keyspace = mutation.getKeyspace(); + Token tk = mutation.key().getToken(); - for (WriteResponseHandlerWrapper wrapper : wrappers) + ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeNormal); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(keyspace.getName()); + if (replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null) + metrics.writeMetrics.localRequests.mark(); + else + metrics.writeMetrics.remoteRequests.mark(); + + AbstractReplicationStrategy rs = replicaPlan.replicationStrategy(); + + AbstractWriteResponseHandler responseHandler = rs.getWriteResponseHandler(replicaPlan, callback, writeType, mutation.hintOnFailure(), requestTime); + if (callback instanceof CounterMutationCallback) { - EndpointsForToken sendTo = wrapper.handler.replicaPlan.liveAndDown(); - Replicas.temporaryAssertFull(sendTo); // TODO: CASSANDRA-14549 - sendToHintedReplicas(wrapper.mutation, wrapper.handler.replicaPlan.withContacts(sendTo), wrapper.handler, localDataCenter, stage, requestTime); + ((CounterMutationCallback) callback).setReplicaCount(replicaPlan.contacts().size()); } - - for (WriteResponseHandlerWrapper wrapper : wrappers) - wrapper.handler.get(); + return responseHandler; } /** @@ -1376,45 +1661,22 @@ public static AbstractWriteResponseHandler performWrite(IMutation mut WriteType writeType, Dispatcher.RequestTime requestTime) { - String keyspaceName = mutation.getKeyspaceName(); - Keyspace keyspace = Keyspace.open(keyspaceName); - Token tk = mutation.key().getToken(); - - ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeNormal); - - if (replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null) - writeMetrics.localRequests.mark(); - else - writeMetrics.remoteRequests.mark(); - - AbstractReplicationStrategy rs = replicaPlan.replicationStrategy(); - AbstractWriteResponseHandler responseHandler = rs.getWriteResponseHandler(replicaPlan, callback, writeType, mutation.hintOnFailure(), requestTime); - - performer.apply(mutation, replicaPlan, responseHandler, localDataCenter, requestTime); + AbstractWriteResponseHandler responseHandler = getWriteResponseHandler(mutation, consistencyLevel, callback, writeType, requestTime); + performer.apply(mutation, responseHandler.replicaPlan, responseHandler, localDataCenter, requestTime); return responseHandler; } // same as performWrites except does not initiate writes (but does perform availability checks). - private static WriteResponseHandlerWrapper wrapBatchResponseHandler(Mutation mutation, - ConsistencyLevel consistencyLevel, - ConsistencyLevel batchConsistencyLevel, - WriteType writeType, - BatchlogResponseHandler.BatchlogCleanup cleanup, - Dispatcher.RequestTime requestTime) + public static WriteResponseHandlerWrapper wrapBatchResponseHandler(Mutation mutation, + ConsistencyLevel consistencyLevel, + ConsistencyLevel batchConsistencyLevel, + WriteType writeType, + BatchlogResponseHandler.BatchlogCleanup cleanup, + Dispatcher.RequestTime requestTime) { - Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName()); - Token tk = mutation.key().getToken(); - - ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeNormal); - - if (replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null) - writeMetrics.localRequests.mark(); - else - writeMetrics.remoteRequests.mark(); - - AbstractReplicationStrategy rs = replicaPlan.replicationStrategy(); - AbstractWriteResponseHandler writeHandler = rs.getWriteResponseHandler(replicaPlan, null, writeType, mutation, requestTime); - BatchlogResponseHandler batchHandler = new BatchlogResponseHandler<>(writeHandler, batchConsistencyLevel.blockFor(rs), cleanup, requestTime); + AbstractWriteResponseHandler writeHandler = getWriteResponseHandler(mutation, consistencyLevel, null, writeType, requestTime); + int batchlogBlockFor = batchConsistencyLevel.blockFor(writeHandler.replicaPlan().replicationStrategy()); + BatchlogResponseHandler batchHandler = new BatchlogResponseHandler<>(writeHandler, batchlogBlockFor, cleanup, requestTime); return new WriteResponseHandlerWrapper(batchHandler, mutation); } @@ -1429,26 +1691,27 @@ private static WriteResponseHandlerWrapper wrapViewBatchResponseHandler(Mutation AtomicLong baseComplete, WriteType writeType, BatchlogResponseHandler.BatchlogCleanup cleanup, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + ClientRequestsMetrics metrics) { Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName()); ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, liveAndDown, ReplicaPlans.writeAll); AbstractReplicationStrategy replicationStrategy = replicaPlan.replicationStrategy(); AbstractWriteResponseHandler writeHandler = replicationStrategy.getWriteResponseHandler(replicaPlan, () -> { long delay = Math.max(0, currentTimeMillis() - baseComplete.get()); - viewWriteMetrics.viewWriteLatency.update(delay, MILLISECONDS); + metrics.viewWriteMetrics.viewWriteLatency.update(delay, MILLISECONDS); }, writeType, mutation, requestTime); - BatchlogResponseHandler batchHandler = new ViewWriteMetricsWrapped(writeHandler, batchConsistencyLevel.blockFor(replicationStrategy), cleanup, requestTime); + BatchlogResponseHandler batchHandler = new ViewWriteMetricsWrapped(writeHandler, batchConsistencyLevel.blockFor(replicationStrategy), cleanup, requestTime, metrics); return new WriteResponseHandlerWrapper(batchHandler, mutation); } // used by atomic_batch_mutate to decouple availability check from the write itself, caches consistency level and endpoints. - private static class WriteResponseHandlerWrapper + public static class WriteResponseHandlerWrapper { - final BatchlogResponseHandler handler; - final Mutation mutation; + public final BatchlogResponseHandler handler; + public final Mutation mutation; - WriteResponseHandlerWrapper(BatchlogResponseHandler handler, Mutation mutation) + public WriteResponseHandlerWrapper(BatchlogResponseHandler handler, Mutation mutation) { this.handler = handler; this.mutation = mutation; @@ -1722,8 +1985,12 @@ protected Verb verb() */ public static AbstractWriteResponseHandler mutateCounter(CounterMutation cm, String localDataCenter, Dispatcher.RequestTime requestTime) throws UnavailableException, OverloadedException { - Replica replica = findSuitableReplica(cm.getKeyspaceName(), cm.key(), localDataCenter, cm.consistency()); + return mutator.mutateCounter(cm, localDataCenter, requestTime); + } + private static AbstractWriteResponseHandler defaultMutateCounter(CounterMutation cm, String localDataCenter, Dispatcher.RequestTime requestTime) throws UnavailableException, OverloadedException + { + Replica replica = findSuitableReplica(cm.getKeyspaceName(), cm.key(), localDataCenter, cm.consistency()); if (replica.isSelf()) { return applyCounterMutationOnCoordinator(cm, localDataCenter, requestTime); @@ -1741,7 +2008,8 @@ public static AbstractWriteResponseHandler mutateCounter(CounterMutat // This host isn't a replica, so mark the request as being remote. If this host is a // replica, applyCounterMutationOnCoordinator() in the branch above will call performWrite(), and // there we'll mark a local request against the metrics. - writeMetrics.remoteRequests.mark(); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(keyspaceName); + metrics.writeMetrics.remoteRequests.mark(); // Forward the actual update to the chosen leader replica AbstractWriteResponseHandler responseHandler = new WriteResponseHandler<>(ReplicaPlans.forForwardingCounterWrite(keyspace, tk, replica), @@ -1758,11 +2026,6 @@ public static AbstractWriteResponseHandler mutateCounter(CounterMutat * Find a suitable replica as leader for counter update. * For now, we pick a random replica in the local DC (or ask the snitch if * there is no replica alive in the local DC). - * TODO: if we track the latency of the counter writes (which makes sense - * contrarily to standard writes since there is a read involved), we could - * trust the dynamic snitch entirely, which may be a better solution. It - * is unclear we want to mix those latencies with read latencies, so this - * may be a bit involved. */ private static Replica findSuitableReplica(String keyspaceName, DecoratedKey key, String localDataCenter, ConsistencyLevel cl) throws UnavailableException { @@ -1771,11 +2034,18 @@ private static Replica findSuitableReplica(String keyspaceName, DecoratedKey key AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); EndpointsForToken replicas = replicationStrategy.getNaturalReplicasForToken(key); - // CASSANDRA-13043: filter out those endpoints not accepting clients yet, maybe because still bootstrapping - replicas = replicas.filter(replica -> StorageService.instance.isRpcReady(replica.endpoint())); + // CASSANDRA-13043: filter out those endpoints not live yet, maybe because still bootstrapping + // We have a keyspace, so filter by affinity too + replicas = replicas.filter(IFailureDetector.isReplicaAlive) + .filter(snitch.filterByAffinityForReads(keyspace.getName())); - // CASSANDRA-17411: filter out endpoints that are not alive - replicas = replicas.filter(replica -> FailureDetector.instance.isAlive(replica.endpoint())); + // Counter leader involves local read, coordinator will prioritize faster replica if configured + boolean endpointsSorted = false; + if (DatabaseDescriptor.isDynamicEndpointSnitch() && useDynamicSnitchForCounterLeader) + { + replicas = snitch.sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); + endpointsSorted = true; + } // TODO have a way to compute the consistency level if (replicas.isEmpty()) @@ -1794,11 +2064,16 @@ private static Replica findSuitableReplica(String keyspaceName, DecoratedKey key throw UnavailableException.create(cl, cl.blockFor(replicationStrategy), 0); // No endpoint in local DC, pick the closest endpoint according to the snitch - replicas = snitch.sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); + if (!endpointsSorted) + replicas = snitch.sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); return replicas.get(0); } - return localReplicas.get(ThreadLocalRandom.current().nextInt(localReplicas.size())); + // if it's ordered, exclude the slowest one and pick randomly from the rest to avoid overloading single replica + int replicasToPick = localReplicas.size(); + if (endpointsSorted && localReplicas.size() > 1) + replicasToPick--; + return localReplicas.get(ThreadLocalRandom.current().nextInt(replicasToPick)); } // Must be called on a replica of the mutation. This replica becomes the @@ -1806,7 +2081,7 @@ private static Replica findSuitableReplica(String keyspaceName, DecoratedKey key public static AbstractWriteResponseHandler applyCounterMutationOnLeader(CounterMutation cm, String localDataCenter, Runnable callback, Dispatcher.RequestTime requestTime) throws UnavailableException, OverloadedException { - return performWrite(cm, cm.consistency(), localDataCenter, counterWritePerformer, callback, WriteType.COUNTER, requestTime); + return mutator.mutateCounterOnLeader(cm, localDataCenter, counterWritePerformer, callback, requestTime); } // Same as applyCounterMutationOnLeader but must with the difference that it use the MUTATION stage to execute the write (while @@ -1814,7 +2089,7 @@ public static AbstractWriteResponseHandler applyCounterMutationOnLead public static AbstractWriteResponseHandler applyCounterMutationOnCoordinator(CounterMutation cm, String localDataCenter, Dispatcher.RequestTime requestTime) throws UnavailableException, OverloadedException { - return performWrite(cm, cm.consistency(), localDataCenter, counterWriteOnCoordinatorPerformer, null, WriteType.COUNTER, requestTime); + return mutator.mutateCounterOnLeader(cm, localDataCenter, counterWriteOnCoordinatorPerformer, null, requestTime); } private static Runnable counterWriteTask(final IMutation mutation, @@ -1832,6 +2107,7 @@ public void runMayThrow() throws OverloadedException, WriteTimeoutException Mutation result = ((CounterMutation) mutation).applyCounterMutation(); responseHandler.onResponse(null); + mutator.onAppliedCounter(result, responseHandler); sendToHintedReplicas(result, replicaPlan, responseHandler, localDataCenter, Stage.COUNTER_MUTATION, requestTime); } }; @@ -1845,23 +2121,63 @@ private static boolean systemKeyspaceQuery(List cmds) return true; } - public static RowIterator readOne(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + public static RowIterator readOne(SinglePartitionReadCommand command, + ConsistencyLevel consistencyLevel, + ClientState clientState, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException { - return PartitionIterators.getOnlyElement(read(SinglePartitionReadCommand.Group.one(command), consistencyLevel, requestTime), command); + return PartitionIterators.getOnlyElement(read(SinglePartitionReadCommand.Group.one(command), + consistencyLevel, + clientState, + requestTime, + readTracker), + command); + } + + public static PartitionIterator read(SinglePartitionReadCommand.Group group, + ConsistencyLevel consistencyLevel, + ClientState clientState, + Dispatcher.RequestTime requestTime) + { + QueryInfoTracker.ReadTracker readTracker = StorageProxy.queryTracker().onRead(clientState, + group.metadata(), + group.queries, + consistencyLevel); + // Request sensors are utilized to track usages from replicas serving a read request + // Check if RequestSensors already exists (e.g., from CAS operation) and reuse it + RequestSensors requestSensors = RequestTracker.instance.get(); + if (requestSensors == null) + { + requestSensors = SensorsFactory.instance.createRequestSensors(group.metadata().keyspace); + RequestTracker.instance.set(requestSensors); + } + Context context = Context.from(group.metadata()); + requestSensors.registerSensor(context, Type.READ_BYTES); + PartitionIterator partitions = read(group, consistencyLevel, clientState, requestTime, readTracker); + partitions = PartitionIterators.filteredRowTrackingIterator(partitions, readTracker::onFilteredPartition, readTracker::onFilteredRow, readTracker::onFilteredRow); + return PartitionIterators.doOnClose(partitions, readTracker::onDone); } /** * Performs the actual reading of a row out of the StorageService, fetching * a specific set of column names from a given column family. */ - public static PartitionIterator read(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + public static PartitionIterator read(SinglePartitionReadCommand.Group group, + ConsistencyLevel consistencyLevel, + ClientState clientState, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException { - if (!isSafeToPerformRead(group.queries)) + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(group.metadata().keyspace); + ColumnFamilyStore cfs = Keyspace.openAndGetStore(group.metadata()); + + if (!cfs.isReadyToServeData() && !systemKeyspaceQuery(group.queries)) { - readMetrics.unavailables.mark(); - readMetricsForLevel(consistencyLevel).unavailables.mark(); + metrics.readMetrics.unavailables.mark(); + metrics.readMetricsForLevel(consistencyLevel).unavailables.mark(); IsBootstrappingException exception = new IsBootstrappingException(); logRequestException(exception, group.queries); throw exception; @@ -1881,8 +2197,8 @@ public static PartitionIterator read(SinglePartitionReadCommand.Group group, Con } return consistencyLevel.isSerialConsistency() - ? readWithPaxos(group, consistencyLevel, requestTime) - : readRegular(group, consistencyLevel, requestTime); + ? readWithPaxos(group, consistencyLevel, clientState, requestTime, readTracker) + : readRegular(group, consistencyLevel, requestTime, readTracker); } public static boolean isSafeToPerformRead(List queries) @@ -1895,21 +2211,29 @@ public static boolean isSafeToPerformRead() return !StorageService.instance.isBootstrapMode(); } - private static PartitionIterator readWithPaxos(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + /** + * Performs a read for paxos reads and paxos writes (cas method) + */ + private static PartitionIterator readWithPaxos(SinglePartitionReadCommand.Group group, + ConsistencyLevel consistencyLevel, + ClientState clientState, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException { return Paxos.useV2() ? Paxos.read(group, consistencyLevel, requestTime) - : legacyReadWithPaxos(group, consistencyLevel, requestTime); + : legacyReadWithPaxos(group, consistencyLevel, clientState, requestTime, readTracker); } - private static PartitionIterator legacyReadWithPaxos(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + private static PartitionIterator legacyReadWithPaxos(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ClientState clientState, Dispatcher.RequestTime requestTime, QueryInfoTracker.ReadTracker readTracker) throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException { long start = nanoTime(); if (group.queries.size() > 1) throw new InvalidRequestException("SERIAL/LOCAL_SERIAL consistency may only be requested for one partition at a time"); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(group.metadata().keyspace); SinglePartitionReadCommand command = group.queries.get(0); TableMetadata metadata = command.metadata(); DecoratedKey key = command.partitionKey(); @@ -1940,9 +2264,11 @@ private static PartitionIterator legacyReadWithPaxos(SinglePartitionReadCommand. consistencyLevel, consistencyForReplayCommitsOrFetch, ConsistencyLevel.ANY, + clientState, requestTime, - casReadMetrics, - updateProposer); + metrics.casReadMetrics, + updateProposer, + true); // skip guardrail for ANY which is blocked by CNDB } catch (WriteTimeoutException e) { @@ -1953,36 +2279,46 @@ private static PartitionIterator legacyReadWithPaxos(SinglePartitionReadCommand. throw new ReadFailureException(consistencyLevel, e.received, e.blockFor, false, e.failureReasonByEndpoint); } - result = fetchRows(group.queries, consistencyForReplayCommitsOrFetch, requestTime); + result = fetchRows(group.queries, consistencyForReplayCommitsOrFetch, requestTime, readTracker); + } + catch (CasWriteUnknownResultException e) + { + metrics.casReadMetrics.unknownResult.mark(); + readTracker.onError(e); + throw e; } catch (UnavailableException e) { - readMetrics.unavailables.mark(); - casReadMetrics.unavailables.mark(); - readMetricsForLevel(consistencyLevel).unavailables.mark(); + metrics.readMetrics.unavailables.mark(); + metrics.casReadMetrics.unavailables.mark(); + metrics.readMetricsForLevel(consistencyLevel).unavailables.mark(); logRequestException(e, group.queries); + readTracker.onError(e); throw e; } catch (ReadTimeoutException e) { - readMetrics.timeouts.mark(); - casReadMetrics.timeouts.mark(); - readMetricsForLevel(consistencyLevel).timeouts.mark(); + metrics.readMetrics.timeouts.mark(); + metrics.casReadMetrics.timeouts.mark(); + metrics.readMetricsForLevel(consistencyLevel).timeouts.mark(); logRequestException(e, group.queries); + readTracker.onError(e); throw e; } catch (ReadAbortException e) { - readMetrics.markAbort(e); - casReadMetrics.markAbort(e); - readMetricsForLevel(consistencyLevel).markAbort(e); + metrics.readMetrics.markAbort(e); + metrics.casReadMetrics.markAbort(e); + metrics.readMetricsForLevel(consistencyLevel).markAbort(e); + readTracker.onError(e); throw e; } catch (ReadFailureException e) { - readMetrics.failures.mark(); - casReadMetrics.failures.mark(); - readMetricsForLevel(consistencyLevel).failures.mark(); + metrics.readMetrics.failures.mark(); + metrics.casReadMetrics.failures.mark(); + metrics.readMetricsForLevel(consistencyLevel).failures.mark(); + readTracker.onError(e); throw e; } finally @@ -1991,24 +2327,37 @@ private static PartitionIterator legacyReadWithPaxos(SinglePartitionReadCommand. // internal paging may be composed of multiple distinct reads, whereas RequestTime relates to the single // client request. This is a measure of how long this specific individual read took, not total time since // processing of the client began. - long latency = nanoTime() - start; - readMetrics.addNano(latency); - casReadMetrics.addNano(latency); - readMetricsForLevel(consistencyLevel).addNano(latency); - Keyspace.open(metadata.keyspace).getColumnFamilyStore(metadata.name).metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS); + long endTime = nanoTime(); + long latency = endTime - start; + long serviceLatency = endTime - requestTime.startedAtNanos(); + metrics.readMetrics.executionTimeMetrics.addNano(latency); + metrics.readMetrics.serviceTimeMetrics.addNano(serviceLatency); + metrics.casReadMetrics.executionTimeMetrics.addNano(latency); + metrics.casReadMetrics.serviceTimeMetrics.addNano(serviceLatency); + metrics.readMetricsForLevel(consistencyLevel).executionTimeMetrics.addNano(latency); + metrics.readMetricsForLevel(consistencyLevel).serviceTimeMetrics.addNano(serviceLatency); + ColumnFamilyStore cfs = Keyspace.open(metadata.keyspace).getColumnFamilyStore(metadata.name); + cfs.metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS); + cfs.metric.coordinatorCasReadLatency.update(latency, TimeUnit.NANOSECONDS); } - return result; } - @SuppressWarnings("resource") - private static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + private static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, + ConsistencyLevel consistencyLevel, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) throws UnavailableException, ReadFailureException, ReadTimeoutException { + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(group.metadata().keyspace); long start = nanoTime(); try { - PartitionIterator result = fetchRows(group.queries, consistencyLevel, requestTime); + PartitionIterator result = fetchRows(group.queries, consistencyLevel, requestTime, readTracker); + + // Let the indexes do any processing here, including reordering results, before applying the limits. + result = group.postReconciliationProcessing(result); + // Note that the only difference between the command in a group must be the partition key on which // they applied. boolean enforceStrictLiveness = group.queries.get(0).metadata().enforceStrictLiveness(); @@ -2020,27 +2369,31 @@ private static PartitionIterator readRegular(SinglePartitionReadCommand.Group gr } catch (UnavailableException e) { - readMetrics.unavailables.mark(); - readMetricsForLevel(consistencyLevel).unavailables.mark(); + metrics.readMetrics.unavailables.mark(); + metrics.readMetricsForLevel(consistencyLevel).unavailables.mark(); logRequestException(e, group.queries); + readTracker.onError(e); throw e; } catch (ReadTimeoutException e) { - readMetrics.timeouts.mark(); - readMetricsForLevel(consistencyLevel).timeouts.mark(); + metrics.readMetrics.timeouts.mark(); + metrics.readMetricsForLevel(consistencyLevel).timeouts.mark(); logRequestException(e, group.queries); + readTracker.onError(e); throw e; } catch (ReadAbortException e) { - recordReadRegularAbort(consistencyLevel, e); + recordReadRegularAbort(consistencyLevel, e, metrics); + readTracker.onError(e); throw e; } catch (ReadFailureException e) { - readMetrics.failures.mark(); - readMetricsForLevel(consistencyLevel).failures.mark(); + metrics.readMetrics.failures.mark(); + metrics.readMetricsForLevel(consistencyLevel).failures.mark(); + readTracker.onError(e); throw e; } finally @@ -2049,19 +2402,23 @@ private static PartitionIterator readRegular(SinglePartitionReadCommand.Group gr // internal paging may be composed of multiple distinct reads, whereas RequestTime relates to the single // client request. This is a measure of how long this specific individual read took, not total time since // processing of the client began. - long latency = nanoTime() - start; - readMetrics.addNano(latency); - readMetricsForLevel(consistencyLevel).addNano(latency); + long endTime = nanoTime(); + long latency = endTime - start; + long serviceLatency = endTime - requestTime.startedAtNanos(); + metrics.readMetrics.executionTimeMetrics.addNano(latency); + metrics.readMetrics.serviceTimeMetrics.addNano(serviceLatency); + metrics.readMetricsForLevel(consistencyLevel).executionTimeMetrics.addNano(latency); + metrics.readMetricsForLevel(consistencyLevel).serviceTimeMetrics.addNano(serviceLatency); // TODO avoid giving every command the same latency number. Can fix this in CASSADRA-5329 for (ReadCommand command : group.queries) Keyspace.openAndGetStore(command.metadata()).metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS); } } - public static void recordReadRegularAbort(ConsistencyLevel consistencyLevel, Throwable cause) + public static void recordReadRegularAbort(ConsistencyLevel consistencyLevel, Throwable cause, ClientRequestsMetrics metrics) { - readMetrics.markAbort(cause); - readMetricsForLevel(consistencyLevel).markAbort(cause); + metrics.readMetrics.markAbort(cause); + metrics.readMetricsForLevel(consistencyLevel).markAbort(cause); } public static PartitionIterator concatAndBlockOnRepair(List iterators, List> repairs) @@ -2105,7 +2462,8 @@ public RowIterator next() */ private static PartitionIterator fetchRows(List commands, ConsistencyLevel consistencyLevel, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) throws UnavailableException, ReadFailureException, ReadTimeoutException { int cmdCount = commands.size(); @@ -2116,24 +2474,24 @@ private static PartitionIterator fetchRows(List comm // for type of speculation we'll use in this read for (int i=0; i> getSchemaVersions() @@ -2506,9 +2878,11 @@ public static boolean shouldHint(Replica replica, boolean tryEnablePersistentWin * @param keyspace * @param cfname * @throws UnavailableException If some of the hosts in the ring are down. - * @throws TimeoutException + * @throws TimeoutException If the truncate operation doesn't complete within the truncation timeout limit. + * @throws TruncateException If the truncate operation fails on some replica. */ - public static void truncateBlocking(String keyspace, String cfname) throws UnavailableException, TimeoutException + public void truncateBlocking(String keyspace, String cfname) + throws UnavailableException, TimeoutException, TruncateException { logger.debug("Starting a blocking truncate operation on keyspace {}, CF {}", keyspace, cfname); if (isAnyStorageHostDown()) @@ -2522,7 +2896,27 @@ public static void truncateBlocking(String keyspace, String cfname) throws Unava } Set allEndpoints = StorageService.instance.getLiveRingMembers(true); + truncateBlocking(allEndpoints, keyspace, cfname); + } + /** + * Performs the truncate operatoin, which effectively deletes all data from + * the column family cfname. + * This method sends truncate requests and waits for the answers. It assumes taht all endpoints + * are live. This is either enforced by {@link StorageProxy#truncateBlocking(String, String)} or by the CNDB + * override. + * + * @param allEndpoints All endpoints where to send truncate requests. + * @param keyspace + * @param cfname + * @throws UnavailableException If some of the hosts in the ring are down (all nodes need to be up to perform + * a truncate operation). + * @throws TimeoutException If the truncate operation doesn't complete within the truncation timeout limit. + * @throws TruncateException If the truncate operation fails on some replica. + */ + public void truncateBlocking(Set allEndpoints, String keyspace, String cfname) + throws UnavailableException, TimeoutException, TruncateException + { int blockFor = allEndpoints.size(); final TruncateResponseHandler responseHandler = new TruncateResponseHandler(blockFor); @@ -2567,16 +2961,19 @@ public void apply(IMutation mutation, */ private static class ViewWriteMetricsWrapped extends BatchlogResponseHandler { - public ViewWriteMetricsWrapped(AbstractWriteResponseHandler writeHandler, int i, BatchlogCleanup cleanup, Dispatcher.RequestTime requestTime) + ClientRequestsMetrics metrics; + + public ViewWriteMetricsWrapped(AbstractWriteResponseHandler writeHandler, int i, BatchlogCleanup cleanup, Dispatcher.RequestTime requestTime, ClientRequestsMetrics metrics) { super(writeHandler, i, cleanup, requestTime); - viewWriteMetrics.viewReplicasAttempted.inc(candidateReplicaCount()); + this.metrics = metrics; + metrics.viewWriteMetrics.viewReplicasAttempted.inc(candidateReplicaCount()); } public void onResponse(Message msg) { super.onResponse(msg); - viewWriteMetrics.viewReplicasSuccess.inc(); + metrics.viewWriteMetrics.viewReplicasSuccess.inc(); } } @@ -2697,7 +3094,7 @@ public static void logRequestException(Exception exception, Collection new Object[] { exception.getMessage(), - commands.stream().map(ReadCommand::toCQLString).collect(Collectors.joining("; ")) + commands.stream().map(ReadCommand::toRedactedCQLString).collect(Collectors.joining("; ")) }); } @@ -2894,6 +3291,66 @@ public String setIdealConsistencyLevel(String cl) return String.format("Updating ideal consistency level new value: %s old value %s", newCL, original.toString()); } + @Override + public int getNonIndexMemtableFlushPeriodInSeconds() + { + return CassandraRelevantProperties.FLUSH_PERIOD_IN_MILLIS.getInt(); + } + + @Override + public void setNonIndexMemtableFlushPeriodInSeconds(int flushPeriodInSeconds) + { + CassandraRelevantProperties.FLUSH_PERIOD_IN_MILLIS.setInt(flushPeriodInSeconds); + } + + @Override + public int getVectorIndexMemtableFlushPeriodInSecond() + { + return CassandraRelevantProperties.SAI_VECTOR_FLUSH_PERIOD_IN_MILLIS.getInt(); + } + + @Override + public void setVectorMemtableFlushPeriodInSecond(int flushPeriodInSecond) + { + CassandraRelevantProperties.SAI_VECTOR_FLUSH_PERIOD_IN_MILLIS.setInt(flushPeriodInSecond); + } + + @Override + public int getNonVectorIndexMemtableFlushPeriodInSecond() + { + return CassandraRelevantProperties.SAI_NON_VECTOR_FLUSH_PERIOD_IN_MILLIS.getInt(); + } + + @Override + public void setNonVectorMemtableFlushPeriodInSecond(int flushPeriodInSecond) + { + CassandraRelevantProperties.SAI_NON_VECTOR_FLUSH_PERIOD_IN_MILLIS.setInt(flushPeriodInSecond); + } + + @Override + public int getVectorIndexMemtableFlushMaxRows() + { + return CassandraRelevantProperties.SAI_VECTOR_FLUSH_THRESHOLD_MAX_ROWS.getInt(); + } + + @Override + public void setVectorMemtableFlushMaxRows(int threshold) + { + CassandraRelevantProperties.SAI_VECTOR_FLUSH_THRESHOLD_MAX_ROWS.setInt(threshold); + } + + @Override + public int getNonVectorIndexMemtableFlushMaxRows() + { + return CassandraRelevantProperties.SAI_NON_VECTOR_FLUSH_THRESHOLD_MAX_ROWS.getInt(); + } + + @Override + public void setNonVectorMemtableFlushPeriodMaxRows(int threshold) + { + CassandraRelevantProperties.SAI_NON_VECTOR_FLUSH_THRESHOLD_MAX_ROWS.setInt(threshold); + } + /** @deprecated See CASSANDRA-15066 */ @Deprecated(since = "4.0") public int getOtcBacklogExpirationInterval() { @@ -3270,4 +3727,5 @@ public void setClientRequestSizeMetricsEnabled(boolean enabled) { DatabaseDescriptor.setClientRequestSizeMetricsEnabled(enabled); } + } diff --git a/src/java/org/apache/cassandra/service/StorageProxyMBean.java b/src/java/org/apache/cassandra/service/StorageProxyMBean.java index a3c7de0d6418..73407d6c6373 100644 --- a/src/java/org/apache/cassandra/service/StorageProxyMBean.java +++ b/src/java/org/apache/cassandra/service/StorageProxyMBean.java @@ -99,6 +99,26 @@ public interface StorageProxyMBean public String getIdealConsistencyLevel(); public String setIdealConsistencyLevel(String cl); + // Default memtable flush period for tables without indexes. New value will take effect when new memtable is created + public int getNonIndexMemtableFlushPeriodInSeconds(); + public void setNonIndexMemtableFlushPeriodInSeconds(int flushPeriodInSeconds); + + // Default memtable flush period for tables with vector SAI indexes. New value will take effect when new memtable is created + public int getVectorIndexMemtableFlushPeriodInSecond(); + public void setVectorMemtableFlushPeriodInSecond(int flushPeriodInSecond); + + // Default memtable flush period for tables with non-vector SAI indexes. New value will take effect when new index memtable is created + public int getNonVectorIndexMemtableFlushPeriodInSecond(); + public void setNonVectorMemtableFlushPeriodInSecond(int flushPeriodInSecond); + + // When num of rows in SAI vector memtable index reaches the threshold, it triggers flush. New value will take effect when new memtable index is created + public int getVectorIndexMemtableFlushMaxRows(); + public void setVectorMemtableFlushMaxRows(int threshold); + + // When num of rows in SAI non-vector memtable index reaches the threshold, it triggers flush. New value will take effect when new memtable index is created + public int getNonVectorIndexMemtableFlushMaxRows(); + public void setNonVectorMemtableFlushPeriodMaxRows(int threshold); + public void logBlockingReadRepairAttemptsForNSeconds(int seconds); public boolean isLoggingReadRepairs(); diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java index 0f178b00f08f..7afe0d697607 100644 --- a/src/java/org/apache/cassandra/service/StorageService.java +++ b/src/java/org/apache/cassandra/service/StorageService.java @@ -17,8 +17,6 @@ */ package org.apache.cassandra.service; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; import java.io.IOError; import java.io.IOException; import java.net.InetAddress; @@ -35,11 +33,11 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.Scanner; import java.util.Set; @@ -88,10 +86,15 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.Uninterruptibles; + +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.repair.autorepair.AutoRepairConfig; +import org.apache.cassandra.repair.autorepair.AutoRepair; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.codahale.metrics.Meter; import org.apache.cassandra.audit.AuditLogManager; import org.apache.cassandra.audit.AuditLogOptions; import org.apache.cassandra.auth.AuthCacheService; @@ -107,7 +110,6 @@ import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.Config.PaxosStatePurging; -import org.apache.cassandra.config.Converters; import org.apache.cassandra.config.DataStorageSpec; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DurationSpec; @@ -122,7 +124,10 @@ import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.guardrails.GuardrailsConfig; +import org.apache.cassandra.db.guardrails.GuardrailsConfigProvider; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; import org.apache.cassandra.dht.BootStrapper; @@ -142,11 +147,9 @@ import org.apache.cassandra.fql.FullQueryLoggerOptionsCompositeData; import org.apache.cassandra.gms.ApplicationState; import org.apache.cassandra.gms.EndpointState; -import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.IEndpointStateChangeSubscriber; import org.apache.cassandra.gms.IFailureDetector; -import org.apache.cassandra.gms.TokenSerializer; import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.hints.Hint; import org.apache.cassandra.hints.HintsService; @@ -175,6 +178,7 @@ import org.apache.cassandra.locator.Replicas; import org.apache.cassandra.locator.SystemReplicas; import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.locator.TokenMetadataProvider; import org.apache.cassandra.metrics.Sampler; import org.apache.cassandra.metrics.SamplingManager; import org.apache.cassandra.metrics.StorageMetrics; @@ -183,6 +187,8 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.repair.RepairCoordinator; import org.apache.cassandra.repair.SharedContext; +import org.apache.cassandra.nodes.Nodes; +import org.apache.cassandra.repair.autorepair.AutoRepairUtils; import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.schema.CompactionParams.TombstoneOption; import org.apache.cassandra.schema.KeyspaceMetadata; @@ -226,6 +232,7 @@ import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.concurrent.ImmediateFuture; +import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import org.apache.cassandra.utils.logging.LoggingSupportFactory; import org.apache.cassandra.utils.progress.ProgressEvent; @@ -265,8 +272,10 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_WRITE_SURVEY; import static org.apache.cassandra.index.SecondaryIndexManager.getIndexName; import static org.apache.cassandra.index.SecondaryIndexManager.isIndexColumnFamily; +import static org.apache.cassandra.io.util.FileUtils.ONE_MIB; import static org.apache.cassandra.net.NoPayload.noPayload; import static org.apache.cassandra.net.Verb.REPLICATION_DONE_REQ; +import static org.apache.cassandra.locator.InetAddressAndPort.stringify; import static org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus; import static org.apache.cassandra.service.ActiveRepairService.repairCommandExecutor; import static org.apache.cassandra.service.StorageService.Mode.DECOMMISSIONED; @@ -297,7 +306,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE PathUtils.setDeletionListener(path -> { if (isDaemonSetupCompleted()) PathUtils.setDeletionListener(ignore -> {}); - else + else if (logger.isTraceEnabled()) logger.trace("Deleting file during startup: {}", path); }); } @@ -306,16 +315,12 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private static int getRingDelay() { - String newdelay = CassandraRelevantProperties.RING_DELAY.getString(); - if (newdelay != null) - { - logger.info("Overriding RING_DELAY to {}ms", newdelay); - return Integer.parseInt(newdelay); - } - else - { - return 30 * 1000; - } + int defaultDelay = 30 * 1000; + int newDelay = CassandraRelevantProperties.RING_DELAY.getInt(defaultDelay); + Preconditions.checkArgument(newDelay >= 0, "%s must be >= 0", CassandraRelevantProperties.RING_DELAY.getKey()); + if (newDelay != defaultDelay) + logger.info("Overriding {} to {}ms", CassandraRelevantProperties.RING_DELAY.getKey(), newDelay); + return newDelay; } private static int getSchemaDelay() @@ -332,12 +337,8 @@ private static int getSchemaDelay() } } - /* This abstraction maintains the token/endpoint metadata information */ - private TokenMetadata tokenMetadata = new TokenMetadata(); + public volatile VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(TokenMetadataProvider.instance.getTokenMetadata().partitioner); - public volatile VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(tokenMetadata.partitioner); - - private Thread drainOnShutdown = null; private volatile boolean isShutdown = false; private final List preShutdownHooks = new ArrayList<>(); private final List postShutdownHooks = new ArrayList<>(); @@ -348,6 +349,8 @@ private static int getSchemaDelay() private final SamplingManager samplingManager = new SamplingManager(); + // Newer versions of mockito contain mockito-inline which creates an issue in our test environment. Without this + // change, mocking of static methods is a problem with our DTest framework @VisibleForTesting // this is used for dtests only, see CASSANDRA-18152 public volatile boolean skipNotificationListeners = false; @@ -408,7 +411,12 @@ public RangesAtEndpoint getLocalReplicas(String keyspaceName) public RangesAtEndpoint getReplicas(String keyspaceName, InetAddressAndPort endpoint) { - return Keyspace.open(keyspaceName).getReplicationStrategy().getAddressReplicas(endpoint); + return getReplicas(Keyspace.open(keyspaceName).getReplicationStrategy(), endpoint); + } + + public RangesAtEndpoint getReplicas(AbstractReplicationStrategy replicationStrategy, InetAddressAndPort endpoint) + { + return replicationStrategy.getAddressReplicas(getTokenMetadata().cloneOnlyTokenMap(), endpoint); } public List> getLocalRanges(String ks) @@ -428,7 +436,7 @@ public List> getLocalAndPendingRanges(String ks) List> ranges = new ArrayList<>(); for (Replica r : keyspace.getReplicationStrategy().getAddressReplicas(broadcastAddress)) ranges.add(r.range()); - for (Replica r : getTokenMetadata().getPendingRanges(ks, broadcastAddress)) + for (Replica r : getTokenMetadataForKeyspace(ks).getPendingRanges(ks, broadcastAddress)) ranges.add(r.range()); return ranges; } @@ -482,10 +490,13 @@ public enum Mode { STARTING, NORMAL, JOINING, JOINING_FAILED, LEAVING, DECOMMISS /* Used for tracking drain progress */ private volatile int totalCFs, remainingCFs; - private static final AtomicInteger nextRepairCommand = new AtomicInteger(); + public static final AtomicInteger nextRepairCommand = new AtomicInteger(); private final List lifecycleSubscribers = new CopyOnWriteArrayList<>(); + /** Hooks run by {@link #decommission(boolean)} once this node has left the ring; see {@link DecommissionHook}. */ + private final List decommissionHooks = new CopyOnWriteArrayList<>(); + private final String jmxObjectName; private Collection bootstrapTokens = null; @@ -521,7 +532,7 @@ public void setTokens(Collection tokens) SystemKeyspace.updateTokens(tokens); Collection localTokens = getLocalTokens(); setGossipTokens(localTokens); - tokenMetadata.updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); + getTokenMetadata().updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); setMode(Mode.NORMAL, false); invalidateLocalRanges(); } @@ -566,6 +577,24 @@ public void unregister(IEndpointLifecycleSubscriber subscriber) lifecycleSubscribers.remove(subscriber); } + /** + * Registers work to run on this node while it is being decommissioned, after it has left the + * ring and before anything is shut down. Hooks run in registration order; see + * {@link DecommissionHook} for the full contract. + */ + public void registerDecommissionHook(DecommissionHook hook) + { + // Fail the caller now rather than during the decommission: CopyOnWriteArrayList accepts + // nulls, and by the time hooks run there is no safe way to fail. + decommissionHooks.add(Objects.requireNonNull(hook, "decommission hook must not be null")); + } + + /** @return true if {@code hook} was registered. */ + public boolean unregisterDecommissionHook(DecommissionHook hook) + { + return decommissionHooks.remove(hook); + } + // should only be called via JMX public void stopGossiping() { @@ -750,68 +779,65 @@ private synchronized UUID prepareForReplacement() throws ConfigurationException if (state == null) throw new RuntimeException(String.format("Cannot replace_address %s because it doesn't exist in gossip", replaceAddress)); - validateEndpointSnitch(epStates.values().iterator()); - - try - { - VersionedValue tokensVersionedValue = state.getApplicationState(ApplicationState.TOKENS); - if (tokensVersionedValue == null) - throw new RuntimeException(String.format("Could not find tokens for %s to replace", replaceAddress)); - - Collection tokens = TokenSerializer.deserialize(tokenMetadata.partitioner, new DataInputStream(new ByteArrayInputStream(tokensVersionedValue.toBytes()))); - bootstrapTokens = validateReplacementBootstrapTokens(tokenMetadata, replaceAddress, tokens); + validateEndpointSnitch(epStates.keySet()); + return replaceNodeAndOwnTokens(replaceAddress, epStates, state); + } - if (state.isEmptyWithoutStatus() && REPLACEMENT_ALLOW_EMPTY.getBoolean()) - { - logger.warn("Gossip state not present for replacing node {}. Adding temporary entry to continue.", replaceAddress); - - // When replacing a node, we take ownership of all its tokens. - // If that node is currently down and not present in the gossip info - // of any other live peers, then we will not be able to take ownership - // of its tokens during bootstrap as they have no way of being propagated - // to this node's TokenMetadata. TM is loaded at startup (in which case - // it will be/ empty for a new replacement node) and only updated with - // tokens for an endpoint during normal state propagation (which will not - // occur if no peers have gossip state for it). - // However, the presence of host id and tokens in the system tables implies - // that the node managed to complete bootstrap at some point in the past. - // Peers may include this information loaded directly from system tables - // in a GossipDigestAck *only if* the GossipDigestSyn was sent as part of a - // shadow round (otherwise, a GossipDigestAck contains only state about peers - // learned via gossip). - // It is safe to do this here as since we completed a shadow round we know - // that : - // * replaceAddress successfully bootstrapped at some point and owned these - // tokens - // * we know that no other node currently owns these tokens - // * we are going to completely take over replaceAddress's ownership of - // these tokens. - tokenMetadata.updateNormalTokens(bootstrapTokens, replaceAddress); - UUID hostId = Gossiper.instance.getHostId(replaceAddress, epStates); - if (hostId != null) - tokenMetadata.updateHostId(hostId, replaceAddress); - - // If we were only able to learn about the node being replaced through the - // shadow gossip round (i.e. there is no state in gossip across the cluster - // about it, perhaps because the entire cluster has been bounced since it went - // down), then we're safe to proceed with the replacement. In this case, there - // will be no local endpoint state as we discard the results of the shadow - // round after preparing replacement info. We inject a minimal EndpointState - // to keep FailureDetector::isAlive and Gossiper::compareEndpointStartup from - // failing later in the replacement, as they both expect the replaced node to - // be fully present in gossip. - // Otherwise, if the replaced node is present in gossip, we need check that - // it is not in fact live. - // We choose to not include the EndpointState provided during the shadow round - // as its possible to include more state than is desired, so by creating a - // new empty endpoint without that information we can control what is in our - // local gossip state - Gossiper.instance.initializeUnreachableNodeUnsafe(replaceAddress); - } - } - catch (IOException e) - { - throw new RuntimeException(e); + @VisibleForTesting + UUID replaceNodeAndOwnTokens(InetAddressAndPort replaceAddress, Map epStates, EndpointState state) + { + Collection tokens = state.getTokens(getTokenMetadata().partitioner); + if (tokens == null) + throw new RuntimeException(String.format("Could not find tokens for %s to replace", replaceAddress)); + + bootstrapTokens = validateReplacementBootstrapTokens(getTokenMetadata(), replaceAddress, tokens); + + if (state.isEmptyWithoutStatus() && REPLACEMENT_ALLOW_EMPTY.getBoolean()) + { + logger.warn("Gossip state not present for replacing node {}. Adding temporary entry to continue.", replaceAddress); + + // When replacing a node, we take ownership of all its tokens. + // If that node is currently down and not present in the gossip info + // of any other live peers, then we will not be able to take ownership + // of its tokens during bootstrap as they have no way of being propagated + // to this node's TokenMetadata. TM is loaded at startup (in which case + // it will be/ empty for a new replacement node) and only updated with + // tokens for an endpoint during normal state propagation (which will not + // occur if no peers have gossip state for it). + // However, the presence of host id and tokens in the system tables implies + // that the node managed to complete bootstrap at some point in the past. + // Peers may include this information loaded directly from system tables + // in a GossipDigestAck *only if* the GossipDigestSyn was sent as part of a + // shadow round (otherwise, a GossipDigestAck contains only state about peers + // learned via gossip). + // It is safe to do this here as since we completed a shadow round we know + // that : + // * replaceAddress successfully bootstrapped at some point and owned these + // tokens + // * we know that no other node currently owns these tokens + // * we are going to completely take over replaceAddress's ownership of + // these tokens. + getTokenMetadata().updateNormalTokens(bootstrapTokens, replaceAddress); + UUID hostId = Gossiper.instance.getHostId(replaceAddress, epStates); + if (hostId != null) + getTokenMetadata().updateHostId(hostId, replaceAddress); + + // If we were only able to learn about the node being replaced through the + // shadow gossip round (i.e. there is no state in gossip across the cluster + // about it, perhaps because the entire cluster has been bounced since it went + // down), then we're safe to proceed with the replacement. In this case, there + // will be no local endpoint state as we discard the results of the shadow + // round after preparing replacement info. We inject a minimal EndpointState + // to keep IFailureDetector::isAlive and Gossiper::compareEndpointStartup from + // failing later in the replacement, as they both expect the replaced node to + // be fully present in gossip. + // Otherwise, if the replaced node is present in gossip, we need check that + // it is not in fact live. + // We choose to not include the EndpointState provided during the shadow round + // as its possible to include more state than is desired, so by creating a + // new empty endpoint without that information we can control what is in our + // local gossip state + Gossiper.instance.initializeUnreachableNodeUnsafe(replaceAddress); } UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId(); @@ -866,7 +892,7 @@ public synchronized void checkForEndpointCollision(UUID localHostId, Set epStates = Gossiper.instance.doShadowRound(peers); if (epStates.isEmpty() && DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort())) - logger.info("Unable to gossip with any peers but continuing anyway since node is in its own seed list"); + logger.info("Unable to gossip with any peers but continuing anyway since node is in its own seed list. Broadcast address: {}, seeds: {}", FBUtilities.getBroadcastAddressAndPort(), DatabaseDescriptor.getSeeds()); // If bootstrapping, check whether any previously known status for the endpoint makes it unsafe to do so. // If not bootstrapping, compare the host id for this endpoint learned from gossip (if any) with the local @@ -879,7 +905,7 @@ public synchronized void checkForEndpointCollision(UUID localHostId, Set endpointStates) + private static void validateEndpointSnitch(Collection endpoints) { Set datacenters = new HashSet<>(); Set racks = new HashSet<>(); - while (endpointStates.hasNext()) - { - EndpointState state = endpointStates.next(); - VersionedValue val = state.getApplicationState(ApplicationState.DC); - if (val != null) - datacenters.add(val.value); - val = state.getApplicationState(ApplicationState.RACK); - if (val != null) - racks.add(val.value); - } + endpoints.stream().map(Nodes::localOrPeerInfo).filter(Objects::nonNull).forEach(nodeInfo -> { + if (nodeInfo.getDataCenter() != null) + datacenters.add(nodeInfo.getDataCenter()); + if (nodeInfo.getRack() != null) + racks.add(nodeInfo.getRack()); + }); IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); if (!snitch.validate(datacenters, racks)) @@ -974,14 +996,14 @@ public synchronized void initServer(int schemaTimeoutMillis, int ringTimeoutMill if (LOAD_RING_STATE.getBoolean()) { - logger.info("Loading persisted ring state"); + logger.debug("Loading persisted ring state"); populatePeerTokenMetadata(); - for (InetAddressAndPort endpoint : tokenMetadata.getAllEndpoints()) + for (InetAddressAndPort endpoint : getTokenMetadata().getAllEndpoints()) Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.addSavedEndpoint(endpoint)); } // daemon threads, like our executors', continue to run while shutdown hooks are invoked - drainOnShutdown = NamedThreadFactory.createThread(new WrappedRunnable() + Thread drainOnShutdown = NamedThreadFactory.createThread(new WrappedRunnable() { @Override public void runMayThrow() throws InterruptedException, ExecutionException, IOException @@ -1002,8 +1024,12 @@ public void runMayThrow() throws InterruptedException, ExecutionException, IOExc } } }, "StorageServiceShutdownHook"); - Runtime.getRuntime().addShutdownHook(drainOnShutdown); + registerMBeans(); + JVMStabilityInspector.registerShutdownHook(drainOnShutdown, this::onShutdownHookRemoved); + + // Register signal handlers to log received signals for shutdown investigation + registerSignalHandlers(); replacing = isReplacing(); @@ -1036,7 +1062,7 @@ public void runMayThrow() throws InterruptedException, ExecutionException, IOExc Collection tokens = SystemKeyspace.getSavedTokens(); if (!tokens.isEmpty()) { - tokenMetadata.updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); + getTokenMetadata().updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); // order is important here, the gossiper can fire in between adding these two states. It's ok to send TOKENS without STATUS, but *not* vice versa. List> states = new ArrayList>(); states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(tokens))); @@ -1064,15 +1090,15 @@ public void populateTokenMetadata() populatePeerTokenMetadata(); // if we have not completed bootstrapping, we should not add ourselves as a normal token if (!shouldBootstrap()) - tokenMetadata.updateNormalTokens(SystemKeyspace.getSavedTokens(), FBUtilities.getBroadcastAddressAndPort()); + getTokenMetadata().updateNormalTokens(SystemKeyspace.getSavedTokens(), FBUtilities.getBroadcastAddressAndPort()); - logger.info("Token metadata: {}", tokenMetadata); + logger.info("Token metadata: {}", getTokenMetadata()); } } private void populatePeerTokenMetadata() { - logger.info("Populating token metadata from system tables"); + logger.debug("Populating token metadata from system tables"); Multimap loadedTokens = SystemKeyspace.loadTokens(); // entry has been mistakenly added, delete it @@ -1087,8 +1113,8 @@ private void populatePeerTokenMetadata() if (hostId != null) hostIdToEndpointMap.put(hostId, ep); } - tokenMetadata.updateNormalTokens(loadedTokens); - tokenMetadata.updateHostIds(hostIdToEndpointMap); + getTokenMetadata().updateNormalTokens(loadedTokens); + getTokenMetadata().updateHostIds(hostIdToEndpointMap); } public boolean isReplacing() @@ -1108,12 +1134,9 @@ public boolean isReplacing() /** * In the event of forceful termination we need to remove the shutdown hook to prevent hanging (OOM for instance) */ - public void removeShutdownHook() + public void onShutdownHookRemoved() { PathUtils.clearOnExitThreads(); - - if (drainOnShutdown != null) - Runtime.getRuntime().removeShutdownHook(drainOnShutdown); } private boolean shouldBootstrap() @@ -1126,6 +1149,57 @@ public static boolean isSeed() return DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort()); } + private void registerSignalHandlers() + { + registerSignalHandlersInternal(new String[]{"TERM", "INT", "HUP"}); + } + + @VisibleForTesting + void registerSignalHandlersForTest(String[] testSignals) + { + registerSignalHandlersInternal(testSignals); + } + + private void registerSignalHandlersInternal(String[] signals) + { + try + { + for (String signalName : signals) + { + try + { + sun.misc.Signal signal = new sun.misc.Signal(signalName); + // Use an array to hold the old handler reference so it can be captured in the inner class + final sun.misc.SignalHandler[] oldHandlerHolder = new sun.misc.SignalHandler[1]; + oldHandlerHolder[0] = sun.misc.Signal.handle(signal, + new sun.misc.SignalHandler() + { + @Override + public void handle(sun.misc.Signal sig) + { + logger.info("Received signal: SIG{} ({})", sig.getName(), sig.getNumber()); + // Chain to the previous handler to ensure normal shutdown proceeds + if (oldHandlerHolder[0] != null && oldHandlerHolder[0] != sun.misc.SignalHandler.SIG_DFL && + oldHandlerHolder[0] != sun.misc.SignalHandler.SIG_IGN) + { + oldHandlerHolder[0].handle(sig); + } + } + }); + } + catch (IllegalArgumentException e) + { + logger.debug("Signal SIG{} is not available on this platform", signalName); + } + } + } + catch (Throwable t) + { + // Don't let signal handler registration failure prevent startup + logger.warn("Failed to register signal handlers for shutdown logging", t); + } + } + private void prepareToJoin() throws ConfigurationException { if (!joined) @@ -1202,7 +1276,7 @@ else if (isReplacingSameAddress()) appStates.put(ApplicationState.RELEASE_VERSION, valueFactory.releaseVersion()); appStates.put(ApplicationState.SSTABLE_VERSIONS, valueFactory.sstableVersions(sstablesTracker.versionsInUse())); - logger.info("Starting up server gossip"); + logger.debug("Starting up server gossip"); Gossiper.instance.register(this); Gossiper.instance.start(SystemKeyspace.incrementAndGetGeneration(), appStates); // needed for node-ring gathering. gossipActive = true; @@ -1295,14 +1369,14 @@ public void joinTokenRing(boolean finishJoiningRing, bootstrapTokens = SystemKeyspace.getSavedTokens(); if (bootstrapTokens.isEmpty()) { - bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddressAndPort(), schemaTimeoutMillis, ringTimeoutMillis); + bootstrapTokens = BootStrapper.getBootstrapTokens(getTokenMetadata(), FBUtilities.getBroadcastAddressAndPort(), schemaTimeoutMillis, ringTimeoutMillis); } else { if (bootstrapTokens.size() != DatabaseDescriptor.getNumTokens()) throw new ConfigurationException("Cannot change the number of tokens from " + bootstrapTokens.size() + " to " + DatabaseDescriptor.getNumTokens()); else - logger.info("Using saved tokens {}", bootstrapTokens); + logger.debug("Using saved tokens {}", bootstrapTokens); } } @@ -1313,6 +1387,14 @@ public void joinTokenRing(boolean finishJoiningRing, if (dataAvailable) { finishJoiningRing(shouldBootstrap, bootstrapTokens); + AutoRepairConfig repairConfig = DatabaseDescriptor.getAutoRepairConfig(); + // this node might have just bootstrapped; check if we should run repair immediately + if (shouldBootstrap && repairConfig.isAutoRepairSchedulingEnabled()) + { + for (AutoRepairConfig.RepairType rType : AutoRepairConfig.RepairType.values()) + if (repairConfig.isAutoRepairEnabled(rType) && repairConfig.getForceRepairNewNode(rType)) + AutoRepairUtils.setForceRepairNewNode(rType); + } // remove the existing info about the replaced node. if (!current.isEmpty()) { @@ -1414,7 +1496,9 @@ public void finishJoiningRing(boolean didBootstrap, Collection tokens) executePreJoinTasks(didBootstrap); setTokens(tokens); - assert tokenMetadata.sortedTokens().size() > 0; + assert getTokenMetadata().sortedTokens().size() > 0; + + doAutoRepairSetup(); } @VisibleForTesting @@ -1431,13 +1515,32 @@ public void doAuthSetup(boolean setUpSchema) DatabaseDescriptor.getAuthenticator().setup(); DatabaseDescriptor.getAuthorizer().setup(); DatabaseDescriptor.getNetworkAuthorizer().setup(); - DatabaseDescriptor.getCIDRAuthorizer().setup(); + if (!DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5)) + DatabaseDescriptor.getCIDRAuthorizer().setup(); AuthCacheService.initializeAndRegisterCaches(); Schema.instance.registerListener(new AuthSchemaChangeListener()); authSetupComplete = true; } } + public void doAutoRepairSetup() + { + if (!CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean()) + { + logger.info("Auto-repair service is disabled via JVM property, skipping setup"); + return; + } + + AutoRepairService.setup(); + if (DatabaseDescriptor.getAutoRepairConfig().isAutoRepairSchedulingEnabled()) + { + logger.info("Enabling auto-repair scheduling"); + AutoRepair.instance.setup(); + logger.info("AutoRepair setup complete!"); + } + } + + public boolean isAuthSetupComplete() { return authSetupComplete; @@ -1460,7 +1563,7 @@ public void setUpDistributedSystemKeyspaces() public boolean isJoined() { - return tokenMetadata.isMember(FBUtilities.getBroadcastAddressAndPort()) && !isSurveyMode; + return getTokenMetadata().isMember(FBUtilities.getBroadcastAddressAndPort()) && !isSurveyMode; } public void rebuild(String sourceDc) @@ -1511,7 +1614,7 @@ public void rebuild(String sourceDc, String keyspace, String tokens, String spec repairPaxosForTopologyChange("rebuild"); - RangeStreamer streamer = new RangeStreamer(tokenMetadata, + RangeStreamer streamer = new RangeStreamer(getTokenMetadata(), null, FBUtilities.getBroadcastAddressAndPort(), StreamOperation.REBUILD, @@ -1906,6 +2009,21 @@ public void setCompressedReadAheadBufferInKB(int sizeInKb) } + /** + * Get the Current Compaction Throughput + * key is 1/5/15minute time dimension for statistics + * value is the metric double string (unit is:mib/s) + */ + public Map getCurrentCompactionThroughputMebibytesPerSec() + { + HashMap result = new LinkedHashMap<>(); + Meter rate = CompactionManager.instance.getCompactionThroughput(); + result.put("1minute", String.format("%.3f", rate.getOneMinuteRate() / ONE_MIB)); + result.put("5minute", String.format("%.3f", rate.getFiveMinuteRate() / ONE_MIB)); + result.put("15minute", String.format("%.3f", rate.getFifteenMinuteRate() / ONE_MIB)); + return result; + } + public int getBatchlogReplayThrottleInKB() { return DatabaseDescriptor.getBatchlogReplayThrottleInKiB(); @@ -2068,14 +2186,14 @@ public Collection prepareForBootstrap(long schemaTimeoutMill if (useStrictConsistency && !allowSimultaneousMoves() && ( - tokenMetadata.getBootstrapTokens().valueSet().size() > 0 || - tokenMetadata.getSizeOfLeavingEndpoints() > 0 || - tokenMetadata.getSizeOfMovingEndpoints() > 0 + getTokenMetadata().getBootstrapTokens().valueSet().size() > 0 || + getTokenMetadata().getSizeOfLeavingEndpoints() > 0 || + getTokenMetadata().getSizeOfMovingEndpoints() > 0 )) { - String bootstrapTokens = StringUtils.join(tokenMetadata.getBootstrapTokens().valueSet(), ','); - String leavingTokens = StringUtils.join(tokenMetadata.getLeavingEndpoints(), ','); - String movingTokens = StringUtils.join(tokenMetadata.getMovingEndpoints().stream().map(e -> e.right).toArray(), ','); + String bootstrapTokens = StringUtils.join(getTokenMetadata().getBootstrapTokens().valueSet(), ','); + String leavingTokens = StringUtils.join(getTokenMetadata().getLeavingEndpoints(), ','); + String movingTokens = StringUtils.join(getTokenMetadata().getMovingEndpoints().stream().map(e -> e.right).toArray(), ','); throw new UnsupportedOperationException(String.format("Other bootstrapping/leaving/moving nodes detected, cannot bootstrap while %s is true. Nodes detected, bootstrapping: %s; leaving: %s; moving: %s;", CONSISTENT_RANGE_MOVEMENT.getKey(), bootstrapTokens, leavingTokens, movingTokens)); } @@ -2083,13 +2201,13 @@ public Collection prepareForBootstrap(long schemaTimeoutMill // get bootstrap tokens if (!replacing) { - if (tokenMetadata.isMember(FBUtilities.getBroadcastAddressAndPort())) + if (getTokenMetadata().isMember(FBUtilities.getBroadcastAddressAndPort())) { String s = "This node is already a member of the token ring; bootstrap aborted. (If replacing a dead node, remove the old one from the ring first.)"; throw new UnsupportedOperationException(s); } setMode(Mode.JOINING, "getting bootstrap token", true); - bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddressAndPort(), schemaTimeoutMillis, ringTimeoutMillis); + bootstrapTokens = BootStrapper.getBootstrapTokens(getTokenMetadata(), FBUtilities.getBroadcastAddressAndPort(), schemaTimeoutMillis, ringTimeoutMillis); } else { @@ -2117,7 +2235,7 @@ public Collection prepareForBootstrap(long schemaTimeoutMill long nanoDelay = MILLISECONDS.toNanos(ringTimeoutMillis); for (Token token : bootstrapTokens) { - InetAddressAndPort existing = tokenMetadata.getEndpoint(token); + InetAddressAndPort existing = getTokenMetadata().getEndpoint(token); if (existing != null) { EndpointState endpointStateForExisting = Gossiper.instance.getEndpointStateForEndpoint(existing); @@ -2189,11 +2307,15 @@ public boolean bootstrap(final Collection tokens, long bootstrapTimeoutMi else { // Dont set any state for the node which is bootstrapping the existing token... - tokenMetadata.updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); + getTokenMetadata().updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); SystemKeyspace.removeEndpoint(DatabaseDescriptor.getReplaceAddress()); } if (!Gossiper.instance.seenAnySeed()) + { + logger.info("Announcing shutdown to get out of the hibernation deadlock"); + Gossiper.instance.announceShutdown(); throw new IllegalStateException("Unable to contact any seeds: " + Gossiper.instance.getSeeds()); + } if (RESET_BOOTSTRAP_PROGRESS.getBoolean()) { @@ -2232,7 +2354,7 @@ public Future startBootstrap(Collection tokens) public Future startBootstrap(Collection tokens, boolean replacing) { setMode(Mode.JOINING, "Starting to bootstrap...", true); - BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddressAndPort(), tokens, tokenMetadata); + BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddressAndPort(), tokens, getTokenMetadata()); bootstrapper.addProgressListener(progressSupport); return bootstrapper.bootstrap(streamStateStore, useStrictConsistency && !replacing); // handles token update } @@ -2245,7 +2367,7 @@ private void invalidateLocalRanges() { for (final ColumnFamilyStore store : cfs.concatWithIndexes()) { - store.invalidateLocalRanges(); + store.invalidateLocalRangesAndDiskBoundaries(); } } } @@ -2287,7 +2409,7 @@ public boolean resumeBootstrap() // get bootstrap tokens saved in system keyspace final Collection tokens = SystemKeyspace.getSavedTokens(); // already bootstrapped ranges are filtered during bootstrap - BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddressAndPort(), tokens, tokenMetadata); + BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddressAndPort(), tokens, getTokenMetadata()); bootstrapper.addProgressListener(progressSupport); Future bootstrapStream = bootstrapper.bootstrap(streamStateStore, useStrictConsistency && !replacing); // handles token update bootstrapStream.addCallback(new FutureCallback() @@ -2370,7 +2492,12 @@ public boolean isBootstrapMode() public TokenMetadata getTokenMetadata() { - return tokenMetadata; + return TokenMetadataProvider.instance.getTokenMetadata(); + } + + public TokenMetadata getTokenMetadataForKeyspace(String keyspaceName) + { + return TokenMetadataProvider.instance.getTokenMetadataForKeyspace(keyspaceName); } public Map, List> getRangeToEndpointMap(String keyspace) @@ -2399,21 +2526,20 @@ public Map, List> getRangeToEndpointMap(String keyspace, bo return map; } - /** - * Return the native address associated with an endpoint as a string. - * @param endpoint The endpoint to get rpc address for - * @return the native address - */ - public String getNativeaddress(InetAddressAndPort endpoint, boolean withPort) + public InetAddressAndPort getNativeAddressAndPort(InetAddressAndPort endpoint) { + InetAddressAndPort addr = Nodes.getNativeTransportAddressAndPort(endpoint, null); + if (addr != null) + return addr; + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return FBUtilities.getBroadcastNativeAddressAndPort().getHostAddress(withPort); + return FBUtilities.getBroadcastNativeAddressAndPort(); else if (Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.NATIVE_ADDRESS_AND_PORT) != null) { try { InetAddressAndPort address = InetAddressAndPort.getByName(Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.NATIVE_ADDRESS_AND_PORT).value); - return address.getHostAddress(withPort); + return address; } catch (UnknownHostException e) { @@ -2422,29 +2548,39 @@ else if (Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationS } else { - final String ipAddress; - // If RPC_ADDRESS present in gossip for this endpoint use it. This is expected for 3.x nodes. - if (Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.RPC_ADDRESS) != null) - { - ipAddress = Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.RPC_ADDRESS).value; - } - else - { - // otherwise just use the IP of the endpoint itself. - ipAddress = endpoint.getHostAddress(false); - } - - // include the configured native_transport_port. - try - { - InetAddressAndPort address = InetAddressAndPort.getByNameOverrideDefaults(ipAddress, DatabaseDescriptor.getNativeTransportPort()); - return address.getHostAddress(withPort); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - } + final String ipAddress; + // If RPC_ADDRESS present in gossip for this endpoint use it. This is expected for 3.x nodes. + if (Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.RPC_ADDRESS) != null) + { + ipAddress = Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.RPC_ADDRESS).value; + } + else + { + // otherwise just use the IP of the endpoint itself. + ipAddress = endpoint.getHostAddress(false); + } + + // include the configured native_transport_port. + try + { + InetAddressAndPort address = InetAddressAndPort.getByNameOverrideDefaults(ipAddress, DatabaseDescriptor.getNativeTransportPort()); + return address; + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + } + + /** + * Return the native address associated with an endpoint as a string. + * @param endpoint The endpoint to get rpc address for + * @return the native address + */ + public String getNativeAddress(InetAddressAndPort endpoint, boolean withPort) + { + return getNativeAddressAndPort(endpoint).getHostAddress(withPort); } public Map, List> getRangeToRpcaddressMap(String keyspace) @@ -2471,7 +2607,7 @@ private Map, List> getRangeToNativeaddressMap(String keyspa List rpcaddrs = new ArrayList<>(entry.getValue().size()); for (Replica replicas: entry.getValue()) { - rpcaddrs.add(getNativeaddress(replicas.endpoint(), withPort)); + rpcaddrs.add(getNativeAddress(replicas.endpoint(), withPort)); } map.put(entry.getKey().asList(), rpcaddrs); } @@ -2496,7 +2632,7 @@ private Map, List> getPendingRangeToEndpointMap(String keys keyspace = Schema.instance.distributedKeyspaces().iterator().next().name; Map, List> map = new HashMap<>(); - for (Map.Entry, EndpointsForRange> entry : tokenMetadata.getPendingRangesMM(keyspace).asMap().entrySet()) + for (Map.Entry, EndpointsForRange> entry : getTokenMetadata().getPendingRangesMM(keyspace).asMap().entrySet()) { map.put(entry.getKey().asList(), Replicas.stringify(entry.getValue(), withPort)); } @@ -2505,7 +2641,7 @@ private Map, List> getPendingRangeToEndpointMap(String keys public EndpointsByRange getRangeToAddressMap(String keyspace) { - return getRangeToAddressMap(keyspace, tokenMetadata.sortedTokens()); + return getRangeToAddressMap(keyspace, getTokenMetadataForKeyspace(keyspace).sortedTokens()); } public EndpointsByRange getRangeToAddressMapInLocalDC(String keyspace) @@ -2526,9 +2662,9 @@ public EndpointsByRange getRangeToAddressMapInLocalDC(String keyspace) private List getTokensInLocalDC() { List filteredTokens = Lists.newArrayList(); - for (Token token : tokenMetadata.sortedTokens()) + for (Token token : getTokenMetadata().sortedTokens()) { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(token); + InetAddressAndPort endpoint = getTokenMetadata().getEndpoint(token); if (isLocalDC(endpoint)) filteredTokens.add(token); } @@ -2646,7 +2782,7 @@ public Map getTokenToEndpointWithPortMap() private Map getTokenToEndpointMap(boolean withPort) { - Map mapInetAddress = tokenMetadata.getNormalAndBootstrappingTokenToEndpointMap(); + Map mapInetAddress = getTokenMetadata().getNormalAndBootstrappingTokenToEndpointMap(); // in order to preserve tokens in ascending order, we use LinkedHashMap here Map mapString = new LinkedHashMap<>(mapInetAddress.size()); List tokens = new ArrayList<>(mapInetAddress.keySet()); @@ -2828,43 +2964,9 @@ public void onChange(InetAddressAndPort endpoint, ApplicationState state, Versio { switch (state) { - case RELEASE_VERSION: - SystemKeyspace.updatePeerInfo(endpoint, "release_version", value.value); - break; case DC: - updateTopology(endpoint); - SystemKeyspace.updatePeerInfo(endpoint, "data_center", value.value); - break; case RACK: updateTopology(endpoint); - SystemKeyspace.updatePeerInfo(endpoint, "rack", value.value); - break; - case RPC_ADDRESS: - try - { - SystemKeyspace.updatePeerInfo(endpoint, "rpc_address", InetAddress.getByName(value.value)); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case NATIVE_ADDRESS_AND_PORT: - try - { - InetAddressAndPort address = InetAddressAndPort.getByName(value.value); - SystemKeyspace.updatePeerNativeAddress(endpoint, address); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case SCHEMA: - SystemKeyspace.updatePeerInfo(endpoint, "schema_version", UUID.fromString(value.value)); - break; - case HOST_ID: - SystemKeyspace.updatePeerInfo(endpoint, "host_id", UUID.fromString(value.value)); break; case RPC_READY: notifyRpcChange(endpoint, epState.isRpcReady()); @@ -2917,71 +3019,6 @@ public void updateTopology() getTokenMetadata().updateTopology(); } - private void updatePeerInfo(InetAddressAndPort endpoint) - { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - InetAddress native_address = null; - int native_port = DatabaseDescriptor.getNativeTransportPort(); - - for (Map.Entry entry : epState.states()) - { - switch (entry.getKey()) - { - case RELEASE_VERSION: - SystemKeyspace.updatePeerInfo(endpoint, "release_version", entry.getValue().value); - break; - case DC: - SystemKeyspace.updatePeerInfo(endpoint, "data_center", entry.getValue().value); - break; - case RACK: - SystemKeyspace.updatePeerInfo(endpoint, "rack", entry.getValue().value); - break; - case RPC_ADDRESS: - try - { - native_address = InetAddress.getByName(entry.getValue().value); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case NATIVE_ADDRESS_AND_PORT: - try - { - InetAddressAndPort address = InetAddressAndPort.getByName(entry.getValue().value); - native_address = address.getAddress(); - native_port = address.getPort(); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case SCHEMA: - SystemKeyspace.updatePeerInfo(endpoint, "schema_version", UUID.fromString(entry.getValue().value)); - break; - case HOST_ID: - SystemKeyspace.updatePeerInfo(endpoint, "host_id", UUID.fromString(entry.getValue().value)); - break; - case INDEX_STATUS: - // Need to set the peer index status in SIM here - // to ensure the status is correct before the node - // fully joins the ring - updateIndexStatus(endpoint, entry.getValue()); - break; - } - } - - //Some tests won't set all the states - if (native_address != null) - { - SystemKeyspace.updatePeerNativeAddress(endpoint, - InetAddressAndPort.getByAddressOverrideDefaults(native_address, - native_port)); - } - } - private void notifyRpcChange(InetAddressAndPort endpoint, boolean ready) { if (ready) @@ -3058,22 +3095,7 @@ public void setRpcReady(boolean value) public Collection getTokensFor(InetAddressAndPort endpoint) { - try - { - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null) - return Collections.emptyList(); - - VersionedValue versionedValue = state.getApplicationState(ApplicationState.TOKENS); - if (versionedValue == null) - return Collections.emptyList(); - - return TokenSerializer.deserialize(tokenMetadata.partitioner, new DataInputStream(new ByteArrayInputStream(versionedValue.toBytes()))); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + return Gossiper.instance.getTokensFor(endpoint, getTokenMetadata().partitioner); } /** @@ -3093,22 +3115,22 @@ private void handleStateBootstrap(InetAddressAndPort endpoint) // if this node is present in token metadata, either we have missed intermediate states // or the node had crashed. Print warning if needed, clear obsolete stuff and // continue. - if (tokenMetadata.isMember(endpoint)) + if (getTokenMetadata().isMember(endpoint)) { // If isLeaving is false, we have missed both LEAVING and LEFT. However, if // isLeaving is true, we have only missed LEFT. Waiting time between completing // leave operation and rebootstrapping is relatively short, so the latter is quite // common (not enough time for gossip to spread). Therefore we report only the // former in the log. - if (!tokenMetadata.isLeaving(endpoint)) + if (!getTokenMetadata().isLeaving(endpoint)) logger.info("Node {} state jump to bootstrap", endpoint); - tokenMetadata.removeEndpoint(endpoint); + getTokenMetadata().removeEndpoint(endpoint); } - tokenMetadata.addBootstrapTokens(tokens, endpoint); + getTokenMetadata().addBootstrapTokens(tokens, endpoint); PendingRangeCalculatorService.instance.update(); - tokenMetadata.updateHostId(Gossiper.instance.getHostId(endpoint), endpoint); + getTokenMetadata().updateHostId(Objects.requireNonNull(Nodes.localOrPeerInfo(endpoint)).getHostId(), endpoint); } private void handleStateBootreplacing(InetAddressAndPort newNode, String[] pieces) @@ -3124,12 +3146,12 @@ private void handleStateBootreplacing(InetAddressAndPort newNode, String[] piece return; } - if (FailureDetector.instance.isAlive(oldNode)) + if (IFailureDetector.instance.isAlive(oldNode)) { throw new RuntimeException(String.format("Node %s is trying to replace alive node %s.", newNode, oldNode)); } - Optional replacingNode = tokenMetadata.getReplacingNode(newNode); + Optional replacingNode = getTokenMetadata().getReplacingNode(newNode); if (replacingNode.isPresent() && !replacingNode.get().equals(oldNode)) { throw new RuntimeException(String.format("Node %s is already replacing %s but is trying to replace %s.", @@ -3141,10 +3163,10 @@ private void handleStateBootreplacing(InetAddressAndPort newNode, String[] piece if (logger.isDebugEnabled()) logger.debug("Node {} is replacing {}, tokens {}", newNode, oldNode, tokens); - tokenMetadata.addReplaceTokens(tokens, newNode, oldNode); + getTokenMetadata().addReplaceTokens(tokens, newNode, oldNode); PendingRangeCalculatorService.instance.update(); - tokenMetadata.updateHostId(Gossiper.instance.getHostId(newNode), newNode); + getTokenMetadata().updateHostId(Objects.requireNonNull(Nodes.localOrPeerInfo(newNode)).getHostId(), newNode); } private void ensureUpToDateTokenMetadata(String status, InetAddressAndPort endpoint) @@ -3157,12 +3179,12 @@ private void ensureUpToDateTokenMetadata(String status, InetAddressAndPort endpo // If the node is previously unknown or tokens do not match, update tokenmetadata to // have this node as 'normal' (it must have been using this token before the // leave). This way we'll get pending ranges right. - if (!tokenMetadata.isMember(endpoint)) + if (!getTokenMetadata().isMember(endpoint)) { logger.info("Node {} state jump to {}", endpoint, status); updateTokenMetadata(endpoint, tokens); } - else if (!tokens.equals(new TreeSet<>(tokenMetadata.getTokens(endpoint)))) + else if (!tokens.equals(new TreeSet<>(getTokenMetadata().getTokens(endpoint)))) { logger.warn("Node {} '{}' token mismatch. Long network partition?", endpoint, status); updateTokenMetadata(endpoint, tokens); @@ -3182,7 +3204,7 @@ private void updateTokenMetadata(InetAddressAndPort endpoint, Iterable to for (final Token token : tokens) { // we don't want to update if this node is responsible for the token and it has a later startup time than endpoint. - InetAddressAndPort currentOwner = tokenMetadata.getEndpoint(token); + InetAddressAndPort currentOwner = getTokenMetadata().getEndpoint(token); if (currentOwner == null) { logger.debug("New node {} at token {}", endpoint, token); @@ -3197,7 +3219,7 @@ else if (endpoint.equals(currentOwner)) } // Note: in test scenarios, there may not be any delta between the heartbeat generations of the old // and new nodes, so we first check whether the new endpoint is marked as a replacement for the old. - else if (endpoint.equals(tokenMetadata.getReplacementNode(currentOwner).orElse(null)) || Gossiper.instance.compareEndpointStartup(endpoint, currentOwner) > 0) + else if (endpoint.equals(getTokenMetadata().getReplacementNode(currentOwner).orElse(null)) || Gossiper.instance.compareEndpointStartup(endpoint, currentOwner) > 0) { tokensToUpdateInMetadata.add(token); tokensToUpdateInSystemKeyspace.add(token); @@ -3217,7 +3239,7 @@ else if (endpoint.equals(tokenMetadata.getReplacementNode(currentOwner).orElse(n } } - tokenMetadata.updateNormalTokens(tokensToUpdateInMetadata, endpoint); + getTokenMetadata().updateNormalTokens(tokensToUpdateInMetadata, endpoint); for (InetAddressAndPort ep : endpointsToRemove) { removeEndpoint(ep); @@ -3238,7 +3260,7 @@ public boolean isReplacingSameHostAddressAndHostId(UUID hostId) { return isReplacingSameAddress() && Gossiper.instance.getEndpointStateForEndpoint(DatabaseDescriptor.getReplaceAddress()) != null - && hostId.equals(Gossiper.instance.getHostId(DatabaseDescriptor.getReplaceAddress())); + && Objects.equals(hostId, Nodes.getHostId(DatabaseDescriptor.getReplaceAddress(), null)); } catch (RuntimeException ex) { @@ -3277,7 +3299,7 @@ private void handleStateNormal(final InetAddressAndPort endpoint, final String s if (logger.isDebugEnabled()) logger.debug("Node {} state {}, token {}", endpoint, status, tokens); - if (tokenMetadata.isMember(endpoint)) + if (getTokenMetadata().isMember(endpoint)) logger.info("Node {} state jump to {}", endpoint, status); if (tokens.isEmpty() && status.equals(VersionedValue.STATUS_NORMAL)) @@ -3285,12 +3307,12 @@ private void handleStateNormal(final InetAddressAndPort endpoint, final String s endpoint, Gossiper.instance.getEndpointStateForEndpoint(endpoint)); - Optional replacingNode = tokenMetadata.getReplacingNode(endpoint); + Optional replacingNode = getTokenMetadata().getReplacingNode(endpoint); if (replacingNode.isPresent()) { assert !endpoint.equals(replacingNode.get()) : "Pending replacement endpoint with same address is not supported"; logger.info("Node {} will complete replacement of {} for tokens {}", endpoint, replacingNode.get(), tokens); - if (FailureDetector.instance.isAlive(replacingNode.get())) + if (IFailureDetector.instance.isAlive(replacingNode.get())) { logger.error("Node {} cannot complete replacement of alive node {}.", endpoint, replacingNode.get()); return; @@ -3298,16 +3320,15 @@ private void handleStateNormal(final InetAddressAndPort endpoint, final String s endpointsToRemove.add(replacingNode.get()); } - Optional replacementNode = tokenMetadata.getReplacementNode(endpoint); + Optional replacementNode = getTokenMetadata().getReplacementNode(endpoint); if (replacementNode.isPresent()) { logger.warn("Node {} is currently being replaced by node {}.", endpoint, replacementNode.get()); } - updatePeerInfo(endpoint); // Order Matters, TM.updateHostID() should be called before TM.updateNormalToken(), (see CASSANDRA-4300). - UUID hostId = Gossiper.instance.getHostId(endpoint); - InetAddressAndPort existing = tokenMetadata.getEndpointForHostId(hostId); + UUID hostId = Nodes.getHostId(endpoint, null); + InetAddressAndPort existing = getTokenMetadata().getEndpointForHostId(hostId); if (replacing && isReplacingSameHostAddressAndHostId(hostId)) { logger.warn("Not updating token metadata for {} because I am replacing it", endpoint); @@ -3319,36 +3340,36 @@ private void handleStateNormal(final InetAddressAndPort endpoint, final String s if (existing.equals(FBUtilities.getBroadcastAddressAndPort())) { logger.warn("Not updating host ID {} for {} because it's mine", hostId, endpoint); - tokenMetadata.removeEndpoint(endpoint); + getTokenMetadata().removeEndpoint(endpoint); endpointsToRemove.add(endpoint); } else if (Gossiper.instance.compareEndpointStartup(endpoint, existing) > 0) { logger.warn("Host ID collision for {} between {} and {}; {} is the new owner", hostId, existing, endpoint, endpoint); - tokenMetadata.removeEndpoint(existing); + getTokenMetadata().removeEndpoint(existing); endpointsToRemove.add(existing); - tokenMetadata.updateHostId(hostId, endpoint); + getTokenMetadata().updateHostId(hostId, endpoint); } else { logger.warn("Host ID collision for {} between {} and {}; ignored {}", hostId, existing, endpoint, endpoint); - tokenMetadata.removeEndpoint(endpoint); + getTokenMetadata().removeEndpoint(endpoint); endpointsToRemove.add(endpoint); } } else - tokenMetadata.updateHostId(hostId, endpoint); + getTokenMetadata().updateHostId(hostId, endpoint); } // capture because updateNormalTokens clears moving and member status - boolean isMember = tokenMetadata.isMember(endpoint); - boolean isMoving = tokenMetadata.isMoving(endpoint); + boolean isMember = getTokenMetadata().isMember(endpoint); + boolean isMoving = getTokenMetadata().isMoving(endpoint); updateTokenMetadata(endpoint, tokens, endpointsToRemove); if (isMoving || operationMode == Mode.MOVING) { - tokenMetadata.removeFromMoving(endpoint); + getTokenMetadata().removeFromMoving(endpoint); // The above may change the local ownership. invalidateLocalRanges(); notifyMoved(endpoint); @@ -3376,7 +3397,7 @@ private void handleStateLeaving(InetAddressAndPort endpoint) // at this point the endpoint is certainly a member with this token, so let's proceed // normally - tokenMetadata.addLeavingEndpoint(endpoint); + getTokenMetadata().addLeavingEndpoint(endpoint); PendingRangeCalculatorService.instance.update(); } @@ -3413,7 +3434,7 @@ private void handleStateMoving(InetAddressAndPort endpoint, String[] pieces) if (logger.isDebugEnabled()) logger.debug("Node {} state moving, new token {}", endpoint, token); - tokenMetadata.addMovingEndpoint(token, endpoint); + getTokenMetadata().addMovingEndpoint(token, endpoint); PendingRangeCalculatorService.instance.update(); } @@ -3441,10 +3462,10 @@ private void handleStateRemoving(InetAddressAndPort endpoint, String[] pieces) } return; } - if (tokenMetadata.isMember(endpoint)) + if (getTokenMetadata().isMember(endpoint)) { String state = pieces[0]; - Collection removeTokens = tokenMetadata.getTokens(endpoint); + Collection removeTokens = getTokenMetadata().getTokens(endpoint); if (VersionedValue.REMOVED_TOKEN.equals(state)) { @@ -3458,14 +3479,14 @@ else if (VersionedValue.REMOVING_TOKEN.equals(state)) logger.debug("Tokens {} removed manually (endpoint was {})", removeTokens, endpoint); // Note that the endpoint is being removed - tokenMetadata.addLeavingEndpoint(endpoint); + getTokenMetadata().addLeavingEndpoint(endpoint); PendingRangeCalculatorService.instance.update(); // find the endpoint coordinating this removal that we need to notify when we're done String[] coordinator = splitValue(Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.REMOVAL_COORDINATOR)); UUID hostId = UUID.fromString(coordinator[1]); // grab any data we are now responsible for and notify responsible node - restoreReplicaCount(endpoint, tokenMetadata.getEndpointForHostId(hostId)); + restoreReplicaCount(endpoint, getTokenMetadata().getEndpointForHostId(hostId)); } } else // now that the gossiper has told us about this nonexistent member, notify the gossiper to remove it @@ -3480,8 +3501,8 @@ private void excise(Collection tokens, InetAddressAndPort endpoint) { logger.info("Removing tokens {} for {}", tokens, endpoint); - UUID hostId = tokenMetadata.getHostId(endpoint); - if (hostId != null && tokenMetadata.isMember(endpoint)) + UUID hostId = getTokenMetadata().getHostId(endpoint); + if (hostId != null && getTokenMetadata().isMember(endpoint)) { // enough time for writes to expire and MessagingService timeout reporter callback to fire, which is where // hints are mostly written from - using getMinRpcTimeout() / 2 for the interval. @@ -3490,9 +3511,9 @@ private void excise(Collection tokens, InetAddressAndPort endpoint) } removeEndpoint(endpoint); - tokenMetadata.removeEndpoint(endpoint); + getTokenMetadata().removeEndpoint(endpoint); if (!tokens.isEmpty()) - tokenMetadata.removeBootstrapTokens(tokens); + getTokenMetadata().removeBootstrapTokens(tokens); notifyLeft(endpoint); PendingRangeCalculatorService.instance.update(); } @@ -3507,7 +3528,7 @@ private void excise(Collection tokens, InetAddressAndPort endpoint, long private void removeEndpoint(InetAddressAndPort endpoint) { Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.removeEndpoint(endpoint)); - SystemKeyspace.removeEndpoint(endpoint); + Nodes.peers().remove(endpoint, true, false); } protected void addExpireTimeIfFound(InetAddressAndPort endpoint, long expireTime) @@ -3533,13 +3554,19 @@ protected long extractExpireTime(String[] pieces) private Multimap getNewSourceReplicas(String keyspaceName, Set leavingReplicas) { InetAddressAndPort myAddress = FBUtilities.getBroadcastAddressAndPort(); - EndpointsByRange rangeReplicas = Keyspace.open(keyspaceName).getReplicationStrategy().getRangeAddresses(tokenMetadata.cloneOnlyTokenMap()); - Multimap sourceRanges = HashMultimap.create(); - IFailureDetector failureDetector = FailureDetector.instance; + EndpointsByRange rangeReplicas = Keyspace.open(keyspaceName).getReplicationStrategy().getRangeAddresses(getTokenMetadata().cloneOnlyTokenMap()); logger.debug("Getting new source replicas for {}", leavingReplicas); + return findLiveReplicasForRanges(leavingReplicas, rangeReplicas, myAddress); + } + + // find alive sources for ranges + @VisibleForTesting + public Multimap findLiveReplicasForRanges(Set leavingReplicas, EndpointsByRange rangeReplicas, InetAddressAndPort myAddress) + { + Multimap sourceRanges = HashMultimap.create(); + IFailureDetector failureDetector = IFailureDetector.instance; - // find alive sources for our new ranges for (LeavingReplica leaver : leavingReplicas) { //We need this to find the replicas from before leaving to supply the data @@ -3591,7 +3618,7 @@ private void sendReplicationNotification(InetAddressAndPort remote) { // notify the remote token Message msg = Message.out(REPLICATION_DONE_REQ, noPayload); - IFailureDetector failureDetector = FailureDetector.instance; + IFailureDetector failureDetector = IFailureDetector.instance; if (logger.isDebugEnabled()) logger.debug("Notifying {} of replication completion\n", remote); while (failureDetector.isAlive(remote)) @@ -3609,7 +3636,8 @@ private void sendReplicationNotification(InetAddressAndPort remote) } } - private static class LeavingReplica + @VisibleForTesting + public static class LeavingReplica { //The node that is leaving private final Replica leavingReplica; @@ -3671,7 +3699,7 @@ private void restoreReplicaCount(InetAddressAndPort endpoint, final InetAddressA for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) { logger.debug("Restoring replica count for keyspace {}", keyspaceName); - EndpointsByReplica changedReplicas = getChangedReplicasForLeaving(keyspaceName, endpoint, tokenMetadata, Keyspace.open(keyspaceName).getReplicationStrategy()); + EndpointsByReplica changedReplicas = getChangedReplicasForLeaving(keyspaceName, endpoint, getTokenMetadata(), Keyspace.open(keyspaceName).getReplicationStrategy()); Set myNewReplicas = new HashSet<>(); for (Map.Entry entry : changedReplicas.flattenEntries()) { @@ -3804,7 +3832,6 @@ static EndpointsByReplica getChangedReplicasForLeaving(String keyspaceName, Inet return changedRanges.build(); } - public void onJoin(InetAddressAndPort endpoint, EndpointState epState) { // Explicitly process STATUS or STATUS_WITH_PORT before the other @@ -3832,13 +3859,13 @@ public void onJoin(InetAddressAndPort endpoint, EndpointState epState) public void onAlive(InetAddressAndPort endpoint, EndpointState state) { - if (tokenMetadata.isMember(endpoint)) + if (getTokenMetadata().isMember(endpoint)) notifyUp(endpoint); } public void onRemove(InetAddressAndPort endpoint) { - tokenMetadata.removeEndpoint(endpoint); + getTokenMetadata().removeEndpoint(endpoint); PendingRangeCalculatorService.instance.update(); } @@ -3913,13 +3940,13 @@ public Collection getLocalTokens() @Nullable public InetAddressAndPort getEndpointForHostId(UUID hostId) { - return tokenMetadata.getEndpointForHostId(hostId); + return getTokenMetadata().getEndpointForHostId(hostId); } @Nullable public UUID getHostIdForEndpoint(InetAddressAndPort address) { - return tokenMetadata.getHostId(address); + return getTokenMetadata().getHostId(address); } /* These methods belong to the MBean interface */ @@ -3972,12 +3999,12 @@ public String getKeyspaceReplicationInfo(String keyspaceName) @Deprecated(since = "4.0") public List getLeavingNodes() { - return stringify(tokenMetadata.getLeavingEndpoints(), false); + return stringify(getTokenMetadata().getLeavingEndpoints(), false); } public List getLeavingNodesWithPort() { - return stringify(tokenMetadata.getLeavingEndpoints(), true); + return stringify(getTokenMetadata().getLeavingEndpoints(), true); } /** @deprecated See CASSANDRA-7544 */ @@ -3986,7 +4013,7 @@ public List getMovingNodes() { List endpoints = new ArrayList<>(); - for (Pair node : tokenMetadata.getMovingEndpoints()) + for (Pair node : getTokenMetadata().getMovingEndpoints()) { endpoints.add(node.right.getAddress().getHostAddress()); } @@ -3998,7 +4025,7 @@ public List getMovingNodesWithPort() { List endpoints = new ArrayList<>(); - for (Pair node : tokenMetadata.getMovingEndpoints()) + for (Pair node : getTokenMetadata().getMovingEndpoints()) { endpoints.add(node.right.getHostAddressAndPort()); } @@ -4010,12 +4037,12 @@ public List getMovingNodesWithPort() @Deprecated(since = "4.0") public List getJoiningNodes() { - return stringify(tokenMetadata.getBootstrapTokens().valueSet(), false); + return stringify(getTokenMetadata().getBootstrapTokens().valueSet(), false); } public List getJoiningNodesWithPort() { - return stringify(tokenMetadata.getBootstrapTokens().valueSet(), true); + return stringify(getTokenMetadata().getBootstrapTokens().valueSet(), true); } /** @deprecated See CASSANDRA-7544 */ @@ -4037,18 +4064,24 @@ public Set getLiveRingMembers() public Set getLiveRingMembers(boolean excludeDeadStates) { - Set ret = new HashSet<>(); - for (InetAddressAndPort ep : Gossiper.instance.getLiveMembers()) + Set allRingMembers = getTokenMetadata().getAllRingMembers(); + Set ret = new HashSet<>(allRingMembers.size()); + for (InetAddressAndPort ep : getTokenMetadata().getAllRingMembers()) { - if (excludeDeadStates) + if (Gossiper.instance.isEnabled()) { EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(ep); - if (epState == null || Gossiper.instance.isDeadState(epState)) + if (epState == null) + continue; + + if (excludeDeadStates && Gossiper.instance.isDeadState(epState)) continue; } - if (tokenMetadata.isMember(ep)) - ret.add(ep); + if (!IFailureDetector.instance.isAlive(ep)) + continue; + + ret.add(ep); } return ret; } @@ -4072,7 +4105,7 @@ public String[] getAllDataFileLocations() return getCanonicalPaths(DatabaseDescriptor.getAllDataFileLocations()); } - private String[] getCanonicalPaths(String[] paths) + private String[] getCanonicalPaths(File[] paths) { String[] locations = new String[paths.length]; for (int i = 0; i < paths.length; i++) @@ -4102,16 +4135,6 @@ public String getSavedCachesLocation() return FileUtils.getCanonicalPath(DatabaseDescriptor.getSavedCachesLocation()); } - private List stringify(Iterable endpoints, boolean withPort) - { - List stringEndpoints = new ArrayList<>(); - for (InetAddressAndPort ep : endpoints) - { - stringEndpoints.add(ep.getHostAddress(withPort)); - } - return stringEndpoints; - } - public int getCurrentGenerationNumber() { return Gossiper.instance.getCurrentGenerationNumber(FBUtilities.getBroadcastAddressAndPort()); @@ -4127,7 +4150,7 @@ public int forceKeyspaceCleanup(int jobs, String keyspaceName, String... tableNa if (SchemaConstants.isLocalSystemKeyspace(keyspaceName)) throw new RuntimeException("Cleanup of the system keyspace is neither necessary nor wise"); - if (!tokenMetadata.getPendingRanges(keyspaceName, getBroadcastAddressAndPort()).isEmpty()) + if (getTokenMetadata().getPendingRanges(keyspaceName, getBroadcastAddressAndPort()).size() > 0) throw new RuntimeException("Node is involved in cluster membership changes. Not safe to run cleanup."); CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL; @@ -4173,16 +4196,24 @@ public int verify(boolean extendedVerify, String keyspaceName, String... tableNa return verify(extendedVerify, false, false, false, false, false, keyspaceName, tableNames); } + /** @deprecated See CNDB-10054 */ + @Deprecated(since = "CC4.0") public int verify(boolean extendedVerify, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException + { + return verify(extendedVerify, false, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspaceName, tableNames); + } + + public int verify(boolean extendedVerify, boolean validateAllRows, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException { CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL; IVerifier.Options options = IVerifier.options().invokeDiskFailurePolicy(diskFailurePolicy) .extendedVerification(extendedVerify) + .validateAllRows(validateAllRows) .checkVersion(checkVersion) .mutateRepairStatus(mutateRepairStatus) .checkOwnsTokens(checkOwnsTokens) .quick(quick).build(); - logger.info("Staring {} on {}.{} with options = {}", OperationType.VERIFY, keyspaceName, Arrays.toString(tableNames), options); + logger.info("Starting {} on {}.{} with options = {}", OperationType.VERIFY, keyspaceName, Arrays.toString(tableNames), options); for (ColumnFamilyStore cfStore : getValidColumnFamilies(false, false, keyspaceName, tableNames)) { CompactionManager.AllSSTableOpStatus oneStatus = cfStore.verify(options); @@ -4238,7 +4269,7 @@ public List> getPreparedStatements() { List> statements = new ArrayList<>(); for (Entry e : QueryProcessor.instance.getPreparedStatements().entrySet()) - statements.add(Pair.create(e.getKey().toString(), e.getValue().rawCQLStatement)); + statements.add(Pair.create(e.getKey().toString(), e.getValue().statement.getRawCQLStatement())); return statements; } @@ -4257,6 +4288,14 @@ public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, St } } + public void forceKeyspaceCompaction(boolean splitOutput, int parallelism, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException + { + for (ColumnFamilyStore cfStore : getValidColumnFamilies(true, false, keyspaceName, tableNames)) + { + cfStore.forceMajorCompaction(splitOutput, parallelism); + } + } + public int relocateSSTables(String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException { return relocateSSTables(0, keyspaceName, tableNames); @@ -4560,9 +4599,9 @@ public void clearSnapshot(Map options, String tag, String... key options = Collections.emptyMap(); Set keyspaces = new HashSet<>(); - for (String dataDir : DatabaseDescriptor.getAllDataFileLocations()) + for (File dataDir : DatabaseDescriptor.getAllDataFileLocations()) { - for (String keyspaceDir : new File(dataDir).tryListNames()) + for (String keyspaceDir : dataDir.tryListNames()) { // Only add a ks if it has been specified as a param, assuming params were actually provided. if (keyspaceNames.length > 0 && !Arrays.asList(keyspaceNames).contains(keyspaceDir)) @@ -4741,7 +4780,7 @@ public int repairAsync(String keyspace, Map repairSpec) public Pair> repair(String keyspace, Map repairSpec, List listeners) { - RepairOption option = RepairOption.parse(repairSpec, tokenMetadata.partitioner); + RepairOption option = RepairOption.parse(repairSpec, getTokenMetadata().partitioner); return repair(keyspace, option, listeners); } @@ -4766,8 +4805,11 @@ else if (option.isInLocalDCOnly()) Iterables.addAll(option.getRanges(), getLocalReplicas(keyspace).onlyFull().ranges()); } } - if (option.getRanges().isEmpty() || Keyspace.open(keyspace).getReplicationStrategy().getReplicationFactor().allReplicas < 2) + if (option.getRanges().isEmpty() || Keyspace.open(keyspace).getReplicationStrategy().getReplicationFactor().allReplicas < 2 + || getTokenMetadata().getAllEndpoints().size() < 2) + { return Pair.create(0, ImmediateFuture.success(null)); + } int cmd = nextRepairCommand.incrementAndGet(); return Pair.create(cmd, repairCommandExecutor().submit(createRepairTask(cmd, keyspace, option, listeners))); @@ -4789,7 +4831,7 @@ Collection> createRepairRangeFrom(String beginToken, String endToke // Break up given range to match ring layout in TokenMetadata ArrayList> repairingRange = new ArrayList<>(); - ArrayList tokens = new ArrayList<>(tokenMetadata.sortedTokens()); + ArrayList tokens = new ArrayList<>(getTokenMetadata().sortedTokens()); if (!tokens.contains(parsedBeginToken)) { tokens.add(parsedBeginToken); @@ -4813,7 +4855,7 @@ Collection> createRepairRangeFrom(String beginToken, String endToke public TokenFactory getTokenFactory() { - return tokenMetadata.partitioner.getTokenFactory(); + return getTokenMetadata().partitioner.getTokenFactory(); } private FutureTask createRepairTask(final int cmd, final String keyspace, final RepairOption options, List listeners) @@ -4822,7 +4864,7 @@ private FutureTask createRepairTask(final int cmd, final String keyspace { throw new IllegalArgumentException("the local data center must be part of the repair; requested " + options.getDataCenters() + " but DC is " + DatabaseDescriptor.getLocalDataCenter()); } - Set existingDatacenters = tokenMetadata.cloneOnlyTokenMap().getTopology().getDatacenterEndpoints().keys().elementSet(); + Set existingDatacenters = getTokenMetadata().cloneOnlyTokenMap().getTopology().getDatacenterEndpoints().keys().elementSet(); List datacenters = new ArrayList<>(options.getDataCenters()); if (!existingDatacenters.containsAll(datacenters)) { @@ -5000,7 +5042,7 @@ public Collection> getPrimaryRangesForEndpoint(String keyspace, Ine { AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); Collection> primaryRanges = new HashSet<>(); - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); + TokenMetadata metadata = strategy.getTokenMetadata().cloneOnlyTokenMap(); for (Token token : metadata.sortedTokens()) { EndpointsForRange replicas = strategy.calculateNaturalReplicas(token, metadata); @@ -5023,10 +5065,10 @@ public Collection> getPrimaryRangesForEndpoint(String keyspace, Ine */ public Collection> getPrimaryRangeForEndpointWithinDC(String keyspace, InetAddressAndPort referenceEndpoint) { - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); + AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); + TokenMetadata metadata = strategy.getTokenMetadata().cloneOnlyTokenMap(); String localDC = DatabaseDescriptor.getEndpointSnitch().getDatacenter(referenceEndpoint); Collection localDcNodes = metadata.getTopology().getDatacenterEndpoints().get(localDC); - AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); Collection> localDCPrimaryRanges = new HashSet<>(); for (Token token : metadata.sortedTokens()) @@ -5056,7 +5098,7 @@ public Collection> getLocalPrimaryRange() public Collection> getLocalPrimaryRangeForEndpoint(InetAddressAndPort referenceEndpoint) { IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - TokenMetadata tokenMetadata = this.tokenMetadata.cloneOnlyTokenMap(); + TokenMetadata tokenMetadata = this.getTokenMetadata().cloneOnlyTokenMap(); if (!tokenMetadata.isMember(referenceEndpoint)) return Collections.emptySet(); String dc = snitch.getDatacenter(referenceEndpoint); @@ -5149,13 +5191,13 @@ public EndpointsForToken getNaturalReplicasForToken(String keyspaceName, String public EndpointsForToken getNaturalReplicasForToken(String keyspaceName, ByteBuffer key) { - Token token = tokenMetadata.partitioner.getToken(key); + Token token = getTokenMetadata().partitioner.getToken(key); return Keyspace.open(keyspaceName).getReplicationStrategy().getNaturalReplicasForToken(token); } public DecoratedKey getKeyFromPartition(String keyspaceName, String table, String partitionKey) { - return tokenMetadata.partitioner.decorateKey(partitionKeyToBytes(keyspaceName, table, partitionKey)); + return getTokenMetadata().partitioner.decorateKey(partitionKeyToBytes(keyspaceName, table, partitionKey)); } private static ByteBuffer partitionKeyToBytes(String keyspaceName, String cf, String key) @@ -5174,7 +5216,7 @@ private static ByteBuffer partitionKeyToBytes(String keyspaceName, String cf, St @Override public String getToken(String keyspaceName, String table, String key) { - return tokenMetadata.partitioner.getToken(partitionKeyToBytes(keyspaceName, table, key)).toString(); + return getTokenMetadata().partitioner.getToken(partitionKeyToBytes(keyspaceName, table, key)).toString(); } public boolean isEndpointValidForWrite(String keyspace, Token token) @@ -5255,14 +5297,14 @@ private List keySamples(Iterable cfses, Range failedHooks = Collections.emptyList(); try { PendingRangeCalculatorService.instance.blockUntilFinished(); @@ -5308,7 +5351,7 @@ public void decommission(boolean force) throws InterruptedException if (operationMode != Mode.LEAVING) { int rf, numNodes; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) + for (String keyspaceName : Schema.instance.getPartitionedKeyspaces().names()) { if (!force) { @@ -5338,7 +5381,7 @@ public void decommission(boolean force) throws InterruptedException + " Perform a forceful decommission to ignore."); } // TODO: do we care about fixing transient/full self-movements here? probably - if (tokenMetadata.getPendingRanges(keyspaceName, FBUtilities.getBroadcastAddressAndPort()).size() > 0) + if (getTokenMetadata().getPendingRanges(keyspaceName, FBUtilities.getBroadcastAddressAndPort()).size() > 0) throw new UnsupportedOperationException("data is currently moving to this node; unable to leave the ring"); } } @@ -5350,6 +5393,14 @@ public void decommission(boolean force) throws InterruptedException unbootstrap(); + // Hooks run here, in the window unbootstrap() opens and the shutdown below closes: + // this node has left the ring (so no coordinator routes mutations to it) and the batch + // log has finished its final replay, but messaging, the native transport and the stages + // are all still up, so a hook can still run queries. Hooks may block for hours; nothing + // below this line runs until they are all done. A failing hook is collected rather than + // thrown -- see the report after the finally block. + failedHooks = runDecommissionHooks(); + // shutdown cql, gossip, messaging, Stage and set state to DECOMMISSIONED shutdownClientServers(); @@ -5390,12 +5441,125 @@ public void decommission(boolean force) throws InterruptedException { isDecommissioning.set(false); } + + // Reported only now that the node is DECOMMISSIONED, and deliberately not by failing the + // decommission: hooks run after unbootstrap(), so the data has already streamed away and + // leaveRing() has run. There is nothing to roll back to, and stopping at + // DECOMMISSION_FAILED here would strand the node -- a retry is rejected by the ring + // membership check above (leaveRing() removed us from TokenMetadata), and leaveRing() has + // persisted NEEDS_BOOTSTRAP, so a restart would bootstrap the node back into the ring. + // Finishing and then throwing still fails `nodetool decommission` loudly. + if (!failedHooks.isEmpty()) + throw new RuntimeException("Node decommissioned, but decommission hook(s) failed: " + + String.join(", ", failedHooks) + "; see the log for details"); + } + + /** + * Runs the registered {@link DecommissionHook}s in registration order, on the decommission + * thread, blocking for as long as they take. + * + * Never throws. That is the whole point: this runs past leaveRing(), the one window where + * NEEDS_BOOTSTRAP is already persisted but DECOMMISSIONED is not, so anything escaping here + * strands the node (see the caller). Every hook runs even if an earlier one fails, so one + * broken hook cannot silently skip the rest. + * + * @return the names of the hooks that did not complete, in order; empty if all succeeded. + */ + private List runDecommissionHooks() + { + // Snapshot: a hook is free to unregister itself (or another) while we are iterating. + List hooks = new ArrayList<>(decommissionHooks); + if (hooks.isEmpty()) + return Collections.emptyList(); + + logger.info("Running {} decommission hook(s)", hooks.size()); + List failed = new ArrayList<>(); + for (int i = 0; i < hooks.size(); i++) + { + DecommissionHook hook = hooks.get(i); + String name = hookName(hook); + setMode(Mode.LEAVING, "running decommission hook " + name, true); + long startedAt = nanoTime(); + try + { + hook.onDecommission(); + logger.info("Decommission hook {} completed in {} ms", name, elapsedMillis(startedAt)); + } + catch (InterruptedException e) + { + // Report and stop, without re-asserting the interrupt (throwing InterruptedException + // already cleared it). Re-asserting would fail every remaining hook the moment it + // blocked, and this runs on a pooled JMX handler thread, so the flag would leak onto + // whatever that thread ran next. + logger.error("Decommission hook {} was interrupted after {} ms; skipping the remaining hook(s)", + name, elapsedMillis(startedAt), e); + failed.add(name + " (interrupted)"); + for (int j = i + 1; j < hooks.size(); j++) + failed.add(hookName(hooks.get(j)) + " (not run)"); + break; + } + catch (Throwable t) + { + // Deliberately no JVMStabilityInspector.inspectThrowable(t): it rethrows an + // OutOfMemoryError found anywhere in the cause chain, and acts on FSError, either of + // which would escape and strand the node here. A hook is plugin code; its failure is + // reported, not escalated into a JVM-stability event. + logger.error("Decommission hook {} failed after {} ms", name, elapsedMillis(startedAt), t); + failed.add(name); + } + finally + { + // A hook can hand us back an interrupted thread whichever way it leaves: by catching + // InterruptedException and restoring the flag before returning (the standard idiom), + // or by restoring it and then throwing something else. Consume it here, on every + // path, so it can never reach the next hook -- which is entitled to a clear flag and + // would otherwise fail the moment it blocked -- nor the shutdown below, which waits + // on futures that fail instantly with the flag set. The decommission cannot be + // abandoned this late, so there is nothing the flag could usefully signal. + if (Thread.interrupted()) + logger.warn("Decommission hook {} left the interrupt flag set; clearing it", name); + } + } + + // Logged here as well as reported by the caller: if the shutdown sequence after this point + // throws, the caller never gets to report, and a hook failure must not be silent. + if (!failed.isEmpty()) + logger.error("{} of {} decommission hook(s) did not complete: {}", + failed.size(), hooks.size(), String.join(", ", failed)); + + // The flag must not outlive this method: the shutdown below, and the JMX thread we borrow, + // both misbehave with an interrupted thread. + if (Thread.interrupted()) + logger.warn("Decommission thread was left interrupted by the hooks; clearing the flag"); + + return failed; + } + + /** A hook is third-party code: even name() may misbehave, and must not abort the run. */ + private static String hookName(DecommissionHook hook) + { + if (hook == null) + return "null"; + try + { + String name = hook.name(); + return name == null ? hook.getClass().getName() : name; + } + catch (Throwable t) + { + return hook.getClass().getName(); + } + } + + private static long elapsedMillis(long startedAtNanos) + { + return TimeUnit.NANOSECONDS.toMillis(nanoTime() - startedAtNanos); } private void leaveRing() { SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.NEEDS_BOOTSTRAP); - tokenMetadata.removeEndpoint(FBUtilities.getBroadcastAddressAndPort()); + getTokenMetadata().removeEndpoint(FBUtilities.getBroadcastAddressAndPort()); PendingRangeCalculatorService.instance.update(); Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, valueFactory.left(getLocalTokens(),Gossiper.computeExpireTime())); @@ -5411,7 +5575,7 @@ public Supplier> prepareUnbootstrapStreaming() for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) { - EndpointsByReplica rangesMM = getChangedReplicasForLeaving(keyspaceName, FBUtilities.getBroadcastAddressAndPort(), tokenMetadata, Keyspace.open(keyspaceName).getReplicationStrategy()); + EndpointsByReplica rangesMM = getChangedReplicasForLeaving(keyspaceName, FBUtilities.getBroadcastAddressAndPort(), getTokenMetadata(), Keyspace.open(keyspaceName).getReplicationStrategy()); if (logger.isDebugEnabled()) logger.debug("Ranges needing transfer are [{}]", StringUtils.join(rangesMM.keySet(), ",")); @@ -5465,10 +5629,11 @@ private Future streamHints() return HintsService.instance.transferHints(this::getPreferredHintsStreamTarget); } - private static EndpointsForRange getStreamCandidates(Collection endpoints) + @VisibleForTesting + public static EndpointsForRange getStreamCandidates(Collection endpoints) { endpoints = endpoints.stream() - .filter(endpoint -> FailureDetector.instance.isAlive(endpoint) && !FBUtilities.getBroadcastAddressAndPort().equals(endpoint)) + .filter(endpoint -> IFailureDetector.instance.isAlive(endpoint) && !FBUtilities.getBroadcastAddressAndPort().equals(endpoint)) .collect(Collectors.toList()); return SystemReplicas.getSystemReplicas(endpoints); @@ -5491,7 +5656,7 @@ private UUID getPreferredHintsStreamTarget() // stream to the closest peer as chosen by the snitch candidates = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), candidates); InetAddressAndPort hintsDestinationHost = candidates.get(0).endpoint(); - return tokenMetadata.getHostId(hintsDestinationHost); + return getTokenMetadata().getHostId(hintsDestinationHost); } } @@ -5520,7 +5685,7 @@ private void move(Token newToken) throws IOException if (newToken == null) throw new IOException("Can't move to the undefined (null) token."); - if (tokenMetadata.sortedTokens().contains(newToken)) + if (getTokenMetadata().sortedTokens().contains(newToken)) throw new IOException("target token " + newToken + " is already owned by another node."); // address of the current node @@ -5540,7 +5705,7 @@ private void move(Token newToken) throws IOException for (String keyspaceName : keyspacesToProcess) { // TODO: do we care about fixing transient/full self-movements here? - if (tokenMetadata.getPendingRanges(keyspaceName, localAddress).size() > 0) + if (getTokenMetadata().getPendingRanges(keyspaceName, localAddress).size() > 0) throw new UnsupportedOperationException("data is currently moving to this node; unable to leave the ring"); } @@ -5551,7 +5716,7 @@ private void move(Token newToken) throws IOException setMode(Mode.MOVING, String.format("Sleeping %s ms before start streaming/fetching ranges", RING_DELAY_MILLIS), true); Uninterruptibles.sleepUninterruptibly(RING_DELAY_MILLIS, MILLISECONDS); - RangeRelocator relocator = new RangeRelocator(Collections.singleton(newToken), keyspacesToProcess, tokenMetadata); + RangeRelocator relocator = new RangeRelocator(Collections.singleton(newToken), keyspacesToProcess, getTokenMetadata()); relocator.calculateToFromStreams(); repairPaxosForTopologyChange("move"); @@ -5613,7 +5778,7 @@ private String getRemovalStatus(boolean withPort) } return String.format("Removing token (%s). Waiting for replication confirmation from [%s].", - tokenMetadata.getTokens(removingNode).iterator().next(), + getTokenMetadata().getTokens(removingNode).iterator().next(), StringUtils.join(toFormat, ",")); } @@ -5624,14 +5789,14 @@ private String getRemovalStatus(boolean withPort) */ public void forceRemoveCompletion() { - if (!replicatingNodes.isEmpty() || tokenMetadata.getSizeOfLeavingEndpoints() > 0) + if (!replicatingNodes.isEmpty() || getTokenMetadata().getSizeOfLeavingEndpoints() > 0) { logger.warn("Removal not confirmed for for {}", StringUtils.join(this.replicatingNodes, ",")); - for (InetAddressAndPort endpoint : tokenMetadata.getLeavingEndpoints()) + for (InetAddressAndPort endpoint : getTokenMetadata().getLeavingEndpoints()) { - UUID hostId = tokenMetadata.getHostId(endpoint); + UUID hostId = getTokenMetadata().getHostId(endpoint); Gossiper.instance.advertiseTokenRemoved(endpoint, hostId); - excise(tokenMetadata.getTokens(endpoint), endpoint); + excise(getTokenMetadata().getTokens(endpoint), endpoint); } replicatingNodes.clear(); removingNode = null; @@ -5654,14 +5819,14 @@ public void forceRemoveCompletion() public void removeNode(String hostIdString) { InetAddressAndPort myAddress = FBUtilities.getBroadcastAddressAndPort(); - UUID localHostId = tokenMetadata.getHostId(myAddress); + UUID localHostId = getTokenMetadata().getHostId(myAddress); UUID hostId = UUID.fromString(hostIdString); - InetAddressAndPort endpoint = tokenMetadata.getEndpointForHostId(hostId); + InetAddressAndPort endpoint = getTokenMetadata().getEndpointForHostId(hostId); if (endpoint == null) throw new UnsupportedOperationException("Host ID not found."); - if (!tokenMetadata.isMember(endpoint)) + if (!getTokenMetadata().isMember(endpoint)) throw new UnsupportedOperationException("Node to be removed is not a member of the token ring"); if (endpoint.equals(myAddress)) @@ -5671,13 +5836,13 @@ public void removeNode(String hostIdString) throw new UnsupportedOperationException("Node " + endpoint + " is alive and owns this ID. Use decommission command to remove it from the ring"); // A leaving endpoint that is dead is already being removed. - if (tokenMetadata.isLeaving(endpoint)) + if (getTokenMetadata().isLeaving(endpoint)) logger.warn("Node {} is already being removed, continuing removal anyway", endpoint); if (!replicatingNodes.isEmpty()) throw new UnsupportedOperationException("This node is already processing a removal. Wait for it to complete, or use 'removenode force' if this has failed."); - Collection tokens = tokenMetadata.getTokens(endpoint); + Collection tokens = getTokenMetadata().getTokens(endpoint); // Find the endpoints that are going to become responsible for data for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) @@ -5688,8 +5853,8 @@ public void removeNode(String hostIdString) // get all ranges that change ownership (that is, a node needs // to take responsibility for new range) - EndpointsByReplica changedRanges = getChangedReplicasForLeaving(keyspaceName, endpoint, tokenMetadata, Keyspace.open(keyspaceName).getReplicationStrategy()); - IFailureDetector failureDetector = FailureDetector.instance; + EndpointsByReplica changedRanges = getChangedReplicasForLeaving(keyspaceName, endpoint, getTokenMetadata(), Keyspace.open(keyspaceName).getReplicationStrategy()); + IFailureDetector failureDetector = IFailureDetector.instance; for (InetAddressAndPort ep : transform(changedRanges.flattenValues(), Replica::endpoint)) { if (failureDetector.isAlive(ep)) @@ -5700,7 +5865,7 @@ public void removeNode(String hostIdString) } removingNode = endpoint; - tokenMetadata.addLeavingEndpoint(endpoint); + getTokenMetadata().addLeavingEndpoint(endpoint); PendingRangeCalculatorService.instance.update(); // the gossiper will handle spoofing this node's state to REMOVING_TOKEN for us @@ -5820,13 +5985,16 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I assert !isShutdown; isShutdown = true; + logger.info("Running StorageService shutdown hook"); + Throwable preShutdownHookThrowable = Throwables.perform(null, preShutdownHooks.stream().map(h -> h::run)); if (preShutdownHookThrowable != null) logger.error("Attempting to continue draining after pre-shutdown hooks returned exception", preShutdownHookThrowable); try { - setMode(Mode.DRAINING, "starting drain process", !isFinalShutdown); + logger.info("DRAINING: starting drain process"); + setMode(Mode.DRAINING, "starting drain process", false); try { @@ -5847,6 +6015,7 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I Gossiper.instance.stop(); ActiveRepairService.instance().stop(); + logger.info("DRAINING: shutting down MessageService"); if (!isFinalShutdown) setMode(Mode.DRAINING, "shutting down MessageService", false); @@ -5864,13 +6033,7 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I logger.error("Messaging service timed out shutting down", t); } - if (!isFinalShutdown) - setMode(Mode.DRAINING, "clearing mutation stage", false); - Stage.shutdownAndAwaitMutatingExecutors(false, - DRAIN_EXECUTOR_TIMEOUT_MS.getInt(), TimeUnit.MILLISECONDS); - - StorageProxy.instance.verifyNoHintsInProgress(); - + logger.info("DRAINING: flushing column families"); if (!isFinalShutdown) setMode(Mode.DRAINING, "flushing column families", false); @@ -5923,7 +6086,30 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I } FBUtilities.waitOnFutures(flushes); + // Now that client requests, messaging service and compactions are shutdown, there shouldn't be any more + // mutations so let's wait for any pending mutations and then clear the stages. Note that the compaction + // manager can generated mutations, for example because of the view builder or the sstable_activity updates + // in the SSTableReader.GlobalTidy, so we do this step quite late, but before shutting down the CL + logger.info("DRAINING: stopping mutations"); + if (!isFinalShutdown) + setMode(Mode.DRAINING, "stopping mutations", false); + + List barriers = StreamSupport.stream(Keyspace.all().spliterator(), false) + .map(Keyspace::stopMutations) + .collect(Collectors.toList()); + barriers.forEach(OpOrder.Barrier::await); // we could parallelize this... + + logger.info("DRAINING: clearing mutation stage"); + if (!isFinalShutdown) + setMode(Mode.DRAINING, "clearing mutation stage", false); + Stage.shutdownAndAwaitMutatingExecutors(false, + DRAIN_EXECUTOR_TIMEOUT_MS.getInt(), TimeUnit.MILLISECONDS); + + StorageProxy.instance.verifyNoHintsInProgress(); + + SnapshotManager.shutdownAndWait(1L, MINUTES); + HintsService.instance.shutdownBlocking(); // Interrupt ongoing compactions and shutdown CM to prevent further compactions. @@ -5935,6 +6121,8 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I CommitLog.instance.shutdownBlocking(); + AutoRepair.instance.shutdownBlocking(); + // wait for miscellaneous tasks like sstable and commitlog segment deletion ColumnFamilyStore.shutdownPostFlushExecutor(); @@ -5949,7 +6137,8 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I } finally { - setMode(Mode.DRAINED, !isFinalShutdown); + logger.info("DRAINED"); + setMode(Mode.DRAINED, false); } } catch (Throwable t) @@ -6042,15 +6231,16 @@ synchronized void checkServiceAllowedToStart(String service) public IPartitioner setPartitionerUnsafe(IPartitioner newPartitioner) { IPartitioner oldPartitioner = DatabaseDescriptor.setPartitionerUnsafe(newPartitioner); - tokenMetadata = tokenMetadata.cloneWithNewPartitioner(newPartitioner); + setTokenMetadataUnsafe(StorageService.instance.getTokenMetadata().cloneWithNewPartitioner(newPartitioner)); valueFactory = new VersionedValue.VersionedValueFactory(newPartitioner); return oldPartitioner; } + @VisibleForTesting TokenMetadata setTokenMetadataUnsafe(TokenMetadata tmd) { - TokenMetadata old = tokenMetadata; - tokenMetadata = tmd; + TokenMetadata old = getTokenMetadata(); + TokenMetadataProvider.instance.replaceTokenMetadata(tmd); return old; } @@ -6060,7 +6250,7 @@ public void truncate(String keyspace, String table) throws TimeoutException, IOE try { - StorageProxy.truncateBlocking(keyspace, table); + StorageProxy.instance.truncateBlocking(keyspace, table); } catch (UnavailableException e) { @@ -6070,13 +6260,13 @@ public void truncate(String keyspace, String table) throws TimeoutException, IOE public Map getOwnership() { - List sortedTokens = tokenMetadata.sortedTokens(); + List sortedTokens = getTokenMetadata().sortedTokens(); // describeOwnership returns tokens in an unspecified order, let's re-order them - Map tokenMap = new TreeMap(tokenMetadata.partitioner.describeOwnership(sortedTokens)); + Map tokenMap = new TreeMap(getTokenMetadata().partitioner.describeOwnership(sortedTokens)); Map nodeMap = new LinkedHashMap<>(); for (Map.Entry entry : tokenMap.entrySet()) { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(entry.getKey()); + InetAddressAndPort endpoint = getTokenMetadata().getEndpoint(entry.getKey()); Float tokenOwnership = entry.getValue(); if (nodeMap.containsKey(endpoint.getAddress())) nodeMap.put(endpoint.getAddress(), nodeMap.get(endpoint.getAddress()) + tokenOwnership); @@ -6088,13 +6278,13 @@ public Map getOwnership() public Map getOwnershipWithPort() { - List sortedTokens = tokenMetadata.sortedTokens(); + List sortedTokens = getTokenMetadata().sortedTokens(); // describeOwnership returns tokens in an unspecified order, let's re-order them - Map tokenMap = new TreeMap(tokenMetadata.partitioner.describeOwnership(sortedTokens)); + Map tokenMap = new TreeMap(getTokenMetadata().partitioner.describeOwnership(sortedTokens)); Map nodeMap = new LinkedHashMap<>(); for (Map.Entry entry : tokenMap.entrySet()) { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(entry.getKey()); + InetAddressAndPort endpoint = getTokenMetadata().getEndpoint(entry.getKey()); Float tokenOwnership = entry.getValue(); if (nodeMap.containsKey(endpoint.toString())) nodeMap.put(endpoint.toString(), nodeMap.get(endpoint.toString()) + tokenOwnership); @@ -6151,7 +6341,7 @@ private LinkedHashMap getEffectiveOwnership(String ke strategy = keyspaceInstance.getReplicationStrategy(); } - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); + TokenMetadata metadata = getTokenMetadata().cloneOnlyTokenMap(); Collection> endpointsGroupedByDc = new ArrayList<>(); // mapping of dc's to nodes, use sorted map so that we get dcs sorted @@ -6159,7 +6349,7 @@ private LinkedHashMap getEffectiveOwnership(String ke for (Collection endpoints : sortedDcsToEndpoints.values()) endpointsGroupedByDc.add(endpoints); - Map tokenOwnership = tokenMetadata.partitioner.describeOwnership(tokenMetadata.sortedTokens()); + Map tokenOwnership = getTokenMetadata().partitioner.describeOwnership(getTokenMetadata().sortedTokens()); LinkedHashMap finalOwnership = Maps.newLinkedHashMap(); RangesByEndpoint endpointToRanges = strategy.getAddressReplicas(); @@ -6215,7 +6405,7 @@ public List getNonLocalStrategyKeyspaces() public Map getViewBuildStatuses(String keyspace, String view, boolean withPort) { Map coreViewStatus = SystemDistributedKeyspace.viewStatus(keyspace, view); - Map hostIdToEndpoint = tokenMetadata.getEndpointToHostIdMapForReading(); + Map hostIdToEndpoint = getTokenMetadata().getEndpointToHostIdMapForReading(); Map result = new HashMap<>(); for (Map.Entry entry : hostIdToEndpoint.entrySet()) @@ -6657,11 +6847,15 @@ public int getSSTablePreemptiveOpenIntervalInMB() return DatabaseDescriptor.getSSTablePreemptiveOpenIntervalInMiB(); } + /** @deprecated CPU-intensive optimization that visibly slows down compaction but does not provide a clear benefit (see STAR-782) */ + @Deprecated(since = "CC 4.0") public boolean getMigrateKeycacheOnCompaction() { return DatabaseDescriptor.shouldMigrateKeycacheOnCompaction(); } + /** @deprecated CPU-intensive optimization that visibly slows down compaction but does not provide a clear benefit (see STAR-782) */ + @Deprecated(since = "CC 4.0") public void setMigrateKeycacheOnCompaction(boolean invalidateKeyCacheOnCompaction) { DatabaseDescriptor.setMigrateKeycacheOnCompaction(invalidateKeyCacheOnCompaction); @@ -6679,23 +6873,25 @@ public void setInvalidateKeycacheOnSSTableDeletion(boolean invalidate) public int getTombstoneWarnThreshold() { - return DatabaseDescriptor.getTombstoneWarnThreshold(); + return GuardrailsConfigProvider.instance.getOrCreate(null).getTombstoneWarnThreshold(); } public void setTombstoneWarnThreshold(int threshold) { - DatabaseDescriptor.setTombstoneWarnThreshold(threshold); + GuardrailsConfig guardrailsConfig = GuardrailsConfigProvider.instance.getOrCreate(null); + guardrailsConfig.setTombstonesThreshold(threshold, guardrailsConfig.getTombstoneFailThreshold()); logger.info("updated tombstone_warn_threshold to {}", threshold); } public int getTombstoneFailureThreshold() { - return DatabaseDescriptor.getTombstoneFailureThreshold(); + return GuardrailsConfigProvider.instance.getOrCreate(null).getTombstoneFailThreshold(); } public void setTombstoneFailureThreshold(int threshold) { - DatabaseDescriptor.setTombstoneFailureThreshold(threshold); + GuardrailsConfig guardrailsConfig = GuardrailsConfigProvider.instance.getOrCreate(null); + guardrailsConfig.setTombstonesThreshold(guardrailsConfig.getTombstoneWarnThreshold(), threshold); logger.info("updated tombstone_failure_threshold to {}", threshold); } @@ -6765,7 +6961,7 @@ public int getColumnIndexCacheSize() @Override public void setColumnIndexCacheSize(int cacheSizeInKB) { - DatabaseDescriptor.setColumnIndexCacheSize(cacheSizeInKB); + DatabaseDescriptor.setColumnIndexCacheSizeInKiB(cacheSizeInKB); logger.info("Updated column_index_cache_size to {}", cacheSizeInKB); } @@ -6785,7 +6981,7 @@ public void setColumnIndexCacheSizeInKiB(int cacheSizeInKiB) { try { - DatabaseDescriptor.setColumnIndexCacheSize(cacheSizeInKiB); + DatabaseDescriptor.setColumnIndexCacheSizeInKiB(cacheSizeInKiB); } catch (ConfigurationException e) { @@ -7087,15 +7283,6 @@ public void setOutOfTokenRangeRequestRejectionEnabled(boolean enabled) DatabaseDescriptor.setRejectOutOfTokenRangeRequests(enabled); } - @VisibleForTesting - public void shutdownServer() - { - if (drainOnShutdown != null) - { - Runtime.getRuntime().removeShutdownHook(drainOnShutdown); - } - } - @Override public void enableFullQueryLogger(String path, String rollCycle, Boolean blocking, int maxQueueWeight, long maxLogSize, String archiveCommand, int maxArchiveRetries) { @@ -7184,42 +7371,6 @@ public void setAutoOptimisePreviewRepairStreams(boolean enabled) DatabaseDescriptor.setAutoOptimisePreviewRepairStreams(enabled); } - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - public int getTableCountWarnThreshold() - { - return (int) Converters.TABLE_COUNT_THRESHOLD_TO_GUARDRAIL.unconvert(Guardrails.instance.getTablesWarnThreshold()); - } - - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - public void setTableCountWarnThreshold(int value) - { - if (value < 0) - throw new IllegalStateException("Table count warn threshold should be positive, not "+value); - logger.info("Changing table count warn threshold from {} to {}", getTableCountWarnThreshold(), value); - Guardrails.instance.setTablesThreshold((int) Converters.TABLE_COUNT_THRESHOLD_TO_GUARDRAIL.convert(value), - Guardrails.instance.getTablesFailThreshold()); - } - - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - public int getKeyspaceCountWarnThreshold() - { - return (int) Converters.KEYSPACE_COUNT_THRESHOLD_TO_GUARDRAIL.unconvert(Guardrails.instance.getKeyspacesWarnThreshold()); - } - - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - public void setKeyspaceCountWarnThreshold(int value) - { - if (value < 0) - throw new IllegalStateException("Keyspace count warn threshold should be positive, not "+value); - logger.info("Changing keyspace count warn threshold from {} to {}", getKeyspaceCountWarnThreshold(), value); - Guardrails.instance.setKeyspacesThreshold((int) Converters.KEYSPACE_COUNT_THRESHOLD_TO_GUARDRAIL.convert(value), - Guardrails.instance.getKeyspacesFailThreshold()); - } - @Override public void setCompactionTombstoneWarningThreshold(int count) { @@ -7698,4 +7849,50 @@ public boolean getPaxosRepairRaceWait() { return DatabaseDescriptor.getPaxosRepairRaceWait(); } + + @Override + public List getTablesForKeyspace(String keyspace) + { + return Keyspace.open(keyspace).getColumnFamilyStores().stream().map(cfs -> cfs.name).collect(Collectors.toList()); + } + + @Override + public List mutateSSTableRepairedState(boolean repaired, boolean preview, String keyspace, List tableNames) + { + Map tables = Keyspace.open(keyspace).getColumnFamilyStores() + .stream().collect(Collectors.toMap(c -> c.name, c -> c)); + for (String tableName : tableNames) + { + if (!tables.containsKey(tableName)) + throw new RuntimeException("Table " + tableName + " does not exist in keyspace " + keyspace); + } + + // only select SSTables that are unrepaired when repaired is true and vice versa + Predicate predicate = sst -> repaired != sst.isRepaired(); + + // mutate SSTables + long repairedAt = !repaired ? 0 : currentTimeMillis(); + List sstablesTouched = new ArrayList<>(); + for (String tableName : tableNames) + { + ColumnFamilyStore table = tables.get(tableName); + Set result = table.runWithCompactionsDisabled(() -> { + Set sstables = table.getLiveSSTables().stream().filter(predicate).collect(Collectors.toSet()); + if (!preview) + { + try + { + table.mutateRepaired(sstables, repairedAt, null, false); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + return sstables; + }, predicate, OperationType.ANTICOMPACTION, true, false, true, TableOperation.StopTrigger.NONE); + sstablesTouched.addAll(result.stream().map(sst -> sst.descriptor.baseFile().name()).collect(Collectors.toList())); + } + return sstablesTouched; + } } diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/java/org/apache/cassandra/service/StorageServiceMBean.java index 875df369421b..0f1f1da1631a 100644 --- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java +++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java @@ -364,6 +364,11 @@ public interface StorageServiceMBean extends NotificationEmitter */ public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException; + /** + * Forces major compaction of a single keyspace with the given parallelism limit + */ + public void forceKeyspaceCompaction(boolean splitOutput, int parallelism, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException; + /** @deprecated See CASSANDRA-11179 */ @Deprecated(since = "3.5") public int relocateSSTables(String keyspace, String ... cfnames) throws IOException, ExecutionException, InterruptedException; @@ -426,9 +431,14 @@ default int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkD * If tableNames array is empty, all CFs are verified. * * The entire sstable will be read to ensure each cell validates if extendedVerify is true + * @deprecated See CNDB-10054 */ + @Deprecated(since = "CC4.0") public int verify(boolean extendedVerify, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException; + /** @deprecated See CNDB-10054 */ + @Deprecated(since = "CC4.0") public int verify(boolean extendedVerify, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException; + public int verify(boolean extendedVerify, boolean validateAllRows, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException; /** * Rewrite all sstables to the latest version. @@ -810,6 +820,7 @@ default int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, @Deprecated(since = "4.1") public int getCompactionThroughputMbPerSec(); public void setCompactionThroughputMbPerSec(int value); + Map getCurrentCompactionThroughputMebibytesPerSec(); public int getCompressedReadAheadBufferInKB(); public void setCompressedReadAheadBufferInKB(int sizeInKb); @@ -833,7 +844,11 @@ default int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, public int getSSTablePreemptiveOpenIntervalInMB(); public void setSSTablePreemptiveOpenIntervalInMB(int intervalInMB); + /** @deprecated CPU-intensive optimization that visibly slows down compaction but does not provide a clear benefit (see STAR-782) */ + @Deprecated(since = "CC 4.0") public boolean getMigrateKeycacheOnCompaction(); + /** @deprecated CPU-intensive optimization that visibly slows down compaction but does not provide a clear benefit (see STAR-782) */ + @Deprecated(since = "CC 4.0") public void setMigrateKeycacheOnCompaction(boolean invalidateKeyCacheOnCompaction); public boolean getInvalidateKeycacheOnSSTableDeletion(); public void setInvalidateKeycacheOnSSTableDeletion(boolean invalidate); @@ -1191,19 +1206,6 @@ public void enableAuditLog(String loggerName, String includedKeyspaces, String e public void setAutoOptimisePreviewRepairStreams(boolean enabled); // warning thresholds will be replaced by equivalent guardrails - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - int getTableCountWarnThreshold(); - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - void setTableCountWarnThreshold(int value); - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - int getKeyspaceCountWarnThreshold(); - /** @deprecated See CASSANDRA-17195 */ - @Deprecated(since = "4.1") - void setKeyspaceCountWarnThreshold(int value); - /** @deprecated See CASSANDRA-17194 */ @Deprecated(since = "5.0") void setCompactionTombstoneWarningThreshold(int count); @@ -1330,4 +1332,10 @@ public void enableAuditLog(String loggerName, String includedKeyspaces, String e boolean getPaxosRepairRaceWait(); public void dropPreparedStatements(boolean memoryOnly); + + /** Gets the names of all tables for the given keyspace */ + public List getTablesForKeyspace(String keyspace); + + /** Mutates the repaired state of all SSTables for the given SSTables */ + public List mutateSSTableRepairedState(boolean repaired, boolean preview, String keyspace, List tables); } diff --git a/src/java/org/apache/cassandra/service/TokenRange.java b/src/java/org/apache/cassandra/service/TokenRange.java index 37971f5e4f6c..416ce0cdad10 100644 --- a/src/java/org/apache/cassandra/service/TokenRange.java +++ b/src/java/org/apache/cassandra/service/TokenRange.java @@ -60,7 +60,7 @@ public static TokenRange create(Token.TokenFactory tokenFactory, Range ra IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); for (InetAddressAndPort ep : endpoints) details.add(new EndpointDetails(ep, - StorageService.instance.getNativeaddress(ep, withPorts), + StorageService.instance.getNativeAddress(ep, withPorts), snitch.getDatacenter(ep), snitch.getRack(ep))); return new TokenRange(tokenFactory, range, details); diff --git a/src/java/org/apache/cassandra/service/TracingClientState.java b/src/java/org/apache/cassandra/service/TracingClientState.java new file mode 100644 index 000000000000..8039c6cbe477 --- /dev/null +++ b/src/java/org/apache/cassandra/service/TracingClientState.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.service; + +import javax.annotation.Nullable; + +/** + * As tracing can happen at both coordinator and replicas, at replica side, CNDB needs to know the traced keyspace for billing + */ +public class TracingClientState extends ClientState +{ + private final @Nullable String tracedKeyspace; + + protected TracingClientState(String tracedKeyspace, ClientState state) + { + super(state); + this.tracedKeyspace = tracedKeyspace; + } + + @Override + public ClientState cloneWithKeyspaceIfSet(String keyspace) + { + if (keyspace == null) + return this; + return new TracingClientState(tracedKeyspace, super.cloneWithKeyspaceIfSet(keyspace)); + } + + /** + * @return the keyspace being traced + */ + @Nullable + public String tracedKeyspace() + { + return tracedKeyspace; + } + + /** + * @return a ClientState object for internal C* calls (not limited by any kind of auth) with traced keyspace + */ + public static TracingClientState withTracedKeyspace(@Nullable String tracedKeyspace) + { + return new TracingClientState(tracedKeyspace, ClientState.forInternalCalls()); + } +} diff --git a/src/java/org/apache/cassandra/service/TruncateResponseHandler.java b/src/java/org/apache/cassandra/service/TruncateResponseHandler.java index 54b1241006d7..630330394d00 100644 --- a/src/java/org/apache/cassandra/service/TruncateResponseHandler.java +++ b/src/java/org/apache/cassandra/service/TruncateResponseHandler.java @@ -60,7 +60,7 @@ public TruncateResponseHandler(int responseCount) start = nanoTime(); } - public void get() throws TimeoutException + public void get() throws TimeoutException, TruncateException { long timeoutNanos = getTruncateRpcTimeout(NANOSECONDS) - (nanoTime() - start); boolean signaled; diff --git a/src/java/org/apache/cassandra/service/WriteResponseHandler.java b/src/java/org/apache/cassandra/service/WriteResponseHandler.java index ec18238f9932..20f61c4fc4a3 100644 --- a/src/java/org/apache/cassandra/service/WriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/WriteResponseHandler.java @@ -20,14 +20,16 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.Supplier; -import org.apache.cassandra.db.Mutation; -import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.net.Message; +import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.WriteType; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.Verb; /** * Handles blocking writes for ONE, ANY, TWO, THREE, QUORUM, and ALL consistency levels. @@ -36,6 +38,8 @@ public class WriteResponseHandler extends AbstractWriteResponseHandler { protected static final Logger logger = LoggerFactory.getLogger(WriteResponseHandler.class); + private static final boolean useDynamicSnitchForCounterLeader = CassandraRelevantProperties.USE_DYNAMIC_SNITCH_FOR_COUNTER_LEADER.getBoolean(); + protected volatile int responses; private static final AtomicIntegerFieldUpdater responsesUpdater = AtomicIntegerFieldUpdater.newUpdater(WriteResponseHandler.class, "responses"); @@ -55,8 +59,16 @@ public WriteResponseHandler(ReplicaPlan.ForWrite replicaPlan, WriteType writeTyp this(replicaPlan, null, writeType, hintOnFailure, requestTime); } + @Override + public boolean trackLatencyForSnitch(Verb responseVerb, boolean isTimeout) + { + return useDynamicSnitchForCounterLeader && responseVerb == Verb.COUNTER_MUTATION_RSP && isTimeout; + } + + @Override public void onResponse(Message m) { + trackReplicaResponseSize(m); if (responsesUpdater.decrementAndGet(this) == 0) signal(); //Must be last after all subclass processing @@ -65,7 +77,7 @@ public void onResponse(Message m) logResponseToIdealCLDelegate(m); } - protected int ackCount() + public int ackCount() { return blockFor() - responses; } diff --git a/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java b/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java new file mode 100644 index 000000000000..71be34600df6 --- /dev/null +++ b/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.context; + +import java.util.function.Supplier; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.marshal.Redaction; + +/** + * Default implementation of {@link OperationContext}. + *

    + * This default implementation is mostly only useful for debugging as the only concrete method is provices is a + * {@link #toString()} method giving details on the operation the context corresponds to (though the context object + * also identify the operation, so it could also theoretically be used from 2 separate place in the code to decide + * if they execute as part of the same operation). + */ +public class DefaultOperationContext implements OperationContext +{ + private final Supplier toDebugString; + + private DefaultOperationContext(Supplier toDebugString) + { + this.toDebugString = toDebugString; + } + + @Override + public void close() + { + } + + @Override + public String toString() + { + return String.format("[%d] %s", System.identityHashCode(this), toDebugString.get()); + } + + /** + * Simple default implementation of {@link OperationContext.Factory} that creates {@link DefaultOperationContext}. + */ + static class Factory implements OperationContext.Factory + { + @Override + public OperationContext forRead(ReadCommand command, ColumnFamilyStore cfs) + { + return new DefaultOperationContext(() -> command.toCQLString(Redaction.NONE)); + } + } +} diff --git a/src/java/org/apache/cassandra/service/context/OperationContext.java b/src/java/org/apache/cassandra/service/context/OperationContext.java new file mode 100644 index 000000000000..926143990cc6 --- /dev/null +++ b/src/java/org/apache/cassandra/service/context/OperationContext.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.context; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.OPERATION_CONTEXT_FACTORY; + +/** + * Represents some context about a "top-level" operation. + *

    + * This interface is fairly open on purpose, as implementations for different operations could look fairly different. + * But it is also open-ended as it is an extension point: the {@link #FACTORY} used to create the context instances + * is configurable, and meant to allow extensions to add whatever information they need to the context. + *

    + * Also note that what consistute a "top-level" operation is not strictly defined. At the time of this writing, those + * context are not serialized across nodes, so "top-level" is understood as "for a node", and so correspond to + * operations like "a `ReadCommand` execution on a replica". + *

    + * The context of executing operation is tracked by {@link OperationContextTracker} which use the {@link ExecutorLocal} + * concept to make that context available to any methods that execute as part of the operation. Basically, this is a way + * to make the context available everwhere along the path of execution of the operation, without needing to pass that + * context as argument of every single method that could be involved by the operation execution (which in most cases + * would be a lot of methods). +*/ +public interface OperationContext extends AutoCloseable +{ + Factory FACTORY = OPERATION_CONTEXT_FACTORY.getString() == null + ? new DefaultOperationContext.Factory() + : FBUtilities.construct(OPERATION_CONTEXT_FACTORY.getString(), "operation context factory"); + + + /** + * Called when the operation this is a context of terminates, and thus when the context will not be used/retrieved + * anymore. + */ + @Override + void close(); + + /** + * Factory used to create {@link OperationContext} instances. + *

    + * The intent is that every operation that wants to set a context should have its own method in this interface, but + * operations are added as needed (instead of trying to cover every possible operation upfront). + *

    + * Do note however that there can only be one operation context "active" at any given time (meaning, any thread + * execute can only see at most one context), so the context should be set at the higher level that make sense + * (and if necessary, sub-operations can enrich the context of their parent, assuming the parent context make room + * for this). + */ + interface Factory + { + OperationContext forRead(ReadCommand command, ColumnFamilyStore cfs); + } +} diff --git a/src/java/org/apache/cassandra/service/context/OperationContextTracker.java b/src/java/org/apache/cassandra/service/context/OperationContextTracker.java new file mode 100644 index 000000000000..d88145dc771c --- /dev/null +++ b/src/java/org/apache/cassandra/service/context/OperationContextTracker.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.context; + +import org.apache.cassandra.concurrent.ExecutorLocals; + +public class OperationContextTracker extends ExecutorLocals.Impl +{ + public static final OperationContextTracker instance = new OperationContextTracker(); + + private OperationContextTracker() + {} + + public OperationContext get() + { + return ExecutorLocals.current().operationContext; + } + + public void set(OperationContext operationContext) + { + ExecutorLocals current = ExecutorLocals.current(); + ExecutorLocals.Impl.set(current.traceState, current.clientWarnState, current.sensors, operationContext); + } + + public static void start(OperationContext context) + { + instance.set(context); + } + + public static void endCurrent() + { + OperationContext ctx = instance.get(); + if (ctx != null) + { + ctx.close(); + instance.set(null); + } + } +} diff --git a/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java b/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java index 292264984f8f..942f9a5da405 100644 --- a/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java +++ b/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java @@ -17,6 +17,14 @@ */ package org.apache.cassandra.service.pager; +import java.util.StringJoiner; + +import javax.annotation.concurrent.NotThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.db.*; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.db.partitions.*; @@ -27,23 +35,42 @@ import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.ProtocolVersion; +@NotThreadSafe abstract class AbstractQueryPager implements QueryPager { + private static final Logger logger = LoggerFactory.getLogger(AbstractQueryPager.class); + protected final T query; + + // the limits provided as a part of the query protected final DataLimits limits; protected final ProtocolVersion protocolVersion; private final boolean enforceStrictLiveness; - private int remaining; + // This is the counter which was used for the last page we fetched. It can be used to obtain the number of + // fetched rows or bytes. + private DataLimits.Counter lastCounter; - // This is the last key we've been reading from (or can still be reading within). This the key for + // This is the last key we've been reading from (or can still be reading within). This is the key for // which remainingInPartition makes sense: if we're starting another key, we should reset remainingInPartition // (and this is done in PagerIterator). This can be null (when we start). private DecoratedKey lastKey; + + // The remaining and remainingInPartition are initially set to the user limits provided in the query (via the + // LIMIT and PER PARTITION LIMIT clauses). When a page is fetched, iterated and closed, those values are updated + // with the number of items counted on that recently fetched page. + private int remaining; private int remainingInPartition; + // Whether the pager is exhausted or not - the pager gets exhausted if the recently fetched, iterated and closed + // page has less items than the requested page size private boolean exhausted; + // The paging transformation which is used for the recently requested page. It is set when we request the new page + // and then cleaned when the page is closed. We use it to prevent fetching a new page until the previous one is + // closed. + private PagerTransformation currentPagerTransformation; + protected AbstractQueryPager(T query, ProtocolVersion protocolVersion) { this.query = query; @@ -60,58 +87,77 @@ public ReadExecutionController executionController() return query.executionController(); } - public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) + public PartitionIterator fetchPage(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) { + assert currentPagerTransformation == null; + if (isExhausted()) return EmptyIterators.partition(); - pageSize = Math.min(pageSize, remaining); - Pager pager = new RowPager(limits.forPaging(pageSize), query.nowInSec()); - ReadQuery readQuery = nextPageReadQuery(pageSize); + DataLimits updatedQueryLimits = nextPageLimits(); + RowPagerTransformation pagerTransformation = new RowPagerTransformation(updatedQueryLimits.forPaging(pageSize), query.nowInSec()); + ReadQuery readQuery = nextPageReadQuery(pageSize, updatedQueryLimits); if (readQuery == null) { exhausted = true; return EmptyIterators.partition(); } - return Transformation.apply(readQuery.execute(consistency, clientState, requestTime), pager); + currentPagerTransformation = pagerTransformation; + return Transformation.apply(readQuery.execute(consistency, clientState, requestTime), pagerTransformation); } - public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) + @Override + public PartitionIterator fetchPageInternal(PageSize pageSize, ReadExecutionController executionController) { + assert currentPagerTransformation == null; + if (isExhausted()) return EmptyIterators.partition(); - pageSize = Math.min(pageSize, remaining); - RowPager pager = new RowPager(limits.forPaging(pageSize), query.nowInSec()); - ReadQuery readQuery = nextPageReadQuery(pageSize); + DataLimits updatedQueryLimits = nextPageLimits(); + RowPagerTransformation pagerTransformation = new RowPagerTransformation(updatedQueryLimits.forPaging(pageSize), query.nowInSec()); + ReadQuery readQuery = nextPageReadQuery(pageSize, updatedQueryLimits); if (readQuery == null) { exhausted = true; return EmptyIterators.partition(); } - return Transformation.apply(readQuery.executeInternal(executionController), pager); + currentPagerTransformation = pagerTransformation; + return Transformation.apply(readQuery.executeInternal(executionController), pagerTransformation); } - public UnfilteredPartitionIterator fetchPageUnfiltered(TableMetadata metadata, int pageSize, ReadExecutionController executionController) + public UnfilteredPartitionIterator fetchPageUnfiltered(TableMetadata metadata, PageSize pageSize, ReadExecutionController executionController) { + assert currentPagerTransformation == null; + if (isExhausted()) return EmptyIterators.unfilteredPartition(metadata); - pageSize = Math.min(pageSize, remaining); - UnfilteredPager pager = new UnfilteredPager(limits.forPaging(pageSize), query.nowInSec()); - ReadQuery readQuery = nextPageReadQuery(pageSize); + DataLimits updatedQueryLimits = nextPageLimits(); + UnfilteredPagerTransformation pagerTransformation = new UnfilteredPagerTransformation(updatedQueryLimits.forPaging(pageSize), query.nowInSec()); + ReadQuery readQuery = nextPageReadQuery(pageSize, updatedQueryLimits); if (readQuery == null) { exhausted = true; return EmptyIterators.unfilteredPartition(metadata); } - return Transformation.apply(readQuery.executeLocally(executionController), pager); + currentPagerTransformation = pagerTransformation; + return Transformation.apply(readQuery.executeLocally(executionController), pagerTransformation); } - private class UnfilteredPager extends Pager + /** + * For subsequent pages we want to limit the number of rows to the minimum of the currently set limit in the query + * and the number of remaining rows in page. Note that paging itself will be applied separately. + */ + protected DataLimits nextPageLimits() { + return limits.withCountedLimit(Math.min(limits.count(), remaining)); + } - private UnfilteredPager(DataLimits pageLimits, long nowInSec) + private class UnfilteredPagerTransformation extends PagerTransformation + { + + private UnfilteredPagerTransformation(DataLimits pageLimits, long nowInSec) { super(pageLimits, nowInSec); } @@ -122,10 +168,10 @@ protected BaseRowIterator apply(BaseRowIterator partitio } } - private class RowPager extends Pager + private class RowPagerTransformation extends PagerTransformation { - private RowPager(DataLimits pageLimits, long nowInSec) + private RowPagerTransformation(DataLimits pageLimits, long nowInSec) { super(pageLimits, nowInSec); } @@ -136,7 +182,7 @@ protected BaseRowIterator apply(BaseRowIterator partition) } } - private abstract class Pager extends Transformation> + private abstract class PagerTransformation extends Transformation> { private final DataLimits pageLimits; protected final DataLimits.Counter counter; @@ -144,10 +190,20 @@ private abstract class Pager extends Transformation applyToPartition(BaseRowIterator partition) @Override public void onClose() { + assert lastCounter == counter; // In some case like GROUP BY a counter need to know when the processing is completed. counter.onClose(); @@ -197,7 +254,9 @@ public void onClose() { remainingInPartition -= counter.countedInCurrentPartition(); } - exhausted = pageLimits.isExhausted(counter); + // if the counter did not count up to the page limits, then the iteration must have reached the end + exhausted = pageLimits.isCounterBelowLimits(counter); + currentPagerTransformation = null; } public Row applyToStatic(Row row) @@ -223,6 +282,18 @@ public Row applyToRow(Row row) lastRow = row; return row; } + + @Override + public String toString() + { + return new StringJoiner(", ", PagerTransformation.class.getSimpleName() + "[", "]") + .add("pageLimits=" + pageLimits) + .add("counter=" + counter) + .add("currentKey=" + currentKey) + .add("lastRow=" + lastRow) + .add("isFirstPartition=" + isFirstPartition) + .toString(); + } } protected void restoreState(DecoratedKey lastKey, int remaining, int remainingInPartition) @@ -234,7 +305,7 @@ protected void restoreState(DecoratedKey lastKey, int remaining, int remainingIn public boolean isExhausted() { - return exhausted || remaining == 0 || ((this instanceof SinglePartitionPager) && remainingInPartition == 0); + return exhausted || remaining == 0; } public int maxRemaining() @@ -247,7 +318,30 @@ protected int remainingInPartition() return remainingInPartition; } - protected abstract T nextPageReadQuery(int pageSize); + /** + * Returns the {@link DataLimits.Counter} for the page which was last fetched (the last page in the meaning + * the last returned and traversed row iterator, the iterator must be closed in order for this method to return + * proper counter) + */ + public DataLimits.Counter getLastCounter() + { + return lastCounter; + } + + protected abstract T nextPageReadQuery(PageSize pageSize, DataLimits limits); protected abstract void recordLast(DecoratedKey key, Row row); protected abstract boolean isPreviouslyReturnedPartition(DecoratedKey key); + + @Override + public String toString() + { + return new StringJoiner(", ", AbstractQueryPager.class.getSimpleName() + "[", "]") + .add("limits=" + limits) + .add("remaining=" + remaining) + .add("lastCounter=" + lastCounter) + .add("lastKey=" + lastKey) + .add("remainingInPartition=" + remainingInPartition) + .add("exhausted=" + exhausted) + .toString(); + } } diff --git a/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java b/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java index 95d910de6052..c386131d8fb3 100644 --- a/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java +++ b/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java @@ -19,47 +19,94 @@ import java.nio.ByteBuffer; import java.util.NoSuchElementException; - -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.db.*; +import java.util.StringJoiner; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.aggregation.GroupingState; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.OperationExecutionException; +import org.apache.cassandra.exceptions.ReadTimeoutException; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.Clock; /** * {@code QueryPager} that takes care of fetching the pages for aggregation queries. *

    * For aggregation/group by queries, the user page size is in number of groups. But each group could be composed of very * many rows so to avoid running into OOMs, this pager will page internal queries into sub-pages. So each call to - * {@link fetchPage} may (transparently) yield multiple internal queries (sub-pages). + * {@link #fetchPage(PageSize, ConsistencyLevel, ClientState, Dispatcher.RequestTime)} may (transparently) yield multiple internal queries + * (sub-pages). */ public final class AggregationQueryPager implements QueryPager { + private static final Logger logger = LoggerFactory.getLogger(AggregationQueryPager.class); + private final DataLimits limits; + private final PageSize subPageSize; + // The sub-pager, used to retrieve the next sub-page. private QueryPager subPager; - public AggregationQueryPager(QueryPager subPager, DataLimits limits) + // the timeout in nanoseconds, if more time has elapsed, a ReadTimeoutException will be raised + private final long timeoutNanos; + + public AggregationQueryPager(QueryPager subPager, PageSize subPageSize, DataLimits limits) + { + this(subPager, subPageSize, limits, DatabaseDescriptor.getAggregationRpcTimeout(TimeUnit.NANOSECONDS)); + } + + public AggregationQueryPager(QueryPager subPager, PageSize subPageSize, DataLimits limits, long timeoutNanos) { this.subPager = subPager; this.limits = limits; + this.subPageSize = subPageSize; + this.timeoutNanos = timeoutNanos; } + /** + * This will return the iterator over the partitions. The iterator is limited by the provided page size and the user + * specified limit (in the query). Both the limit and the page size are applied to the number of groups covered by + * the returned data. + *

    + * In case of group-by queries the page size can be provided only in rows unit ({@link OperationExecutionException} + * is thrown otherwise). In case of 'aggregate everything' queries, the provided page size and the limits are + * ignored as we always return a single row. + * + * @param pageSize the maximum number of elements to return in the next page (groups) + * @param consistency the consistency level to achieve for the query + * @param clientState the {@code QueryState} for the query. In practice, this can be null unless + * {@code consistency} is a serial consistency + */ @Override - public PartitionIterator fetchPage(int pageSize, + public PartitionIterator fetchPage(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) { + if (pageSize.isDefined() && pageSize.getUnit() != PageSize.PageUnit.ROWS) + throw new InvalidRequestException("Paging in bytes is not supported for aggregation queries. Please specify the page size in rows."); + if (limits.isGroupByLimit()) - return new GroupByPartitionIterator(pageSize, consistency, clientState, requestTime); + return new GroupByPartitionIterator(pageSize, subPageSize, consistency, clientState, requestTime); - return new AggregationPartitionIterator(pageSize, consistency, clientState, requestTime); + return new AggregationPartitionIterator(subPageSize, consistency, clientState, requestTime); } @Override @@ -68,13 +115,22 @@ public ReadExecutionController executionController() return subPager.executionController(); } + /** + * {@see #fetchPage} + * + * @param pageSize the maximum number of elements to return in the next page + * @param executionController the {@code ReadExecutionController} protecting the read + */ @Override - public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) + public PartitionIterator fetchPageInternal(PageSize pageSize, ReadExecutionController executionController) { + if (pageSize.isDefined() && pageSize.getUnit() != PageSize.PageUnit.ROWS) + throw new InvalidRequestException("Paging in bytes is not supported for aggregation queries. Please specify the page size in rows."); + if (limits.isGroupByLimit()) - return new GroupByPartitionIterator(pageSize, executionController, Dispatcher.RequestTime.forImmediateExecution()); + return new GroupByPartitionIterator(pageSize, subPageSize, executionController, Dispatcher.RequestTime.forImmediateExecution()); - return new AggregationPartitionIterator(pageSize, executionController, Dispatcher.RequestTime.forImmediateExecution()); + return new AggregationPartitionIterator(subPageSize, executionController, Dispatcher.RequestTime.forImmediateExecution()); } @Override @@ -116,11 +172,17 @@ public class GroupByPartitionIterator implements PartitionIterator /** * The top-level page size in number of groups. */ - private final int pageSize; + private final PageSize groupsPageSize; + + /** + * Page size for internal paging + */ + private final PageSize subPageSize; // For "normal" queries private final ConsistencyLevel consistency; private final ClientState clientState; + private final long queryStartNanoTime; // For internal queries private final ReadExecutionController executionController; @@ -158,43 +220,45 @@ public class GroupByPartitionIterator implements PartitionIterator /** * The initial amount of row remaining */ - private int initialMaxRemaining; + protected int initialMaxRemaining; private Dispatcher.RequestTime requestTime; - public GroupByPartitionIterator(int pageSize, + public GroupByPartitionIterator(PageSize groupsPageSize, + PageSize subPageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) { - this(pageSize, consistency, clientState, null, requestTime); + this(groupsPageSize, subPageSize, consistency, clientState, null, requestTime); } - public GroupByPartitionIterator(int pageSize, + public GroupByPartitionIterator(PageSize groupsPageSize, + PageSize subPageSize, ReadExecutionController executionController, Dispatcher.RequestTime requestTime) { - this(pageSize, null, null, executionController, requestTime); + this(groupsPageSize, subPageSize, null, null, executionController, requestTime); } - private GroupByPartitionIterator(int pageSize, + private GroupByPartitionIterator(PageSize groupsPageSize, + PageSize subPageSize, ConsistencyLevel consistency, ClientState clientState, ReadExecutionController executionController, Dispatcher.RequestTime requestTime) { - this.pageSize = handlePagingOff(pageSize); + this.groupsPageSize = groupsPageSize; + this.subPageSize = subPageSize; this.consistency = consistency; this.clientState = clientState; this.executionController = executionController; this.requestTime = requestTime; - } + this.queryStartNanoTime = Clock.Global.nanoTime(); + subPager = subPager.withUpdatedLimit(limits.withCountedLimit(groupsPageSize.minRowsCount(maxRemaining()))); - private int handlePagingOff(int pageSize) - { - // If the paging is off, the pageSize will be <= 0. So we need to replace - // it by DataLimits.NO_LIMIT - return pageSize <= 0 ? DataLimits.NO_LIMIT : pageSize; + if (logger.isTraceEnabled()) + logger.trace("Fetching a new page - created {}", this); } public final void close() @@ -219,40 +283,58 @@ public final boolean hasNext() return next != null; } + private void checkTimeout() + { + // internal queries are not guarded by a timeout because cont. paging queries can be aborted + // and system queries should not be aborted + if (consistency == null) + return; + + long elapsed = Clock.Global.nanoTime() - queryStartNanoTime; + if (elapsed > AggregationQueryPager.this.timeoutNanos) + { + logger.debug("Aggregation query timeout triggered: elapsed={} ns, timeout={} ns", elapsed, AggregationQueryPager.this.timeoutNanos); + throw new ReadTimeoutException(consistency); + } + } + /** - * Loads the next RowIterator to be returned. + * Loads the next RowIterator to be returned. The iteration finishes when we reach either the + * user groups limit or the groups page size. The user provided limit is initially set in subPager.maxRemaining(). */ private void fetchNextRowIterator() { + // we haven't started yet, fetch the first sub page (partition iterator with sub-page limit) if (partitionIterator == null) { initialMaxRemaining = subPager.maxRemaining(); - partitionIterator = fetchSubPage(pageSize); + partitionIterator = fetchSubPage(subPageSize); } while (!partitionIterator.hasNext()) { partitionIterator.close(); - int counted = initialMaxRemaining - subPager.maxRemaining(); - - if (isDone(pageSize, counted) || subPager.isExhausted()) + int remaining = getRemaining(); + assert remaining >= 0; + if (remaining == 0 || subPager.isExhausted()) { endOfData = true; closed = true; return; } - subPager = updatePagerLimit(subPager, limits, lastPartitionKey, lastClustering); - partitionIterator = fetchSubPage(computeSubPageSize(pageSize, counted)); + subPager = updatePagerLimit(subPager, limits.withCountedLimit(remaining), lastPartitionKey, lastClustering); + partitionIterator = fetchSubPage(subPageSize); } next = partitionIterator.next(); } - protected boolean isDone(int pageSize, int counted) + protected int getRemaining() { - return counted == pageSize; + int counted = initialMaxRemaining - subPager.maxRemaining(); + return groupsPageSize.withDecreasedRows(counted).rows(); } /** @@ -274,26 +356,16 @@ protected QueryPager updatePagerLimit(QueryPager pager, return pager.withUpdatedLimit(newLimits); } - /** - * Computes the size of the next sub-page to retrieve. - * - * @param pageSize the top-level page size - * @param counted the number of result returned so far by the previous sub-pages - * @return the size of the next sub-page to retrieve - */ - protected int computeSubPageSize(int pageSize, int counted) - { - return pageSize - counted; - } - /** * Fetchs the next sub-page. * * @param subPageSize the sub-page size in number of groups * @return the next sub-page */ - private final PartitionIterator fetchSubPage(int subPageSize) + private final PartitionIterator fetchSubPage(PageSize subPageSize) { + checkTimeout(); + return consistency != null ? subPager.fetchPage(subPageSize, consistency, clientState, requestTime) : subPager.fetchPageInternal(subPageSize, executionController); } @@ -391,11 +463,42 @@ public boolean hasNext() public Row next() { + // we need to check this because this.rowIterator may exhaust if the sub-page is done and in such a case + // #hasNext switches this.rowIterator to the new one, which is obtained for the next page + if (!hasNext()) + throw new NoSuchElementException(); + Row row = this.rowIterator.next(); lastClustering = row.clustering(); return row; } } + + @Override + public String toString() + { + return new StringJoiner(", ", GroupByPartitionIterator.class.getSimpleName() + "[", "]") + .add("groupsPageSize=" + groupsPageSize) + .add("subPageSize=" + subPageSize) + .add("endOfData=" + endOfData) + .add("closed=" + closed) + .add("limits=" + limits) + .add("lastPartitionKey=" + lastPartitionKey) + .add("lastClustering=" + ((lastClustering != null && subPager.executionController() != null) ? lastClustering.toString(subPager.executionController().metadata()): String.valueOf(lastClustering))) + .add("initialMaxRemaining=" + initialMaxRemaining) + .add("sub-pager=" + subPager.toString()) + .toString(); + } + } + + @Override + public String toString() + { + return new StringJoiner(", ", AggregationQueryPager.class.getSimpleName() + "[", "]") + .add("limits=" + limits) + .add("subPageSize=" + subPageSize) + .add("subPager=" + subPager) + .toString(); } /** @@ -405,19 +508,19 @@ public Row next() */ public final class AggregationPartitionIterator extends GroupByPartitionIterator { - public AggregationPartitionIterator(int pageSize, + public AggregationPartitionIterator(PageSize subPageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) { - super(pageSize, consistency, clientState, requestTime); + super(PageSize.NONE, subPageSize, consistency, clientState, requestTime); } - public AggregationPartitionIterator(int pageSize, + public AggregationPartitionIterator(PageSize subPageSize, ReadExecutionController executionController, Dispatcher.RequestTime requestTime) { - super(pageSize, executionController, requestTime); + super(PageSize.NONE, subPageSize, executionController, requestTime); } @Override @@ -430,15 +533,9 @@ protected QueryPager updatePagerLimit(QueryPager pager, } @Override - protected boolean isDone(int pageSize, int counted) - { - return false; - } - - @Override - protected int computeSubPageSize(int pageSize, int counted) + protected int getRemaining() { - return pageSize; + return initialMaxRemaining; } } } diff --git a/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java b/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java index 842eb35e8196..740c7de452fb 100644 --- a/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java +++ b/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java @@ -18,10 +18,17 @@ package org.apache.cassandra.service.pager; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.AbstractIterator; import java.util.Arrays; +import java.util.StringJoiner; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.db.*; import org.apache.cassandra.db.rows.*; @@ -47,12 +54,24 @@ */ public class MultiPartitionPager implements QueryPager { + private static final Logger logger = LoggerFactory.getLogger(MultiPartitionPager.class); + + private static final SinglePartitionPager[] NO_PAGERS = new SinglePartitionPager[0]; + + // a pager per queried partition + @Nonnull private final SinglePartitionPager[] pagers; + + // user limit private final DataLimits limit; private final long nowInSec; + // the number of rows left to be returned according to the user limits (those provided in query) + // when remaining reaches 0, the pager is considered exhausted private int remaining; + + // the index of the current single partition pager private int current; public MultiPartitionPager(SinglePartitionReadQuery.Group group, PagingState state, ProtocolVersion protocolVersion) @@ -70,7 +89,7 @@ public MultiPartitionPager(SinglePartitionReadQuery.Group group, PagingState if (i >= group.queries.size()) { - pagers = null; + pagers = NO_PAGERS; return; } @@ -123,7 +142,8 @@ public PagingState state() public boolean isExhausted() { - if (remaining <= 0 || pagers == null) + assert remaining >= 0; + if (remaining == 0) return true; while (current < pagers.length) @@ -150,22 +170,26 @@ public ReadExecutionController executionController() @SuppressWarnings("resource") // iter closed via countingIter @Override - public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) throws RequestValidationException, RequestExecutionException + public PartitionIterator fetchPage(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) throws RequestValidationException, RequestExecutionException { - int toQuery = Math.min(remaining, pageSize); - return new PagersIterator(toQuery, consistency, clientState, null, requestTime); + return new PagersIterator(pageSize, consistency, clientState, null, requestTime); } - public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException + public PartitionIterator fetchPageInternal(PageSize pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException { - int toQuery = Math.min(remaining, pageSize); - return new PagersIterator(toQuery, null, null, executionController, Dispatcher.RequestTime.forImmediateExecution()); + return new PagersIterator(pageSize, null, null, executionController, Dispatcher.RequestTime.forImmediateExecution()); } + /** + * This is an iterator over RowIterators (subsequent partitions). It starts from {@link #pagers} at {@link #current} + * and make sure that the overall amount of data does not exceed the provided {@link PagersIterator#pageSize}. + * This means that it can cut the row iteration in the first partition or return multiple partitions and cut the + * row iterator in n-th partition. It will update the {@link #current} index and {@link #remaining} as it goes. + */ private class PagersIterator extends AbstractIterator implements PartitionIterator { - private final int pageSize; - private PartitionIterator result; + private final PageSize pageSize; + private PartitionIterator partitionIterator; private boolean closed; private final Dispatcher.RequestTime requestTime; @@ -176,32 +200,48 @@ private class PagersIterator extends AbstractIterator implements Pa // For internal queries private final ReadExecutionController executionController; - private int pagerMaxRemaining; + private int countedRows; + private int countedBytes; private int counted; - public PagersIterator(int pageSize, ConsistencyLevel consistency, ClientState clientState, ReadExecutionController executionController, Dispatcher.RequestTime requestTime) + public PagersIterator(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, ReadExecutionController executionController, Dispatcher.RequestTime requestTime) { this.pageSize = pageSize; this.consistency = consistency; this.clientState = clientState; this.executionController = executionController; this.requestTime = requestTime; + + if (logger.isTraceEnabled()) + logger.trace("Fetching a new page - created {}", this); } protected RowIterator computeNext() { - while (result == null || !result.hasNext()) + while (partitionIterator == null || !partitionIterator.hasNext()) { - if (result != null) + DataLimits.Counter lastPageCounter = null; + if (partitionIterator != null) { - result.close(); - counted += pagerMaxRemaining - pagers[current].maxRemaining(); + // we've just reached the end of partition, + // let's close the row iterator and update the global counters + partitionIterator.close(); + + lastPageCounter = pagers[current].getLastCounter(); + countedRows += lastPageCounter.rowsCounted(); + countedBytes += lastPageCounter.bytesCounted(); + counted += lastPageCounter.counted(); + remaining -= lastPageCounter.counted(); } - // We are done if we have reached the page size or in the case of GROUP BY if the current pager - // is not exhausted. - boolean isDone = counted >= pageSize - || (result != null && limit.isGroupByLimit() && !pagers[current].isExhausted()); + // We are done if: + // - we have reached the page size, + // - or in the case of GROUP BY if the current pager is not exhausted - which means that we read all the rows withing the limit before exhausting the pager + boolean isDone = pageSize.isCompleted(countedRows, PageSize.PageUnit.ROWS) + || pageSize.isCompleted(countedBytes, PageSize.PageUnit.BYTES) + || limit.count() <= counted + || limit.bytes() <= countedBytes + || (partitionIterator != null && limit.isGroupByLimit() && !pagers[current].isExhausted()); // isExhausted() will sets us on the first non-exhausted pager if (isDone || isExhausted()) @@ -210,20 +250,44 @@ protected RowIterator computeNext() return endOfData(); } - pagerMaxRemaining = pagers[current].maxRemaining(); - int toQuery = pageSize - counted; - result = consistency == null - ? pagers[current].fetchPageInternal(toQuery, executionController) - : pagers[current].fetchPage(toQuery, consistency, clientState, requestTime); + // we will update the limits for the current pager before using it so that we can be sure we don't fetch + // more than remaining or more than what was left to be fetched according to the recently set limits + // (for example in case of groups paging) - that later limit is just the limit which was set minus what + // we counted so far + int newCountedLimit = Math.max(0, Math.min(remaining, limit.count() - counted)); + // this works exactly the same way as above - it is required for the limits imposed by Guardrails, + // whihc are set on the query + int newBytesLimit = Math.max(0, limit.bytes() - countedBytes); + + DataLimits updatedLimit = pagers[current].limits.withCountedLimit(newCountedLimit).withBytesLimit(newBytesLimit); + pagers[current] = pagers[current].withUpdatedLimit(updatedLimit); + + PageSize remainingPagePart = pageSize.withDecreasedRows(countedRows) + .withDecreasedBytes(countedBytes); + + partitionIterator = consistency == null + ? pagers[current].fetchPageInternal(remainingPagePart, executionController) + : pagers[current].fetchPage(remainingPagePart, consistency, clientState, requestTime); } - return result.next(); + return partitionIterator.next(); } public void close() { - remaining -= counted; - if (result != null && !closed) - result.close(); + if (partitionIterator != null && !closed) + partitionIterator.close(); + } + + @Override + public String toString() + { + return new StringJoiner(", ", PagersIterator.class.getSimpleName() + "[", "]") + .add("pageSize=" + pageSize) + .add("closed=" + closed) + .add("countedRows=" + countedRows) + .add("countedBytes=" + countedBytes) + .add("counted=" + counted) + .toString(); } } @@ -231,4 +295,15 @@ public int maxRemaining() { return remaining; } + + @Override + public String toString() + { + return new StringJoiner(", ", MultiPartitionPager.class.getSimpleName() + "[", "]") + .add("current=" + current) + .add("pagers.length=" + pagers.length) + .add("limit=" + limit) + .add("remaining=" + remaining) + .toString(); + } } diff --git a/src/java/org/apache/cassandra/service/pager/PagedPartitionIterator.java b/src/java/org/apache/cassandra/service/pager/PagedPartitionIterator.java new file mode 100644 index 000000000000..862558c0ba49 --- /dev/null +++ b/src/java/org/apache/cassandra/service/pager/PagedPartitionIterator.java @@ -0,0 +1,129 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.service.pager; + +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Dispatcher; + +/** + * A partition iterator that reads its rows from a provided {@link QueryPager}, consuming it until it's exhausted. + */ +abstract class PagedPartitionIterator implements PartitionIterator +{ + protected final QueryPager pager; + protected final PageSize pageSize; + protected PartitionIterator current; + + protected PagedPartitionIterator(QueryPager pager, PageSize pageSize) + { + this.pager = pager; + this.pageSize = pageSize; + } + + @Override + public void close() + { + if (current != null) + { + current.close(); + current = null; + } + } + + @Override + public boolean hasNext() + { + maybeFetch(); + return current != null && current.hasNext(); + } + + @Override + public RowIterator next() + { + maybeFetch(); + return current.next(); + } + + private void maybeFetch() + { + if (current == null || !current.hasNext()) + { + if (current != null) + { + current.close(); + current = null; + } + + if (!pager.isExhausted()) + current = fetch(); + } + } + + protected abstract PartitionIterator fetch(); + + /** + * {@link PagedPartitionIterator} that for local queries. + */ + public static class Internal extends PagedPartitionIterator + { + private final ReadExecutionController controller; + + public Internal(QueryPager pager, PageSize pageSize, ReadExecutionController controller) + { + super(pager, pageSize); + this.controller = controller; + } + + @Override + protected PartitionIterator fetch() + { + return pager.fetchPageInternal(pageSize, controller); + } + } + + /** + * {@link PagedPartitionIterator} that for distributed queries. + */ + public static class Distributed extends PagedPartitionIterator + { + private final ConsistencyLevel consistency; + private final ClientState state; + private final Dispatcher.RequestTime requestTime; + + public Distributed(QueryPager pager, + PageSize pageSize, + ConsistencyLevel consistency, + ClientState state, + Dispatcher.RequestTime requestTime) + { + super(pager, pageSize); + this.consistency = consistency; + this.state = state; + this.requestTime = requestTime; + } + + @Override + protected PartitionIterator fetch() + { + return pager.fetchPage(pageSize, consistency, state, requestTime); + } + } +} diff --git a/src/java/org/apache/cassandra/service/pager/PagingState.java b/src/java/org/apache/cassandra/service/pager/PagingState.java index 627f958ff3ec..ec43068f3830 100644 --- a/src/java/org/apache/cassandra/service/pager/PagingState.java +++ b/src/java/org/apache/cassandra/service/pager/PagingState.java @@ -23,6 +23,9 @@ import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ByteBufferAccessor; @@ -47,6 +50,8 @@ @SuppressWarnings("WeakerAccess") public class PagingState { + private static final Logger logger = LoggerFactory.getLogger(PagingState.class); + public final ByteBuffer partitionKey; // Can be null for single partition queries. public final RowMark rowMark; // Can be null if not needed. public final int remaining; @@ -113,10 +118,14 @@ public static PagingState deserialize(ByteBuffer bytes, ProtocolVersion protocol } catch (IOException e) { - throw new ProtocolException("Invalid value for the paging state"); + String msg = "Failed to deserialize the paging state with protocol version: " + protocolVersion; + logger.trace(msg, e); + throw new ProtocolException(msg, protocolVersion); } - throw new ProtocolException("Invalid value for the paging state"); + String msg = "The serialized paging state does not match any serialization format for protocol version: " + protocolVersion; + logger.trace(msg); + throw new ProtocolException(msg, protocolVersion); } /* diff --git a/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java b/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java index 3ee90d707416..ef64f65ba19d 100644 --- a/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java +++ b/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java @@ -17,6 +17,9 @@ */ package org.apache.cassandra.service.pager; +import java.util.StringJoiner; + +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.db.*; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.rows.Row; @@ -74,15 +77,14 @@ public PagingState state() } @Override - protected PartitionRangeReadQuery nextPageReadQuery(int pageSize) + protected PartitionRangeReadQuery nextPageReadQuery(PageSize pageSize, DataLimits limits) { - DataLimits limits; DataRange fullRange = query.dataRange(); DataRange pageRange; if (lastReturnedKey == null) { pageRange = fullRange; - limits = query.limits().forPaging(pageSize); + limits = limits.forPaging(pageSize); } // if the last key was the one of the end of the range we know that we are done else if (lastReturnedKey.equals(fullRange.keyRange().right) && remainingInPartition() == 0 && lastReturnedRow == null) @@ -97,12 +99,12 @@ else if (lastReturnedKey.equals(fullRange.keyRange().right) && remainingInPartit if (includeLastKey) { pageRange = fullRange.forPaging(bounds, query.metadata().comparator, lastReturnedRow.clustering(query.metadata()), false); - limits = query.limits().forPaging(pageSize, lastReturnedKey.getKey(), remainingInPartition()); + limits = limits.forPaging(pageSize, lastReturnedKey.getKey(), remainingInPartition()); } else { pageRange = fullRange.forSubRange(bounds); - limits = query.limits().forPaging(pageSize); + limits = limits.forPaging(pageSize); } } @@ -145,4 +147,14 @@ public boolean isTopK() { return query.isTopK(); } + + @Override + public String toString() + { + return new StringJoiner(", ", PartitionRangeQueryPager.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("lastReturnedKey=" + lastReturnedKey) + .add("lastReturnedRow=" + (lastReturnedRow != null ? lastReturnedRow.clustering(query.metadata()).toString(query.metadata()) : null)) + .toString(); + } } diff --git a/src/java/org/apache/cassandra/service/pager/QueryPager.java b/src/java/org/apache/cassandra/service/pager/QueryPager.java index 1619af8a3898..cbd76e173216 100644 --- a/src/java/org/apache/cassandra/service/pager/QueryPager.java +++ b/src/java/org/apache/cassandra/service/pager/QueryPager.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.service.pager; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.filter.DataLimits; @@ -55,12 +56,12 @@ public ReadExecutionController executionController() return ReadExecutionController.empty(); } - public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) throws RequestValidationException, RequestExecutionException + public PartitionIterator fetchPage(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) throws RequestValidationException, RequestExecutionException { return EmptyIterators.partition(); } - public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException + public PartitionIterator fetchPageInternal(PageSize pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException { return EmptyIterators.partition(); } @@ -95,13 +96,13 @@ public QueryPager withUpdatedLimit(DataLimits newLimits) * {@code consistency} is a serial consistency. * @return the page of result. */ - public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) throws RequestValidationException, RequestExecutionException; + public PartitionIterator fetchPage(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) throws RequestValidationException, RequestExecutionException; /** * Starts a new read operation. *

    - * This must be called before {@link fetchPageInternal} and passed to it to protect the read. - * The returned object must be closed on all path and it is thus strongly advised to + * This must be called before {@link #fetchPageInternal(PageSize, ReadExecutionController)} and passed to it + * to protect the read. The returned object must be closed on all path and it is thus strongly advised to * use it in a try-with-ressource construction. * * @return a newly started order group for this {@code QueryPager}. @@ -115,7 +116,7 @@ public QueryPager withUpdatedLimit(DataLimits newLimits) * @param executionController the {@code ReadExecutionController} protecting the read. * @return the page of result. */ - public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException; + public PartitionIterator fetchPageInternal(PageSize pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException; /** * Whether or not this pager is exhausted, i.e. whether or not a call to @@ -150,7 +151,6 @@ public QueryPager withUpdatedLimit(DataLimits newLimits) */ public QueryPager withUpdatedLimit(DataLimits newLimits); - /** * @return true given read query is a top-k request */ @@ -158,4 +158,36 @@ default boolean isTopK() { return false; } + + /** + * Reads all the rows in this pager using paging internally. + *

    + * Pages will be lazily fetched according to the provided page size as the returned {@link PartitionIterator} is + * consumed. + * + * @param pageSize the maximum number of elements to be fetched on each internal page. + * @param consistency the consistency level to achieve for the query. + * @param clientState the {@code ClientState} for the query. In practice, this can be null unless {@code consistency} + * is a serial consistency. + * @return all the rows in this pager. + */ + default PartitionIterator readAll(PageSize pageSize, ConsistencyLevel consistency, ClientState clientState, Dispatcher.RequestTime requestTime) + { + return new PagedPartitionIterator.Distributed(this, pageSize, consistency, clientState, requestTime); + } + + /** + * Reads all the rows in this pager using paging internally, using local queries. + *

    + * Pages will be lazily fetched according to the provided page size as the returned {@link PartitionIterator} is + * consumed. + * + * @param pageSize the maximum number of elements to be fetched on each internal page. + * @param executionController the {@code ReadExecutionController} protecting the read. + * @return all the rows in this pager. + */ + default PartitionIterator readAllInternal(PageSize pageSize, ReadExecutionController executionController) + { + return new PagedPartitionIterator.Internal(this, pageSize, executionController); + } } diff --git a/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java b/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java index 832526e5ce6c..68d355571490 100644 --- a/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java +++ b/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java @@ -18,7 +18,9 @@ package org.apache.cassandra.service.pager; import java.nio.ByteBuffer; +import java.util.StringJoiner; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.db.*; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.db.filter.*; @@ -26,7 +28,7 @@ /** * Common interface to single partition queries (by slice and by name). - * + *

    * For use by MultiPartitionPager. */ public class SinglePartitionPager extends AbstractQueryPager @@ -78,21 +80,27 @@ public DataLimits limits() public PagingState state() { return lastReturned == null - ? null - : new PagingState(null, lastReturned, maxRemaining(), remainingInPartition()); + ? null + : new PagingState(null, lastReturned, maxRemaining(), remainingInPartition()); } @Override - protected SinglePartitionReadQuery nextPageReadQuery(int pageSize) + protected SinglePartitionReadQuery nextPageReadQuery(PageSize pageSize, DataLimits limits) { Clustering clustering = lastReturned == null ? null : lastReturned.clustering(query.metadata()); - DataLimits limits = lastReturned == null - ? limits().forPaging(pageSize) - : limits().forPaging(pageSize, key(), remainingInPartition()); + limits = lastReturned == null + ? limits.forPaging(pageSize) + : limits.forPaging(pageSize, key(), remainingInPartition()); return query.forPaging(clustering, limits); } + @Override + public boolean isExhausted() + { + return super.isExhausted() || remainingInPartition() == 0; + } + protected void recordLast(DecoratedKey key, Row last) { if (last != null && last.clustering() != Clustering.STATIC_CLUSTERING) @@ -103,4 +111,13 @@ protected boolean isPreviouslyReturnedPartition(DecoratedKey key) { return lastReturned != null; } + + @Override + public String toString() + { + return new StringJoiner(", ", SinglePartitionPager.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("lastReturned=" + (lastReturned != null ? lastReturned.clustering(query.metadata()).toString(query.metadata()) : null)) + .toString(); + } } diff --git a/src/java/org/apache/cassandra/service/paxos/Commit.java b/src/java/org/apache/cassandra/service/paxos/Commit.java index 3aa8d65bcef0..c1d123ae060f 100644 --- a/src/java/org/apache/cassandra/service/paxos/Commit.java +++ b/src/java/org/apache/cassandra/service/paxos/Commit.java @@ -29,6 +29,7 @@ import com.google.common.base.Objects; import org.apache.cassandra.db.*; +import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.io.IVersionedSerializer; @@ -342,7 +343,7 @@ public String toString() public String toString(String kind) { - return String.format("%s(%d:%s, %d:%s)", kind, ballot.uuidTimestamp(), ballot, update.stats().minTimestamp, update.toString(false)); + return String.format("%s(%d:%s, %d:%s)", kind, ballot.uuidTimestamp(), ballot, update.stats().minTimestamp, Partition.toString(update, false)); } /** @@ -472,7 +473,7 @@ public static boolean timestampsClash(@Nullable Ballot a, @Nullable Ballot b) private static PartitionUpdate withTimestamp(PartitionUpdate update, long timestamp) { - return new PartitionUpdate.Builder(update, 0).updateAllTimestamp(timestamp).build(); + return update.withUpdatedTimestamps(timestamp); } public static class CommitSerializer implements IVersionedSerializer diff --git a/src/java/org/apache/cassandra/service/paxos/CommitVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/CommitVerbHandler.java new file mode 100644 index 000000000000..5f889fe9cc1f --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/CommitVerbHandler.java @@ -0,0 +1,63 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.cassandra.service.paxos; + +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; +import org.apache.cassandra.service.MutatorProvider; +import org.apache.cassandra.tracing.Tracing; + +public class CommitVerbHandler implements IVerbHandler +{ + public static final CommitVerbHandler instance = new CommitVerbHandler(); + + public void doVerb(Message message) + { + // Initialize the sensor and set ExecutorLocals + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(message.payload.update.metadata().keyspace); + Context context = Context.from(message.payload.update.metadata()); + + // Commit phase reads from the Paxos table and writes the proposal to the user table + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + sensors.registerSensor(context, Type.INTERNODE_BYTES); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + RequestTracker.instance.set(sensors); + + PaxosState.commitDirect(message.payload, p -> MutatorProvider.instance.onAppliedProposal(p)); + + Tracing.trace("Enqueuing acknowledge to {}", message.from()); + Message.Builder reply = message.emptyResponseBuilder(); + + // No need to calculate outbound internode bytes for NoPayload response + sensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(sensors, reply); + MessagingService.instance().send(reply.build(), message.from()); + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java b/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java index 7f38567f6a15..a2b8970d7f6e 100644 --- a/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java +++ b/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java @@ -26,7 +26,10 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.metrics.ClientRequestsMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetricsProvider; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.NoSpamLogger; @@ -48,8 +51,6 @@ import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.*; import static org.apache.cassandra.config.DatabaseDescriptor.*; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casReadMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casWriteMetrics; import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.Clock.waitUntil; @@ -316,8 +317,9 @@ static class Bound this.onFailure = onFailure; this.modifier = modifier; this.selector = selector; - this.reads = new TimeLimitedLatencySupplier(casReadMetrics.latency::getSnapshot, 10L, SECONDS); - this.writes = new TimeLimitedLatencySupplier(casWriteMetrics.latency::getSnapshot, 10L, SECONDS); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(null); + this.reads = new TimeLimitedLatencySupplier(metrics.casReadMetrics.executionTimeMetrics.latency::getSnapshot, 10L, SECONDS); + this.writes = new TimeLimitedLatencySupplier(metrics.casWriteMetrics.executionTimeMetrics.latency::getSnapshot, 10L, SECONDS); } long get(int attempts) @@ -377,7 +379,7 @@ long computeWaitUntilForContention(int attempts, TableMetadata table, DecoratedK { if (attempts >= traceAfterAttempts && !Tracing.isTracing()) { - Tracing.instance.newSession(Tracing.TraceType.QUERY); + Tracing.instance.newSession(ClientState.forInternalCalls(), Tracing.TraceType.QUERY); Tracing.instance.begin(type.traceTitle, ImmutableMap.of( "keyspace", table.keyspace, diff --git a/src/java/org/apache/cassandra/service/paxos/Paxos.java b/src/java/org/apache/cassandra/service/paxos/Paxos.java index 1ab943ecc2d7..76180c9bbc09 100644 --- a/src/java/org/apache/cassandra/service/paxos/Paxos.java +++ b/src/java/org/apache/cassandra/service/paxos/Paxos.java @@ -35,6 +35,13 @@ import com.google.common.collect.Iterators; import com.google.common.collect.Maps; +import org.apache.cassandra.metrics.ClientRequestsMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetricsProvider; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.Type; +import org.apache.cassandra.service.QueryInfoTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,6 +60,7 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Keyspace; @@ -85,6 +93,9 @@ import org.apache.cassandra.service.CASRequest; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.FailureRecordingCallback.AsMap; +import org.apache.cassandra.service.Mutator; +import org.apache.cassandra.service.MutatorProvider; +import org.apache.cassandra.service.paxos.Commit.Agreed; import org.apache.cassandra.service.paxos.Commit.Proposal; import org.apache.cassandra.service.paxos.cleanup.PaxosRepairState; import org.apache.cassandra.service.reads.DataResolver; @@ -112,11 +123,6 @@ import static org.apache.cassandra.db.ConsistencyLevel.*; import static org.apache.cassandra.locator.InetAddressAndPort.Serializer.inetAddressAndPortSerializer; import static org.apache.cassandra.locator.ReplicaLayout.forTokenWriteLiveAndDown; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casReadMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casWriteMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetrics; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetricsMap; -import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetricsMap; import static org.apache.cassandra.service.paxos.Ballot.Flag.GLOBAL; import static org.apache.cassandra.service.paxos.Ballot.Flag.LOCAL; import static org.apache.cassandra.service.paxos.BallotGenerator.Global.nextBallot; @@ -425,7 +431,7 @@ void assureSufficientLiveNodes(boolean isWrite) throws UnavailableException { if (sizeOfConsensusQuorum > sizeOfPoll()) { - mark(isWrite, m -> m.unavailables, consistencyForConsensus); + mark(isWrite, m -> m.unavailables, consistencyForConsensus, ClientRequestsMetricsProvider.instance.metrics(keyspace.getName())); throw new UnavailableException("Cannot achieve consistency level " + consistencyForConsensus, consistencyForConsensus, sizeOfConsensusQuorum, sizeOfPoll()); } } @@ -548,11 +554,11 @@ private static int failureCount(Map fa /** * update relevant counters and throw the relevant exception */ - RequestExecutionException markAndThrowAsTimeoutOrFailure(boolean isWrite, ConsistencyLevel consistency, int failedAttemptsDueToContention) + RequestExecutionException markAndThrowAsTimeoutOrFailure(boolean isWrite, ConsistencyLevel consistency, int failedAttemptsDueToContention, ClientRequestsMetrics metrics) { if (isFailure) { - mark(isWrite, m -> m.failures, consistency); + mark(isWrite, m -> m.failures, consistency, metrics); throw serverError != null ? new RequestFailureException(ExceptionCode.SERVER_ERROR, serverError, consistency, successes, required, failures) : isWrite ? new WriteFailureException(consistency, successes, required, WriteType.CAS, failures) @@ -560,7 +566,7 @@ RequestExecutionException markAndThrowAsTimeoutOrFailure(boolean isWrite, Consis } else { - mark(isWrite, m -> m.timeouts, consistency); + mark(isWrite, m -> m.timeouts, consistency, metrics); throw isWrite ? new CasWriteTimeoutException(WriteType.CAS, consistency, successes, required, failedAttemptsDueToContention) : new ReadTimeoutException(consistency, successes, required, false); @@ -661,14 +667,25 @@ private static RowIterator cas(DecoratedKey partitionKey, SinglePartitionReadCommand readCommand = request.readCommand(FBUtilities.nowInSeconds()); TableMetadata metadata = readCommand.metadata(); - consistencyForConsensus.validateForCas(); - consistencyForCommit.validateForCasCommit(Keyspace.open(metadata.keyspace).getReplicationStrategy()); + // Register sensors for CAS operations so coordinator can aggregate replica sensor values + RequestSensors sensors = RequestTracker.instance.get(); + if (sensors != null) + { + Context context = Context.from(metadata); + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + } + + consistencyForConsensus.validateForCas(metadata.keyspace, clientState); + consistencyForCommit.validateForCasCommit(Keyspace.open(metadata.keyspace).getReplicationStrategy(), metadata.keyspace, clientState); Ballot minimumBallot = null; int failedAttemptsDueToContention = 0; + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(metadata.keyspace); try (PaxosOperationLock lock = PaxosState.lock(partitionKey, metadata, proposeDeadline, consistencyForConsensus, true)) { Paxos.Async commit = null; + Agreed committedAgreed = null; // the value behind 'commit', for the onCasCommitCompleted notification done: while (true) { // read the current values and check they validate the conditions @@ -693,7 +710,7 @@ private static RowIterator cas(DecoratedKey partitionKey, if (getPaxosVariant() == v2_without_linearizable_reads_or_rejected_writes) { Tracing.trace("CAS precondition rejected", current); - casWriteMetrics.conditionNotMet.inc(); + metrics.casWriteMetrics.conditionNotMet.inc(); return current.rowIterator(); } @@ -750,7 +767,7 @@ else if (begin.isPromised) default: throw new IllegalStateException(); case MAYBE_FAILURE: - throw propose.maybeFailure().markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + throw propose.maybeFailure().markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); case SUCCESS: { @@ -761,7 +778,12 @@ else if (begin.isPromised) // 1) reached a majority, in which case it was agreed, had no effect and we can do nothing; or // 2) did not reach a majority, was not agreed, and was not user visible as a result so we can ignore it if (!proposal.update.isEmpty()) - commit = commit(proposal.agreed(), participants, consistencyForConsensus, consistencyForCommit, true); + { + Agreed agreed = proposal.agreed(); + MutatorProvider.notifyCasCommit(agreed, consistencyForCommit, Mutator.CasCommitOrigin.CLIENT_OPERATION); + commit = commit(agreed, participants, consistencyForConsensus, consistencyForCommit, true); + committedAgreed = agreed; + } break done; } @@ -777,14 +799,14 @@ else if (begin.isPromised) // our proposal. We yield our uncertainty to the caller via timeout exception. // TODO: should return more useful result to client, and should also avoid this situation where possible throw new MaybeFailure(false, participants.sizeOfPoll(), participants.sizeOfConsensusQuorum, 0, emptyMap()) - .markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + .markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); case NO: minimumBallot = propose.superseded().by; // We have been superseded without our proposal being accepted by anyone, so we can safely retry Tracing.trace("Paxos proposal not accepted (pre-empted by a higher ballot)"); if (!waitForContention(proposeDeadline, ++failedAttemptsDueToContention, metadata, partitionKey, consistencyForConsensus, WRITE)) - throw MaybeFailure.noResponses(participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + throw MaybeFailure.noResponses(participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); } } } @@ -795,7 +817,20 @@ else if (begin.isPromised) { PaxosCommit.Status result = commit.awaitUntil(commitDeadline); if (!result.isSuccess()) - throw result.maybeFailure().markAndThrowAsTimeoutOrFailure(true, consistencyForCommit, failedAttemptsDueToContention); + { + // decided (agreed by a quorum) but the commit was not acknowledged in time: report the + // terminal as UNCONFIRMED before surfacing the failure to the client. The value may still + // be completed by a later operation or repair. + MutatorProvider.notifyCasCommitCompleted(committedAgreed, consistencyForCommit, Mutator.CasCommitOrigin.CLIENT_OPERATION, Mutator.CasCommitOutcome.UNCONFIRMED); + throw result.maybeFailure().markAndThrowAsTimeoutOrFailure(true, consistencyForCommit, failedAttemptsDueToContention, metrics); + } + // the commit reached a consistencyForCommit quorum: the value is now readable at that CL. + // Note that, unlike the v1 path in StorageProxy, we do not need to special case CL=ANY here: + // v1 does not block at all for ANY (it fires the commits off and returns), so it must suppress + // this notification; here we always await PaxosCommit, which requires blockForWrite(ANY) == 1 + // genuine replica acknowledgement (hints are not counted as accepts), so reaching this point + // means the commit really was applied on at least the replicas that consistencyForCommit requires. + MutatorProvider.notifyCasCommitCompleted(committedAgreed, consistencyForCommit, Mutator.CasCommitOrigin.CLIENT_OPERATION, Mutator.CasCommitOutcome.APPLIED); } Tracing.trace("CAS successful"); return null; @@ -807,20 +842,21 @@ else if (begin.isPromised) if (failedAttemptsDueToContention > 0) { - casWriteMetrics.contention.update(failedAttemptsDueToContention); + metrics.casWriteMetrics.contention.update(failedAttemptsDueToContention); openAndGetStore(metadata).metric.topCasPartitionContention.addSample(partitionKey.getKey(), failedAttemptsDueToContention); } - casWriteMetrics.addNano(latency); - writeMetricsMap.get(consistencyForConsensus).addNano(latency); + metrics.casWriteMetrics.executionTimeMetrics.addNano(latency); + metrics.writeMetricsForLevel(consistencyForConsensus).executionTimeMetrics.addNano(latency); } } private static RowIterator conditionNotMet(FilteredPartition read) { Tracing.trace("CAS precondition rejected", read); - casWriteMetrics.conditionNotMet.inc(); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(null); + metrics.casWriteMetrics.conditionNotMet.inc(); return read.rowIterator(); } @@ -847,6 +883,7 @@ private static PartitionIterator read(SinglePartitionReadCommand.Group group, Co int failedAttemptsDueToContention = 0; Ballot minimumBallot = null; SinglePartitionReadCommand read = group.queries.get(0); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(read.metadata().keyspace); try (PaxosOperationLock lock = PaxosState.lock(read.partitionKey(), read.metadata(), deadline, consistencyForConsensus, false)) { while (true) @@ -876,7 +913,7 @@ private static PartitionIterator read(SinglePartitionReadCommand.Group group, Co default: throw new IllegalStateException(); case MAYBE_FAILURE: - throw propose.maybeFailure().markAndThrowAsTimeoutOrFailure(false, consistencyForConsensus, failedAttemptsDueToContention); + throw propose.maybeFailure().markAndThrowAsTimeoutOrFailure(false, consistencyForConsensus, failedAttemptsDueToContention, metrics); case SUCCESS: return begin.readResponse; @@ -891,14 +928,14 @@ private static PartitionIterator read(SinglePartitionReadCommand.Group group, Co // our proposal. We yield our uncertainty to the caller via timeout exception. // TODO: should return more useful result to client, and should also avoid this situation where possible throw new MaybeFailure(false, begin.participants.sizeOfPoll(), begin.participants.sizeOfConsensusQuorum, 0, emptyMap()) - .markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + .markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); case NO: minimumBallot = propose.superseded().by; // We have been superseded without our proposal being accepted by anyone, so we can safely retry Tracing.trace("Paxos proposal not accepted (pre-empted by a higher ballot)"); if (!waitForContention(deadline, ++failedAttemptsDueToContention, group.metadata(), group.queries.get(0).partitionKey(), consistencyForConsensus, READ)) - throw MaybeFailure.noResponses(begin.participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + throw MaybeFailure.noResponses(begin.participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); } } } @@ -910,13 +947,15 @@ private static PartitionIterator read(SinglePartitionReadCommand.Group group, Co // client request. This is a measure of how long this specific individual read took, not total time since // processing of the client began. long latency = nanoTime() - start; - readMetrics.addNano(latency); - casReadMetrics.addNano(latency); - readMetricsMap.get(consistencyForConsensus).addNano(latency); + metrics.readMetrics.executionTimeMetrics.addNano(latency); + metrics.casReadMetrics.executionTimeMetrics.addNano(latency); + metrics.readMetricsForLevel(consistencyForConsensus).executionTimeMetrics.addNano(latency); TableMetadata table = read.metadata(); - Keyspace.open(table.keyspace).getColumnFamilyStore(table.name).metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS); + ColumnFamilyStore cfs = Keyspace.open(table.keyspace).getColumnFamilyStore(table.name); + cfs.metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS); + cfs.metric.coordinatorCasReadLatency.update(latency, TimeUnit.NANOSECONDS); if (failedAttemptsDueToContention > 0) - casReadMetrics.contention.update(failedAttemptsDueToContention); + metrics.casReadMetrics.contention.update(failedAttemptsDueToContention); } } @@ -977,11 +1016,34 @@ private static BeginResult begin(long deadline, Participants initialParticipants = Participants.get(query.metadata(), query.partitionKey(), consistencyForConsensus); initialParticipants.assureSufficientLiveNodes(isWrite); PaxosPrepare preparing = prepare(minimumBallot, initialParticipants, query, isWrite, acceptEarlyReadPermission); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(query.metadata().keyspace); + // A commit fused into the following prepare (commitAndPrepare, used to finish another proposer's + // round) has no separable ack; its fate is only known when that prepare resolves at the next + // awaitUntil below. Stash it here so we can deliver a terminal onCasCommitCompleted then. + Commit pendingFused = null; + Mutator.CasCommitOrigin pendingFusedOrigin = null; while (true) { // prepare PaxosPrepare retry = null; - PaxosPrepare.Status prepare = preparing.awaitUntil(deadline); + PaxosPrepare.Status prepare; + try + { + prepare = preparing.awaitUntil(deadline); + } + catch (Throwable t) + { + // the fused prepare (and its batched commit) did not resolve: report it as not confirmed + if (pendingFused != null) + MutatorProvider.notifyCasCommitCompleted(pendingFused, consistencyForConsensus, pendingFusedOrigin, Mutator.CasCommitOutcome.UNCONFIRMED); + throw t; + } + if (pendingFused != null) + { + MutatorProvider.notifyCasCommitCompleted(pendingFused, consistencyForConsensus, pendingFusedOrigin, fusedCommitOutcome(prepare.outcome)); + pendingFused = null; + pendingFusedOrigin = null; + } boolean isPromised = false; retry: switch (prepare.outcome) { @@ -991,7 +1053,10 @@ private static BeginResult begin(long deadline, { FoundIncompleteCommitted incomplete = prepare.incompleteCommitted(); Tracing.trace("Repairing replicas that missed the most recent commit"); + MutatorProvider.notifyCasCommit(incomplete.committed, consistencyForConsensus, Mutator.CasCommitOrigin.REFRESH_COMMITTED); retry = commitAndPrepare(incomplete.committed, incomplete.participants, query, isWrite, acceptEarlyReadPermission); + pendingFused = incomplete.committed; // terminal delivered when 'retry' resolves + pendingFusedOrigin = Mutator.CasCommitOrigin.REFRESH_COMMITTED; break; } case FOUND_INCOMPLETE_ACCEPTED: @@ -999,9 +1064,9 @@ private static BeginResult begin(long deadline, FoundIncompleteAccepted inProgress = prepare.incompleteAccepted(); Tracing.trace("Finishing incomplete paxos round {}", inProgress.accepted); if (isWrite) - casWriteMetrics.unfinishedCommit.inc(); + metrics.casWriteMetrics.unfinishedCommit.inc(); else - casReadMetrics.unfinishedCommit.inc(); + metrics.casReadMetrics.unfinishedCommit.inc(); // we DO NOT need to change the timestamp of this commit - either we or somebody else will finish it // and the original timestamp is correctly linearised. By not updatinig the timestamp we leave enough @@ -1016,11 +1081,17 @@ private static BeginResult begin(long deadline, default: throw new IllegalStateException(); case MAYBE_FAILURE: - throw proposeResult.maybeFailure().markAndThrowAsTimeoutOrFailure(isWrite, consistencyForConsensus, failedAttemptsDueToContention); + throw proposeResult.maybeFailure().markAndThrowAsTimeoutOrFailure(isWrite, consistencyForConsensus, failedAttemptsDueToContention, metrics); case SUCCESS: - retry = commitAndPrepare(repropose.agreed(), inProgress.participants, query, isWrite, acceptEarlyReadPermission); + { + Agreed reproposeAgreed = repropose.agreed(); + MutatorProvider.notifyCasCommit(reproposeAgreed, consistencyForConsensus, Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS); + retry = commitAndPrepare(reproposeAgreed, inProgress.participants, query, isWrite, acceptEarlyReadPermission); + pendingFused = reproposeAgreed; // terminal delivered when 'retry' resolves + pendingFusedOrigin = Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS; break retry; + } case SUPERSEDED: // since we are proposing a previous value that was maybe superseded by us before completion @@ -1036,7 +1107,7 @@ private static BeginResult begin(long deadline, Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting"); // sleep a random amount to give the other proposer a chance to finish if (!waitForContention(deadline, ++failedAttemptsDueToContention, query.metadata(), query.partitionKey(), consistencyForConsensus, isWrite ? WRITE : READ)) - throw MaybeFailure.noResponses(prepare.participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + throw MaybeFailure.noResponses(prepare.participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); retry = prepare(prepare.retryWithAtLeast(), prepare.participants, query, isWrite, acceptEarlyReadPermission); break; } @@ -1047,7 +1118,7 @@ private static BeginResult begin(long deadline, // round's proposal (if any). PaxosPrepare.Success success = prepare.success(); - DataResolver resolver = new DataResolver(query, success.participants, NoopReadRepair.instance, new Dispatcher.RequestTime(query.creationTimeNanos())); + DataResolver resolver = new DataResolver(query, success.participants, NoopReadRepair.instance, new Dispatcher.RequestTime(query.creationTimeNanos()), QueryInfoTracker.ReadTracker.NOOP); for (int i = 0 ; i < success.responses.size() ; ++i) resolver.preprocess(success.responses.get(i)); @@ -1069,7 +1140,7 @@ class WasRun implements Runnable { boolean v; public void run() { v = true; } } } case MAYBE_FAILURE: - throw prepare.maybeFailure().markAndThrowAsTimeoutOrFailure(isWrite, consistencyForConsensus, failedAttemptsDueToContention); + throw prepare.maybeFailure().markAndThrowAsTimeoutOrFailure(isWrite, consistencyForConsensus, failedAttemptsDueToContention, metrics); case ELECTORATE_MISMATCH: Participants participants = Participants.get(query.metadata(), query.partitionKey(), consistencyForConsensus); @@ -1084,7 +1155,7 @@ class WasRun implements Runnable { boolean v; public void run() { v = true; } } Tracing.trace("Some replicas have already promised a higher ballot than ours; retrying"); // sleep a random amount to give the other proposer a chance to finish if (!waitForContention(deadline, ++failedAttemptsDueToContention, query.metadata(), query.partitionKey(), consistencyForConsensus, isWrite ? WRITE : READ)) - throw MaybeFailure.noResponses(prepare.participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention); + throw MaybeFailure.noResponses(prepare.participants).markAndThrowAsTimeoutOrFailure(true, consistencyForConsensus, failedAttemptsDueToContention, metrics); retry = prepare(prepare.retryWithAtLeast(), prepare.participants, query, isWrite, acceptEarlyReadPermission); } @@ -1100,6 +1171,29 @@ public static boolean isInRangeAndShouldProcess(InetAddressAndPort from, Decorat ).contains(getBroadcastAddressAndPort()); } + /** + * Maps the outcome of a fused commit-and-prepare (the prepare that carried a batched commit) to the + * terminal {@link Mutator.CasCommitOutcome} for that commit. A promise-quorum outcome + * (PROMISED/READ_PERMITTED, or FOUND_INCOMPLETE_* which likewise require a quorum of promises) implies + * the batched commit reached that quorum, because each replica applies the commit before answering the + * prepare; SUPERSEDED means pre-empted before a quorum; anything else is not confirmed. + */ + private static Mutator.CasCommitOutcome fusedCommitOutcome(PaxosPrepare.Status.Outcome outcome) + { + switch (outcome) + { + case PROMISED: + case READ_PERMITTED: + case FOUND_INCOMPLETE_ACCEPTED: + case FOUND_INCOMPLETE_COMMITTED: + return Mutator.CasCommitOutcome.CONFIRMED_BY_PREPARE; + case SUPERSEDED: + return Mutator.CasCommitOutcome.SUPERSEDED; + default: // MAYBE_FAILURE, ELECTORATE_MISMATCH + return Mutator.CasCommitOutcome.UNCONFIRMED; + } + } + static ConsistencyLevel nonSerial(ConsistencyLevel serial) { switch (serial) @@ -1110,17 +1204,17 @@ static ConsistencyLevel nonSerial(ConsistencyLevel serial) } } - private static void mark(boolean isWrite, Function toMark, ConsistencyLevel consistency) + private static void mark(boolean isWrite, Function toMark, ConsistencyLevel consistency, ClientRequestsMetrics metrics) { if (isWrite) { - toMark.apply(casWriteMetrics).mark(); - toMark.apply(writeMetricsMap.get(consistency)).mark(); + toMark.apply(metrics.casWriteMetrics).mark(); + toMark.apply(metrics.writeMetricsForLevel(consistency)).mark(); } else { - toMark.apply(casReadMetrics).mark(); - toMark.apply(readMetricsMap.get(consistency)).mark(); + toMark.apply(metrics.casReadMetrics).mark(); + toMark.apply(metrics.readMetricsForLevel(consistency)).mark(); } } diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java b/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java index 1821ec4004fd..d1ac4feb34c0 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java @@ -38,6 +38,13 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.paxos.Paxos.Participants; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.concurrent.ConditionAsConsumer; @@ -125,6 +132,11 @@ public PaxosCommit(Agreed commit, boolean allowHints, ConsistencyLevel consisten onDone.accept(status()); } + public TableMetadata getTableMetadata() + { + return commit.update.metadata(); + } + /** * Submit the proposal for commit with all replicas, and wait synchronously until at most {@code deadline} for the result */ @@ -304,12 +316,33 @@ public static class RequestHandler implements IVerbHandler @Override public void doVerb(Message message) { + // Initialize the sensor and set ExecutorLocals + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(message.payload.update.metadata().keyspace); + Context context = Context.from(message.payload.update.metadata()); + + // Commit phase writes the proposal to the table, so a read sensor is registered in addition to the write sensor + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + sensors.registerSensor(context, Type.INTERNODE_BYTES); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + RequestTracker.instance.set(sensors); + NoPayload response = execute(message.payload, message.from()); - // NOTE: for correctness, this must be our last action, so that we cannot throw an error and send both a response and a failure response - if (response == null) - MessagingService.instance().respondWithFailure(UNKNOWN, message); + + // calculate outbound internode bytes before adding the sensor to the response + if (response != null) + { + Message.Builder reply = message.responseWithBuilder(response); + int size = reply.currentPayloadSize(MessagingService.current_version); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, size); + sensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(sensors, reply); + MessagingService.instance().send(reply.build(), message.from()); + } else - MessagingService.instance().respond(response, message); + { + MessagingService.instance().respondWithFailure(UNKNOWN, message); + } } private static NoPayload execute(Agreed agreed, InetAddressAndPort from) diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java b/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java index c0a2353e1f19..6554e7c71fb4 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java @@ -52,6 +52,12 @@ import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.paxos.PaxosPrepare.Status.Outcome; import org.apache.cassandra.tracing.Tracing; @@ -312,6 +318,11 @@ private ElectorateMismatch(Participants participants, Ballot ballot) this.onDone = onDone; } + public TableMetadata getTableMetadata() + { + return request.table; + } + private boolean hasInProgressProposal() { // no need to commit a no-op; either it @@ -1021,11 +1032,33 @@ public static class RequestHandler implements IVerbHandler @Override public void doVerb(Message message) { + // Initialize the sensor and set ExecutorLocals + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(message.payload.table.keyspace); + Context context = Context.from(message.payload.table); + + // Prepare phase incorporates a read to check the cas condition, so a read sensor is registered in addition to the write sensor + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + sensors.registerSensor(context, Type.INTERNODE_BYTES); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + RequestTracker.instance.set(sensors); + Response response = execute(message.payload, message.from()); - if (response == null) - MessagingService.instance().respondWithFailure(UNKNOWN, message); + + // calculate outbound internode bytes before adding the sensor to the response + if (response != null) + { + Message.Builder reply = message.responseWithBuilder(response); + int size = reply.currentPayloadSize(MessagingService.current_version); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, size); + sensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(sensors, reply); + MessagingService.instance().send(reply.build(), message.from()); + } else - MessagingService.instance().respond(response, message); + { + MessagingService.instance().respondWithFailure(UNKNOWN, message); + } } static Response execute(AbstractRequest request, InetAddressAndPort from) diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosPropose.java b/src/java/org/apache/cassandra/service/paxos/PaxosPropose.java index 57d3459f4030..2f54941ec694 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosPropose.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosPropose.java @@ -37,6 +37,13 @@ import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; import org.apache.cassandra.service.paxos.Commit.Proposal; import org.apache.cassandra.utils.concurrent.ConditionAsConsumer; @@ -155,6 +162,11 @@ private PaxosPropose(Proposal proposal, int participants, int required, boolean this.onDone = onDone; } + public TableMetadata getTableMetadata() + { + return proposal.update.metadata(); + } + /** * Submit the proposal for commit with all replicas, and return an object that can be waited on synchronously for the result, * or for the present status if the time elapses without a final result being reached. @@ -408,11 +420,33 @@ public static class RequestHandler implements IVerbHandler @Override public void doVerb(Message message) { + // Initialize the sensor and set ExecutorLocals + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(message.payload.proposal.update.metadata().keyspace); + Context context = Context.from(message.payload.proposal.update.metadata()); + + // Propose phase consults the Paxos table for more recent promises, so a read sensor is registered in addition to the write sensor + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + sensors.registerSensor(context, Type.INTERNODE_BYTES); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + RequestTracker.instance.set(sensors); + Response response = execute(message.payload.proposal, message.from()); - if (response == null) - MessagingService.instance().respondWithFailure(UNKNOWN, message); + + // calculate outbound internode bytes before adding the sensor to the response + if (response != null) + { + Message.Builder reply = message.responseWithBuilder(response); + int size = reply.currentPayloadSize(MessagingService.current_version); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, size); + sensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(sensors, reply); + MessagingService.instance().send(reply.build(), message.from()); + } else - MessagingService.instance().respond(response, message); + { + MessagingService.instance().respondWithFailure(UNKNOWN, message); + } } public static Response execute(Proposal proposal, InetAddressAndPort from) diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java b/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java index ae5bc557c749..77eb7b479ab3 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java @@ -56,6 +56,8 @@ import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.Mutator; +import org.apache.cassandra.service.MutatorProvider; import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.FBUtilities; @@ -243,10 +245,11 @@ private State execute() // we have a new enough commit, but it might not have reached enough participants; make sure it has before terminating // note: we could send to only those we know haven't witnessed it, but this is a rare operation so a small amount of redundant work is fine - return oldestCommitted.equals(latestCommitted.ballot) - ? DONE - : PaxosCommit.commit(latestCommitted, participants, paxosConsistency, commitConsistency(), true, - new CommittingRepair()); + if (oldestCommitted.equals(latestCommitted.ballot)) + return DONE; + MutatorProvider.notifyCasCommit(latestCommitted, commitConsistency(), Mutator.CasCommitOrigin.REFRESH_COMMITTED); + return PaxosCommit.commit(latestCommitted, participants, paxosConsistency, commitConsistency(), true, + new CommittingRepair(latestCommitted, Mutator.CasCommitOrigin.REFRESH_COMMITTED)); } else if (isAcceptedButNotCommitted && !isPromisedButNotAccepted && !reproposalMayBeRejected) { @@ -329,8 +332,9 @@ public State execute(Status input) throws Throwable // finish the in-progress commit FoundIncompleteCommitted incomplete = input.incompleteCommitted(); logger.trace("PaxosRepair of {} found in progress {}", partitionKey(), incomplete.committed); + MutatorProvider.notifyCasCommit(incomplete.committed, commitConsistency(), Mutator.CasCommitOrigin.REFRESH_COMMITTED); return PaxosCommit.commit(incomplete.committed, participants, paxosConsistency, commitConsistency(), true, - new CommitAndRestart()); // we don't know if we're done, so we must restart + new CommitAndRestart(incomplete.committed, Mutator.CasCommitOrigin.REFRESH_COMMITTED)); // we don't know if we're done, so we must restart } case PROMISED: @@ -377,8 +381,10 @@ public State execute(PaxosPropose.Status input) } logger.trace("PaxosRepair of {} committing successful proposal {}", partitionKey(), proposal); - return PaxosCommit.commit(proposal.agreed(), participants, paxosConsistency, commitConsistency(), true, - new CommittingRepair()); + Agreed agreed = proposal.agreed(); + MutatorProvider.notifyCasCommit(agreed, commitConsistency(), Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS); + return PaxosCommit.commit(agreed, participants, paxosConsistency, commitConsistency(), true, + new CommittingRepair(agreed, Mutator.CasCommitOrigin.REPAIR_IN_PROGRESS)); default: throw new IllegalStateException(); @@ -388,19 +394,43 @@ public State execute(PaxosPropose.Status input) private class CommittingRepair extends ConsumerState { + private final Agreed committed; + private final Mutator.CasCommitOrigin origin; + + private CommittingRepair(Agreed committed, Mutator.CasCommitOrigin origin) + { + this.committed = committed; + this.origin = origin; + } + @Override public State execute(PaxosCommit.Status input) { logger.trace("PaxosRepair of {} {}", partitionKey(), input); - return input.isSuccess() ? DONE : retry(this); + if (!input.isSuccess()) + return retry(this); + // the commit reached a commitConsistency() quorum: the recovered value is now readable + MutatorProvider.notifyCasCommitCompleted(committed, commitConsistency(), origin, Mutator.CasCommitOutcome.APPLIED); + return DONE; } } private class CommitAndRestart extends ConsumerState { + private final Agreed committed; + private final Mutator.CasCommitOrigin origin; + + private CommitAndRestart(Agreed committed, Mutator.CasCommitOrigin origin) + { + this.committed = committed; + this.origin = origin; + } + @Override public State execute(PaxosCommit.Status input) { + if (input.isSuccess()) + MutatorProvider.notifyCasCommitCompleted(committed, commitConsistency(), origin, Mutator.CasCommitOutcome.APPLIED); return restart(this); } } diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosState.java b/src/java/org/apache/cassandra/service/paxos/PaxosState.java index a3f019e4bf25..b3b4dae5dae8 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosState.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosState.java @@ -1,5 +1,5 @@ /* - * + * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -7,16 +7,16 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.service.paxos; @@ -26,9 +26,9 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BiConsumer; import java.util.function.Function; - import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.function.Consumer; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -37,12 +37,19 @@ import com.github.benmanes.caffeine.cache.Caffeine; import org.apache.cassandra.concurrent.ImmediateExecutor; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.*; -import org.apache.cassandra.metrics.PaxosMetrics; -import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.WriteOptions; +import org.apache.cassandra.db.WriteType; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.RequestTimeoutException; import org.apache.cassandra.exceptions.WriteTimeoutException; +import org.apache.cassandra.metrics.PaxosMetrics; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.paxos.uncommitted.PaxosBallotTracker; import org.apache.cassandra.service.paxos.uncommitted.PaxosStateTracker; import org.apache.cassandra.service.paxos.uncommitted.PaxosUncommittedTracker; @@ -51,15 +58,23 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.cassandra.config.CassandraRelevantProperties.PAXOS_DISABLE_COORDINATOR_LOCKING; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.config.Config.PaxosStatePurging.gc_grace; import static org.apache.cassandra.config.Config.PaxosStatePurging.legacy; import static org.apache.cassandra.config.DatabaseDescriptor.paxosStatePurging; -import static org.apache.cassandra.service.paxos.Commit.*; -import static org.apache.cassandra.service.paxos.PaxosState.MaybePromise.Outcome.*; +import static org.apache.cassandra.service.paxos.Commit.Accepted; import static org.apache.cassandra.service.paxos.Commit.Accepted.latestAccepted; +import static org.apache.cassandra.service.paxos.Commit.AcceptedWithTTL; +import static org.apache.cassandra.service.paxos.Commit.Agreed; +import static org.apache.cassandra.service.paxos.Commit.Committed; import static org.apache.cassandra.service.paxos.Commit.Committed.latestCommitted; +import static org.apache.cassandra.service.paxos.Commit.CommittedWithTTL; +import static org.apache.cassandra.service.paxos.Commit.Proposal; import static org.apache.cassandra.service.paxos.Commit.isAfter; +import static org.apache.cassandra.service.paxos.Commit.latest; +import static org.apache.cassandra.service.paxos.PaxosState.MaybePromise.Outcome.PERMIT_READ; +import static org.apache.cassandra.service.paxos.PaxosState.MaybePromise.Outcome.PROMISE; +import static org.apache.cassandra.service.paxos.PaxosState.MaybePromise.Outcome.REJECT; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; /** * We save to memory the result of each operation before persisting to disk, however each operation that performs @@ -412,7 +427,7 @@ public static PaxosOperationLock lock(DecoratedKey partitionKey, TableMetadata m throw t; } } - + private static RequestTimeoutException throwTimeout(TableMetadata metadata, ConsistencyLevel consistencyForConsensus, boolean isWrite) { int blockFor = consistencyForConsensus.blockFor(Keyspace.open(metadata.keyspace).getReplicationStrategy()); @@ -664,14 +679,19 @@ public Ballot acceptIfLatest(Proposal proposal) public void commit(Agreed commit) { - applyCommit(commit, this, (apply, to) -> + applyCommit(commit, this, c -> {}, (apply, to) -> currentUpdater.accumulateAndGet(to, new UnsafeSnapshot(apply), Snapshot::merge) ); } public static void commitDirect(Commit commit) { - applyCommit(commit, null, (apply, ignore) -> { + commitDirect(commit, c -> {}); + } + + public static void commitDirect(Commit commit, Consumer callback) + { + applyCommit(commit, null, callback, (apply, ignore) -> { try (PaxosState state = tryGetUnsafe(apply.update.partitionKey(), apply.update.metadata())) { if (state != null) @@ -680,7 +700,7 @@ public static void commitDirect(Commit commit) }); } - private static void applyCommit(Commit commit, PaxosState state, BiConsumer postCommit) + private static void applyCommit(Commit commit, PaxosState state, Consumer callback, BiConsumer postCommit) { if (paxosStatePurging() == legacy && !(commit instanceof CommittedWithTTL)) commit = CommittedWithTTL.withDefaultTTL(commit); @@ -695,7 +715,8 @@ private static void applyCommit(Commit commit, PaxosState state, BiConsumer 0 ? left : right; } - static class Reducer extends MergeIterator.Reducer + static class Reducer extends org.apache.cassandra.utils.Reducer { private PaxosKeyState mostRecent = null; @@ -127,12 +127,12 @@ public void reduce(int idx, PaxosKeyState current) mostRecent = merge(mostRecent, current); } - protected PaxosKeyState getReduced() + public PaxosKeyState getReduced() { return mostRecent; } - protected void onKeyChange() + public void onKeyChange() { super.onKeyChange(); mostRecent = null; @@ -141,7 +141,7 @@ protected void onKeyChange() public static CloseableIterator mergeUncommitted(CloseableIterator... iterators) { - return MergeIterator.get(Lists.newArrayList(iterators), PaxosKeyState.KEY_COMPARATOR, new Reducer()); + return MergeIterator.getCloseable(Lists.newArrayList(iterators), PaxosKeyState.KEY_COMPARATOR, new Reducer()); } public static CloseableIterator toUncommittedInfo(CloseableIterator iter) diff --git a/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java b/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java index 744dd4d07dc3..1649641f746a 100644 --- a/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java +++ b/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java @@ -225,7 +225,7 @@ void truncate() } } - private static class Reducer extends MergeIterator.Reducer + private static class Reducer extends org.apache.cassandra.utils.Reducer { PaxosKeyState merged = null; @@ -234,12 +234,12 @@ public void reduce(int idx, PaxosKeyState current) merged = PaxosKeyState.merge(merged, current); } - protected PaxosKeyState getReduced() + public PaxosKeyState getReduced() { return merged; } - protected void onKeyChange() + public void onKeyChange() { merged = null; } @@ -256,7 +256,7 @@ private static CloseableIterator merge(Collection implements RequestCallback protected final CountDownLatch latch; protected final int targets; + private final TableMetadata metadata; private final ConsistencyLevel consistency; private final Dispatcher.RequestTime requestTime; - public AbstractPaxosCallback(int targets, ConsistencyLevel consistency, Dispatcher.RequestTime requestTime) + private final RequestSensors requestSensors; + + public AbstractPaxosCallback(TableMetadata metadata, int targets, ConsistencyLevel consistency, Dispatcher.RequestTime requestTime) { + this.metadata = metadata; this.targets = targets; this.consistency = consistency; latch = newCountDownLatch(targets); this.requestTime = requestTime; + this.requestSensors = RequestTracker.instance.get(); + } + + @Override + public RequestSensors getRequestSensors() + { + return requestSensors; } public int getResponseCount() @@ -57,6 +71,11 @@ public int getResponseCount() return targets - latch.count(); } + public TableMetadata getMetadata() + { + return metadata; + } + public void await() throws WriteTimeoutException { try diff --git a/src/java/org/apache/cassandra/service/paxos/v1/PrepareCallback.java b/src/java/org/apache/cassandra/service/paxos/v1/PrepareCallback.java index 717acf4ab58f..d5086fa52646 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/PrepareCallback.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/PrepareCallback.java @@ -54,7 +54,7 @@ public class PrepareCallback extends AbstractPaxosCallback public PrepareCallback(DecoratedKey key, TableMetadata metadata, int targets, ConsistencyLevel consistency, Dispatcher.RequestTime requestTime) { - super(targets, consistency, requestTime); + super(metadata, targets, consistency, requestTime); // need to inject the right key in the empty commit so comparing with empty commits in the response works as expected mostRecentCommit = Commit.emptyCommit(key, metadata); mostRecentInProgressCommit = Commit.emptyCommit(key, metadata); diff --git a/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java index b31900ea40f5..df69abe89214 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java @@ -19,9 +19,15 @@ package org.apache.cassandra.service.paxos.v1; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.sensors.RequestTracker; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.service.paxos.PrepareResponse; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; public class PrepareVerbHandler extends AbstractPaxosVerbHandler { @@ -35,7 +41,24 @@ public static PrepareResponse doPrepare(Commit toPrepare) @Override public void processMessage(Message message) { - Message reply = message.responseWith(doPrepare(message.payload)); - MessagingService.instance().send(reply, message.from()); + // Initialize the sensor and set ExecutorLocals + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(message.payload.update.metadata().keyspace); + Context context = Context.from(message.payload.update.metadata()); + + // Prepare phase incorporates a read to check the cas condition, so a read sensor is registered in addition to the write sensor + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + sensors.registerSensor(context, Type.INTERNODE_BYTES); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + RequestTracker.instance.set(sensors); + + Message.Builder reply = message.responseWithBuilder(doPrepare(message.payload)); + + // calculate outbound internode bytes before adding the sensor to the response + int size = reply.currentPayloadSize(MessagingService.current_version); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, size); + sensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(sensors, reply); + MessagingService.instance().send(reply.build(), message.from()); } } diff --git a/src/java/org/apache/cassandra/service/paxos/v1/ProposeCallback.java b/src/java/org/apache/cassandra/service/paxos/v1/ProposeCallback.java index 2d83644e07b0..e1426f24806e 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/ProposeCallback.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/ProposeCallback.java @@ -28,6 +28,7 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.Nemesis; +import org.apache.cassandra.schema.TableMetadata; /** * ProposeCallback has two modes of operation, controlled by the failFast parameter. @@ -50,13 +51,14 @@ public class ProposeCallback extends AbstractPaxosCallback private final int requiredAccepts; private final boolean failFast; - public ProposeCallback(int totalTargets, int requiredTargets, boolean failFast, ConsistencyLevel consistency, Dispatcher.RequestTime requestTime) + public ProposeCallback(TableMetadata metadata, int totalTargets, int requiredTargets, boolean failFast, ConsistencyLevel consistency, Dispatcher.RequestTime requestTime) { - super(totalTargets, consistency, requestTime); + super(metadata, totalTargets, consistency, requestTime); this.requiredAccepts = requiredTargets; this.failFast = failFast; } + @Override public void onResponse(Message msg) { logger.trace("Propose response {} from {}", msg.payload, msg.from()); diff --git a/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java index d3069a290c37..ca3b98340278 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java @@ -20,8 +20,14 @@ import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.sensors.RequestTracker; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.PaxosState; +import org.apache.cassandra.sensors.Context; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.SensorsCustomParams; +import org.apache.cassandra.sensors.SensorsFactory; +import org.apache.cassandra.sensors.Type; public class ProposeVerbHandler extends AbstractPaxosVerbHandler implements IVerbHandler { @@ -35,8 +41,24 @@ public static Boolean doPropose(Commit proposal) @Override void processMessage(Message message) { - Boolean response = doPropose(message.payload); - Message reply = message.responseWith(response); - MessagingService.instance().send(reply, message.from()); + // Initialize the sensor and set ExecutorLocals + RequestSensors sensors = SensorsFactory.instance.createRequestSensors(message.payload.update.metadata().keyspace); + Context context = Context.from(message.payload.update.metadata()); + + // Propose phase consults the Paxos table for more recent promises, so a read sensor is registered in addition to the write sensor + sensors.registerSensor(context, Type.READ_BYTES); + sensors.registerSensor(context, Type.WRITE_BYTES); + sensors.registerSensor(context, Type.INTERNODE_BYTES); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, message.payloadSize(MessagingService.current_version)); + RequestTracker.instance.set(sensors); + + Message.Builder reply = message.responseWithBuilder(doPropose(message.payload)); + + // calculate outbound internode bytes before adding the sensor to the response + int size = reply.currentPayloadSize(MessagingService.current_version); + sensors.incrementSensor(context, Type.INTERNODE_BYTES, size); + sensors.syncAllSensors(); + SensorsCustomParams.addSensorsToInternodeResponse(sensors, reply); + MessagingService.instance().send(reply.build(), message.from()); } } diff --git a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java index 967673482437..f30fc0d0f8cb 100644 --- a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java +++ b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java @@ -16,7 +16,6 @@ * limitations under the License. */ package org.apache.cassandra.service.reads; - import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,8 +38,10 @@ import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.locator.ReplicaPlans; +import org.apache.cassandra.metrics.ReadCoordinationMetrics; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.service.StorageProxy.LocalReadRunnable; import org.apache.cassandra.service.reads.repair.ReadRepair; import org.apache.cassandra.tracing.TraceState; @@ -76,20 +77,30 @@ public abstract class AbstractReadExecutor private final int initialDataRequestCount; protected volatile PartitionIterator result = null; - - AbstractReadExecutor(ColumnFamilyStore cfs, ReadCommand command, ReplicaPlan.ForTokenRead replicaPlan, int initialDataRequestCount, Dispatcher.RequestTime requestTime) + protected final QueryInfoTracker.ReadTracker readTracker; + static + { + MessagingService.instance().latencySubscribers.subscribe(ReadCoordinationMetrics::updateReplicaLatency); + } + + AbstractReadExecutor(ColumnFamilyStore cfs, + ReadCommand command, + ReplicaPlan.ForTokenRead replicaPlan, + int initialDataRequestCount, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { this.command = command; this.replicaPlan = ReplicaPlan.shared(replicaPlan); this.initialDataRequestCount = initialDataRequestCount; // the ReadRepair and DigestResolver both need to see our updated this.readRepair = ReadRepair.create(command, this.replicaPlan, requestTime); - this.digestResolver = new DigestResolver<>(command, this.replicaPlan, requestTime); + this.digestResolver = new DigestResolver<>(command, this.replicaPlan, requestTime, readTracker); this.handler = new ReadCallback<>(digestResolver, command, this.replicaPlan, requestTime); this.cfs = cfs; this.traceState = Tracing.instance.get(); this.requestTime = requestTime; - + this.readTracker = readTracker; // Set the digest version (if we request some digests). This is the smallest version amongst all our target replicas since new nodes // knows how to produce older digest but the reverse is not true. @@ -99,6 +110,8 @@ public abstract class AbstractReadExecutor for (Replica replica : replicaPlan.contacts()) digestVersion = Math.min(digestVersion, MessagingService.instance().versions.get(replica.endpoint())); command.setDigestVersion(digestVersion); + + readTracker.onReplicaPlan(replicaPlan); } public DecoratedKey getKey() @@ -167,8 +180,9 @@ private void makeRequests(ReadCommand readCommand, Iterable replicas) } /** - * Perform additional requests if it looks like the original will time out. May block while it waits - * to see if the original requests are answered first. + * Perform additional requests if it looks like the original takes "too much time", as defined + * by the subclass. + * May block while it waits to see if the original requests are answered first. */ public abstract void maybeTryAdditionalReplicas(); @@ -187,7 +201,10 @@ public void executeAsync() /** * @return an executor appropriate for the configured speculative read policy */ - public static AbstractReadExecutor getReadExecutor(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) throws UnavailableException + public static AbstractReadExecutor getReadExecutor(SinglePartitionReadCommand command, + ConsistencyLevel consistencyLevel, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) throws UnavailableException { Keyspace keyspace = Keyspace.open(command.metadata().keyspace); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id); @@ -199,23 +216,32 @@ public static AbstractReadExecutor getReadExecutor(SinglePartitionReadCommand co consistencyLevel, retry); + if (replicaPlan.readCandidates().stream().noneMatch(replica -> replica.endpoint().equals(FBUtilities.getBroadcastAddressAndPort()))) + { + ReadCoordinationMetrics.nonreplicaRequests.inc(); + } + else if (replicaPlan.contacts().stream().noneMatch(replica -> replica.endpoint().equals(FBUtilities.getBroadcastAddressAndPort()))) + { + ReadCoordinationMetrics.preferredOtherReplicas.inc(); + } + // Speculative retry is disabled *OR* // 11980: Disable speculative retry if using EACH_QUORUM in order to prevent miscounting DC responses if (retry.equals(NeverSpeculativeRetryPolicy.INSTANCE) || consistencyLevel == ConsistencyLevel.EACH_QUORUM) - return new NeverSpeculatingReadExecutor(cfs, command, replicaPlan, requestTime, false); + return new NeverSpeculatingReadExecutor(cfs, command, replicaPlan, requestTime, false, readTracker); // There are simply no extra replicas to speculate. // Handle this separately so it can record failed attempts to speculate due to lack of replicas if (replicaPlan.contacts().size() == replicaPlan.readCandidates().size()) { boolean recordFailedSpeculation = consistencyLevel != ConsistencyLevel.ALL; - return new NeverSpeculatingReadExecutor(cfs, command, replicaPlan, requestTime, recordFailedSpeculation); + return new NeverSpeculatingReadExecutor(cfs, command, replicaPlan, requestTime, recordFailedSpeculation, readTracker); } if (retry.equals(AlwaysSpeculativeRetryPolicy.INSTANCE)) - return new AlwaysSpeculatingReadExecutor(cfs, command, replicaPlan, requestTime); + return new AlwaysSpeculatingReadExecutor(cfs, command, replicaPlan, requestTime, readTracker); else // PERCENTILE or CUSTOM. - return new SpeculatingReadExecutor(cfs, command, replicaPlan, requestTime); + return new SpeculatingReadExecutor(cfs, command, replicaPlan, requestTime, readTracker); } public boolean hasLocalRead() @@ -268,13 +294,9 @@ public static class NeverSpeculatingReadExecutor extends AbstractReadExecutor */ private final boolean logFailedSpeculation; - public NeverSpeculatingReadExecutor(ColumnFamilyStore cfs, - ReadCommand command, - ReplicaPlan.ForTokenRead replicaPlan, - Dispatcher.RequestTime requestTime, - boolean logFailedSpeculation) + public NeverSpeculatingReadExecutor(ColumnFamilyStore cfs, ReadCommand command, ReplicaPlan.ForTokenRead replicaPlan, Dispatcher.RequestTime requestTime, boolean logFailedSpeculation, QueryInfoTracker.ReadTracker readTracker) { - super(cfs, command, replicaPlan, 1, requestTime); + super(cfs, command, replicaPlan, 1, requestTime, readTracker); this.logFailedSpeculation = logFailedSpeculation; } @@ -294,12 +316,13 @@ static class SpeculatingReadExecutor extends AbstractReadExecutor public SpeculatingReadExecutor(ColumnFamilyStore cfs, ReadCommand command, ReplicaPlan.ForTokenRead replicaPlan, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { // We're hitting additional targets for read repair (??). Since our "extra" replica is the least- // preferred by the snitch, we do an extra data read to start with against a replica more // likely to respond; better to let RR fail than the entire query. - super(cfs, command, replicaPlan, replicaPlan.readQuorum() < replicaPlan.contacts().size() ? 2 : 1, requestTime); + super(cfs, command, replicaPlan, replicaPlan.readQuorum() < replicaPlan.contacts().size() ? 2 : 1, requestTime, readTracker); } public void maybeTryAdditionalReplicas() @@ -365,11 +388,12 @@ private static class AlwaysSpeculatingReadExecutor extends AbstractReadExecutor public AlwaysSpeculatingReadExecutor(ColumnFamilyStore cfs, ReadCommand command, ReplicaPlan.ForTokenRead replicaPlan, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { // presumably, we speculate an extra data request here in case it is our data request that fails to respond, // and there are no more nodes to consult - super(cfs, command, replicaPlan, replicaPlan.contacts().size() > 1 ? 2 : 1, requestTime); + super(cfs, command, replicaPlan, replicaPlan.contacts().size() > 1 ? 2 : 1, requestTime, readTracker); } public void maybeTryAdditionalReplicas() @@ -436,7 +460,7 @@ public void awaitResponses(boolean logBlockingReadRepairAttempt) throws ReadTime if (logBlockingReadRepairAttempt) { logger.info("Blocking Read Repair triggered for query [{}] at CL.{} with endpoints {}", - command.toCQLString(), + command.toRedactedCQLString(), replicaPlan().consistencyLevel(), replicaPlan().contacts()); } diff --git a/src/java/org/apache/cassandra/service/reads/DataResolver.java b/src/java/org/apache/cassandra/service/reads/DataResolver.java index 332a78570851..7efec3300a05 100644 --- a/src/java/org/apache/cassandra/service/reads/DataResolver.java +++ b/src/java/org/apache/cassandra/service/reads/DataResolver.java @@ -50,8 +50,10 @@ import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.net.Message; +import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.reads.repair.NoopReadRepair; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.service.reads.repair.ReadRepair; import org.apache.cassandra.service.reads.repair.RepairedDataTracker; import org.apache.cassandra.service.reads.repair.RepairedDataVerifier; @@ -64,18 +66,24 @@ public class DataResolver, P extends ReplicaPlan.ForRead< private final boolean enforceStrictLiveness; private final ReadRepair readRepair; private final boolean trackRepairedStatus; + protected final QueryInfoTracker.ReadTracker readTracker; - public DataResolver(ReadCommand command, Supplier replicaPlan, ReadRepair readRepair, Dispatcher.RequestTime requestTime) + public DataResolver(ReadCommand command, + Supplier replicaPlan, + ReadRepair readRepair, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { - this(command, replicaPlan, readRepair, requestTime, false); + this(command, replicaPlan, readRepair, requestTime, false, readTracker); } - public DataResolver(ReadCommand command, Supplier replicaPlan, ReadRepair readRepair, Dispatcher.RequestTime requestTime, boolean trackRepairedStatus) + public DataResolver(ReadCommand command, Supplier replicaPlan, ReadRepair readRepair, Dispatcher.RequestTime requestTime, boolean trackRepairedStatus, QueryInfoTracker.ReadTracker readTracker) { super(command, replicaPlan, requestTime); this.enforceStrictLiveness = command.metadata().enforceStrictLiveness(); this.readRepair = readRepair; this.trackRepairedStatus = trackRepairedStatus; + this.readTracker = readTracker; } public PartitionIterator getData() @@ -119,17 +127,19 @@ public PartitionIterator resolve(@Nullable Runnable runOnShortRead) }); } - if (usesReplicaFilteringProtection()) - return resolveWithReplicaFilteringProtection(replicas, repairedDataTracker); + if (!needsReplicaFilteringProtection()) + { + ResolveContext context = new ResolveContext(replicas, true); + return resolveWithReadRepair(context, + i -> shortReadProtectedResponse(i, context, runOnShortRead), + UnaryOperator.identity(), + repairedDataTracker); + } - ResolveContext context = new ResolveContext(replicas, true); - return resolveWithReadRepair(context, - i -> shortReadProtectedResponse(i, context, runOnShortRead), - UnaryOperator.identity(), - repairedDataTracker); + return resolveWithReplicaFilteringProtection(replicas, repairedDataTracker); } - private boolean usesReplicaFilteringProtection() + private boolean needsReplicaFilteringProtection() { if (command.rowFilter().isEmpty()) return false; @@ -138,15 +148,19 @@ private boolean usesReplicaFilteringProtection() return false; Index.QueryPlan queryPlan = command.indexQueryPlan(); - if (queryPlan == null) + IndexMetadata indexMetadata = queryPlan == null ? null : queryPlan.getFirst().getIndexMetadata(); + + if (indexMetadata == null || !indexMetadata.isCustom()) + { return true; + } return queryPlan.supportsReplicaFilteringProtection(command.rowFilter()); } - private class ResolveContext + protected class ResolveContext { - private final E replicas; + public final E replicas; private final DataLimits.Counter mergedResultCounter; /** @@ -161,31 +175,32 @@ private ResolveContext(E replicas, boolean enforceLimits) command.selectsFullPartition(), enforceStrictLiveness); - // In case of top-k query, do not trim reconciled rows here because QueryPlan#postProcessor() - // needs to compare all rows. Also avoid enforcing the limit if explicitly requested. + // In case of top-k query, do not trim reconciled rows here because QueryPlan#postProcessor() needs to compare all rows if (command.isTopK() || !enforceLimits) this.mergedResultCounter.onlyCount(); } private boolean needsReadRepair() { - // Each replica may return different estimated top-K rows, it doesn't mean data is not replicated. - // Even though top-K queries are limited to CL ONE & LOCAL-ONE, they use the ScanAllRangesCommandIterator - // that combines the separate replica plans of each data range into a single replica plan. This is an - // optimisation but can result in the number of replicas being > 1. + // each replica may return different estimated top-K rows, it doesn't mean data is not replicated. if (command.isTopK()) return false; return replicas.size() > 1; } - private boolean needShortReadProtection() + public DataLimits.Counter mergedResultCounter() + { + return mergedResultCounter; + } + + public boolean needShortReadProtection() { // SRP doesn't make sense for top-k which needs to re-query replica with larger limit instead of fetching more partitions if (command.isTopK()) return false; - // If we have only one result, there is no read repair to do, and we can't get short reads + // If we have only one result, there is no read repair to do and we can't get short reads // Also, so-called "short reads" stems from nodes returning only a subset of the results they have for a // partition due to the limit, but that subset not being enough post-reconciliation. So if we don't have limit, // don't bother protecting against short reads. @@ -199,19 +214,24 @@ private interface ResponseProvider UnfilteredPartitionIterator getResponse(int i); } - private UnfilteredPartitionIterator shortReadProtectedResponse(int i, ResolveContext context, @Nullable Runnable onShortRead) + protected UnfilteredPartitionIterator shortReadProtectedResponse(int i, ResolveContext context, @Nullable Runnable onShortRead) { UnfilteredPartitionIterator originalResponse = responses.get(i).payload.makeIterator(command); - return context.needShortReadProtection() - ? ShortReadProtection.extend(context.replicas.get(i), - () -> { responses.clearUnsafe(i); if (onShortRead != null) onShortRead.run(); }, - originalResponse, - command, - context.mergedResultCounter, - requestTime, - enforceStrictLiveness) - : originalResponse; + if (context.needShortReadProtection()) + { + DataLimits.Counter singleResultCounter = command.createLimitedCounter(false); + return ShortReadProtection.extend(originalResponse, + command, + new ShortReadPartitionsProtection(command, + context.replicas.get(i), + () -> { responses.clearUnsafe(i); if (onShortRead != null) onShortRead.run(); }, + singleResultCounter, + context.mergedResultCounter(), + requestTime), + singleResultCounter); + } + return originalResponse; } private PartitionIterator resolveWithReadRepair(ResolveContext context, @@ -226,7 +246,7 @@ private PartitionIterator resolveWithReadRepair(ResolveContext context, listener = wrapMergeListener(readRepair.getMergeListener(sources), sources, repairedDataTracker); } - return resolveInternal(context, listener, responseProvider, preCountFilter); + return resolveInternal(context, listener, responseProvider, preCountFilter, readTracker); } private PartitionIterator resolveWithReplicaFilteringProtection(E replicas, RepairedDataTracker repairedDataTracker) @@ -261,7 +281,8 @@ private PartitionIterator resolveWithReplicaFilteringProtection(E replicas, Repa PartitionIterator firstPhasePartitions = resolveInternal(firstPhaseContext, rfp.mergeController(), i -> shortReadProtectedResponse(i, firstPhaseContext, null), - null); + null, + QueryInfoTracker.ReadTracker.NOOP); ResolveContext secondPhaseContext = new ResolveContext(replicas, true); PartitionIterator completedPartitions = resolveWithReadRepair(secondPhaseContext, @@ -279,19 +300,18 @@ private UnaryOperator preCountFilterForReplicaFilteringProte if (!command.rowFilter().hasNonKeyExpression()) return results -> results; - return results -> { - Index.Searcher searcher = command.indexSearcher(); - // in case of "ALLOW FILTERING" without index - if (searcher == null) - return command.rowFilter().filter(results, command.metadata(), command.nowInSec()); - return searcher.filterReplicaFilteringProtection(results); - }; + return results -> command.rowFilter().filter(results, command.metadata(), command.nowInSec()); } + /** + * Uses the provided {@link org.apache.cassandra.service.QueryInfoTracker.ReadTracker} as internal calls + * may be not tracked, e.g. the first phase of RFP. + */ private PartitionIterator resolveInternal(ResolveContext context, UnfilteredPartitionIterators.MergeListener mergeListener, ResponseProvider responseProvider, - @Nullable UnaryOperator preCountFilter) + @Nullable UnaryOperator preCountFilter, + QueryInfoTracker.ReadTracker resolveReadTracker) { int count = context.replicas.size(); List results = new ArrayList<>(count); @@ -313,6 +333,10 @@ private PartitionIterator resolveInternal(ResolveContext context, */ UnfilteredPartitionIterator merged = UnfilteredPartitionIterators.merge(results, mergeListener); + if (!QueryInfoTracker.ReadTracker.NOOP.equals(resolveReadTracker) && !QueryInfoTracker.LWTWriteTracker.NOOP.equals(resolveReadTracker)) + { + merged = Transformation.apply(merged, new ReadTrackingTransformation(resolveReadTracker)); + } Filter filter = new Filter(command.nowInSec(), command.metadata().enforceStrictLiveness()); FilteredPartitions filtered = FilteredPartitions.filter(merged, filter); diff --git a/src/java/org/apache/cassandra/service/reads/DigestResolver.java b/src/java/org/apache/cassandra/service/reads/DigestResolver.java index cc248422c06c..df58894b47a6 100644 --- a/src/java/org/apache/cassandra/service/reads/DigestResolver.java +++ b/src/java/org/apache/cassandra/service/reads/DigestResolver.java @@ -28,12 +28,15 @@ import org.apache.cassandra.db.ReadResponse; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.locator.Endpoints; -import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.net.Message; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.service.reads.repair.NoopReadRepair; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.ByteBufferUtil; @@ -44,12 +47,17 @@ public class DigestResolver, P extends ReplicaPlan.ForRead> extends ResponseResolver { private volatile Message dataResponse; + private final QueryInfoTracker.ReadTracker readTracker; - public DigestResolver(ReadCommand command, ReplicaPlan.Shared replicaPlan, Dispatcher.RequestTime requestTime) + public DigestResolver(ReadCommand command, + ReplicaPlan.Shared replicaPlan, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { super(command, replicaPlan, requestTime); Preconditions.checkArgument(command instanceof SinglePartitionReadCommand, "DigestResolver can only be used with SinglePartitionReadCommand commands"); + this.readTracker = readTracker; } @Override @@ -80,14 +88,20 @@ public PartitionIterator getData() if (!hasTransientResponse(responses)) { - return UnfilteredPartitionIterators.filter(dataResponse.payload.makeIterator(command), command.nowInSec()); + UnfilteredPartitionIterator unfilteredPartitionIterator = dataResponse.payload.makeIterator(command); + if (!QueryInfoTracker.ReadTracker.NOOP.equals(readTracker) && !QueryInfoTracker.LWTWriteTracker.NOOP.equals(readTracker)) + { + unfilteredPartitionIterator = Transformation.apply(unfilteredPartitionIterator, + new ReadTrackingTransformation(readTracker)); + } + return UnfilteredPartitionIterators.filter(unfilteredPartitionIterator, command.nowInSec()); } else { // This path can be triggered only if we've got responses from full replicas and they match, but // transient replica response still contains data, which needs to be reconciled. DataResolver dataResolver - = new DataResolver<>(command, replicaPlan, NoopReadRepair.instance, requestTime); + = new DataResolver<>(command, replicaPlan, NoopReadRepair.instance, requestTime, readTracker); dataResolver.preprocess(dataResponse); // Reconcile with transient replicas @@ -151,6 +165,11 @@ public DigestResolverDebugResult[] getDigestsByEndpoint() return ret; } + public QueryInfoTracker.ReadTracker getReadTracker() + { + return readTracker; + } + public static class DigestResolverDebugResult { public InetAddressAndPort from; diff --git a/src/java/org/apache/cassandra/service/reads/ReadCallback.java b/src/java/org/apache/cassandra/service/reads/ReadCallback.java index 899c55a8194e..d9e263c122db 100644 --- a/src/java/org/apache/cassandra/service/reads/ReadCallback.java +++ b/src/java/org/apache/cassandra/service/reads/ReadCallback.java @@ -45,10 +45,14 @@ import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings; import org.apache.cassandra.service.reads.thresholds.WarningContext; import org.apache.cassandra.service.reads.thresholds.WarningsSnapshot; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.sensors.RequestTracker; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.concurrent.Condition; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import org.apache.cassandra.metrics.ReplicaResponseSizeMetrics; +import org.apache.cassandra.net.MessagingService; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater; @@ -74,6 +78,8 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< private volatile WarningContext warningContext; private static final AtomicReferenceFieldUpdater warningsUpdater = AtomicReferenceFieldUpdater.newUpdater(ReadCallback.class, WarningContext.class, "warningContext"); + private final boolean couldSpeculate; + private final RequestSensors requestSensors; public ReadCallback(ResponseResolver resolver, ReadCommand command, ReplicaPlan.Shared replicaPlan, Dispatcher.RequestTime requestTime) { @@ -85,9 +91,16 @@ public ReadCallback(ResponseResolver resolver, ReadCommand command, Replic this.failureReasonByEndpoint = new ConcurrentHashMap<>(); // we don't support read repair (or rapid read protection) for range scans yet (CASSANDRA-6897) assert !(command instanceof PartitionRangeReadCommand) || blockFor >= replicaPlan().contacts().size(); + SpeculativeRetryPolicy retry = replicaPlan() + .keyspace() + .getColumnFamilyStore(command.metadata().id) + .metadata() + .params.speculativeRetry; + this.couldSpeculate = !NeverSpeculativeRetryPolicy.INSTANCE.equals(retry); if (logger.isTraceEnabled()) logger.trace("Blockfor is {}; setting up requests to {}", blockFor, this.replicaPlan); + this.requestSensors = RequestTracker.instance.get(); } protected P replicaPlan() @@ -95,6 +108,17 @@ protected P replicaPlan() return replicaPlan.get(); } + public ReadCommand command() + { + return command; + } + + @Override + public RequestSensors getRequestSensors() + { + return requestSensors; + } + public boolean await(long commandTimeout, TimeUnit unit) { return awaitUntil(requestTime.computeDeadline(unit.toNanos(commandTimeout))); @@ -198,7 +222,10 @@ public void onResponse(Message message) return; } } + resolver.preprocess(message); + + trackReplicaResponseSize(message); /* * Ensure that data is present and the response accumulator has properly published the @@ -224,6 +251,25 @@ private WarningContext getWarningContext() return current; } + /** + * Track the size of a response message from a replica + * @param message the response message + */ + private void trackReplicaResponseSize(Message message) + { + if (!ReplicaResponseSizeMetrics.isMetricsEnabled()) + return; + + // Only track remote responses (local responses have null from field) + // check that we have a valid payload and serializer and the response type supports size tracking + if (message != null && message.from() != null && message.payload != null + && message.verb().serializer() != null && message.payload.supportsResponseSizeTracking()) + { + int responseSize = message.payloadSize(MessagingService.current_version); + ReplicaResponseSizeMetrics.recordReadResponseSize(responseSize); + } + } + public void response(ReadResponse result) { Verb kind = command.isRangeRequest() ? Verb.RANGE_RSP : Verb.READ_RSP; @@ -233,7 +279,7 @@ public void response(ReadResponse result) } @Override - public boolean trackLatencyForSnitch() + public boolean trackLatencyForSnitch(Verb responseVerb, boolean isTimeout) { return true; } @@ -245,7 +291,12 @@ public void onFailure(InetAddressAndPort from, RequestFailureReason failureReaso failureReasonByEndpoint.put(from, failureReason); - if (blockFor + failuresUpdater.incrementAndGet(this) > replicaPlan().contacts().size()) + int numContacts = replicaPlan().contacts().size(); + int numCandidates = replicaPlan().readCandidates().size(); + // If potentially there is a replica which could be requested as part of the speculative read path + // then increase the number of nodes we wait for in case of failures. + int failFastPoint = (numContacts < numCandidates && couldSpeculate) ? numContacts + 1 : numContacts; + if (blockFor + failuresUpdater.incrementAndGet(this) > failFastPoint) condition.signalAll(); } diff --git a/src/java/org/apache/cassandra/service/reads/ReadTrackingTransformation.java b/src/java/org/apache/cassandra/service/reads/ReadTrackingTransformation.java new file mode 100644 index 000000000000..bae14b91ac8e --- /dev/null +++ b/src/java/org/apache/cassandra/service/reads/ReadTrackingTransformation.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.reads; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.service.QueryInfoTracker; +import org.apache.cassandra.utils.NoSpamLogger; + +/** + * {@code UnfilteredRowIterator} transformation that callbacks {@link QueryInfoTracker.ReadTracker} on + * each row and partition. The transformation may be extended with other callback methods (like + * {@code applyToStatic} or {@code applyToDeletion} if necessary. + * + * Do not move closing the tracker here (to @{code onClose} method). One read may include more than + * one row iterator, closing the tracker here may result in multiple close callbacks. + */ +class ReadTrackingTransformation extends Transformation +{ + private final QueryInfoTracker.ReadTracker readTracker; + private static final Logger logger = LoggerFactory.getLogger(ReadTrackingTransformation.class); + + public ReadTrackingTransformation(QueryInfoTracker.ReadTracker readTracker) + { + this.readTracker = readTracker; + } + + @Override + protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) + { + return Transformation.apply(partition, this); + } + + @Override + protected Row applyToRow(Row row) + { + try + { + readTracker.onRow(row); + } + catch (Exception exc) + { + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 60, TimeUnit.SECONDS, + "Tracking callback for read rows failed", exc); + } + return super.applyToRow(row); + } + + @Override + protected Row applyToStatic(Row row) + { + try + { + if (!row.isEmpty()) + { + readTracker.onRow(row); + } + } + catch (Exception exc) + { + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 60, TimeUnit.SECONDS, + "Tracking callback for read rows failed", exc); + } + return super.applyToRow(row); + } + + @Override + protected DecoratedKey applyToPartitionKey(DecoratedKey key) + { + try + { + readTracker.onPartition(key); + } + catch (Exception exc) + { + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 60, TimeUnit.SECONDS, + "Tracking callback for read partitions failed", exc); + } + return super.applyToPartitionKey(key); + } +} diff --git a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java index 72c1c85fc84b..9c7b618353e3 100644 --- a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java @@ -46,6 +46,7 @@ import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; @@ -70,6 +71,7 @@ import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.reads.repair.NoopReadRepair; import org.apache.cassandra.service.reads.repair.PartitionIteratorMergeListener; @@ -101,6 +103,13 @@ public class ReplicaFilteringProtection> private static final Function NULL_TO_NO_STATS = rowIterator -> rowIterator == null ? EncodingStats.NO_STATS : rowIterator.stats(); + private static final String CACHED_ROWS_WARN_MESSAGE = "Replica filtering protection has cached up to %d rows during query %s, " + + "which is over the warning threshold of %d rows defined by " + + "'cached_replica_rows_warn_threshold' in cassandra.yaml."; + private static final String CACHED_ROWS_FAIL_MESSAGE = "Replica filtering protection has cached %d rows during query %s, " + + "which is over the failure threshold of %d rows defined by " + + "'cached_replica_rows_fail_threshold' in cassandra.yaml."; + private final Keyspace keyspace; private final ReadCommand command; private final ConsistencyLevel consistency; @@ -113,8 +122,8 @@ public class ReplicaFilteringProtection> private final int cachedRowsWarnThreshold; private final int cachedRowsFailThreshold; - /** Tracks whether or not we've already hit the warning threshold while evaluating a partition. */ - private boolean hitWarningThreshold = false; + /** Tracks whether or not we've already hit the failure threshold while evaluating a partition. */ + private boolean hitFailureThreshold = false; private int currentRowsCached = 0; // tracks the current number of cached rows private int maxRowsCached = 0; // tracks the high watermark for the number of cached rows @@ -163,7 +172,11 @@ private UnfilteredPartitionIterator executeReadCommand(ReadCommand cmd, Replica { @SuppressWarnings("unchecked") DataResolver resolver = - new DataResolver<>(cmd, replicaPlan, (NoopReadRepair) NoopReadRepair.instance, requestTime); + new DataResolver<>(cmd, + replicaPlan, + (NoopReadRepair) NoopReadRepair.instance, + requestTime, + QueryInfoTracker.ReadTracker.NOOP); ReadCallback handler = new ReadCallback<>(resolver, cmd, replicaPlan, requestTime); @@ -286,7 +299,27 @@ private class QueryMergeListener implements UnfilteredPartitionIterators.MergeLi public void close() { // If we hit the failure threshold before consuming a single partition, record the current rows cached. - tableMetrics.rfpRowsCachedPerQuery.update(Math.max(currentRowsCached, maxRowsCached)); + maxRowsCached = Math.max(currentRowsCached, maxRowsCached); + tableMetrics.rfpRowsCachedPerQuery.update(maxRowsCached); + + // Check the cached rows warning threshold at the end of the query, so we can report the maximum number + // of cached rows we have had during the query. + if (!hitFailureThreshold && maxRowsCached > cachedRowsWarnThreshold) + { + String unredactedMessage = cachedRowsWarnMessage(Redaction.NONE); + String redactedMessage = cachedRowsWarnMessage(Redaction.REDACT); + ClientWarn.instance.warn(unredactedMessage); + oneMinuteLogger.warn(redactedMessage); + Tracing.trace(unredactedMessage); + } + } + + private String cachedRowsWarnMessage(Redaction redaction) + { + return String.format(CACHED_ROWS_WARN_MESSAGE, + maxRowsCached, + command.toCQLString(redaction), + cachedRowsWarnThreshold); } @Override @@ -320,33 +353,31 @@ private void incrementCachedRows() { currentRowsCached++; + // Check the cached rows failure threshold every time the cached row count is incremented, + // so we can detect a violation and abort as soon as the threshold is crossed. if (currentRowsCached == cachedRowsFailThreshold + 1) { - String message = String.format("Replica filtering protection has cached over %d rows during query %s. " + - "(See 'cached_replica_rows_fail_threshold' in cassandra.yaml.)", - cachedRowsFailThreshold, command.toCQLString()); + hitFailureThreshold = true; + String unredactedMessage = cachedRowsFailMessage(Redaction.NONE); + String redactedMessage = cachedRowsFailMessage(Redaction.REDACT); - logger.error(message); - Tracing.trace(message); - throw new OverloadedException(message); + logger.error(redactedMessage); + Tracing.trace(unredactedMessage); + throw new OverloadedException(redactedMessage); } - else if (currentRowsCached == cachedRowsWarnThreshold + 1 && !hitWarningThreshold) - { - hitWarningThreshold = true; - - String message = String.format("Replica filtering protection has cached over %d rows during query %s. " + - "(See 'cached_replica_rows_warn_threshold' in cassandra.yaml.)", - cachedRowsWarnThreshold, command.toCQLString()); + } - ClientWarn.instance.warn(message); - oneMinuteLogger.warn(message); - Tracing.trace(message); - } + private String cachedRowsFailMessage(Redaction redaction) + { + return String.format(CACHED_ROWS_FAIL_MESSAGE, + currentRowsCached, + command.toCQLString(redaction), + cachedRowsFailThreshold); } private void releaseCachedRows(int count) { - maxRowsCached = Math.max(maxRowsCached, currentRowsCached); + maxRowsCached = Math.max(currentRowsCached, maxRowsCached); currentRowsCached -= count; } diff --git a/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java b/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java index e9870f1f1d7b..3db8f7456c5f 100644 --- a/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java @@ -18,9 +18,8 @@ package org.apache.cassandra.service.reads; -import org.apache.cassandra.locator.Endpoints; -import org.apache.cassandra.locator.ReplicaPlan; -import org.apache.cassandra.locator.ReplicaPlans; +import java.util.concurrent.TimeUnit; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,27 +40,35 @@ import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.ExcludingBounds; import org.apache.cassandra.dht.Range; +import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.service.reads.repair.NoopReadRepair; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.service.reads.repair.NoopReadRepair; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.NoSpamLogger; public class ShortReadPartitionsProtection extends Transformation implements MorePartitions { private static final Logger logger = LoggerFactory.getLogger(ShortReadPartitionsProtection.class); + private static final NoSpamLogger oneMinuteLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + private final ReadCommand command; private final Replica source; private final Runnable preFetchCallback; // called immediately before fetching more contents - private final DataLimits.Counter singleResultCounter; // unmerged per-source counter + protected final DataLimits.Counter singleResultCounter; // unmerged per-source counter private final DataLimits.Counter mergedResultCounter; // merged end-result counter private DecoratedKey lastPartitionKey; // key of the last observed partition private boolean partitionsFetched; // whether we've seen any new partitions since iteration start or last moreContents() call + protected boolean rangeFetched = false; // fetched by original read request or SRP request private final Dispatcher.RequestTime requestTime; @@ -84,6 +91,7 @@ public ShortReadPartitionsProtection(ReadCommand command, public UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition) { partitionsFetched = true; + rangeFetched = true; lastPartitionKey = partition.partitionKey(); @@ -128,16 +136,17 @@ public UnfilteredPartitionIterator moreContents() * Can only take the short cut if there is no per partition limit set. Otherwise it's possible to hit false * positives due to some rows being uncounted for in certain scenarios (see CASSANDRA-13911). */ - if (command.limits().isExhausted(singleResultCounter) && command.limits().perPartitionCount() == DataLimits.NO_LIMIT) + if (command.limits().isCounterBelowLimits(singleResultCounter) && command.limits().perPartitionCount() == DataLimits.NO_LIMIT) return null; /* * Either we had an empty iterator as the initial response, or our moreContents() call got us an empty iterator. * There is no point to ask the replica for more rows - it has no more in the requested range. */ - if (!partitionsFetched) + if (rangeExhausted()) return null; partitionsFetched = false; + rangeFetched = true; /* * We are going to fetch one partition at a time for thrift and potentially more for CQL. @@ -151,7 +160,9 @@ public UnfilteredPartitionIterator moreContents() ColumnFamilyStore.metricsFor(command.metadata().id).shortReadProtectionRequests.mark(); Tracing.trace("Requesting {} extra rows from {} for short read protection", toQuery, source); - logger.info("Requesting {} extra rows from {} for short read protection", toQuery, source); + // This is a NoSpamLogger because, in the event of unrepaired data or missing data on nodes in + // a cluster, we can end up spamming the logs with this message + oneMinuteLogger.info("Requesting {} extra rows from {} for short read protection", toQuery, source); // If we've arrived here, all responses have been consumed, and we're about to request more. preFetchCallback.run(); @@ -159,6 +170,11 @@ public UnfilteredPartitionIterator moreContents() return makeAndExecuteFetchAdditionalPartitionReadCommand(toQuery); } + public boolean rangeExhausted() + { + return !partitionsFetched; + } + private UnfilteredPartitionIterator makeAndExecuteFetchAdditionalPartitionReadCommand(int toQuery) { PartitionRangeReadCommand cmd = (PartitionRangeReadCommand) command; @@ -178,7 +194,11 @@ private UnfilteredPartitionIterator makeAndExecuteFetchAdditionalPartitionReadCo private , P extends ReplicaPlan.ForRead> UnfilteredPartitionIterator executeReadCommand(ReadCommand cmd, ReplicaPlan.Shared replicaPlan) { - DataResolver resolver = new DataResolver<>(cmd, replicaPlan, (NoopReadRepair)NoopReadRepair.instance, requestTime); + DataResolver resolver = new DataResolver<>(cmd, + replicaPlan, + (NoopReadRepair)NoopReadRepair.instance, + requestTime, + QueryInfoTracker.ReadTracker.NOOP); ReadCallback handler = new ReadCallback<>(resolver, cmd, replicaPlan, requestTime); if (source.isSelf()) diff --git a/src/java/org/apache/cassandra/service/reads/ShortReadProtection.java b/src/java/org/apache/cassandra/service/reads/ShortReadProtection.java index 1eca190a7343..1d50b30c0224 100644 --- a/src/java/org/apache/cassandra/service/reads/ShortReadProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ShortReadProtection.java @@ -18,14 +18,11 @@ package org.apache.cassandra.service.reads; - import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.transform.MorePartitions; import org.apache.cassandra.db.transform.Transformation; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.transport.Dispatcher; /** * We have a potential short read if the result from a given node contains the requested number of rows @@ -39,26 +36,11 @@ */ public class ShortReadProtection { - public static UnfilteredPartitionIterator extend(Replica source, - Runnable preFetchCallback, - UnfilteredPartitionIterator partitions, + public static UnfilteredPartitionIterator extend(UnfilteredPartitionIterator partitions, ReadCommand command, - DataLimits.Counter mergedResultCounter, - Dispatcher.RequestTime requestTime, - boolean enforceStrictLiveness) + ShortReadPartitionsProtection protection, + DataLimits.Counter singleResultCounter) { - DataLimits.Counter singleResultCounter = command.limits().newCounter(command.nowInSec(), - false, - command.selectsFullPartition(), - enforceStrictLiveness).onlyCount(); - - ShortReadPartitionsProtection protection = new ShortReadPartitionsProtection(command, - source, - preFetchCallback, - singleResultCounter, - mergedResultCounter, - requestTime); - /* * The order of extention and transformations is important here. Extending with more partitions has to happen * first due to the way BaseIterator.hasMoreContents() works: only transformations applied after extension will diff --git a/src/java/org/apache/cassandra/service/reads/ShortReadRowsProtection.java b/src/java/org/apache/cassandra/service/reads/ShortReadRowsProtection.java index 4ed9e329563b..1c08e0ff293b 100644 --- a/src/java/org/apache/cassandra/service/reads/ShortReadRowsProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ShortReadRowsProtection.java @@ -92,7 +92,7 @@ public UnfilteredRowIterator moreContents() * Can only take the short cut if there is no per partition limit set. Otherwise it's possible to hit false * positives due to some rows being uncounted for in certain scenarios (see CASSANDRA-13911). */ - if (command.limits().isExhausted(singleResultCounter) && command.limits().perPartitionCount() == DataLimits.NO_LIMIT) + if (command.limits().isCounterBelowLimits(singleResultCounter) && command.limits().perPartitionCount() == DataLimits.NO_LIMIT) return null; /* diff --git a/src/java/org/apache/cassandra/service/reads/range/EndpointGroupingCoordinator.java b/src/java/org/apache/cassandra/service/reads/range/EndpointGroupingCoordinator.java new file mode 100644 index 000000000000..6714062e904f --- /dev/null +++ b/src/java/org/apache/cassandra/service/reads/range/EndpointGroupingCoordinator.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.reads.range; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.db.MultiRangeReadCommand; +import org.apache.cassandra.db.MultiRangeReadResponse; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadResponse; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.PartitionIterators; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.locator.Endpoints; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.service.QueryInfoTracker; +import org.apache.cassandra.service.reads.DataResolver; +import org.apache.cassandra.service.reads.ReadCallback; +import org.apache.cassandra.service.reads.ShortReadPartitionsProtection; +import org.apache.cassandra.service.reads.ShortReadProtection; +import org.apache.cassandra.service.reads.repair.NoopReadRepair; +import org.apache.cassandra.service.reads.repair.ReadRepair; +import org.apache.cassandra.transport.Dispatcher; + +import javax.annotation.Nullable; + +/** + * Coordinates the process of endpoint grouping queries for given vnode ranges based on concurrency factor: + *

      + *
    1. Collect token ranges required by concurrency factor in token order and group token ranges by endpoint + *
    2. Create single-range read callbacks corresponding to each vnode range, note that: + *
        + *
      • In order to maintain proper single result counting for short-read-protection, single-range read callback + * cannot start resolving before previous one has finished resolving. + *
      + *
    3. Execute {@link MultiRangeReadCommand} on each selected endpoint with all its replicated ranges at once. + *
    4. Upon receiving individual {@link MultiRangeReadResponse}: + *
        + *
      1. It will split multi-range response into single-range responses by queried vnode ranges. + *
      2. It will pass single-range responses to their corresponding single-range read callback to allow progressive data merging. + *
      + *
    5. Return single-range handlers' result in token order. + *
    + */ +public class EndpointGroupingCoordinator +{ + private final PartitionRangeReadCommand command; + private final DataLimits.Counter counter; + private final Map endpointContexts; + private final List> perRangeHandlers; + private final List concurrentQueries; + + private final Dispatcher.RequestTime requestTime; + private QueryInfoTracker.ReadTracker readTracker; + private final int vnodeRanges; + + /** + * @param command current range read command + * @param counter the unlimited counter for the command + * @param replicaPlans to be queried + * @param concurrencyFactor number of vnode ranges to query at once + * @param requestTime the start time of the query + * @param readTracker + */ + public EndpointGroupingCoordinator(PartitionRangeReadCommand command, + DataLimits.Counter counter, + Iterator replicaPlans, + int concurrencyFactor, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) + { + this.command = command; + this.counter = counter; + this.requestTime = requestTime; + this.readTracker = readTracker; + this.endpointContexts = new HashMap<>(); + + // Read callbacks in token order + perRangeHandlers = new ArrayList<>(concurrencyFactor); + // Range responses in token order + concurrentQueries = new ArrayList<>(concurrencyFactor); + int vnodeRanges = 0; + + while (replicaPlans.hasNext() && vnodeRanges < concurrencyFactor) + { + ReplicaPlan.ForRangeRead replicaPlan = replicaPlans.next(); + readTracker.onReplicaPlan(replicaPlan); + + boolean isFirst = vnodeRanges == 0; + vnodeRanges += replicaPlan.vnodeCount(); + concurrentQueries.add(createResponse(replicaPlan, isFirst)); + } + this.vnodeRanges = vnodeRanges; + } + + public int vnodeRanges() + { + return vnodeRanges; + } + + public PartitionIterator execute() + { + for (EndpointQueryContext replica : replicas()) + replica.queryReplica(); + + return counter.applyTo(PartitionIterators.concat(concurrentQueries)); + } + + @VisibleForTesting + Collection endpointRanges() + { + return endpointContexts.values(); + } + + /** + * @return number of endpoints to be queried + */ + int endpoints() + { + return endpointContexts.size(); + } + + private Collection replicas() + { + return endpointContexts.values(); + } + + /** + * Create a {@link SingleRangeResponse} for a given vnode range. The responses are collected and concatenated by + * {@code execute}. + */ + private SingleRangeResponse createResponse(ReplicaPlan.ForRangeRead replicaPlan, boolean isFirst) + { + PartitionRangeReadCommand subrangeCommand = command.forSubRange(replicaPlan.range(), isFirst); + + ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan); + + DataResolver resolver = new EndpointDataResolver(subrangeCommand, + sharedReplicaPlan, + NoopReadRepair.instance, + requestTime, + readTracker); + + // Create a handler for the range and add it, by replica, to the endpoint contexts. + ReadCallback handler = + new ReadCallback<>(resolver, subrangeCommand, sharedReplicaPlan, requestTime); + + perRangeHandlers.add(handler); + for (Replica replica : replicaPlan.contacts()) + { + endpointContexts.computeIfAbsent(replica.endpoint(), + k -> new EndpointQueryContext(replica.endpoint(), + command.createLimitedCounter(false), + requestTime)).add(handler); + } + return new SingleRangeResponse(resolver, handler, NoopReadRepair.instance); + } + + /** + * Collect and query all involved ranges of a given endpoint + */ + public static class EndpointQueryContext + { + private final InetAddressAndPort endpoint; + private final List> handlers; + // used by SRP to track fetched data from each endpoint to determine if an endpoint is exhausted, + // aka. no more data can be fetched. + private final DataLimits.Counter singleResultCounter; + private final Dispatcher.RequestTime requestTime; + + private MultiRangeReadCommand multiRangeCommand; + + public EndpointQueryContext(InetAddressAndPort endpoint, DataLimits.Counter singleResultCounter, Dispatcher.RequestTime requestTime) + { + this.endpoint = endpoint; + this.handlers = new ArrayList<>(); + this.singleResultCounter = singleResultCounter; + this.requestTime = requestTime; + } + + /** + * @param handler read callback for a given vnode range on the current endpoint + */ + public void add(ReadCallback handler) + { + assert multiRangeCommand == null : "Cannot add range to already queried context"; + handlers.add(handler); + } + + /** + * Query a single endpoint with multiple vnode ranges asynchronously + */ + public void queryReplica() + { + assert multiRangeCommand == null : "Can only query given endpoint once"; + this.multiRangeCommand = MultiRangeReadCommand.create(handlers); + + SingleEndpointCallback proxy = new SingleEndpointCallback(); + Message message = multiRangeCommand.createMessage(false, requestTime); + MessagingService.instance().sendWithCallback(message, endpoint, proxy); + } + + @VisibleForTesting + public int rangesCount() + { + return handlers.size(); + } + + /** + * A proxy responsible for: + * 0. propagating failure/timeout to single-range handlers + * 1. receiving multi-range responses from a given endpoint + * 2. spliting the multi-range responses by vnode ranges + * 3. passing the split single-range response to a corresponding read callback which will + * start resolving responses if it has got enough responses for the consistency level requirement. + */ + private class SingleEndpointCallback implements RequestCallback + { + @Override + public void onResponse(Message response) + { + // split single-endpoint multi-range response into per-range handlers. + MultiRangeReadResponse multiRangeResponse = (MultiRangeReadResponse) response.payload; + for (ReadCallback handler : handlers) + { + AbstractBounds range = ((PartitionRangeReadCommand) handler.command()).dataRange().keyRange(); + + // extract subrange response in token order + ReadResponse subrangeResponse = multiRangeResponse.subrangeResponse(multiRangeCommand, range); + handler.onResponse(Message.remoteResponse(response.header.from, Verb.RANGE_RSP, response.header.params(), subrangeResponse)); + } + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) + { + for (ReadCallback handler : handlers) + handler.onFailure(from, failureReason); + } + + @Override + public boolean invokeOnFailure() + { + return true; + } + + @Override + public boolean trackLatencyForSnitch(Verb responseVerb, boolean isTimeout) + { + return true; + } + } + } + + /** + * Short-read-protection needs to know if an endpoint has any more data or it has already reached the limit: + * If the endpoint has no more data, aka. the counter hasn't reached the limit, there is no point in doing SRP. + * If the endpoint might have more data, aka. the counter has reached the limit, SRP might be needed. + * + * With token ordered range query or single partition query, {@link DataResolver} uses a new single result counter + * per replica for a given range, as all replicas are queried with the same range. + * + * But with endpoint grouping, each source is queried with different token ranges. So we need a shared + * cross-range counter for each replica to know if given endpoint has more data. + */ + private class EndpointDataResolver, P extends ReplicaPlan.ForRead> extends DataResolver + { + public EndpointDataResolver(ReadCommand command, ReplicaPlan.Shared replicaPlan, ReadRepair readRepair, Dispatcher.RequestTime requestTime, QueryInfoTracker.ReadTracker readTracker) + { + super(command, replicaPlan, readRepair, requestTime, readTracker); + } + + @Override + protected UnfilteredPartitionIterator shortReadProtectedResponse(int i, DataResolver.ResolveContext context, @Nullable Runnable onShortRead) + { + UnfilteredPartitionIterator originalResponse = responses.get(i).payload.makeIterator(command); + + if (context.needShortReadProtection()) + { + DataLimits.Counter singleResultCounter = endpointContexts.get(context.replicas.get(i).endpoint()).singleResultCounter; + return ShortReadProtection.extend(originalResponse, + command, + new EndpointShortReadResponseProtection(command, + context.replicas.get(i), + () -> { responses.clearUnsafe(i); if (onShortRead != null) onShortRead.run(); }, + singleResultCounter, + context.mergedResultCounter(), + requestTime), + singleResultCounter); + } + else + return originalResponse; + } + + /** + * On replica, {@link MultiRangeReadCommand} stops fetching remaining ranges when it reaches limit. + * + * We should do short-read-protection if current range is not fetched due to limit. + */ + public class EndpointShortReadResponseProtection extends ShortReadPartitionsProtection + { + public EndpointShortReadResponseProtection(ReadCommand command, + Replica source, + Runnable preFetchCallback, + DataLimits.Counter singleResultCounter, + DataLimits.Counter mergedResultCounter, + Dispatcher.RequestTime requestTime) + { + super(command, source, preFetchCallback, singleResultCounter, mergedResultCounter, requestTime); + } + + @Override + public boolean rangeExhausted() + { + // if the range is not fetched by original request or SRP, SRP is needed. + return super.rangeExhausted() && (rangeFetched || !singleResultCounter.isDone()); + } + } + } +} diff --git a/src/java/org/apache/cassandra/service/reads/range/EndpointGroupingRangeCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/EndpointGroupingRangeCommandIterator.java new file mode 100644 index 000000000000..383d847dd67c --- /dev/null +++ b/src/java/org/apache/cassandra/service/reads/range/EndpointGroupingRangeCommandIterator.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.reads.range; + +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.service.QueryInfoTracker; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.CloseableIterator; + +/** + * A range command iterator that executes requests by endpoints and then merges responses in token order. It's designed to + * reduce the number of range requests when scanning the whole token ring (eg. rows per range is low) for all range + * reads that don't use digests and also to reduce the amount of disk-access for storage-attached indexes, as they will + * be able to read index content for all required ranges at once. + * + *
      + *
    • With the non-grouping range command iterator, scanning the entire ring requires "num_of_nodes * num_of_tokens * consistency" + * range requests (assuming no ranges are merged by {@link ReplicaPlanMerger}) to their respective replicas. + * + *
    • With the endpoint grouping range command iterator, scanning the entire ring only requires at most "num_of_nodes" multi-range + * requests to their respective replicas. So coordinator will cache up to "num_of_nodes" responses. + *
    + */ +public class EndpointGroupingRangeCommandIterator extends RangeCommandIterator +{ + EndpointGroupingRangeCommandIterator(CloseableIterator replicaPlans, + PartitionRangeReadCommand command, + int concurrencyFactor, + int maxConcurrencyFactor, + int totalRangeCount, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) + { + super(replicaPlans, command, concurrencyFactor, maxConcurrencyFactor, totalRangeCount, requestTime, readTracker); + } + + @Override + protected PartitionIterator sendNextRequests() + { + counter = command.createUnlimitedCounter(true); + + EndpointGroupingCoordinator coordinator = new EndpointGroupingCoordinator(command, + counter, + replicaPlans, + concurrencyFactor(), + requestTime, + readTracker); + PartitionIterator partitions = coordinator.execute(); + + rangesQueried += coordinator.vnodeRanges(); + batchesRequested++; + Tracing.trace("Submitted concurrent grouped range read requests to {} endpoints", coordinator.endpoints()); + return partitions; + } +} diff --git a/src/java/org/apache/cassandra/service/reads/range/NonGroupingRangeCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/NonGroupingRangeCommandIterator.java new file mode 100644 index 000000000000..ed4548666298 --- /dev/null +++ b/src/java/org/apache/cassandra/service/reads/range/NonGroupingRangeCommandIterator.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.reads.range; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.QueryInfoTracker; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.service.reads.DataResolver; +import org.apache.cassandra.service.reads.ReadCallback; +import org.apache.cassandra.service.reads.repair.ReadRepair; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.CloseableIterator; + +public class NonGroupingRangeCommandIterator extends RangeCommandIterator +{ + NonGroupingRangeCommandIterator(CloseableIterator replicaPlans, + PartitionRangeReadCommand command, + int concurrencyFactor, + int maxConcurrencyFactor, + int totalRangeCount, + final Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) + { + super(replicaPlans, command, concurrencyFactor, maxConcurrencyFactor, totalRangeCount, requestTime, readTracker); + } + + protected PartitionIterator sendNextRequests() + { + List concurrentQueries = new ArrayList<>(concurrencyFactor); + List> readRepairs = new ArrayList<>(concurrencyFactor); + + try + { + for (int i = 0; i < concurrencyFactor() && replicaPlans.hasNext(); ) + { + ReplicaPlan.ForRangeRead replicaPlan = replicaPlans.next(); + readTracker.onReplicaPlan(replicaPlan); + + @SuppressWarnings("resource") // response will be closed by concatAndBlockOnRepair, or in the catch block below + SingleRangeResponse response = query(replicaPlan, i == 0); + concurrentQueries.add(response); + readRepairs.add(response.getReadRepair()); + // due to RangeMerger, coordinator may fetch more ranges than required by concurrency factor. + rangesQueried += replicaPlan.vnodeCount(); + i += replicaPlan.vnodeCount(); + } + batchesRequested++; + } + catch (Throwable t) + { + for (PartitionIterator response : concurrentQueries) + response.close(); + throw t; + } + + Tracing.trace("Submitted {} concurrent range requests", concurrentQueries.size()); + // We want to count the results for the sake of updating the concurrency factor (see updateConcurrencyFactor) + // but we don't want to enforce any particular limit at this point (this could break code than rely on + // postReconciliationProcessing), hence the unlimited counter that uses DataLimits.NONE. + counter = command.createUnlimitedCounter(true); + return counter.applyTo(StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs)); + } + + /** + * Queries the provided sub-range. + * + * @param replicaPlan the subRange to query. + * @param isFirst in the case where multiple queries are sent in parallel, whether that's the first query on + * that batch or not. The reason it matters is that whe paging queries, the command (more specifically the + * {@code DataLimits}) may have "state" information and that state may only be valid for the first query (in + * that it's the query that "continues" whatever we're previously queried). + */ + private SingleRangeResponse query(ReplicaPlan.ForRangeRead replicaPlan, boolean isFirst) + { + PartitionRangeReadCommand rangeCommand = command.forSubRange(replicaPlan.range(), isFirst); + + // If enabled, request repaired data tracking info from full replicas but + // only if there are multiple full replicas to compare results from + boolean trackRepairData = DatabaseDescriptor.getRepairedDataTrackingForRangeReadsEnabled() + && replicaPlan.contacts().filter(Replica::isFull).size() > 1; + + ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan); + ReadRepair readRepair = + ReadRepair.create(command, sharedReplicaPlan, requestTime); + DataResolver resolver = + new DataResolver<>(rangeCommand, sharedReplicaPlan, readRepair, requestTime, trackRepairData, readTracker); + ReadCallback handler = + new ReadCallback<>(resolver, rangeCommand, sharedReplicaPlan, requestTime); + + if (replicaPlan.contacts().size() == 1 && replicaPlan.contacts().get(0).isSelf()) + { + Stage.READ.execute(new StorageProxy.LocalReadRunnable(rangeCommand, handler, requestTime)); + } + else + { + for (Replica replica : replicaPlan.contacts()) + { + Tracing.trace("Enqueuing request to {}", replica); + ReadCommand command = replica.isFull() ? rangeCommand : rangeCommand.copyAsTransientQuery(replica); + Message message = command.createMessage(trackRepairData && replica.isFull(), requestTime); + MessagingService.instance().sendWithCallback(message, replica.endpoint(), handler); + } + } + + return new SingleRangeResponse(resolver, handler, readRepair); + } +} diff --git a/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java index 3e9ac453c70c..b23a12511302 100644 --- a/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java +++ b/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java @@ -18,17 +18,13 @@ package org.apache.cassandra.service.reads.range; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.concurrent.TimeUnit; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.concurrent.Stage; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionRangeReadCommand; import org.apache.cassandra.db.ReadCommand; @@ -39,62 +35,89 @@ import org.apache.cassandra.exceptions.ReadFailureException; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.UnavailableException; -import org.apache.cassandra.locator.EndpointsForRange; -import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.index.Index; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.metrics.ClientRangeRequestMetrics; -import org.apache.cassandra.net.Message; -import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.service.StorageProxy; -import org.apache.cassandra.service.reads.DataResolver; -import org.apache.cassandra.service.reads.ReadCallback; -import org.apache.cassandra.service.reads.repair.ReadRepair; +import org.apache.cassandra.metrics.ClientRequestsMetricsProvider; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.CloseableIterator; import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import static org.apache.cassandra.config.CassandraRelevantProperties.RANGE_READ_ENDPOINT_GROUPING_ENABLED; @VisibleForTesting -public class RangeCommandIterator extends AbstractIterator implements PartitionIterator +public abstract class RangeCommandIterator extends AbstractIterator implements PartitionIterator { private static final Logger logger = LoggerFactory.getLogger(RangeCommandIterator.class); + private static final boolean ENDPOINT_GROUPING_ENABLED = RANGE_READ_ENDPOINT_GROUPING_ENABLED.getBoolean(); - public static final ClientRangeRequestMetrics rangeMetrics = new ClientRangeRequestMetrics("RangeSlice"); + @VisibleForTesting + public final ClientRangeRequestMetrics rangeMetrics; + final Dispatcher.RequestTime requestTime; final CloseableIterator replicaPlans; final int totalRangeCount; final PartitionRangeReadCommand command; final boolean enforceStrictLiveness; - final Dispatcher.RequestTime requestTime; - - int rangesQueried; - int batchesRequested = 0; - - private DataLimits.Counter counter; + protected DataLimits.Counter counter; private PartitionIterator sentQueryIterator; + protected QueryInfoTracker.ReadTracker readTracker; private final int maxConcurrencyFactor; - private int concurrencyFactor; + protected int concurrencyFactor; // The two following "metric" are maintained to improve the concurrencyFactor // when it was not good enough initially. private int liveReturned; + int rangesQueried; + int batchesRequested = 0; + + @SuppressWarnings("resource") + public static RangeCommandIterator create(CloseableIterator replicaPlans, + PartitionRangeReadCommand command, + int concurrencyFactor, + int maxConcurrencyFactor, + int totalRangeCount, + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) + { + return ENDPOINT_GROUPING_ENABLED && supportsEndpointGrouping(command) ? new EndpointGroupingRangeCommandIterator(replicaPlans, + command, + concurrencyFactor, + maxConcurrencyFactor, + totalRangeCount, + requestTime, + readTracker) + : new NonGroupingRangeCommandIterator(replicaPlans, + command, + concurrencyFactor, + maxConcurrencyFactor, + totalRangeCount, + requestTime, + readTracker); + } RangeCommandIterator(CloseableIterator replicaPlans, PartitionRangeReadCommand command, int concurrencyFactor, int maxConcurrencyFactor, int totalRangeCount, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { + this.rangeMetrics = ClientRequestsMetricsProvider.instance.metrics(command.metadata().keyspace).rangeMetrics; this.replicaPlans = replicaPlans; this.command = command; this.concurrencyFactor = concurrencyFactor; this.maxConcurrencyFactor = maxConcurrencyFactor; this.totalRangeCount = totalRangeCount; this.requestTime = requestTime; + this.readTracker = readTracker; + enforceStrictLiveness = command.metadata().enforceStrictLiveness(); } @@ -155,6 +178,17 @@ private void updateConcurrencyFactor() concurrencyFactor = computeConcurrencyFactor(totalRangeCount, rangesQueried, maxConcurrencyFactor, command.limits().count(), liveReturned); } + private static boolean supportsEndpointGrouping(ReadCommand command) + { + // With endpoint grouping, ranges executed on each endpoint are different, digest is unlikely to match. + if (command.isDigestQuery()) + return false; + + // Endpoint grouping is currently only supported by SAI + Index.QueryPlan queryPlan = command.indexQueryPlan(); + return queryPlan != null && queryPlan.supportsMultiRangeReadCommand(); + } + @VisibleForTesting static int computeConcurrencyFactor(int totalRangeCount, int rangesQueried, int maxConcurrencyFactor, int limit, int liveReturned) { @@ -176,84 +210,7 @@ static int computeConcurrencyFactor(int totalRangeCount, int rangesQueried, int return concurrencyFactor; } - /** - * Queries the provided sub-range. - * - * @param replicaPlan the subRange to query. - * @param isFirst in the case where multiple queries are sent in parallel, whether that's the first query on - * that batch or not. The reason it matters is that whe paging queries, the command (more specifically the - * {@code DataLimits}) may have "state" information and that state may only be valid for the first query (in - * that it's the query that "continues" whatever we're previously queried). - */ - private SingleRangeResponse query(ReplicaPlan.ForRangeRead replicaPlan, boolean isFirst) - { - PartitionRangeReadCommand rangeCommand = command.forSubRange(replicaPlan.range(), isFirst); - - // If enabled, request repaired data tracking info from full replicas, but - // only if there are multiple full replicas to compare results from. - boolean trackRepairedStatus = DatabaseDescriptor.getRepairedDataTrackingForRangeReadsEnabled() - && replicaPlan.contacts().filter(Replica::isFull).size() > 1; - - ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan); - ReadRepair readRepair = - ReadRepair.create(command, sharedReplicaPlan, requestTime); - DataResolver resolver = - new DataResolver<>(rangeCommand, sharedReplicaPlan, readRepair, requestTime, trackRepairedStatus); - ReadCallback handler = - new ReadCallback<>(resolver, rangeCommand, sharedReplicaPlan, requestTime); - - if (replicaPlan.contacts().size() == 1 && replicaPlan.contacts().get(0).isSelf()) - { - Stage.READ.execute(new StorageProxy.LocalReadRunnable(rangeCommand, handler, requestTime, trackRepairedStatus)); - } - else - { - for (Replica replica : replicaPlan.contacts()) - { - Tracing.trace("Enqueuing request to {}", replica); - ReadCommand command = replica.isFull() ? rangeCommand : rangeCommand.copyAsTransientQuery(replica); - Message message = command.createMessage(trackRepairedStatus && replica.isFull(), requestTime); - MessagingService.instance().sendWithCallback(message, replica.endpoint(), handler); - } - } - - return new SingleRangeResponse(resolver, handler, readRepair); - } - - PartitionIterator sendNextRequests() - { - List concurrentQueries = new ArrayList<>(concurrencyFactor); - List> readRepairs = new ArrayList<>(concurrencyFactor); - - try - { - for (int i = 0; i < concurrencyFactor && replicaPlans.hasNext(); ) - { - ReplicaPlan.ForRangeRead replicaPlan = replicaPlans.next(); - - SingleRangeResponse response = query(replicaPlan, i == 0); - concurrentQueries.add(response); - readRepairs.add(response.getReadRepair()); - // due to RangeMerger, coordinator may fetch more ranges than required by concurrency factor. - rangesQueried += replicaPlan.vnodeCount(); - i += replicaPlan.vnodeCount(); - } - batchesRequested++; - } - catch (Throwable t) - { - for (PartitionIterator response : concurrentQueries) - response.close(); - throw t; - } - - Tracing.trace("Submitted {} concurrent range requests", concurrentQueries.size()); - // We want to count the results for the sake of updating the concurrency factor (see updateConcurrencyFactor) - // but we don't want to enforce any particular limit at this point (this could break code than rely on - // postReconciliationProcessing), hence the DataLimits.NONE. - counter = DataLimits.NONE.newCounter(command.nowInSec(), true, command.selectsFullPartition(), enforceStrictLiveness); - return counter.applyTo(StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs)); - } + protected abstract PartitionIterator sendNextRequests(); @Override public void close() @@ -267,11 +224,12 @@ public void close() } finally { + rangeMetrics.roundTrips.update(batchesRequested); // We track latency based on request processing time, since the amount of time that request spends in the queue // is not a representative metric of replica performance. long latency = nanoTime() - requestTime.startedAtNanos(); - rangeMetrics.addNano(latency); - rangeMetrics.roundTrips.update(batchesRequested); + rangeMetrics.executionTimeMetrics.addNano(latency); + rangeMetrics.serviceTimeMetrics.addNano(latency); Keyspace.openAndGetStore(command.metadata()).metric.coordinatorScanLatency.update(latency, TimeUnit.NANOSECONDS); } } diff --git a/src/java/org/apache/cassandra/service/reads/range/RangeCommands.java b/src/java/org/apache/cassandra/service/reads/range/RangeCommands.java index ded4d4cdbea6..dce6f190242c 100644 --- a/src/java/org/apache/cassandra/service/reads/range/RangeCommands.java +++ b/src/java/org/apache/cassandra/service/reads/range/RangeCommands.java @@ -34,6 +34,7 @@ import org.apache.cassandra.index.Index; import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.FBUtilities; @@ -55,10 +56,11 @@ public class RangeCommands public static PartitionIterator partitions(PartitionRangeReadCommand command, ConsistencyLevel consistencyLevel, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { // Note that in general, a RangeCommandIterator will honor the command limit for each range, but will not enforce it globally. - RangeCommandIterator rangeCommands = rangeCommandIterator(command, consistencyLevel, requestTime); + RangeCommandIterator rangeCommands = rangeCommandIterator(command, consistencyLevel, requestTime, readTracker); return command.limits().filter(command.postReconciliationProcessing(rangeCommands), command.nowInSec(), command.selectsFullPartition(), @@ -68,7 +70,8 @@ public static PartitionIterator partitions(PartitionRangeReadCommand command, @VisibleForTesting static RangeCommandIterator rangeCommandIterator(PartitionRangeReadCommand command, ConsistencyLevel consistencyLevel, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { Tracing.trace("Computing ranges to query"); @@ -77,9 +80,8 @@ static RangeCommandIterator rangeCommandIterator(PartitionRangeReadCommand comma command.indexQueryPlan(), keyspace, consistencyLevel); - if (command.isTopK()) - return new ScanAllRangesCommandIterator(keyspace, replicaPlans, command, replicaPlans.size(), requestTime); + return new ScanAllRangesCommandIterator(keyspace, replicaPlans, command, replicaPlans.size(), requestTime, readTracker); int maxConcurrencyFactor = Math.min(replicaPlans.size(), MAX_CONCURRENT_RANGE_REQUESTS); int concurrencyFactor = maxConcurrencyFactor; @@ -107,12 +109,14 @@ static RangeCommandIterator rangeCommandIterator(PartitionRangeReadCommand comma } ReplicaPlanMerger mergedReplicaPlans = new ReplicaPlanMerger(replicaPlans, keyspace, consistencyLevel); - return new RangeCommandIterator(mergedReplicaPlans, - command, - concurrencyFactor, - maxConcurrencyFactor, - replicaPlans.size(), - requestTime); + + return RangeCommandIterator.create(mergedReplicaPlans, + command, + concurrencyFactor, + maxConcurrencyFactor, + replicaPlans.size(), + requestTime, + readTracker); } /** @@ -128,7 +132,7 @@ static float estimateResultsPerRange(PartitionRangeReadCommand command, Keyspace Index.QueryPlan index = command.indexQueryPlan(); float maxExpectedResults = index == null ? command.limits().estimateTotalResults(cfs) - : index.getEstimatedResultRows(); + : command.indexQueryPlan().getEstimatedResultRows(); // adjust maxExpectedResults by the number of tokens this node has and the replication factor for this ks return (maxExpectedResults / DatabaseDescriptor.getNumTokens()) diff --git a/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java b/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java index c14dc3c4c850..70476be62fe2 100644 --- a/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java +++ b/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; - import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; @@ -38,7 +37,6 @@ import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.Pair; @@ -60,9 +58,10 @@ class ReplicaPlanIterator extends AbstractIterator this.keyspace = keyspace; this.consistency = consistency; + List> l = keyspace.getReplicationStrategy() instanceof LocalStrategy ? keyRange.unwrap() - : getRestrictedRanges(keyRange); + : getRestrictedRanges(keyspace.getReplicationStrategy().getTokenMetadata(), keyRange); this.ranges = l.iterator(); this.rangeCount = l.size(); } @@ -88,7 +87,7 @@ protected ReplicaPlan.ForRangeRead computeNext() * Compute all ranges we're going to query, in sorted order. Nodes can be replica destinations for many ranges, * so we need to restrict each scan to the specific range we want, or else we'd get duplicate results. */ - private static List> getRestrictedRanges(final AbstractBounds queryRange) + private static List> getRestrictedRanges(TokenMetadata tokenMetadata, final AbstractBounds queryRange) { // special case for bounds containing exactly 1 (non-minimum) token if (queryRange instanceof Bounds && queryRange.left.equals(queryRange.right) && !queryRange.left.isMinimum()) @@ -96,8 +95,6 @@ private static List> getRestrictedRanges(final return Collections.singletonList(queryRange); } - TokenMetadata tokenMetadata = StorageService.instance.getTokenMetadata(); - List> ranges = new ArrayList<>(); // divide the queryRange into pieces delimited by the ring and minimum tokens Iterator ringIter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), queryRange.left.getToken(), true); diff --git a/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java index 53f55f8938ae..e4630ae9f638 100644 --- a/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java +++ b/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java @@ -36,6 +36,7 @@ import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.QueryInfoTracker; import org.apache.cassandra.service.reads.DataResolver; import org.apache.cassandra.service.reads.ReadCallback; import org.apache.cassandra.service.reads.repair.NoopReadRepair; @@ -46,15 +47,9 @@ /** * A custom {@link RangeCommandIterator} that queries all replicas required by consistency level at once with data range * specify in {@link PartitionRangeReadCommand}. - *

    + * * This is to speed up {@link Index.QueryPlan#isTopK()} queries that needs to find global top-k rows in the cluster, because * existing {@link RangeCommandIterator} has to execute a top-k search per vnode range which is wasting resources. - *

    - * The implementation combines the replica plans for each data range into a single shared replica plan. This results in - * queries using reconciliation where it may not be expected. This is handled in the {@link DataResolver} for top-K queries - * so any usage for queries other that top-K should bear this in mind. - *

    - * It is important to note that this implementation can only be used with {@link ConsistencyLevel#ONE} and {@link ConsistencyLevel#LOCAL_ONE} */ public class ScanAllRangesCommandIterator extends RangeCommandIterator { @@ -63,9 +58,10 @@ public class ScanAllRangesCommandIterator extends RangeCommandIterator ScanAllRangesCommandIterator(Keyspace keyspace, CloseableIterator replicaPlans, PartitionRangeReadCommand command, int totalRangeCount, - Dispatcher.RequestTime requestTime) + Dispatcher.RequestTime requestTime, + QueryInfoTracker.ReadTracker readTracker) { - super(replicaPlans, command, totalRangeCount, totalRangeCount, totalRangeCount, requestTime); + super(replicaPlans, command, totalRangeCount, totalRangeCount, totalRangeCount, requestTime, readTracker); Preconditions.checkState(command.isTopK()); this.keyspace = keyspace; @@ -92,7 +88,7 @@ protected PartitionIterator sendNextRequests() ReplicaPlan.ForRangeRead plan = ReplicaPlans.forFullRangeRead(keyspace, consistencyLevel, command.dataRange().keyRange(), replicasToQuery, totalRangeCount); ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(plan); - DataResolver resolver = new DataResolver<>(command, sharedReplicaPlan, NoopReadRepair.instance, requestTime, false); + DataResolver resolver = new DataResolver<>(command, sharedReplicaPlan, NoopReadRepair.instance, requestTime, false, readTracker); ReadCallback handler = new ReadCallback<>(resolver, command, sharedReplicaPlan, requestTime); int nodes = 0; diff --git a/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java b/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java index 8343b83b071e..a43fb5b3d8e1 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java @@ -139,7 +139,7 @@ public void startRepair(DigestResolver digestResolver, Consumer resolver = new DataResolver<>(command, replicaPlan, this, requestTime, trackRepairedStatus); + DataResolver resolver = new DataResolver<>(command, replicaPlan, this, requestTime, trackRepairedStatus, digestResolver.getReadTracker()); ReadCallback readCallback = new ReadCallback<>(resolver, command, replicaPlan, requestTime); digestRepair = new DigestRepair<>(resolver, readCallback, resultConsumer); diff --git a/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java b/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java index a63cc7f6bfca..557b5b840709 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java @@ -70,7 +70,7 @@ ReadRepair create(ReadCommand command, ReplicaPlan.Shared replicaPla * to additional replicas not contacted in the initial full data read. If the collection of nodes that * end up responding in time end up agreeing on the data, and we don't consider the response from the * disagreeing replica that triggered the read repair, that's ok, since the disagreeing data would not - * have been successfully written and won't be included in the response the the client, preserving the + * have been successfully written and won't be included in the response the client, preserving the * expectation of monotonic quorum reads */ public void maybeSendAdditionalReads(); diff --git a/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java b/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java index a30efa1a776c..7640552930c8 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java +++ b/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java @@ -32,6 +32,7 @@ import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.marshal.Redaction; import org.apache.cassandra.diag.DiagnosticEvent; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.service.reads.DigestResolver; @@ -65,7 +66,7 @@ enum ReadRepairEventType { this.keyspace = readRepair.cfs.keyspace; this.tableName = readRepair.cfs.getTableName(); - this.cqlCommand = readRepair.command.toCQLString(); + this.cqlCommand = readRepair.command.toCQLString(Redaction.REDACT); this.consistency = readRepair.replicaPlan().consistencyLevel(); this.speculativeRetry = readRepair.cfs.metadata().params.speculativeRetry.kind(); this.destinations = destinations; diff --git a/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java b/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java index 353200088fe5..83093f74e32b 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java +++ b/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java @@ -183,13 +183,13 @@ private void applyToPartition(int i, Consumer f) if (writeBackTo.get(i)) { if (repairs[i] == null) - repairs[i] = new PartitionUpdate.Builder(command.metadata(), partitionKey, columns, 1); + repairs[i] = PartitionUpdate.builder(command.metadata(), partitionKey, columns, 1); f.accept(repairs[i]); } if (buildFullDiff) { if (repairs[repairs.length - 1] == null) - repairs[repairs.length - 1] = new PartitionUpdate.Builder(command.metadata(), partitionKey, columns, 1); + repairs[repairs.length - 1] = PartitionUpdate.builder(command.metadata(), partitionKey, columns, 1); f.accept(repairs[repairs.length - 1]); } } diff --git a/src/java/org/apache/cassandra/service/reads/thresholds/CoordinatorWarnings.java b/src/java/org/apache/cassandra/service/reads/thresholds/CoordinatorWarnings.java index f69be50ed285..d8ab03efeaba 100644 --- a/src/java/org/apache/cassandra/service/reads/thresholds/CoordinatorWarnings.java +++ b/src/java/org/apache/cassandra/service/reads/thresholds/CoordinatorWarnings.java @@ -86,7 +86,7 @@ public static void done() if (cfs == null) return; - String cql = command.toCQLString(); + String cql = command.toRedactedCQLString(); String loggableTokens = command.loggableTokens(); recordAborts(merged.tombstones, cql, loggableTokens, cfs.metric.clientTombstoneAborts, WarningsSnapshot::tombstoneAbortMessage); recordWarnings(merged.tombstones, cql, loggableTokens, cfs.metric.clientTombstoneWarnings, WarningsSnapshot::tombstoneWarnMessage); diff --git a/src/java/org/apache/cassandra/service/reads/thresholds/WarningsSnapshot.java b/src/java/org/apache/cassandra/service/reads/thresholds/WarningsSnapshot.java index 0a07c8360ea9..2c5647db9a80 100644 --- a/src/java/org/apache/cassandra/service/reads/thresholds/WarningsSnapshot.java +++ b/src/java/org/apache/cassandra/service/reads/thresholds/WarningsSnapshot.java @@ -107,19 +107,19 @@ WarningsSnapshot merge(WarningsSnapshot other) public void maybeAbort(ReadCommand command, ConsistencyLevel cl, int received, int blockFor, boolean isDataPresent, Map failureReasonByEndpoint) { if (!tombstones.aborts.instances.isEmpty()) - throw new TombstoneAbortException(tombstoneAbortMessage(tombstones.aborts.instances.size(), tombstones.aborts.maxValue, command.toCQLString()), tombstones.aborts.instances.size(), tombstones.aborts.maxValue, isDataPresent, + throw new TombstoneAbortException(tombstoneAbortMessage(tombstones.aborts.instances.size(), tombstones.aborts.maxValue, command.toRedactedCQLString()), tombstones.aborts.instances.size(), tombstones.aborts.maxValue, isDataPresent, cl, received, blockFor, failureReasonByEndpoint); if (!localReadSize.aborts.instances.isEmpty()) - throw new ReadSizeAbortException(localReadSizeAbortMessage(localReadSize.aborts.instances.size(), localReadSize.aborts.maxValue, command.toCQLString()), + throw new ReadSizeAbortException(localReadSizeAbortMessage(localReadSize.aborts.instances.size(), localReadSize.aborts.maxValue, command.toRedactedCQLString()), cl, received, blockFor, isDataPresent, failureReasonByEndpoint); if (!rowIndexReadSize.aborts.instances.isEmpty()) - throw new ReadSizeAbortException(rowIndexReadSizeAbortMessage(rowIndexReadSize.aborts.instances.size(), rowIndexReadSize.aborts.maxValue, command.toCQLString()), + throw new ReadSizeAbortException(rowIndexReadSizeAbortMessage(rowIndexReadSize.aborts.instances.size(), rowIndexReadSize.aborts.maxValue, command.toRedactedCQLString()), cl, received, blockFor, isDataPresent, failureReasonByEndpoint); if (!indexReadSSTablesCount.aborts.instances.isEmpty()) - throw new QueryReferencesTooManyIndexesAbortException(tooManyIndexesReadAbortMessage(indexReadSSTablesCount.aborts.instances.size(), indexReadSSTablesCount.aborts.maxValue, command.toCQLString()), + throw new QueryReferencesTooManyIndexesAbortException(tooManyIndexesReadAbortMessage(indexReadSSTablesCount.aborts.instances.size(), indexReadSSTablesCount.aborts.maxValue, command.toRedactedCQLString()), indexReadSSTablesCount.aborts.instances.size(), indexReadSSTablesCount.aborts.maxValue, isDataPresent, @@ -194,7 +194,10 @@ public int hashCode() @Override public String toString() { - return "(tombstones=" + tombstones + ", localReadSize=" + localReadSize + ", rowIndexTooLarge=" + rowIndexReadSize + ')'; + return "(tombstones=" + tombstones + + ", localReadSize=" + localReadSize + + ", rowIndexTooLarge=" + rowIndexReadSize + + ", indexReadSSTablesCount=" + indexReadSSTablesCount + ')'; } public static final class Warnings diff --git a/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java b/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java index d31df361e5f7..64e94d105efc 100644 --- a/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java +++ b/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java @@ -41,8 +41,11 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.Schema; import static org.apache.cassandra.db.Directories.SNAPSHOT_SUBDIR; import static org.apache.cassandra.service.snapshot.TableSnapshot.buildSnapshotId; @@ -54,28 +57,25 @@ public class SnapshotLoader { private static final Logger logger = LoggerFactory.getLogger(SnapshotLoader.class); - static final Pattern SNAPSHOT_DIR_PATTERN = Pattern.compile("(?\\w+)/(?\\w+)-(?[0-9a-f]{32})/snapshots/(?.+)$"); + static final Pattern SNAPSHOT_DIR_PATTERN = Pattern.compile("(?\\w+)/(?\\w+)" + + "(-(?[0-9a-f]{32}))?" + + "/snapshots/(?.+)$"); - private final Collection dataDirectories; + private final Collection dataDirectories; public SnapshotLoader() { this(DatabaseDescriptor.getAllDataFileLocations()); } - public SnapshotLoader(String[] dataDirectories) + public SnapshotLoader(File[] dataDirectories) { - this(Arrays.stream(dataDirectories).map(File::getPath).collect(Collectors.toList())); - } - - public SnapshotLoader(Collection dataDirs) - { - this.dataDirectories = dataDirs; + this.dataDirectories = Arrays.stream(dataDirectories).collect(Collectors.toList()); } public SnapshotLoader(Directories directories) { - this(directories.getCFDirectories().stream().map(File::toPath).collect(Collectors.toList())); + this.dataDirectories = directories.getCFDirectories(); } @VisibleForTesting @@ -149,12 +149,48 @@ private void loadSnapshotFromDir(Matcher snapshotDirMatcher, Path snapshotDir) { String keyspaceName = snapshotDirMatcher.group("keyspace"); String tableName = snapshotDirMatcher.group("tableName"); - UUID tableId = parseUUID(snapshotDirMatcher.group("tableId")); + final UUID tableId = maybeDetermineTableId(snapshotDirMatcher, snapshotDir, keyspaceName, tableName); String tag = snapshotDirMatcher.group("tag"); String snapshotId = buildSnapshotId(keyspaceName, tableName, tableId, tag); TableSnapshot.Builder builder = snapshots.computeIfAbsent(snapshotId, k -> new TableSnapshot.Builder(keyspaceName, tableName, tableId, tag)); builder.addSnapshotDir(new File(snapshotDir)); } + + private UUID maybeDetermineTableId(Matcher snapshotDirMatcher, Path snapshotDir, String keyspaceName, String tableName) + { + final UUID tableId; + if (snapshotDirMatcher.group("tableId") == null) + { + logger.debug("Snapshot directory without tableId found (pre-2.1 format): {}", snapshotDir); + // If we don't have a tableId in folder name (e.g pre 2.1 created table) + // Then attempt to get tableId from CFS on startup + // falling back to null is fine as it still yields a unique result in buildSnapshotId for pre-2.1 table + if (Keyspace.isInitialized() && Schema.instance.getKeyspaceMetadata(keyspaceName) != null) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(keyspaceName, tableName); + tableId = cfs != null && cfs.metadata.id != null + ? cfs.metadata.id.asUUID() + : null; + + if (tableId == null) + { + logger.warn("Snapshot directory without tableId found (pre-2.1 format), " + + "unable to resolve table id from column family, defaulting to null, snapshot dir: {}", snapshotDir); + } + } + else + { + logger.warn("Snapshot directory without tableId found (pre-2.1 format), " + + "keyspace is not initialized or there is a schema missing, defaulting to null, snapshot dir: {}", snapshotDir); + tableId = null; + } + } + else + { + tableId = parseUUID(snapshotDirMatcher.group("tableId")); + } + return tableId; + } } public Set loadSnapshots(String keyspace) @@ -166,15 +202,15 @@ public Set loadSnapshots(String keyspace) Map snapshots = new HashMap<>(); Visitor visitor = new Visitor(snapshots); - for (Path dataDir : dataDirectories) + for (File dataDir : dataDirectories) { if (keyspace != null) dataDir = dataDir.resolve(keyspace); try { - if (new File(dataDir).exists()) - Files.walkFileTree(dataDir, Collections.emptySet(), maxDepth, visitor); + if (dataDir.exists()) + Files.walkFileTree(dataDir.toPath(), Collections.emptySet(), maxDepth, visitor); else logger.debug("Skipping non-existing data directory {}", dataDir); } diff --git a/src/java/org/apache/cassandra/service/snapshot/SnapshotManager.java b/src/java/org/apache/cassandra/service/snapshot/SnapshotManager.java index 3925f3f9dc7b..b59e2dfc1c2c 100644 --- a/src/java/org/apache/cassandra/service/snapshot/SnapshotManager.java +++ b/src/java/org/apache/cassandra/service/snapshot/SnapshotManager.java @@ -53,7 +53,18 @@ public class SnapshotManager { private final long initialDelaySeconds; private final long cleanupPeriodSeconds; - private final SnapshotLoader snapshotLoader; + + private static class SnapshotLoaderHolder + { + // Use subclass for lazy initialization to avoid race with DatabaseDescriptor.createAllDirectories() + private static final SnapshotLoader snapshotLoader = new SnapshotLoader(DatabaseDescriptor.getAllDataFileLocations());; + } + + private static SnapshotLoader getSnapshotLoader() + { + // Return the singleton SnapshotLoader instance + return SnapshotManager.SnapshotLoaderHolder.snapshotLoader; + } @VisibleForTesting protected volatile ScheduledFuture cleanupTaskFuture; @@ -75,7 +86,6 @@ protected SnapshotManager(long initialDelaySeconds, long cleanupPeriodSeconds) { this.initialDelaySeconds = initialDelaySeconds; this.cleanupPeriodSeconds = cleanupPeriodSeconds; - snapshotLoader = new SnapshotLoader(DatabaseDescriptor.getAllDataFileLocations()); } public Collection getExpiringSnapshots() @@ -111,12 +121,12 @@ public synchronized void addSnapshot(TableSnapshot snapshot) public synchronized Set loadSnapshots(String keyspace) { - return snapshotLoader.loadSnapshots(keyspace); + return getSnapshotLoader().loadSnapshots(keyspace); } public synchronized Set loadSnapshots() { - return snapshotLoader.loadSnapshots(); + return getSnapshotLoader().loadSnapshots(); } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java b/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java index d10092a41af8..a048815c19f5 100644 --- a/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java +++ b/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java @@ -29,6 +29,8 @@ import java.util.UUID; import java.util.function.Predicate; +import javax.annotation.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +45,9 @@ public class TableSnapshot private final String keyspaceName; private final String tableName; + // tableId may be null under some rare circumstance namely pre-2.1 table + // whose snapshot is loaded upon startup rather than created while the jvm is running + @Nullable private final UUID tableId; private final String tag; private final boolean ephemeral; @@ -70,7 +75,8 @@ public TableSnapshot(String keyspaceName, String tableName, UUID tableId, * Unique identifier of a snapshot. Used * only to deduplicate snapshots internally, * not exposed externally. - * + * table_id may be empty for tables created prior to 2.1 + *

    * Format: "$ks:$table_name:$table_id:$tag" */ public String getId() @@ -224,7 +230,8 @@ public String toString() '}'; } - static class Builder { + static class Builder + { private final String keyspaceName; private final String tableName; private final UUID tableId; @@ -282,9 +289,9 @@ TableSnapshot build() } } - protected static String buildSnapshotId(String keyspaceName, String tableName, UUID tableId, String tag) + protected static String buildSnapshotId(String keyspaceName, String tableName, @Nullable UUID tableId, String tag) { - return String.format("%s:%s:%s:%s", keyspaceName, tableName, tableId, tag); + return String.format("%s:%s:%s:%s", keyspaceName, tableName, tableId == null ? "" : tableId, tag); } public static class SnapshotTrueSizeCalculator extends DirectorySizeCalculator diff --git a/src/java/org/apache/cassandra/streaming/ProgressInfo.java b/src/java/org/apache/cassandra/streaming/ProgressInfo.java index 159775c324cc..683b1360aa98 100644 --- a/src/java/org/apache/cassandra/streaming/ProgressInfo.java +++ b/src/java/org/apache/cassandra/streaming/ProgressInfo.java @@ -60,7 +60,6 @@ public static Direction fromByte(byte direction) public ProgressInfo(InetAddressAndPort peer, int sessionIndex, String fileName, Direction direction, long currentBytes, long deltaBytes, long totalBytes) { - this.peer = peer; this.sessionIndex = sessionIndex; this.fileName = fileName; diff --git a/src/java/org/apache/cassandra/streaming/StreamHook.java b/src/java/org/apache/cassandra/streaming/StreamHook.java index 84db420f04aa..df1e8ee76ef0 100644 --- a/src/java/org/apache/cassandra/streaming/StreamHook.java +++ b/src/java/org/apache/cassandra/streaming/StreamHook.java @@ -37,7 +37,7 @@ static StreamHook createHook() String className = STREAM_HOOK.getString(); if (className != null) { - return FBUtilities.construct(className, StreamHook.class.getSimpleName()); + return FBUtilities.construct(className, StreamHook.class.getSimpleName(), StreamHook.class); } else { diff --git a/src/java/org/apache/cassandra/streaming/StreamOperation.java b/src/java/org/apache/cassandra/streaming/StreamOperation.java index 98a4070d2b0c..c195b4a0cf6c 100644 --- a/src/java/org/apache/cassandra/streaming/StreamOperation.java +++ b/src/java/org/apache/cassandra/streaming/StreamOperation.java @@ -17,6 +17,8 @@ */ package org.apache.cassandra.streaming; +import org.apache.cassandra.db.compaction.OperationType; + public enum StreamOperation { OTHER("Other", true, false), // Fallback to avoid null types when deserializing from string @@ -26,7 +28,9 @@ public enum StreamOperation BOOTSTRAP("Bootstrap", false, true), REBUILD("Rebuild", false, true), BULK_LOAD("Bulk Load", true, false), - REPAIR("Repair", true, false); + REPAIR("Repair", true, false), + REGION_DECOMMISSION("Region Decommission", false, true), + REGION_REPAIR("Region Repair", true, false); private final String description; private final boolean requiresViewBuild; @@ -71,4 +75,17 @@ public boolean keepSSTableLevel() { return keepSSTableLevel; } + + /** + * @return the corresponding compaction operation type + */ + public OperationType opType() + { + switch (this) + { + case REGION_DECOMMISSION: return OperationType.REGION_DECOMMISSION; + case REGION_REPAIR: return OperationType.REGION_REPAIR; + default: return OperationType.STREAM; + } + } } diff --git a/src/java/org/apache/cassandra/streaming/StreamPlan.java b/src/java/org/apache/cassandra/streaming/StreamPlan.java index 47fa9e1463bf..46a0d98420f2 100644 --- a/src/java/org/apache/cassandra/streaming/StreamPlan.java +++ b/src/java/org/apache/cassandra/streaming/StreamPlan.java @@ -215,6 +215,11 @@ public TimeUUID getPendingRepair() return coordinator.getPendingRepair(); } + public TimeUUID getPlanId() + { + return planId; + } + public boolean getFlushBeforeTransfer() { return flushBeforeTransfer; diff --git a/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java b/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java index 002e1827148a..f1d7d595bb48 100644 --- a/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java +++ b/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java @@ -25,6 +25,8 @@ import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.utils.JVMStabilityInspector; @@ -126,7 +128,8 @@ public void run() { try { - if (ColumnFamilyStore.getIfExists(task.tableId) == null) + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(task.tableId); + if (cfs == null) { // schema was dropped during streaming task.receiver.abort(); @@ -134,6 +137,10 @@ public void run() return; } + + if (!CassandraRelevantProperties.CDC_STREAMING_ENABLED.getBoolean() && cfs.metadata().params.cdc) + throw new RuntimeException(String.format("Streaming CDC-enabled sstables is not supported, aborting table %s", cfs)); + task.receiver.finished(); task.session.taskCompleted(task); } diff --git a/src/java/org/apache/cassandra/streaming/StreamRequest.java b/src/java/org/apache/cassandra/streaming/StreamRequest.java index dba67917b965..656403812c27 100644 --- a/src/java/org/apache/cassandra/streaming/StreamRequest.java +++ b/src/java/org/apache/cassandra/streaming/StreamRequest.java @@ -76,6 +76,7 @@ public String toString() public static class StreamRequestSerializer implements IVersionedSerializer { + @Override public void serialize(StreamRequest request, DataOutputPlus out, int version) throws IOException { out.writeUTF(request.keyspace); @@ -100,6 +101,7 @@ private void serializeReplicas(RangesAtEndpoint replicas, DataOutputPlus out, in } } + @Override public StreamRequest deserialize(DataInputPlus in, int version) throws IOException { String keyspace = in.readUTF(); @@ -131,13 +133,14 @@ RangesAtEndpoint deserializeReplicas(DataInputPlus in, int version, InetAddressA return replicas.build(); } + @Override public long serializedSize(StreamRequest request, int version) { - int size = TypeSizes.sizeof(request.keyspace); + long size = TypeSizes.sizeof(request.keyspace); size += TypeSizes.sizeof(request.columnFamilies.size()); size += inetAddressAndPortSerializer.serializedSize(request.full.endpoint(), version); - size += replicasSerializedSize(request.transientReplicas, version); size += replicasSerializedSize(request.full, version); + size += replicasSerializedSize(request.transientReplicas, version); for (String cf : request.columnFamilies) size += TypeSizes.sizeof(cf); return size; diff --git a/src/java/org/apache/cassandra/streaming/StreamSession.java b/src/java/org/apache/cassandra/streaming/StreamSession.java index 33f02c3d0daf..c819e772c692 100644 --- a/src/java/org/apache/cassandra/streaming/StreamSession.java +++ b/src/java/org/apache/cassandra/streaming/StreamSession.java @@ -49,6 +49,7 @@ import io.netty.channel.Channel; import io.netty.util.concurrent.Future; //checkstyle: permit this import +import org.apache.cassandra.db.compaction.CompactionStrategyContainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +59,6 @@ import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.compaction.CompactionManager; -import org.apache.cassandra.db.compaction.CompactionStrategyManager; import org.apache.cassandra.db.lifecycle.TransactionAlreadyCompletedException; import org.apache.cassandra.dht.OwnedRanges; import org.apache.cassandra.dht.Range; @@ -446,7 +446,7 @@ synchronized void addTransferRanges(String keyspace, RangesAtEndpoint replicas, { failIfFinished(); Collection stores = getColumnFamilyStores(keyspace, columnFamilies); - if (flushTables) + if (flushTables && DatabaseDescriptor.supportsFlushBeforeStreaming()) flushSSTables(stores); //Was it safe to remove this normalize, sorting seems not to matter, merging? Maybe we should have? @@ -969,7 +969,7 @@ static boolean checkPendingCompactions(Map perTableIdIncomingByte .collect(Collectors.toMap(ks::getColumnFamilyStore, Function.identity())); for (ColumnFamilyStore cfs : ks.getColumnFamilyStores()) { - CompactionStrategyManager csm = cfs.getCompactionStrategyManager(); + CompactionStrategyContainer csm = cfs.getCompactionStrategyContainer(); int tasksOther = csm.getEstimatedRemainingTasks(); int tasksStreamed = tasksOther; if (cfStreamed.containsKey(cfs)) diff --git a/src/java/org/apache/cassandra/streaming/async/NettyStreamingChannel.java b/src/java/org/apache/cassandra/streaming/async/NettyStreamingChannel.java index 8515ad6089e7..c646eef0b5e0 100644 --- a/src/java/org/apache/cassandra/streaming/async/NettyStreamingChannel.java +++ b/src/java/org/apache/cassandra/streaming/async/NettyStreamingChannel.java @@ -59,6 +59,9 @@ public class NettyStreamingChannel extends ChannelInboundHandlerAdapter implemen @VisibleForTesting static final AttributeKey TRANSFERRING_FILE_ATTR = valueOf("transferringFile"); + + public static final AttributeKey STREAMING_VERSION_ATTR = valueOf("streamingVersion"); + final Channel channel; /** @@ -124,6 +127,12 @@ public StreamingDataInputPlus in() return in; } + int streamingVersion(int defaultVersion) + { + Integer channelStreamingVersion = channel.attr(STREAMING_VERSION_ATTR).get(); + return channelStreamingVersion == null ? defaultVersion : channelStreamingVersion; + } + public StreamingDataOutputPlus acquireOut() { if (!channel.attr(TRANSFERRING_FILE_ATTR).compareAndSet(false, true)) diff --git a/src/java/org/apache/cassandra/streaming/async/NettyStreamingConnectionFactory.java b/src/java/org/apache/cassandra/streaming/async/NettyStreamingConnectionFactory.java index 4002f9b61fc5..c54746e4c015 100644 --- a/src/java/org/apache/cassandra/streaming/async/NettyStreamingConnectionFactory.java +++ b/src/java/org/apache/cassandra/streaming/async/NettyStreamingConnectionFactory.java @@ -65,7 +65,9 @@ public static NettyStreamingChannel connect(OutboundConnectionSettings template, result.awaitUninterruptibly(); // initiate has its own timeout, so this is "guaranteed" to return relatively promptly if (result.isSuccess()) { - Channel channel = result.getNow().success().channel; + StreamingSuccess success = result.getNow().success(); + Channel channel = success.channel; + channel.attr(NettyStreamingChannel.STREAMING_VERSION_ATTR).set(success.messagingVersion); NettyStreamingChannel streamingChannel = new NettyStreamingChannel(channel, kind); if (kind == StreamingChannel.Kind.CONTROL) { diff --git a/src/java/org/apache/cassandra/streaming/async/StreamCompressionSerializer.java b/src/java/org/apache/cassandra/streaming/async/StreamCompressionSerializer.java index f7d8101a8d4a..a56517fba2ca 100644 --- a/src/java/org/apache/cassandra/streaming/async/StreamCompressionSerializer.java +++ b/src/java/org/apache/cassandra/streaming/async/StreamCompressionSerializer.java @@ -29,8 +29,6 @@ import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.streaming.StreamingDataOutputPlus; -import static org.apache.cassandra.net.MessagingService.current_version; - /** * A serialiazer for stream compressed files (see package-level documentation). Much like a typical compressed * output stream, this class operates on buffers or chunks of the data at a a time. The format for each compressed @@ -56,7 +54,6 @@ public StreamCompressionSerializer(ByteBufAllocator allocator) public static StreamingDataOutputPlus.Write serialize(LZ4Compressor compressor, ByteBuffer in, int version) { - assert version == current_version; return bufferSupplier -> { int uncompressedLength = in.remaining(); int maxLength = compressor.maxCompressedLength(uncompressedLength); diff --git a/src/java/org/apache/cassandra/streaming/async/StreamingMultiplexedChannel.java b/src/java/org/apache/cassandra/streaming/async/StreamingMultiplexedChannel.java index 751928c19096..bdd424f43433 100644 --- a/src/java/org/apache/cassandra/streaming/async/StreamingMultiplexedChannel.java +++ b/src/java/org/apache/cassandra/streaming/async/StreamingMultiplexedChannel.java @@ -170,7 +170,7 @@ private StreamingChannel createControlChannel() throws IOException StreamingChannel channel = factory.create(to, messagingVersion, StreamingChannel.Kind.CONTROL); executorFactory().startThread(String.format("Stream-Deserializer-%s-%s", to.toString(), channel.id()), - new StreamDeserializingTask(session, channel, messagingVersion)); + new StreamDeserializingTask(session, channel, streamingVersion(channel))); session.attachInbound(channel); session.attachOutbound(channel); @@ -227,7 +227,8 @@ public Future sendMessage(StreamingChannel channel, StreamMessage message) { Future promise = channel.send(outSupplier -> { // we anticipate that the control messages are rather small, so allocating a ByteBuf shouldn't blow out of memory. - long messageSize = serializedSize(message, messagingVersion); + int channelStreamingVersion = streamingVersion(channel); + long messageSize = serializedSize(message, channelStreamingVersion); if (messageSize > 1 << 30) { throw new IllegalStateException(format("%s something is seriously wrong with the calculated stream control message's size: %d bytes, type is %s", @@ -235,7 +236,7 @@ public Future sendMessage(StreamingChannel channel, StreamMessage message) } try (StreamingDataOutputPlus out = outSupplier.apply((int) messageSize)) { - StreamMessage.serialize(message, out, messagingVersion, session); + StreamMessage.serialize(message, out, channelStreamingVersion, session); } }); promise.addListener(future -> onMessageComplete(future, message)); @@ -317,7 +318,7 @@ public void run() // close the DataOutputStreamPlus as we're done with it - but don't close the channel try (StreamingDataOutputPlus out = channel.acquireOut()) { - serialize(msg, out, messagingVersion, session); + serialize(msg, out, streamingVersion(channel), session); } } catch (Exception e) @@ -499,6 +500,13 @@ int semaphoreAvailablePermits() return fileTransferSemaphore.permits(); } + private int streamingVersion(StreamingChannel channel) + { + return channel instanceof NettyStreamingChannel + ? ((NettyStreamingChannel) channel).streamingVersion(messagingVersion) + : messagingVersion; + } + public boolean connected() { return !closed && (controlChannel == null || controlChannel.connected()); diff --git a/src/java/org/apache/cassandra/tools/AuditLogViewer.java b/src/java/org/apache/cassandra/tools/AuditLogViewer.java index f226aa2e706d..586f2824d5bc 100644 --- a/src/java/org/apache/cassandra/tools/AuditLogViewer.java +++ b/src/java/org/apache/cassandra/tools/AuditLogViewer.java @@ -32,10 +32,10 @@ import org.apache.commons.cli.ParseException; import net.openhft.chronicle.core.io.IORuntimeException; -import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; import net.openhft.chronicle.queue.ExcerptTailer; import net.openhft.chronicle.queue.RollCycles; import net.openhft.chronicle.queue.impl.single.SingleChronicleQueue; +import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; import net.openhft.chronicle.threads.Pauser; import net.openhft.chronicle.wire.ReadMarshallable; import net.openhft.chronicle.wire.WireIn; diff --git a/src/java/org/apache/cassandra/tools/CompactionLogAnalyzer.java b/src/java/org/apache/cassandra/tools/CompactionLogAnalyzer.java new file mode 100644 index 000000000000..e754936240e6 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/CompactionLogAnalyzer.java @@ -0,0 +1,561 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.io.BufferedReader; +import java.io.File; //checkstyle: permit this import +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.common.io.ByteStreams; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.PosixParser; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.utils.FBUtilities; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + + +// +// Analyzes a collection of CSV logs from the unified compaction strategy. Run with +// +// tools/bin/analyzecompactionlog +// +// It will process the CSVs are create a compaction_report.html file in the target directory. The file is similar to our +// performance reports. +// +public class CompactionLogAnalyzer +{ + + private static final Options options = new Options(); + private static CommandLine cmd; + + public static final String OPTION_LIMIT = "l"; + public static final String OPTION_RESOLUTION = "r"; + + static + { + DatabaseDescriptor.toolInitialization(); + + Option optLimit = new Option(OPTION_LIMIT, true, "If specified, will only read this number of events " + + "from the first file, and up to that time from the others."); + optLimit.setArgs(1); + options.addOption(optLimit); + + Option optResolution = new Option(OPTION_RESOLUTION, true, "The resolution of the produced" + + "report in milliseconds, 100 by default."); + optResolution.setArgs(1); + options.addOption(optResolution); + } + + /** + * A data point represents both an input data point as well as aggregated data for a level or total. + */ + static class DataPoint + { + String shardId; + long timestamp; + int bucket; + // number of sstables + int sstables; + // max number of overlapping sstables in bucket + int overlap; + // total size of the sstables + long size; + // number of running compactions + int compactionsInProgress; + // number of compactions to do + int compactionsPending; + // bytes read per second + long readBytesPerSecond; + // bytes written per second + long writeBytesPerSecond; + // total bytes to compact + long totalBytes; + // remaining bytes to compact + long remainingReadBytes; + // value of scaling parameter W + int scalingParameter; + + /** + * Called to aggregate data in response to a new data point for a bucket. + * Unless the process is just starting, the new data point will be replacing the older state of the bucket, + * thus this will add the new data but also remove the older values. + */ + private void updateTotals(DataPoint toAdd, DataPoint toRemove) + { + timestamp = toAdd.timestamp; + compactionsInProgress += toAdd.compactionsInProgress - toRemove.compactionsInProgress; + compactionsPending += toAdd.compactionsPending - toRemove.compactionsPending; + sstables += toAdd.sstables - toRemove.sstables; + size += toAdd.size - toRemove.size; + readBytesPerSecond += toAdd.readBytesPerSecond - toRemove.readBytesPerSecond; + writeBytesPerSecond += toAdd.writeBytesPerSecond - toRemove.writeBytesPerSecond; + totalBytes += toAdd.totalBytes - toRemove.totalBytes; + remainingReadBytes += toAdd.remainingReadBytes - toRemove.remainingReadBytes; + scalingParameter = toAdd.scalingParameter; + overlap = toAdd.overlap; + } + } + + + static final Pattern CSVNamePattern = Pattern.compile("compaction-(\\w+)-([^-]*)-([^-]*)(-([^.]*))?\\.csv"); + private static final String fullDateFormatter = "yyyy-MM-dd' 'HH:mm:ss.SSS"; + + static int reportResolutionInMs; + + // Indexes of the relevant columns in the source CSV, set by initializeIndexes below. + static int timestampIndex = -1; + static int eventIndex; + static int bucketIndex; + static int sstablesIndex; + static int overlapIndex; + static int compactingSstablesIndex; + static int sizeIndex; + static int compactionsIndex; + static int readPerSecIndex; + static int writePerSecIndex; + static int sizesIndex; + static int Windex; + + private static void initializeIndexes(String header) + { + if (timestampIndex < 0) + synchronized (CompactionLogAnalyzer.class) { + if (timestampIndex < 0) + { + Map indexMap = new HashMap<>(); + String[] headers = header.split(","); + for (int i = 0; i < headers.length; ++i) + indexMap.put(headers[i], i); + + timestampIndex = indexMap.get("Timestamp"); + eventIndex = indexMap.get("Event"); + bucketIndex = indexMap.getOrDefault("Level", indexMap.get("Bucket")); + sstablesIndex = indexMap.get("Tot. SSTables"); + overlapIndex = indexMap.get("Overlap"); + compactingSstablesIndex = indexMap.get("Comp. SSTables"); + sizeIndex = indexMap.getOrDefault("Size (bytes)", -1); + sizeIndex = indexMap.get("Tot. size (bytes)"); + compactionsIndex = indexMap.get("Compactions"); + readPerSecIndex = indexMap.get("Read (bytes/sec)"); + writePerSecIndex = indexMap.get("Write (bytes/sec)"); + sizesIndex = indexMap.getOrDefault("Tot/Read/Written", -1); + sizesIndex = indexMap.get("Tot. comp. size/Read/Written (bytes)"); + Windex = indexMap.get("W"); + } + } + } + + static DataPoint parse(String shardId, String dataLine) throws ParseException + { + String[] data = dataLine.split(","); + + DataPoint dp = new DataPoint(); + dp.shardId = shardId; + dp.timestamp = getTimestamp(data[timestampIndex]); + dp.bucket = Integer.parseInt(data[bucketIndex]); + dp.sstables = Integer.parseInt(data[sstablesIndex]); + dp.size = parseHumanReadableSize(data[sizeIndex]); + final String[] compactions = data[compactionsIndex].split("/"); + dp.compactionsInProgress = Integer.parseInt(compactions[1]); + dp.compactionsPending = Integer.parseInt(compactions[0]); + dp.readBytesPerSecond = parseHumanReadableRate(data[readPerSecIndex]); + dp.writeBytesPerSecond = parseHumanReadableRate(data[writePerSecIndex]); + String[] sizes = data[sizesIndex].split("/"); + dp.totalBytes = parseHumanReadableSize(sizes[0]); + dp.remainingReadBytes = dp.totalBytes - parseHumanReadableSize(sizes[1]); + dp.scalingParameter = UnifiedCompactionStrategy.parseScalingParameter(data[Windex]); + if (overlapIndex >= 0) + { + dp.overlap = Integer.parseInt(data[overlapIndex]); + // Note: This overlap does not include the sstables that are currently compacting. Having such a measure + // could be valuable, but it needs processing that the strategy does not do (to improve efficiency the + // overlap sets construction only uses non-compacting sstables). + } + else + { + // The number of non-compacting sstables in a bucket is the proxy the strategy used for overlapping sstables. + int compactingSSTables = Integer.parseInt(data[compactingSstablesIndex].split("/")[1]); + dp.overlap = dp.sstables - compactingSSTables; + } + return dp; + } + + private static long getTimestamp(String datum) throws ParseException + { + Date date = new SimpleDateFormat(fullDateFormatter).parse(datum); + return date.getTime(); + } + + private static long parseHumanReadableSize(String datum) + { + return FBUtilities.parseHumanReadableBytes(datum); + } + + private static long parseHumanReadableRate(String datum) + { + return (long) FBUtilities.parseHumanReadable(datum, null, "B/s"); + } + + public static void generateGraph(File htmlFile, JSONObject stats) + { + try (PrintWriter out = new PrintWriter(htmlFile)) + { + String statsBlock = "/* stats start */\nstats = " + stats.toJSONString() + ";\n/* stats end */\n"; + String html = getGraphHTML().replaceFirst("/\\* stats start \\*/\n\n/\\* stats end \\*/\n", statsBlock); + out.write(html); + } + catch (IOException e) + { + throw new RuntimeException("Couldn't write stats html."); + } + } + + private static String getGraphHTML() + { + try (InputStream graphHTMLRes = CompactionLogAnalyzer.class.getClassLoader().getResourceAsStream("org/apache/cassandra/graph/graph.html")) + { + return new String(ByteStreams.toByteArray(graphHTMLRes)); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) throws Exception + { + CommandLineParser parser = new PosixParser(); + try + { + cmd = parser.parse(options, args); + } + catch (org.apache.commons.cli.ParseException e1) + { + System.err.println(e1.getMessage()); + printUsage(); + System.exit(1); + } + + if (cmd.getArgs().length != 1) + { + System.err.println("You must supply exactly one log csv path."); + printUsage(); + System.exit(1); + } + + File logPath = new File(cmd.getArgs()[0]); // checkstyle: permit this instantiation + File[] files = logPath.listFiles(f -> CSVNamePattern.matcher(f.getName()).matches()); + Arrays.sort(files); + + reportResolutionInMs = Integer.parseInt(cmd.getOptionValue(OPTION_RESOLUTION, "100")); + + final String limitOption = cmd.getOptionValue(OPTION_LIMIT); + Integer lineCountLimit = limitOption == null ? null : Integer.parseInt(limitOption); + + List dataPoints = readDataPoints(files, lineCountLimit); + dataPoints.sort((a, b) -> Long.compare(a.timestamp, b.timestamp)); + + JSONArray marr = processData(dataPoints); + JSONObject main = new JSONObject(); + main.put("title", "Compaction report"); + main.put("stats", marr); + + generateGraph(new File(logPath.getPath() + File.separator + "compaction_report.html"), main); // checkstyle: permit this instantiation + + System.exit(0); + } + + @VisibleForTesting + static List readDataPoints(File[] files, @Nullable Integer lineCountLimit) throws IOException, ParseException + { + List dataPoints; + + if (lineCountLimit != null) + { + long timestampLimit = Long.MAX_VALUE; + dataPoints = new ArrayList<>(); + + for (File file : files) + timestampLimit = readDataPoints(dataPoints, lineCountLimit, timestampLimit, file); + } + else + { + // Reading the files can take a long time. Do it in parallel. + dataPoints = Arrays.stream(files) + .parallel() + .flatMap(file -> + { + List pts = new ArrayList<>(); + try + { + readDataPoints(pts, Integer.MAX_VALUE, Long.MAX_VALUE, file); + return pts.stream(); + } + catch (Exception e) + { + throw Throwables.propagate(e); + } + }) + .collect(Collectors.toList()); + } + + return dataPoints; + } + + private static long readDataPoints(List dataPoints, int lineCountLimit, long timestampLimit, File file) throws IOException, ParseException + { + Matcher m = CSVNamePattern.matcher(file.getName()); + if (!m.matches()) + throw new AssertionError(); + + String shardId = m.group(5); + if (shardId == null) + shardId = "none"; + try (BufferedReader rdr = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) + { + String header = rdr.readLine(); + initializeIndexes(header); + DataPoint curr = null; + + int lineCount = 0; + + while (rdr.ready()) + { + if (++lineCount > lineCountLimit && curr != null) + { + timestampLimit = curr.timestamp; + break; + } + + String line = rdr.readLine(); + if (line.isEmpty()) + continue; + + try + { + curr = parse(shardId, line); + } + catch (NumberFormatException | ParseException | ArrayIndexOutOfBoundsException e) + { + System.out.format("%s parsing line %s, skipping.\n", e.getMessage(), line); + continue; + } + if (curr.timestamp > timestampLimit) + break; + dataPoints.add(curr); + } + System.out.format("%d data points processed for shard %s.\n", lineCount, shardId); + } + return timestampLimit; + } + + @VisibleForTesting + static JSONArray processData(List dataPoints) + { + int levels = dataPoints.stream().mapToInt(dp -> dp.bucket).max().getAsInt() + 1; + + // Prepare the JSON objects representing the data in the report + JSONArray marr = new JSONArray(); + + JSONArray[] intervalsPerLevel = new JSONArray[levels + 1]; + Table progressMap = HashBasedTable.create(); + DataPoint totals = new DataPoint(); + DataPoint[] perLevel = new DataPoint[levels + 1]; + perLevel[levels] = totals; + DataPoint zero = new DataPoint(); + totals.shardId = "Total"; + totals.bucket = levels; + + JSONArray metricsHeader = makeMetricsHeader(); + for (int i = 0; i < levels; ++i) + { + perLevel[i] = new DataPoint(); + perLevel[i].shardId = "Level " + i; + perLevel[i].bucket = i; + } + + + for (int i = 0; i <= levels; ++i) + { + intervalsPerLevel[i] = new JSONArray(); + + JSONObject stats = new JSONObject(); + stats.put("revision", perLevel[i].shardId); + stats.put("test", "Compaction"); + stats.put("metrics", metricsHeader); + stats.put("intervals", intervalsPerLevel[i]); + marr.add(stats); + } + + System.out.println("Totals"); + System.out.format("%25s %8s %9s %15s %15s %15s %15s\n", "Timestamp", "SSTables", "Run/Pendg", "Read tput", "Write tput", "TotalCompBytes", "RemCompBytes"); + + // Process the data points to compile aggregate state and report it with the specified resolution. + long startTimestamp = -1; + int count = 0; + for (DataPoint dp : dataPoints) + { + // Data points replace previous data for the given bucket. This map is used to find what is replaced. + DataPoint prev = progressMap.get(dp.shardId, dp.bucket); + if (prev == null) + prev = zero; + + if (startTimestamp == -1) + startTimestamp = dp.timestamp; + else if (dp.timestamp >= totals.timestamp + reportResolutionInMs) + { + report(intervalsPerLevel, progressMap, perLevel, startTimestamp); + ++count; + } + + totals.updateTotals(dp, prev); + perLevel[dp.bucket].updateTotals(dp, prev); + progressMap.put(dp.shardId, dp.bucket, dp); + } + report(intervalsPerLevel, progressMap, perLevel, startTimestamp); + ++count; + + System.out.format("Wrote %d datapoints, spanning %.1f seconds\n", count, (totals.timestamp - startTimestamp) / 1000.0); + return marr; + } + + private static void report(JSONArray[] intervalsPerLevel, + Table progressMap, + DataPoint[] perLevel, + long startTimestamp) + { + // Collect a histogram of the number of sstables per bucket. + int levels = perLevel.length - 1; + + int maxOverlap = -1; + for (DataPoint bucket : progressMap.values()) + { + maxOverlap = Math.max(maxOverlap, bucket.overlap); + } + perLevel[levels].overlap = maxOverlap; + + print(perLevel[levels]); // print out the totals on the console + for (int i = 0; i <= levels; ++i) + addMetrics(perLevel[i], intervalsPerLevel[i], startTimestamp); + } + + private static JSONArray makeMetricsHeader() + { + JSONArray metrics = new JSONArray(); + metrics.add("SSTables"); + metrics.add("Size MB"); + metrics.add("Running compactions"); + metrics.add("Pending compactions"); + metrics.add("Read throughput MB/s"); + metrics.add("Write throughput MB/s"); + metrics.add("Read throughput per thread MB/s"); + metrics.add("Write throughput per thread MB/s"); + metrics.add("Total GB to compact"); + metrics.add("Remaining GB to compact"); + metrics.add("Max overlapping SSTables"); + metrics.add("Scaling parameter W"); + + metrics.add("time"); + return metrics; + } + + private static void addMetrics(DataPoint totals, JSONArray intervals, long startTimestamp) + { + if (totals.timestamp < startTimestamp) + return; // nothing to add yet + + JSONArray metrics = new JSONArray(); + metrics.add(totals.sstables); + metrics.add(Math.scalb(totals.size, -20)); + metrics.add(totals.compactionsInProgress); + metrics.add(totals.compactionsPending); + metrics.add(Math.scalb(totals.readBytesPerSecond, -20)); + metrics.add(Math.scalb(totals.writeBytesPerSecond, -20)); + if (totals.compactionsInProgress > 0) + { + long readThroughput = totals.readBytesPerSecond / totals.compactionsInProgress; + long writeThroughput = totals.writeBytesPerSecond / totals.compactionsInProgress; + metrics.add(Math.scalb(readThroughput, -20)); + metrics.add(Math.scalb(writeThroughput, -20)); + } + else + { + metrics.add(null); + metrics.add(null); + } + metrics.add(Math.scalb(totals.totalBytes, -30)); + metrics.add(Math.scalb(totals.remainingReadBytes, -30)); + + metrics.add(totals.overlap); + + metrics.add(totals.scalingParameter); + + metrics.add((totals.timestamp - startTimestamp) / 1000.0); + intervals.add(metrics); + } + + static void print(DataPoint dp) + { + System.out.format("%25s %8s %3d/%5d %13s/s %13s/s %15s %15s\n", + new SimpleDateFormat(fullDateFormatter).format(new Date(dp.timestamp)), + dp.sstables, + dp.compactionsInProgress, + dp.compactionsPending, + FBUtilities.prettyPrintMemory(dp.readBytesPerSecond), + FBUtilities.prettyPrintMemory(dp.writeBytesPerSecond), + FBUtilities.prettyPrintMemory(dp.totalBytes), + FBUtilities.prettyPrintMemory(dp.remainingReadBytes)); + } + + private static void printUsage() + { + String usage = String.format("analyzecompactionlog %n"); + String header = "Perform an analysis of the UCS compaction log.\n\n" + + "The input is a directory that contains the per-shard CSV files generated using the " + + "'logAll: true' flag by the unified compaction strategy.\n" + + "Constructs a compaction_report.html in the target directory with summarized metrics."; + new HelpFormatter().printHelp(usage, header, options, ""); + } +} diff --git a/src/java/org/apache/cassandra/tools/JsonTransformer.java b/src/java/org/apache/cassandra/tools/JsonTransformer.java index 8debfd3b7594..6b4f50f14889 100644 --- a/src/java/org/apache/cassandra/tools/JsonTransformer.java +++ b/src/java/org/apache/cassandra/tools/JsonTransformer.java @@ -169,9 +169,9 @@ private void serializePartitionKey(DecoratedKey key) } int i = 0; - while (keyBytes.remaining() > 0 && i < compositeType.getComponents().size()) + while (keyBytes.remaining() > 0 && i < compositeType.subTypes().size()) { - AbstractType colType = compositeType.getComponents().get(i); + AbstractType colType = compositeType.subTypes().get(i); ByteBuffer value = ByteBufferUtil.readBytesWithShortLength(keyBytes); String colValue = colType.getString(value); diff --git a/src/java/org/apache/cassandra/tools/LoaderOptions.java b/src/java/org/apache/cassandra/tools/LoaderOptions.java index 03c8ee60244d..a4265c3ce44a 100644 --- a/src/java/org/apache/cassandra/tools/LoaderOptions.java +++ b/src/java/org/apache/cassandra/tools/LoaderOptions.java @@ -50,6 +50,7 @@ import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.tools.BulkLoader.CmdLineOptions; +import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND; @@ -546,8 +547,8 @@ public Builder parseArgs(String cmdArgs[]) throttleBytes = config.stream_throughput_outbound.toBytesPerSecondAsInt(); if (cmd.hasOption(SSL_STORAGE_PORT_OPTION)) - logger.info("ssl storage port is deprecated and not used, all communication goes though storage port " + - "which is able to handle encrypted communication too."); + System.out.println("ssl storage port is deprecated and not used, all communication goes through storage port " + + "which is able to handle encrypted communication too."); // Copy the encryption options and apply the config so that argument parsing can accesss isEnabled. clientEncOptions = config.client_encryption_options.applyConfig(); @@ -715,11 +716,12 @@ private void constructAuthProvider() { try { - Class authProviderClass = Class.forName(authProviderName); - Constructor constructor = authProviderClass.getConstructor(String.class, String.class); - authProvider = (AuthProvider)constructor.newInstance(user, passwd); + Class authProviderClass = + FBUtilities.classForNameWithoutInitialization(authProviderName, "auth provider", AuthProvider.class); + Constructor constructor = authProviderClass.getConstructor(String.class, String.class); + authProvider = constructor.newInstance(user, passwd); } - catch (ClassNotFoundException e) + catch (ConfigurationException e) { errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions()); } @@ -746,9 +748,9 @@ else if (authProviderName != null) { try { - authProvider = (AuthProvider)Class.forName(authProviderName).newInstance(); + authProvider = FBUtilities.construct(authProviderName, "auth provider", AuthProvider.class); } - catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) + catch (ConfigurationException e) { errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions()); } diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index b72098523a2a..6a7cf53dc30d 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -105,6 +105,8 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.MessagingServiceMBean; import org.apache.cassandra.service.ActiveRepairServiceMBean; +import org.apache.cassandra.service.AutoRepairService; +import org.apache.cassandra.service.AutoRepairServiceMBean; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.CacheServiceMBean; import org.apache.cassandra.service.GCInspector; @@ -126,7 +128,7 @@ import com.google.common.collect.Sets; import com.google.common.util.concurrent.Uninterruptibles; import org.apache.cassandra.tools.nodetool.GetTimeout; -import org.apache.cassandra.utils.NativeLibrary; +import org.apache.cassandra.utils.INativeLibrary; import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS; import static org.apache.cassandra.config.CassandraRelevantProperties.SSL_ENABLE; @@ -173,6 +175,7 @@ public class NodeProbe implements AutoCloseable protected PermissionsCacheMBean pcProxy; protected RolesCacheMBean rcProxy; protected GuardrailsMBean grProxy; + protected AutoRepairServiceMBean autoRepairProxy; protected Output output; private boolean failed; @@ -314,6 +317,8 @@ protected void connect() throws IOException name = new ObjectName(Guardrails.MBEAN_NAME); grProxy = JMX.newMBeanProxy(mbeanServerConn, name, GuardrailsMBean.class); + name = new ObjectName(AutoRepairService.MBEAN_NAME); + autoRepairProxy = JMX.newMBeanProxy(mbeanServerConn, name, AutoRepairServiceMBean.class); } catch (MalformedObjectNameException e) { @@ -368,9 +373,9 @@ public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkDa return ssProxy.scrub(disableSnapshot, skipCorrupted, checkData, reinsertOverflowedTTL, jobs, keyspaceName, tables); } - public int verify(boolean extendedVerify, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException + public int verify(boolean extendedVerify, boolean validateAllRows, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException { - return ssProxy.verify(extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspaceName, tableNames); + return ssProxy.verify(extendedVerify, validateAllRows, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspaceName, tableNames); } public int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, long maxSSTableTimestamp, int jobs, String... tableNames) throws IOException, ExecutionException, InterruptedException @@ -411,10 +416,10 @@ public void scrub(PrintStream out, boolean disableSnapshot, boolean skipCorrupte "scrubbing"); } - public void verify(PrintStream out, boolean extendedVerify, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException + public void verify(PrintStream out, boolean extendedVerify, boolean validateAllRows, boolean checkVersion, boolean diskFailurePolicy, boolean mutateRepairStatus, boolean checkOwnsTokens, boolean quick, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException { perform(out, keyspaceName, - () -> verify(extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspaceName, tableNames), + () -> verify(extendedVerify, validateAllRows, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspaceName, tableNames), "verifying"); } @@ -473,6 +478,11 @@ public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, St ssProxy.forceKeyspaceCompaction(splitOutput, keyspaceName, tableNames); } + public void forceKeyspaceCompaction(boolean splitOutput, int parallelism, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException + { + ssProxy.forceKeyspaceCompaction(splitOutput, parallelism, keyspaceName, tableNames); + } + public void relocateSSTables(int jobs, String keyspace, String[] cfnames) throws IOException, ExecutionException, InterruptedException { ssProxy.relocateSSTables(jobs, keyspace, cfnames); @@ -1418,6 +1428,11 @@ public long getCompactionThroughputBytes() return ssProxy.getCompactionThroughtputBytesPerSec(); } + public Map getCurrentCompactionThroughputMiBPerSec() + { + return ssProxy.getCurrentCompactionThroughputMebibytesPerSec(); + } + public void setBatchlogReplayThrottle(int value) { ssProxy.setBatchlogReplayThrottleInKB(value); @@ -1930,10 +1945,10 @@ private String getSaiMetricScope(String metricName) case "SSTableIndexesHit": case "IndexSegmentsHit": case "RowsFiltered": - return TableQueryMetrics.PerQueryMetrics.PER_QUERY_METRICS_TYPE; + return TableQueryMetrics.PerQuery.METRIC_TYPE; case "PostFilteringReadLatency": case "TotalQueryTimeouts": - return TableQueryMetrics.TABLE_QUERY_METRIC_TYPE; + return TableQueryMetrics.PerTable.METRIC_TYPE; case "DiskUsedBytes": return IndexGroupMetrics.INDEX_GROUP_METRICS_TYPE; case "TotalIndexCount": @@ -2085,6 +2100,7 @@ public Object getCompactionMetric(String metricName) switch(metricName) { case "BytesCompacted": + case "CompressedBytesCompacted": case "CompactionsAborted": case "CompactionsReduced": case "SSTablesDroppedFromCompaction": @@ -2094,6 +2110,9 @@ public Object getCompactionMetric(String metricName) case "CompletedTasks": case "PendingTasks": case "PendingTasksByTableName": + case "WriteAmplificationByTableName": + case "AggregateCompactions": + case "MaxOverlapsMap": return JMX.newMBeanProxy(mbeanServerConn, new ObjectName("org.apache.cassandra.metrics:type=Compaction,name=" + metricName), CassandraMetricsRegistry.JmxGaugeMBean.class).getValue(); @@ -2258,7 +2277,7 @@ public Map getLoggingLevels() public long getPid() { - return NativeLibrary.getProcessID(); + return INativeLibrary.instance.getProcessID(); } public void resumeBootstrap(PrintStream out) throws IOException @@ -2443,6 +2462,146 @@ public GuardrailsMBean getGuardrailsMBean() { return grProxy; } + + public boolean isAutoRepairDisabled() + { + return autoRepairProxy.isAutoRepairDisabled(); + } + + public String autoRepairConfiguration() + { + return autoRepairProxy.getAutoRepairConfiguration(); + } + + public void setAutoRepairTokenRangeSplitterParameter(String repairType, String key, String value) + { + autoRepairProxy.setAutoRepairTokenRangeSplitterParameter(repairType, key, value); + } + + public void setAutoRepairEnabled(String repairType, boolean enabled) + { + autoRepairProxy.setAutoRepairEnabled(repairType, enabled); + } + + public void setAutoRepairThreads(String repairType, int repairThreads) + { + autoRepairProxy.setRepairThreads(repairType, repairThreads); + } + + public void setAutoRepairPriorityForHosts(String repairType, String commaSeparatedHostSet) + { + autoRepairProxy.setRepairPriorityForHosts(repairType, commaSeparatedHostSet); + } + + public void setAutoRepairForceRepairForHosts(String repairType, String commaSeparatedHostSet) + { + autoRepairProxy.setForceRepairForHosts(repairType, commaSeparatedHostSet); + } + + public void setAutoRepairMinInterval(String repairType, String minRepairInterval) + { + autoRepairProxy.setRepairMinInterval(repairType, minRepairInterval); + } + + public void setAutoRepairHistoryClearDeleteHostsBufferDuration(String duration) + { + autoRepairProxy.setAutoRepairHistoryClearDeleteHostsBufferDuration(duration); + } + + public void startAutoRepairScheduler() + { + autoRepairProxy.startScheduler(); + } + + public void setAutoRepairMinRepairTaskDuration(String duration) + { + autoRepairProxy.setAutoRepairMinRepairTaskDuration(duration); + } + + public void setAutoRepairSSTableCountHigherThreshold(String repairType, int ssTableHigherThreshold) + { + autoRepairProxy.setRepairSSTableCountHigherThreshold(repairType, ssTableHigherThreshold); + } + + public void setAutoRepairTableMaxRepairTime(String repairType, String autoRepairTableMaxRepairTime) + { + autoRepairProxy.setAutoRepairTableMaxRepairTime(repairType, autoRepairTableMaxRepairTime); + } + + public void setAutoRepairIgnoreDCs(String repairType, Set ignoreDCs) + { + autoRepairProxy.setIgnoreDCs(repairType, ignoreDCs); + } + + public void setAutoRepairParallelRepairPercentage(String repairType, int percentage) + { + autoRepairProxy.setParallelRepairPercentage(repairType, percentage); + } + + public void setAutoRepairParallelRepairCount(String repairType, int count) + { + autoRepairProxy.setParallelRepairCount(repairType, count); + } + + public void setAutoRepairAllowParallelReplicaRepair(String repairType, boolean enabled) + { + autoRepairProxy.setAllowParallelReplicaRepair(repairType, enabled); + } + + public void setAutoRepairAllowParallelReplicaRepairAcrossSchedules(String repairType, boolean enabled) + { + autoRepairProxy.setAllowParallelReplicaRepairAcrossSchedules(repairType, enabled); + } + + public void setAutoRepairPrimaryTokenRangeOnly(String repairType, boolean primaryTokenRangeOnly) + { + autoRepairProxy.setPrimaryTokenRangeOnly(repairType, primaryTokenRangeOnly); + } + + public void setAutoRepairMaterializedViewRepairEnabled(String repairType, boolean enabled) + { + autoRepairProxy.setMVRepairEnabled(repairType, enabled); + } + + public List mutateSSTableRepairedState(boolean repair, boolean preview, String keyspace, List tables) + { + return ssProxy.mutateSSTableRepairedState(repair, preview, keyspace, tables); + } + + public List getAutoRepairTablesForKeyspace(String keyspace) + { + return ssProxy.getTablesForKeyspace(keyspace); + } + + public void setAutoRepairSessionTimeout(String repairType, String timeout) + { + autoRepairProxy.setRepairSessionTimeout(repairType, timeout); + } + + public Set getAutoRepairOnGoingRepairHostIds(String repairType) + { + return autoRepairProxy.getOnGoingRepairHostIds(repairType); + } + + public void setAutoRepairRepairByKeyspace(String repairType, boolean enabled) + { + autoRepairProxy.setRepairByKeyspace(repairType, enabled); + } + + public void setAutoRepairMaxRetriesCount(String repairType, int retries) + { + autoRepairProxy.setAutoRepairMaxRetriesCount(repairType, retries); + } + + public void setAutoRepairRetryBackoff(String repairType, String interval) + { + autoRepairProxy.setAutoRepairRetryBackoff(repairType, interval); + } + + public void setMixedMajorVersionRepairEnabled(boolean enabled) + { + autoRepairProxy.setMixedMajorVersionRepairEnabled(enabled); + } } class ColumnFamilyStoreMBeanIterator implements Iterator> diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index aa66719474a2..65ecb5944a09 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -96,6 +96,7 @@ public NodeTool(INodeProbeFactory nodeProbeFactory, Output output) public int execute(String... args) { List> commands = newArrayList( + AutoRepairStatus.class, Assassinate.class, CassHelp.class, CIDRFilteringStats.class, @@ -106,6 +107,7 @@ public int execute(String... args) Compact.class, CompactionHistory.class, CompactionStats.class, + CreateSystemKey.class, DataPaths.class, Decommission.class, DescribeCluster.class, @@ -136,6 +138,7 @@ public int execute(String... args) GcStats.class, GetAuditLog.class, GetAuthCacheConfig.class, + GetAutoRepairConfig.class, GetBatchlogReplayTrottle.class, GetCIDRGroupsOfIP.class, GetColumnIndexSize.class, @@ -199,6 +202,7 @@ public int execute(String... args) Ring.class, Scrub.class, SetAuthCacheConfig.class, + SetAutoRepairConfig.class, SetBatchlogReplayThrottle.class, SetCacheCapacity.class, SetCacheKeysToSave.class, @@ -220,6 +224,7 @@ public int execute(String... args) SetTraceProbability.class, Sjk.class, Snapshot.class, + SSTableRepairedSet.class, Status.class, StatusAutoCompaction.class, StatusBackup.class, diff --git a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java index 256c80d26903..276c8d67fb41 100644 --- a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java +++ b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java @@ -322,7 +322,7 @@ private void printSStableMetadata(File file, boolean scan) throws IOException CompactionMetadata compaction = statsComponent.compactionMetadata(); SerializationHeader.Component header = statsComponent.serializationHeader(); Class compressorClass = null; - try (CompressionMetadata compression = CompressionInfoComponent.loadIfExists(descriptor)) + try (CompressionMetadata compression = CompressionInfoComponent.loadIfExists(descriptor, stats.zeroCopyMetadata)) { compressorClass = compression != null ? compression.compressor().getClass() : null; } diff --git a/src/java/org/apache/cassandra/tools/StandaloneScrubber.java b/src/java/org/apache/cassandra/tools/StandaloneScrubber.java index 36fda102e976..b82aed6c158f 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneScrubber.java +++ b/src/java/org/apache/cassandra/tools/StandaloneScrubber.java @@ -37,6 +37,7 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.compaction.AbstractStrategyHolder; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.CompactionSSTable; import org.apache.cassandra.db.compaction.CompactionStrategyManager; import org.apache.cassandra.db.compaction.LeveledCompactionStrategy; import org.apache.cassandra.db.compaction.LeveledManifest; @@ -190,7 +191,7 @@ public static void main(String args[]) } // Check (and repair) manifests - checkManifest(cfs.getCompactionStrategyManager(), cfs, sstables); + checkManifest(cfs, sstables); CompactionManager.instance.finishCompactionsAndShutdown(5, TimeUnit.MINUTES); LifecycleTransaction.waitForDeletions(); System.exit(0); // We need that to stop non daemonized threads @@ -204,17 +205,18 @@ public static void main(String args[]) } } - private static void checkManifest(CompactionStrategyManager strategyManager, ColumnFamilyStore cfs, Collection sstables) + private static void checkManifest(ColumnFamilyStore cfs, Collection sstables) { - if (strategyManager.getCompactionParams().klass().equals(LeveledCompactionStrategy.class)) + if (cfs.getCompactionParams().klass().equals(LeveledCompactionStrategy.class)) { - int maxSizeInMiB = (int)((cfs.getCompactionStrategyManager().getMaxSSTableBytes()) / (1024L * 1024L)); - int fanOut = cfs.getCompactionStrategyManager().getLevelFanoutSize(); - for (AbstractStrategyHolder.GroupedSSTableContainer sstableGroup : strategyManager.groupSSTables(sstables)) + int maxSizeInMiB = (int)((cfs.getCompactionStrategy().getMaxSSTableBytes()) / (1024L * 1024L)); + int fanOut = cfs.getCompactionStrategy().getLevelFanoutSize(); + CompactionStrategyManager csm = (CompactionStrategyManager) cfs.getCompactionStrategyContainer(); + for (AbstractStrategyHolder.GroupedSSTableContainer sstableGroup : csm.groupSSTables(sstables)) { for (int i = 0; i < sstableGroup.numGroups(); i++) { - List groupSSTables = new ArrayList<>(sstableGroup.getGroup(i)); + List groupSSTables = new ArrayList<>(sstableGroup.getGroup(i)); // creating the manifest makes sure the leveling is sane: LeveledManifest.create(cfs, maxSizeInMiB, fanOut, groupSSTables); } diff --git a/src/java/org/apache/cassandra/tools/StandaloneVerifier.java b/src/java/org/apache/cassandra/tools/StandaloneVerifier.java index 547a1e05f2b2..e41971d3fc59 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneVerifier.java +++ b/src/java/org/apache/cassandra/tools/StandaloneVerifier.java @@ -59,6 +59,7 @@ public class StandaloneVerifier private static final String TOOL_NAME = "sstableverify"; private static final String VERBOSE_OPTION = "verbose"; private static final String EXTENDED_OPTION = "extended"; + private static final String VALIDATE_ALL_ROWS = "validate_all_rows"; private static final String DEBUG_OPTION = "debug"; private static final String HELP_OPTION = "help"; private static final String CHECK_VERSION = "check_version"; @@ -125,6 +126,7 @@ public static void main(String args[]) } IVerifier.Options verifyOptions = IVerifier.options().invokeDiskFailurePolicy(false) .extendedVerification(options.extended) + .validateAllRows(options.validateAllRows) .checkVersion(options.checkVersion) .mutateRepairStatus(options.mutateRepairStatus) .checkOwnsTokens(!options.tokens.isEmpty()) @@ -182,6 +184,7 @@ private static class Options public boolean debug; public boolean verbose; public boolean extended; + public boolean validateAllRows; public boolean checkVersion; public boolean mutateRepairStatus; public boolean quick; @@ -225,6 +228,7 @@ public static Options parseArgs(String cmdArgs[]) opts.debug = cmd.hasOption(DEBUG_OPTION); opts.verbose = cmd.hasOption(VERBOSE_OPTION); opts.extended = cmd.hasOption(EXTENDED_OPTION); + opts.validateAllRows = cmd.hasOption(VALIDATE_ALL_ROWS); opts.checkVersion = cmd.hasOption(CHECK_VERSION); opts.mutateRepairStatus = cmd.hasOption(MUTATE_REPAIR_STATUS); opts.quick = cmd.hasOption(QUICK); diff --git a/src/java/org/apache/cassandra/tools/Util.java b/src/java/org/apache/cassandra/tools/Util.java index d8ef121f89fa..78df1d2afa6f 100644 --- a/src/java/org/apache/cassandra/tools/Util.java +++ b/src/java/org/apache/cassandra/tools/Util.java @@ -37,6 +37,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.exceptions.ConfigurationException; @@ -303,13 +304,26 @@ public static Stream iterToStream(Iterator iter) } /** - * Construct table schema from info stored in SSTable's Stats.db + * Construct table schema from info stored in SSTable's Stats.db. + * Hardcodes the keyspace and table name to default values to preserve the existing behavior. * * @param desc SSTable's descriptor * @return Restored CFMetaData * @throws IOException when Stats.db cannot be read */ public static TableMetadata metadataFromSSTable(Descriptor desc) throws IOException + { + return metadataFromSSTable(desc, "keyspace", "table"); + } + + /** + * Construct table schema from info stored in SSTable's Stats.db, using the specified keyspace and table names. + * + * @param desc SSTable's descriptor + * @return Restored CFMetaData + * @throws IOException when Stats.db cannot be read + */ + public static TableMetadata metadataFromSSTable(Descriptor desc, String keyspaceName, String tableName) throws IOException { if (!desc.version.isCompatible()) throw new IOException("Unsupported SSTable version " + desc.getFormat().name() + "/" + desc.version); @@ -319,17 +333,17 @@ public static TableMetadata metadataFromSSTable(Descriptor desc) throws IOExcept IPartitioner partitioner = FBUtilities.newPartitioner(desc); - TableMetadata.Builder builder = TableMetadata.builder("keyspace", "table").partitioner(partitioner); + TableMetadata.Builder builder = TableMetadata.builder(keyspaceName, tableName).partitioner(partitioner); header.getStaticColumns().entrySet().stream() - .forEach(entry -> { - ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true); - builder.addStaticColumn(ident, entry.getValue()); - }); + .forEach(entry -> { + ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true); + builder.addStaticColumn(ident, entry.getValue()); + }); header.getRegularColumns().entrySet().stream() - .forEach(entry -> { - ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true); - builder.addRegularColumn(ident, entry.getValue()); - }); + .forEach(entry -> { + ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true); + builder.addRegularColumn(ident, entry.getValue()); + }); builder.addPartitionKeyColumn("PartitionKey", header.getKeyType()); for (int i = 0; i < header.getClusteringTypes().size(); i++) { @@ -344,6 +358,8 @@ public static TableMetadata metadataFromSSTable(Descriptor desc) throws IOExcept builder.indexes(indexes); builder.kind(TableMetadata.Kind.INDEX); } + boolean isCounter = header.getRegularColumns().values().stream().anyMatch(AbstractType::isCounter) || header.getStaticColumns().values().stream().anyMatch(AbstractType::isCounter); + builder.isCounter(isCounter); return builder.build(); } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tools/nodetool/AutoRepairStatus.java b/src/java/org/apache/cassandra/tools/nodetool/AutoRepairStatus.java new file mode 100644 index 000000000000..bb594a010ff1 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/AutoRepairStatus.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool; + +import java.io.PrintStream; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; + +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; +import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Provides currently running auto-repair tasks. + */ +@Command(name = "autorepairstatus", description = "Print autorepair status") +public class AutoRepairStatus extends NodeTool.NodeToolCmd +{ + @VisibleForTesting + @Option(title = "repair type", name = { "-t", "--repair-type" }, description = "Repair type") + protected String repairType; + + @Override + public void execute(NodeProbe probe) + { + checkArgument(repairType != null, "--repair-type is required."); + PrintStream out = probe.output().out; + + if (probe.isAutoRepairDisabled()) + { + out.println("Auto-repair is not enabled"); + return; + } + + TableBuilder table = new TableBuilder(); + table.add("Active Repairs"); + Set ongoingRepairHostIds = probe.getAutoRepairOnGoingRepairHostIds(repairType); + table.add(getSetString(ongoingRepairHostIds)); + table.printTo(out); + } + + private String getSetString(Set hostIds) + { + if (hostIds.isEmpty()) + { + return "NONE"; + } + StringBuilder sb = new StringBuilder(); + for (String id : hostIds) + { + sb.append(id); + sb.append(","); + } + // remove last "," + sb.setLength(Math.max(sb.length() - 1, 0)); + return sb.toString(); + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/Compact.java b/src/java/org/apache/cassandra/tools/nodetool/Compact.java index f5a83ed90475..1f8b4c4b46e6 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Compact.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Compact.java @@ -50,6 +50,12 @@ public class Compact extends NodeToolCmd @Option(title = "partition_key", name = {"--partition"}, description = "String representation of the partition key") private String partitionKey = EMPTY; + @Option(title = "jobs", + name = {"-j", "--jobs"}, + description = "Use -j to specify the maximum number of threads to use for parallel compaction. " + + "If not set, up to half the compaction threads will be used. " + + "If set to 0, the major compaction will use all threads and will not permit other compactions to run until it completes (use with caution).") + private Integer parallelism = null; @Override public void execute(NodeProbe probe) @@ -95,7 +101,10 @@ else if (partitionKeyProvided) } else { - probe.forceKeyspaceCompaction(splitOutput, keyspace, tableNames); + if (parallelism != null) + probe.forceKeyspaceCompaction(splitOutput, parallelism, keyspace, tableNames); + else // avoid referring to the new method to work with older versions + probe.forceKeyspaceCompaction(splitOutput, keyspace, tableNames); } } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java b/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java index c80de91d97d1..b634078ebeab 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java +++ b/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java @@ -20,15 +20,22 @@ import java.io.PrintStream; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; +import com.codahale.metrics.Counter; +import com.codahale.metrics.Meter; +import io.airlift.airline.Arguments; import io.airlift.airline.Command; import io.airlift.airline.Option; -import org.apache.cassandra.db.compaction.CompactionInfo; -import org.apache.cassandra.db.compaction.CompactionInfo.Unit; +import org.apache.cassandra.db.compaction.CompactionStrategyStatistics; +import org.apache.cassandra.db.compaction.TableOperation; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.metrics.CassandraMetricsRegistry; import org.apache.cassandra.tools.NodeProbe; @@ -40,6 +47,8 @@ @Command(name = "compactionstats", description = "Print statistics on compactions") public class CompactionStats extends NodeToolCmd { + private static final String TOTAL_COMPRESSED = "totalCompressed"; + @Option(title = "human_readable", name = {"-H", "--human-readable"}, description = "Display bytes in human readable form, i.e. KiB, MiB, GiB, TiB") @@ -50,6 +59,22 @@ public class CompactionStats extends NodeToolCmd description = "Display fields matching vtable output") private boolean vtableOutput = false; + @Option(title = "aggregate", + name = {"-A", "--aggregate"}, + description = "Show the compaction aggregates for the compactions in progress, e.g. the levels for LCS or the buckets for STCS and TWCS.") + private boolean aggregate = false; + + @Option(title = "overlap", + name = {"-O", "--overlap"}, + description = "Show a map of the maximum sstable overlap per compaction region.\n" + + "Note: This map includes all sstables in the system, including ones that are currently being compacted, " + + "and also takes into account early opened sstables. Overlaps per level may be greater than the values " + + "the --aggregate option reports.") + private boolean overlap = false; + + @Arguments(usage = "[ ...]", description = "With --aggregate or --overlap, optionally list only the data for the specified keyspace and tables.") + private List args = new ArrayList<>(); + @Override public void execute(NodeProbe probe) { @@ -58,56 +83,152 @@ public void execute(NodeProbe probe) pendingTasksAndConcurrentCompactorsStats(probe, tableBuilder); compactionsStats(probe, tableBuilder); reportCompactionTable(probe.getCompactionManagerProxy().getCompactions(), probe.getCompactionThroughputBytes(), humanReadable, vtableOutput, out, tableBuilder); + + Set keyspaces = new HashSet<>(parseOptionalKeyspace(args, probe)); + Set tableNames = new HashSet<>(Arrays.asList(parseOptionalTables(args))); + + if (aggregate) + { + reportAggregateCompactions(probe, keyspaces, tableNames, out); + } + + if (overlap) + reportOverlap((Map>>) probe.getCompactionMetric("MaxOverlapsMap"), keyspaces, tableNames, out); } private void pendingTasksAndConcurrentCompactorsStats(NodeProbe probe, TableBuilder tableBuilder) { Map> pendingTaskNumberByTable = - (Map>) probe.getCompactionMetric("PendingTasksByTableName"); + (Map>) probe.getCompactionMetric("PendingTasksByTableName"); + Map> writeAmplificationByTableName = + (Map>) probe.getCompactionMetric("WriteAmplificationByTableName"); tableBuilder.add("concurrent compactors", Integer.toString(probe.getConcurrentCompactors())); - tableBuilder.add("pending tasks", Integer.toString(numPendingTasks(pendingTaskNumberByTable))); - - for (Entry> ksEntry : pendingTaskNumberByTable.entrySet()) - for (Entry tableEntry : ksEntry.getValue().entrySet()) - tableBuilder.add(ksEntry.getKey(), tableEntry.getKey(), tableEntry.getValue().toString()); - } - - private int numPendingTasks(Map> pendingTaskNumberByTable) - { int numTotalPendingTasks = 0; + double totWriteAmplification = 0; for (Entry> ksEntry : pendingTaskNumberByTable.entrySet()) + { + Map ksWriteAmplification = writeAmplificationByTableName.get(ksEntry.getKey()); for (Entry tableEntry : ksEntry.getValue().entrySet()) + { numTotalPendingTasks += tableEntry.getValue(); + if (ksWriteAmplification != null) + totWriteAmplification += ksWriteAmplification.get(tableEntry.getKey()); + } + } + tableBuilder.add("pending tasks", Integer.toString(numTotalPendingTasks)); + tableBuilder.add("write amplification", String.format("%.2f", totWriteAmplification)); - return numTotalPendingTasks; + for (Entry> ksEntry : pendingTaskNumberByTable.entrySet()) + { + Map ksWriteAmplification = writeAmplificationByTableName.get(ksEntry.getKey()); + for (Entry tableEntry : ksEntry.getValue().entrySet()) + { + double wa = ksWriteAmplification == null ? 0 : ksWriteAmplification.get(tableEntry.getKey()); + tableBuilder.add(ksEntry.getKey(), tableEntry.getKey(), tableEntry.getValue().toString()); + tableBuilder.add(ksEntry.getKey(), String.format("%s write amplification", tableEntry.getKey()), String.format("%.2f", wa)); + } + } } private void compactionsStats(NodeProbe probe, TableBuilder tableBuilder) { - CassandraMetricsRegistry.JmxMeterMBean totalCompactionsCompletedMetrics = - (CassandraMetricsRegistry.JmxMeterMBean) probe.getCompactionMetric("TotalCompactionsCompleted"); - tableBuilder.add("compactions completed", String.valueOf(totalCompactionsCompletedMetrics.getCount())); + // FIXME this is a hack to get the compaction metrics for NodeToolTest using InternalNodeProbe without JMX + Object totalCompactionsCompleted = probe.getCompactionMetric("TotalCompactionsCompleted"); + double totalCompactionsCompletedFifteenMinuteRate; + double totalCompactionsCompletedMeanRate; + if (totalCompactionsCompleted instanceof Meter) + { + Meter totalCompactionsCompletedMeter = (Meter) totalCompactionsCompleted; + tableBuilder.add("compactions completed", String.valueOf(totalCompactionsCompletedMeter.getCount())); + totalCompactionsCompletedFifteenMinuteRate = totalCompactionsCompletedMeter.getFifteenMinuteRate(); + totalCompactionsCompletedMeanRate = totalCompactionsCompletedMeter.getMeanRate(); + } + else + { + CassandraMetricsRegistry.JmxMeterMBean totalCompactionsCompletedJmxMeterMBean = (CassandraMetricsRegistry.JmxMeterMBean) totalCompactionsCompleted; + tableBuilder.add("compactions completed", String.valueOf(totalCompactionsCompletedJmxMeterMBean.getCount())); + totalCompactionsCompletedFifteenMinuteRate = totalCompactionsCompletedJmxMeterMBean.getFifteenMinuteRate(); + totalCompactionsCompletedMeanRate = totalCompactionsCompletedJmxMeterMBean.getMeanRate(); + } + + Object bytesCompacted = probe.getCompactionMetric("BytesCompacted"); + long bytesCompactedCount; + if (bytesCompacted instanceof Counter) + { + Counter bytesCompactedCounter = (Counter) bytesCompacted; + bytesCompactedCount = bytesCompactedCounter.getCount(); + } + else + { + CassandraMetricsRegistry.JmxCounterMBean bytesCompactedJmxCounterMBean = (CassandraMetricsRegistry.JmxCounterMBean) bytesCompacted; + bytesCompactedCount = bytesCompactedJmxCounterMBean.getCount(); + } + + Object compressedBytesCompacted = probe.getCompactionMetric("CompressedBytesCompacted"); + long compressedBytesCompactedCount; + if (compressedBytesCompacted instanceof Counter) + { + Counter compressedBytesCompactedCounter = (Counter) compressedBytesCompacted; + compressedBytesCompactedCount = compressedBytesCompactedCounter.getCount(); + } + else + { + CassandraMetricsRegistry.JmxCounterMBean compressedBytesCompactedJmxCounterMBean = (CassandraMetricsRegistry.JmxCounterMBean) compressedBytesCompacted; + compressedBytesCompactedCount = compressedBytesCompactedJmxCounterMBean.getCount(); + } - CassandraMetricsRegistry.JmxCounterMBean bytesCompacted = (CassandraMetricsRegistry.JmxCounterMBean) probe.getCompactionMetric("BytesCompacted"); if (humanReadable) - tableBuilder.add("data compacted", FileUtils.stringifyFileSize(Double.parseDouble(Long.toString(bytesCompacted.getCount())))); + { + tableBuilder.add("data compacted", FileUtils.stringifyFileSize(Double.parseDouble(Long.toString(bytesCompactedCount)))); + tableBuilder.add("compressed data compacted", FileUtils.stringifyFileSize(Double.parseDouble(Long.toString(compressedBytesCompactedCount)))); + } else - tableBuilder.add("data compacted", Long.toString(bytesCompacted.getCount())); + { + tableBuilder.add("data compacted", Long.toString(bytesCompactedCount)); + tableBuilder.add("compressed data compacted", Long.toString(compressedBytesCompactedCount)); + } - CassandraMetricsRegistry.JmxCounterMBean compactionsAborted = (CassandraMetricsRegistry.JmxCounterMBean) probe.getCompactionMetric("CompactionsAborted"); - tableBuilder.add("compactions aborted", Long.toString(compactionsAborted.getCount())); + Object compactionsAborted = probe.getCompactionMetric("CompactionsAborted"); + if (compactionsAborted instanceof Counter) + { + Counter compactionsAbortedCounter = (Counter) compactionsAborted; + tableBuilder.add("compactions aborted", Long.toString(compactionsAbortedCounter.getCount())); + } + else + { + CassandraMetricsRegistry.JmxCounterMBean compactionsAbortedJmxCounterMBean = (CassandraMetricsRegistry.JmxCounterMBean) compactionsAborted; + tableBuilder.add("compactions aborted", Long.toString(compactionsAbortedJmxCounterMBean.getCount())); + } - CassandraMetricsRegistry.JmxCounterMBean compactionsReduced = (CassandraMetricsRegistry.JmxCounterMBean) probe.getCompactionMetric("CompactionsReduced"); - tableBuilder.add("compactions reduced", Long.toString(compactionsReduced.getCount())); + Object compactionsReduced = probe.getCompactionMetric("CompactionsReduced"); + if (compactionsReduced instanceof Counter) + { + Counter compactionsReducedCounter = (Counter) compactionsReduced; + tableBuilder.add("compactions reduced", Long.toString(compactionsReducedCounter.getCount())); + } + else + { + CassandraMetricsRegistry.JmxCounterMBean compactionsReducedJmxCounterMBean = (CassandraMetricsRegistry.JmxCounterMBean) compactionsReduced; + tableBuilder.add("compactions reduced", Long.toString(compactionsReducedJmxCounterMBean.getCount())); + } - CassandraMetricsRegistry.JmxCounterMBean sstablesDroppedFromCompaction = (CassandraMetricsRegistry.JmxCounterMBean) probe.getCompactionMetric("SSTablesDroppedFromCompaction"); - tableBuilder.add("sstables dropped from compaction", Long.toString(sstablesDroppedFromCompaction.getCount())); + Object sstablesDroppedFromCompaction = probe.getCompactionMetric("SSTablesDroppedFromCompaction"); + if (sstablesDroppedFromCompaction instanceof Counter) + { + Counter sstablesDroppedFromCompactionCounter = (Counter) sstablesDroppedFromCompaction; + tableBuilder.add("sstables dropped from compaction", Long.toString(sstablesDroppedFromCompactionCounter.getCount())); + } + else + { + CassandraMetricsRegistry.JmxCounterMBean sstablesDroppedFromCompactionJmxCounterMBean = (CassandraMetricsRegistry.JmxCounterMBean) sstablesDroppedFromCompaction; + tableBuilder.add("sstables dropped from compaction", Long.toString(sstablesDroppedFromCompactionJmxCounterMBean.getCount())); + } NumberFormat formatter = new DecimalFormat("0.00"); - tableBuilder.add("15 minute rate", String.format("%s/minute", formatter.format(totalCompactionsCompletedMetrics.getFifteenMinuteRate() * 60))); - tableBuilder.add("mean rate", String.format("%s/hour", formatter.format(totalCompactionsCompletedMetrics.getMeanRate() * 60 * 60))); + tableBuilder.add("15 minute rate", String.format("%s/minute", formatter.format(totalCompactionsCompletedFifteenMinuteRate * 60))); + tableBuilder.add("mean rate", String.format("%s/hour", formatter.format(totalCompactionsCompletedMeanRate * 60 * 60))); double configured = probe.getStorageService().getCompactionThroughtputMibPerSecAsDouble(); tableBuilder.add("compaction throughput (MiB/s)", configured == 0 ? "throttling disabled (0)" : Double.toString(configured)); @@ -129,28 +250,39 @@ public static void reportCompactionTable(List> compactions, l long remainingBytes = 0; if (vtableOutput) - table.add("keyspace", "table", "task id", "completion ratio", "kind", "progress", "sstables", "total", "unit", "target directory"); + table.add("keyspace", "table", "task id", "completion ratio", "kind", "progress", "sstables", "total", "total compressed", "unit", "target directory"); else table.add("id", "compaction type", "keyspace", "table", "completed", "total", "unit", "progress"); for (Map c : compactions) { - long total = Long.parseLong(c.get(CompactionInfo.TOTAL)); - long completed = Long.parseLong(c.get(CompactionInfo.COMPLETED)); - String taskType = c.get(CompactionInfo.TASK_TYPE); - String keyspace = c.get(CompactionInfo.KEYSPACE); - String columnFamily = c.get(CompactionInfo.COLUMNFAMILY); - String unit = c.get(CompactionInfo.UNIT); - boolean toFileSize = humanReadable && Unit.isFileSize(unit); - String[] tables = c.get(CompactionInfo.SSTABLES).split(","); + long total = Long.parseLong(c.get(TableOperation.Progress.TOTAL)); + String totalCompressedValue = c.get(TOTAL_COMPRESSED); + long completed = Long.parseLong(c.get(TableOperation.Progress.COMPLETED)); + String taskType = c.get(TableOperation.Progress.OPERATION_TYPE); + String keyspace = c.get(TableOperation.Progress.KEYSPACE); + String columnFamily = c.get(TableOperation.Progress.COLUMNFAMILY); + String unit = c.get(TableOperation.Progress.UNIT); + boolean toFileSize = humanReadable && TableOperation.Unit.isFileSize(unit); + String[] tables = c.get(TableOperation.Progress.SSTABLES).split(","); String progressStr = toFileSize ? FileUtils.stringifyFileSize(completed) : Long.toString(completed); String totalStr = toFileSize ? FileUtils.stringifyFileSize(total) : Long.toString(total); + String totalCompressedStr; + if (totalCompressedValue != null) + { + long totalCompressed = Long.parseLong(totalCompressedValue); + totalCompressedStr = toFileSize ? FileUtils.stringifyFileSize(totalCompressed) : Long.toString(totalCompressed); + } + else + { + totalCompressedStr = "n/a"; + } String percentComplete = total == 0 ? "n/a" : new DecimalFormat("0.00").format((double) completed / total * 100) + '%'; - String id = c.get(CompactionInfo.COMPACTION_ID); + String id = c.get(TableOperation.Progress.OPERATION_ID); if (vtableOutput) { - String targetDirectory = c.get(CompactionInfo.TARGET_DIRECTORY); - table.add(keyspace, columnFamily, id, percentComplete, taskType, progressStr, String.valueOf(tables.length), totalStr, unit, targetDirectory); + String targetDirectory = c.get(TableOperation.Progress.TARGET_DIRECTORY); + table.add(keyspace, columnFamily, id, percentComplete, taskType, progressStr, String.valueOf(tables.length), totalStr, totalCompressedStr, unit, targetDirectory); } else table.add(id, taskType, keyspace, columnFamily, progressStr, totalStr, unit, percentComplete); @@ -169,4 +301,47 @@ public static void reportCompactionTable(List> compactions, l table.printTo(out); } -} \ No newline at end of file + private static void reportAggregateCompactions(NodeProbe probe, Set keyspaces, Set tableNames, PrintStream out) + { + List statistics = (List) probe.getCompactionMetric("AggregateCompactions"); + if (statistics.isEmpty()) + return; + + out.println("Aggregated view:"); + for (CompactionStrategyStatistics stat : statistics) + { + if (!keyspaces.isEmpty() && !keyspaces.contains(stat.keyspace())) + continue; + if (!tableNames.isEmpty() && !tableNames.contains(stat.table())) + continue; + out.println(stat.toString()); + } + } + + private static void reportOverlap(Map>> maxOverlap, Set keyspaces, Set tableNames, PrintStream out) + { + if (maxOverlap == null) + { + out.println("Overlap map is not available."); + return; + } + + for (Map.Entry>> ksEntry : maxOverlap.entrySet()) + { + String ksName = ksEntry.getKey(); + if (!keyspaces.isEmpty() && !keyspaces.contains(ksName)) + continue; + for (Map.Entry> tableEntry : ksEntry.getValue().entrySet()) + { + String tableName = tableEntry.getKey(); + if (!tableNames.isEmpty() && !tableNames.contains(tableName)) + continue; + out.println("Max overlap map for " + ksName + "." + tableName + ":"); + for (Map.Entry compactionEntry : tableEntry.getValue().entrySet()) + { + out.println(" " + compactionEntry.getKey() + ": " + compactionEntry.getValue()); + } + } + } + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/CreateSystemKey.java b/src/java/org/apache/cassandra/tools/nodetool/CreateSystemKey.java new file mode 100644 index 000000000000..9ee5978cacbb --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/CreateSystemKey.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.IOException; +import java.io.PrintStream; +import java.nio.file.Path; +import java.security.InvalidParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import javax.crypto.NoSuchPaddingException; + +import io.airlift.airline.Arguments; +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.crypto.LocalSystemKey; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool.NodeToolCmd; + +@Command(name = "createsystemkey", description = "Creates a system key for sstable encryption") +public class CreateSystemKey extends NodeToolCmd +{ + @Arguments(usage = "[ []", description = "\n" + + "\n" + + "[]\n" + + "Key strength not required for Hmac algorithms. will be appended to the directory defined in system_key_directory.") + private List args = new ArrayList<>(); + + @Option(title = "directory", name = "-d", description = "Output directory") + private String directoryOption = null; + + @Override + public void execute(NodeProbe probe) + { + if (args.size() < 2) + { + throw new RuntimeException("Usage: nodetool createsystemkey []"); + } + + String cipherName = args.get(0); + int keyStrength = cipherName.startsWith("Hmac") ? 0 : Integer.parseInt(args.get(1)); + + Path directory = directoryOption != null ? Path.of(directoryOption) : null; + String keyLocation = null; + PrintStream out = probe.output().out; + PrintStream err = probe.output().err; + + try + { + keyLocation = args.size() > 2 ? args.get(2) : "system_key"; + Path keyPath = LocalSystemKey.createKey(directory, keyLocation, cipherName, keyStrength); + + out.printf("Successfully created key %s%n", keyPath.toString()); + } + catch (NoSuchAlgorithmException e) + { + err.printf("System key (%s %s) was not created at %s%n", cipherName, keyStrength, keyLocation); + err.println(e.getMessage()); + err.println("Available algorithms are: AES, ARCFOUR, Blowfish, DES, DESede, HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 and RC2"); + System.exit(1); + } + catch (InvalidParameterException | NoSuchPaddingException | IOException e) + { + err.printf("System key (%s %s) was not created at %s%n", cipherName, keyStrength, keyLocation); + err.println(e.getMessage()); + System.exit(1); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetAutoRepairConfig.java b/src/java/org/apache/cassandra/tools/nodetool/GetAutoRepairConfig.java new file mode 100644 index 000000000000..9744498de757 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/GetAutoRepairConfig.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.tools.nodetool; + +import java.io.PrintStream; + +import com.google.common.annotations.VisibleForTesting; + +import io.airlift.airline.Command; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool.NodeToolCmd; + +/** + * Prints all the configurations for AutoRepair through nodetool. + */ +@Command(name = "getautorepairconfig", description = "Print autorepair configurations") +public class GetAutoRepairConfig extends NodeToolCmd +{ + @VisibleForTesting + protected static PrintStream out = System.out; + + @Override + public void execute(NodeProbe probe) + { + if (probe.isAutoRepairDisabled()) + out.println("Auto-repair is not enabled"); + else + out.println(probe.autoRepairConfiguration()); + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java b/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java index e71fe0adef3e..cc917c0c3117 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java +++ b/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java @@ -18,6 +18,7 @@ package org.apache.cassandra.tools.nodetool; import com.google.common.math.DoubleMath; +import java.util.Map; import io.airlift.airline.Command; @@ -45,6 +46,11 @@ public void execute(NodeProbe probe) throw new RuntimeException("Use the -d flag to quiet this error and get the exact throughput in MiB/s"); probe.output().out.println("Current compaction throughput: " + probe.getCompactionThroughput() + " MB/s"); + + Map currentCompactionThroughputMetricsMap = probe.getCurrentCompactionThroughputMiBPerSec(); + probe.output().out.println("Current compaction throughput (1 minute): " + currentCompactionThroughputMetricsMap.get("1minute") + " MiB/s"); + probe.output().out.println("Current compaction throughput (5 minute): " + currentCompactionThroughputMetricsMap.get("5minute") + " MiB/s"); + probe.output().out.println("Current compaction throughput (15 minute): " + currentCompactionThroughputMetricsMap.get("15minute") + " MiB/s"); } } } diff --git a/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java b/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java index 4ccfa0d56727..7066a3f544a8 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java +++ b/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -348,13 +349,23 @@ private T getNumber(String value, Function transformer, T default * Special map for methods which do not adhere to camel-case convention precisely. * These will be translated manually. */ - private static final Map toSnakeCaseTranslationMap = Map.of("ZeroTTLOnTWCSEnabled", "zero_ttl_on_twcs_enabled", + private static final Map toSnakeCaseTranslationMap = Map.copyOf(new HashMap<>() + {{putAll( + Map.of("ZeroTTLOnTWCSEnabled", "zero_ttl_on_twcs_enabled", "ZeroTTLOnTWCSWarned", "zero_ttl_on_twcs_warned", "FieldsPerUDTFailThreshold", "fields_per_udt_fail_threshold", "FieldsPerUDTWarnThreshold", "fields_per_udt_warn_threshold", "FieldsPerUDTThreshold", "fields_per_udt_threshold", "SimpleStrategyEnabled", "simplestrategy_enabled", - "NonPartitionRestrictedQueryEnabled", "non_partition_restricted_index_query_enabled"); + "NonPartitionRestrictedQueryEnabled", "non_partition_restricted_index_query_enabled")); + putAll(Map.of( + "SaiAnnRerankKFailThreshold", "sai_ann_rerank_k_fail_threshold", + "SaiAnnRerankKWarnThreshold", "sai_ann_rerank_k_warn_threshold", + "StorageAttachedIndexesPerTableWarnThreshold", "sai_indexes_per_table_warn_threshold", + "StorageAttachedIndexesPerTableFailThreshold", "sai_indexes_per_table_fail_threshold", + "StorageAttachedIndexesTotalWarnThreshold", "sai_indexes_total_warn_threshold", + "StorageAttachedIndexesTotalFailThreshold", "sai_indexes_total_fail_threshold")); + }}); /** * Set of guardrails which are flags, even though their suffix would suggest they are part of "values" which have warned, ignored, and disallowed sub-categories */ @@ -372,7 +383,6 @@ public static GuardrailCategory parseCategory(String category, PrintStream out) { if (category == null) return null; - try { return GuardrailCategory.valueOf(category.toLowerCase()); diff --git a/src/java/org/apache/cassandra/tools/nodetool/Import.java b/src/java/org/apache/cassandra/tools/nodetool/Import.java index f64c7893dd99..1690dc9d0692 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Import.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Import.java @@ -76,7 +76,7 @@ public class Import extends NodeToolCmd private boolean extendedVerify = false; @Option(title = "copy_data", - name = {"-p", "--copy-data"}, + name = {"-cd", "--copy-data"}, description = "Copy data from source directories instead of moving them") private boolean copyData = false; diff --git a/src/java/org/apache/cassandra/tools/nodetool/Repair.java b/src/java/org/apache/cassandra/tools/nodetool/Repair.java index 35832408301c..7e96fccfc365 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Repair.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Repair.java @@ -93,6 +93,9 @@ public class Repair extends NodeToolCmd @Option(title = "trace_repair", name = {"-tr", "--trace"}, description = "Use -tr to trace the repair. Traces are logged to system_traces.events.") private boolean trace = false; + @Option(title = "push_repair", name = {"-ps", "--push"}, description = "Use --push to perform a one way repair where data is only streamed from local node to remote node.") + private boolean pushRepair = false; + @Option(title = "pull_repair", name = {"-pl", "--pull"}, description = "Use --pull to perform a one way repair where data is only streamed from a remote node to this node.") private boolean pullRepair = false; @@ -179,6 +182,7 @@ else if (dcParallel) options.put(RepairOption.JOB_THREADS_KEY, Integer.toString(numJobThreads)); options.put(RepairOption.TRACE_KEY, Boolean.toString(trace)); options.put(RepairOption.COLUMNFAMILIES_KEY, StringUtils.join(cfnames, ",")); + options.put(RepairOption.PUSH_REPAIR_KEY, Boolean.toString(pushRepair)); options.put(RepairOption.PULL_REPAIR_KEY, Boolean.toString(pullRepair)); options.put(RepairOption.FORCE_REPAIR_KEY, Boolean.toString(force)); options.put(RepairOption.PREVIEW, getPreviewKind().toString()); diff --git a/src/java/org/apache/cassandra/tools/nodetool/SSTableRepairedSet.java b/src/java/org/apache/cassandra/tools/nodetool/SSTableRepairedSet.java new file mode 100644 index 000000000000..2a7b56732ac9 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/SSTableRepairedSet.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +import io.airlift.airline.Arguments; +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; + +/** + * Provides a way to set the repaired state of SSTables without any downtime through nodetool. + */ +@Command(name = "sstablerepairedset", description = "Set the repaired state of SSTables for given keyspace/tables") +public class SSTableRepairedSet extends NodeTool.NodeToolCmd +{ + @Arguments(usage = "[ ]", description = "Optional keyspace followed by zero or more tables") + protected List args = new ArrayList<>(); + + @Option(title = "really-set", + name = { "--really-set" }, + description = "Really set the repaired state of SSTables. If not set, only print SSTables that would be affected.") + protected boolean reallySet = false; + + @Option(title = "is-repaired", + name = { "--is-repaired" }, + description = "Set SSTables to repaired state.") + protected boolean isRepaired = false; + + @Option(title = "is-unrepaired", + name = { "--is-unrepaired" }, + description = "Set SSTables to unrepaired state.") + protected boolean isUnrepaired = false; + + @Override + public void execute(NodeProbe probe) + { + PrintStream out = probe.output().out; + + if (isRepaired == isUnrepaired) + { + out.println("Exactly one of --is-repaired or --is-unrepaired must be provided."); + return; + } + + String message; + if (reallySet) + message = "Mutating repaired state of SSTables for"; + else + message = "Previewing repaired state mutation of SSTables for"; + + List keyspaces = parseOptionalKeyspace(args, probe, KeyspaceSet.NON_LOCAL_STRATEGY); + List tables = new ArrayList<>(Arrays.asList(parseOptionalTables(args))); + + if (args.isEmpty()) + message += " all keyspaces"; + else + message += tables.isEmpty() ? " all tables" : " tables " + String.join(", ", tables) + + " in keyspace " + keyspaces.get(0); + message += " to " + (isRepaired ? "repaired" : "unrepaired"); + out.println(message); + + List sstableList = new ArrayList<>(); + for (String keyspace : keyspaces) + { + try + { + sstableList.addAll(probe.mutateSSTableRepairedState(isRepaired, !reallySet, keyspace, + tables.isEmpty() + ? probe.getAutoRepairTablesForKeyspace(keyspace) // mutate all tables + : tables)); // mutate specific tables + } + catch (InvalidRequestException e) + { + out.println(e.getMessage()); + } + } + if (!reallySet) + out.println("The following SSTables would be mutated:"); + else + out.println("The following SSTables were mutated:"); + for (String sstable : sstableList) + out.println(sstable); + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/SetAutoRepairConfig.java b/src/java/org/apache/cassandra/tools/nodetool/SetAutoRepairConfig.java new file mode 100644 index 000000000000..bc0e5d88031c --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/SetAutoRepairConfig.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.tools.nodetool; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.base.Throwables; + +import io.airlift.airline.Arguments; +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool.NodeToolCmd; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.management.InstanceNotFoundException; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Allows to set AutoRepair configuration through nodetool. + */ +@Command(name = "setautorepairconfig", description = "sets the autorepair configuration") +public class SetAutoRepairConfig extends NodeToolCmd +{ + @VisibleForTesting + @Arguments(title = " ", usage = " ", + description = "autorepair param and value.\nPossible autorepair parameters are as following: " + + "[start_scheduler|number_of_repair_threads|min_repair_interval|sstable_upper_threshold" + + "|enabled|table_max_repair_time|priority_hosts|forcerepair_hosts|ignore_dcs" + + "|history_clear_delete_hosts_buffer_interval|repair_primary_token_range_only" + + "|parallel_repair_count|parallel_repair_percentage" + + "|allow_parallel_replica_repair|allow_parallel_repair_across_schedules" + + "|materialized_view_repair_enabled|repair_max_retries" + + "|repair_retry_backoff|repair_session_timeout|min_repair_task_duration" + + "|repair_by_keyspace|mixed_major_version_repair_enabled|token_range_splitter.]", + required = true) + protected List args = new ArrayList<>(); + + @VisibleForTesting + @Option(title = "repair type", name = { "-t", "--repair-type" }, description = "Repair type") + protected String repairTypeStr; + + @VisibleForTesting + protected PrintStream out = System.out; + + private static final String TOKEN_RANGE_SPLITTER_PROPERTY_PREFIX = "token_range_splitter."; + + @Override + public void execute(NodeProbe probe) + { + checkArgument(args.size() == 2, "setautorepairconfig requires param-type, and value args."); + String paramType = args.get(0); + String paramVal = args.get(1); + + try + { + probe.isAutoRepairDisabled(); + } + catch (Throwable e) + { + if (Throwables.getRootCause(e) instanceof InstanceNotFoundException) + { + out.println("Auto-repair is not supported or not enabled via -Dcassandra.autorepair.enable=true on the server."); + return; + } + else + { + throw e; + } + } + + if (probe.isAutoRepairDisabled() && !paramType.equalsIgnoreCase("start_scheduler")) + { + out.println("Auto-repair is not enabled"); + return; + } + + // options that do not require --repair-type option + switch (paramType) + { + case "start_scheduler": + if (Boolean.parseBoolean(paramVal)) + { + probe.startAutoRepairScheduler(); + } + return; + case "history_clear_delete_hosts_buffer_interval": + probe.setAutoRepairHistoryClearDeleteHostsBufferDuration(paramVal); + return; + case "min_repair_task_duration": + probe.setAutoRepairMinRepairTaskDuration(paramVal); + return; + case "mixed_major_version_repair_enabled": + probe.setMixedMajorVersionRepairEnabled(Boolean.parseBoolean(paramVal)); + return; + default: + // proceed to options that require --repair-type option + break; + } + + // options below require --repair-type option + Objects.requireNonNull(repairTypeStr, "--repair-type is required for this parameter."); + + if(paramType.startsWith(TOKEN_RANGE_SPLITTER_PROPERTY_PREFIX)) + { + final String key = paramType.replace(TOKEN_RANGE_SPLITTER_PROPERTY_PREFIX, ""); + probe.setAutoRepairTokenRangeSplitterParameter(repairTypeStr, key, paramVal); + return; + } + + switch (paramType) + { + case "enabled": + probe.setAutoRepairEnabled(repairTypeStr, Boolean.parseBoolean(paramVal)); + break; + case "number_of_repair_threads": + probe.setAutoRepairThreads(repairTypeStr, Integer.parseInt(paramVal)); + break; + case "min_repair_interval": + probe.setAutoRepairMinInterval(repairTypeStr, paramVal); + break; + case "sstable_upper_threshold": + probe.setAutoRepairSSTableCountHigherThreshold(repairTypeStr, Integer.parseInt(paramVal)); + break; + case "table_max_repair_time": + probe.setAutoRepairTableMaxRepairTime(repairTypeStr, paramVal); + break; + case "priority_hosts": + if (paramVal!= null && !paramVal.isEmpty()) + { + probe.setAutoRepairPriorityForHosts(repairTypeStr, paramVal); + } + break; + case "forcerepair_hosts": + probe.setAutoRepairForceRepairForHosts(repairTypeStr, paramVal); + break; + case "ignore_dcs": + Set ignoreDCs = new HashSet<>(); + for (String dc : Splitter.on(',').split(paramVal)) + { + ignoreDCs.add(dc); + } + probe.setAutoRepairIgnoreDCs(repairTypeStr, ignoreDCs); + break; + case "repair_primary_token_range_only": + probe.setAutoRepairPrimaryTokenRangeOnly(repairTypeStr, Boolean.parseBoolean(paramVal)); + break; + case "parallel_repair_count": + probe.setAutoRepairParallelRepairCount(repairTypeStr, Integer.parseInt(paramVal)); + break; + case "parallel_repair_percentage": + probe.setAutoRepairParallelRepairPercentage(repairTypeStr, Integer.parseInt(paramVal)); + break; + case "allow_parallel_replica_repair": + probe.setAutoRepairAllowParallelReplicaRepair(repairTypeStr, Boolean.parseBoolean(paramVal)); + break; + case "allow_parallel_replica_repair_across_schedules": + probe.setAutoRepairAllowParallelReplicaRepairAcrossSchedules(repairTypeStr, Boolean.parseBoolean(paramVal)); + break; + case "materialized_view_repair_enabled": + probe.setAutoRepairMaterializedViewRepairEnabled(repairTypeStr, Boolean.parseBoolean(paramVal)); + break; + case "repair_session_timeout": + probe.setAutoRepairSessionTimeout(repairTypeStr, paramVal); + break; + case "repair_by_keyspace": + probe.setAutoRepairRepairByKeyspace(repairTypeStr, Boolean.parseBoolean(paramVal)); + break; + case "repair_max_retries": + probe.setAutoRepairMaxRetriesCount(repairTypeStr, Integer.parseInt(paramVal)); + break; + case "repair_retry_backoff": + probe.setAutoRepairRetryBackoff(repairTypeStr, paramVal); + break; + default: + throw new IllegalArgumentException("Unknown parameter: " + paramType); + } + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/Sjk.java b/src/java/org/apache/cassandra/tools/nodetool/Sjk.java index d7f7a043f606..1472ccc89fa5 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Sjk.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Sjk.java @@ -25,8 +25,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Enumeration; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -405,6 +405,7 @@ private static List> findClasses(String packageName) List> result = new ArrayList<>(); try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); String path = packageName.replace('.', '/'); for (String f : findFiles(path)) { @@ -412,7 +413,7 @@ private static List> findClasses(String packageName) { f = f.substring(0, f.length() - ".class".length()); f = f.replace('/', '.'); - result.add(Class.forName(f)); + result.add(Class.forName(f, false, cl)); } } return result; diff --git a/src/java/org/apache/cassandra/tools/nodetool/Verify.java b/src/java/org/apache/cassandra/tools/nodetool/Verify.java index 0a610b3266a0..7fa126b37c85 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Verify.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Verify.java @@ -36,9 +36,14 @@ public class Verify extends NodeToolCmd @Option(title = "extended_verify", name = {"-e", "--extended-verify"}, - description = "Verify each cell data, beyond simply checking sstable checksums") + description = "Verify each partition data, beyond simply checking sstable checksums") private boolean extendedVerify = false; + @Option(title = "validate_all_rows", + name = {"-v", "--validate-all-rows"}, + description = "Verify each row and cell data in the partition, beyond checking partition key. Must be enabled with extended verification") + private boolean validateAllRows = false; + @Option(title = "check_version", name = {"-c", "--check-version"}, description = "Also check that all sstables are the latest version") @@ -93,7 +98,7 @@ public void execute(NodeProbe probe) { try { - probe.verify(out, extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspace, tableNames); + probe.verify(out, extendedVerify, validateAllRows, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspace, tableNames); } catch (Exception e) { throw new RuntimeException("Error occurred during verifying", e); diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java b/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java index a225ccb88cd4..1bc93199119d 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java +++ b/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java @@ -245,7 +245,7 @@ private void initializeKeyspaces(NodeProbe probe, boolean ignore, List t statsTable.maxSSTableSize = sstableSize == null ? 0 : sstableSize; int[] leveledSStables = table.getSSTableCountPerLevel(); - if (leveledSStables != null) + if (leveledSStables.length > 0) { statsTable.isLeveledSstable = true; diff --git a/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java b/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java index bf9508020191..e7dfd3974015 100644 --- a/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java +++ b/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java @@ -29,7 +29,7 @@ class ExpiredTraceState extends TraceState ExpiredTraceState(TraceState delegate) { - super(FBUtilities.getBroadcastAddressAndPort(), delegate.sessionId, delegate.traceType); + super(delegate.clientState, FBUtilities.getBroadcastAddressAndPort(), delegate.sessionId, delegate.traceType); this.delegate = delegate; } diff --git a/src/java/org/apache/cassandra/tracing/TraceKeyspace.java b/src/java/org/apache/cassandra/tracing/TraceKeyspace.java index fb92f4dac59a..f46f3c46f369 100644 --- a/src/java/org/apache/cassandra/tracing/TraceKeyspace.java +++ b/src/java/org/apache/cassandra/tracing/TraceKeyspace.java @@ -109,7 +109,9 @@ private static TableMetadata parse(String table, String description, String cql) public static KeyspaceMetadata metadata() { - return KeyspaceMetadata.create(SchemaConstants.TRACE_KEYSPACE_NAME, KeyspaceParams.simple(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), Tables.of(Sessions, Events)); + return KeyspaceMetadata.create(SchemaConstants.TRACE_KEYSPACE_NAME, + KeyspaceParams.systemDistributed(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), + Tables.of(Sessions, Events)); } static Mutation makeStartSessionMutation(ByteBuffer sessionId, diff --git a/src/java/org/apache/cassandra/tracing/TraceState.java b/src/java/org/apache/cassandra/tracing/TraceState.java index 17133698e830..5c4a6d54d125 100644 --- a/src/java/org/apache/cassandra/tracing/TraceState.java +++ b/src/java/org/apache/cassandra/tracing/TraceState.java @@ -22,6 +22,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; import com.google.common.base.Stopwatch; import org.slf4j.helpers.MessageFormatter; @@ -29,6 +30,8 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.TracingClientState; import org.apache.cassandra.utils.progress.ProgressEvent; import org.apache.cassandra.utils.progress.ProgressEventNotifier; import org.apache.cassandra.utils.progress.ProgressListener; @@ -45,6 +48,10 @@ public abstract class TraceState implements ProgressEventNotifier public final ByteBuffer sessionIdBytes; public final Tracing.TraceType traceType; public final int ttl; + public final ClientState clientState; + + private boolean rangeQuery; + private String tracedKeyspace; private boolean notify; private final List listeners = new CopyOnWriteArrayList<>(); @@ -63,11 +70,12 @@ public enum Status // See CASSANDRA-7626 for more details. private final AtomicInteger references = new AtomicInteger(1); - protected TraceState(InetAddressAndPort coordinator, TimeUUID sessionId, Tracing.TraceType traceType) + protected TraceState(ClientState clientState, InetAddressAndPort coordinator, TimeUUID sessionId, Tracing.TraceType traceType) { assert coordinator != null; assert sessionId != null; + this.clientState = clientState; this.coordinator = coordinator; this.sessionId = sessionId; sessionIdBytes = sessionId.toBytes(); @@ -103,6 +111,34 @@ public void removeProgressListener(ProgressListener listener) listeners.remove(listener); } + public boolean isRangeQuery() + { + return rangeQuery; + } + + public void setRangeQuery(boolean rangeQuery) + { + this.rangeQuery = rangeQuery; + } + + /** + * @return the keyspace being traced. + */ + public @Nullable String tracedKeyspace() + { + if (clientState instanceof TracingClientState) + return ((TracingClientState) clientState).tracedKeyspace(); + return tracedKeyspace; + } + + /** + * @param tracedKeyspace the keyspace being traced. + */ + public void tracedKeyspace(String tracedKeyspace) + { + this.tracedKeyspace = tracedKeyspace; + } + public int elapsed() { long elapsed = watch.elapsed(TimeUnit.MICROSECONDS); diff --git a/src/java/org/apache/cassandra/tracing/TraceStateImpl.java b/src/java/org/apache/cassandra/tracing/TraceStateImpl.java index edc2cb796a86..2232ec76e834 100644 --- a/src/java/org/apache/cassandra/tracing/TraceStateImpl.java +++ b/src/java/org/apache/cassandra/tracing/TraceStateImpl.java @@ -32,6 +32,9 @@ import org.apache.cassandra.db.Mutation; import org.apache.cassandra.exceptions.OverloadedException; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.metrics.ClientRequestsMetrics; +import org.apache.cassandra.metrics.ClientRequestsMetricsProvider; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.JVMStabilityInspector; @@ -55,9 +58,9 @@ public class TraceStateImpl extends TraceState private final Set> pendingFutures = ConcurrentHashMap.newKeySet(); - public TraceStateImpl(InetAddressAndPort coordinator, TimeUUID sessionId, Tracing.TraceType traceType) + public TraceStateImpl(ClientState state, InetAddressAndPort coordinator, TimeUUID sessionId, Tracing.TraceType traceType) { - super(coordinator, sessionId, traceType); + super(state, coordinator, sessionId, traceType); } protected void traceImpl(String message) @@ -103,17 +106,18 @@ protected void waitForPendingEvents() void executeMutation(final Mutation mutation) { - Future fut = Stage.TRACING.executor().submit(() -> mutateWithCatch(mutation), null); + Future fut = Stage.TRACING.submit(() -> mutateWithCatch(clientState, mutation), null); boolean ret = pendingFutures.add(fut); if (!ret) logger.warn("Failed to insert pending future, tracing synchronization may not work"); } - static void mutateWithCatch(Mutation mutation) + static void mutateWithCatch(ClientState state, Mutation mutation) { try { - StorageProxy.mutate(singletonList(mutation), ANY, Dispatcher.RequestTime.forImmediateExecution()); + ClientRequestsMetrics metrics = ClientRequestsMetricsProvider.instance.metrics(mutation.getKeyspaceName()); + StorageProxy.mutate(singletonList(mutation), ANY, Dispatcher.RequestTime.forImmediateExecution(), metrics, state); } catch (OverloadedException e) { diff --git a/src/java/org/apache/cassandra/tracing/Tracing.java b/src/java/org/apache/cassandra/tracing/Tracing.java index f1c5b54b94f1..adbcc7a9d9b8 100644 --- a/src/java/org/apache/cassandra/tracing/Tracing.java +++ b/src/java/org/apache/cassandra/tracing/Tracing.java @@ -23,6 +23,7 @@ import java.net.InetAddress; import java.nio.ByteBuffer; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -32,12 +33,17 @@ import org.apache.cassandra.concurrent.ExecutorLocals; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.statements.BatchStatement; +import org.apache.cassandra.cql3.statements.ModificationStatement; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.ParamType; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.TracingClientState; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.TimeUUID; @@ -69,6 +75,12 @@ public long serializedSize(TraceType traceType, int version) } }; + public static void logAndTrace(Logger logger, String message, Object... args) + { + logger.trace(message, args); + trace(message, args); + } + /* this enum is used in serialization; preserve order for compatibility */ public enum TraceType { @@ -102,8 +114,6 @@ public int getTTL() protected static final Logger logger = LoggerFactory.getLogger(Tracing.class); - private final InetAddressAndPort localAddress = FBUtilities.getLocalAddressAndPort(); - protected final ConcurrentMap sessions = new ConcurrentHashMap<>(); public static final Tracing instance; @@ -116,7 +126,7 @@ public int getTTL() { 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()); } @@ -126,7 +136,7 @@ public int getTTL() logger.error(String.format("Cannot use class %s for tracing, ignoring by defaulting to normal tracing", customTracingClass), e); } } - instance = null != tracing ? tracing : new TracingImpl(); + instance = tracing == null ? new TracingImpl() : tracing; } public TimeUUID getSessionId() @@ -147,6 +157,41 @@ public int getTTL() return get().ttl; } + public static boolean traceSinglePartitions() + { + return instance.get() != null && !instance.get().isRangeQuery(); + } + + public void setRangeQuery(boolean rangeQuery) + { + assert isTracing(); + get().setRangeQuery(rangeQuery); + } + + /** + * set traced keyspace into trace state which is later used to for billing to track source tenant at replicas. + */ + public static void setupTracedKeyspace(CQLStatement statement) + { + if (!Tracing.isTracing()) + return; + + String keyspace = null; + if (statement instanceof CQLStatement.SingleKeyspaceCqlStatement) + { + keyspace = ((CQLStatement.SingleKeyspaceCqlStatement) statement).keyspace(); + } + + if (keyspace == null && statement instanceof BatchStatement) + { + // for batch statement, just pick any keyspace, as it's only used to extract tenant id in BillingQueryInfoTracker + List batches = ((BatchStatement) statement).getStatements(); + if (batches.size() > 0) + keyspace = batches.get(0).keyspace(); + } + Tracing.instance.get().tracedKeyspace(keyspace); + } + /** * Indicates if the current thread's execution is being traced. */ @@ -155,33 +200,35 @@ public static boolean isTracing() return instance.get() != null; } - public TimeUUID newSession(Map customPayload) + public TimeUUID newSession(ClientState state, Map customPayload) { return newSession( + state, nextTimeUUID(), TraceType.QUERY, customPayload); } - public TimeUUID newSession(TraceType traceType) + public TimeUUID newSession(ClientState state, TraceType traceType) { return newSession( + state, nextTimeUUID(), traceType, Collections.EMPTY_MAP); } - public TimeUUID newSession(TimeUUID sessionId, Map customPayload) + public TimeUUID newSession(ClientState state, TimeUUID sessionId, Map customPayload) { - return newSession(sessionId, TraceType.QUERY, customPayload); + return newSession(state, sessionId, TraceType.QUERY, customPayload); } /** This method is intended to be overridden in tracing implementations that need access to the customPayload */ - protected TimeUUID newSession(TimeUUID sessionId, TraceType traceType, Map customPayload) + protected TimeUUID newSession(ClientState state, TimeUUID sessionId, TraceType traceType, Map customPayload) { assert get() == null; - TraceState ts = newTraceState(localAddress, sessionId, traceType); + TraceState ts = newTraceState(state, FBUtilities.getLocalAddressAndPort(), sessionId, traceType); set(ts); sessions.put(sessionId, ts); @@ -230,7 +277,7 @@ public TraceState get(TimeUUID sessionId) public void set(TraceState tls) { ExecutorLocals current = ExecutorLocals.current(); - ExecutorLocals.Impl.set(tls, current.clientWarnState); + ExecutorLocals.Impl.set(tls, current.clientWarnState, current.sensors, current.operationContext); } public TraceState begin(final String request, final Map parameters) @@ -257,14 +304,16 @@ public TraceState initializeFromMessage(final Message.Header header) TraceType traceType = header.traceType(); + ClientState clientState = TracingClientState.withTracedKeyspace(header.traceKeyspace()); + ts = newTraceState(clientState, header.from, sessionId, traceType); + if (header.verb.isResponse()) { // received a message for a session we've already closed out. see CASSANDRA-5668 - return new ExpiredTraceState(newTraceState(header.from, sessionId, traceType)); + return new ExpiredTraceState(ts); } else { - ts = newTraceState(header.from, sessionId, traceType); sessions.put(sessionId, ts); return ts; } @@ -288,7 +337,9 @@ public void traceOutgoingMessage(Message message, int serializedSize, InetAdd if (state == null) // session may have already finished; see CASSANDRA-5668 { TraceType traceType = message.traceType(); - trace(sessionId.toBytes(), logMessage, traceType.getTTL()); + String traceKeyspace = message.header.traceKeyspace(); + ClientState clientState = TracingClientState.withTracedKeyspace(traceKeyspace); + trace(clientState, sessionId.toBytes(), logMessage, traceType.getTTL()); } else { @@ -309,10 +360,19 @@ public Map addTraceHeaders(Map addToMutabl addToMutable.put(ParamType.TRACE_SESSION, Tracing.instance.getSessionId()); addToMutable.put(ParamType.TRACE_TYPE, Tracing.instance.getTraceType()); + String keyspace = Tracing.instance.get().tracedKeyspace(); + if (keyspace != null) + { + addToMutable.put(ParamType.TRACE_KEYSPACE, keyspace); + } return addToMutable; } - protected abstract TraceState newTraceState(InetAddressAndPort coordinator, TimeUUID sessionId, Tracing.TraceType traceType); + protected abstract TraceState newTraceState( + ClientState state, + InetAddressAndPort coordinator, + TimeUUID sessionId, + Tracing.TraceType traceType); // repair just gets a varargs method since it's so heavyweight anyway public static void traceRepair(String format, Object... args) @@ -364,5 +424,5 @@ public static void trace(String format, Object... args) /** * Called for non-local traces (traces that are not initiated by local node == coordinator). */ - public abstract void trace(ByteBuffer sessionId, String message, int ttl); + public abstract void trace(ClientState clientState, ByteBuffer sessionId, String message, int ttl); } diff --git a/src/java/org/apache/cassandra/tracing/TracingImpl.java b/src/java/org/apache/cassandra/tracing/TracingImpl.java index 1885146bee2b..5b828187fa68 100644 --- a/src/java/org/apache/cassandra/tracing/TracingImpl.java +++ b/src/java/org/apache/cassandra/tracing/TracingImpl.java @@ -24,8 +24,10 @@ import java.util.Map; import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.db.Mutation; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.WrappedRunnable; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -95,15 +97,15 @@ private TraceStateImpl getStateImpl() } @Override - protected TraceState newTraceState(InetAddressAndPort coordinator, TimeUUID sessionId, TraceType traceType) + protected TraceState newTraceState(ClientState state, InetAddressAndPort coordinator, TimeUUID sessionId, TraceType traceType) { - return new TraceStateImpl(coordinator, sessionId, traceType); + return new TraceStateImpl(state, coordinator, sessionId, traceType); } /** * Called for non-local traces (traces that are not initiated by local node == coordinator). */ - public void trace(final ByteBuffer sessionId, final String message, final int ttl) + public void trace(ClientState clientState, final ByteBuffer sessionId, final String message, final int ttl) { final String threadName = Thread.currentThread().getName(); @@ -111,7 +113,8 @@ public void trace(final ByteBuffer sessionId, final String message, final int tt { public void runMayThrow() { - TraceStateImpl.mutateWithCatch(TraceKeyspace.makeEventMutation(sessionId, message, -1, threadName, ttl)); + Mutation mutation = TraceKeyspace.makeEventMutation(sessionId, message, -1, threadName, ttl); + TraceStateImpl.mutateWithCatch(clientState, mutation); } }); } diff --git a/src/java/org/apache/cassandra/transport/CBUtil.java b/src/java/org/apache/cassandra/transport/CBUtil.java index e9bafe5cfad8..d96f330bce18 100644 --- a/src/java/org/apache/cassandra/transport/CBUtil.java +++ b/src/java/org/apache/cassandra/transport/CBUtil.java @@ -454,6 +454,9 @@ public static ByteBuffer readValueNoCopy(ByteBuf cb) int length = cb.readInt(); if (length < 0) return null; + if (length > cb.readableBytes()) + throw new ProtocolException(String.format("Cannot read value of length %d, only %d bytes remaining in the message", + length, cb.readableBytes())); ByteBuffer buffer = cb.nioBuffer(cb.readerIndex(), length); cb.skipBytes(length); @@ -660,6 +663,9 @@ public static byte[] readRawBytes(ByteBuf cb) private static byte[] readRawBytes(ByteBuf cb, int length) { + if (length > cb.readableBytes()) + throw new ProtocolException(String.format("Cannot read value of length %d, only %d bytes remaining in the message", + length, cb.readableBytes())); byte[] bytes = new byte[length]; cb.readBytes(bytes); return bytes; diff --git a/src/java/org/apache/cassandra/transport/CQLMessageHandler.java b/src/java/org/apache/cassandra/transport/CQLMessageHandler.java index e8711b32b055..d922d8d170ca 100644 --- a/src/java/org/apache/cassandra/transport/CQLMessageHandler.java +++ b/src/java/org/apache/cassandra/transport/CQLMessageHandler.java @@ -104,7 +104,7 @@ public class CQLMessageHandler extends AbstractMessageHandler interface MessageConsumer { - void dispatch(Channel channel, M message, Dispatcher.FlushItemConverter toFlushItem, Overload backpressure); +

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

    toFlushItem, P param, Overload backpressure); boolean hasQueueCapacity(); } @@ -157,6 +157,12 @@ public boolean process(FrameDecoder.Frame frame) throws IOException return super.process(frame); } + @Override + protected void onDecoderReactivated() + { + ClientMetrics.instance.unpauseConnection(); + } + /** * Checks limits on bytes in flight and the request rate limiter (if enabled), then takes one of three actions: * @@ -388,7 +394,7 @@ protected boolean processRequest(Envelope request, Overload backpressure) try { message = messageDecoder.decode(channel, request); - dispatcher.dispatch(channel, message, this::toFlushItem, backpressure); + dispatcher.dispatch(channel, message, CQLMessageHandler::toFlushItem, this, backpressure); // sucessfully delivered a CQL message to the execution // stage, so reset the counter of consecutive errors @@ -484,7 +490,8 @@ private Framed toFlushItem(Channel channel, Message.Request request, Message.Res // The Dispatcher will call this to obtain the FlushItem to enqueue with its Flusher once // a dispatched request has been processed. - Envelope responseFrame = response.encode(request.getSource().header.version); + Envelope.Header header = request.getSource().header; + Envelope responseFrame = response.encode(header.version, header.streamId); int responseSize = envelopeSize(responseFrame.header); ClientMessageSizeMetrics.bytesSent.inc(responseSize); ClientMessageSizeMetrics.bytesSentPerResponse.update(responseSize); @@ -498,9 +505,9 @@ private Framed toFlushItem(Channel channel, Message.Request request, Message.Res private void release(Flusher.FlushItem flushItem) { - release(flushItem.request.header); - flushItem.request.release(); - flushItem.response.release(); + release(flushItem.requestEnvelope.header); + flushItem.requestEnvelope.release(); + flushItem.responseEnvelope.release(); } private void release(Envelope.Header header) @@ -522,8 +529,9 @@ protected boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpo if (!extracted.isSuccess()) { // Hard fail on any decoding error as we can't trust the subsequent frames of - // the large message - handleError(ProtocolException.toFatalException(extracted.error())); + // the large message. The stream id is a best-effort value read before extraction + // failed, so route it back where possible rather than defaulting. + handleError(ProtocolException.toFatalException(extracted.error()), extracted.streamId()); return false; } @@ -540,7 +548,7 @@ protected boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpo // not make sense to continue processing subsequent frames handleError(ProtocolException.toFatalException(new OversizedAuthMessageException( MULTI_FRAME_AUTH_ERROR_MESSAGE_PREFIX + - "type = " + header.type + ", size = " + header.bodySizeInBytes))); + "type = " + header.type + ", size = " + header.bodySizeInBytes)), header.streamId); ClientMetrics.instance.markRequestDiscarded(); return false; } diff --git a/src/java/org/apache/cassandra/transport/Client.java b/src/java/org/apache/cassandra/transport/Client.java index 45f5e1f2fad4..c9ba4de6dd4c 100644 --- a/src/java/org/apache/cassandra/transport/Client.java +++ b/src/java/org/apache/cassandra/transport/Client.java @@ -29,6 +29,7 @@ import org.apache.cassandra.auth.PasswordAuthenticator; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.EncryptionOptions; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.marshal.Int32Type; @@ -131,14 +132,14 @@ else if (msgType.equals("QUERY")) line = line.substring(6); // Ugly hack to allow setting a page size, but that's playground code anyway String query = line; - int pageSize = -1; + PageSize pageSize = PageSize.NONE; if (line.matches(".+ !\\d+$")) { int idx = line.lastIndexOf('!'); query = line.substring(0, idx-1); try { - pageSize = Integer.parseInt(line.substring(idx+1, line.length())); + pageSize = PageSize.inRows(Integer.parseInt(line.substring(idx + 1, line.length()))); } catch (NumberFormatException e) { diff --git a/src/java/org/apache/cassandra/transport/DataType.java b/src/java/org/apache/cassandra/transport/DataType.java index 1d1a9130b646..e1862bebfb91 100644 --- a/src/java/org/apache/cassandra/transport/DataType.java +++ b/src/java/org/apache/cassandra/transport/DataType.java @@ -21,15 +21,41 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.Map; import java.util.List; +import java.util.Map; import com.google.common.annotations.VisibleForTesting; import io.netty.buffer.ByteBuf; - import org.apache.cassandra.cql3.FieldIdentifier; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.ByteType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.CounterColumnType; +import org.apache.cassandra.db.marshal.DateType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.DoubleType; +import org.apache.cassandra.db.marshal.DurationType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.ShortType; +import org.apache.cassandra.db.marshal.SimpleDateType; +import org.apache.cassandra.db.marshal.TimeType; +import org.apache.cassandra.db.marshal.TimeUUIDType; +import org.apache.cassandra.db.marshal.TimestampType; +import org.apache.cassandra.db.marshal.TupleType; +import org.apache.cassandra.db.marshal.TypeParser; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.utils.Pair; @@ -224,8 +250,7 @@ public static Pair fromType(AbstractType type, ProtocolVersion { // For CQL3 clients, ReversedType is an implementation detail and they // shouldn't have to care about it. - if (type instanceof ReversedType) - type = ((ReversedType)type).baseType; + type = type.unwrap(); // For compatibility sake, we still return DateType as the timestamp type in resultSet metadata (#5723) if (type instanceof DateType) diff --git a/src/java/org/apache/cassandra/transport/Dispatcher.java b/src/java/org/apache/cassandra/transport/Dispatcher.java index d6cb5e822f9f..98e042f7cf25 100644 --- a/src/java/org/apache/cassandra/transport/Dispatcher.java +++ b/src/java/org/apache/cassandra/transport/Dispatcher.java @@ -32,7 +32,9 @@ import io.netty.channel.EventLoop; import io.netty.util.AttributeKey; import org.apache.cassandra.concurrent.DebuggableTask; +import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.concurrent.LocalAwareExecutorPlus; +import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.OverloadedException; import org.apache.cassandra.metrics.ClientMetrics; @@ -40,10 +42,12 @@ import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings; +import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.ClientResourceLimits.Overload; import org.apache.cassandra.transport.Flusher.FlushItem; import org.apache.cassandra.transport.messages.ErrorMessage; import org.apache.cassandra.transport.messages.EventMessage; +import org.apache.cassandra.transport.messages.StartupMessage; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MonotonicClock; import org.apache.cassandra.utils.NoSpamLogger; @@ -55,10 +59,7 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer { - FlushItem toFlushItem(Channel channel, Message.Request request, Message.Response response); + FlushItem toFlushItem(P param, Channel channel, Message.Request request, Message.Response response); } public Dispatcher(boolean useLegacyFlusher) @@ -101,18 +101,17 @@ public Dispatcher(boolean useLegacyFlusher) } @Override - public void dispatch(Channel channel, Message.Request request, FlushItemConverter forFlusher, Overload backpressure) + public

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

    forFlusher, P param, Overload backpressure) { if (!request.connection().getTracker().isRunning()) { // We can not respond with a custom, transport, or server exceptions since, given current implementation of clients, // they will defunct the connection. Without a protocol version bump that introduces an "I am going away message", // we have to stick to an existing error code. - Message.Response response = ErrorMessage.fromException(new OverloadedException("Server is shutting down")); - response.setStreamId(request.getStreamId()); + Message.Response response = ErrorMessage.fromTransportException(new OverloadedException("Server is shutting down")); response.setWarnings(ClientWarn.instance.getWarnings()); response.attach(request.connection); - FlushItem toFlush = forFlusher.toFlushItem(channel, request, response); + FlushItem toFlush = forFlusher.toFlushItem(param, channel, request, response); flush(toFlush); return; } @@ -122,9 +121,9 @@ public void dispatch(Channel channel, Message.Request request, FlushItemConverte (request.type == Message.Type.AUTH_RESPONSE || request.type == Message.Type.CREDENTIALS); // Importantly, the authExecutor will handle the AUTHENTICATE message which may be CPU intensive. - LocalAwareExecutorPlus executor = isAuthQuery ? authExecutor : requestExecutor; + ExecutorPlus executor = isAuthQuery ? authExecutor : requestExecutor; - executor.submit(new RequestProcessor(channel, request, forFlusher, backpressure)); + executor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure)); ClientMetrics.instance.markRequestDispatched(); } @@ -283,20 +282,22 @@ public long timeSpentInQueueNanos() * is the only way we can keep it not wrapped into a callable on SEPExecutor submission path. And we need this * functionality for tracking time purposes. */ - public class RequestProcessor implements DebuggableTask.RunnableDebuggableTask + public class RequestProcessor

    implements DebuggableTask.RunnableDebuggableTask { private final Channel channel; private final Message.Request request; - private final FlushItemConverter forFlusher; + private final FlushItemConverter

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

    forFlusher, P flusherParam, Overload backpressure) { this.channel = channel; this.request = request; this.forFlusher = forFlusher; + this.flusherParam = flusherParam; this.backpressure = backpressure; } @@ -304,7 +305,7 @@ public RequestProcessor(Channel channel, Message.Request request, FlushItemConve public void run() { startTimeNanos = MonotonicClock.Global.preciseTime.now(); - processRequest(channel, request, forFlusher, backpressure, new RequestTime(request.createdAtNanos, startTimeNanos)); + processRequest(channel, request, forFlusher, flusherParam, backpressure, new RequestTime(request.createdAtNanos, startTimeNanos)); } @Override @@ -348,23 +349,15 @@ public boolean hasQueueCapacity() return requestExecutor.oldestTaskQueueTime() < (DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS) * threshold); } - /** - * Note: this method may be executed on the netty event loop, during initial protocol negotiation; the caller is - * responsible for cleaning up any global or thread-local state. (ex. tracing, client warnings, etc.). - */ private static Message.Response processRequest(ServerConnection connection, Message.Request request, Overload backpressure, RequestTime requestTime) { long queueTime = requestTime.timeSpentInQueueNanos(); - // If we have already crossed the max timeout for all possible RPCs, we time out the query immediately. - // We do not differentiate between query types here, since if we got into a situation when, say, we have a PREPARE - // query that is stuck behind the EXECUTE query, we would rather time it out and catch up with a backlog, expecting - // that the bursts are going to be short-lived. ClientMetrics.instance.queueTime(queueTime, TimeUnit.NANOSECONDS); if (queueTime > DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)) { ClientMetrics.instance.markTimedOutBeforeProcessing(); - return ErrorMessage.fromException(new OverloadedException("Query timed out before it could start")); + return ErrorMessage.fromTransportException(new OverloadedException("Query timed out before it could start")); } if (connection.getVersion().isGreaterOrEqualTo(ProtocolVersion.V4)) @@ -413,26 +406,22 @@ private static Message.Response processRequest(ServerConnection connection, Mess Message.logger.trace("Received: {}, v={}", request, connection.getVersion()); connection.requests.inc(); - Message.Response response = request.execute(qstate, requestTime); + Message.Response response = request.execute(qstate, requestTime).syncUninterruptibly().getNow(); if (request.isTrackable()) CoordinatorWarnings.done(); - response.setStreamId(request.getStreamId()); - response.setWarnings(ClientWarn.instance.getWarnings()); response.attach(connection); connection.applyStateTransition(request.type, response.type); return response; } - - /** - * Note: this method may be executed on the netty event loop. - */ + static Message.Response processRequest(Channel channel, Message.Request request, Overload backpressure, RequestTime requestTime) { + Message.Response response = null; try { - return processRequest((ServerConnection) request.connection(), request, backpressure, requestTime); + response = processRequest((ServerConnection) request.connection(), request, backpressure, requestTime); } catch (Throwable t) { @@ -442,29 +431,48 @@ static Message.Response processRequest(Channel channel, Message.Request request, CoordinatorWarnings.done(); Predicate handler = ExceptionHandlers.getUnexpectedExceptionHandler(channel, true); - ErrorMessage error = ErrorMessage.fromException(t, handler); - error.setStreamId(request.getStreamId()); - error.setWarnings(ClientWarn.instance.getWarnings()); - return error; + response = ErrorMessage.fromExceptionNoStreamId(t, handler); } finally { + if (response != null) + response.setWarnings(ClientWarn.instance.getWarnings()); CoordinatorWarnings.reset(); ClientWarn.instance.resetWarnings(); + Tracing.instance.set(null); } + return response; } - /** - * Note: this method is not expected to execute on the netty event loop. - */ - void processRequest(Channel channel, Message.Request request, FlushItemConverter forFlusher, Overload backpressure, RequestTime requestTime) +

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

    forFlusher, P param, Overload backpressure, RequestTime requestTime) { Message.Response response = processRequest(channel, request, backpressure, requestTime); - FlushItem toFlush = forFlusher.toFlushItem(channel, request, response); - Message.logger.trace("Responding: {}, v={}", response, request.connection().getVersion()); + FlushItem toFlush = forFlusher.toFlushItem(param, channel, request, response); flush(toFlush); } + static Message.Response processInit(ServerConnection connection, StartupMessage request) + { + Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution(); + if (connection.getVersion().isGreaterOrEqualTo(ProtocolVersion.V4)) + ClientWarn.instance.captureWarnings(); + + QueryState qstate = connection.validateNewMessage(request.type, connection.getVersion()); + + Message.logger.trace("Received: {}, v={}", request, connection.getVersion()); + connection.requests.inc(); + + Message.Response result = request.execute(qstate, requestTime).syncUninterruptibly().getNow(); + if (result != null) + { + result.setWarnings(ClientWarn.instance.getWarnings()); + result.attach(connection); + connection.applyStateTransition(request.type, result.type); + } + ClientWarn.instance.resetWarnings(); + return result; + } + private void flush(FlushItem item) { EventLoop loop = item.channel.eventLoop(); @@ -488,7 +496,6 @@ public boolean isDone() public static void shutdown() { - requestExecutor.shutdown(); authExecutor.shutdown(); } @@ -497,7 +504,7 @@ public static void shutdown() * for delivering events to registered clients is dependent on protocol version and the configuration * of the pipeline. For v5 and newer connections, the event message is encoded into an Envelope, * wrapped in a FlushItem and then delivered via the pipeline's flusher, in a similar way to - * a Response returned from {@link #processRequest(Channel, Message.Request, FlushItemConverter, Overload, RequestTime)}. + * a Response returned from {@link #processRequest(Channel, Message.Request, FlushItemConverter, Object, Overload, RequestTime)}. * It's worth noting that events are not generally fired as a direct response to a client request, * so this flush item has a null request attribute. The dispatcher itself is created when the * pipeline is first configured during protocol negotiation and is attached to the channel for @@ -511,9 +518,9 @@ Consumer eventDispatcher(final Channel channel, final FrameEncoder.PayloadAllocator allocator) { return eventMessage -> flush(new FlushItem.Framed(channel, - eventMessage.encode(version), + eventMessage.encode(version, EventMessage.EVENT_MESSAGE_STREAM_ID), // -1 was set in EventMessage previously null, allocator, - f -> f.response.release())); + f -> f.responseEnvelope.release())); } } diff --git a/src/java/org/apache/cassandra/transport/Envelope.java b/src/java/org/apache/cassandra/transport/Envelope.java index 99c6e135afe6..e209b7a28546 100644 --- a/src/java/org/apache/cassandra/transport/Envelope.java +++ b/src/java/org/apache/cassandra/transport/Envelope.java @@ -141,6 +141,11 @@ public static class Header public final Message.Type type; public final long bodySizeInBytes; + public static Header dummy(int streamId, Message.Type type) + { + return new Header(ProtocolVersion.CURRENT, Flag.deserialize(0), streamId, type, 0); + } + private Header(ProtocolVersion version, EnumSet flags, int streamId, Message.Type type, long bodySizeInBytes) { this.version = version; @@ -242,7 +247,7 @@ HeaderExtractionResult extractHeader(ByteBuffer buffer) // This throws a protocol exception if the version number is unsupported, // the opcode is unknown or invalid flags are set for the version version = ProtocolVersion.decode(versionNum, DatabaseDescriptor.getNativeTransportAllowOlderProtocols()); - decodedFlags = decodeFlags(version, flags); + decodedFlags = decodeFlags(version, flags, streamId); type = Message.Type.fromOpcode(opcode, direction); return new HeaderExtractionResult.Success(new Header(version, decodedFlags, streamId, type, bodyLength)); } @@ -256,6 +261,10 @@ HeaderExtractionResult extractHeader(ByteBuffer buffer) // cause the channel to be closed. return new HeaderExtractionResult.Error(e, streamId, bodyLength); } + catch (ErrorMessage.WrappedException e) + { + return new HeaderExtractionResult.Error((ProtocolException) e.getCause(), e.getStreamId(), bodyLength); + } } public static abstract class HeaderExtractionResult @@ -354,7 +363,8 @@ Envelope decode(ByteBuf buffer) Message.Direction direction = Message.Direction.extractFromVersion(firstByte); int versionNum = firstByte & PROTOCOL_VERSION_MASK; - ProtocolVersion version; + ProtocolVersion version = null; + ProtocolException protocolException = null; try { @@ -362,19 +372,43 @@ Envelope decode(ByteBuf buffer) } catch (ProtocolException e) { - // Skip the remaining useless bytes. Otherwise the channel closing logic may try to decode again. - buffer.skipBytes(readableBytes); - throw e; + // defer throw to attempt to extract the stream id + protocolException = e; } // Wait until we have the complete header if (readableBytes < Header.LENGTH) + { + if (protocolException != null) + { + // Skip the remaining useless bytes. Otherwise the channel closing logic may try to decode again. + buffer.skipBytes(readableBytes); + // The header is incomplete, so there is no stream id to recover. Wrap with the unset + // sentinel; the channel-level exception handler sees it has no routable stream id and + // closes the connection rather than emit an unroutable error frame. + throw protocolException; + } return null; + } int flags = buffer.getByte(idx++); - EnumSet decodedFlags = decodeFlags(version, flags); - int streamId = buffer.getShort(idx); + + if (protocolException != null) + { + // Protocol versions 1 and 2 use a shorter header with a single-byte stream id. Reading a + // 16-bit stream id from such a header splices the stream id byte together with the opcode + // byte and recovers a bogus id, routing the error to a stream the client never used + // (CASSANDRA-21508). A v1/v2 client that downgrades would then never see the error and time + // out, so recover the stream id using the attempted version's header layout. + int recoveredStreamId = versionNum < ProtocolVersion.V3.asInt() ? buffer.getByte(idx) : streamId; + // Skip the remaining useless bytes. Otherwise the channel closing logic may try to decode again. + buffer.skipBytes(readableBytes); + throw ErrorMessage.wrap(protocolException, recoveredStreamId); + } + + EnumSet decodedFlags = decodeFlags(version, flags, streamId); + idx += 2; // This throws a protocol exceptions if the opcode is unknown @@ -420,13 +454,14 @@ Envelope decode(ByteBuf buffer) return new Envelope(new Header(version, decodedFlags, streamId, type, bodyLength), body); } - private EnumSet decodeFlags(ProtocolVersion version, int flags) + private EnumSet decodeFlags(ProtocolVersion version, int flags, int streamId) { EnumSet decodedFlags = Header.Flag.deserialize(flags); if (version.isBeta() && !decodedFlags.contains(Header.Flag.USE_BETA)) - throw new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version), - version); + throw ErrorMessage.wrap(new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version), + version), + streamId); return decodedFlags; } diff --git a/src/java/org/apache/cassandra/transport/Event.java b/src/java/org/apache/cassandra/transport/Event.java index 5e8e201d9a4d..c26fcc7d03c9 100644 --- a/src/java/org/apache/cassandra/transport/Event.java +++ b/src/java/org/apache/cassandra/transport/Event.java @@ -20,10 +20,12 @@ import java.net.InetSocketAddress; import java.util.Iterator; import java.util.List; +import java.util.function.UnaryOperator; import com.google.common.base.Objects; import io.netty.buffer.ByteBuf; +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.functions.UDAggregate; import org.apache.cassandra.cql3.functions.UDFunction; import org.apache.cassandra.locator.InetAddressAndPort; @@ -270,6 +272,15 @@ public SchemaChange(Change change, String keyspace) this(change, Target.KEYSPACE, keyspace, null); } + public SchemaChange withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + if (keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) + return this; + + String newKeyspaceName = keyspaceMapper.apply(keyspace); + return keyspace.equals(newKeyspaceName) ? this : new SchemaChange(change, target, newKeyspaceName, name, argTypes); + } + public static SchemaChange forFunction(Change change, UDFunction function) { return new SchemaChange(change, Target.FUNCTION, function.name().keyspace, function.name().name, function.argumentsList()); diff --git a/src/java/org/apache/cassandra/transport/ExceptionHandlers.java b/src/java/org/apache/cassandra/transport/ExceptionHandlers.java index 4d36fa6cd78c..bcca354712c3 100644 --- a/src/java/org/apache/cassandra/transport/ExceptionHandlers.java +++ b/src/java/org/apache/cassandra/transport/ExceptionHandlers.java @@ -42,6 +42,7 @@ import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.net.FrameEncoder; import org.apache.cassandra.transport.messages.ErrorMessage; +import org.apache.cassandra.transport.messages.ErrorMessage.WithStreamId; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Throwables; @@ -74,23 +75,53 @@ public void exceptionCaught(final ChannelHandlerContext ctx, Throwable cause) if (ctx.channel().isOpen()) { Predicate handler = getUnexpectedExceptionHandler(ctx.channel(), false); - ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler); - Envelope response = errorMessage.encode(version); - FrameEncoder.Payload payload = allocator.allocate(true, CQLMessageHandler.envelopeSize(response.header)); + // No request in scope at the channel level; a WrappedException cause carries the frame's + // stream id and overrides this fallback. + WithStreamId withStreamId = ErrorMessage.fromException(cause, handler); + ErrorMessage errorMessage = withStreamId.message; try { - response.encodeInto(payload.buffer); - response.release(); - payload.finish(); - ChannelPromise promise = ctx.newPromise(); - // On protocol exception, close the channel as soon as the message has been sent - if (isFatal(cause)) - promise.addListener(future -> ctx.close()); - ctx.writeAndFlush(payload, promise); + int streamId = withStreamId.streamId; + boolean isFatal = isFatal(cause); + if (streamId == Message.UNSET_STREAM_ID) + { + // No stream id could be recovered, so we have no request to route a response to. + // Close the connection rather than emit an unroutable frame (CASSANDRA-21508). + isFatal = true; + streamId = 0; + } + + Envelope response = errorMessage.encode(version, streamId); + FrameEncoder.Payload payload = allocator.allocate(true, CQLMessageHandler.envelopeSize(response.header)); + try + { + response.encodeInto(payload.buffer); + response.release(); + payload.finish(); + ChannelPromise promise = ctx.newPromise(); + // On a fatal error, close the channel only once the error frame has been written, + // so the client receives the diagnostic before the connection is torn down. Closing + // synchronously here can abort the in-flight flush and drop the frame when the socket + // can't drain it immediately (TCP backpressure, TLS buffering, or a large frame). + // Matches PreV5Handlers.ExceptionHandler and InitialConnectionHandler. + // + // Trade-off of deferring the close (CASSANDRA-21508): + // - There is a slim chance we send two frames with the same streamId. Responses + // already queued on this connection will have a chance to flush before the close + // fires. For the majority of cases, each frame will carry its own unique stream + // id, so nothing is misrouted. These are valid responses to requests that were + // correctly-decoded earlier. + if (isFatal) + promise.addListener(future -> ctx.close()); + ctx.writeAndFlush(payload, promise); + } + finally + { + payload.release(); + } } finally { - payload.release(); JVMStabilityInspector.inspectThrowable(cause); } } diff --git a/src/java/org/apache/cassandra/transport/Flusher.java b/src/java/org/apache/cassandra/transport/Flusher.java index 50261de0368a..cfa09c0146f9 100644 --- a/src/java/org/apache/cassandra/transport/Flusher.java +++ b/src/java/org/apache/cassandra/transport/Flusher.java @@ -35,7 +35,6 @@ import org.apache.cassandra.net.FrameEncoder; import org.apache.cassandra.net.FrameEncoderCrc; import org.apache.cassandra.net.FrameEncoderLZ4; -import org.apache.cassandra.transport.Message.Response; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.memory.BufferPool; @@ -49,22 +48,22 @@ abstract class Flusher implements Runnable Math.min(BufferPool.NORMAL_CHUNK_SIZE, FrameEncoder.Payload.MAX_SIZE - Math.max(FrameEncoderCrc.HEADER_AND_TRAILER_LENGTH, FrameEncoderLZ4.HEADER_AND_TRAILER_LENGTH)); - static class FlushItem + public static class FlushItem { enum Kind {FRAMED, UNFRAMED} final Kind kind; final Channel channel; - final T response; - final Envelope request; + final T responseEnvelope; + final Envelope requestEnvelope; final Consumer> tidy; - FlushItem(Kind kind, Channel channel, T response, Envelope request, Consumer> tidy) + FlushItem(Kind kind, Channel channel, T responseEnvelope, Envelope requestEnvelope, Consumer> tidy) { this.kind = kind; this.channel = channel; - this.request = request; - this.response = response; + this.requestEnvelope = requestEnvelope; + this.responseEnvelope = responseEnvelope; this.tidy = tidy; } @@ -77,21 +76,21 @@ static class Framed extends FlushItem { final FrameEncoder.PayloadAllocator allocator; Framed(Channel channel, - Envelope response, - Envelope request, + Envelope responseEnvelope, + Envelope requestEnvelope, FrameEncoder.PayloadAllocator allocator, Consumer> tidy) { - super(Kind.FRAMED, channel, response, request, tidy); + super(Kind.FRAMED, channel, responseEnvelope, requestEnvelope, tidy); this.allocator = allocator; } } - static class Unframed extends FlushItem + static class Unframed extends FlushItem { - Unframed(Channel channel, Response response, Envelope request, Consumer> tidy) + Unframed(Channel channel, Envelope responseEnvelope, Envelope requestEnvelope, Consumer> tidy) { - super(Kind.UNFRAMED, channel, response, request, tidy); + super(Kind.UNFRAMED, channel, responseEnvelope, requestEnvelope, tidy); } } } @@ -143,13 +142,13 @@ boolean isEmpty() private void processUnframedResponse(FlushItem.Unframed flush) { - flush.channel.write(flush.response, flush.channel.voidPromise()); + flush.channel.write(flush.responseEnvelope, flush.channel.voidPromise()); channels.add(flush.channel); } private void processFramedResponse(FlushItem.Framed flush) { - Envelope outbound = flush.response; + Envelope outbound = flush.responseEnvelope; if (envelopeSize(outbound.header) >= MAX_FRAMED_PAYLOAD_SIZE) { flushLargeMessage(flush.channel, outbound, flush.allocator); @@ -157,7 +156,7 @@ private void processFramedResponse(FlushItem.Framed flush) else { payloads.computeIfAbsent(flush.channel, channel -> new FlushBuffer(channel, flush.allocator, 5)) - .add(flush.response); + .add(flush.responseEnvelope); } } diff --git a/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java b/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java index 463d824c4c0a..9103b659e6a0 100644 --- a/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java +++ b/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java @@ -25,11 +25,11 @@ import java.util.List; import java.util.Map; -import org.apache.cassandra.transport.ClientResourceLimits.Overload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.VoidChannelPromise; @@ -90,8 +90,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List li supportedOptions.put(StartupMessage.COMPRESSION, compressions); supportedOptions.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions()); SupportedMessage supported = new SupportedMessage(supportedOptions); - supported.setStreamId(inbound.header.streamId); - outbound = supported.encode(inbound.header.version); + outbound = supported.encode(inbound.header.version, inbound.header.streamId); ctx.writeAndFlush(outbound); break; @@ -130,8 +129,8 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List li if (null == cause) cause = new ServerError("Unexpected error establishing connection"); logger.warn("Writing response to STARTUP failed, unable to configure pipeline", cause); - ErrorMessage error = ErrorMessage.fromException(cause); - Envelope response = error.encode(inbound.header.version); + ErrorMessage error = ErrorMessage.fromExceptionNoStreamId(cause); + Envelope response = error.encode(inbound.header.version, inbound.header.streamId); ChannelPromise closeChannel = AsyncChannelPromise.withListener(ctx, f -> ctx.close()); ctx.writeAndFlush(response, closeChannel); if (ctx.channel().isOpen()) @@ -149,20 +148,32 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List li promise = new VoidChannelPromise(ctx.channel(), false); } - final Message.Response response = Dispatcher.processRequest(ctx.channel(), startup, Overload.NONE, Dispatcher.RequestTime.forImmediateExecution()); - - outbound = response.encode(inbound.header.version); - ctx.writeAndFlush(outbound, promise); - logger.trace("Configured pipeline: {}", ctx.pipeline()); + ProtocolVersion version = inbound.header.version; + int streamId = inbound.header.streamId; + try + { + Message.Response response = Dispatcher.processInit((ServerConnection) connection, startup); + Envelope encoded = response.encode(version, streamId); + ctx.writeAndFlush(encoded, promise); + logger.debug("Configured pipeline: {}", ctx.pipeline()); + } + catch (Throwable error) + { + ErrorMessage message = ErrorMessage.fromExceptionNoStreamId(new ProtocolException(String.format("Unexpected error %s", error.getMessage()))); + Envelope encoded = message.encode(version, streamId); + ctx.writeAndFlush(encoded); + } break; default: ErrorMessage error = - ErrorMessage.fromException( + ErrorMessage.fromTransportException( new ProtocolException(String.format("Unexpected message %s, expecting STARTUP or OPTIONS", inbound.header.type))); - outbound = error.encode(inbound.header.version); - ctx.writeAndFlush(outbound); + outbound = error.encode(inbound.header.version, inbound.header.streamId); + // An unexpected message during initial connection setup leaves the connection in a + // corrupted state; send the error, then close the connection. + ctx.writeAndFlush(outbound).addListener(ChannelFutureListener.CLOSE); } } finally diff --git a/src/java/org/apache/cassandra/transport/Message.java b/src/java/org/apache/cassandra/transport/Message.java index ed853c0cbd7c..e6b3b969ea49 100644 --- a/src/java/org/apache/cassandra/transport/Message.java +++ b/src/java/org/apache/cassandra/transport/Message.java @@ -23,22 +23,32 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import com.google.common.annotations.VisibleForTesting; import io.netty.buffer.ByteBuf; import io.netty.channel.*; +import org.apache.cassandra.utils.Closeable; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.concurrent.ExecutorLocals; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.exceptions.OverloadedException; +import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.messages.*; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MonotonicClock; import org.apache.cassandra.utils.ReflectionUtils; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.concurrent.Future; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; @@ -49,6 +59,12 @@ public abstract class Message { protected static final Logger logger = LoggerFactory.getLogger(Message.class); + /** + * Sentinel default for a {@link Response}'s stream id; + * must be overwritten before {@link #encode}. + **/ + public static final int UNSET_STREAM_ID = Integer.MIN_VALUE; + public interface Codec extends CBCodec {} public enum Direction @@ -164,17 +180,6 @@ public Connection connection() return connection; } - public Message setStreamId(int streamId) - { - this.streamId = streamId; - return this; - } - - public int getStreamId() - { - return streamId; - } - public void setSource(Envelope source) { this.source = source; @@ -195,6 +200,17 @@ public void setCustomPayload(Map customPayload) this.customPayload = customPayload; } + public Message setStreamId(int streamId) + { + this.streamId = streamId; + return this; + } + + public int getStreamId() + { + return streamId; + } + @Override public String toString() { @@ -231,10 +247,32 @@ protected boolean isTrackable() return false; } - protected abstract Response execute(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest); + protected abstract Future maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest); + + /** + * Returns the time elapsed since this request was created. Note that this is the total lifetime of the request + * in the system, so we expect increasing returned values across multiple calls to elapsedTimeSinceCreation. + * + * @param timeUnit the time unit in which to return the elapsed time + * @return the time elapsed since this request was created + */ + protected long elapsedTimeSinceCreation(TimeUnit timeUnit) + { + return timeUnit.convert(MonotonicClock.Global.preciseTime.now() - createdAtNanos, TimeUnit.NANOSECONDS); + } + - public final Response execute(QueryState queryState, Dispatcher.RequestTime requestTime) + public final Future execute(QueryState queryState, Dispatcher.RequestTime requestTime) { + // at the time of the check, this is approximately the time spent in the NTR stage's queue + long elapsedTimeSinceCreation = elapsedTimeSinceCreation(TimeUnit.NANOSECONDS); + ClientMetrics.instance.recordQueueTime(elapsedTimeSinceCreation, TimeUnit.NANOSECONDS); + if (elapsedTimeSinceCreation > DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)) + { + ClientMetrics.instance.markTimedOutBeforeProcessing(); + return ImmediateFuture.success(ErrorMessage.fromExceptionNoStreamId(new OverloadedException("Query timed out before it could start"))); + } + boolean shouldTrace = false; TimeUUID tracingSessionId = null; @@ -244,33 +282,41 @@ public final Response execute(QueryState queryState, Dispatcher.RequestTime requ { shouldTrace = true; tracingSessionId = nextTimeUUID(); - Tracing.instance.newSession(tracingSessionId, getCustomPayload()); + Tracing.instance.newSession(queryState.getClientState(), tracingSessionId, getCustomPayload()); } else if (StorageService.instance.shouldTraceProbablistically()) { shouldTrace = true; - Tracing.instance.newSession(getCustomPayload()); + Tracing.instance.newSession(queryState.getClientState(), getCustomPayload()); } } - Response response; - try - { - response = execute(queryState, requestTime, shouldTrace); - } - finally - { - if (shouldTrace) - Tracing.instance.stopSession(); - } - - if (isTraceable() && isTracingRequested()) - response.setTracingId(tracingSessionId); - - return response; + Tracing.trace("Initialized tracing in execute. Already elapsed {} ns", (Clock.Global.nanoTime() - requestTime.startedAtNanos())); + boolean finalShouldTrace = shouldTrace; + TimeUUID finalTracingSessionId = tracingSessionId; + + // Capture ExecutorLocals containing thread-local state (TraceState, ClientWarn, RequestSensors, + // OperationContext) before async execution and restore in the callback, which may be on a different thread. + ExecutorLocals executorLocals = ExecutorLocals.current(); + return maybeExecuteAsync(queryState, requestTime, shouldTrace) + .addCallback((result, ignored) -> { + try (Closeable close = executorLocals.get()) + { + if (finalShouldTrace) + Tracing.instance.stopSession(); + + if (result != null && isTraceable() && isTracingRequested()) + result.setTracingId(finalTracingSessionId); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + logger.error("Error in tracing cleanup", t); + } + }); } - void setTracingRequested() + public void setTracingRequested() { tracingRequested = true; } @@ -303,13 +349,13 @@ protected Response(Type type) throw new IllegalArgumentException(); } - Message setTracingId(TimeUUID tracingId) + public Message setTracingId(TimeUUID tracingId) { this.tracingId = tracingId; return this; } - TimeUUID getTracingId() + public TimeUUID getTracingId() { return tracingId; } @@ -326,8 +372,16 @@ public List getWarnings() } } - public Envelope encode(ProtocolVersion version) + public Envelope encode(ProtocolVersion version, int streamId) { + // A Response's stream id must be stamped before it is serialized to the wire. UNSET_STREAM_ID here + // means a server code path produced a response without routing information; sending it would risk + // delivering it to an unrelated in-flight request (CASSANDRA-21508). Fail fatally so the connection + // is torn down rather than mis-route a response. Checked before the try below so it is not caught and + // re-wrapped (which would carry the unset id forward). + if (streamId == UNSET_STREAM_ID) + throw ProtocolException.toFatalException(new ProtocolException("Attempted to encode a response with an unset stream id: " + this)); + EnumSet flags = EnumSet.noneOf(Envelope.Header.Flag.class); @SuppressWarnings("unchecked") Codec codec = (Codec)this.type.codec; @@ -414,11 +468,11 @@ public Envelope encode(ProtocolVersion version) if (responseVersion.isBeta()) flags.add(Envelope.Header.Flag.USE_BETA); - return Envelope.create(type, getStreamId(), responseVersion, flags, body); + return Envelope.create(type, streamId, responseVersion, flags, body); } catch (Throwable e) { - throw ErrorMessage.wrap(e, getStreamId()); + throw ErrorMessage.wrap(e, streamId); } } @@ -439,7 +493,6 @@ static Message decodeMessage(Channel channel, Envelope inbound) throw new ProtocolException("Received frame with CUSTOM_PAYLOAD flag for native protocol version < 4"); Message message = inbound.header.type.codec.decode(inbound.body, inbound.header.version); - message.setStreamId(inbound.header.streamId); message.setSource(inbound); message.setCustomPayload(customPayload); diff --git a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java index 15e6a2432cab..859c813227b1 100644 --- a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java +++ b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java @@ -144,7 +144,7 @@ public ChannelFuture initializeChannel(final EventLoopGroup workerGroup, bootstrap.childHandler(initializer); // Bind and start to accept incoming connections. - logger.info("Using Netty Version: {}", Version.identify().entrySet()); + logger.debug("Using Netty Version: {}", Version.identify().entrySet()); logger.info("Starting listening for CQL clients on {} ({})...", socket, tlsEncryptionPolicy.description()); return bootstrap.bind(socket); } @@ -392,7 +392,7 @@ public void configureLegacyPipeline(ChannelHandlerContext ctx, ClientResourceLim pipeline.addBefore(INITIAL_HANDLER, MESSAGE_DECOMPRESSOR, Envelope.Decompressor.instance); pipeline.addBefore(INITIAL_HANDLER, MESSAGE_COMPRESSOR, Envelope.Compressor.instance); pipeline.addBefore(INITIAL_HANDLER, MESSAGE_DECODER, PreV5Handlers.ProtocolDecoder.instance); - pipeline.addBefore(INITIAL_HANDLER, MESSAGE_ENCODER, PreV5Handlers.ProtocolEncoder.instance); + pipeline.addBefore(INITIAL_HANDLER, MESSAGE_ENCODER, PreV5Handlers.EventMessageEncoder.instance); pipeline.addBefore(INITIAL_HANDLER, LEGACY_MESSAGE_PROCESSOR, new PreV5Handlers.LegacyDispatchHandler(dispatcher, queueBackpressure, limits)); pipeline.remove(INITIAL_HANDLER); onNegotiationComplete(pipeline); diff --git a/src/java/org/apache/cassandra/transport/PreV5Handlers.java b/src/java/org/apache/cassandra/transport/PreV5Handlers.java index d8c2067f5b49..be8b3326ed74 100644 --- a/src/java/org/apache/cassandra/transport/PreV5Handlers.java +++ b/src/java/org/apache/cassandra/transport/PreV5Handlers.java @@ -20,6 +20,7 @@ import java.util.List; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +41,7 @@ import org.apache.cassandra.net.ResourceLimits; import org.apache.cassandra.transport.ClientResourceLimits.Overload; import org.apache.cassandra.transport.messages.ErrorMessage; +import org.apache.cassandra.transport.messages.EventMessage; import org.apache.cassandra.utils.JVMStabilityInspector; import static org.apache.cassandra.transport.CQLMessageHandler.RATE_LIMITER_DELAY_UNIT; @@ -82,23 +84,26 @@ protected void channelRead0(ChannelHandlerContext ctx, Message.Request request) // The only reason we won't process this message is if checkLimits() throws an OverloadedException. // (i.e. Even if backpressure is applied, the current request is allowed to finish.) checkLimits(ctx, request); - dispatcher.dispatch(ctx.channel(), request, this::toFlushItem, backpressure); + dispatcher.dispatch(ctx.channel(), request, this::toFlushItem, ctx, backpressure); } // Acts as a Dispatcher.FlushItemConverter - private Flusher.FlushItem.Unframed toFlushItem(Channel channel, Message.Request request, Message.Response response) + private Flusher.FlushItem.Unframed toFlushItem(ChannelHandlerContext ctx, Channel channel, Message.Request request, Message.Response response) { - return new Flusher.FlushItem.Unframed(channel, response, request.getSource(), this::releaseItem); + ProtocolVersion version = getConnectionVersion(ctx); + Envelope requestEnvelope = request.getSource(); + Envelope responseEnvelope = response.encode(version, requestEnvelope.header.streamId); + return new Flusher.FlushItem.Unframed(channel, responseEnvelope, requestEnvelope, this::releaseItem); } - private void releaseItem(Flusher.FlushItem item) + private void releaseItem(Flusher.FlushItem item) { // Note: in contrast to the equivalent for V5 protocol, CQLMessageHandler::release(FlushItem item), // this does not release the FlushItem's Message.Response. In V4, the buffers for the response's body // and serialised header are emitted directly down the Netty pipeline from Envelope.Encoder, so // releasing them is handled by the pipeline itself. - long itemSize = item.request.header.bodySizeInBytes; - item.request.release(); + long itemSize = item.requestEnvelope.header.bodySizeInBytes; + item.requestEnvelope.release(); // since the request has been processed, decrement inflight payload at channel, endpoint and global levels channelPayloadBytesInFlight -= itemSize; @@ -296,14 +301,14 @@ public void decode(ChannelHandlerContext ctx, Envelope source, List resu * Simple adaptor to plug CQL message encoding into pre-V5 pipelines */ @ChannelHandler.Sharable - public static class ProtocolEncoder extends MessageToMessageEncoder + public static class EventMessageEncoder extends MessageToMessageEncoder { - public static final ProtocolEncoder instance = new ProtocolEncoder(); - private ProtocolEncoder(){} - public void encode(ChannelHandlerContext ctx, Message source, List results) + public static final EventMessageEncoder instance = new EventMessageEncoder(); + private EventMessageEncoder(){} + public void encode(ChannelHandlerContext ctx, EventMessage source, List results) { ProtocolVersion version = getConnectionVersion(ctx); - results.add(source.encode(version)); + results.add(source.encode(version, EventMessage.EVENT_MESSAGE_STREAM_ID)); } } @@ -326,13 +331,28 @@ public void exceptionCaught(final ChannelHandlerContext ctx, Throwable cause) if (ctx.channel().isOpen()) { Predicate handler = ExceptionHandlers.getUnexpectedExceptionHandler(ctx.channel(), false); - ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler); - ChannelFuture future = ctx.writeAndFlush(errorMessage.encode(getConnectionVersion(ctx))); - // On protocol exception, close the channel as soon as the message have been sent. - // Most cases of PE are wrapped so the type check below is expected to fail more often than not. - // At this moment Fatal exceptions are not thrown in v4, but just as a precaustion we check for them here - if (isFatal(cause)) - future.addListener((ChannelFutureListener) f -> ctx.close()); + // No request in scope at the channel level; a WrappedException cause carries the frame's + // stream id and overrides this fallback. + ErrorMessage.WithStreamId withStreamId = ErrorMessage.fromException(cause, handler); + + if (withStreamId.streamId == Message.UNSET_STREAM_ID) + { + // No stream id could be recovered, so we have no request to route a response to. + // Close the connection rather than emit an unroutable frame (CASSANDRA-21508). + ctx.close(); + } + else + { + ErrorMessage errorMessage = withStreamId.message; + int streamId = withStreamId.streamId; + + ChannelFuture future = ctx.writeAndFlush(errorMessage.encode(getConnectionVersion(ctx), streamId)); + // On protocol exception, close the channel as soon as the message have been sent. + // Most cases of PE are wrapped so the type check below is expected to fail more often than not. + // At this moment Fatal exceptions are not thrown in v4, but just as a precaustion we check for them here + if (isFatal(cause)) + future.addListener((ChannelFutureListener) f -> ctx.close()); + } } if (DatabaseDescriptor.getClientErrorReportingExclusions().contains(ctx.channel().remoteAddress())) @@ -354,7 +374,8 @@ private static boolean isFatal(Throwable cause) } } - private static ProtocolVersion getConnectionVersion(ChannelHandlerContext ctx) + @VisibleForTesting + static ProtocolVersion getConnectionVersion(ChannelHandlerContext ctx) { Connection connection = ctx.channel().attr(Connection.attributeKey).get(); // The only case the connection can be null is when we send the initial STARTUP message diff --git a/src/java/org/apache/cassandra/transport/Server.java b/src/java/org/apache/cassandra/transport/Server.java index 6abaf72515e6..b6ed49428511 100644 --- a/src/java/org/apache/cassandra/transport/Server.java +++ b/src/java/org/apache/cassandra/transport/Server.java @@ -19,7 +19,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -409,19 +408,9 @@ private void registerConnectionTracker(ConnectionTracker connectionTracker) this.connectionTracker = connectionTracker; } - private InetAddressAndPort getNativeAddress(InetAddressAndPort endpoint) + protected InetAddressAndPort getNativeAddress(InetAddressAndPort endpoint) { - try - { - return InetAddressAndPort.getByName(StorageService.instance.getNativeaddress(endpoint, true)); - } - catch (UnknownHostException e) - { - // That should not happen, so log an error, but return the - // endpoint address since there's a good change this is right - logger.error("Problem retrieving RPC address for {}", endpoint, e); - return InetAddressAndPort.getByAddressOverrideDefaults(endpoint.getAddress(), DatabaseDescriptor.getNativeTransportPort()); - } + return StorageService.instance.getNativeAddressAndPort(endpoint); } private void send(InetAddressAndPort endpoint, Event.NodeEvent event) diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java b/src/java/org/apache/cassandra/transport/SimpleClient.java index d81765cb96a0..58593d09d6cc 100644 --- a/src/java/org/apache/cassandra/transport/SimpleClient.java +++ b/src/java/org/apache/cassandra/transport/SimpleClient.java @@ -318,7 +318,7 @@ public Map execute(List requ for (int i = 0; i < requests.size(); i++) { Message.Request message = requests.get(i); - message.setStreamId(i); + message.setSource(new Envelope(Envelope.Header.dummy(i, message.type), null)); message.attach(connection); } lastWriteFuture = channel.writeAndFlush(requests); @@ -331,7 +331,7 @@ public Map execute(List requ throw new RuntimeException("timeout"); if (msg instanceof ErrorMessage) throw new RuntimeException((Throwable) ((ErrorMessage) msg).error); - rrMap.put(requests.get(msg.getStreamId()), msg); + rrMap.put(requests.get(msg.getSource().header.streamId), msg); } } else @@ -349,6 +349,18 @@ public Map execute(List requ } } + /** + * The stream id to frame an outbound client request with. SimpleClient carries the intended id on the + * request's (dummy) source envelope (see {@link #execute(List)} and callers that pipeline requests). + * When no source has been assigned, we fall back to 0, which is sufficient for the non-pipelined path + * where only a single request is ever in flight. + */ + private static int outboundStreamId(Message message) + { + Envelope source = message.getSource(); + return source == null ? 0 : source.header.streamId; + } + public interface EventHandler { void onEvent(Event event); @@ -404,36 +416,37 @@ private static class InitialHandler extends MessageToMessageDecoder this.largeMessageThreshold = largeMessageThreshold; } - protected void decode(ChannelHandlerContext ctx, Envelope response, List results) + @Override + protected void decode(ChannelHandlerContext ctx, Envelope request, List results) { - switch(response.header.type) + switch(request.header.type) { case READY: case AUTHENTICATE: - if (response.header.version.isGreaterOrEqualTo(ProtocolVersion.V5)) + if (request.header.version.isGreaterOrEqualTo(ProtocolVersion.V5)) { - configureModernPipeline(ctx, response, largeMessageThreshold); + configureModernPipeline(ctx, request, largeMessageThreshold); // consuming the message is done when setting up the pipeline } else { configureLegacyPipeline(ctx); // really just removes self from the pipeline, so pass this message on - ctx.pipeline().context(Envelope.Decoder.class).fireChannelRead(response); + ctx.pipeline().context(Envelope.Decoder.class).fireChannelRead(request); } break; case SUPPORTED: // just pass through - results.add(response); + results.add(request); break; default: - throw new ProtocolException(String.format("Unexpected %s response expecting " + + throw new ProtocolException(String.format("Unexpected %s request expecting " + "READY, AUTHENTICATE or SUPPORTED", - response.header.type)); + request.header.type)); } } - private void configureModernPipeline(ChannelHandlerContext ctx, Envelope response, int largeMessageThreshold) + private void configureModernPipeline(ChannelHandlerContext ctx, Envelope request, int largeMessageThreshold) { logger.info("Configuring modern pipeline"); ChannelPipeline pipeline = ctx.pipeline(); @@ -455,7 +468,7 @@ private void configureModernPipeline(ChannelHandlerContext ctx, Envelope respons CQLMessageHandler.MessageConsumer responseConsumer = new CQLMessageHandler.MessageConsumer() { - public void dispatch(Channel channel, Message.Response message, Dispatcher.FlushItemConverter toFlushItem, Overload backpressure) + public

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

    toFlushItem, P param, Overload backpressure) { responseHandler.handleResponse(channel, message); } @@ -550,15 +563,15 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion(); SimpleFlusher flusher = new SimpleFlusher(frameEncoder, largeMessageThreshold); for (Message message : (List) msg) - flusher.enqueue(message.encode(version)); + flusher.enqueue(message.encode(version, outboundStreamId(message))); flusher.maybeWrite(ctx, promise); } }); pipeline.remove(this); - Message.Response message = messageDecoder.decode(ctx.channel(), response); - responseConsumer.dispatch(channel, message, (ch, req, resp) -> null, Overload.NONE); + Message.Response message = messageDecoder.decode(ctx.channel(), request); + responseConsumer.dispatch(channel, message, (p, ch, req, resp) -> null, null, Overload.NONE); } private FrameDecoder frameDecoder(ChannelHandlerContext ctx, BufferPoolAllocator allocator) @@ -603,7 +616,8 @@ public void encode(ChannelHandlerContext ctx, List messages, List maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) + { + return ImmediateFuture.success(executeSync(queryState, requestTime, traceRequest)); + } + + private Response executeSync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) { try { @@ -93,7 +100,7 @@ protected Response execute(QueryState queryState, Dispatcher.RequestTime request { ClientMetrics.instance.markAuthFailure(); AuthEvents.instance.notifyAuthFailure(queryState, e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromTransportException(e); } } } diff --git a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java index d45105f109ce..7a9a146d4493 100644 --- a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java @@ -20,10 +20,14 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; import com.google.common.collect.ImmutableMap; import io.netty.buffer.ByteBuf; +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.Attributes; import org.apache.cassandra.cql3.BatchQueryOptions; import org.apache.cassandra.cql3.CQLStatement; @@ -35,19 +39,23 @@ import org.apache.cassandra.cql3.statements.BatchStatement; import org.apache.cassandra.cql3.statements.ModificationStatement; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.OverloadedException; import org.apache.cassandra.exceptions.PreparedQueryNotFoundException; +import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.CBUtil; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.Message; import org.apache.cassandra.transport.ProtocolException; import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MD5Digest; - -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; public class BatchMessage extends Message.Request { @@ -170,7 +178,7 @@ protected boolean isTrackable() } @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + public Future maybeExecuteAsync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { List prepared = null; try @@ -192,9 +200,9 @@ protected Message.Response execute(QueryState state, Dispatcher.RequestTime requ } else { - p = handler.getPrepared((MD5Digest)query); + p = handler.getPrepared((MD5Digest) query); if (null == p) - throw new PreparedQueryNotFoundException((MD5Digest)query); + throw new PreparedQueryNotFoundException((MD5Digest) query); } List queryValues = values.get(i); @@ -213,7 +221,7 @@ protected Message.Response execute(QueryState state, Dispatcher.RequestTime requ { CQLStatement statement = prepared.get(i).statement; if (queries != null) - queries.add(prepared.get(i).rawCQLStatement); + queries.add(statement.getRawCQLStatement()); batchOptions.prepareStatement(i, statement.getBindVariables()); if (!(statement instanceof ModificationStatement)) @@ -224,20 +232,88 @@ protected Message.Response execute(QueryState state, Dispatcher.RequestTime requ // Note: It's ok at this point to pass a bogus value for the number of bound terms in the BatchState ctor // (and no value would be really correct, so we prefer passing a clearly wrong one). - BatchStatement batch = new BatchStatement(batchType, VariableSpecifications.empty(), statements, Attributes.none()); + BatchStatement batch = new BatchStatement(null, batchType, + VariableSpecifications.empty(), statements, Attributes.none()); + + Tracing.trace("Processing batch start"); + long requestStartMillisTime = Clock.Global.currentTimeMillis(); + Optional asyncStage = Stage.fromStatement(batch); + if (asyncStage.isPresent()) + { + // Execution will continue on a new thread. Dispatcher.processRequest calls CoordinatorWarnings.init() + // and CoordinatorWarnings.done() on the NTR thread. For async execution, warnings are collected on the + // async stage thread, so we must also call CoordinatorWarnings.init()/done() there. The NTR-thread + // done() call will see an empty STATE (no warnings collected on NTR thread) and is harmless. + // See CNDB-13432 and CNDB-10759. + List finalPrepared = prepared; + return asyncStage.get().submit(() -> + { + Response response; + try + { + if (isTrackable()) + CoordinatorWarnings.init(); + + // at the time of the check, this includes the time spent in the NTR queue, basic query parsing/set up, + // and any time spent in the queue for the async stage + long elapsedTime = elapsedTimeSinceCreation(TimeUnit.NANOSECONDS); + ClientMetrics.instance.recordAsyncQueueTime(elapsedTime, TimeUnit.NANOSECONDS); + if (elapsedTime > DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)) + { + ClientMetrics.instance.markTimedOutBeforeAsyncProcessing(); + throw new OverloadedException("Query timed out before it could start"); + } + response = handleRequest(state, requestTime, handler, batch, batchOptions, queries, statements, finalPrepared, requestStartMillisTime); + } + catch (Exception e) + { + response = handleException(state, finalPrepared, e); + } + finally + { + if (isTrackable()) + { + CoordinatorWarnings.done(); + CoordinatorWarnings.reset(); + } + } + return response; + }); + } + else + return ImmediateFuture.success(handleRequest(state, requestTime, handler, batch, batchOptions, queries, statements, prepared, requestStartMillisTime)); + } + catch (Exception e) + { + return ImmediateFuture.success(handleException(state, prepared, e)); + } + } - long queryTime = currentTimeMillis(); - Message.Response response = handler.processBatch(batch, state, batchOptions, getCustomPayload(), requestTime); + private Response handleRequest(QueryState queryState, Dispatcher.RequestTime requestTime, QueryHandler queryHandler, BatchStatement batch, BatchQueryOptions batchOptions, List queries, List statements, List preparedList, long requestStartMillisTime) + { + try + { + Response response = queryHandler.processBatch(batch, queryState, batchOptions, getCustomPayload(), requestTime); if (queries != null) - QueryEvents.instance.notifyBatchSuccess(batchType, statements, queries, values, options, state, queryTime, response); + QueryEvents.instance.notifyBatchSuccess(batchType, statements, queries, values, options, queryState, requestStartMillisTime, response); + return response; } - catch (Exception e) + catch (Exception exception) { - QueryEvents.instance.notifyBatchFailure(prepared, batchType, queryOrIdList, values, options, state, e); - JVMStabilityInspector.inspectThrowable(e); - return ErrorMessage.fromException(e); + return handleException(queryState, preparedList, exception); } + finally + { + Tracing.trace("Processing batch complete"); + } + } + + private ErrorMessage handleException(QueryState state, List prepared, Exception e) + { + QueryEvents.instance.notifyBatchFailure(prepared, batchType, queryOrIdList, values, options, state, e); + JVMStabilityInspector.inspectThrowable(e); + return ErrorMessage.fromExceptionNoStreamId(e); } private void traceQuery(QueryState state) @@ -245,8 +321,8 @@ private void traceQuery(QueryState state) ImmutableMap.Builder builder = ImmutableMap.builder(); if (options.getConsistency() != null) builder.put("consistency_level", options.getConsistency().name()); - if (options.getSerialConsistency() != null) - builder.put("serial_consistency_level", options.getSerialConsistency().name()); + if (options.getSerialConsistency(state) != null) + builder.put("serial_consistency_level", options.getSerialConsistency(state).name()); // TODO we don't have [typed] access to CQL bind variables here. CASSANDRA-4560 is open to add support. Tracing.instance.begin("Execute batch of CQL3 queries", state.getClientAddress(), builder.build()); diff --git a/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java b/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java index 5d29d3afd402..39a98eb20907 100644 --- a/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java @@ -394,25 +394,49 @@ private ErrorMessage(TransportException error) this.error = error; } - private ErrorMessage(TransportException error, int streamId) + public static ErrorMessage fromTransportException(TransportException e) { - this(error); - setStreamId(streamId); + ErrorMessage message = new ErrorMessage(e); + if (e instanceof ProtocolException) + { + // if the driver attempted to connect with a protocol version not supported then + // respond with the appropiate version, see ProtocolVersion.decode() + ProtocolVersion forcedProtocolVersion = ((ProtocolException) e).getForcedProtocolVersion(); + if (forcedProtocolVersion != null) + message.forcedProtocolVersion = forcedProtocolVersion; + } + return message; } - public static ErrorMessage fromException(Throwable e) + public static ErrorMessage fromExceptionNoStreamId(Throwable e) { - return fromException(e, null); + return fromExceptionNoStreamId(e, null); } + public static ErrorMessage fromExceptionNoStreamId(Throwable e, Predicate unexpectedExceptionHandler) + { + return fromException(e, unexpectedExceptionHandler).message; + } + + public static class WithStreamId + { + public final ErrorMessage message; + public final int streamId; + + WithStreamId(ErrorMessage message, int streamId) + { + this.message = message; + this.streamId = streamId; + } + } /** * @param e the exception * @param unexpectedExceptionHandler a callback for handling unexpected exceptions. If null, or if this * returns false, the error is logged at ERROR level via sl4fj */ - public static ErrorMessage fromException(Throwable e, Predicate unexpectedExceptionHandler) + public static WithStreamId fromException(Throwable e, Predicate unexpectedExceptionHandler) { - int streamId = 0; + int streamId = UNSET_STREAM_ID; // Netty will wrap exceptions during decoding in a CodecException. If the cause was one of our ProtocolExceptions // or some other internal exception, extract that and use it. @@ -440,23 +464,15 @@ else if (e instanceof WrappedException) if (e instanceof TransportException) { - ErrorMessage message = new ErrorMessage((TransportException) e, streamId); - if (e instanceof ProtocolException) - { - // if the driver attempted to connect with a protocol version not supported then - // respond with the appropiate version, see ProtocolVersion.decode() - ProtocolVersion forcedProtocolVersion = ((ProtocolException) e).getForcedProtocolVersion(); - if (forcedProtocolVersion != null) - message.forcedProtocolVersion = forcedProtocolVersion; - } - return message; + ErrorMessage message = fromTransportException((TransportException) e); + return new WithStreamId(message, streamId); } // Unexpected exception if (unexpectedExceptionHandler == null || !unexpectedExceptionHandler.apply(e)) logger.error("Unexpected exception during request", e); - return new ErrorMessage(new ServerError(e), streamId); + return new WithStreamId(new ErrorMessage(new ServerError(e)), streamId); } @Override diff --git a/src/java/org/apache/cassandra/transport/messages/EventMessage.java b/src/java/org/apache/cassandra/transport/messages/EventMessage.java index 0af9e143994c..4caef1b56c8d 100644 --- a/src/java/org/apache/cassandra/transport/messages/EventMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/EventMessage.java @@ -25,6 +25,8 @@ public class EventMessage extends Message.Response { + public static final int EVENT_MESSAGE_STREAM_ID = -1; + public static final Message.Codec codec = new Message.Codec() { public EventMessage decode(ByteBuf body, ProtocolVersion version) @@ -49,7 +51,6 @@ public EventMessage(Event event) { super(Message.Type.EVENT); this.event = event; - this.setStreamId(-1); } @Override diff --git a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java index 89b7e2a4a224..695a75ed5f36 100644 --- a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java @@ -19,11 +19,15 @@ import java.nio.ByteBuffer; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; import com.google.common.collect.ImmutableMap; import io.netty.buffer.ByteBuf; +import org.apache.cassandra.concurrent.ExecutorLocals; +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.QueryEvents; @@ -31,9 +35,12 @@ import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.ResultSet; import org.apache.cassandra.cql3.statements.BatchStatement; +import org.apache.cassandra.exceptions.OverloadedException; import org.apache.cassandra.exceptions.PreparedQueryNotFoundException; +import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.CBUtil; import org.apache.cassandra.transport.Dispatcher; @@ -41,11 +48,13 @@ import org.apache.cassandra.transport.ProtocolException; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.Closeable; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.MD5Digest; import org.apache.cassandra.utils.NoSpamLogger; - -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; public class ExecuteMessage extends Message.Request { @@ -128,7 +137,7 @@ protected boolean isTrackable() } @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + protected Future maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) { QueryHandler.Prepared prepared = null; try @@ -139,36 +148,100 @@ protected Message.Response execute(QueryState state, Dispatcher.RequestTime requ throw new PreparedQueryNotFoundException(statementId); if (!prepared.fullyQualified - && !Objects.equals(state.getClientState().getRawKeyspace(), prepared.keyspace) + && !Objects.equals(queryState.getClientState().getRawKeyspace(), prepared.keyspace) // We can not reliably detect inconsistencies for batches yet - && !(prepared.statement instanceof BatchStatement) - ) + && !(prepared.statement instanceof BatchStatement)) { - state.getClientState().warnAboutUseWithPreparedStatements(statementId, prepared.keyspace); + queryState.getClientState().warnAboutUseWithPreparedStatements(statementId, prepared.keyspace); String msg = String.format("Tried to execute a prepared unqalified statement on a keyspace it was not prepared on. " + - " Executing the resulting prepared statement will return unexpected results: %s (on keyspace %s, previously prepared on %s)", - statementId, state.getClientState().getRawKeyspace(), prepared.keyspace); + "Executing the resulting prepared statement will return unexpected results: %s (on keyspace %s, previously prepared on %s)", + statementId, queryState.getClientState().getRawKeyspace(), prepared.keyspace); nospam.error(msg); } CQLStatement statement = prepared.statement; options.prepare(statement.getBindVariables()); - if (options.getPageSize() == 0) + if (options.getPageSize().getSize() == 0) throw new ProtocolException("The page size cannot be 0"); if (traceRequest) - traceQuery(state, prepared); + traceQuery(queryState, prepared); - // Some custom QueryHandlers are interested by the bound names. We provide them this information + // Some custom QueryHandlers are interested in the bound names. We provide them this information // by wrapping the QueryOptions. QueryOptions queryOptions = QueryOptions.addColumnSpecifications(options, prepared.statement.getBindVariables()); - long requestStartTime = currentTimeMillis(); + Tracing.trace("Executing prepared message started"); + long requestStartMillisTime = Clock.Global.currentTimeMillis(); + Optional asyncStage = Stage.fromStatement(statement); + if (asyncStage.isPresent()) + { + // Execution will continue on a new thread. Dispatcher.processRequest calls CoordinatorWarnings.init() + // and CoordinatorWarnings.done() on the NTR thread. For async execution, warnings are collected on the + // async stage thread, so we must also call CoordinatorWarnings.init()/done() there. The NTR-thread + // done() call will see an empty STATE (no warnings collected on NTR thread) and is harmless. + // See CNDB-13432 and CNDB-10759. + // + // Capture ExecutorLocals (including ClientWarn.State) to propagate to the async stage thread + // so that warnings generated during query execution are properly captured. + ExecutorLocals executorLocals = ExecutorLocals.current(); + QueryHandler.Prepared finalPrepared = prepared; + return asyncStage.get().submit(() -> + { + // Restore ExecutorLocals on the async stage thread + try (Closeable ignored = executorLocals.get()) + { + Response response; + try + { + if (isTrackable()) + CoordinatorWarnings.init(); + + // at the time of the check, this includes the time spent in the NTR queue, basic query parsing/set up, + // and any time spent in the queue for the async stage + long elapsedTime = elapsedTimeSinceCreation(TimeUnit.NANOSECONDS); + ClientMetrics.instance.recordAsyncQueueTime(elapsedTime, TimeUnit.NANOSECONDS); + if (elapsedTime > DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)) + { + ClientMetrics.instance.markTimedOutBeforeAsyncProcessing(); + throw new OverloadedException("Query timed out before it could start"); + } + response = handleRequest(queryState, requestTime, handler, queryOptions, statement, finalPrepared, requestStartMillisTime); + } + catch (Exception e) + { + response = handleException(queryState, finalPrepared, e); + } + finally + { + if (isTrackable()) + { + CoordinatorWarnings.done(); + CoordinatorWarnings.reset(); + } + } + return response; + } + }); + } + else + return ImmediateFuture.success(handleRequest(queryState, requestTime, handler, queryOptions, statement, prepared, requestStartMillisTime)); + } + catch (Exception e) + { + return ImmediateFuture.success(handleException(queryState, prepared, e)); + } + } - Message.Response response = handler.processPrepared(statement, state, queryOptions, getCustomPayload(), requestTime); + private Response handleRequest(QueryState queryState, Dispatcher.RequestTime requestTime, QueryHandler queryHandler, QueryOptions queryOptions, CQLStatement statement, QueryHandler.Prepared prepared, long requestStartMillisTime) + { + try + { + Response response = queryHandler.processPrepared(statement, queryState, queryOptions, getCustomPayload(), requestTime); - QueryEvents.instance.notifyExecuteSuccess(prepared.statement, prepared.rawCQLStatement, options, state, requestStartTime, response); + QueryEvents.instance.notifyExecuteSuccess(prepared.statement, options, queryState, + requestStartMillisTime, response); if (response instanceof ResultMessage.Rows) { @@ -204,23 +277,35 @@ else if (options.skipMetadata()) } catch (Exception e) { - QueryEvents.instance.notifyExecuteFailure(prepared, options, state, e); - JVMStabilityInspector.inspectThrowable(e); - return ErrorMessage.fromException(e); + return handleException(queryState, prepared, e); + } + finally + { + Tracing.trace("Executing prepared message completed"); } } + private ErrorMessage handleException(QueryState queryState, QueryHandler.Prepared prepared, Exception e) + { + QueryEvents.instance.notifyExecuteFailure(prepared, options, queryState, e); + JVMStabilityInspector.inspectThrowable(e); + return ErrorMessage.fromExceptionNoStreamId(e); + } + private void traceQuery(QueryState state, QueryHandler.Prepared prepared) { ImmutableMap.Builder builder = ImmutableMap.builder(); - if (options.getPageSize() > 0) - builder.put("page_size", Integer.toString(options.getPageSize())); + if (options.getPageSize().isDefined()) + { + builder.put("page_size", Integer.toString(options.getPageSize().getSize())); + builder.put("page_size_unit", options.getPageSize().getUnit().name()); + } if (options.getConsistency() != null) builder.put("consistency_level", options.getConsistency().name()); - if (options.getSerialConsistency() != null) - builder.put("serial_consistency_level", options.getSerialConsistency().name()); + if (options.getSerialConsistency(state) != null) + builder.put("serial_consistency_level", options.getSerialConsistency(state).name()); - builder.put("query", prepared.rawCQLStatement); + builder.put("query", prepared.statement.getRawCQLStatement()); for (int i = 0; i < prepared.statement.getBindVariables().size(); i++) { diff --git a/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java b/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java index 1ed109db2eeb..f917563b5947 100644 --- a/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java @@ -18,24 +18,35 @@ package org.apache.cassandra.transport.messages; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import io.netty.buffer.ByteBuf; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.service.QueryState; import org.apache.cassandra.transport.Compressor; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.Message; import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.ProductType; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; /** * Message to indicate that the server is ready to receive requests. */ public class OptionsMessage extends Message.Request { + private static final List supportedPageUnits = Arrays.stream(PageSize.PageUnit.values()).map(PageSize.PageUnit::name).collect(Collectors.toList()); + public static final Message.Codec codec = new Message.Codec() { public OptionsMessage decode(ByteBuf body, ProtocolVersion version) @@ -59,7 +70,12 @@ public OptionsMessage() } @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + protected Future maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) + { + return ImmediateFuture.success(executeSync(queryState, requestTime, traceRequest)); + } + + private Message.Response executeSync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { List cqlVersions = new ArrayList(); cqlVersions.add(QueryProcessor.CQL_VERSION.toString()); @@ -74,6 +90,10 @@ protected Message.Response execute(QueryState state, Dispatcher.RequestTime requ supported.put(StartupMessage.CQL_VERSION, cqlVersions); supported.put(StartupMessage.COMPRESSION, compressions); supported.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions()); + supported.put(StartupMessage.EMULATE_DBAAS_DEFAULTS, Collections.singletonList(String.valueOf(DatabaseDescriptor.isEmulateDbaasDefaults()))); + supported.put(StartupMessage.PAGE_UNIT, supportedPageUnits); + supported.put(StartupMessage.SERVER_VERSION, Collections.singletonList(FBUtilities.getReleaseVersionString())); + supported.put(StartupMessage.PRODUCT_TYPE, Collections.singletonList(ProductType.getProduct().toString())); return new SupportedMessage(supported); } diff --git a/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java b/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java index bebea3cc5b2d..c890689f42c4 100644 --- a/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java @@ -36,6 +36,8 @@ import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -50,12 +52,13 @@ public PrepareMessage decode(ByteBuf body, ProtocolVersion version) { String query = CBUtil.readLongString(body); String keyspace = null; - if (version.isGreaterOrEqualTo(ProtocolVersion.V5)) { + if (version.isGreaterOrEqualTo(ProtocolVersion.V5)) + { // If flags grows, we may want to consider creating a PrepareOptions class with an internal codec // class that handles flags and options of the prepare message. Since there's only one right now, // we just take care of business here. - int flags = (int)body.readUnsignedInt(); + int flags = (int) body.readUnsignedInt(); if ((flags & 0x1) == 0x1) { keyspace = CBUtil.readString(body); @@ -73,8 +76,11 @@ public void encode(PrepareMessage msg, ByteBuf dest, ProtocolVersion version) { // If we have no keyspace, write out a 0-valued flag field. if (msg.keyspace == null) + { dest.writeInt(0x0); - else { + } + else + { dest.writeInt(0x1); CBUtil.writeAsciiString(msg.keyspace, dest); } @@ -115,7 +121,12 @@ protected boolean isTraceable() } @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + protected Future maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) + { + return ImmediateFuture.success(executeSync(queryState, requestTime, traceRequest)); + } + + private Message.Response executeSync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { try { @@ -133,7 +144,7 @@ protected Message.Response execute(QueryState state, Dispatcher.RequestTime requ { QueryEvents.instance.notifyPrepareFailure(null, query, state, e); JVMStabilityInspector.inspectThrowable(e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromExceptionNoStreamId(e); } } diff --git a/src/java/org/apache/cassandra/transport/messages/QueryMessage.java b/src/java/org/apache/cassandra/transport/messages/QueryMessage.java index 665d62a8cb08..8acb904475e4 100644 --- a/src/java/org/apache/cassandra/transport/messages/QueryMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/QueryMessage.java @@ -17,26 +17,37 @@ */ package org.apache.cassandra.transport.messages; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + import com.google.common.collect.ImmutableMap; import io.netty.buffer.ByteBuf; +import org.apache.cassandra.concurrent.ExecutorLocals; +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QueryEvents; import org.apache.cassandra.cql3.QueryHandler; import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.exceptions.OverloadedException; import org.apache.cassandra.exceptions.RequestExecutionException; import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.CBUtil; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.Message; import org.apache.cassandra.transport.ProtocolException; import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.Closeable; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.JVMStabilityInspector; - -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; /** * A CQL query @@ -99,49 +110,128 @@ protected boolean isTrackable() } @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + protected Future maybeExecuteAsync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { CQLStatement statement = null; try { - if (options.getPageSize() == 0) + if (options.getPageSize().getSize() == 0) throw new ProtocolException("The page size cannot be 0"); if (traceRequest) traceQuery(state); - long queryStartTime = currentTimeMillis(); + long requestStartMillisTime = Clock.Global.currentTimeMillis(); + Tracing.trace("Executing query started"); QueryHandler queryHandler = ClientState.getCQLQueryHandler(); statement = queryHandler.parse(query, state, options); - Message.Response response = queryHandler.process(statement, state, options, getCustomPayload(), requestTime); - QueryEvents.instance.notifyQuerySuccess(statement, query, options, state, queryStartTime, response); + + Optional asyncStage = Stage.fromStatement(statement); + if (asyncStage.isPresent()) + { + // Execution will continue on a new executor. Dispatcher.processRequest calls CoordinatorWarnings.init() + // and CoordinatorWarnings.done() on the NTR thread. For async execution, warnings are collected on the + // async stage thread, so we must also call CoordinatorWarnings.init()/done() there. The NTR-thread + // done() call will see an empty STATE (no warnings collected on NTR thread) and is harmless. + // See CNDB-13432 and CNDB-10759. + // + // Capture ExecutorLocals (including ClientWarn.State) to propagate to the async stage thread + // so that warnings generated during query execution are properly captured. + ExecutorLocals executorLocals = ExecutorLocals.current(); + CQLStatement finalStatement = statement; + return asyncStage.get().submit(() -> + { + // Restore ExecutorLocals on the async stage thread + try (Closeable ignored = executorLocals.get()) + { + Response response; + try + { + if (isTrackable()) + CoordinatorWarnings.init(); + + // at the time of the check, this includes the time spent in the NTR queue, basic query parsing/set up, + // and any time spent in the queue for the async stage + long elapsedTime = elapsedTimeSinceCreation(TimeUnit.NANOSECONDS); + ClientMetrics.instance.recordAsyncQueueTime(elapsedTime, TimeUnit.NANOSECONDS); + if (elapsedTime > DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)) + { + ClientMetrics.instance.markTimedOutBeforeAsyncProcessing(); + throw new OverloadedException("Query timed out before it could start"); + } + response = handleRequest(state, queryHandler, requestTime, finalStatement, requestStartMillisTime); + } + catch (Exception e) + { + response = handleException(state, finalStatement, e); + } + finally + { + if (isTrackable()) + { + CoordinatorWarnings.done(); + CoordinatorWarnings.reset(); + } + } + return response; + } + }); + } + else + return ImmediateFuture.success(handleRequest(state, queryHandler, requestTime, statement, requestStartMillisTime)); + } + catch (Exception exception) + { + return ImmediateFuture.success(handleException(state, statement, exception)); + } + } + + private Response handleRequest(QueryState queryState, QueryHandler queryHandler, Dispatcher.RequestTime requestTime, CQLStatement statement, long requestStartMillisTime) + { + try + { + Response response = queryHandler.process(statement, queryState, options, getCustomPayload(), requestTime); + QueryEvents.instance.notifyQuerySuccess(statement, query, options, queryState, requestStartMillisTime, response); if (options.skipMetadata() && response instanceof ResultMessage.Rows) - ((ResultMessage.Rows)response).result.metadata.setSkipMetadata(); + ((ResultMessage.Rows) response).result.metadata.setSkipMetadata(); return response; } - catch (Exception e) + catch (Exception ex) { - QueryEvents.instance.notifyQueryFailure(statement, query, options, state, e); - JVMStabilityInspector.inspectThrowable(e); - if (!((e instanceof RequestValidationException) || (e instanceof RequestExecutionException))) - logger.error("Unexpected error during query", e); - return ErrorMessage.fromException(e); + return handleException(queryState, statement, ex); + } + finally + { + Tracing.trace("Executing query completed"); } } + private ErrorMessage handleException(QueryState queryState, CQLStatement statement, Exception exception) + { + QueryEvents.instance.notifyQueryFailure(statement, query, options, queryState, exception); + JVMStabilityInspector.inspectThrowable(exception); + if (!((exception instanceof RequestValidationException) || (exception instanceof RequestExecutionException))) + logger.error("Unexpected error during query", exception); + + return ErrorMessage.fromExceptionNoStreamId(exception); + } + private void traceQuery(QueryState state) { ImmutableMap.Builder builder = ImmutableMap.builder(); builder.put("query", query); - if (options.getPageSize() > 0) - builder.put("page_size", Integer.toString(options.getPageSize())); + if (options.getPageSize().isDefined()) + { + builder.put("page_size", Integer.toString(options.getPageSize().getSize())); + builder.put("page_size_unit", options.getPageSize().getUnit().name()); + } if (options.getConsistency() != null) builder.put("consistency_level", options.getConsistency().name()); - if (options.getSerialConsistency() != null) - builder.put("serial_consistency_level", options.getSerialConsistency().name()); + if (options.getSerialConsistency(state) != null) + builder.put("serial_consistency_level", options.getSerialConsistency(state).name()); Tracing.instance.begin("Execute CQL3 query", state.getClientAddress(), builder.build()); } @@ -149,7 +239,7 @@ private void traceQuery(QueryState state) @Override public String toString() { - return String.format("QUERY %s [pageSize = %d] at consistency %s", + return String.format("QUERY %s [pageSize = %s] at consistency %s", query, options.getPageSize(), options.getConsistency()); } } diff --git a/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java b/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java index 83f9cac3160a..570344717818 100644 --- a/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java @@ -24,6 +24,8 @@ import org.apache.cassandra.service.QueryState; import org.apache.cassandra.transport.*; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; public class RegisterMessage extends Message.Request { @@ -63,7 +65,12 @@ public RegisterMessage(List eventTypes) } @Override - protected Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + protected Future maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) + { + return ImmediateFuture.success(executeSync(queryState, requestTime, traceRequest)); + } + + private Response executeSync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { assert connection instanceof ServerConnection; Connection.Tracker tracker = connection.getTracker(); diff --git a/src/java/org/apache/cassandra/transport/messages/ResultMessage.java b/src/java/org/apache/cassandra/transport/messages/ResultMessage.java index a8d8daec28bf..433479aec8a8 100644 --- a/src/java/org/apache/cassandra/transport/messages/ResultMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/ResultMessage.java @@ -18,15 +18,21 @@ package org.apache.cassandra.transport.messages; +import java.util.function.UnaryOperator; + import com.google.common.annotations.VisibleForTesting; import io.netty.buffer.ByteBuf; - +import org.apache.cassandra.cql3.Constants; import org.apache.cassandra.cql3.ResultSet; -import org.apache.cassandra.transport.*; +import org.apache.cassandra.transport.CBUtil; +import org.apache.cassandra.transport.Event; +import org.apache.cassandra.transport.Message; +import org.apache.cassandra.transport.ProtocolException; +import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.MD5Digest; -public abstract class ResultMessage extends Message.Response +public abstract class ResultMessage> extends Message.Response { public static final Message.Codec codec = new Message.Codec() { @@ -97,7 +103,12 @@ protected ResultMessage(Kind kind) this.kind = kind; } - public static class Void extends ResultMessage + public T withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + return (T) this; + } + + public static class Void extends ResultMessage { // Even though we have no specific information here, don't make a // singleton since as each message it has in fact a streamid and connection. @@ -131,7 +142,7 @@ public String toString() } } - public static class SetKeyspace extends ResultMessage + public static class SetKeyspace extends ResultMessage { public final String keyspace; @@ -167,9 +178,28 @@ public String toString() { return "RESULT set keyspace " + keyspace; } + + @Override + public SetKeyspace withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + if (keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) + return this; + + String newKeyspaceName = keyspaceMapper.apply(keyspace); + if (keyspace.equals(newKeyspaceName)) + return this; + + SetKeyspace r = new SetKeyspace(newKeyspaceName); + r.setWarnings(getWarnings()); + r.setCustomPayload(getCustomPayload()); + r.setSource(getSource()); + r.setStreamId(getStreamId()); + + return r; + } } - public static class Rows extends ResultMessage + public static class Rows extends ResultMessage { public static final Message.Codec subcodec = new Message.Codec() { @@ -206,9 +236,32 @@ public String toString() { return "ROWS " + result; } + + @Override + public Rows withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + if (keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) + return this; + + return withResultSet(result.withOverriddenKeyspace(keyspaceMapper)); + } + + public Rows withResultSet(ResultSet newResultSet) + { + if (newResultSet == result) + return this; + + Rows r = new Rows(newResultSet); + r.setWarnings(getWarnings()); + r.setCustomPayload(getCustomPayload()); + r.setSource(getSource()); + r.setStreamId(getStreamId()); + + return r; + } } - public static class Prepared extends ResultMessage + public static class Prepared extends ResultMessage { public static final Message.Codec subcodec = new Message.Codec() { @@ -283,6 +336,29 @@ public Prepared withResultMetadata(ResultSet.ResultMetadata resultMetadata) return new Prepared(statementId, resultMetadata.getResultMetadataId(), metadata, resultMetadata); } + @Override + public Prepared withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + if (keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) + return this; + + ResultSet.PreparedMetadata newPreparedMetadata = metadata.withOverriddenKeyspace(keyspaceMapper); + ResultSet.ResultMetadata newResultSetMetadata = resultMetadata.withOverriddenKeyspace(keyspaceMapper); + if (newPreparedMetadata == metadata && newResultSetMetadata == resultMetadata) + return this; + + Prepared r = new Prepared(statementId, + resultMetadataId, + newPreparedMetadata, + newResultSetMetadata); + r.setWarnings(getWarnings()); + r.setCustomPayload(getCustomPayload()); + r.setSource(getSource()); + r.setStreamId(getStreamId()); + + return r; + } + @Override public String toString() { @@ -290,7 +366,7 @@ public String toString() } } - public static class SchemaChange extends ResultMessage + public static class SchemaChange extends ResultMessage { public final Event.SchemaChange change; @@ -322,6 +398,25 @@ public int encodedSize(ResultMessage msg, ProtocolVersion version) } }; + @Override + public SchemaChange withOverriddenKeyspace(UnaryOperator keyspaceMapper) + { + if (keyspaceMapper == Constants.IDENTITY_STRING_MAPPER) + return this; + + Event.SchemaChange newEvent = change.withOverriddenKeyspace(keyspaceMapper); + if (change == newEvent) + return this; + + SchemaChange r = new SchemaChange(newEvent); + r.setWarnings(getWarnings()); + r.setCustomPayload(getCustomPayload()); + r.setSource(getSource()); + r.setStreamId(getStreamId()); + + return r; + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/transport/messages/StartupMessage.java b/src/java/org/apache/cassandra/transport/messages/StartupMessage.java index 2969009f448b..4f0d93c82b9d 100644 --- a/src/java/org/apache/cassandra/transport/messages/StartupMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/StartupMessage.java @@ -28,6 +28,8 @@ import org.apache.cassandra.service.QueryState; import org.apache.cassandra.transport.*; import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; /** * The initial message of the protocol. @@ -41,6 +43,10 @@ public class StartupMessage extends Message.Request public static final String DRIVER_NAME = "DRIVER_NAME"; public static final String DRIVER_VERSION = "DRIVER_VERSION"; public static final String THROW_ON_OVERLOAD = "THROW_ON_OVERLOAD"; + public static final String EMULATE_DBAAS_DEFAULTS = "EMULATE_DBAAS_DEFAULTS"; + public static final String PAGE_UNIT = "PAGE_UNIT"; + public static final String SERVER_VERSION = "SERVER_VERSION"; + public static final String PRODUCT_TYPE = "PRODUCT_TYPE"; public static final Message.Codec codec = new Message.Codec() { @@ -69,7 +75,12 @@ public StartupMessage(Map options) } @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + protected Future maybeExecuteAsync(QueryState queryState, Dispatcher.RequestTime requestTime, boolean traceRequest) + { + return ImmediateFuture.success(executeSync(queryState, requestTime, traceRequest)); + } + + private Message.Response executeSync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { String cqlVersion = options.get(CQL_VERSION); if (cqlVersion == null) diff --git a/src/java/org/apache/cassandra/triggers/TriggerExecutor.java b/src/java/org/apache/cassandra/triggers/TriggerExecutor.java index c76c6bd4b271..c2513d20912b 100644 --- a/src/java/org/apache/cassandra/triggers/TriggerExecutor.java +++ b/src/java/org/apache/cassandra/triggers/TriggerExecutor.java @@ -19,18 +19,26 @@ package org.apache.cassandra.triggers; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.cassandra.cql3.QueryProcessor; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.CounterMutation; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.exceptions.CassandraException; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.io.util.File; import org.apache.cassandra.schema.TableId; @@ -252,11 +260,26 @@ private List executeInternal(PartitionUpdate update) } } + public synchronized Class loadTriggerClass(String triggerClass) throws Exception + { + // Load without initialization so the type can be verified before the class's static initializer runs. + try + { + return FBUtilities.classForNameWithoutInitialization(triggerClass, "trigger", ITrigger.class, customClassLoader); + } + catch (ConfigurationException e) + { + if (e.getCause() instanceof ClassNotFoundException) + throw (ClassNotFoundException) e.getCause(); + throw e; + } + } + public synchronized ITrigger loadTriggerInstance(String triggerClass) throws Exception { // double check. if (cachedTriggers.get(triggerClass) != null) return cachedTriggers.get(triggerClass); - return (ITrigger) customClassLoader.loadClass(triggerClass).getConstructor().newInstance(); + return loadTriggerClass(triggerClass).getConstructor().newInstance(); } } diff --git a/src/java/org/apache/cassandra/utils/AbstractGuavaIterator.java b/src/java/org/apache/cassandra/utils/AbstractGuavaIterator.java index 00756df87779..5ae4d13a05fe 100644 --- a/src/java/org/apache/cassandra/utils/AbstractGuavaIterator.java +++ b/src/java/org/apache/cassandra/utils/AbstractGuavaIterator.java @@ -29,7 +29,7 @@ import static com.google.common.base.Preconditions.checkState; /** - * This is fork of the Guava AbstractIterator, the only difference + * This is fork of the Guava AbstractGuavaIterator, the only difference * is that the next variable is now protected so that the KeyRangeIterator.skipTo * method can avoid early state changed. */ @@ -151,7 +151,7 @@ public void remove() * Returns the next element in the iteration without advancing the iteration, * according to the contract of {@link PeekingIterator#peek()}. * - *

    Implementations of {@code AbstractIterator} that wish to expose this + *

    Implementations of {@code AbstractGuavaIterator} that wish to expose this * functionality should implement {@code PeekingIterator}. */ public final T peek() diff --git a/src/java/org/apache/cassandra/utils/BinaryHeap.java b/src/java/org/apache/cassandra/utils/BinaryHeap.java new file mode 100644 index 000000000000..26e8f1e0e188 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/BinaryHeap.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Comparator; + +import com.google.common.base.Preconditions; + +/** + * A base binary heap implementation with fixed size, supporting only operations that push + * data down in the heap (i.e. after the initial initialization of the heap from a collection + * of items, only top/smallest items can be modified (removed or replaced)). + *

    + * This class's purpose is to implement various sources of sorted entries, e.g. + * for merging iterators (see e.g. {@link TrieMemoryIndex.SortingSingletonOrSetIterator}, producing + * a sorted iterator from an unsorted list of items ({@link SortingIterator}) or selecting the + * top items from data of unbounded size ({@link TopKSelector}). + *

    + * As it does not support adding elements after the initial construction, the class does not + * implement a priority queue, where items need to be repeatedly added and removed. If a priority + * queue is required, consider using {@link LucenePriorityQueue}. + *

    + * By default, the implementation supports nulls among the source entries (by comparing them greater + * than all other elements and using null as a marker of completion) and achieves removal by + * replacing items with null. This adds slight overhead for simple comparators (e.g. ints), but + * significantly improves performance when the comparator is complex. + */ +public abstract class BinaryHeap +{ + // Note: This class is tested via its descendants by SortingIteratorTest and TopKSelectorTest. + + protected final Object[] heap; + + /** + * Create a binary heap with the given array. The data must be heapified before being used. + */ + protected BinaryHeap(Object[] data) + { + Preconditions.checkArgument(data.length > 0, "Binary heap needs at least one item."); + this.heap = data; + // Note that we can't perform any preparation here because the subclass defining greaterThan may have not been + // initialized yet. + } + + /** + * Compare two objects and return true iff the first is greater. + * The method must treat nulls as greater than non-null objects. + */ + protected abstract boolean greaterThan(Object a, Object b); + + /** + * Get the size. Usually just the heap length because we don't count removed elements, but some descendants may + * choose to control size differently. + */ + protected int size() + { + return heap.length; + } + + /** + * Advance an item. Return null if there are no further entries. + * The default implementations assumes entries are single items and always returns null. + * Override it to implement merging of sorted iterators. + * @param item The heap item to advance + */ + protected Object advanceItem(Object item) + { + return null; + } + + /** + * Advance an item to the closest entry greater than or equal to the target. + * Return null if no such entry exists. + * The default implementations assumes entries are single items and always returns null. + * Override it to implement merging of sorted seeking iterators. + * @param item The heap item to advance + * @param targetKey The comparison key + */ + protected Object advanceItemTo(Object item, Object targetKey) + { + return null; + } + + /** + * Turn the current list of items into a binary heap by using the initial heap construction + * of the heapsort algorithm with complexity O(size()). Done recursively to improve caching on + * larger heaps. + */ + protected void heapify() + { + heapifyRecursively(0, size()); + } + + protected boolean isEmpty() + { + return heap[0] == null; + } + + /** + * Return the next element in the heap without advancing. + */ + protected Object top() + { + return heap[0]; + } + + /** + * Get and remove the next element in the heap. + * If the heap contains duplicates, they will be returned in an arbitrary order. + */ + protected Object pop() + { + Object item = heap[0]; + heapifyDown(advanceItem(item), 0); + return item; + } + + /** + * Get and replace the top item with a new one. + */ + protected Object replaceTop(Object newItem) + { + Object item = heap[0]; + heapifyDown(newItem, 0); + return item; + } + + /** + * Get the next element and skip over all items equal to it. + * Calling this instead of {@link #pop} results in deduplication of the list + * of entries. + */ + protected Object popAndSkipEqual() + { + Object item = heap[0]; + advanceBeyond(item, item); + return item; + } + + protected void advanceBeyond(Object targetKey, Object topItem) + { + Object advanced = advanceItem(topItem); + // avoid recomparing top element + int size = size(); + if (1 < size) + { + if (2 < size) + applyAdvance(targetKey, 2, ADVANCE_BEYOND, size); + applyAdvance(targetKey, 1, ADVANCE_BEYOND, size); + } + heapifyDown(advanced, 0); + } + + /** + * Skip to the first element that is greater than or equal to the given key. + */ + protected void advanceTo(Object targetKey) + { + applyAdvance(targetKey, 0, ADVANCE_TO, size()); + } + + /** + * Interface used to specify an advancing operation for {@link #applyAdvance}. + */ + protected interface AdvanceOperation + { + /** + * Return true if the necessary condition is satisfied by this heap entry. + * The condition is assumed to also be satisfied for all descendants of the + * entry (as they are equal or greater). + */ + boolean shouldStop(BinaryHeap self, Object heapEntry, Object targetKey); + + /** + * Apply the relevant advancing operation and return the entry to use. + */ + Object advanceItem(BinaryHeap self, Object heapEntry, Object targetKey); + } + + static final AdvanceOperation ADVANCE_BEYOND = new AdvanceOperation() + { + @Override + public boolean shouldStop(BinaryHeap self, Object heapEntry, Object targetKey) + { + return self.greaterThan(heapEntry, targetKey); + } + + @Override + public Object advanceItem(BinaryHeap self, Object heapEntry, Object targetKey) + { + return self.advanceItem(heapEntry); + } + }; + + static final AdvanceOperation ADVANCE_TO = new AdvanceOperation() + { + @Override + public boolean shouldStop(BinaryHeap self, Object heapEntry, Object targetKey) + { + return !self.greaterThan(targetKey, heapEntry); + } + + @Override + public Object advanceItem(BinaryHeap self, Object heapEntry, Object targetKey) + { + return self.advanceItemTo(heapEntry, targetKey); + } + }; + + /** + * Recursively apply the advance operation to all elements in the subheap rooted at the given heapIndex + * that do not satisfy the shouldStop condition, and restore the heap ordering on the way back from the recursion. + */ + private void applyAdvance(Object targetKey, int heapIndex, AdvanceOperation advanceOperation, int size) + { + if (advanceOperation.shouldStop(this, heap[heapIndex], targetKey)) + return; + + if (heapIndex * 2 + 1 < size) + { + if (heapIndex * 2 + 2 < size) + applyAdvance(targetKey, heapIndex * 2 + 2, advanceOperation, size); + applyAdvance(targetKey, heapIndex * 2 + 1, advanceOperation, size); + + Object advanced = advanceOperation.advanceItem(this, heap[heapIndex], targetKey); + heapifyDown(advanced, heapIndex); + } + else + { + Object advanced = advanceOperation.advanceItem(this, heap[heapIndex], targetKey); + heap[heapIndex] = advanced; + } + } + + /** + * Perform the initial heapification of the data. This could be achieved with the method above (with shouldStop + * always false and advanceItem returning the item unchanged), but a direct implementation is much simpler and + * performs better. + */ + + private void heapifyRecursively(int heapIndex, int size) + { + if (heapIndex * 2 + 1 < size) + { + if (heapIndex * 2 + 2 < size) + heapifyRecursively(heapIndex * 2 + 2, size); + heapifyRecursively(heapIndex * 2 + 1, size); + + heapifyDown(heap[heapIndex], heapIndex); + } + } + + /** + * Push the given state down in the heap from the given index until it finds its proper place among + * the subheap rooted at that position. + */ + private void heapifyDown(Object item, int index) + { + heapifyDownUpTo(item, index, size()); + } + + /** + * Push the given state down in the heap from the given index until it finds its proper place among + * the subheap rooted at that position. + */ + private void heapifyDownUpTo(Object item, int index, int size) + { + while (true) + { + int next = index * 2 + 1; + if (next >= size) + break; + // Select the smaller of the two children to push down to. + if (next + 1 < size && greaterThan(heap[next], heap[next + 1])) + ++next; + // If the child is greater or equal, the invariant has been restored. + if (!greaterThan(item, heap[next])) + break; + heap[index] = heap[next]; + index = next; + } + heap[index] = item; + } + + /** + * Sort the heap by repeatedly popping the top item and placing it at the end of the heap array. + * The result will contain the elements in the heap sorted in descending order. + * The heap must be heapified before calling this method. + */ + protected void heapSort() + { + // Sorting the ones from 1 will also make put the right value in heap[0] + heapSortFrom(1); + } + + /** + * Partially sort the heap by repeatedly popping the top item and placing it at the end of the heap array, + * until the given start position is reached. This results in a partial sorting where the smallest items + * (according to the comparator) are placed at positions of the heap between start and size in descending order, + * and the items before that are left heapified. + * The heap must be heapified up to the size before calling this method. + * Used to fetch items after a certain offset in a top-k selection. + */ + protected void heapSortFrom(int start) + { + // Data must already be heapified up to that size, comparator must be reverse + for (int i = size() - 1; i >= start; --i) + { + Object top = heap[0]; + heapifyDownUpTo(heap[i], 0, i); + heap[i] = top; + } + } + + /** + * A binary heap that uses a comparator to determine the order of elements, implementing the necessary handling + * of nulls. + */ + public static class WithComparator extends BinaryHeap + { + final Comparator comparator; + + public WithComparator(Comparator comparator, Object[] data) + { + super(data); + this.comparator = comparator; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean greaterThan(Object a, Object b) + { + // nulls are treated as greater than non-nulls to be placed at the end of the sequence + if (a == null || b == null) + return b != null; + return comparator.compare((T) a, (T) b) > 0; + } + } + + /** + * Create a mermaid graph for the current state of the heap. Used to create visuals for documentation/slides. + */ + String toMermaid() + { + StringBuilder builder = new StringBuilder(); + builder.append("flowchart\n"); + int size = size(); + for (int i = 0; i < size; ++i) + builder.append(" s" + i + "(" + heap[i] + ")\n"); + builder.append("\n"); + for (int i = 0; i * 2 + 1 < size; ++i) + { + builder.append(" s" + i + " ---|<=| s" + (i * 2 + 1) + "\n"); + if (i * 2 + 2 < size) + builder.append(" s" + i + " ---|<=| s" + (i * 2 + 2) + "\n"); + } + return builder.toString(); + } +} diff --git a/src/java/org/apache/cassandra/utils/BloomFilter.java b/src/java/org/apache/cassandra/utils/BloomFilter.java index a95d131a3913..d718a9d75063 100644 --- a/src/java/org/apache/cassandra/utils/BloomFilter.java +++ b/src/java/org/apache/cassandra/utils/BloomFilter.java @@ -23,13 +23,28 @@ import io.netty.util.concurrent.FastThreadLocal; import net.nicoulaj.compilecommand.annotations.Inline; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.io.util.DataOutputStreamPlus; import org.apache.cassandra.utils.concurrent.Ref; import org.apache.cassandra.utils.concurrent.WrappedSharedCloseable; import org.apache.cassandra.utils.obs.IBitSet; +import org.apache.cassandra.utils.obs.MemoryLimiter; + +import static org.apache.cassandra.metrics.RestorableMeter.AVAILABLE_WINDOWS; public class BloomFilter extends WrappedSharedCloseable implements IFilter { + private static final long maxMemory = CassandraRelevantProperties.BF_MAX_MEMORY_MB.getLong() << 20; + + /** + * If true, Bloom filters ignore the memory limit during flush. CNDB uses this to avoid missing Bloom filters + * when reloading from remote storage. + */ + public static final String IGNORE_MEMORY_LIMIT_ON_FLUSH_PROP = CassandraRelevantProperties.BF_IGNORE_MEMORY_LIMIT_ON_FLUSH.getKey(); + + public static final MemoryLimiter memoryLimiter = new MemoryLimiter(maxMemory != 0 ? maxMemory : Long.MAX_VALUE, + "Allocating %s for Bloom filter would reach max of %s (current %s)"); + private final static FastThreadLocal reusableIndexes = new FastThreadLocal() { @Override @@ -56,6 +71,49 @@ private BloomFilter(BloomFilter copy) this.bitset = copy.bitset; } + /** + * @return true to ignore bloom filter memory limit during flush + */ + public static boolean ignoreMemoryLimitOnFlush() + { + return CassandraRelevantProperties.BF_IGNORE_MEMORY_LIMIT_ON_FLUSH.getBoolean(); + } + + /** + * @return true if sstable's bloom filter should be deserialized on read instead of when opening sstable. This + * doesn't affect flushed sstable because there is bloom filter deserialization + */ + public static boolean lazyLoading() + { + return CassandraRelevantProperties.BLOOM_FILTER_LAZY_LOADING.getBoolean(); + } + + /** + * @return sstable hits per second to determine if a sstable is hot. 0 means BF should be loaded immediately on read. + * + * Note that when WINDOW <= 0, this is used as absolute primary index access count. + */ + public static long lazyLoadingThreshold() + { + return CassandraRelevantProperties.BLOOM_FILTER_LAZY_LOADING_THRESHOLD.getInt(); + } + + /** + * @return Window of time by minute, available: 1 (default), 5, 15, 120. + * + * Note that if <= 0 then we use threshold as the absolute count + */ + public static int lazyLoadingWindow() + { + int window = CassandraRelevantProperties.BLOOM_FILTER_LAZY_LOADING_WINDOW.getInt(); + if (window >= 1 && !AVAILABLE_WINDOWS.contains(window)) + throw new IllegalArgumentException(String.format("Found invalid %s=%s, available windows: %s", + CassandraRelevantProperties.BLOOM_FILTER_LAZY_LOADING_WINDOW.getKey(), + window, + AVAILABLE_WINDOWS)); + return window; + } + public long serializedSize(boolean old) { return BloomFilterSerializer.forVersion(old).serializedSize(this); @@ -159,6 +217,12 @@ public boolean isInformative() return bitset.offHeapSize() > 0; } + @Override + public boolean isSerializable() + { + return true; + } + @Override public String toString() { @@ -171,4 +235,5 @@ public void addTo(Ref.IdentityCollection identities) super.addTo(identities); bitset.addTo(identities); } + } diff --git a/src/java/org/apache/cassandra/utils/BloomFilterSerializer.java b/src/java/org/apache/cassandra/utils/BloomFilterSerializer.java index 91ec13f53c52..4a3a2e8b7c7c 100644 --- a/src/java/org/apache/cassandra/utils/BloomFilterSerializer.java +++ b/src/java/org/apache/cassandra/utils/BloomFilterSerializer.java @@ -19,15 +19,20 @@ import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.io.IGenericSerializer; import org.apache.cassandra.io.util.DataInputPlus.DataInputStreamPlus; import org.apache.cassandra.io.util.DataOutputStreamPlus; import org.apache.cassandra.utils.obs.IBitSet; +import org.apache.cassandra.utils.obs.MemoryLimiter; import org.apache.cassandra.utils.obs.OffHeapBitSet; public final class BloomFilterSerializer implements IGenericSerializer { + private final static Logger logger = LoggerFactory.getLogger(BloomFilterSerializer.class); public final static BloomFilterSerializer newFormatInstance = new BloomFilterSerializer(false); public final static BloomFilterSerializer oldFormatInstance = new BloomFilterSerializer(true); @@ -64,16 +69,29 @@ public void serialize(BloomFilter bf, DataOutputStreamPlus out) throws IOExcepti @Override public long serializedSize(BloomFilter bf) { - int size = TypeSizes.sizeof(bf.hashCount); // hash count + long size = TypeSizes.sizeof(bf.hashCount); // hash count size += bf.bitset.serializedSize(); return size; } @Override public BloomFilter deserialize(DataInputStreamPlus in) throws IOException + { + return deserialize(in, BloomFilter.memoryLimiter); + } + + public BloomFilter deserialize(DataInputStreamPlus in, MemoryLimiter memoryLimiter) throws IOException { int hashes = in.readInt(); - IBitSet bs = OffHeapBitSet.deserialize(in, oldFormat); + IBitSet bs; + try + { + bs = OffHeapBitSet.deserialize(in, oldFormat, memoryLimiter); + } + catch (MemoryLimiter.ReachedMemoryLimitException | OutOfMemoryError e) + { + throw new RuntimeException("Out of native memory occured, You can avoid it by increasing the system ram space or by increasing bloom_filter_fp_chance."); + } return new BloomFilter(hashes, bs); } diff --git a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java index 4d3d0ca0f32c..ed48936077da 100644 --- a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java +++ b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java @@ -45,6 +45,7 @@ import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.io.util.FileUtils; /** @@ -313,6 +314,26 @@ public static void copyBytes(ByteBuffer src, int srcPos, ByteBuffer dst, int dst FastByteOperations.copy(src, srcPos, dst, dstPos, length); } + /** + * Transfer bytes from one ByteBuffer to another. + * This function acts as System.arrayCopy() but for ByteBuffers. + * + * @param src the source ByteBuffer + * @param srcPos starting position in the source ByteBuffer + * @param dst the destination ByteBuffer + * @param dstPos starting position in the destination ByteBuffer + * @param length the number of bytes to copy + */ + public static void arrayCopy(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int length) + { + FastByteOperations.copy(src, srcPos, dst, dstPos, length); + } + + public static void arrayCopy(ByteBuffer src, int srcPos, byte[] dst, int dstPos, int length) + { + FastByteOperations.copy(src, srcPos, dst, dstPos, length); + } + public static int put(ByteBuffer src, ByteBuffer trg) { int length = Math.min(src.remaining(), trg.remaining()); @@ -441,6 +462,28 @@ public static void skipShortLength(DataInputPlus in) throws IOException in.skipBytesFully(skip); } + /** + * Returns true if the buffer at the current position in the input matches given buffer. + * If true, the input is positioned at the end of the consumed buffer. + * If false, the position of the input is undefined. + *

    + * The matched buffer is unchanged + * + * @throws IOException + */ + public static boolean equalsWithShortLength(FileDataInput in, ByteBuffer toMatch) throws IOException + { + int length = readShortLength(in); + if (length != toMatch.remaining()) + return false; + int limit = toMatch.limit(); + for (int i = toMatch.position(); i < limit; ++i) + if (toMatch.get(i) != in.readByte()) + return false; + + return true; + } + public static ByteBuffer read(DataInput in, int length) throws IOException { if (length == 0) @@ -932,4 +975,23 @@ public static void readFully(FileChannel channel, ByteBuffer dst, long position) position += read; } } -} \ No newline at end of file + + /** + * Essentially the same as {@link #bytesToHex(ByteBuffer)} (though it prepends "0x" for clarity) but takes care of + * not output a string too long if the value is too big. This is to be used for error/debug message where we don't + * want to blow things up. + * + * @param bytes the bytes to convert to hexadecimal string. + * @return a string representation of {@code bytes} that may be only partial if {@code bytes} is too big. + */ + public static String toDebugHexString(ByteBuffer bytes) + { + int maxSize = 50; // kind of arbitrary tbh but that's not hugely important + if (bytes.remaining() > maxSize) + { + bytes = bytes.duplicate(); + bytes.limit(bytes.position() + maxSize); + } + return "0x" + bytesToHex(bytes); + } +} diff --git a/src/java/org/apache/cassandra/utils/Clock.java b/src/java/org/apache/cassandra/utils/Clock.java index c8ba785cab9f..3f72a959de56 100644 --- a/src/java/org/apache/cassandra/utils/Clock.java +++ b/src/java/org/apache/cassandra/utils/Clock.java @@ -60,7 +60,7 @@ public static class Global try { outcome = "Using custom clock implementation: " + classname; - clock = (Clock) Class.forName(classname).newInstance(); + clock = FBUtilities.construct(classname, "clock", Clock.class); } catch (Throwable t) { diff --git a/src/java/org/apache/cassandra/utils/CloseableIterator.java b/src/java/org/apache/cassandra/utils/CloseableIterator.java index 32de799ba93f..069095d6c8f2 100644 --- a/src/java/org/apache/cassandra/utils/CloseableIterator.java +++ b/src/java/org/apache/cassandra/utils/CloseableIterator.java @@ -17,54 +17,85 @@ */ package org.apache.cassandra.utils; +import java.io.Closeable; +import java.util.Collections; import java.util.Iterator; -import java.util.NoSuchElementException; + +import org.apache.cassandra.io.util.FileUtils; + // so we can instantiate anonymous classes implementing both interfaces public interface CloseableIterator extends Iterator, AutoCloseable { public void close(); - public static CloseableIterator wrap(Iterator iter) + CloseableIterator EMPTY = CloseableIterator.wrap(Collections.emptyIterator()); + + /** + * Returns an empty {@link CloseableIterator}. + */ + @SuppressWarnings("unchecked") + static CloseableIterator emptyIterator() { - return new CloseableIterator() + return (CloseableIterator) EMPTY; + } + + /** + * Wraps an {@link Iterator} making it a {@link CloseableIterator}. + */ + static CloseableIterator wrap(Iterator iterator) + { + return new CloseableIterator<>() { - public void close() + public boolean hasNext() { - // noop + return iterator.hasNext(); } - public boolean hasNext() + public T next() { - return iter.hasNext(); + return iterator.next(); } - public T next() + public void remove() + { + iterator.remove(); + } + + public void close() { - return iter.next(); } }; } - public static CloseableIterator empty() + /** + * Pairs a {@link CloseableIterator} and an {@link AutoCloseable} so that the latter is closed when the former is + * closed. + */ + static CloseableIterator withOnClose(CloseableIterator iterator, Closeable onClose) { - return new CloseableIterator() + return new CloseableIterator<>() { - public void close() + public boolean hasNext() { - // noop + return iterator.hasNext(); } - public boolean hasNext() + public T next() { - return false; + return iterator.next(); } - public T next() + public void remove() + { + iterator.remove(); + } + + public void close() { - throw new NoSuchElementException(); + iterator.close(); + FileUtils.closeQuietly(onClose); } }; } - } diff --git a/src/java/org/apache/cassandra/utils/Collections3.java b/src/java/org/apache/cassandra/utils/Collections3.java new file mode 100644 index 000000000000..3dd899b7f13d --- /dev/null +++ b/src/java/org/apache/cassandra/utils/Collections3.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Collection; + +import com.google.common.collect.ImmutableList; + +public class Collections3 +{ + public static ImmutableList withAppended(Iterable list, T... elements) + { + if (elements.length == 0) + return ImmutableList.copyOf(list); + + ImmutableList.Builder builder = list instanceof Collection + ? ImmutableList.builderWithExpectedSize(((Collection) list).size() + elements.length) + : ImmutableList.builder(); + builder.addAll(list); + for (T element : elements) + builder.add(element); + return builder.build(); + } +} diff --git a/src/java/org/apache/cassandra/utils/DseLegacy.java b/src/java/org/apache/cassandra/utils/DseLegacy.java new file mode 100644 index 000000000000..14c92b33d6c1 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/DseLegacy.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +/** + * used to mark DSE legacy interface + * which will be removed once we transition CNDB to CC core + */ +public @interface DseLegacy +{ +} diff --git a/src/java/org/apache/cassandra/utils/EstimatedHistogram.java b/src/java/org/apache/cassandra/utils/EstimatedHistogram.java index 198f92286f74..ca6f4a268d9f 100644 --- a/src/java/org/apache/cassandra/utils/EstimatedHistogram.java +++ b/src/java/org/apache/cassandra/utils/EstimatedHistogram.java @@ -89,6 +89,11 @@ public EstimatedHistogram(long[] offsets, long[] bucketData) } public static long[] newOffsets(int size, boolean considerZeroes) + { + return newCassandraOffsets(size, considerZeroes); + } + + public static long[] newCassandraOffsets(int size, boolean considerZeroes) { long[] result = new long[size + (considerZeroes ? 1 : 0)]; int i = 0; @@ -429,7 +434,7 @@ public EstimatedHistogram deserialize(DataInputPlus in) throws IOException public long serializedSize(EstimatedHistogram eh) { - int size = 0; + long size = 0; long[] offsets = eh.getBucketOffsets(); long[] buckets = eh.getBuckets(false); diff --git a/src/java/org/apache/cassandra/utils/ExpiringMemoizingSupplier.java b/src/java/org/apache/cassandra/utils/ExpiringMemoizingSupplier.java index 7cc13782e3c1..d261bf52bd67 100644 --- a/src/java/org/apache/cassandra/utils/ExpiringMemoizingSupplier.java +++ b/src/java/org/apache/cassandra/utils/ExpiringMemoizingSupplier.java @@ -41,7 +41,7 @@ public class ExpiringMemoizingSupplier implements Supplier // The special value 0 means "not yet initialized". transient volatile long expirationNanos; - public static Supplier memoizeWithExpiration(Supplier> delegate, long duration, TimeUnit unit) + public static ExpiringMemoizingSupplier memoizeWithExpiration(Supplier> delegate, long duration, TimeUnit unit) { return new ExpiringMemoizingSupplier<>(delegate, duration, unit); } diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java index ca3444d4654a..bd4b86bf6eea 100644 --- a/src/java/org/apache/cassandra/utils/FBUtilities.java +++ b/src/java/org/apache/cassandra/utils/FBUtilities.java @@ -212,9 +212,9 @@ public static InetAddressAndPort getLocalAddressAndPort() { if (localInetAddressAndPort == null) { - if(DatabaseDescriptor.getRawConfig() == null) + if (DatabaseDescriptor.getRawConfig() == null) { - localInetAddressAndPort = InetAddressAndPort.getByAddress(getJustLocalAddress()); + throw new AssertionError("Local address and port should never be accessed before initializing DatabaseDescriptor"); } else { @@ -249,7 +249,7 @@ public static InetAddressAndPort getBroadcastAddressAndPort() { if(DatabaseDescriptor.getRawConfig() == null) { - broadcastInetAddressAndPort = InetAddressAndPort.getByAddress(getJustBroadcastAddress()); + throw new AssertionError("Broadcast address and port should never be accessed before initializing DatabaseDescriptor"); } else { @@ -560,6 +560,29 @@ public static T waitOnFuture(Future future) } } + // Used in CNDB + public static T waitOnFuture(Future future, Duration timeout) + { + Preconditions.checkArgument(!timeout.isNegative(), "Timeout must not be negative, provided %s", timeout); + try + { + return future.get(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + catch (ExecutionException ee) + { + logger.info("Exception occurred in async code", ee); + throw Throwables.cleaned(ee); + } + catch (InterruptedException ie) + { + throw new AssertionError(ie); + } + catch (TimeoutException e) + { + throw new RuntimeException("Timeout - task did not finish in " + timeout, e); + } + } + public static > F waitOnFirstFuture(Iterable futures) { return waitOnFirstFuture(futures, 100); @@ -642,7 +665,7 @@ static IPartitioner newPartitioner(String partitionerClassName, Optional parameters) throws ConfigurationException @@ -652,8 +675,9 @@ public static IAuditLogger newAuditLogger(String className, Map try { - Class auditLoggerClass = FBUtilities.classForName(className, "Audit logger"); - return (IAuditLogger) auditLoggerClass.getConstructor(Map.class).newInstance(parameters); + Class auditLoggerClass = + FBUtilities.classForNameWithoutInitialization(className, "Audit logger", IAuditLogger.class); + return auditLoggerClass.getConstructor(Map.class).newInstance(parameters); } catch (Exception ex) { @@ -668,12 +692,16 @@ public static ISslContextFactory newSslContextFactory(String className, Map sslContextFactoryClass = Class.forName(className); - return (ISslContextFactory) sslContextFactoryClass.getConstructor(Map.class).newInstance(parameters); + Class sslContextFactoryClass = + FBUtilities.classForNameWithoutInitialization(className, "ISslContextFactory", ISslContextFactory.class); + return sslContextFactoryClass.getConstructor(Map.class).newInstance(parameters); } catch (Exception ex) { - throw new ConfigurationException("Unable to create instance of ISslContextFactory for " + className, ex); + // Surface the underlying load failure (e.g. ClassNotFoundException) as the direct cause rather than the + // intermediate ConfigurationException that reports it. + Throwable cause = ex instanceof ConfigurationException && ex.getCause() != null ? ex.getCause() : ex; + throw new ConfigurationException("Unable to create instance of ISslContextFactory for " + className, cause); } } @@ -684,8 +712,9 @@ public static AbstractCryptoProvider newCryptoProvider(String className, Map cryptoProviderClass = FBUtilities.classForName(className, "crypto provider class"); - return (AbstractCryptoProvider) cryptoProviderClass.getConstructor(Map.class).newInstance(Collections.unmodifiableMap(parameters)); + Class cryptoProviderClass = + FBUtilities.classForNameWithoutInitialization(className, "crypto provider class", AbstractCryptoProvider.class); + return cryptoProviderClass.getConstructor(Map.class).newInstance(Collections.unmodifiableMap(parameters)); } catch (Exception e) { @@ -698,6 +727,8 @@ public static AbstractCryptoProvider newCryptoProvider(String className, Map Class classForName(String classname, String readable) throw } } + /** + * Loads a class without initializing it, then verifies it extends or implements the expected base type. + * + * @return The Class for the given name. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static Class classForNameWithoutInitialization(String classname, + String readable, + Class expectedType) throws ConfigurationException + { + return classForNameWithoutInitialization(classname, readable, expectedType, FBUtilities.class.getClassLoader()); + } + + /** + * Loads a class without initializing it, then verifies it extends or implements the expected base type. + * + * @return The Class for the given name. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @param classLoader ClassLoader to use. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static Class classForNameWithoutInitialization(String classname, + String readable, + Class expectedType, + ClassLoader classLoader) throws ConfigurationException + { + try + { + Class klass = Class.forName(classname, false, classLoader); + if (!expectedType.isAssignableFrom(klass)) + throw new ConfigurationException(String.format("Invalid %s class '%s': must extend or implement %s", + readable, + classname, + expectedType.getName())); + return klass.asSubclass(expectedType); + } + catch (ClassNotFoundException | NoClassDefFoundError e) + { + throw new ConfigurationException(String.format("Unable to find %s class '%s'", readable, classname), e); + } + } + /** * Constructs an instance of the given class, which must have a no-arg or default constructor. * @param classname Fully qualified classname. @@ -724,6 +802,25 @@ public static Class classForName(String classname, String readable) throw public static T instanceOrConstruct(String classname, String readable) throws ConfigurationException { Class cls = FBUtilities.classForName(classname, readable); + return instanceOrConstruct(cls, classname, readable); + } + + /** + * Constructs an instance of the given class, or gets its static {@code instance} field, after verifying the + * class without initializing it. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static T instanceOrConstruct(String classname, String readable, Class expectedType) throws ConfigurationException + { + Class cls = FBUtilities.classForNameWithoutInitialization(classname, readable, expectedType); + return instanceOrConstruct(cls, classname, readable); + } + + private static T instanceOrConstruct(Class cls, String classname, String readable) throws ConfigurationException + { try { Field instance = cls.getField("instance"); @@ -748,7 +845,20 @@ public static T construct(String classname, String readable) throws Configur return construct(cls, classname, readable); } - private static T construct(Class cls, String classname, String readable) throws ConfigurationException + /** + * Constructs an instance of the given class after verifying it without initializing it. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static T construct(String classname, String readable, Class expectedType) throws ConfigurationException + { + Class cls = FBUtilities.classForNameWithoutInitialization(classname, readable, expectedType); + return construct(cls, classname, readable); + } + + private static T construct(Class cls, String classname, String readable) throws ConfigurationException { try { @@ -832,7 +942,8 @@ public static CloseableIterator closeableIterator(Iterator iterator) /** * Convert the given size in bytes to a human-readable value using binary (i.e. 2^10-based) modifiers. * For example, 1.000KiB, 2.100GiB etc., up to 8.000 EiB. - * @param size Number to convert. + * + * @param size Number to convert. */ public static String prettyPrintMemory(long size) { @@ -848,6 +959,7 @@ public static String prettyPrintMemory(long size) public static String prettyPrintMemory(long size, String separator) { int prefixIndex = (63 - Long.numberOfLeadingZeros(Math.abs(size))) / 10; + // Note: if size is 0 we get prefixIndex=0 because the division truncates towards 0 (i.e. -1/10 = 0). if (prefixIndex == 0) return String.format("%d%sB", size, separator); else @@ -886,9 +998,9 @@ else if (prefixIndex > UNIT_PREFIXES_BASE || prefixIndex < -UNIT_PREFIXES_BASE) /** * Convert the given value to a human-readable string using decimal (i.e. 10^3-based) modifiers. - * If the number is outside the modifier range (i.e. < 1 qi or > 1 Qi), it will be printed as vEe where e is a + * If the number is outside the modifier range (i.e. < 1 q or > 1 Q), it will be printed as vEe where e is a * multiple of 3 with sign. - * For example, 1.000km, 2.100 ms, 10E+45, NaN. + * For example, 1.000km, 215.100 ms, 10.000E+45, NaN. * @param value Number to convert. * @param separator Separator between the number and the (modified) unit. */ @@ -1020,6 +1132,17 @@ public static double parsePercent(String value) return Double.parseDouble(value); } + /** + * Parse an integer value, allowing the string "max" to mean Integer.MAX_VALUE. + */ + public static int parseIntAllowingMax(String value) + { + if (value.equalsIgnoreCase("max")) + return Integer.MAX_VALUE; + else + return Integer.parseInt(value); + } + /** * Starts and waits for the given @param pb to finish. * @throws java.io.IOException on non-zero exit code @@ -1415,4 +1538,73 @@ static Semver parseKernelVersion(String versionString) } throw new IllegalArgumentException("Error while trying to parse kernel version - no version found"); } -} \ No newline at end of file + + /** + * A class containing some debug methods to be added and removed manually when debugging problems + * like failing unit tests. + */ + public static final class Debug + { + public static final class ThreadInfo + { + private final String name; + private final boolean isDaemon; + private final StackTraceElement[] stack; + + public ThreadInfo() + { + this(Thread.currentThread()); + } + + public ThreadInfo(Thread thread) + { + this.name = thread.getName(); + this.isDaemon = thread.isDaemon(); + this.stack = thread.getStackTrace(); + } + + } + + public static String getStackTrace() + { + return getStackTrace(new ThreadInfo()); + } + + public static String getStackTrace(Thread thread) + { + return getStackTrace(new ThreadInfo(thread)); + } + + public static String getStackTrace(ThreadInfo threadInfo) + { + StringBuilder sb = new StringBuilder(); + sb.append("Thread ") + .append(threadInfo.name) + .append(" (") + .append(threadInfo.isDaemon ? "daemon" : "non-daemon") + .append(")") + .append("\n"); + for (StackTraceElement element : threadInfo.stack) + { + sb.append(element); + sb.append("\n"); + } + return sb.toString(); + } + } + + public static void busyWaitWhile(Supplier condition) + { + while (condition.get()) + { + try + { + Thread.sleep(1); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/java/org/apache/cassandra/utils/FilterFactory.java b/src/java/org/apache/cassandra/utils/FilterFactory.java index 50dbffb7d143..dbd26c81e590 100644 --- a/src/java/org/apache/cassandra/utils/FilterFactory.java +++ b/src/java/org/apache/cassandra/utils/FilterFactory.java @@ -19,17 +19,33 @@ import java.io.IOException; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.io.util.DataOutputStreamPlus; import org.apache.cassandra.utils.concurrent.Ref; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.metrics.DefaultNameFactory; +import org.apache.cassandra.metrics.MetricNameFactory; +import org.apache.cassandra.metrics.MicrometerMetrics; import org.apache.cassandra.utils.obs.IBitSet; +import org.apache.cassandra.utils.obs.MemoryLimiter; import org.apache.cassandra.utils.obs.OffHeapBitSet; +import static org.apache.cassandra.config.CassandraRelevantProperties.USE_MICROMETER; +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + public class FilterFactory { public static final IFilter AlwaysPresent = AlwaysPresentFilter.instance; + // marker for lazy bloom filter + public static final IFilter AlwaysPresentForLazyLoading = new AlwaysPresentFilter(); + + public static final FilterFactoryMetrics metrics = FilterFactoryMetrics.create(); private static final Logger logger = LoggerFactory.getLogger(FilterFactory.class); private static final long BITSET_EXCESS = 20; @@ -39,6 +55,11 @@ public class FilterFactory * probability for the given number of elements. */ public static IFilter getFilter(long numElements, int targetBucketsPerElem) + { + return getFilter(numElements, targetBucketsPerElem, BloomFilter.memoryLimiter); + } + + public static IFilter getFilter(long numElements, int targetBucketsPerElem, MemoryLimiter memoryLimiter) { int maxBucketsPerElement = Math.max(1, BloomCalculations.maxBucketsPerElement(numElements)); int bucketsPerElement = Math.min(targetBucketsPerElem, maxBucketsPerElement); @@ -47,31 +68,86 @@ public static IFilter getFilter(long numElements, int targetBucketsPerElem) logger.warn("Cannot provide an optimal BloomFilter for {} elements ({}/{} buckets per element).", numElements, bucketsPerElement, targetBucketsPerElem); } BloomCalculations.BloomSpecification spec = BloomCalculations.computeBloomSpec(bucketsPerElement); - return createFilter(spec.K, numElements, spec.bucketsPerElement); + return createFilter(spec.K, numElements, spec.bucketsPerElement, memoryLimiter, true); } /** * @return The smallest BloomFilter that can provide the given false - * positive probability rate for the given number of elements. - * - * Asserts that the given probability can be satisfied using this - * filter. + * positive probability rate for the given number of elements. + *

    + * Asserts that the given probability can be satisfied using this + * filter. */ + @VisibleForTesting public static IFilter getFilter(long numElements, double maxFalsePosProbability) { - assert maxFalsePosProbability <= 1.0 : "Invalid probability"; - if (maxFalsePosProbability == 1.0) + return getFilter(numElements, maxFalsePosProbability, BloomFilter.memoryLimiter); + } + + public static IFilter getFilter(long numElements, double maxFalsePosProbability, MemoryLimiter memoryLimiter) + { + return createFilter(numElements, maxFalsePosProbability, memoryLimiter, true); + } + + public static IFilter getFilterForWrite(long numElements, double maxFalsePosProbability, OperationType operationType) + { + return getFilterForWrite(numElements, maxFalsePosProbability, operationType, BloomFilter.memoryLimiter); + } + + @VisibleForTesting + static IFilter getFilterForWrite(long numElements, double maxFalsePosProbability, OperationType operationType, MemoryLimiter memoryLimiter) + { + boolean ignoreMemoryLimit = operationType == OperationType.FLUSH && BloomFilter.ignoreMemoryLimitOnFlush(); + return createFilter(numElements, maxFalsePosProbability, memoryLimiter, !ignoreMemoryLimit); + } + + private static IFilter createFilter(long numElements, double maxFalsePosProbability, MemoryLimiter memoryLimiter, boolean failOnExceedingLimit) + { + BloomCalculations.BloomSpecification spec = getBloomSpecification(numElements, maxFalsePosProbability); + if (spec == null) return FilterFactory.AlwaysPresent; - int bucketsPerElement = BloomCalculations.maxBucketsPerElement(numElements); - BloomCalculations.BloomSpecification spec = BloomCalculations.computeBloomSpec(bucketsPerElement, maxFalsePosProbability); - return createFilter(spec.K, numElements, spec.bucketsPerElement); + return createFilter(spec.K, numElements, spec.bucketsPerElement, memoryLimiter, failOnExceedingLimit); + } + + @SuppressWarnings("resource") + private static IFilter createFilter(int hash, long numElements, int bucketsPer, MemoryLimiter memoryLimiter, boolean failOnExceedingLimit) + { + try + { + long numBits = (numElements * bucketsPer) + BITSET_EXCESS; + IBitSet bitset = new OffHeapBitSet(numBits, memoryLimiter, failOnExceedingLimit); + return new BloomFilter(hash, bitset); + } + catch (MemoryLimiter.ReachedMemoryLimitException | OutOfMemoryError e) + { + logger.error("Failed to create new Bloom filter with {} elements: ({}) - " + + "continuing but this will have severe performance implications. Consider increasing FP chance " + + "(bloom_filter_fp_chance) or increasing system ram space or" + + "lowering number of sstables through compaction", numElements, e.getMessage()); + metrics.incrementOOMError(); + return AlwaysPresent; + } } - private static IFilter createFilter(int hash, long numElements, int bucketsPer) + public static long getFilterOffHeapSize(long numElements, double maxFalsePosProbability) { - long numBits = (numElements * bucketsPer) + BITSET_EXCESS; - IBitSet bitset = new OffHeapBitSet(numBits); - return new BloomFilter(hash, bitset); + BloomCalculations.BloomSpecification spec = getBloomSpecification(numElements, maxFalsePosProbability); + if (spec == null) + return 0; + + long numBits = (numElements * spec.bucketsPerElement) + BITSET_EXCESS; + long wordCount = (((numBits - 1) >>> 6) + 1); + return wordCount * 8L; + } + + private static BloomCalculations.BloomSpecification getBloomSpecification(long numElements, double maxFalsePosProbability) + { + assert maxFalsePosProbability <= 1.0 : "Invalid probability"; + if (maxFalsePosProbability == 1.0) + return null; + + int bucketsPerElement = BloomCalculations.maxBucketsPerElement(numElements); + return BloomCalculations.computeBloomSpec(bucketsPerElement, maxFalsePosProbability); } private static class AlwaysPresentFilter implements IFilter @@ -124,5 +200,84 @@ public boolean isInformative() { return false; } + + @Override + public boolean isSerializable() + { + return false; + } + + @Override + public String toString() + { + return "AlwaysPresentFilter"; + } + } + + public interface FilterFactoryMetrics + { + static FilterFactoryMetrics create() + { + return USE_MICROMETER.getBoolean() ? new FilterFactoryMicormeterMetrics() + : new FilterFactoryCodahaleMetrics(); + } + + void incrementOOMError(); + + long oomErrors(); + } + + /** + * Metrics exposed in Prometheus friendly format + */ + public static final class FilterFactoryMicormeterMetrics extends MicrometerMetrics implements FilterFactoryMetrics + { + public static final String METRICS_PREFIX = "bloom_filter"; + public static final String OOM_ERRORS = METRICS_PREFIX + "_oom_errors"; + private volatile Counter oomCounter; + public FilterFactoryMicormeterMetrics() + { + this.oomCounter = counter(OOM_ERRORS); + } + + @Override + public synchronized void register(MeterRegistry newRegistry, Tags newTags) + { + super.register(newRegistry, newTags); + this.oomCounter = counter(OOM_ERRORS); + } + + @Override + public void incrementOOMError() + { + oomCounter.increment(); + } + + @Override + public long oomErrors() + { + return (long) oomCounter.count(); + } + } + + /** + * Metrics exposed in Codahale format + */ + public static final class FilterFactoryCodahaleMetrics implements FilterFactoryMetrics + { + private static final MetricNameFactory metricNameFactory = new DefaultNameFactory("BloomFilter"); + private static final com.codahale.metrics.Counter oomCounter = Metrics.counter(metricNameFactory.createMetricName("OutOfMemory")); + + @Override + public void incrementOOMError() + { + oomCounter.inc(); + } + + @Override + public long oomErrors() + { + return oomCounter.getCount(); + } } } diff --git a/src/java/org/apache/cassandra/utils/Flags.java b/src/java/org/apache/cassandra/utils/Flags.java new file mode 100644 index 000000000000..0d2f22fcaa24 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/Flags.java @@ -0,0 +1,57 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +package org.apache.cassandra.utils; + +import net.nicoulaj.compilecommand.annotations.Inline; + +public interface Flags +{ + @Inline + static boolean isEmpty(int flags) + { + return flags == 0; + } + + @Inline + static boolean containsAll(int flags, int testFlags) + { + return (flags & testFlags) == testFlags; + } + + @Inline + static boolean contains(int flags, int testFlags) + { + return (flags & testFlags) != 0; + } + + @Inline + static int add(int flags, int toAdd) + { + return flags | toAdd; + } + + @Inline + static int remove(int flags, int toRemove) + { + return flags & ~toRemove; + } +} diff --git a/src/java/org/apache/cassandra/utils/GuidGenerator.java b/src/java/org/apache/cassandra/utils/GuidGenerator.java index e06270fa1d94..01e91aae72b4 100644 --- a/src/java/org/apache/cassandra/utils/GuidGenerator.java +++ b/src/java/org/apache/cassandra/utils/GuidGenerator.java @@ -26,24 +26,29 @@ public class GuidGenerator { - private static final Random myRand; - private static final SecureRandom mySecureRand; - private static final String s_id; - - static + private static class Instance { - if (!JAVA_SECURITY_EGD.isPresent()) + final static Instance instance = new Instance(); + + final Random myRand; + final SecureRandom mySecureRand; + final String s_id; + + private Instance() { - JAVA_SECURITY_EGD.setString("file:/dev/urandom"); - } - mySecureRand = new SecureRandom(); - long secureInitializer = mySecureRand.nextLong(); - myRand = new Random(secureInitializer); - try { - s_id = FBUtilities.getLocalAddressAndPort().toString(); - } - catch (RuntimeException e) { - throw new AssertionError(e); + if (!JAVA_SECURITY_EGD.isPresent()) + JAVA_SECURITY_EGD.setString("file:/dev/urandom"); + mySecureRand = new SecureRandom(); + long secureInitializer = mySecureRand.nextLong(); + myRand = new Random(secureInitializer); + try + { + s_id = FBUtilities.getLocalAddressAndPort().toString(); + } + catch (RuntimeException e) + { + throw new AssertionError(e); + } } } @@ -59,7 +64,7 @@ public static String guid() sb.append(Integer.toHexString(b)); } - return convertToStandardFormat( sb.toString() ); + return convertToStandardFormat(sb.toString()); } public static String guidToString(byte[] bytes) @@ -72,7 +77,7 @@ public static String guidToString(byte[] bytes) sb.append(Integer.toHexString(b)); } - return convertToStandardFormat( sb.toString() ); + return convertToStandardFormat(sb.toString()); } public static ByteBuffer guidAsBytes(Random random, String hostId, long time) @@ -91,14 +96,13 @@ public static ByteBuffer guidAsBytes(Random random, String hostId, long time) public static ByteBuffer guidAsBytes() { - return guidAsBytes(myRand, s_id, currentTimeMillis()); + return guidAsBytes(Instance.instance.myRand, Instance.instance.s_id, currentTimeMillis()); } /* - * Convert to the standard format for GUID - * Example: C2FEEEAC-CFCD-11D1-8B05-00600806D9B6 - */ - + * Convert to the standard format for GUID + * Example: C2FEEEAC-CFCD-11D1-8B05-00600806D9B6 + */ private static String convertToStandardFormat(String valueAfterMD5) { String raw = valueAfterMD5.toUpperCase(); diff --git a/src/java/org/apache/cassandra/utils/HeapUtils.java b/src/java/org/apache/cassandra/utils/HeapUtils.java index 6171866b4022..df58e7c7d314 100644 --- a/src/java/org/apache/cassandra/utils/HeapUtils.java +++ b/src/java/org/apache/cassandra/utils/HeapUtils.java @@ -186,9 +186,9 @@ private static void logProcessOutput(Process p) throws IOException * Retrieves the process ID or null if the process ID cannot be retrieved. * @return the process ID or null if the process ID cannot be retrieved. */ - private static Long getProcessId() + public static Long getProcessId() { - long pid = NativeLibrary.getProcessID(); + long pid = INativeLibrary.instance.getProcessID(); if (pid >= 0) return pid; diff --git a/src/java/org/apache/cassandra/utils/IFilter.java b/src/java/org/apache/cassandra/utils/IFilter.java index f06ae6e86491..faf41e207b59 100644 --- a/src/java/org/apache/cassandra/utils/IFilter.java +++ b/src/java/org/apache/cassandra/utils/IFilter.java @@ -26,9 +26,7 @@ public interface IFilter extends SharedCloseable { interface FilterKey { - /** - * Places the murmur3 hash of the key in the given long array of size at least two. - */ + /** Places the murmur3 hash of the key in the first two elements of the given long array. */ void filterHash(long[] dest); default short filterHashLowerBits() @@ -61,4 +59,11 @@ default short filterHashLowerBits() long offHeapSize(); boolean isInformative(); + + /** + * This is used to avoid creating empty file for filters that do not support serialization + * + * @return true if current filter supports serialization to disk + */ + boolean isSerializable(); } diff --git a/src/java/org/apache/cassandra/utils/IMergeIterator.java b/src/java/org/apache/cassandra/utils/IMergeIterator.java deleted file mode 100644 index e45b8976e9fe..000000000000 --- a/src/java/org/apache/cassandra/utils/IMergeIterator.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.utils; - -import java.util.Iterator; - -public interface IMergeIterator extends CloseableIterator -{ - - Iterable> iterators(); -} diff --git a/src/java/org/apache/cassandra/utils/INativeLibrary.java b/src/java/org/apache/cassandra/utils/INativeLibrary.java new file mode 100644 index 000000000000..5d9b8aae2e92 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/INativeLibrary.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils; + +import java.io.FileDescriptor; +import java.nio.MappedByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.FileChannel; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.util.File; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CUSTOM_NATIVE_LIBRARY; + +public interface INativeLibrary +{ + static final Logger logger = LoggerFactory.getLogger(INativeLibrary.class); + + INativeLibrary instance = !CUSTOM_NATIVE_LIBRARY.isPresent() + ? new NativeLibrary() + : FBUtilities.construct(CUSTOM_NATIVE_LIBRARY.getString(), "native library"); + + public enum OSType + { + LINUX, + MAC, + WINDOWS, + AIX, + OTHER; + } + + /** + * @return true if current OS type is same the provided type + */ + boolean isOS(INativeLibrary.OSType type); + + /** + * Checks if the library has been successfully linked. + * @return {@code true} if the library has been successfully linked, {@code false} otherwise. + */ + boolean isAvailable(); + + /** + * @return true if jna memory is lockable + */ + boolean jnaMemoryLockable(); + + /** + * try to lock JVM memory to avoid memory being swapped out + */ + void tryMlockall(); + + /** + * try to advice OS to to free cached pages associated with the specified region. + */ + void trySkipCache(File f, long offset, long len); + + /** + * try to advice OS to to free cached pages associated with the specified region. + */ + void trySkipCache(int fd, long offset, long len, String fileName); + + /** + * try to advice OS to to free cached pages associated with the specified region. + */ + void trySkipCache(int fd, long offset, int len, String fileName); + + /** + * advise the OS to expect random i/o performed against the mapped address + */ + void adviseRandom(MappedByteBuffer buffer, long len, String s); + + /** + * execute OS file control command + */ + int tryFcntl(int fd, int command, int flags); + + /** + * try to open given directory + */ + int tryOpenDirectory(File path); + + /** + * try to open given directory + */ + int tryOpenDirectory(String path); + + /** + * try fsync on given file + */ + void trySync(int fd); + + /** + * try to close given file + */ + void tryCloseFD(int fd); + + /** + * @return file descriptor for given async channel + */ + int getfd(AsynchronousFileChannel channel); + + /** + * @return file descriptor for given async channel + */ + FileDescriptor getFileDescriptor(AsynchronousFileChannel channel); + + /** + * @return file descriptor for given channel + */ + int getfd(FileChannel channel); + + /** + * @return file descriptor for given channel + */ + @Nullable + FileDescriptor getFileDescriptor(FileChannel channel); + + /** + * Get system file descriptor from FileDescriptor object. + * @param descriptor - FileDescriptor objec to get fd from + * @return file descriptor, -1 or error + */ + int getfd(FileDescriptor descriptor); + + /** + * @return the PID of the JVM or -1 if we failed to get the PID + */ + long getProcessID(); +} diff --git a/src/java/org/apache/cassandra/utils/ImmutableUtils.java b/src/java/org/apache/cassandra/utils/ImmutableUtils.java new file mode 100644 index 000000000000..e0e20b1396b1 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/ImmutableUtils.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Objects; + +import com.google.common.collect.ImmutableMap; + +public class ImmutableUtils +{ + public static ImmutableMap without(ImmutableMap map, K keyToRemove) + { + if (map.containsKey(keyToRemove)) + { + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(map.size() - 1); + map.forEach((k, v) -> { + if (!Objects.equals(k, keyToRemove)) + builder.put(k, v); + }); + return builder.build(); + } + return map; + } + + public static ImmutableMap withAddedOrUpdated(ImmutableMap map, K keyToAdd, V valueToAdd) + { + V currentValue = map.get(keyToAdd); + if (Objects.equals(currentValue, valueToAdd)) + return map; + + ImmutableMap.Builder builder; + if (currentValue != null) + { + builder = ImmutableMap.builderWithExpectedSize(map.size()); + map.forEach((k, v) -> { + if (Objects.equals(k, keyToAdd)) + builder.put(keyToAdd, valueToAdd); + else + builder.put(k, v); + }); + } + else + { + builder = ImmutableMap.builderWithExpectedSize(map.size() + 1); + builder.putAll(map); + builder.put(keyToAdd, valueToAdd); + } + return builder.build(); + } +} diff --git a/src/java/org/apache/cassandra/utils/InsertionOrderedNavigableSet.java b/src/java/org/apache/cassandra/utils/InsertionOrderedNavigableSet.java index 7d9d841046d4..ad33dc824911 100644 --- a/src/java/org/apache/cassandra/utils/InsertionOrderedNavigableSet.java +++ b/src/java/org/apache/cassandra/utils/InsertionOrderedNavigableSet.java @@ -31,7 +31,7 @@ import com.google.common.base.Preconditions; /** - * A {@link NavigableSet} that enforces in-order insertion of elements. This is helpful when we + * A {@link NavigableSet} that enforces in-order insertion of elements. This is helpful when we * have an already-ordered collection with no duplicates and want constant time insertion. *

    * Note: Not all methods of {@link NavigableSet} are implemented. @@ -78,7 +78,7 @@ public E pollFirst() { if (isEmpty()) return null; - + return elements.remove(0); } diff --git a/src/java/org/apache/cassandra/utils/IntMerger.java b/src/java/org/apache/cassandra/utils/IntMerger.java new file mode 100644 index 000000000000..4901d80ea320 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/IntMerger.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.function.Consumer; + +/** + *

    + * Integer version of the {@link Merger} class, throwing away the {@code equalParent} optimization as it is + * not beneficial for the integer comparisons. + *

    + * This class merges sorted integer streams by direct value comparison. For simplicity, it assumes the user has + * some external method of recognizing completion (e.g. using {@code Integer.MAX_VALUE} as sentinel). The class + * will not advance any of the source iterators until a request for data has been made. If a source's input has been + * processed and returned by the merger, the next value will only be requested when the merger is asked for the next. + *

    + * The most straightforward way to implement merging of iterators is to use a {@code PriorityQueue}, + * {@code poll} it to find the next item to consume, then {@code add} the iterator back after advancing. + * This is not very efficient as {@code poll} and {@code add} in all cases require at least + * {@code log(size)} comparisons and swaps (usually more than {@code 2*log(size)}) per consumed item, even + * if the input is suitable for fast iteration. + *

    + * The implementation below makes use of the fact that replacing the top element in a binary heap can be + * done much more efficiently than separately removing it and placing it back, especially in the cases where + * the top iterator is to be used again very soon (e.g. when there are large sections of the output where + * only a limited number of input iterators overlap, which is normally the case in many practically useful + * situations, e.g. levelled compaction). + *

    + * The implementation builds and maintains a binary heap of sources (stored in an array), where we do not + * add items after the initial construction. Instead we advance the smallest element (which is at the top + * of the heap) and push it down to find its place for its new position. Should this source be exhausted, + * we swap it with the last source in the heap and proceed by pushing that down in the heap. + *

    + * Duplicate values in multiple sources are merged together, but duplicates in any individual source are not resolved. + * In the case where we have multiple sources with matching positions, {@link #advance} advances all equal sources and + * then restores the heap structure in one operation over the heap. The latter is done equivalently to the process of + * initial construction of a min-heap using back-to-front heapification as done in the classic heapsort algorithm. It + * only needs to heapify subheaps whose top item is advanced (i.e. one whose position matches the current), and we can + * do that recursively from bottom to top. + *

    + * To make it easier to advance efficienty in single-sourced branches of tries, we extract the current smallest + * source (the head) separately, and start any advance with comparing that to the heap's first. When the smallest + * source remains the same (e.g. in branches coming from a single source) this makes it possible to advance with + * just one comparison instead of two at the expense of increasing the number by one in the general case. + *

    + */ +public abstract class IntMerger +{ + /** + * The current smallest item from the sources, tracked separately to improve performance in single-source + * sections of the input. + */ + protected int headItem; + /** + * The source corresponding to the smallest item. + */ + protected S headSource; + + /** + * Binary heap of the current items from each source. The smallest element is at position 0. + * Every element i is smaller than or equal to its two children, i.e.
    + * {@code item[i] <= item[i*2 + 1] && item[i] <= item[i*2 + 2]} + */ + private final int[] items; + /** + * Binary heap of the sources. + *

    + * Sources are moved up and down the heap together with the items, i.e. the source index corresponds to the item + * index in these two arrays. + */ + private final S[] sources; + + boolean started; + + /** Advance the given source by one item and return it. */ + protected abstract int advanceSource(S s) throws IOException; + /** Skip the given source to the smallest item that is greater or equal to the given target, and return that item. */ + protected abstract int skipSource(S s, int target) throws IOException; + + protected IntMerger(Collection inputs, Class sourceClass) + { + int count = inputs.size(); + + // Get sources for all inputs. Put one of them in head and the rest in the heap. + @SuppressWarnings("unchecked") + S[] s = (S[]) Array.newInstance(sourceClass, count - 1); + sources = s; + items = new int[count - 1]; + int i = -1; + for (S source : inputs) + { + if (i >= 0) + sources[i] = source; + else + headSource = source; + ++i; + } + // Do not fetch items until requested. + started = false; + } + + /** + * Advance the merged state and return the next item. + */ + protected int advance() throws IOException + { + if (started) + advanceHeap(headItem, 0); + else + initializeHeap(); + + return headItem = maybeSwapHead(advanceSource(headSource)); + } + + /** + * Descend recursively in the subheap structure from the given index to all children that match the given position. + * On the way back from the recursion, advance each matching iterator and restore the heap invariants. + */ + private void advanceHeap(int advancedItem, int index) throws IOException + { + if (index >= items.length) + return; + + if (items[index] != advancedItem) + return; + + // If any of the children are at the same position, they also need advancing and their subheap + // invariant to be restored. + advanceHeap(advancedItem, index * 2 + 1); + advanceHeap(advancedItem, index * 2 + 2); + + // On the way back from the recursion, advance and form a heap from the (already advanced and well-formed) + // children and the current node. + advanceSourceAndHeapify(index); + // The heap rooted at index is now advanced and well-formed. + } + + + /** + * Advance the source at the given index and restore the heap invariant for its subheap, assuming its child subheaps + * are already well-formed. + */ + private void advanceSourceAndHeapify(int index) throws IOException + { + // Advance the source. + S source = sources[index]; + int next = advanceSource(source); + + // Place current node in its proper position, pulling any smaller child up. This completes the construction + // of the subheap rooted at this index. + heapifyDown(source, next, index); + } + + /** + * Push the given state down in the heap from the given index until it finds its proper place among + * the subheap rooted at that position. + */ + private void heapifyDown(S source, int item, int index) + { + while (true) + { + int next = index * 2 + 1; + if (next >= items.length) + break; + // Select the smaller of the two children to push down to. + int nextItem = items[next]; + if (next + 1 < items.length) + { + int nextP1Item = items[next + 1]; + if (nextItem > nextP1Item) + { + nextItem = nextP1Item; + ++next; + } + } + // If the child is greater or equal, the invariant has been restored. + if (item <= nextItem) + break; + items[index] = nextItem; + sources[index] = sources[next]; + index = next; + } + items[index] = item; + sources[index] = source; + } + + /** + * Check if the head is greater than the top element in the heap, and if so, swap them and push down the new + * top until its proper place. + */ + private int maybeSwapHead(int newHeadItem) + { + int heap0Item = items[0]; + if (newHeadItem <= heap0Item) + return newHeadItem; // head is still smallest + + // otherwise we need to swap heap and heap[0] + S newHeap0 = headSource; + headSource = sources[0]; + heapifyDown(newHeap0, newHeadItem, 0); + return heap0Item; + } + + /** + * Initialize the heap for the retrieving the first item. We do this in a separate method because we don't yet have + * target items with which to compare in the methods above. + */ + private void initializeHeap() throws IOException + { + for (int i = items.length - 1; i >= 0; --i) + advanceSourceAndHeapify(i); + + started = true; + } + + /** + * Skip the merged iterator to the smallest value equal to or greater than the target and return the next item. + */ + protected int skipTo(int target) throws IOException + { + // We need to advance all sources that stand before the requested position. + // If a child source does not need to advance as it is at the skip position or greater, neither of the ones + // below it in the heap hierarchy do as they can't have an earlier position. + if (started) + skipHeap(target, 0); + else + initializeSkipping(target); + + return headItem = maybeSwapHead(skipSource(headSource, target)); + } + + + /** + * Descend recursively in the subheap structure from the given index to all children that are smaller than the + * requested position. + * On the way back from the recursion, skip each matching iterator and restore the heap invariants. + */ + private void skipHeap(int target, int index) throws IOException + { + if (index >= items.length) + return; + + if (items[index] >= target) + return; + + // If any of the children are at a smaller position, they also need advancing and their subheap + // invariant to be restored. + skipHeap(target, index * 2 + 1); + skipHeap(target, index * 2 + 2); + + // On the way back from the recursion, advance and form a heap from the (already advanced and well-formed) + // children and the current node. + skipSourceAndHeapify(index, target); + + // The heap rooted at index is now advanced and well-formed. + } + + /** + * Skip the source at the given index and restore the heap invariant for its subheap, assuming its child subheaps + * are already well-formed. + */ + private void skipSourceAndHeapify(int index, int target) throws IOException + { + // Advance the source. + S source = sources[index]; + int next = skipSource(source, target); + + // Place current node in its proper position, pulling any smaller child up. This completes the construction + // of the subheap rooted at this index. + heapifyDown(source, next, index); + } + + /** + * Initialize the heap by skipping to the given target. We do this in a separate method because we don't yet have + * items with which to compare in the methods above. + */ + private void initializeSkipping(int target) throws IOException + { + for (int i = items.length - 1; i >= 0; --i) + skipSourceAndHeapify(i, target); + + started = true; + } + + /** + * Apply a method to all sources. + */ + protected void applyToAllSources(Consumer op) + { + for (int i = sources.length - 1; i >= 0; --i) + op.accept(sources[i]); + op.accept(headSource); + } + + // currentItem(), forEachCurrentSource() methods can be easily implemented if required +} + diff --git a/src/java/org/apache/cassandra/utils/IteratorWithLowerBound.java b/src/java/org/apache/cassandra/utils/IteratorWithLowerBound.java deleted file mode 100644 index 85eeede2e7cc..000000000000 --- a/src/java/org/apache/cassandra/utils/IteratorWithLowerBound.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.utils; - -public interface IteratorWithLowerBound -{ - In lowerBound(); -} diff --git a/src/java/org/apache/cassandra/utils/JMXServerUtils.java b/src/java/org/apache/cassandra/utils/JMXServerUtils.java index 78c8ced8d2fb..ac106114c7f8 100644 --- a/src/java/org/apache/cassandra/utils/JMXServerUtils.java +++ b/src/java/org/apache/cassandra/utils/JMXServerUtils.java @@ -209,7 +209,7 @@ private static MBeanServerForwarder configureJmxAuthorization(Map globalHandler; + private static volatile Consumer diskHandler; + private static volatile Function> commitLogHandler; + private static final List> shutdownHooks = new ArrayList<>(1); // It is used for unit test public static OnKillHook killerHook; + static + { + setGlobalErrorHandler(JVMStabilityInspector::defaultGlobalErrorHandler); + setDiskErrorHandler(JVMStabilityInspector::inspectDiskError); + setCommitLogErrorHandler(JVMStabilityInspector::createDefaultCommitLogErrorHandler); + } + private JVMStabilityInspector() {} public static void uncaughtException(Thread thread, Throwable t) @@ -78,6 +92,32 @@ public static void uncaughtException(Thread thread, Throwable t) inspectThrowable(t, JVMStabilityInspector::inspectDiskError, true); } + public static void setGlobalErrorHandler(BiConsumer errorHandler) + { + globalHandler = errorHandler; + } + + @VisibleForTesting + public static BiConsumer getGlobalErrorHandler() + { + return globalHandler; + } + + public static void setDiskErrorHandler(Consumer errorHandler) + { + diskHandler = errorHandler; + } + + public static void setCommitLogErrorHandler(Function> errorHandler) + { + commitLogHandler = errorHandler; + } + + public static Consumer getDiskErrorHandler() + { + return diskHandler; + } + /** * Certain Throwables and Exceptions represent "Die" conditions for the server. * This recursively checks the input Throwable's cause hierarchy until null. @@ -86,12 +126,12 @@ public static void uncaughtException(Thread thread, Throwable t) */ public static void inspectThrowable(Throwable t) throws OutOfMemoryError { - inspectThrowable(t, JVMStabilityInspector::inspectDiskError, false); + inspectThrowable(t, diskHandler, false); } - public static void inspectCommitLogThrowable(Throwable t) + public static void inspectCommitLogThrowable(String message, Throwable t) { - inspectThrowable(t, JVMStabilityInspector::inspectCommitLogError, false); + inspectThrowable(t, commitLogHandler.apply(message), false); } private static void inspectDiskError(Throwable t) @@ -103,6 +143,21 @@ else if (t instanceof FSError) } public static void inspectThrowable(Throwable t, Consumer fn, boolean isUncaughtException) throws OutOfMemoryError + { + if (t == null) + return; + globalHandler.accept(t, isUncaughtException); + fn.accept(t); + + if (t.getSuppressed() != null) + for (Throwable suppressed : t.getSuppressed()) + inspectThrowable(suppressed, fn, isUncaughtException); + + if (t.getCause() != null) + inspectThrowable(t.getCause(), fn, isUncaughtException); + } + + private static void defaultGlobalErrorHandler(Throwable t, boolean isUncaughtException) { boolean isUnstable = false; if (t instanceof OutOfMemoryError) @@ -122,7 +177,7 @@ public static void inspectThrowable(Throwable t, Consumer fn, boolean logger.error("OutOfMemory error letting the JVM handle the error:", t); - StorageService.instance.removeShutdownHook(); + removeShutdownHooks(); forceHeapSpaceOomMaybe((OutOfMemoryError) t); @@ -165,20 +220,8 @@ else if (t instanceof UnrecoverableIllegalStateException) { if (!StorageService.instance.isDaemonSetupCompleted()) FileUtils.handleStartupFSError(t); - killer.killCurrentJVM(t); + killer.killJVM(t); } - - try - { - fn.accept(t); - } - catch (Exception | Error e) - { - logger.warn("Unexpected error while handling unexpected error", e); - } - - if (t.getCause() != null) - inspectThrowable(t.getCause(), fn, isUncaughtException); } private static final Set FORCE_HEAP_OOM_IGNORE_SET = ImmutableSet.of("Java heap space", "GC Overhead limit exceeded"); @@ -205,20 +248,25 @@ private static void forceHeapSpaceOomMaybe(OutOfMemoryError oom) } } + private static Consumer createDefaultCommitLogErrorHandler(String message) + { + return JVMStabilityInspector::inspectCommitLogError; + } + private static void inspectCommitLogError(Throwable t) { if (!StorageService.instance.isDaemonSetupCompleted()) { logger.error("Exiting due to error while processing commit log during initialization.", t); - killer.killCurrentJVM(t, true); + killer.killJVM(t, true); } else if (DatabaseDescriptor.getCommitFailurePolicy() == Config.CommitFailurePolicy.die) - killer.killCurrentJVM(t); + killer.killJVM(t); } public static void killCurrentJVM(Throwable t, boolean quiet) { - killer.killCurrentJVM(t, quiet); + killer.killJVM(t, quiet); } public static void userFunctionTimeout(Throwable t) @@ -227,10 +275,10 @@ public static void userFunctionTimeout(Throwable t) { case die: // policy to give 250ms grace time to - ScheduledExecutors.nonPeriodicTasks.schedule(() -> killer.killCurrentJVM(t), 250, TimeUnit.MILLISECONDS); + ScheduledExecutors.nonPeriodicTasks.schedule(() -> killer.killJVM(t), 250, TimeUnit.MILLISECONDS); break; case die_immediate: - killer.killCurrentJVM(t); + killer.killJVM(t); break; case ignore: logger.error(t.getMessage()); @@ -238,31 +286,48 @@ public static void userFunctionTimeout(Throwable t) } } + public static void registerShutdownHook(Thread hook, Runnable runOnHookRemoved) + { + Runtime.getRuntime().addShutdownHook(hook); + shutdownHooks.add(Pair.create(hook, runOnHookRemoved)); + } + + public static void removeShutdownHooks() + { + Throwable err = null; + for (Pair hook : shutdownHooks) + { + err = Throwables.perform(err, + () -> Runtime.getRuntime().removeShutdownHook(hook.left), + hook.right::run); + } + + if (err != null) + logger.error("Got error(s) when removing shutdown hook(s): {}", err.getMessage(), err); + + shutdownHooks.clear(); + } + @VisibleForTesting - public static Killer replaceKiller(Killer newKiller) + public static JVMKiller replaceKiller(JVMKiller newKiller) { - Killer oldKiller = JVMStabilityInspector.killer; + JVMKiller oldKiller = JVMStabilityInspector.killer; JVMStabilityInspector.killer = newKiller; return oldKiller; } + public static JVMKiller killer() + { + return killer; + } + @VisibleForTesting - public static class Killer + public static class Killer implements JVMKiller { private final AtomicBoolean killing = new AtomicBoolean(); - /** - * Certain situations represent "Die" conditions for the server, and if so, the reason is logged and the current JVM is killed. - * - * @param t - * The Throwable to log before killing the current JVM - */ - protected void killCurrentJVM(Throwable t) - { - killCurrentJVM(t, false); - } - - protected void killCurrentJVM(Throwable t, boolean quiet) + @Override + public void killJVM(Throwable t, boolean quiet) { if (!quiet) { @@ -274,7 +339,7 @@ protected void killCurrentJVM(Throwable t, boolean quiet) if (doExit && killing.compareAndSet(false, true)) { - StorageService.instance.removeShutdownHook(); + removeShutdownHooks(); System.exit(100); } } diff --git a/src/java/org/apache/cassandra/utils/JsonUtils.java b/src/java/org/apache/cassandra/utils/JsonUtils.java index 1cdc13c55cba..dffb7c2c49f7 100644 --- a/src/java/org/apache/cassandra/utils/JsonUtils.java +++ b/src/java/org/apache/cassandra/utils/JsonUtils.java @@ -27,7 +27,7 @@ import java.util.Map; import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.util.BufferRecyclers; +import com.fasterxml.jackson.core.io.JsonStringEncoder; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; @@ -60,8 +60,7 @@ private JsonUtils() */ public static String quoteAsJsonString(String s) { - // In future should update to directly use `JsonStringEncoder.getInstance()` but for now: - return new String(BufferRecyclers.getJsonStringEncoder().quoteAsString(s)); + return new String(JsonStringEncoder.getInstance().quoteAsString(s)); } public static Object decodeJson(byte[] json) @@ -180,6 +179,38 @@ public static void serializeToJsonFile(Object object, File outputFile) throws IO } } + public static void serializeToJsonFileAtomic(Object object, File outputFile) throws IOException + { + // Try to write then perform atomic move so that file can't be corrupted + // by process crash in the middle of the writing. + File tempFile = new File(outputFile.path() + ".tmp"); + try + { + // Serialize to bytes first so we can flush and fsync before close. + // Jackson's writeValue(OutputStream, ...) auto-closes the stream, + // which would prevent us from calling sync() afterwards. + byte[] data = JSON_OBJECT_PRETTY_WRITER.writeValueAsBytes(object); + try (FileOutputStreamPlus out = tempFile.newOutputStream(OVERWRITE)) + { + out.write(data); + // Force data to disk before rename to ensure durability. + // Without this, a crash after rename but before OS flushes to disk + // can leave the file with zero-filled or corrupted blocks. + out.sync(); + } + tempFile.move(outputFile); + // Fsync the parent directory to ensure the rename is durable. + // Without this, a crash after rename can revert to the old directory entry. + // See: https://transactional.blog/how-to-learn/disk-io + SyncUtil.trySyncDir(outputFile.parent()); + } + catch (IOException ex) + { + tempFile.deleteIfExists(); + throw ex; + } + } + public static T deserializeFromJsonFile(Class tClass, File file) throws IOException { try (FileInputStreamPlus in = file.newInputStream()) @@ -188,6 +219,11 @@ public static T deserializeFromJsonFile(Class tClass, File file) throws I } } + public static T deserializeFromJsonBytes(Class tClass, byte[] bytes) throws IOException + { + return JSON_OBJECT_MAPPER.readValue(bytes, tClass); + } + /** * Handles unquoting and case-insensitivity in map keys. */ diff --git a/src/java/org/apache/cassandra/utils/LucenePriorityQueue.java b/src/java/org/apache/cassandra/utils/LucenePriorityQueue.java new file mode 100644 index 000000000000..54319e40d37d --- /dev/null +++ b/src/java/org/apache/cassandra/utils/LucenePriorityQueue.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Comparator; + +import org.apache.lucene.util.PriorityQueue; + +/** + * Version of lucene's priority queue that accepts a comparator. + *

    + * This priority queue has several performance advantages compared to java's: + *

      + *
    • it can efficiently order items added through {@code addAll} using the O(n) bottom-up heapification process
    • + *
    • it implements an {@code updateTop} method which is much more efficient than {@code poll} + {@code add} for e.g. + * advancing a source and keeping it in the queue
    • + *
    + *

    + * Use this class when elements need to be added to the queue after the initial construction. In case all elements are + * predetermined, a {@link SortingIterator} is usually preferable as it also implements skipping and deduplication. + * When sorting multiple iterators into one, a {@link MergeIterator} or the underlying {@link Merger} may provide a + * simpler solution. Finally, when operating on integer iterators, we have a special-case {@link IntMerger}. + */ +public class LucenePriorityQueue extends PriorityQueue +{ + final Comparator comparator; + + public LucenePriorityQueue(int size, Comparator comparator) + { + super(size); + this.comparator = comparator; + } + + @Override + protected boolean lessThan(T t, T t1) + { + return comparator.compare(t, t1) < 0; + } +} diff --git a/src/java/org/apache/cassandra/utils/MBeanWrapper.java b/src/java/org/apache/cassandra/utils/MBeanWrapper.java index 0ef342da30d4..bf2f07d4f271 100644 --- a/src/java/org/apache/cassandra/utils/MBeanWrapper.java +++ b/src/java/org/apache/cassandra/utils/MBeanWrapper.java @@ -77,7 +77,7 @@ static MBeanWrapper getMBeanWrapper() return new PlatformMBeanWrapper(); } } - return FBUtilities.construct(klass, "mbean"); + return FBUtilities.construct(klass, "mbean", MBeanWrapper.class); } // Passing true for graceful will log exceptions instead of rethrowing them diff --git a/src/java/org/apache/cassandra/utils/MergeIterator.java b/src/java/org/apache/cassandra/utils/MergeIterator.java index 1dd1f7833bd1..3a8add3e0c47 100644 --- a/src/java/org/apache/cassandra/utils/MergeIterator.java +++ b/src/java/org/apache/cassandra/utils/MergeIterator.java @@ -17,475 +17,158 @@ */ package org.apache.cassandra.utils; -import java.util.*; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +import com.google.common.base.Preconditions; /** Merges sorted input iterators which individually contain unique items. */ -public abstract class MergeIterator extends AbstractIterator implements IMergeIterator +public abstract class MergeIterator { - protected final Reducer reducer; - protected final List> iterators; - - protected MergeIterator(List> iters, Reducer reducer) + public static CloseableIterator getCloseable(List> sources, + Comparator comparator, + Reducer reducer) { - this.iterators = iters; - this.reducer = reducer; + if (sources.size() == 1) + { + return reducer.singleSourceReduceIsTrivial() + ? (CloseableIterator) sources.get(0) + : new OneToOneCloseable<>(sources.get(0), reducer); + } + return new ManyToOneCloseable<>(sources, comparator, reducer); } - public static MergeIterator get(List> sources, - Comparator comparator, - Reducer reducer) + public static Iterator get(List> sources, + Comparator comparator, + Reducer reducer) { if (sources.size() == 1) { - return reducer.trivialReduceIsTrivial() - ? new TrivialOneToOne<>(sources, reducer) - : new OneToOne<>(sources, reducer); + return reducer.singleSourceReduceIsTrivial() + ? (Iterator) sources.get(0) + : new OneToOne<>(sources.get(0), reducer); } return new ManyToOne<>(sources, comparator, reducer); } - public Iterable> iterators() + public static Iterator getNonReducing(List> sources, + Comparator comparator) { - return iterators; + if (sources.size() == 1) + return sources.get(0); + else + return new NonReducing<>(sources, comparator); } - public void close() + public static CloseableIterator getNonReducingCloseable(List> sources, + Comparator comparator) { - for (int i=0, isize=iterators.size(); i iterator = iterators.get(i); - try - { - if (iterator instanceof AutoCloseable) - ((AutoCloseable)iterator).close(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } - - reducer.close(); + if (sources.size() == 1) + return sources.get(0); + else + return new NonReducingCloseable<>(sources, comparator); } - /** - * A MergeIterator that consumes multiple input values per output value. - * - * The most straightforward way to implement this is to use a {@code PriorityQueue} of iterators, {@code poll} it to - * find the next item to consume, then {@code add} the iterator back after advancing. This is not very efficient as - * {@code poll} and {@code add} in all cases require at least {@code log(size)} comparisons (usually more than - * {@code 2*log(size)}) per consumed item, even if the input is suitable for fast iteration. - * - * The implementation below makes use of the fact that replacing the top element in a binary heap can be done much - * more efficiently than separately removing it and placing it back, especially in the cases where the top iterator - * is to be used again very soon (e.g. when there are large sections of the output where only a limited number of - * input iterators overlap, which is normally the case in many practically useful situations, e.g. levelled - * compaction). To further improve this particular scenario, we also use a short sorted section at the start of the - * queue. - * - * The heap is laid out as this (for {@code SORTED_SECTION_SIZE == 2}): - * 0 - * | - * 1 - * | - * 2 - * / \ - * 3 4 - * / \ / \ - * 5 6 7 8 - * .. .. .. .. - * Where each line is a <= relationship. - * - * In the sorted section we can advance with a single comparison per level, while advancing a level within the heap - * requires two (so that we can find the lighter element to pop up). - * The sorted section adds a constant overhead when data is uniformly distributed among the iterators, but may up - * to halve the iteration time when one iterator is dominant over sections of the merged data (as is the case with - * non-overlapping iterators). - * - * The iterator is further complicated by the need to avoid advancing the input iterators until an output is - * actually requested. To achieve this {@code consume} walks the heap to find equal items without advancing the - * iterators, and {@code advance} moves them and restores the heap structure before any items can be consumed. - * - * To avoid having to do additional comparisons in consume to identify the equal items, we keep track of equality - * between children and their parents in the heap. More precisely, the lines in the diagram above define the - * following relationship: - * parent <= child && (parent == child) == child.equalParent - * We can track, make use of and update the equalParent field without any additional comparisons. - * - * For more formal definitions and proof of correctness, see CASSANDRA-8915. - */ - static final class ManyToOne extends MergeIterator + private static class ManyToOne, Out> extends Merger implements Iterator { - protected final Candidate[] heap; - - /** Number of non-exhausted iterators. */ - int size; - - /** - * Position of the deepest, right-most child that needs advancing before we can start consuming. - * Because advancing changes the values of the items of each iterator, the parent-chain from any position - * in this range that needs advancing is not in correct order. The trees rooted at any position that does - * not need advancing, however, retain their prior-held binary heap property. - */ - int needingAdvance; - - /** - * The number of elements to keep in order before the binary heap starts, exclusive of the top heap element. - */ - static final int SORTED_SECTION_SIZE = 4; - - public ManyToOne(List> iters, Comparator comp, Reducer reducer) + public ManyToOne(List iters, Comparator comp, Reducer reducer) { - super(iters, reducer); - - @SuppressWarnings("unchecked") - Candidate[] heap = new Candidate[iters.size()]; - this.heap = heap; - size = 0; - - for (int i = 0; i < iters.size(); i++) - { - Candidate candidate = new Candidate<>(i, iters.get(i), comp); - heap[size++] = candidate; - } - needingAdvance = size; + this(iters, null, comp, reducer); } - protected final Out computeNext() + ManyToOne(List iters, Consumer onClose, Comparator comp, Reducer reducer) { - advance(); - return consume(); + super(iters, it -> it.hasNext() ? Preconditions.checkNotNull(it.next()) : null, onClose, comp, reducer); } + } - /** - * Advance all iterators that need to be advanced and place them into suitable positions in the heap. - * - * By walking the iterators backwards we know that everything after the point being processed already forms - * correctly ordered subheaps, thus we can build a subheap rooted at the current position by only sinking down - * the newly advanced iterator. Because all parents of a consumed iterator are also consumed there is no way - * that we can process one consumed iterator but skip over its parent. - * - * The procedure is the same as the one used for the initial building of a heap in the heapsort algorithm and - * has a maximum number of comparisons {@code (2 * log(size) + SORTED_SECTION_SIZE / 2)} multiplied by the - * number of iterators whose items were consumed at the previous step, but is also at most linear in the size of - * the heap if the number of consumed elements is high (as it is in the initial heap construction). With non- or - * lightly-overlapping iterators the procedure finishes after just one (resp. a couple of) comparisons. - */ - private void advance() + private static class ManyToOneCloseable, Out> extends ManyToOne implements CloseableIterator + { + public ManyToOneCloseable(List iters, Comparator comp, Reducer reducer) { - // Turn the set of candidates into a heap. - for (int i = needingAdvance - 1; i >= 0; --i) - { - Candidate candidate = heap[i]; - /** - * needingAdvance runs to the maximum index (and deepest-right node) that may need advancing; - * since the equal items that were consumed at-once may occur in sub-heap "veins" of equality, - * not all items above this deepest-right position may have been consumed; these already form - * valid sub-heaps and can be skipped-over entirely - */ - if (candidate.needsAdvance()) - replaceAndSink(candidate.advance(), i); - } + super(iters, CloseableIterator::close, comp, reducer); } + } - /** - * Consume all items that sort like the current top of the heap. As we cannot advance the iterators to let - * equivalent items pop up, we walk the heap to find them and mark them as needing advance. - * - * This relies on the equalParent flag to avoid doing any comparisons. - */ - private Out consume() + private static class NonReducing> extends Merger implements Iterator + { + public NonReducing(List iters, Comparator comp) { - if (size == 0) - return endOfData(); - - reducer.onKeyChange(); - assert !heap[0].equalParent; - heap[0].consume(reducer); - final int size = this.size; - final int sortedSectionSize = Math.min(size, SORTED_SECTION_SIZE); - int i; - consume: { - for (i = 1; i < sortedSectionSize; ++i) - { - if (!heap[i].equalParent) - break consume; - heap[i].consume(reducer); - } - i = Math.max(i, consumeHeap(i) + 1); - } - needingAdvance = i; - return reducer.getReduced(); + this(iters, null, comp); } - /** - * Recursively consume all items equal to equalItem in the binary subheap rooted at position idx. - * - * @return the largest equal index found in this search. - */ - private int consumeHeap(int idx) + NonReducing(List iters, Consumer onClose, Comparator comp) { - if (idx >= size || !heap[idx].equalParent) - return -1; - - heap[idx].consume(reducer); - int nextIdx = (idx << 1) - (SORTED_SECTION_SIZE - 1); - return Math.max(idx, Math.max(consumeHeap(nextIdx), consumeHeap(nextIdx + 1))); + super(iters, it -> it.hasNext() ? Preconditions.checkNotNull(it.next()) : null, onClose, comp, null); } - /** - * Replace an iterator in the heap with the given position and move it down the heap until it finds its proper - * position, pulling lighter elements up the heap. - * - * Whenever an equality is found between two elements that form a new parent-child relationship, the child's - * equalParent flag is set to true if the elements are equal. - */ - private void replaceAndSink(Candidate candidate, int currIdx) + @Override + public In next() { - if (candidate == null) - { - // Drop iterator by replacing it with the last one in the heap. - candidate = heap[--size]; - heap[size] = null; // not necessary but helpful for debugging - } - // The new element will be top of its heap, at this point there is no parent to be equal to. - candidate.equalParent = false; - - final int size = this.size; - final int sortedSectionSize = Math.min(size - 1, SORTED_SECTION_SIZE); - - int nextIdx; - - // Advance within the sorted section, pulling up items lighter than candidate. - while ((nextIdx = currIdx + 1) <= sortedSectionSize) - { - if (!heap[nextIdx].equalParent) // if we were greater then an (or were the) equal parent, we are >= the child - { - int cmp = candidate.compareTo(heap[nextIdx]); - if (cmp <= 0) - { - heap[nextIdx].equalParent = cmp == 0; - heap[currIdx] = candidate; - return; - } - } - - heap[currIdx] = heap[nextIdx]; - currIdx = nextIdx; - } - // If size <= SORTED_SECTION_SIZE, nextIdx below will be no less than size, - // because currIdx == sortedSectionSize == size - 1 and nextIdx becomes - // (size - 1) * 2) - (size - 1 - 1) == size. - - // Advance in the binary heap, pulling up the lighter element from the two at each level. - while ((nextIdx = (currIdx * 2) - (sortedSectionSize - 1)) + 1 < size) - { - if (!heap[nextIdx].equalParent) - { - if (!heap[nextIdx + 1].equalParent) - { - // pick the smallest of the two children - int siblingCmp = heap[nextIdx + 1].compareTo(heap[nextIdx]); - if (siblingCmp < 0) - ++nextIdx; - - // if we're smaller than this, we are done, and must only restore the heap and equalParent properties - int cmp = candidate.compareTo(heap[nextIdx]); - if (cmp <= 0) - { - if (cmp == 0) - { - heap[nextIdx].equalParent = true; - if (siblingCmp == 0) // siblingCmp == 0 => nextIdx is the left child - heap[nextIdx + 1].equalParent = true; - } - - heap[currIdx] = candidate; - return; - } - - if (siblingCmp == 0) - { - // siblingCmp == 0 => nextIdx is still the left child - // if the two siblings were equal, and we are inserting something greater, we will - // pull up the left one; this means the right gets an equalParent - heap[nextIdx + 1].equalParent = true; - } - } - else - ++nextIdx; // descend down the path where we found the equal child - } - - heap[currIdx] = heap[nextIdx]; - currIdx = nextIdx; - } - - // our loop guard ensures there are always two siblings to process; typically when we exit the loop we will - // be well past the end of the heap and this next condition will match... - if (nextIdx >= size) - { - heap[currIdx] = candidate; - return; - } - - // ... but sometimes we will have one last child to compare against, that has no siblings - if (!heap[nextIdx].equalParent) - { - int cmp = candidate.compareTo(heap[nextIdx]); - if (cmp <= 0) - { - heap[nextIdx].equalParent = cmp == 0; - heap[currIdx] = candidate; - return; - } - } - - heap[currIdx] = heap[nextIdx]; - heap[nextIdx] = candidate; + return super.nonReducingNext(); } } - // Holds and is comparable by the head item of an iterator it owns - protected static final class Candidate implements Comparable> + private static class NonReducingCloseable> extends NonReducing implements CloseableIterator { - private final Iterator iter; - private final Comparator comp; - private final int idx; - private In item; - private In lowerBound; - boolean equalParent; - - public Candidate(int idx, Iterator iter, Comparator comp) + public NonReducingCloseable(List iters, Comparator comp) { - this.iter = iter; - this.comp = comp; - this.idx = idx; - this.lowerBound = iter instanceof IteratorWithLowerBound ? ((IteratorWithLowerBound)iter).lowerBound() : null; - } - - /** @return this if our iterator had an item, and it is now available, otherwise null */ - protected Candidate advance() - { - if (lowerBound != null) - { - item = lowerBound; - return this; - } - - if (!iter.hasNext()) - return null; - - item = iter.next(); - return this; + super(iters, CloseableIterator::close, comp); } + } - public int compareTo(Candidate that) - { - assert this.item != null && that.item != null; - int ret = comp.compare(this.item, that.item); - if (ret == 0 && (this.isLowerBound() ^ that.isLowerBound())) - { // if the items are equal and one of them is a lower bound (but not the other one) - // then ensure the lower bound is less than the real item so we can safely - // skip lower bounds when consuming - return this.isLowerBound() ? -1 : 1; - } - return ret; - } + private static class OneToOne implements Iterator + { + private final Iterator source; + private final Reducer reducer; - private boolean isLowerBound() + public OneToOne(Iterator source, Reducer reducer) { - assert item != null; - return item == lowerBound; + this.reducer = reducer; + this.source = source; } - public void consume(Reducer reducer) + public boolean hasNext() { - if (isLowerBound()) - { - item = null; - lowerBound = null; - } - else - { - reducer.reduce(idx, item); - item = null; - } + return source.hasNext(); } - public boolean needsAdvance() + public Out next() { - return item == null; + reducer.onKeyChange(); + reducer.reduce(0, source.next()); + return reducer.getReduced(); } } - /** Accumulator that collects values of type A, and outputs a value of type B. */ - public static abstract class Reducer + private static class OneToOneCloseable implements CloseableIterator { - /** - * @return true if Out is the same as In for the case of a single source iterator - */ - public boolean trivialReduceIsTrivial() + private final CloseableIterator source; + private final Reducer reducer; + + public OneToOneCloseable(CloseableIterator source, Reducer reducer) { - return false; + this.reducer = reducer; + this.source = source; } - /** - * combine this object with the previous ones. - * intermediate state is up to your implementation. - */ - public abstract void reduce(int idx, In current); - - /** @return The last object computed by reduce */ - protected abstract Out getReduced(); - - /** - * Called at the beginning of each new key, before any reduce is called. - * To be overridden by implementing classes. - */ - protected void onKeyChange() {} - - /** - * May be overridden by implementations that require cleaning up after use - */ - public void close() {} - } - - private static class OneToOne extends MergeIterator - { - private final Iterator source; - - public OneToOne(List> sources, Reducer reducer) + public boolean hasNext() { - super(sources, reducer); - source = sources.get(0); + return source.hasNext(); } - protected Out computeNext() + public Out next() { - if (!source.hasNext()) - return endOfData(); reducer.onKeyChange(); reducer.reduce(0, source.next()); return reducer.getReduced(); } - } - - private static class TrivialOneToOne extends MergeIterator - { - private final Iterator source; - - public TrivialOneToOne(List> sources, Reducer reducer) - { - super(sources, reducer); - source = sources.get(0); - } - @SuppressWarnings("unchecked") - protected Out computeNext() + public void close() { - if (!source.hasNext()) - return endOfData(); - return (Out) source.next(); + source.close(); } } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/utils/Merger.java b/src/java/org/apache/cassandra/utils/Merger.java new file mode 100644 index 000000000000..ff3cddecb0bb --- /dev/null +++ b/src/java/org/apache/cassandra/utils/Merger.java @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A merger of input streams (e.g. iterators or cursors) that may consume multiple input values per output value. + * + * The most straightforward way to implement this is to use a {@code PriorityQueue} of iterators, {@code poll} it to + * find the next item to consume, then {@code add} the iterator back after advancing. This is not very efficient as + * {@code poll} and {@code add} in all cases require at least {@code log(size)} comparisons (usually more than + * {@code 2*log(size)}) per consumed item, even if the input is suitable for fast iteration. + * + * The implementation below makes use of the fact that replacing the top element in a binary heap can be done much + * more efficiently than separately removing it and placing it back, especially in the cases where the top iterator + * is to be used again very soon (e.g. when there are large sections of the output where only a limited number of + * input iterators overlap, which is normally the case in many practically useful situations, e.g. levelled + * compaction). To further improve this particular scenario, we also use a short sorted section at the start of the + * queue. + * + * The heap is laid out as this (for {@code SORTED_SECTION_SIZE == 2}): + * 0 + * | + * 1 + * | + * 2 + * / \ + * 3 4 + * / \ / \ + * 5 6 7 8 + * .. .. .. .. + * Where each line is a <= relationship. + * + * In the sorted section we can advance with a single comparison per level, while advancing a level within the heap + * requires two (so that we can find the lighter element to pop up). + * The sorted section adds a constant overhead when data is uniformly distributed among the iterators, but may up + * to halve the iteration time when one iterator is dominant over sections of the merged data (as is the case with + * non-overlapping iterators). + * + * The iterator is further complicated by the need to avoid advancing the input iterators until an output is + * actually requested. To achieve this {@code consume} walks the heap to find equal items without advancing the + * iterators, and {@code advance} moves them and restores the heap structure before any items can be consumed. + * + * To avoid having to do additional comparisons in consume to identify the equal items, we keep track of equality + * between children and their parents in the heap. More precisely, the lines in the diagram above define the + * following relationship: + * parent <= child && (parent == child) == child.equalParent + * We can track, make use of and update the equalParent field without any additional comparisons. + * + * For more formal definitions and proof of correctness, see CASSANDRA-8915. + */ +public class Merger implements AutoCloseable +{ + /** The heap of candidates, each containing their current item and the source from which it was obtained. */ + protected final Candidate[] heap; + + /** Reducer, called for each input item to combine them into the output. */ + final Reducer reducer; + + /** Function called on each source to get the next item. Should return null if the source is exhausted. */ + final Function inputRetriever; + + /** Method to call on each source on close, may be null. */ + final Consumer onClose; + + /** Number of non-exhausted iterators. */ + int size; + + /** + * Position of the deepest, right-most child that needs advancing before we can start consuming. + * Because advancing changes the values of the items of each source, the parent-chain from any position + * in this range that needs advancing is not in correct order. The trees rooted at any position that does + * not need advancing, however, retain their prior-held binary heap property. + */ + int needingAdvance; + + /** + * The number of elements to keep in order before the binary heap starts, exclusive of the top heap element. + */ + static final int SORTED_SECTION_SIZE = 4; + + /** + * @param sources The input sources. + * @param inputRetriever Function called on each source to get the next item. Should return null if the source is + * exhausted. + * @param onClose Method to call on each source on close, may be null. + * @param comparator Comparator of input items. + * @param reducer Reducer, called for each input item to combine them into the output. + */ + public Merger(List sources, + Function inputRetriever, + Consumer onClose, + Comparator comparator, + Reducer reducer) + { + this.inputRetriever = inputRetriever; + this.onClose = onClose; + this.reducer = reducer; + + heap = new Candidate[sources.size()]; + size = 0; + + for (int i = 0; i < sources.size(); i++) + { + Candidate candidate = new Candidate<>(i, sources.get(i), comparator); + heap[size++] = candidate; + } + needingAdvance = size; + } + + public boolean hasNext() + { + advance(); // no-op if already advanced + return size > 0; + } + + public Out next() + { + advance(); // no-op if already advanced (e.g. hasNext() called) + assert size > 0; + return consume(); + } + + public In nonReducingNext() + { + advance(); // no-op if already advanced (e.g. hasNext() called) + assert size > 0; + return consumeOne(); + } + + public void close() + { + if (onClose == null) + return; + + Throwable t = null; + for (Candidate c : heap) + { + try + { + onClose.accept(c.input); + } + catch (Throwable e) + { + t = Throwables.merge(t, e); + } + } + Throwables.maybeFail(t); + } + + + /** + * Advance all sources that need to be advanced and place them into suitable positions in the heap. + * + * By walking the sources backwards we know that everything after the point being processed already forms + * correctly ordered subheaps, thus we can build a subheap rooted at the current position by only sinking down + * the newly advanced source. Because all parents of a consumed source are also consumed there is no way + * that we can process one consumed source but skip over its parent. + * + * The procedure is the same as the one used for the initial building of a heap in the heapsort algorithm and + * has a maximum number of comparisons {@code (2 * log(size) + SORTED_SECTION_SIZE / 2)} multiplied by the + * number of sources whose items were consumed at the previous step, but is also at most linear in the size of + * the heap if the number of consumed elements is high (as it is in the initial heap construction). With non- or + * lightly-overlapping sources the procedure finishes after just one (resp. a couple of) comparisons. + */ + private void advance() + { + // Turn the set of candidates into a heap. + for (int i = needingAdvance - 1; i >= 0; --i) + { + Candidate candidate = heap[i]; + /** + * needingAdvance runs to the maximum index (and deepest-right node) that may need advancing; + * since the equal items that were consumed at-once may occur in sub-heap "veins" of equality, + * not all items above this deepest-right position may have been consumed; these already form + * valid sub-heaps and can be skipped-over entirely + */ + if (candidate.needsAdvance()) + replaceAndSink(candidate, !candidate.advance(inputRetriever), i); + } + needingAdvance = 0; + } + + /** + * Consume all items that sort like the current top of the heap. As we cannot advance the sources to let + * equivalent items pop up, we walk the heap to find them and mark them as needing advance. + * + * This relies on the equalParent flag to avoid doing any comparisons. + */ + private Out consume() + { + reducer.onKeyChange(); + assert !heap[0].equalParent; + heap[0].consume(reducer); + final int size = this.size; + final int sortedSectionSize = Math.min(size, SORTED_SECTION_SIZE); + int i; + consume: { + for (i = 1; i < sortedSectionSize; ++i) + { + if (!heap[i].equalParent) + break consume; + heap[i].consume(reducer); + } + i = Math.max(i, consumeHeap(i) + 1); + } + needingAdvance = i; + return reducer.getReduced(); + } + + /** + * Consume only the top item, regardless if there are others that sort like it on the heap. + * No reducer is required for this. + */ + private In consumeOne() + { + needingAdvance = 1; + return heap[0].consumeItem(); + } + + /** + * Recursively consume all items equal to equalItem in the binary subheap rooted at position idx. + * + * @return the largest equal index found in this search. + */ + private int consumeHeap(int idx) + { + if (idx >= size || !heap[idx].equalParent) + return -1; + + heap[idx].consume(reducer); + int nextIdx = (idx << 1) - (SORTED_SECTION_SIZE - 1); + return Math.max(idx, Math.max(consumeHeap(nextIdx), consumeHeap(nextIdx + 1))); + } + + /** + * Replace a source in the heap with the given position and move it down the heap until it finds its proper + * position, pulling lighter elements up the heap. + * + * Whenever an equality is found between two elements that form a new parent-child relationship, the child's + * equalParent flag is set to true if the elements are equal. + */ + private void replaceAndSink(Candidate candidate, boolean candidateDone, int currIdx) + { + if (candidateDone) + { + // Drop source by swapping it with the last one in the heap. + Candidate replacement = heap[--size]; + heap[size] = candidate; + candidate = replacement; + } + // The new element will be top of its heap, at this point there is no parent to be equal to. + candidate.equalParent = false; + + final int size = this.size; + final int sortedSectionSize = Math.min(size - 1, SORTED_SECTION_SIZE); + + int nextIdx; + + // Advance within the sorted section, pulling up items lighter than candidate. + while ((nextIdx = currIdx + 1) <= sortedSectionSize) + { + if (!heap[nextIdx].equalParent) // if we were greater then an (or were the) equal parent, we are >= the child + { + int cmp = candidate.compareTo(heap[nextIdx]); + if (cmp <= 0) + { + heap[nextIdx].equalParent = cmp == 0; + heap[currIdx] = candidate; + return; + } + } + + heap[currIdx] = heap[nextIdx]; + currIdx = nextIdx; + } + // If size <= SORTED_SECTION_SIZE, nextIdx below will be no less than size, + // because currIdx == sortedSectionSize == size - 1 and nextIdx becomes + // (size - 1) * 2) - (size - 1 - 1) == size. + + // Advance in the binary heap, pulling up the lighter element from the two at each level. + while ((nextIdx = (currIdx * 2) - (sortedSectionSize - 1)) + 1 < size) + { + if (!heap[nextIdx].equalParent) + { + if (!heap[nextIdx + 1].equalParent) + { + // pick the smallest of the two children + int siblingCmp = heap[nextIdx + 1].compareTo(heap[nextIdx]); + if (siblingCmp < 0) + ++nextIdx; + + // if we're smaller than this, we are done, and must only restore the heap and equalParent properties + int cmp = candidate.compareTo(heap[nextIdx]); + if (cmp <= 0) + { + if (cmp == 0) + { + heap[nextIdx].equalParent = true; + if (siblingCmp == 0) // siblingCmp == 0 => nextIdx is the left child + heap[nextIdx + 1].equalParent = true; + } + + heap[currIdx] = candidate; + return; + } + + if (siblingCmp == 0) + { + // siblingCmp == 0 => nextIdx is still the left child + // if the two siblings were equal, and we are inserting something greater, we will + // pull up the left one; this means the right gets an equalParent + heap[nextIdx + 1].equalParent = true; + } + } + else + ++nextIdx; // descend down the path where we found the equal child + } + + heap[currIdx] = heap[nextIdx]; + currIdx = nextIdx; + } + + // our loop guard ensures there are always two siblings to process; typically when we exit the loop we will + // be well past the end of the heap and this next condition will match... + if (nextIdx >= size) + { + heap[currIdx] = candidate; + return; + } + + // ... but sometimes we will have one last child to compare against, that has no siblings + if (!heap[nextIdx].equalParent) + { + int cmp = candidate.compareTo(heap[nextIdx]); + if (cmp <= 0) + { + heap[nextIdx].equalParent = cmp == 0; + heap[currIdx] = candidate; + return; + } + } + + heap[currIdx] = heap[nextIdx]; + heap[nextIdx] = candidate; + } + + /** + * Returns an iterable listing all the sources of this merger. + */ + public Iterable allSources() + { + return () -> new Iterator() + { + int index = 0; + + public boolean hasNext() + { + return index < heap.length; + } + + public Source next() + { + return heap[index++].input; + } + }; + } + + /** + * Returns an iterable that lists all inputs that are positioned after the current position, + * i.e. all inputs that are not equal to the current top. + * Meant to be called inside getReduced. + */ + public Iterable allGreaterValues() + { + return () -> new AbstractIterator() + { + int index = 1; // skip first item, it's always equal + + protected In computeNext() + { + while (true) + { + if (index >= size) + return endOfData(); + Candidate candidate = heap[index++]; + if (!candidate.needsAdvance()) + return candidate.item; + } + } + }; + } + + // Holds and is comparable by the head item of a source it owns + protected static final class Candidate implements Comparable> + { + private final Source input; + private final Comparator comp; + private final int idx; + private In item; + boolean equalParent; + + public Candidate(int idx, Source input, Comparator comp) + { + this.input = input; + this.comp = comp; + this.idx = idx; + } + + /** Advance this source and returns true if it had an item, i.e. was not exhausted. */ + protected boolean advance(Function inputRetriever) + { + item = inputRetriever.apply(input); + return item != null; + } + + public int compareTo(Candidate that) + { + assert this.item != null && that.item != null; + return comp.compare(this.item, that.item); + } + + public void consume(Reducer reducer) + { + reducer.reduce(idx, consumeItem()); + } + + public In consumeItem() + { + In v = item; + item = null; + return v; + } + + public boolean needsAdvance() + { + return item == null; + } + } +} diff --git a/src/java/org/apache/cassandra/utils/MonotonicClock.java b/src/java/org/apache/cassandra/utils/MonotonicClock.java index 7be54c008b7f..73e957a7ad88 100644 --- a/src/java/org/apache/cassandra/utils/MonotonicClock.java +++ b/src/java/org/apache/cassandra/utils/MonotonicClock.java @@ -92,7 +92,7 @@ private static MonotonicClock precise() try { logger.debug("Using custom clock implementation: {}", sclock); - return (MonotonicClock) Class.forName(sclock).newInstance(); + return FBUtilities.construct(sclock, "monotonic clock", MonotonicClock.class); } catch (Exception e) { @@ -111,7 +111,8 @@ private static MonotonicClock approx(MonotonicClock precise) try { logger.debug("Using custom clock implementation: {}", sclock); - Class clazz = (Class) Class.forName(sclock); + Class clazz = + FBUtilities.classForNameWithoutInitialization(sclock, "monotonic clock", MonotonicClock.class); if (SystemClock.class.equals(clazz) && SystemClock.class.equals(precise.getClass())) return precise; @@ -205,7 +206,7 @@ public synchronized void resumeEpochSampling() if (almostSameTimeUpdater != null) throw new IllegalStateException("Already running"); updateAlmostSameTime(); - logger.info("Scheduling approximate time conversion task with an interval of {} milliseconds", UPDATE_INTERVAL_MS); + logger.debug("Scheduling approximate time conversion task with an interval of {} milliseconds", UPDATE_INTERVAL_MS); almostSameTimeUpdater = ScheduledExecutors.scheduledFastTasks.scheduleWithFixedDelay(this::updateAlmostSameTime, UPDATE_INTERVAL_MS, UPDATE_INTERVAL_MS, MILLISECONDS); } @@ -268,13 +269,13 @@ public long error() @Override public boolean isAfter(long instant) { - return now() > instant; + return instant - now() < 0; } @Override public boolean isAfter(long now, long instant) { - return now > instant; + return instant - now < 0; } } @@ -341,7 +342,7 @@ public synchronized void resumeNowSampling() throw new IllegalStateException("Already running"); almostNow = precise.now(); - logger.info("Scheduling approximate time-check task with a precision of {} milliseconds", UPDATE_INTERVAL_MS); + logger.debug("Scheduling approximate time-check task with a precision of {} milliseconds", UPDATE_INTERVAL_MS); almostNowUpdater = ScheduledExecutors.scheduledFastTasks.scheduleWithFixedDelay(() -> almostNow = precise.now(), UPDATE_INTERVAL_MS, UPDATE_INTERVAL_MS, MILLISECONDS); } diff --git a/src/java/org/apache/cassandra/utils/NativeLibrary.java b/src/java/org/apache/cassandra/utils/NativeLibrary.java index 934843393975..538019fd1d5d 100644 --- a/src/java/org/apache/cassandra/utils/NativeLibrary.java +++ b/src/java/org/apache/cassandra/utils/NativeLibrary.java @@ -20,6 +20,8 @@ import java.io.FileDescriptor; import java.io.IOException; import java.lang.reflect.Field; +import java.nio.MappedByteBuffer; +import java.nio.channels.AsynchronousFileChannel; import java.nio.channels.FileChannel; import java.util.concurrent.TimeUnit; @@ -28,30 +30,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.sun.jna.LastErrorException; - +import com.sun.jna.Native; +import com.sun.jna.Pointer; import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.util.PageAware; +import org.apache.cassandra.utils.NativeLibraryWrapper.NativeError; import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORE_MISSING_NATIVE_FILE_HINTS; import static org.apache.cassandra.config.CassandraRelevantProperties.OS_ARCH; import static org.apache.cassandra.config.CassandraRelevantProperties.OS_NAME; -import static org.apache.cassandra.utils.NativeLibrary.OSType.LINUX; -import static org.apache.cassandra.utils.NativeLibrary.OSType.MAC; -import static org.apache.cassandra.utils.NativeLibrary.OSType.AIX; +import static org.apache.cassandra.utils.INativeLibrary.OSType.AIX; +import static org.apache.cassandra.utils.INativeLibrary.OSType.LINUX; +import static org.apache.cassandra.utils.INativeLibrary.OSType.MAC; -public final class NativeLibrary +public class NativeLibrary implements INativeLibrary { private static final Logger logger = LoggerFactory.getLogger(NativeLibrary.class); private static final boolean REQUIRE = !IGNORE_MISSING_NATIVE_FILE_HINTS.getBoolean(); - public enum OSType - { - LINUX, - MAC, - AIX, - OTHER; - } - public static final OSType osType; private static final int MCL_CURRENT; @@ -72,11 +68,18 @@ public enum OSType private static final int POSIX_FADV_DONTNEED = 4; /* fadvise.h */ private static final int POSIX_FADV_NOREUSE = 5; /* fadvise.h */ + private static final int MADV_NORMAL = 0; /* mman.h */ + private static final int MADV_RANDOM = 1; /* mman.h */ + private static final int MADV_SEQUENTIAL = 2; /* mman.h */ + private static final int MADV_WILLNEED = 3; /* mman.h */ + private static final int MADV_DONTNEED = 4; /* mman.h */ + private static final NativeLibraryWrapper wrappedLibrary; private static boolean jnaLockable = false; private static final Field FILE_DESCRIPTOR_FD_FIELD; private static final Field FILE_CHANNEL_FD_FIELD; + private static final Field FILE_ASYNC_CHANNEL_FD_FIELD; static { @@ -84,6 +87,7 @@ public enum OSType try { FILE_CHANNEL_FD_FIELD = FBUtilities.getProtectedField(Class.forName("sun.nio.ch.FileChannelImpl"), "fd"); + FILE_ASYNC_CHANNEL_FD_FIELD = FBUtilities.getProtectedField(Class.forName("sun.nio.ch.AsynchronousFileChannelImpl"), "fdObj"); } catch (ClassNotFoundException e) { @@ -127,7 +131,7 @@ else if (osType == AIX) } } - private NativeLibrary() {} + NativeLibrary() {} /** * @return the detected OSType of the Operating System running the JVM using crude string matching @@ -148,36 +152,26 @@ else if (osName.contains("mac")) return LINUX; } - private static int errno(RuntimeException e) + @Override + public boolean isOS(INativeLibrary.OSType type) { - assert e instanceof LastErrorException; - try - { - return ((LastErrorException) e).getErrorCode(); - } - catch (NoSuchMethodError x) - { - if (REQUIRE) - logger.warn("Obsolete version of JNA present; unable to read errno. Upgrade to JNA 3.2.7 or later"); - return 0; - } + return osType == type; } - /** - * Checks if the library has been successfully linked. - * @return {@code true} if the library has been successfully linked, {@code false} otherwise. - */ - public static boolean isAvailable() + @Override + public boolean isAvailable() { return wrappedLibrary.isAvailable(); } - public static boolean jnaMemoryLockable() + @Override + public boolean jnaMemoryLockable() { return jnaLockable; } - public static void tryMlockall() + @Override + public void tryMlockall() { try { @@ -189,12 +183,9 @@ public static void tryMlockall() { // this will have already been logged by CLibrary, no need to repeat it } - catch (RuntimeException e) + catch (NativeError e) { - if (!(e instanceof LastErrorException)) - throw e; - - if (errno(e) == ENOMEM && osType == LINUX) + if (e.getErrno() == ENOMEM && osType == LINUX) { logger.warn("Unable to lock JVM memory (ENOMEM)." + " This can result in part of the JVM being swapped out, especially with mmapped I/O enabled." @@ -203,125 +194,153 @@ public static void tryMlockall() else if (osType != MAC) { // OS X allows mlockall to be called, but always returns an error - logger.warn("Unknown mlockall error {}", errno(e)); + logger.error("Unknown mlockall error", e); } } } - public static void trySkipCache(String path, long offset, long len) + @Override + public void trySkipCache(File f, long offset, long len) { - File f = new File(path); if (!f.exists()) return; try (FileInputStreamPlus fis = new FileInputStreamPlus(f)) { - trySkipCache(getfd(fis.getChannel()), offset, len, path); + trySkipCache(getfd(fis.getChannel()), offset, len, f.path()); } catch (IOException e) { - logger.warn("Could not skip cache", e); + logger.error("Could not open file to skip cache", e); } } - public static void trySkipCache(int fd, long offset, long len, String path) + @Override + public void trySkipCache(int fd, long offset, long len, String fileName) { if (len == 0) - trySkipCache(fd, 0, 0, path); + trySkipCache(fd, 0, 0, fileName); while (len > 0) { int sublen = (int) Math.min(Integer.MAX_VALUE, len); - trySkipCache(fd, offset, sublen, path); + trySkipCache(fd, offset, sublen, fileName); len -= sublen; offset -= sublen; } } - public static void trySkipCache(int fd, long offset, int len, String path) + @Override + public void trySkipCache(int fd, long offset, int len, String fileName) { if (fd < 0) return; try { - if (osType == LINUX) - { - int result = wrappedLibrary.callPosixFadvise(fd, offset, len, POSIX_FADV_DONTNEED); - if (result != 0) - NoSpamLogger.log( - logger, - NoSpamLogger.Level.WARN, - 10, - TimeUnit.MINUTES, - "Failed trySkipCache on file: {} Error: " + wrappedLibrary.callStrerror(result).getString(0), - path); - } + wrappedLibrary.callPosixFadvise(fd, offset, len, POSIX_FADV_DONTNEED); } catch (UnsatisfiedLinkError e) { // if JNA is unavailable just skipping Direct I/O // instance of this class will act like normal RandomAccessFile } - catch (RuntimeException e) + catch (NativeError e) { - if (!(e instanceof LastErrorException)) - throw e; - - logger.warn("posix_fadvise({}, {}) failed, errno ({}).", fd, offset, errno(e)); + NoSpamLogger.log(logger, + NoSpamLogger.Level.ERROR, + 10, + TimeUnit.MINUTES, + "Failed trySkipCache on file: {} Error: " + e.getMessage(), + fileName); } } - public static int tryFcntl(int fd, int command, int flags) - { - // fcntl return value may or may not be useful, depending on the command - int result = -1; + /** + * @param buffer + * @param length + * @param filename -- source file backing buffer; logged on error + *

    + * adviseRandom works even on buffers that are not aligned to page boundaries (which is the + * common case for how MmappedRegions is used). + */ + public void adviseRandom(MappedByteBuffer buffer, long length, String filename) { + assert buffer != null; + + var rawAddress = Native.getDirectBufferPointer(buffer); + // align to the nearest lower page boundary + var alignedAddress = new Pointer(Pointer.nativeValue(rawAddress) & -PageAware.PAGE_SIZE); + // we want to advise the whole buffer, so if the aligned address is lower than the raw one, + // we need to pad the length accordingly. (we do not need to align `length`, Linux + // takes care of rounding it up for us.) + length += Pointer.nativeValue(rawAddress) - Pointer.nativeValue(alignedAddress); try { - result = wrappedLibrary.callFcntl(fd, command, flags); + wrappedLibrary.callPosixMadvise(alignedAddress, length, MADV_RANDOM); } catch (UnsatisfiedLinkError e) { - // if JNA is unavailable just skipping + // if JNA is unavailable just skipping Direct I/O + // instance of this class will act like normal RandomAccessFile } - catch (RuntimeException e) + catch (NativeError e) { - if (!(e instanceof LastErrorException)) - throw e; + NoSpamLogger.log(logger, + NoSpamLogger.Level.ERROR, + 10, + TimeUnit.MINUTES, + "Failed madvise on file: {}. Error: " + e.getMessage(), + filename); + } + } + @Override + public int tryFcntl(int fd, int command, int flags) + { + try + { + return wrappedLibrary.callFcntl(fd, command, flags); + } + catch (UnsatisfiedLinkError e) + { + // Unsupported on this platform + } + catch (NativeError e) + { if (REQUIRE) - logger.warn("fcntl({}, {}, {}) failed, errno ({}).", fd, command, flags, errno(e)); + logger.error("fcntl({}, {}, {}) failed, error {}", fd, command, flags, e.getMessage()); } - - return result; + return -1; } - public static int tryOpenDirectory(String path) + @Override + public int tryOpenDirectory(File file) { - int fd = -1; + return tryOpenDirectory(file.path()); + } + @Override + public int tryOpenDirectory(String path) + { try { return wrappedLibrary.callOpen(path, O_RDONLY); } catch (UnsatisfiedLinkError e) { - // JNA is unavailable just skipping Direct I/O + // Unsupported on this platform } - catch (RuntimeException e) + catch (NativeError e) { - if (!(e instanceof LastErrorException)) - throw e; - if (REQUIRE) - logger.warn("open({}, O_RDONLY) failed, errno ({}).", path, errno(e)); + logger.error("open({}, O_RDONLY) failed, error {}", path, e.getMessage()); } - - return fd; + return -1; } - public static void trySync(int fd) + @Override + public void trySync(int fd) { if (fd == -1) return; @@ -334,21 +353,19 @@ public static void trySync(int fd) { // JNA is unavailable just skipping Direct I/O } - catch (RuntimeException e) + catch (NativeError e) { - if (!(e instanceof LastErrorException)) - throw e; - if (REQUIRE) { - String errMsg = String.format("fsync(%s) failed, errno (%s) %s", fd, errno(e), e.getMessage()); + String errMsg = String.format("fsync(%s) failed, error %s", fd, e.getMessage()); logger.warn(errMsg); throw new FSWriteError(e, errMsg); } } } - public static void tryCloseFD(int fd) + @Override + public void tryCloseFD(int fd) { if (fd == -1) return; @@ -361,21 +378,46 @@ public static void tryCloseFD(int fd) { // JNA is unavailable just skipping Direct I/O } - catch (RuntimeException e) + catch (NativeError e) { - if (!(e instanceof LastErrorException)) - throw e; - if (REQUIRE) { - String errMsg = String.format("close(%d) failed, errno (%d).", fd, errno(e)); + String errMsg = String.format("close(%d) failed, error %s", fd, e.getMessage()); logger.warn(errMsg); throw new FSWriteError(e, errMsg); } } } - public static int getfd(FileChannel channel) + @Override + public int getfd(AsynchronousFileChannel channel) + { + try + { + return getfd((FileDescriptor) FILE_ASYNC_CHANNEL_FD_FIELD.get(channel)); + } + catch (IllegalArgumentException|IllegalAccessException e) + { + logger.error("Unable to read fd field from FileChannel"); + } + return -1; + } + + @Override + public FileDescriptor getFileDescriptor(AsynchronousFileChannel channel) + { + try + { + return (FileDescriptor) FILE_ASYNC_CHANNEL_FD_FIELD.get(channel); + } + catch (IllegalArgumentException | IllegalAccessException e) + { + throw new RuntimeException(e); + } + } + + @Override + public int getfd(FileChannel channel) { try { @@ -384,7 +426,7 @@ public static int getfd(FileChannel channel) catch (IllegalArgumentException|IllegalAccessException e) { if (REQUIRE) - logger.warn("Unable to read fd field from FileChannel", e); + logger.error("Unable to read fd field from FileChannel"); } return -1; } @@ -394,7 +436,8 @@ public static int getfd(FileChannel channel) * @param descriptor - FileDescriptor objec to get fd from * @return file descriptor, -1 or error */ - public static int getfd(FileDescriptor descriptor) + @Override + public int getfd(FileDescriptor descriptor) { try { @@ -405,7 +448,7 @@ public static int getfd(FileDescriptor descriptor) if (REQUIRE) { JVMStabilityInspector.inspectThrowable(e); - logger.warn("Unable to read fd field from FileDescriptor", e); + logger.error("Unable to read fd field from FileDescriptor"); } } @@ -415,22 +458,32 @@ public static int getfd(FileDescriptor descriptor) /** * @return the PID of the JVM or -1 if we failed to get the PID */ - public static long getProcessID() + @Override + public long getProcessID() { try { return wrappedLibrary.callGetpid(); } - catch (UnsatisfiedLinkError e) - { - // if JNA is unavailable just skipping - } - catch (Exception e) + catch (NativeError e) { if (REQUIRE) - logger.info("Failed to get PID from JNA", e); + logger.error("Failed to get PID from JNA", e); } return -1; } + + @Override + public FileDescriptor getFileDescriptor(FileChannel channel) + { + try + { + return (FileDescriptor)FILE_CHANNEL_FD_FIELD.get(channel); + } + catch (IllegalArgumentException | IllegalAccessException e) + { + throw new RuntimeException(e); + } + } } diff --git a/src/java/org/apache/cassandra/utils/NativeLibraryDarwin.java b/src/java/org/apache/cassandra/utils/NativeLibraryDarwin.java index c1193113700f..a6cf95911568 100644 --- a/src/java/org/apache/cassandra/utils/NativeLibraryDarwin.java +++ b/src/java/org/apache/cassandra/utils/NativeLibraryDarwin.java @@ -23,7 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.sun.jna.LastErrorException; import com.sun.jna.Native; import com.sun.jna.Pointer; @@ -40,7 +39,7 @@ * unavailable simply because of one native defined method not supported * on the runtime operating system. * @see org.apache.cassandra.utils.NativeLibraryWrapper - * @see NativeLibrary + * @see INativeLibrary */ @Shared public class NativeLibraryDarwin implements NativeLibraryWrapper @@ -70,61 +69,93 @@ public class NativeLibraryDarwin implements NativeLibraryWrapper } } - private static native int mlockall(int flags) throws LastErrorException; - private static native int munlockall() throws LastErrorException; - private static native int fcntl(int fd, int command, long flags) throws LastErrorException; - private static native int open(String path, int flags) throws LastErrorException; - private static native int fsync(int fd) throws LastErrorException; - private static native int close(int fd) throws LastErrorException; - private static native Pointer strerror(int errnum) throws LastErrorException; - private static native long getpid() throws LastErrorException; + private static native int mlockall(int flags); + private static native int munlockall(); + private static native int fcntl(int fd, int command, long flags); + private static native int open(String path, int flags); + private static native int fsync(int fd); + private static native int close(int fd); + private static native Pointer strerror(int errnum); + private static native long getpid(); - public int callMlockall(int flags) throws UnsatisfiedLinkError, RuntimeException + private void throwNativeError() throws NativeError { - return mlockall(flags); + var errno = Native.getLastError(); + throw new NativeError(strerror(errno).getString(0), errno); } - public int callMunlockall() throws UnsatisfiedLinkError, RuntimeException + @Override + public int callMlockall(int flags) throws NativeError { - return munlockall(); + if (0 != mlockall(flags)) + throwNativeError(); + return 0; } - public int callFcntl(int fd, int command, long flags) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callMunlockall() throws NativeError { - return fcntl(fd, command, flags); + if (0 != munlockall()) + throwNativeError(); + return 0; } - public int callPosixFadvise(int fd, long offset, int len, int flag) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callFcntl(int fd, int command, long flags) throws NativeError + { + int r = fcntl(fd, command, flags); + if (r < 0) + throwNativeError(); + return r; + } + + @Override + public int callPosixFadvise(int fd, long offset, int len, int flag) { - // posix_fadvise is not available on Darwin/Mac throw new UnsatisfiedLinkError(); } - public int callOpen(String path, int flags) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callPosixMadvise(Pointer addr, long length, int advice) { - return open(path, flags); + throw new UnsatisfiedLinkError(); } - public int callFsync(int fd) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callOpen(String path, int flags) throws NativeError { - return fsync(fd); + int r = open(path, flags); + if (r < 0) + throwNativeError(); + return r; } - public int callClose(int fd) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callFsync(int fd) throws NativeError { - return close(fd); + if (0 != fsync(fd)) + throwNativeError(); + return 0; } - public Pointer callStrerror(int errnum) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callClose(int fd) throws NativeError { - return strerror(errnum); + if (0 != close(fd)) + throwNativeError(); + return 0; } - public long callGetpid() throws UnsatisfiedLinkError, RuntimeException + @Override + public long callGetpid() throws NativeError { - return getpid(); + long r = getpid(); + if (r < 0) + throwNativeError(); + return r; } + @Override public boolean isAvailable() { return available; diff --git a/src/java/org/apache/cassandra/utils/NativeLibraryLinux.java b/src/java/org/apache/cassandra/utils/NativeLibraryLinux.java index 9c7bb3b73b11..e0f43b160816 100644 --- a/src/java/org/apache/cassandra/utils/NativeLibraryLinux.java +++ b/src/java/org/apache/cassandra/utils/NativeLibraryLinux.java @@ -23,7 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.sun.jna.LastErrorException; import com.sun.jna.Native; import com.sun.jna.Pointer; @@ -40,7 +39,7 @@ * unavailable simply because of one native defined method not supported * on the runtime operating system. * @see org.apache.cassandra.utils.NativeLibraryWrapper - * @see NativeLibrary + * @see INativeLibrary */ @Shared public class NativeLibraryLinux implements NativeLibraryWrapper @@ -70,61 +69,99 @@ public class NativeLibraryLinux implements NativeLibraryWrapper } } - private static native int mlockall(int flags) throws LastErrorException; - private static native int munlockall() throws LastErrorException; - private static native int fcntl(int fd, int command, long flags) throws LastErrorException; - private static native int posix_fadvise(int fd, long offset, int len, int flag) throws LastErrorException; - private static native int open(String path, int flags) throws LastErrorException; - private static native int fsync(int fd) throws LastErrorException; - private static native int close(int fd) throws LastErrorException; - private static native Pointer strerror(int errnum) throws LastErrorException; - private static native long getpid() throws LastErrorException; - - public int callMlockall(int flags) throws UnsatisfiedLinkError, RuntimeException + private static native int mlockall(int flags); + private static native int munlockall(); + private static native int fcntl(int fd, int command, long flags); + private static native int posix_fadvise(int fd, long offset, int len, int flag); + private static native int posix_madvise(Pointer addr, long length, int advice); + private static native int open(String path, int flags); + private static native int fsync(int fd); + private static native int close(int fd); + private static native Pointer strerror(int errnum); + private static native long getpid(); + + private void throwNativeError() throws NativeError { - return mlockall(flags); + var errno = Native.getLastError(); + throw new NativeError(strerror(errno).getString(0), errno); } - public int callMunlockall() throws UnsatisfiedLinkError, RuntimeException + @Override + public int callMlockall(int flags) throws NativeError { - return munlockall(); + if (0 != mlockall(flags)) + throwNativeError(); + return 0; } - public int callFcntl(int fd, int command, long flags) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callMunlockall() throws NativeError { - return fcntl(fd, command, flags); + if (0 != munlockall()) + throwNativeError(); + return 0; } - public int callPosixFadvise(int fd, long offset, int len, int flag) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callFcntl(int fd, int command, long flags) throws NativeError { - return posix_fadvise(fd, offset, len, flag); + int r = fcntl(fd, command, flags); + if (r < 0) + throwNativeError(); + return r; } - public int callOpen(String path, int flags) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callPosixFadvise(int fd, long offset, int len, int flag) throws NativeError { - return open(path, flags); + if (0 != posix_fadvise(fd, offset, len, flag)) + throwNativeError(); + return 0; } - public int callFsync(int fd) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callPosixMadvise(Pointer addr, long length, int advice) throws NativeError { - return fsync(fd); + if (0 != posix_madvise(addr, length, advice)) + throwNativeError(); + return 0; } - public int callClose(int fd) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callOpen(String path, int flags) throws NativeError { - return close(fd); + int r = open(path, flags); + if (r < 0) + throwNativeError(); + return r; } - public Pointer callStrerror(int errnum) throws UnsatisfiedLinkError, RuntimeException + @Override + public int callFsync(int fd) throws NativeError { - return strerror(errnum); + if (0 != fsync(fd)) + throwNativeError(); + return 0; } - public long callGetpid() throws UnsatisfiedLinkError, RuntimeException + @Override + public int callClose(int fd) throws NativeError { - return getpid(); + if (0 != close(fd)) + throwNativeError(); + return 0; } + @Override + public long callGetpid() throws NativeError + { + long r = getpid(); + if (r < 0) + throwNativeError(); + return r; + } + + @Override public boolean isAvailable() { return available; diff --git a/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java b/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java new file mode 100644 index 000000000000..497dd074219a --- /dev/null +++ b/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +/** + * A {@code NativeLibraryWrapper} implementation for Windows. + *

    This implementation only offers support for the {@code callGetpid} method + * using the Windows/Kernel32 library.

    + * + * @see org.apache.cassandra.utils.NativeLibraryWrapper + * @see INativeLibrary + */ +@Shared +public class NativeLibraryWindows implements NativeLibraryWrapper +{ + private static final Logger logger = LoggerFactory.getLogger(NativeLibraryWindows.class); + + private static boolean available; + + static + { + try + { + Native.register(com.sun.jna.NativeLibrary.getInstance("kernel32", Collections.emptyMap())); + available = true; + } + catch (NoClassDefFoundError e) + { + logger.warn("JNA not found. Native methods will be disabled."); + } + catch (UnsatisfiedLinkError e) + { + logger.error("Failed to link the Windows/Kernel32 library against JNA. Native methods will be unavailable.", e); + } + catch (NoSuchMethodError e) + { + logger.warn("Obsolete version of JNA present; unable to register Windows/Kernel32 library. Upgrade to JNA 3.2.7 or later"); + } + } + + /** + * Retrieves the process identifier of the calling process (GetCurrentProcessId function). + * + * @return the process identifier of the calling process + */ + private static native long GetCurrentProcessId(); + + private void throwNativeError() throws NativeError + { + var errno = Native.getLastError(); + // TODO figure out how to get a human-readable error message on Windows + throw new NativeError(String.valueOf(errno), errno); + } + + @Override + public int callMlockall(int flags) + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callMunlockall() + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callFcntl(int fd, int command, long flags) + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callPosixFadvise(int fd, long offset, int len, int flag) + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callPosixMadvise(Pointer addr, long length, int advice) + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callOpen(String path, int flags) + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callFsync(int fd) + { + throw new UnsatisfiedLinkError(); + } + + @Override + public int callClose(int fd) + { + throw new UnsatisfiedLinkError(); + } + + /** + * @return the PID of the JVM running + */ + @Override + public long callGetpid() throws NativeError + { + long r = GetCurrentProcessId(); + if (r < 0) + throwNativeError(); + return r; + } + + @Override + public boolean isAvailable() + { + return available; + } +} diff --git a/src/java/org/apache/cassandra/utils/NativeLibraryWrapper.java b/src/java/org/apache/cassandra/utils/NativeLibraryWrapper.java index 2c3d47fa162c..796c8d940ff3 100644 --- a/src/java/org/apache/cassandra/utils/NativeLibraryWrapper.java +++ b/src/java/org/apache/cassandra/utils/NativeLibraryWrapper.java @@ -22,9 +22,15 @@ /** * An interface to implement for using OS specific native methods. - * @see NativeLibrary + * @see INativeLibrary */ @Shared +// Implementors are advised to NOT use JNA's convenient LastErrorException because it relies +// on checking errno(), which is not reliable. Linux man page explains, +// The value in errno is significant only when the return value of +// the call indicated an error (i.e., -1 from most system calls; -1 +// or NULL from most library functions); ***a function that succeeds is +// allowed to change errno.*** public interface NativeLibraryWrapper { /** @@ -33,13 +39,33 @@ public interface NativeLibraryWrapper */ boolean isAvailable(); - int callMlockall(int flags) throws UnsatisfiedLinkError, RuntimeException; - int callMunlockall() throws UnsatisfiedLinkError, RuntimeException; - int callFcntl(int fd, int command, long flags) throws UnsatisfiedLinkError, RuntimeException; - int callPosixFadvise(int fd, long offset, int len, int flag) throws UnsatisfiedLinkError, RuntimeException; - int callOpen(String path, int flags) throws UnsatisfiedLinkError, RuntimeException; - int callFsync(int fd) throws UnsatisfiedLinkError, RuntimeException; - int callClose(int fd) throws UnsatisfiedLinkError, RuntimeException; - Pointer callStrerror(int errnum) throws UnsatisfiedLinkError, RuntimeException; - long callGetpid() throws UnsatisfiedLinkError, RuntimeException; + int callMlockall(int flags) throws UnsatisfiedLinkError, NativeError; + int callMunlockall() throws UnsatisfiedLinkError, NativeError; + int callFcntl(int fd, int command, long flags) throws UnsatisfiedLinkError, NativeError; + int callPosixFadvise(int fd, long offset, int len, int flag) throws UnsatisfiedLinkError, NativeError; + int callPosixMadvise(Pointer addr, long length, int advice) throws UnsatisfiedLinkError, NativeError; + int callOpen(String path, int flags) throws UnsatisfiedLinkError, NativeError; + int callFsync(int fd) throws UnsatisfiedLinkError, NativeError; + int callClose(int fd) throws UnsatisfiedLinkError, NativeError; + long callGetpid() throws NativeError; + + /** + * This is a checked exception because the correct handling of the error is almost + * always to log it and move on, not to propagate it up the stack. + */ + class NativeError extends Exception + { + private final int errno; + + public NativeError(String nativeMessage, int errno) + { + super(nativeMessage); + this.errno = errno; + } + + public int getErrno() + { + return errno; + } + } } diff --git a/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java b/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java index 19dcc23dcf3a..cd5454284ac3 100644 --- a/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java +++ b/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java @@ -17,23 +17,42 @@ */ package org.apache.cassandra.utils; -import java.nio.ByteBuffer; import java.net.InetSocketAddress; -import java.util.*; - -import com.datastax.driver.core.*; - -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.schema.*; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.ColumnMetadata.ClusteringOrder; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.datastax.driver.core.AuthProvider; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Host; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PlainTextAuthProvider; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.SSLOptions; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.TokenRange; import org.apache.cassandra.cql3.ColumnIdentifier; -import org.apache.cassandra.db.marshal.*; -import org.apache.cassandra.dht.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.ReversedType; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.dht.Token.TokenFactory; import org.apache.cassandra.io.sstable.SSTableLoader; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.CQLTypeParser; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.ColumnMetadata.ClusteringOrder; +import org.apache.cassandra.schema.DroppedColumn; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SchemaKeyspaceTables; +import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.schema.Types; public class NativeSSTableLoaderClient extends SSTableLoader.Client { @@ -220,7 +239,7 @@ private static DroppedColumn createDroppedColumnFromRow(Row row, String keyspace String name = row.getString("column_name"); AbstractType type = CQLTypeParser.parse(keyspace, row.getString("type"), Types.none()); ColumnMetadata.Kind kind = ColumnMetadata.Kind.valueOf(row.getString("kind").toUpperCase()); - ColumnMetadata column = new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, ColumnMetadata.NO_POSITION, kind, null); + ColumnMetadata column = ColumnMetadata.droppedColumn(keyspace, table, ColumnIdentifier.getInterned(name, true), type, kind, null); long droppedTime = row.getTimestamp("dropped_time").getTime(); return new DroppedColumn(column, droppedTime); } diff --git a/src/java/org/apache/cassandra/utils/NoSpamLogger.java b/src/java/org/apache/cassandra/utils/NoSpamLogger.java index 0a13f6b2a5ae..729bea3575bb 100644 --- a/src/java/org/apache/cassandra/utils/NoSpamLogger.java +++ b/src/java/org/apache/cassandra/utils/NoSpamLogger.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.utils; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -24,6 +25,10 @@ import org.cliffc.high_scale_lib.NonBlockingHashMap; import org.slf4j.Logger; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; +import com.github.benmanes.caffeine.cache.Ticker; import com.google.common.annotations.VisibleForTesting; import static org.apache.cassandra.utils.Clock.Global; @@ -36,8 +41,11 @@ * result in the original time being used. No warning is provided if there is a mismatch. * * If the statement is cached and used to log directly then only a volatile read will be required in the common case. - * If the Logger is cached then there is a single concurrent hash map lookup + the volatile read. - * If neither the logger nor the statement is cached then it is two concurrent hash map lookups + the volatile read. + * If the Logger is cached then there is a single Caffeine cache lookup + the volatile read. + * If neither the logger nor the statement is cached then it is a NonBlockingHashMap lookup + a Caffeine cache lookup + the volatile read. + * + * The implementation uses Caffeine cache with time-based expiration to automatically evict log statements + * after their minimum interval has passed, preventing unbounded memory growth from dynamic log messages. * */ public class NoSpamLogger @@ -47,7 +55,7 @@ public class NoSpamLogger */ public enum Level { - INFO, WARN, ERROR + DEBUG, INFO, WARN, ERROR } @VisibleForTesting @@ -64,6 +72,41 @@ public static void unsafeSetClock(Clock clock) CLOCK = clock; } + private static Ticker TICKER = Ticker.systemTicker(); + + @VisibleForTesting + public static void unsafeSetTicker(Ticker ticker) + { + TICKER = ticker; + } + + /** + * Maximum number of log statements cached per NoSpamLogger instance. + * This prevents unbounded memory growth when log messages contain dynamic content. + * Defaults to MAX_VALUE as a default behavior since we rely on the cache time-based expiration. + */ + private static long noSpamLoggerMaxStatementsPerLogger = getOrDefaultNoSpamLoggerMaxStatementsPerLogger(); + + @VisibleForTesting + public static void setNospamLoggerMaxStatementsPerLoggerUnsafe(long maxStatementsPerLogger) + { + noSpamLoggerMaxStatementsPerLogger = maxStatementsPerLogger; + } + + @VisibleForTesting + public static void resetNospamLoggerMaxStatementsPerLoggerUnsafe() + { + noSpamLoggerMaxStatementsPerLogger = getOrDefaultNoSpamLoggerMaxStatementsPerLogger(); + } + + public static final String NOSPAM_LOGGER_MAX_STATEMENTS_PER_LOGGER_PROPERTY = "cassandra.nospam_logger.max_statements_per_logger"; + + private static long getOrDefaultNoSpamLoggerMaxStatementsPerLogger() + { + // checkstyle: suppress below 'blockSystemPropertyUsage' + return Long.getLong(NOSPAM_LOGGER_MAX_STATEMENTS_PER_LOGGER_PROPERTY, Long.MAX_VALUE); + } + public class NoSpamLogStatement extends AtomicLong { private static final long serialVersionUID = 1L; @@ -100,6 +143,9 @@ private boolean logNoCheck(Level l, Object... objects) { switch (l) { + case DEBUG: + wrapped.debug(statement, objects); + break; case INFO: wrapped.info(statement, objects); break; @@ -115,6 +161,16 @@ private boolean logNoCheck(Level l, Object... objects) return true; } + public boolean debug(long nowNanos, Object... objects) + { + return NoSpamLogStatement.this.log(Level.DEBUG, nowNanos, objects); + } + + public boolean debug(Object... objects) + { + return NoSpamLogStatement.this.debug(CLOCK.nanoTime(), objects); + } + public boolean info(long nowNanos, Object... objects) { return NoSpamLogStatement.this.log(Level.INFO, nowNanos, objects); @@ -144,6 +200,11 @@ public boolean error(Object... objects) { return NoSpamLogStatement.this.error(CLOCK.nanoTime(), objects); } + + public long expiry() + { + return minIntervalNanos; + } } private static final NonBlockingHashMap wrappedLoggers = new NonBlockingHashMap<>(); @@ -154,6 +215,28 @@ static void clearWrappedLoggersForTest() wrappedLoggers.clear(); } + /** + * Forces eviction of entries from the {@link NoSpamLogStatement} cache for this logger instance. + * This is useful for testing to ensure cache size limits are enforced immediately. + */ + @VisibleForTesting + void cleanUpStatementsForTest() + { + lastMessage.cleanUp(); + } + + /** + * Returns the current size of the lastMessage cache for this logger instance. + * This is useful for testing cache eviction behavior. + * + * @return the number of log statements currently cached for this logger + */ + @VisibleForTesting + long getStatementsCount() + { + return lastMessage.estimatedSize(); + } + public static NoSpamLogger getLogger(Logger logger, long minInterval, TimeUnit unit) { NoSpamLogger wrapped = wrappedLoggers.get(logger); @@ -209,7 +292,47 @@ public static NoSpamLogStatement getStatement(Logger logger, String message, lon private final Logger wrapped; private final long minIntervalNanos; - private final NonBlockingHashMap lastMessage = new NonBlockingHashMap<>(); + + /** + * Custom expiry policy for NoSpamLogStatement cache entries. + * Each entry expires based on its own minIntervalNanos value. + */ + private static class StatementExpiry implements Expiry + { + @Override + public long expireAfterCreate(String key, NoSpamLogStatement value, long currentTime) + { + return value.expiry(); + } + + @Override + public long expireAfterUpdate(String key, NoSpamLogStatement value, + long currentTime, long currentDuration) + { + return value.expiry(); + } + + @Override + public long expireAfterRead(String key, NoSpamLogStatement value, + long currentTime, long currentDuration) + { + return currentDuration; + } + } + + /** + * Cache of NoSpamLogStatement instances per NoSpamLogger instance. + * Bounded by size and time to prevent memory exhaustion from dynamic log messages. + * Uses Caffeine with W-TinyLFU eviction policy. + * Uses custom per-entry expiry based on each statement's minIntervalNanos. + */ + private final Cache lastMessage = Caffeine.newBuilder() + .maximumSize(noSpamLoggerMaxStatementsPerLogger) + .expireAfter(new StatementExpiry()) + .ticker(TICKER) + .executor(ForkJoinPool.commonPool()) + .recordStats() + .build(); private NoSpamLogger(Logger wrapped, long minInterval, TimeUnit timeUnit) { @@ -217,6 +340,16 @@ private NoSpamLogger(Logger wrapped, long minInterval, TimeUnit timeUnit) minIntervalNanos = timeUnit.toNanos(minInterval); } + public boolean debug(long nowNanos, String s, Object... objects) + { + return NoSpamLogger.this.log( Level.DEBUG, s, nowNanos, objects); + } + + public boolean debug(String s, Object... objects) + { + return NoSpamLogger.this.debug(CLOCK.nanoTime(), s, objects); + } + public boolean info(long nowNanos, String s, Object... objects) { return NoSpamLogger.this.log( Level.INFO, s, nowNanos, objects); @@ -274,14 +407,6 @@ public NoSpamLogStatement getStatement(String s, long minIntervalNanos) public NoSpamLogStatement getStatement(String key, String s, long minIntervalNanos) { - NoSpamLogStatement statement = lastMessage.get(key); - if (statement == null) - { - statement = new NoSpamLogStatement(s, minIntervalNanos); - NoSpamLogStatement temp = lastMessage.putIfAbsent(key, statement); - if (temp != null) - statement = temp; - } - return statement; + return lastMessage.get(key, k -> new NoSpamLogStatement(s, minIntervalNanos)); } } diff --git a/src/java/org/apache/cassandra/utils/NonThrowingCloseable.java b/src/java/org/apache/cassandra/utils/NonThrowingCloseable.java new file mode 100644 index 000000000000..684d6bd30346 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/NonThrowingCloseable.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.io.Closeable; + +/** + * A closeable that will not throw. + */ +public interface NonThrowingCloseable extends Closeable +{ + void close(); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/utils/OutputHandler.java b/src/java/org/apache/cassandra/utils/OutputHandler.java index 76eb34558ff2..95507763d685 100644 --- a/src/java/org/apache/cassandra/utils/OutputHandler.java +++ b/src/java/org/apache/cassandra/utils/OutputHandler.java @@ -57,7 +57,19 @@ default void warn(String msg, Object ... args) class LogOutput implements OutputHandler { - private static Logger logger = LoggerFactory.getLogger(LogOutput.class); + private static final Logger LOGGER_LOGOUTPUT = LoggerFactory.getLogger(LogOutput.class); + + private final Logger logger; + + public LogOutput(Logger logger) + { + this.logger = logger; + } + + public LogOutput() + { + this(LOGGER_LOGOUTPUT); + } public void output(String msg) { @@ -80,6 +92,15 @@ public void warn(Throwable th, String msg) } } + @DseLegacy + class CustomLogOutput extends LogOutput + { + public CustomLogOutput(Logger customLogger) + { + super(customLogger); + } + } + class SystemOutput implements OutputHandler { public final boolean debug; diff --git a/src/java/org/apache/cassandra/utils/Overlaps.java b/src/java/org/apache/cassandra/utils/Overlaps.java index 6e7c2ef41636..9aae6072f06e 100644 --- a/src/java/org/apache/cassandra/utils/Overlaps.java +++ b/src/java/org/apache/cassandra/utils/Overlaps.java @@ -19,60 +19,126 @@ package org.apache.cassandra.utils; import java.util.ArrayList; +import java.util.BitSet; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.PriorityQueue; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.stream.IntStream; public class Overlaps { - /** - * Construct a minimal list of overlap sets, i.e. the sections of the range span when we have overlapping items, - * where we ensure: - * - non-overlapping items are never put in the same set - * - no item is present in non-consecutive sets - * - for any point where items overlap, the result includes a set listing all overlapping items - *

    - * For example, for inputs A[0, 4), B[2, 8), C[6, 10), D[1, 9) the result would be the sets ABD and BCD. We are not - * interested in the spans where A, B, or C are present on their own or in combination with D, only that there - * exists a set in the list that is a superset of any such combination, and that the non-overlapping A and C are - * never together in a set. - *

    - * Note that the full list of overlap sets A, AD, ABD, BD, BCD, CD, C is also an answer that satisfies the three - * conditions above, but it contains redundant sets (e.g. AD is already contained in ABD). - * - * @param items A list of items to distribute in overlap sets. This is assumed to be a transient list and the method - * may modify or consume it. It is assumed that the start and end positions of an item are ordered, - * and the items are non-empty. - * @param startsAfter Predicate determining if its left argument's start if fully after the right argument's end. - * This will only be used with arguments where left's start is known to be after right's start. - * It is up to the caller if this is a strict comparison -- strict (>) for end-inclusive spans - * and non-strict (>=) for end-exclusive. - * @param startsComparator Comparator of items' starting positions. - * @param endsComparator Comparator of items' ending positions. - * @return List of overlap sets. - */ - public static List> constructOverlapSets(List items, + /// Construct a minimal list of overlap sets, i.e. the sections of the range span when we have overlapping items, + /// where we ensure: + /// - non-overlapping items are never put in the same set + /// - no item is present in non-consecutive sets + /// - for any point where items overlap, the result includes a set listing all overlapping items + /// + /// For example, for inputs A[0, 4), B[2, 8), C[6, 10), D[1, 9) the result would be the sets ABD and BCD. We are not + /// interested in the spans where A, B, or C are present on their own or in combination with D, only that there + /// exists a set in the list that is a superset of any such combination, and that the non-overlapping A and C are + /// never together in a set. + /// + /// Note that the full list of overlap sets A, AD, ABD, BD, BCD, CD, C is also an answer that satisfies the three + /// conditions above, but it contains redundant sets (e.g. AD is already contained in ABD). + /// + /// @param items A list of items to distribute in overlap sets. This is assumed to be a transient list and the method + /// may modify or consume it. It is assumed that the start and end positions of an item are ordered, + /// and the items are non-empty. + /// @param startsAfter Predicate determining if its left argument's start if fully after the right argument's end. + /// This will only be used with arguments where left's start is known to be after right's start. + /// It is up to the caller if this is a strict comparison -- strict (>) for end-inclusive spans + /// and non-strict (>=) for end-exclusive. + /// @param startsComparator Comparator of items' starting positions. + /// @param endsComparator Comparator of items' ending positions. + /// @return List of overlap sets. + public static List> constructOverlapSets(Collection items, BiPredicate startsAfter, Comparator startsComparator, Comparator endsComparator) { - List> overlaps = new ArrayList<>(); + return constructOverlapSets(items, startsAfter, startsComparator, endsComparator, + (sets, active) -> { + sets.add(new HashSet<>(active)); + return sets; + }, + new ArrayList<>()); + } + + /// This is the same as the method above, but only returns the size of the biggest overlap set + /// + /// @param items A list of items to distribute in overlap sets. This is assumed to be a transient list and the method + /// may modify or consume it. It is assumed that the start and end positions of an item are ordered, + /// and the items are non-empty. + /// @param startsAfter Predicate determining if its left argument's start if fully after the right argument's end. + /// This will only be used with arguments where left's start is known to be after right's start. + /// It is up to the caller if this is a strict comparison -- strict (>) for end-inclusive spans + /// and non-strict (>=) for end-exclusive. + /// @param startsComparator Comparator of items' starting positions. + /// @param endsComparator Comparator of items' ending positions. + /// @return The maximum overlap in the given set of items. + public static int maxOverlap(Collection items, + BiPredicate startsAfter, + Comparator startsComparator, + Comparator endsComparator) + { + return constructOverlapSets(items, startsAfter, startsComparator, endsComparator, + (max, active) -> Math.max(max, active.size()), 0); + } + + /// Construct a minimal list of overlap sets, i.e. the sections of the range span when we have overlapping items, + /// where we ensure: + /// - non-overlapping items are never put in the same set + /// - no item is present in non-consecutive sets + /// - for any point where items overlap, the result includes a set listing all overlapping items + /// and process it with the given reducer function. Implements the methods above. + /// + /// For example, for inputs A[0, 4), B[2, 8), C[6, 10), D[1, 9) the result would be the sets ABD and BCD. We are not + /// interested in the spans where A, B, or C are present on their own or in combination with D, only that there + /// exists a set in the list that is a superset of any such combination, and that the non-overlapping A and C are + /// never together in a set. + /// + /// Note that the full list of overlap sets A, AD, ABD, BD, BCD, CD, C is also an answer that satisfies the three + /// conditions above, but it contains redundant sets (e.g. AD is already contained in ABD). + /// + /// @param items A list of items to distribute in overlap sets. It is assumed that the start and end + /// positions of an item are ordered, and the items are non-empty. + /// @param startsAfter Predicate determining if its left argument's start if fully after the right argument's end. + /// This will only be used with arguments where left's start is known to be after right's start. + /// It is up to the caller if this is a strict comparison -- strict (>) for end-inclusive spans + /// and non-strict (>=) for end-exclusive. + /// @param startsComparator Comparator of items' starting positions. + /// @param endsComparator Comparator of items' ending positions. + /// @param reducer Function to apply to each overlap set. + /// @param initialValue Initial value for the reducer. + /// @return The result of processing the overlap sets. + public static R constructOverlapSets(Collection items, + BiPredicate startsAfter, + Comparator startsComparator, + Comparator endsComparator, + BiFunction, R> reducer, + R initialValue) + { + R overlaps = initialValue; if (items.isEmpty()) return overlaps; PriorityQueue active = new PriorityQueue<>(endsComparator); - items.sort(startsComparator); - for (E item : items) + SortingIterator itemsSorted = SortingIterator.create(startsComparator, items); + while (itemsSorted.hasNext()) { + E item = itemsSorted.next(); if (!active.isEmpty() && startsAfter.test(item, active.peek())) { // New item starts after some active ends. It does not overlap with it, so: // -- output the previous active set - overlaps.add(new HashSet<>(active)); + overlaps = reducer.apply(overlaps, active); // -- remove all items that also end before the current start do { @@ -87,13 +153,71 @@ public static List> constructOverlapSets(List items, } assert !active.isEmpty(); - overlaps.add(new HashSet<>(active)); + overlaps = reducer.apply(overlaps, active); return overlaps; } + + /// Transform a list to transitively combine adjacent sets that have a common element, resulting in disjoint sets. + public static List> combineSetsWithCommonElement(List> overlapSets) + { + Set group = overlapSets.get(0); + List> groups = new ArrayList<>(); + for (int i = 1; i < overlapSets.size(); ++i) + { + Set current = overlapSets.get(i); + if (Collections.disjoint(current, group)) + { + groups.add(group); + group = current; + } + else + { + group.addAll(current); + } + } + groups.add(group); + return groups; + } + + /// Split a list of items into disjoint non-overlapping sets. + /// + /// @param items A list of items to distribute in overlap sets. It is assumed that the start and end + /// positions of an item are ordered, and the items are non-empty. + /// @param startsAfter Predicate determining if its left argument's start if fully after the right argument's end. + /// This will only be used with arguments where left's start is known to be after right's start. + /// It is up to the caller if this is a strict comparison -- strict (>) for end-inclusive spans + /// and non-strict (>=) for end-exclusive. + /// @param startsComparator Comparator of items' starting positions. + /// @param endsComparator Comparator of items' ending positions. + /// @return list of non-overlapping sets of items + public static List> splitInNonOverlappingSets(List items, + BiPredicate startsAfter, + Comparator startsComparator, + Comparator endsComparator) + { + if (items.isEmpty()) + return List.of(); + + List> overlapSets = Overlaps.constructOverlapSets(items, startsAfter, startsComparator, endsComparator); + return combineSetsWithCommonElement(overlapSets); + } + + + /// Overlap inclusion method to use when combining overlap sections into a bucket. For example, with + /// items A(0, 5), B(2, 9), C(6, 12), D(10, 12) whose overlap sections calculation returns \[AB, BC, CD\], + /// - `NONE` means no sections are to be merged. AB, BC and CD will be separate buckets, selections AB, BC and CD + /// will be added separately, thus some items will be partially used / single-source compacted, likely + /// to be recompacted again with the next selected bucket. + /// - `SINGLE` means only overlaps of the sstables in the selected bucket will be added. AB+BC will be one bucket, + /// and CD will be another (as BC is already used). A middle ground of sorts, should reduce overcompaction but + /// still has some. + /// - `TRANSITIVE` means a transitive closure of overlapping sstables will be selected. AB+BC+CD will be in the + /// same bucket, selected compactions will apply to all overlapping sstables and no overcompaction will be done, + /// at the cost of reduced compaction parallelism and increased length of the operation. public enum InclusionMethod { - NONE, SINGLE, TRANSITIVE; + NONE, SINGLE, TRANSITIVE } public interface BucketMaker @@ -101,65 +225,166 @@ public interface BucketMaker B makeBucket(List> sets, int startIndexInclusive, int endIndexExclusive); } - /** - * Assign overlap sections into buckets. Identify sections that have at least threshold-many overlapping - * items and apply the overlap inclusion method to combine with any neighbouring sections that contain - * selected sstables to make sure we make full use of any sstables selected for compaction (i.e. avoid - * recompacting, see {@link org.apache.cassandra.db.compaction.unified.Controller#overlapInclusionMethod()}). - * - * @param threshold Threshold for selecting a bucket. Sets below this size will be ignored, unless they need - * to be grouped with a neighboring set due to overlap. - * @param inclusionMethod NONE to only form buckets of the overlapping sets, SINGLE to include all - * sets that share an sstable with a selected bucket, or TRANSITIVE to include - * all sets that have an overlap chain to a selected bucket. - * @param overlaps An ordered list of overlap sets as returned by {@link #constructOverlapSets}. - * @param bucketer Method used to create a bucket out of the supplied set indexes. - */ + /// Assign overlap sections into buckets. Identify sections that have at least threshold-many overlapping + /// items and apply the overlap inclusion method to combine with any neighbouring sections that contain + /// selected sstables to make sure we make full use of any sstables selected for compaction (i.e. avoid + /// recompacting, see [InclusionMethod]). + /// + /// For non-transitive inclusion method the order in which we select the buckets matters because an sstables that + /// spans overlap sets could be chosen for only one of the candidate buckets containing it. To make the most + /// efficient selection we thus perform it by descending size, starting with the sets with most overlap. + /// + /// @param threshold Threshold for selecting a bucket. Sets below this size will be ignored, unless they need + /// to be grouped with a neighboring set due to overlap. + /// @param inclusionMethod `NONE` to only form buckets of the overlapping sets, `SINGLE` to include all + /// sets that share an sstable with a selected bucket, or `TRANSITIVE` to include + /// all sets that have an overlap chain to a selected bucket. + /// @param overlaps An ordered list of overlap sets as returned by [#constructOverlapSets]. + /// @param bucketer Method used to create a bucket out of the supplied set indexes. + /// @param unselectedHandler Action to take on sets that are below the threshold and not included in any bucket. public static List assignOverlapsIntoBuckets(int threshold, InclusionMethod inclusionMethod, List> overlaps, - BucketMaker bucketer) + BucketMaker bucketer, + Consumer> unselectedHandler) + { + switch (inclusionMethod) + { + case TRANSITIVE: + return assignOverlapsTransitive(threshold, overlaps, bucketer, unselectedHandler); + case SINGLE: + case NONE: + return assignOverlapsSingleOrNone(threshold, inclusionMethod, overlaps, bucketer, unselectedHandler); + default: + throw new UnsupportedOperationException(inclusionMethod + " is not supported"); + } + } + + private static List assignOverlapsSingleOrNone(int threshold, + InclusionMethod inclusionMethod, + List> overlaps, + BucketMaker bucketer, + Consumer> unselectedHandler) { List buckets = new ArrayList<>(); int regionCount = overlaps.size(); - int lastEnd = -1; - for (int i = 0; i < regionCount; ++i) + SortingIterator bySize = new SortingIterator<>((a, b) -> Integer.compare(overlaps.get(b).size(), + overlaps.get(a).size()), + overlaps.isEmpty() ? new Integer[1] : IntStream.range(0, overlaps.size()).boxed().toArray()); + + BitSet used = new BitSet(overlaps.size()); + while (bySize.hasNext()) { - Set bucket = overlaps.get(i); - int maxOverlap = bucket.size(); - if (maxOverlap < threshold) + final int i = bySize.next(); + if (used.get(i)) continue; + + Set bucket = overlaps.get(i); + if (bucket.size() < threshold) + break; // no more buckets will be above threshold + used.set(i); + + Set allOverlapping = bucket; + int j = i - 1; + int k = i + 1; int startIndex = i; int endIndex = i + 1; - - if (inclusionMethod != InclusionMethod.NONE) + // expand to include neighbors that intersect with current bucket + if (inclusionMethod == InclusionMethod.SINGLE) { - Set allOverlapping = new HashSet<>(bucket); - Set overlapTarget = inclusionMethod == InclusionMethod.TRANSITIVE - ? allOverlapping - : bucket; - int j; - for (j = i - 1; j > lastEnd; --j) + // expand the bucket to include all overlapping sets + allOverlapping = new HashSet<>(bucket); + Set overlapTarget = bucket; + for (; j >= 0 && !used.get(j); --j) { Set next = overlaps.get(j); if (!setsIntersect(next, overlapTarget)) break; allOverlapping.addAll(next); + used.set(j); } startIndex = j + 1; - for (j = i + 1; j < regionCount; ++j) + for (; k < regionCount && !used.get(k); ++k) { - Set next = overlaps.get(j); + Set next = overlaps.get(k); if (!setsIntersect(next, overlapTarget)) break; allOverlapping.addAll(next); + used.set(k); } - i = j - 1; - endIndex = j; + endIndex = k; + } + // Now mark all overlapping with the extended as used + Set overlapTarget = allOverlapping; + for (; j >= 0 && !used.get(j); --j) + { + Set next = overlaps.get(j); + if (!setsIntersect(next, overlapTarget)) + break; + used.set(j); + unselectedHandler.accept(next); + } + for (; k < regionCount && !used.get(k); ++k) + { + Set next = overlaps.get(k); + if (!setsIntersect(next, overlapTarget)) + break; + used.set(k); + unselectedHandler.accept(next); + } + buckets.add(bucketer.makeBucket(overlaps, startIndex, endIndex)); + } + + for (int i = used.nextClearBit(0); i < regionCount; i = used.nextClearBit(i + 1)) + unselectedHandler.accept(overlaps.get(i)); + + return buckets; + } + + private static List assignOverlapsTransitive(int threshold, + List> overlaps, + BucketMaker bucketer, + Consumer> unselectedHandler) + { + List buckets = new ArrayList<>(); + int regionCount = overlaps.size(); + int lastEnd = 0; + for (int i = 0; i < regionCount; ++i) + { + Set bucket = overlaps.get(i); + int maxOverlap = bucket.size(); + if (maxOverlap < threshold) + continue; + + // expand to include neighbors that intersect with expanded buckets + Set allOverlapping = new HashSet<>(bucket); + Set overlapTarget = allOverlapping; + int j; + for (j = i - 1; j >= lastEnd; --j) + { + Set next = overlaps.get(j); + if (!setsIntersect(next, overlapTarget)) + break; + allOverlapping.addAll(next); + } + int startIndex = j + 1; + for (j = i + 1; j < regionCount; ++j) + { + Set next = overlaps.get(j); + if (!setsIntersect(next, overlapTarget)) + break; + allOverlapping.addAll(next); } + i = j - 1; + int endIndex = j; + buckets.add(bucketer.makeBucket(overlaps, startIndex, endIndex)); - lastEnd = i; + for (int k = lastEnd; k < startIndex; ++k) + unselectedHandler.accept(overlaps.get(k)); + lastEnd = endIndex; } + for (int k = lastEnd; k < regionCount; ++k) + unselectedHandler.accept(overlaps.get(k)); return buckets; } @@ -173,9 +398,7 @@ private static boolean setsIntersect(Set s1, Set s2) return false; } - /** - * Pull the last elements from the given list, up to the given limit. - */ + /// Pull the last elements from the given list, up to the given limit. public static List pullLast(List source, int limit) { List result = new ArrayList<>(limit); @@ -184,11 +407,9 @@ public static List pullLast(List source, int limit) return result; } - /** - * Select up to `limit` sstables from each overlapping set (more than `limit` in total) by taking the last entries - * from `allObjectsSorted`. To achieve this, keep selecting the last sstable until the next one we would add would - * bring the number selected in some overlap section over `limit`. - */ + /// Select up to `limit` sstables from each overlapping set (more than `limit` in total) by taking the last entries + /// from `allObjectsSorted`. To achieve this, keep selecting the last sstable until the next one we would add would + /// bring the number selected in some overlap section over `limit`. public static Collection pullLastWithOverlapLimit(List allObjectsSorted, List> overlapSets, int limit) { int setsCount = overlapSets.size(); diff --git a/src/java/org/apache/cassandra/utils/ProductType.java b/src/java/org/apache/cassandra/utils/ProductType.java new file mode 100644 index 000000000000..35bfaa02f197 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/ProductType.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProductType +{ + private static final Logger logger = LoggerFactory.getLogger(ProductType.class); + + public static Product product = getProduct(); + + public enum Product + { + /** + * On-Premises product + */ + DATASTAX_CASSANDRA, + + /** + * Datastax constellation database-as-a-service product (NOT referring to dse-db, aka. apollo) + */ + DATASTAX_APOLLO + } + + @VisibleForTesting + public static Product getProduct() + { + Product defaultType = Product.DATASTAX_CASSANDRA; + // checkstyle: suppress below 'blockSystemPropertyUsage' + String productType = System.getProperty("dse.product_type", defaultType.name()); + try + { + return Product.valueOf(productType.toUpperCase()); + } + catch (IllegalArgumentException e) + { + logger.info("Unknown product type '{}', will use default product type '{}'.", productType, defaultType.name()); + return defaultType; + } + } +} diff --git a/src/java/org/apache/cassandra/utils/RandomPlus.java b/src/java/org/apache/cassandra/utils/RandomPlus.java new file mode 100644 index 000000000000..f5046c9e151c --- /dev/null +++ b/src/java/org/apache/cassandra/utils/RandomPlus.java @@ -0,0 +1,98 @@ +/* + * Copyright IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Random; + +import com.google.common.base.Preconditions; + +/** + * Extension of {@link Random} with additional utility methods for generating random values + * within specified ranges for various numeric types. + */ +public class RandomPlus extends Random +{ + public RandomPlus() + { + super(); + } + + public RandomPlus(long seed) + { + super(seed); + } + + public int nextInt(int min, int max) + { + return (int) nextDouble(min, max); + } + + public long nextLong(long min, long max) + { + return (long) nextDouble(min, max); + } + + public byte[] nextBytes(int minLength, int maxLength) + { + int length = nextInt(minLength, maxLength); + byte[] bytes = new byte[length]; + nextBytes(bytes); + return bytes; + } + + public double nextDouble(double min, double max) + { + Preconditions.checkArgument(min < max, "max must be greater than min"); + return min + (max - min) * nextDouble(); + } + + public float nextFloat(float min, float max) + { + Preconditions.checkArgument(min < max, "max must be greater than min"); + return min + (max - min) * nextFloat(); + } + + public BigInteger nextBigInteger(BigInteger min, BigInteger max) + { + Preconditions.checkArgument(min.compareTo(max) < 0, "max must be greater than min"); + BigInteger range = max.subtract(min); + int len = range.bitLength(); + BigInteger res; + do + { + res = new BigInteger(len, this).add(min); + } + while (res.compareTo(max) >= 0); + return res; + } + + public BigDecimal nextBigDecimal(BigDecimal min, BigDecimal max) + { + return BigDecimal.valueOf(nextDouble(min.doubleValue(), max.doubleValue())); + } + + public String nextAlphanumeric(int length) + { + return ints(48, 123) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/src/java/org/apache/cassandra/utils/RangesSerializer.java b/src/java/org/apache/cassandra/utils/RangesSerializer.java index 5707503f6bd4..a7cab8fc42d2 100644 --- a/src/java/org/apache/cassandra/utils/RangesSerializer.java +++ b/src/java/org/apache/cassandra/utils/RangesSerializer.java @@ -65,9 +65,9 @@ public Collection> deserialize(DataInputPlus in, int version) throw @Override public long serializedSize(Collection> ranges, int version) { - int size = TypeSizes.sizeof(ranges.size()); + long size = TypeSizes.sizeof(ranges.size()); if (ranges.size() > 0) - size += ranges.size() * 2 * Token.serializer.serializedSize(ranges.iterator().next().left, version); + size += ranges.size() * 2L * Token.serializer.serializedSize(ranges.iterator().next().left, version); return size; } } diff --git a/src/java/org/apache/cassandra/utils/ReadWriteLockedList.java b/src/java/org/apache/cassandra/utils/ReadWriteLockedList.java new file mode 100644 index 000000000000..e6535003802e --- /dev/null +++ b/src/java/org/apache/cassandra/utils/ReadWriteLockedList.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.AbstractList; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class ReadWriteLockedList extends AbstractList +{ + private final List list; + private final Lock readLock; + private final Lock writeLock; + + public ReadWriteLockedList(List list) + { + this.list = list; + ReadWriteLock rwLock = new ReentrantReadWriteLock(); + readLock = rwLock.readLock(); + writeLock = rwLock.writeLock(); + } + + @Override + public T set(int index, T element) + { + writeLock.lock(); + try + { + return list.set(index, element); + } + finally + { + writeLock.unlock(); + } + } + + @Override + public boolean add(T item) + { + writeLock.lock(); + try + { + return list.add(item); + } + finally + { + writeLock.unlock(); + } + } + + @Override + public T get(int index) + { + readLock.lock(); + try + { + return list.get(index); + } + finally + { + readLock.unlock(); + } + } + + @Override + public int size() + { + readLock.lock(); + try + { + return list.size(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean isEmpty() + { + readLock.lock(); + try + { + return list.isEmpty(); + } + finally + { + readLock.unlock(); + } + } + + public static ReadWriteLockedList wrap(List list) + { + return new ReadWriteLockedList<>(list); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/utils/Reducer.java b/src/java/org/apache/cassandra/utils/Reducer.java new file mode 100644 index 000000000000..1915c460c1ff --- /dev/null +++ b/src/java/org/apache/cassandra/utils/Reducer.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils; + +/** Accumulator that collects values of type A, and outputs a value of type B. */ +public abstract class Reducer +{ + /** + * @return true if Out is the same as In for the case of a single source iterator + */ + public boolean singleSourceReduceIsTrivial() + { + return false; + } + + /** + * combine this object with the previous ones. + * intermediate state is up to your implementation. + */ + public abstract void reduce(int idx, In current); + + Throwable errors = null; + + public void error(Throwable error) + { + errors = Throwables.merge(errors, error); + } + + public Throwable getErrors() + { + Throwable toReturn = errors; + errors = null; + return toReturn; + } + + /** @return The last object computed by reduce */ + public abstract Out getReduced(); + + /** + * Called at the beginning of each new key, before any reduce is called. + * To be overridden by implementing classes. + * + * Note: There's no need to clear error; merging completes once one is found. + */ + public void onKeyChange() {} + + public static Reducer getIdentity() + { + return new IdentityReducer<>(); + } + + private static class IdentityReducer extends Reducer + { + private In reduced; + + @Override + public void reduce(int idx, In current) + { + this.reduced = current; + } + + @Override + public In getReduced() + { + return reduced; + } + + @Override + public void onKeyChange() { + this.reduced = null; + } + + @Override + public boolean singleSourceReduceIsTrivial() + { + return true; + } + } +} diff --git a/src/java/org/apache/cassandra/utils/ResourceWatcher.java b/src/java/org/apache/cassandra/utils/ResourceWatcher.java index e8dcb8574372..71a2cace8f4e 100644 --- a/src/java/org/apache/cassandra/utils/ResourceWatcher.java +++ b/src/java/org/apache/cassandra/utils/ResourceWatcher.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.utils; +import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.apache.cassandra.io.util.File; @@ -27,7 +28,7 @@ public class ResourceWatcher { - public static void watch(String resource, Runnable callback, int period) + public static void watch(String resource, Callable callback, int period) { ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(new WatchedResource(resource, callback), period, period, TimeUnit.MILLISECONDS); } @@ -36,10 +37,10 @@ public static class WatchedResource implements Runnable { private static final Logger logger = LoggerFactory.getLogger(WatchedResource.class); private final String resource; - private final Runnable callback; + private final Callable callback; private long lastLoaded; - public WatchedResource(String resource, Runnable callback) + public WatchedResource(String resource, Callable callback) { this.resource = resource; this.callback = callback; @@ -54,8 +55,8 @@ public void run() long lastModified = new File(filename).lastModified(); if (lastModified > lastLoaded) { - callback.run(); - lastLoaded = lastModified; + if (callback.call()) + lastLoaded = lastModified; } } catch (Throwable t) diff --git a/src/java/org/apache/cassandra/utils/SigarLibrary.java b/src/java/org/apache/cassandra/utils/SigarLibrary.java index 830f7cab8eb7..578eba8a7089 100644 --- a/src/java/org/apache/cassandra/utils/SigarLibrary.java +++ b/src/java/org/apache/cassandra/utils/SigarLibrary.java @@ -44,7 +44,7 @@ public class SigarLibrary private SigarLibrary() { - logger.info("Initializing SIGAR library"); + logger.debug("Initializing SIGAR library"); try { sigar = new Sigar(); diff --git a/src/java/org/apache/cassandra/utils/SortingIterator.java b/src/java/org/apache/cassandra/utils/SortingIterator.java new file mode 100644 index 000000000000..3bc53185f9b0 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/SortingIterator.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.function.Function; + +/** + * An iterator that lists a set of items in order. + *

    + * This is intended for use where we would normally read only a small subset of the elements, or where we would skip + * over large sections of the sorted set. To implement this efficiently, we put the data in a binary heap and extract + * elements as the iterator is queried, effectively performing heapsort. We also implement a quicker skipTo operation + * where we remove all smaller elements and restore the heap for all of them in one step. + *

    + * As in heapsort, the first stage of the process has complexity O(n), and every next item is extracted in O(log n) + * steps. skipTo works in O(m.log n) steps (where m is the number of skipped items), but is also limited to O(n) when m + * is large by the same argument as the initial heap construction. + *

    + * The class accepts and stores nulls as non-present values, which turns out to be quite a bit more efficient for + * iterating these sets when the comparator is complex at the expense of a small slowdown for simple comparators. The + * reason for this is that we can remove entries by replacing them with nulls and letting these descend the heap, which + * avoids half the comparisons compared to using one of the largest live elements. + *

    + * If the number of items necessary is small and known in advance, it may be preferable to use {@link TopKSelector} + * which keeps a smaller memory footprint. + */ +public class SortingIterator extends BinaryHeap.WithComparator implements Iterator +{ + SortingIterator(Comparator comparator, Object[] data) + { + super(comparator, data); + heapify(); + } + + /** + * Create a sorting iterator from a list of sources. + * Duplicates will be returned in arbitrary order. + */ + public static SortingIterator create(Comparator comparator, Collection sources) + { + return new SortingIterator<>(comparator, sources.isEmpty() ? new Object[1] : sources.toArray()); + } + + /** + * Create a closeable sorting iterator from a list of sources, calling the given method on close. + * Duplicates will be returned in arbitrary order. + */ + public static CloseableIterator createCloseable(Comparator comparator, Collection sources, Function mapper, Runnable onClose) + { + return new Builder<>(sources, mapper).closeable(comparator, onClose); + } + + /** + * Create a sorting and deduplicating iterator from a list of sources. + * Duplicate values will only be reported once, using an arbitrarily-chosen representative. + */ + public static SortingIterator createDeduplicating(Comparator comparator, Collection sources) + { + return new Deduplicating<>(comparator, sources.isEmpty() ? new Object[1] : sources.toArray()); + } + + @Override + protected Object advanceItem(Object item) + { + return null; + } + + @Override + protected Object advanceItemTo(Object item, Object targetKey) + { + return null; + } + + @SuppressWarnings("unchecked") + public T peek() + { + return (T) super.top(); + } + + @Override + public boolean hasNext() + { + return !isEmpty(); + } + + @SuppressWarnings("unchecked") + @Override + public T next() + { + Object item = pop(); + if (item == null) + throw new NoSuchElementException(); + return (T) item; + } + + /** + * Skip to the first element that is greater than or equal to the given key. + */ + public void skipTo(T targetKey) + { + advanceTo(targetKey); + } + + public static class Closeable extends SortingIterator implements CloseableIterator + { + final Runnable onClose; + + public Closeable(Comparator comparator, + Object[] data, + Runnable onClose) + { + super(comparator, data); + this.onClose = onClose; + } + + @Override + public void close() + { + onClose.run(); + } + } + + public static class Deduplicating extends SortingIterator + { + public Deduplicating(Comparator comparator, Object[] data) + { + super(comparator, data); + } + + @Override + public T next() + { + Object item = popAndSkipEqual(); + if (item == null) + throw new NoSuchElementException(); + return (T) item; + } + } + + public static class Builder + { + Object[] data; + int count; + + public Builder() + { + this(16); + } + + public Builder(int initialSize) + { + data = new Object[Math.max(initialSize, 1)]; // at least one element so that we don't need to special-case empty + count = 0; + } + + public Builder(Collection collection, Function mapper) + { + this(collection.size()); + for (V item : collection) + data[count++] = mapper.apply(item); // this may be null, which the iterator will properly handle + } + + public Builder add(T element) + { + if (element != null) // avoid growing if we don't need to + { + if (count == data.length) + data = Arrays.copyOf(data, data.length * 2); + data[count++] = element; + } + return this; + } + + public Builder addAll(Collection collection) + { + if (count + collection.size() > data.length) + data = Arrays.copyOf(data, count + collection.size()); + for (T item : collection) + data[count++] = item; + return this; + } + + public Builder addAll(Collection collection, Function mapper) + { + if (count + collection.size() > data.length) + data = Arrays.copyOf(data, count + collection.size()); + for (V item : collection) + data[count++] = mapper.apply(item); // this may be null, which the iterator will properly handle + return this; + } + + public int size() + { + return count; // Note: may include null elements, depending on how data is added + } + + /** + * Build a sorting iterator from the data added so far. + * The returned iterator will report duplicates in arbitrary order. + */ + public SortingIterator build(Comparator comparator) + { + return new SortingIterator<>(comparator, data); // this will have nulls at the end, which is okay + } + + /** + * Build a closeable sorting iterator from the data added so far. + * The returned iterator will report duplicates in arbitrary order. + */ + public Closeable closeable(Comparator comparator, Runnable onClose) + { + return new Closeable<>(comparator, data, onClose); + } + + /** + * Build a sorting and deduplicating iterator from the data added so far. + * The returned iterator will only report equal items once, using an arbitrarily-chosen representative. + */ + public SortingIterator deduplicating(Comparator comparator) + { + return new Deduplicating<>(comparator, data); + } + + // This does not offer build methods that trim the array to count (i.e. Arrays.copyOf(data, count) instead of + // data), because it is only meant for short-lived operations where the iterator is not expected to live much + // longer than the builder and thus both the builder and iterator will almost always expire in the same GC cycle + // and thus the cost of trimming is not offset by any gains. + } +} diff --git a/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java b/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java index 2969597c2381..e99b5be42de1 100644 --- a/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java +++ b/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java @@ -18,10 +18,16 @@ package org.apache.cassandra.utils; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.bti.BtiFormat; +import org.apache.cassandra.net.MessagingService; /** * The mode of compatibility with older Cassandra versions. @@ -35,6 +41,17 @@ public enum StorageCompatibilityMode */ CASSANDRA_4(4), + /** + * Same major version as {@link #CASSANDRA_4}, but with additional CC_4-specific behaviors: + *

      + *
    • Schema storage: The memtable column in system_schema.tables and system_schema.views is stored + * as {@code frozen>} (CC4 format) instead of {@code text} (CC5 format). + * This ensures safe downgrades to CC4.
    • + *
    • SSTable format: Allows BTI format in {@link #validateSstableFormat}.
    • + *
    + */ + HCD_1(4), + /** * Use the storage formats of the current version, but disabling features that are not compatible with any * not-upgraded nodes in the cluster. Use this during rolling upgrades to a new major Cassandra version. Once all @@ -49,6 +66,9 @@ public enum StorageCompatibilityMode */ NONE(Integer.MAX_VALUE); + private static final Logger logger = LoggerFactory.getLogger(StorageCompatibilityMode.class); + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + public final int major; StorageCompatibilityMode(int major) @@ -78,4 +98,30 @@ public void validateSstableFormat(SSTableFormat selectedFormat) selectedFormat.name(), this)); } + + /** + * Returns the messaging version to use for on-disk storage formats (commit log, hints, batch logs). + * When a compatibility mode is set (e.g., HCD_1), this ensures that on-disk formats are written + * in a way that older versions can read. + * + * @return the messaging version appropriate for storage serialization + */ + public int storageMessagingVersion() + { + int version; + switch (this) + { + case CASSANDRA_4: + case HCD_1: + version = MessagingService.VERSION_40; + break; + case UPGRADING: + case NONE: + default: + version = MessagingService.current_version; + break; + } + noSpamLogger.info("Storage messaging version selected: {} for compatibility mode: {}", version, this); + return version; + } } diff --git a/src/java/org/apache/cassandra/utils/StringSerializer.java b/src/java/org/apache/cassandra/utils/StringSerializer.java new file mode 100644 index 000000000000..1f8cb0dc11ce --- /dev/null +++ b/src/java/org/apache/cassandra/utils/StringSerializer.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils; + +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + +public class StringSerializer implements IVersionedSerializer +{ + public static StringSerializer serializer = new StringSerializer(); + + @Override + public void serialize(String value, DataOutputPlus out, int version) throws IOException + { + out.writeUTF(value); + } + + @Override + public String deserialize(DataInputPlus in, int version) throws IOException + { + return in.readUTF(); + } + + @Override + public long serializedSize(String value, int version) + { + return TypeSizes.sizeof(value); + } +} diff --git a/src/java/org/apache/cassandra/utils/SyncUtil.java b/src/java/org/apache/cassandra/utils/SyncUtil.java index 96985cef6398..84eca25d70d0 100644 --- a/src/java/org/apache/cassandra/utils/SyncUtil.java +++ b/src/java/org/apache/cassandra/utils/SyncUtil.java @@ -108,7 +108,7 @@ public static void trySync(int fd) if (SKIP_SYNC) return; - NativeLibrary.trySync(fd); + INativeLibrary.instance.trySync(fd); } public static void trySyncDir(File dir) @@ -116,14 +116,14 @@ public static void trySyncDir(File dir) if (SKIP_SYNC) return; - int directoryFD = NativeLibrary.tryOpenDirectory(dir.path()); + int directoryFD = INativeLibrary.instance.tryOpenDirectory(dir); try { trySync(directoryFD); } finally { - NativeLibrary.tryCloseFD(directoryFD); + INativeLibrary.instance.tryCloseFD(directoryFD); } } } diff --git a/src/java/org/apache/cassandra/utils/ThreadsFactory.java b/src/java/org/apache/cassandra/utils/ThreadsFactory.java new file mode 100644 index 000000000000..492a2cea936c --- /dev/null +++ b/src/java/org/apache/cassandra/utils/ThreadsFactory.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.concurrent.ExecutorService; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.InlinedThreadLocalThread; + +public class ThreadsFactory +{ + /** + * @param name name of the thread for this executor + * @return a single threaded executor whose threads have names + */ + public static ExecutorService newSingleThreadedExecutor(String name) + { + return ExecutorFactory.Global.executorFactory().sequential(name); + } + + /** + * @param r runnable task for the thread + * @param name for the thread + * @return a new daemon thread which has the given name and task + */ + public static Thread newDaemonThread(Runnable r, String name) + { + return newThread(r, name, true); + } + + /** + * @param r runnable task for the thread + * @param name for the thread + * @param isDaemon + * @return a new thread which has the given name and task + */ + public static Thread newThread(Runnable r, String name, boolean isDaemon) + { + Thread t = new InlinedThreadLocalThread(r, name); + t.setDaemon(isDaemon); + return t; + } + + public static void addShutdownHook(Runnable r, String name) + { + // shutdown hook threads should not be daemon + Runtime.getRuntime().addShutdownHook(newThread(r, name, false)); + } +} diff --git a/src/java/org/apache/cassandra/utils/Throwables.java b/src/java/org/apache/cassandra/utils/Throwables.java index 3665dfca82d0..5d5ae5a01a77 100644 --- a/src/java/org/apache/cassandra/utils/Throwables.java +++ b/src/java/org/apache/cassandra/utils/Throwables.java @@ -48,11 +48,50 @@ public interface DiscreteAction void perform() throws E; } + /** + * Check if the provided throwable is of the provided class, or than any of the throwable in his clause chain is + * of the provided class. + * + * @param t the {@link Throwable} to check. + * @param causeClass the class to check if the exception is an instance of, or is caused by. + * @return {@code true} if {@code t} is of class {@code causeClass} or any of its cause is. + */ + public static boolean isCausedBy(Throwable t, Class causeClass) + { + while (t != null) + { + if (causeClass.isInstance(t)) + return true; + t = t.getCause(); + } + return false; + } + public static boolean isCausedBy(Throwable t, Predicate cause) { return cause.test(t) || (t.getCause() != null && cause.test(t.getCause())); } + /** + * Returns an Optional containing the provided throwable if it is of the provided class or the first throwable in the + * cause chain that is of the provided class. + * + * @param t the {@link Throwable} to check. + * @param causeClass the class to check if the Throwable is an instance of, or is caused by. + * @return Optional containing the provided throwable if it is of the provided class or the first throwable in the + * cause chain that is of the provided class, or an empty Optional if no such throwable is found. + */ + public static Optional getCauseOfType(Throwable t, Class causeClass) + { + while (t != null) + { + if (causeClass.isInstance(t)) + return Optional.of(causeClass.cast(t)); + t = t.getCause(); + } + return Optional.empty(); + } + public static boolean anyCauseMatches(Throwable t, Predicate cause) { do @@ -245,8 +284,13 @@ public static Throwable close(Throwable accumulate, AutoCloseable ... closeables */ public static Throwable close(Throwable accumulate, Iterable closeables) { + if (closeables == null) + return accumulate; + for (AutoCloseable closeable : closeables) { + if (closeable != null) + { try { closeable.close(); @@ -256,6 +300,7 @@ public static Throwable close(Throwable accumulate, Iterable caus if (!anyCauseMatches(err, cause::isInstance)) throw new AssertionError("The exception is not caused by " + cause.getName(), err); } + + @VisibleForTesting + public static void assertAnyCause(Throwable err, Class... causeClasses) + { + if (Arrays.stream(causeClasses).noneMatch(c -> anyCauseMatches(err, c::isInstance))) + throw new AssertionError("The exception is not caused by any of " + Arrays.toString(causeClasses), err); + } } diff --git a/src/java/org/apache/cassandra/utils/TimeUUID.java b/src/java/org/apache/cassandra/utils/TimeUUID.java index 8cdcfc5f6813..d3e3f086163b 100644 --- a/src/java/org/apache/cassandra/utils/TimeUUID.java +++ b/src/java/org/apache/cassandra/utils/TimeUUID.java @@ -203,6 +203,14 @@ public long unixMicros() return rawTimestampToUnixMicros(uuidTimestamp); } + /** + * The Cassandra internal millis-resolution timestamp of the TimeUUID, as of unix epoch + */ + public long unixMillis() + { + return (uuidTimestamp / 10_000L) + UUID_EPOCH_UNIX_MILLIS; + } + /** * The UUID-format timestamp, i.e. 10x micros-resolution, as of UUIDGen.UUID_EPOCH_UNIX_MILLIS * The tenths of a microsecond are used to store a flag value. @@ -241,7 +249,7 @@ public static long msbToRawTimestamp(long msb) { assert (UUID_VERSION_BITS_IN_MSB & msb) == TIMESTAMP_UUID_VERSION_IN_MSB; msb &= ~TIMESTAMP_UUID_VERSION_IN_MSB; - return (msb & 0xFFFFL) << 48 + return (msb & 0xFFFFL) << 48 | (msb & 0xFFFF0000L) << 16 | (msb >>> 32); } @@ -263,7 +271,12 @@ public int hashCode() @Override public boolean equals(Object that) { - return (that instanceof UUID && equals((UUID) that)) + if (this == that) + return true; + if (that == null) + return false; + + return (that instanceof UUID && equals((UUID) that)) || (that instanceof TimeUUID && equals((TimeUUID) that)); } @@ -416,6 +429,24 @@ public static byte[] nextTimeUUIDAsBytes() return toBytes(rawTimestampToMsb(unixMicrosToRawTimestamp(nextUnixMicros())), clockSeqAndNode); } + public static int sequence(TimeUUID timeUUID) + { + long lsb = timeUUID.asUUID().getLeastSignificantBits(); + return (int) ((lsb >> 48) & 0x0000000000003FFFL); + } + + /** + * Returns a new TimeUUID with the same timestamp as this one, but with the provided sequence value. + */ + public static TimeUUID withSequence(TimeUUID timeUUID, long sequence) + { + long sequenceBits = 0x0000000000003FFFL; + long sequenceMask = ~(sequenceBits << 48); + final long bits = (sequence & sequenceBits) << 48; + UUID uuid = timeUUID.asUUID(); + return TimeUUID.fromBytes(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits() & sequenceMask | bits); + } + // needs to return two different values for the same when. // we can generate at most 10k UUIDs per ms. private static long nextUnixMicros() @@ -504,7 +535,7 @@ private static byte[] hash(Collection data) } // Identify the process on the load: we use both the PID and class loader hash. - long pid = NativeLibrary.getProcessID(); + long pid = INativeLibrary.instance.getProcessID(); if (pid < 0) pid = new Random(currentTimeMillis()).nextLong(); updateWithLong(hasher, pid); diff --git a/src/java/org/apache/cassandra/utils/TopKSelector.java b/src/java/org/apache/cassandra/utils/TopKSelector.java new file mode 100644 index 000000000000..7b32851c991e --- /dev/null +++ b/src/java/org/apache/cassandra/utils/TopKSelector.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +/** + * This class selects the smallest k items from a stream. + *

    + * This is implemented as a binary heap with reversed comparator which keeps track of k items and keeps the largest of + * them on top of the heap. When a new item arrives, it is checked against the top: if it is larger or equal, it can + * be ignored as we already have k better items; if not, it replaces the top item and is pushed down to restore the + * properties of the heap. + *

    + * This process has a time complexity of O(n log k) for n > k and uses O(k) space. Duplicates are not removed and are + * returned in arbitrary order. + *

    + * If the number of items required is not known in advance, {@link SortingIterator} can be used instead to get an + * arbitrary number of ordered items at the expense of keeping track of all of them (using O(n + k log n) time and O(n) + * space). + */ +public class TopKSelector extends BinaryHeap +{ + private final Comparator comparator; + private int size; + + public TopKSelector(Comparator comparator, int limit) + { + super(new Object[limit]); + this.comparator = comparator; + size = 0; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean greaterThan(Object a, Object b) + { + // Top-k uses an inverted comparator, so that the largest item, the one we should compare with and replace + // if something smaller is added, sits at the top. This is also the comparator suitable for doing the final + // heapsort steps required to arrange the end result in sort order. + return comparator.compare((T) a, (T) b) < 0; + } + + public void add(T newItem) + { + if (newItem == null) + return; + + if (size < heap.length) + { + heap[size] = newItem; + if (++size == heap.length) + heapify(); + } + else + { + if (greaterThan(newItem, top())) + replaceTop(newItem); + } + } + + public void addAll(Iterable items) + { + for (T item : items) + add(item); + } + + @Override + public int size() + { + return size; + } + + private void maybeHeapify() + { + if (size < heap.length) + heapify(); + } + + /** + * Get a copy of the top K elements. + * After this call the collector can be reused. + */ + public List get() + { + return new ArrayList<>(getShared()); + } + + /** + * Get a copy of the top K elements, applying the given transformation. + * After this call the collector can be reused. + */ + public List getTransformed(Function transformer) + { + return getTransformedSliced(transformer, 0); + } + + /** + * Get a copy of the lowest size-startIndex elements. + * The top startIndex elements will remain in the selector. + */ + public List getSliced(int startIndex) + { + return getTransformedSliced(Function.identity(), startIndex); + } + + /** + * Get a copy of the lowest size-startIndex elements, applying the given transformation. + * The top startIndex elements will remain in the selector. + */ + public List getTransformedSliced(Function transformer, int startIndex) + { + return new ArrayList<>(getTransformedSlicedShared(transformer, startIndex)); + } + + /** + * Get a shared list of the top K elements. + * If the selector is not used further, this is a quicker alternative to get(). + */ + public List getShared() + { + maybeHeapify(); + heapSort(); + int completedSize = size; + size = 0; + return getUnsortedShared(completedSize); + } + + /** + * Get a shared list of the top K elements in unsorted order. + * This avoids the final sort phase (and heapification if there are fewer than K elements). + */ + public List getUnsortedShared() + { + return getUnsortedShared(size); + } + + private List getUnsortedShared(int size) + { + return new AbstractList() + { + @Override + public T get(int i) + { + return (T) heap[i]; + } + + @Override + public int size() + { + return size; + } + }; + } + + /** + * Get a shared list of the lowest size-startIndex elements, applying the given transformation. + * If the selector is not used further, this is a quicker alternative to getTransformedSliced(). + */ + public List getTransformedSlicedShared(Function transformer, int startIndex) + { + int selectedSize = size() - startIndex; + if (selectedSize <= 0) + return List.of(); + maybeHeapify(); + + heapSortFrom(startIndex); + size = startIndex; // the rest of the top items remain heapified and can be extracted later + return new AbstractList() + { + @Override + public R get(int i) + { + return transformer.apply((T) heap[i + startIndex]); + } + + @Override + public int size() + { + return selectedSize; + } + }; + } +} diff --git a/src/java/org/apache/cassandra/utils/UniqueComparator.java b/src/java/org/apache/cassandra/utils/UniqueComparator.java new file mode 100644 index 000000000000..9c6513f9647b --- /dev/null +++ b/src/java/org/apache/cassandra/utils/UniqueComparator.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Comparator; + +/** + * Converts any comparator to a comparator that never treats distinct objects as equal, + * even if the original comparator considers them equal. + * For all other items, the order of the original comparator is preserved. + * Allows to store duplicate items in sorted sets. + */ +public class UniqueComparator implements Comparator +{ + private final Comparator comparator; + + public UniqueComparator(Comparator comparator) + { + this.comparator = comparator; + } + + @Override + public int compare(T o1, T o2) + { + int result = comparator.compare(o1, o2); + if (result == 0 && o1 != o2) + { + // If the wrapped comparator considers the items equal, + // but they are not actually the same object, distinguish them + return System.identityHashCode(o1) - System.identityHashCode(o2); + } + return result; + } +} diff --git a/src/java/org/apache/cassandra/utils/btree/BTree.java b/src/java/org/apache/cassandra/utils/btree/BTree.java index 8674d714daf8..e5a3a41493ad 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTree.java +++ b/src/java/org/apache/cassandra/utils/btree/BTree.java @@ -350,8 +350,8 @@ public static Object if (isEmpty(toUpdate)) { - if (isSimple(updateF)) - return insert; // if update is empty and updateF is trivial, return our new input +// if (isSimple(updateF)) +// return insert; // if update is empty and updateF is trivial, return our new input // if update is empty and updateF is non-trivial, perform a simple fast transformation of the input tree insert = BTree.transform(insert, updateF::insert); @@ -3081,7 +3081,7 @@ Object[] drain() * was constructed from for the contents of {@code buffer}. *

    * For {@link FastBuilder} these are mostly the same, so they are fetched from a global cache and - * resized accordingly, but for {@link AbstractUpdater} we maintain a buffer of sizes. + * resized accordingly, but for {@link Updater} we maintain a buffer of sizes. */ int setDrainSizeMap(Object[] original, int keysInOriginal, Object[] branch, int keysInBranch) { @@ -3110,7 +3110,7 @@ int setDrainSizeMap(Object[] original, int keysInOriginal, Object[] branch, int * was constructed from for the contents of {@code savedBuffer}. *

    * For {@link FastBuilder} these are always the same size, so they are fetched from a global cache, - * but for {@link AbstractUpdater} we maintain a buffer of sizes. + * but for {@link Updater} we maintain a buffer of sizes. * * @return the size of {@code branch} */ @@ -3141,7 +3141,7 @@ int setOverflowSizeMap(Object[] branch, int keys) * was constructed from the contents of both {@code savedBuffer} and {@code buffer} *

    * For {@link FastBuilder} these are mostly the same size, so they are fetched from a global cache - * and only the last items updated, but for {@link AbstractUpdater} we maintain a buffer of sizes. + * and only the last items updated, but for {@link Updater} we maintain a buffer of sizes. */ void setRedistributedSizeMap(Object[] branch, int steal) { @@ -3269,11 +3269,57 @@ final LeafBuilder leaf() /** * Clear any references we might still retain, to avoid holding onto memory. - *

    - * While this method is not strictly necessary, it exists to - * ensure the implementing classes are aware they must handle it. */ - abstract void reset(); + void reset() + { + leaf().count = 0; + clearLeafBuffer(leaf().buffer); + if (leaf().savedBuffer != null) + clearLeafBuffer(leaf().savedBuffer); + leaf().savedNextKey = null; + BranchBuilder branch = leaf().parent; + while (branch != null && branch.inUse) + { + branch.count = 0; + clearBranchBuffer(branch.buffer); + if (branch.savedBuffer != null) + clearBranchBuffer(branch.savedBuffer); + branch.savedNextKey = null; + branch.inUse = false; + branch = branch.parent; + } + } + + /** + * Clear the contents of a leaf buffer, aborting once we encounter a null entry + * to save time on small trees + */ + private void clearLeafBuffer(Object[] array) + { + if (array[0] == null) + return; + // find first null entry; loop from beginning, to amortise cost over size of working set + int i = 1; + while (i < array.length && array[i] != null) + ++i; + Arrays.fill(array, 0, i, null); + } + + /** + * Clear the contents of a branch buffer, aborting once we encounter a null entry + * to save time on small trees + */ + private void clearBranchBuffer(Object[] array) + { + if (array[0] == null && array[MAX_KEYS] == null) + return; + // find first null entry; loop from beginning, to amortise cost over size of working set + int i = 1; + while (i < MAX_KEYS && array[i] != null) + ++i; + Arrays.fill(array, 0, i, null); + Arrays.fill(array, MAX_KEYS, MAX_KEYS + i + 1, null); + } } /** @@ -3330,16 +3376,21 @@ void reset() { Arrays.fill(leaf().buffer, null); leaf().count = 0; + leaf().savedBuffer = null; + leaf().savedNextKey = null; BranchBuilder branch = leaf().parent; while (branch != null && branch.inUse) { Arrays.fill(branch.buffer, null); branch.count = 0; + branch.savedBuffer = null; + branch.savedNextKey = null; branch.inUse = false; branch = branch.parent; } } + @VisibleForTesting public boolean validateEmpty() { LeafOrBranchBuilder cur = leaf(); @@ -3374,6 +3425,7 @@ void reset() clearLeafBuffer(leaf().buffer); if (leaf().savedBuffer != null) Arrays.fill(leaf().savedBuffer, null); + leaf().savedNextKey = null; BranchBuilder branch = leaf().parent; while (branch != null && branch.inUse) @@ -3382,6 +3434,7 @@ void reset() clearBranchBuffer(branch.buffer); if (branch.savedBuffer != null && branch.savedBuffer[0] != null) Arrays.fill(branch.savedBuffer, null); // by definition full, if non-empty + branch.savedNextKey = null; branch.inUse = false; branch = branch.parent; } @@ -3434,7 +3487,7 @@ private void clearBranchBuffer(Object[] array) * Searches within both trees to accelerate the process of modification, instead of performing a simple * iteration over the new tree. */ - private static class Updater extends AbstractUpdater implements AutoCloseable + private static class Updater extends AbstractFastBuilder implements AutoCloseable { static final TinyThreadLocalPool POOL = new TinyThreadLocalPool<>(); TinyThreadLocalPool.TinyPool pool; @@ -3647,7 +3700,7 @@ static int searchResultToComparison(int searchResult) *

    * The approach taken here hopefully balances simplicity, garbage generation and execution time. */ - private static abstract class AbstractTransformer extends AbstractUpdater implements AutoCloseable + private static abstract class AbstractTransformer extends AbstractFastBuilder implements AutoCloseable { /** * An iterator over the tree we are updating @@ -4218,4 +4271,38 @@ int copyKeysSmallerThan(Compare bound, Comparator comp } } } + + public interface ReduceFunction extends BiFunction + { + default public boolean stop(ACC res) + { + return false; + } + } + + /** + * Walk the btree forwards and apply a reduce function. Return the reduced value. + */ + public static R reduce(Object[] btree, R seed, ReduceFunction function) + { + boolean isLeaf = isLeaf(btree); + int childOffset = isLeaf ? Integer.MAX_VALUE : getChildStart(btree); + int limit = isLeaf ? getLeafKeyEnd(btree) : btree.length - 1; + for (int i = 0 ; i < limit ; i++) + { + // we want to visit in iteration order, so we visit our key nodes inbetween our children + int idx = isLeaf ? i : (i / 2) + (i % 2 == 0 ? childOffset : 0); + Object current = btree[idx]; + if (idx < childOffset) + seed = function.apply(seed, (V)current); + else + seed = reduce((Object[])current, seed, function); + + if (function.stop(seed)) + break; + } + + return seed; + } + } diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeSet.java b/src/java/org/apache/cassandra/utils/btree/BTreeSet.java index 1bd324a7e178..08b0152113a6 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTreeSet.java +++ b/src/java/org/apache/cassandra/utils/btree/BTreeSet.java @@ -323,6 +323,48 @@ public ListIterator listIterator(int index) throw new UnsupportedOperationException(); } + // @Override needed in JDK 21+. + public BTreeSet reversed() + { + throw new UnsupportedOperationException(); + } + + // @Override needed in JDK 21+. + public V removeLast() + { + throw new UnsupportedOperationException(); + } + + // @Override needed in JDK 21+. + public V removeFirst() + { + throw new UnsupportedOperationException(); + } + + // @Override needed in JDK 21+. + public V getLast() + { + throw new UnsupportedOperationException(); + } + + // @Override needed in JDK 21+. + public V getFirst() + { + throw new UnsupportedOperationException(); + } + + // @Override needed in JDK 21+. + public void addLast(V v) + { + throw new UnsupportedOperationException(); + } + + // @Override needed in JDK 21+. + public void addFirst(V v) + { + throw new UnsupportedOperationException(); + } + public static class BTreeRange extends BTreeSet { // both inclusive diff --git a/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.java b/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.java index c79dffcd79d7..7b18805373cb 100644 --- a/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.java +++ b/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.java @@ -20,6 +20,9 @@ import java.nio.ByteBuffer; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + /** * Interface indicating a value can be represented/identified by a comparable {@link ByteSource}. * @@ -36,8 +39,9 @@ public interface ByteComparable enum Version { - LEGACY, // Encoding used in legacy sstable format; forward (value to byte-comparable) translation only - OSS50, // CASSANDRA 5.0 encoding + LEGACY, + OSS41, // CASSANDRA 4.1 encoding, used in trie-based indices + OSS50, // CASSANDRA 5.0 encoding, used by the trie memtable } ByteComparable EMPTY = (Version version) -> ByteSource.EMPTY; @@ -56,10 +60,25 @@ default String byteComparableAsString(Version version) return builder.toString(); } + /** + * Returns the full byte-comparable representation of the value as a byte array. + */ + default byte[] asByteComparableArray(Version version) + { + return ByteSourceInverse.readBytes(asComparableBytes(version)); + } + + default Preencoded preencode(Version version) + { + return preencoded(version, asByteComparableArray(version)); + } + // Simple factories used for testing + @VisibleForTesting static ByteComparable of(String s) { + // Note: This is not prefix-free return v -> ByteSource.of(s, v); } @@ -73,19 +92,54 @@ static ByteComparable of(int value) return v -> ByteSource.of(value); } - static ByteComparable fixedLength(ByteBuffer bytes) + interface Preencoded extends ByteComparable { - return v -> ByteSource.fixedLength(bytes); + Version encodingVersion(); + + ByteSource.Duplicatable getPreencodedBytes(); + + @Override + default ByteSource.Duplicatable asComparableBytes(Version version) + { + Preconditions.checkState(version == encodingVersion(), + "Preencoded byte-source at version %s queried at version %s", + encodingVersion(), + version); + return getPreencodedBytes(); + } + + @Override + default byte[] asByteComparableArray(Version version) + { + return asComparableBytes(version).remainingBytesToArray(); + } } - static ByteComparable fixedLength(byte[] bytes) + /** + * A ByteComparable value that is already encoded for a specific version. Requesting the source with a different + * version will result in an exception. + */ + static Preencoded preencoded(Version version, ByteBuffer bytes) { - return v -> ByteSource.fixedLength(bytes); + return new PreencodedByteComparable.Buffer(version, bytes); } - static ByteComparable fixedLength(byte[] bytes, int offset, int len) + /** + * A ByteComparable value that is already encoded for a specific version. Requesting the source with a different + * version will result in an exception. + */ + static Preencoded preencoded(Version version, byte[] bytes) { - return v -> ByteSource.fixedLength(bytes, offset, len); + return new PreencodedByteComparable.Array(version, bytes); + } + + /** + * A ByteComparable value that is already encoded for a specific version. Requesting the source with a different + * version will result in an exception. + */ + static Preencoded preencoded(Version version, byte[] bytes, int offset, int len) + { + return new PreencodedByteComparable.Array(version, bytes, offset, len); } /** @@ -127,29 +181,29 @@ static int length(ByteComparable src, Version version) } /** - * Compare two byte-comparable values by their byte-comparable representation. Used for tests. + * Compare two byte-comparable values by their byte-comparable representation. * * @return the result of the lexicographic unsigned byte comparison of the byte-comparable representations of the * two arguments */ static int compare(ByteComparable bytes1, ByteComparable bytes2, Version version) { - ByteSource s1 = bytes1.asComparableBytes(version); - ByteSource s2 = bytes2.asComparableBytes(version); - - if (s1 == null || s2 == null) - return Boolean.compare(s1 != null, s2 != null); + return ByteSource.compare(bytes1.asComparableBytes(version), bytes2.asComparableBytes(version)); + } - while (true) - { - int b1 = s1.next(); - int b2 = s2.next(); - int cmp = Integer.compare(b1, b2); - if (cmp != 0) - return cmp; - if (b1 == ByteSource.END_OF_STREAM) - return 0; - } + /** + * Compare two preencoded byte-comparable values, using their encoding versions. + * + * @return the result of the lexicographic unsigned byte comparison of the byte-comparable representations of the + * two arguments + */ + static int compare(Preencoded a, Preencoded b) + { + Preconditions.checkArgument(a.encodingVersion() == b.encodingVersion(), + "Cannot compare preencoded byte-comparables of different versions %s vs %s", + a.encodingVersion(), + b.encodingVersion()); + return ByteSource.compare(a.getPreencodedBytes(), b.getPreencodedBytes()); } /** diff --git a/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.md b/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.md index 8012e27b03e5..f26b256669ec 100644 --- a/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.md +++ b/src/java/org/apache/cassandra/utils/bytecomparable/ByteComparable.md @@ -334,7 +334,7 @@ This is the trivial case, as we can simply use the input bytes in big-endian ord and fixed length values are trivially prefix free, i.e. (1) and (2) are satisfied, and thus (3) and (4) follow from the observation above. -## Fixed-length signed integers (byte, short, int, legacy bigint) +## Fixed-length signed integers (byte, short, int, bigint for versions <= OSS41) As above, but we need to invert the sign bit of the number to put negative numbers before positives. This maps `MIN_VALUE` to `0x00`..., `-1` to `0x7F…`, `0` to `0x80…`, and `MAX_VALUE` to `0xFF…`; comparing the resulting number @@ -457,15 +457,15 @@ end. The values we chose for the separator and terminator are `0x40` and `0x38`, Examples: -| Types and values | bytes | encodes as | -| ------------------------ | ---------------------- | ------------------------------ | +| Types and values | bytes | encodes as | +| ------------------------ | ---------------------- |----------------------------| | (short 1, float 1.0) | 00 01, 3F 80 00 00 | 40·80 01·40·BF 80 00 00·38 | -| (short -1, null) | FF FF, — | 40·7F FF·3E·38 | +| (short -1, null) | FF FF, — | 40·7F FF·3E·38 | | ≥ (short 0, float -Inf) | 00 00, FF 80 00 00, >= | 40·80 00·40·00 7F FF FF·20 | -| < (short MIN) | 80 00, <= | 40·00 00·20 | -| \> (null) | | 3E·60 | -| BOTTOM | | 20 | -| TOP | | 60 | +| < (short MIN) | 80 00, <= | 40·00 00·20 | +| \> (null) | | 3E·60 | +| BOTTOM | | 20 | +| TOP | | 60 | (The middle dot · doesn't exist in the encoding, it’s just a visualisation of the boundaries in the examples.) @@ -501,21 +501,21 @@ The method we chose for this is the following: Examples: -| bytes/sequence | encodes as | -| ------------------ | ------------------------ | -| 22 00 | 22 00 FE | -| 22 00 00 33 | 22 00 FE FF 33 00 | -| 22 00 11 | 22 00 FF 11 00 | +| bytes/sequence | encodes as | +| ------------------ |----------------------| +| 22 00 | 22 00 FE | +| 22 00 00 33 | 22 00 FE FF 33 00 | +| 22 00 11 | 22 00 FF 11 00 | | (blob 22, short 0) | 40·22 00·40·80 00·40 | -| ≥ (blob 22 00) | 40·22 00 FE·20 | -| ≤ (blob 22 00 00) | 40·22 00 FE FE·60 | +| ≥ (blob 22 00) | 40·22 00 FE·20 | +| ≤ (blob 22 00 00) | 40·22 00 FE FE·60 | Within the encoding, a `00` byte can only be followed by a `FE` or `FF` byte, and hence if an encoding is a prefix of another, the latter has to have a `FE` or `FF` as the next byte, which ensures both (4) (adding `10`-`EF` to the former makes it no longer a prefix of the latter) and (3) (adding `10`-`EF` to the former makes it smaller than the latter; in this case the original value of the former is a prefix of the original value of the latter). -## Variable-length integers (varint, RandomPartitioner token), legacy encoding +## Variable-length integers (varint, RandomPartitioner token), OSS41 and earlier If integers of unbounded length are guaranteed to start with a non-zero digit, to compare them we can first use a signed length, as numbers with longer representations have higher magnitudes. Only if the lengths match we need to compare the @@ -544,8 +544,8 @@ as well. Examples: -| value | bytes | encodes as | -| ------: | ---------------- | ----------------------- | +| value | bytes | encodes as | +| ------: | ---------------- |------------------------| | 0 | 00 | 80·00 | | 1 | 01 | 80·01 | | -1 | FF | 7F·FF | @@ -585,18 +585,18 @@ inverted length bytes), and bigger when positive. Examples: -| value | bytes | encodes as | -| ------: | ----------------------- | ------------------------------- | -| 0 | 00 | 80 | -| 1 | 01 | 81 | -| -1 | FF | 7F | -| 255 | 00 FF | C0 FF | -| -256 | FF 00 | 3F 00 | -| 256 | 01 00 | C1 00 | -| 2^16 | 01 00 00 | E1 00 00 | -| -2^32 | FF 00 00 00 00 | 07 00 00 00 00 | -| 2^56-1 | 00 FF FF FF FF FF FF FF | FE FF FF FF FF FF FF FF | -| -2^56 | FF 00 00 00 00 00 00 00 | 01 00 00 00 00 00 00 00 | +| value | bytes | encodes as | +| ------: | ----------------------- |-------------------------------| +| 0 | 00 | 80 | +| 1 | 01 | 81 | +| -1 | FF | 7F | +| 255 | 00 FF | C0 FF | +| -256 | FF 00 | 3F 00 | +| 256 | 01 00 | C1 00 | +| 2^16 | 01 00 00 | E1 00 00 | +| -2^32 | FF 00 00 00 00 | 07 00 00 00 00 | +| 2^56-1 | 00 FF FF FF FF FF FF FF | FE FF FF FF FF FF FF FF | +| -2^56 | FF 00 00 00 00 00 00 00 | 01 00 00 00 00 00 00 00 | | 2^56 | 01 00 00 00 00 00 00 00 | FF·00·01 00 00 00 00 00 00 00 | | -2^56-1 | FE FF FF FF FF FF FF FF | 00·FF·FE FF FF FF FF FF FF FF | | 2^1024 | 01 00(128 times) | FF·7A·01 00(128 times) | @@ -671,13 +671,13 @@ byte: Examples: -| value | mexp | mantissa | mantissa in bytes | encodes as | -| ---------: | ----: | -------- | ----------------- | -------------------- | +| value | mexp | mantissa | mantissa in bytes | encodes as | +| ---------: | ----: | -------- | ----------------- |-------------------| | 1.1 | 1 | 0.0110 | . 01 10 | C1·01·81 8A·00 | | 1 | 1 | 0.01 | . 01 | C1·01·81·00 | -| 0.01 | 0 | 0.01 | . 01 | C0·81·00 | -| 0 | | | | 80 | -| -0.01 | 0 | -0.01 | . -01 | 40·81·00 | +| 0.01 | 0 | 0.01 | . 01 | C0·81·00 | +| 0 | | | | 80 | +| -0.01 | 0 | -0.01 | . -01 | 40·81·00 | | -1 | -1 | -0.01 | . -01 | 3F·FF·7F·00 | | -1.1 | -1 | -0.0110 | . -02 90 | 3F·FF·7E DA·00 | | -98.9 | -1 | -0.9890 | . -99 10 | 3F·FF·1D 8A·00 | diff --git a/src/java/org/apache/cassandra/utils/bytecomparable/ByteSource.java b/src/java/org/apache/cassandra/utils/bytecomparable/ByteSource.java index 83bb828f3096..9b73c6d5454b 100644 --- a/src/java/org/apache/cassandra/utils/bytecomparable/ByteSource.java +++ b/src/java/org/apache/cassandra/utils/bytecomparable/ByteSource.java @@ -19,8 +19,12 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.cassandra.db.marshal.ByteArrayAccessor; import org.apache.cassandra.db.marshal.ValueAccessor; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FastByteOperations; import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.memory.MemoryUtil; @@ -36,6 +40,26 @@ public interface ByteSource /** Consume the next byte, unsigned. Must be between 0 and 255, or END_OF_STREAM if there are no more bytes. */ int next(); + /** + * Consume the next bytes of the source and transfer them to the given array. + * + * @return The number of bytes transferred. If equal to the size of the destination, the source may have further + * bytes to consume. Otherwise, the source has been fully consumed, and it would be an error to call this + * method (or next()) again. + */ + default int nextBytes(byte[] dest) + { + int i; + for (i = 0; i < dest.length; ++i) + { + int next = next(); + if (next == END_OF_STREAM) + return i; + dest[i] = (byte) next; + } + return i; + } + /** Value returned if at the end of the stream. */ int END_OF_STREAM = -1; @@ -57,11 +81,14 @@ public interface ByteSource // Next component marker. int NEXT_COMPONENT = 0x40; - // Marker used to present null values represented by empty buffers (e.g. by Int32Type) - int NEXT_COMPONENT_EMPTY = 0x3F; - int NEXT_COMPONENT_EMPTY_REVERSED = 0x41; - // Marker for null components in tuples, maps, sets and clustering keys. - int NEXT_COMPONENT_NULL = 0x3E; + // Marker used to present null values represented by empty buffers (e.g. by Int32Type), as well as nulls in + // collections. + int NEXT_COMPONENT_NULL = 0x3F; + int NEXT_COMPONENT_NULL_REVERSED = 0x41; + // Marker for null components in clustering keys. Null clusterings are normally encoded by empty buffers (which end + // up using NEXT_COMPONENT_EMPTY above), but in some cases (secondary indexes and compact storage) we may get null + // pointers that compare differently. + int NEXT_CLUSTERING_NULL = 0x3E; // Section for next component markers which is not allowed for use int MIN_NEXT_COMPONENT = 0x3C; @@ -274,37 +301,27 @@ public int next() }; } - /** - * Wrap a ByteSource in a length-fixing facade. - * - * If the length of {@code src} is less than {@code cutoff}, then pad it on the right with {@code padding} until - * the overall length equals {@code cutoff}. If the length of {@code src} is greater than {@code cutoff}, then - * truncate {@code src} to that size. Effectively a noop if {@code src} happens to have length {@code cutoff}. - * - * @param src the input source to wrap - * @param cutoff the size of the source returned - * @param padding a padding byte (an int subject to a 0xFF mask) - */ - public static ByteSource cutOrRightPad(ByteSource src, int cutoff, int padding) + public static ByteSource append(ByteSource src, int lastByte) { return new ByteSource() { - int pos = 0; + boolean done = false; @Override public int next() { - if (pos++ >= cutoff) - { + if (done) return END_OF_STREAM; - } - int next = src.next(); - return next == END_OF_STREAM ? padding : next; + int n = src.next(); + if (n != END_OF_STREAM) + return n; + + done = true; + return lastByte; } }; } - /** * Variable-length encoding. Escapes 0s as ESCAPE + zero or more ESCAPED_0_CONT + ESCAPED_0_DONE. * If the source ends in 0, we use ESCAPED_0_CONT to make sure that the encoding remains smaller than that source @@ -666,7 +683,8 @@ public int next() } /** - * Combination of multiple byte sources. Adds {@link NEXT_COMPONENT} before sources, or {@link NEXT_COMPONENT_NULL} if next is null. + * Combination of multiple byte sources. Adds {@link #NEXT_COMPONENT} before sources, or {@link #NEXT_COMPONENT_NULL} + * if next is null. */ static class Multi implements ByteSource { @@ -737,29 +755,78 @@ public int next() } } + /** + * A byte source representing a value of fixed length than can be compared using unsigned byte comparison. Such + * value can be used unchanged because their fixed length ensures that the encoding is prefix-free. + * This method also permits the value to be empty and encodes this as null. + */ static ByteSource optionalFixedLength(ValueAccessor accessor, V data) { - return !accessor.isEmpty(data) ? fixedLength(accessor, data) : null; + return !accessor.isEmpty(data) ? preencoded(accessor, data) : null; } /** - * A byte source of the given bytes without any encoding. - * The resulting source is only guaranteed to give correct comparison results and be prefix-free if the - * underlying type has a fixed length. - * In tests, this method is also used to generate non-escaped test cases. + * A byte source of the given bytes without any encoding. This has several uses: + * - to store a value that is already encoded for a given version (see ByteComparable.preencoded) + * - to store fixed-length values that can be used directly because their length ensures that the encoding is + * prefix-free (see optionalFixedLength) + * - to implement ByteSource duplication + * - to store a value that has a custom encoding not handled by ByteSource and AbstractType implementations + * (e.g. some SAI indexes) + * - to generate non-escaped test cases */ - public static ByteSource fixedLength(ValueAccessor accessor, V data) + public static ByteSource preencoded(ValueAccessor accessor, V data) { - return new ByteSource() + return new PreencodedBytesByAccessor<>(accessor, data, 0, accessor.size(data)); + } + + class PreencodedBytesByAccessor implements Duplicatable + { + int pos; + final int end; + final V data; + final ValueAccessor accessor; + + PreencodedBytesByAccessor(ValueAccessor accessor, V data, int start, int end) { - int pos = -1; + this.data = data; + this.accessor = accessor; + this.pos = start; + this.end = end; + } - @Override - public int next() - { - return ++pos < accessor.size(data) ? accessor.getByte(data, pos) & 0xFF : END_OF_STREAM; - } - }; + @Override + public int next() + { + return pos < end ? accessor.getByte(data, pos++) & 0xFF : END_OF_STREAM; + } + + @Override + public int peek() + { + return pos < end ? accessor.getByte(data, pos) & 0xFF : END_OF_STREAM; + } + + @Override + public int nextBytes(byte[] array) + { + int len = Math.min(end - pos, array.length); + accessor.copyTo(data, pos, array, ByteArrayAccessor.instance, 0, len); + pos += len; + return len; + } + + @Override + public byte[] remainingBytesToArray() + { + return accessor.toArray(data, pos, end - pos); + } + + @Override + public Duplicatable duplicate() + { + return new PreencodedBytesByAccessor(accessor, data, pos, end); + } } /** @@ -768,18 +835,56 @@ public int next() * underlying type has a fixed length. * In tests, this method is also used to generate non-escaped test cases. */ - public static ByteSource fixedLength(ByteBuffer b) + public static Duplicatable preencoded(ByteBuffer b) { - return new ByteSource() + return new PreencodedByteBuffer(b, b.position(), b.limit()); + } + + class PreencodedByteBuffer implements Duplicatable + { + int pos; + final int end; + final ByteBuffer b; + + PreencodedByteBuffer(ByteBuffer b, int start, int end) { - int pos = b.position() - 1; + this.b = b; + this.pos = start; + this.end = end; + } - @Override - public int next() - { - return ++pos < b.limit() ? b.get(pos) & 0xFF : END_OF_STREAM; - } - }; + @Override + public int next() + { + return pos < end ? b.get(pos++) & 0xFF : END_OF_STREAM; + } + + @Override + public int peek() + { + return pos < end ? b.get(pos) & 0xFF : END_OF_STREAM; + } + + @Override + public int nextBytes(byte[] array) + { + int len = Math.min(end - pos, array.length); + FastByteOperations.copy(b, pos, array, 0, len); + pos += len; + return len; + } + + @Override + public byte[] remainingBytesToArray() + { + return ByteBufferUtil.getArray(b, pos, end - pos); + } + + @Override + public Duplicatable duplicate() + { + return new PreencodedByteBuffer(b, pos, end); + } } /** @@ -788,36 +893,79 @@ public int next() * underlying type has a fixed length. * In tests, this method is also used to generate non-escaped test cases. */ - public static ByteSource fixedLength(byte[] b) + public static Duplicatable preencoded(byte[] b) { - return fixedLength(b, 0, b.length); + return preencoded(b, 0, b.length); } - public static ByteSource fixedLength(byte[] b, int offset, int length) + public static Duplicatable preencoded(byte[] b, int offset, int length) { checkArgument(offset >= 0 && offset <= b.length); checkArgument(length >= 0 && offset + length <= b.length); - return new ByteSource() + return new PreencodedBytes(b, offset, offset + length); + } + + class PreencodedBytes implements Duplicatable + { + int pos; + final int end; + final byte[] b; + + PreencodedBytes(byte[] b, int start, int end) { - int pos = offset - 1; + this.b = b; + this.pos = start; + this.end = end; + } - @Override - public int next() - { - return ++pos < offset + length ? b[pos] & 0xFF : END_OF_STREAM; - } - }; + @Override + public int next() + { + return pos < end ? b[pos++] & 0xFF : END_OF_STREAM; + } + + @Override + public int peek() + { + return pos < end ? b[pos] & 0xFF : END_OF_STREAM; + } + + @Override + public int nextBytes(byte[] array) + { + int len = Math.min(end - pos, array.length); + FastByteOperations.copy(b, pos, array, 0, len); + pos += len; + return len; + } + + @Override + public byte[] remainingBytesToArray() + { + return Arrays.copyOfRange(b, pos, end); + } + + @Override + public Duplicatable duplicate() + { + return new PreencodedBytes(b, pos, end); + } } - public class Peekable implements ByteSource + interface Peekable extends ByteSource + { + int peek(); + } + + public class PeekableImpl implements Peekable { private static final int NONE = Integer.MIN_VALUE; private final ByteSource wrapped; private int peeked = NONE; - public Peekable(ByteSource wrapped) + public PeekableImpl(ByteSource wrapped) { this.wrapped = wrapped; } @@ -852,6 +1000,41 @@ public static Peekable peekable(ByteSource p) return null; return (p instanceof Peekable) ? (Peekable) p - : new Peekable(p); + : new PeekableImpl(p); + } + + interface ConvertableToArray extends Peekable + { + byte[] remainingBytesToArray(); + } + + interface Duplicatable extends ConvertableToArray + { + Duplicatable duplicate(); + } + + public static Duplicatable duplicatable(ByteSource src) + { + if (src instanceof Duplicatable) + return (Duplicatable) src; + + return preencoded(ByteSourceInverse.readBytes(src)); + } + + static int compare(ByteSource s1, ByteSource s2) + { + if (s1 == null || s2 == null) + return Boolean.compare(s1 != null, s2 != null); + + while (true) + { + int b1 = s1.next(); + int b2 = s2.next(); + int cmp = Integer.compare(b1, b2); + if (cmp != 0) + return cmp; + if (b1 == END_OF_STREAM) + return 0; + } } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/utils/bytecomparable/ByteSourceInverse.java b/src/java/org/apache/cassandra/utils/bytecomparable/ByteSourceInverse.java index 4bf9d8c36522..a18bf2d2bd2e 100644 --- a/src/java/org/apache/cassandra/utils/bytecomparable/ByteSourceInverse.java +++ b/src/java/org/apache/cassandra/utils/bytecomparable/ByteSourceInverse.java @@ -18,11 +18,14 @@ package org.apache.cassandra.utils.bytecomparable; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import com.google.common.base.Preconditions; import org.apache.cassandra.db.marshal.ValueAccessor; +import org.apache.cassandra.utils.ByteArrayUtil; /** * Contains inverse transformation utilities for {@link ByteSource}s. @@ -31,7 +34,6 @@ */ public final class ByteSourceInverse { - private static final int INITIAL_BUFFER_CAPACITY = 32; private static final int BYTE_ALL_BITS = 0xFF; private static final int BYTE_NO_BITS = 0x00; private static final int BYTE_SIGN_BIT = 1 << 7; @@ -353,53 +355,43 @@ public int next() * Reads the bytes of the given source into a byte array. Doesn't do any transformation on the bytes, just reads * them until it reads an {@link ByteSource#END_OF_STREAM} byte, after which it returns an array of all the read * bytes, excluding the {@link ByteSource#END_OF_STREAM}. - *

    - * This method sizes a tentative internal buffer array at {@code initialBufferCapacity}. However, if - * {@code byteSource} exceeds this size, the buffer array is recreated with doubled capacity as many times as - * necessary. If, after {@code byteSource} is fully exhausted, the number of bytes read from it does not exactly - * match the current size of the tentative buffer array, then it is copied into another array sized to fit the - * number of bytes read; otherwise, it is returned without that final copy step. * * @param byteSource The source which bytes we're interested in. - * @param initialBufferCapacity The initial size of the internal buffer. * @return A byte array containing exactly all the read bytes. In case of a {@code null} source, the returned byte * array will be empty. */ - public static byte[] readBytes(ByteSource byteSource, final int initialBufferCapacity) + public static byte[] readBytes(ByteSource byteSource) { - Preconditions.checkNotNull(byteSource); + if (byteSource instanceof ByteSource.ConvertableToArray) + return ((ByteSource.ConvertableToArray) byteSource).remainingBytesToArray(); - int readBytes = 0; - byte[] buf = new byte[initialBufferCapacity]; - int data; - while ((data = byteSource.next()) != ByteSource.END_OF_STREAM) + if (byteSource == null) + return ByteArrayUtil.EMPTY_BYTE_ARRAY; + + int step = 232; // size chosen so that new byte[step] fits into 256 bytes + byte[] last = new byte[step]; + int copied = byteSource.nextBytes(last); + if (copied < step) + return Arrays.copyOf(last, copied); + + List other = new ArrayList<>(); + do { - buf = ensureCapacity(buf, readBytes); - buf[readBytes++] = (byte) data; + other.add(last); + last = new byte[step]; + copied = byteSource.nextBytes(last); } + while (copied == step); - if (readBytes != buf.length) + byte[] dest = new byte[other.size() * step + copied]; + int pos = 0; + for (byte[] b : other) { - buf = Arrays.copyOf(buf, readBytes); + System.arraycopy(b, 0, dest, pos, step); + pos += step; } - return buf; - } - - /** - * Reads the bytes of the given source into a byte array. Doesn't do any transformation on the bytes, just reads - * them until it reads an {@link ByteSource#END_OF_STREAM} byte, after which it returns an array of all the read - * bytes, excluding the {@link ByteSource#END_OF_STREAM}. - *

    - * This is equivalent to {@link #readBytes(ByteSource, int)} where the second actual parameter is - * {@linkplain #INITIAL_BUFFER_CAPACITY} ({@value INITIAL_BUFFER_CAPACITY}). - * - * @param byteSource The source which bytes we're interested in. - * @return A byte array containing exactly all the read bytes. In case of a {@code null} source, the returned byte - * array will be empty. - */ - public static byte[] readBytes(ByteSource byteSource) - { - return readBytes(byteSource, INITIAL_BUFFER_CAPACITY); + System.arraycopy(last, 0, dest, pos, copied); + return dest; } public static void copyBytes(ByteSource byteSource, byte[] bytes) @@ -416,19 +408,17 @@ public static void copyBytes(ByteSource byteSource, byte[] bytes) } /** - * Ensures the given buffer has capacity for taking data with the given length - if it doesn't, it returns a copy - * of the buffer, but with double the capacity. + * Reads the bytes of the given source into the given byte array and returns the number of bytes read. Doesn't do + * any transformation on the bytes, just reads them until it reads an {@code ByteSource.END_OF_STREAM} byte. If the + * target byte array does not have enough space to fit the whole source, a {@code RuntimeException} is thrown. See + * also {@link ByteSource#nextBytes(byte[])}. */ - private static byte[] ensureCapacity(byte[] buf, int dataLengthInBytes) + public static int readBytesMustFit(ByteSource byteSource, byte[] dest) { - if (dataLengthInBytes == buf.length) - // We won't gain much with guarding against overflow. We'll overflow when dataLengthInBytes >= 1 << 30, - // and if we do guard, we'll be able to extend the capacity to Integer.MAX_VALUE (which is 1 << 31 - 1). - // Controlling the exception that will be thrown shouldn't matter that much, and in practice, we almost - // surely won't be reading gigabytes of ByteSource data at once. - return Arrays.copyOf(buf, dataLengthInBytes * 2); - else - return buf; + int read = byteSource.nextBytes(dest); + if (read == dest.length && byteSource.next() != ByteSource.END_OF_STREAM) + throw new RuntimeException(String.format("Number of bytes available exceeds the buffer size of %d.", dest.length)); + return read; } /** @@ -478,7 +468,7 @@ public static ByteSource.Peekable nextComponentSource(ByteSource.Peekable source public static boolean nextComponentNull(int separator) { - return separator == ByteSource.NEXT_COMPONENT_NULL || separator == ByteSource.NEXT_COMPONENT_EMPTY - || separator == ByteSource.NEXT_COMPONENT_EMPTY_REVERSED; + return separator == ByteSource.NEXT_COMPONENT_NULL + || separator == ByteSource.NEXT_COMPONENT_NULL_REVERSED; } } diff --git a/src/java/org/apache/cassandra/utils/bytecomparable/PreencodedByteComparable.java b/src/java/org/apache/cassandra/utils/bytecomparable/PreencodedByteComparable.java new file mode 100644 index 000000000000..d14f466dcfce --- /dev/null +++ b/src/java/org/apache/cassandra/utils/bytecomparable/PreencodedByteComparable.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils.bytecomparable; + +import java.nio.ByteBuffer; + +abstract class PreencodedByteComparable implements ByteComparable.Preencoded +{ + private final Version version; + + PreencodedByteComparable(Version version) + { + this.version = version; + } + + @Override + public Version encodingVersion() + { + return version; + } + + static class Array extends PreencodedByteComparable + { + private final byte[] bytes; + private final int offset; + private final int length; + + Array(Version version, byte[] bytes) + { + this(version, bytes, 0, bytes.length); + } + + Array(Version version, byte[] bytes, int offset, int length) + { + super(version); + this.bytes = bytes; + this.offset = offset; + this.length = length; + } + + @Override + public ByteSource.Duplicatable getPreencodedBytes() + { + return ByteSource.preencoded(bytes, offset, length); + } + } + + static class Buffer extends PreencodedByteComparable + { + private final ByteBuffer bytes; + + Buffer(Version version, ByteBuffer bytes) + { + super(version); + this.bytes = bytes; + } + + @Override + public ByteSource.Duplicatable getPreencodedBytes() + { + return ByteSource.preencoded(bytes); + } + } +} diff --git a/src/java/org/apache/cassandra/utils/concurrent/LightweightRecycler.java b/src/java/org/apache/cassandra/utils/concurrent/LightweightRecycler.java index 31fbf0c4794e..cdc6ea34c6e9 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/LightweightRecycler.java +++ b/src/java/org/apache/cassandra/utils/concurrent/LightweightRecycler.java @@ -48,6 +48,7 @@ default T reuse() } /** + * @param supplier * @return a reusable instance, or allocate one via the provided supplier */ default T reuseOrAllocate(Supplier supplier) diff --git a/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java b/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java index 7f18a0ceaee4..096b102d7e52 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java +++ b/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java @@ -425,6 +425,17 @@ public void await() current.await(); } + /** + * @return true if all operations started prior to barrier.issue() have completed + */ + public boolean allPriorOpsAreFinished() + { + Group current = orderOnOrBefore; + if (current == null) + throw new IllegalStateException("This barrier needs to have issue() called on it before prior operations can complete"); + return current.isFinished(); + } + /** * returns the Group we are waiting on - any Group with {@code .compareTo(getSyncPoint()) <= 0} * must complete before await() returns diff --git a/src/java/org/apache/cassandra/utils/concurrent/Ref.java b/src/java/org/apache/cassandra/utils/concurrent/Ref.java index e268f5fd73c2..c87afe498a5f 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/Ref.java +++ b/src/java/org/apache/cassandra/utils/concurrent/Ref.java @@ -53,7 +53,6 @@ import org.apache.cassandra.utils.Shared; import sun.misc.Unsafe; import sun.nio.ch.DirectBuffer; - import org.cliffc.high_scale_lib.NonBlockingHashMap; import static java.util.Collections.emptyList; @@ -131,6 +130,13 @@ public Ref(T referent, Tidy tidy) public void release() { state.release(false); + // We require a reachability fence here to prevent the JIT from clearing the Ref reference from the stack + // during the state.release() call. If that happens, the ref may become phantom reachable, and the GC may clear + // the state as a phantom reference to the ref and then enqueue the state on the phantom queue. This allows + // the Reference-Reaper to race with our state.release call above, and the reference-reaper may be able to + // update the released flag before the non-leak release path. In this case, we report a spurious leak and a + // spurious bad release. + Reference.reachabilityFence(this); } public Throwable ensureReleased(Throwable accumulate) @@ -154,6 +160,11 @@ public T get() return referent; } + public boolean refers(T object) + { + return referent == object; + } + public Ref tryRef() { return state.globalState.ref() ? new Ref<>(referent, state.globalState) : null; diff --git a/src/java/org/apache/cassandra/utils/concurrent/Refs.java b/src/java/org/apache/cassandra/utils/concurrent/Refs.java index fb6067e21d55..aa6d465ad19d 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/Refs.java +++ b/src/java/org/apache/cassandra/utils/concurrent/Refs.java @@ -94,7 +94,7 @@ public Ref get(T referenced) */ public void release(T referenced) { - Ref ref = references.remove(referenced); + Ref ref = references.remove(referenced); if (ref == null) throw new IllegalStateException("This Refs collection does not hold a reference to " + referenced); ref.release(); @@ -107,7 +107,7 @@ public void release(T referenced) */ public boolean releaseIfHolds(T referenced) { - Ref ref = references.remove(referenced); + Ref ref = references.remove(referenced); if (ref != null) ref.release(); return ref != null; @@ -119,9 +119,9 @@ public void relaseAllExcept(Collection keep) release.retainAll(keep); release(release); } + /** * Release a retained Ref to all of the provided objects; if any is not held, an exception will be thrown - * @param release */ public void release(Collection release) { @@ -222,7 +222,7 @@ public static > Refs tryRef(Iterable ref } refs.put(rc, ref); } - return new Refs(refs); + return new Refs<>(refs); } public static > Refs ref(Iterable reference) @@ -237,9 +237,10 @@ public static void release(Iterable> refs) { maybeFail(release(refs, null)); } + public static Throwable release(Iterable> refs, Throwable accumulate) { - for (Ref ref : refs) + for (Ref ref : refs) { try { diff --git a/src/java/org/apache/cassandra/utils/concurrent/Timer.java b/src/java/org/apache/cassandra/utils/concurrent/Timer.java new file mode 100644 index 000000000000..dc586c0b68de --- /dev/null +++ b/src/java/org/apache/cassandra/utils/concurrent/Timer.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils.concurrent; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import org.apache.cassandra.concurrent.ExecutorLocals; +import org.apache.cassandra.sensors.RequestSensors; +import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.context.OperationContext; +import org.apache.cassandra.tracing.TraceState; + +/** + * Timer implementation based on the hashed wheel algorithm with 100ms precision, using Netty's + * {@link HashedWheelTimer} under the hood. + * + * @see #onTimeout(Runnable, long, TimeUnit) + * @see #onTimeout(Runnable, long, TimeUnit, ExecutorLocals) + */ +public class Timer +{ + private static final String THREAD_NAME = "hashed-wheel-timer"; + public static final Timer INSTANCE = new Timer(); + + private final HashedWheelTimer timer; + + private Timer() + { + this.timer = new HashedWheelTimer(new ThreadFactoryBuilder().setDaemon(true).setNameFormat(THREAD_NAME).build(), + 100, TimeUnit.MILLISECONDS); + + this.timer.start(); + } + + /** + * @see #onTimeout(Runnable, long, TimeUnit, ExecutorLocals) + */ + public Future onTimeout(Runnable task, long timeout, TimeUnit unit) + { + return onTimeout(task, timeout, unit, null); + } + + /** + * Schedules the given {@code task} to be run after the given {@code timeout} with related {@code unit} expires, + * and returns a {@link Future} that can be used to check for expiration and cancel the timeout. Passed + * {@code executorLocals} are eventually propagated to the executed task. + */ + public Future onTimeout(Runnable task, long timeout, TimeUnit unit, ExecutorLocals executorLocals) + { + ClientWarn.State clientWarnState = executorLocals == null ? null : executorLocals.clientWarnState; + TraceState traceState = executorLocals == null ? null : executorLocals.traceState; + RequestSensors sensors = executorLocals == null ? null : executorLocals.sensors; + OperationContext operationContext = executorLocals == null ? null : executorLocals.operationContext; + AsyncPromise result = new AsyncPromise<>(); + Timeout handle = timer.newTimeout(ignored -> + { + + ExecutorLocals.Impl.set(traceState, clientWarnState, sensors, operationContext); + try + { + task.run(); + result.setSuccess(null); + } + catch (Throwable ex) + { + result.setFailure(ex); + } + }, timeout, unit); + + return new Future() + { + @Override + public boolean cancel(boolean mayInterruptIfRunning) + { + return handle.cancel(); + } + + @Override + public boolean isCancelled() + { + return handle.isCancelled(); + } + + @Override + public boolean isDone() + { + return handle.isExpired(); + } + + @Override + public Void get() throws InterruptedException, ExecutionException + { + return result.get(); + } + + @Override + public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + return result.get(timeout, unit); + } + }; + } + + public void shutdown() + { + timer.stop(); + } +} diff --git a/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java b/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java index b42cc51756cd..888fb73a1482 100644 --- a/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java +++ b/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java @@ -20,13 +20,19 @@ import java.security.AccessControlException; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + import com.google.common.collect.Maps; import org.apache.commons.lang3.StringUtils; +import org.slf4j.ILoggerFactory; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Level; @@ -38,7 +44,9 @@ import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.classic.util.ContextInitializer; import ch.qos.logback.core.Appender; +import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.hook.DefaultShutdownHook; +import ch.qos.logback.core.spi.AppenderAttachable; import org.apache.cassandra.security.ThreadAwareSecurityManager; /** @@ -171,6 +179,30 @@ private void checkOnlyOneVirtualTableAppender() VirtualTableAppender.class.getName(), String.join(",", virtualAppenderNames))); } + private Set> getAllLogbackAppenders() + { + ILoggerFactory factory = LoggerFactory.getILoggerFactory(); + LoggerContext ctx = (LoggerContext) factory; + + Set> appenders = new HashSet<>(); + ctx.getLoggerList().forEach(logger -> logger.iteratorForAppenders().forEachRemaining(a -> collectAppenders(a, appenders))); + return appenders; + } + + private static void collectAppenders(Appender appender, Collection> collection) + { + collection.add(appender); + if (appender instanceof AppenderAttachable) + ((AppenderAttachable) appender).iteratorForAppenders().forEachRemaining(a -> collectAppenders(a, collection)); + } + + public Set> getAllLogbackFilters() + { + return getAllLogbackAppenders().stream() + .flatMap(a -> a.getCopyOfAttachedFiltersList().stream()) + .collect(Collectors.toSet()); + } + private boolean hasAppenders(Logger logBackLogger) { Iterator> it = logBackLogger.iteratorForAppenders(); diff --git a/src/java/org/apache/cassandra/utils/memory/BufferPool.java b/src/java/org/apache/cassandra/utils/memory/BufferPool.java index b95a981fb922..373f38fbe770 100644 --- a/src/java/org/apache/cassandra/utils/memory/BufferPool.java +++ b/src/java/org/apache/cassandra/utils/memory/BufferPool.java @@ -26,11 +26,14 @@ import java.util.Collections; import java.util.Queue; import java.util.Set; -import java.util.concurrent.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongFieldUpdater; -import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.LongAdder; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Supplier; @@ -39,12 +42,11 @@ import jdk.internal.ref.Cleaner; import net.nicoulaj.compilecommand.annotations.Inline; -import org.apache.cassandra.concurrent.Shutdownable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.util.concurrent.FastThreadLocal; - +import org.apache.cassandra.concurrent.Shutdownable; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.metrics.BufferPoolMetrics; @@ -57,7 +59,8 @@ import static com.google.common.collect.ImmutableList.of; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE; -import static org.apache.cassandra.utils.ExecutorUtils.*; +import static org.apache.cassandra.config.CassandraRelevantProperties.BUFFERPOOL_DISABLE_COMBINED_ALLOCATION; +import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait; import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory; import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; import static org.apache.cassandra.utils.memory.MemoryUtil.isExactlyDirect; @@ -134,6 +137,7 @@ public class BufferPool private static final Logger logger = LoggerFactory.getLogger(BufferPool.class); private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 15L, TimeUnit.MINUTES); private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0); + private static final boolean DISABLE_COMBINED_ALLOCATION = BUFFERPOOL_DISABLE_COMBINED_ALLOCATION.getBoolean(); private volatile Debug debug = Debug.NO_OP; private volatile DebugLeaks debugLeaks = DebugLeaks.NO_OP; @@ -190,7 +194,7 @@ public BufferPool(String name, long memoryUsageThreshold, boolean recyclePartial this.memoryUsageThreshold = memoryUsageThreshold; this.readableMemoryUsageThreshold = prettyPrintMemory(memoryUsageThreshold); this.globalPool = new GlobalPool(); - this.metrics = new BufferPoolMetrics(name, this); + this.metrics = BufferPoolMetrics.create(name, this); this.recyclePartially = recyclePartially; this.localPoolCleaner = executorFactory().infiniteLoop("LocalPool-Cleaner-" + name, this::cleanupOneReference, UNSAFE); } @@ -219,6 +223,44 @@ public ByteBuffer getAtLeast(int size, BufferType bufferType) return localPool.get().getAtLeast(size); } + + /// Allocate the given amount of memory, where the caller can accept either of: + /// - a single buffer that can fit the whole region; + /// - multiple buffers of the given `chunkSize`. + /// + /// The total size must be a multiple of the chunk size. + /// + /// @param totalSize the total size to be allocated + /// @param chunkSize the size of each buffer returned, if the space cannot be allocated as one buffer + /// + /// @return an array of allocated buffers + public ByteBuffer[] getMultiple(int totalSize, int chunkSize, BufferType bufferType) + { + if (bufferType == BufferType.ON_HEAP) + return new ByteBuffer[] { allocate(totalSize, bufferType) }; + + // Try to find a buffer to fit the full request. Fragmentation can make this impossible even if we are below + // the limits. + LocalPool pool = localPool.get(); + if (!DISABLE_COMBINED_ALLOCATION) + { + ByteBuffer full = pool.tryGet(totalSize, false); + if (full != null) + return new ByteBuffer[]{ full }; + } + + // If we don't get a whole buffer, allocate buffers of the requested chunk size. + int numBuffers = totalSize / chunkSize; + assert totalSize == chunkSize * numBuffers + : "Total size " + totalSize + " is not a multiple of chunk size " + chunkSize; + ByteBuffer[] buffers = new ByteBuffer[numBuffers]; + + for (int idx = 0; idx < numBuffers; ++idx) + buffers[idx] = pool.get(chunkSize); + + return buffers; + } + /** Unlike the get methods, this will return null if the pool is exhausted */ public ByteBuffer tryGet(int size) { @@ -246,6 +288,17 @@ public void put(ByteBuffer buffer) updateOverflowMemoryUsage(-buffer.capacity()); } + /** + * Bulk release multiple buffers. + * + * @param buffers The buffers to be released. + */ + public void putMultiple(ByteBuffer[] buffers) + { + for (ByteBuffer buffer : buffers) + put(buffer); + } + public void putUnusedPortion(ByteBuffer buffer) { if (isExactlyDirect(buffer)) @@ -751,9 +804,9 @@ private void release() clearForEach(Chunk::release); } - private void unsafeRecycle() + private void unsafeRecycle(boolean forceEvicted) { - clearForEach(Chunk::unsafeRecycle); + clearForEach(chunk -> Chunk.unsafeRecycle(chunk, forceEvicted)); } } @@ -939,19 +992,19 @@ private ByteBuffer tryGet(int size, boolean sizeIsLowerBound) } else if (size > NORMAL_CHUNK_SIZE) { - metrics.misses.mark(); + metrics.markMissed(); return null; } ByteBuffer ret = pool.tryGetInternal(size, sizeIsLowerBound); if (ret != null) { - metrics.hits.mark(); + metrics.markHit(); memoryInUse.add(ret.capacity()); } else { - metrics.misses.mark(); + metrics.markMissed(); } return ret; } @@ -1046,9 +1099,9 @@ public void release() } @VisibleForTesting - void unsafeRecycle() + void unsafeRecycle(boolean forceEvicted) { - chunks.unsafeRecycle(); + chunks.unsafeRecycle(forceEvicted); } @VisibleForTesting @@ -1546,7 +1599,7 @@ void freeUnusedPortion(ByteBuffer buffer) @Override public String toString() { - return String.format("[slab %s, slots bitmap %s, capacity %d, free %d]", slab, Long.toBinaryString(freeSlots), capacity(), free()); + return String.format("[slab %s, slots bitmap %s, capacity %d, free %d, owner %s, recycler %s]", slab, Long.toBinaryString(freeSlots), capacity(), free(), owner, recycler); } @VisibleForTesting @@ -1562,15 +1615,17 @@ void unsafeFree() if (parent != null) parent.free(slab); else - FileUtils.clean(slab); + FileUtils.cleanWithAttachment(slab); } - static void unsafeRecycle(Chunk chunk) + static void unsafeRecycle(Chunk chunk, boolean forceRecycle) { if (chunk != null) { chunk.owner = null; chunk.freeSlots = 0L; + if (forceRecycle && !chunk.recycler.canRecyclePartially()) + chunk.setEvicted(); chunk.recycleFully(); } } @@ -1626,11 +1681,16 @@ public BufferPoolMetrics metrics() /** This is not thread safe and should only be used for unit testing. */ @VisibleForTesting public void unsafeReset() + { + unsafeReset(false); + } + @VisibleForTesting + public void unsafeReset(boolean forceEvicted) { overflowMemoryUsage.reset(); memoryInUse.reset(); memoryAllocated.set(0); - localPool.get().unsafeRecycle(); + localPool.get().unsafeRecycle(forceEvicted); globalPool.unsafeFree(); } diff --git a/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java b/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java index 34b9eaacd2f0..f03e29e64490 100644 --- a/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java +++ b/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java @@ -59,8 +59,9 @@ public DecoratedKey applyToPartitionKey(DecoratedKey key) public Row applyToRow(Row row) { - if (row == null) - return null; + // If current "row" is Rows.EMPTY_STATIC_ROW, don't copy it again, as "copied_empty_static_row" != EMPTY_STATIC_ROW + if (row == null || row == Rows.EMPTY_STATIC_ROW) + return row; return row.clone(HeapCloner.instance); } diff --git a/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java b/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java index 86416c49a703..f1ca98c452cd 100644 --- a/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java +++ b/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java @@ -21,15 +21,18 @@ import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.concurrent.atomic.AtomicLong; import com.sun.jna.Native; +import org.apache.cassandra.io.util.FileUtils; import sun.misc.Unsafe; public abstract class MemoryUtil { private static final long UNSAFE_COPY_THRESHOLD = 1024 * 1024L; // copied from java.nio.Bits + private static final AtomicLong memoryAllocated = new AtomicLong(0); protected static final Unsafe unsafe; private static final Class DIRECT_BYTE_BUFFER_CLASS, RO_DIRECT_BYTE_BUFFER_CLASS; private static final long DIRECT_BYTE_BUFFER_ADDRESS_OFFSET; @@ -47,19 +50,25 @@ public abstract class MemoryUtil Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null); - Class clazz = ByteBuffer.allocateDirect(0).getClass(); + // OpenJDK for some reason allocates bytes when capacity == 0. When -Dsun.nio.PageAlignDirectMemory is false + // DirectByteBuffer allocates 1 byte, when true a whole page is allocated. + // This breaks our native memory metrics tests that don't expect Bits.RESERVED_MEMORY > Bits.TOTAL_CAPACITY. + // The buffer will be manually cleaned to mitigate the problem. + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(0); + Class clazz = byteBuffer.getClass(); DIRECT_BYTE_BUFFER_ADDRESS_OFFSET = unsafe.objectFieldOffset(Buffer.class.getDeclaredField("address")); DIRECT_BYTE_BUFFER_CAPACITY_OFFSET = unsafe.objectFieldOffset(Buffer.class.getDeclaredField("capacity")); DIRECT_BYTE_BUFFER_LIMIT_OFFSET = unsafe.objectFieldOffset(Buffer.class.getDeclaredField("limit")); DIRECT_BYTE_BUFFER_POSITION_OFFSET = unsafe.objectFieldOffset(Buffer.class.getDeclaredField("position")); DIRECT_BYTE_BUFFER_ATTACHMENT_OFFSET = unsafe.objectFieldOffset(clazz.getDeclaredField("att")); DIRECT_BYTE_BUFFER_CLASS = clazz; - RO_DIRECT_BYTE_BUFFER_CLASS = ByteBuffer.allocateDirect(0).asReadOnlyBuffer().getClass(); + RO_DIRECT_BYTE_BUFFER_CLASS = byteBuffer.asReadOnlyBuffer().getClass(); clazz = ByteBuffer.allocate(0).getClass(); BYTE_BUFFER_CLASS = clazz; BYTE_ARRAY_BASE_OFFSET = unsafe.arrayBaseOffset(byte[].class); + FileUtils.clean(byteBuffer); } catch (Exception e) { @@ -80,14 +89,21 @@ public static long getAddress(ByteBuffer buffer) public static long allocate(long size) { + memoryAllocated.addAndGet(size); return Native.malloc(size); } - public static void free(long peer) + public static void free(long peer, long size) { + memoryAllocated.addAndGet(-size); Native.free(peer); } + public static long allocated() + { + return memoryAllocated.get(); + } + public static void setByte(long address, byte b) { unsafe.putByte(address, b); @@ -103,6 +119,30 @@ public static byte getByte(long address) return unsafe.getByte(address); } + public static long getStaticFieldOffset(Field field) + { + return unsafe.staticFieldOffset(field); + } + + /** + * @param address the memory address to use for the new buffer + * @param length in bytes of the new buffer + * @param capacity in bytes of the new buffer + * @param order byte order of the new buffer + * @param attachment byte buffer attachment + * @return a new DirectByteBuffer setup with the address, length and order required + */ + public static ByteBuffer allocateByteBuffer(long address, int length, int capacity, ByteOrder order, Object attachment) + { + ByteBuffer instance = getHollowDirectByteBuffer(order); + setDirectByteBuffer(instance, address, length, capacity); + + if (attachment != null) + MemoryUtil.setAttachment(instance, attachment); + + return instance; + } + public static ByteBuffer getByteBuffer(long address, int length) { return getByteBuffer(address, length, ByteOrder.nativeOrder()); @@ -147,7 +187,7 @@ public static Object getAttachment(ByteBuffer instance) } // Note: If encryption is used, the Object attached must implement sun.nio.ch.DirectBuffer - // @see CASSANDRA-18081 + // @see CASSANDRA-18180 public static void setAttachment(ByteBuffer instance, Object next) { assert instance.getClass() == DIRECT_BYTE_BUFFER_CLASS; @@ -172,18 +212,34 @@ public static ByteBuffer sliceDirectByteBuffer(ByteBuffer source, ByteBuffer hol } public static void setDirectByteBuffer(ByteBuffer instance, long address, int length) + { + setDirectByteBuffer(instance, address, length, length); + } + + public static void setDirectByteBuffer(ByteBuffer instance, long address, int length, int capacity) { unsafe.putLong(instance, DIRECT_BYTE_BUFFER_ADDRESS_OFFSET, address); unsafe.putInt(instance, DIRECT_BYTE_BUFFER_POSITION_OFFSET, 0); - unsafe.putInt(instance, DIRECT_BYTE_BUFFER_CAPACITY_OFFSET, length); + unsafe.putInt(instance, DIRECT_BYTE_BUFFER_CAPACITY_OFFSET, capacity); unsafe.putInt(instance, DIRECT_BYTE_BUFFER_LIMIT_OFFSET, length); } + public static void setObjectVolatile(Object o, long l, Object o1) + { + unsafe.putObjectVolatile(o, l, o1); + } + public static void setByteBufferCapacity(ByteBuffer instance, int capacity) { unsafe.putInt(instance, DIRECT_BYTE_BUFFER_CAPACITY_OFFSET, capacity); } + /** + * Transfers the contents of a buffer to Memory + * + * @param address start offset in the memory + * @param buffer the data buffer + */ public static void setBytes(long address, ByteBuffer buffer) { int start = buffer.position(); diff --git a/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java b/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java index 139d4a06b20d..02c5ddb20242 100644 --- a/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java +++ b/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java @@ -27,6 +27,7 @@ import org.apache.cassandra.utils.concurrent.OpOrder; import org.apache.cassandra.utils.concurrent.WaitQueue; +import org.github.jamm.Unmetered; public abstract class MemtableAllocator { @@ -76,6 +77,23 @@ public SubAllocator offHeap() return offHeap; } + /** + * Enforce the memtable memory limits once, before a mutation starts. + * + * Called by AbstractAllocatorMemtable.put() before a mutation starts, prior to any + * memtable-internal locks; individual allocations no longer wait for room. + */ + public void awaitRoomToStart(OpOrder.Group opGroup) + { + onHeap.awaitRoom(opGroup); + offHeap.awaitRoom(opGroup); + } + + public long unusedReservedOnHeapMemory() + { + return 0; // only slabbed allocators would have non-zero here + } + /** * Mark this allocator reclaiming; this will permit any outstanding allocations to temporarily * overshoot the maximum memory limit so that flushing can begin immediately @@ -105,6 +123,7 @@ public boolean isLive() public static class SubAllocator { // the tracker we are owning memory from + @Unmetered // total pool size should not be included in memtable's deep size private final MemtablePool.SubPool parent; // the state of the memtable @@ -165,34 +184,54 @@ public void adjust(long size, OpOrder.Group opGroup) allocate(size, opGroup); } - // allocate memory in the tracker, and mark ourselves as owning it + // account memory in the tracker, and mark ourselves as owning it public void allocate(long size, OpOrder.Group opGroup) { assert size >= 0; + // CASSANDRA-21019: individual allocations only track usage (which still drives + // cleaner/flush triggering via maybeClean); the memory limit is enforced once, + // in awaitRoom(), before a mutation starts. Blocking here mid-mutation, + // potentially while holding memtable-internal locks such as TrieMemtable's + // shard write lock, can deadlock the flush writeBarrier: a pre-barrier + // writer queued behind such a lock cannot be released by Barrier.markBlocking(), + // which only reaches threads parked in this allocator. Letting a started + // mutation run to completion also retains less memory than parking it with a + // partial copy already written and locks held. + allocated(size); + } + + /** + * Wait, if necessary, until the parent pool is below its limit, without reserving + * any memory. + * + * This is the single point at which the memory limit + * pauses writes, memtables call it before starting to apply a mutation, before + * any internal locks are taken. Groups marked blocking by Barrier.markBlocking() + * skip or are released from this wait, exactly as they were from allocate(), so a + * flush can always drain the ops its barrier awaits. + */ + public void awaitRoom(OpOrder.Group opGroup) + { + // A pool with no limit configured is never allocated from and never signalled + // (e.g. the off-heap pool under heap_buffers / unslabbed_heap_buffers, both + // created with an off-heap limit of 0) + if (parent.limit <= 0) + return; + while (true) { - if (parent.tryAllocate(size)) - { - acquired(size); - return; - } - if (opGroup.isBlocking()) - { - allocated(size); + if (parent.belowLimit() || opGroup.isBlocking()) return; - } - WaitQueue.Signal signal = parent.hasRoom().register(parent.blockedTimerContext(), Timer.Context::stop); + + WaitQueue.Signal signal = parent.hasRoom().register(parent.markMemoryBlockedOnAllocating(), Timer.Context::stop); opGroup.notifyIfBlocking(signal); - boolean allocated = parent.tryAllocate(size); - if (allocated) + if (parent.belowLimit() || opGroup.isBlocking()) { signal.cancel(); - acquired(size); return; } - else - signal.awaitThrowUncheckedOnInterrupt(); + signal.awaitThrowUncheckedOnInterrupt(); } } @@ -214,24 +253,6 @@ private void allocated(long size) } } - /** - * Retroactively mark an amount acquired in the tracker, and owned by us. If the state is discarding, - * then also update reclaiming since the flush operation is waiting at the barrier for in-flight writes, - * and it will flush this memory too. - */ - private void acquired(long size) - { - parent.acquired(); - ownsUpdater.addAndGet(this, size); - - if (state == LifeCycle.DISCARDING) - { - if (logger.isTraceEnabled()) - logger.trace("Allocated {} bytes whilst discarding", size); - updateReclaiming(); - } - } - /** * If the state is still live, then we update the memory we own here and in the parent. * diff --git a/src/java/org/apache/cassandra/utils/memory/MemtablePool.java b/src/java/org/apache/cassandra/utils/memory/MemtablePool.java index 26c47912a2eb..f0c02651877f 100644 --- a/src/java/org/apache/cassandra/utils/memory/MemtablePool.java +++ b/src/java/org/apache/cassandra/utils/memory/MemtablePool.java @@ -25,6 +25,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Timer; import org.apache.cassandra.metrics.CassandraMetricsRegistry; @@ -48,6 +49,11 @@ public abstract class MemtablePool public final SubPool offHeap; public final Timer blockedOnAllocating; + /** + * Counter metric that registers blocked write immediately in contrast to Timer blockedOnAllocating that waits for + * allocation to register wait duration. + */ + public final Counter blockedOnAllocatingCount; public final Gauge numPendingTasks; final WaitQueue hasRoom = newWaitQueue(); @@ -61,6 +67,7 @@ public abstract class MemtablePool this.cleaner = getCleaner(cleaner); DefaultNameFactory nameFactory = new DefaultNameFactory("MemtablePool"); blockedOnAllocating = CassandraMetricsRegistry.Metrics.timer(nameFactory.createMetricName("BlockedOnAllocation")); + blockedOnAllocatingCount = CassandraMetricsRegistry.Metrics.counter(nameFactory.createMetricName("BlockedOnAllocationTotal")); numPendingTasks = CassandraMetricsRegistry.Metrics.register(nameFactory.createMetricName("PendingFlushTasks"), () -> (long) this.cleaner.numPendingTasks()); } @@ -94,9 +101,9 @@ public Long getNumPendingtasks() } /** - * Note the difference between acquire() and allocate(); allocate() makes more resources available to all owners, - * and acquire() makes shared resources unavailable but still recorded. An Owner must always acquire resources, - * but only needs to allocate if there are none already available. This distinction is not always meaningful. + * Tracks memory attributed to this pool. Since CASSANDRA-21019 allocations only + * record usage (allocated/reclaiming) and drive cleaning; the limit is enforced + * before a mutation starts, via SubAllocator.awaitRoom() against belowLimit(). */ public class SubPool { @@ -148,16 +155,10 @@ private boolean updateNextClean() /** Methods to allocate space **/ - boolean tryAllocate(long size) + /** True if the pool is under its limit; reserves nothing. See SubAllocator.awaitRoom() */ + boolean belowLimit() { - while (true) - { - long cur; - if ((cur = allocated) + size > limit) - return false; - if (allocatedUpdater.compareAndSet(this, cur, cur + size)) - return true; - } + return allocated < limit; } /** @@ -184,11 +185,6 @@ void allocated(long size) maybeClean(); } - void acquired() - { - maybeClean(); - } - void released(long size) { assert size >= 0 : "Negative released: " + size; @@ -249,8 +245,9 @@ public WaitQueue hasRoom() return hasRoom; } - public Timer.Context blockedTimerContext() + public Timer.Context markMemoryBlockedOnAllocating() { + blockedOnAllocatingCount.inc(); return blockedOnAllocating.time(); } } diff --git a/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java b/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java index 0d1fdd488fce..5618fe1153ce 100644 --- a/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java +++ b/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java @@ -23,11 +23,17 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.NativeClustering; +import org.apache.cassandra.db.NativeDecoratedKey; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.NativeCell; +import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.utils.concurrent.OpOrder; -import org.apache.cassandra.utils.concurrent.Semaphore; import org.apache.cassandra.utils.concurrent.OpOrder.Group; +import org.apache.cassandra.utils.concurrent.Semaphore; import static org.apache.cassandra.utils.concurrent.Semaphore.newSemaphore; @@ -79,7 +85,7 @@ private CloningBTreeRowBuilder(OpOrder.Group writeOp, NativeAllocator allocator) @Override public void newRow(Clustering clustering) { - if (clustering != Clustering.STATIC_CLUSTERING) + if (clustering != Clustering.EMPTY && clustering != Clustering.STATIC_CLUSTERING) clustering = new NativeClustering(allocator, writeOp, clustering); super.newRow(clustering); } @@ -180,7 +186,7 @@ private void trySwapRegion(Region current, int minSize) if (currentRegion.compareAndSet(current, next)) regions.add(next); else if (!raceAllocated.stash(next)) - MemoryUtil.free(next.peer); + MemoryUtil.free(next.peer, next.capacity); } private long allocateOversize(int size) @@ -200,7 +206,7 @@ private long allocateOversize(int size) public void setDiscarded() { for (Region region : regions) - MemoryUtil.free(region.peer); + MemoryUtil.free(region.peer, region.capacity); super.setDiscarded(); } diff --git a/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java b/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java index 05f99275e467..682aef9877d7 100644 --- a/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java +++ b/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java @@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +49,8 @@ public class SlabAllocator extends MemtableBufferAllocator { private static final Logger logger = LoggerFactory.getLogger(SlabAllocator.class); - private final static int REGION_SIZE = 1024 * 1024; + @VisibleForTesting + public final static int REGION_SIZE = 1024 * 1024; private final static int MAX_CLONED_SIZE = 128 * 1024; // bigger than this don't go in the region // globally stash any Regions we allocate but are beaten to using, and use these up before allocating any more @@ -75,6 +77,17 @@ public EnsureOnHeap ensureOnHeap() return ensureOnHeap; } + @Override + public long unusedReservedOnHeapMemory() + { + if (!allocateOnHeapOnly) + return 0; + Region current = currentRegion.get(); + if (current == null) + return 0; + return current.unusedReservedMemory(); + } + public ByteBuffer allocate(int size) { return allocate(size, null); @@ -152,9 +165,10 @@ private Region getRegion() } } - public Cloner cloner(OpOrder.Group writeOp) + @Override + public Cloner cloner(OpOrder.Group opGroup) { - return allocator(writeOp); + return allocator(opGroup); } /** @@ -211,5 +225,10 @@ public String toString() return "Region@" + System.identityHashCode(this) + "waste=" + Math.max(0, data.capacity() - nextFreeOffset.get()); } + + long unusedReservedMemory() + { + return data.capacity() - nextFreeOffset.get(); + } } } diff --git a/src/java/org/apache/cassandra/utils/obs/MemoryLimiter.java b/src/java/org/apache/cassandra/utils/obs/MemoryLimiter.java new file mode 100644 index 000000000000..0715829c5006 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/obs/MemoryLimiter.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils.obs; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.cassandra.utils.FBUtilities; + +public class MemoryLimiter +{ + public final long maxMemory; + private final AtomicLong currentMemory; + private final String exceptionFormat; + + public MemoryLimiter(long maxMemory, String exceptionFormat) + { + this.maxMemory = maxMemory; + this.currentMemory = new AtomicLong(); + this.exceptionFormat = exceptionFormat; + } + + public void increment(long bytesCount, boolean failOnExceedingLimit) throws ReachedMemoryLimitException + { + assert bytesCount >= 0; + long bytesCountAfterAllocation = this.currentMemory.addAndGet(bytesCount); + // if overflow or exceeded max memory + if (bytesCountAfterAllocation < 0 || (failOnExceedingLimit && bytesCountAfterAllocation >= maxMemory)) + { + this.currentMemory.addAndGet(-bytesCount); + + throw new ReachedMemoryLimitException(String.format(exceptionFormat, + FBUtilities.prettyPrintMemory(bytesCount), + FBUtilities.prettyPrintMemory(maxMemory), + FBUtilities.prettyPrintMemory(bytesCountAfterAllocation - bytesCount))); + } + } + + public void decrement(long bytesCount) + { + assert bytesCount >= 0; + long result = this.currentMemory.addAndGet(-bytesCount); + assert result >= 0; + } + + public long memoryAllocated() + { + return currentMemory.get(); + } + + public static class ReachedMemoryLimitException extends Exception + { + public ReachedMemoryLimitException(String message) + { + super(message); + } + } +} diff --git a/src/java/org/apache/cassandra/utils/obs/OffHeapBitSet.java b/src/java/org/apache/cassandra/utils/obs/OffHeapBitSet.java index be0ecf556f0b..f9b16f952a04 100644 --- a/src/java/org/apache/cassandra/utils/obs/OffHeapBitSet.java +++ b/src/java/org/apache/cassandra/utils/obs/OffHeapBitSet.java @@ -18,6 +18,7 @@ package org.apache.cassandra.utils.obs; import java.io.*; +import java.io.IOException; import com.google.common.annotations.VisibleForTesting; @@ -34,30 +35,53 @@ */ public class OffHeapBitSet implements IBitSet { + /** + * The maximum memory that can be used by bloom filters, in megabytes, overall. + * The default is unlimited, a limit should only be set as a last resort measure. + */ + @VisibleForTesting private final Memory bytes; + private final MemoryLimiter memoryLimiter; - public OffHeapBitSet(long numBits) + public OffHeapBitSet(long numBits, MemoryLimiter memoryLimiter, boolean failOnExceedingLimit) throws MemoryLimiter.ReachedMemoryLimitException { - /** returns the number of 64 bit words it would take to hold numBits */ + this.memoryLimiter = memoryLimiter; + // returns the number of 64 bit words it would take to hold numBits long wordCount = (((numBits - 1) >>> 6) + 1); if (wordCount > Integer.MAX_VALUE) throw new UnsupportedOperationException("Bloom filter size is > 16GB, reduce the bloom_filter_fp_chance"); + + long byteCount = wordCount * 8L; + bytes = allocate(byteCount, memoryLimiter, failOnExceedingLimit); // Can possibly throw OOM, but we handle it in the caller + // flush/clear the existing memory. + clear(); + } + + private OffHeapBitSet(Memory bytes, MemoryLimiter memoryLimiter) + { + this.memoryLimiter = memoryLimiter; + this.bytes = bytes; + } + + private static Memory allocate(long byteCount, MemoryLimiter memoryLimiter, boolean failOnExceedingLimit) throws MemoryLimiter.ReachedMemoryLimitException + { + memoryLimiter.increment(byteCount, failOnExceedingLimit); try { - long byteCount = wordCount * 8L; - bytes = Memory.allocate(byteCount); + return Memory.allocate(byteCount); } catch (OutOfMemoryError e) { - throw new RuntimeException("Out of native memory occured, You can avoid it by increasing the system ram space or by increasing bloom_filter_fp_chance."); + memoryLimiter.decrement(byteCount); + throw e; } - // flush/clear the existing memory. - clear(); } - private OffHeapBitSet(Memory bytes) + private static void release(Memory memory, MemoryLimiter memoryLimiter) { - this.bytes = bytes; + long size = memory.size(); + memory.free(); + memoryLimiter.decrement(size); } public long capacity() @@ -141,10 +165,11 @@ public long serializedSize() return TypeSizes.sizeof((int) bytes.size()) + bytes.size(); } - public static OffHeapBitSet deserialize(I in, boolean oldBfFormat) throws IOException + @SuppressWarnings("resource") + public static OffHeapBitSet deserialize(I in, boolean oldBfFormat, MemoryLimiter memoryLimiter) throws IOException, MemoryLimiter.ReachedMemoryLimitException { long byteCount = in.readInt() * 8L; - Memory memory = Memory.allocate(byteCount); + Memory memory = allocate(byteCount, memoryLimiter, true); if (oldBfFormat) { for (long i = 0; i < byteCount; ) @@ -164,12 +189,12 @@ public static OffHeapBitSet deserialize(I in { FBUtilities.copy(in, new MemoryOutputStream(memory), byteCount); } - return new OffHeapBitSet(memory); + return new OffHeapBitSet(memory, memoryLimiter); } public void close() { - bytes.free(); + release(bytes, memoryLimiter); } @Override @@ -188,7 +213,7 @@ public int hashCode() { // Similar to open bitset. long h = 0; - for (long i = bytes.size(); --i >= 0;) + for (long i = bytes.size(); --i >= 0; ) { h ^= bytes.getByte(i); h = (h << 1) | (h >>> 63); // rotate left @@ -198,6 +223,6 @@ public int hashCode() public String toString() { - return "[OffHeapBitSet]"; + return String.format("[OffHeapBitSet %s]", FBUtilities.prettyPrintMemory(serializedSize())); } } diff --git a/src/java/org/apache/cassandra/utils/units/RateUnit.java b/src/java/org/apache/cassandra/utils/units/RateUnit.java new file mode 100644 index 000000000000..4dd1612e2eaf --- /dev/null +++ b/src/java/org/apache/cassandra/utils/units/RateUnit.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils.units; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.utils.Comparables; + +/** + * Represents the unit of a rate of transfer/work in term of byte sizes dealt with in a given time. As such, a + * {@link RateUnit} unit is simply the combination of a {@link SizeUnit} and a {@link TimeUnit}. + *

    + * Note that while the code is relatively in that it can manipulate any combination of size unit and time unit, we + * pre-declare only a handful of the most common rates (only in seconds in practice). + */ +public class RateUnit implements Comparable +{ + /** + * Bytes per Seconds. + */ + public static final RateUnit B_S = RateUnit.of(SizeUnit.BYTES, TimeUnit.SECONDS); + /** + * KiloBytes per Seconds. + */ + public static final RateUnit KB_S = RateUnit.of(SizeUnit.KILOBYTES, TimeUnit.SECONDS); + /** + * MegaBytes per Seconds. + */ + public static final RateUnit MB_S = RateUnit.of(SizeUnit.MEGABYTES, TimeUnit.SECONDS); + /** + * GigaBytes per Seconds. + */ + public static final RateUnit GB_S = RateUnit.of(SizeUnit.GIGABYTES, TimeUnit.SECONDS); + /** + * TeraBytes per Seconds. + */ + public static final RateUnit TB_S = RateUnit.of(SizeUnit.TERABYTES, TimeUnit.SECONDS); + + public final SizeUnit sizeUnit; + public final TimeUnit timeUnit; + + private RateUnit(SizeUnit sizeUnit, TimeUnit timeUnit) + { + this.sizeUnit = sizeUnit; + this.timeUnit = timeUnit; + } + + public static RateUnit of(SizeUnit sizeUnit, TimeUnit timeUnit) + { + return new RateUnit(sizeUnit, timeUnit); + } + + /** + * Convert the given rate in the given unit to this unit. Conversions from finer to coarser granularities truncate, + * so lose precision, conversions from coarser to finer granularities with arguments that would numerically overflow + * saturate to Long.MIN_VALUE if negative or Long.MAX_VALUE if positive. + *

    + * For example, to convert 10 megabytes per seconds to bytes per seconds, use: {@code B_S.convert(10L, MB_S)}. + * + * @param sourceRate the rate to convert in the given {@code sourceUnit}. + * @param sourceUnit the unit of the {@code sourceSize} argument + * @return the converted size in this unit, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + */ + public long convert(long sourceRate, RateUnit sourceUnit) + { + // We need to convert the size unit and the time unit. For the time unit, since it's a rate, we basically want + // to do the opposite of converting from the sourceUnit to the destinationUnit, so we convert from the + // destinationUnit to the sourceUnit, even though the value is obviously not in the destination unit in the + // first place. + // The order we apply the conversion matters however: say we convert '10 MB/s' to 'GB/days': if we were to apply + // the size conversion first, we'd get 0, since 10MB is 0GB. So we should apply the time conversion first + // ('10 MB/s' is '10 * 3600 * 24 MB/days') and then do the size conversion. Conversely, when converting + // '10 MB/s' to 'B/ms', we shouldn't convert by time first, as 10ms is 0s (we do the inverse conversion). + // In practice, if the source size unit is smaller than the destination one, we want to apply the time conversion + // first, otherwise, we can apply the size one first. + if (sourceUnit.sizeUnit.compareTo(sizeUnit) < 0) + return sizeUnit.convert(sourceUnit.timeUnit.convert(sourceRate, timeUnit), sourceUnit.sizeUnit); + + return sourceUnit.timeUnit.convert(sizeUnit.convert(sourceRate, sourceUnit.sizeUnit), timeUnit); + } + + /** + * Returns a Human Readable representation of the provided value in this unit. + *

    + * Note that this method may discard precision for the sake of returning a more human readable value. In other + * words, if {@code value} is large, it will be converted to a bigger, more readable unit, even this imply + * truncating the value. + * + * @param value the value in this unit. + * @return a potentially truncated but human readable representation of {@code value}. + */ + public String toHumanReadableString(long value) + { + return Units.toString(value, this); + } + + public String toString(long value) + { + return Units.formatValue(value) + this; + } + + static String toString(SizeUnit sizeUnit, TimeUnit timeUnit) + { + return String.format("%s/%s", sizeUnit.symbol, Units.TIME_UNIT_SYMBOL_FCT.apply(timeUnit)); + } + + @Override + public int hashCode() + { + return Objects.hash(sizeUnit, timeUnit); + } + + @Override + public boolean equals(Object other) + { + if (!(other instanceof RateUnit)) + return false; + + RateUnit that = (RateUnit) other; + return this.sizeUnit == that.sizeUnit && this.timeUnit == that.timeUnit; + } + + @Override + public String toString() + { + return toString(sizeUnit, timeUnit); + } + + /** + * Given a value in this unit, returns the smallest (most fine grained) unit in which that value can be represented + * without overflowing. + * + * @param value the value in this unit. + * @return the smallest unit, potentially this unit, at which the value can be represented without overflowing. If + * {@code value == Long.MAX_VALUE}, then this unit is returned. + */ + RateUnit smallestRepresentableUnit(long value) + { + // This is kind of subtle because we get a smaller unit that this one by both decreasing the size unit + // and increasing the time unit, and both don't have the same effect, so we need to find the most optimal + // application of both operation that don't overflow our value. + // For instance, consider v1=(Long.MAX_VALUE-1 / 1000), then the smallest representable unit for + // v1 MB/ms is MB/s (kB/ms doesn't work), while for v2=(Long.MAX_VALUE-1 / 1024) MB/ms, the smallest + // representable unit is actually kB/ms (it's also representable as MB/s, but it's a bigger unit). + // + // So we proceed by applying each option (decreasing size or incrementing time), check if we overflow with each + // and if we don't apply recursively. We then compare the unit from both recursive call to find the smallest + // one. + if (value == Long.MAX_VALUE) + return this; + + SizeUnit nextSizeUnit = next(sizeUnit); + TimeUnit nextTimeUnit = next(timeUnit); + + long vSize = nextSizeUnit == null ? Long.MAX_VALUE : nextSizeUnit.convert(value, sizeUnit); + // Reminder that because the time divide the rate, the conversion should be applied in reverse + long vTime = nextTimeUnit == null ? Long.MAX_VALUE : timeUnit.convert(value, nextTimeUnit); + + RateUnit smallestWithSize = vSize == Long.MAX_VALUE + ? this + : RateUnit.of(nextSizeUnit, timeUnit).smallestRepresentableUnit(vSize); + RateUnit smallestWithTime = vTime == Long.MAX_VALUE + ? this + : RateUnit.of(sizeUnit, nextTimeUnit).smallestRepresentableUnit(vTime); + + return Comparables.min(smallestWithSize, smallestWithTime); + } + + private static SizeUnit next(SizeUnit unit) + { + int ordinal = unit.ordinal(); + return ordinal == 0 ? null : SizeUnit.values()[ordinal - 1]; + } + + private static TimeUnit next(TimeUnit unit) + { + int ordinal = unit.ordinal(); + return ordinal == TimeUnit.values().length - 1 ? null : TimeUnit.values()[ordinal + 1]; + } + + public int compareTo(RateUnit that) + { + // Comparing rate units is a tad tricky. We're asking what is the biggest "transfer rate" between 1 of this unit + // versus 1 of 'that' unit. This is easier when one of the unit is the same in each unit however. + if (this.sizeUnit == that.sizeUnit) + return that.timeUnit.compareTo(this.timeUnit); // 1 MB/h is smaller/slower than 1 MB/s + + if (this.timeUnit == that.timeUnit) + return this.sizeUnit.compareTo(that.sizeUnit); // 1 MB/s is smaller/slower than 1 TB/s + + // Otherwise, we have to compute by how much it differs in size versus by how much it differs in time. + if (this.sizeUnit.compareTo(that.sizeUnit) < 0) + { + if (this.timeUnit.compareTo(that.timeUnit) < 0) + { + // this = 1 B/ms and that = 1 MB/s + // How much we'll multiply 'that' to get it into 'this' size unit + long thatScale = valueDiff(this.sizeUnit, that.sizeUnit); + // How much we'll multiply 'this' to get it into 'that' time unit + long thisScale = valueDiff(this.timeUnit, that.timeUnit); + // 'that' is bigger if it is bigger when put in the same unit than 'this', that is if we'll multiply it + // by a bigger value + return Long.compare(thisScale, thatScale); + } + else + { + // this = 1 B/s and that = 1 MB/ms + // That transfers more data in less time, it's definitively faster (bigger) + return -1; + } + } + else + { + if (this.timeUnit.compareTo(that.timeUnit) < 0) + { + // This transfers more data in less time, it's definitively faster (bigger) + return 1; + } + else + { + // this = 1 MB/s and that = 1 B/ms + // How much we'll multiply 'this' to get it into 'that' size unit + long thisScale = valueDiff(that.sizeUnit, this.sizeUnit); + // How much we'll multiply 'that' to get it into 'this' time unit + long thatScale = valueDiff(that.timeUnit, this.timeUnit); + // 'that' is bigger if it is bigger when put in the same unit than 'this', that is if we'll multiply it + // by a bigger value + return Long.compare(thisScale, thatScale); + } + } + } + + /** + * The difference in value between 2 different size unit min and max, where min < max. + */ + private static long valueDiff(SizeUnit min, SizeUnit max) + { + return 1024L * (max.ordinal() - min.ordinal()); + } + + /** + * The difference in value between 2 different time unit min and max, where min < max. + */ + private static long valueDiff(TimeUnit min, TimeUnit max) + { + TimeUnit[] all = TimeUnit.values(); + long val = 1; + for (int i = min.ordinal(); i < max.ordinal(); i++) + val *= Units.TIME_UNIT_SCALE_FCT.applyAsLong(all[i]); + return val; + } +} diff --git a/src/java/org/apache/cassandra/utils/units/RateValue.java b/src/java/org/apache/cassandra/utils/units/RateValue.java new file mode 100644 index 000000000000..102dea76930f --- /dev/null +++ b/src/java/org/apache/cassandra/utils/units/RateValue.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils.units; + +import org.apache.cassandra.utils.Comparables; + +/** + * A {@code RateValue} represents a particular rate in a particular {@link RateUnit}. + *

    + * Note that this can only represent positive sizes. + */ +public class RateValue implements Comparable +{ + public static final RateValue ZERO = new RateValue(0, RateUnit.B_S); + + public final long value; + public final RateUnit unit; + + private RateValue(long value, RateUnit unit) + { + assert value >= 0 && value != Long.MAX_VALUE; + this.value = value; + this.unit = unit; + } + + /** + * Creates a new {@link RateValue} for the provided value in the provided unit. + * + * @param value the value in {@code unit}, which must be positive and strictly less than {@code Long.MAX_VALUE} + * (the latter being used to represent overflows). + * @param unit the unit of {@code value}. + * @return a newly created {@link RateValue} for {@code value} in {@code unit}. + * @throws IllegalArgumentException if {@code value} is negative or equal to {@code Long.MAX_VALUE}. + */ + public static RateValue of(long value, RateUnit unit) + { + if (value < 0) + throw new IllegalArgumentException("Invalid negative value for a rate: " + value); + if (value == Long.MAX_VALUE) + throw new IllegalArgumentException("Invalid value for a rate, cannot be Long.MAX_VALUE"); + return new RateValue(value, unit); + } + + /** + * Computes the rate corresponding to "processing" {@code size} in {@code duration}. + * + * @param size the size processed. + * @param duration the duration of the process. + * @return the rate corresponding to processing {@code size} in {@code duration}. + */ + public static RateValue compute(SizeValue size, TimeValue duration) + { + SizeUnit bestSizeUnit = size.smallestRepresentableUnit(); + return RateValue.of(size.in(bestSizeUnit) / duration.value, RateUnit.of(bestSizeUnit, duration.unit)); + } + + /** + * Returns the value this represents in the provided unit. + * + * @param destinationUnit the unit to return the value in. + * @return the value this represent in {@code unit}. + */ + public long in(RateUnit destinationUnit) + { + return destinationUnit.convert(value, unit); + } + + public RateValue convert(RateUnit destinationUnit) + { + return RateValue.of(in(destinationUnit), destinationUnit); + } + + /** + * Returns the time required to "process" the provided size at this rate. + */ + public TimeValue timeFor(SizeValue size) + { + // Convert both the rate and size in the smallest unit in which they don't overflow: this will ensure the most + // precise return value. + RateUnit smallestForRate = smallestRepresentableUnit(); + SizeUnit smallestForSize = size.smallestRepresentableUnit(); + + SizeUnit toConvert = Comparables.max(smallestForSize, smallestForRate.sizeUnit); + return TimeValue.of(size.in(toConvert) / toConvert.convert(value, unit.sizeUnit), unit.timeUnit); + } + + private RateUnit smallestRepresentableUnit() + { + return unit.smallestRepresentableUnit(value); + } + + /** + * Returns a string representation of this value in the unit it was created with. + */ + public String toRawString() + { + return unit.toString(value); + } + + /** + * Returns a Human Readable representation of this value. + *

    + * Note that this method may discard precision for the sake of returning a more human readable value. In other + * words, this will display the value is a bigger unit than the one it was created with if that improve readability + * and this even this imply truncating the value. + * + * @return a potentially truncated but human readable representation of this value. + */ + @Override + public String toString() + { + return unit.toHumanReadableString(value); + } + + @Override + public int hashCode() + { + // Make sure that equals() => same hashCode() + return Long.hashCode(in(smallestRepresentableUnit())); + } + + /** + * Checks the equality of this value with another value. + *

    + * Two {@link RateValue} are equal if they represent exactly the same number of bytes in the same number of time. + * + * @param other the value to check equality with. + * @return whether this value and {@code other} represent the same rate. + */ + @Override + public boolean equals(Object other) + { + if (!(other instanceof RateValue)) + return false; + + RateValue that = (RateValue) other; + + // Convert both value to the most precise unit in which they can both be represented without overflowing and + // check we get the same value. If both don't have the same smallest representable unit, they can't be + // representing the same number of bytes. + RateUnit smallest = this.smallestRepresentableUnit(); + return smallest.equals(that.smallestRepresentableUnit()) && this.in(smallest) == that.in(smallest); + } + + public int compareTo(RateValue that) + { + // To compare, we need to have the same unit. + RateUnit thisSmallest = this.smallestRepresentableUnit(); + RateUnit thatSmallest = that.smallestRepresentableUnit(); + + if (thisSmallest.equals(thatSmallest)) + return Long.compare(this.in(thisSmallest), that.in(thatSmallest)); + + // If one value overflow "before" (it has a bigger smallest representable unit) the other one, then that value + // is bigger. Note that rate units are not comparable in the absolute + return thisSmallest.compareTo(thatSmallest) > 0 ? 1 : -1; + } +} diff --git a/src/java/org/apache/cassandra/utils/units/SizeUnit.java b/src/java/org/apache/cassandra/utils/units/SizeUnit.java new file mode 100644 index 000000000000..783097050a37 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/units/SizeUnit.java @@ -0,0 +1,356 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils.units; + +import com.google.common.annotations.VisibleForTesting; + +/** + * A {@code SizeUnit} represents byte sizes at a given unit of granularity and provide utility methods to convert across + * units. A {@code SizeUnit} does not maintain size information (see {@link SizeValue}), but only represent the unit + * itself. A kilobyte is defined a 1024 bytes, a megabyte as 1024 kilobytes, etc... + */ +public enum SizeUnit +{ + BYTES("B") + { + public long convert(long s, SizeUnit u) + { + return u.toBytes(s); + } + + public long toBytes(long s) + { + return s; + } + + public long toKiloBytes(long s) + { + return s / (C1 / C0); + } + + public long toMegaBytes(long s) + { + return s / (C2 / C0); + } + + public long toGigaBytes(long s) + { + return s / (C3 / C0); + } + + public long toTeraBytes(long s) + { + return s / (C4 / C0); + } + }, + KILOBYTES("kB") + { + public long convert(long s, SizeUnit u) + { + return u.toKiloBytes(s); + } + + public long toBytes(long s) + { + return x(s, C1 / C0, MAX / (C1 / C0)); + } + + public long toKiloBytes(long s) + { + return s; + } + + public long toMegaBytes(long s) + { + return s / (C2 / C1); + } + + public long toGigaBytes(long s) + { + return s / (C3 / C1); + } + + public long toTeraBytes(long s) + { + return s / (C4 / C1); + } + }, + MEGABYTES("MB") + { + public long convert(long s, SizeUnit u) + { + return u.toMegaBytes(s); + } + + public long toBytes(long s) + { + return x(s, C2 / C0, MAX / (C2 / C0)); + } + + public long toKiloBytes(long s) + { + return x(s, C2 / C1, MAX / (C2 / C1)); + } + + public long toMegaBytes(long s) + { + return s; + } + + public long toGigaBytes(long s) + { + return s / (C3 / C2); + } + + public long toTeraBytes(long s) + { + return s / (C4 / C2); + } + }, + GIGABYTES("GB") + { + public long convert(long s, SizeUnit u) + { + return u.toGigaBytes(s); + } + + public long toBytes(long s) + { + return x(s, C3 / C0, MAX / (C3 / C0)); + } + + public long toKiloBytes(long s) + { + return x(s, C3 / C1, MAX / (C3 / C1)); + } + + public long toMegaBytes(long s) + { + return x(s, C3 / C2, MAX / (C3 / C2)); + } + + public long toGigaBytes(long s) + { + return s; + } + + public long toTeraBytes(long s) + { + return s / (C4 / C3); + } + }, + TERABYTES("TB") + { + public long convert(long s, SizeUnit u) + { + return u.toTeraBytes(s); + } + + public long toBytes(long s) + { + return x(s, C4 / C0, MAX / (C4 / C0)); + } + + public long toKiloBytes(long s) + { + return x(s, C4 / C1, MAX / (C4 / C1)); + } + + public long toMegaBytes(long s) + { + return x(s, C4 / C2, MAX / (C4 / C2)); + } + + public long toGigaBytes(long s) + { + return x(s, C4 / C3, MAX / (C4 / C3)); + } + + public long toTeraBytes(long s) + { + return s; + } + }; + + /** + * The string symbol for that unit + **/ + public final String symbol; + + SizeUnit(String symbol) + { + this.symbol = symbol; + } + + // Handy constants for conversion methods (all are visible for testing) + static final long C0 = 1L; + static final long C1 = C0 * 1024L; + static final long C2 = C1 * 1024L; + static final long C3 = C2 * 1024L; + static final long C4 = C3 * 1024L; + + private static final long MAX = Long.MAX_VALUE; + + /** + * Scale d by m, checking for overflow. + * This has a short name to make above code more readable. + */ + @VisibleForTesting + static long x(long d, long m, long over) + { + if (d > over) return Long.MAX_VALUE; + if (d < -over) return Long.MIN_VALUE; + return d * m; + } + + /** + * Convert the given size in the given unit to this unit. Conversions from finer to coarser granularities truncate, + * so lose precision. For example converting {@code 1023} bytes to kilobytes results in {@code 0}. Conversions from + * coarser to finer granularities with arguments that would numerically overflow saturate to Long.MIN_VALUE + * if negative or Long.MAX_VALUE if positive. + *

    + * For example, to convert 10 megabytes to bytes, use: {@code SizeUnit.BYTES.convert(10L, SizeUnit.MEGABYTES)}. + * + * @param sourceSize the size in the given {@code sourceUnit}. + * @param sourceUnit the unit of the {@code sourceSize} argument + * @return the converted size in this unit, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + */ + public abstract long convert(long sourceSize, SizeUnit sourceUnit); + + /** + * Equivalent to {@code BYTES.convert(size, this)}. + * + * @param size the size + * @return the converted size, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + * @see #convert + */ + public abstract long toBytes(long size); + + /** + * Equivalent to {@code KILOBYTES.convert(size, this)}. + * + * @param size the size + * @return the converted size, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + * @see #convert + */ + public abstract long toKiloBytes(long size); + + /** + * Equivalent to {@code MEGABYTES.convert(size, this)}. + * + * @param size the size + * @return the converted size, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + * @see #convert + */ + public abstract long toMegaBytes(long size); + + /** + * Equivalent to {@code GIGABYTES.convert(size, this)}. + * + * @param size the size + * @return the converted size, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + * @see #convert + */ + public abstract long toGigaBytes(long size); + + /** + * Equivalent to {@code TERABYTES.convert(size, this)}. + * + * @param size the size + * @return the converted size, or {@code Long.MIN_VALUE} if conversion would negatively overflow, or + * {@code Long.MAX_VALUE} if it would positively overflow. + * @see #convert + */ + public abstract long toTeraBytes(long size); + + /** + * Creates a {@link SizeValue} using the provided {@code value} and this unit. + * + * @param value the value. + * @return a new {@link SizeValue} for {@code value} at this unit. + */ + public SizeValue value(long value) + { + return SizeValue.of(value, this); + } + + /** + * Returns a Human Readable representation of the provided value in this unit. + *

    + * Note that this method may discard precision for the sake of returning a more human readable value. In other + * words, if {@code value} is large, it will be converted to a bigger, more readable unit, even this imply + * truncating the value. + * + * @param value the value in this unit. + * @return a potentially truncated but human readable representation of {@code value}. + */ + public String toHumanReadableString(long value) + { + return Units.toString(value, this); + } + + /** + * Returns a string representation particularly suitable for logging a value. of this unit. + *

    + * The returned representation combines the value displayed in bytes (for the sake of script parsing the log, so + * they don't have to bother with unit conversion), followed by the representation from {@link #toHumanReadableString} for + * humans. + * + * @param value the value in this unit. + * @return a string representation suitable for logging the value. + */ + public String toLogString(long value) + { + return Units.toLogString(value, this); + } + + /** + * Returns a string representation of a value in this unit. + * + * @param value the value in this unit. + * @return a string representation of {@code value} in this unit. + */ + public String toString(long value) + { + return Units.formatValue(value) + symbol; + } + + /** + * Given a value in this unit, returns the smallest (most fine grained) unit in which that value can be represented + * without overflowing. + * + * @param value the value in this unit. + * @return the smallest unit, potentially this unit, at which the value can be represented without overflowing. If + * {@code value == Long.MAX_VALUE}, then this unit is returned. + */ + SizeUnit smallestRepresentableUnit(long value) + { + int i = ordinal(); + while (i > 0 && value < Long.MAX_VALUE) + { + value = x(value, C1, MAX / C1); + i--; + } + return SizeUnit.values()[i]; + } +} diff --git a/src/java/org/apache/cassandra/utils/units/SizeValue.java b/src/java/org/apache/cassandra/utils/units/SizeValue.java new file mode 100644 index 000000000000..1ec494d7d096 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/units/SizeValue.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils.units; + +/** + * A {@code SizeValue} represents a particular size in a particular {@link SizeUnit}. + *

    + * Note that this can only represent positive sizes. + */ +public class SizeValue implements Comparable +{ + public static final SizeValue ZERO = new SizeValue(0, SizeUnit.BYTES); + + public final long value; + public final SizeUnit unit; + + private SizeValue(long value, SizeUnit unit) + { + assert value >= 0 && value != Long.MAX_VALUE; + this.value = value; + this.unit = unit; + } + + /** + * Creates a new {@link SizeValue} for the provided value in the provided unit. + * + * @param value the value in {@code unit}, which must be positive and strictly less than {@code Long.MAX_VALUE} + * (the latter being used to represent overflows). + * @param unit the unit of {@code value}. + * @return a newly created {@link SizeValue} for {@code value} in {@code unit}. + * @throws IllegalArgumentException if {@code value} is negative or equal to {@code Long.MAX_VALUE}. + */ + public static SizeValue of(long value, SizeUnit unit) + { + if (value < 0) + throw new IllegalArgumentException("Invalid negative value for a size in bytes: " + value); + if (value == Long.MAX_VALUE) + throw new IllegalArgumentException("Invalid value for a size in bytes, cannot be Long.MAX_VALUE"); + return new SizeValue(value, unit); + } + + /** + * Returns the value this represents in the provided unit. + * + * @param destinationUnit the unit to return the value in. + * @return the value this represent in {@code unit}. + */ + public long in(SizeUnit destinationUnit) + { + return destinationUnit.convert(value, unit); + } + + SizeUnit smallestRepresentableUnit() + { + return unit.smallestRepresentableUnit(value); + } + + /** + * Returns a string representation of this value in the unit it was created with. + */ + public String toRawString() + { + return unit.toString(value); + } + + /** + * Returns a string representation particularly suitable for logging the value. + *

    + * The returned representation combines the value displayed in bytes (for the sake of script parsing the log, so + * they don't have to bother with unit conversion), followed by the representation from + * {@link SizeUnit#toHumanReadableString(long)} for humans. + * + * @return a string representation suitable for logging the value. + */ + public String toLogString() + { + return unit.toLogString(value); + } + + /** + * Returns a Human Readable representation of this value. + *

    + * Note that this method may discard precision for the sake of returning a more human readable value. In other + * words, this will display the value is a bigger unit than the one it was created with if that improve readability + * and this even this imply truncating the value. + * + * @return a potentially truncated but human readable representation of this value. + */ + @Override + public String toString() + { + return unit.toHumanReadableString(value); + } + + @Override + public int hashCode() + { + // Make sure that equals() => same hashCode() + return Long.hashCode(in(smallestRepresentableUnit())); + } + + /** + * Checks the equality of this value with another value. + *

    + * Two {@link SizeValue} are equal if they represent exactly the same number of bytes. + * + * @param other the value to check equality with. + * @return whether this value and {@code other} represent the same number of bytes. + */ + @Override + public boolean equals(Object other) + { + if (!(other instanceof SizeValue)) + return false; + + SizeValue that = (SizeValue) other; + + // Convert both value to the most precise unit in which they can both be represented without overflowing and + // check we get the same value. If both don't have the same smallest representable unit, they can't be + // representing the same number of bytes. + SizeUnit smallest = this.smallestRepresentableUnit(); + return smallest == that.smallestRepresentableUnit() && this.in(smallest) == that.in(smallest); + } + + public int compareTo(SizeValue that) + { + // To compare, we need to have the same unit. + SizeUnit thisSmallest = this.smallestRepresentableUnit(); + SizeUnit thatSmallest = that.smallestRepresentableUnit(); + + if (thisSmallest == thatSmallest) + return Long.compare(this.in(thisSmallest), that.in(thatSmallest)); + + // If one value overflow "before" (it has a bigger smallest representable unit) the other one, then that value + // is bigger. + return thisSmallest.compareTo(thatSmallest) > 0 ? 1 : -1; + } +} diff --git a/src/java/org/apache/cassandra/utils/units/TimeValue.java b/src/java/org/apache/cassandra/utils/units/TimeValue.java new file mode 100644 index 000000000000..90885a73271b --- /dev/null +++ b/src/java/org/apache/cassandra/utils/units/TimeValue.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils.units; + +import java.util.concurrent.TimeUnit; + +/** + * A {@code TimeValue} represents a particular duration in a particular {@link TimeUnit}. + */ +public class TimeValue implements Comparable +{ + public static final TimeValue ZERO = new TimeValue(0, TimeUnit.NANOSECONDS); + + final long value; + final TimeUnit unit; + + private TimeValue(long value, TimeUnit unit) + { + this.value = value; + this.unit = unit; + } + + /** + * Creates a new {@link TimeValue} for the provided value in the provided unit. + * + * @param value the value in {@code unit}. + * @param unit the unit of {@code value}. + * @return a newly created {@link TimeValue} for {@code value} in {@code unit}. + */ + public static TimeValue of(long value, TimeUnit unit) + { + return new TimeValue(value, unit); + } + + /** + * Returns the value this represents in the provided unit. + * + * @param destinationUnit the unit to return the value in. + * @return the value this represent in {@code unit}. + */ + public long in(TimeUnit destinationUnit) + { + return destinationUnit.convert(value, unit); + } + + static TimeUnit smallestRepresentableUnit(long value, TimeUnit unit) + { + long v = value; + int i = unit.ordinal(); + TimeUnit u = unit; + while (i > 0 && v < Long.MAX_VALUE) + { + TimeUnit current = u; + u = TimeUnit.values()[--i]; + v = u.convert(v, current); + } + return u; + } + + private TimeUnit smallestRepresentableUnit() + { + return smallestRepresentableUnit(value, unit); + } + + /** + * Returns a string representation of this value in the unit it was created with. + */ + public String toRawString() + { + return Units.formatValue(value) + Units.TIME_UNIT_SYMBOL_FCT.apply(unit); + } + + /** + * Returns a Human Readable representation of this value. + *

    + * Note that this method may discard precision for the sake of returning a more human readable value. In other + * words, this will display the value is a bigger unit than the one it was created with if that improve readability + * and this even this imply truncating the value. + * + * @return a potentially truncated but human readable representation of this value. + */ + @Override + public String toString() + { + return Units.toString(value, unit); + } + + @Override + public int hashCode() + { + // Make sure that equals() => same hashCode() + return Long.hashCode(in(smallestRepresentableUnit())); + } + + /** + * Checks the equality of this value with another value. + *

    + * Two {@link TimeValue} are equal if they represent exactly the same number of nanoseconds. + * + * @param other the value to check equality with. + * @return whether this value and {@code other} represent the same number of nanoseconds. + */ + @Override + public boolean equals(Object other) + { + if (!(other instanceof TimeValue)) + return false; + + TimeValue that = (TimeValue) other; + + // Convert both value to the most precise unit in which they can both be represented without overflowing and + // check we get the same value. If both don't have the same smallest representable unit, they can't be + // representing the same number of bytes. + TimeUnit smallest = this.smallestRepresentableUnit(); + return smallest == that.smallestRepresentableUnit() && this.in(smallest) == that.in(smallest); + } + + public int compareTo(TimeValue that) + { + // To compare, we need to have the same unit. + TimeUnit thisSmallest = this.smallestRepresentableUnit(); + TimeUnit thatSmallest = that.smallestRepresentableUnit(); + + if (thisSmallest == thatSmallest) + return Long.compare(this.in(thisSmallest), that.in(thatSmallest)); + + // If one value overflow "before" (it has a bigger smallest representable unit) the other one, then that value + // is bigger. + return thisSmallest.compareTo(thatSmallest) > 0 ? 1 : -1; + } +} diff --git a/src/java/org/apache/cassandra/utils/units/Units.java b/src/java/org/apache/cassandra/utils/units/Units.java new file mode 100644 index 000000000000..482e25555fc7 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/units/Units.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.utils.units; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.ToLongFunction; + +/** + * Static methods used by work with units. + *

    + * This is mostly useful for {@link TimeUnit}, as for other units the method provided are more directly accessible in the + * unit class itself (we can't modify {@link TimeUnit}), but contains methods for all unit for symmetry. + */ +public class Units +{ + static final ToLongFunction TIME_UNIT_SCALE_FCT = u -> + { + switch (u) + { + case NANOSECONDS: + case MICROSECONDS: + case MILLISECONDS: + return 1000L; + case SECONDS: + case MINUTES: + return 60L; + case HOURS: + return 24L; + case DAYS: + return 365; // Never actually use but well... + default: + throw new AssertionError(); + } + }; + static final Function TIME_UNIT_SYMBOL_FCT = u -> + { + switch (u) + { + case NANOSECONDS: + return "ns"; + case MICROSECONDS: + return "us"; + case MILLISECONDS: + return "ms"; + case SECONDS: + return "s"; + case MINUTES: + return "m"; + case HOURS: + return "h"; + case DAYS: + return "d"; + default: + throw new AssertionError(); + } + }; + + private static final ToLongFunction SIZE_UNIT_SCALE_FCT = u -> SizeUnit.C1; + private static final Function SIZE_UNIT_SYMBOL_FCT = u -> u.symbol; + + + /** + * Returns a Human Readable representation of the provided duration given the unit of said duration. + *

    + * This method strives to produce a short and human readable representation and may trade precision for that. In + * other words, if the value is large, this will display the value in a bigger unit than the one provided to improve + * readability and this even this imply truncating the value. + * + * @param value the value to build a string of. + * @param unit the unit of {@code value}. + * @return a potentially truncated but human readable representation of {@code value}. + */ + public static String toString(long value, TimeUnit unit) + { + return toString(value, unit, TimeUnit.class, TIME_UNIT_SCALE_FCT, TIME_UNIT_SYMBOL_FCT); + } + + /** + * Returns a Human Readable representation of the provided size given the unit of said size. + *

    + * This method strives to produce a short and human readable representation and may trade precision for that. In + * other words, if the value is large, this will display the value in a bigger unit than the one provided to improve + * readability and this even this imply truncating the value. + * + * @param value the value to build a string of. + * @param unit the unit of {@code value}. + * @return a potentially truncated but human readable representation of {@code value}. + */ + public static String toString(long value, SizeUnit unit) + { + return toString(value, unit, SizeUnit.class, SIZE_UNIT_SCALE_FCT, SIZE_UNIT_SYMBOL_FCT); + } + + /** + * Returns a string representation for a size value (in a particular unit) that is suitable for logging the value. + *

    + * The returned representation combines the value displayed in bytes (for the sake of script parsing the log, so + * they don't have to bother with unit conversion), followed by the representation from {@link #toString} for + * humans. + * + * @param value a size in {@code unit}. + * @param unit the unit for {@code value}. + * @return a string representation suitable for logging the value. + */ + public static String toLogString(long value, SizeUnit unit) + { + return String.format("%s (%s)", SizeUnit.BYTES.toString(unit.toBytes(value)), toString(value, unit)); + } + + /** + * Returns a Human Readable representation of the provided rate given the unit of said rate. + *

    + * This method strives to produce a short and human readable representation and may trade precision for that. In + * other words, if the value is large, this will display the value in a bigger unit than the one provided to improve + * readability and this even this imply truncating the value. + * + * @param value the value to build a string of. + * @param unit the unit of {@code value}. + * @return a potentially truncated but human readable representation of {@code value}. + */ + public static String toString(long value, RateUnit unit) + { + // There is theoretically multiple options for any given (large) value since we can play on both the size + // and time unit. In practice though, it's much more common to reason with rate 'per second' so we force + // seconds as unit of time and play only on the size unit. + value = RateUnit.of(unit.sizeUnit, TimeUnit.SECONDS).convert(value, unit); + return toString(value, unit.sizeUnit, SizeUnit.class, SIZE_UNIT_SCALE_FCT, u -> RateUnit.toString(u, unit.timeUnit)); + } + + /** + * Format a value a in a human readable way, adding a comma (',') to separate every thousands. + *

    + * For instance, {@code formatValue(4693234L) == "4,693,234"} + * + * @param value the value to format. + * @return a more human readable representation of {@code value}. + */ + static String formatValue(long value) + { + return String.format("%,d", value); + } + + /** + * The number of comma to use to format {@code digits} digit using ',' on every thousands. + */ + private static int commaCount(int digits) + { + return (digits - 1) / 3; + } + + /** + * Returns a Human Readable representation of the provided size/rate given the unit of said size/rate. + *

    + * This method strives to produce a short and human readable representation and may trade precision for that. In + * other words, if the value is large, this will display the value in a bigger unit than the one provided to improve + * readability and this even this imply truncating the value. + * + * @param value the value to build a string of. + * @param unit the unit of {@code value}, which is currently either {@link SizeUnit} or {@link RateUnit} + * @param klass Currently can be either a {@link SizeUnit} or {@link RateUnit} class + * @param scaleFct A function that knows how to scale between units of the given {@code unit} + * @param symbolFct A function that knows how to scale between symbols of the given {@code unit} + * @param currently either {@link SizeUnit} or {@link RateUnit} + * @return a potentially truncated but human readable representation of {@code value}. + */ + private static > String toString(long value, + E unit, + Class klass, + ToLongFunction scaleFct, + Function symbolFct) + { + E[] enumVals = klass.getEnumConstants(); + + long v = value; + int i = unit.ordinal(); + long remainder = 0; + // The scale is how much we need to go from unit to the next one + long scale = scaleFct.applyAsLong(unit); + + while (i < enumVals.length - 1 && v >= scale) + { + remainder = v % scale; + v = v / scale; + unit = enumVals[++i]; + scale = scaleFct.applyAsLong(unit); + } + + // If the value is small (<10), include one decimal so the precision is not too truncated. Otherwise, don't + // bother, it's less relevant. + if (v >= 10 || remainder == 0) + return fmt(v, unit, symbolFct); + + // Note that scale is the scale of the current unit, but remainder relates to the previous unit. Also not that + // can only get here is remainder != 0 so we know accessing the previous unit is legit + long prevScale = scaleFct.applyAsLong(enumVals[i - 1]); + int decimal = Math.round(((float) remainder / prevScale) * 10); + if (decimal == 0) + return fmt(v, unit, symbolFct); + + // If the remainder amounts to more than 0.95 of C1, decimal will be 10. In that case, just bump the value by 1 + if (decimal == 10) + return fmt(v + 1, unit, symbolFct); + + return formatValue(v) + '.' + decimal + symbolFct.apply(unit); + } + + private static > String fmt(long value, E unit, Function symbolFct) + { + return formatValue(value) + symbolFct.apply(unit); + } +} diff --git a/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt b/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt index 8a1d2987f9d2..ab0d6b06343f 100644 --- a/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt +++ b/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt @@ -54,4 +54,5 @@ USE USING VIEW WHERE -WITH \ No newline at end of file +WITH +GEO_DISTANCE \ No newline at end of file diff --git a/src/resources/org/apache/cassandra/graph/graph.html b/src/resources/org/apache/cassandra/graph/graph.html new file mode 100644 index 000000000000..dd1f951fe748 --- /dev/null +++ b/src/resources/org/apache/cassandra/graph/graph.html @@ -0,0 +1,572 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/anttasks/org/apache/cassandra/anttasks/JdkProperties.java b/test/anttasks/org/apache/cassandra/anttasks/JdkProperties.java index 2e5d202a20d9..59aba5e46420 100644 --- a/test/anttasks/org/apache/cassandra/anttasks/JdkProperties.java +++ b/test/anttasks/org/apache/cassandra/anttasks/JdkProperties.java @@ -27,7 +27,7 @@ public class JdkProperties extends Task public void execute() { Project project = getProject(); - project.setNewProperty("java.version." + project.getProperty("ant.java.version").replace("1.", ""), "true"); - project.setNewProperty("use-jdk" + project.getProperty("ant.java.version").replace("1.", ""), "true"); + project.setNewProperty("java.version." + project.getProperty("ant.java.version"), "true"); + project.setNewProperty("use-jdk" + project.getProperty("ant.java.version"), "true"); } } diff --git a/test/burn/org/apache/cassandra/index/sai/LongBM25Test.java b/test/burn/org/apache/cassandra/index/sai/LongBM25Test.java new file mode 100644 index 000000000000..0f6ae9481b56 --- /dev/null +++ b/test/burn/org/apache/cassandra/index/sai/LongBM25Test.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; + +import static org.apache.cassandra.config.CassandraRelevantProperties.MEMTABLE_SHARD_COUNT; + +public class LongBM25Test extends SAITester +{ + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(LongBM25Test.class); + + // null when test/resources/bm25/ is absent — see CNDB-13621 + private static final List documentLines = loadDocumentLines(); + + private static List loadDocumentLines() + { + try + { + var cl = LongBM25Test.class.getClassLoader(); + var resourceDir = cl.getResource("bm25"); + if (resourceDir == null) + return null; + + var lines = new ArrayList(); + var dirPath = java.nio.file.Paths.get(resourceDir.toURI()); + try (var files = java.nio.file.Files.list(dirPath)) + { + files.forEach(file -> { + try (var fileLines = java.nio.file.Files.lines(file)) + { + fileLines.map(String::trim) + .filter(line -> !line.isEmpty()) + .forEach(lines::add); + } + catch (IOException e) + { + throw new RuntimeException("Failed to read file: " + file, e); + } + }); + } + return lines.isEmpty() ? null : lines; + } + catch (IOException | URISyntaxException e) + { + return null; + } + } + + KeySet keysInserted = new KeySet(); + private final int threadCount = 12; + + // CNDB-13621: test/resources/bm25/ corpus files are not committed to the repo. + // Skip all tests in this class rather than crashing with ExceptionInInitializerError. + @BeforeClass + public static void requireBm25Resources() + { + Assume.assumeTrue("Skipping LongBM25Test: test/resources/bm25/ not found (CNDB-13621)", + documentLines != null); + } + + @Before + public void setup() throws Throwable + { + MEMTABLE_SHARD_COUNT.setInt(4 * threadCount); + } + + @FunctionalInterface + private interface Op + { + void run(int i) throws Throwable; + } + + public void testConcurrentOps(Op op) throws ExecutionException, InterruptedException + { + createTable("CREATE TABLE %s (key int primary key, value text)"); + // Create analyzed index following BM25Test pattern + createIndex("CREATE CUSTOM INDEX ON %s(value) " + + "USING 'org.apache.cassandra.index.sai.StorageAttachedIndex' " + + "WITH OPTIONS = {" + + "'index_analyzer': '{" + + "\"tokenizer\" : {\"name\" : \"standard\"}, " + + "\"filters\" : [{\"name\" : \"porterstem\"}]" + + "}'}" + ); + + AtomicInteger counter = new AtomicInteger(); + long start = System.currentTimeMillis(); + var fjp = new ForkJoinPool(threadCount); + var keys = IntStream.range(0, 10_000_000).boxed().collect(Collectors.toList()); + Collections.shuffle(keys); + var task = fjp.submit(() -> keys.stream().parallel().forEach(i -> + { + wrappedOp(op, i); + if (counter.incrementAndGet() % 10_000 == 0) + { + var elapsed = System.currentTimeMillis() - start; + logger.info("{} ops in {}ms = {} ops/s", counter.get(), elapsed, counter.get() * 1000.0 / elapsed); + } + if (ThreadLocalRandom.current().nextDouble() < 0.001) + flush(); + })); + fjp.shutdown(); + task.get(); // re-throw + } + + private static void wrappedOp(Op op, Integer i) + { + try + { + op.run(i); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + private static String randomDocument() + { + var R = ThreadLocalRandom.current(); + int numLines = R.nextInt(5, 51); // 5 to 50 lines inclusive + var selectedLines = new ArrayList(); + + for (int i = 0; i < numLines; i++) + { + selectedLines.add(randomQuery(R)); + } + + return String.join("\n", selectedLines); + } + + private static String randomLine(ThreadLocalRandom R) + { + return documentLines.get(R.nextInt(documentLines.size())); + } + + @Ignore("CNDB-13621") + @Test + public void testConcurrentReadsWritesDeletes() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var R = ThreadLocalRandom.current(); + if (R.nextDouble() < 0.2 || keysInserted.isEmpty()) + { + var doc = randomDocument(); + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, doc); + keysInserted.add(i); + } + else if (R.nextDouble() < 0.1) + { + var key = keysInserted.getRandom(); + execute("DELETE FROM %s WHERE key = ?", key); + } + else + { + var line = randomQuery(R); + execute("SELECT * FROM %s ORDER BY value BM25 OF ? LIMIT ?", line, R.nextInt(1, 100)); + } + }); + } + + private static String randomQuery(ThreadLocalRandom R) + { + while (true) + { + var line = randomLine(R); + if (line.chars().anyMatch(Character::isAlphabetic)) + return line; + } + } + + @Ignore("CNDB-13621") + @Test + public void testConcurrentReadsWrites() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var R = ThreadLocalRandom.current(); + if (R.nextDouble() < 0.1 || keysInserted.isEmpty()) + { + var doc = randomDocument(); + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, doc); + keysInserted.add(i); + } + else + { + var line = randomQuery(R); + execute("SELECT * FROM %s ORDER BY value BM25 OF ? LIMIT ?", line, R.nextInt(1, 100)); + } + }); + } + + @Ignore("CNDB-13621") + @Test + public void testConcurrentWrites() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var doc = randomDocument(); + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, doc); + }); + } + + private static class KeySet + { + private final Map keys = new ConcurrentHashMap<>(); + private final AtomicInteger ordinal = new AtomicInteger(); + + public void add(int key) + { + var i = ordinal.getAndIncrement(); + keys.put(i, key); + } + + public int getRandom() + { + if (isEmpty()) + throw new IllegalStateException(); + var i = ThreadLocalRandom.current().nextInt(ordinal.get()); + // in case there is race with add(key), retry another random + return keys.containsKey(i) ? keys.get(i) : getRandom(); + } + + public boolean isEmpty() + { + return keys.isEmpty(); + } + } +} diff --git a/test/burn/org/apache/cassandra/index/sai/LongVectorTest.java b/test/burn/org/apache/cassandra/index/sai/LongVectorTest.java new file mode 100644 index 000000000000..7de1acffbb51 --- /dev/null +++ b/test/burn/org/apache/cassandra/index/sai/LongVectorTest.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; + +import static org.apache.cassandra.config.CassandraRelevantProperties.MEMTABLE_SHARD_COUNT; +import static org.assertj.core.api.Assertions.assertThat; + +public class LongVectorTest extends SAITester +{ + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(LongVectorTest.class); + + int numKeys = 100_000; + int dimension = 16; // getRandom().nextIntBetween(128, 768); + + KeySet keysInserted = new KeySet(); + private static final int threadCount = 12; + + @BeforeClass + public static void setShardCount() + { + MEMTABLE_SHARD_COUNT.setInt(4 * threadCount); + } + + @FunctionalInterface + private interface Op + { + public void run(int i) throws Throwable; + } + + public void testConcurrentOps(Op op) throws ExecutionException, InterruptedException + { + createTable(String.format("CREATE TABLE %%s (key int primary key, value vector)", dimension)); + createIndex("CREATE CUSTOM INDEX ON %s(value) USING 'StorageAttachedIndex' WITH OPTIONS = { 'similarity_function': 'dot_product' }"); + + AtomicInteger counter = new AtomicInteger(); + long start = System.currentTimeMillis(); + var fjp = new ForkJoinPool(threadCount); + var keys = IntStream.range(0, numKeys).boxed().collect(Collectors.toList()); + Collections.shuffle(keys); + var task = fjp.submit(() -> keys.stream().parallel().forEach(i -> + { + wrappedOp(op, i); + if (counter.incrementAndGet() % 10_000 == 0) + { + var elapsed = System.currentTimeMillis() - start; + logger.info("{} ops in {}ms = {} ops/s", counter.get(), elapsed, counter.get() * 1000.0 / elapsed); + } + if (ThreadLocalRandom.current().nextDouble() < 0.001) + flush(); + })); + fjp.shutdown(); + task.get(); // re-throw + } + + private static void wrappedOp(Op op, Integer i) + { + try + { + op.run(i); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + @Test + public void testConcurrentReadsWritesDeletes() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var R = ThreadLocalRandom.current(); + var v = randomVectorBoxed(dimension); + if (R.nextDouble() < 0.2 || keysInserted.isEmpty()) + { + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, v); + keysInserted.add(i); + } else if (R.nextDouble() < 0.1) { + var key = keysInserted.getRandom(); + execute("DELETE FROM %s WHERE key = ?", key); + } else if (R.nextDouble() < 0.5) { + var key = keysInserted.getRandom(); + execute("SELECT * FROM %s WHERE key = ? ORDER BY value ANN OF ? LIMIT ?", key, v, R.nextInt(1, 100)); + } else { + execute("SELECT * FROM %s ORDER BY value ANN OF ? LIMIT ?", v, R.nextInt(1, 100)); + } + }); + } + + // like testConcurrentReadsWritesDeletes, but generates multiple rows w/ the same vector, and + // the sub-op weights are biased more towards doing additional inserts + @Test + public void testMultiplePostings() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var R = ThreadLocalRandom.current(); + var v = sequentiallyDuplicateVector(i, dimension); + if (R.nextDouble() < 0.8 || keysInserted.isEmpty()) + { + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, v); + keysInserted.add(i); + } else if (R.nextDouble() < 0.1) { + var key = keysInserted.getRandom(); + execute("DELETE FROM %s WHERE key = ?", key); + } else if (R.nextDouble() < 0.5) { + var key = keysInserted.getRandom(); + execute("SELECT * FROM %s WHERE key = ? ORDER BY value ANN OF ? LIMIT ?", key, v, R.nextInt(1, 100)); + } else { + execute("SELECT * FROM %s ORDER BY value ANN OF ? LIMIT ?", v, R.nextInt(1, 100)); + } + }); + } + + @Test + public void testConcurrentReadsWrites() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var R = ThreadLocalRandom.current(); + var v = randomVectorBoxed(dimension); + if (R.nextDouble() < 0.1 || keysInserted.isEmpty()) + { + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, v); + keysInserted.add(i); + } else if (R.nextDouble() < 0.5) { + var key = keysInserted.getRandom(); + var results = execute("SELECT * FROM %s WHERE key = ? ORDER BY value ANN OF ? LIMIT ?", key, v, R.nextInt(1, 100)); + assertThat(results).hasSize(1); + } else { + var results = execute("SELECT * FROM %s ORDER BY value ANN OF ? LIMIT ?", v, R.nextInt(1, 100)); + assertThat(results).hasSizeGreaterThan(0); // VSTODO can we make a stronger assertion? + } + }); + } + + @Test + public void testConcurrentWrites() throws ExecutionException, InterruptedException + { + testConcurrentOps(i -> { + var v = randomVectorBoxed(dimension); + execute("INSERT INTO %s (key, value) VALUES (?, ?)", i, v); + }); + } + + /** + * @return a normalized vector with the given dimension, where each vector from 0 .. N-1 is the same, + * N .. 2N-1 is the same, etc., where N is the number of cores. + */ + private Vector sequentiallyDuplicateVector(int i, int dimension) + { + int j = 1 + i / Runtime.getRuntime().availableProcessors(); + var vector = new float[dimension]; + outer: + while (true) + { + for (int k = 0; k < dimension; k++) + { + vector[k] += 1.0f; + if (j-- <= 0) + break outer; + } + } + normalize(vector); + return vector(vector); + } + + private static class KeySet + { + private final Map keys = new ConcurrentHashMap<>(); + private final AtomicInteger ordinal = new AtomicInteger(); + + public void add(int key) + { + var i = ordinal.getAndIncrement(); + keys.put(i, key); + } + + public int getRandom() + { + if (isEmpty()) + throw new IllegalStateException(); + var i = ThreadLocalRandom.current().nextInt(ordinal.get()); + // in case there is race with add(key), retry another random + return keys.containsKey(i) ? keys.get(i) : getRandom(); + } + + public boolean isEmpty() + { + return keys.isEmpty(); + } + } +} diff --git a/test/burn/org/apache/cassandra/net/Connection.java b/test/burn/org/apache/cassandra/net/Connection.java index de5df6b65de5..52c0fcae164d 100644 --- a/test/burn/org/apache/cassandra/net/Connection.java +++ b/test/burn/org/apache/cassandra/net/Connection.java @@ -333,6 +333,11 @@ public void onExecuted(int messageSize, Message.Header header, long timeElapsed, { } + @Override + public void onMessageHandlingCompleted(Message.Header header, long timeElapsed, TimeUnit unit) + { + } + InboundCounters inboundCounters() { return inbound.countersFor(outbound.type()); diff --git a/test/burn/org/apache/cassandra/net/ConnectionBurnTest.java b/test/burn/org/apache/cassandra/net/ConnectionBurnTest.java index abacb6e034bd..941622ad4844 100644 --- a/test/burn/org/apache/cassandra/net/ConnectionBurnTest.java +++ b/test/burn/org/apache/cassandra/net/ConnectionBurnTest.java @@ -86,10 +86,21 @@ static class NoGlobalInboundMetrics implements InboundMessageHandlers.GlobalMetr static final NoGlobalInboundMetrics instance = new NoGlobalInboundMetrics(); public LatencyConsumer internodeLatencyRecorder(InetAddressAndPort to) { - return (timeElapsed, timeUnit) -> {}; + return (verb, timeElapsed, timeUnit) -> {}; } - public void recordInternalLatency(Verb verb, long timeElapsed, TimeUnit timeUnit) {} + public void recordInternalLatency(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit timeUnit) {} + public void recordInternodeDroppedMessage(Verb verb, long timeElapsed, TimeUnit timeUnit) {} + + @Override + public void recordMessageStageProcessingTime(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit unit) + { + } + + @Override + public void recordTotalMessageProcessingTime(Verb verb, InetAddressAndPort from, long timeElapsed, TimeUnit unit) + { + } } static class Inbound @@ -564,6 +575,13 @@ public void onExecuted(int messageSize, Message.Header header, long timeElapsed, forId(header.id).onExecuted(messageSize, header, timeElapsed, unit); wrapped.onExecuted(messageSize, header, timeElapsed, unit); } + + @Override + public void onMessageHandlingCompleted(Message.Header header, long timeElapsed, TimeUnit unit) + { + forId(header.id).onMessageHandlingCompleted(header, timeElapsed, unit); + wrapped.onMessageHandlingCompleted(header, timeElapsed, unit); + } } public void fail(Message.Header header, Throwable failure) diff --git a/test/burn/org/apache/cassandra/transport/BurnTestUtil.java b/test/burn/org/apache/cassandra/transport/BurnTestUtil.java index c8017d151f69..6a33cbd7a3bd 100644 --- a/test/burn/org/apache/cassandra/transport/BurnTestUtil.java +++ b/test/burn/org/apache/cassandra/transport/BurnTestUtil.java @@ -27,6 +27,7 @@ import com.datastax.driver.core.SimpleStatement; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.ResultSet; import org.apache.cassandra.db.ConsistencyLevel; @@ -78,7 +79,7 @@ public static QueryMessage generateQueryMessage(int idx, SizeCaps sizeCaps, Prot QueryOptions queryOptions = QueryOptions.create(ConsistencyLevel.ONE, values, true, - 10, + PageSize.inRows(10), null, null, version, diff --git a/test/burn/org/apache/cassandra/transport/DriverBurnTest.java b/test/burn/org/apache/cassandra/transport/DriverBurnTest.java index 42b7c6bbaadb..4b5d21994b9a 100644 --- a/test/burn/org/apache/cassandra/transport/DriverBurnTest.java +++ b/test/burn/org/apache/cassandra/transport/DriverBurnTest.java @@ -19,8 +19,14 @@ package org.apache.cassandra.transport; import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; @@ -31,7 +37,12 @@ import org.junit.Before; import org.junit.Test; -import com.datastax.driver.core.*; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ProtocolOptions; +import com.datastax.driver.core.ResultSetFuture; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.SimpleStatement; import io.netty.buffer.ByteBuf; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.service.NativeTransportService; @@ -39,6 +50,8 @@ import org.apache.cassandra.transport.messages.QueryMessage; import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.AssertUtil; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED; import static org.apache.cassandra.transport.BurnTestUtil.SizeCaps; @@ -78,18 +91,20 @@ public QueryMessage decode(ByteBuf body, ProtocolVersion version) { QueryMessage queryMessage = QueryMessage.codec.decode(body, version); return new QueryMessage(queryMessage.query, queryMessage.options) { - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + + @Override + protected Future maybeExecuteAsync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { try { int idx = Integer.parseInt(queryMessage.query); SizeCaps caps = idx % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap; - return generateRows(idx, caps); + return ImmediateFuture.success(generateRows(idx, caps)); } catch (NumberFormatException e) { // for the requests driver issues under the hood - return super.execute(state, requestTime, traceRequest); + return super.maybeExecuteAsync(state, requestTime, traceRequest); } } }; @@ -327,10 +342,10 @@ public void perfTest(SizeCaps requestCaps, SizeCaps responseCaps, Cluster.Builde SimpleStatement request = generateQueryStatement(0, requestCaps); ResultMessage.Rows response = generateRows(0, responseCaps); QueryMessage requestMessage = generateQueryMessage(0, requestCaps, version); - Envelope message = requestMessage.encode(version); + Envelope message = requestMessage.encode(version, 0); int requestSize = message.body.readableBytes(); message.release(); - message = response.encode(version); + message = response.encode(version, 0); int responseSize = message.body.readableBytes(); message.release(); Message.Type.QUERY.unsafeSetCodec(new Message.Codec() { @@ -338,17 +353,19 @@ public QueryMessage decode(ByteBuf body, ProtocolVersion version) { QueryMessage queryMessage = QueryMessage.codec.decode(body, version); return new QueryMessage(queryMessage.query, queryMessage.options) { - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + + @Override + protected Future maybeExecuteAsync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { try { int idx = Integer.parseInt(queryMessage.query); // unused - return generateRows(idx, responseCaps); + return ImmediateFuture.success(generateRows(idx, responseCaps)); } catch (NumberFormatException e) { // for the requests driver issues under the hood - return super.execute(state, requestTime, traceRequest); + return super.maybeExecuteAsync(state, requestTime, traceRequest); } } }; diff --git a/test/burn/org/apache/cassandra/transport/SimpleClientBurnTest.java b/test/burn/org/apache/cassandra/transport/SimpleClientBurnTest.java index ef29146be2f8..7d7e4716c1fd 100644 --- a/test/burn/org/apache/cassandra/transport/SimpleClientBurnTest.java +++ b/test/burn/org/apache/cassandra/transport/SimpleClientBurnTest.java @@ -44,6 +44,8 @@ import org.apache.cassandra.transport.messages.QueryMessage; import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.AssertUtil; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED; import static org.apache.cassandra.transport.BurnTestUtil.SizeCaps; @@ -114,11 +116,11 @@ public QueryMessage decode(ByteBuf body, ProtocolVersion version) return new QueryMessage(queryMessage.query, queryMessage.options) { @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + public Future maybeExecuteAsync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { int idx = Integer.parseInt(queryMessage.query); SizeCaps caps = idx % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap; - return generateRows(idx, caps); + return ImmediateFuture.success(generateRows(idx, caps)); } }; } diff --git a/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java b/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java index 3f990cbca31d..8bda9e52adf1 100644 --- a/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java +++ b/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java @@ -50,6 +50,8 @@ import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.AssertUtil; import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import static org.apache.cassandra.transport.BurnTestUtil.SizeCaps; import static org.apache.cassandra.transport.BurnTestUtil.generateQueryMessage; @@ -148,10 +150,10 @@ public void perfTest(SizeCaps requestCaps, SizeCaps responseCaps, AssertUtil.Thr { ResultMessage.Rows response = generateRows(0, responseCaps); QueryMessage requestMessage = generateQueryMessage(0, requestCaps, version); - Envelope message = requestMessage.encode(version); + Envelope message = requestMessage.encode(version, 0); int requestSize = message.body.readableBytes(); message.release(); - message = response.encode(version); + message = response.encode(version, 0); int responseSize = message.body.readableBytes(); message.release(); @@ -170,10 +172,10 @@ public QueryMessage decode(ByteBuf body, ProtocolVersion version) return new QueryMessage(queryMessage.query, queryMessage.options) { @Override - protected Message.Response execute(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) + public Future maybeExecuteAsync(QueryState state, Dispatcher.RequestTime requestTime, boolean traceRequest) { int idx = Integer.parseInt(queryMessage.query); // unused - return generateRows(idx, responseCaps); + return ImmediateFuture.success(generateRows(idx, responseCaps)); } }; } diff --git a/test/burn/org/apache/cassandra/utils/LongBTreeTest.java b/test/burn/org/apache/cassandra/utils/LongBTreeTest.java index d8a4f81637e1..e7099c87899e 100644 --- a/test/burn/org/apache/cassandra/utils/LongBTreeTest.java +++ b/test/burn/org/apache/cassandra/utils/LongBTreeTest.java @@ -653,7 +653,7 @@ private static RandomTree randomTreeByBuilder(long seed, Random random, int minS // return a value with the search position private static List randomKeys(Random random, Iterable canonical, boolean mixInNotPresentItems) { - boolean useFake = mixInNotPresentItems && random.nextBoolean(); + final boolean useFake = mixInNotPresentItems && random.nextBoolean(); final float fakeRatio = random.nextFloat(); List results = new ArrayList<>(); Long fakeLb = (long) Integer.MIN_VALUE, fakeUb = null; @@ -671,12 +671,12 @@ private static List randomKeys(Random random, Iterable canonic } else { - // otherwise we emit a fake value in the range immediately proceeding the last real value, and not + // otherwise we emit a fake value in the range immediately preceeding the last real value, and not // exceeding the real value that would have proceeded (ignoring any other suppressed real values since) if (fakeUb == null) fakeUb = v.longValue() - 1; long mid = (fakeLb + fakeUb) / 2; - assert mid < fakeUb; + assert mid < v.longValue(); results.add((int) mid); fakeLb = mid; } @@ -730,7 +730,6 @@ public void testFastBuilder() Object[] btree = builder.build(); assertEquals(i + 1, BTree.size(btree)); assertTrue(""+i, BTree.isWellFormed(btree, naturalOrder())); - assertTrue(""+i, BTree.isWellFormed(btree, naturalOrder())); builder.close(); assertTrue(builder.validateEmpty()); } diff --git a/test/conf/bigtable.yaml b/test/conf/bigtable.yaml new file mode 100644 index 000000000000..e1e454f5d38d --- /dev/null +++ b/test/conf/bigtable.yaml @@ -0,0 +1,2 @@ +# Test legacy sstable format along with legacy sstable identifiers +uuid_sstable_identifiers_enabled: false diff --git a/test/conf/cassandra-mtls.yaml b/test/conf/cassandra-mtls.yaml index d6f1b3e52c6b..c4b531f13c1b 100644 --- a/test/conf/cassandra-mtls.yaml +++ b/test/conf/cassandra-mtls.yaml @@ -28,6 +28,7 @@ commitlog_directory: build/test/cassandra/commitlog cdc_raw_directory: build/test/cassandra/cdc_raw cdc_enabled: false hints_directory: build/test/cassandra/hints +metadata_directory: build/test/cassandra/metadata partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner listen_address: 127.0.0.1 storage_port: 7012 diff --git a/test/conf/cassandra-murmur.yaml b/test/conf/cassandra-murmur.yaml index 2e5828fb56a0..46d6ed9cdd19 100644 --- a/test/conf/cassandra-murmur.yaml +++ b/test/conf/cassandra-murmur.yaml @@ -11,6 +11,7 @@ commitlog_directory: build/test/cassandra/commitlog cdc_raw_directory: build/test/cassandra/cdc_raw cdc_enabled: false hints_directory: build/test/cassandra/hints +metadata_directory: build/test/cassandra/metadata partitioner: org.apache.cassandra.dht.Murmur3Partitioner listen_address: 127.0.0.1 storage_port: 7012 @@ -42,3 +43,7 @@ user_defined_functions_enabled: true scripted_user_defined_functions_enabled: false sasi_indexes_enabled: true materialized_views_enabled: true +default_compaction: + class_name: UnifiedCompactionStrategy + parameters: + base_shard_count: 1 diff --git a/test/conf/cassandra-old.yaml b/test/conf/cassandra-old.yaml index b8c3b028c519..9b9bb51fcabf 100644 --- a/test/conf/cassandra-old.yaml +++ b/test/conf/cassandra-old.yaml @@ -14,6 +14,7 @@ commitlog_directory: build/test/cassandra/commitlog cdc_raw_directory: build/test/cassandra/cdc_raw cdc_enabled: false hints_directory: build/test/cassandra/hints +metadata_directory: build/test/cassandra/metadata partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner listen_address: 127.0.0.1 storage_port: 7012 diff --git a/test/conf/cassandra-seeds.yaml b/test/conf/cassandra-seeds.yaml index 53f82dd6ecd7..bdb15b8817ec 100644 --- a/test/conf/cassandra-seeds.yaml +++ b/test/conf/cassandra-seeds.yaml @@ -12,6 +12,7 @@ commitlog_directory: build/test/cassandra/commitlog cdc_raw_directory: build/test/cassandra/cdc_raw cdc_enabled: false hints_directory: build/test/cassandra/hints +metadata_directory: build/test/cassandra/metadata partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner listen_address: 127.0.0.1 storage_port: 7012 diff --git a/test/conf/cassandra.yaml b/test/conf/cassandra.yaml index e9ba02c4415e..f871dac3c4ce 100644 --- a/test/conf/cassandra.yaml +++ b/test/conf/cassandra.yaml @@ -13,8 +13,8 @@ commitlog_disk_access_mode: legacy # commitlog_compression: # - class_name: LZ4Compressor cdc_raw_directory: build/test/cassandra/cdc_raw -cdc_enabled: false hints_directory: build/test/cassandra/hints +metadata_directory: build/test/cassandra/metadata partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner listen_address: 127.0.0.1 storage_port: 7012 @@ -67,15 +67,30 @@ local_read_size_warn_threshold: 4096KiB local_read_size_fail_threshold: 8192KiB row_index_read_size_warn_threshold: 4096KiB row_index_read_size_fail_threshold: 8192KiB +read_request_timeout_in_ms: 20000 +range_request_timeout_in_ms: 20000 +write_request_timeout_in_ms: 20000 +counter_write_request_timeout_in_ms: 20000 +cas_contention_timeout_in_ms: 20000 +request_timeout_in_ms: 20000 +aggregation_request_timeout_in_ms: 120000 +default_compaction: + class_name: UnifiedCompactionStrategy + parameters: + base_shard_count: 1 memtable: configurations: skiplist: class_name: SkipListMemtable + persistent_memory: + class_name: PersistentMemoryMemtable trie: class_name: TrieMemtable parameters: shards: 4 + trie_stage1: + class_name: TrieMemtableStage1 skiplist_sharded: class_name: ShardedSkipListMemtable parameters: diff --git a/test/conf/cassandra_ssl_test.keystore b/test/conf/cassandra_ssl_test.keystore index 8b2b218efab6..739b8b5dd115 100644 Binary files a/test/conf/cassandra_ssl_test.keystore and b/test/conf/cassandra_ssl_test.keystore differ diff --git a/test/conf/cassandra_ssl_test.keystore.pem b/test/conf/cassandra_ssl_test.keystore.pem index ed981cce6a60..b447aab50ee0 100644 --- a/test/conf/cassandra_ssl_test.keystore.pem +++ b/test/conf/cassandra_ssl_test.keystore.pem @@ -1,51 +1,52 @@ -----BEGIN ENCRYPTED PRIVATE KEY----- -MIIE6jAcBgoqhkiG9w0BDAEDMA4ECOWqSzq5PBIdAgIFxQSCBMjXsCK30J0aT3J/ -g5kcbmevTOY1pIhJGbf5QYYrMUPiuDK2ydxIbiPzoTE4/S+OkCeHhlqwn/YydpBl -xgjZZ1Z5rLJHO27d2biuESqanDiBVXYuVmHmaifRnFy0uUTFkStB5mjVZEiJgO29 -L83hL60uWru71EVuVriC2WCfmZ/EXp6wyYszOqCFQ8Quk/rDO6XuaBl467MJbx5V -sucGT6E9XKNd9hB14/Izb2jtVM5kqKxoiHpz1na6yhEYJiE5D1uOonznWjBnjwB/ -f0x+acpDfVDoJKTlRdz+DEcbOF7mb9lBVVjP6P/AAsmQzz6JKwHjvCrjYfQmyyN8 -RI4KRQnWgm4L3dtByLqY8HFU4ogisCMCgI+hZQ+OKMz/hoRO540YGiPcTRY3EOUR -0bd5JxU6tCJDMTqKP9aSL2KmLoiLowdMkSPz7TCzLsZ2bGJemuCfpAs4XT1vXCHs -evrUbOnh8et1IA8mZ9auThfqsZtNagJLEXA6hWIKp1FfVL3Q49wvMKZt4eTn/zwU -tLL0m5yPo6/HAaOA3hbm/oghZS0dseshXl7PZrmZQtvYnIvjyoxEL7ducYDQCDP6 -wZ7Nzyh1QZAauSS15hl3vLFRZCA9hWAVgwQAviTvhB342O0i9qI7TQkcHk+qcTPN -K+iGNbFZ8ma1izXNKSJ2PgI/QqFNIeJWvZrb9PhJRmaZVsTJ9fERm1ewpebZqkVv -zMqMhlKgx9ggAaSKgnGZkwXwB6GrSbbzUrwRCKm3FieD1QE4VVYevaadVUU75GG5 -mrFKorJEH7kFZlic8OTjDksYnHbcgU36XZrGEXa2+ldVeGKL3CsXWciaQRcJg8yo -WQDjZpcutGI0eMJWCqUkv8pYZC2/wZU4htCve5nVJUU4t9uuo9ex7lnwlLWPvheQ -jUBMgzSRsZ+zwaIusvufAAxiKK/cJm4ubZSZPIjBbfd4U7VPxtirP4Accydu7EK6 -eG/MZwtAMFNJxfxUR+/aYzJU/q1ePw7fWVHrpt58t/22CX2SJBEiUGmSmuyER4Ny -DPw6d6mhvPUS1jRhIZ9A81ht8MOX7VL5uVp307rt7o5vRpV1mo0iPiRHzGscMpJn -AP36klEAUNTf0uLTKZa7KHiwhn5iPmsCrENHkOKJjxhRrqHjD2wy3YHs3ow2voyY -Ua4Cids+c1hvRkNEDGNHm4+rKGFOGOsG/ZU7uj/6gflO4JXxNGiyTLflqMdWBvow -Zd7hk1zCaGAAn8nZ0hPweGxQ4Q30I9IBZrimGxB0vjiUqNio9+qMf33dCHFJEuut -ZGJMaUGVaPhXQcTy4uD5hzsPZV5xcsU4H3vBYyBcZgrusJ6OOgkuZQaU7p8rWQWr -bUEVbXuZdwEmxsCe7H/vEVv5+aA4sF4kWnMMFL7/LIYaiEzkTqdJlRv/KyJJgcAH -hg2BvR3XTAq8wiX0C98CdmTbsx2eyQdj5tCU606rEohFLKUxWkJYAKxCiUbxGGpI -RheVmxkef9ErxJiq7hsAsGrSJvMtJuDKIasnD14SOEwD/7jRAq6WdL9VLpxtzlOw -pWnIl8kUCO3WoaG9Jf+ZTIv2hnxJhaSzYrdXzGPNnaWKhBlwnXJRvQEdrIxZOimP -FujZhqbKUDbYAcqTkoQ= +MIIE6jAcBgoqhkiG9w0BDAEDMA4ECHQQRT5r0IzlAgIIAASCBMi0JvjwTU1QrPo6 +aklVRM+9aUomvqdZqNRIooBOyzhbFi7UrXwS1rkfpv4AVHLikVb5pdbTJE/IGHIk +k566oWUDbnWyL9EFjp95I1d6ce5A4n17NatNiR+y63E7f760QnvWJ5b5X5bAR0bW +cUh+r4FiCPjS8rPIPUHuTdi87cmQwqlWK8iKxE6ZE9j9oj16mY/RPBBycpzTUzKb +YaWDMTGIm4f/bMj10OTbIgRh0zqbF85nKgEJrHtVdBw0SvKTBHT/XG224pU9scDJ +GoAlFIvvV8C98gP8KMAFbDFlxiXFznYQwJVtJ/Hx7sHPNc3hRv8rCU8sr+svqGCH +D1g4myLWVh9+jqLdRbsxOXYkpcm34AwD3PUZh0KwhceER/h9zTPNEPAMG5DEYmOy +yu3hwAgkpBRgFlmpjsRdJYFpiRnKEo21u+NaV2BgSZH8oIUs0p3Ud2W/3ePWvsvI +S9ReEQgoWx9fi1jRsGgNYvg5h0QO4UIHj6eXjQwC5ITzZsQ2qZQJ2O1XOT6qpP3S +oMMYGvjydjkVpxtjFsyT/TT5t9bqTI6sufOY8V1HslmkgyxFcGko0HrOH353s9xC +1CS1dZ07RcfC02y3/8G1kge68+kS2GJ4/Yhc7UbI1Hds5jgUyyaRYhQh6BnSW9Ap +USLoboC7gKvSE3eRnD9tTfif9bCJSKmNvvKsI8Ge6Z1l4WSgfvxScqEGzaiNWzHH +y66q3jMeDThdg/EikAj706RlgGt+2arobwUnajx6yCMrUeeS77hxP38kARwRC0Yz +qxB2BP9dG7dAWAxvn9A2POz6OGECI9Wt2MGo9L7jSA/KETQxS+TMVj0siG5UsLpi +vlYM5sbsd59bboKQCrqdeYDHBT15/lZPWsSttAohdoDki+e0mQJmDI/fJFGr/zK3 +RJQ6Lr8MaeeNDyvVsRiQmNA8Jh3Vk8O4hwjZ1JWsN1MwYNUWi4vxLSUbT76FoyIf +mlZEUii9NHg+brFsLgUWKD0MAfso8e1lKh/nvqDRu62esNMVBd7RcXmqz2oHZZf1 +08KEGK45FttNwjGMDsoh8s0lShOuOatY9dspzpdRwurlQvuE6FYzYF/IZC5nHxZh +i/+OL5RVITsv3CaCAxUFHHGEeJlhYlWCoAWSNRKohos0PwHDZ5FzqTdx9no6Pkja +JMxlgg1mCp1uRXCMrObuL8QDyK0qFKd15hXH1SELstVX65VM3nzSUiJQMvIA01zE +MpLVqK+ZtLbc7fLvNvMXCeRvaT/NMEb2KwOrjyVQ9llMhyejD7Mv5gebtNA/Nuey +fYgGbKfvL+tfjUvalAiDM04Ab0BewuqnKGc2H3Vbs8J+wcdhjmOfbYVP283NGYlZ +GUtI0uroILXS8RJL22SFhwyfxOu843WLCcLo9i7bKl+u4agymINWFD1VGH0vMk5u +trI90ftSDwXnWT8iWNkuMrrtMbYYEHRKCKYC2Ja56fTCq+uxCRQ4uwtalJvW01DO +NWWv/Xj1xQ+BuOAXAuogdYsL2FFVI2J3B5XukA24qm90LLPl5I+AI8qdAydbdPnT +hZX4WXcWyg3xcHWwgPkFoB40OquoI6Lz7+VQDLS3smhSmESgBhy+qnEOrx05tM03 +uauB9dHXVa3F0XhB2Qo= -----END ENCRYPTED PRIVATE KEY----- -----BEGIN CERTIFICATE----- -MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV -bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD -VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh -Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx -EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu -a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw -FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d -ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy -q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 -TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto -TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA -YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD -N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v -iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh -IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv -6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG -qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa -HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru -n3MVF9w= +MIIDyzCCArOgAwIBAgIUFQcij5VSD+aWUjrprTpr9Eat5ogwDQYJKoZIhvcNAQEL +BQAweTEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECAwHVW5rbm93bjEQMA4GA1UE +BwwHVW5rbm93bjEQMA4GA1UECgwHVW5rbm93bjEUMBIGA1UECwwLc3NsX3Rlc3Rp +bmcxGTAXBgNVBAMMEEFwYWNoZSBDYXNzYW5kcmEwIBcNMjYwNjA5MTIxNDUzWhgP +MjEyNjA1MTYxMjE0NTNaMHkxEDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgMB1Vu +a25vd24xEDAOBgNVBAcMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24xFDASBgNV +BAsMC3NzbF90ZXN0aW5nMRkwFwYDVQQDDBBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPIuqo5Sr/bH+nT1LeHW6dgxnYuc +EIGmX7x/7KRC/9Wal/myQXf0xgLxuRxffy6enkqHPxuHIMIW8wzPOOJCIRvVM//F +RWjRMrANz+Rx37VtTIg6ishNVGkrhmE2QoUWDIfZfeUItSBAKriHnF0TVPWNwiY+ +j2EZb8uJohzoZNUIRQFRAJYPvPkU5ANLxYNXjCGqNQcaj+/rB00BDe/G1fwB8czs +MygfMpgINosFJoVEf4iW7kQxoWbjbE+4FlXAkG1ku9mkkblJJIG0B0bkwulPkuhW +Ly7YoFMpq9SBmPEVf7aUx9ZlUhXRyIisKHQGdBYT9f7YXJSmsHDS+vEZ7QIDAQAB +o0kwRzAdBgNVHQ4EFgQUMN6gupxWKgTDRC6NxM4d7Ir2HnswJgYDVR0RBB8wHYIJ +bG9jYWxob3N0hwR/AAABhwR/AAAChwR/AAADMA0GCSqGSIb3DQEBCwUAA4IBAQBu +OgdzPIcX3o/YNmw+GxRAJmIUni0s9VenA2koC6Vy5AxnSKLhLwO6Q5CyMR8fMRJm +35e5n5VpvELj5bhLfWb5tANNzNJxsEMYyc2VOPEBer2HeJ4LAlEiDF8sBDAjbzE+ ++4FcIjG+VuemQLRxSL1As6k8Z+u8H+19ckAogbDcgkR5E/j+rVSvzgBhA4jWCogU +zjcvlz9HrdGATSn5ysv0aM/AbiY6gOmNsem02jMiFh/qO4EcOEaO7lhISFKTUtKv +zL5YyDRMOU1zJa0Ku05s1GA/ioKt8p72yid+DxuoS3noIlyOJF+dasgJh0QIlJmz ++dADc97efgsjhrmVWs5n -----END CERTIFICATE----- diff --git a/test/conf/cassandra_ssl_test.truststore b/test/conf/cassandra_ssl_test.truststore index 10abf12f5335..3f0e7da1beae 100644 Binary files a/test/conf/cassandra_ssl_test.truststore and b/test/conf/cassandra_ssl_test.truststore differ diff --git a/test/conf/cassandra_ssl_test.truststore.pem b/test/conf/cassandra_ssl_test.truststore.pem index 8806ce818bba..7f520927fc14 100644 --- a/test/conf/cassandra_ssl_test.truststore.pem +++ b/test/conf/cassandra_ssl_test.truststore.pem @@ -1,22 +1,23 @@ -----BEGIN CERTIFICATE----- -MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV -bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD -VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh -Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx -EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu -a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw -FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d -ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy -q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 -TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto -TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA -YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD -N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v -iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh -IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv -6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG -qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa -HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru -n3MVF9w= +MIIDyzCCArOgAwIBAgIUFQcij5VSD+aWUjrprTpr9Eat5ogwDQYJKoZIhvcNAQEL +BQAweTEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECAwHVW5rbm93bjEQMA4GA1UE +BwwHVW5rbm93bjEQMA4GA1UECgwHVW5rbm93bjEUMBIGA1UECwwLc3NsX3Rlc3Rp +bmcxGTAXBgNVBAMMEEFwYWNoZSBDYXNzYW5kcmEwIBcNMjYwNjA5MTIxNDUzWhgP +MjEyNjA1MTYxMjE0NTNaMHkxEDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgMB1Vu +a25vd24xEDAOBgNVBAcMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24xFDASBgNV +BAsMC3NzbF90ZXN0aW5nMRkwFwYDVQQDDBBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPIuqo5Sr/bH+nT1LeHW6dgxnYuc +EIGmX7x/7KRC/9Wal/myQXf0xgLxuRxffy6enkqHPxuHIMIW8wzPOOJCIRvVM//F +RWjRMrANz+Rx37VtTIg6ishNVGkrhmE2QoUWDIfZfeUItSBAKriHnF0TVPWNwiY+ +j2EZb8uJohzoZNUIRQFRAJYPvPkU5ANLxYNXjCGqNQcaj+/rB00BDe/G1fwB8czs +MygfMpgINosFJoVEf4iW7kQxoWbjbE+4FlXAkG1ku9mkkblJJIG0B0bkwulPkuhW +Ly7YoFMpq9SBmPEVf7aUx9ZlUhXRyIisKHQGdBYT9f7YXJSmsHDS+vEZ7QIDAQAB +o0kwRzAdBgNVHQ4EFgQUMN6gupxWKgTDRC6NxM4d7Ir2HnswJgYDVR0RBB8wHYIJ +bG9jYWxob3N0hwR/AAABhwR/AAAChwR/AAADMA0GCSqGSIb3DQEBCwUAA4IBAQBu +OgdzPIcX3o/YNmw+GxRAJmIUni0s9VenA2koC6Vy5AxnSKLhLwO6Q5CyMR8fMRJm +35e5n5VpvELj5bhLfWb5tANNzNJxsEMYyc2VOPEBer2HeJ4LAlEiDF8sBDAjbzE+ ++4FcIjG+VuemQLRxSL1As6k8Z+u8H+19ckAogbDcgkR5E/j+rVSvzgBhA4jWCogU +zjcvlz9HrdGATSn5ysv0aM/AbiY6gOmNsem02jMiFh/qO4EcOEaO7lhISFKTUtKv +zL5YyDRMOU1zJa0Ku05s1GA/ioKt8p72yid+DxuoS3noIlyOJF+dasgJh0QIlJmz ++dADc97efgsjhrmVWs5n -----END CERTIFICATE----- diff --git a/test/conf/cassandra_ssl_test.unencrypted_keystore.pem b/test/conf/cassandra_ssl_test.unencrypted_keystore.pem index ce3d8e7584bd..d04f79960cf9 100644 --- a/test/conf/cassandra_ssl_test.unencrypted_keystore.pem +++ b/test/conf/cassandra_ssl_test.unencrypted_keystore.pem @@ -1,50 +1,51 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCOSZVf8dLj1xLw -efqjbogbAwhwRXd3tmEfQY1zHyudJF3XR1T0Xp26BKNvAYUxxZRDNg16M3prPRZv -wkhDJdE9NaN+BpZPJUavRsZOfGDq/CHN8j/2PPKrn3G/b065JjV3GZjfV/Sln047 -DeZptaSyOg7ZN7F8qjGqxEcnw+szV/wTzhnjGjZMlcbOm4jFGQAn6xUOooQCGsoB -9FgxKB0nvNG3xVe/2eCNfbS4DabT3Y1wfQqZ62hOa5ZS0rwT+pw3tQs3zFFY1Sfi -G7qYbqZrKTheWIZGojVVm6mqH4yA2ofOe5N3RsBitCwU/D0dpnaG9Wcl98nmXueM -B6Rk04v7AgMBAAECggEAYnxIKjrFz/JkJ5MmiszM5HV698r9YB0aqHnFIHPoykIL -uiCjiumantDrFsCkosixULwvI/BRwbxstTpyrheU9psT6P1CONICVPvV8ylgJAYU -l+ofn56cEXKxVuICSWFLDH7pM1479g+IJJQAchbKQpqxAGTuMu3SpvJolfuj5srt -bM7/RYhJFLwDuvHNA3ivlogMneItP03+C25aaxstM+lBuBf68+n78zMgSvt6J/6Y -G2TOMKnxveMlG2qu9l2lAw/2i8daG/qre08nTH7wpRx0gZLZqNpe45exkrzticzF -FgWYjG2K2brX21jqHroFgMhdXF7zhhRgLoIeC0BrIQKBgQDCfGfWrJESKBbVai5u -7wqD9nlzjv6N6FXfTDOPXO1vz5frdvtLVWbs0SMPy+NglkaZK0iqHvb9mf2of8eC -0D5cmewjn7WCDBQMypIMYgT912ak/BBVuGXcxb6UgD+xARfSARo2C8NG1hfprw1W -ad14CjS5xhFMs44HpVYhI7iPYwKBgQC7SqVG/b37vZ7CINemdvoMujLvvYXDJJM8 -N21LqNJfVXdukdH3T0xuLnh9Z/wPHjJDMF/9+1foxSEPHijtyz5P19EilNEC/3qw -fI19+VZoY0mdhPtXSGzy+rbTE2v71QgwFLizSos14Gr+eNiIjF7FYccK05++K/zk -cd8ZA3bwiQKBgQCl+HTFBs9mpz+VMOAfW2+l3hkXPNiPUc62mNkHZ05ZNNd44jjh -uSf0wSUiveR08MmevQlt5K7zDQ8jVKh2QjB15gVXAVxsdtJFeDnax2trFP9LnLBz -9sE2/qn9INU5wK0LUlWD+dXUBbCyg+jl7cJKRqtoPldVFYYHkFlIPqup8QKBgHXv -hyuw1FUVDkdHzwOvn70r8q8sNHKxMVWVwWkHIZGOi+pAQGrusD4hXRX6yKnsZdIR -QCD6iFy25R5T64nxlYdJaxPPid3NakB/7ckJnPOWseBSwMIxhQlr/nvjmve1Kba9 -FaEwq4B9lGIxToiNe4/nBiM3JzvlDxX67nUdzWOhAoGAdFvriyvjshSJ4JHgIY9K -37BVB0VKMcFV2P8fLVWO5oyRtE1bJhU4QVpQmauABU4RGSojJ3NPIVH1wxmJeYtj -Q3b7EZaqI6ovna2eK2qtUx4WwxhRaXTT8xueBI2lgL6sBSTGG+K69ZOzGQzG/Mfr -RXKInnLInFD9JD94VqmMozo= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw8i6qjlKv9sf6 +dPUt4dbp2DGdi5wQgaZfvH/spEL/1ZqX+bJBd/TGAvG5HF9/Lp6eSoc/G4cgwhbz +DM844kIhG9Uz/8VFaNEysA3P5HHftW1MiDqKyE1UaSuGYTZChRYMh9l95Qi1IEAq +uIecXRNU9Y3CJj6PYRlvy4miHOhk1QhFAVEAlg+8+RTkA0vFg1eMIao1BxqP7+sH +TQEN78bV/AHxzOwzKB8ymAg2iwUmhUR/iJbuRDGhZuNsT7gWVcCQbWS72aSRuUkk +gbQHRuTC6U+S6FYvLtigUymr1IGY8RV/tpTH1mVSFdHIiKwodAZ0FhP1/thclKaw +cNL68RntAgMBAAECggEASybI+CpTZyXmgHLNMSwChbYLOJDzezU9btrV8DFBvXvA +2Xw1H8YtYS4d8RNiYddwieU4dO6hoSGd8qOFnXDHSl2SWy/t3pFqjF8mtp2dWbiq +D7+qMYhqA4hZcoz2KSFyIGdQUb6FSLxGVH6kJh6H1+UkzIlGt8mzLn6hWYdykmQM +76uxEp0qqPVr15QtjrYBl9F4iUh2D6UHIIE5CUtAablZEXbzApxUr0oFrPigQpjU +DP/IzexGneD8gg8ulU1XxUkZRJGD0Wv1zr7bi7lAYSeULsjt5fD8n1Z8+Llw7Mgz +b2Clzjgs1k0gQbBnE3nvo8ZFdHXnYB5sDMOIzSMwJwKBgQDmaC3THefkbsSIACGW +h99xC61ngasYn1GykFHGMdcPbsIt1hU3Reiq+21RIX1iUrP9I0vd8Elm9MYC/v0L +j2pnVekWcR6ME9kUJNIkyFAohfKMbT/BFreDJwJpzTnHFNb8hXrnpDMPGRzHq/Gi +t8tsXSIwK3KBIBeYSEEggFkvjwKBgQDEmcuuWhz9ELibGZDO156XPxDgccPN4fdP +wHfxn0ShmMq6YXp1RbHDc/oahJ4LRzbVRTfTsJINq4hzLEA5FplWFiglBJJk+MVm +TZy/obwPici49mI/5MrpvBO371uiYDjxa2Uaf/35keo0L6GhL6SyVhCYySbKiqcm +gIHxVfAgwwKBgGM5FBrpsxaFuS8UV2KbCteE/t0nU0ZcPfOXARBIIGRt/0N8AVD3 +UzZm5nHc6UExen+V9rMSKpoi6S8bHmAfF+R+c82NU2lhlsd8/96FQTfiT0y4M490 +t/zMDNcBYVNhnx/KX95nsPFckC9Q1dOMMRdumC2EWGBRMLgMzbcwbrfzAoGABX1d +6KJviMl8vif6mSwAK60BJaNHmmoi48E2GDMgUXrYvleWecvWaOTGKRNm7l2wtEfY +hTq6+VK+3qhvqqhs47B+snnsNJGVwYONSvSTMcPhLPkESVB0Mg6kZlByuJgDPwqG +qjTEvMFMTuS9mKih6rDoibukL6erfzG7bye9Ks0CgYEAumIui49lXJR5rB5fNevF +XzGHxkhofpx7y2Fdtpym7rK46AVagKF7jqa/XuFk1P37KgEhrTPtnCkICwjjhoUD +yYJggN07TUGIU+9WD4RQ+tvBpyLl1gEtwpCVw4k3hkRlulA5wDQjtDgf4FPLgphX +hbzjlSLJkHJYOthBGLrEsG0= -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- -MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV -bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD -VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh -Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx -EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu -a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw -FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d -ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy -q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 -TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto -TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA -YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD -N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v -iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh -IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv -6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG -qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa -HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru -n3MVF9w= +MIIDyzCCArOgAwIBAgIUFQcij5VSD+aWUjrprTpr9Eat5ogwDQYJKoZIhvcNAQEL +BQAweTEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECAwHVW5rbm93bjEQMA4GA1UE +BwwHVW5rbm93bjEQMA4GA1UECgwHVW5rbm93bjEUMBIGA1UECwwLc3NsX3Rlc3Rp +bmcxGTAXBgNVBAMMEEFwYWNoZSBDYXNzYW5kcmEwIBcNMjYwNjA5MTIxNDUzWhgP +MjEyNjA1MTYxMjE0NTNaMHkxEDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgMB1Vu +a25vd24xEDAOBgNVBAcMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24xFDASBgNV +BAsMC3NzbF90ZXN0aW5nMRkwFwYDVQQDDBBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPIuqo5Sr/bH+nT1LeHW6dgxnYuc +EIGmX7x/7KRC/9Wal/myQXf0xgLxuRxffy6enkqHPxuHIMIW8wzPOOJCIRvVM//F +RWjRMrANz+Rx37VtTIg6ishNVGkrhmE2QoUWDIfZfeUItSBAKriHnF0TVPWNwiY+ +j2EZb8uJohzoZNUIRQFRAJYPvPkU5ANLxYNXjCGqNQcaj+/rB00BDe/G1fwB8czs +MygfMpgINosFJoVEf4iW7kQxoWbjbE+4FlXAkG1ku9mkkblJJIG0B0bkwulPkuhW +Ly7YoFMpq9SBmPEVf7aUx9ZlUhXRyIisKHQGdBYT9f7YXJSmsHDS+vEZ7QIDAQAB +o0kwRzAdBgNVHQ4EFgQUMN6gupxWKgTDRC6NxM4d7Ir2HnswJgYDVR0RBB8wHYIJ +bG9jYWxob3N0hwR/AAABhwR/AAAChwR/AAADMA0GCSqGSIb3DQEBCwUAA4IBAQBu +OgdzPIcX3o/YNmw+GxRAJmIUni0s9VenA2koC6Vy5AxnSKLhLwO6Q5CyMR8fMRJm +35e5n5VpvELj5bhLfWb5tANNzNJxsEMYyc2VOPEBer2HeJ4LAlEiDF8sBDAjbzE+ ++4FcIjG+VuemQLRxSL1As6k8Z+u8H+19ckAogbDcgkR5E/j+rVSvzgBhA4jWCogU +zjcvlz9HrdGATSn5ysv0aM/AbiY6gOmNsem02jMiFh/qO4EcOEaO7lhISFKTUtKv +zL5YyDRMOU1zJa0Ku05s1GA/ioKt8p72yid+DxuoS3noIlyOJF+dasgJh0QIlJmz ++dADc97efgsjhrmVWs5n -----END CERTIFICATE----- diff --git a/test/conf/cassandra_ssl_test_nopassword.keystore b/test/conf/cassandra_ssl_test_nopassword.keystore index 8778a3876b27..fed6bbdc9675 100644 Binary files a/test/conf/cassandra_ssl_test_nopassword.keystore and b/test/conf/cassandra_ssl_test_nopassword.keystore differ diff --git a/test/conf/logback-burntest.xml b/test/conf/logback-burntest.xml index 4c0b062b3bc3..4aa8967bd4f2 100644 --- a/test/conf/logback-burntest.xml +++ b/test/conf/logback-burntest.xml @@ -59,7 +59,7 @@ - + diff --git a/test/conf/logback-test-jenkins.xml b/test/conf/logback-test-jenkins.xml new file mode 100644 index 000000000000..104b311046ce --- /dev/null +++ b/test/conf/logback-test-jenkins.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + ./build/test/logs/${cassandra.testtag}/TEST-${suitename}.log + + ./build/test/logs/${cassandra.testtag}/TEST-${suitename}.log.%i.gz + 1 + 20 + + + + 20MB + + + + %-5level [%thread] %date{ISO8601} %msg%n + + false + + + DEBUG + + + + + + 0 + 0 + 1024 + + true + + + + + + + diff --git a/test/conf/logback-test.xml b/test/conf/logback-test.xml index 757806e35f2b..9e617f6665ea 100644 --- a/test/conf/logback-test.xml +++ b/test/conf/logback-test.xml @@ -22,7 +22,7 @@ - + @@ -53,8 +53,8 @@ - - + + @@ -64,14 +64,17 @@ - 0 - 0 - 1024 - - true + 0 + 0 + 1024 + + true + - + + + diff --git a/test/conf/unit-test-conf/test-native-port.yaml b/test/conf/unit-test-conf/test-native-port.yaml index 2d0b184f15c7..18a5e9fa66c1 100644 --- a/test/conf/unit-test-conf/test-native-port.yaml +++ b/test/conf/unit-test-conf/test-native-port.yaml @@ -43,7 +43,7 @@ compaction_throughput: 0MiB/s row_cache_class_name: org.apache.cassandra.cache.OHCProvider row_cache_size: 16MiB user_defined_functions_enabled: true -scripted_user_defined_functions_enabled: true +scripted_user_defined_functions_enabled: false prepared_statements_cache_size: 1MiB corrupted_tombstone_strategy: exception stream_entire_sstables: true diff --git a/test/data/config/version=5.0-alpha1.yml b/test/data/config/version=5.0-alpha1.yml index 8dad0f60acc2..a700630ebb9e 100644 --- a/test/data/config/version=5.0-alpha1.yml +++ b/test/data/config/version=5.0-alpha1.yml @@ -143,8 +143,6 @@ networking_cache_size: "org.apache.cassandra.config.DataStorageSpec.IntMebibytes fields_per_udt_fail_threshold: "java.lang.Integer" key_cache_size: "org.apache.cassandra.config.DataStorageSpec.LongMebibytesBound" max_hint_window: "org.apache.cassandra.config.DurationSpec.IntMillisecondsBound" -sai_options: - segment_write_buffer_size: "org.apache.cassandra.config.DataStorageSpec.IntMebibytesBound" vector_dimensions_fail_threshold: "java.lang.Integer" max_hints_size_per_host: "org.apache.cassandra.config.DataStorageSpec.LongBytesBound" partition_size_fail_threshold: "org.apache.cassandra.config.DataStorageSpec.LongBytesBound" diff --git a/test/data/jmxdump/cassandra-4.0-jmx.yaml b/test/data/jmxdump/cassandra-4.0-jmx.yaml index e0d01272c83a..d097bc2f2333 100644 --- a/test/data/jmxdump/cassandra-4.0-jmx.yaml +++ b/test/data/jmxdump/cassandra-4.0-jmx.yaml @@ -3359,7 +3359,6 @@ org.apache.cassandra.db:type=StorageService: - {access: read-only, name: Joined, type: boolean} - {access: read-only, name: JoiningNodes, type: java.util.List} - {access: read-only, name: JoiningNodesWithPort, type: java.util.List} - - {access: read/write, name: KeyspaceCountWarnThreshold, type: int} - {access: read-only, name: Keyspaces, type: java.util.List} - {access: read-only, name: LeavingNodes, type: java.util.List} - {access: read-only, name: LeavingNodesWithPort, type: java.util.List} @@ -3401,7 +3400,6 @@ org.apache.cassandra.db:type=StorageService: - {access: read/write, name: SnapshotLinksPerSecond, type: long} - {access: read-only, name: Starting, type: boolean} - {access: read/write, name: StreamThroughputMbPerSec, type: int} - - {access: read/write, name: TableCountWarnThreshold, type: int} - {access: read-only, name: TokenToEndpointMap, type: java.util.Map} - {access: read-only, name: TokenToEndpointWithPortMap, type: java.util.Map} - {access: read-only, name: Tokens, type: java.util.List} @@ -10190,6 +10188,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,n org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -10204,6 +10207,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,n org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11067,6 +11075,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11081,6 +11094,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11944,6 +11962,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11958,6 +11981,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -12821,6 +12849,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -12835,6 +12868,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -13698,6 +13736,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -13712,6 +13755,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -14575,6 +14623,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -14589,6 +14642,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_history,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -15452,6 +15510,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -15466,6 +15529,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -16329,6 +16397,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -16343,6 +16416,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -17206,6 +17284,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -17220,6 +17303,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18083,6 +18171,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18097,6 +18190,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18960,6 +19058,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18974,6 +19077,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -19837,6 +19945,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,na org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -19851,6 +19964,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,na org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -20714,6 +20832,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_st org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_statements,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -20728,6 +20851,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_st org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_statements,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -21591,6 +21719,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -21605,6 +21738,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -22468,6 +22606,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estima org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -22482,6 +22625,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estima org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -23345,6 +23493,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_act org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_activity,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -23359,6 +23512,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_act org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_activity,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -24222,6 +24380,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estim org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -24236,6 +24399,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estim org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25099,6 +25267,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25113,6 +25286,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25976,6 +26154,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25990,6 +26173,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -26853,6 +27041,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=view_builds org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=view_builds_in_progress,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -27730,6 +27923,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=networ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=network_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -29484,6 +29682,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_m org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_members,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -29498,6 +29701,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_m org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_members,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -30361,6 +30569,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_p org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -30375,6 +30588,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_p org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_permissions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -31238,6 +31456,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles, org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -31252,6 +31475,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles, org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -32992,6 +33220,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope=repair_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -33869,6 +34102,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope=view_build_status,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -34746,6 +34984,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggr org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggregates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -34760,6 +35003,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggr org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggregates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -35623,6 +35871,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=colu org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -35637,6 +35890,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=colu org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=columns,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -36500,6 +36758,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=drop org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=dropped_columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -37377,6 +37640,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=func org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=functions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -37391,6 +37659,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=func org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=functions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -38254,6 +38527,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=inde org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=indexes,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -38268,6 +38546,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=inde org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=indexes,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -39131,6 +39414,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keys org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keyspaces,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -39145,6 +39433,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keys org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keyspaces,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40008,6 +40301,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tabl org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tables,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40022,6 +40320,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tabl org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tables,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40885,6 +41188,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=trig org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=triggers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40899,6 +41207,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=trig org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=triggers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -41762,6 +42075,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=type org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=types,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -41776,6 +42094,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=type org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=types,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -42639,6 +42962,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=view org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -42653,6 +42981,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=view org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -43516,6 +43849,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=even org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -43530,6 +43868,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=even org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -44393,6 +44736,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sess org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sessions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -44407,6 +44755,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sess org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sessions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -45269,7 +45622,12 @@ org.apache.cassandra.metrics:type=ColumnFamily,name=BloomFilterOffHeapMemoryUsed returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=ColumnFamily,name=BytesAnticompacted: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -45283,7 +45641,12 @@ org.apache.cassandra.metrics:type=ColumnFamily,name=BytesFlushed: returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=ColumnFamily,name=BytesMutatedAnticompaction: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -57152,6 +57515,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=Blo org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -57166,6 +57534,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=Byt org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -58233,6 +58606,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,n org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -58247,6 +58625,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,n org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -59314,6 +59697,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -59328,6 +59716,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -60395,6 +60788,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=Bloom org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -60409,6 +60807,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=Bytes org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -61476,6 +61879,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -61490,6 +61898,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -62557,6 +62970,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -62571,6 +62989,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -63638,6 +64061,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BloomFi org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -63652,6 +64080,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BytesFl org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -64719,6 +65152,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BloomFi org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -64733,6 +65171,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BytesFl org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -65800,6 +66243,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -65814,6 +66262,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -66881,6 +67334,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -66895,6 +67353,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -67962,6 +68425,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BloomFi org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -67976,6 +68444,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BytesFl org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -69043,6 +69516,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=Bloo org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -69057,6 +69535,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=Byte org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -70124,6 +70607,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statement org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statements,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -70138,6 +70626,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statement org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statements,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -71205,6 +71698,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=Bloom org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -71219,6 +71717,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=Bytes org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -72286,6 +72789,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -72300,6 +72808,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -73367,6 +73880,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity,n org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -73381,6 +73899,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity,n org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -74448,6 +74971,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,na org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -74462,6 +74990,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,na org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -75529,6 +76062,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -75543,6 +76081,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -76610,6 +77153,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -76624,6 +77172,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -77691,6 +78244,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_pro org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_progress,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -77705,6 +78263,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_pro org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_progress,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -78772,6 +79335,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -78786,6 +79354,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permissions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -79853,6 +80426,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=resource_role org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=resource_role_permissons_index,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -80934,6 +81512,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members, org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -80948,6 +81531,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members, org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -82015,6 +82603,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -82029,6 +82622,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -83096,6 +83694,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=Bl org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -83110,6 +83713,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=By org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -84177,6 +84785,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=parent org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=parent_repair_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -85258,6 +85871,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -85272,6 +85890,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair_history,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -86339,6 +86962,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=view_b org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=view_build_status,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -87420,6 +88048,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates, org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -87434,6 +88067,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates, org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -88501,6 +89139,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -88515,6 +89158,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -89582,6 +90230,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_col org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -89596,6 +90249,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_col org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_columns,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -90663,6 +91321,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -90677,6 +91340,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -91744,6 +92412,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -91758,6 +92431,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -92825,6 +93503,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -92839,6 +93522,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -93906,6 +94594,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -93920,6 +94613,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -94987,6 +95685,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,na org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -95001,6 +95704,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,na org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -96068,6 +96776,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -96082,6 +96795,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -97149,6 +97867,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -97163,6 +97886,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -98230,6 +98958,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -98244,6 +98977,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -99311,6 +100049,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,na org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -99325,6 +100068,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,na org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -100391,7 +101139,12 @@ org.apache.cassandra.metrics:type=Table,name=BloomFilterOffHeapMemoryUsed: returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=Table,name=BytesAnticompacted: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -100405,7 +101158,12 @@ org.apache.cassandra.metrics:type=Table,name=BytesFlushed: returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=Table,name=BytesMutatedAnticompaction: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] diff --git a/test/data/jmxdump/cassandra-4.1-jmx.yaml b/test/data/jmxdump/cassandra-4.1-jmx.yaml index a5ea2a74a16b..feb3c847f5a3 100644 --- a/test/data/jmxdump/cassandra-4.1-jmx.yaml +++ b/test/data/jmxdump/cassandra-4.1-jmx.yaml @@ -3359,7 +3359,6 @@ org.apache.cassandra.db:type=StorageService: - {access: read-only, name: Joined, type: boolean} - {access: read-only, name: JoiningNodes, type: java.util.List} - {access: read-only, name: JoiningNodesWithPort, type: java.util.List} - - {access: read/write, name: KeyspaceCountWarnThreshold, type: int} - {access: read-only, name: Keyspaces, type: java.util.List} - {access: read-only, name: LeavingNodes, type: java.util.List} - {access: read-only, name: LeavingNodesWithPort, type: java.util.List} @@ -3401,7 +3400,6 @@ org.apache.cassandra.db:type=StorageService: - {access: read/write, name: SnapshotLinksPerSecond, type: long} - {access: read-only, name: Starting, type: boolean} - {access: read/write, name: StreamThroughputMbPerSec, type: int} - - {access: read/write, name: TableCountWarnThreshold, type: int} - {access: read-only, name: TokenToEndpointMap, type: java.util.Map} - {access: read-only, name: TokenToEndpointWithPortMap, type: java.util.Map} - {access: read-only, name: Tokens, type: java.util.List} @@ -10190,6 +10188,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,n org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -10204,6 +10207,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,n org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=IndexInfo,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11067,6 +11075,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11081,6 +11094,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11944,6 +11962,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -11958,6 +11981,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_r org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=available_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -12821,6 +12849,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -12835,6 +12868,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=batches,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -13698,6 +13736,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -13712,6 +13755,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=built_views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -14575,6 +14623,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -14589,6 +14642,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=compaction_history,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -15452,6 +15510,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -15466,6 +15529,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=local,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -16329,6 +16397,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -16343,6 +16416,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=paxos,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -17206,6 +17284,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -17220,6 +17303,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18083,6 +18171,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18097,6 +18190,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peer_events_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18960,6 +19058,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -18974,6 +19077,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name= org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -19837,6 +19945,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,na org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -19851,6 +19964,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,na org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=peers_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -20714,6 +20832,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_st org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_statements,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -20728,6 +20851,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_st org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=prepared_statements,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -21591,6 +21719,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -21605,6 +21738,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,nam org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=repairs,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -22468,6 +22606,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estima org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -22482,6 +22625,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estima org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=size_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -23345,6 +23493,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_act org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_activity_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -23359,6 +23512,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_act org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=sstable_activity_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -24222,6 +24380,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estim org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -24236,6 +24399,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estim org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=table_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25099,6 +25267,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25113,6 +25286,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25976,6 +26154,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -25990,6 +26173,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=transferred_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -26853,6 +27041,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=view_builds org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system,scope=view_builds_in_progress,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -27730,6 +27923,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=networ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=network_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -29484,6 +29682,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_m org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_members,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -29498,6 +29701,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_m org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_members,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -30361,6 +30569,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_p org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -30375,6 +30588,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_p org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=role_permissions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -31238,6 +31456,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles, org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -31252,6 +31475,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles, org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_auth,scope=roles,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -32992,6 +33220,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope=repair_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -33869,6 +34102,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_distributed,scope=view_build_status,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -34746,6 +34984,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggr org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggregates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -34760,6 +35003,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggr org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=aggregates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -35623,6 +35871,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=colu org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -35637,6 +35890,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=colu org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=columns,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -36500,6 +36758,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=drop org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=dropped_columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -37377,6 +37640,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=func org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=functions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -37391,6 +37659,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=func org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=functions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -38254,6 +38527,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=inde org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=indexes,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -38268,6 +38546,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=inde org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=indexes,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -39131,6 +39414,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keys org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keyspaces,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -39145,6 +39433,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keys org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=keyspaces,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40008,6 +40301,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tabl org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tables,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40022,6 +40320,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tabl org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=tables,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40885,6 +41188,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=trig org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=triggers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -40899,6 +41207,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=trig org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=triggers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -41762,6 +42075,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=type org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=types,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -41776,6 +42094,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=type org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=types,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -42639,6 +42962,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=view org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -42653,6 +42981,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=view org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_schema,scope=views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -43516,6 +43849,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=even org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -43530,6 +43868,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=even org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -44393,6 +44736,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sess org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sessions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -44407,6 +44755,11 @@ org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sess org.apache.cassandra.metrics:type=ColumnFamily,keyspace=system_traces,scope=sessions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -45269,7 +45622,12 @@ org.apache.cassandra.metrics:type=ColumnFamily,name=BloomFilterOffHeapMemoryUsed returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=ColumnFamily,name=BytesAnticompacted: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -45283,7 +45641,12 @@ org.apache.cassandra.metrics:type=ColumnFamily,name=BytesFlushed: returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=ColumnFamily,name=BytesMutatedAnticompaction: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -57152,6 +57515,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=Blo org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -57166,6 +57534,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=Byt org.apache.cassandra.metrics:type=Table,keyspace=system,scope=IndexInfo,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -58233,6 +58606,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,n org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -58247,6 +58625,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,n org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -59314,6 +59697,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -59328,6 +59716,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v org.apache.cassandra.metrics:type=Table,keyspace=system,scope=available_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -60395,6 +60788,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=Bloom org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -60409,6 +60807,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=Bytes org.apache.cassandra.metrics:type=Table,keyspace=system,scope=batches,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -61476,6 +61879,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -61490,6 +61898,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=built_views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -62557,6 +62970,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -62571,6 +62989,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history org.apache.cassandra.metrics:type=Table,keyspace=system,scope=compaction_history,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -63638,6 +64061,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BloomFi org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -63652,6 +64080,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BytesFl org.apache.cassandra.metrics:type=Table,keyspace=system,scope=local,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -64719,6 +65152,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BloomFi org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -64733,6 +65171,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BytesFl org.apache.cassandra.metrics:type=Table,keyspace=system,scope=paxos,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -65800,6 +66243,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -65814,6 +66262,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=B org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -66881,6 +67334,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -66895,6 +67353,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peer_events_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -67962,6 +68425,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BloomFi org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -67976,6 +68444,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BytesFl org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -69043,6 +69516,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=Bloo org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -69057,6 +69535,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=Byte org.apache.cassandra.metrics:type=Table,keyspace=system,scope=peers_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -70124,6 +70607,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statement org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statements,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -70138,6 +70626,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statement org.apache.cassandra.metrics:type=Table,keyspace=system,scope=prepared_statements,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -71205,6 +71698,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=Bloom org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -71219,6 +71717,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=Bytes org.apache.cassandra.metrics:type=Table,keyspace=system,scope=repairs,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -72286,6 +72789,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -72300,6 +72808,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,nam org.apache.cassandra.metrics:type=Table,keyspace=system,scope=size_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -73367,6 +73880,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity_v org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -73381,6 +73899,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity_v org.apache.cassandra.metrics:type=Table,keyspace=system,scope=sstable_activity_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -74448,6 +74971,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,na org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -74462,6 +74990,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,na org.apache.cassandra.metrics:type=Table,keyspace=system,scope=table_estimates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -75529,6 +76062,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -75543,6 +76081,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -76610,6 +77153,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges_v2,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -76624,6 +77172,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges org.apache.cassandra.metrics:type=Table,keyspace=system,scope=transferred_ranges_v2,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -77691,6 +78244,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_pro org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_progress,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -77705,6 +78263,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_pro org.apache.cassandra.metrics:type=Table,keyspace=system,scope=view_builds_in_progress,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -78772,6 +79335,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -78786,6 +79354,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=network_permissions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -79853,6 +80426,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=resource_role org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=resource_role_permissons_index,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -80934,6 +81512,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members, org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -80948,6 +81531,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members, org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_members,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -82015,6 +82603,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -82029,6 +82622,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissi org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=role_permissions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -83096,6 +83694,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=Bl org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -83110,6 +83713,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=By org.apache.cassandra.metrics:type=Table,keyspace=system_auth,scope=roles,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -84177,6 +84785,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=parent org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=parent_repair_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -85258,6 +85871,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair_history,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -85272,6 +85890,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=repair_history,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -86339,6 +86962,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=view_b org.apache.cassandra.metrics:type=Table,keyspace=system_distributed,scope=view_build_status,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -87420,6 +88048,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates, org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -87434,6 +88067,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates, org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=aggregates,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -88501,6 +89139,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -88515,6 +89158,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=columns,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -89582,6 +90230,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_col org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_columns,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -89596,6 +90249,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_col org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=dropped_columns,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -90663,6 +91321,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -90677,6 +91340,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=functions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -91744,6 +92412,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -91758,6 +92431,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,nam org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=indexes,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -92825,6 +93503,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -92839,6 +93522,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,n org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=keyspaces,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -93906,6 +94594,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -93920,6 +94613,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=tables,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -94987,6 +95685,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,na org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -95001,6 +95704,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,na org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=triggers,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -96068,6 +96776,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -96082,6 +96795,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=types,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -97149,6 +97867,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -97163,6 +97886,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name= org.apache.cassandra.metrics:type=Table,keyspace=system_schema,scope=views,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -98230,6 +98958,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -98244,6 +98977,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=events,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -99311,6 +100049,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,na org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,name=BytesAnticompacted: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -99325,6 +100068,11 @@ org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,na org.apache.cassandra.metrics:type=Table,keyspace=system_traces,scope=sessions,name=BytesMutatedAnticompaction: attributes: - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -100391,7 +101139,12 @@ org.apache.cassandra.metrics:type=Table,name=BloomFilterOffHeapMemoryUsed: returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=Table,name=BytesAnticompacted: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] @@ -100405,7 +101158,12 @@ org.apache.cassandra.metrics:type=Table,name=BytesFlushed: returnType: javax.management.ObjectName org.apache.cassandra.metrics:type=Table,name=BytesMutatedAnticompaction: attributes: - - {access: read-only, name: Value, type: java.lang.Object} + - {access: read-only, name: Count, type: long} + - {access: read-only, name: FifteenMinuteRate, type: double} + - {access: read-only, name: FiveMinuteRate, type: double} + - {access: read-only, name: MeanRate, type: double} + - {access: read-only, name: OneMinuteRate, type: double} + - {access: read-only, name: RateUnit, type: java.lang.String} operations: - name: objectName parameters: [] diff --git a/test/data/legacy-sai/aa/bb-1-bti-CRC.db b/test/data/legacy-sai/aa/bb-1-bti-CRC.db new file mode 100644 index 000000000000..d64361d7e03f Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-CRC.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-Data.db b/test/data/legacy-sai/aa/bb-1-bti-Data.db new file mode 100644 index 000000000000..13734053a155 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-Data.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-Digest.crc32 b/test/data/legacy-sai/aa/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..ed002f710a8b --- /dev/null +++ b/test/data/legacy-sai/aa/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3882779689 \ No newline at end of file diff --git a/test/data/legacy-sai/aa/bb-1-bti-Filter.db b/test/data/legacy-sai/aa/bb-1-bti-Filter.db new file mode 100644 index 000000000000..ec742fc73dbd Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-Filter.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-Partitions.db b/test/data/legacy-sai/aa/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..43c824ad4fff Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-Partitions.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-Rows.db b/test/data/legacy-sai/aa/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_GroupComplete.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_GroupComplete.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_GroupMeta.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_GroupMeta.db new file mode 100644 index 000000000000..3a4def209bc8 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_GroupMeta.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_OffsetsValues.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_OffsetsValues.db new file mode 100644 index 000000000000..839c13e73ec5 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_OffsetsValues.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_TokenValues.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_TokenValues.db new file mode 100644 index 000000000000..b608f5d8efcb Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_TokenValues.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_ColumnComplete.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_ColumnComplete.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_KDTree.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_KDTree.db new file mode 100644 index 000000000000..ca7105fcc8ba Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_KDTree.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_KDTreePostingLists.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_KDTreePostingLists.db new file mode 100644 index 000000000000..1f6e4bd0f23e Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_KDTreePostingLists.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_Meta.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_Meta.db new file mode 100644 index 000000000000..bb82c3fd88d0 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_int_index_Meta.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_ColumnComplete.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_ColumnComplete.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_Meta.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_Meta.db new file mode 100644 index 000000000000..93637ad663b5 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_Meta.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_PostingLists.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_PostingLists.db new file mode 100644 index 000000000000..aaa178868518 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_PostingLists.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_TermsData.db b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_TermsData.db new file mode 100644 index 000000000000..f5cda8197f06 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-SAI_text_index_TermsData.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-Statistics.db b/test/data/legacy-sai/aa/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..1e42b4385ab1 Binary files /dev/null and b/test/data/legacy-sai/aa/bb-1-bti-Statistics.db differ diff --git a/test/data/legacy-sai/aa/bb-1-bti-TOC.txt b/test/data/legacy-sai/aa/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..8fc8892e036e --- /dev/null +++ b/test/data/legacy-sai/aa/bb-1-bti-TOC.txt @@ -0,0 +1,20 @@ +Digest.crc32 +SAI_table_0_text_value_idx_ColumnComplete.db +SAI_table_0_int_value_idx_KDTreePostingLists.db +SAI_table_0_text_value_idx_PostingLists.db +SAI_table_0_int_value_idx_ColumnComplete.db +SAI_table_0_int_value_idx_KDTree.db +Data.db +SAI_OffsetsValues.db +Partitions.db +SAI_table_0_text_value_idx_Meta.db +SAI_table_0_int_value_idx_Meta.db +SAI_table_0_text_value_idx_TermsData.db +SAI_GroupComplete.db +Statistics.db +TOC.txt +SAI_TokenValues.db +Rows.db +CRC.db +Filter.db +SAI_GroupMeta.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..dbc18f6cc256 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Data.db new file mode 100644 index 000000000000..1f9357b1f068 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..3d9631973846 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3036065180 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Rows.db new file mode 100644 index 000000000000..2cf64b034c96 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..3d552f20e788 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..2e57b49d47a0 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Data.db new file mode 100644 index 000000000000..b038dc27c272 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..4c817caeae51 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +112902994 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Rows.db new file mode 100644 index 000000000000..50052c4fdc6f Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..e37caf6feba7 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_compact/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..9f719378e34d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Data.db new file mode 100644 index 000000000000..f4c625fb8992 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..5720255cb5e0 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +647001919 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Rows.db new file mode 100644 index 000000000000..6b74492bf38e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..02f7d47d3059 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..1cdf45e437d7 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Data.db new file mode 100644 index 000000000000..200f4d9e8256 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..672e8beb7691 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +400579342 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Rows.db new file mode 100644 index 000000000000..6b74492bf38e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..a8c896b1bfe5 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_clust_counter_compact/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..b8449a126036 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Data.db new file mode 100644 index 000000000000..801ff7c5dd85 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..a65a24d4b144 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2180385804 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b58e3946e230 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..c0f56d107fca Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..50bb4e272568 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..582d8fbce369 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_inaccurate_min_max/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Partitions.db +Filter.db +Data.db +TOC.txt +Statistics.db +CompressionInfo.db +Rows.db +Digest.crc32 diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fc38a25eea5d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Data.db new file mode 100644 index 000000000000..9f7645e8e8e0 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..36c915b373f1 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +4174191692 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..5328b8858270 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..1c738aa0288a Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Data.db new file mode 100644 index 000000000000..95ea5e12f87a Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..314119cbea6e --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +230017823 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..657d5463fca8 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..c265700c200e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_compact/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..e2860e1eb16a Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Data.db new file mode 100644 index 000000000000..be45380232b1 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..9d786a90ad7a --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3862701472 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..036a76a00ade Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-CompressionInfo.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..1237cc7f0057 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Data.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Data.db new file mode 100644 index 000000000000..eccef889e67e Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Digest.crc32 b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Digest.crc32 new file mode 100644 index 000000000000..50f631b1a2c3 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3409102979 \ No newline at end of file diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Filter.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Filter.db new file mode 100644 index 000000000000..b8cb5146f59d Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Partitions.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Partitions.db new file mode 100644 index 000000000000..6c9a78cd043c Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Rows.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Statistics.db b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Statistics.db new file mode 100644 index 000000000000..6ede946fc245 Binary files /dev/null and b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-TOC.txt b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-TOC.txt new file mode 100644 index 000000000000..db06c09bbb50 --- /dev/null +++ b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_simple_counter_compact/aa-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Rows.db +Data.db +Statistics.db +TOC.txt +Filter.db +Digest.crc32 +CompressionInfo.db +Partitions.db diff --git a/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_tuple/.keep b/test/data/legacy-sstables/aa/legacy_tables/legacy_aa_tuple/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a1a8ebd10d Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Data.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Data.db new file mode 100644 index 000000000000..3d65c28a8e39 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Digest.crc32 b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Digest.crc32 new file mode 100644 index 000000000000..703e9a110763 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1685416100 \ No newline at end of file diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Filter.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Partitions.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Rows.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Rows.db new file mode 100644 index 000000000000..88f2a3b55db8 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Statistics.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Statistics.db new file mode 100644 index 000000000000..bddde4d66453 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-TOC.txt b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-TOC.txt new file mode 100644 index 000000000000..de43ad25cf42 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust/ac-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Statistics.db +CompressionInfo.db +TOC.txt +Data.db +Partitions.db +Digest.crc32 +Rows.db +Filter.db diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..f90cafcfb320 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Data.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Data.db new file mode 100644 index 000000000000..d0438a81a233 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Digest.crc32 b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Digest.crc32 new file mode 100644 index 000000000000..dc2697987fc2 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3561445797 \ No newline at end of file diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Filter.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Partitions.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Rows.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Rows.db new file mode 100644 index 000000000000..1a324e57b52c Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Statistics.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Statistics.db new file mode 100644 index 000000000000..838d351e6ce2 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-TOC.txt b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-TOC.txt new file mode 100644 index 000000000000..de43ad25cf42 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_clust_counter/ac-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Statistics.db +CompressionInfo.db +TOC.txt +Data.db +Partitions.db +Digest.crc32 +Rows.db +Filter.db diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fc38a25eea5d Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Data.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Data.db new file mode 100644 index 000000000000..485ae9a782b5 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Digest.crc32 b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Digest.crc32 new file mode 100644 index 000000000000..773778b02738 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3030696842 \ No newline at end of file diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Filter.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Partitions.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Rows.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Statistics.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Statistics.db new file mode 100644 index 000000000000..17cd637bc236 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-TOC.txt b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-TOC.txt new file mode 100644 index 000000000000..de43ad25cf42 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple/ac-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Statistics.db +CompressionInfo.db +TOC.txt +Data.db +Partitions.db +Digest.crc32 +Rows.db +Filter.db diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..e2860e1eb16a Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Data.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Data.db new file mode 100644 index 000000000000..c95bc74083d1 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Digest.crc32 b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Digest.crc32 new file mode 100644 index 000000000000..f8c73f51b6b7 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1495453984 \ No newline at end of file diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Filter.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Partitions.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Rows.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Statistics.db b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Statistics.db new file mode 100644 index 000000000000..4bc9a659aa90 Binary files /dev/null and b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-TOC.txt b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-TOC.txt new file mode 100644 index 000000000000..de43ad25cf42 --- /dev/null +++ b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_simple_counter/ac-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Statistics.db +CompressionInfo.db +TOC.txt +Data.db +Partitions.db +Digest.crc32 +Rows.db +Filter.db diff --git a/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_tuple/.keep b/test/data/legacy-sstables/ac/legacy_tables/legacy_ac_tuple/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..a8a93a03c6e3 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Data.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Data.db new file mode 100644 index 000000000000..63bb37e15c9d Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Digest.crc32 b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Digest.crc32 new file mode 100644 index 000000000000..b00168f4ba45 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1723118615 \ No newline at end of file diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Filter.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Partitions.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Rows.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Rows.db new file mode 100644 index 000000000000..88f2a3b55db8 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Statistics.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Statistics.db new file mode 100644 index 000000000000..0662076e3897 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-TOC.txt b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-TOC.txt new file mode 100644 index 000000000000..c1b10099fd70 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust/ad-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Partitions.db +Rows.db +Data.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Statistics.db +Filter.db diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..aed58e0e5d50 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Data.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Data.db new file mode 100644 index 000000000000..2fe9d5e2ed34 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Digest.crc32 b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Digest.crc32 new file mode 100644 index 000000000000..3c2551889938 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2961106595 \ No newline at end of file diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Filter.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Partitions.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Rows.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Rows.db new file mode 100644 index 000000000000..2f8e2aefce5f Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Statistics.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Statistics.db new file mode 100644 index 000000000000..627ee8ecadb9 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-TOC.txt b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-TOC.txt new file mode 100644 index 000000000000..c1b10099fd70 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_clust_counter/ad-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Partitions.db +Rows.db +Data.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Statistics.db +Filter.db diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fc38a25eea5d Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Data.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Data.db new file mode 100644 index 000000000000..a2eff457f1c1 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Digest.crc32 b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Digest.crc32 new file mode 100644 index 000000000000..5dd842571884 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3089812609 \ No newline at end of file diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Filter.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Partitions.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Rows.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Statistics.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Statistics.db new file mode 100644 index 000000000000..50687c4f16e9 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-TOC.txt b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-TOC.txt new file mode 100644 index 000000000000..c1b10099fd70 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple/ad-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Partitions.db +Rows.db +Data.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Statistics.db +Filter.db diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-CompressionInfo.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..e2860e1eb16a Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Data.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Data.db new file mode 100644 index 000000000000..f2b7b5e0d297 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Digest.crc32 b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Digest.crc32 new file mode 100644 index 000000000000..a3dad7e92a66 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1039976897 \ No newline at end of file diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Filter.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Partitions.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Rows.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Statistics.db b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Statistics.db new file mode 100644 index 000000000000..474cedbc0b2f Binary files /dev/null and b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-TOC.txt b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-TOC.txt new file mode 100644 index 000000000000..c1b10099fd70 --- /dev/null +++ b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_simple_counter/ad-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Partitions.db +Rows.db +Data.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Statistics.db +Filter.db diff --git a/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_tuple/.keep b/test/data/legacy-sstables/ad/legacy_tables/legacy_ad_tuple/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-CompressionInfo.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-CompressionInfo.db new file mode 100644 index 000000000000..03761e45c847 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Data.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Data.db new file mode 100644 index 000000000000..7870960812a4 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Data.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Digest.crc32 b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Digest.crc32 new file mode 100644 index 000000000000..5e9e4cb24e7d --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Digest.crc32 @@ -0,0 +1 @@ +1160050405 \ No newline at end of file diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Filter.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Partitions.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Rows.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Rows.db new file mode 100644 index 000000000000..88f2a3b55db8 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Statistics.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Statistics.db new file mode 100644 index 000000000000..a35484d4ec15 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-TOC.txt b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust/ba-3gqh_1kh6_2g3ay2g29ohf9ileii-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-CompressionInfo.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-CompressionInfo.db new file mode 100644 index 000000000000..768b133abdb5 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Data.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Data.db new file mode 100644 index 000000000000..66dc30c582c5 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Data.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Digest.crc32 b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Digest.crc32 new file mode 100644 index 000000000000..1d976ff8d18e --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Digest.crc32 @@ -0,0 +1 @@ +4209240038 \ No newline at end of file diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Filter.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Partitions.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Rows.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Rows.db new file mode 100644 index 000000000000..c42c4df0ad4d Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Statistics.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Statistics.db new file mode 100644 index 000000000000..24404e35de1d Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-TOC.txt b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_clust_counter/ba-3gqh_1kh6_2c0p62g29ohf9ileii-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-CompressionInfo.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-CompressionInfo.db new file mode 100644 index 000000000000..ef683177e8f6 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Data.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Data.db new file mode 100644 index 000000000000..fe2140ac33eb Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Data.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Digest.crc32 b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Digest.crc32 new file mode 100644 index 000000000000..010c38fc92b3 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Digest.crc32 @@ -0,0 +1 @@ +3708273177 \ No newline at end of file diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Filter.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Partitions.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Rows.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Statistics.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Statistics.db new file mode 100644 index 000000000000..92b922f84252 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-TOC.txt b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple/ba-3gqh_1kh6_2klc02g29ohf9ileii-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-CompressionInfo.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-CompressionInfo.db new file mode 100644 index 000000000000..1db9aa06b311 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Data.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Data.db new file mode 100644 index 000000000000..762fc102f355 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Data.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Digest.crc32 b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Digest.crc32 new file mode 100644 index 000000000000..b70d8f7014a7 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Digest.crc32 @@ -0,0 +1 @@ +356790932 \ No newline at end of file diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Filter.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Partitions.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Rows.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Statistics.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Statistics.db new file mode 100644 index 000000000000..16413471a4af Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-TOC.txt b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_simple_counter/ba-3gqh_1kh6_26vi82g29ohf9ileii-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/.keep b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-CompressionInfo.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-CompressionInfo.db new file mode 100644 index 000000000000..f69c841bc292 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Data.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Data.db new file mode 100644 index 000000000000..d28f306df534 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Data.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Digest.crc32 b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Digest.crc32 new file mode 100644 index 000000000000..b7433aaa4916 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Digest.crc32 @@ -0,0 +1 @@ +3554805671 \ No newline at end of file diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Filter.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Partitions.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Partitions.db new file mode 100644 index 000000000000..61ce86abe5a2 Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Rows.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Statistics.db b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Statistics.db new file mode 100644 index 000000000000..51008d4c24cd Binary files /dev/null and b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-TOC.txt b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ba/legacy_tables/legacy_ba_tuple/ba-3gqh_1kh6_2og8a2g29ohf9ileii-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-CompressionInfo.db new file mode 100644 index 000000000000..5e2350b852d3 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Data.db new file mode 100644 index 000000000000..de3f040df243 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Digest.crc32 new file mode 100644 index 000000000000..28195f1f2a45 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Digest.crc32 @@ -0,0 +1 @@ +916950919 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Rows.db new file mode 100644 index 000000000000..a5dc1671b8c6 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Rows.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Statistics.db new file mode 100644 index 000000000000..47e51c71760c Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust/bb-3gqh_1kae_0ffk02gr7690uktnjo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-CompressionInfo.db new file mode 100644 index 000000000000..20cdb8597303 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Data.db new file mode 100644 index 000000000000..ad8b43636b07 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Digest.crc32 new file mode 100644 index 000000000000..dfa6bdefd459 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Digest.crc32 @@ -0,0 +1 @@ +2500336870 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Rows.db new file mode 100644 index 000000000000..02fca86639dc Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Rows.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Statistics.db new file mode 100644 index 000000000000..196139a0585e Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_clust_counter/bb-3gqh_1kae_0ksgq2gr7690uktnjo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-CompressionInfo.db new file mode 100644 index 000000000000..ef683177e8f6 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Data.db new file mode 100644 index 000000000000..db1d474fd986 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Digest.crc32 new file mode 100644 index 000000000000..6210e81cb492 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Digest.crc32 @@ -0,0 +1 @@ +2998190093 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Statistics.db new file mode 100644 index 000000000000..6862f0531745 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple/bb-3gqh_1kae_0tsj42gr7690uktnjo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-CompressionInfo.db new file mode 100644 index 000000000000..1db9aa06b311 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Data.db new file mode 100644 index 000000000000..2d5421cc4aa4 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Digest.crc32 new file mode 100644 index 000000000000..c9f7973aaff7 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Digest.crc32 @@ -0,0 +1 @@ +657349080 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Statistics.db new file mode 100644 index 000000000000..97c4b0e39192 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_simple_counter/bb-3gqh_1kae_0xv562gr7690uktnjo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..30032a93402e Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Data.db new file mode 100644 index 000000000000..0cef0112752b Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..c8809d0fd5a9 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1407325515 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..c45ee70c8514 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..88301c7734d1 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..e8e21f06096e --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +TOC.txt +Statistics.db +Data.db +Partitions.db +Rows.db +Filter.db +Digest.crc32 +CompressionInfo.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-CompressionInfo.db new file mode 100644 index 000000000000..f69c841bc292 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Data.db new file mode 100644 index 000000000000..c6b01439f299 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Digest.crc32 new file mode 100644 index 000000000000..dd6dd18b0d3a --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Digest.crc32 @@ -0,0 +1 @@ +3607418605 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Partitions.db new file mode 100644 index 000000000000..61ce86abe5a2 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Statistics.db new file mode 100644 index 000000000000..360a3c9dffde Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_bb_tuple/bb-3gqh_1kae_0ppxm2gr7690uktnjo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..1d46ea1f34fc Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Data.db new file mode 100644 index 000000000000..9abeaf6bace9 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..0bf54b479e72 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3216856663 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..7da287b2fec3 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..5e9d24a1985f Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..489a6b3dda32 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Rows.db +TOC.txt +Partitions.db +Filter.db +Statistics.db diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-CompressionInfo.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa343602e81a Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Data.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Data.db new file mode 100644 index 000000000000..5bdeaa001c0d Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Data.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Digest.crc32 b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..205411074bb0 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3503210033 \ No newline at end of file diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Filter.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Filter.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Partitions.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..cfabfbeb04a4 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Rows.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e0ca02d49d05 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Rows.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Statistics.db b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..f067a5643362 Binary files /dev/null and b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-TOC.txt b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..489a6b3dda32 --- /dev/null +++ b/test/data/legacy-sstables/bb/legacy_tables/legacy_encrypted_table_pk_ck/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Rows.db +TOC.txt +Partitions.db +Filter.db +Statistics.db diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-CompressionInfo.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-CompressionInfo.db new file mode 100644 index 000000000000..f30864db15b0 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Data.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Data.db new file mode 100644 index 000000000000..a419b87f0781 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Data.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Digest.crc32 b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Digest.crc32 new file mode 100644 index 000000000000..c15c0e649448 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Digest.crc32 @@ -0,0 +1 @@ +1209336060 \ No newline at end of file diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Filter.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Partitions.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Rows.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Rows.db new file mode 100644 index 000000000000..ee0b0889b461 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Statistics.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Statistics.db new file mode 100644 index 000000000000..bc5353ea6806 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-TOC.txt b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust/ca-3gqh_1je9_0pai221tb9if6g995s-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-CompressionInfo.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-CompressionInfo.db new file mode 100644 index 000000000000..d12548dce508 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Data.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Data.db new file mode 100644 index 000000000000..754e4fa48c2f Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Data.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Digest.crc32 b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Digest.crc32 new file mode 100644 index 000000000000..1d420d3aea2e --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Digest.crc32 @@ -0,0 +1 @@ +2034473607 \ No newline at end of file diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Filter.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Partitions.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Rows.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Rows.db new file mode 100644 index 000000000000..1a324e57b52c Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Rows.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Statistics.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Statistics.db new file mode 100644 index 000000000000..dab132fdd433 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-TOC.txt b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_clust_counter/ca-3gqh_1je9_0td3u21tb9if6g995s-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-CompressionInfo.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-CompressionInfo.db new file mode 100644 index 000000000000..ef683177e8f6 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Data.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Data.db new file mode 100644 index 000000000000..ca0b63504201 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Data.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Digest.crc32 b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Digest.crc32 new file mode 100644 index 000000000000..3719b30b7c32 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Digest.crc32 @@ -0,0 +1 @@ +2338175197 \ No newline at end of file diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Filter.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Partitions.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Rows.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Statistics.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Statistics.db new file mode 100644 index 000000000000..5a03b76263ba Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-TOC.txt b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple/ca-3gqh_1je9_0gaf421tb9if6g995s-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-CompressionInfo.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-CompressionInfo.db new file mode 100644 index 000000000000..1db9aa06b311 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Data.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Data.db new file mode 100644 index 000000000000..12feb684bcc9 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Data.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Digest.crc32 b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Digest.crc32 new file mode 100644 index 000000000000..1e8ee3feebd7 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Digest.crc32 @@ -0,0 +1 @@ +417849817 \ No newline at end of file diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Filter.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Partitions.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Rows.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Statistics.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Statistics.db new file mode 100644 index 000000000000..076cb2528a3d Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-TOC.txt b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_simple_counter/ca-3gqh_1je9_0ksgq21tb9if6g995s-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/.keep b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-CompressionInfo.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-CompressionInfo.db new file mode 100644 index 000000000000..f69c841bc292 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Data.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Data.db new file mode 100644 index 000000000000..7d76017b80e3 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Data.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Digest.crc32 b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Digest.crc32 new file mode 100644 index 000000000000..b09f443978fd --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Digest.crc32 @@ -0,0 +1 @@ +1933380653 \ No newline at end of file diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Filter.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Filter.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Partitions.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Partitions.db new file mode 100644 index 000000000000..61ce86abe5a2 Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Rows.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Statistics.db b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Statistics.db new file mode 100644 index 000000000000..bcad1b0422fe Binary files /dev/null and b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-TOC.txt b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/ca/legacy_tables/legacy_ca_tuple/ca-3gqh_1je9_0xnfe21tb9if6g995s-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-CompressionInfo.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-CompressionInfo.db new file mode 100644 index 000000000000..341b2ec6350c Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Data.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Data.db new file mode 100644 index 000000000000..4aa4481f060f Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Data.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Digest.crc32 b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Digest.crc32 new file mode 100644 index 000000000000..537419e3d523 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Digest.crc32 @@ -0,0 +1 @@ +4134286179 \ No newline at end of file diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Filter.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Partitions.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Rows.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Rows.db new file mode 100644 index 000000000000..bfca83b9cfba Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Rows.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Statistics.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Statistics.db new file mode 100644 index 000000000000..ac58810a2b42 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-TOC.txt b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust/cb-3gqh_1ii9_36o342jkpkbxpuwnlo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-CompressionInfo.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-CompressionInfo.db new file mode 100644 index 000000000000..556cf54d5cd3 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Data.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Data.db new file mode 100644 index 000000000000..3cd61c5b131b Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Data.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Digest.crc32 b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Digest.crc32 new file mode 100644 index 000000000000..1fbfec20f221 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Digest.crc32 @@ -0,0 +1 @@ +869675051 \ No newline at end of file diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Filter.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Partitions.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Rows.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Rows.db new file mode 100644 index 000000000000..c42c4df0ad4d Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Rows.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Statistics.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Statistics.db new file mode 100644 index 000000000000..3ff2cee647d6 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-TOC.txt b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_clust_counter/cb-3gqh_1ii9_3ab9m2jkpkbxpuwnlo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-CompressionInfo.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-CompressionInfo.db new file mode 100644 index 000000000000..ef683177e8f6 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Data.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Data.db new file mode 100644 index 000000000000..798976872958 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Data.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Digest.crc32 b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Digest.crc32 new file mode 100644 index 000000000000..2370b564858b --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Digest.crc32 @@ -0,0 +1 @@ +130349283 \ No newline at end of file diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Filter.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Partitions.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Rows.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Statistics.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Statistics.db new file mode 100644 index 000000000000..08163814ac66 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-TOC.txt b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple/cb-3gqh_1ii9_3e65m2jkpkbxpuwnlo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-CompressionInfo.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-CompressionInfo.db new file mode 100644 index 000000000000..1db9aa06b311 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Data.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Data.db new file mode 100644 index 000000000000..d852ff256f44 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Data.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Digest.crc32 b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Digest.crc32 new file mode 100644 index 000000000000..60919d9c904c --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Digest.crc32 @@ -0,0 +1 @@ +4158893718 \ No newline at end of file diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Filter.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Partitions.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Rows.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Statistics.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Statistics.db new file mode 100644 index 000000000000..dcb775d90f18 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-TOC.txt b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_simple_counter/cb-3gqh_1ii9_32t7e2jkpkbxpuwnlo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/.keep b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-CompressionInfo.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-CompressionInfo.db new file mode 100644 index 000000000000..f69c841bc292 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Data.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Data.db new file mode 100644 index 000000000000..36863af050e5 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Data.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Digest.crc32 b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Digest.crc32 new file mode 100644 index 000000000000..8906cfea9387 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Digest.crc32 @@ -0,0 +1 @@ +4132661928 \ No newline at end of file diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Filter.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Partitions.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Partitions.db new file mode 100644 index 000000000000..61ce86abe5a2 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Rows.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Statistics.db b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Statistics.db new file mode 100644 index 000000000000..949d27d35555 Binary files /dev/null and b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-TOC.txt b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cb/legacy_tables/legacy_cb_tuple/cb-3gqh_1ii9_2yqlc2jkpkbxpuwnlo-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-CompressionInfo.db new file mode 100644 index 000000000000..1dc65d51699b Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Data.db new file mode 100644 index 000000000000..e6b7781a566a Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Digest.crc32 new file mode 100644 index 000000000000..9ac09485ccd8 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Digest.crc32 @@ -0,0 +1 @@ +3826451487 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Rows.db new file mode 100644 index 000000000000..29d05a1dfd7c Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Rows.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Statistics.db new file mode 100644 index 000000000000..dcf3a783b55c Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust/cc-3gqh_1gkm_5lh3e2tq8flafy8w4g-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-CompressionInfo.db new file mode 100644 index 000000000000..da46fe4815bc Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Data.db new file mode 100644 index 000000000000..59b4e6d5dcaa Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Digest.crc32 new file mode 100644 index 000000000000..4d54a4075a58 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Digest.crc32 @@ -0,0 +1 @@ +1096157276 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Rows.db new file mode 100644 index 000000000000..88f2a3b55db8 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Rows.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Statistics.db new file mode 100644 index 000000000000..7cbf2d533cfc Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-TOC.txt new file mode 100644 index 000000000000..5bfa06ac544e --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_be_index_summary/cc-3gy6_1a04_04i1c2g30zafr3x0fh-bti-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Partitions.db +Rows.db +Statistics.db +TOC.txt diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-CompressionInfo.db new file mode 100644 index 000000000000..244a60308b64 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Data.db new file mode 100644 index 000000000000..ad2c0c851dff Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Digest.crc32 new file mode 100644 index 000000000000..2b89370c13b5 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Digest.crc32 @@ -0,0 +1 @@ +886405145 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Partitions.db new file mode 100644 index 000000000000..daf1b01ec12e Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Rows.db new file mode 100644 index 000000000000..aab6a54a1f00 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Rows.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Statistics.db new file mode 100644 index 000000000000..c9ebde49fcd2 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_clust_counter/cc-3gqh_1gkm_56gyo2tq8flafy8w4g-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-CompressionInfo.db new file mode 100644 index 000000000000..ef683177e8f6 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Data.db new file mode 100644 index 000000000000..264da4dce74e Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Digest.crc32 new file mode 100644 index 000000000000..ace636184fe8 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Digest.crc32 @@ -0,0 +1 @@ +1754994348 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Partitions.db new file mode 100644 index 000000000000..e20b4e2f2700 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Statistics.db new file mode 100644 index 000000000000..05be491d4a00 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple/cc-3gqh_1gkm_5grc02tq8flafy8w4g-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-CompressionInfo.db new file mode 100644 index 000000000000..1db9aa06b311 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Data.db new file mode 100644 index 000000000000..f842c5fabfcb Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Digest.crc32 new file mode 100644 index 000000000000..6a339f2c8649 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Digest.crc32 @@ -0,0 +1 @@ +3885526892 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Partitions.db new file mode 100644 index 000000000000..773d3c8891c3 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Statistics.db new file mode 100644 index 000000000000..3b096ca6c085 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_simple_counter/cc-3gqh_1gkm_5c9ao2tq8flafy8w4g-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-CompressionInfo.db new file mode 100644 index 000000000000..361f57d7d4df Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Data.db new file mode 100644 index 000000000000..3dab4da4c35e Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Digest.crc32 new file mode 100644 index 000000000000..28d4e5637fa0 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Digest.crc32 @@ -0,0 +1 @@ +645653999 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Partitions.db new file mode 100644 index 000000000000..c45ee70c8514 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Statistics.db new file mode 100644 index 000000000000..67361764be40 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-TOC.txt new file mode 100644 index 000000000000..a42c653d37ac --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3glk_0z9v_2f0ps1zlpy7dco1gdh-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Statistics.db +TOC.txt +Rows.db +Digest.crc32 +Partitions.db +Data.db +CompressionInfo.db diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-CompressionInfo.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-CompressionInfo.db new file mode 100644 index 000000000000..f69c841bc292 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Data.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Data.db new file mode 100644 index 000000000000..1ef6092e659d Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Data.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Digest.crc32 b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Digest.crc32 new file mode 100644 index 000000000000..8fbe872f2d24 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Digest.crc32 @@ -0,0 +1 @@ +1855643858 \ No newline at end of file diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Filter.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Filter.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Partitions.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Partitions.db new file mode 100644 index 000000000000..61ce86abe5a2 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Partitions.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Rows.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Statistics.db b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Statistics.db new file mode 100644 index 000000000000..c13f4ffe01a7 Binary files /dev/null and b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-Statistics.db differ diff --git a/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-TOC.txt b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/legacy-sstables/cc/legacy_tables/legacy_cc_tuple/cc-3gqh_1gkm_5rh4w2tq8flafy8w4g-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/legacy-sstables/md/legacy_tables/legacy_md_tuple/md-31110-big-TOC.txt b/test/data/legacy-sstables/md/legacy_tables/legacy_md_tuple/md-31110-big-TOC.txt index f2df26ca5694..378c367b40d8 100644 --- a/test/data/legacy-sstables/md/legacy_tables/legacy_md_tuple/md-31110-big-TOC.txt +++ b/test/data/legacy-sstables/md/legacy_tables/legacy_md_tuple/md-31110-big-TOC.txt @@ -1,8 +1,9 @@ -TOC.txt +Filter.db Data.db -Statistics.db Summary.db +TOC.txt Index.db -Filter.db +Statistics.db +Digest.crc32 CompressionInfo.db Digest.crc32 diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-CompressionInfo.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-CompressionInfo.db new file mode 100644 index 000000000000..53c889d0e457 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Data.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Data.db new file mode 100644 index 000000000000..549eff9a75e6 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Data.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Digest.crc32 b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Digest.crc32 new file mode 100644 index 000000000000..8c20e643cb2c --- /dev/null +++ b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Digest.crc32 @@ -0,0 +1 @@ +1581868109 \ No newline at end of file diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Filter.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Filter.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Index.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Index.db new file mode 100644 index 000000000000..3e735baf9ae0 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Index.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Statistics.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Statistics.db new file mode 100644 index 000000000000..bf3fbcb29be3 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Statistics.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Summary.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Summary.db new file mode 100644 index 000000000000..9b24e0450c73 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-Summary.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-TOC.txt b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-TOC.txt new file mode 100644 index 000000000000..2c313733c508 --- /dev/null +++ b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple/me-3025-big-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Index.db +TOC.txt +Filter.db +CompressionInfo.db +Digest.crc32 +Summary.db +Statistics.db diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-CompressionInfo.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-CompressionInfo.db new file mode 100644 index 000000000000..53c889d0e457 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Data.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Data.db new file mode 100644 index 000000000000..f1c701638c08 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Data.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Digest.crc32 b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Digest.crc32 new file mode 100644 index 000000000000..04dc1cbfeb51 --- /dev/null +++ b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Digest.crc32 @@ -0,0 +1 @@ +2218709547 \ No newline at end of file diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Filter.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Filter.db new file mode 100644 index 000000000000..2e1d5d29ca06 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Filter.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Index.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Index.db new file mode 100644 index 000000000000..3e735baf9ae0 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Index.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Statistics.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Statistics.db new file mode 100644 index 000000000000..710471b1763c Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Statistics.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Summary.db b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Summary.db new file mode 100644 index 000000000000..9b24e0450c73 Binary files /dev/null and b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-Summary.db differ diff --git a/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-TOC.txt b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-TOC.txt new file mode 100644 index 000000000000..2c313733c508 --- /dev/null +++ b/test/data/legacy-sstables/me/legacy_tables/legacy_me_tuple_compact/me-3025-big-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Index.db +TOC.txt +Filter.db +CompressionInfo.db +Digest.crc32 +Summary.db +Statistics.db diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-CompressionInfo.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-CompressionInfo.db index 8fad34fe9e11..ab804238abcb 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-CompressionInfo.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Data.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Data.db index ae35335fbf9a..4cddfdb98d08 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Data.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Data.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Digest.crc32 b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Digest.crc32 index 8a92f3c58325..83138ff5a6b7 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Digest.crc32 +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Digest.crc32 @@ -1 +1 @@ -2977407251 \ No newline at end of file +3857770523 \ No newline at end of file diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Index.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Index.db index d50fdeb4e209..aeeff9304559 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Index.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Index.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Statistics.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Statistics.db index 734186497e79..c9506ee78c2c 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Statistics.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-Statistics.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-TOC.txt b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-TOC.txt index b03b28372b5d..8a6a30b6db77 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-TOC.txt +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust/na-400-big-TOC.txt @@ -1,8 +1,8 @@ -Filter.db -Digest.crc32 -Index.db -TOC.txt Summary.db -Statistics.db -CompressionInfo.db Data.db +TOC.txt +CompressionInfo.db +Statistics.db +Digest.crc32 +Index.db +Filter.db diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-CompressionInfo.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-CompressionInfo.db index f0a1cfb59e84..d9592e6c848b 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-CompressionInfo.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Data.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Data.db index b487fe88edf6..e21851ee0823 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Data.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Data.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Digest.crc32 b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Digest.crc32 index ca286e0954b9..a85dfe8c5274 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Digest.crc32 +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Digest.crc32 @@ -1 +1 @@ -2759187708 \ No newline at end of file +2266872816 \ No newline at end of file diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Index.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Index.db index c981a226039e..0e8dc66e9f2d 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Index.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Index.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Statistics.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Statistics.db index 33fccc9c84bd..a14bd80fa4a1 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Statistics.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-Statistics.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-TOC.txt b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-TOC.txt index b03b28372b5d..8a6a30b6db77 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-TOC.txt +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_clust_counter/na-400-big-TOC.txt @@ -1,8 +1,8 @@ -Filter.db -Digest.crc32 -Index.db -TOC.txt Summary.db -Statistics.db -CompressionInfo.db Data.db +TOC.txt +CompressionInfo.db +Statistics.db +Digest.crc32 +Index.db +Filter.db diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-CompressionInfo.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-CompressionInfo.db index fc38a25eea5d..ef683177e8f6 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-CompressionInfo.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Data.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Data.db index 11219d037a8d..d0ce6c3af9bd 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Data.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Data.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Digest.crc32 b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Digest.crc32 index 985d6dcf36d6..2e84cd9a71a0 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Digest.crc32 +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Digest.crc32 @@ -1 +1 @@ -462858821 \ No newline at end of file +739757235 \ No newline at end of file diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Statistics.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Statistics.db index 3c68ac568f56..daf1c16c63ee 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Statistics.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-Statistics.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-TOC.txt b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-TOC.txt index b03b28372b5d..8a6a30b6db77 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-TOC.txt +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple/na-400-big-TOC.txt @@ -1,8 +1,8 @@ -Filter.db -Digest.crc32 -Index.db -TOC.txt Summary.db -Statistics.db -CompressionInfo.db Data.db +TOC.txt +CompressionInfo.db +Statistics.db +Digest.crc32 +Index.db +Filter.db diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-CompressionInfo.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-CompressionInfo.db index e2860e1eb16a..1db9aa06b311 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-CompressionInfo.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Data.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Data.db index 620cdf260e5a..11c0a684f10e 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Data.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Data.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Digest.crc32 b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Digest.crc32 index bc5f671e0191..b905530e1d8c 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Digest.crc32 +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Digest.crc32 @@ -1 +1 @@ -3987542254 \ No newline at end of file +3918697890 \ No newline at end of file diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Statistics.db b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Statistics.db index 689bec8f1a85..c803970afe4c 100644 Binary files a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Statistics.db and b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-Statistics.db differ diff --git a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-TOC.txt b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-TOC.txt index b03b28372b5d..8a6a30b6db77 100644 --- a/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-TOC.txt +++ b/test/data/legacy-sstables/na/legacy_tables/legacy_na_simple_counter/na-400-big-TOC.txt @@ -1,8 +1,8 @@ -Filter.db -Digest.crc32 -Index.db -TOC.txt Summary.db -Statistics.db -CompressionInfo.db Data.db +TOC.txt +CompressionInfo.db +Statistics.db +Digest.crc32 +Index.db +Filter.db diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-CompressionInfo.db b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-CompressionInfo.db new file mode 100644 index 000000000000..ef683177e8f6 Binary files /dev/null and b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-CompressionInfo.db differ diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Data.db b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Data.db new file mode 100644 index 000000000000..fe53589beb7c Binary files /dev/null and b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Data.db differ diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Digest.crc32 b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Digest.crc32 new file mode 100644 index 000000000000..67f6298a97ca --- /dev/null +++ b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Digest.crc32 @@ -0,0 +1 @@ +1155625239 \ No newline at end of file diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Filter.db b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Filter.db new file mode 100644 index 000000000000..8868e5c18008 Binary files /dev/null and b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Filter.db differ diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Index.db b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Index.db new file mode 100644 index 000000000000..b3094bffbad9 Binary files /dev/null and b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Index.db differ diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Statistics.db b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Statistics.db new file mode 100644 index 000000000000..f9940d0cf819 Binary files /dev/null and b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Statistics.db differ diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Summary.db b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Summary.db new file mode 100644 index 000000000000..9b24e0450c73 Binary files /dev/null and b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-Summary.db differ diff --git a/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-TOC.txt b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-TOC.txt new file mode 100644 index 000000000000..6ea912e92c15 --- /dev/null +++ b/test/data/legacy-sstables/nb/legacy_tables/legacy_nb_simple/nc-1-big-TOC.txt @@ -0,0 +1,8 @@ +TOC.txt +Data.db +Statistics.db +Summary.db +Filter.db +Digest.crc32 +Index.db +CompressionInfo.db diff --git a/test/data/serialization/DSE_68/batch.bin b/test/data/serialization/DSE_68/batch.bin new file mode 100644 index 000000000000..1e82e37f1674 Binary files /dev/null and b/test/data/serialization/DSE_68/batch.bin differ diff --git a/test/data/types-compatibility/cc-4.0.json.gz b/test/data/types-compatibility/cc-4.0.json.gz new file mode 100644 index 000000000000..24778abf7576 Binary files /dev/null and b/test/data/types-compatibility/cc-4.0.json.gz differ diff --git a/test/data/types-compatibility/cc-5.0.json.gz b/test/data/types-compatibility/cc-5.0.json.gz new file mode 100644 index 000000000000..1c765e1fe0e7 Binary files /dev/null and b/test/data/types-compatibility/cc-5.0.json.gz differ diff --git a/test/data/types-compatibility/dse-6.8-cndb.json.gz b/test/data/types-compatibility/dse-6.8-cndb.json.gz new file mode 100644 index 000000000000..a48dae61a300 Binary files /dev/null and b/test/data/types-compatibility/dse-6.8-cndb.json.gz differ diff --git a/test/data/types-compatibility/legacy-cc-4.0.json.gz b/test/data/types-compatibility/legacy-cc-4.0.json.gz new file mode 100644 index 000000000000..f803e415cf72 Binary files /dev/null and b/test/data/types-compatibility/legacy-cc-4.0.json.gz differ diff --git a/test/data/udt/c40/commitlog/CommitLog-7-1770660370463.log b/test/data/udt/c40/commitlog/CommitLog-7-1770660370463.log new file mode 100644 index 000000000000..8c12906cffd4 Binary files /dev/null and b/test/data/udt/c40/commitlog/CommitLog-7-1770660370463.log differ diff --git a/test/data/udt/c40/data.json b/test/data/udt/c40/data.json new file mode 100644 index 000000000000..8227d25eed37 --- /dev/null +++ b/test/data/udt/c40/data.json @@ -0,0 +1 @@ +{"tab5_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab8_frozen_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab6_frozen_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab2_frozen_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab10_frozen_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab4_frozen_udt2":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab7_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]]} \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db new file mode 100644 index 000000000000..8f606d35da0f Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..efa341431ed8 --- /dev/null +++ b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +2020853061 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db new file mode 100644 index 000000000000..91f35b65c5a2 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db new file mode 100644 index 000000000000..596ad6d48fd1 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..edca9c171fe5 --- /dev/null +++ b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +3195883603 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db new file mode 100644 index 000000000000..83f32df82281 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..9223a08faf38 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db new file mode 100644 index 000000000000..a2deeb6c2116 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..6160ffe7a225 --- /dev/null +++ b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +3528028441 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db new file mode 100644 index 000000000000..182c80feefc2 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db new file mode 100644 index 000000000000..fa6c08e4508d Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db new file mode 100644 index 000000000000..6278c8e4946b Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..245f5c76089f --- /dev/null +++ b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +1064711791 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db new file mode 100644 index 000000000000..ba1dbf3b73ca Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db new file mode 100644 index 000000000000..41c90cb30007 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..4179bd9a46d2 --- /dev/null +++ b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +2039766478 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db new file mode 100644 index 000000000000..95c6d3ffc8c7 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db new file mode 100644 index 000000000000..2cb3d000348a Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..41a175ad4856 --- /dev/null +++ b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1849523333 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db new file mode 100644 index 000000000000..b794fc9c6f09 Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db new file mode 100644 index 000000000000..fdb9eba73470 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..69d2af4c6fcc --- /dev/null +++ b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +1488631779 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db new file mode 100644 index 000000000000..5e00ae299c4f Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db new file mode 100644 index 000000000000..831a6b172c88 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..ec83b10aa8fb --- /dev/null +++ b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +438355547 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db new file mode 100644 index 000000000000..03b9717a7447 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..9223a08faf38 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db new file mode 100644 index 000000000000..251a105fe942 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..d4a2f90b9d83 --- /dev/null +++ b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1946281461 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db new file mode 100644 index 000000000000..182c80feefc2 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db new file mode 100644 index 000000000000..f96fa8fafc87 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..1ab8a9f5ad72 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db new file mode 100644 index 000000000000..67d20fffd0db Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..540899d47218 --- /dev/null +++ b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +521722014 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db new file mode 100644 index 000000000000..34a25604107f Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db new file mode 100644 index 000000000000..24c08de2809e Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db new file mode 100644 index 000000000000..56eccdb14de8 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..a833519533a6 --- /dev/null +++ b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +3926969847 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db new file mode 100644 index 000000000000..c8f4eb34d2d5 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db new file mode 100644 index 000000000000..c735dab6fe45 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..79a67f097ba4 --- /dev/null +++ b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +2830451563 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db new file mode 100644 index 000000000000..e89a5e84b22e Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..1ab8a9f5ad72 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db new file mode 100644 index 000000000000..f145d562e2f5 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..e397287e3df8 --- /dev/null +++ b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +2533234992 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db new file mode 100644 index 000000000000..34a25604107f Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db new file mode 100644 index 000000000000..b1cc75fee2e3 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db new file mode 100644 index 000000000000..6047be97c824 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..af8f757b7700 --- /dev/null +++ b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +862109477 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db new file mode 100644 index 000000000000..f417d693a8b3 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db new file mode 100644 index 000000000000..d5cbd8cadb24 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..7663bf67427d --- /dev/null +++ b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +3090328874 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db new file mode 100644 index 000000000000..36d9af5f1c85 Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db new file mode 100644 index 000000000000..c9caf547e36c Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..5c08ecda119d --- /dev/null +++ b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3528809888 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db new file mode 100644 index 000000000000..1ebec8cecbd2 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db new file mode 100644 index 000000000000..6d4299116a7c Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..ca93b2a02bc9 --- /dev/null +++ b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +2448547989 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db new file mode 100644 index 000000000000..f6b561df6987 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..9223a08faf38 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db new file mode 100644 index 000000000000..19ad2be73c65 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..4b6e09d92c23 --- /dev/null +++ b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +3370102142 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db new file mode 100644 index 000000000000..182c80feefc2 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db new file mode 100644 index 000000000000..0c4f92621bbb Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db new file mode 100644 index 000000000000..d7accaa826f4 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..e4268c97a2b5 --- /dev/null +++ b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +226662000 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db new file mode 100644 index 000000000000..1324628966c0 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db new file mode 100644 index 000000000000..a922c870a97a Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..f1ab79efe791 --- /dev/null +++ b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +1856925334 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db new file mode 100644 index 000000000000..1e961065a4c7 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..9223a08faf38 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db new file mode 100644 index 000000000000..25cdd4264f2a Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..31351fb9ebbd --- /dev/null +++ b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1092685179 \ No newline at end of file diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db new file mode 100644 index 000000000000..182c80feefc2 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db new file mode 100644 index 000000000000..8c9a381ed7c7 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db differ diff --git a/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt new file mode 100644 index 000000000000..515e3e612461 --- /dev/null +++ b/test/data/udt/c40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +TOC.txt +CompressionInfo.db +Digest.crc32 +Index.db +Statistics.db +Summary.db +Data.db diff --git a/test/data/udt/c40/schema.txt b/test/data/udt/c40/schema.txt new file mode 100644 index 000000000000..289a5be69977 --- /dev/null +++ b/test/data/udt/c40/schema.txt @@ -0,0 +1,64 @@ +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +ALTER TABLE ks.tab5_tuple DROP b_complex USING TIMESTAMP 1770660372518000; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +ALTER TABLE ks.tab8_frozen_tuple_with_udt DROP b_complex USING TIMESTAMP 1770660372589000; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +ALTER TABLE ks.tab6_frozen_tuple DROP b_complex USING TIMESTAMP 1770660372542002; +CREATE TYPE ks.udt3 ( + foo int, + bar frozen>, + baz int +); +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +ALTER TABLE ks.tab2_frozen_udt1 DROP b_complex USING TIMESTAMP 1770660372460000; +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +ALTER TABLE ks.tab10_frozen_udt_with_tuple DROP b_complex USING TIMESTAMP 1770660372609002; +CREATE TYPE ks.udt2 ( + foo int, + bar frozen, + baz int +); +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +ALTER TABLE ks.tab4_frozen_udt2 DROP b_complex USING TIMESTAMP 1770660372491001; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5; +ALTER TABLE ks.tab7_tuple_with_udt DROP b_complex USING TIMESTAMP 1770660372566002; \ No newline at end of file diff --git a/test/data/udt/c40/schema0.txt b/test/data/udt/c40/schema0.txt new file mode 100644 index 000000000000..3e1d5a498df5 --- /dev/null +++ b/test/data/udt/c40/schema0.txt @@ -0,0 +1,57 @@ +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen>, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, int>>, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen>, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +CREATE TYPE ks.udt3 ( + foo int, + bar frozen>, + baz int +); +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +CREATE TYPE ks.udt2 ( + foo int, + bar frozen, + baz int +); +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, int>>, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5 \ No newline at end of file diff --git a/test/data/udt/c41/commitlog/CommitLog-7-1770661718587.log b/test/data/udt/c41/commitlog/CommitLog-7-1770661718587.log new file mode 100644 index 000000000000..aa721e840e2d Binary files /dev/null and b/test/data/udt/c41/commitlog/CommitLog-7-1770661718587.log differ diff --git a/test/data/udt/c41/data.json b/test/data/udt/c41/data.json new file mode 100644 index 000000000000..8227d25eed37 --- /dev/null +++ b/test/data/udt/c41/data.json @@ -0,0 +1 @@ +{"tab5_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab8_frozen_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab6_frozen_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab2_frozen_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab10_frozen_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab4_frozen_udt2":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab7_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]]} \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db new file mode 100644 index 000000000000..ddebe5d6f37c Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..4ce443d22dc8 --- /dev/null +++ b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +421974468 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db new file mode 100644 index 000000000000..94d2a71fbe54 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db new file mode 100644 index 000000000000..78516e8c7324 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..ad8fb26ccdb5 --- /dev/null +++ b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +116308636 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db new file mode 100644 index 000000000000..af23d7817f7b Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db new file mode 100644 index 000000000000..4ad47e40e88f Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..9cf42a062822 --- /dev/null +++ b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +4028230874 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db new file mode 100644 index 000000000000..5cfba08b70e2 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db new file mode 100644 index 000000000000..70a2f684a313 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..ebba7c80bc9d --- /dev/null +++ b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3580019992 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db new file mode 100644 index 000000000000..994f1b4dd27f Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..1ecdcfaf8ae6 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db new file mode 100644 index 000000000000..84e22fcd7d2e Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..6d0f367c4708 --- /dev/null +++ b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +997315134 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db new file mode 100644 index 000000000000..a33aa3a447fc Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db new file mode 100644 index 000000000000..f469dcf75fa7 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db new file mode 100644 index 000000000000..abfef63ed8da Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..bd9e7a821b90 --- /dev/null +++ b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1597665516 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db new file mode 100644 index 000000000000..f55be5c279b9 Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db new file mode 100644 index 000000000000..08da6d2fd9ff Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..62b9d4fb525f --- /dev/null +++ b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +1553891375 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db new file mode 100644 index 000000000000..10a8120eb7e6 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db new file mode 100644 index 000000000000..bd71872cb051 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..9a3c1bfc9baf --- /dev/null +++ b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +3786129270 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db new file mode 100644 index 000000000000..1eb71a3cc385 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db new file mode 100644 index 000000000000..b85e96d5700f Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..48fcd13fd1e8 --- /dev/null +++ b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1213984856 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db new file mode 100644 index 000000000000..225fecdf4dfb Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db new file mode 100644 index 000000000000..c42b3084c24b Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..56bdcd7b8351 --- /dev/null +++ b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +2290443645 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db new file mode 100644 index 000000000000..01a816502a68 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db new file mode 100644 index 000000000000..b455d1729061 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..f3f35137b0f4 --- /dev/null +++ b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +3304451337 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db new file mode 100644 index 000000000000..9c90533022c8 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db new file mode 100644 index 000000000000..f4a4b139b9b8 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..7f838e1bd7f9 --- /dev/null +++ b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1574787032 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db new file mode 100644 index 000000000000..545f1cbe6c1a Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db new file mode 100644 index 000000000000..8d627800b514 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..2d244da32ba5 --- /dev/null +++ b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3701809397 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db new file mode 100644 index 000000000000..922f205cbbf9 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db new file mode 100644 index 000000000000..b54268daed8e Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..cbca81958a97 --- /dev/null +++ b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +1585933714 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db new file mode 100644 index 000000000000..970239cf1973 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db new file mode 100644 index 000000000000..9d7d6cd73e66 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..eccdcdc918be --- /dev/null +++ b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1786148577 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db new file mode 100644 index 000000000000..3d79705c0356 Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db new file mode 100644 index 000000000000..840e84b00a70 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..ffdbf15c0701 --- /dev/null +++ b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +222628861 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db new file mode 100644 index 000000000000..d90d32041415 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db new file mode 100644 index 000000000000..a16c130af2d3 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..5283dfa4a800 --- /dev/null +++ b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +2717442855 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db new file mode 100644 index 000000000000..56cdd2bc7000 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db new file mode 100644 index 000000000000..23cf67a01640 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..8036065b0d84 --- /dev/null +++ b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +3167815531 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db new file mode 100644 index 000000000000..f2909ea319c5 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..055f0aa9ef6b Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db new file mode 100644 index 000000000000..9d14c7a4a5a6 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..8b429eb68aa9 --- /dev/null +++ b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3225237150 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db new file mode 100644 index 000000000000..6cd79adf9c7b Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db new file mode 100644 index 000000000000..6bb5081a0035 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..c26eb564641f Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db new file mode 100644 index 000000000000..9d0a519f25e7 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..3f892d5387c7 --- /dev/null +++ b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +1869836850 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db new file mode 100644 index 000000000000..2a269ce05aae Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db new file mode 100644 index 000000000000..16119b352f88 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db new file mode 100644 index 000000000000..bdd451e28ad9 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..f0c6adab3d76 --- /dev/null +++ b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +2249640665 \ No newline at end of file diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db new file mode 100644 index 000000000000..c12bf32dd665 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db differ diff --git a/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt new file mode 100644 index 000000000000..3528e2effbab --- /dev/null +++ b/test/data/udt/c41/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +Summary.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +CompressionInfo.db +TOC.txt +Data.db diff --git a/test/data/udt/c41/schema.txt b/test/data/udt/c41/schema.txt new file mode 100644 index 000000000000..9ed560a55df8 --- /dev/null +++ b/test/data/udt/c41/schema.txt @@ -0,0 +1,64 @@ +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +ALTER TABLE ks.tab5_tuple DROP b_complex USING TIMESTAMP 1770661720952002; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +ALTER TABLE ks.tab8_frozen_tuple_with_udt DROP b_complex USING TIMESTAMP 1770661721057001; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +ALTER TABLE ks.tab6_frozen_tuple DROP b_complex USING TIMESTAMP 1770661720989002; +CREATE TYPE ks.udt3 ( + foo int, + bar frozen>, + baz int +); +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +ALTER TABLE ks.tab2_frozen_udt1 DROP b_complex USING TIMESTAMP 1770661720874001; +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +ALTER TABLE ks.tab10_frozen_udt_with_tuple DROP b_complex USING TIMESTAMP 1770661721090002; +CREATE TYPE ks.udt2 ( + foo int, + bar frozen, + baz int +); +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +ALTER TABLE ks.tab4_frozen_udt2 DROP b_complex USING TIMESTAMP 1770661720919002; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5; +ALTER TABLE ks.tab7_tuple_with_udt DROP b_complex USING TIMESTAMP 1770661721024002; \ No newline at end of file diff --git a/test/data/udt/c41/schema0.txt b/test/data/udt/c41/schema0.txt new file mode 100644 index 000000000000..3e1d5a498df5 --- /dev/null +++ b/test/data/udt/c41/schema0.txt @@ -0,0 +1,57 @@ +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen>, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, int>>, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen>, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +CREATE TYPE ks.udt3 ( + foo int, + bar frozen>, + baz int +); +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +CREATE TYPE ks.udt2 ( + foo int, + bar frozen, + baz int +); +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, int>>, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5 \ No newline at end of file diff --git a/test/data/udt/c50/commitlog/CommitLog-7-1770669648129.log b/test/data/udt/c50/commitlog/CommitLog-7-1770669648129.log new file mode 100644 index 000000000000..f99692cb4e22 Binary files /dev/null and b/test/data/udt/c50/commitlog/CommitLog-7-1770669648129.log differ diff --git a/test/data/udt/c50/data.json b/test/data/udt/c50/data.json new file mode 100644 index 000000000000..8227d25eed37 --- /dev/null +++ b/test/data/udt/c50/data.json @@ -0,0 +1 @@ +{"tab5_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab8_frozen_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab6_frozen_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab2_frozen_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab10_frozen_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab4_frozen_udt2":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab7_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]]} \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db new file mode 100644 index 000000000000..989d2c861983 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..4ae76010235f --- /dev/null +++ b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +2134155207 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db new file mode 100644 index 000000000000..a867ba399818 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db new file mode 100644 index 000000000000..584932d6f89b Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db new file mode 100644 index 000000000000..7fdc162ecf7b Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..521a01ba5acb --- /dev/null +++ b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +2966606840 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db new file mode 100644 index 000000000000..e6377ed62d80 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db new file mode 100644 index 000000000000..4f57854c5fec Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db new file mode 100644 index 000000000000..b6a6d287ca89 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..299b189cecae --- /dev/null +++ b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +2720297877 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db new file mode 100644 index 000000000000..a805006b9c72 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db new file mode 100644 index 000000000000..917494244aba Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..e22f6ea8d597 --- /dev/null +++ b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +727620698 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db new file mode 100644 index 000000000000..4a9a9a4c3b08 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..1ecdcfaf8ae6 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db new file mode 100644 index 000000000000..6e9c26a734f2 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..eabc7d6a9a57 --- /dev/null +++ b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +1082388375 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db new file mode 100644 index 000000000000..a33aa3a447fc Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db new file mode 100644 index 000000000000..2e8223613184 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db new file mode 100644 index 000000000000..ff3462d9df28 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..38c37e981460 --- /dev/null +++ b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1284713423 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db new file mode 100644 index 000000000000..bfd48ae61d07 Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db new file mode 100644 index 000000000000..22c2a5e0d046 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..fbf68a677907 --- /dev/null +++ b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +4213007271 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db new file mode 100644 index 000000000000..a867ba399818 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db new file mode 100644 index 000000000000..9a70f63b2525 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db new file mode 100644 index 000000000000..8cce6b5cdc35 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..3abd068f0f64 --- /dev/null +++ b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +3078913155 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db new file mode 100644 index 000000000000..e6377ed62d80 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db new file mode 100644 index 000000000000..011c7e1ccffc Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db new file mode 100644 index 000000000000..4e1fdaa796ca Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..c494befc8577 --- /dev/null +++ b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +619719768 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db new file mode 100644 index 000000000000..d9d21852512f Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db new file mode 100644 index 000000000000..fa94dec412a4 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..471c2a12eb89 --- /dev/null +++ b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3487670676 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db new file mode 100644 index 000000000000..15206b295acc Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db new file mode 100644 index 000000000000..5bd4dc47de1d Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..63a7310291d4 --- /dev/null +++ b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +1048482196 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db new file mode 100644 index 000000000000..37b28e9a8028 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db new file mode 100644 index 000000000000..b113e8520d88 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..2a9c67125291 --- /dev/null +++ b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +1821384100 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db new file mode 100644 index 000000000000..637b8afa0792 Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab5_tuple-90826dd3843745859de415908236687f/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db new file mode 100644 index 000000000000..a6faf2b6b739 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..8efd7f990e49 --- /dev/null +++ b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3733688359 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db new file mode 100644 index 000000000000..c32ee97affe3 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db new file mode 100644 index 000000000000..c6e3f1f6cad2 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db new file mode 100644 index 000000000000..e5a4a26948ff Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db new file mode 100644 index 000000000000..ad832065be18 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db new file mode 100644 index 000000000000..280d82253748 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..cbc583649e80 --- /dev/null +++ b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +568436345 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db new file mode 100644 index 000000000000..93024dfa9a8b Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db new file mode 100644 index 000000000000..ae5913b2da10 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db new file mode 100644 index 000000000000..486adc1f1932 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db new file mode 100644 index 000000000000..c67bbbde58e0 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db new file mode 100644 index 000000000000..bece6e9ae777 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..950fdf047e1c --- /dev/null +++ b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +3840107401 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db new file mode 100644 index 000000000000..87b3d6e6625c Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db new file mode 100644 index 000000000000..be8e41005ca2 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db new file mode 100644 index 000000000000..d7f122fdd2e5 Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db new file mode 100644 index 000000000000..85a8febf932a Binary files /dev/null and b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db new file mode 100644 index 000000000000..1ffa2577a070 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..8fadd6b57f67 --- /dev/null +++ b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +2787209974 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db new file mode 100644 index 000000000000..a867ba399818 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db new file mode 100644 index 000000000000..cba4074b310b Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db new file mode 100644 index 000000000000..15cfcab575ec Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..ef4b974bdf8d --- /dev/null +++ b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +2242480282 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db new file mode 100644 index 000000000000..e6377ed62d80 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db new file mode 100644 index 000000000000..0499e9856085 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db new file mode 100644 index 000000000000..49d2e056a859 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..56eb6dd72278 --- /dev/null +++ b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +4153255207 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db new file mode 100644 index 000000000000..52c3295b81ff Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db new file mode 100644 index 000000000000..40ede09618cf Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 new file mode 100644 index 000000000000..fbb51c2887e5 --- /dev/null +++ b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +25135210 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db new file mode 100644 index 000000000000..f588219fa5f7 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db new file mode 100644 index 000000000000..a867ba399818 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db new file mode 100644 index 000000000000..fa073544af8d Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db new file mode 100644 index 000000000000..35548297abe8 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db new file mode 100644 index 000000000000..1c0fe7ebd986 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 new file mode 100644 index 000000000000..0435d2d86343 --- /dev/null +++ b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Digest.crc32 @@ -0,0 +1 @@ +919111146 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db new file mode 100644 index 000000000000..7348d46cacba Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db new file mode 100644 index 000000000000..e6377ed62d80 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db new file mode 100644 index 000000000000..03dc8234a43d Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db new file mode 100644 index 000000000000..eb45e52fc252 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-2-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-CompressionInfo.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db new file mode 100644 index 000000000000..59eccb975779 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Data.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 new file mode 100644 index 000000000000..a119f9b26f40 --- /dev/null +++ b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Digest.crc32 @@ -0,0 +1 @@ +2060407197 \ No newline at end of file diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db new file mode 100644 index 000000000000..16483766eb9c Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Filter.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db new file mode 100644 index 000000000000..0dc22344f898 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Index.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db new file mode 100644 index 000000000000..ab0e78e15b7a Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Statistics.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db new file mode 100644 index 000000000000..b427c8a48471 Binary files /dev/null and b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-Summary.db differ diff --git a/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt new file mode 100644 index 000000000000..f2df0f413fd8 --- /dev/null +++ b/test/data/udt/c50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/nb-3-big-TOC.txt @@ -0,0 +1,8 @@ +CompressionInfo.db +Data.db +Digest.crc32 +Filter.db +Index.db +Statistics.db +Summary.db +TOC.txt diff --git a/test/data/udt/c50/schema.txt b/test/data/udt/c50/schema.txt new file mode 100644 index 000000000000..2a6dd85d4aff --- /dev/null +++ b/test/data/udt/c50/schema.txt @@ -0,0 +1,64 @@ +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +ALTER TABLE ks.tab5_tuple DROP b_complex USING TIMESTAMP 1770669650934001; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +ALTER TABLE ks.tab8_frozen_tuple_with_udt DROP b_complex USING TIMESTAMP 1770669651048001; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +ALTER TABLE ks.tab6_frozen_tuple DROP b_complex USING TIMESTAMP 1770669650980002; +CREATE TYPE ks.udt3 ( + foo int, + bar frozen>, + baz int +); +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen> +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +ALTER TABLE ks.tab2_frozen_udt1 DROP b_complex USING TIMESTAMP 1770669650858000; +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +ALTER TABLE ks.tab10_frozen_udt_with_tuple DROP b_complex USING TIMESTAMP 1770669651085002; +CREATE TYPE ks.udt2 ( + foo int, + bar frozen, + baz int +); +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +ALTER TABLE ks.tab4_frozen_udt2 DROP b_complex USING TIMESTAMP 1770669650900002; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex frozen>, int>> +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5; +ALTER TABLE ks.tab7_tuple_with_udt DROP b_complex USING TIMESTAMP 1770669651017000; \ No newline at end of file diff --git a/test/data/udt/c50/schema0.txt b/test/data/udt/c50/schema0.txt new file mode 100644 index 000000000000..3e1d5a498df5 --- /dev/null +++ b/test/data/udt/c50/schema0.txt @@ -0,0 +1,57 @@ +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen>, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, int>>, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen>, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +CREATE TYPE ks.udt3 ( + foo int, + bar frozen>, + baz int +); +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +CREATE TYPE ks.udt2 ( + foo int, + bar frozen, + baz int +); +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, int>>, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5 \ No newline at end of file diff --git a/test/data/udt/cc40/commitlog/CommitLog-101-1770670437748.log b/test/data/udt/cc40/commitlog/CommitLog-101-1770670437748.log new file mode 100644 index 000000000000..497c495dc3cf Binary files /dev/null and b/test/data/udt/cc40/commitlog/CommitLog-101-1770670437748.log differ diff --git a/test/data/udt/cc40/data.json b/test/data/udt/cc40/data.json new file mode 100644 index 000000000000..5b6cebdebb4e --- /dev/null +++ b/test/data/udt/cc40/data.json @@ -0,0 +1 @@ +{"tab5_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab8_frozen_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab6_frozen_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab2_frozen_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab9_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab10_frozen_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab1_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab4_frozen_udt2":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab7_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]]} \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Data.db new file mode 100644 index 000000000000..3ae92c660d04 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..e141961b5fe1 --- /dev/null +++ b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1018096591 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..9468d249674e Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Data.db new file mode 100644 index 000000000000..8e66ed47129b Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..dd7bb134300f --- /dev/null +++ b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +3781645211 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..31bdcdbd97bc Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Data.db new file mode 100644 index 000000000000..22e25d79b3f1 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..0037286bb373 --- /dev/null +++ b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +4142392884 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..78d7d704fc52 Binary files /dev/null and b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..e151d23e0668 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Data.db new file mode 100644 index 000000000000..0ac278596afb Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..f054da9c4436 --- /dev/null +++ b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2572550418 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..291e00744817 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..ce04b9d31dec Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..6403723a2827 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Data.db new file mode 100644 index 000000000000..f18b7abaf586 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..a554e368aafb --- /dev/null +++ b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +2793062682 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..540d266cf81a Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..4871f07507b2 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..2e009a9881e5 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Data.db new file mode 100644 index 000000000000..efae05170fd5 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..a22670f22657 --- /dev/null +++ b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +1803798712 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..ad334dfc1204 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..1fbfea6b3244 Binary files /dev/null and b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Data.db new file mode 100644 index 000000000000..bc2ce4c36610 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..0f06ca50eb3c --- /dev/null +++ b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +662969551 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..8365fb958008 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..0f07db8e9a1c Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Data.db new file mode 100644 index 000000000000..814920e303a1 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..990e715c113b --- /dev/null +++ b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +1188328261 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..368a8927a29d Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..71883bc8507e Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Data.db new file mode 100644 index 000000000000..13aafab61673 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..2ca4fbdc890b --- /dev/null +++ b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +2085283881 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..8dc0c107b71f Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..cf023561d6da Binary files /dev/null and b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Data.db new file mode 100644 index 000000000000..f616fbe70e15 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..c796d6f835c6 --- /dev/null +++ b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +255433873 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..32a4af710f0e Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..467f101fb398 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Data.db new file mode 100644 index 000000000000..dc302a0fc9ed Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..3fb308d18b26 --- /dev/null +++ b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +2917172532 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..0106f3c813b7 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..1892c3547c4c Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Data.db new file mode 100644 index 000000000000..a887f6b7b8ed Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..9745e0973b9c --- /dev/null +++ b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +2117875772 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..ecee77ee3e57 Binary files /dev/null and b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Data.db new file mode 100644 index 000000000000..904858b85a84 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..5afc467b0a8e --- /dev/null +++ b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2989893607 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..8365fb958008 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..4791b5f09e27 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Data.db new file mode 100644 index 000000000000..da0313f2aa28 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..5624ce84cf63 --- /dev/null +++ b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +879731634 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..368a8927a29d Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..81b0e105b877 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Data.db new file mode 100644 index 000000000000..6b606825452a Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..3c2df8392966 --- /dev/null +++ b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +3519475724 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..8dc0c107b71f Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..5b3f13a7fb0d Binary files /dev/null and b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..597c78abdcc4 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Data.db new file mode 100644 index 000000000000..7c04d75ac7d3 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..a547521d4b8f --- /dev/null +++ b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3853755573 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..8365fb958008 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..c2894097679e Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Data.db new file mode 100644 index 000000000000..d578c1896afd Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..4d2483280182 --- /dev/null +++ b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +2358960263 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..368a8927a29d Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..a04766e26c1d Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Data.db new file mode 100644 index 000000000000..b5c784eec8d6 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..ed2ca084c2e1 --- /dev/null +++ b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +2025294819 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..8dc0c107b71f Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..dc9f37e3fa48 Binary files /dev/null and b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Data.db new file mode 100644 index 000000000000..e0e7890d94c8 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..75a1fb727a5a --- /dev/null +++ b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +800429136 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..18f7a2afe6d8 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Data.db new file mode 100644 index 000000000000..a7eaef778f43 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..8b41c4ac205a --- /dev/null +++ b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +1598394020 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..676c6b5807ec Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Data.db new file mode 100644 index 000000000000..a41f15a40c80 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..50793284524c --- /dev/null +++ b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +1201182912 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..8ca8d4f3252b Binary files /dev/null and b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Data.db new file mode 100644 index 000000000000..b2e83e2f60b3 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..582754fe0bcd --- /dev/null +++ b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3960217897 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..2affe6cf6443 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Data.db new file mode 100644 index 000000000000..a474551695bd Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..4b429163c139 --- /dev/null +++ b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +1050941113 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..55997a0e6742 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Data.db new file mode 100644 index 000000000000..f0f72e1d9fc7 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..020189d9d3a4 --- /dev/null +++ b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +406517965 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..14ae352ac2fa Binary files /dev/null and b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..3740ade07054 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Data.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Data.db new file mode 100644 index 000000000000..23bd683170e4 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..60f8243db55a --- /dev/null +++ b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3926864485 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Filter.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Partitions.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..2469f5e11569 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Rows.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Statistics.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..997960497c10 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-TOC.txt b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..d08053987ca8 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Data.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Data.db new file mode 100644 index 000000000000..31519a4d9fa6 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..0c70431611c4 --- /dev/null +++ b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +4096596214 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Filter.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Partitions.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..9988b61afcc7 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Rows.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Statistics.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..a542cf3ffd26 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-TOC.txt b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-CompressionInfo.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..8bd57895fc93 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Data.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Data.db new file mode 100644 index 000000000000..81c14fd68016 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Digest.crc32 b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..0acab28f64be --- /dev/null +++ b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +1411197964 \ No newline at end of file diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Filter.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Partitions.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..246d10a7fe26 Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Rows.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Statistics.db b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..62b75313bead Binary files /dev/null and b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-TOC.txt b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..3d38ff63c4f5 --- /dev/null +++ b/test/data/udt/cc40/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +CompressionInfo.db +Digest.crc32 +Partitions.db +Rows.db +TOC.txt +Filter.db +Statistics.db diff --git a/test/data/udt/cc40/schema.txt b/test/data/udt/cc40/schema.txt new file mode 100644 index 000000000000..f3226b5274d4 --- /dev/null +++ b/test/data/udt/cc40/schema.txt @@ -0,0 +1,69 @@ +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TYPE ks.udt2 ( + foo int, + bar udt1, + baz int +); +CREATE TYPE ks.udt3 ( + foo int, + bar tuple, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770670440968002; +CREATE TABLE ks.tab1_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 513f2627-9356-41c4-a379-7ad42be97432 + AND DROPPED COLUMN RECORD b_complex tuple USING TIMESTAMP 1770670440627000; +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5 + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1770670440678000; +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770670440716002; +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1770670440763000; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48 + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1770670440804000; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5 + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770670440842003; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17 + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770670440887000; +CREATE TABLE ks.tab9_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6 + AND DROPPED COLUMN RECORD b_complex tuple>, int> USING TIMESTAMP 1770670440932000 \ No newline at end of file diff --git a/test/data/udt/cc40/schema0.txt b/test/data/udt/cc40/schema0.txt new file mode 100644 index 000000000000..e2eaba40a8fe --- /dev/null +++ b/test/data/udt/cc40/schema0.txt @@ -0,0 +1,69 @@ +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TYPE ks.udt2 ( + foo int, + bar udt1, + baz int +); +CREATE TYPE ks.udt3 ( + foo int, + bar tuple, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +CREATE TABLE ks.tab1_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex udt1 +) WITH ID = 513f2627-9356-41c4-a379-7ad42be97432; +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +CREATE TABLE ks.tab9_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex udt3 +) WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6 \ No newline at end of file diff --git a/test/data/udt/cc50/commitlog/CommitLog-110-1770304028095.log b/test/data/udt/cc50/commitlog/CommitLog-110-1770304028095.log new file mode 100644 index 000000000000..9d6206d451a2 Binary files /dev/null and b/test/data/udt/cc50/commitlog/CommitLog-110-1770304028095.log differ diff --git a/test/data/udt/cc50/data.json b/test/data/udt/cc50/data.json new file mode 100644 index 000000000000..5b6cebdebb4e --- /dev/null +++ b/test/data/udt/cc50/data.json @@ -0,0 +1 @@ +{"tab5_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab8_frozen_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab6_frozen_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab2_frozen_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab9_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab10_frozen_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab1_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab4_frozen_udt2":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab7_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]]} \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Data.db new file mode 100644 index 000000000000..92a7373ce5e0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..a2b5b919040e --- /dev/null +++ b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3008766861 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..174bed4dc70d Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Data.db new file mode 100644 index 000000000000..6b1c16a8e8bb Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..59ed472f6790 --- /dev/null +++ b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +3341884432 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..d5b37e1893c8 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Data.db new file mode 100644 index 000000000000..2146b6f9da32 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..0fdfd866db44 --- /dev/null +++ b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +2379115177 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..fce9570a4c1c Binary files /dev/null and b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..6fdc8f08d048 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Data.db new file mode 100644 index 000000000000..f2c196e42a2b Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..290f547d145e --- /dev/null +++ b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3906947134 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..b96cefe2bb35 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..cd7279956675 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Data.db new file mode 100644 index 000000000000..c21869ca9a35 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..ee6e16a7bfe1 --- /dev/null +++ b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +2066361380 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..59cfff836e1e Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..13a941817665 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..2e009a9881e5 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Data.db new file mode 100644 index 000000000000..a30f715a893d Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..6a19758680f3 --- /dev/null +++ b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +2166383736 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..ad334dfc1204 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..16da4c5a0a62 Binary files /dev/null and b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..f49057b299e4 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Data.db new file mode 100644 index 000000000000..234fb46c61d4 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..d525e8ff3e9e --- /dev/null +++ b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3027691244 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..8154cc68c9b7 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..1d288e06ba9c Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Data.db new file mode 100644 index 000000000000..4d24dd6c903f Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..fea89223f3cb --- /dev/null +++ b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +407364535 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..368a8927a29d Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..fe01b2834854 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Data.db new file mode 100644 index 000000000000..b0a6ccfb04c7 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..ee898b1a7b6b --- /dev/null +++ b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +280659909 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..8dc0c107b71f Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..42bedbf715b8 Binary files /dev/null and b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Data.db new file mode 100644 index 000000000000..bd3bfb22e97a Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..79b4528da28a --- /dev/null +++ b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2108110811 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..dd153d63d3d6 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Data.db new file mode 100644 index 000000000000..bc694ed02552 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..bc3e65f141e0 --- /dev/null +++ b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +481347332 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..f86f64b309eb Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Data.db new file mode 100644 index 000000000000..76039980eb1d Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..ac2a18f24815 --- /dev/null +++ b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +3261661967 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..2cce83c51849 Binary files /dev/null and b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..f49057b299e4 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Data.db new file mode 100644 index 000000000000..dc1a8f8b5c85 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..1faa5e858654 --- /dev/null +++ b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +64321394 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..8154cc68c9b7 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..2cacd48c728b Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Data.db new file mode 100644 index 000000000000..c5fee1c55602 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..05634cde649f --- /dev/null +++ b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +2370871802 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..368a8927a29d Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..0580bc20b514 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Data.db new file mode 100644 index 000000000000..14e9343d0372 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..81437e2ad7db --- /dev/null +++ b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +3891956612 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..8dc0c107b71f Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..34a426475a30 Binary files /dev/null and b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab5_tuple-90826dd3843745859de415908236687f/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..f49057b299e4 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Data.db new file mode 100644 index 000000000000..cf25ed72b8c9 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..eb7138d0050b --- /dev/null +++ b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1157173436 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Filter.db new file mode 100644 index 000000000000..a6cf405c611e Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..8154cc68c9b7 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..368aa8600424 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..ea25c8633fe1 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Data.db new file mode 100644 index 000000000000..f6173692114b Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..e17af5814a06 --- /dev/null +++ b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +3487175586 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Filter.db new file mode 100644 index 000000000000..031a8a0767ae Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..368a8927a29d Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..da3c129e360b Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..c1a45e46087a Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Data.db new file mode 100644 index 000000000000..7bd9b4f5c3a8 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..4fedecfa259b --- /dev/null +++ b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +1093803440 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Filter.db new file mode 100644 index 000000000000..dd587b2e9db0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..8dc0c107b71f Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..8e04fc5b5305 Binary files /dev/null and b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Data.db new file mode 100644 index 000000000000..fffe7efa4f4b Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..db2bd1935933 --- /dev/null +++ b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +882860880 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..27ed244b7b38 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Data.db new file mode 100644 index 000000000000..35b333fe47e0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..07decc6029d3 --- /dev/null +++ b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +3763420509 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..ecc7f07b81fb Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Data.db new file mode 100644 index 000000000000..bf8b67e1c770 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..2d642f920635 --- /dev/null +++ b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +4000718437 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..63896fc29ca5 Binary files /dev/null and b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..fa4e706de859 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Data.db new file mode 100644 index 000000000000..17e4c7287070 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..ec6603e012ff --- /dev/null +++ b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2521749694 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..a70d4b8ec3bf Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..07ce3f3f7ce8 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..749bf370de1c Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Data.db new file mode 100644 index 000000000000..19385918d5a5 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..9ffbbabc2c67 --- /dev/null +++ b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +1748248752 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..c9c85e8a2d07 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..591748495ca3 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..00de5722ab5d Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Data.db new file mode 100644 index 000000000000..a5a712c1a7a1 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..3e0664c74b7c --- /dev/null +++ b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +3669976075 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..f7c3cae5f1a3 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..4b1481516ea7 Binary files /dev/null and b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..3740ade07054 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Data.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Data.db new file mode 100644 index 000000000000..8f730bbbb233 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Digest.crc32 new file mode 100644 index 000000000000..e339b8f89af7 --- /dev/null +++ b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3631838608 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Filter.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Filter.db new file mode 100644 index 000000000000..bec3a946bf18 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Partitions.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Partitions.db new file mode 100644 index 000000000000..2469f5e11569 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Rows.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Statistics.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Statistics.db new file mode 100644 index 000000000000..a013166176e0 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-TOC.txt b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-CompressionInfo.db new file mode 100644 index 000000000000..d08053987ca8 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Data.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Data.db new file mode 100644 index 000000000000..3af6c914bb3b Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Digest.crc32 new file mode 100644 index 000000000000..ac660aa40a12 --- /dev/null +++ b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Digest.crc32 @@ -0,0 +1 @@ +3961316041 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Filter.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Filter.db new file mode 100644 index 000000000000..f16444a8fd08 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Partitions.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Partitions.db new file mode 100644 index 000000000000..9988b61afcc7 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Rows.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Statistics.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Statistics.db new file mode 100644 index 000000000000..2b5dd918add3 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-TOC.txt b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-2-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-CompressionInfo.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..8bd57895fc93 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-CompressionInfo.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Data.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Data.db new file mode 100644 index 000000000000..f880e0a37ade Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Data.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Digest.crc32 b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Digest.crc32 new file mode 100644 index 000000000000..739ae938922a --- /dev/null +++ b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Digest.crc32 @@ -0,0 +1 @@ +658414136 \ No newline at end of file diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Filter.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Filter.db new file mode 100644 index 000000000000..65478b8c051e Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Filter.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Partitions.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Partitions.db new file mode 100644 index 000000000000..246d10a7fe26 Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Partitions.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Rows.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Statistics.db b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Statistics.db new file mode 100644 index 000000000000..ddb6c39c90cc Binary files /dev/null and b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-Statistics.db differ diff --git a/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-TOC.txt b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-TOC.txt new file mode 100644 index 000000000000..298910cfdc58 --- /dev/null +++ b/test/data/udt/cc50/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/cc-3-bti-TOC.txt @@ -0,0 +1,8 @@ +Data.db +Statistics.db +Digest.crc32 +TOC.txt +CompressionInfo.db +Filter.db +Partitions.db +Rows.db diff --git a/test/data/udt/cc50/schema.txt b/test/data/udt/cc50/schema.txt new file mode 100644 index 000000000000..4e50e74b187f --- /dev/null +++ b/test/data/udt/cc50/schema.txt @@ -0,0 +1,69 @@ +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TYPE ks.udt2 ( + foo int, + bar udt1, + baz int +); +CREATE TYPE ks.udt3 ( + foo int, + bar tuple, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770304031828003; +CREATE TABLE ks.tab1_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 513f2627-9356-41c4-a379-7ad42be97432 + AND DROPPED COLUMN RECORD b_complex tuple USING TIMESTAMP 1770304031397000; +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5 + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1770304031489001; +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770304031539001; +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1770304031593001; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48 + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1770304031642000; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5 + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770304031689001; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17 + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1770304031739000; +CREATE TABLE ks.tab9_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int +) WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6 + AND DROPPED COLUMN RECORD b_complex tuple>, int> USING TIMESTAMP 1770304031785001 \ No newline at end of file diff --git a/test/data/udt/cc50/schema0.txt b/test/data/udt/cc50/schema0.txt new file mode 100644 index 000000000000..e2eaba40a8fe --- /dev/null +++ b/test/data/udt/cc50/schema0.txt @@ -0,0 +1,69 @@ +CREATE TYPE ks.udt1 ( + foo int, + bar text, + baz int +); +CREATE TYPE ks.udt2 ( + foo int, + bar udt1, + baz int +); +CREATE TYPE ks.udt3 ( + foo int, + bar tuple, + baz int +); +CREATE TABLE ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa; +CREATE TABLE ks.tab1_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex udt1 +) WITH ID = 513f2627-9356-41c4-a379-7ad42be97432; +CREATE TABLE ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +CREATE TABLE ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int +) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +CREATE TABLE ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +CREATE TABLE ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +CREATE TABLE ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5; +CREATE TABLE ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int +) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +CREATE TABLE ks.tab9_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex udt3 +) WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6 \ No newline at end of file diff --git a/test/data/udt/dse/commitlog/CommitLog-680-1716886413137.log b/test/data/udt/dse/commitlog/CommitLog-680-1716886413137.log new file mode 100644 index 000000000000..a01849ca11a9 Binary files /dev/null and b/test/data/udt/dse/commitlog/CommitLog-680-1716886413137.log differ diff --git a/test/data/udt/dse/commitlog/CommitLog-680-1716886413138.log b/test/data/udt/dse/commitlog/CommitLog-680-1716886413138.log new file mode 100644 index 000000000000..4c2f6e5864db Binary files /dev/null and b/test/data/udt/dse/commitlog/CommitLog-680-1716886413138.log differ diff --git a/test/data/udt/dse/data.json b/test/data/udt/dse/data.json new file mode 100644 index 000000000000..5b6cebdebb4e --- /dev/null +++ b/test/data/udt/dse/data.json @@ -0,0 +1 @@ +{"tab5_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab8_frozen_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab6_frozen_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab2_frozen_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab9_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab10_frozen_udt_with_tuple":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab1_udt1":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,32],[17,17,34],[18,null,36],[19,19,38],[20,null,40],[21,21,42],[22,null,44],[23,23,46],[24,null,48],[25,25,50],[26,null,52],[27,27,54],[28,null,56],[29,29,58],[30,null,60],[31,31,62],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,32],[145,17,34],[146,null,32],[147,17,34],[148,null,32],[149,17,34],[150,null,32],[151,17,34],[152,null,32],[153,17,34],[154,null,32],[155,17,34],[156,null,32],[157,17,34],[158,null,32],[159,17,34],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,32],[273,17,34],[274,1,36],[275,19,38],[276,1,40],[277,21,42],[278,1,44],[279,23,46],[280,1,48],[281,25,50],[282,1,52],[283,27,54],[284,1,56],[285,29,58],[286,1,60],[287,31,62],[288,1,null],[289,null,32],[290,17,34],[291,null,32],[292,17,34],[293,null,32],[294,17,34],[295,null,32],[296,17,34],[297,null,32],[298,17,34],[299,null,32],[300,17,34],[301,null,32],[302,17,34],[303,null,32],[304,17,34]],"tab4_frozen_udt2":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]],"tab7_tuple_with_udt":[[0,null,null],[1,1,null],[2,null,null],[3,3,null],[4,null,null],[5,5,null],[6,null,null],[7,7,null],[8,null,null],[9,9,null],[10,null,null],[11,11,null],[12,null,null],[13,13,null],[14,null,null],[15,15,null],[16,null,null],[17,17,null],[18,null,null],[19,19,null],[20,null,null],[21,21,null],[22,null,null],[23,23,null],[24,null,null],[25,25,null],[26,null,null],[27,27,null],[28,null,null],[29,29,null],[30,null,null],[31,31,null],[32,null,null],[33,33,null],[34,null,null],[35,35,null],[36,null,null],[37,37,null],[38,null,null],[39,39,null],[40,null,null],[41,41,null],[42,null,null],[43,43,null],[44,null,null],[45,45,null],[46,null,null],[47,47,null],[48,null,null],[49,49,null],[50,null,null],[51,51,null],[52,null,null],[53,53,null],[54,null,null],[55,55,null],[56,null,null],[57,57,null],[58,null,null],[59,59,null],[60,null,null],[61,61,null],[62,null,null],[63,63,null],[64,null,128],[65,65,130],[66,null,132],[67,67,134],[68,null,136],[69,69,138],[70,null,140],[71,71,142],[72,null,144],[73,73,146],[74,null,148],[75,75,150],[76,null,152],[77,77,154],[78,null,156],[79,79,158],[80,null,160],[81,81,162],[82,null,164],[83,83,166],[84,null,168],[85,85,170],[86,null,172],[87,87,174],[88,null,176],[89,89,178],[90,null,180],[91,91,182],[92,null,184],[93,93,186],[94,null,188],[95,95,190],[96,null,192],[97,97,194],[98,null,196],[99,99,198],[100,null,200],[101,101,202],[102,null,204],[103,103,206],[104,null,208],[105,105,210],[106,null,212],[107,107,214],[108,null,216],[109,109,218],[110,null,220],[111,111,222],[112,null,224],[113,113,226],[114,null,228],[115,115,230],[116,null,232],[117,117,234],[118,null,236],[119,119,238],[120,null,240],[121,121,242],[122,null,244],[123,123,246],[124,null,248],[125,125,250],[126,null,252],[127,127,254],[128,null,null],[129,1,null],[130,null,null],[131,1,null],[132,null,null],[133,1,null],[134,null,null],[135,1,null],[136,null,null],[137,1,null],[138,null,null],[139,1,null],[140,null,null],[141,1,null],[142,null,null],[143,1,null],[144,null,null],[145,1,null],[146,null,null],[147,1,null],[148,null,null],[149,1,null],[150,null,null],[151,1,null],[152,null,null],[153,1,null],[154,null,null],[155,1,null],[156,null,null],[157,1,null],[158,null,null],[159,1,null],[160,null,null],[161,1,null],[162,null,null],[163,1,null],[164,null,null],[165,1,null],[166,null,null],[167,1,null],[168,null,null],[169,1,null],[170,null,null],[171,1,null],[172,null,null],[173,1,null],[174,null,null],[175,1,null],[176,null,null],[177,1,null],[178,null,null],[179,1,null],[180,null,null],[181,1,null],[182,null,null],[183,1,null],[184,null,null],[185,1,null],[186,null,null],[187,1,null],[188,null,null],[189,1,null],[190,null,null],[191,1,null],[192,null,128],[193,65,130],[194,null,128],[195,65,130],[196,null,128],[197,65,130],[198,null,128],[199,65,130],[200,null,128],[201,65,130],[202,null,128],[203,65,130],[204,null,128],[205,65,130],[206,null,128],[207,65,130],[208,null,128],[209,65,130],[210,null,128],[211,65,130],[212,null,128],[213,65,130],[214,null,128],[215,65,130],[216,null,128],[217,65,130],[218,null,128],[219,65,130],[220,null,128],[221,65,130],[222,null,128],[223,65,130],[224,null,128],[225,65,130],[226,null,128],[227,65,130],[228,null,128],[229,65,130],[230,null,128],[231,65,130],[232,null,128],[233,65,130],[234,null,128],[235,65,130],[236,null,128],[237,65,130],[238,null,128],[239,65,130],[240,null,128],[241,65,130],[242,null,128],[243,65,130],[244,null,128],[245,65,130],[246,null,128],[247,65,130],[248,null,128],[249,65,130],[250,null,128],[251,65,130],[252,null,128],[253,65,130],[254,null,128],[255,65,130],[256,null,null],[257,1,null],[258,null,null],[259,3,null],[260,null,null],[261,5,null],[262,null,null],[263,7,null],[264,null,null],[265,9,null],[266,null,null],[267,11,null],[268,null,null],[269,13,null],[270,null,null],[271,15,null],[272,null,null],[273,17,null],[274,1,null],[275,19,null],[276,1,null],[277,21,null],[278,1,null],[279,23,null],[280,1,null],[281,25,null],[282,1,null],[283,27,null],[284,1,null],[285,29,null],[286,1,null],[287,31,null],[288,1,null],[289,33,null],[290,1,null],[291,35,null],[292,1,null],[293,37,null],[294,1,null],[295,39,null],[296,1,null],[297,41,null],[298,1,null],[299,43,null],[300,1,null],[301,45,null],[302,1,null],[303,47,null],[304,1,null],[305,49,null],[306,1,null],[307,51,null],[308,1,null],[309,53,null],[310,1,null],[311,55,null],[312,1,null],[313,57,null],[314,1,null],[315,59,null],[316,1,null],[317,61,null],[318,1,null],[319,63,null],[320,1,128],[321,65,130],[322,1,132],[323,67,134],[324,1,136],[325,69,138],[326,1,140],[327,71,142],[328,1,144],[329,73,146],[330,1,148],[331,75,150],[332,1,152],[333,77,154],[334,1,156],[335,79,158],[336,1,160],[337,81,128],[338,65,130],[339,83,128],[340,65,130],[341,85,128],[342,65,130],[343,87,128],[344,65,130],[345,89,128],[346,65,130],[347,91,128],[348,65,130],[349,93,128],[350,65,130],[351,95,128],[352,65,130],[353,97,128],[354,65,130],[355,99,128],[356,65,130],[357,101,128],[358,65,130],[359,103,128],[360,65,130],[361,105,128],[362,65,130],[363,107,128],[364,65,130],[365,109,128],[366,65,130],[367,111,128],[368,65,130],[369,113,128],[370,65,130],[371,115,128],[372,65,130],[373,117,128],[374,65,130],[375,119,128],[376,65,130],[377,121,128],[378,65,130],[379,123,128],[380,65,130],[381,125,128],[382,65,130],[383,127,128],[384,65,130],[385,null,128],[386,65,130],[387,null,128],[388,65,130],[389,null,128],[390,65,130],[391,null,128],[392,65,130],[393,null,128],[394,65,130],[395,null,128],[396,65,130],[397,null,128],[398,65,130],[399,null,128],[400,65,130]]} \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..02d50ba350dc Binary files /dev/null and b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Data.db new file mode 100644 index 000000000000..7a2a2bdc96f5 Binary files /dev/null and b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..bbd8e52fac83 --- /dev/null +++ b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1585509291 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8ec204b222ec Binary files /dev/null and b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..1d148399d8c1 Binary files /dev/null and b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..cf914532a760 Binary files /dev/null and b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab10_frozen_udt_with_tuple-6a5cff4e2f944c8b9aa20fbd65292caa/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..08a110019595 Binary files /dev/null and b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Data.db new file mode 100644 index 000000000000..d73b49a2a733 Binary files /dev/null and b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..0afdc44be807 --- /dev/null +++ b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +267106807 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Filter.db new file mode 100644 index 000000000000..ddc10d84cfb5 Binary files /dev/null and b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..f7e2506b30fe Binary files /dev/null and b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..d00caed6d2aa Binary files /dev/null and b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab1_udt1-513f2627935641c4a3797ad42be97432/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..be07a5c39d2f Binary files /dev/null and b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Data.db new file mode 100644 index 000000000000..912133935ae0 Binary files /dev/null and b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..6c42aa800a2f --- /dev/null +++ b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +4251699674 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Filter.db new file mode 100644 index 000000000000..ddc10d84cfb5 Binary files /dev/null and b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..703ab1ae97a5 Binary files /dev/null and b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..cf0c67e696f2 Binary files /dev/null and b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab2_frozen_udt1-450f91fe7c4741c997bffdad854fa7e5/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..02d50ba350dc Binary files /dev/null and b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Data.db new file mode 100644 index 000000000000..e1a8763aa84f Binary files /dev/null and b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..d1d702e13ea8 --- /dev/null +++ b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +81536759 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8ec204b222ec Binary files /dev/null and b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..1d148399d8c1 Binary files /dev/null and b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..9653232e72a9 Binary files /dev/null and b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab4_frozen_udt2-9c03c71c6775435791730f8808901afa/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..be07a5c39d2f Binary files /dev/null and b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Data.db new file mode 100644 index 000000000000..df71aecde743 Binary files /dev/null and b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..4db23d2bb3a7 --- /dev/null +++ b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1056628318 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Filter.db new file mode 100644 index 000000000000..ddc10d84cfb5 Binary files /dev/null and b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..703ab1ae97a5 Binary files /dev/null and b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..47a5ff770a3c Binary files /dev/null and b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab5_tuple-90826dd3843745859de415908236687f/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..be07a5c39d2f Binary files /dev/null and b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Data.db new file mode 100644 index 000000000000..374af52eea79 Binary files /dev/null and b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..efa6aca2b123 --- /dev/null +++ b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +1446632359 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Filter.db new file mode 100644 index 000000000000..ddc10d84cfb5 Binary files /dev/null and b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..703ab1ae97a5 Binary files /dev/null and b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..478d9d98dfe1 Binary files /dev/null and b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab6_frozen_tuple-54185f9aa6fd487cabc3c01bd5835e48/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..02d50ba350dc Binary files /dev/null and b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Data.db new file mode 100644 index 000000000000..ea2d5e1de23c Binary files /dev/null and b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..5aa5ec180adf --- /dev/null +++ b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +2998342438 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8ec204b222ec Binary files /dev/null and b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..1d148399d8c1 Binary files /dev/null and b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..7909334265a8 Binary files /dev/null and b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab7_tuple_with_udt-4e78f4037b634e0da23142e42cba7cb5/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..02d50ba350dc Binary files /dev/null and b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Data.db new file mode 100644 index 000000000000..c9a4926c9fce Binary files /dev/null and b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..f588ecdef934 --- /dev/null +++ b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3453581958 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8ec204b222ec Binary files /dev/null and b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..1d148399d8c1 Binary files /dev/null and b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..83a6ca6aaeb2 Binary files /dev/null and b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab8_frozen_tuple_with_udt-8660f235081640199cc91798fa7beb17/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-CompressionInfo.db b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-CompressionInfo.db new file mode 100644 index 000000000000..3a678f05fb40 Binary files /dev/null and b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-CompressionInfo.db differ diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Data.db b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Data.db new file mode 100644 index 000000000000..f63bccf07d14 Binary files /dev/null and b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Data.db differ diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Digest.crc32 b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Digest.crc32 new file mode 100644 index 000000000000..9c208ff22f01 --- /dev/null +++ b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Digest.crc32 @@ -0,0 +1 @@ +3940802110 \ No newline at end of file diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Filter.db b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Filter.db new file mode 100644 index 000000000000..8ec204b222ec Binary files /dev/null and b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Filter.db differ diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Partitions.db b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Partitions.db new file mode 100644 index 000000000000..bd3bc4ac61f6 Binary files /dev/null and b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Partitions.db differ diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Rows.db b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Statistics.db b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Statistics.db new file mode 100644 index 000000000000..61b723912e0f Binary files /dev/null and b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-Statistics.db differ diff --git a/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-TOC.txt b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-TOC.txt new file mode 100644 index 000000000000..96495ac1347d --- /dev/null +++ b/test/data/udt/dse/ks/tab9_udt_with_tuple-f670fd5a81454669aceb75667c000ea6/bb-1-bti-TOC.txt @@ -0,0 +1,8 @@ +Filter.db +Data.db +Statistics.db +TOC.txt +Digest.crc32 +Rows.db +CompressionInfo.db +Partitions.db diff --git a/test/data/udt/dse/schema.txt b/test/data/udt/dse/schema.txt new file mode 100644 index 000000000000..c95278739f17 --- /dev/null +++ b/test/data/udt/dse/schema.txt @@ -0,0 +1,57 @@ +CREATE TYPE IF NOT EXISTS ks.udt1 (foo int, bar text, baz int); +CREATE TYPE IF NOT EXISTS ks.udt2 (foo int, bar udt1, baz int); +CREATE TYPE IF NOT EXISTS ks.udt3 (foo int, bar tuple, baz int); +CREATE TABLE IF NOT EXISTS ks.tab1_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 513f2627-9356-41c4-a379-7ad42be97432 + AND DROPPED COLUMN RECORD b_complex tuple USING TIMESTAMP 1716886419358000; +CREATE TABLE IF NOT EXISTS ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5 + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1716886419453000; +CREATE TABLE IF NOT EXISTS ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1716886419544000; +CREATE TABLE IF NOT EXISTS ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 90826dd3-8437-4585-9de4-15908236687f + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1716886419641000; +CREATE TABLE IF NOT EXISTS ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48 + AND DROPPED COLUMN RECORD b_complex frozen> USING TIMESTAMP 1716886419729000; +CREATE TABLE IF NOT EXISTS ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5 + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1716886419826000; +CREATE TABLE IF NOT EXISTS ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17 + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1716886419931000; +CREATE TABLE IF NOT EXISTS ks.tab9_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6 + AND DROPPED COLUMN RECORD b_complex tuple>, int> USING TIMESTAMP 1716886420031000; +CREATE TABLE IF NOT EXISTS ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int) + WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa + AND DROPPED COLUMN RECORD b_complex frozen>, int>> USING TIMESTAMP 1716886420131000 \ No newline at end of file diff --git a/test/data/udt/dse/schema0.txt b/test/data/udt/dse/schema0.txt new file mode 100644 index 000000000000..78d4c3562dee --- /dev/null +++ b/test/data/udt/dse/schema0.txt @@ -0,0 +1,57 @@ +CREATE TYPE IF NOT EXISTS ks.udt1 (foo int, bar text, baz int); +CREATE TYPE IF NOT EXISTS ks.udt2 (foo int, bar udt1, baz int); +CREATE TYPE IF NOT EXISTS ks.udt3 (foo int, bar tuple, baz int); +CREATE TABLE IF NOT EXISTS ks.tab1_udt1 ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex udt1) + WITH ID = 513f2627-9356-41c4-a379-7ad42be97432; +CREATE TABLE IF NOT EXISTS ks.tab2_frozen_udt1 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int) + WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5; +CREATE TABLE IF NOT EXISTS ks.tab4_frozen_udt2 ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int) + WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa; +CREATE TABLE IF NOT EXISTS ks.tab5_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int) + WITH ID = 90826dd3-8437-4585-9de4-15908236687f; +CREATE TABLE IF NOT EXISTS ks.tab6_frozen_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int) + WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48; +CREATE TABLE IF NOT EXISTS ks.tab7_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int) + WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5; +CREATE TABLE IF NOT EXISTS ks.tab8_frozen_tuple_with_udt ( + pk int PRIMARY KEY, + a_int int, + b_complex tuple, + c_int int) + WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17; +CREATE TABLE IF NOT EXISTS ks.tab9_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + c_int int, + b_complex udt3) + WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6; +CREATE TABLE IF NOT EXISTS ks.tab10_frozen_udt_with_tuple ( + pk int PRIMARY KEY, + a_int int, + b_complex frozen, + c_int int) + WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa \ No newline at end of file diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-CompressionInfo.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-CompressionInfo.db new file mode 100644 index 000000000000..7bd849dd2935 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-CompressionInfo.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Data.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Data.db new file mode 100644 index 000000000000..b31194318fe6 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Data.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Filter.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Filter.db new file mode 100644 index 000000000000..edad7a615522 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Filter.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Partitions.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Partitions.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Rows.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Statistics.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Statistics.db new file mode 100644 index 000000000000..dd754d5a9b1f Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Statistics.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-TOC.txt b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-TOC.txt new file mode 100644 index 000000000000..d11c9ca66cdb --- /dev/null +++ b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-TOC.txt @@ -0,0 +1,7 @@ +CompressionInfo.db +Data.db +Partitions.db +TOC.txt +Statistics.db +Filter.db +Rows.db diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-CompressionInfo.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-CompressionInfo.db new file mode 100644 index 000000000000..7bd849dd2935 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-CompressionInfo.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Data.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Data.db new file mode 100644 index 000000000000..e0ccfc590677 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Data.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Filter.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Filter.db new file mode 100644 index 000000000000..e7e7bee15e1f Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Filter.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Partitions.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Partitions.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Rows.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Statistics.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Statistics.db new file mode 100644 index 000000000000..d3f62bdc50b9 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Statistics.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-TOC.txt b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-TOC.txt new file mode 100644 index 000000000000..d11c9ca66cdb --- /dev/null +++ b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-TOC.txt @@ -0,0 +1,7 @@ +CompressionInfo.db +Data.db +Partitions.db +TOC.txt +Statistics.db +Filter.db +Rows.db diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-CompressionInfo.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-CompressionInfo.db new file mode 100644 index 000000000000..7bd849dd2935 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-CompressionInfo.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Data.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Data.db new file mode 100644 index 000000000000..b31194318fe6 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Data.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Filter.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Filter.db new file mode 100644 index 000000000000..edad7a615522 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Filter.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Partitions.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Partitions.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Rows.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Statistics.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Statistics.db new file mode 100644 index 000000000000..dd754d5a9b1f Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Statistics.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-TOC.txt b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-TOC.txt new file mode 100644 index 000000000000..d11c9ca66cdb --- /dev/null +++ b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-TOC.txt @@ -0,0 +1,7 @@ +CompressionInfo.db +Data.db +Partitions.db +TOC.txt +Statistics.db +Filter.db +Rows.db diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-CompressionInfo.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-CompressionInfo.db new file mode 100644 index 000000000000..7bd849dd2935 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-CompressionInfo.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Data.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Data.db new file mode 100644 index 000000000000..e0ccfc590677 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Data.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Filter.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Filter.db new file mode 100644 index 000000000000..e7e7bee15e1f Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Filter.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Partitions.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Partitions.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Rows.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Statistics.db b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Statistics.db new file mode 100644 index 000000000000..d3f62bdc50b9 Binary files /dev/null and b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Statistics.db differ diff --git a/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-TOC.txt b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-TOC.txt new file mode 100644 index 000000000000..d11c9ca66cdb --- /dev/null +++ b/test/data/zcs/compressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-TOC.txt @@ -0,0 +1,7 @@ +CompressionInfo.db +Data.db +Partitions.db +TOC.txt +Statistics.db +Filter.db +Rows.db diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-CRC.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-CRC.db new file mode 100644 index 000000000000..1b96b2201276 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-CRC.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Data.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Data.db new file mode 100644 index 000000000000..9d7da55dcec1 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Data.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Filter.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Filter.db new file mode 100644 index 000000000000..edad7a615522 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Filter.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Partitions.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Partitions.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Rows.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Statistics.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Statistics.db new file mode 100644 index 000000000000..04650d480586 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-Statistics.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-TOC.txt b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-TOC.txt new file mode 100644 index 000000000000..073da9f4a1b5 --- /dev/null +++ b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-3-bti-TOC.txt @@ -0,0 +1,7 @@ +Statistics.db +Filter.db +CRC.db +Rows.db +Data.db +Partitions.db +TOC.txt diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-CRC.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-CRC.db new file mode 100644 index 000000000000..1b96b2201276 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-CRC.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Data.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Data.db new file mode 100644 index 000000000000..02001ea2daac Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Data.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Filter.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Filter.db new file mode 100644 index 000000000000..e7e7bee15e1f Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Filter.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Partitions.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Partitions.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Rows.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Statistics.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Statistics.db new file mode 100644 index 000000000000..8c4ec100ca7b Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-Statistics.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-TOC.txt b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-TOC.txt new file mode 100644 index 000000000000..073da9f4a1b5 --- /dev/null +++ b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-4-bti-TOC.txt @@ -0,0 +1,7 @@ +Statistics.db +Filter.db +CRC.db +Rows.db +Data.db +Partitions.db +TOC.txt diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-CRC.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-CRC.db new file mode 100644 index 000000000000..1b96b2201276 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-CRC.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Data.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Data.db new file mode 100644 index 000000000000..9d7da55dcec1 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Data.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Filter.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Filter.db new file mode 100644 index 000000000000..edad7a615522 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Filter.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Partitions.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Partitions.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Rows.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Statistics.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Statistics.db new file mode 100644 index 000000000000..04650d480586 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-Statistics.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-TOC.txt b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-TOC.txt new file mode 100644 index 000000000000..073da9f4a1b5 --- /dev/null +++ b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-6-bti-TOC.txt @@ -0,0 +1,7 @@ +Statistics.db +Filter.db +CRC.db +Rows.db +Data.db +Partitions.db +TOC.txt diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-CRC.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-CRC.db new file mode 100644 index 000000000000..1b96b2201276 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-CRC.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Data.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Data.db new file mode 100644 index 000000000000..02001ea2daac Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Data.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Filter.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Filter.db new file mode 100644 index 000000000000..e7e7bee15e1f Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Filter.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Partitions.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Partitions.db new file mode 100644 index 000000000000..0c14a5d171f0 Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Partitions.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Rows.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Rows.db new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Statistics.db b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Statistics.db new file mode 100644 index 000000000000..8c4ec100ca7b Binary files /dev/null and b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-Statistics.db differ diff --git a/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-TOC.txt b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-TOC.txt new file mode 100644 index 000000000000..073da9f4a1b5 --- /dev/null +++ b/test/data/zcs/uncompressed/ZeroCopyStreamingTest/Standard1/bb-7-bti-TOC.txt @@ -0,0 +1,7 @@ +Statistics.db +Filter.db +CRC.db +Rows.db +Data.db +Partitions.db +TOC.txt diff --git a/test/distributed/org/apache/cassandra/db/commitlog/MemoryMappedSegmentStartupTest.java b/test/distributed/org/apache/cassandra/db/commitlog/MemoryMappedSegmentStartupTest.java new file mode 100644 index 000000000000..e6cdfd0b4d9c --- /dev/null +++ b/test/distributed/org/apache/cassandra/db/commitlog/MemoryMappedSegmentStartupTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.commitlog; + +import java.io.IOException; + +import org.junit.After; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; + +import static org.apache.cassandra.config.CassandraRelevantProperties.COMMITLOG_SKIP_FILE_ADVICE; +import static org.junit.Assert.assertEquals; + +public class MemoryMappedSegmentStartupTest +{ + @After + public void tearDown() throws Exception + { + COMMITLOG_SKIP_FILE_ADVICE.reset(); + } + + @Test + public void shouldSetSkipFileAdviceTrueWithParameterTrue() throws IOException + { + COMMITLOG_SKIP_FILE_ADVICE.setBoolean(true); + try (Cluster cluster = Cluster.build(1).start()) + { + assertEquals(true, cluster.get(1).callOnInstance(() -> MemoryMappedSegment.skipFileAdviseToFreePageCache)); + } + } + + @Test + public void shouldSetSkipFileAdviceFalseWithParameterFalse() throws IOException + { + COMMITLOG_SKIP_FILE_ADVICE.setBoolean(false); + try (Cluster cluster = Cluster.build(1).start()) + { + assertEquals(false, cluster.get(1).callOnInstance(() -> MemoryMappedSegment.skipFileAdviseToFreePageCache)); + } + } + + @Test + public void shouldSetSkipFileAdviceFalseWithParameterMissing() throws IOException + { + try (Cluster cluster = Cluster.build(1).start()) + { + assertEquals(false, cluster.get(1).callOnInstance(() -> MemoryMappedSegment.skipFileAdviseToFreePageCache)); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java b/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java index 54fe2303ad3a..5170bbaf5b8f 100644 --- a/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java +++ b/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java @@ -48,6 +48,7 @@ import org.apache.cassandra.cql3.statements.StatementType; import org.apache.cassandra.db.ClusteringBound; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.filter.IndexHints; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DeletionTime; @@ -307,16 +308,17 @@ Mutation delete(long lts, long pd, Query query) new AbstractMarker.Raw(values.size() - 1))); } - StatementRestrictions restrictions = new StatementRestrictions(null, - StatementType.DELETE, - metadata, - builder.build(), - new VariableSpecifications(variableNames), - Collections.emptyList(), - false, - false, - false, - false); + StatementRestrictions restrictions = StatementRestrictions.create(null, + StatementType.DELETE, + metadata, + builder.build(), + new VariableSpecifications(variableNames), + Collections.emptyList(), + IndexHints.NONE, + false, + false, + false, + false); QueryOptions options = QueryOptions.forInternalCalls(ConsistencyLevel.QUORUM, values); SortedSet> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options); diff --git a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java index b670fc868c2d..61099da542ec 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java @@ -191,7 +191,7 @@ public static abstract class AbstractBuilder tokens = tokenSupplier.tokens(nodeNum); NetworkTopology topology = buildNetworkTopology(provisionStrategy, nodeIdTopology); InstanceConfig config = InstanceConfig.generate(nodeNum, provisionStrategy, topology, root, tokens, datadirCount); + logger.info("Instance {} config: {}", nodeNum, config); config.set(Constants.KEY_DTEST_API_CLUSTER_ID, clusterId.toString()); // if a test sets num_tokens directly, then respect it and only run if vnode or no-vnode is defined int defaultTokenCount = config.getInt("num_tokens"); @@ -1006,7 +1007,13 @@ protected IListen.Cancel startPolling(IInstance instance) protected boolean isCompleted() { - return instances.stream().allMatch(i -> !i.config().has(Feature.GOSSIP) || i.liveMemberCount() == instances.size()); + return instances.stream().allMatch(i -> { + if (!i.config().has(Feature.GOSSIP)) + return true; + + logger.info("Instance {} reports {} live members count, required {}", i, i.liveMemberCount(), instances.size()); + return i.liveMemberCount() == instances.size(); + }); } protected String getMonitorTimeoutMessage() diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java b/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java index 71cf5aaa5a3e..d3e917659054 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java @@ -29,6 +29,7 @@ import com.google.common.collect.Iterators; import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.statements.SelectStatement; @@ -61,12 +62,13 @@ public SimpleQueryResult executeWithResult(String query, ConsistencyLevel consis return instance().sync(() -> unsafeExecuteInternal(query, consistencyLevel, boundValues)).call(); } + @Override public Future asyncExecuteWithTracingWithResult(UUID sessionId, String query, ConsistencyLevel consistencyLevelOrigin, Object... boundValues) { return instance.async(() -> { try { - Tracing.instance.newSession(TimeUUID.fromUuid(sessionId), Collections.emptyMap()); + Tracing.instance.newSession(ClientState.forInternalCalls(), TimeUUID.fromUuid(sessionId), Collections.emptyMap()); return unsafeExecuteInternal(query, consistencyLevelOrigin, boundValues); } finally @@ -135,7 +137,7 @@ public QueryResult executeWithPagingWithResult(String query, ConsistencyLevel co QueryOptions initialOptions = QueryOptions.create(toCassandraCL(consistencyLevel), boundBBValues, false, - pageSize, + PageSize.inRows(pageSize), null, null, ProtocolVersion.CURRENT, @@ -158,7 +160,7 @@ public boolean hasNext() QueryOptions nextOptions = QueryOptions.create(toCassandraCL(consistencyLevel), boundBBValues, true, - pageSize, + PageSize.inRows(pageSize), rows.result.metadata.getPagingState(), null, ProtocolVersion.CURRENT, diff --git a/test/distributed/org/apache/cassandra/distributed/impl/CoordinatorHelper.java b/test/distributed/org/apache/cassandra/distributed/impl/CoordinatorHelper.java index 414b30e05c61..70bed51816a4 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/CoordinatorHelper.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/CoordinatorHelper.java @@ -24,6 +24,7 @@ import java.util.List; import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.distributed.api.ConsistencyLevel; @@ -68,7 +69,7 @@ public static SimpleQueryResult unsafeExecuteInternal(String query, ConsistencyL QueryOptions.create(toCassandraCL(commitConsistencyLevel), boundBBValues, false, - Integer.MAX_VALUE, + PageSize.NONE, null, toCassandraSerialCL(serialConsistencyLevel), ProtocolVersion.CURRENT, diff --git a/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java b/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java index 6a892c416bb3..28bd058499b8 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java @@ -31,6 +31,7 @@ import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.AbstractNetworkTopologySnitch; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; @@ -39,6 +40,20 @@ public class DistributedTestSnitch extends AbstractNetworkTopologySnitch private static NetworkTopology mapping = null; private static final Map cache = new ConcurrentHashMap<>(); private static final Map cacheInverse = new ConcurrentHashMap<>(); + public static volatile InetAddressAndPort sortByProximityAddressOverride = null; + + public > C sortedByProximity(InetAddressAndPort address, C unsortedAddress) + { + C s; + if (sortByProximityAddressOverride != null) + { + return super.sortedByProximity(sortByProximityAddressOverride, unsortedAddress); + } + else + { + return super.sortedByProximity(address, unsortedAddress); + } + } public static InetAddressAndPort toCassandraInetAddressAndPort(InetSocketAddress addressAndPort) { diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java index 89d7a9c348cc..d12668c58a1f 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java @@ -60,6 +60,7 @@ import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.concurrent.SharedExecutorPool; import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.YamlConfigurationLoader; @@ -75,6 +76,9 @@ import org.apache.cassandra.db.compaction.CompactionLogger; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.memtable.AbstractAllocatorMemtable; +import org.apache.cassandra.db.virtual.SystemViewsKeyspace; +import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; +import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.action.GossipHelper; @@ -112,9 +116,12 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.nodes.Nodes; import org.apache.cassandra.schema.MigrationCoordinator; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.repair.autorepair.AutoRepair; +import org.apache.cassandra.schema.SchemaKeyspace; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.CassandraDaemon; import org.apache.cassandra.service.ClientState; @@ -337,7 +344,9 @@ private void registerMockMessaging(ICluster cluster) MessagingService.instance().outboundSink.add((message, to) -> { if (!internodeMessagingStarted) { - inInstancelogger.debug("Dropping outbound message {} to {} as internode messaging has not been started yet", + // to avoid NPE in case this is called before inInstancelogger is created + if (inInstancelogger != null) + inInstancelogger.debug("Dropping outbound message {} to {} as internode messaging has not been started yet", message, to); return false; } @@ -491,8 +500,10 @@ private SerializableConsumer receiveMessageRunnable(IMessage message) return runOnCaller -> { if (!internodeMessagingStarted) { - inInstancelogger.debug("Dropping inbound message {} to {} as internode messaging has not been started yet", - message, config().broadcastAddress()); + // to avoid NPE in case this is called before inInstancelogger is created + if (inInstancelogger != null) + inInstancelogger.debug("Dropping inbound message {} to {} as internode messaging has not been started yet", + message, config().broadcastAddress()); return; } if (message.version() > MessagingService.current_version) @@ -589,6 +600,8 @@ public void startup(ICluster cluster) inInstancelogger = LoggerFactory.getLogger(Instance.class); try { + JVMStabilityInspector.replaceKiller(new InstanceKiller(Instance.this::shutdown)); + // org.apache.cassandra.distributed.impl.AbstractCluster.startup sets the exception handler for the thread // so extract it to populate ExecutorFactory.Global ExecutorFactory.Global.tryUnsafeSet(new ExecutorFactory.Default(Thread.currentThread().getContextClassLoader(), null, Thread.getDefaultUncaughtExceptionHandler())); @@ -624,6 +637,19 @@ public void startup(ICluster cluster) CassandraDaemon.logSystemInfo(inInstancelogger); CommitLog.instance.start(); + // MessagingService setup needs to be configured before any interaction with Schema because Schema + // uses MessagingService under the hood (it does not need to listen yet, but we need to set filters + // and mocks + if (!config.has(NETWORK)) + { + // Even though we don't use MessagingService, access the static SocketFactory + // instance here so that we start the static event loop state + // -- not sure what that means? SocketFactory.instance.getClass(); + registerMockMessaging(cluster); + } + registerInboundFilter(cluster); + registerOutboundFilter(cluster); + CassandraDaemon.getInstanceForTesting().runStartupChecks(); // We need to persist this as soon as possible after startup checks. @@ -631,6 +657,9 @@ public void startup(ICluster cluster) SystemKeyspace.persistLocalMetadata(config::hostId); SystemKeyspaceMigrator41.migrate(); + VirtualKeyspaceRegistry.instance.register(VirtualSchemaKeyspace.instance); + VirtualKeyspaceRegistry.instance.register(SystemViewsKeyspace.instance); + // Same order to populate tokenMetadata for the first time, // see org.apache.cassandra.service.CassandraDaemon.setup StorageService.instance.populateTokenMetadata(); @@ -656,13 +685,15 @@ public void startup(ICluster cluster) // Replay any CommitLogSegments found on disk try { - CommitLog.instance.recoverSegmentsOnDisk(); + CommitLog.instance.recoverSegmentsOnDiskWithArchive(ColumnFamilyStore.FlushReason.STARTUP); } catch (IOException e) { throw new RuntimeException(e); } + Nodes.getInstance().reload(); + // Re-populate token metadata after commit log recover (new peers might be loaded onto system keyspace #10293) StorageService.instance.populateTokenMetadata(); @@ -678,25 +709,11 @@ public void startup(ICluster cluster) Verb.HINT_REQ.unsafeSetSerializer(DTestSerializer::new); if (config.has(NETWORK)) - { MessagingService.instance().listen(); - } else - { - // Even though we don't use MessagingService, access the static SocketFactory - // instance here so that we start the static event loop state -// -- not sure what that means? SocketFactory.instance.getClass(); - registerMockMessaging(cluster); - } - registerInboundFilter(cluster); - registerOutboundFilter(cluster); - if (!config.has(NETWORK)) - { propagateMessagingVersions(cluster); // fake messaging needs to know messaging version for filters - } - internodeMessagingStarted = true; - JVMStabilityInspector.replaceKiller(new InstanceKiller(Instance.this::shutdown)); + internodeMessagingStarted = true; // TODO: this is more than just gossip StorageService.instance.registerDaemon(CassandraDaemon.getInstanceForTesting()); @@ -715,14 +732,14 @@ public void startup(ICluster cluster) throw new RuntimeException("Unable to bind, run the following in a termanl and try again:\nfor subnet in $(seq 0 5); do for id in $(seq 0 5); do sudo ifconfig lo0 alias \"127.0.$subnet.$id\"; done; done;", e); throw e; } - StorageService.instance.removeShutdownHook(); + JVMStabilityInspector.removeShutdownHooks(); Gossiper.waitToSettle(); } else { Schema.instance.startSync(); - Stream peers = cluster.stream().filter(instance -> ((IInstance) instance).isValid()); + Stream peers = cluster.stream().filter(instance -> ((IInstance) instance).isValid()); SystemKeyspace.setLocalHostId(config.hostId()); if (config.has(BLANK_GOSSIP)) peers.forEach(peer -> GossipHelper.statusToBlank((IInvokableInstance) peer).accept(this)); @@ -763,6 +780,7 @@ else if (cluster instanceof Cluster) } catch (Throwable t) { + startedAt.set(0); if (t instanceof RuntimeException) throw (RuntimeException) t; throw new RuntimeException(t); @@ -840,6 +858,9 @@ public Future shutdown() public Future shutdown(boolean graceful) { inInstancelogger.info("Shutting down instance {} / {}", config.num(), config.broadcastAddress().getHostString()); + if (!CassandraRelevantProperties.UNSAFE_SYSTEM.getBoolean() && !Stage.areMutationExecutorsTerminated()) + flush(SchemaKeyspace.metadata().name); + Future future = async((ExecutorService executor) -> { Throwable error = null; @@ -850,7 +871,7 @@ public Future shutdown(boolean graceful) if (config.has(GOSSIP) || config.has(NETWORK)) { - StorageService.instance.shutdownServer(); + JVMStabilityInspector.removeShutdownHooks(); } error = parallelRun(error, executor, StorageService.instance::disableAutoCompaction); @@ -902,6 +923,7 @@ public Future shutdown(boolean graceful) () -> SSTableReader.shutdownBlocking(1L, MINUTES), () -> shutdownAndWait(Collections.singletonList(ActiveRepairService.repairCommandExecutor())), () -> ActiveRepairService.instance().shutdownNowAndWait(1L, MINUTES), + () -> AutoRepair.instance.shutdownBlocking(), () -> SnapshotManager.shutdownAndWait(1L, MINUTES) ); @@ -955,6 +977,11 @@ public Future shutdown(boolean graceful) { super.shutdown(); startedAt.set(0L); + + // when the instance is eventually stopped, we need to release buffer pools manually + // they are assumed to gone along with JVM, but this is not the case in dtests + BufferPools.forNetworking().unsafeReset(true); + BufferPools.forChunkCache().unsafeReset(true); } }); } diff --git a/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java b/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java index adb9bc35db79..c6912fc152f4 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java @@ -30,6 +30,12 @@ import java.util.function.Function; import java.util.stream.Collectors; +import com.google.common.base.Splitter; +import com.google.common.net.HostAndPort; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.vdurmont.semver4j.Semver; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.distributed.api.Feature; @@ -39,8 +45,12 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.SimpleSeedProvider; +import org.apache.commons.lang3.ObjectUtils; + public class InstanceConfig implements IInstanceConfig { + private static final Logger logger = LoggerFactory.getLogger(InstanceConfig.class); + public final int num; private final int jmxPort; @@ -72,6 +82,7 @@ private InstanceConfig(int num, String commitlog_directory, String hints_directory, String cdc_raw_directory, + String metadata_directory, Collection initial_token, int storage_port, int native_transport_port, @@ -92,6 +103,7 @@ private InstanceConfig(int num, .set("commitlog_directory", commitlog_directory) .set("hints_directory", hints_directory) .set("cdc_raw_directory", cdc_raw_directory) + .set("metadata_directory", metadata_directory) .set("partitioner", "org.apache.cassandra.dht.Murmur3Partitioner") .set("start_native_transport", true) .set("concurrent_writes", 2) @@ -162,7 +174,8 @@ private InstanceConfig(int num, .set("default_secondary_index", "sai") .set("default_secondary_index_enabled", "true") - .set("storage_compatibility_mode", "NONE"); + // Respect TEST_STORAGE_COMPATIBILITY_MODE system property (defaults to HCD_1 in tests) + .set("storage_compatibility_mode", CassandraRelevantProperties.TEST_STORAGE_COMPATIBILITY_MODE.getString()); } this.featureFlags = EnumSet.noneOf(Feature.class); this.jmxPort = jmx_port; @@ -325,6 +338,7 @@ public static InstanceConfig generate(int nodeNum, String.format("%s/node%d/commitlog", root, nodeNum), String.format("%s/node%d/hints", root, nodeNum), String.format("%s/node%d/cdc", root, nodeNum), + String.format("%s/node%d/metadata", root, nodeNum), tokens, provisionStrategy.storagePort(nodeNum), provisionStrategy.nativeTransportPort(nodeNum), @@ -342,13 +356,28 @@ private static String[] datadirs(int datadirCount, Path root, int nodeNum) public InstanceConfig forVersion(Semver version) { + ParameterizedClass seedProviderConfig = (ParameterizedClass) params.get("seed_provider"); + // Versions before 4.0 need to set 'seed_provider' without specifying the port - if (UpgradeTestBase.v40.compareTo(version) < 0) + // the extra comparison to strict version is due to a bug in Semver (see STAR-871) + if (version.isGreaterThanOrEqualTo(UpgradeTestBase.v40) || version.isGreaterThanOrEqualTo(UpgradeTestBase.v40.toStrict()) || seedProviderConfig == null) return this; - else - return new InstanceConfig(this) - .set("seed_provider", new ParameterizedClass(SimpleSeedProvider.class.getName(), - Collections.singletonMap("seeds", "127.0.0.1"))); + + assert ObjectUtils.equals(seedProviderConfig.class_name, SimpleSeedProvider.class.getName()); + String seedsStr = seedProviderConfig.parameters.get("seeds"); + assert seedsStr != null; + seedsStr = Splitter.on(',') + .omitEmptyStrings() + .trimResults() + .splitToList(seedsStr) + .stream() + .map(str -> HostAndPort.fromString(str).getHost()) + .collect(Collectors.joining(",")); + + seedProviderConfig = new ParameterizedClass(seedProviderConfig.class_name, Collections.singletonMap("seeds", seedsStr)); + + logger.warn("Stripping ports from seed addresses because the version {} is < {}, new seeds list is: {}", version, UpgradeTestBase.v40, seedsStr); + return new InstanceConfig(this).set("seed_provider", seedProviderConfig); } public String toString() diff --git a/test/distributed/org/apache/cassandra/distributed/impl/InstanceKiller.java b/test/distributed/org/apache/cassandra/distributed/impl/InstanceKiller.java index 38b045b381dc..4e24e8b76bcd 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/InstanceKiller.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/InstanceKiller.java @@ -45,7 +45,7 @@ public static void clear() } @Override - protected void killCurrentJVM(Throwable t, boolean quiet) + public void killJVM(Throwable t, boolean quiet) { KILL_ATTEMPTS.incrementAndGet(); onKill.accept(quiet); diff --git a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java index 41a722a3d1a6..13907dc8e840 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java @@ -48,7 +48,6 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; import static org.apache.cassandra.config.CassandraRelevantProperties.SUN_RMI_TRANSPORT_TCP_THREADKEEPALIVETIME; import static org.apache.cassandra.distributed.api.Feature.JMX; -import static org.apache.cassandra.utils.ReflectionUtils.clearMapField; public class IsolatedJmx { @@ -205,7 +204,7 @@ public void stopJmx() // make sure to remove the reference to them when the instance is shutting down. // Additionally, we must make sure to only clear endpoints created by this instance // As clearning the entire map can cause issues with starting and stopping nodes mid-test. - clearMapField(TCPEndpoint.class, null, "localEndpoints", this::endpointCreateByThisInstance); +// clearMapField(TCPEndpoint.class, null, "localEndpoints", this::endpointCreateByThisInstance); Uninterruptibles.sleepUninterruptibly(2 * RMI_KEEPALIVE_TIME, TimeUnit.MILLISECONDS); // Double the keep-alive time to give Distributed GC some time to clean up } diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Query.java b/test/distributed/org/apache/cassandra/distributed/impl/Query.java index 57aefe3a816b..b284a93d6882 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Query.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Query.java @@ -23,6 +23,7 @@ import java.util.List; import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.PageSize; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.db.ConsistencyLevel; @@ -75,7 +76,7 @@ public Object[][] call() QueryOptions.create(commitConsistency, boundBBValues, false, - Integer.MAX_VALUE, + PageSize.NONE, null, serialConsistency, ProtocolVersion.V4, diff --git a/test/distributed/org/apache/cassandra/distributed/impl/TracingUtil.java b/test/distributed/org/apache/cassandra/distributed/impl/TracingUtil.java index 9c347e07a276..f62247463bb4 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/TracingUtil.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/TracingUtil.java @@ -27,6 +27,8 @@ import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.utils.TimeUUID; +import static org.apache.cassandra.config.CassandraRelevantProperties.WAIT_FOR_TRACING_EVENTS_TIMEOUT_SECS; + /** * Utilities for accessing the system_traces table from in-JVM dtests */ @@ -103,4 +105,12 @@ public static List getTraces(AbstractCluster cluster, ConsistencyLev } return traces; } + + // Set up the wait for tracing time system property, returning the previous value. + // Handles being called again to reset with the original value, replacing the null + // with the default value. + public static String setWaitForTracingEventTimeoutSecs(String timeoutInSeconds) + { + return WAIT_FOR_TRACING_EVENTS_TIMEOUT_SECS.setString(timeoutInSeconds == null ? "0" : timeoutInSeconds); + } } diff --git a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java index 4de180636c89..e8d2b2a0222d 100644 --- a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java +++ b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java @@ -37,6 +37,7 @@ import org.apache.cassandra.locator.EndpointSnitchInfo; import org.apache.cassandra.locator.EndpointSnitchInfoMBean; import org.apache.cassandra.metrics.CassandraMetricsRegistry; +import org.apache.cassandra.metrics.CompactionMetrics; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.CacheService; @@ -160,7 +161,36 @@ public CassandraMetricsRegistry.JmxTimerMBean getMessagingQueueWaitMetrics(Strin @Override public Object getCompactionMetric(String metricName) { - throw new UnsupportedOperationException(); + CompactionMetrics metrics = CompactionManager.instance.getMetrics(); + switch(metricName) + { + case "BytesCompacted": + return metrics.bytesCompacted; + case "CompletedTasks": + return metrics.completedTasks.getValue(); + case "CompactionsAborted": + return metrics.compactionsAborted; + case "CompactionsReduced": + return metrics.compactionsReduced; + case "PendingTasks": + return metrics.pendingTasks.getValue(); + case "PendingTasksByTableName": + return metrics.pendingTasksByTableName.getValue(); + case "WriteAmplificationByTableName": + return metrics.writeAmplificationByTableName.getValue(); + case "AggregateCompactions": + return metrics.aggregateCompactions.getValue(); + case "MaxOverlapsMap": + return metrics.overlapsMap.getValue(); + case "SSTablesDroppedFromCompaction": + return metrics.sstablesDropppedFromCompactions; + case "TotalCompactionsCompleted": + return metrics.totalCompactionsCompleted; + case "CompressedBytesCompacted": + return metrics.compressedBytesCompacted; + default: + throw new RuntimeException("Unknown compaction metric: " + metricName); + } } @Override diff --git a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java index 3bded5cd1605..175292f04d02 100644 --- a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java +++ b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java @@ -41,14 +41,10 @@ import java.util.stream.Collectors; import com.google.common.util.concurrent.Futures; - -import org.apache.cassandra.distributed.api.Feature; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.VersionedValue; -import org.apache.cassandra.io.util.File; import org.junit.Assert; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInstanceConfig; @@ -57,6 +53,9 @@ import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.impl.AbstractCluster; import org.apache.cassandra.distributed.impl.InstanceConfig; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.io.util.File; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tools.SystemExitException; import org.apache.cassandra.utils.FBUtilities; @@ -744,6 +743,19 @@ public static List getDataDirectories(IInstance instance) return files; } + /** + * Get the metadata directory where the information about nodes is stored. + * + * @param instance to get the metadata directory for + * @return metadata directory + */ + public static File getMetadataDirectory(IInstance instance) + { + IInstanceConfig conf = instance.config(); + String d = conf.getString("metadata_directory"); + return new File(d); + } + /** * Get the commit log directory for the given instance. * @@ -802,6 +814,7 @@ public static List getDirectories(IInstance instance) out.add(getCommitLogDirectory(instance)); out.add(getHintsDirectory(instance)); out.add(getSavedCachesDirectory(instance)); + // out.add(getMetadataDirectory(instance)); return out; } diff --git a/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java b/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java index d17d3e6f3af4..fc1864d8a07c 100644 --- a/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java +++ b/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java @@ -52,6 +52,15 @@ public WithProperties set(CassandraRelevantProperties prop, String value) return set(prop, () -> prop.setString(value)); } + public WithProperties clear(CassandraRelevantProperties prop) + { + return set(prop, () -> { + String prev = prop.getString(); + prop.clearValue(); // checkstyle: suppress nearby 'clearValueSystemPropertyUsage' + return prev; + }); + } + public WithProperties set(CassandraRelevantProperties prop, String... values) { return set(prop, Arrays.asList(values)); diff --git a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java index b48886743308..02c01f909322 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java @@ -48,8 +48,6 @@ import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.FutureListener; import org.apache.cassandra.config.EncryptionOptions; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.security.ISslContextFactory; import org.apache.cassandra.security.SSLFactory; @@ -335,27 +333,4 @@ void assertReceivedHandshakeException() lastThrowable.getCause() instanceof SSLHandshakeException); } } - - /* Provde the cluster cannot start with the configured options */ - void assertCannotStartDueToConfigurationException(Cluster cluster) - { - Throwable tr = null; - try - { - cluster.startup(); - } - catch (Throwable maybeConfigException) - { - tr = maybeConfigException; - } - - if (tr == null) - { - Assert.fail("Expected a ConfigurationException"); - } - else - { - Assert.assertEquals(ConfigurationException.class.getName(), tr.getClass().getName()); - } - } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java b/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java index 67cf4c79fb52..886dc8c271d9 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java @@ -29,6 +29,7 @@ import com.datastax.driver.core.PlainTextAuthProvider; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; +import com.datastax.driver.core.exceptions.AuthenticationException; import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; @@ -36,17 +37,23 @@ import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IMessageFilters.Filter; +import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.api.TokenSupplier; -import org.apache.cassandra.distributed.util.Auth; import org.apache.cassandra.locator.SimpleSeedProvider; import org.apache.cassandra.service.StorageService; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_DEFAULT_ROLE_SETUP; +import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ONE; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.distributed.util.Auth.waitForExistingRoles; import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class AuthTest extends TestBaseImpl @@ -82,7 +89,7 @@ public void testZeroTimestampForDefaultRoleCreation() throws Exception .set("authenticator", "PasswordAuthenticator")) .start()) { - Auth.waitForExistingRoles(cluster.get(1)); + waitForExistingRoles(cluster.get(1)); long writeTime = getPasswordWritetime(cluster.coordinator(1)); // TIMESTAMP 0 in action @@ -101,7 +108,7 @@ public void testZeroTimestampForDefaultRoleCreation() throws Exception Filter from = cluster.filters().allVerbs().outbound().drop(); secondNode.startup(); - Auth.waitForExistingRoles(secondNode); + waitForExistingRoles(secondNode); long passwordWritetimeOnSecondNode = getPasswordWritetime(cluster.coordinator(2)); @@ -155,6 +162,42 @@ public void testZeroTimestampForDefaultRoleCreation() throws Exception } } + @Test + public void testSkipDefaultRoleCreation() throws Exception + { + try (Cluster cluster = builder().withDCs(1) + .withNodes(1) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(1, 1)) + .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL) + .with() + .set("authenticator", "PasswordAuthenticator")) + .createWithoutStarting()) // don't start the cluster yet as we need to set the skip_default_role_setup property first + { + withProperty(SKIP_DEFAULT_ROLE_SETUP, true, + cluster::startup); + + waitForExistingRoles(cluster.get(1)); + + long writeTime = getPasswordWritetime(cluster.coordinator(1)); + // TIMESTAMP 1 when skip_default_role_setup is true + assertEquals(1, writeTime); + + String defaultRoleQuery = "select is_superuser, can_login, salted_hash from system_auth.roles where role = 'cassandra'"; + SimpleQueryResult result = cluster.coordinator(1).executeWithResult(defaultRoleQuery, ONE); + assertTrue(result.hasNext()); + org.apache.cassandra.distributed.api.Row row = result.next(); + assertFalse(row.get("is_superuser")); + assertFalse(row.get("can_login")); + assertEquals("", row.get("salted_hash")); + + // make sure SU cannot really login + assertThrows(AuthenticationException.class, () -> doWithSession("127.0.0.1", + "datacenter1", + "cassandra", + session -> session.execute(defaultRoleQuery))); + } + } + private IInvokableInstance getSecondNode(Cluster cluster) { IInstanceConfig config = cluster.newInstanceConfig(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/BTreeFastBuilderContaminationTest.java b/test/distributed/org/apache/cassandra/distributed/test/BTreeFastBuilderContaminationTest.java new file mode 100644 index 000000000000..a82bd46c2474 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/BTreeFastBuilderContaminationTest.java @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.util.List; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.shared.ShutdownException; +import org.apache.cassandra.net.Verb; + +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.junit.Assert.fail; + +public class BTreeFastBuilderContaminationTest extends TestBaseImpl +{ + // 4200 columns * ~18 bytes/name > 64KB large-message threshold + // READ_REQ is deserialized on SEPWorker threads, not Netty event loop. + private static final int NUM_WIDE_COLUMNS = 4200; + + // Small-message scenario: both READ_REQ and MUTATION_REQ stay under 64KB + // Messages are deserialized on Netty event loop threads. + private static final int NUM_SMALL_SOURCE_COLUMNS = 150; // >31 to trigger FastBuilder overflow + private static final int NUM_SMALL_VICTIM_COLUMNS = 2000; + + private static final int NUM_PARTITIONS = 200; + private static final int NUM_DELETE_PARTITIONS = 300; + + // Verify CASSANDRA-21216/CASSANDRA-21260 fix: stale ColumnMetadata from a failed + // READ_REQ deserialization must not leak into a Row BTree during mutation, which can + // cause ClassCastException. Source table is wide (~4200 columns), so READ_REQ exceeds + // 64KB and is deserialized on SEPWorker. Victim table is narrow; without the fix, + // corruption can happen via BTree.updateLeaves() during mutation execution on the + // same SEPWorker thread (SharedExecutorPool threads hop between stages). + @Test + public void testSchemaDisagreementCorruptsPartitionViaFastBuilder() throws Throwable + { + try (Cluster cluster = init(builder().withNodes(2) + .withConfig(config -> { + config.with(NETWORK, GOSSIP); + config.set("concurrent_reads", 2); + config.set("concurrent_writes", 2); + config.set("read_request_timeout_in_ms", 5000L); + config.set("write_request_timeout_in_ms", 5000L); + }) + .start())) + { + createWideSourceTable(cluster); + + cluster.schemaChange(withKeyspace( + "CREATE TABLE %s.victim (pk int, ck int, v text, PRIMARY KEY (pk, ck))")); + + cluster.coordinator(1).execute( + withKeyspace("INSERT INTO %s.source (pk, src_wide_col_0000) VALUES (1, 42)"), ALL); + + for (int pk = 0; pk < NUM_PARTITIONS; pk++) + cluster.get(2).executeInternal(withKeyspace( + "INSERT INTO %s.victim (pk, ck, v) VALUES (" + pk + ", 1, 'seed')")); + + createSchemaDisagreement(cluster); + poisonFastBuilder(cluster); + + for (int pk = 0; pk < NUM_PARTITIONS; pk++) + { + try + { + cluster.coordinator(1).execute(withKeyspace( + "INSERT INTO %s.victim (pk, ck, v) VALUES (" + pk + ", 2, 'probe')"), ALL); + } + catch (Exception e) + { + if (rootCauseIs(e, ClassCastException.class)) + fail("ClassCastException from corrupted partition BTree (CASSANDRA-21216): " + e.getMessage()); + } + } + + for (int pk = 0; pk < NUM_PARTITIONS; pk++) + { + try + { + cluster.coordinator(1).execute(withKeyspace( + "SELECT * FROM %s.victim WHERE pk = " + pk), ALL); + } + catch (Exception e) + { + if (rootCauseIs(e, ClassCastException.class)) + fail("ClassCastException from corrupted partition BTree (CASSANDRA-21216): " + e.getMessage()); + } + } + + try + { + cluster.get(2).flush(KEYSPACE); + } + catch (Exception e) + { + if (rootCauseIs(e, ClassCastException.class)) + fail("ClassCastException from corrupted partition BTree (CASSANDRA-21216): " + e.getMessage()); + } + } + catch (ShutdownException e) + { + if (rootCauseIs(e, ClassCastException.class)) + fail("ClassCastException from corrupted partition BTree during shutdown (CASSANDRA-21216): " + e.getMessage()); + throw e; + } + } + + // Verify CASSANDRA-21260 fix: SSTable header must not be contaminated via small + // messages on Netty event loop. + // Source: 150 columns (>31 -> FastBuilder overflow) but only ~3KB -> small message. + // Victim: 2000 columns, but partition DELETE has empty updatedColumns, so the message is tiny. + // Both deserialized on the same Netty event loop thread (channel-to-EventLoop binding). + // Without the fix, the poisoned FastBuilder is reused for the victim's SerializationHeader + // deserialization. + @Test + public void testSmallMessageContaminatesSSTableHeaderViaNettyEventLoop() throws Throwable + { + try (Cluster cluster = init(builder().withNodes(2) + .withConfig(config -> { + config.with(NETWORK, GOSSIP); + config.set("read_request_timeout_in_ms", 5000L); + config.set("write_request_timeout_in_ms", 5000L); + }) + .start())) + { + createTable(cluster, "source", NUM_SMALL_SOURCE_COLUMNS, "src_col"); + createTable(cluster, "victim", NUM_SMALL_VICTIM_COLUMNS, "vic_col"); + + createSchemaDisagreement(cluster); + poisonFastBuilder(cluster); + + // Partition deletions to the victim table. Despite the victim having 2000 columns, + // a partition-level DELETE has empty updatedColumns (no column operations), so + // the MUTATION_REQ is tiny. It is deserialized on the same Netty event loop thread + // that handled the failed READ_REQ. The poisoned FastBuilder's stale savedBuffer + // is drained even though 0 new columns are added; build() calls propagateOverflow() + // when hasOverflow() is true from the previous use. + int batchSize = NUM_DELETE_PARTITIONS / 5; + for (int round = 0; round < 5; round++) + { + if (round > 0) + poisonFastBuilder(cluster); + + for (int pk = round * batchSize; pk < (round + 1) * batchSize; pk++) + { + try + { + cluster.coordinator(1).execute(withKeyspace( + "DELETE FROM %s.victim WHERE pk = " + pk), ALL); + } + catch (Exception ignored) + { + } + } + } + + cluster.get(2).flush(KEYSPACE); + + List foreignColumns = cluster.get(2).callOnInstance(() -> { + java.util.List result = new java.util.ArrayList<>(); + org.apache.cassandra.db.ColumnFamilyStore cfs = + org.apache.cassandra.db.ColumnFamilyStore.getIfExists(KEYSPACE, "victim"); + if (cfs == null) + return result; + org.apache.cassandra.schema.TableMetadata metadata = cfs.metadata.get(); + for (org.apache.cassandra.io.sstable.format.SSTableReader sstable : cfs.getLiveSSTables()) + { + try + { + org.apache.cassandra.db.SerializationHeader.Component header = + (org.apache.cassandra.db.SerializationHeader.Component) + sstable.descriptor.getMetadataSerializer() + .deserialize(sstable.descriptor, + org.apache.cassandra.io.sstable.metadata.MetadataType.HEADER); + result.addAll(getUnknownColumns(header, metadata)); + } + catch (Exception e) + { + result.add("ERROR reading header: " + e.getMessage()); + } + } + return result; + }); + + if (!foreignColumns.isEmpty()) + fail("SSTable header contamination detected (CASSANDRA-21260): foreign columns " + + "found in victim's SSTable header: " + foreignColumns); + } + } + + private void createTable(Cluster cluster, String tableName, int numColumns, String columnPrefix) + { + StringBuilder ddl = new StringBuilder( + withKeyspace("CREATE TABLE %s." + tableName + " (pk int PRIMARY KEY")); + for (int i = 0; i < numColumns; i++) + ddl.append(String.format(", %s_%04d int", columnPrefix, i)); + ddl.append(')'); + cluster.schemaChange(ddl.toString()); + } + + // Wide source table: 4200 columns * ~18 bytes/name > 64KB large-message threshold + private void createWideSourceTable(Cluster cluster) + { + createTable(cluster, "source", NUM_WIDE_COLUMNS, "src_wide_col"); + } + + private void createSchemaDisagreement(Cluster cluster) + { + cluster.filters().verbs(Verb.SCHEMA_PUSH_REQ.id).from(1).to(2).drop(); + cluster.filters().verbs(Verb.SCHEMA_PULL_RSP.id).from(1).to(2).drop(); + cluster.filters().verbs(Verb.SCHEMA_VERSION_RSP.id).from(1).to(2).drop(); + + cluster.get(1).schemaChangeInternal( + withKeyspace("ALTER TABLE %s.source ADD zzz_new_col text")); + } + + // Trigger a failed READ_REQ on node2 (schema disagreement), poisoning the + // deserializing thread's FastBuilder with stale savedBuffer/savedNextKey. + private void poisonFastBuilder(Cluster cluster) + { + try + { + cluster.coordinator(1).execute( + withKeyspace("SELECT * FROM %s.source WHERE pk = 1"), ALL); + } + catch (Exception e) + { + // Expected: schema disagreement causes unknown column exception on node2 + } + } + + // Check for columns in an SSTable header that don't belong to the table's schema. + private static java.util.List getUnknownColumns( + org.apache.cassandra.db.SerializationHeader.Component header, + org.apache.cassandra.schema.TableMetadata metadata) + { + java.util.List unknownColumns = new java.util.ArrayList<>(); + java.util.Map>[] maps = + new java.util.Map[] { header.getStaticColumns(), header.getRegularColumns() }; + boolean[] isStatic = { true, false }; + for (int i = 0; i < maps.length; i++) + { + for (java.nio.ByteBuffer name : maps[i].keySet()) + { + org.apache.cassandra.schema.ColumnMetadata column = metadata.getColumn(name); + if (column == null || column.isStatic() != isStatic[i]) + { + column = metadata.getDroppedColumn(name, isStatic[i]); + if (column == null) + { + unknownColumns.add(org.apache.cassandra.db.marshal.UTF8Type.instance.getString(name)); + } + } + } + } + return unknownColumns; + } + + private static boolean rootCauseIs(Throwable t, Class type) + { + while (t != null) + { + if (type.isInstance(t)) + return true; + for (Throwable suppressed : t.getSuppressed()) + { + if (rootCauseIs(suppressed, type)) + return true; + } + t = t.getCause(); + } + return false; + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/CasWriteTest.java b/test/distributed/org/apache/cassandra/distributed/test/CasWriteTest.java index d6bc4a39b2be..854bcf2da57f 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CasWriteTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CasWriteTest.java @@ -63,7 +63,6 @@ import org.apache.cassandra.exceptions.CasWriteUnknownResultException; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.net.Verb; -import org.apache.cassandra.notifications.SSTableMetadataChanged; import org.apache.cassandra.service.paxos.Ballot; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.TimeUUID; @@ -339,7 +338,7 @@ public void testStaleCommitInSystemPaxos() throws InterruptedException { StatsMetadata oldMetadata = s.getSSTableMetadata(); s.mutateLevelAndReload(3); - cfs.getCompactionStrategyManager().handleNotification(new SSTableMetadataChanged(s, oldMetadata), null); +// cfs.getCompactionStrategyContainer().handleNotification(new SSTableMetadataChanged(s, oldMetadata), null); } catch (Throwable t) { @@ -380,7 +379,7 @@ public void testStaleCommitInSystemPaxos() throws InterruptedException { ((IInvokableInstance)cluster.get(k)).runOnInstance(() -> { ColumnFamilyStore cfs = Keyspace.open("system").getColumnFamilyStore("paxos"); - while (cfs.getCompactionStrategyManager().getEstimatedRemainingTasks() > 0) + while (cfs.getCompactionStrategy().getEstimatedRemainingTasks() > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/CompactionControllerConfigTest.java b/test/distributed/org/apache/cassandra/distributed/test/CompactionControllerConfigTest.java new file mode 100644 index 000000000000..a7982584fdd0 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/CompactionControllerConfigTest.java @@ -0,0 +1,368 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.util.Arrays; +import java.util.function.Consumer; + +import org.junit.Test; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.UnifiedCompactionContainer; +import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; +import org.apache.cassandra.db.compaction.unified.AdaptiveController; +import org.apache.cassandra.db.compaction.unified.Controller; +import org.apache.cassandra.db.compaction.unified.StaticController; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.schema.TableMetadata; + + +import static org.apache.cassandra.config.CassandraRelevantProperties.UCS_OVERRIDE_UCS_CONFIG_FOR_VECTOR_TABLES; +import static org.apache.cassandra.distributed.shared.FutureUtils.waitOn; + +import static org.apache.cassandra.SchemaLoader.standardCFMD; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CompactionControllerConfigTest extends TestBaseImpl +{ + + private static final String quiteLongkeyspaceName = "g38373639353166362d356631322d343864652d393063362d653862616534343165333764_tpch"; + private static final String longTableName = "test_create_k8yq1r75bpzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"; + + @Test + public void storedAdaptiveCompactionOptionsTest() throws Throwable + { + try(Cluster cluster = init(Cluster.build(1).start())) + { + cluster.schemaChange(withKeyspace("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks.tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'true'};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks.tbl2 (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'true'};")); + cluster.get(1).runOnInstance(() -> + { + ColumnFamilyStore cfs = Keyspace.open("ks").getColumnFamilyStore("tbl"); + ColumnFamilyStore cfs2 = Keyspace.open("ks").getColumnFamilyStore("tbl2"); + UnifiedCompactionContainer container = (UnifiedCompactionContainer) cfs.getCompactionStrategy(); + UnifiedCompactionStrategy ucs = (UnifiedCompactionStrategy) container.getStrategies().get(0); + Controller controller = ucs.getController(); + assertTrue(controller instanceof AdaptiveController); + //scaling parameter on L0 should be 0 to start + assertEquals(0, controller.getScalingParameter(0)); + + //manually write new scaling parameters and flushSizeBytes to see if they are picked up on restart + int[] scalingParameters = new int[32]; + Arrays.fill(scalingParameters, 5); + AdaptiveController.storeOptions(cfs.metadata(), scalingParameters, 10 << 20); + + + //write different scaling parameters to second table to make sure each table keeps its own configuration + Arrays.fill(scalingParameters, 8); + AdaptiveController.storeOptions(cfs2.metadata(), scalingParameters, 10 << 20); + }); + waitOn(cluster.get(1).shutdown()); + cluster.get(1).startup(); + + cluster.get(1).runOnInstance(() -> + { + ColumnFamilyStore cfs = Keyspace.open("ks").getColumnFamilyStore("tbl"); + UnifiedCompactionContainer container = (UnifiedCompactionContainer) cfs.getCompactionStrategy(); + UnifiedCompactionStrategy ucs = (UnifiedCompactionStrategy) container.getStrategies().get(0); + Controller controller = ucs.getController(); + assertTrue(controller instanceof AdaptiveController); + //when the node is restarted, it should see the new configuration that was manually written + assertEquals(5, controller.getScalingParameter(0)); + assertEquals(10 << 20, controller.getFlushSizeBytes()); + + ColumnFamilyStore cfs2 = Keyspace.open("ks").getColumnFamilyStore("tbl2"); + UnifiedCompactionContainer container2 = (UnifiedCompactionContainer) cfs2.getCompactionStrategy(); + UnifiedCompactionStrategy ucs2 = (UnifiedCompactionStrategy) container2.getStrategies().get(0); + Controller controller2 = ucs2.getController(); + assertTrue(controller2 instanceof AdaptiveController); + //when the node is restarted, it should see the new configuration that was manually written + assertEquals(8, controller2.getScalingParameter(0)); + assertEquals(10 << 20, controller2.getFlushSizeBytes()); + }); + } + } + + @Test + public void storedStaticCompactionOptionsTest() throws Throwable + { + try(Cluster cluster = init(Cluster.build(1).start())) + { + cluster.schemaChange(withKeyspace("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks.tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'false', " + + "'scaling_parameters': '0'};")); + cluster.get(1).runOnInstance(() -> + { + ColumnFamilyStore cfs = Keyspace.open("ks").getColumnFamilyStore("tbl"); + UnifiedCompactionContainer container = (UnifiedCompactionContainer) cfs.getCompactionStrategy(); + UnifiedCompactionStrategy ucs = (UnifiedCompactionStrategy) container.getStrategies().get(0); + Controller controller = ucs.getController(); + assertTrue(controller instanceof StaticController); + //scaling parameter on L0 should be 0 to start + assertEquals(0, controller.getScalingParameter(0)); + + //manually write new flushSizeBytes to see if it is picked up on restart + int[] scalingParameters = new int[32]; + Arrays.fill(scalingParameters, 0); + AdaptiveController.storeOptions(cfs.metadata(), scalingParameters, 10 << 20); + }); + waitOn(cluster.get(1).shutdown()); + cluster.get(1).startup(); + + cluster.get(1).runOnInstance(() -> + { + ColumnFamilyStore cfs = Keyspace.open("ks").getColumnFamilyStore("tbl"); + UnifiedCompactionContainer container = (UnifiedCompactionContainer) cfs.getCompactionStrategy(); + UnifiedCompactionStrategy ucs = (UnifiedCompactionStrategy) container.getStrategies().get(0); + Controller controller = ucs.getController(); + assertTrue(controller instanceof StaticController); + //when the node is restarted, it should see the new configuration that was manually written + assertEquals(10 << 20, controller.getFlushSizeBytes()); + }); + } + } + + @Test + public void testStoreAndCleanupControllerConfig() throws Throwable + { + try(Cluster cluster = init(Cluster.build(1).start())) + { + cluster.schemaChange(withKeyspace("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks.tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'false', " + + "'scaling_parameters': '0'};")); + cluster.schemaChange(withKeyspace("CREATE KEYSPACE ks2 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks2.tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'false', " + + "'scaling_parameters': '0'};")); + cluster.schemaChange(withKeyspace("CREATE KEYSPACE "+quiteLongkeyspaceName+" WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); + + + cluster.get(1).runOnInstance(() -> + { + //logs should show that scaling parameters and flush size are written to a file for each table + CompactionManager.storeControllerConfig(); + TableMetadata metadata = standardCFMD("does_not", "exist").build(); + + //store controller config for a table that does not exist to see if it is removed by the cleanup method + int[] scalingParameters = new int[32]; + Arrays.fill(scalingParameters, 5); + + AdaptiveController.storeOptions(metadata, scalingParameters, 10 << 20); + + //verify that the file was created + assert Controller.getControllerConfigPath(metadata).exists(); + + //cleanup method should remove the file corresponding to the table "does_not.exist" + CompactionManager.cleanupControllerConfig(); + + //verify that the file was deleted + assert !Controller.getControllerConfigPath(metadata).exists(); + + // Verify that existing tables' controller config files were not deleted + assertThat(Controller.getControllerConfigPath(ColumnFamilyStore.getIfExists("ks", "tbl").metadata()).toJavaIOFile()).exists(); + assertThat(Controller.getControllerConfigPath(ColumnFamilyStore.getIfExists("ks2", "tbl").metadata()).toJavaIOFile()).exists(); + + }); + + } + } + + @Test + public void testStoreLongTableName() throws Throwable + { + try (Cluster cluster = init(Cluster.build(1).start())) + { + cluster.get(1).runOnInstance(() -> + { + CompactionManager.storeControllerConfig(); + + // try to store controller config for a table with a long name + TableMetadata metadata = standardCFMD(quiteLongkeyspaceName, longTableName).build(); + int[] scalingParameters = new int[32]; + Arrays.fill(scalingParameters, 5); + AdaptiveController.storeOptions(metadata, scalingParameters, 10 << 20); + + // verify that the file WAS created (CNDB-12972) + assert Controller.getControllerConfigPath(metadata).exists(); + + CompactionManager.cleanupControllerConfig(); + + assert !Controller.getControllerConfigPath(metadata).exists(); // table not really exists + }); + } + } + + @Test + public void testVectorControllerConfig() throws Throwable + { + vectorControllerConfig(true); + vectorControllerConfig(false); + } + + /** + * Test to reproduce the bug where orphaned controller-config.JSON files + * cause IllegalArgumentException during node restart. + * + * The bug occurs when: + * 1. A table with UCS compaction strategy is created + * 2. Data is written and flushed + * 3. The UCS config file is saved + * 4. Table is dropped (file is NOT deleted) + * 5. Node is restarted (cleanupControllerConfig() throws IllegalArgumentException) + */ + @Test + public void testDropTableOrphanedControllerConfigFileCleanup() throws Throwable + { + testOrphanedControllerConfigFileCleanup(cluster -> cluster.schemaChange("DROP TABLE test_ks.test_table;")); + } + + /** + * Same as testDropTableOrphanedControllerConfigFileCleanup but for dropping keyspace + */ + @Test + public void testDropKeyspaceOrphanedControllerConfigFileCleanup() throws Throwable + { + testOrphanedControllerConfigFileCleanup(cluster -> cluster.schemaChange("DROP KEYSPACE test_ks;")); + } + + private void testOrphanedControllerConfigFileCleanup(Consumer schemaRemover) throws Throwable + { + try (Cluster cluster = init(Cluster.build(1).start())) + { + // create keyspace and table with UCS compaction strategy + cluster.schemaChange(withKeyspace("CREATE KEYSPACE test_ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};")); + cluster.schemaChange(withKeyspace("CREATE TABLE test_ks.test_table (pk int PRIMARY KEY, v int) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', 'adaptive': 'true'};")); + + // insert data and flush to trigger controller-config.JSON creation + for (int i = 0; i < 100; i++) + { + cluster.coordinator(1).execute(withKeyspace("INSERT INTO test_ks.test_table (pk, v) VALUES (?, ?)"), + ConsistencyLevel.ONE, i, i); + } + + cluster.get(1).flush("test_ks"); + + // store the controller-config.JSON file and verify it exists + cluster.get(1).runOnInstance(() -> { + CompactionManager.storeControllerConfig(); + TableMetadata metadata = TableMetadata.minimal("test_ks", "test_table"); + assertTrue("Controller config file should exist after flush", + Controller.getControllerConfigPath(metadata).exists()); + }); + + cluster.get(1).forceCompact("test_ks", "test_table"); + + // drop the schema - this should delete the controller-config.JSON file but currently doesn't + schemaRemover.accept(cluster); + + // verify the orphaned file still exists + cluster.get(1).runOnInstance(() -> { + // This assertion will pass, showing the file is orphaned + TableMetadata metadata = TableMetadata.minimal("test_ks", "test_table"); + assertTrue("Controller config file is orphaned after table drop", + Controller.getControllerConfigPath(metadata).exists()); + }); + + // when + // stopping the node + waitOn(cluster.get(1).shutdown()); + + // then + // starting the node again should succeed + cluster.get(1).startup(); + // after restart, the orphaned file should be cleaned up + cluster.get(1).runOnInstance(() -> { + // we are calling storeControllerConfig by hand as it's first call might be delayed + CompactionManager.storeControllerConfig(); + TableMetadata metadata = TableMetadata.minimal("test_ks", "test_table"); + assertFalse("Controller config file should be deleted after restart cleanup", + Controller.getControllerConfigPath(metadata).exists()); + }); + } + } + + public void vectorControllerConfig(boolean vectorOverride) throws Throwable + { + UCS_OVERRIDE_UCS_CONFIG_FOR_VECTOR_TABLES.setBoolean(vectorOverride); + try(Cluster cluster = init(Cluster.build(1).start())) + { + cluster.schemaChange(withKeyspace("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks.tbl (pk int, ck int, val vector, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'false', " + + "'scaling_parameters': '0'};")); + cluster.schemaChange(withKeyspace("CREATE TABLE ks.tbl2 (pk int, ck int, PRIMARY KEY (pk, ck)) WITH compaction = " + + "{'class': 'UnifiedCompactionStrategy', " + + "'adaptive': 'false', " + + "'scaling_parameters': '0'};")); + + cluster.get(1).runOnInstance(() -> + { + ColumnFamilyStore cfs = Keyspace.open("ks").getColumnFamilyStore("tbl"); + UnifiedCompactionContainer container = (UnifiedCompactionContainer) cfs.getCompactionStrategy(); + UnifiedCompactionStrategy ucs = (UnifiedCompactionStrategy) container.getStrategies().get(0); + Controller controller = ucs.getController(); + // ucs config should be set to the vector config + assertEquals(vectorOverride ? Controller.DEFAULT_VECTOR_TARGET_SSTABLE_SIZE + : Controller.DEFAULT_TARGET_SSTABLE_SIZE, + controller.getTargetSSTableSize()); + // but any property set in the table compaction config should override the vector config + assertEquals(0, controller.getScalingParameter(0)); + + ColumnFamilyStore cfs2 = Keyspace.open("ks").getColumnFamilyStore("tbl2"); + UnifiedCompactionContainer container2 = (UnifiedCompactionContainer) cfs2.getCompactionStrategy(); + UnifiedCompactionStrategy ucs2 = (UnifiedCompactionStrategy) container2.getStrategies().get(0); + Controller controller2 = ucs2.getController(); + // since tbl2 does not have a vectorType the ucs config should not be set to the vector config + assertEquals(Controller.DEFAULT_TARGET_SSTABLE_SIZE, controller2.getTargetSSTableSize()); + assertEquals(0, controller2.getScalingParameter(0)); + }); + cluster.schemaChange(withKeyspace("ALTER TABLE ks.tbl2 ADD val vector;")); + cluster.get(1).runOnInstance(() -> + { + ColumnFamilyStore cfs2 = Keyspace.open("ks").getColumnFamilyStore("tbl2"); + UnifiedCompactionContainer container2 = (UnifiedCompactionContainer) cfs2.getCompactionStrategy(); + UnifiedCompactionStrategy ucs2 = (UnifiedCompactionStrategy) container2.getStrategies().get(0); + Controller controller2 = ucs2.getController(); + // a vector was added to tbl2 so it should now have the vector config + assertEquals(vectorOverride ? Controller.DEFAULT_VECTOR_TARGET_SSTABLE_SIZE + : Controller.DEFAULT_TARGET_SSTABLE_SIZE, + controller2.getTargetSSTableSize()); + assertEquals(0, controller2.getScalingParameter(0)); + }); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java index 099f87dd40bd..fa999fa56d99 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicLong; import com.google.common.collect.ImmutableMap; + import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileStoreUtils; import org.apache.cassandra.io.util.PathUtils; @@ -37,7 +38,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.compaction.ActiveCompactions; +import org.apache.cassandra.db.compaction.ActiveOperations; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; @@ -112,7 +113,7 @@ public static class BB static File sstableDir; public static void install(ClassLoader cl, Integer node) { - new ByteBuddy().rebase(ActiveCompactions.class) + new ByteBuddy().rebase(ActiveOperations.class) .method(named("estimatedRemainingWriteBytes")) .intercept(MethodDelegation.to(BB.class)) .make() diff --git a/test/distributed/org/apache/cassandra/distributed/test/CounterLeaderDynamicSnitchTest.java b/test/distributed/org/apache/cassandra/distributed/test/CounterLeaderDynamicSnitchTest.java new file mode 100644 index 000000000000..f4dae98b6038 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/CounterLeaderDynamicSnitchTest.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.net.InetSocketAddress; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.locator.DynamicEndpointSnitch; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertEquals; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertTrue; + +public class CounterLeaderDynamicSnitchTest extends TestBaseImpl +{ + @BeforeClass + public static void init() + { + CassandraRelevantProperties.USE_DYNAMIC_SNITCH_FOR_COUNTER_LEADER.setBoolean(true); + // test latency could be lower than 1ms. Disable it for before accuracy + CassandraRelevantProperties.DYNAMIC_ENDPOINT_SNITCH_QUANTIZE_TO_MILLIS.setBoolean(false); + } + + @AfterClass + public static void cleanup() + { + CassandraRelevantProperties.USE_DYNAMIC_SNITCH_FOR_COUNTER_LEADER.reset(); + CassandraRelevantProperties.DYNAMIC_ENDPOINT_SNITCH_QUANTIZE_TO_MILLIS.reset(); + } + + @Test + public void testDynamicSnitchScore() throws Throwable + { + testDynamicSnitchScore(false); + } + + @Test + public void testDynamicSnitchScoreWithTimeout() throws Throwable + { + testDynamicSnitchScore(true); + } + + private void testDynamicSnitchScore(boolean remoteReplicaTimeout) throws Throwable + { + try (Cluster cluster = Cluster.build(2).withConfig(c -> c.with(GOSSIP, NATIVE_PROTOCOL) + // effectively disable auto-update + .set("dynamic_snitch_update_interval_in_ms", "3600000") + .set("dynamic_snitch", "true")).start()) + { + cluster.schemaChange("CREATE KEYSPACE k WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + + String createTable = "CREATE TABLE k.t (k int, c int, total counter, PRIMARY KEY (k, c))"; + cluster.schemaChange(createTable); + + ConsistencyLevel cl = ConsistencyLevel.ONE; + + int coordinator = 1; + // before executing counter requests: no score on the coordinator + cluster.get(coordinator).runOnInstance(() -> { + DynamicEndpointSnitch snitch = (DynamicEndpointSnitch) DatabaseDescriptor.getEndpointSnitch(); + snitch.updateScores(); + assertTrue("Expect 0 scores, but got " + snitch.getScores(), snitch.getScores().isEmpty()); + }); + + if (remoteReplicaTimeout) + { + // simulate timeout on remote replica + cluster.filters().verbs(Verb.COUNTER_MUTATION_REQ.id).from(coordinator).to(2).drop(); + } + + int failures = 0; + int requests = 10; + for (int key = 0; key < requests; key++) + { + try + { + cluster.coordinator(coordinator).execute("UPDATE k.t SET total = total + 1 WHERE k = ? AND c = 1", cl, key); + } + catch (Throwable t) + { + failures++; + } + } + + if (remoteReplicaTimeout) + assertTrue("Expected remote counter leader failure " + failures, failures > 0 && failures < requests); + else + assertTrue("Expected no remote counter leader failure " + failures, failures == 0); + + // after executing counter requests: 1 score for remote replica on the coordinator + InetSocketAddress remoteReplica = cluster.get(2).broadcastAddress(); + cluster.get(coordinator).runOnInstance(() -> { + DynamicEndpointSnitch snitch = (DynamicEndpointSnitch) DatabaseDescriptor.getEndpointSnitch(); + snitch.updateScores(); + if (remoteReplicaTimeout) + { + assertEquals("Expect 1 score for remote replica, but got " + snitch.getScores(), snitch.getScores().size(), 1); + if (!snitch.getScores().get(remoteReplica.getAddress()).equals(1.0)) + throw new RuntimeException("Expect 1.0 max score for remote replica, but got " + snitch.getScores().get(remoteReplica.getAddress())); + } + else + { + assertEquals("Expect no score for remote replica, but got " + snitch.getScores(), snitch.getScores().size(), 0); + } + }); + } + } + + @Test + public void testApplyDynamicSnitch() throws Throwable + { + try (Cluster cluster = Cluster.build(3).withConfig(c -> c.with(GOSSIP, NATIVE_PROTOCOL) + // effectively disable auto-update + .set("dynamic_snitch_update_interval_in_ms", "3600000") + .set("dynamic_snitch", "true")).start()) + { + cluster.schemaChange("CREATE KEYSPACE k WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}"); + + String createTable = "CREATE TABLE k.t (k int, c int, total counter, PRIMARY KEY (k, c))"; + cluster.schemaChange(createTable); + + ConsistencyLevel cl = ConsistencyLevel.ALL; + + int coordinator = 1; + // before executing counter requests: no score on the coordinator + cluster.get(coordinator).runOnInstance(() -> { + DynamicEndpointSnitch snitch = (DynamicEndpointSnitch) DatabaseDescriptor.getEndpointSnitch(); + snitch.updateScores(); + assertTrue("Expect 0 scores, but got " + snitch.getScores(), snitch.getScores().isEmpty()); + }); + + // fail if node2 is selected as counter leader + cluster.filters().verbs(Verb.COUNTER_MUTATION_REQ.id).from(coordinator).to(2).drop(); + int failures = 0; + int requests = 100; + int idx = 0; + while (failures == 0 && idx++ < requests) + { + try + { + cluster.coordinator(coordinator).execute("UPDATE k.t SET total = total + 1 WHERE k = ? AND c = 1", cl, idx); + } + catch (Throwable t) + { + failures++; + } + } + assertTrue("Expected node2 failure " + failures, failures > 0); + + // wait for callback expired + FBUtilities.sleepQuietly(2000); + + // update dynamic snitch score: subsequent request should avoid coordinator as counter leader + InetSocketAddress remoteReplica2 = cluster.get(2).broadcastAddress(); + cluster.get(coordinator).runOnInstance(() -> { + DynamicEndpointSnitch snitch = (DynamicEndpointSnitch) DatabaseDescriptor.getEndpointSnitch(); + snitch.updateScores(); + assertEquals("Expect 1 score from expired request, but got " + snitch.getScores(), snitch.getScores().size(), 1); + if (!snitch.getScores().get(remoteReplica2.getAddress()).equals(1.0)) + throw new RuntimeException("Expect 1.0 max score for node2, but got " + snitch.getScores().get(remoteReplica2.getAddress())); + }); + + // subsequent requests should not select node2 as leader + for (int key = 0; key < requests; key++) + cluster.coordinator(coordinator).execute("UPDATE k.t SET total = total + 1 WHERE k = ? AND c = 1", cl, key); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/DecommissionHookTest.java b/test/distributed/org/apache/cassandra/distributed/test/DecommissionHookTest.java new file mode 100644 index 000000000000..1bb59f1c030c --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/DecommissionHookTest.java @@ -0,0 +1,399 @@ +/* + * Copyright IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.SystemKeyspace.BootstrapState; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.service.DecommissionHook; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.service.StorageService.Mode.DECOMMISSIONED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * {@link DecommissionHook}: hooks run on a decommissioning node once it has left the ring, while + * it can still query the cluster. + * + * 3 nodes at RF=2, decommissioning node3, so the two survivors can still serve the hook's QUORUM + * read. The decommission is forceful because system_distributed is RF=3 on a 3-node cluster, which + * trips the RF-vs-live-nodes check whatever the test keyspace does -- the same reason + * {@link DecommissionTest} forces. + * + * Note the hooks below are static nested classes, not lambdas or anonymous classes: an anonymous + * class declared in a test method captures the test instance, which makes the enclosing + * runOnInstance closure unserializable. + */ +public class DecommissionHookTest extends TestBaseImpl +{ + /** + * What the hooks observed. Lives in the node's classloader: the hooks write it during the + * decommission, and the assertions read it back via a later runOnInstance on the same node. + */ + public static class Observed + { + static final List order = Collections.synchronizedList(new ArrayList<>()); + /** Names of the hooks that were entered with the interrupt flag already set. Must stay empty. */ + static final List enteredInterrupted = Collections.synchronizedList(new ArrayList<>()); + static volatile boolean stillRingMember = true; + static volatile boolean nativeTransportRunning = false; + static volatile int rowsReadByHook = -1; + static volatile String queryError = null; + } + + /** Records that it ran, under its own name, and how it found the interrupt flag on entry. */ + public static class RecordingHook implements DecommissionHook + { + private final String name; + + RecordingHook(String name) + { + this.name = name; + } + + public String name() + { + return name; + } + + public void onDecommission() + { + // A hook is entitled to a clear flag: it may block, and a leaked interrupt would fail + // its very first blocking call. Checked without consuming, so a leak stays visible. + if (Thread.currentThread().isInterrupted()) + Observed.enteredInterrupted.add(name); + Observed.order.add(name); + } + } + + /** Records what the node looks like from inside a hook, and tries to query the cluster. */ + public static class ProbingHook implements DecommissionHook + { + public String name() + { + return "probe"; + } + + public void onDecommission() + { + Observed.order.add("probe"); + // unbootstrap() has run, so leaveRing() has removed us from the ring: no coordinator + // routes mutations here any more. This also transitively proves the batch log finished + // its final replay -- batchlogReplay.get() is sequenced before leaveRing() inside + // unbootstrap(). + Observed.stillRingMember = StorageService.instance.getTokenMetadata() + .isMember(FBUtilities.getBroadcastAddressAndPort()); + // ...but nothing is shut down yet, so we can still coordinate queries. + Observed.nativeTransportRunning = StorageService.instance.isNativeTransportRunning(); + try + { + UntypedResultSet rs = QueryProcessor.execute("SELECT * FROM " + KEYSPACE + ".tbl", + ConsistencyLevel.QUORUM); + Observed.rowsReadByHook = rs.size(); + } + catch (Throwable t) + { + Observed.queryError = t.toString(); + } + } + } + + /** + * Returns normally but leaves the thread interrupted -- the standard + * {@code catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }} idiom + * minus the rethrow. runDecommissionHooks() has to consume that flag, or the shutdown steps + * after the hooks fail on it and strand the node. + */ + public static class InterruptRestoringHook implements DecommissionHook + { + public String name() + { + return "interrupt-restoring"; + } + + public void onDecommission() + { + Observed.order.add("interrupt-restoring"); + Thread.currentThread().interrupt(); + } + } + + /** + * Restores the interrupt flag and then throws something else -- the other way a hook can leave + * with the flag set, e.g. {@code catch (InterruptedException e) { Thread.currentThread() + * .interrupt(); throw new RuntimeException(e); }}. The flag has to be consumed on this path too, + * or it leaks into the next hook. + */ + public static class InterruptRestoringExplodingHook implements DecommissionHook + { + public String name() + { + return "interrupt-restoring-exploding"; + } + + public void onDecommission() + { + Observed.order.add("interrupt-restoring-exploding"); + Thread.currentThread().interrupt(); + throw new RuntimeException("simulated hook failure with the interrupt flag restored"); + } + } + + /** Fails, to show the hooks behind it still run and the decommission does not claim success. */ + public static class ExplodingHook implements DecommissionHook + { + public String name() + { + return "exploding"; + } + + public void onDecommission() + { + Observed.order.add("exploding"); + throw new RuntimeException("simulated hook failure"); + } + } + + @Test + public void testHooksRunInOrderAfterLeavingTheRingAndCanQuery() throws Throwable + { + // NATIVE_PROTOCOL so the "transport is still up" assertion below means something: without + // it the transport would never have been running and the assertion would be vacuous. + try (Cluster cluster = init(Cluster.build(3) + .withConfig(config -> config.with(GOSSIP).with(NETWORK).with(NATIVE_PROTOCOL)) + .start(), 2)) + { + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (k int PRIMARY KEY, v int)"); + for (int i = 0; i < 10; i++) + cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (k, v) VALUES (?, ?)", + org.apache.cassandra.distributed.api.ConsistencyLevel.ALL, i, i); + + cluster.get(3).runOnInstance(() -> { + StorageService.instance.registerDecommissionHook(new ProbingHook()); + StorageService.instance.registerDecommissionHook(new RecordingHook("second")); + + try + { + StorageService.instance.decommission(true); + } + catch (Throwable t) + { + fail("decommission should have succeeded, but failed on: " + t); + } + }); + + cluster.get(3).runOnInstance(() -> { + assertEquals("both hooks must run, in registration order", + Arrays.asList("probe", "second"), Observed.order); + assertFalse("hooks must run after the node has left the ring, so no mutations arrive", + Observed.stillRingMember); + assertTrue("the native transport must still be up so a hook can be queried through", + Observed.nativeTransportRunning); + assertNull("the hook's coordinated read failed: " + Observed.queryError, + Observed.queryError); + assertEquals("a hook must be able to read the cluster at QUORUM", + 10, Observed.rowsReadByHook); + }); + } + } + + /** + * A hook that hands back an interrupted thread must not derail the rest of the decommission: + * the shutdown after the hooks waits on futures that fail instantly if the flag is still set. + */ + @Test + public void testHookLeavingTheThreadInterruptedStillCompletesTheDecommission() throws Throwable + { + try (Cluster cluster = init(Cluster.build(3) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start(), 2)) + { + cluster.get(3).runOnInstance(() -> { + Observed.order.clear(); + Observed.enteredInterrupted.clear(); + StorageService.instance.registerDecommissionHook(new InterruptRestoringHook()); + StorageService.instance.registerDecommissionHook(new RecordingHook("after-the-interrupt")); + + try + { + StorageService.instance.decommission(true); + } + catch (Throwable t) + { + fail("a hook returning with the interrupt flag set must not fail the decommission, got: " + t); + } + + assertEquals("a restored interrupt flag must not skip the hooks behind it", + Arrays.asList("interrupt-restoring", "after-the-interrupt"), Observed.order); + assertEquals("a restored interrupt flag must not leak into the next hook", + Collections.emptyList(), Observed.enteredInterrupted); + assertEquals(DECOMMISSIONED.name(), StorageService.instance.getOperationMode()); + assertFalse("the interrupt flag must not outlive the hook run", + Thread.currentThread().isInterrupted()); + }); + } + } + + /** + * The same, for a hook that restores the flag and then throws. The interrupt must be consumed on + * the failure path too: the next hook is entitled to a clear flag, and would otherwise fail on + * its first blocking call for a reason that has nothing to do with it. + */ + @Test + public void testHookThrowingWithTheInterruptFlagSetDoesNotLeakItToTheNextHook() throws Throwable + { + try (Cluster cluster = init(Cluster.build(3) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start(), 2)) + { + cluster.get(3).runOnInstance(() -> { + Observed.order.clear(); + Observed.enteredInterrupted.clear(); + StorageService.instance.registerDecommissionHook(new InterruptRestoringExplodingHook()); + StorageService.instance.registerDecommissionHook(new RecordingHook("after-the-interrupting-explosion")); + + Throwable thrown = null; + try + { + StorageService.instance.decommission(true); + } + catch (Throwable t) + { + thrown = t; + } + assertNotNull("a failing hook must be reported to the caller", thrown); + assertTrue("the failure must name the offending hook, was: " + thrown, + String.valueOf(thrown.getMessage()).contains("interrupt-restoring-exploding")); + + assertEquals("a hook that throws must not skip the hooks behind it", + Arrays.asList("interrupt-restoring-exploding", "after-the-interrupting-explosion"), + Observed.order); + assertEquals("an interrupt restored before throwing must not leak into the next hook", + Collections.emptyList(), Observed.enteredInterrupted); + assertEquals(DECOMMISSIONED.name(), StorageService.instance.getOperationMode()); + assertFalse("the interrupt flag must not outlive the hook run", + Thread.currentThread().isInterrupted()); + }); + } + } + + /** Registering a null hook must fail the caller, not the decommission. */ + @Test + public void testNullHookIsRejectedAtRegistration() throws Throwable + { + try (Cluster cluster = init(Cluster.build(1) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start(), 1)) + { + cluster.get(1).runOnInstance(() -> { + try + { + StorageService.instance.registerDecommissionHook(null); + fail("a null hook must be rejected at registration"); + } + catch (NullPointerException expected) + { + // A null reaching runDecommissionHooks() would have no safe way to fail. + } + }); + } + } + + /** + * A hook that throws must not silently skip the hooks behind it, must be reported, and must + * still leave the node fully decommissioned. + * + * The node state is the subtle part. Hooks run after unbootstrap(), so the data has already + * streamed away and leaveRing() has run. Stopping at DECOMMISSION_FAILED there would strand the + * node for good: a retry is rejected by decommission()'s ring-membership check (leaveRing() + * removed us from TokenMetadata), and leaveRing() persisted NEEDS_BOOTSTRAP, so a restart would + * bootstrap the node back into the ring instead. So the decommission finishes and the failure + * is reported afterwards. + */ + @Test + public void testFailingHookIsReportedButStillCompletesTheDecommission() throws Throwable + { + try (Cluster cluster = init(Cluster.build(3) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start(), 2)) + { + cluster.get(3).runOnInstance(() -> { + Observed.order.clear(); + Observed.enteredInterrupted.clear(); + StorageService.instance.registerDecommissionHook(new ExplodingHook()); + StorageService.instance.registerDecommissionHook(new RecordingHook("after-the-explosion")); + + // Capture rather than assert inside the try: a fail() there would be caught by the + // catch below and re-reported under the wrong message. + Throwable thrown = null; + try + { + StorageService.instance.decommission(true); + } + catch (Throwable t) + { + thrown = t; + } + assertNotNull("a failing hook must be reported to the caller", thrown); + assertTrue("the failure must name the offending hook, was: " + thrown, + String.valueOf(thrown.getMessage()).contains("exploding")); + + assertEquals("a failing hook must not skip the hooks behind it", + Arrays.asList("exploding", "after-the-explosion"), Observed.order); + + // The decommission itself completed: the node is not stranded mid-leave. + assertEquals(DECOMMISSIONED.name(), StorageService.instance.getOperationMode()); + assertEquals(BootstrapState.DECOMMISSIONED.name(), StorageService.instance.getBootstrapState()); + assertFalse(StorageService.instance.isDecommissioning()); + + // ...so the operator is not stuck. Reaching DECOMMISSIONED is what makes the repeat + // call hit decommission()'s existing early return instead of the ring-membership + // check that would reject it forever -- which is the whole reason a hook failure + // must not stop the decommission short. Hooks do not run a second time. + Observed.order.clear(); + try + { + StorageService.instance.decommission(true); + } + catch (Throwable t) + { + fail("decommissioning an already decommissioned node should be a no-op, got: " + t); + } + assertEquals("a repeat decommission must not re-run the hooks", + Collections.emptyList(), Observed.order); + }); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/DisableBinaryTest.java b/test/distributed/org/apache/cassandra/distributed/test/DisableBinaryTest.java index a5c0b1a5c33b..eaff71153723 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/DisableBinaryTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/DisableBinaryTest.java @@ -109,6 +109,16 @@ public void testFinishInProgressQueries() throws Throwable finally { executor.shutdown(); + try + { + boolean shutdown = executor.awaitTermination(10, TimeUnit.SECONDS); + if (!shutdown) + throw new AssertionError("Executor did not terminate"); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/DistributedRepairUtils.java b/test/distributed/org/apache/cassandra/distributed/test/DistributedRepairUtils.java index c71a611c012c..16e9016661bb 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/DistributedRepairUtils.java +++ b/test/distributed/org/apache/cassandra/distributed/test/DistributedRepairUtils.java @@ -190,7 +190,8 @@ public static void assertNoSSTableLeak(ICluster cluster, Str if (session != null && !session.isCompleted()) continue; // The session is complete, yet the sstable is not updated... is this still pending in compaction? - if (cfs.getCompactionStrategyManager().hasPendingRepairSSTable(pendingRepair, sstable)) +// if (cfs.getCompactionStrategyManager().hasPendingRepairSSTable(pendingRepair, sstable)) + if (cfs.hasPendingRepairSSTables(pendingRepair)) continue; // compaction does not know about the pending repair... race condition since this check started? if (sstable.getSSTableMetadata().pendingRepair == null) diff --git a/test/distributed/org/apache/cassandra/distributed/test/DropUDTWithRestartTest.java b/test/distributed/org/apache/cassandra/distributed/test/DropUDTWithRestartTest.java new file mode 100644 index 000000000000..ed53fe0d3193 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/DropUDTWithRestartTest.java @@ -0,0 +1,686 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; +import org.assertj.core.api.Assertions; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.junit.Ignore; +import org.junit.Assume; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Session; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.StorageCompatibilityMode; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.ICoordinator; +import org.apache.cassandra.distributed.api.IInstance; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tools.SSTableExport; +import org.apache.cassandra.tools.ToolRunner; +import org.apache.cassandra.utils.Collectors3; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.util.Arrays.asList; + +import static org.apache.cassandra.config.DatabaseDescriptor.getCommitLogLocation; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; +import static org.apache.cassandra.distributed.shared.AssertUtils.row; +import static org.assertj.core.api.Assertions.assertThat; + +public class DropUDTWithRestartTest extends TestBaseImpl +{ + private final static Logger logger = LoggerFactory.getLogger(DropUDTWithRestartTest.class); + + private final static Path TEST_DATA_UDT_PATH = Paths.get("test/data/udt"); + private final static Path CASSANDRA_40_PRODUCT_PATH = TEST_DATA_UDT_PATH.resolve("c40"); + private final static Path CASSANDRA_41_PRODUCT_PATH = TEST_DATA_UDT_PATH.resolve("c41"); + private final static Path CASSANDRA_5_PRODUCT_PATH = TEST_DATA_UDT_PATH.resolve("c50"); + private final static Path CC40_PRODUCT_PATH = TEST_DATA_UDT_PATH.resolve("cc40"); + private final static Path CC50_PRODUCT_PATH = TEST_DATA_UDT_PATH.resolve("cc50"); + private final static Path DSE6_PRODUCT_PATH = TEST_DATA_UDT_PATH.resolve("dse"); + private final static Path THIS_PRODUCT_PATH = CC50_PRODUCT_PATH; + private final static String COMMITLOG_DIR = "commitlog"; + private final static String KS = "ks"; + private final static String SCHEMA_TXT = "schema.txt"; + private final static String SCHEMA0_TXT = "schema0.txt"; + private final static String DATA_JSON = "data.json"; + + private Cluster startCluster() throws IOException + { + Cluster cluster = Cluster.build(1).withConfig(config -> config.set("auto_snapshot", "false") + .set("uuid_sstable_identifiers_enabled", "false") + .with(NATIVE_PROTOCOL)).start(); + cluster.setUncaughtExceptionsFilter(t -> { + String cause = Optional.ofNullable(t.getCause()).map(c -> c.getClass().getName()).orElse(""); + return t.getClass().getName().equals(FSWriteError.class.getName()) && cause.equals(AccessDeniedException.class.getName()); + }); + return cluster; + } + + @Test + public void mergeDataFromSSTableAndCommitLogWithDroppedColumnTest() throws Throwable + { + try (Cluster cluster = startCluster()) + { + // Create tables, populate them with the first dataset, drop complex column (which follows flushing), + // and then populate with the second dataset. This way we will have data on disk and in the memtable. + // Finally record query results. + IInvokableInstance node = cluster.get(1); + node.executeInternal("DROP KEYSPACE IF EXISTS " + KS); + node.executeInternal("CREATE KEYSPACE " + KS + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + createTables(node); + cluster.disableAutoCompaction(KS); + + // let's have some data in sstables + insertData(node, 0, true); + insertData(node, 256, true); + + // then drop the complex column + dropComplexColumn(node); + + // insert some other data + insertData(node, 128, false); + insertData(node, 256 + 17, false); + + // and see what we have + Map>> data0 = selectData(node); + Map>> cqlData0 = selectCQLData(node); + + assertThat(cqlData0).isEqualTo(data0); + + // make sure we have the same after flushing and compacting + node.flush(KS); + assertThat(selectData(node)).isEqualTo(data0); + for (String table : data0.keySet()) + node.forceCompact(KS, table); + assertThat(selectData(node)).isEqualTo(data0); + + // Create tables, populate them with the first dataset, block data dir and drop complex column (prevent + // flushing so that the data stays in the commit log), restart the node to replay the commit log, populate + // the tables with the second data set, and finally record query results. + node.executeInternal("DROP KEYSPACE " + KS); + node.executeInternal("CREATE KEYSPACE " + KS + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + createTables(node); + List dataDirs = getDataDirectories(node); + + cluster.disableAutoCompaction(KS); + blockFlushing(dataDirs); + insertData(node, 0, true); + insertData(node, 256, true); + try + { + dropComplexColumn(node); + } + finally + { + unblockFlushing(dataDirs); + } + node.shutdown(true).get(10, TimeUnit.SECONDS); + + node.startup(); + insertData(node, 128, false); + insertData(node, 256 + 17, false); + + // eventually we expect that the result sets from both runs are the same + assertThat(selectData(node)).isEqualTo(data0); + + // make sure we have the same after flushing and compacting + node.flush(KS); + node.shutdown(true).get(10, TimeUnit.SECONDS); + node.startup(); + assertThat(selectData(node)).isEqualTo(data0); + + for (String table : data0.keySet()) + node.forceCompact(KS, table); + assertThat(selectData(node)).isEqualTo(data0); + } + } + + private static List getDataDirectories(IInvokableInstance node) + { + // CNDB-16146: Use getCFDirectories() to get ALL directories where SSTables may exist, + // not just getDirectoryForNewSSTables() which only returns one directory per table. + // SSTables can be spread across multiple data directories (data0, data1, data2). + return node.callOnInstance(() -> Keyspace.open(KS).getColumnFamilyStores().stream() + .flatMap(cfs -> cfs.getDirectories().getCFDirectories().stream()) + .map(File::toPath) + .collect(Collectors.toList())); + } + + /** + * This is actually not a test - it is used to generate data files to be used by {@code loadCommitLogAndSSTablesWithDroppedColumnTest*}. + * Those files should be populated across different products between which we want to verify the compatibility. + */ + @Test + @Ignore + public void storeCommitLogAndSSTablesWithDroppedColumn() throws Throwable + { + Files.createDirectories(THIS_PRODUCT_PATH); + try (Cluster cluster = startCluster()) + { + IInvokableInstance node = cluster.get(1); + node.executeInternal("DROP KEYSPACE IF EXISTS " + KS); + node.executeInternal("CREATE KEYSPACE " + KS + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + createTables(node); + cluster.disableAutoCompaction(KS); + + List dataDirs = getDataDirectories(node); + Path commitLogDir = node.callOnInstance(() -> getCommitLogLocation().toPath()); + + Map schema0 = getSchemaDesc(node); + Files.writeString(THIS_PRODUCT_PATH.resolve(SCHEMA0_TXT), + String.join(";\n", schema0.values()).replaceAll(";;", ";"), + UTF_8, + CREATE, TRUNCATE_EXISTING); + + insertData(node, 0, true); + insertData(node, 256, true); + node.flush(KS); + + blockFlushing(dataDirs); + try + { + dropComplexColumn(node); + insertData(node, 128, false); + insertData(node, 256 + 17, false); + + Map schema1 = getSchemaDesc(node); + Files.writeString(THIS_PRODUCT_PATH.resolve(SCHEMA_TXT), + String.join(";\n", schema1.values()).replaceAll(";;", ";"), + UTF_8, + CREATE, TRUNCATE_EXISTING); + + node.shutdown(true).get(10, TimeUnit.SECONDS); + + Path clTargetPath = THIS_PRODUCT_PATH.resolve(COMMITLOG_DIR); + Files.createDirectories(clTargetPath); + PathUtils.deleteContent(clTargetPath); + FileUtils.copyDirectory(commitLogDir.toFile(), clTargetPath.toFile()); + + Path ksTargetPath = THIS_PRODUCT_PATH.resolve(KS); + Files.createDirectories(ksTargetPath); + PathUtils.deleteContent(ksTargetPath); + for (Path dir : dataDirs) + { + String name = dir.getFileName().toString(); + Path targetDir = ksTargetPath.resolve(name); + Files.createDirectories(targetDir); + FileUtils.copyDirectory(dir.toFile(), targetDir.toFile(), pathname -> !pathname.toString().endsWith(".log")); + } + } + finally + { + unblockFlushing(dataDirs); + } + + node.startup(); + node.flush(KS); + Map>> data = selectData(node); + Files.writeString(THIS_PRODUCT_PATH.resolve(DATA_JSON), JSONObject.toJSONString(data), UTF_8, CREATE, TRUNCATE_EXISTING); + } + } + + @Test + public void loadCommitLogAndSSTablesWithDroppedColumnTestCassandra40() throws Exception + { + // Cassandra limitations + // - user types cannot include other non-frozen udt + // - cannot drop non-frozen columns + // - doesn't support DROPPED COLUMN RECORD table option + loadCommitLogAndSSTablesWithDroppedColumnTest(CASSANDRA_40_PRODUCT_PATH); + } + + @Test + public void loadCommitLogAndSSTablesWithDroppedColumnTestCC40() throws Exception + { + loadCommitLogAndSSTablesWithDroppedColumnTest(CC40_PRODUCT_PATH); + } + + @Test + public void loadCommitLogAndSSTablesWithDroppedColumnTestCassandra41() throws Exception + { + // Cassandra limitations + // - user types cannot include other non-frozen udt + // - cannot drop non-frozen columns + // - doesn't support DROPPED COLUMN RECORD table option + loadCommitLogAndSSTablesWithDroppedColumnTest(CASSANDRA_41_PRODUCT_PATH); + } + + @Test + public void loadCommitLogAndSSTablesWithDroppedColumnTestCassandra5() throws Exception + { + // Skip this test if running in compatibility mode < 5.0, as it loads CC5.0 format commit logs and SSTables + StorageCompatibilityMode mode = CassandraRelevantProperties.TEST_STORAGE_COMPATIBILITY_MODE.getEnum(true, StorageCompatibilityMode.class); + Assume.assumeFalse("Test requires Cassandra 5.0+ format data", + mode != null && mode.isBefore(CassandraVersion.CASSANDRA_5_0.major)); + + // Cassandra limitations + // - user types cannot include other non-frozen udt + // - cannot drop non-frozen columns + // - doesn't support DROPPED COLUMN RECORD table option + loadCommitLogAndSSTablesWithDroppedColumnTest(CASSANDRA_5_PRODUCT_PATH); + } + + @Test + public void loadCommitLogAndSSTablesWithDroppedColumnTestCC50() throws Exception + { + // Skip this test if running in compatibility mode < 5.0, as it loads CC5.0 format commit logs and SSTables + StorageCompatibilityMode mode = CassandraRelevantProperties.TEST_STORAGE_COMPATIBILITY_MODE.getEnum(true, StorageCompatibilityMode.class); + Assume.assumeFalse("Test requires Cassandra 5.0+ format data", + mode != null && mode.isBefore(CassandraVersion.CASSANDRA_5_0.major)); + + loadCommitLogAndSSTablesWithDroppedColumnTest(CC50_PRODUCT_PATH); + } + + @Test + public void loadCommitLogAndSSTablesWithDroppedColumnTestDSE6() throws Exception + { + loadCommitLogAndSSTablesWithDroppedColumnTest(DSE6_PRODUCT_PATH); + } + + private void loadCommitLogAndSSTablesWithDroppedColumnTest(Path productPath) throws IOException, ExecutionException, InterruptedException, TimeoutException, ParseException + { + try (Cluster cluster = startCluster()) + { + IInvokableInstance node = cluster.get(1); + node.executeInternal("DROP KEYSPACE IF EXISTS " + KS); + node.executeInternal("CREATE KEYSPACE " + KS + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + + for (String stmt : Files.readString(productPath.resolve(SCHEMA_TXT), UTF_8).split(";")) + { + if (!stmt.isBlank()) + { + logger.info("Executing: {}", stmt); + node.executeInternal(stmt); + } + } + + cluster.disableAutoCompaction(KS); + + List dataDirs = getDataDirectories(node); + Path commitLogDir = node.callOnInstance(() -> getCommitLogLocation().toPath()); + + node.shutdown(true).get(10, TimeUnit.SECONDS); + + Path commitLogSourcePath = productPath.resolve(COMMITLOG_DIR); + FileUtils.copyDirectory(commitLogSourcePath.toFile(), commitLogDir.toFile()); + + Path ksSourcePath = productPath.resolve(KS); + for (Path dir : dataDirs) + { + String name = dir.getFileName().toString(); + Path sourceDir = ksSourcePath.resolve(name); + FileUtils.copyDirectory(sourceDir.toFile(), dir.toFile()); + } + + logger.info("Restarting node"); + node.startup(); + + // verify same data new cluster and schema recreated + Map>> data1 = selectData(node); + String jsonData0 = Files.readString(productPath.resolve(DATA_JSON), UTF_8); + for (String table1 : data1.keySet()) + { + List> table1Data = data1.get(table1); + JSONArray table1Json = new JSONArray(); + table1Json.addAll(table1Data); + String table0Json = JSONValue.toJSONString(((JSONObject) new JSONParser().parse(jsonData0)).get(table1)); + String missingRows = table0Json; + int originalRowCount = (missingRows.length() - missingRows.replace("[", "").length() -1); + for (List row1 : table1Data) + { + JSONArray row1Json = new JSONArray(); + row1Json.addAll(row1); + missingRows = missingRows.replace(row1Json.toJSONString(), ""); + } + String missingMsg = String.format("missing %s/%s rows in %s: %s", + (missingRows.length() - missingRows.replace("[", "").length() -1), + originalRowCount, table1, missingRows.replaceAll(",+", ",")); + + assertThat(table1Json.toJSONString()).as(missingMsg).isEqualTo(table0Json); + } + String jsonData1 = JSONObject.toJSONString(data1); + assertThat(jsonData1).isEqualTo(jsonData0); + + node.flush(KS); + node.shutdown(true).get(10, TimeUnit.SECONDS); + node.startup(); + + // verify same data post-flush + for (String table : data1.keySet()) + assertThat(selectData(node).get(table)).isEqualTo(data1.get(table)); + + for (String table : data1.keySet()) + node.forceCompact(KS, table); + + // verify same data post compact + for (String table : data1.keySet()) + assertThat(selectData(node).get(table)).isEqualTo(data1.get(table)); + } + } + + private Map getSchemaDesc(IInvokableInstance node) + { + return Arrays.stream(node.executeInternal("DESCRIBE " + KS + " WITH INTERNALS")) + .filter(r -> r[1].equals("table") || r[1].equals("type")) + .collect(Collectors3.toImmutableMap(r -> r[2].toString(), + r -> Arrays.stream(r[3].toString().split("\\n")) + .filter(s -> !s.strip().startsWith("AND") || s.contains("DROPPED COLUMN RECORD")) + .collect(Collectors.joining("\n")))); + } + + private static String udtValue(int i, List bits, BiFunction vals) + { + List cols = asList("foo", "bar", "baz"); + ArrayList udtVals = new ArrayList<>(); + for (int j = 0; j < bits.size(); j++) + { + if ((i & bits.get(j)) != 0) + udtVals.add(cols.get(j) + ": " + vals.apply(i, j)); + } + return '{' + String.join(", ", udtVals) + '}'; + } + + private static String tupleValue(int i, List bits, BiFunction vals) + { + ArrayList tupleVals = new ArrayList<>(); + for (int j = 0; j < bits.size(); j++) + { + if ((i & bits.get(j)) != 0) + tupleVals.add(vals.apply(i, j)); + else + tupleVals.add("null"); + } + return '(' + String.join(", ", tupleVals) + ')'; + } + + private static String genInsert(int pk, int i, List bits, BiFunction vals) + { + List cols = asList("a_int", "b_complex", "c_int"); + ArrayList c = new ArrayList<>(); + ArrayList v = new ArrayList<>(); + for (int j = 0; j < bits.size(); j++) + { + if ((i & bits.get(j)) != 0) + { + c.add(cols.get(j)); + v.add(vals.apply(i, j)); + } + } + if (c.isEmpty()) + return String.format("(pk) VALUES (%d)", pk); + else + return String.format("(pk, %s) VALUES (%d, %s)", String.join(", ", c), pk, String.join(", ", v)); + } + + private static BiFunction valsFunction(IntFunction nonIntFunction) + { + return (i, j) -> { + if (j == 0) + return Integer.toString(i); + if (j == 1) + return nonIntFunction.apply(i); + if (j == 2) + return Integer.toString(i * 2); + assert false; + return null; + }; + } + + private static BiFunction valsFunction() + { + return (i, j) -> { + if (j == 0) + return Integer.toString(i); + if (j == 1) + return String.format("'bar%d'", i); + if (j == 2) + return Integer.toString(i * 2); + assert false; + return null; + }; + } + + private void insertData(IInstance node, int offset, boolean withComplex) + { + for (int pk = offset; pk < offset + (1 << 5); pk++) + { + int i = withComplex ? (pk - offset) : (pk - offset) & ~(2 + 4 + 8); + node.executeInternal("INSERT INTO " + KS + ".tab1_udt1 " + genInsert(pk, i, asList(1, 2 + 4 + 8, 16), valsFunction(j -> udtValue(j, asList(2, 4, 8), valsFunction())))); + node.executeInternal("INSERT INTO " + KS + ".tab2_frozen_udt1 " + genInsert(pk, i, asList(1, 2 + 4 + 8, 16), valsFunction(j -> udtValue(j, asList(2, 4, 8), valsFunction())))); + node.executeInternal("INSERT INTO " + KS + ".tab5_tuple " + genInsert(pk, i, asList(1, 2 + 4 + 8, 16), valsFunction(j -> tupleValue(j, asList(2, 4, 8), valsFunction())))); + node.executeInternal("INSERT INTO " + KS + ".tab6_frozen_tuple " + genInsert(pk, i, asList(1, 2 + 4 + 8, 16), valsFunction(j -> tupleValue(j, asList(2, 4, 8), valsFunction())))); + } + + for (int pk = offset; pk < offset + (1 << 7); pk++) + { + int i = withComplex ? (pk - offset) : (pk - offset) & ~(2 + 4 + 8 + 16 + 32); + node.executeInternal("INSERT INTO " + KS + ".tab4_frozen_udt2 " + genInsert(pk, i, asList(1, 2 + 4 + 8 + 16 + 32, 64), + valsFunction(j -> udtValue(j, asList(2, 4 + 8 + 16, 32), valsFunction(k -> udtValue(k, asList(4, 8, 16), valsFunction())))))); + node.executeInternal("INSERT INTO " + KS + ".tab7_tuple_with_udt " + genInsert(pk, i, asList(1, 2 + 4 + 8 + 16 + 32, 64), + valsFunction(j -> tupleValue(j, asList(2, 4 + 8 + 16, 32), valsFunction(k -> udtValue(k, asList(4, 8, 16), valsFunction())))))); + node.executeInternal("INSERT INTO " + KS + ".tab8_frozen_tuple_with_udt " + genInsert(pk, i, asList(1, 2 + 4 + 8 + 16 + 32, 64), + valsFunction(j -> tupleValue(j, asList(2, 4 + 8 + 16, 32), valsFunction(k -> udtValue(k, asList(4, 8, 16), valsFunction())))))); + node.executeInternal("INSERT INTO " + KS + ".tab9_udt_with_tuple " + genInsert(pk, i, asList(1, 2 + 4 + 8 + 16 + 32, 64), + valsFunction(j -> udtValue(j, asList(2, 4 + 8 + 16, 32), valsFunction(k -> tupleValue(k, asList(4, 8, 16), valsFunction())))))); + node.executeInternal("INSERT INTO " + KS + ".tab10_frozen_udt_with_tuple " + genInsert(pk, i, asList(1, 2 + 4 + 8 + 16 + 32, 64), + valsFunction(j -> udtValue(j, asList(2, 4 + 8 + 16, 32), valsFunction(k -> tupleValue(k, asList(4, 8, 16), valsFunction())))))); + } + } + + private static void dropComplexColumn(IInvokableInstance node) + { + List tables = node.callOnInstance(() -> Schema.instance.getKeyspaceMetadata(KS).tables.stream().map(t -> t.name).collect(Collectors.toList())); + for (String table : tables) + node.executeInternal("ALTER TABLE " + KS + "." + table + " DROP b_complex"); + } + + private Map>> selectData(IInvokableInstance node) + { + Map>> results = new HashMap<>(); + List tables = node.callOnInstance(() -> Schema.instance.getKeyspaceMetadata(KS).tables.stream().map(t -> t.name).collect(Collectors.toList())); + for (String table : tables) + { + Object[][] rows = node.executeInternal("SELECT * FROM " + KS + "." + table); + Arrays.sort(rows, Comparator.comparing(a -> ((Integer) a[0]))); + results.put(table, Arrays.stream(rows).map(Arrays::asList).collect(Collectors.toList())); + } + return results; + } + + private Map>> selectCQLData(IInvokableInstance node) + { + Map>> results = new HashMap<>(); + List tables = node.callOnInstance(() -> Schema.instance.getKeyspaceMetadata(KS).tables.stream().map(t -> t.name).collect(Collectors.toList())); + try (com.datastax.driver.core.Cluster cluster = com.datastax.driver.core.Cluster.builder().addContactPoint(node.broadcastAddress().getHostString()).build(); + Session session = cluster.connect()) + { + for (String table : tables) + { + ResultSet rs = session.execute("SELECT * FROM " + KS + "." + table); + assertThat(rs.getColumnDefinitions().contains("b_complex")).isFalse(); + List> rows = rs.all().stream().map(r -> Arrays.asList(r.get("pk", Integer.class), r.get("a_int", Integer.class), r.get("c_int", Integer.class))) + .sorted(Comparator.comparing(a -> ((Integer) a.get(0)))) + .collect(Collectors.toList()); + results.put(table, rows); + } + } + return results; + } + + private static void createTables(IInvokableInstance node) + { + node.executeInternal("CREATE TYPE " + KS + ".udt1(foo int, bar text, baz int)"); + node.executeInternal("CREATE TYPE " + KS + ".udt2(foo int, bar udt1, baz int)"); + node.executeInternal("CREATE TYPE " + KS + ".udt3(foo int, bar tuple, baz int)"); + + node.executeInternal("CREATE TABLE " + KS + ".tab1_udt1 (pk int PRIMARY KEY, a_int int, b_complex udt1, c_int int) WITH ID = 513f2627-9356-41c4-a379-7ad42be97432"); + node.executeInternal("CREATE TABLE " + KS + ".tab2_frozen_udt1 (pk int PRIMARY KEY, a_int int, b_complex frozen, c_int int) WITH ID = 450f91fe-7c47-41c9-97bf-fdad854fa7e5"); + Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy( + () -> node.executeInternal("CREATE TABLE " + KS + ".tab3_udt2 (pk int PRIMARY KEY, a_int int, b_complex udt2, c_int int) WITH ID = b613aee8-645c-4384-90d2-fc9e82fb1a59")); + node.executeInternal("CREATE TABLE " + KS + ".tab4_frozen_udt2 (pk int PRIMARY KEY, a_int int, b_complex frozen, c_int int) WITH ID = 9c03c71c-6775-4357-9173-0f8808901afa"); + node.executeInternal("CREATE TABLE " + KS + ".tab5_tuple (pk int PRIMARY KEY, a_int int, b_complex tuple, c_int int) WITH ID = 90826dd3-8437-4585-9de4-15908236687f"); + node.executeInternal("CREATE TABLE " + KS + ".tab6_frozen_tuple (pk int PRIMARY KEY, a_int int, b_complex frozen>, c_int int) WITH ID = 54185f9a-a6fd-487c-abc3-c01bd5835e48"); + node.executeInternal("CREATE TABLE " + KS + ".tab7_tuple_with_udt (pk int PRIMARY KEY, a_int int, b_complex tuple, c_int int) WITH ID = 4e78f403-7b63-4e0d-a231-42e42cba7cb5"); + node.executeInternal("CREATE TABLE " + KS + ".tab8_frozen_tuple_with_udt (pk int PRIMARY KEY, a_int int, b_complex frozen>, c_int int) WITH ID = 8660f235-0816-4019-9cc9-1798fa7beb17"); + node.executeInternal("CREATE TABLE " + KS + ".tab9_udt_with_tuple (pk int PRIMARY KEY, a_int int, b_complex udt3, c_int int) WITH ID = f670fd5a-8145-4669-aceb-75667c000ea6"); + node.executeInternal("CREATE TABLE " + KS + ".tab10_frozen_udt_with_tuple (pk int PRIMARY KEY, a_int int, b_complex frozen, c_int int) WITH ID = 6a5cff4e-2f94-4c8b-9aa2-0fbd65292caa"); + } + + private void blockFlushing(List dirs) throws IOException + { + for (Path dir : dirs) + { + Set permissions = Files.getPosixFilePermissions(dir); + permissions.remove(PosixFilePermission.OWNER_WRITE); + permissions.remove(PosixFilePermission.GROUP_WRITE); + permissions.remove(PosixFilePermission.OTHERS_WRITE); + Files.setPosixFilePermissions(dir, permissions); + } + } + + private void unblockFlushing(List dirs) throws IOException + { + for (Path dir : dirs) + { + Set permissions = Files.getPosixFilePermissions(dir); + permissions.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(dir, permissions); + } + } + + @Test + public void testReadingValuesOfDroppedColumns() throws Throwable + { + // given there is a table with a UDT column and some additional non-UDT columns, and there are rows with + // different combinations of values and nulls for all columns + try (Cluster cluster = Cluster.build(1).withConfig(c -> c.with(GOSSIP, NATIVE_PROTOCOL)).start()) + { + IInvokableInstance node = cluster.get(1); + node.executeInternal("CREATE KEYSPACE " + KS + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + node.executeInternal("CREATE TYPE " + KS + ".udt (foo text, bar text)"); + node.executeInternal("CREATE TABLE " + KS + ".tab (pk int PRIMARY KEY, a_udt udt, b text, c text)"); + node.executeInternal("INSERT INTO " + KS + ".tab (pk, c) VALUES (1, 'c_value')"); + node.executeInternal("INSERT INTO " + KS + ".tab (pk, b) VALUES (2, 'b_value')"); + node.executeInternal("INSERT INTO " + KS + ".tab (pk, a_udt) VALUES (3, {foo: 'a_foo', bar: 'a_bar'})"); + + File dataDir = new File(node.callOnInstance(() -> Keyspace.open(KS) + .getColumnFamilyStore("tab") + .getDirectories() + .getDirectoryForNewSSTables() + .absolutePath())); + checkData(cluster); + + // when the UDT columns is dropped while the data cannot be flushed before drop and must remain in the commitlog + + // prevent flushing the data + Set permissions = Files.getPosixFilePermissions(dataDir.toPath()); + permissions.remove(PosixFilePermission.OWNER_WRITE); + permissions.remove(PosixFilePermission.GROUP_WRITE); + permissions.remove(PosixFilePermission.OTHERS_WRITE); + Files.setPosixFilePermissions(dataDir.toPath(), permissions); + + node.executeInternal("ALTER TABLE " + KS + ".tab DROP a_udt"); + + // and the node is restarted + // restart is needed because this way we can simulate the situation where the commit log contains the data + // of the dropped cell, while the schema is already altered (the column moved to dropped columns and transformed) + node.shutdown(false).get(10, TimeUnit.SECONDS); + + // unlock the ability to flush data + permissions = Files.getPosixFilePermissions(dataDir.toPath()); + permissions.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(dataDir.toPath(), permissions); + node.startup(); + + // then, we should still be able to read the data of the remaining columns correctly + checkData(cluster); + + // and even after flushing and restarting the node again + // the next restart is needed to make sure that the sstable header is read from disk + node.flush(KS); + node.shutdown(false).get(10, TimeUnit.SECONDS); + node.startup(); + + checkData(cluster); + + // verify that the sstable can be read with sstabledump + String sstable = node.callOnInstance(() -> Keyspace.open(KS).getColumnFamilyStore("tab") + .getDirectories().getCFDirectories() + .get(0).tryList()[0].toString()); + ToolRunner.ToolResult tool = ToolRunner.invokeClass(SSTableExport.class, sstable); + tool.assertCleanStdErr(); + tool.assertOnExitCode(); + assertThat(tool.getStdout()) + .contains("\"key\" : [ \"1\" ],") + .contains("\"key\" : [ \"2\" ],") + .contains("{ \"name\" : \"c\", \"value\" : \"c_value\" }") + .contains("{ \"name\" : \"b\", \"value\" : \"b_value\" }"); + } + } + + private void checkData(Cluster cluster) + { + ICoordinator coordinator = cluster.coordinator(1); + String query = "SELECT b, c FROM " + KS + ".tab WHERE pk = ?"; + assertRows(coordinator.execute(query, ConsistencyLevel.QUORUM, 1), row(null, "c_value")); + assertRows(coordinator.execute(query, ConsistencyLevel.QUORUM, 2), row("b_value", null)); + assertRows(coordinator.execute(query, ConsistencyLevel.QUORUM, 3), row(null, null)); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/DurableWritesTest.java b/test/distributed/org/apache/cassandra/distributed/test/DurableWritesTest.java index b3debad19525..d7948f5b03e1 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/DurableWritesTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/DurableWritesTest.java @@ -47,7 +47,7 @@ public void durableWritesDisabledTest() throws Throwable cluster.get(1).runOnInstance(() -> { TableId wanted = TableId.fromString(Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl").metadata.id.toString()); - boolean containsTbl = CommitLog.instance.segmentManager + boolean containsTbl = CommitLog.instance.getSegmentManager() .getActiveSegments() .stream() .anyMatch(s -> s.getDirtyTableIds().contains(wanted)); diff --git a/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java index 405279aae609..155bee54d10c 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java @@ -62,6 +62,7 @@ import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableRunnable; import org.apache.cassandra.distributed.impl.InstanceKiller; +import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.io.sstable.CorruptSSTableException; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.SSTableReadsListener; @@ -81,12 +82,12 @@ public class FailingRepairTest extends TestBaseImpl implements Serializable { private static ICluster CLUSTER; - private final Verb messageType; + private final int messageType; private final RepairParallelism parallelism; private final boolean withTracing; private final SerializableRunnable setup; - public FailingRepairTest(Verb messageType, RepairParallelism parallelism, boolean withTracing, SerializableRunnable setup) + public FailingRepairTest(int messageType, RepairParallelism parallelism, boolean withTracing, SerializableRunnable setup) { this.messageType = messageType; this.parallelism = parallelism; @@ -102,15 +103,16 @@ public static Collection messages() { for (Boolean withTracing : Arrays.asList(Boolean.TRUE, Boolean.FALSE)) { - tests.add(new Object[]{ Verb.VALIDATION_REQ, parallelism, withTracing, failingReaders(Verb.VALIDATION_REQ, parallelism, withTracing) }); + tests.add(new Object[]{ Verb.VALIDATION_REQ.id, parallelism, withTracing, failingReaders(Verb.VALIDATION_REQ.id, parallelism, withTracing) }); } } return tests; } - private static SerializableRunnable failingReaders(Verb type, RepairParallelism parallelism, boolean withTracing) + private static SerializableRunnable failingReaders(int typeId, RepairParallelism parallelism, boolean withTracing) { return () -> { + Verb type = Verb.fromId(typeId); String cfName = getCfName(type, parallelism, withTracing); ColumnFamilyStore cf = Keyspace.open(KEYSPACE).getColumnFamilyStore(cfName); Util.flush(cf); @@ -148,7 +150,11 @@ public static void setupCluster() throws IOException .start()); CLUSTER.setUncaughtExceptionsFilter((throwable) -> { if (throwable.getClass().toString().contains("InstanceShutdown") || // can't check instanceof as it is thrown by a different classloader - throwable.getMessage() != null && throwable.getMessage().contains("Parent repair session with id")) + (throwable.getMessage() != null && throwable.getMessage().contains("Parent repair session with id")) || + (throwable.getClass().toString().contains("RepairException") && + throwable.getMessage() != null && + throwable.getMessage().contains("Validation failed")) + ) return true; return false; }); @@ -169,7 +175,14 @@ public void cleanupState() IInvokableInstance inst = CLUSTER.get(i); if (inst.isShutdown()) inst.startup(); - inst.runOnInstance(InstanceKiller::clear); + inst.runOnInstance(() -> { + InstanceKiller.clear(); + if (!StorageService.instance.isGossipActive()) + { + StorageService.instance.startGossiping(); + Gossiper.waitToSettle(); + } + }); } } @@ -178,7 +191,7 @@ public void testFailingMessage() throws IOException { final int replica = 1; final int coordinator = 2; - String tableName = getCfName(messageType, parallelism, withTracing); + String tableName = getCfName(Verb.fromId(messageType), parallelism, withTracing); String fqtn = KEYSPACE + "." + tableName; CLUSTER.schemaChange("CREATE TABLE " + fqtn + " (k INT, PRIMARY KEY (k))"); @@ -336,6 +349,11 @@ public Set getBackingSSTables() return Collections.emptySet(); } + public int level() + { + return 0; + } + public TableMetadata metadata() { return null; diff --git a/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java b/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java index 58d44f558c19..421428d13a64 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java @@ -39,8 +39,8 @@ import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.service.reads.range.RangeCommandIterator; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.service.reads.range.NonGroupingRangeCommandIterator; import static net.bytebuddy.matcher.ElementMatchers.named; import static org.junit.Assert.assertEquals; @@ -142,7 +142,7 @@ static void install(ClassLoader cl, int nodeNumber) .make() .load(cl, ClassLoadingStrategy.Default.INJECTION); - bb.redefine(RangeCommandIterator.class) + bb.redefine(NonGroupingRangeCommandIterator.class) .method(named("sendNextRequests")) .intercept(MethodDelegation.to(BBRequestFailures.class)) .make() diff --git a/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java b/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java index 3314c2a6ba11..a9441d65ebe1 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.concurrent.ExecutionException; +import org.junit.Assert; import org.junit.Test; import org.apache.cassandra.distributed.Cluster; @@ -124,6 +125,7 @@ public void testUpgradeSStables() throws IOException } } + /* See CASSANDRA-19764 */ @Test public void testDivergentSchemas() throws Throwable { @@ -133,11 +135,20 @@ public void testDivergentSchemas() throws Throwable cluster.schemaChange("create table " + KEYSPACE + ".x (id int, ck frozen, i int, primary key (id, ck))"); cluster.get(1).executeInternal("alter type " + KEYSPACE + ".a add bar text"); - cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 1) + ", ? )", ConsistencyLevel.ALL, - 1, 1); - cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 2) + ", ? )", ConsistencyLevel.ALL, - 2, 2); - cluster.get(2).flush(KEYSPACE); + try + { + cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 2) + ", ? )", ConsistencyLevel.ALL, + 1, 2); + cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 1) + ", ? )", ConsistencyLevel.ALL, + 1, 1); + cluster.get(2).flush(KEYSPACE); + Assert.fail("Expected an exception to be thrown."); + } + catch (Exception e) + { + // correct path + System.out.println(e); + } } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java b/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java index 341d85482d0c..2d1b2c2f3dc6 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java @@ -32,6 +32,7 @@ import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.SystemDistributedKeyspace; import org.apache.cassandra.schema.SchemaConstants; @@ -64,7 +65,7 @@ public void testGossipSettles() throws Throwable // First prove that the storage port is added Assert.assertEquals("stuff 127.0.0.1:7012 morestuff 127.0.0.2:7012", addStoragePortToIP("stuff 127.0.0.1 morestuff 127.0.0.2")); - FailureDetector fd = ((FailureDetector) FailureDetector.instance); + FailureDetector fd = ((FailureDetector) IFailureDetector.instance); Assert.assertEquals(addStoragePortToInstanceName(fd.getAllEndpointStates(false)), fd.getAllEndpointStates(true)); Assert.assertEquals(addPortToKeys(fd.getSimpleStates()), fd.getSimpleStatesWithPort()); diff --git a/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java b/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java index 382530daa822..4c60524e1531 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java @@ -19,6 +19,7 @@ package org.apache.cassandra.distributed.test; import java.io.Closeable; +import java.io.IOException; import java.net.InetSocketAddress; import java.util.Collection; import java.util.Set; @@ -39,6 +40,7 @@ import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.*; @@ -48,17 +50,20 @@ import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.GossipDigestSyn; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.IClusterVersionProvider; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.StreamPlan; import org.apache.cassandra.streaming.StreamResultFuture; +import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.FBUtilities; import org.assertj.core.api.Assertions; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; +import static org.apache.cassandra.config.CassandraRelevantProperties.CLUSTER_VERSION_PROVIDER_CLASS_NAME; import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; import static org.apache.cassandra.config.CassandraRelevantProperties.RING_DELAY; import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; @@ -68,6 +73,7 @@ import static org.apache.cassandra.distributed.impl.DistributedTestSnitch.toCassandraInetAddressAndPort; import static org.apache.cassandra.distributed.shared.ClusterUtils.runAndWaitForLogs; import static org.apache.cassandra.distributed.shared.NetworkTopology.singleDcNetworkTopology; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -75,6 +81,79 @@ public class GossipTest extends TestBaseImpl { private static final Logger logger = LoggerFactory.getLogger(GossipTest.class); + public static class CustomClusterVersionProvider implements IClusterVersionProvider + { + public static volatile CassandraVersion version = CassandraVersion.NULL_VERSION; + public static volatile boolean initialized = false; + public static volatile long lastReset = 0; + public static volatile boolean upgradeInProgress = true; + + public CustomClusterVersionProvider() + { + initialized = true; + } + + @Override + public CassandraVersion getMinClusterVersion() + { + return version; + } + + @Override + public void reset() + { + lastReset = System.currentTimeMillis(); + } + + @Override + public boolean isUpgradeInProgress() + { + return upgradeInProgress; + } + } + + @Test + public void testCustomMinClusterVersionProvider() throws IOException + { + CLUSTER_VERSION_PROVIDER_CLASS_NAME.setString(CustomClusterVersionProvider.class.getName()); + + try (Cluster cluster = Cluster.build(1).withConfig(config -> config.with(GOSSIP)).start()) + { + IInvokableInstance i = cluster.get(1); + assertThat(i.callOnInstance(() -> CustomClusterVersionProvider.initialized)).isTrue(); + + i.runOnInstance(() -> CustomClusterVersionProvider.version = CassandraVersion.NULL_VERSION); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_3_4))).isTrue(); + + i.runOnInstance(() -> CustomClusterVersionProvider.version = CassandraVersion.CASSANDRA_3_4); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_3_4))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0_RC2))).isTrue(); + + i.runOnInstance(() -> CustomClusterVersionProvider.version = CassandraVersion.CASSANDRA_4_0); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_3_4))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0_RC2))).isTrue(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION))).isTrue(); + + i.runOnInstance(() -> CustomClusterVersionProvider.version = CassandraVersion.CASSANDRA_4_0_RC2); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_3_4))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0_RC2))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION))).isTrue(); + + i.runOnInstance(() -> CustomClusterVersionProvider.version = SystemKeyspace.CURRENT_VERSION); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_3_4))).isTrue(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0))).isTrue(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0_RC2))).isTrue(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION))).isTrue(); + + i.runOnInstance(() -> CustomClusterVersionProvider.upgradeInProgress = false); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_3_4))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0_RC2))).isFalse(); + assertThat(i.callOnInstance(() -> Gossiper.instance.isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION))).isFalse(); + } + } @Test public void nodeDownDuringMove() throws Throwable diff --git a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java index 83bcaaad3c14..6ae233699082 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java @@ -220,13 +220,6 @@ public void allInternodeEncryptionEstablishedTest() throws Throwable /** * Tests that the negotiated protocol is the highest common protocol between the client and server. - *

    - * Note: This test uses TLSV1.1, which is disabled by default in JDK 8 and higher. If the test fails with - * FAILED_TO_NEGOTIATE, it may be necessary to check the java.security file in your JDK installation and remove - * TLSv1.1 from the jdk.tls.disabledAlgorithms. - * @see CASSANDRA-18540 - * @see - * TLSv1 and TLSv1.1 Protocols are Disabled in Java! */ @Test public void negotiatedProtocolMustBeAcceptedProtocolTest() throws Throwable @@ -236,7 +229,7 @@ public void negotiatedProtocolMustBeAcceptedProtocolTest() throws Throwable c.set("server_encryption_options", ImmutableMap.builder().putAll(validKeystore) .put("internode_encryption", "all") - .put("accepted_protocols", ImmutableList.of("TLSv1.1", "TLSv1.2", "TLSv1.3")) + .put("accepted_protocols", ImmutableList.of("TLSv1.2", "TLSv1.3")) .build()); }).start()) { @@ -250,9 +243,9 @@ public void negotiatedProtocolMustBeAcceptedProtocolTest() throws Throwable tls10Connection.assertReceivedHandshakeException(); TlsConnection tls11Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1.1")); - Assert.assertEquals("Should be possible to establish a TLSv1.1 connection", - ConnectResult.NEGOTIATED, tls11Connection.connect()); - Assert.assertEquals("TLSv1.1", tls11Connection.lastProtocol()); + Assert.assertEquals("Should not be possible to establish a TLSv1.1 connection", + ConnectResult.FAILED_TO_NEGOTIATE, tls11Connection.connect()); + tls11Connection.assertReceivedHandshakeException(); TlsConnection tls12Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1.2")); Assert.assertEquals("Should be possible to establish a TLSv1.2 connection", diff --git a/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java b/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java index b63179b70e02..3112bc1d6c68 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java @@ -231,7 +231,7 @@ public UnfilteredRowIterator rowIterator(DecoratedKey key, Slices slices, Column private CorruptSSTableException throwCorrupted() { - throw new CorruptSSTableException(new IOException("failed to get position"), descriptor.baseFile()); + throw new CorruptSSTableException(new IOException("failed to get position"), descriptor.baseFileUri()); } private FSError throwFSError() diff --git a/test/distributed/org/apache/cassandra/distributed/test/MetricsCountQueriesTest.java b/test/distributed/org/apache/cassandra/distributed/test/MetricsCountQueriesTest.java index a742e483793c..16e9f361882c 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/MetricsCountQueriesTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/MetricsCountQueriesTest.java @@ -52,6 +52,6 @@ public void testMetricsCountQueries() throws Throwable private static long readCount(IInvokableInstance instance) { - return instance.callOnInstance(() -> Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl").metric.readLatency.latency.getCount()); + return instance.callOnInstance(() -> Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl").metric.readLatency.tableOrKeyspaceMetric().latency.getCount()); } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java index 3e8c92648099..098aa7e236d6 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java @@ -170,7 +170,7 @@ public void negotiatedProtocolMustBeAcceptedProtocolTest() throws Throwable c.set("client_encryption_options", ImmutableMap.builder().putAll(validKeystore) .put("enabled", true) - .put("accepted_protocols", ImmutableList.of("TLSv1.1", "TLSv1.2", "TLSv1.3")) + .put("accepted_protocols", ImmutableList.of("TLSv1.2", "TLSv1.3")) .build()); }).start()) { @@ -183,9 +183,9 @@ public void negotiatedProtocolMustBeAcceptedProtocolTest() throws Throwable tls10Connection.assertReceivedHandshakeException(); TlsConnection tls11Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1.1")); - Assert.assertEquals("Should be possible to establish a TLSv1.1 connection", - ConnectResult.NEGOTIATED, tls11Connection.connect()); - Assert.assertEquals("TLSv1.1", tls11Connection.lastProtocol()); + Assert.assertEquals("Should not be possible to establish a TLSv1.1 connection", + ConnectResult.FAILED_TO_NEGOTIATE, tls11Connection.connect()); + tls11Connection.assertReceivedHandshakeException(); TlsConnection tls12Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1.2")); Assert.assertEquals("Should be possible to establish a TLSv1.2 connection", diff --git a/test/distributed/org/apache/cassandra/distributed/test/NodeToolEnableDisableBinaryTest.java b/test/distributed/org/apache/cassandra/distributed/test/NodeToolEnableDisableBinaryTest.java index 36803fbdfd9c..fed4df7447e6 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/NodeToolEnableDisableBinaryTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/NodeToolEnableDisableBinaryTest.java @@ -96,7 +96,7 @@ public void testMaybeChangeDocs() " Remote jmx agent username\n" + "\n" + "\n"; - Assertions.assertThat(tool.getStdout()).isEqualTo(help); + Assertions.assertThat(tool.getCleanedStdout()).isEqualTo(help); tool = ToolRunner.invokeNodetoolJvmDtest(cluster.get(1), "help", "enablebinary"); help = "NAME\n" + @@ -128,7 +128,7 @@ public void testMaybeChangeDocs() " Remote jmx agent username\n" + "\n" + "\n"; - Assertions.assertThat(tool.getStdout()).isEqualTo(help); + Assertions.assertThat(tool.getCleanedStdout()).isEqualTo(help); } @Test diff --git a/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java b/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java index 24a65e3d4e55..ce0dc2a76f75 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java @@ -19,19 +19,26 @@ package org.apache.cassandra.distributed.test; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.function.Consumer; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.crypto.TDEConfigurationProvider; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.NodeToolResult; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class NodeToolTest extends TestBaseImpl { @@ -147,4 +154,99 @@ public void testVersionIncludesGitSHAWhenVerbose() throws Throwable .success() .stdoutContains("GitSHA:"); } + + @Test + public void testCompactionStats() throws Throwable + { + NodeToolResult result = NODE.nodetoolResult("compactionstats", "--aggregate", "--overlap"); + result.asserts().success().stdoutContains("pending tasks"); + result.asserts().success().stdoutContains("Aggregated view"); + result.asserts().success().stdoutContains("Max overlap map"); + + result = NODE.nodetoolResult("compactionstats", "--aggregate", "--overlap", "--human-readable", "system_schema", "tables"); + result.asserts().success().stdoutContains("system_schema.tables"); + result.asserts().success().stdoutNotContains("system.peers"); + result.asserts().success().stdoutNotContains("system_schema.keyspaces"); + } + + @Test + public void testSuccesfulSystemKeyCreation() throws Throwable + { + try + { + // given a command with default key name and location + Path testDir1 = Files.createTempDirectory("test_1"); + CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.setString(testDir1.toString()); + Path systemKey = Paths.get(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).resolve("system_key"); + assertFalse(Files.exists(systemKey)); + // when + NodeToolResult result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "128"); + // then should create a key + result.asserts().success(); + result.asserts().stdoutContains("Successfully created key"); + assertTrue(Files.exists(systemKey)); + + // given a command with specified key name and default key location + Path testDir2 = Files.createTempDirectory("test_2"); + CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.setString(testDir2.toString()); + systemKey = Paths.get(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).resolve("system_key_2"); + assertFalse(Files.exists(systemKey)); + // when + result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "128", "system_key_2"); + // then should create a key + result.asserts().success(); + result.asserts().stdoutContains("Successfully created key"); + assertTrue(Files.exists(systemKey)); + + // given a command with specified key location and default key name + Path testDir3 = Files.createTempDirectory("test_3"); + assertFalse(Files.exists(testDir3.resolve("system_key"))); + // when + result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "128", "-d", testDir3.toString()); + // then should create a key + result.asserts().success(); + result.asserts().stdoutContains("Successfully created key"); + assertTrue(Files.exists(testDir3.resolve("system_key"))); + } + finally + { + CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.reset(); + } + } + + @Test + public void testUnsuccesfulSystemKeyCreation() + { + try + { + // given a command without key type and strength + NodeToolResult result = NODE.nodetoolResult("createsystemkey"); + // then should fail creation + result.asserts().failure(); + result.asserts().stderrContains("Usage: nodetool createsystemkey []"); + + // given a command without key strength + result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding"); + // then should fail creation + result.asserts().failure(); + result.asserts().stderrContains("Usage: nodetool createsystemkey []"); + + // given a command with incorrect algorithm name + result = NODE.nodetoolResult("createsystemkey", "INVALIDNAME", "128"); + // then should fail creation + result.asserts().failure(); + result.asserts().stderrContains("System key (INVALIDNAME 128) was not created"); + result.asserts().stderrContains("Available algorithms are: AES, ARCFOUR, Blowfish, DES, DESede, HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 and RC2"); + + // given a command with incorrect algorithm strength + result = NODE.nodetoolResult("createsystemkey", "AES", "99"); + // then should fail creation + result.asserts().failure(); + result.asserts().stderrContains("System key (AES 99) was not created"); + } + finally + { + CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.reset(); + } + } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java index a150a33a95ae..7137d5d78e33 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java @@ -596,7 +596,7 @@ public SingleUpdateSupplier(TableMetadata cfm, DecoratedKey dk, Ballot ballot) public CloseableIterator repairIterator(TableId cfId, Collection> ranges) { if (!cfId.equals(cfm.id)) - return CloseableIterator.empty(); + return CloseableIterator.emptyIterator(); return CloseableIterator.wrap(Collections.singleton(new PaxosKeyState(cfId, dk, ballot, false)).iterator()); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/PreviewRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/PreviewRepairTest.java index a0b643f0d309..39cd73a0f3ad 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PreviewRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PreviewRepairTest.java @@ -111,7 +111,7 @@ public void testWithMismatchingPending() throws Throwable // also disables autocompaction on the nodes cluster.forEach((node) -> node.runOnInstance(() -> { ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl"); - FBUtilities.waitOnFutures(CompactionManager.instance.submitBackground(cfs)); + FBUtilities.waitOnFuture(CompactionManager.instance.submitBackground(cfs)); cfs.disableAutoCompaction(); })); long[] marks = logMark(cluster); @@ -120,7 +120,7 @@ public void testWithMismatchingPending() throws Throwable cluster.get(1).runOnInstance(() -> { ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl"); cfs.enableAutoCompaction(); - FBUtilities.waitOnFutures(CompactionManager.instance.submitBackground(cfs)); + FBUtilities.waitOnFuture(CompactionManager.instance.submitBackground(cfs)); }); waitLogsRepairFullyFinished(cluster, marks); @@ -210,6 +210,9 @@ public void testConcurrentIncRepairDuringPreview() throws IOException, Interrupt config.with(GOSSIP) .with(NETWORK)).start())) { + cluster.setUncaughtExceptionsFilter(t -> t.getClass().toString().contains("RepairException") && + t.getMessage() != null && + t.getMessage().contains("Validation failed")); cluster.schemaChange("create table " + KEYSPACE + ".tbl (id int primary key, t int)"); insert(cluster.coordinator(1), 0, 100); cluster.forEach((node) -> node.flush(KEYSPACE)); @@ -419,7 +422,7 @@ private void unmarkRepaired(IInvokableInstance instance, String table) ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(table); try { - cfs.getCompactionStrategyManager().mutateRepaired(cfs.getLiveSSTables(), ActiveRepairService.UNREPAIRED_SSTABLE, null, false); + cfs.mutateRepaired(cfs.getLiveSSTables(), ActiveRepairService.UNREPAIRED_SSTABLE, null, false); } catch (IOException e) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java b/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java index b0c3902ad152..de6cd6e86672 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java @@ -30,6 +30,8 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; + import com.datastax.driver.core.Session; import org.apache.cassandra.db.Keyspace; @@ -43,10 +45,15 @@ import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.utils.Throwables; +import org.jboss.byteman.contrib.bmunit.BMRule; +import org.jboss.byteman.contrib.bmunit.BMUnitRunner; + + import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import static org.junit.Assert.assertTrue; +@RunWith(BMUnitRunner.class) public class QueriesTableTest extends TestBaseImpl { private static Cluster SHARED_CLUSTER; @@ -76,6 +83,10 @@ public static void closeCluster() } @Test + @BMRule(name = "Make mutations slow", + targetClass = "Mutation", + targetMethod = "apply", + action = "Thread.sleep(100)") public void shouldExposeReadsAndWrites() throws Throwable { SHARED_CLUSTER.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (k int primary key, v int)"); diff --git a/test/distributed/org/apache/cassandra/distributed/test/QueryInfoTrackerDistributedTest.java b/test/distributed/org/apache/cassandra/distributed/test/QueryInfoTrackerDistributedTest.java new file mode 100644 index 000000000000..733026c340b1 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/QueryInfoTrackerDistributedTest.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.test.sai.SAIUtil; +import org.apache.cassandra.service.QueryInfoTrackerTest.TestQueryInfoTracker; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; +import static org.apache.cassandra.distributed.shared.AssertUtils.row; + +public class QueryInfoTrackerDistributedTest extends TestBaseImpl +{ + private static Cluster cluster; + private final static String rfOneKs = "rfoneks"; + + @BeforeClass + public static void setupCluster() throws Throwable + { + cluster = init(Cluster.build().withNodes(3).withConfig(config -> config.with(NETWORK, GOSSIP)).start()); + cluster.schemaChange("CREATE KEYSPACE " + rfOneKs + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); + } + + @AfterClass + public static void close() throws Exception + { + cluster.close(); + cluster = null; + } + + @Test + @SuppressWarnings("rawtypes") + public void testTrackingInDataResolverResolve() + { + ReadRepairTester tester = new ReadRepairTester(cluster, ReadRepairStrategy.BLOCKING, 1, false, false, false) + { + @Override + ReadRepairTester self() + { + return this; + } + }; + + String keyspace = tester.qualifiedTableName.split("\\.")[0]; + + tester.createTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + cluster.coordinator(1).execute("INSERT INTO " + tester.qualifiedTableName + " (pk, ck, v) VALUES (1, 1, 1)", + ConsistencyLevel.QUORUM); + + tester.mutate(2, "INSERT INTO %s (pk, ck, v) VALUES (1, 1, 2)"); + + setQueryTracker(tester.coordinator, keyspace); + + tester.assertRowsDistributed("SELECT * FROM %s WHERE pk=1 AND ck=1", + 2, + row(1, 1, 2)); + + assertQueryTracker(tester.coordinator, tracker -> { + Assert.assertEquals(1, tracker.reads.get()); + Assert.assertEquals(1, tracker.readPartitions.get()); + Assert.assertEquals(1, tracker.readRows.get()); + Assert.assertEquals(1, tracker.replicaPlans.get()); + }); + } + + @Test + public void testTrackingInDigestResolverGetData() + { + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (1, 1, 1)", + ConsistencyLevel.QUORUM); + + setQueryTracker(1, KEYSPACE); + + assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", + ConsistencyLevel.QUORUM), + row(1, 1, 1)); + + assertQueryTracker(1, tracker -> { + Assert.assertEquals(1, tracker.reads.get()); + Assert.assertEquals(1, tracker.readPartitions.get()); + Assert.assertEquals(1, tracker.readRows.get()); + Assert.assertEquals(1, tracker.replicaPlans.get()); + }); + } + + @Test + public void testTrackingReadsWithEndpointGrouping() + { + String table = rfOneKs + ".saiTbl"; + cluster.schemaChange("CREATE TABLE " + table + " (id1 TEXT PRIMARY KEY, v1 INT, v2 TEXT)"); + cluster.schemaChange("CREATE CUSTOM INDEX IF NOT EXISTS test_idx ON " + table + " (v1) USING 'StorageAttachedIndex'"); + SAIUtil.waitForIndexQueryableOnFirstNode(cluster, KEYSPACE); + + int rowsCount = 1000; + + for (int i = 0; i < rowsCount; ++i) + { + cluster.coordinator(1).execute("INSERT INTO " + table + " (id1, v1, v2) VALUES (?, ?, ?);", + ConsistencyLevel.QUORUM, + String.valueOf(i), + i, + String.valueOf(i)); + } + + setQueryTracker(1, rfOneKs); + + cluster.coordinator(1).execute(String.format("SELECT id1 FROM %s WHERE v1>=0", table), + ConsistencyLevel.QUORUM); + + assertQueryTracker(1, tracker -> { + Assert.assertEquals(1, tracker.rangeReads.get()); + Assert.assertEquals(rowsCount, tracker.readPartitions.get()); + Assert.assertEquals(rowsCount, tracker.readRows.get()); + Assert.assertEquals(4, tracker.replicaPlans.get()); + }); + } + + @Test + public void testANNQueryWithIndexRestrictionAndLIMIT() + { + String table = rfOneKs + ".ann_table"; + cluster.schemaChange("CREATE TABLE " + table + " (p int PRIMARY KEY, v int, ni int, vec VECTOR)"); + cluster.schemaChange("CREATE CUSTOM INDEX ON " + table + "(vec) USING 'StorageAttachedIndex'"); + cluster.schemaChange("CREATE CUSTOM INDEX ON " + table + "(v) USING 'StorageAttachedIndex'"); + SAIUtil.waitForIndexQueryableOnFirstNode(cluster, rfOneKs); + + for (int rowIdx = 0; rowIdx < 100; rowIdx++) + { + cluster.coordinator(1).execute("INSERT INTO " + table + "(p, v, ni, vec) VALUES (?, ?, ?, [0.5, 0.3])", + ConsistencyLevel.ALL, rowIdx, rowIdx, rowIdx); + } + + setQueryTracker(1, rfOneKs); + + cluster.coordinator(1).execute("SELECT * FROM " + table + " WHERE v > 50 ORDER BY vec ANN OF [0.1, 0.9] LIMIT 3", + ConsistencyLevel.ONE); + + assertQueryTracker(1, tracker -> { + Assert.assertEquals(1, tracker.rangeReads.get()); + Assert.assertEquals(3, tracker.readFilteredRows.get()); + }); + } + + private void setQueryTracker(int node, String keyspace) + { + cluster.get(node).runOnInstance(() -> StorageProxy.instance.registerQueryTracker(new TestQueryInfoTracker(keyspace))); + } + + private void assertQueryTracker(int node, IIsolatedExecutor.SerializableConsumer tester) + { + cluster.get(node).runOnInstance(() -> { + TestQueryInfoTracker tracker = (TestQueryInfoTracker) StorageProxy.queryTracker(); + tester.accept(tracker); + }); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReadCoordinationMetricsTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReadCoordinationMetricsTest.java new file mode 100644 index 000000000000..0990af8a9e8e --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ReadCoordinationMetricsTest.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.net.InetAddress; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.impl.DistributedTestSnitch; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.ReadCoordinationMetrics; +import org.apache.cassandra.service.reads.AbstractReadExecutor; + +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ONE; + +public class ReadCoordinationMetricsTest extends TestBaseImpl +{ + private static final int NUM_ROWS = 100; + + private static long countNonreplicaRequests(IInvokableInstance node) + { + return node.callOnInstance(() -> ReadCoordinationMetrics.nonreplicaRequests.getCount()); + } + + private static long countPreferredOtherReplicas(IInvokableInstance node) + { + return node.callOnInstance(() -> ReadCoordinationMetrics.preferredOtherReplicas.getCount()); + } + + /** + * Two nodes with RF=1 so half the data will be owned by each node and the coordinator for queries is not + * always a replica in the list of candidates in {@link AbstractReadExecutor#getReadExecutor} where + * {@link ReadCoordinationMetrics#nonreplicaRequests} will be incremented. + * + * @throws Throwable + */ + @Test + public void testNonReplicaRequests() throws Throwable + { + try (Cluster cluster = init(Cluster.create(2), 1)) + { + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))")); + for (int i = 0; i < NUM_ROWS; i++) + cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl (pk, ck, v) VALUES (?,?,?)"), ALL, i, i, i); + + long nonReplicaRequests1 = countNonreplicaRequests(cluster.get(1)); + long nonReplicaRequests2 = countNonreplicaRequests(cluster.get(2)); + + for (int i = 0; i < NUM_ROWS; i++) + { + // When the coordinator is not a candidate replica, which will be half the time due to RF=1, + // the non-replica count metric will be incremented. + cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ? and ck = ?"), ALL, i, i); + cluster.coordinator(2).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ? and ck = ?"), ALL, i, i); + } + + nonReplicaRequests1 = countNonreplicaRequests(cluster.get(1)) - nonReplicaRequests1; + nonReplicaRequests2 = countNonreplicaRequests(cluster.get(2)) - nonReplicaRequests2; + Assert.assertEquals(NUM_ROWS, nonReplicaRequests1 + nonReplicaRequests2); + } + } + + /** + * Two nodes with RF=2 so that both nodes are replicas for all data; this ensures that the coordinator node for + * queries will be a replica in the list of candidates in {@link AbstractReadExecutor#getReadExecutor}. + *

    + * When the candidates collection is created, the sort order is changed so that the coordinator node is last. Then, + * using with CL=1 in the query, the resulting contacts collection will not contain the coordinator node, causing + * {@link ReadCoordinationMetrics#preferredOtherReplicas} to be incremented. + * + * @throws Throwable + */ + @Test + public void testPreferredOtherReplicas() throws Throwable + { + try (Cluster cluster = init(builder() + .withNodes(2) + .withConfig(config -> config.set("dynamic_snitch", false) + ).start(), 2)) + { + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))")); + for (int i = 0; i < NUM_ROWS; i++) + cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl (pk, ck, v) VALUES (?,?,?)"), ALL, i, i, i); + + long preferredOtherReplicas1 = countPreferredOtherReplicas(cluster.get(1)); + + // Replica nodes are normally sorted by distance from the coordinator; override the test snitch to + // sort with respect to another node so that the coordinator node is last in the list of replicas. + // This will be used together with CL=1 to drop the coordinator node from the list of contacts, while + // remaining in the list of candidates. + InetAddress address2 = cluster.get(2).broadcastAddress().getAddress(); + cluster.get(1).acceptsOnInstance((IIsolatedExecutor.SerializableConsumer) (ks) -> { + DistributedTestSnitch.sortByProximityAddressOverride = InetAddressAndPort.getByAddress(ks); + }).accept(address2); + + for (int i = 0; i < NUM_ROWS; i++) + { + // Query using CL=1 so that the subset of "candidate" replcas selected for the "contacts" collection + // will have just one node; since the "candidate" list was sorted with respect to the non-coordinator + // node, this will cause the preferredOtherReplicas count to be incremented. + cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ? and ck = ?"), ONE, i, i); + } + + preferredOtherReplicas1 = countPreferredOtherReplicas(cluster.get(1)) - preferredOtherReplicas1; + Assert.assertEquals(NUM_ROWS, preferredOtherReplicas1); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReadFailureTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReadFailureTest.java deleted file mode 100644 index be8db6c7782f..000000000000 --- a/test/distributed/org/apache/cassandra/distributed/test/ReadFailureTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.distributed.test; - -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.junit.Test; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.distributed.api.ICluster; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.exceptions.RequestFailureReason; - -public class ReadFailureTest extends TestBaseImpl -{ - static final int TOMBSTONE_FAIL_THRESHOLD = 20; - static final int TOMBSTONE_FAIL_KEY = 100001; - static final String TABLE = "t"; - - /** - * This test attempts to create a race condition with speculative executions that would previously cause an AssertionError. - * N=2, RF=2, read ONE - * The read will fail on the local node due to tombstone read threshold. At the same time, a spec exec is triggered - * reading from the other node. - *

    - * See CASSANDRA-16097 for further details. - */ - @Test - public void testSpecExecRace() throws Throwable - { - try (Cluster cluster = init(Cluster.build().withNodes(2).withConfig(config -> config.set("tombstone_failure_threshold", TOMBSTONE_FAIL_THRESHOLD)).start())) - { - // Create a table with the spec exec policy set to a low percentile so it's more likely to produce a spec exec racing with the local request. - // Not using 'Always' because that actually uses a different class/mechanism and doesn't exercise the bug - // we're trying to produce. - cluster.schemaChange(String.format("CREATE TABLE %s.%s (k int, c int, v int, PRIMARY KEY (k,c)) WITH speculative_retry = '5p';", KEYSPACE, TABLE)); - - // Create a partition with enough tombstones to create a read failure according to the configured threshold - for (int i = 0; i <= TOMBSTONE_FAIL_THRESHOLD; ++i) - cluster.coordinator(1).execute(String.format("DELETE FROM %s.t WHERE k=%d AND c=%d", KEYSPACE, TOMBSTONE_FAIL_KEY, i), - ConsistencyLevel.TWO); - - // Create a bunch of latency samples for this failed operation. - loopFailStatement(cluster, 5000); - // Update the spec exec threshold based on the above samples. - // This would normally be done by the periodic task CassandraDaemon.SPECULATION_THRESHOLD_UPDATER. - cluster.get(1).runOnInstance(() -> - { - ColumnFamilyStore cfs = Keyspace.open(KEYSPACE) - .getColumnFamilyStore(TABLE); - cfs.updateSpeculationThreshold(); - }); - - // Run the request a bunch of times under racy conditions. - loopFailStatement(cluster, 5000); - } - } - - private void loopFailStatement(ICluster cluster, int iterations) - { - final String query = String.format("SELECT k FROM %s.t WHERE k=%d", KEYSPACE, TOMBSTONE_FAIL_KEY); - for (int i = 0; i < iterations; ++i) - { - try - { - cluster.coordinator(1).execute(query, ConsistencyLevel.ONE); - fail("Request did not throw a ReadFailureException as expected."); - } - catch (Throwable t) // Throwable because the raised ReadFailure is loaded from a different classloader and doesn't match "ours" - { - String onFail = String.format("Did not receive expected ReadFailureException. Instead caught %s\n%s", - t, ExceptionUtils.getStackTrace(t)); - assertNotNull(onFail, t.getMessage()); - assertTrue(onFail, t.getMessage().contains(RequestFailureReason.READ_TOO_MANY_TOMBSTONES.name())); - } - } - } -} - diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorBase.java b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorBase.java index 0fc2554b0139..321bc731d7fc 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorBase.java @@ -33,6 +33,7 @@ import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairParallelism; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairType; +import org.apache.cassandra.io.util.FileUtils; import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS; @@ -88,8 +89,7 @@ public static void setupCluster() throws IOException @AfterClass public static void teardownCluster() { - if (CLUSTER != null) - CLUSTER.close(); + FileUtils.closeQuietly(CLUSTER); } protected String tableName(String prefix) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java index 590c65aa7282..da3762720485 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java @@ -33,7 +33,7 @@ import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairParallelism; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairType; -import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Verb; import org.apache.cassandra.utils.FBUtilities; @@ -92,7 +92,7 @@ public void neighbourDown() { throw new RuntimeException(e); } - while (FailureDetector.instance.isAlive(neighbor)) + while (IFailureDetector.instance.isAlive(neighbor)) Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS); }); diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorTimeout.java b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorTimeout.java index b475e5515510..d2ebf75dd72a 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorTimeout.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorTimeout.java @@ -24,6 +24,7 @@ import org.junit.Before; import org.junit.Test; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairParallelism; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairType; @@ -46,6 +47,12 @@ public RepairCoordinatorTimeout(RepairType repairType, RepairParallelism paralle public void beforeTest() { CLUSTER.filters().reset(); + + CLUSTER.forEach(node -> node.runOnInstance(() -> { + // Set a larger PREPARE_MSG timeout for these tests to avoid faulure callbacks from being triggered, + // causing IllegalStateException errors. + DatabaseDescriptor.setRepairPrepareMessageTimeout(120_000L); + })); } @Test diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java b/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java index b702855f680e..ae9890468cfa 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java @@ -610,7 +610,7 @@ private long getConfirmedInconsistencies(IInvokableInstance instance) .getColumnFamilyStore(TABLE) .metric .confirmedRepairedInconsistencies - .table + .tableOrKeyspaceMeter() .getCount()); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java b/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java index 537599c29746..c4cd07434430 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java @@ -27,6 +27,7 @@ import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.db.compaction.TableOperation; import org.assertj.core.api.Assertions; import org.junit.Test; @@ -262,7 +263,7 @@ public static void validateSSTableBoundsForAnticompaction(TimeUUID sessionID, Collection sstables, RangesAtEndpoint ranges) { - throw new CompactionInterruptedException(String.valueOf(sessionID)); + throw new CompactionInterruptedException(String.valueOf(sessionID), TableOperation.StopTrigger.UNIT_TESTS); } @SuppressWarnings("unused") diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/RepairTest.java index 857c05eeb495..9b292be6eed6 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairTest.java @@ -29,19 +29,25 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.utils.concurrent.Condition; + import org.junit.AfterClass; import org.junit.Assert; -import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SystemDistributedKeyspace; import org.apache.cassandra.service.StorageService; import static com.google.common.collect.ImmutableList.of; + import static java.util.concurrent.TimeUnit.MINUTES; + import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NETWORK; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; @@ -50,10 +56,19 @@ import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition; import static org.apache.cassandra.utils.progress.ProgressEventType.COMPLETE; +@RunWith(Parameterized.class) public class RepairTest extends TestBaseImpl { + private static boolean nodesHaveCDC; + private static boolean tableHasCDC; private static ICluster cluster; + @Parameterized.Parameters(name = "nodesHaveCDC={0}, tableHasCDC={1}") + public static Iterable data() + { + return Arrays.asList(new Object[][] {{ false, false }, { false, true } , { true, false }, { true, true }}); + } + private static void insert(ICluster cluster, String keyspace, int start, int end, int ... nodes) { String insert = String.format("INSERT INTO %s.test (k, c1, c2) VALUES (?, 'value1', 'value2');", keyspace); @@ -85,7 +100,7 @@ private static void flush(ICluster cluster, String keyspace, ColumnFamilyStore.FlushReason.UNIT_TESTS))); } - private static ICluster create(Consumer configModifier) throws IOException + private ICluster create(Consumer configModifier) throws IOException { configModifier = configModifier.andThen( config -> config.set("hinted_handoff_enabled", false) @@ -98,6 +113,15 @@ private static ICluster create(Consumer configModifier) throws static void repair(ICluster cluster, String keyspace, Map options) { + long[] startPositions = new long[cluster.size()]; + for (int i = 1; i <= cluster.size(); i++) + { + IInvokableInstance node = cluster.get(i); + if (node.isShutdown()) + continue; + startPositions[i - 1] = node.logs().mark(); + } + cluster.get(1).runOnInstance(rethrow(() -> { Condition await = newOneTimeCondition(); instance.repair(keyspace, options, of((tag, event) -> { @@ -106,14 +130,25 @@ static void repair(ICluster cluster, String keyspace, Map cluster, String keyspace, String compression) throws Exception + void populate(ICluster cluster, String keyspace, String compression) throws Exception { try { cluster.schemaChange(String.format("DROP TABLE IF EXISTS %s.test;", keyspace)); - cluster.schemaChange(String.format("CREATE TABLE %s.test (k text, c1 text, c2 text, PRIMARY KEY (k)) WITH compression = %s", keyspace, compression)); + cluster.schemaChange(String.format("CREATE TABLE %s.test (k text, c1 text, c2 text, PRIMARY KEY (k)) WITH compression = %s AND cdc = %s;", keyspace, compression, tableHasCDC)); insert(cluster, keyspace, 0, 1000, 1, 2, 3); flush(cluster, keyspace, 1); @@ -147,10 +182,21 @@ void shutDownNodesAndForceRepair(ICluster cluster, String ke repair(cluster, keyspace, ImmutableMap.of("forceRepair", "true")); } - @BeforeClass - public static void setupCluster() throws IOException + public RepairTest(boolean nodesHaveCDC, boolean tableHasCDC) throws Exception { - cluster = create(config -> {}); + // This runs per method, but we only want to rebuild the cluster if nodesHaveCDC has changed since the last + // build and we need to update the configuration accordingly + if (cluster != null && RepairTest.nodesHaveCDC != nodesHaveCDC) + { + cluster.close(); + cluster = null; + } + + if (cluster == null) + cluster = create(config -> config.set("cdc_enabled", nodesHaveCDC)); + + RepairTest.nodesHaveCDC = nodesHaveCDC; + RepairTest.tableHasCDC = tableHasCDC; } @AfterClass @@ -203,7 +249,12 @@ public void testForcedNormalRepairWithOneNodeDown() throws Exception String forceRepairKeyspace = "test_force_repair_keyspace"; int rf = 2; int tokenCount = ClusterUtils.getTokenCount(cluster.get(1)); - cluster.schemaChange("CREATE KEYSPACE " + forceRepairKeyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + rf + "};"); + + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + forceRepairKeyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + rf + "};"); + + // Truncate distributed repair keyspace due to test class parameterization. We only want results + // from our run + cluster.schemaChange("TRUNCATE TABLE " + SchemaConstants.DISTRIBUTED_KEYSPACE_NAME + "." + SystemDistributedKeyspace.PARENT_REPAIR_HISTORY); try { diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReplicaFilteringProtectionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReplicaFilteringProtectionTest.java index fd8110cba72a..13db710b77f0 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ReplicaFilteringProtectionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ReplicaFilteringProtectionTest.java @@ -19,7 +19,6 @@ package org.apache.cassandra.distributed.test; import java.io.IOException; -import java.util.List; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -31,6 +30,8 @@ import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.exceptions.OverloadedException; import org.apache.cassandra.service.StorageService; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; import static org.apache.cassandra.config.ReplicaFilteringProtectionOptions.DEFAULT_FAIL_THRESHOLD; import static org.apache.cassandra.config.ReplicaFilteringProtectionOptions.DEFAULT_WARN_THRESHOLD; @@ -112,6 +113,9 @@ public void testMissedUpdatesAroundCachingFailThreshold() catch (RuntimeException e) { assertEquals(e.getClass().getName(), OverloadedException.class.getName()); + Assertions.assertThat(e) + .hasMessageContaining("Replica filtering protection has cached " + REPLICAS * ROWS_PER_PARTITION) + .hasMessageContaining("cached_replica_rows_fail_threshold"); } } @@ -147,7 +151,8 @@ private void testMissedUpdates(String tableName, int warnThreshold, int failThre // of that row for all replicas. SimpleQueryResult oldResult = cluster.coordinator(1).executeWithResult(query, ALL, "old", PARTITIONS * ROWS_PER_PARTITION); assertRows(oldResult.toObjectArrays()); - verifyWarningState(shouldWarn, oldResult); + verifyWarningState(shouldWarn, ROWS_PER_PARTITION * REPLICAS, oldResult); +// verifyWarningState(shouldWarn, ROWS_PER_PARTITION * REPLICAS, oldResult); // We should have made 3 row "completion" requests. assertEquals(PARTITIONS, protectionQueryCount(cluster.get(1), tableName)); @@ -170,7 +175,7 @@ private void testMissedUpdates(String tableName, int warnThreshold, int failThre row(0, 0, "new"), row(0, 1, "new"), row(0, 2, "new"), row(2, 0, "new"), row(2, 1, "new"), row(2, 2, "new")); - verifyWarningState(warnThreshold < REPLICAS * ROWS_PER_PARTITION, newResult); + verifyWarningState(warnThreshold < REPLICAS * ROWS_PER_PARTITION, REPLICAS * ROWS_PER_PARTITION, newResult); // We still sould only have made 3 row "completion" requests, with no replica divergence in the last query. assertEquals(PARTITIONS, protectionQueryCount(cluster.get(1), tableName)); @@ -193,7 +198,7 @@ private void testMissedUpdates(String tableName, int warnThreshold, int failThre row(0, 0, "future"), row(0, 1, "future"), row(0, 2, "future"), row(2, 0, "future"), row(2, 1, "future"), row(2, 2, "future")); - verifyWarningState(shouldWarn, futureResult); + verifyWarningState(shouldWarn, ROWS_PER_PARTITION * REPLICAS, futureResult); // We sould have made 3 more row "completion" requests. assertEquals(PARTITIONS * 2, protectionQueryCount(cluster.get(1), tableName)); @@ -212,11 +217,19 @@ private void updateAllRowsOn(int node, String table, String value) cluster.get(node).executeInternal("UPDATE " + table + " SET v = ? WHERE k = ? and c = ?", value, i, j); } - private void verifyWarningState(boolean shouldWarn, SimpleQueryResult futureResult) + private void verifyWarningState(boolean shouldWarn, int cached, SimpleQueryResult result) { - List futureWarnings = futureResult.warnings(); - assertEquals(shouldWarn, futureWarnings.stream().anyMatch(w -> w.contains("cached_replica_rows_warn_threshold"))); - assertEquals(shouldWarn ? 1 : 0, futureWarnings.size()); + ListAssert warnings = Assertions.assertThat(result.warnings()); + if (shouldWarn) + { + warnings.hasSize(1) + .anyMatch(w -> w.contains("Replica filtering protection has cached up to " + cached + " rows")) + .anyMatch(w -> w.contains("cached_replica_rows_warn_threshold")); + } + else + { + warnings.isEmpty(); + } } private long protectionQueryCount(IInvokableInstance instance, String tableName) diff --git a/test/distributed/org/apache/cassandra/distributed/test/SSTableEncryptionTest.java b/test/distributed/org/apache/cassandra/distributed/test/SSTableEncryptionTest.java new file mode 100644 index 000000000000..a019c4a728a6 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/SSTableEncryptionTest.java @@ -0,0 +1,426 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.stream.Collectors; +import javax.crypto.NoSuchPaddingException; + +import com.google.common.primitives.Bytes; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.crypto.LocalSystemKey; +import org.apache.cassandra.crypto.TDEConfigurationProvider; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.NodeToolResult; +import org.apache.cassandra.io.sstable.Component; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.bti.BtiFormat; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.ChecksumType; + +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.distributed.shared.FutureUtils.waitOn; +import static org.apache.cassandra.io.compress.EncryptedSequentialWriter.CHUNK_SIZE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SSTableEncryptionTest extends TestBaseImpl +{ + private static final String KEYSPACE_PREFIX = "ks"; + private static final String TABLE_PREFIX = "tbl"; + private static final String SENSITIVE_KEY = "Key with sensitive information"; + private static final int ROWS_COUNT = 20000; + + + @BeforeClass + public static void beforeAll() throws IOException + { + Path systemKeyDirectory = Files.createTempDirectory("system_key_directory"); + CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.setString(systemKeyDirectory.toString()); + } + + @AfterClass + public static void tearDown() + { + CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.reset(); + } + + @Test + public void shouldCreateQueryableEncryptedSSTables() throws Throwable + { + try (Cluster cluster = builder().withNodes(2) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start()) + { + // given a table with data encrypted using local key + String keyspace = createKeyspace(cluster); + Path secretKey = createLocalSecretKey(cluster); + String table = createEncryptedTable(cluster, keyspace, secretKey); + int numberOfRows = 10; + + for (int i = 0; i < numberOfRows; i++) + { + for (int j = 0; j < numberOfRows; j++) + { + cluster.coordinator(1).execute(String.format("INSERT INTO %s.%s (id, cc, value) VALUES ('%s', '%s', '%s')", keyspace, table, i, j, j), ConsistencyLevel.ALL); + } + } + // flush to make sure we have sstables + cluster.get(1).flush(keyspace); + + insertAndFlush(cluster, keyspace, table, numberOfRows); + + // when querying all + Object[][] rows = cluster.coordinator(1).execute(String.format("SELECT * FROM %s.%s ", keyspace, table), ALL); + + // then read should succeed + assertThat(rows.length).isEqualTo(100); + + // when querying by id + for (int i = 0; i < 10; i++) + { + Object[][] byIdRows = cluster.coordinator(1).execute(String.format(String.format("SELECT * FROM %%s.%%s WHERE id = '%s';", i), keyspace, table), ALL); + + // then read should succeed + assertThat(byIdRows.length).isEqualTo(10); + assertThat(byIdRows[0][0]).isEqualTo(String.valueOf(i)); + assertThat(byIdRows[0][1]).isEqualTo("0"); + } + + // when querying via a range + Object[][] byIdRows = cluster.coordinator(1).execute(String.format(String.format("SELECT * FROM %%s.%%s WHERE id = '%s' and cc >= '%s' and cc <= '%s';", 5, 2, 8), keyspace, table), ALL); + + // then read should succeed + assertThat(byIdRows.length).isEqualTo(7); + assertThat(byIdRows[0][0]).isEqualTo(String.valueOf(5)); + assertThat(byIdRows[0][1]).isEqualTo(String.valueOf(2)); + assertThat(byIdRows[0][2]).isEqualTo(String.valueOf(2)); + } + } + + @Test + public void shouldEncryptSensitiveData() throws Exception + { + try (Cluster cluster = builder().withNodes(1) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start()) + { + // given tables with and without encryption + String keyspace = createKeyspace(cluster); + TestTable nonEncryptedTable = createTableWithSampleData(cluster, keyspace, ""); + Path secretKey = createLocalSecretKey(cluster); + TestTable encryptedTable = createTableWithSampleData(cluster, keyspace, localSystemKeyEncryptionCompressionSuffix("Encryptor", secretKey.toAbsolutePath().toString())); + + // then + // sensitive key should not be present in encrypted data + byte[] sensitiveBytes = SENSITIVE_KEY.getBytes(StandardCharsets.UTF_8); + assertThat(Bytes.indexOf(nonEncryptedTable.sstableBytes, sensitiveBytes)).isNotEqualTo(-1); + assertThat(Bytes.indexOf(encryptedTable.sstableBytes, sensitiveBytes)).isEqualTo(-1); + // sensitive key should not be present in encrypted partition index + assertThat(Bytes.indexOf(nonEncryptedTable.partitionIndexBytes, sensitiveBytes)).isNotEqualTo(-1); + assertThat(Bytes.indexOf(encryptedTable.partitionIndexBytes, sensitiveBytes)).isEqualTo(-1); + + + // indexes with encryption should pass the checksum check + assertThat(checkEncryptionCrc(encryptedTable.partitionIndexBytes)).isTrue(); + assertThat(encryptedTable.rowIndexBytes.length).isGreaterThan(0); + assertThat(checkEncryptionCrc(encryptedTable.rowIndexBytes)).isTrue(); + // indexes without encryption should fail the checksum check + assertThat(checkEncryptionCrc(nonEncryptedTable.partitionIndexBytes)).isFalse(); + assertThat(checkEncryptionCrc(nonEncryptedTable.rowIndexBytes)).isFalse(); + } + } + + private boolean checkEncryptionCrc(byte[] bytes) + { + try + { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.position(0).limit(CHUNK_SIZE - 4); + int calculatedChecksum = (int) ChecksumType.CRC32.of(buffer); + //Change the limit to include the checksum + buffer.limit(CHUNK_SIZE); + int readChecksum = buffer.getInt(); + return calculatedChecksum == readChecksum; + } + catch (Exception e) + { + return false; + } + } + + @Test + public void shouldNotReadRowsFromEncryptedTableWithoutTheSecretKey() throws Exception + { + try (Cluster cluster = builder().withNodes(1) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start()) + { + // ignore throwing an exception when closing the cluster as missing key will result in exceptions in logs + cluster.setUncaughtExceptionsFilter(t -> true); + + // given a table with data encrypted using local key + String keyspace = createKeyspace(cluster); + Path secretKey = createLocalSecretKey(cluster); + String encryptedTableName = createEncryptedTable(cluster, keyspace, secretKey); + String nonEncryptedTableName = createTable(cluster, keyspace); + int numberOfRows = 10; + insertAndFlush(cluster, keyspace, encryptedTableName, numberOfRows); + insertAndFlush(cluster, keyspace, nonEncryptedTableName, numberOfRows); + + // delete secret key file + assertTrue("secret key should be deleted", Files.deleteIfExists(secretKey)); + + // restart to clear in memory secret key cache + waitOn(cluster.get(1).shutdown()); + cluster.get(1).startup(); + + // when + Object[][] rows = cluster. get(1).executeInternal(String.format("SELECT * FROM %s.%s", keyspace, nonEncryptedTableName)); + Throwable throwable = catchThrowable(() -> cluster.get(0).executeInternal(String.format("SELECT * FROM %s.%s ", keyspace, encryptedTableName))); + + // then it should be possible to read the table without encryption + assertThat(rows.length).isEqualTo(numberOfRows); + // then it should not be possible to read the encrypted table + assertThat(throwable).isInstanceOf(IndexOutOfBoundsException.class); + } + } + + @Test + public void shouldFailWhenReadingWithDifferentKey() throws Exception + { + try (Cluster cluster = builder().withNodes(1) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start()) + { + // ignore throwing an exception when closing the cluster as missing key will result in exceptions in logs + cluster.setUncaughtExceptionsFilter(t -> true); + + // given a table with data encrypted using local key + String keyspace = createKeyspace(cluster); + Path secretKey = createLocalSecretKey(cluster); + String encryptedTableName = createEncryptedTable(cluster, keyspace, secretKey); + String nonEncryptedTableName = createTable(cluster, keyspace); + int numberOfRows = 10; + insertAndFlush(cluster, keyspace, encryptedTableName, numberOfRows); + insertAndFlush(cluster, keyspace, nonEncryptedTableName, numberOfRows); + + // delete secret key file + assertTrue("secret key should be deleted", Files.deleteIfExists(secretKey)); + + Path secretKey2 = createLocalSecretKey(secretKey.toString()); + + assertEquals(secretKey, secretKey2); + + // restart to clear in memory secret key cache + waitOn(cluster.get(1).shutdown()); + cluster.get(1).startup(); + + // when + Object[][] rows = cluster. get(1).executeInternal(String.format("SELECT * FROM %s.%s", keyspace, nonEncryptedTableName)); + Throwable throwable = catchThrowable(() -> cluster.get(0).executeInternal(String.format("SELECT * FROM %s.%s ", keyspace, encryptedTableName))); + + // then it should be possible to read the table without encryption + assertThat(rows.length).isEqualTo(numberOfRows); + // then it should not be possible to read the encrypted table + assertThat(throwable).isInstanceOf(IndexOutOfBoundsException.class); + } + } + + private TestTable createTableWithSampleData(Cluster cluster, String keyspace, String tableDefSuffix) throws IOException + { + String tableName = randomTableName(); + String createTableCql = "CREATE TABLE %s.%s (id text, cc text, value text, PRIMARY KEY ((id), cc))" + tableDefSuffix; + cluster.schemaChange(String.format(createTableCql, keyspace, tableName)); + + int k = 0; + for (int i = 0; i < 10; i++) + { + for (int j = 0; j < ROWS_COUNT; j++) + { + cluster.coordinator(1).execute(String.format("INSERT INTO %s.%s (id, cc, value) VALUES ('%s', '%s', '%s')", keyspace, tableName, i, j, k), ALL); + k++; + } + } + + cluster.coordinator(1).execute(String.format("INSERT INTO %s.%s (id, cc, value) VALUES ('%s', '%s', '%s')", keyspace, tableName, SENSITIVE_KEY, SENSITIVE_KEY, SENSITIVE_KEY), ALL); + + // flush to make sure we have sstable + cluster.get(1).flush(keyspace); + + List sstablePaths = getPathsFor(cluster, keyspace, tableName, SSTableFormat.Components.DATA); + List partitionIndexPaths = getPathsFor(cluster, keyspace, tableName, BtiFormat.Components.PARTITION_INDEX); + List rowIndexPaths = getPathsFor(cluster, keyspace, tableName, BtiFormat.Components.ROW_INDEX); + + String sstablePath = sstablePaths.get(0); + byte[] sstableBytes = Files.readAllBytes(Path.of(sstablePath)); + + String partitionIndexPath = partitionIndexPaths.get(0); + byte[] partitionIndexBytes = Files.readAllBytes(Path.of(partitionIndexPath)); + + String rowIndexPath = rowIndexPaths.get(0); + byte[] rowIndexBytes = Files.readAllBytes(Path.of(rowIndexPath)); + + return new TestTable(tableName, sstableBytes, sstablePath, partitionIndexBytes, partitionIndexPath, rowIndexBytes, rowIndexPath); + } + + private enum ComponentType { DATA, PARTITION_INDEX, ROW_INDEX } + + private List getPathsFor(Cluster cluster, String keyspace, String tableName, Component component) + { + // Determine component type before passing to lambda + ComponentType componentType; + if (component == SSTableFormat.Components.DATA) { + componentType = ComponentType.DATA; + } else if (component == BtiFormat.Components.PARTITION_INDEX) { + componentType = ComponentType.PARTITION_INDEX; + } else if (component == BtiFormat.Components.ROW_INDEX) { + componentType = ComponentType.ROW_INDEX; + } else { + throw new IllegalArgumentException("Unsupported component: " + component); + } + + return cluster.get(1).callOnInstance(() -> { + Component comp; + switch (componentType) { + case DATA: + comp = SSTableFormat.Components.DATA; + break; + case PARTITION_INDEX: + comp = BtiFormat.Components.PARTITION_INDEX; + break; + case ROW_INDEX: + comp = BtiFormat.Components.ROW_INDEX; + break; + default: + throw new IllegalArgumentException("Unsupported component type"); + } + return Keyspace.open(keyspace).getColumnFamilyStore(tableName).getLiveSSTables() + .stream() + .map(SSTableReader::getDescriptor) + .map(d -> d.pathFor(comp).toString()) + .collect(Collectors.toList()); + }); + } + + private String createKeyspace(Cluster cluster) + { + String randomKeyspaceName = KEYSPACE_PREFIX + "_" + RandomStringUtils.randomNumeric(5); + cluster.schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':'1'}", randomKeyspaceName)); + return randomKeyspaceName; + } + + private Path createLocalSecretKey(Cluster cluster) + { + String keyPath = "system_key_" + RandomStringUtils.random(10, true, true); + Path keyFullPath = Paths.get(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).resolve(keyPath); + assertThat(Files.exists(keyFullPath)).isFalse(); + NodeToolResult result = cluster.get(1).nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "256", keyPath); + result.asserts().success(); + assertThat(Files.exists(keyFullPath)).isTrue(); + return keyFullPath; + } + + private Path createLocalSecretKey(String keyPath) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException + { + return LocalSystemKey.createKey(keyPath, "AES", 256); + } + + private String createEncryptedTable(Cluster cluster, String keyspace, Path secretKey) + { + String table = randomTableName(); + cluster.schemaChange(String.format("CREATE TABLE %s.%s (id text, cc text, value text, PRIMARY KEY ((id), cc)) WITH compression = " + + "{'class' : 'Encryptor', " + + "'cipher_algorithm' : 'AES/ECB/PKCS5Padding', " + + "'secret_key_strength' : 128, " + + "'key_provider' : 'LocalFileSystemKeyProviderFactory', " + + "'secret_key_file': '%s' };", keyspace, table, secretKey.toAbsolutePath())); + return table; + } + + private String createTable(Cluster cluster, String keyspace) + { + String table = randomTableName(); + cluster.schemaChange(String.format("CREATE TABLE %s.%s (id text, cc text, value text, PRIMARY KEY ((id), cc))", keyspace, table)); + return table; + } + + private String randomTableName() + { + return TABLE_PREFIX + "_" + RandomStringUtils.randomNumeric(5); + } + + private void insertAndFlush(Cluster cluster, String keyspace, String table, int rows) + { + for (int i = 0; i < rows; i++) + { + cluster.coordinator(1).execute(String.format("INSERT INTO %s.%s (id, cc, value) VALUES ('%s', '%s', '%s')", keyspace, table, i, i, i), ALL); + } + // flush to make sure we have sstables + cluster.get(1).flush(keyspace); + } + + private String localSystemKeyEncryptionCompressionSuffix(String className, String secretKeyPath) + { + return String.format(" WITH compression = " + + "{'class' : '%s', " + + "'cipher_algorithm' : 'AES/ECB/PKCS5Padding', " + + "'secret_key_strength' : 128, " + + "'key_provider' : 'LocalFileSystemKeyProviderFactory', " + + "'secret_key_file': '%s' };", className, secretKeyPath); + } + + private static class TestTable + { + public final String tableName; + public final byte[] sstableBytes; + public final String sstablePath; + public final String partitionIndexPath; + public final byte[] partitionIndexBytes; + public final String rowIndexPath; + public final byte[] rowIndexBytes; + + public TestTable(String tableName, byte[] tableBytes, String sstablePath, byte[] partitionIndexBytes, String partitionIndexPath, byte[] rowIndexBytes, String rowIndexPath) + { + this.tableName = tableName; + this.sstableBytes = tableBytes; + this.sstablePath = sstablePath; + this.partitionIndexPath = partitionIndexPath; + this.partitionIndexBytes = partitionIndexBytes; + this.rowIndexPath = rowIndexPath; + this.rowIndexBytes = rowIndexBytes; + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java b/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java index 62f6139b34ee..70155e7e9588 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java @@ -35,7 +35,7 @@ import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; +import org.apache.cassandra.db.compaction.CompactionStrategy; import org.apache.cassandra.db.compaction.LeveledCompactionStrategy; import org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy; import org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy; @@ -160,7 +160,7 @@ public final void testCompactionStrategiesWithMixedSSTables() throws Exception * would get by merging data from the initial sstables. */ @SafeVarargs - private final void testCompactionStrategiesWithMixedSSTables(final Class... compactionStrategyClasses) throws Exception + private final void testCompactionStrategiesWithMixedSSTables(final Class... compactionStrategyClasses) throws Exception { try (Cluster cluster = init(Cluster.build(1) .withDataDirCount(1) @@ -168,7 +168,7 @@ private final void testCompactionStrategiesWithMixedSSTables(final Class compactionStrategyClass : compactionStrategyClasses) + for (Class compactionStrategyClass : compactionStrategyClasses) { String tableName = "tbl_" + compactionStrategyClass.getSimpleName().toLowerCase(); cluster.schemaChange(createTableStmt(KEYSPACE, tableName, compactionStrategyClass)); @@ -181,7 +181,7 @@ private final void testCompactionStrategiesWithMixedSSTables(final Class compactionStrategyClass : compactionStrategyClasses) + for (Class compactionStrategyClass : compactionStrategyClasses) { String tableName = "tbl_" + compactionStrategyClass.getSimpleName().toLowerCase(); @@ -407,7 +407,7 @@ private static Set snapshot(IInvokableInstance instance, String ks, Stri return snapshotDirs; } - private static String createTableStmt(String ks, String name, Class compactionStrategy) + private static String createTableStmt(String ks, String name, Class compactionStrategy) { if (compactionStrategy == null) compactionStrategy = SizeTieredCompactionStrategy.class; diff --git a/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java index 94ea1d04416b..9958c36b8f17 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java @@ -34,9 +34,9 @@ import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tools.BulkLoader; import org.apache.cassandra.tools.ToolRunner; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.NativeSSTableLoaderClient; import static org.junit.Assert.assertNotEquals; @@ -98,7 +98,7 @@ public void bulkLoaderSuccessfullyStreamsOverSsl() throws Throwable "--truststore", validTrustStorePath, "--truststore-password", validTrustStorePassword, "--conf-path", "test/conf/sstableloader_with_encryption.yaml", - "--ssl-ciphers", "TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA", + "--ssl-ciphers", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", sstables_to_upload.absolutePath()); tool.assertOnCleanExit(); assertTrue(tool.getStdout().contains("Summary statistics")); diff --git a/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexCompactionTest.java b/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexCompactionTest.java index 9d168145c55b..674df194eee1 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexCompactionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexCompactionTest.java @@ -26,7 +26,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.compaction.CompactionInfo; +import org.apache.cassandra.db.compaction.AbstractTableOperation; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.distributed.Cluster; @@ -34,6 +34,7 @@ import org.apache.cassandra.index.internal.CassandraIndex; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.NonThrowingCloseable; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; @@ -58,15 +59,16 @@ public void test2iCompaction() throws IOException i.getIndexCfs().forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS); Set idxSSTables = i.getIndexCfs().getLiveSSTables(); // emulate ongoing index compaction: - CompactionInfo.Holder h = new MockHolder(i.getIndexCfs().metadata(), idxSSTables); - CompactionManager.instance.active.beginCompaction(h); - CompactionManager.instance.active.estimatedRemainingWriteBytes(); - CompactionManager.instance.active.finishCompaction(h); + AbstractTableOperation h = new MockHolder(i.getIndexCfs().metadata(), idxSSTables); + try (NonThrowingCloseable c = CompactionManager.instance.active.onOperationStart(h)) + { + CompactionManager.instance.active.estimatedRemainingWriteBytes(); + } }); } } - static class MockHolder extends CompactionInfo.Holder + static class MockHolder extends AbstractTableOperation { private final Set sstables; private final TableMetadata metadata; @@ -77,9 +79,9 @@ public MockHolder(TableMetadata metadata, Set sstables) this.sstables = sstables; } @Override - public CompactionInfo getCompactionInfo() + public OperationProgress getProgress() { - return new CompactionInfo(metadata, OperationType.COMPACTION, 0, 1000, nextTimeUUID(), sstables); + return new OperationProgress(metadata, OperationType.COMPACTION, 0, 1000, nextTimeUUID(), sstables); } @Override diff --git a/test/distributed/org/apache/cassandra/distributed/test/SelectStatementExecuteWithPagerDistributedTest.java b/test/distributed/org/apache/cassandra/distributed/test/SelectStatementExecuteWithPagerDistributedTest.java new file mode 100644 index 000000000000..7671837c0dce --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/SelectStatementExecuteWithPagerDistributedTest.java @@ -0,0 +1,126 @@ +/* + * Copyright IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.PageSize; +import org.apache.cassandra.cql3.statements.SelectStatement; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.pager.PagingState; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.FBUtilities; + +/** + * End-to-end integration coverage for {@link SelectStatement#executeWithReadQuery}: a real two-node cluster + * (RF=2), reads coordinated through {@link org.apache.cassandra.service.StorageProxy}, and the continuation + * {@link PagingState} round-tripped through its wire format between pages. Asserts that an externally-ordered + * multi-partition read is returned in the pager's command order — not token order — across page boundaries, + * with every row delivered exactly once. + */ +public class SelectStatementExecuteWithPagerDistributedTest extends TestBaseImpl +{ + @Test + public void pagesExternallyOrderedReadAcrossNodes() throws Exception + { + try (Cluster cluster = init(builder().withNodes(2).start())) + { + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int PRIMARY KEY, v text)")); + + final int rows = 12; + for (int pk = 0; pk < rows; pk++) + cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl (pk, v) VALUES (?, ?)"), + ConsistencyLevel.ALL, pk, "v" + pk); + + final String ks = KEYSPACE; + + // All assertions run inside the instance; a thrown AssertionError propagates as a test failure. + cluster.get(1).runOnInstance(() -> { + SelectStatement select = (SelectStatement) org.apache.cassandra.cql3.QueryProcessor.parseStatement( + "SELECT pk, v FROM " + ks + ".tbl WHERE pk IN (0,1,2,3,4,5,6,7,8,9,10,11)", + ClientState.forInternalCalls()); + + long nowInSec = FBUtilities.nowInSeconds(); + SinglePartitionReadCommand.Group base = + (SinglePartitionReadCommand.Group) select.getQuery(probe(null), nowInSec); + + // Reverse the (token-ordered) command list, so a token-ordered result would fail this test. + List reversed = new ArrayList<>(base.queries); + Collections.reverse(reversed); + SinglePartitionReadCommand.Group group = + SinglePartitionReadCommand.Group.create(reversed, base.limits()); + + List expected = new ArrayList<>(); + for (SinglePartitionReadCommand c : reversed) + expected.add(Int32Type.instance.compose(c.partitionKey().getKey())); + + List got = new ArrayList<>(); + PagingState state = null; + int guard = 0; + do + { + QueryOptions opts = probe(state); + ResultMessage.Rows msg = select.executeWithReadQuery(QueryState.forInternalCalls(), opts, group, + Dispatcher.RequestTime.forImmediateExecution()); + if (msg.result.rows.size() > 5) + throw new AssertionError("page exceeded requested size: " + msg.result.rows.size()); + for (List row : msg.result.rows) + got.add(Int32Type.instance.compose(row.get(0))); + + PagingState next = msg.result.metadata.getPagingState(); + state = next == null ? null + : PagingState.deserialize(next.serialize(ProtocolVersion.CURRENT), ProtocolVersion.CURRENT); + if (++guard > 100) + throw new AssertionError("paging did not terminate"); + } + while (state != null); + + if (!expected.equals(got)) + throw new AssertionError("expected pager order " + expected + " but got " + got); + }); + } + } + + /** + * A page-size-5 read at CL ALL (RF=2), resuming from {@code state} (null on the first page). CL ALL forces + * every partition to be read from both replicas, so the coordinator on node1 genuinely fans out to node2. + */ + private static QueryOptions probe(PagingState state) + { + return QueryOptions.create(org.apache.cassandra.db.ConsistencyLevel.ALL, + Collections.emptyList(), + false, + PageSize.inRows(5), + state, + null, + ProtocolVersion.CURRENT, + null); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/SlowQueryLoggerTest.java b/test/distributed/org/apache/cassandra/distributed/test/SlowQueryLoggerTest.java new file mode 100644 index 000000000000..c29c272b38cb --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/SlowQueryLoggerTest.java @@ -0,0 +1,350 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.monitoring.MonitoringTask; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ICoordinator; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.utils.Throwables; +import org.assertj.core.api.AbstractIterableAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; + +import static java.util.regex.Pattern.quote; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; + +public class SlowQueryLoggerTest extends TestBaseImpl +{ + private static final int SLOW_QUERY_LOG_TIMEOUT_MS = 100; + private static final AtomicInteger SEQ = new AtomicInteger(); + + private static Cluster cluster; + private static String table; + private static ICoordinator coordinator; + private static IInvokableInstance node; + + @BeforeClass + public static void setupCluster() throws Exception + { + // effectively disable the scheduled monitoring task so we control it manually for better test stability + CassandraRelevantProperties.MONITORING_REPORT_INTERVAL_MS.setLong(TimeUnit.HOURS.toMillis(1)); + + cluster = init(Cluster.build(2) + .withInstanceInitializer(SlowQueryLoggerTest.BBHelper::install) + .withConfig(config -> { + config.set("slow_query_log_timeout_in_ms", SLOW_QUERY_LOG_TIMEOUT_MS); + config.set("read_request_timeout_in_ms", 60000L); + config.set("range_request_timeout_in_ms", 60000L); + }) + .start()); + coordinator = cluster.coordinator(1); + node = cluster.get(2); + } + + @AfterClass + public static void closeCluster() + { + if (cluster != null) + cluster.close(); + } + + @Before + public void before() + { + CassandraRelevantProperties.MONITORING_EXECUTION_INFO_ENABLED.setBoolean(true); + table = "t_" + SEQ.getAndIncrement(); + + // trigger the monitoring task to flush any pending slow operations before the test starts + node.runOnInstance(() -> MonitoringTask.instance.logOperations(approxTime.now())); + } + + @After + public void after() + { + cluster.schemaChange(format("DROP TABLE IF EXISTS %s.%s")); + } + + /** + * Test that the slow query logger does not log sensitive data. + */ + @Test + public void testDoesNotLogSensitiveData() + { + cluster.schemaChange(format("CREATE TABLE %s.%s (k text, c text, v text, b blob, PRIMARY KEY (k, c))")); + coordinator.execute(format("INSERT INTO %s.%s (k, c, v) VALUES ('secret_k', 'secret_c', 'secret_v')"), ALL); + + // verify that slow queries are logged with redacted values + long mark = node.logs().mark(); + String query = format("SELECT * FROM %s.%s WHERE k = 'secret_k' AND c = 'secret_c' AND v = 'secret_v' ALLOW FILTERING"); + Object[][] rows = coordinator.execute(query, ALL); + Assertions.assertThat(rows).hasNumberOfRows(1); + assertLogsContain(mark, node, "operations were slow", format(""))); + assertLogsContain(mark, node, "operations were slow", quote(format("100B\\] ALLOW FILTERING>"), + format("10KiB\\] ALLOW FILTERING>")); + + coordinator.execute(format("SELECT * FROM %s.%s"), ALL); + } + + /** + * Test that the slow query logger outputs the correct metrics for number of returned partitions, rows, etc. + */ + @Test + public void testLogsReadMetrics() + { + cluster.schemaChange(format("CREATE TABLE %s.%s (k int, c int, v int, l int, s int, PRIMARY KEY (k, c))")); + cluster.schemaChange(format("CREATE INDEX legacy_idx ON %s.%s (l)")); + cluster.schemaChange(format("CREATE CUSTOM INDEX sai_idx ON %s.%s (s) USING 'StorageAttachedIndex'")); + int numPartitions = 10; + int numClusterings = 10; + int numRows = 0; + for (int k = 0; k < numPartitions; k++) + for (int c = 0; c < numClusterings; c++) + coordinator.execute(format("INSERT INTO %s.%s (k, c, v, l, s) VALUES (?, ?, ?, ?, ?)"), + ALL, k, c, numRows++, numRows, numRows); + + // unrestricted query + long mark = node.logs().mark(); + Object[][] rows = coordinator.execute(format("SELECT * FROM %s.%s"), ALL); + Assertions.assertThat(rows).hasNumberOfRows(numRows); + assertLogsContain(mark, node, + format(""), + " Fetched/returned/tombstones:", + " partitions: 1/1/0", + " rows: 10/10/0"); + + // clustering query + mark = node.logs().mark(); + rows = coordinator.execute(format("SELECT * FROM %s.%s WHERE k = 2 AND c = 2"), ALL); + Assertions.assertThat(rows).hasNumberOfRows(1); + assertLogsContain(mark, node, + format(""), + " Fetched/returned/tombstones:", + " partitions: 10/3/0", + " rows: 100/25/0"); + + // paged query + mark = node.logs().mark(); + Iterator pagedRows = coordinator.executeWithPaging(format("SELECT * FROM %s.%s"), ALL, 5); + int readRows = 0; + while (pagedRows.hasNext()) + { + pagedRows.next(); + readRows++; + } + Assertions.assertThat(readRows).isEqualTo(numRows); + assertLogsContain(mark, node, quote(format("= token(?) LIMIT 5 ALLOW FILTERING [paging continuation]>"))); + + // test multiple slow runs of different queries with the same redacted form, it should log the slowest one + mark = node.logs().mark(); + for (int i = 0; i < numPartitions; i++) + { + rows = coordinator.execute(format("SELECT * FROM %s.%s WHERE k = " + i), ALL); + Assertions.assertThat(rows).hasNumberOfRows(numClusterings); + } + assertLogsContain(mark, node, + format(""), + " Fetched/returned/tombstones:", + " partitions: 9/9/1", + " rows: 90/90/0"); + + // delete a row and query again, to see row tombstone metrics + coordinator.execute(format("DELETE FROM %s.%s WHERE k = 1 AND c = 1"), ALL); + mark = node.logs().mark(); + rows = coordinator.execute(format("SELECT * FROM %s.%s"), ALL); + Assertions.assertThat(rows).hasNumberOfRows(numRows - numClusterings - 1); + assertLogsContain(mark, node, + format(""), + " Fetched/returned/tombstones:", + " partitions: 9/9/1", + " rows: 84/84/3"); // one from before, plus opening and closing bounds + + // query with a legacy index, which doesn't provide its own execution info, so generic execution info should be used + mark = node.logs().mark(); + rows = coordinator.execute(format("SELECT * FROM %s.%s WHERE l = 99"), ALL); + Assertions.assertThat(rows).hasNumberOfRows(1); + assertLogsContain(mark, node, + format("")); + assertLogsDoNotContain(mark, node, " Fetched/returned/tombstones:"); + + // disable execution info logging and verify that info is not logged anymore + CassandraRelevantProperties.MONITORING_EXECUTION_INFO_ENABLED.setBoolean(false); + mark = node.logs().mark(); + coordinator.execute(format("SELECT * FROM %s.%s"), ALL); + assertLogsContain(mark, node, format("= ? LIMIT 1 ALLOW FILTERING>")), + "NumericIndexScan"); + assertLogsContain(mark, node, + quote(withKeyspace("